Merge branch 'develop' into gsouquet/voice-messages-waveform-perf
						commit
						6cb86057c5
					
				|  | @ -172,7 +172,7 @@ | |||
|   "jest": { | ||||
|     "testEnvironment": "./__test-utils__/environment.js", | ||||
|     "testMatch": [ | ||||
|       "<rootDir>/test/**/*-test.[jt]s" | ||||
|       "<rootDir>/test/**/*-test.[jt]s?(x)" | ||||
|     ], | ||||
|     "setupFiles": [ | ||||
|       "jest-canvas-mock" | ||||
|  |  | |||
|  | @ -111,6 +111,29 @@ $roomListCollapsedWidth: 68px; | |||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             .mx_LeftPanel_dialPadButton { | ||||
|                 width: 32px; | ||||
|                 height: 32px; | ||||
|                 border-radius: 8px; | ||||
|                 background-color: $roomlist-button-bg-color; | ||||
|                 position: relative; | ||||
|                 margin-left: 8px; | ||||
| 
 | ||||
|                 &::before { | ||||
|                     content: ''; | ||||
|                     position: absolute; | ||||
|                     top: 8px; | ||||
|                     left: 8px; | ||||
|                     width: 16px; | ||||
|                     height: 16px; | ||||
|                     mask-image: url('$(res)/img/element-icons/call/dialpad.svg'); | ||||
|                     mask-position: center; | ||||
|                     mask-size: contain; | ||||
|                     mask-repeat: no-repeat; | ||||
|                     background: $secondary-fg-color; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             .mx_LeftPanel_exploreButton { | ||||
|                 width: 32px; | ||||
|                 height: 32px; | ||||
|  | @ -185,6 +208,12 @@ $roomListCollapsedWidth: 68px; | |||
|                 flex-direction: column; | ||||
|                 justify-content: center; | ||||
| 
 | ||||
|                 .mx_LeftPanel_dialPadButton { | ||||
|                     margin-left: 0; | ||||
|                     margin-top: 8px; | ||||
|                     background-color: transparent; | ||||
|                 } | ||||
| 
 | ||||
|                 .mx_LeftPanel_exploreButton { | ||||
|                     margin-left: 0; | ||||
|                     margin-top: 8px; | ||||
|  |  | |||
|  | @ -295,6 +295,7 @@ limitations under the License. | |||
| 
 | ||||
|     .mx_InviteDialog_content { | ||||
|         overflow: hidden; | ||||
|         height: 100%; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -316,3 +317,42 @@ limitations under the License. | |||
| .mx_InviteDialog_helpText .mx_AccessibleButton_kind_link { | ||||
|     padding: 0; | ||||
| } | ||||
| 
 | ||||
| .mx_InviteDialog_multiInviterError { | ||||
|     > h4 { | ||||
|         font-size: $font-15px; | ||||
|         line-height: $font-24px; | ||||
|         color: $secondary-fg-color; | ||||
|         font-weight: normal; | ||||
|     } | ||||
| 
 | ||||
|     > div { | ||||
|         .mx_InviteDialog_multiInviterError_entry { | ||||
|             margin-bottom: 24px; | ||||
| 
 | ||||
|             .mx_InviteDialog_multiInviterError_entry_userProfile { | ||||
|                 .mx_InviteDialog_multiInviterError_entry_name { | ||||
|                     margin-left: 6px; | ||||
|                     font-size: $font-15px; | ||||
|                     line-height: $font-24px; | ||||
|                     font-weight: $font-semi-bold; | ||||
|                     color: $primary-fg-color; | ||||
|                 } | ||||
| 
 | ||||
|                 .mx_InviteDialog_multiInviterError_entry_userId { | ||||
|                     margin-left: 6px; | ||||
|                     font-size: $font-12px; | ||||
|                     line-height: $font-15px; | ||||
|                     color: $tertiary-fg-color; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             .mx_InviteDialog_multiInviterError_entry_error { | ||||
|                 margin-left: 32px; | ||||
|                 font-size: $font-15px; | ||||
|                 line-height: $font-24px; | ||||
|                 color: $notice-primary-color; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -17,4 +17,9 @@ limitations under the License. | |||
| .mx_TextualEvent { | ||||
|     opacity: 0.5; | ||||
|     overflow-y: hidden; | ||||
| 
 | ||||
|     a { | ||||
|         color: $accent-color; | ||||
|         cursor: pointer; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -73,7 +73,7 @@ limitations under the License. | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .mx_AccessibleButton { | ||||
|     .mx_AccessibleButton_hasKind { | ||||
|         padding: 8px 22px; | ||||
|         margin-left: auto; | ||||
|         display: block; | ||||
|  |  | |||
|  | @ -0,0 +1,3 @@ | |||
| <svg width="12" height="18" viewBox="0 0 12 18" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||
| <path d="M6 14.25C5.175 14.25 4.5 14.925 4.5 15.75C4.5 16.575 5.175 17.25 6 17.25C6.825 17.25 7.5 16.575 7.5 15.75C7.5 14.925 6.825 14.25 6 14.25ZM1.5 0.75C0.675 0.75 0 1.425 0 2.25C0 3.075 0.675 3.75 1.5 3.75C2.325 3.75 3 3.075 3 2.25C3 1.425 2.325 0.75 1.5 0.75ZM1.5 5.25C0.675 5.25 0 5.925 0 6.75C0 7.575 0.675 8.25 1.5 8.25C2.325 8.25 3 7.575 3 6.75C3 5.925 2.325 5.25 1.5 5.25ZM1.5 9.75C0.675 9.75 0 10.425 0 11.25C0 12.075 0.675 12.75 1.5 12.75C2.325 12.75 3 12.075 3 11.25C3 10.425 2.325 9.75 1.5 9.75ZM10.5 3.75C11.325 3.75 12 3.075 12 2.25C12 1.425 11.325 0.75 10.5 0.75C9.675 0.75 9 1.425 9 2.25C9 3.075 9.675 3.75 10.5 3.75ZM6 9.75C5.175 9.75 4.5 10.425 4.5 11.25C4.5 12.075 5.175 12.75 6 12.75C6.825 12.75 7.5 12.075 7.5 11.25C7.5 10.425 6.825 9.75 6 9.75ZM10.5 9.75C9.675 9.75 9 10.425 9 11.25C9 12.075 9.675 12.75 10.5 12.75C11.325 12.75 12 12.075 12 11.25C12 10.425 11.325 9.75 10.5 9.75ZM10.5 5.25C9.675 5.25 9 5.925 9 6.75C9 7.575 9.675 8.25 10.5 8.25C11.325 8.25 12 7.575 12 6.75C12 5.925 11.325 5.25 10.5 5.25ZM6 5.25C5.175 5.25 4.5 5.925 4.5 6.75C4.5 7.575 5.175 8.25 6 8.25C6.825 8.25 7.5 7.575 7.5 6.75C7.5 5.925 6.825 5.25 6 5.25ZM6 0.75C5.175 0.75 4.5 1.425 4.5 2.25C4.5 3.075 5.175 3.75 6 3.75C6.825 3.75 7.5 3.075 7.5 2.25C7.5 1.425 6.825 0.75 6 0.75Z" fill="#737D8C"/> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 1.4 KiB | 
|  | @ -15,20 +15,8 @@ limitations under the License. | |||
| */ | ||||
| 
 | ||||
| declare module "diff-dom" { | ||||
|     enum Action { | ||||
|         AddElement = "addElement", | ||||
|         AddTextElement = "addTextElement", | ||||
|         RemoveTextElement = "removeTextElement", | ||||
|         RemoveElement = "removeElement", | ||||
|         ReplaceElement = "replaceElement", | ||||
|         ModifyTextElement = "modifyTextElement", | ||||
|         AddAttribute = "addAttribute", | ||||
|         RemoveAttribute = "removeAttribute", | ||||
|         ModifyAttribute = "modifyAttribute", | ||||
|     } | ||||
| 
 | ||||
|     export interface IDiff { | ||||
|         action: Action; | ||||
|         action: string; | ||||
|         name: string; | ||||
|         text?: string; | ||||
|         route: number[]; | ||||
|  |  | |||
|  | @ -307,7 +307,7 @@ function readFileAsArrayBuffer(file: File | Blob): Promise<ArrayBuffer> { | |||
|  *  If the file is unencrypted then the object will have a "url" key. | ||||
|  *  If the file is encrypted then the object will have a "file" key. | ||||
|  */ | ||||
| function uploadFile( | ||||
| export function uploadFile( | ||||
|     matrixClient: MatrixClient, | ||||
|     roomId: string, | ||||
|     file: File | Blob, | ||||
|  |  | |||
|  | @ -68,7 +68,7 @@ export const Notifier = { | |||
|     // or not
 | ||||
|     pendingEncryptedEventIds: [], | ||||
| 
 | ||||
|     notificationMessageForEvent: function(ev: MatrixEvent) { | ||||
|     notificationMessageForEvent: function(ev: MatrixEvent): string { | ||||
|         if (typehandlers.hasOwnProperty(ev.getContent().msgtype)) { | ||||
|             return typehandlers[ev.getContent().msgtype](ev); | ||||
|         } | ||||
|  |  | |||
|  | @ -1,7 +1,5 @@ | |||
| /* | ||||
| Copyright 2016 OpenMarket Ltd | ||||
| Copyright 2017, 2018 New Vector Ltd | ||||
| Copyright 2020 The Matrix.org Foundation C.I.C. | ||||
| Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
|  | @ -16,15 +14,26 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import React from 'react'; | ||||
| import {MatrixClientPeg} from './MatrixClientPeg'; | ||||
| import MultiInviter from './utils/MultiInviter'; | ||||
| import React from "react"; | ||||
| import { Room } from "matrix-js-sdk/src/models/room"; | ||||
| import { MatrixEvent } from "matrix-js-sdk/src/models/event"; | ||||
| import { User } from "matrix-js-sdk/src/models/user"; | ||||
| 
 | ||||
| import { MatrixClientPeg } from './MatrixClientPeg'; | ||||
| import MultiInviter, { CompletionStates } from './utils/MultiInviter'; | ||||
| import Modal from './Modal'; | ||||
| import * as sdk from './'; | ||||
| import { _t } from './languageHandler'; | ||||
| import InviteDialog, {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog"; | ||||
| import InviteDialog, { KIND_DM, KIND_INVITE, Member } from "./components/views/dialogs/InviteDialog"; | ||||
| import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; | ||||
| import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore"; | ||||
| import { CommunityPrototypeStore } from "./stores/CommunityPrototypeStore"; | ||||
| import BaseAvatar from "./components/views/avatars/BaseAvatar"; | ||||
| import { mediaFromMxc } from "./customisations/Media"; | ||||
| 
 | ||||
| export interface IInviteResult { | ||||
|     states: CompletionStates; | ||||
|     inviter: MultiInviter; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Invites multiple addresses to a room | ||||
|  | @ -32,15 +41,15 @@ import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore"; | |||
|  * no option to cancel. | ||||
|  * | ||||
|  * @param {string} roomId The ID of the room to invite to | ||||
|  * @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids. | ||||
|  * @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids. | ||||
|  * @returns {Promise} Promise | ||||
|  */ | ||||
| export function inviteMultipleToRoom(roomId, addrs) { | ||||
| export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promise<IInviteResult> { | ||||
|     const inviter = new MultiInviter(roomId); | ||||
|     return inviter.invite(addrs).then(states => Promise.resolve({states, inviter})); | ||||
|     return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter })); | ||||
| } | ||||
| 
 | ||||
| export function showStartChatInviteDialog(initialText) { | ||||
| export function showStartChatInviteDialog(initialText = ""): void { | ||||
|     // This dialog handles the room creation internally - we don't need to worry about it.
 | ||||
|     const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); | ||||
|     Modal.createTrackedDialog( | ||||
|  | @ -49,7 +58,7 @@ export function showStartChatInviteDialog(initialText) { | |||
|     ); | ||||
| } | ||||
| 
 | ||||
| export function showRoomInviteDialog(roomId, initialText = "") { | ||||
| export function showRoomInviteDialog(roomId: string, initialText = ""): void { | ||||
|     // This dialog handles the room creation internally - we don't need to worry about it.
 | ||||
|     Modal.createTrackedDialog( | ||||
|         "Invite Users", "", InviteDialog, { | ||||
|  | @ -61,14 +70,14 @@ export function showRoomInviteDialog(roomId, initialText = "") { | |||
|     ); | ||||
| } | ||||
| 
 | ||||
| export function showCommunityRoomInviteDialog(roomId, communityName) { | ||||
| export function showCommunityRoomInviteDialog(roomId: string, communityName: string): void { | ||||
|     Modal.createTrackedDialog( | ||||
|         'Invite Users to Community', '', CommunityPrototypeInviteDialog, {communityName, roomId}, | ||||
|         /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| export function showCommunityInviteDialog(communityId) { | ||||
| export function showCommunityInviteDialog(communityId: string): void { | ||||
|     const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId); | ||||
|     if (chat) { | ||||
|         const name = CommunityPrototypeStore.instance.getCommunityName(communityId); | ||||
|  | @ -83,7 +92,7 @@ export function showCommunityInviteDialog(communityId) { | |||
|  * @param {MatrixEvent} event The event to check | ||||
|  * @returns {boolean} True if valid, false otherwise | ||||
|  */ | ||||
| export function isValid3pidInvite(event) { | ||||
| export function isValid3pidInvite(event: MatrixEvent): boolean { | ||||
|     if (!event || event.getType() !== "m.room.third_party_invite") return false; | ||||
| 
 | ||||
|     // any events without these keys are not valid 3pid invites, so we ignore them
 | ||||
|  | @ -96,7 +105,7 @@ export function isValid3pidInvite(event) { | |||
|     return true; | ||||
| } | ||||
| 
 | ||||
| export function inviteUsersToRoom(roomId, userIds) { | ||||
| export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise<void> { | ||||
|     return inviteMultipleToRoom(roomId, userIds).then((result) => { | ||||
|         const room = MatrixClientPeg.get().getRoom(roomId); | ||||
|         showAnyInviteErrors(result.states, room, result.inviter); | ||||
|  | @ -110,9 +119,14 @@ export function inviteUsersToRoom(roomId, userIds) { | |||
|     }); | ||||
| } | ||||
| 
 | ||||
| export function showAnyInviteErrors(addrs, room, inviter) { | ||||
| export function showAnyInviteErrors( | ||||
|     states: CompletionStates, | ||||
|     room: Room, | ||||
|     inviter: MultiInviter, | ||||
|     userMap?: Map<string, Member>, | ||||
| ): boolean { | ||||
|     // Show user any errors
 | ||||
|     const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error'); | ||||
|     const failedUsers = Object.keys(states).filter(a => states[a] === 'error'); | ||||
|     if (failedUsers.length === 1 && inviter.fatal) { | ||||
|         // Just get the first message because there was a fatal problem on the first
 | ||||
|         // user. This usually means that no other users were attempted, making it
 | ||||
|  | @ -126,19 +140,47 @@ export function showAnyInviteErrors(addrs, room, inviter) { | |||
|     } else { | ||||
|         const errorList = []; | ||||
|         for (const addr of failedUsers) { | ||||
|             if (addrs[addr] === "error") { | ||||
|             if (states[addr] === "error") { | ||||
|                 const reason = inviter.getErrorText(addr); | ||||
|                 errorList.push(addr + ": " + reason); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const cli = MatrixClientPeg.get(); | ||||
|         if (errorList.length > 0) { | ||||
|             // React 16 doesn't let us use `errorList.join(<br />)` anymore, so this is our solution
 | ||||
|             const description = <div>{errorList.map(e => <div key={e}>{e}</div>)}</div>; | ||||
|             const description = <div className="mx_InviteDialog_multiInviterError"> | ||||
|                 <h4>{ _t("We sent the others, but the below people couldn't be invited to <RoomName/>", {}, { | ||||
|                     RoomName: () => <b>{ room.name }</b>, | ||||
|                 }) }</h4> | ||||
|                 <div> | ||||
|                     { failedUsers.map(addr => { | ||||
|                         const user = userMap?.get(addr) || cli.getUser(addr); | ||||
|                         const name = (user as Member).name || (user as User).rawDisplayName; | ||||
|                         const avatarUrl = (user as Member).getMxcAvatarUrl?.() || (user as User).avatarUrl; | ||||
|                         return <div key={addr} className="mx_InviteDialog_multiInviterError_entry"> | ||||
|                             <div className="mx_InviteDialog_multiInviterError_entry_userProfile"> | ||||
|                                 <BaseAvatar | ||||
|                                     url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(24) : null} | ||||
|                                     name={name} | ||||
|                                     idName={user.userId} | ||||
|                                     width={24} | ||||
|                                     height={24} | ||||
|                                 /> | ||||
|                                 <span className="mx_InviteDialog_multiInviterError_entry_name">{ name }</span> | ||||
|                                 <span className="mx_InviteDialog_multiInviterError_entry_userId">{ user.userId }</span> | ||||
|                             </div> | ||||
|                             <div className="mx_InviteDialog_multiInviterError_entry_error"> | ||||
|                                 { inviter.getErrorText(addr) } | ||||
|                             </div> | ||||
|                         </div>; | ||||
|                     }) } | ||||
|                 </div> | ||||
|             </div>; | ||||
| 
 | ||||
|             const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); | ||||
|             Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, { | ||||
|                 title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), | ||||
|             Modal.createTrackedDialog("Some invites could not be sent", "", ErrorDialog, { | ||||
|                 title: _t("Some invites couldn't be sent"), | ||||
|                 description, | ||||
|             }); | ||||
|             return false; | ||||
|  | @ -14,7 +14,7 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import { ICryptoCallbacks, IDeviceTrustLevel, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix'; | ||||
| import { ICryptoCallbacks, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix'; | ||||
| import { MatrixClient } from 'matrix-js-sdk/src/client'; | ||||
| import Modal from './Modal'; | ||||
| import * as sdk from './index'; | ||||
|  | @ -28,6 +28,7 @@ import AccessSecretStorageDialog from './components/views/dialogs/security/Acces | |||
| import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog'; | ||||
| import SettingsStore from "./settings/SettingsStore"; | ||||
| import SecurityCustomisations from "./customisations/Security"; | ||||
| import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; | ||||
| 
 | ||||
| // This stores the secret storage private keys in memory for the JS SDK. This is
 | ||||
| // only meant to act as a cache to avoid prompting the user multiple times
 | ||||
|  | @ -244,7 +245,7 @@ async function onSecretRequested( | |||
|     deviceId: string, | ||||
|     requestId: string, | ||||
|     name: string, | ||||
|     deviceTrust: IDeviceTrustLevel, | ||||
|     deviceTrust: DeviceTrustLevel, | ||||
| ): Promise<string> { | ||||
|     console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust); | ||||
|     const client = MatrixClientPeg.get(); | ||||
|  |  | |||
|  | @ -13,6 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import React from 'react'; | ||||
| import {MatrixClientPeg} from './MatrixClientPeg'; | ||||
| import { _t } from './languageHandler'; | ||||
| import * as Roles from './Roles'; | ||||
|  | @ -20,6 +22,11 @@ import {isValid3pidInvite} from "./RoomInvite"; | |||
| import SettingsStore from "./settings/SettingsStore"; | ||||
| import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList"; | ||||
| import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore"; | ||||
| import { RightPanelPhases } from './stores/RightPanelStorePhases'; | ||||
| import { Action } from './dispatcher/actions'; | ||||
| import defaultDispatcher from './dispatcher/dispatcher'; | ||||
| import { SetRightPanelPhasePayload } from './dispatcher/payloads/SetRightPanelPhasePayload'; | ||||
| import { MatrixEvent } from "matrix-js-sdk/src/models/event"; | ||||
| 
 | ||||
| // These functions are frequently used just to check whether an event has
 | ||||
| // any text to display at all. For this reason they return deferred values
 | ||||
|  | @ -31,76 +38,89 @@ function textForMemberEvent(ev): () => string | null { | |||
|     const targetName = ev.target ? ev.target.name : ev.getStateKey(); | ||||
|     const prevContent = ev.getPrevContent(); | ||||
|     const content = ev.getContent(); | ||||
|     const reason = content.reason; | ||||
| 
 | ||||
|     const getReason = () => content.reason ? (_t('Reason') + ': ' + content.reason) : ''; | ||||
|     switch (content.membership) { | ||||
|         case 'invite': { | ||||
|             const threePidContent = content.third_party_invite; | ||||
|             if (threePidContent) { | ||||
|                 if (threePidContent.display_name) { | ||||
|                     return () => _t('%(targetName)s accepted the invitation for %(displayName)s.', { | ||||
|                     return () => _t('%(targetName)s accepted the invitation for %(displayName)s', { | ||||
|                         targetName, | ||||
|                         displayName: threePidContent.display_name, | ||||
|                     }); | ||||
|                 } else { | ||||
|                     return () => _t('%(targetName)s accepted an invitation.', {targetName}); | ||||
|                     return () => _t('%(targetName)s accepted an invitation', { targetName }); | ||||
|                 } | ||||
|             } else { | ||||
|                 return () => _t('%(senderName)s invited %(targetName)s.', {senderName, targetName}); | ||||
|                 return () => _t('%(senderName)s invited %(targetName)s', { senderName, targetName }); | ||||
|             } | ||||
|         } | ||||
|         case 'ban': | ||||
|             return () => _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + getReason(); | ||||
|             return () => reason | ||||
|                 ? _t('%(senderName)s banned %(targetName)s: %(reason)s', { senderName, targetName, reason }) | ||||
|                 : _t('%(senderName)s banned %(targetName)s', { senderName, targetName }); | ||||
|         case 'join': | ||||
|             if (prevContent && prevContent.membership === 'join') { | ||||
|                 if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) { | ||||
|                     return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s.', { | ||||
|                     return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s', { | ||||
|                         oldDisplayName: prevContent.displayname, | ||||
|                         displayName: content.displayname, | ||||
|                     }); | ||||
|                 } else if (!prevContent.displayname && content.displayname) { | ||||
|                     return () => _t('%(senderName)s set their display name to %(displayName)s.', { | ||||
|                     return () => _t('%(senderName)s set their display name to %(displayName)s', { | ||||
|                         senderName: ev.getSender(), | ||||
|                         displayName: content.displayname, | ||||
|                     }); | ||||
|                 } else if (prevContent.displayname && !content.displayname) { | ||||
|                     return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s).', { | ||||
|                     return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s)', { | ||||
|                         senderName, | ||||
|                         oldDisplayName: prevContent.displayname, | ||||
|                     }); | ||||
|                 } else if (prevContent.avatar_url && !content.avatar_url) { | ||||
|                     return () => _t('%(senderName)s removed their profile picture.', {senderName}); | ||||
|                     return () => _t('%(senderName)s removed their profile picture', { senderName }); | ||||
|                 } else if (prevContent.avatar_url && content.avatar_url && | ||||
|                     prevContent.avatar_url !== content.avatar_url) { | ||||
|                     return () => _t('%(senderName)s changed their profile picture.', {senderName}); | ||||
|                     return () => _t('%(senderName)s changed their profile picture', { senderName }); | ||||
|                 } else if (!prevContent.avatar_url && content.avatar_url) { | ||||
|                     return () => _t('%(senderName)s set a profile picture.', {senderName}); | ||||
|                     return () => _t('%(senderName)s set a profile picture', { senderName }); | ||||
|                 } else if (SettingsStore.getValue("showHiddenEventsInTimeline")) { | ||||
|                     // This is a null rejoin, it will only be visible if the Labs option is enabled
 | ||||
|                     return () => _t("%(senderName)s made no change.", {senderName}); | ||||
|                     // This is a null rejoin, it will only be visible if using 'show hidden events' (labs)
 | ||||
|                     return () => _t("%(senderName)s made no change", { senderName }); | ||||
|                 } else { | ||||
|                     return null; | ||||
|                 } | ||||
|             } else { | ||||
|                 if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key); | ||||
|                 return () => _t('%(targetName)s joined the room.', {targetName}); | ||||
|                 return () => _t('%(targetName)s joined the room', { targetName }); | ||||
|             } | ||||
|         case 'leave': | ||||
|             if (ev.getSender() === ev.getStateKey()) { | ||||
|                 if (prevContent.membership === "invite") { | ||||
|                     return () => _t('%(targetName)s rejected the invitation.', {targetName}); | ||||
|                     return () => _t('%(targetName)s rejected the invitation', { targetName }); | ||||
|                 } else { | ||||
|                     return () => _t('%(targetName)s left the room.', {targetName}); | ||||
|                     return () => reason | ||||
|                         ? _t('%(targetName)s left the room: %(reason)s', { targetName, reason }) | ||||
|                         : _t('%(targetName)s left the room', { targetName }); | ||||
|                 } | ||||
|             } else if (prevContent.membership === "ban") { | ||||
|                 return () => _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName}); | ||||
|                 return () => _t('%(senderName)s unbanned %(targetName)s', { senderName, targetName }); | ||||
|             } else if (prevContent.membership === "invite") { | ||||
|                 return () => _t('%(senderName)s withdrew %(targetName)s\'s invitation.', { | ||||
|                     senderName, | ||||
|                     targetName, | ||||
|                 }) + ' ' + getReason(); | ||||
|                 return () => reason | ||||
|                     ? _t('%(senderName)s withdrew %(targetName)s\'s invitation: %(reason)s', { | ||||
|                         senderName, | ||||
|                         targetName, | ||||
|                         reason, | ||||
|                     }) | ||||
|                     : _t('%(senderName)s withdrew %(targetName)s\'s invitation', { senderName, targetName }) | ||||
|             } else if (prevContent.membership === "join") { | ||||
|                 return () => _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + getReason(); | ||||
|                 return () => reason | ||||
|                     ? _t('%(senderName)s kicked %(targetName)s: %(reason)s', { | ||||
|                         senderName, | ||||
|                         targetName, | ||||
|                         reason, | ||||
|                     }) | ||||
|                     : _t('%(senderName)s kicked %(targetName)s', { senderName, targetName }); | ||||
|             } else { | ||||
|                 return null; | ||||
|             } | ||||
|  | @ -466,9 +486,33 @@ function textForPowerEvent(event): () => string | null { | |||
|     }); | ||||
| } | ||||
| 
 | ||||
| function textForPinnedEvent(event): () => string | null { | ||||
| const onPinnedMessagesClick = (): void => { | ||||
|     defaultDispatcher.dispatch<SetRightPanelPhasePayload>({ | ||||
|         action: Action.SetRightPanelPhase, | ||||
|         phase: RightPanelPhases.PinnedMessages, | ||||
|         allowClose: false, | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string | JSX.Element | null { | ||||
|     if (!SettingsStore.getValue("feature_pinning")) return null; | ||||
|     const senderName = event.sender ? event.sender.name : event.getSender(); | ||||
|     return () => _t("%(senderName)s changed the pinned messages for the room.", {senderName}); | ||||
| 
 | ||||
|     if (allowJSX) { | ||||
|         return () => ( | ||||
|             <span> | ||||
|                 { | ||||
|                     _t( | ||||
|                         "%(senderName)s changed the <a>pinned messages</a> for the room.", | ||||
|                         { senderName }, | ||||
|                         { "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> }, | ||||
|                     ) | ||||
|                 } | ||||
|             </span> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     return () => _t("%(senderName)s changed the pinned messages for the room.", { senderName }); | ||||
| } | ||||
| 
 | ||||
| function textForWidgetEvent(event): () => string | null { | ||||
|  | @ -594,7 +638,7 @@ function textForMjolnirEvent(event): () => string | null { | |||
| } | ||||
| 
 | ||||
| interface IHandlers { | ||||
|     [type: string]: (ev: any) => (() => string | null); | ||||
|     [type: string]: (ev: MatrixEvent, allowJSX?: boolean) => (() => string | JSX.Element | null); | ||||
| } | ||||
| 
 | ||||
| const handlers: IHandlers = { | ||||
|  | @ -635,7 +679,9 @@ export function hasText(ev): boolean { | |||
|     return Boolean(handler?.(ev)); | ||||
| } | ||||
| 
 | ||||
| export function textForEvent(ev): string { | ||||
| export function textForEvent(ev: MatrixEvent): string; | ||||
| export function textForEvent(ev: MatrixEvent, allowJSX: true): string | JSX.Element; | ||||
| export function textForEvent(ev: MatrixEvent, allowJSX = false): string | JSX.Element { | ||||
|     const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; | ||||
|     return handler?.(ev)?.() || ''; | ||||
|     return handler?.(ev, allowJSX)?.() || ''; | ||||
| } | ||||
|  | @ -1,5 +1,5 @@ | |||
| /* | ||||
| Copyright 2017 New Vector Ltd | ||||
| Copyright 2017 - 2021 The Matrix.org Foundation C.I.C. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
|  | @ -14,15 +14,19 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| const emailRegex = /^\S+@\S+\.\S+$/; | ||||
| import PropTypes from "prop-types"; | ||||
| 
 | ||||
| const emailRegex = /^\S+@\S+\.\S+$/; | ||||
| const mxUserIdRegex = /^@\S+:\S+$/; | ||||
| const mxRoomIdRegex = /^!\S+:\S+$/; | ||||
| 
 | ||||
| import PropTypes from 'prop-types'; | ||||
| export const addressTypes = [ | ||||
|     'mx-user-id', 'mx-room-id', 'email', | ||||
| ]; | ||||
| export const addressTypes = ['mx-user-id', 'mx-room-id', 'email']; | ||||
| 
 | ||||
| export enum AddressType { | ||||
|     Email = "email", | ||||
|     MatrixUserId = "mx-user-id", | ||||
|     MatrixRoomId = "mx-room-id", | ||||
| } | ||||
| 
 | ||||
| // PropType definition for an object describing
 | ||||
| // an address that can be invited to a room (which
 | ||||
|  | @ -40,18 +44,13 @@ export const UserAddressType = PropTypes.shape({ | |||
|     isKnown: PropTypes.bool, | ||||
| }); | ||||
| 
 | ||||
| export function getAddressType(inputText) { | ||||
|     const isEmailAddress = emailRegex.test(inputText); | ||||
|     const isUserId = mxUserIdRegex.test(inputText); | ||||
|     const isRoomId = mxRoomIdRegex.test(inputText); | ||||
| 
 | ||||
|     // sanity check the input for user IDs
 | ||||
|     if (isEmailAddress) { | ||||
|         return 'email'; | ||||
|     } else if (isUserId) { | ||||
|         return 'mx-user-id'; | ||||
|     } else if (isRoomId) { | ||||
|         return 'mx-room-id'; | ||||
| export function getAddressType(inputText: string): AddressType | null { | ||||
|     if (emailRegex.test(inputText)) { | ||||
|         return AddressType.Email; | ||||
|     } else if (mxUserIdRegex.test(inputText)) { | ||||
|         return AddressType.MatrixUserId; | ||||
|     } else if (mxRoomIdRegex.test(inputText)) { | ||||
|         return AddressType.MatrixRoomId; | ||||
|     } else { | ||||
|         return null; | ||||
|     } | ||||
|  | @ -24,6 +24,7 @@ import CustomRoomTagPanel from "./CustomRoomTagPanel"; | |||
| import dis from "../../dispatcher/dispatcher"; | ||||
| import { _t } from "../../languageHandler"; | ||||
| import RoomList from "../views/rooms/RoomList"; | ||||
| import CallHandler from "../../CallHandler"; | ||||
| import { HEADER_HEIGHT } from "../views/rooms/RoomSublist"; | ||||
| import { Action } from "../../dispatcher/actions"; | ||||
| import UserMenu from "./UserMenu"; | ||||
|  | @ -124,6 +125,10 @@ export default class LeftPanel extends React.Component<IProps, IState> { | |||
|         this.setState({ activeSpace }); | ||||
|     }; | ||||
| 
 | ||||
|     private onDialPad = () => { | ||||
|         dis.fire(Action.OpenDialPad); | ||||
|     } | ||||
| 
 | ||||
|     private onExplore = () => { | ||||
|         dis.fire(Action.ViewRoomDirectory); | ||||
|     }; | ||||
|  | @ -397,7 +402,20 @@ export default class LeftPanel extends React.Component<IProps, IState> { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private renderSearchExplore(): React.ReactNode { | ||||
|     private renderSearchDialExplore(): React.ReactNode { | ||||
|         let dialPadButton = null; | ||||
| 
 | ||||
|         // If we have dialer support, show a button to bring up the dial pad
 | ||||
|         // to start a new call
 | ||||
|         if (CallHandler.sharedInstance().getSupportsPstnProtocol()) { | ||||
|             dialPadButton = | ||||
|                 <AccessibleTooltipButton | ||||
|                     className={classNames("mx_LeftPanel_dialPadButton", {})} | ||||
|                     onClick={this.onDialPad} | ||||
|                     title={_t("Open dial pad")} | ||||
|                 />; | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|             <div | ||||
|                 className="mx_LeftPanel_filterContainer" | ||||
|  | @ -410,6 +428,9 @@ export default class LeftPanel extends React.Component<IProps, IState> { | |||
|                     onKeyDown={this.onKeyDown} | ||||
|                     onSelectRoom={this.selectRoom} | ||||
|                 /> | ||||
| 
 | ||||
|                 {dialPadButton} | ||||
| 
 | ||||
|                 <AccessibleTooltipButton | ||||
|                     className={classNames("mx_LeftPanel_exploreButton", { | ||||
|                         mx_LeftPanel_exploreButton_space: !!this.state.activeSpace, | ||||
|  | @ -458,7 +479,7 @@ export default class LeftPanel extends React.Component<IProps, IState> { | |||
|                 {leftLeftPanel} | ||||
|                 <aside className="mx_LeftPanel_roomListContainer"> | ||||
|                     {this.renderHeader()} | ||||
|                     {this.renderSearchExplore()} | ||||
|                     {this.renderSearchDialExplore()} | ||||
|                     {this.renderBreadcrumbs()} | ||||
|                     <RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} /> | ||||
|                     <div className="mx_LeftPanel_roomListWrapper"> | ||||
|  |  | |||
|  | @ -48,7 +48,7 @@ import createRoom, {IOpts} from "../../createRoom"; | |||
| import {_t, _td, getCurrentLanguage} from '../../languageHandler'; | ||||
| import SettingsStore from "../../settings/SettingsStore"; | ||||
| import ThemeController from "../../settings/controllers/ThemeController"; | ||||
| import { startAnyRegistrationFlow } from "../../Registration.js"; | ||||
| import { startAnyRegistrationFlow } from "../../Registration"; | ||||
| import { messageForSyncError } from '../../utils/ErrorUtils'; | ||||
| import ResizeNotifier from "../../utils/ResizeNotifier"; | ||||
| import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; | ||||
|  |  | |||
|  | @ -14,58 +14,60 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import React, {RefObject, useContext, useRef, useState} from "react"; | ||||
| import {EventType} from "matrix-js-sdk/src/@types/event"; | ||||
| import {Room} from "matrix-js-sdk/src/models/room"; | ||||
| import {EventSubscription} from "fbemitter"; | ||||
| import React, { RefObject, useContext, useRef, useState } from "react"; | ||||
| import { EventType } from "matrix-js-sdk/src/@types/event"; | ||||
| import { Preset } from "matrix-js-sdk/src/@types/partials"; | ||||
| import { Room } from "matrix-js-sdk/src/models/room"; | ||||
| import { EventSubscription } from "fbemitter"; | ||||
| 
 | ||||
| import MatrixClientContext from "../../contexts/MatrixClientContext"; | ||||
| import RoomAvatar from "../views/avatars/RoomAvatar"; | ||||
| import {_t} from "../../languageHandler"; | ||||
| import { _t } from "../../languageHandler"; | ||||
| import AccessibleButton from "../views/elements/AccessibleButton"; | ||||
| import RoomName from "../views/elements/RoomName"; | ||||
| import RoomTopic from "../views/elements/RoomTopic"; | ||||
| import InlineSpinner from "../views/elements/InlineSpinner"; | ||||
| import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite"; | ||||
| import {useRoomMembers} from "../../hooks/useRoomMembers"; | ||||
| import createRoom, {IOpts} from "../../createRoom"; | ||||
| import { inviteMultipleToRoom, showRoomInviteDialog } from "../../RoomInvite"; | ||||
| import { useRoomMembers } from "../../hooks/useRoomMembers"; | ||||
| import createRoom, { IOpts } from "../../createRoom"; | ||||
| import Field from "../views/elements/Field"; | ||||
| import {useEventEmitter} from "../../hooks/useEventEmitter"; | ||||
| import { useEventEmitter } from "../../hooks/useEventEmitter"; | ||||
| import withValidation from "../views/elements/Validation"; | ||||
| import * as Email from "../../email"; | ||||
| import defaultDispatcher from "../../dispatcher/dispatcher"; | ||||
| import {Action} from "../../dispatcher/actions"; | ||||
| import dis from "../../dispatcher/dispatcher"; | ||||
| import { Action } from "../../dispatcher/actions"; | ||||
| import ResizeNotifier from "../../utils/ResizeNotifier" | ||||
| import MainSplit from './MainSplit'; | ||||
| import ErrorBoundary from "../views/elements/ErrorBoundary"; | ||||
| import {ActionPayload} from "../../dispatcher/payloads"; | ||||
| import { ActionPayload } from "../../dispatcher/payloads"; | ||||
| import RightPanel from "./RightPanel"; | ||||
| import RightPanelStore from "../../stores/RightPanelStore"; | ||||
| import {RightPanelPhases} from "../../stores/RightPanelStorePhases"; | ||||
| import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload"; | ||||
| import {useStateArray} from "../../hooks/useStateArray"; | ||||
| import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; | ||||
| import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPanelPhasePayload"; | ||||
| import { useStateArray } from "../../hooks/useStateArray"; | ||||
| import SpacePublicShare from "../views/spaces/SpacePublicShare"; | ||||
| import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space"; | ||||
| import {showRoom, SpaceHierarchy} from "./SpaceRoomDirectory"; | ||||
| import { shouldShowSpaceSettings, showAddExistingRooms, showCreateNewRoom, showSpaceSettings } from "../../utils/space"; | ||||
| import { showRoom, SpaceHierarchy } from "./SpaceRoomDirectory"; | ||||
| import MemberAvatar from "../views/avatars/MemberAvatar"; | ||||
| import {useStateToggle} from "../../hooks/useStateToggle"; | ||||
| import { useStateToggle } from "../../hooks/useStateToggle"; | ||||
| import SpaceStore from "../../stores/SpaceStore"; | ||||
| import FacePile from "../views/elements/FacePile"; | ||||
| import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog"; | ||||
| import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu"; | ||||
| import { AddExistingToSpace } from "../views/dialogs/AddExistingToSpaceDialog"; | ||||
| import { ChevronFace, ContextMenuButton, useContextMenu } from "./ContextMenu"; | ||||
| import IconizedContextMenu, { | ||||
|     IconizedContextMenuOption, | ||||
|     IconizedContextMenuOptionList, | ||||
| } from "../views/context_menus/IconizedContextMenu"; | ||||
| import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; | ||||
| import {BetaPill} from "../views/beta/BetaCard"; | ||||
| import { BetaPill } from "../views/beta/BetaCard"; | ||||
| import { UserTab } from "../views/dialogs/UserSettingsDialog"; | ||||
| import SettingsStore from "../../settings/SettingsStore"; | ||||
| import dis from "../../dispatcher/dispatcher"; | ||||
| import Modal from "../../Modal"; | ||||
| import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog"; | ||||
| import SdkConfig from "../../SdkConfig"; | ||||
| import { Preset } from "matrix-js-sdk/src/@types/partials"; | ||||
| import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership"; | ||||
| import { JoinRule } from "../views/settings/tabs/room/SecurityRoomSettingsTab"; | ||||
| 
 | ||||
| interface IProps { | ||||
|     space: Room; | ||||
|  | @ -178,6 +180,9 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => | |||
| 
 | ||||
|     const spacesEnabled = SettingsStore.getValue("feature_spaces"); | ||||
| 
 | ||||
|     const cannotJoin = getEffectiveMembership(myMembership) === EffectiveMembership.Leave | ||||
|         && space.getJoinRule() !== JoinRule.Public; | ||||
| 
 | ||||
|     let inviterSection; | ||||
|     let joinButtons; | ||||
|     if (myMembership === "join") { | ||||
|  | @ -244,7 +249,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => | |||
|                     setBusy(true); | ||||
|                     onJoinButtonClicked(); | ||||
|                 }} | ||||
|                 disabled={!spacesEnabled} | ||||
|                 disabled={!spacesEnabled || cannotJoin} | ||||
|             > | ||||
|                 { _t("Join") } | ||||
|             </AccessibleButton> | ||||
|  | @ -255,6 +260,30 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => | |||
|         joinButtons = <InlineSpinner />; | ||||
|     } | ||||
| 
 | ||||
|     let footer; | ||||
|     if (!spacesEnabled) { | ||||
|         footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt"> | ||||
|             { myMembership === "join" | ||||
|                 ? _t("To view %(spaceName)s, turn on the <a>Spaces beta</a>", { | ||||
|                     spaceName: space.name, | ||||
|                 }, { | ||||
|                     a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>, | ||||
|                 }) | ||||
|                 : _t("To join %(spaceName)s, turn on the <a>Spaces beta</a>", { | ||||
|                     spaceName: space.name, | ||||
|                 }, { | ||||
|                     a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>, | ||||
|                 }) | ||||
|             } | ||||
|         </div>; | ||||
|     } else if (cannotJoin) { | ||||
|         footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt"> | ||||
|             { _t("To view %(spaceName)s, you need an invite", { | ||||
|                 spaceName: space.name, | ||||
|             }) } | ||||
|         </div>; | ||||
|     } | ||||
| 
 | ||||
|     return <div className="mx_SpaceRoomView_preview"> | ||||
|         <BetaPill onClick={onBetaClick} /> | ||||
|         { inviterSection } | ||||
|  | @ -274,20 +303,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => | |||
|         <div className="mx_SpaceRoomView_preview_joinButtons"> | ||||
|             { joinButtons } | ||||
|         </div> | ||||
|         { !spacesEnabled && <div className="mx_SpaceRoomView_preview_spaceBetaPrompt"> | ||||
|             { myMembership === "join" | ||||
|                 ? _t("To view %(spaceName)s, turn on the <a>Spaces beta</a>", { | ||||
|                     spaceName: space.name, | ||||
|                 }, { | ||||
|                     a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>, | ||||
|                 }) | ||||
|                 : _t("To join %(spaceName)s, turn on the <a>Spaces beta</a>", { | ||||
|                     spaceName: space.name, | ||||
|                 }, { | ||||
|                     a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>, | ||||
|                 }) | ||||
|             } | ||||
|         </div> } | ||||
|         { footer } | ||||
|     </div>; | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ import { _t, _td } from '../../../languageHandler'; | |||
| import * as sdk from '../../../index'; | ||||
| import {MatrixClientPeg} from '../../../MatrixClientPeg'; | ||||
| import dis from '../../../dispatcher/dispatcher'; | ||||
| import { addressTypes, getAddressType } from '../../../UserAddress.js'; | ||||
| import { addressTypes, getAddressType } from '../../../UserAddress'; | ||||
| import GroupStore from '../../../stores/GroupStore'; | ||||
| import * as Email from '../../../email'; | ||||
| import IdentityAuthClient from '../../../IdentityAuthClient'; | ||||
|  |  | |||
|  | @ -766,7 +766,7 @@ class VerificationExplorer extends React.PureComponent<IExplorerProps> { | |||
|     render() { | ||||
|         const cli = this.context; | ||||
|         const room = this.props.room; | ||||
|         const inRoomChannel = cli.crypto._inRoomVerificationRequests; | ||||
|         const inRoomChannel = cli.crypto.inRoomVerificationRequests; | ||||
|         const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map(); | ||||
| 
 | ||||
|         return (<div> | ||||
|  |  | |||
|  | @ -17,37 +17,45 @@ limitations under the License. | |||
| import React, { createRef } from 'react'; | ||||
| import classNames from 'classnames'; | ||||
| 
 | ||||
| import {_t, _td} from "../../../languageHandler"; | ||||
| import { _t, _td } from "../../../languageHandler"; | ||||
| import * as sdk from "../../../index"; | ||||
| import {MatrixClientPeg} from "../../../MatrixClientPeg"; | ||||
| import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks"; | ||||
| import { MatrixClientPeg } from "../../../MatrixClientPeg"; | ||||
| import { makeRoomPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks"; | ||||
| import DMRoomMap from "../../../utils/DMRoomMap"; | ||||
| import {RoomMember} from "matrix-js-sdk/src/models/room-member"; | ||||
| import { RoomMember } from "matrix-js-sdk/src/models/room-member"; | ||||
| import SdkConfig from "../../../SdkConfig"; | ||||
| import * as Email from "../../../email"; | ||||
| import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils"; | ||||
| import {abbreviateUrl} from "../../../utils/UrlUtils"; | ||||
| import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from "../../../utils/IdentityServerUtils"; | ||||
| import { abbreviateUrl } from "../../../utils/UrlUtils"; | ||||
| import dis from "../../../dispatcher/dispatcher"; | ||||
| import IdentityAuthClient from "../../../IdentityAuthClient"; | ||||
| import Modal from "../../../Modal"; | ||||
| import {humanizeTime} from "../../../utils/humanize"; | ||||
| import { humanizeTime } from "../../../utils/humanize"; | ||||
| import createRoom, { | ||||
|     canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted, | ||||
|     canEncryptToAllUsers, | ||||
|     ensureDMExists, | ||||
|     findDMForUser, | ||||
|     privateShouldBeEncrypted, | ||||
| } from "../../../createRoom"; | ||||
| import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite"; | ||||
| import {Key} from "../../../Keyboard"; | ||||
| import {Action} from "../../../dispatcher/actions"; | ||||
| import {DefaultTagID} from "../../../stores/room-list/models"; | ||||
| import { | ||||
|     IInviteResult, | ||||
|     inviteMultipleToRoom, | ||||
|     showAnyInviteErrors, | ||||
|     showCommunityInviteDialog, | ||||
| } from "../../../RoomInvite"; | ||||
| import { Key } from "../../../Keyboard"; | ||||
| import { Action } from "../../../dispatcher/actions"; | ||||
| import { DefaultTagID } from "../../../stores/room-list/models"; | ||||
| import RoomListStore from "../../../stores/room-list/RoomListStore"; | ||||
| import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore"; | ||||
| import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; | ||||
| import SettingsStore from "../../../settings/SettingsStore"; | ||||
| import {UIFeature} from "../../../settings/UIFeature"; | ||||
| import { UIFeature } from "../../../settings/UIFeature"; | ||||
| import CountlyAnalytics from "../../../CountlyAnalytics"; | ||||
| import {Room} from "matrix-js-sdk/src/models/room"; | ||||
| import { Room } from "matrix-js-sdk/src/models/room"; | ||||
| import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; | ||||
| import {replaceableComponent} from "../../../utils/replaceableComponent"; | ||||
| import {mediaFromMxc} from "../../../customisations/Media"; | ||||
| import {getAddressType} from "../../../UserAddress"; | ||||
| import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||||
| import { mediaFromMxc } from "../../../customisations/Media"; | ||||
| import { getAddressType } from "../../../UserAddress"; | ||||
| import BaseAvatar from '../avatars/BaseAvatar'; | ||||
| import AccessibleButton from '../elements/AccessibleButton'; | ||||
| import { compare } from '../../../utils/strings'; | ||||
|  | @ -74,10 +82,10 @@ export const KIND_CALL_TRANSFER = "call_transfer"; | |||
| const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
 | ||||
| const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
 | ||||
| 
 | ||||
| // This is the interface that is expected by various components in this file. It is a bit
 | ||||
| // awkward because it also matches the RoomMember class from the js-sdk with some extra support
 | ||||
| // This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
 | ||||
| // It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
 | ||||
| // for 3PIDs/email addresses.
 | ||||
| abstract class Member { | ||||
| export abstract class Member { | ||||
|     /** | ||||
|      * The display name of this Member. For users this should be their profile's display | ||||
|      * name or user ID if none set. For 3PIDs this should be the 3PID address (email). | ||||
|  | @ -102,7 +110,8 @@ class DirectoryMember extends Member { | |||
|     private readonly displayName: string; | ||||
|     private readonly avatarUrl: string; | ||||
| 
 | ||||
|     constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) { | ||||
|     // eslint-disable-next-line camelcase
 | ||||
|     constructor(userDirResult: { user_id: string, display_name: string, avatar_url: string }) { | ||||
|         super(); | ||||
|         this._userId = userDirResult.user_id; | ||||
|         this.displayName = userDirResult.display_name; | ||||
|  | @ -601,19 +610,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps | |||
|         return members.map(m => ({userId: m.member.userId, user: m.member})); | ||||
|     } | ||||
| 
 | ||||
|     private shouldAbortAfterInviteError(result): boolean { | ||||
|         const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error'); | ||||
|         if (failedUsers.length > 0) { | ||||
|             console.log("Failed to invite users: ", result); | ||||
|             this.setState({ | ||||
|                 busy: false, | ||||
|                 errorText: _t("Failed to invite the following users to chat: %(csvUsers)s", { | ||||
|                     csvUsers: failedUsers.join(", "), | ||||
|                 }), | ||||
|             }); | ||||
|             return true; // abort
 | ||||
|         } | ||||
|         return false; | ||||
|     private shouldAbortAfterInviteError(result: IInviteResult, room: Room): boolean { | ||||
|         this.setState({ busy: false }); | ||||
|         const userMap = new Map<string, Member>(this.state.targets.map(member => [member.userId, member])); | ||||
|         return !showAnyInviteErrors(result.states, room, result.inviter, userMap); | ||||
|     } | ||||
| 
 | ||||
|     private convertFilter(): Member[] { | ||||
|  | @ -731,7 +731,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps | |||
|         try { | ||||
|             const result = await inviteMultipleToRoom(this.props.roomId, targetIds) | ||||
|             CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length); | ||||
|             if (!this.shouldAbortAfterInviteError(result)) { // handles setting error message too
 | ||||
|             if (!this.shouldAbortAfterInviteError(result, room)) { // handles setting error message too
 | ||||
|                 this.props.onFinished(); | ||||
|             } | ||||
| 
 | ||||
|  |  | |||
|  | @ -20,9 +20,9 @@ import PropTypes from 'prop-types'; | |||
| import classNames from 'classnames'; | ||||
| import * as sdk from "../../../index"; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| import { UserAddressType } from '../../../UserAddress.js'; | ||||
| import {replaceableComponent} from "../../../utils/replaceableComponent"; | ||||
| import {mediaFromMxc} from "../../../customisations/Media"; | ||||
| import { UserAddressType } from '../../../UserAddress'; | ||||
| import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||||
| import { mediaFromMxc } from "../../../customisations/Media"; | ||||
| 
 | ||||
| @replaceableComponent("views.elements.AddressTile") | ||||
| export default class AddressTile extends React.Component { | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ | |||
|  */ | ||||
| 
 | ||||
| import React from 'react'; | ||||
| import Flair from '../elements/Flair.js'; | ||||
| import Flair from '../elements/Flair'; | ||||
| import FlairStore from '../../../stores/FlairStore'; | ||||
| import { getUserNameColorClass } from '../../../utils/FormattingUtils'; | ||||
| import MatrixClientContext from "../../../contexts/MatrixClientContext"; | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ export default class TextualEvent extends React.Component { | |||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
|         const text = TextForEvent.textForEvent(this.props.mxEvent); | ||||
|         const text = TextForEvent.textForEvent(this.props.mxEvent, true); | ||||
|         if (text == null || text.length === 0) return null; | ||||
|         return ( | ||||
|             <div className="mx_TextualEvent">{ text }</div> | ||||
|  |  | |||
|  | @ -503,7 +503,7 @@ const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) => | |||
|     return member.powerLevel < levelToSend; | ||||
| }; | ||||
| 
 | ||||
| const getPowerLevels = room => room.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {}; | ||||
| const getPowerLevels = room => room?.currentState?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {}; | ||||
| 
 | ||||
| export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => { | ||||
|     const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>(getPowerLevels(room)); | ||||
|  |  | |||
|  | @ -17,13 +17,23 @@ limitations under the License. | |||
| */ | ||||
| 
 | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import * as sdk from '../../../index'; | ||||
| import AccessibleButton from '../elements/AccessibleButton'; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| import { _td } from '../../../languageHandler'; | ||||
| import classNames from "classnames"; | ||||
| import E2EIcon from './E2EIcon'; | ||||
| import {replaceableComponent} from "../../../utils/replaceableComponent"; | ||||
| import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||||
| import BaseAvatar from '../avatars/BaseAvatar'; | ||||
| import PresenceLabel from "./PresenceLabel"; | ||||
| 
 | ||||
| export enum PowerStatus { | ||||
|     Admin = "admin", | ||||
|     Moderator = "moderator", | ||||
| } | ||||
| 
 | ||||
| const PowerLabel: Record<PowerStatus, string> = { | ||||
|     [PowerStatus.Admin]: _td("Admin"), | ||||
|     [PowerStatus.Moderator]: _td("Mod"), | ||||
| } | ||||
| 
 | ||||
| const PRESENCE_CLASS = { | ||||
|     "offline": "mx_EntityTile_offline", | ||||
|  | @ -31,14 +41,14 @@ const PRESENCE_CLASS = { | |||
|     "unavailable": "mx_EntityTile_unavailable", | ||||
| }; | ||||
| 
 | ||||
| function presenceClassForMember(presenceState, lastActiveAgo, showPresence) { | ||||
| function presenceClassForMember(presenceState: string, lastActiveAgo: number, showPresence: boolean): string { | ||||
|     if (showPresence === false) { | ||||
|         return 'mx_EntityTile_online_beenactive'; | ||||
|     } | ||||
| 
 | ||||
|     // offline is split into two categories depending on whether we have
 | ||||
|     // a last_active_ago for them.
 | ||||
|     if (presenceState == 'offline') { | ||||
|     if (presenceState === 'offline') { | ||||
|         if (lastActiveAgo) { | ||||
|             return PRESENCE_CLASS['offline'] + '_beenactive'; | ||||
|         } else { | ||||
|  | @ -51,29 +61,32 @@ function presenceClassForMember(presenceState, lastActiveAgo, showPresence) { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| @replaceableComponent("views.rooms.EntityTile") | ||||
| class EntityTile extends React.Component { | ||||
|     static propTypes = { | ||||
|         name: PropTypes.string, | ||||
|         title: PropTypes.string, | ||||
|         avatarJsx: PropTypes.any, // <BaseAvatar />
 | ||||
|         className: PropTypes.string, | ||||
|         presenceState: PropTypes.string, | ||||
|         presenceLastActiveAgo: PropTypes.number, | ||||
|         presenceLastTs: PropTypes.number, | ||||
|         presenceCurrentlyActive: PropTypes.bool, | ||||
|         showInviteButton: PropTypes.bool, | ||||
|         shouldComponentUpdate: PropTypes.func, | ||||
|         onClick: PropTypes.func, | ||||
|         suppressOnHover: PropTypes.bool, | ||||
|         showPresence: PropTypes.bool, | ||||
|         subtextLabel: PropTypes.string, | ||||
|         e2eStatus: PropTypes.string, | ||||
|     }; | ||||
| interface IProps { | ||||
|     name?: string; | ||||
|     title?: string; | ||||
|     avatarJsx?: JSX.Element; // <BaseAvatar />
 | ||||
|     className?: string; | ||||
|     presenceState?: string; | ||||
|     presenceLastActiveAgo?: number; | ||||
|     presenceLastTs?: number; | ||||
|     presenceCurrentlyActive?: boolean; | ||||
|     showInviteButton?: boolean; | ||||
|     onClick?(): void; | ||||
|     suppressOnHover?: boolean; | ||||
|     showPresence?: boolean; | ||||
|     subtextLabel?: string; | ||||
|     e2eStatus?: string; | ||||
|     powerStatus?: PowerStatus; | ||||
| } | ||||
| 
 | ||||
| interface IState { | ||||
|     hover: boolean; | ||||
| } | ||||
| 
 | ||||
| @replaceableComponent("views.rooms.EntityTile") | ||||
| export default class EntityTile extends React.PureComponent<IProps, IState> { | ||||
|     static defaultProps = { | ||||
|         shouldComponentUpdate: function(nextProps, nextState) { return true; }, | ||||
|         onClick: function() {}, | ||||
|         onClick: () => {}, | ||||
|         presenceState: "offline", | ||||
|         presenceLastActiveAgo: 0, | ||||
|         presenceLastTs: 0, | ||||
|  | @ -82,13 +95,12 @@ class EntityTile extends React.Component { | |||
|         showPresence: true, | ||||
|     }; | ||||
| 
 | ||||
|     state = { | ||||
|         hover: false, | ||||
|     }; | ||||
|     constructor(props: IProps) { | ||||
|         super(props); | ||||
| 
 | ||||
|     shouldComponentUpdate(nextProps, nextState) { | ||||
|         if (this.state.hover !== nextState.hover) return true; | ||||
|         return this.props.shouldComponentUpdate(nextProps, nextState); | ||||
|         this.state = { | ||||
|             hover: false, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|  | @ -110,7 +122,6 @@ class EntityTile extends React.Component { | |||
|             const activeAgo = this.props.presenceLastActiveAgo ? | ||||
|                 (Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1; | ||||
| 
 | ||||
|             const PresenceLabel = sdk.getComponent("rooms.PresenceLabel"); | ||||
|             let presenceLabel = null; | ||||
|             if (this.props.showPresence) { | ||||
|                 presenceLabel = <PresenceLabel activeAgo={activeAgo} | ||||
|  | @ -155,10 +166,7 @@ class EntityTile extends React.Component { | |||
|         let powerLabel; | ||||
|         const powerStatus = this.props.powerStatus; | ||||
|         if (powerStatus) { | ||||
|             const powerText = { | ||||
|                 [EntityTile.POWER_STATUS_MODERATOR]: _t("Mod"), | ||||
|                 [EntityTile.POWER_STATUS_ADMIN]: _t("Admin"), | ||||
|             }[powerStatus]; | ||||
|             const powerText = PowerLabel[powerStatus]; | ||||
|             powerLabel = <div className="mx_EntityTile_power">{powerText}</div>; | ||||
|         } | ||||
| 
 | ||||
|  | @ -168,14 +176,12 @@ class EntityTile extends React.Component { | |||
|             e2eIcon = <E2EIcon status={e2eStatus} isUser={true} bordered={true} />; | ||||
|         } | ||||
| 
 | ||||
|         const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); | ||||
| 
 | ||||
|         const av = this.props.avatarJsx || | ||||
|             <BaseAvatar name={this.props.name} width={36} height={36} aria-hidden="true" />; | ||||
| 
 | ||||
|         // The wrapping div is required to make the magic mouse listener work, for some reason.
 | ||||
|         return ( | ||||
|             <div ref={(c) => this.container = c} > | ||||
|             <div> | ||||
|                 <AccessibleButton | ||||
|                     className={classNames(mainClassNames)} | ||||
|                     title={this.props.title} | ||||
|  | @ -193,8 +199,3 @@ class EntityTile extends React.Component { | |||
|         ); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| EntityTile.POWER_STATUS_MODERATOR = "moderator"; | ||||
| EntityTile.POWER_STATUS_ADMIN = "admin"; | ||||
| 
 | ||||
| export default EntityTile; | ||||
|  | @ -2,6 +2,7 @@ | |||
| Copyright 2015, 2016 OpenMarket Ltd | ||||
| Copyright 2017 Vector Creations Ltd | ||||
| Copyright 2017, 2018 New Vector Ltd | ||||
| Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com> | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
|  | @ -20,17 +21,28 @@ import React from 'react'; | |||
| import { _t } from '../../../languageHandler'; | ||||
| import SdkConfig from '../../../SdkConfig'; | ||||
| import dis from '../../../dispatcher/dispatcher'; | ||||
| import {isValid3pidInvite} from "../../../RoomInvite"; | ||||
| import rate_limited_func from "../../../ratelimitedfunc"; | ||||
| import {MatrixClientPeg} from "../../../MatrixClientPeg"; | ||||
| import * as sdk from "../../../index"; | ||||
| import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore"; | ||||
| import { isValid3pidInvite } from "../../../RoomInvite"; | ||||
| import rateLimitedFunction from "../../../ratelimitedfunc"; | ||||
| import { MatrixClientPeg } from "../../../MatrixClientPeg"; | ||||
| import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; | ||||
| import BaseCard from "../right_panel/BaseCard"; | ||||
| import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; | ||||
| import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; | ||||
| import RoomAvatar from "../avatars/RoomAvatar"; | ||||
| import RoomName from "../elements/RoomName"; | ||||
| import {replaceableComponent} from "../../../utils/replaceableComponent"; | ||||
| import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||||
| import SettingsStore from "../../../settings/SettingsStore"; | ||||
| import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; | ||||
| import { Room } from 'matrix-js-sdk/src/models/room'; | ||||
| import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; | ||||
| import { RoomState } from 'matrix-js-sdk/src/models/room-state'; | ||||
| import { User } from "matrix-js-sdk/src/models/user"; | ||||
| import TruncatedList from '../elements/TruncatedList'; | ||||
| import Spinner from "../elements/Spinner"; | ||||
| import SearchBox from "../../structures/SearchBox"; | ||||
| import AccessibleButton from '../elements/AccessibleButton'; | ||||
| import EntityTile from "./EntityTile"; | ||||
| import MemberTile from "./MemberTile"; | ||||
| import BaseAvatar from '../avatars/BaseAvatar'; | ||||
| 
 | ||||
| const INITIAL_LOAD_NUM_MEMBERS = 30; | ||||
| const INITIAL_LOAD_NUM_INVITED = 5; | ||||
|  | @ -40,41 +52,59 @@ const SHOW_MORE_INCREMENT = 100; | |||
| // matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
 | ||||
| const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g; | ||||
| 
 | ||||
| interface IProps { | ||||
|     roomId: string; | ||||
|     onClose(): void; | ||||
| } | ||||
| 
 | ||||
| interface IState { | ||||
|     loading: boolean; | ||||
|     members: Array<RoomMember>; | ||||
|     filteredJoinedMembers: Array<RoomMember>; | ||||
|     filteredInvitedMembers: Array<RoomMember | MatrixEvent>; | ||||
|     canInvite: boolean; | ||||
|     truncateAtJoined: number; | ||||
|     truncateAtInvited: number; | ||||
|     searchQuery: string; | ||||
| } | ||||
| 
 | ||||
| @replaceableComponent("views.rooms.MemberList") | ||||
| export default class MemberList extends React.Component { | ||||
| export default class MemberList extends React.Component<IProps, IState> { | ||||
|     private showPresence = true; | ||||
|     private mounted = false; | ||||
|     private collator: Intl.Collator; | ||||
|     private sortNames = new Map<RoomMember, string>(); // RoomMember -> sortName
 | ||||
| 
 | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
| 
 | ||||
|         const cli = MatrixClientPeg.get(); | ||||
|         if (cli.hasLazyLoadMembersEnabled()) { | ||||
|             // show an empty list
 | ||||
|             this.state = this._getMembersState([]); | ||||
|             this.state = this.getMembersState([]); | ||||
|         } else { | ||||
|             this.state = this._getMembersState(this.roomMembers()); | ||||
|             this.state = this.getMembersState(this.roomMembers()); | ||||
|         } | ||||
| 
 | ||||
|         cli.on("Room", this.onRoom); // invites & joining after peek
 | ||||
|         const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"]; | ||||
|         const hsUrl = MatrixClientPeg.get().baseUrl; | ||||
|         this._showPresence = true; | ||||
|         if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) { | ||||
|             this._showPresence = enablePresenceByHsUrl[hsUrl]; | ||||
|         } | ||||
|         this.showPresence = enablePresenceByHsUrl?.[hsUrl] ?? true; | ||||
|     } | ||||
| 
 | ||||
|     // eslint-disable-next-line camelcase
 | ||||
|     UNSAFE_componentWillMount() { | ||||
|         const cli = MatrixClientPeg.get(); | ||||
|         this._mounted = true; | ||||
|         this.mounted = true; | ||||
|         if (cli.hasLazyLoadMembersEnabled()) { | ||||
|             this._showMembersAccordingToMembershipWithLL(); | ||||
|             this.showMembersAccordingToMembershipWithLL(); | ||||
|             cli.on("Room.myMembership", this.onMyMembership); | ||||
|         } else { | ||||
|             this._listenForMembersChanges(); | ||||
|             this.listenForMembersChanges(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     _listenForMembersChanges() { | ||||
|     private listenForMembersChanges(): void { | ||||
|         const cli = MatrixClientPeg.get(); | ||||
|         cli.on("RoomState.members", this.onRoomStateMember); | ||||
|         cli.on("RoomMember.name", this.onRoomMemberName); | ||||
|  | @ -89,7 +119,7 @@ export default class MemberList extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     componentWillUnmount() { | ||||
|         this._mounted = false; | ||||
|         this.mounted = false; | ||||
|         const cli = MatrixClientPeg.get(); | ||||
|         if (cli) { | ||||
|             cli.removeListener("RoomState.members", this.onRoomStateMember); | ||||
|  | @ -103,7 +133,7 @@ export default class MemberList extends React.Component { | |||
|         } | ||||
| 
 | ||||
|         // cancel any pending calls to the rate_limited_funcs
 | ||||
|         this._updateList.cancelPendingCall(); | ||||
|         this.updateList.cancelPendingCall(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -111,7 +141,7 @@ export default class MemberList extends React.Component { | |||
|      * show a spinner and load the members if the user is joined, | ||||
|      * or show the members available so far if the user is invited | ||||
|      */ | ||||
|     async _showMembersAccordingToMembershipWithLL() { | ||||
|     private async showMembersAccordingToMembershipWithLL(): Promise<void> { | ||||
|         const cli = MatrixClientPeg.get(); | ||||
|         if (cli.hasLazyLoadMembersEnabled()) { | ||||
|             const cli = MatrixClientPeg.get(); | ||||
|  | @ -122,31 +152,31 @@ export default class MemberList extends React.Component { | |||
|                 try { | ||||
|                     await room.loadMembersIfNeeded(); | ||||
|                 } catch (ex) {/* already logged in RoomView */} | ||||
|                 if (this._mounted) { | ||||
|                     this.setState(this._getMembersState(this.roomMembers())); | ||||
|                     this._listenForMembersChanges(); | ||||
|                 if (this.mounted) { | ||||
|                     this.setState(this.getMembersState(this.roomMembers())); | ||||
|                     this.listenForMembersChanges(); | ||||
|                 } | ||||
|             } else { | ||||
|                 // show the members we already have loaded
 | ||||
|                 this.setState(this._getMembersState(this.roomMembers())); | ||||
|                 this.setState(this.getMembersState(this.roomMembers())); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     get canInvite() { | ||||
|     private get canInvite(): boolean { | ||||
|         const cli = MatrixClientPeg.get(); | ||||
|         const room = cli.getRoom(this.props.roomId); | ||||
|         return room && room.canInvite(cli.getUserId()); | ||||
|     } | ||||
| 
 | ||||
|     _getMembersState(members) { | ||||
|         // set the state after determining _showPresence to make sure it's
 | ||||
|         // taken into account while rerendering
 | ||||
|     private getMembersState(members: Array<RoomMember>): IState { | ||||
|         // set the state after determining showPresence to make sure it's
 | ||||
|         // taken into account while rendering
 | ||||
|         return { | ||||
|             loading: false, | ||||
|             members: members, | ||||
|             filteredJoinedMembers: this._filterMembers(members, 'join'), | ||||
|             filteredInvitedMembers: this._filterMembers(members, 'invite'), | ||||
|             filteredJoinedMembers: this.filterMembers(members, 'join'), | ||||
|             filteredInvitedMembers: this.filterMembers(members, 'invite'), | ||||
|             canInvite: this.canInvite, | ||||
| 
 | ||||
|             // ideally we'd size this to the page height, but
 | ||||
|  | @ -157,72 +187,72 @@ export default class MemberList extends React.Component { | |||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     onUserPresenceChange = (event, user) => { | ||||
|     private onUserPresenceChange = (event: MatrixEvent, user: User): void => { | ||||
|         // Attach a SINGLE listener for global presence changes then locate the
 | ||||
|         // member tile and re-render it. This is more efficient than every tile
 | ||||
|         // ever attaching their own listener.
 | ||||
|         const tile = this.refs[user.userId]; | ||||
|         // console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`);
 | ||||
|         if (tile) { | ||||
|             this._updateList(); // reorder the membership list
 | ||||
|             this.updateList(); // reorder the membership list
 | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     onRoom = room => { | ||||
|     private onRoom = (room: Room): void => { | ||||
|         if (room.roomId !== this.props.roomId) { | ||||
|             return; | ||||
|         } | ||||
|         // We listen for room events because when we accept an invite
 | ||||
|         // we need to wait till the room is fully populated with state
 | ||||
|         // before refreshing the member list else we get a stale list.
 | ||||
|         this._showMembersAccordingToMembershipWithLL(); | ||||
|         this.showMembersAccordingToMembershipWithLL(); | ||||
|     }; | ||||
| 
 | ||||
|     onMyMembership = (room, membership, oldMembership) => { | ||||
|     private onMyMembership = (room: Room, membership: string, oldMembership: string): void => { | ||||
|         if (room.roomId === this.props.roomId && membership === "join") { | ||||
|             this._showMembersAccordingToMembershipWithLL(); | ||||
|             this.showMembersAccordingToMembershipWithLL(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     onRoomStateMember = (ev, state, member) => { | ||||
|     private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember): void => { | ||||
|         if (member.roomId !== this.props.roomId) { | ||||
|             return; | ||||
|         } | ||||
|         this._updateList(); | ||||
|         this.updateList(); | ||||
|     }; | ||||
| 
 | ||||
|     onRoomMemberName = (ev, member) => { | ||||
|     private onRoomMemberName = (ev: MatrixEvent, member: RoomMember): void => { | ||||
|         if (member.roomId !== this.props.roomId) { | ||||
|             return; | ||||
|         } | ||||
|         this._updateList(); | ||||
|         this.updateList(); | ||||
|     }; | ||||
| 
 | ||||
|     onRoomStateEvent = (event, state) => { | ||||
|     private onRoomStateEvent = (event: MatrixEvent, state: RoomState): void => { | ||||
|         if (event.getRoomId() === this.props.roomId && | ||||
|             event.getType() === "m.room.third_party_invite") { | ||||
|             this._updateList(); | ||||
|             this.updateList(); | ||||
|         } | ||||
| 
 | ||||
|         if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite }); | ||||
|     }; | ||||
| 
 | ||||
|     _updateList = rate_limited_func(() => { | ||||
|         this._updateListNow(); | ||||
|     private updateList = rateLimitedFunction(() => { | ||||
|         this.updateListNow(); | ||||
|     }, 500); | ||||
| 
 | ||||
|     _updateListNow() { | ||||
|         // console.log("Updating memberlist");
 | ||||
|         const newState = { | ||||
|     private updateListNow(): void { | ||||
|         const members = this.roomMembers() | ||||
| 
 | ||||
|         this.setState({ | ||||
|             loading: false, | ||||
|             members: this.roomMembers(), | ||||
|         }; | ||||
|         newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery); | ||||
|         newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite', this.state.searchQuery); | ||||
|         this.setState(newState); | ||||
|             members: members, | ||||
|             filteredJoinedMembers: this.filterMembers(members, 'join', this.state.searchQuery), | ||||
|             filteredInvitedMembers: this.filterMembers(members, 'invite', this.state.searchQuery), | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     getMembersWithUser() { | ||||
|     private getMembersWithUser(): Array<RoomMember> { | ||||
|         if (!this.props.roomId) return []; | ||||
|         const cli = MatrixClientPeg.get(); | ||||
|         const room = cli.getRoom(this.props.roomId); | ||||
|  | @ -230,15 +260,18 @@ export default class MemberList extends React.Component { | |||
| 
 | ||||
|         const allMembers = Object.values(room.currentState.members); | ||||
| 
 | ||||
|         allMembers.forEach(function(member) { | ||||
|         allMembers.forEach((member) => { | ||||
|             // work around a race where you might have a room member object
 | ||||
|             // before the user object exists.  This may or may not cause
 | ||||
|             // before the user object exists. This may or may not cause
 | ||||
|             // https://github.com/vector-im/vector-web/issues/186
 | ||||
|             if (member.user === null) { | ||||
|             if (!member.user) { | ||||
|                 member.user = cli.getUser(member.userId); | ||||
|             } | ||||
| 
 | ||||
|             member.sortName = (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, ""); | ||||
|             this.sortNames.set( | ||||
|                 member, | ||||
|                 (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, ""), | ||||
|             ); | ||||
| 
 | ||||
|             // XXX: this user may have no lastPresenceTs value!
 | ||||
|             // the right solution here is to fix the race rather than leave it as 0
 | ||||
|  | @ -247,7 +280,7 @@ export default class MemberList extends React.Component { | |||
|         return allMembers; | ||||
|     } | ||||
| 
 | ||||
|     roomMembers() { | ||||
|     private roomMembers(): Array<RoomMember> { | ||||
|         const allMembers = this.getMembersWithUser(); | ||||
|         const filteredAndSortedMembers = allMembers.filter((m) => { | ||||
|             return ( | ||||
|  | @ -255,23 +288,21 @@ export default class MemberList extends React.Component { | |||
|             ); | ||||
|         }); | ||||
|         const language = SettingsStore.getValue("language"); | ||||
|         this.collator = new Intl.Collator(language, { sensitivity: 'base', usePunctuation: true }); | ||||
|         this.collator = new Intl.Collator(language, { sensitivity: 'base', ignorePunctuation: false }); | ||||
|         filteredAndSortedMembers.sort(this.memberSort); | ||||
|         return filteredAndSortedMembers; | ||||
|     } | ||||
| 
 | ||||
|     _createOverflowTileJoined = (overflowCount, totalCount) => { | ||||
|         return this._createOverflowTile(overflowCount, totalCount, this._showMoreJoinedMemberList); | ||||
|     private createOverflowTileJoined = (overflowCount: number, totalCount: number): JSX.Element => { | ||||
|         return this.createOverflowTile(overflowCount, totalCount, this.showMoreJoinedMemberList); | ||||
|     }; | ||||
| 
 | ||||
|     _createOverflowTileInvited = (overflowCount, totalCount) => { | ||||
|         return this._createOverflowTile(overflowCount, totalCount, this._showMoreInvitedMemberList); | ||||
|     private createOverflowTileInvited = (overflowCount: number, totalCount: number): JSX.Element => { | ||||
|         return this.createOverflowTile(overflowCount, totalCount, this.showMoreInvitedMemberList); | ||||
|     }; | ||||
| 
 | ||||
|     _createOverflowTile = (overflowCount, totalCount, onClick) => { | ||||
|     private createOverflowTile = (overflowCount: number, totalCount: number, onClick: () => void): JSX.Element=> { | ||||
|         // For now we'll pretend this is any entity. It should probably be a separate tile.
 | ||||
|         const EntityTile = sdk.getComponent("rooms.EntityTile"); | ||||
|         const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); | ||||
|         const text = _t("and %(count)s others...", { count: overflowCount }); | ||||
|         return ( | ||||
|             <EntityTile className="mx_EntityTile_ellipsis" avatarJsx={ | ||||
|  | @ -281,31 +312,48 @@ export default class MemberList extends React.Component { | |||
|         ); | ||||
|     }; | ||||
| 
 | ||||
|     _showMoreJoinedMemberList = () => { | ||||
|     private showMoreJoinedMemberList = (): void => { | ||||
|         this.setState({ | ||||
|             truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     _showMoreInvitedMemberList = () => { | ||||
|     private showMoreInvitedMemberList = (): void => { | ||||
|         this.setState({ | ||||
|             truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     memberString(member) { | ||||
|     /** | ||||
|      * SHOULD ONLY BE USED BY TESTS | ||||
|      */ | ||||
|     public memberString(member: RoomMember): string { | ||||
|         if (!member) { | ||||
|             return "(null)"; | ||||
|         } else { | ||||
|             const u = member.user; | ||||
|             return "(" + member.name + ", " + member.powerLevel + ", " + (u ? u.lastActiveAgo : "<null>") + ", " + (u ? u.getLastActiveTs() : "<null>") + ", " + (u ? u.currentlyActive : "<null>") + ", " + (u ? u.presence : "<null>") + ")"; | ||||
|             return ( | ||||
|                 "(" + | ||||
|                 member.name + | ||||
|                 ", " + | ||||
|                 member.powerLevel + | ||||
|                 ", " + | ||||
|                 (u ? u.lastActiveAgo : "<null>") + | ||||
|                 ", " + | ||||
|                 (u ? u.getLastActiveTs() : "<null>") + | ||||
|                 ", " + | ||||
|                 (u ? u.currentlyActive : "<null>") + | ||||
|                 ", " + | ||||
|                 (u ? u.presence : "<null>") + | ||||
|                 ")" | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // returns negative if a comes before b,
 | ||||
|     // returns 0 if a and b are equivalent in ordering
 | ||||
|     // returns positive if a comes after b.
 | ||||
|     memberSort = (memberA, memberB) => { | ||||
|     private memberSort = (memberA: RoomMember, memberB: RoomMember): number => { | ||||
|         // order by presence, with "active now" first.
 | ||||
|         // ...and then by power level
 | ||||
|         // ...and then by last active
 | ||||
|  | @ -325,7 +373,7 @@ export default class MemberList extends React.Component { | |||
|         if (!userA && userB) return 1; | ||||
| 
 | ||||
|         // First by presence
 | ||||
|         if (this._showPresence) { | ||||
|         if (this.showPresence) { | ||||
|             const convertPresence = (p) => p === 'unavailable' ? 'online' : p; | ||||
|             const presenceIndex = p => { | ||||
|                 const order = ['active', 'online', 'offline']; | ||||
|  | @ -349,31 +397,31 @@ export default class MemberList extends React.Component { | |||
|         } | ||||
| 
 | ||||
|         // Third by last active
 | ||||
|         if (this._showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) { | ||||
|         if (this.showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) { | ||||
|             // console.log("Comparing on last active timestamp - returning");
 | ||||
|             return userB.getLastActiveTs() - userA.getLastActiveTs(); | ||||
|         } | ||||
| 
 | ||||
|         // Fourth by name (alphabetical)
 | ||||
|         return this.collator.compare(memberA.sortName, memberB.sortName); | ||||
|         return this.collator.compare(this.sortNames.get(memberA), this.sortNames.get(memberB)); | ||||
|     }; | ||||
| 
 | ||||
|     onSearchQueryChanged = searchQuery => { | ||||
|     private onSearchQueryChanged = (searchQuery: string): void => { | ||||
|         this.setState({ | ||||
|             searchQuery, | ||||
|             filteredJoinedMembers: this._filterMembers(this.state.members, 'join', searchQuery), | ||||
|             filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', searchQuery), | ||||
|             filteredJoinedMembers: this.filterMembers(this.state.members, 'join', searchQuery), | ||||
|             filteredInvitedMembers: this.filterMembers(this.state.members, 'invite', searchQuery), | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     _onPending3pidInviteClick = inviteEvent => { | ||||
|     private onPending3pidInviteClick = (inviteEvent: MatrixEvent): void => { | ||||
|         dis.dispatch({ | ||||
|             action: 'view_3pid_invite', | ||||
|             event: inviteEvent, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     _filterMembers(members, membership, query) { | ||||
|     private filterMembers(members: Array<RoomMember>, membership: string, query?: string): Array<RoomMember> { | ||||
|         return members.filter((m) => { | ||||
|             if (query) { | ||||
|                 query = query.toLowerCase(); | ||||
|  | @ -389,7 +437,7 @@ export default class MemberList extends React.Component { | |||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     _getPending3PidInvites() { | ||||
|     private getPending3PidInvites(): Array<MatrixEvent> { | ||||
|         // include 3pid invites (m.room.third_party_invite) state events.
 | ||||
|         // The HS may have already converted these into m.room.member invites so
 | ||||
|         // we shouldn't add them if the 3pid invite state key (token) is in the
 | ||||
|  | @ -409,42 +457,40 @@ export default class MemberList extends React.Component { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     _makeMemberTiles(members) { | ||||
|         const MemberTile = sdk.getComponent("rooms.MemberTile"); | ||||
|         const EntityTile = sdk.getComponent("rooms.EntityTile"); | ||||
| 
 | ||||
|     private makeMemberTiles(members: Array<RoomMember | MatrixEvent>) { | ||||
|         return members.map((m) => { | ||||
|             if (m.userId) { | ||||
|             if (m instanceof RoomMember) { | ||||
|                 // Is a Matrix invite
 | ||||
|                 return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this._showPresence} />; | ||||
|                 return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this.showPresence} />; | ||||
|             } else { | ||||
|                 // Is a 3pid invite
 | ||||
|                 return <EntityTile key={m.getStateKey()} name={m.getContent().display_name} suppressOnHover={true} | ||||
|                     onClick={() => this._onPending3pidInviteClick(m)} />; | ||||
|                     onClick={() => this.onPending3pidInviteClick(m)} />; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     _getChildrenJoined = (start, end) => this._makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end)); | ||||
| 
 | ||||
|     _getChildCountJoined = () => this.state.filteredJoinedMembers.length; | ||||
| 
 | ||||
|     _getChildrenInvited = (start, end) => { | ||||
|         let targets = this.state.filteredInvitedMembers; | ||||
|         if (end > this.state.filteredInvitedMembers.length) { | ||||
|             targets = targets.concat(this._getPending3PidInvites()); | ||||
|         } | ||||
| 
 | ||||
|         return this._makeMemberTiles(targets.slice(start, end)); | ||||
|     private getChildrenJoined = (start: number, end: number): Array<JSX.Element> => { | ||||
|         return this.makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end)) | ||||
|     }; | ||||
| 
 | ||||
|     _getChildCountInvited = () => { | ||||
|         return this.state.filteredInvitedMembers.length + (this._getPending3PidInvites() || []).length; | ||||
|     private getChildCountJoined = (): number => this.state.filteredJoinedMembers.length; | ||||
| 
 | ||||
|     private getChildrenInvited = (start: number, end: number): Array<JSX.Element> => { | ||||
|         let targets = this.state.filteredInvitedMembers; | ||||
|         if (end > this.state.filteredInvitedMembers.length) { | ||||
|             targets = targets.concat(this.getPending3PidInvites()); | ||||
|         } | ||||
| 
 | ||||
|         return this.makeMemberTiles(targets.slice(start, end)); | ||||
|     }; | ||||
| 
 | ||||
|     private getChildCountInvited = (): number => { | ||||
|         return this.state.filteredInvitedMembers.length + (this.getPending3PidInvites() || []).length; | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         if (this.state.loading) { | ||||
|             const Spinner = sdk.getComponent("elements.Spinner"); | ||||
|             return <BaseCard | ||||
|                 className="mx_MemberList" | ||||
|                 onClose={this.props.onClose} | ||||
|  | @ -454,9 +500,6 @@ export default class MemberList extends React.Component { | |||
|             </BaseCard>; | ||||
|         } | ||||
| 
 | ||||
|         const SearchBox = sdk.getComponent('structures.SearchBox'); | ||||
|         const TruncatedList = sdk.getComponent("elements.TruncatedList"); | ||||
| 
 | ||||
|         const cli = MatrixClientPeg.get(); | ||||
|         const room = cli.getRoom(this.props.roomId); | ||||
|         let inviteButton; | ||||
|  | @ -470,22 +513,30 @@ export default class MemberList extends React.Component { | |||
|                 inviteButtonText = _t("Invite to this space"); | ||||
|             } | ||||
| 
 | ||||
|             const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); | ||||
|             inviteButton = | ||||
|                 <AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick} disabled={!this.state.canInvite}> | ||||
|             inviteButton = ( | ||||
|                 <AccessibleButton | ||||
|                     className="mx_MemberList_invite" | ||||
|                     onClick={this.onInviteButtonClick} | ||||
|                     disabled={!this.state.canInvite} | ||||
|                 > | ||||
|                     <span>{ inviteButtonText }</span> | ||||
|                 </AccessibleButton>; | ||||
|                 </AccessibleButton> | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         let invitedHeader; | ||||
|         let invitedSection; | ||||
|         if (this._getChildCountInvited() > 0) { | ||||
|         if (this.getChildCountInvited() > 0) { | ||||
|             invitedHeader = <h2>{ _t("Invited") }</h2>; | ||||
|             invitedSection = <TruncatedList className="mx_MemberList_section mx_MemberList_invited" truncateAt={this.state.truncateAtInvited} | ||||
|                 createOverflowElement={this._createOverflowTileInvited} | ||||
|                 getChildren={this._getChildrenInvited} | ||||
|                 getChildCount={this._getChildCountInvited} | ||||
|             />; | ||||
|             invitedSection = ( | ||||
|                 <TruncatedList | ||||
|                     className="mx_MemberList_section mx_MemberList_invited" | ||||
|                     truncateAt={this.state.truncateAtInvited} | ||||
|                     createOverflowElement={this.createOverflowTileInvited} | ||||
|                     getChildren={this.getChildrenInvited} | ||||
|                     getChildCount={this.getChildCountInvited} | ||||
|                 /> | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         const footer = ( | ||||
|  | @ -517,17 +568,19 @@ export default class MemberList extends React.Component { | |||
|             previousPhase={previousPhase} | ||||
|         > | ||||
|             <div className="mx_MemberList_wrapper"> | ||||
|                 <TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined} | ||||
|                     createOverflowElement={this._createOverflowTileJoined} | ||||
|                     getChildren={this._getChildrenJoined} | ||||
|                     getChildCount={this._getChildCountJoined} /> | ||||
|                 <TruncatedList | ||||
|                     className="mx_MemberList_section mx_MemberList_joined" | ||||
|                     truncateAt={this.state.truncateAtJoined} | ||||
|                     createOverflowElement={this.createOverflowTileJoined} | ||||
|                     getChildren={this.getChildrenJoined} | ||||
|                     getChildCount={this.getChildCountJoined} /> | ||||
|                 { invitedHeader } | ||||
|                 { invitedSection } | ||||
|             </div> | ||||
|         </BaseCard>; | ||||
|     } | ||||
| 
 | ||||
|     onInviteButtonClick = () => { | ||||
|     onInviteButtonClick = (): void => { | ||||
|         if (MatrixClientPeg.get().isGuest()) { | ||||
|             dis.dispatch({action: 'require_registration'}); | ||||
|             return; | ||||
|  | @ -17,20 +17,33 @@ limitations under the License. | |||
| 
 | ||||
| import SettingsStore from "../../../settings/SettingsStore"; | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import * as sdk from "../../../index"; | ||||
| import dis from "../../../dispatcher/dispatcher"; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| import { MatrixClientPeg } from "../../../MatrixClientPeg"; | ||||
| import {Action} from "../../../dispatcher/actions"; | ||||
| import {replaceableComponent} from "../../../utils/replaceableComponent"; | ||||
| import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||||
| import { RoomMember } from "matrix-js-sdk/src/models/room-member"; | ||||
| import { MatrixEvent } from "matrix-js-sdk/src/models/event"; | ||||
| import { EventType } from "matrix-js-sdk/src/@types/event"; | ||||
| import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; | ||||
| import EntityTile, { PowerStatus } from "./EntityTile"; | ||||
| import MemberAvatar from "./../avatars/MemberAvatar"; | ||||
| 
 | ||||
| interface IProps { | ||||
|     member: RoomMember; | ||||
|     showPresence?: boolean; | ||||
| } | ||||
| 
 | ||||
| interface IState { | ||||
|     statusMessage: string; | ||||
|     isRoomEncrypted: boolean; | ||||
|     e2eStatus: string; | ||||
| } | ||||
| 
 | ||||
| @replaceableComponent("views.rooms.MemberTile") | ||||
| export default class MemberTile extends React.Component { | ||||
|     static propTypes = { | ||||
|         member: PropTypes.any.isRequired, // RoomMember
 | ||||
|         showPresence: PropTypes.bool, | ||||
|     }; | ||||
| export default class MemberTile extends React.Component<IProps, IState> { | ||||
|     private userLastModifiedTime: number; | ||||
|     private memberLastModifiedTime: number; | ||||
| 
 | ||||
|     static defaultProps = { | ||||
|         showPresence: true, | ||||
|  | @ -52,7 +65,7 @@ export default class MemberTile extends React.Component { | |||
|         if (SettingsStore.getValue("feature_custom_status")) { | ||||
|             const { user } = this.props.member; | ||||
|             if (user) { | ||||
|                 user.on("User._unstable_statusMessage", this._onStatusMessageCommitted); | ||||
|                 user.on("User._unstable_statusMessage", this.onStatusMessageCommitted); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -80,7 +93,7 @@ export default class MemberTile extends React.Component { | |||
|         if (user) { | ||||
|             user.removeListener( | ||||
|                 "User._unstable_statusMessage", | ||||
|                 this._onStatusMessageCommitted, | ||||
|                 this.onStatusMessageCommitted, | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|  | @ -91,8 +104,8 @@ export default class MemberTile extends React.Component { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     onRoomStateEvents = ev => { | ||||
|         if (ev.getType() !== "m.room.encryption") return; | ||||
|     private onRoomStateEvents = (ev: MatrixEvent): void => { | ||||
|         if (ev.getType() !== EventType.RoomEncryption) return; | ||||
|         const { roomId } = this.props.member; | ||||
|         if (ev.getRoomId() !== roomId) return; | ||||
| 
 | ||||
|  | @ -105,17 +118,17 @@ export default class MemberTile extends React.Component { | |||
|         this.updateE2EStatus(); | ||||
|     }; | ||||
| 
 | ||||
|     onUserTrustStatusChanged = (userId, trustStatus) => { | ||||
|     private onUserTrustStatusChanged = (userId: string, trustStatus: string): void => { | ||||
|         if (userId !== this.props.member.userId) return; | ||||
|         this.updateE2EStatus(); | ||||
|     }; | ||||
| 
 | ||||
|     onDeviceVerificationChanged = (userId, deviceId, deviceInfo) => { | ||||
|     private onDeviceVerificationChanged = (userId: string, deviceId: string, deviceInfo: DeviceInfo): void => { | ||||
|         if (userId !== this.props.member.userId) return; | ||||
|         this.updateE2EStatus(); | ||||
|     }; | ||||
| 
 | ||||
|     async updateE2EStatus() { | ||||
|     private async updateE2EStatus(): Promise<void> { | ||||
|         const cli = MatrixClientPeg.get(); | ||||
|         const { userId } = this.props.member; | ||||
|         const isMe = userId === cli.getUserId(); | ||||
|  | @ -143,32 +156,32 @@ export default class MemberTile extends React.Component { | |||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     getStatusMessage() { | ||||
|     private getStatusMessage(): string { | ||||
|         const { user } = this.props.member; | ||||
|         if (!user) { | ||||
|             return ""; | ||||
|         } | ||||
|         return user._unstable_statusMessage; | ||||
|         return user.unstable_statusMessage; | ||||
|     } | ||||
| 
 | ||||
|     _onStatusMessageCommitted = () => { | ||||
|     private onStatusMessageCommitted = (): void => { | ||||
|         // The `User` object has observed a status message change.
 | ||||
|         this.setState({ | ||||
|             statusMessage: this.getStatusMessage(), | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     shouldComponentUpdate(nextProps, nextState) { | ||||
|     shouldComponentUpdate(nextProps: IProps, nextState: IState): boolean { | ||||
|         if ( | ||||
|             this.member_last_modified_time === undefined || | ||||
|             this.member_last_modified_time < nextProps.member.getLastModifiedTime() | ||||
|             this.memberLastModifiedTime === undefined || | ||||
|             this.memberLastModifiedTime < nextProps.member.getLastModifiedTime() | ||||
|         ) { | ||||
|             return true; | ||||
|         } | ||||
|         if ( | ||||
|             nextProps.member.user && | ||||
|             (this.user_last_modified_time === undefined || | ||||
|             this.user_last_modified_time < nextProps.member.user.getLastModifiedTime()) | ||||
|             (this.userLastModifiedTime === undefined || | ||||
|             this.userLastModifiedTime < nextProps.member.user.getLastModifiedTime()) | ||||
|         ) { | ||||
|             return true; | ||||
|         } | ||||
|  | @ -181,18 +194,18 @@ export default class MemberTile extends React.Component { | |||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     onClick = e => { | ||||
|     private onClick = (): void => { | ||||
|         dis.dispatch({ | ||||
|             action: Action.ViewUser, | ||||
|             member: this.props.member, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     _getDisplayName() { | ||||
|     private getDisplayName(): string { | ||||
|         return this.props.member.name; | ||||
|     } | ||||
| 
 | ||||
|     getPowerLabel() { | ||||
|     private getPowerLabel(): string { | ||||
|         return _t("%(userName)s (power %(powerLevelNumber)s)", { | ||||
|             userName: this.props.member.userId, | ||||
|             powerLevelNumber: this.props.member.powerLevel, | ||||
|  | @ -200,11 +213,8 @@ export default class MemberTile extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); | ||||
|         const EntityTile = sdk.getComponent('rooms.EntityTile'); | ||||
| 
 | ||||
|         const member = this.props.member; | ||||
|         const name = this._getDisplayName(); | ||||
|         const name = this.getDisplayName(); | ||||
|         const presenceState = member.user ? member.user.presence : null; | ||||
| 
 | ||||
|         let statusMessage = null; | ||||
|  | @ -217,13 +227,13 @@ export default class MemberTile extends React.Component { | |||
|         ); | ||||
| 
 | ||||
|         if (member.user) { | ||||
|             this.user_last_modified_time = member.user.getLastModifiedTime(); | ||||
|             this.userLastModifiedTime = member.user.getLastModifiedTime(); | ||||
|         } | ||||
|         this.member_last_modified_time = member.getLastModifiedTime(); | ||||
|         this.memberLastModifiedTime = member.getLastModifiedTime(); | ||||
| 
 | ||||
|         const powerStatusMap = new Map([ | ||||
|             [100, EntityTile.POWER_STATUS_ADMIN], | ||||
|             [50, EntityTile.POWER_STATUS_MODERATOR], | ||||
|             [100, PowerStatus.Admin], | ||||
|             [50, PowerStatus.Moderator], | ||||
|         ]); | ||||
| 
 | ||||
|         // Find the nearest power level with a badge
 | ||||
|  | @ -15,26 +15,23 @@ limitations under the License. | |||
| */ | ||||
| 
 | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| 
 | ||||
| import { _t } from '../../../languageHandler'; | ||||
| import {replaceableComponent} from "../../../utils/replaceableComponent"; | ||||
| import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||||
| 
 | ||||
| interface IProps { | ||||
|     // number of milliseconds ago this user was last active.
 | ||||
|     // zero = unknown
 | ||||
|     activeAgo?: number; | ||||
|     // if true, activeAgo is an approximation and "Now" should
 | ||||
|     // be shown instead
 | ||||
|     currentlyActive?: boolean; | ||||
|     // offline, online, etc
 | ||||
|     presenceState?: string; | ||||
| } | ||||
| 
 | ||||
| @replaceableComponent("views.rooms.PresenceLabel") | ||||
| export default class PresenceLabel extends React.Component { | ||||
|     static propTypes = { | ||||
|         // number of milliseconds ago this user was last active.
 | ||||
|         // zero = unknown
 | ||||
|         activeAgo: PropTypes.number, | ||||
| 
 | ||||
|         // if true, activeAgo is an approximation and "Now" should
 | ||||
|         // be shown instead
 | ||||
|         currentlyActive: PropTypes.bool, | ||||
| 
 | ||||
|         // offline, online, etc
 | ||||
|         presenceState: PropTypes.string, | ||||
|     }; | ||||
| 
 | ||||
| export default class PresenceLabel extends React.Component<IProps> { | ||||
|     static defaultProps = { | ||||
|         activeAgo: -1, | ||||
|         presenceState: null, | ||||
|  | @ -42,29 +39,29 @@ export default class PresenceLabel extends React.Component { | |||
| 
 | ||||
|     // Return duration as a string using appropriate time units
 | ||||
|     // XXX: This would be better handled using a culture-aware library, but we don't use one yet.
 | ||||
|     getDuration(time) { | ||||
|     private getDuration(time: number): string { | ||||
|         if (!time) return; | ||||
|         const t = parseInt(time / 1000); | ||||
|         const t = Math.round(time / 1000); | ||||
|         const s = t % 60; | ||||
|         const m = parseInt(t / 60) % 60; | ||||
|         const h = parseInt(t / (60 * 60)) % 24; | ||||
|         const d = parseInt(t / (60 * 60 * 24)); | ||||
|         const m = Math.round(t / 60) % 60; | ||||
|         const h = Math.round(t / (60 * 60)) % 24; | ||||
|         const d = Math.round(t / (60 * 60 * 24)); | ||||
|         if (t < 60) { | ||||
|             if (t < 0) { | ||||
|                 return _t("%(duration)ss", {duration: 0}); | ||||
|                 return _t("%(duration)ss", { duration: 0 }); | ||||
|             } | ||||
|             return _t("%(duration)ss", {duration: s}); | ||||
|             return _t("%(duration)ss", { duration: s }); | ||||
|         } | ||||
|         if (t < 60 * 60) { | ||||
|             return _t("%(duration)sm", {duration: m}); | ||||
|             return _t("%(duration)sm", { duration: m }); | ||||
|         } | ||||
|         if (t < 24 * 60 * 60) { | ||||
|             return _t("%(duration)sh", {duration: h}); | ||||
|             return _t("%(duration)sh", { duration: h }); | ||||
|         } | ||||
|         return _t("%(duration)sd", {duration: d}); | ||||
|         return _t("%(duration)sd", { duration: d }); | ||||
|     } | ||||
| 
 | ||||
|     getPrettyPresence(presence, activeAgo, currentlyActive) { | ||||
|     private getPrettyPresence(presence: string, activeAgo: number, currentlyActive: boolean): string { | ||||
|         if (!currentlyActive && activeAgo !== undefined && activeAgo > 0) { | ||||
|             const duration = this.getDuration(activeAgo); | ||||
|             if (presence === "online") return _t("Online for %(duration)s", { duration: duration }); | ||||
|  | @ -45,7 +45,6 @@ import { objectShallowClone, objectWithOnly } from "../../../utils/objects"; | |||
| import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu"; | ||||
| import AccessibleButton from "../elements/AccessibleButton"; | ||||
| import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; | ||||
| import CallHandler from "../../../CallHandler"; | ||||
| import SpaceStore, {ISuggestedRoom, SUGGESTED_ROOMS} from "../../../stores/SpaceStore"; | ||||
| import {showAddExistingRooms, showCreateNewRoom, showSpaceInvite} from "../../../utils/space"; | ||||
| import {replaceableComponent} from "../../../utils/replaceableComponent"; | ||||
|  | @ -103,38 +102,6 @@ interface ITagAestheticsMap { | |||
|     [tagId: TagID]: ITagAesthetics; | ||||
| } | ||||
| 
 | ||||
| // If we have no dialer support, we just show the create chat dialog
 | ||||
| const dmOnAddRoom = (dispatcher?: Dispatcher<ActionPayload>) => { | ||||
|     (dispatcher || defaultDispatcher).dispatch({action: 'view_create_chat'}); | ||||
| }; | ||||
| 
 | ||||
| // If we have dialer support, show a context menu so the user can pick between
 | ||||
| // the dialer and the create chat dialog
 | ||||
| const dmAddRoomContextMenu = (onFinished: () => void) => { | ||||
|     return <IconizedContextMenuOptionList first> | ||||
|         <IconizedContextMenuOption | ||||
|             label={_t("Start a Conversation")} | ||||
|             iconClassName="mx_RoomList_iconPlus" | ||||
|             onClick={(e) => { | ||||
|                 e.preventDefault(); | ||||
|                 e.stopPropagation(); | ||||
|                 onFinished(); | ||||
|                 defaultDispatcher.dispatch({action: "view_create_chat"}); | ||||
|             }} | ||||
|         /> | ||||
|         <IconizedContextMenuOption | ||||
|             label={_t("Open dial pad")} | ||||
|             iconClassName="mx_RoomList_iconDialpad" | ||||
|             onClick={(e) => { | ||||
|                 e.preventDefault(); | ||||
|                 e.stopPropagation(); | ||||
|                 onFinished(); | ||||
|                 defaultDispatcher.fire(Action.OpenDialPad); | ||||
|             }} | ||||
|         /> | ||||
|     </IconizedContextMenuOptionList>; | ||||
| }; | ||||
| 
 | ||||
| const TAG_AESTHETICS: ITagAestheticsMap = { | ||||
|     [DefaultTagID.Invite]: { | ||||
|         sectionLabel: _td("Invites"), | ||||
|  | @ -151,8 +118,9 @@ const TAG_AESTHETICS: ITagAestheticsMap = { | |||
|         isInvite: false, | ||||
|         defaultHidden: false, | ||||
|         addRoomLabel: _td("Start chat"), | ||||
|         // Either onAddRoom or addRoomContextMenu are set depending on whether we
 | ||||
|         // have dialer support.
 | ||||
|         onAddRoom: (dispatcher?: Dispatcher<ActionPayload>) => { | ||||
|             (dispatcher || defaultDispatcher).dispatch({action: 'view_create_chat'}); | ||||
|         }, | ||||
|     }, | ||||
|     [DefaultTagID.Untagged]: { | ||||
|         sectionLabel: _td("Rooms"), | ||||
|  | @ -271,7 +239,6 @@ function customTagAesthetics(tagId: TagID): ITagAesthetics { | |||
| export default class RoomList extends React.PureComponent<IProps, IState> { | ||||
|     private dispatcherRef; | ||||
|     private customTagStoreRef; | ||||
|     private tagAesthetics: ITagAestheticsMap; | ||||
|     private roomStoreToken: fbEmitter.EventSubscription; | ||||
| 
 | ||||
|     constructor(props: IProps) { | ||||
|  | @ -282,10 +249,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> { | |||
|             isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(), | ||||
|             suggestedRooms: SpaceStore.instance.suggestedRooms, | ||||
|         }; | ||||
| 
 | ||||
|         // shallow-copy from the template as we need to make modifications to it
 | ||||
|         this.tagAesthetics = objectShallowClone(TAG_AESTHETICS); | ||||
|         this.updateDmAddRoomAction(); | ||||
|     } | ||||
| 
 | ||||
|     public componentDidMount(): void { | ||||
|  | @ -311,17 +274,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> { | |||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     private updateDmAddRoomAction() { | ||||
|         const dmTagAesthetics = objectShallowClone(TAG_AESTHETICS[DefaultTagID.DM]); | ||||
|         if (CallHandler.sharedInstance().getSupportsPstnProtocol()) { | ||||
|             dmTagAesthetics.addRoomContextMenu = dmAddRoomContextMenu; | ||||
|         } else { | ||||
|             dmTagAesthetics.onAddRoom = dmOnAddRoom; | ||||
|         } | ||||
| 
 | ||||
|         this.tagAesthetics[DefaultTagID.DM] = dmTagAesthetics; | ||||
|     } | ||||
| 
 | ||||
|     private onAction = (payload: ActionPayload) => { | ||||
|         if (payload.action === Action.ViewRoomDelta) { | ||||
|             const viewRoomDeltaPayload = payload as ViewRoomDeltaPayload; | ||||
|  | @ -335,7 +287,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> { | |||
|                 }); | ||||
|             } | ||||
|         } else if (payload.action === Action.PstnSupportUpdated) { | ||||
|             this.updateDmAddRoomAction(); | ||||
|             this.updateLists(); | ||||
|         } | ||||
|     }; | ||||
|  | @ -524,7 +475,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> { | |||
| 
 | ||||
|                 const aesthetics: ITagAesthetics = isCustomTag(orderedTagId) | ||||
|                     ? customTagAesthetics(orderedTagId) | ||||
|                     : this.tagAesthetics[orderedTagId]; | ||||
|                     : TAG_AESTHETICS[orderedTagId]; | ||||
|                 if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`); | ||||
| 
 | ||||
|                 // The cost of mounting/unmounting this component offsets the cost
 | ||||
|  |  | |||
|  | @ -114,12 +114,13 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps, | |||
|         } | ||||
| 
 | ||||
|         await this.state.recorder.stop(); | ||||
|         const mxc = await this.state.recorder.upload(); | ||||
|         const upload = await this.state.recorder.upload(this.props.room.roomId); | ||||
|         MatrixClientPeg.get().sendMessage(this.props.room.roomId, { | ||||
|             "body": "Voice message", | ||||
|             //"msgtype": "org.matrix.msc2516.voice",
 | ||||
|             "msgtype": MsgType.Audio, | ||||
|             "url": mxc, | ||||
|             "url": upload.mxc, | ||||
|             "file": upload.encrypted, | ||||
|             "info": { | ||||
|                 duration: Math.round(this.state.recorder.durationSeconds * 1000), | ||||
|                 mimetype: this.state.recorder.contentType, | ||||
|  | @ -130,7 +131,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps, | |||
|             // https://github.com/matrix-org/matrix-doc/pull/3245
 | ||||
|             "org.matrix.msc1767.text": "Voice message", | ||||
|             "org.matrix.msc1767.file": { | ||||
|                 url: mxc, | ||||
|                 url: upload.mxc, | ||||
|                 file: upload.encrypted, | ||||
|                 name: "Voice message.ogg", | ||||
|                 mimetype: this.state.recorder.contentType, | ||||
|                 size: this.state.recorder.contentLength, | ||||
|  |  | |||
|  | @ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent { | |||
|     async _getUpdatedStatus() { | ||||
|         const cli = MatrixClientPeg.get(); | ||||
|         const pkCache = cli.getCrossSigningCacheCallbacks(); | ||||
|         const crossSigning = cli.crypto._crossSigningInfo; | ||||
|         const secretStorage = cli.crypto._secretStorage; | ||||
|         const crossSigning = cli.crypto.crossSigningInfo; | ||||
|         const secretStorage = cli.crypto.secretStorage; | ||||
|         const crossSigningPublicKeysOnDevice = crossSigning.getId(); | ||||
|         const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage); | ||||
|         const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master")); | ||||
|  |  | |||
|  | @ -131,7 +131,7 @@ export default class SecureBackupPanel extends React.PureComponent { | |||
| 
 | ||||
|     async _getUpdatedDiagnostics() { | ||||
|         const cli = MatrixClientPeg.get(); | ||||
|         const secretStorage = cli.crypto._secretStorage; | ||||
|         const secretStorage = cli.crypto.secretStorage; | ||||
| 
 | ||||
|         const backupKeyStored = !!(await cli.isKeyBackupKeyStored()); | ||||
|         const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey(); | ||||
|  |  | |||
|  | @ -33,6 +33,9 @@ export enum JoinRule { | |||
|     Public = "public", | ||||
|     Knock = "knock", | ||||
|     Invite = "invite", | ||||
|     /** | ||||
|      * @deprecated Reserved. Should not be used. | ||||
|      */ | ||||
|     Private = "private", | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -129,7 +129,9 @@ const SpaceCreateMenu = ({ onFinished }) => { | |||
|                         events_default: 100, | ||||
|                         ...Visibility.Public ? { invite: 0 } : {}, | ||||
|                     }, | ||||
|                     room_alias_name: alias ? alias.substr(1, alias.indexOf(":") - 1) : undefined, | ||||
|                     room_alias_name: visibility === Visibility.Public && alias | ||||
|                         ? alias.substr(1, alias.indexOf(":") - 1) | ||||
|                         : undefined, | ||||
|                     topic, | ||||
|                 }, | ||||
|                 spinner: false, | ||||
|  |  | |||
|  | @ -62,9 +62,9 @@ const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => { | |||
|     const userId = cli.getUserId(); | ||||
| 
 | ||||
|     const [visibility, setVisibility] = useLocalEcho<SpaceVisibility>( | ||||
|         () => space.getJoinRule() === JoinRule.Private ? SpaceVisibility.Private : SpaceVisibility.Unlisted, | ||||
|         () => space.getJoinRule() === JoinRule.Invite ? SpaceVisibility.Private : SpaceVisibility.Unlisted, | ||||
|         visibility => cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { | ||||
|             join_rule: visibility === SpaceVisibility.Unlisted ? JoinRule.Public : JoinRule.Private, | ||||
|             join_rule: visibility === SpaceVisibility.Unlisted ? JoinRule.Public : JoinRule.Invite, | ||||
|         }, ""), | ||||
|         () => setError(_t("Failed to update the visibility of this space")), | ||||
|     ); | ||||
|  |  | |||
|  | @ -50,7 +50,7 @@ const GenericToast: React.FC<XOR<IPropsExtended, IProps>> = ({ | |||
|             {detailContent} | ||||
|         </div> | ||||
|         <div className="mx_Toast_buttons" aria-live="off"> | ||||
|             {onReject && rejectLabel && <AccessibleButton kind="danger" onClick={onReject}> | ||||
|             {onReject && rejectLabel && <AccessibleButton kind="danger_outline" onClick={onReject}> | ||||
|                 { rejectLabel } | ||||
|             </AccessibleButton> } | ||||
|             <AccessibleButton onClick={onAccept} kind="primary"> | ||||
|  |  | |||
|  | @ -27,6 +27,11 @@ export interface SetRightPanelPhasePayload extends ActionPayload { | |||
| 
 | ||||
|     phase: RightPanelPhases; | ||||
|     refireParams?: SetRightPanelPhaseRefireParams; | ||||
| 
 | ||||
|     /** | ||||
|      * By default SetRightPanelPhase can close the panel, this allows overriding that behaviour | ||||
|      */ | ||||
|     allowClose?: boolean; | ||||
| } | ||||
| 
 | ||||
| export interface SetRightPanelPhaseRefireParams { | ||||
|  |  | |||
|  | @ -396,7 +396,8 @@ | |||
|     "Failed to invite": "Failed to invite", | ||||
|     "Operation failed": "Operation failed", | ||||
|     "Failed to invite users to the room:": "Failed to invite users to the room:", | ||||
|     "Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:", | ||||
|     "We sent the others, but the below people couldn't be invited to <RoomName/>": "We sent the others, but the below people couldn't be invited to <RoomName/>", | ||||
|     "Some invites couldn't be sent": "Some invites couldn't be sent", | ||||
|     "You need to be logged in.": "You need to be logged in.", | ||||
|     "You need to be able to invite users to do that.": "You need to be able to invite users to do that.", | ||||
|     "Unable to create widget.": "Unable to create widget.", | ||||
|  | @ -489,24 +490,27 @@ | |||
|     "Converts the room to a DM": "Converts the room to a DM", | ||||
|     "Converts the DM to a room": "Converts the DM to a room", | ||||
|     "Displays action": "Displays action", | ||||
|     "Reason": "Reason", | ||||
|     "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.", | ||||
|     "%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.", | ||||
|     "%(senderName)s invited %(targetName)s.": "%(senderName)s invited %(targetName)s.", | ||||
|     "%(senderName)s banned %(targetName)s.": "%(senderName)s banned %(targetName)s.", | ||||
|     "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s changed their display name to %(displayName)s.", | ||||
|     "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.", | ||||
|     "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s removed their display name (%(oldDisplayName)s).", | ||||
|     "%(senderName)s removed their profile picture.": "%(senderName)s removed their profile picture.", | ||||
|     "%(senderName)s changed their profile picture.": "%(senderName)s changed their profile picture.", | ||||
|     "%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.", | ||||
|     "%(senderName)s made no change.": "%(senderName)s made no change.", | ||||
|     "%(targetName)s joined the room.": "%(targetName)s joined the room.", | ||||
|     "%(targetName)s rejected the invitation.": "%(targetName)s rejected the invitation.", | ||||
|     "%(targetName)s left the room.": "%(targetName)s left the room.", | ||||
|     "%(senderName)s unbanned %(targetName)s.": "%(senderName)s unbanned %(targetName)s.", | ||||
|     "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s withdrew %(targetName)s's invitation.", | ||||
|     "%(senderName)s kicked %(targetName)s.": "%(senderName)s kicked %(targetName)s.", | ||||
|     "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s accepted the invitation for %(displayName)s", | ||||
|     "%(targetName)s accepted an invitation": "%(targetName)s accepted an invitation", | ||||
|     "%(senderName)s invited %(targetName)s": "%(senderName)s invited %(targetName)s", | ||||
|     "%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s banned %(targetName)s: %(reason)s", | ||||
|     "%(senderName)s banned %(targetName)s": "%(senderName)s banned %(targetName)s", | ||||
|     "%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s changed their display name to %(displayName)s", | ||||
|     "%(senderName)s set their display name to %(displayName)s": "%(senderName)s set their display name to %(displayName)s", | ||||
|     "%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s removed their display name (%(oldDisplayName)s)", | ||||
|     "%(senderName)s removed their profile picture": "%(senderName)s removed their profile picture", | ||||
|     "%(senderName)s changed their profile picture": "%(senderName)s changed their profile picture", | ||||
|     "%(senderName)s set a profile picture": "%(senderName)s set a profile picture", | ||||
|     "%(senderName)s made no change": "%(senderName)s made no change", | ||||
|     "%(targetName)s joined the room": "%(targetName)s joined the room", | ||||
|     "%(targetName)s rejected the invitation": "%(targetName)s rejected the invitation", | ||||
|     "%(targetName)s left the room: %(reason)s": "%(targetName)s left the room: %(reason)s", | ||||
|     "%(targetName)s left the room": "%(targetName)s left the room", | ||||
|     "%(senderName)s unbanned %(targetName)s": "%(senderName)s unbanned %(targetName)s", | ||||
|     "%(senderName)s withdrew %(targetName)s's invitation: %(reason)s": "%(senderName)s withdrew %(targetName)s's invitation: %(reason)s", | ||||
|     "%(senderName)s withdrew %(targetName)s's invitation": "%(senderName)s withdrew %(targetName)s's invitation", | ||||
|     "%(senderName)s kicked %(targetName)s: %(reason)s": "%(senderName)s kicked %(targetName)s: %(reason)s", | ||||
|     "%(senderName)s kicked %(targetName)s": "%(senderName)s kicked %(targetName)s", | ||||
|     "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".", | ||||
|     "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.", | ||||
|     "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.", | ||||
|  | @ -558,6 +562,7 @@ | |||
|     "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s made future room history visible to unknown (%(visibility)s).", | ||||
|     "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.", | ||||
|     "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s", | ||||
|     "%(senderName)s changed the <a>pinned messages</a> for the room.": "%(senderName)s changed the <a>pinned messages</a> for the room.", | ||||
|     "%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.", | ||||
|     "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s", | ||||
|     "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s", | ||||
|  | @ -1410,6 +1415,7 @@ | |||
|     "Failed to unban": "Failed to unban", | ||||
|     "Unban": "Unban", | ||||
|     "Banned by %(displayName)s": "Banned by %(displayName)s", | ||||
|     "Reason": "Reason", | ||||
|     "Error changing power level requirement": "Error changing power level requirement", | ||||
|     "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.", | ||||
|     "Error changing power level": "Error changing power level", | ||||
|  | @ -1576,8 +1582,6 @@ | |||
|     "Search": "Search", | ||||
|     "Voice call": "Voice call", | ||||
|     "Video call": "Video call", | ||||
|     "Start a Conversation": "Start a Conversation", | ||||
|     "Open dial pad": "Open dial pad", | ||||
|     "Invites": "Invites", | ||||
|     "Favourites": "Favourites", | ||||
|     "People": "People", | ||||
|  | @ -2277,7 +2281,6 @@ | |||
|     "Confirm to continue": "Confirm to continue", | ||||
|     "Click the button below to confirm your identity.": "Click the button below to confirm your identity.", | ||||
|     "Invite by email": "Invite by email", | ||||
|     "Failed to invite the following users to chat: %(csvUsers)s": "Failed to invite the following users to chat: %(csvUsers)s", | ||||
|     "We couldn't create your DM.": "We couldn't create your DM.", | ||||
|     "Something went wrong trying to invite the users.": "Something went wrong trying to invite the users.", | ||||
|     "We couldn't invite those users. Please check the users you want to invite and try again.": "We couldn't invite those users. Please check the users you want to invite and try again.", | ||||
|  | @ -2657,6 +2660,7 @@ | |||
|     "Explore Public Rooms": "Explore Public Rooms", | ||||
|     "Create a Group Chat": "Create a Group Chat", | ||||
|     "Upgrade to %(hostSignupBrand)s": "Upgrade to %(hostSignupBrand)s", | ||||
|     "Open dial pad": "Open dial pad", | ||||
|     "Failed to reject invitation": "Failed to reject invitation", | ||||
|     "Cannot create rooms in this community": "Cannot create rooms in this community", | ||||
|     "You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.", | ||||
|  | @ -2763,6 +2767,7 @@ | |||
|     "<inviter/> invites you": "<inviter/> invites you", | ||||
|     "To view %(spaceName)s, turn on the <a>Spaces beta</a>": "To view %(spaceName)s, turn on the <a>Spaces beta</a>", | ||||
|     "To join %(spaceName)s, turn on the <a>Spaces beta</a>": "To join %(spaceName)s, turn on the <a>Spaces beta</a>", | ||||
|     "To view %(spaceName)s, you need an invite": "To view %(spaceName)s, you need an invite", | ||||
|     "Welcome to <name/>": "Welcome to <name/>", | ||||
|     "Random": "Random", | ||||
|     "Support": "Support", | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ limitations under the License. | |||
| // The following interfaces take their names and member names from seshat and the spec
 | ||||
| /* eslint-disable camelcase */ | ||||
| 
 | ||||
| export interface MatrixEvent { | ||||
| export interface IMatrixEvent { | ||||
|     type: string; | ||||
|     sender: string; | ||||
|     content: {}; | ||||
|  | @ -27,37 +27,37 @@ export interface MatrixEvent { | |||
|     roomId: string; | ||||
| } | ||||
| 
 | ||||
| export interface MatrixProfile { | ||||
| export interface IMatrixProfile { | ||||
|     avatar_url: string; | ||||
|     displayname: string; | ||||
| } | ||||
| 
 | ||||
| export interface CrawlerCheckpoint { | ||||
| export interface ICrawlerCheckpoint { | ||||
|     roomId: string; | ||||
|     token: string; | ||||
|     fullCrawl?: boolean; | ||||
|     direction: string; | ||||
| } | ||||
| 
 | ||||
| export interface ResultContext { | ||||
|     events_before: [MatrixEvent]; | ||||
|     events_after: [MatrixEvent]; | ||||
|     profile_info: Map<string, MatrixProfile>; | ||||
| export interface IResultContext { | ||||
|     events_before: [IMatrixEvent]; | ||||
|     events_after: [IMatrixEvent]; | ||||
|     profile_info: Map<string, IMatrixProfile>; | ||||
| } | ||||
| 
 | ||||
| export interface ResultsElement { | ||||
| export interface IResultsElement { | ||||
|     rank: number; | ||||
|     result: MatrixEvent; | ||||
|     context: ResultContext; | ||||
|     result: IMatrixEvent; | ||||
|     context: IResultContext; | ||||
| } | ||||
| 
 | ||||
| export interface SearchResult { | ||||
| export interface ISearchResult { | ||||
|     count: number; | ||||
|     results: [ResultsElement]; | ||||
|     results: [IResultsElement]; | ||||
|     highlights: [string]; | ||||
| } | ||||
| 
 | ||||
| export interface SearchArgs { | ||||
| export interface ISearchArgs { | ||||
|     search_term: string; | ||||
|     before_limit: number; | ||||
|     after_limit: number; | ||||
|  | @ -65,19 +65,19 @@ export interface SearchArgs { | |||
|     room_id?: string; | ||||
| } | ||||
| 
 | ||||
| export interface EventAndProfile { | ||||
|     event: MatrixEvent; | ||||
|     profile: MatrixProfile; | ||||
| export interface IEventAndProfile { | ||||
|     event: IMatrixEvent; | ||||
|     profile: IMatrixProfile; | ||||
| } | ||||
| 
 | ||||
| export interface LoadArgs { | ||||
| export interface ILoadArgs { | ||||
|     roomId: string; | ||||
|     limit: number; | ||||
|     fromEvent?: string; | ||||
|     direction?: string; | ||||
| } | ||||
| 
 | ||||
| export interface IndexStats { | ||||
| export interface IIndexStats { | ||||
|     size: number; | ||||
|     eventCount: number; | ||||
|     roomCount: number; | ||||
|  | @ -119,13 +119,13 @@ export default abstract class BaseEventIndexManager { | |||
|      * Queue up an event to be added to the index. | ||||
|      * | ||||
|      * @param {MatrixEvent} ev The event that should be added to the index. | ||||
|      * @param {MatrixProfile} profile The profile of the event sender at the | ||||
|      * @param {IMatrixProfile} profile The profile of the event sender at the | ||||
|      * time of the event receival. | ||||
|      * | ||||
|      * @return {Promise} A promise that will resolve when the was queued up for | ||||
|      * addition. | ||||
|      */ | ||||
|     async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise<void> { | ||||
|     async addEventToIndex(ev: IMatrixEvent, profile: IMatrixProfile): Promise<void> { | ||||
|         throw new Error("Unimplemented"); | ||||
|     } | ||||
| 
 | ||||
|  | @ -160,10 +160,10 @@ export default abstract class BaseEventIndexManager { | |||
|     /** | ||||
|      * Get statistical information of the index. | ||||
|      * | ||||
|      * @return {Promise<IndexStats>} A promise that will resolve to the index | ||||
|      * @return {Promise<IIndexStats>} A promise that will resolve to the index | ||||
|      * statistics. | ||||
|      */ | ||||
|     async getStats(): Promise<IndexStats> { | ||||
|     async getStats(): Promise<IIndexStats> { | ||||
|         throw new Error("Unimplemented"); | ||||
|     } | ||||
| 
 | ||||
|  | @ -203,13 +203,13 @@ export default abstract class BaseEventIndexManager { | |||
|     /** | ||||
|      * Search the event index using the given term for matching events. | ||||
|      * | ||||
|      * @param {SearchArgs} searchArgs The search configuration for the search, | ||||
|      * @param {ISearchArgs} searchArgs The search configuration for the search, | ||||
|      * sets the search term and determines the search result contents. | ||||
|      * | ||||
|      * @return {Promise<[SearchResult]>} A promise that will resolve to an array | ||||
|      * @return {Promise<[ISearchResult]>} A promise that will resolve to an array | ||||
|      * of search results once the search is done. | ||||
|      */ | ||||
|     async searchEventIndex(searchArgs: SearchArgs): Promise<SearchResult> { | ||||
|     async searchEventIndex(searchArgs: ISearchArgs): Promise<ISearchResult> { | ||||
|         throw new Error("Unimplemented"); | ||||
|     } | ||||
| 
 | ||||
|  | @ -218,12 +218,12 @@ export default abstract class BaseEventIndexManager { | |||
|      * | ||||
|      * This is used to add a batch of events to the index. | ||||
|      * | ||||
|      * @param {[EventAndProfile]} events The list of events and profiles that | ||||
|      * @param {[IEventAndProfile]} events The list of events and profiles that | ||||
|      * should be added to the event index. | ||||
|      * @param {[CrawlerCheckpoint]} checkpoint A new crawler checkpoint that | ||||
|      * @param {[ICrawlerCheckpoint]} checkpoint A new crawler checkpoint that | ||||
|      * should be stored in the index which should be used to continue crawling | ||||
|      * the room. | ||||
|      * @param {[CrawlerCheckpoint]} oldCheckpoint The checkpoint that was used | ||||
|      * @param {[ICrawlerCheckpoint]} oldCheckpoint The checkpoint that was used | ||||
|      * to fetch the current batch of events. This checkpoint will be removed | ||||
|      * from the index. | ||||
|      * | ||||
|  | @ -231,9 +231,9 @@ export default abstract class BaseEventIndexManager { | |||
|      * were already added to the index, false otherwise. | ||||
|      */ | ||||
|     async addHistoricEvents( | ||||
|         events: [EventAndProfile], | ||||
|         checkpoint: CrawlerCheckpoint | null, | ||||
|         oldCheckpoint: CrawlerCheckpoint | null, | ||||
|         events: IEventAndProfile[], | ||||
|         checkpoint: ICrawlerCheckpoint | null, | ||||
|         oldCheckpoint: ICrawlerCheckpoint | null, | ||||
|     ): Promise<boolean> { | ||||
|         throw new Error("Unimplemented"); | ||||
|     } | ||||
|  | @ -241,36 +241,36 @@ export default abstract class BaseEventIndexManager { | |||
|     /** | ||||
|      * Add a new crawler checkpoint to the index. | ||||
|      * | ||||
|      * @param {CrawlerCheckpoint} checkpoint The checkpoint that should be added | ||||
|      * @param {ICrawlerCheckpoint} checkpoint The checkpoint that should be added | ||||
|      * to the index. | ||||
|      * | ||||
|      * @return {Promise} A promise that will resolve once the checkpoint has | ||||
|      * been stored. | ||||
|      */ | ||||
|     async addCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<void> { | ||||
|     async addCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> { | ||||
|         throw new Error("Unimplemented"); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Add a new crawler checkpoint to the index. | ||||
|      * | ||||
|      * @param {CrawlerCheckpoint} checkpoint The checkpoint that should be | ||||
|      * @param {ICrawlerCheckpoint} checkpoint The checkpoint that should be | ||||
|      * removed from the index. | ||||
|      * | ||||
|      * @return {Promise} A promise that will resolve once the checkpoint has | ||||
|      * been removed. | ||||
|      */ | ||||
|     async removeCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<void> { | ||||
|     async removeCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> { | ||||
|         throw new Error("Unimplemented"); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load the stored checkpoints from the index. | ||||
|      * | ||||
|      * @return {Promise<[CrawlerCheckpoint]>} A promise that will resolve to an | ||||
|      * @return {Promise<[ICrawlerCheckpoint]>} A promise that will resolve to an | ||||
|      * array of crawler checkpoints once they have been loaded from the index. | ||||
|      */ | ||||
|     async loadCheckpoints(): Promise<[CrawlerCheckpoint]> { | ||||
|     async loadCheckpoints(): Promise<ICrawlerCheckpoint[]> { | ||||
|         throw new Error("Unimplemented"); | ||||
|     } | ||||
| 
 | ||||
|  | @ -286,11 +286,11 @@ export default abstract class BaseEventIndexManager { | |||
|      * @param  {string} args.direction The direction to which we should continue | ||||
|      * loading events from. This is used only if fromEvent is used as well. | ||||
|      * | ||||
|      * @return {Promise<[EventAndProfile]>} A promise that will resolve to an | ||||
|      * @return {Promise<[IEventAndProfile]>} A promise that will resolve to an | ||||
|      * array of Matrix events that contain mxc URLs accompanied with the | ||||
|      * historic profile of the sender. | ||||
|      */ | ||||
|     async loadFileEvents(args: LoadArgs): Promise<[EventAndProfile]> { | ||||
|     async loadFileEvents(args: ILoadArgs): Promise<IEventAndProfile[]> { | ||||
|         throw new Error("Unimplemented"); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ import { MatrixClientPeg } from "../MatrixClientPeg"; | |||
| import { sleep } from "../utils/promise"; | ||||
| import SettingsStore from "../settings/SettingsStore"; | ||||
| import { SettingLevel } from "../settings/SettingLevel"; | ||||
| import {CrawlerCheckpoint, LoadArgs, SearchArgs} from "./BaseEventIndexManager"; | ||||
| import { ICrawlerCheckpoint, ILoadArgs, ISearchArgs } from "./BaseEventIndexManager"; | ||||
| 
 | ||||
| // The time in ms that the crawler will wait loop iterations if there
 | ||||
| // have not been any checkpoints to consume in the last iteration.
 | ||||
|  | @ -45,9 +45,9 @@ interface ICrawler { | |||
|  * Event indexing class that wraps the platform specific event indexing. | ||||
|  */ | ||||
| export default class EventIndex extends EventEmitter { | ||||
|     private crawlerCheckpoints: CrawlerCheckpoint[] = []; | ||||
|     private crawlerCheckpoints: ICrawlerCheckpoint[] = []; | ||||
|     private crawler: ICrawler = null; | ||||
|     private currentCheckpoint: CrawlerCheckpoint = null; | ||||
|     private currentCheckpoint: ICrawlerCheckpoint = null; | ||||
| 
 | ||||
|     public async init() { | ||||
|         const indexManager = PlatformPeg.get().getEventIndexingManager(); | ||||
|  | @ -111,14 +111,14 @@ export default class EventIndex extends EventEmitter { | |||
|             const timeline = room.getLiveTimeline(); | ||||
|             const token = timeline.getPaginationToken("b"); | ||||
| 
 | ||||
|             const backCheckpoint: CrawlerCheckpoint = { | ||||
|             const backCheckpoint: ICrawlerCheckpoint = { | ||||
|                 roomId: room.roomId, | ||||
|                 token: token, | ||||
|                 direction: "b", | ||||
|                 fullCrawl: true, | ||||
|             }; | ||||
| 
 | ||||
|             const forwardCheckpoint: CrawlerCheckpoint = { | ||||
|             const forwardCheckpoint: ICrawlerCheckpoint = { | ||||
|                 roomId: room.roomId, | ||||
|                 token: token, | ||||
|                 direction: "f", | ||||
|  | @ -668,13 +668,13 @@ export default class EventIndex extends EventEmitter { | |||
|     /** | ||||
|      * Search the event index using the given term for matching events. | ||||
|      * | ||||
|      * @param {SearchArgs} searchArgs The search configuration for the search, | ||||
|      * @param {ISearchArgs} searchArgs The search configuration for the search, | ||||
|      * sets the search term and determines the search result contents. | ||||
|      * | ||||
|      * @return {Promise<[SearchResult]>} A promise that will resolve to an array | ||||
|      * of search results once the search is done. | ||||
|      */ | ||||
|     public async search(searchArgs: SearchArgs) { | ||||
|     public async search(searchArgs: ISearchArgs) { | ||||
|         const indexManager = PlatformPeg.get().getEventIndexingManager(); | ||||
|         return indexManager.searchEventIndex(searchArgs); | ||||
|     } | ||||
|  | @ -709,7 +709,7 @@ export default class EventIndex extends EventEmitter { | |||
|         const client = MatrixClientPeg.get(); | ||||
|         const indexManager = PlatformPeg.get().getEventIndexingManager(); | ||||
| 
 | ||||
|         const loadArgs: LoadArgs = { | ||||
|         const loadArgs: ILoadArgs = { | ||||
|             roomId: room.roomId, | ||||
|             limit: limit, | ||||
|         }; | ||||
|  |  | |||
|  | @ -86,8 +86,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) { | |||
|             body.append('cross_signing_key', client.getCrossSigningId()); | ||||
| 
 | ||||
|             // add cross-signing status information
 | ||||
|             const crossSigning = client.crypto._crossSigningInfo; | ||||
|             const secretStorage = client.crypto._secretStorage; | ||||
|             const crossSigning = client.crypto.crossSigningInfo; | ||||
|             const secretStorage = client.crypto.secretStorage; | ||||
| 
 | ||||
|             body.append("cross_signing_ready", String(await client.isCrossSigningReady())); | ||||
|             body.append("cross_signing_supported_by_hs", | ||||
|  |  | |||
|  | @ -161,6 +161,7 @@ export default class RightPanelStore extends Store<ActionPayload> { | |||
|             case Action.SetRightPanelPhase: { | ||||
|                 let targetPhase = payload.phase; | ||||
|                 let refireParams = payload.refireParams; | ||||
|                 const allowClose = payload.allowClose ?? true; | ||||
|                 // redirect to EncryptionPanel if there is an ongoing verification request
 | ||||
|                 if (targetPhase === RightPanelPhases.RoomMemberInfo && payload.refireParams) { | ||||
|                     const {member} = payload.refireParams; | ||||
|  | @ -192,7 +193,7 @@ export default class RightPanelStore extends Store<ActionPayload> { | |||
|                         }); | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (targetPhase === this.state.lastRoomPhase && !refireParams) { | ||||
|                     if (targetPhase === this.state.lastRoomPhase && !refireParams && allowClose) { | ||||
|                         this.setState({ | ||||
|                             showRoomPanel: !this.state.showRoomPanel, | ||||
|                             previousPhase: null, | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ limitations under the License. | |||
| import React, { ReactNode } from 'react'; | ||||
| import classNames from 'classnames'; | ||||
| import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch'; | ||||
| import { Action, DiffDOM, IDiff } from "diff-dom"; | ||||
| import { DiffDOM, IDiff } from "diff-dom"; | ||||
| import { IContent } from "matrix-js-sdk/src/models/event"; | ||||
| 
 | ||||
| import { bodyToHtml, checkBlockNode, IOptsReturnString } from "../HtmlUtils"; | ||||
|  | @ -149,7 +149,7 @@ function stringAsTextNode(string: string): Text { | |||
| function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatch: DiffMatchPatch): void { | ||||
|     const {refNode, refParentNode} = findRefNodes(originalRootNode, diff.route); | ||||
|     switch (diff.action) { | ||||
|         case Action.ReplaceElement: { | ||||
|         case "replaceElement": { | ||||
|             const container = document.createElement("span"); | ||||
|             const delNode = wrapDeletion(diffTreeToDOM(diff.oldValue)); | ||||
|             const insNode = wrapInsertion(diffTreeToDOM(diff.newValue)); | ||||
|  | @ -158,17 +158,17 @@ function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatc | |||
|             refNode.parentNode.replaceChild(container, refNode); | ||||
|             break; | ||||
|         } | ||||
|         case Action.RemoveTextElement: { | ||||
|         case "removeTextElement": { | ||||
|             const delNode = wrapDeletion(stringAsTextNode(diff.value)); | ||||
|             refNode.parentNode.replaceChild(delNode, refNode); | ||||
|             break; | ||||
|         } | ||||
|         case Action.RemoveElement: { | ||||
|         case "removeElement": { | ||||
|             const delNode = wrapDeletion(diffTreeToDOM(diff.element)); | ||||
|             refNode.parentNode.replaceChild(delNode, refNode); | ||||
|             break; | ||||
|         } | ||||
|         case Action.ModifyTextElement: { | ||||
|         case "modifyTextElement": { | ||||
|             const textDiffs = diffMathPatch.diff_main(diff.oldValue, diff.newValue); | ||||
|             diffMathPatch.diff_cleanupSemantic(textDiffs); | ||||
|             const container = document.createElement("span"); | ||||
|  | @ -184,12 +184,12 @@ function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatc | |||
|             refNode.parentNode.replaceChild(container, refNode); | ||||
|             break; | ||||
|         } | ||||
|         case Action.AddElement: { | ||||
|         case "addElement": { | ||||
|             const insNode = wrapInsertion(diffTreeToDOM(diff.element)); | ||||
|             insertBefore(refParentNode, refNode, insNode); | ||||
|             break; | ||||
|         } | ||||
|         case Action.AddTextElement: { | ||||
|         case "addTextElement": { | ||||
|             // XXX: sometimes diffDOM says insert a newline when there shouldn't be one
 | ||||
|             // but we must insert the node anyway so that we don't break the route child IDs.
 | ||||
|             // See https://github.com/fiduswriter/diffDOM/issues/100
 | ||||
|  | @ -199,9 +199,9 @@ function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatc | |||
|         } | ||||
|         // e.g. when changing a the href of a link,
 | ||||
|         // show the link with old href as removed and with the new href as added
 | ||||
|         case Action.RemoveAttribute: | ||||
|         case Action.AddAttribute: | ||||
|         case Action.ModifyAttribute: { | ||||
|         case "removeAttribute": | ||||
|         case "addAttribute": | ||||
|         case "modifyAttribute": { | ||||
|             const delNode = wrapDeletion(refNode.cloneNode(true)); | ||||
|             const updatedNode = refNode.cloneNode(true) as HTMLElement; | ||||
|             if (diff.action === "addAttribute" || diff.action === "modifyAttribute") { | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| /* | ||||
| Copyright 2016 OpenMarket Ltd | ||||
| Copyright 2017, 2018 New Vector Ltd | ||||
| Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
|  | @ -15,23 +14,51 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import {MatrixClientPeg} from '../MatrixClientPeg'; | ||||
| import {getAddressType} from '../UserAddress'; | ||||
| import { MatrixError } from "matrix-js-sdk/src/http-api"; | ||||
| 
 | ||||
| import { MatrixClientPeg } from '../MatrixClientPeg'; | ||||
| import { AddressType, getAddressType } from '../UserAddress'; | ||||
| import GroupStore from '../stores/GroupStore'; | ||||
| import {_t} from "../languageHandler"; | ||||
| import * as sdk from "../index"; | ||||
| import { _t } from "../languageHandler"; | ||||
| import Modal from "../Modal"; | ||||
| import SettingsStore from "../settings/SettingsStore"; | ||||
| import {defer} from "./promise"; | ||||
| import { defer, IDeferred } from "./promise"; | ||||
| import AskInviteAnywayDialog from "../components/views/dialogs/AskInviteAnywayDialog"; | ||||
| 
 | ||||
| export enum InviteState { | ||||
|     Invited = "invited", | ||||
|     Error = "error", | ||||
| } | ||||
| 
 | ||||
| interface IError { | ||||
|     errorText: string; | ||||
|     errcode: string; | ||||
| } | ||||
| 
 | ||||
| const UNKNOWN_PROFILE_ERRORS = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND']; | ||||
| 
 | ||||
| export type CompletionStates = Record<string, InviteState>; | ||||
| 
 | ||||
| /** | ||||
|  * Invites multiple addresses to a room or group, handling rate limiting from the server | ||||
|  */ | ||||
| export default class MultiInviter { | ||||
|     private readonly roomId?: string; | ||||
|     private readonly groupId?: string; | ||||
| 
 | ||||
|     private canceled = false; | ||||
|     private addresses: string[] = []; | ||||
|     private busy = false; | ||||
|     private _fatal = false; | ||||
|     private completionStates: CompletionStates = {}; // State of each address (invited or error)
 | ||||
|     private errors: Record<string, IError> = {}; // { address: {errorText, errcode} }
 | ||||
|     private deferred: IDeferred<CompletionStates> = null; | ||||
|     private reason: string = null; | ||||
| 
 | ||||
|     /** | ||||
|      * @param {string} targetId The ID of the room or group to invite to | ||||
|      */ | ||||
|     constructor(targetId) { | ||||
|     constructor(targetId: string) { | ||||
|         if (targetId[0] === '+') { | ||||
|             this.roomId = null; | ||||
|             this.groupId = targetId; | ||||
|  | @ -39,41 +66,38 @@ export default class MultiInviter { | |||
|             this.roomId = targetId; | ||||
|             this.groupId = null; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|         this.canceled = false; | ||||
|         this.addrs = []; | ||||
|         this.busy = false; | ||||
|         this.completionStates = {}; // State of each address (invited or error)
 | ||||
|         this.errors = {}; // { address: {errorText, errcode} }
 | ||||
|         this.deferred = null; | ||||
|     public get fatal() { | ||||
|         return this._fatal; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invite users to this room. This may only be called once per | ||||
|      * instance of the class. | ||||
|      * | ||||
|      * @param {array} addrs Array of addresses to invite | ||||
|      * @param {array} addresses Array of addresses to invite | ||||
|      * @param {string} reason Reason for inviting (optional) | ||||
|      * @returns {Promise} Resolved when all invitations in the queue are complete | ||||
|      */ | ||||
|     invite(addrs, reason) { | ||||
|         if (this.addrs.length > 0) { | ||||
|     public invite(addresses, reason?: string): Promise<CompletionStates> { | ||||
|         if (this.addresses.length > 0) { | ||||
|             throw new Error("Already inviting/invited"); | ||||
|         } | ||||
|         this.addrs.push(...addrs); | ||||
|         this.addresses.push(...addresses); | ||||
|         this.reason = reason; | ||||
| 
 | ||||
|         for (const addr of this.addrs) { | ||||
|         for (const addr of this.addresses) { | ||||
|             if (getAddressType(addr) === null) { | ||||
|                 this.completionStates[addr] = 'error'; | ||||
|                 this.completionStates[addr] = InviteState.Error; | ||||
|                 this.errors[addr] = { | ||||
|                     errcode: 'M_INVALID', | ||||
|                     errorText: _t('Unrecognised address'), | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
|         this.deferred = defer(); | ||||
|         this._inviteMore(0); | ||||
|         this.deferred = defer<CompletionStates>(); | ||||
|         this.inviteMore(0); | ||||
| 
 | ||||
|         return this.deferred.promise; | ||||
|     } | ||||
|  | @ -81,33 +105,36 @@ export default class MultiInviter { | |||
|     /** | ||||
|      * Stops inviting. Causes promises returned by invite() to be rejected. | ||||
|      */ | ||||
|     cancel() { | ||||
|     public cancel(): void { | ||||
|         if (!this.busy) return; | ||||
| 
 | ||||
|         this._canceled = true; | ||||
|         this.canceled = true; | ||||
|         this.deferred.reject(new Error('canceled')); | ||||
|     } | ||||
| 
 | ||||
|     getCompletionState(addr) { | ||||
|     public getCompletionState(addr: string): InviteState { | ||||
|         return this.completionStates[addr]; | ||||
|     } | ||||
| 
 | ||||
|     getErrorText(addr) { | ||||
|     public getErrorText(addr: string): string { | ||||
|         return this.errors[addr] ? this.errors[addr].errorText : null; | ||||
|     } | ||||
| 
 | ||||
|     async _inviteToRoom(roomId, addr, ignoreProfile) { | ||||
|     private async inviteToRoom(roomId: string, addr: string, ignoreProfile = false): Promise<{}> { | ||||
|         const addrType = getAddressType(addr); | ||||
| 
 | ||||
|         if (addrType === 'email') { | ||||
|         if (addrType === AddressType.Email) { | ||||
|             return MatrixClientPeg.get().inviteByEmail(roomId, addr); | ||||
|         } else if (addrType === 'mx-user-id') { | ||||
|         } else if (addrType === AddressType.MatrixUserId) { | ||||
|             const room = MatrixClientPeg.get().getRoom(roomId); | ||||
|             if (!room) throw new Error("Room not found"); | ||||
| 
 | ||||
|             const member = room.getMember(addr); | ||||
|             if (member && ['join', 'invite'].includes(member.membership)) { | ||||
|                 throw {errcode: "RIOT.ALREADY_IN_ROOM", error: "Member already invited"}; | ||||
|                 throw new new MatrixError({ | ||||
|                     errcode: "RIOT.ALREADY_IN_ROOM", | ||||
|                     error: "Member already invited", | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             if (!ignoreProfile && SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) { | ||||
|  | @ -124,28 +151,28 @@ export default class MultiInviter { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     _doInvite(address, ignoreProfile) { | ||||
|         return new Promise((resolve, reject) => { | ||||
|     private doInvite(address: string, ignoreProfile = false): Promise<void> { | ||||
|         return new Promise<void>((resolve, reject) => { | ||||
|             console.log(`Inviting ${address}`); | ||||
| 
 | ||||
|             let doInvite; | ||||
|             if (this.groupId !== null) { | ||||
|                 doInvite = GroupStore.inviteUserToGroup(this.groupId, address); | ||||
|             } else { | ||||
|                 doInvite = this._inviteToRoom(this.roomId, address, ignoreProfile); | ||||
|                 doInvite = this.inviteToRoom(this.roomId, address, ignoreProfile); | ||||
|             } | ||||
| 
 | ||||
|             doInvite.then(() => { | ||||
|                 if (this._canceled) { | ||||
|                 if (this.canceled) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 this.completionStates[address] = 'invited'; | ||||
|                 this.completionStates[address] = InviteState.Invited; | ||||
|                 delete this.errors[address]; | ||||
| 
 | ||||
|                 resolve(); | ||||
|             }).catch((err) => { | ||||
|                 if (this._canceled) { | ||||
|                 if (this.canceled) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|  | @ -161,7 +188,7 @@ export default class MultiInviter { | |||
|                 } else if (err.errcode === 'M_LIMIT_EXCEEDED') { | ||||
|                     // we're being throttled so wait a bit & try again
 | ||||
|                     setTimeout(() => { | ||||
|                         this._doInvite(address, ignoreProfile).then(resolve, reject); | ||||
|                         this.doInvite(address, ignoreProfile).then(resolve, reject); | ||||
|                     }, 5000); | ||||
|                     return; | ||||
|                 } else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND'].includes(err.errcode)) { | ||||
|  | @ -171,7 +198,7 @@ export default class MultiInviter { | |||
|                 } else if (err.errcode === 'M_PROFILE_NOT_FOUND' && !ignoreProfile) { | ||||
|                     // Invite without the profile check
 | ||||
|                     console.warn(`User ${address} does not have a profile - inviting anyways automatically`); | ||||
|                     this._doInvite(address, true).then(resolve, reject); | ||||
|                     this.doInvite(address, true).then(resolve, reject); | ||||
|                 } else if (err.errcode === "M_BAD_STATE") { | ||||
|                     errorText = _t("The user must be unbanned before they can be invited."); | ||||
|                 } else if (err.errcode === "M_UNSUPPORTED_ROOM_VERSION") { | ||||
|  | @ -180,14 +207,14 @@ export default class MultiInviter { | |||
|                     errorText = _t('Unknown server error'); | ||||
|                 } | ||||
| 
 | ||||
|                 this.completionStates[address] = 'error'; | ||||
|                 this.errors[address] = {errorText, errcode: err.errcode}; | ||||
|                 this.completionStates[address] = InviteState.Error; | ||||
|                 this.errors[address] = { errorText, errcode: err.errcode }; | ||||
| 
 | ||||
|                 this.busy = !fatal; | ||||
|                 this.fatal = fatal; | ||||
|                 this._fatal = fatal; | ||||
| 
 | ||||
|                 if (fatal) { | ||||
|                     reject(); | ||||
|                     reject(err); | ||||
|                 } else { | ||||
|                     resolve(); | ||||
|                 } | ||||
|  | @ -195,22 +222,22 @@ export default class MultiInviter { | |||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     _inviteMore(nextIndex, ignoreProfile) { | ||||
|         if (this._canceled) { | ||||
|     private inviteMore(nextIndex: number, ignoreProfile = false): void { | ||||
|         if (this.canceled) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (nextIndex === this.addrs.length) { | ||||
|         if (nextIndex === this.addresses.length) { | ||||
|             this.busy = false; | ||||
|             if (Object.keys(this.errors).length > 0 && !this.groupId) { | ||||
|                 // There were problems inviting some people - see if we can invite them
 | ||||
|                 // without caring if they exist or not.
 | ||||
|                 const unknownProfileErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND']; | ||||
|                 const unknownProfileUsers = Object.keys(this.errors).filter(a => unknownProfileErrors.includes(this.errors[a].errcode)); | ||||
|                 const unknownProfileUsers = Object.keys(this.errors) | ||||
|                     .filter(a => UNKNOWN_PROFILE_ERRORS.includes(this.errors[a].errcode)); | ||||
| 
 | ||||
|                 if (unknownProfileUsers.length > 0) { | ||||
|                     const inviteUnknowns = () => { | ||||
|                         const promises = unknownProfileUsers.map(u => this._doInvite(u, true)); | ||||
|                         const promises = unknownProfileUsers.map(u => this.doInvite(u, true)); | ||||
|                         Promise.all(promises).then(() => this.deferred.resolve(this.completionStates)); | ||||
|                     }; | ||||
| 
 | ||||
|  | @ -219,15 +246,17 @@ export default class MultiInviter { | |||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     const AskInviteAnywayDialog = sdk.getComponent("dialogs.AskInviteAnywayDialog"); | ||||
|                     console.log("Showing failed to invite dialog..."); | ||||
|                     Modal.createTrackedDialog('Failed to invite', '', AskInviteAnywayDialog, { | ||||
|                         unknownProfileUsers: unknownProfileUsers.map(u => {return {userId: u, errorText: this.errors[u].errorText};}), | ||||
|                         unknownProfileUsers: unknownProfileUsers.map(u => ({ | ||||
|                             userId: u, | ||||
|                             errorText: this.errors[u].errorText, | ||||
|                         })), | ||||
|                         onInviteAnyways: () => inviteUnknowns(), | ||||
|                         onGiveUp: () => { | ||||
|                             // Fake all the completion states because we already warned the user
 | ||||
|                             for (const addr of unknownProfileUsers) { | ||||
|                                 this.completionStates[addr] = 'invited'; | ||||
|                                 this.completionStates[addr] = InviteState.Invited; | ||||
|                             } | ||||
|                             this.deferred.resolve(this.completionStates); | ||||
|                         }, | ||||
|  | @ -239,25 +268,25 @@ export default class MultiInviter { | |||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const addr = this.addrs[nextIndex]; | ||||
|         const addr = this.addresses[nextIndex]; | ||||
| 
 | ||||
|         // don't try to invite it if it's an invalid address
 | ||||
|         // (it will already be marked as an error though,
 | ||||
|         // so no need to do so again)
 | ||||
|         if (getAddressType(addr) === null) { | ||||
|             this._inviteMore(nextIndex + 1); | ||||
|             this.inviteMore(nextIndex + 1); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // don't re-invite (there's no way in the UI to do this, but
 | ||||
|         // for sanity's sake)
 | ||||
|         if (this.completionStates[addr] === 'invited') { | ||||
|             this._inviteMore(nextIndex + 1); | ||||
|         if (this.completionStates[addr] === InviteState.Invited) { | ||||
|             this.inviteMore(nextIndex + 1); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this._doInvite(addr, ignoreProfile).then(() => { | ||||
|             this._inviteMore(nextIndex + 1, ignoreProfile); | ||||
|         this.doInvite(addr, ignoreProfile).then(() => { | ||||
|             this.inviteMore(nextIndex + 1, ignoreProfile); | ||||
|         }).catch(() => this.deferred.resolve(this.completionStates)); | ||||
|     } | ||||
| } | ||||
|  | @ -27,6 +27,8 @@ import {PayloadEvent, WORKLET_NAME} from "./consts"; | |||
| import {UPDATE_EVENT} from "../stores/AsyncStore"; | ||||
| import {Playback} from "./Playback"; | ||||
| import {createAudioContext} from "./compat"; | ||||
| import { IEncryptedFile } from "matrix-js-sdk/src/@types/event"; | ||||
| import { uploadFile } from "../ContentMessages"; | ||||
| 
 | ||||
| const CHANNELS = 1; // stereo isn't important
 | ||||
| export const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality.
 | ||||
|  | @ -49,6 +51,11 @@ export enum RecordingState { | |||
|     Uploaded = "uploaded", | ||||
| } | ||||
| 
 | ||||
| export interface IUpload { | ||||
|     mxc?: string; // for unencrypted uploads
 | ||||
|     encrypted?: IEncryptedFile; | ||||
| } | ||||
| 
 | ||||
| export class VoiceRecording extends EventEmitter implements IDestroyable { | ||||
|     private recorder: Recorder; | ||||
|     private recorderContext: AudioContext; | ||||
|  | @ -58,7 +65,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { | |||
|     private recorderWorklet: AudioWorkletNode; | ||||
|     private recorderProcessor: ScriptProcessorNode; | ||||
|     private buffer = new Uint8Array(0); // use this.audioBuffer to access
 | ||||
|     private mxc: string; | ||||
|     private lastUpload: IUpload; | ||||
|     private recording = false; | ||||
|     private observable: SimpleObservable<IRecordingUpdate>; | ||||
|     private amplitudes: number[] = []; // at each second mark, generated
 | ||||
|  | @ -214,13 +221,6 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { | |||
|         return this.buffer.length > 0; | ||||
|     } | ||||
| 
 | ||||
|     public get mxcUri(): string { | ||||
|         if (!this.mxc) { | ||||
|             throw new Error("Recording has not been uploaded yet"); | ||||
|         } | ||||
|         return this.mxc; | ||||
|     } | ||||
| 
 | ||||
|     private onAudioProcess = (ev: AudioProcessingEvent) => { | ||||
|         this.processAudioUpdate(ev.playbackTime); | ||||
| 
 | ||||
|  | @ -290,7 +290,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { | |||
|     }; | ||||
| 
 | ||||
|     public async start(): Promise<void> { | ||||
|         if (this.mxc || this.hasRecording) { | ||||
|         if (this.lastUpload || this.hasRecording) { | ||||
|             throw new Error("Recording already prepared"); | ||||
|         } | ||||
|         if (this.recording) { | ||||
|  | @ -362,20 +362,19 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { | |||
|         this.observable.close(); | ||||
|     } | ||||
| 
 | ||||
|     public async upload(): Promise<string> { | ||||
|     public async upload(inRoomId: string): Promise<IUpload> { | ||||
|         if (!this.hasRecording) { | ||||
|             throw new Error("No recording available to upload"); | ||||
|         } | ||||
| 
 | ||||
|         if (this.mxc) return this.mxc; | ||||
|         if (this.lastUpload) return this.lastUpload; | ||||
| 
 | ||||
|         this.emit(RecordingState.Uploading); | ||||
|         this.mxc = await this.client.uploadContent(new Blob([this.audioBuffer], { | ||||
|         const { url: mxc, file: encrypted } = await uploadFile(this.client, inRoomId, new Blob([this.audioBuffer], { | ||||
|             type: this.contentType, | ||||
|         }), { | ||||
|             onlyContentUri: false, // to stop the warnings in the console
 | ||||
|         }).then(r => r['content_uri']); | ||||
|         })); | ||||
|         this.lastUpload = { mxc, encrypted }; | ||||
|         this.emit(RecordingState.Uploaded); | ||||
|         return this.mxc; | ||||
|         return this.lastUpload; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,21 +1,36 @@ | |||
| /* | ||||
| Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com> | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import React from 'react'; | ||||
| import ReactTestUtils from 'react-dom/test-utils'; | ||||
| import ReactDOM from 'react-dom'; | ||||
| 
 | ||||
| import * as TestUtils from '../../../test-utils'; | ||||
| 
 | ||||
| import {MatrixClientPeg} from '../../../../src/MatrixClientPeg'; | ||||
| import sdk from '../../../skinned-sdk'; | ||||
| 
 | ||||
| import {Room, RoomMember, User} from 'matrix-js-sdk'; | ||||
| 
 | ||||
| import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; | ||||
| import { Room } from 'matrix-js-sdk/src/models/room'; | ||||
| import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; | ||||
| import { User } from "matrix-js-sdk/src/models/user"; | ||||
| import { compare } from "../../../../src/utils/strings"; | ||||
| import MemberList from "../../../../src/components/views/rooms/MemberList"; | ||||
| 
 | ||||
| function generateRoomId() { | ||||
|     return '!' + Math.random().toString().slice(2, 10) + ':domain'; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| describe('MemberList', () => { | ||||
|     function createRoom(opts) { | ||||
|         const room = new Room(generateRoomId(), null, client.getUserId()); | ||||
|  | @ -97,13 +112,19 @@ describe('MemberList', () => { | |||
|             memberListRoom.currentState.members[member.userId] = member; | ||||
|         } | ||||
| 
 | ||||
|         const MemberList = sdk.getComponent('views.rooms.MemberList'); | ||||
|         const WrappedMemberList = TestUtils.wrapInMatrixClientContext(MemberList); | ||||
|         const gatherWrappedRef = (r) => { | ||||
|             memberList = r; | ||||
|         }; | ||||
|         root = ReactDOM.render(<WrappedMemberList roomId={memberListRoom.roomId} | ||||
|             wrappedRef={gatherWrappedRef} />, parentDiv); | ||||
|         root = ReactDOM.render( | ||||
|             ( | ||||
|                 <WrappedMemberList | ||||
|                     roomId={memberListRoom.roomId} | ||||
|                     wrappedRef={gatherWrappedRef} | ||||
|                 /> | ||||
|             ), | ||||
|             parentDiv, | ||||
|         ); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach((done) => { | ||||
|  | @ -213,8 +234,8 @@ describe('MemberList', () => { | |||
|                 }); | ||||
| 
 | ||||
|                 // Bypass all the event listeners and skip to the good part
 | ||||
|                 memberList._showPresence = enablePresence; | ||||
|                 memberList._updateListNow(); | ||||
|                 memberList.showPresence = enablePresence; | ||||
|                 memberList.updateListNow(); | ||||
| 
 | ||||
|                 const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); | ||||
|                 expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); | ||||
|  | @ -225,7 +246,7 @@ describe('MemberList', () => { | |||
| 
 | ||||
|                 // Bypass all the event listeners and skip to the good part
 | ||||
|                 memberList._showPresence = enablePresence; | ||||
|                 memberList._updateListNow(); | ||||
|                 memberList.updateListNow(); | ||||
| 
 | ||||
|                 const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); | ||||
|                 expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); | ||||
|  | @ -254,8 +275,8 @@ describe('MemberList', () => { | |||
|                 }); | ||||
| 
 | ||||
|                 // Bypass all the event listeners and skip to the good part
 | ||||
|                 memberList._showPresence = enablePresence; | ||||
|                 memberList._updateListNow(); | ||||
|                 memberList.showPresence = enablePresence; | ||||
|                 memberList.updateListNow(); | ||||
| 
 | ||||
|                 const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); | ||||
|                 expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); | ||||
|  | @ -273,8 +294,8 @@ describe('MemberList', () => { | |||
|                 }); | ||||
| 
 | ||||
|                 // Bypass all the event listeners and skip to the good part
 | ||||
|                 memberList._showPresence = enablePresence; | ||||
|                 memberList._updateListNow(); | ||||
|                 memberList.showPresence = enablePresence; | ||||
|                 memberList.updateListNow(); | ||||
| 
 | ||||
|                 const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); | ||||
|                 expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); | ||||
|  | @ -40,7 +40,7 @@ async function acceptToast(session, expectedTitle) { | |||
| 
 | ||||
| async function rejectToast(session, expectedTitle) { | ||||
|     await assertToast(session, expectedTitle); | ||||
|     const btn = await session.query('.mx_Toast_buttons .mx_AccessibleButton_kind_danger'); | ||||
|     const btn = await session.query('.mx_Toast_buttons .mx_AccessibleButton_kind_danger_outline'); | ||||
|     await btn.click(); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Germain Souquet
						Germain Souquet