diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index b57a5de8c1..6410bd28fa 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -1,4 +1,4 @@ -name: Develop jobs +name: Develop on: push: branches: [develop] @@ -31,9 +31,7 @@ jobs: tool: 'jsperformanceentry' output-file-path: test/end-to-end-tests/performance-entries.json fail-on-alert: false - # Secrets are not passed to fork, the action won't be able to comment - # for community PRs - comment-on-alert: ${{ github.repository_owner == 'matrix-org' }} + comment-on-alert: false # Only temporary to monitor where failures occur alert-comment-cc-users: '@gsouquet' github-token: ${{ secrets.DEPLOY_GH_PAGES }} diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss index 8262075559..168a8bb74b 100644 --- a/res/css/views/voip/_CallContainer.scss +++ b/res/css/views/voip/_CallContainer.scss @@ -98,5 +98,29 @@ limitations under the License. line-height: $font-24px; } } + + .mx_IncomingCallBox_iconButton { + position: absolute; + right: 8px; + + &::before { + content: ''; + + height: 20px; + width: 20px; + background-color: $icon-button-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } + } + + .mx_IncomingCallBox_silence::before { + mask-image: url('$(res)/img/voip/silence.svg'); + } + + .mx_IncomingCallBox_unSilence::before { + mask-image: url('$(res)/img/voip/un-silence.svg'); + } } } diff --git a/res/img/voip/silence.svg b/res/img/voip/silence.svg new file mode 100644 index 0000000000..332932dfff --- /dev/null +++ b/res/img/voip/silence.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/voip/un-silence.svg b/res/img/voip/un-silence.svg new file mode 100644 index 0000000000..c00b366f84 --- /dev/null +++ b/res/img/voip/un-silence.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 22280b8a28..0c6b63dd33 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -44,6 +44,7 @@ import { EventIndexPeg } from "../indexing/EventIndexPeg"; import {VoiceRecordingStore} from "../stores/VoiceRecordingStore"; import PerformanceMonitor from "../performance"; import UIStore from "../stores/UIStore"; +import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; declare global { interface Window { @@ -84,6 +85,7 @@ declare global { mxPerformanceMonitor: PerformanceMonitor; mxPerformanceEntryNames: any; mxUIStore: UIStore; + mxSetupEncryptionStore?: SetupEncryptionStore; } interface Document { diff --git a/src/Avatar.ts b/src/Avatar.ts index a6499c688e..8ea0b0c9fa 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -14,18 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {RoomMember} from "matrix-js-sdk/src/models/room-member"; -import {User} from "matrix-js-sdk/src/models/user"; -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 { Room } from "matrix-js-sdk/src/models/room"; import DMRoomMap from './utils/DMRoomMap'; -import {mediaFromMxc} from "./customisations/Media"; +import { mediaFromMxc } from "./customisations/Media"; import SettingsStore from "./settings/SettingsStore"; export type ResizeMethod = "crop" | "scale"; // Not to be used for BaseAvatar urls as that has similar default avatar fallback already -export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) { +export function avatarUrlForMember( + member: RoomMember, + width: number, + height: number, + resizeMethod: ResizeMethod, +): string { let url: string; if (member?.getMxcAvatarUrl()) { url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); @@ -39,7 +44,12 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu return url; } -export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) { +export function avatarUrlForUser( + user: Pick, + width: number, + height: number, + resizeMethod?: ResizeMethod, +): string | null { if (!user.avatarUrl) return null; return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(width, height, resizeMethod); } diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index bf7cb3473d..448b1cb780 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -99,7 +99,7 @@ const CHECK_PROTOCOLS_ATTEMPTS = 3; // (and store the ID of their native room) export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room'; -enum AudioID { +export enum AudioID { Ring = 'ringAudio', Ringback = 'ringbackAudio', CallEnd = 'callendAudio', diff --git a/src/Searching.js b/src/Searching.js index 2b17aee054..596dd2f3d4 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -468,7 +468,7 @@ function restoreEncryptionInfo(searchResultSlice = []) { ev.event.curve25519Key, ev.event.ed25519Key, ); - ev._forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain; + ev.forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain; delete ev.event.curve25519Key; delete ev.event.ed25519Key; diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 4a7b37b5e5..9700c57d67 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -150,6 +150,10 @@ function success(promise?: Promise) { return {promise}; } +function successSync(value: any) { + return success(Promise.resolve(value)); +} + /* Disable the "unexpected this" error for these commands - all of the run * functions are called with `this` bound to the Command instance. */ @@ -160,7 +164,7 @@ export const Commands = [ args: '', description: _td('Sends the given message as a spoiler'), runFn: function(roomId, message) { - return success(ContentHelpers.makeHtmlMessage( + return successSync(ContentHelpers.makeHtmlMessage( message, `${message}`, )); @@ -176,7 +180,7 @@ export const Commands = [ if (args) { message = message + ' ' + args; } - return success(ContentHelpers.makeTextMessage(message)); + return successSync(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages, }), @@ -189,7 +193,7 @@ export const Commands = [ if (args) { message = message + ' ' + args; } - return success(ContentHelpers.makeTextMessage(message)); + return successSync(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages, }), @@ -202,7 +206,7 @@ export const Commands = [ if (args) { message = message + ' ' + args; } - return success(ContentHelpers.makeTextMessage(message)); + return successSync(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages, }), @@ -215,7 +219,7 @@ export const Commands = [ if (args) { message = message + ' ' + args; } - return success(ContentHelpers.makeTextMessage(message)); + return successSync(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages, }), @@ -224,7 +228,7 @@ export const Commands = [ args: '', description: _td('Sends a message as plain text, without interpreting it as markdown'), runFn: function(roomId, messages) { - return success(ContentHelpers.makeTextMessage(messages)); + return successSync(ContentHelpers.makeTextMessage(messages)); }, category: CommandCategories.messages, }), @@ -233,7 +237,7 @@ export const Commands = [ args: '', description: _td('Sends a message as html, without interpreting it as markdown'), runFn: function(roomId, messages) { - return success(ContentHelpers.makeHtmlMessage(messages, messages)); + return successSync(ContentHelpers.makeHtmlMessage(messages, messages)); }, category: CommandCategories.messages, }), @@ -978,7 +982,7 @@ export const Commands = [ args: '', runFn: function(roomId, args) { if (!args) return reject(this.getUserId()); - return success(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args))); + return successSync(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args))); }, category: CommandCategories.messages, }), @@ -988,7 +992,7 @@ export const Commands = [ args: '', runFn: function(roomId, args) { if (!args) return reject(this.getUserId()); - return success(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args))); + return successSync(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args))); }, category: CommandCategories.messages, }), diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index d576a5434c..dacb4262bd 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -24,7 +24,9 @@ import { Room } from 'matrix-js-sdk/src/models/room'; // is sip virtual: there could be others in the future. export default class VoipUserMapper { - private virtualRoomIdCache = new Set(); + // We store mappings of virtual -> native room IDs here until the local echo for the + // account data arrives. + private virtualToNativeRoomIdCache = new Map(); public static sharedInstance(): VoipUserMapper { if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper(); @@ -49,10 +51,20 @@ export default class VoipUserMapper { native_room: roomId, }); + this.virtualToNativeRoomIdCache.set(virtualRoomId, roomId); + return virtualRoomId; } public nativeRoomForVirtualRoom(roomId: string): string { + const cachedNativeRoomId = this.virtualToNativeRoomIdCache.get(roomId); + if (cachedNativeRoomId) { + console.log( + "Returning native room ID " + cachedNativeRoomId + " for virtual room ID " + roomId + " from cache", + ); + return cachedNativeRoomId; + } + const virtualRoom = MatrixClientPeg.get().getRoom(roomId); if (!virtualRoom) return null; const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE); @@ -67,7 +79,7 @@ export default class VoipUserMapper { public isVirtualRoom(room: Room): boolean { if (this.nativeRoomForVirtualRoom(room.roomId)) return true; - if (this.virtualRoomIdCache.has(room.roomId)) return true; + if (this.virtualToNativeRoomIdCache.has(room.roomId)) return true; // also look in the create event for the claimed native room ID, which is the only // way we can recognise a virtual room we've created when it first arrives down @@ -110,7 +122,7 @@ export default class VoipUserMapper { // also put this room in the virtual room ID cache so isVirtualRoom return the right answer // in however long it takes for the echo of setAccountData to come down the sync - this.virtualRoomIdCache.add(invitedRoom.roomId); + this.virtualToNativeRoomIdCache.set(invitedRoom.roomId, nativeRoom.roomId); } } } diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 3cf43d0b84..687b477133 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -20,19 +20,19 @@ limitations under the License. import React from 'react'; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; -import {PillCompletion} from './Components'; +import { PillCompletion } from './Components'; import * as sdk from '../index'; import QueryMatcher from './QueryMatcher'; -import {sortBy} from 'lodash'; -import {MatrixClientPeg} from '../MatrixClientPeg'; +import { sortBy } from 'lodash'; +import { MatrixClientPeg } from '../MatrixClientPeg'; -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 EventTimeline from "matrix-js-sdk/src/models/event-timeline"; -import {makeUserPermalink} from "../utils/permalinks/Permalinks"; -import {ICompletion, ISelectionRange} from "./Autocompleter"; +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 { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; +import { makeUserPermalink } from "../utils/permalinks/Permalinks"; +import { ICompletion, ISelectionRange } from "./Autocompleter"; const USER_REGEX = /\B@\S*/g; diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index b2f0c70bd7..7d74229421 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -41,7 +41,7 @@ export function getUnsentMessages(room) { } @replaceableComponent("structures.RoomStatusBar") -export default class RoomStatusBar extends React.Component { +export default class RoomStatusBar extends React.PureComponent { static propTypes = { // the room this statusbar is representing. room: PropTypes.object.isRequired, diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index fe90d2f873..1e3adcb518 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -25,6 +25,7 @@ import React, { createRef } from 'react'; import classNames from 'classnames'; import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { SearchResult } from "matrix-js-sdk/src/models/search-result"; import { EventSubscription } from "fbemitter"; import shouldHideEvent from '../../shouldHideEvent'; @@ -80,7 +81,6 @@ import { objectHasDiff } from "../../utils/objects"; import SpaceRoomView from "./SpaceRoomView"; import { IOpts } from "../../createRoom"; import { replaceableComponent } from "../../utils/replaceableComponent"; -import { omit } from 'lodash'; import UIStore from "../../stores/UIStore"; const DEBUG = false; @@ -143,7 +143,7 @@ export interface IState { searchResults?: XOR<{}, { count: number; highlights: string[]; - results: MatrixEvent[]; + results: SearchResult[]; next_batch: string; // eslint-disable-line camelcase }>; searchHighlights?: string[]; @@ -572,16 +572,12 @@ export default class RoomView extends React.Component { shouldComponentUpdate(nextProps, nextState) { const hasPropsDiff = objectHasDiff(this.props, nextProps); - // React only shallow comparison and we only want to trigger - // a component re-render if a room requires an upgrade - const newUpgradeRecommendation = nextState.upgradeRecommendation || {} - - const state = omit(this.state, ['upgradeRecommendation']); - const newState = omit(nextState, ['upgradeRecommendation']) + const { upgradeRecommendation, ...state } = this.state; + const { upgradeRecommendation: newUpgradeRecommendation, ...newState } = nextState; const hasStateDiff = - objectHasDiff(state, newState) || - (newUpgradeRecommendation.needsUpgrade === true) + newUpgradeRecommendation?.needsUpgrade !== upgradeRecommendation?.needsUpgrade || + objectHasDiff(state, newState); return hasPropsDiff || hasStateDiff; } @@ -701,16 +697,11 @@ export default class RoomView extends React.Component { room_id: this.state.room.roomId, event_id: this.state.initialEventId, highlighted: false, + replyingToEvent: this.state.replyToEvent, }); } } - private onLayoutChange = () => { - this.setState({ - layout: SettingsStore.getValue("layout"), - }); - }; - private onRightPanelStoreUpdate = () => { this.setState({ showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, @@ -1644,29 +1635,27 @@ export default class RoomView extends React.Component { let auxPanelMaxHeight = UIStore.instance.windowHeight - (54 + // height of RoomHeader 36 + // height of the status area - 51 + // minimum height of the message compmoser + 51 + // minimum height of the message composer 120); // amount of desired scrollback // XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway // but it's better than the video going missing entirely if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50; - this.setState({auxPanelMaxHeight: auxPanelMaxHeight}); + if (this.state.auxPanelMaxHeight !== auxPanelMaxHeight) { + this.setState({ auxPanelMaxHeight }); + } }; private onStatusBarVisible = () => { - if (this.unmounted) return; - this.setState({ - statusBarVisible: true, - }); + if (this.unmounted || this.state.statusBarVisible) return; + this.setState({ statusBarVisible: true }); }; private onStatusBarHidden = () => { // This is currently not desired as it is annoying if it keeps expanding and collapsing - if (this.unmounted) return; - this.setState({ - statusBarVisible: false, - }); + if (this.unmounted || !this.state.statusBarVisible) return; + this.setState({ statusBarVisible: false }); }; /** diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 3ccf2e5424..aad770888b 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -59,7 +59,7 @@ import IconizedContextMenu, { } from "../views/context_menus/IconizedContextMenu"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import {BetaPill} from "../views/beta/BetaCard"; -import {USER_LABS_TAB} from "../views/dialogs/UserSettingsDialog"; +import { UserTab } from "../views/dialogs/UserSettingsDialog"; import SettingsStore from "../../settings/SettingsStore"; import dis from "../../dispatcher/dispatcher"; import Modal from "../../Modal"; @@ -166,7 +166,7 @@ const SpaceInfo = ({ space }) => { const onBetaClick = () => { defaultDispatcher.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_LABS_TAB, + initialTabId: UserTab.Labs, }); }; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index bb62745d98..03d0b5c6d7 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -18,14 +18,14 @@ limitations under the License. */ import SettingsStore from "../../settings/SettingsStore"; -import {LayoutPropType} from "../../settings/Layout"; -import React, {createRef} from 'react'; +import { LayoutPropType } from "../../settings/Layout"; +import React, { createRef } from 'react'; import ReactDOM from "react-dom"; import PropTypes from 'prop-types'; -import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline"; -import {TimelineWindow} from "matrix-js-sdk/src/timeline-window"; +import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; +import { TimelineWindow } from "matrix-js-sdk/src/timeline-window"; import { _t } from '../../languageHandler'; -import {MatrixClientPeg} from "../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; import RoomContext from "../../contexts/RoomContext"; import UserActivity from "../../UserActivity"; import Modal from "../../Modal"; @@ -35,10 +35,11 @@ import { Key } from '../../Keyboard'; import Timer from '../../utils/Timer'; import shouldHideEvent from '../../shouldHideEvent'; import EditorStateTransfer from '../../utils/EditorStateTransfer'; -import {haveTileForEvent} from "../views/rooms/EventTile"; -import {UIFeature} from "../../settings/UIFeature"; -import {replaceableComponent} from "../../utils/replaceableComponent"; +import { haveTileForEvent } from "../views/rooms/EventTile"; +import { UIFeature } from "../../settings/UIFeature"; +import { replaceableComponent } from "../../utils/replaceableComponent"; import { arrayFastClone } from "../../utils/arrays"; +import { Action } from "../../dispatcher/actions"; const PAGINATE_SIZE = 20; const INITIAL_SIZE = 20; @@ -439,21 +440,42 @@ class TimelinePanel extends React.Component { }; onAction = payload => { - if (payload.action === 'ignore_state_changed') { - this.forceUpdate(); - } - if (payload.action === "edit_event") { - const editState = payload.event ? new EditorStateTransfer(payload.event) : null; - this.setState({editState}, () => { - if (payload.event && this._messagePanel.current) { - this._messagePanel.current.scrollToEventIfNeeded( - payload.event.getId(), - ); + switch (payload.action) { + case "ignore_state_changed": + this.forceUpdate(); + break; + + case "edit_event": { + const editState = payload.event ? new EditorStateTransfer(payload.event) : null; + this.setState({editState}, () => { + if (payload.event && this._messagePanel.current) { + this._messagePanel.current.scrollToEventIfNeeded( + payload.event.getId(), + ); + } + }); + break; + } + + case Action.ComposerInsert: { + // re-dispatch to the correct composer + if (this.state.editState) { + dis.dispatch({ + ...payload, + action: "edit_composer_insert", + }); + } else { + dis.dispatch({ + ...payload, + action: "send_composer_insert", + }); } - }); - } - if (payload.action === "scroll_to_bottom") { - this.jumpToLiveTimeline(); + break; + } + + case "scroll_to_bottom": + this.jumpToLiveTimeline(); + break; } }; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 6a449cf1a2..3cf0dc5f84 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -26,7 +26,7 @@ import { ActionPayload } from "../../dispatcher/payloads"; import { Action } from "../../dispatcher/actions"; import { _t } from "../../languageHandler"; import { ContextMenuButton } from "./ContextMenu"; -import { USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB } from "../views/dialogs/UserSettingsDialog"; +import { UserTab } from "../views/dialogs/UserSettingsDialog"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import FeedbackDialog from "../views/dialogs/FeedbackDialog"; import Modal from "../../Modal"; @@ -408,12 +408,12 @@ export default class UserMenu extends React.Component { this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)} + onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)} /> this.onSettingsOpen(e, USER_SECURITY_TAB)} + onClick={(e) => this.onSettingsOpen(e, UserTab.Security)} /> ; title = _t("Verify this login"); - } else if (phase === PHASE_DONE) { + } else if (phase === Phase.Done) { icon = ; title = _t("Session verified"); - } else if (phase === PHASE_CONFIRM_SKIP) { + } else if (phase === Phase.ConfirmSkip) { icon = ; title = _t("Are you sure?"); - } else if (phase === PHASE_BUSY) { + } else if (phase === Phase.Busy) { icon = ; title = _t("Verify this login"); } else { diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 6feb1e34f7..3a4be6f0d6 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -269,7 +269,7 @@ export default class Registration extends React.Component { ); } - private onUIAuthFinished = async (success, response, extra) => { + private onUIAuthFinished = async (success: boolean, response: any) => { if (!success) { let msg = response.message || response.toString(); // can we give a better error message? diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index 803df19d00..90137e084c 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -21,15 +21,7 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog'; import * as sdk from '../../../index'; -import { - SetupEncryptionStore, - PHASE_LOADING, - PHASE_INTRO, - PHASE_BUSY, - PHASE_DONE, - PHASE_CONFIRM_SKIP, - PHASE_FINISHED, -} from '../../../stores/SetupEncryptionStore'; +import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore'; import {replaceableComponent} from "../../../utils/replaceableComponent"; function keyHasPassphrase(keyInfo) { @@ -63,7 +55,7 @@ export default class SetupEncryptionBody extends React.Component { _onStoreUpdate = () => { const store = SetupEncryptionStore.sharedInstance(); - if (store.phase === PHASE_FINISHED) { + if (store.phase === Phase.Finished) { this.props.onFinished(); return; } @@ -136,7 +128,7 @@ export default class SetupEncryptionBody extends React.Component { onClose={this.props.onFinished} member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)} />; - } else if (phase === PHASE_INTRO) { + } else if (phase === Phase.Intro) { const store = SetupEncryptionStore.sharedInstance(); let recoveryKeyPrompt; if (store.keyInfo && keyHasPassphrase(store.keyInfo)) { @@ -174,7 +166,7 @@ export default class SetupEncryptionBody extends React.Component { ); - } else if (phase === PHASE_DONE) { + } else if (phase === Phase.Done) { let message; if (this.state.backupInfo) { message =

{_t( @@ -200,7 +192,7 @@ export default class SetupEncryptionBody extends React.Component { ); - } else if (phase === PHASE_CONFIRM_SKIP) { + } else if (phase === Phase.ConfirmSkip) { return (

{_t( @@ -224,7 +216,7 @@ export default class SetupEncryptionBody extends React.Component {

); - } else if (phase === PHASE_BUSY || phase === PHASE_LOADING) { + } else if (phase === Phase.Busy || phase === Phase.Loading) { const Spinner = sdk.getComponent('views.elements.Spinner'); return ; } else { diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index adfeeb0968..2fb2ac4d0e 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -33,6 +33,8 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; import ForwardDialog from "../dialogs/ForwardDialog"; +import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; +import { Action } from "../../../dispatcher/actions"; export function canCancel(eventStatus) { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; @@ -199,8 +201,8 @@ export default class MessageContextMenu extends React.Component { }; onQuoteClick = () => { - dis.dispatch({ - action: 'quote', + dis.dispatch({ + action: Action.ComposerInsert, event: this.props.mxEvent, }); this.closeMenu(); diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.js b/src/components/views/dialogs/AskInviteAnywayDialog.tsx similarity index 73% rename from src/components/views/dialogs/AskInviteAnywayDialog.js rename to src/components/views/dialogs/AskInviteAnywayDialog.tsx index e6cd45ba6b..970883aca2 100644 --- a/src/components/views/dialogs/AskInviteAnywayDialog.js +++ b/src/components/views/dialogs/AskInviteAnywayDialog.tsx @@ -15,39 +15,41 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; -import {SettingLevel} from "../../../settings/SettingLevel"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { SettingLevel } from "../../../settings/SettingLevel"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; + +interface IProps { + unknownProfileUsers: Array<{ + userId: string; + errorText: string; + }>; + onInviteAnyways: () => void; + onGiveUp: () => void; + onFinished: (success: boolean) => void; +} @replaceableComponent("views.dialogs.AskInviteAnywayDialog") -export default class AskInviteAnywayDialog extends React.Component { - static propTypes = { - unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ] - onInviteAnyways: PropTypes.func.isRequired, - onGiveUp: PropTypes.func.isRequired, - onFinished: PropTypes.func.isRequired, - }; - - _onInviteClicked = () => { +export default class AskInviteAnywayDialog extends React.Component { + private onInviteClicked = (): void => { this.props.onInviteAnyways(); this.props.onFinished(true); }; - _onInviteNeverWarnClicked = () => { + private onInviteNeverWarnClicked = (): void => { SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false); this.props.onInviteAnyways(); this.props.onFinished(true); }; - _onGiveUpClicked = () => { + private onGiveUpClicked = (): void => { this.props.onGiveUp(); this.props.onFinished(false); }; - render() { + public render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const errorList = this.props.unknownProfileUsers @@ -55,11 +57,12 @@ export default class AskInviteAnywayDialog extends React.Component { return (
+ {/* eslint-disable-next-line */}

{_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?")}

    { errorList } @@ -67,13 +70,13 @@ export default class AskInviteAnywayDialog extends React.Component {
- - -
diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx index 1ae50dd66f..1c2dab4bfc 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -29,7 +29,7 @@ import InfoDialog from "./InfoDialog"; import AccessibleButton from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import {Action} from "../../../dispatcher/actions"; -import {USER_LABS_TAB} from "./UserSettingsDialog"; +import { UserTab } from "./UserSettingsDialog"; interface IProps extends IDialogProps { featureId: string; @@ -70,7 +70,7 @@ const BetaFeedbackDialog: React.FC = ({featureId, onFinished}) => { onFinished(false); defaultDispatcher.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_LABS_TAB, + initialTabId: UserTab.Labs, }); }}> { _t("To leave the beta, visit your settings.") } diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.tsx similarity index 79% rename from src/components/views/dialogs/BugReportDialog.js rename to src/components/views/dialogs/BugReportDialog.tsx index cbe0130649..f938340a50 100644 --- a/src/components/views/dialogs/BugReportDialog.js +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -18,7 +18,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; @@ -27,8 +26,27 @@ import sendBugReport, {downloadBugReport} from '../../../rageshake/submit-ragesh import AccessibleButton from "../elements/AccessibleButton"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +interface IProps { + onFinished: (success: boolean) => void; + initialText?: string; + label?: string; +} + +interface IState { + sendLogs: boolean; + busy: boolean; + err: string; + issueUrl: string; + text: string; + progress: string; + downloadBusy: boolean; + downloadProgress: string; +} + @replaceableComponent("views.dialogs.BugReportDialog") -export default class BugReportDialog extends React.Component { +export default class BugReportDialog extends React.Component { + private unmounted: boolean; + constructor(props) { super(props); this.state = { @@ -41,25 +59,18 @@ export default class BugReportDialog extends React.Component { downloadBusy: false, downloadProgress: null, }; - this._unmounted = false; - this._onSubmit = this._onSubmit.bind(this); - this._onCancel = this._onCancel.bind(this); - this._onTextChange = this._onTextChange.bind(this); - this._onIssueUrlChange = this._onIssueUrlChange.bind(this); - this._onSendLogsChange = this._onSendLogsChange.bind(this); - this._sendProgressCallback = this._sendProgressCallback.bind(this); - this._downloadProgressCallback = this._downloadProgressCallback.bind(this); + this.unmounted = false; } - componentWillUnmount() { - this._unmounted = true; + public componentWillUnmount() { + this.unmounted = true; } - _onCancel(ev) { + private onCancel = (): void => { this.props.onFinished(false); } - _onSubmit(ev) { + private onSubmit = (): void => { if ((!this.state.text || !this.state.text.trim()) && (!this.state.issueUrl || !this.state.issueUrl.trim())) { this.setState({ err: _t("Please tell us what went wrong or, better, create a GitHub issue that describes the problem."), @@ -72,15 +83,15 @@ export default class BugReportDialog extends React.Component { (this.state.issueUrl.length > 0 ? this.state.issueUrl : 'No issue link given'); this.setState({ busy: true, progress: null, err: null }); - this._sendProgressCallback(_t("Preparing to send logs")); + this.sendProgressCallback(_t("Preparing to send logs")); sendBugReport(SdkConfig.get().bug_report_endpoint_url, { userText, sendLogs: true, - progressCallback: this._sendProgressCallback, + progressCallback: this.sendProgressCallback, label: this.props.label, }).then(() => { - if (!this._unmounted) { + if (!this.unmounted) { this.props.onFinished(false); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); // N.B. first param is passed to piwik and so doesn't want i18n @@ -91,7 +102,7 @@ export default class BugReportDialog extends React.Component { }); } }, (err) => { - if (!this._unmounted) { + if (!this.unmounted) { this.setState({ busy: false, progress: null, @@ -101,14 +112,14 @@ export default class BugReportDialog extends React.Component { }); } - _onDownload = async (ev) => { + private onDownload = async (): Promise => { this.setState({ downloadBusy: true }); - this._downloadProgressCallback(_t("Preparing to download logs")); + this.downloadProgressCallback(_t("Preparing to download logs")); try { await downloadBugReport({ sendLogs: true, - progressCallback: this._downloadProgressCallback, + progressCallback: this.downloadProgressCallback, label: this.props.label, }); @@ -117,7 +128,7 @@ export default class BugReportDialog extends React.Component { downloadProgress: null, }); } catch (err) { - if (!this._unmounted) { + if (!this.unmounted) { this.setState({ downloadBusy: false, downloadProgress: _t("Failed to send logs: ") + `${err.message}`, @@ -126,33 +137,29 @@ export default class BugReportDialog extends React.Component { } }; - _onTextChange(ev) { - this.setState({ text: ev.target.value }); + private onTextChange = (ev: React.FormEvent): void => { + this.setState({ text: ev.currentTarget.value }); } - _onIssueUrlChange(ev) { - this.setState({ issueUrl: ev.target.value }); + private onIssueUrlChange = (ev: React.FormEvent): void => { + this.setState({ issueUrl: ev.currentTarget.value }); } - _onSendLogsChange(ev) { - this.setState({ sendLogs: ev.target.checked }); - } - - _sendProgressCallback(progress) { - if (this._unmounted) { + private sendProgressCallback = (progress: string): void => { + if (this.unmounted) { return; } - this.setState({progress: progress}); + this.setState({ progress }); } - _downloadProgressCallback(downloadProgress) { - if (this._unmounted) { + private downloadProgressCallback = (downloadProgress: string): void => { + if (this.unmounted) { return; } this.setState({ downloadProgress }); } - render() { + public render() { const Loader = sdk.getComponent("elements.Spinner"); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -183,7 +190,7 @@ export default class BugReportDialog extends React.Component { } return ( - @@ -213,7 +220,7 @@ export default class BugReportDialog extends React.Component {

- + { _t("Download logs") } {this.state.downloadProgress && {this.state.downloadProgress} ...} @@ -223,7 +230,7 @@ export default class BugReportDialog extends React.Component { type="text" className="mx_BugReportDialog_field_input" label={_t("GitHub issue")} - onChange={this._onIssueUrlChange} + onChange={this.onIssueUrlChange} value={this.state.issueUrl} placeholder="https://github.com/vector-im/element-web/issues/..." /> @@ -232,7 +239,7 @@ export default class BugReportDialog extends React.Component { element="textarea" label={_t("Notes")} rows={5} - onChange={this._onTextChange} + onChange={this.onTextChange} value={this.state.text} placeholder={_t( "If there is additional context that would help in " + @@ -245,17 +252,12 @@ export default class BugReportDialog extends React.Component { {error}
); } } - -BugReportDialog.propTypes = { - onFinished: PropTypes.func.isRequired, - initialText: PropTypes.string, -}; diff --git a/src/components/views/dialogs/ChangelogDialog.js b/src/components/views/dialogs/ChangelogDialog.tsx similarity index 88% rename from src/components/views/dialogs/ChangelogDialog.js rename to src/components/views/dialogs/ChangelogDialog.tsx index efbeba3977..0ded33cdcb 100644 --- a/src/components/views/dialogs/ChangelogDialog.js +++ b/src/components/views/dialogs/ChangelogDialog.tsx @@ -16,21 +16,26 @@ Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import request from 'browser-request'; import { _t } from '../../../languageHandler'; +interface IProps { + newVersion: string; + version: string; + onFinished: (success: boolean) => void; +} + const REPOS = ['vector-im/element-web', 'matrix-org/matrix-react-sdk', 'matrix-org/matrix-js-sdk']; -export default class ChangelogDialog extends React.Component { +export default class ChangelogDialog extends React.Component { constructor(props) { super(props); this.state = {}; } - componentDidMount() { + public componentDidMount() { const version = this.props.newVersion.split('-'); const version2 = this.props.version.split('-'); if (version == null || version2 == null) return; @@ -49,7 +54,7 @@ export default class ChangelogDialog extends React.Component { } } - _elementsForCommit(commit) { + private elementsForCommit(commit): JSX.Element { return (
  • @@ -59,7 +64,7 @@ export default class ChangelogDialog extends React.Component { ); } - render() { + public render() { const Spinner = sdk.getComponent('views.elements.Spinner'); const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); @@ -72,7 +77,7 @@ export default class ChangelogDialog extends React.Component { msg: this.state[repo], }); } else { - content = this.state[repo].map(this._elementsForCommit); + content = this.state[repo].map(this.elementsForCommit); } return (
    @@ -99,9 +104,3 @@ export default class ChangelogDialog extends React.Component { ); } } - -ChangelogDialog.propTypes = { - version: PropTypes.string.isRequired, - newVersion: PropTypes.string.isRequired, - onFinished: PropTypes.func.isRequired, -}; diff --git a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx similarity index 89% rename from src/components/views/dialogs/ConfirmAndWaitRedactDialog.js rename to src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx index 37d5510756..ae7b23c2c9 100644 --- a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js +++ b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx @@ -17,7 +17,17 @@ limitations under the License. import React from 'react'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; + +interface IProps { + redact: () => Promise; + onFinished: (success: boolean) => void; +} + +interface IState { + isRedacting: boolean; + redactionErrorCode: string | number; +} /* * A dialog for confirming a redaction. @@ -32,7 +42,7 @@ import {replaceableComponent} from "../../../utils/replaceableComponent"; * To avoid this, we keep the dialog open as long as /redact is in progress. */ @replaceableComponent("views.dialogs.ConfirmAndWaitRedactDialog") -export default class ConfirmAndWaitRedactDialog extends React.PureComponent { +export default class ConfirmAndWaitRedactDialog extends React.PureComponent { constructor(props) { super(props); this.state = { @@ -41,7 +51,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent { }; } - onParentFinished = async (proceed) => { + public onParentFinished = async (proceed: boolean): Promise => { if (proceed) { this.setState({isRedacting: true}); try { @@ -60,7 +70,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent { } }; - render() { + public render() { if (this.state.isRedacting) { if (this.state.redactionErrorCode) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); diff --git a/src/components/views/dialogs/ConfirmRedactDialog.js b/src/components/views/dialogs/ConfirmRedactDialog.tsx similarity index 95% rename from src/components/views/dialogs/ConfirmRedactDialog.js rename to src/components/views/dialogs/ConfirmRedactDialog.tsx index bd63d3acc1..eee05599e8 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.js +++ b/src/components/views/dialogs/ConfirmRedactDialog.tsx @@ -19,11 +19,15 @@ import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +interface IProps { + onFinished: (success: boolean) => void; +} + /* * A dialog for confirming a redaction. */ @replaceableComponent("views.dialogs.ConfirmRedactDialog") -export default class ConfirmRedactDialog extends React.Component { +export default class ConfirmRedactDialog extends React.Component { render() { const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog'); return ( diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.tsx similarity index 75% rename from src/components/views/dialogs/ConfirmUserActionDialog.js rename to src/components/views/dialogs/ConfirmUserActionDialog.tsx index 8059b9172a..05f8c63ace 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.tsx @@ -15,13 +15,31 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk/src/client'; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { GroupMemberType } from '../../../groups'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {mediaFromMxc} from "../../../customisations/Media"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { mediaFromMxc } from "../../../customisations/Media"; + +interface IProps { + // matrix-js-sdk (room) member object. Supply either this or 'groupMember' + member: RoomMember; + // group member object. Supply either this or 'member' + groupMember: GroupMemberType; + // needed if a group member is specified + matrixClient?: MatrixClient, + action: string; // eg. 'Ban' + title: string; // eg. 'Ban this user?' + + // Whether to display a text field for a reason + // If true, the second argument to onFinished will + // be the string entered. + askReason?: boolean; + danger?: boolean; + onFinished: (success: boolean, reason?: HTMLInputElement) => void; +} /* * A dialog for confirming an operation on another user. @@ -32,53 +50,27 @@ import {mediaFromMxc} from "../../../customisations/Media"; * Also tweaks the style for 'dangerous' actions (albeit only with colour) */ @replaceableComponent("views.dialogs.ConfirmUserActionDialog") -export default class ConfirmUserActionDialog extends React.Component { - static propTypes = { - // matrix-js-sdk (room) member object. Supply either this or 'groupMember' - member: PropTypes.object, - // group member object. Supply either this or 'member' - groupMember: GroupMemberType, - // needed if a group member is specified - matrixClient: PropTypes.instanceOf(MatrixClient), - action: PropTypes.string.isRequired, // eg. 'Ban' - title: PropTypes.string.isRequired, // eg. 'Ban this user?' - - // Whether to display a text field for a reason - // If true, the second argument to onFinished will - // be the string entered. - askReason: PropTypes.bool, - danger: PropTypes.bool, - onFinished: PropTypes.func.isRequired, - }; +export default class ConfirmUserActionDialog extends React.Component { + private reasonField: React.RefObject = React.createRef(); static defaultProps = { danger: false, askReason: false, }; - constructor(props) { - super(props); - - this._reasonField = null; - } - - onOk = () => { + public onOk = (): void => { let reason; - if (this._reasonField) { - reason = this._reasonField.value; + if (this.reasonField) { + reason = this.reasonField.current; } this.props.onFinished(true, reason); }; - onCancel = () => { + public onCancel = (): void => { this.props.onFinished(false); }; - _collectReasonField = e => { - this._reasonField = e; - }; - - render() { + public render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); @@ -92,7 +84,7 @@ export default class ConfirmUserActionDialog extends React.Component {
    diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx similarity index 88% rename from src/components/views/dialogs/ConfirmWipeDeviceDialog.js rename to src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx index 333e1522f1..d95b1fe358 100644 --- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js +++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx @@ -15,22 +15,21 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import {_t} from "../../../languageHandler"; import * as sdk from "../../../index"; import {replaceableComponent} from "../../../utils/replaceableComponent"; -@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog") -export default class ConfirmWipeDeviceDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; +interface IProps { + onFinished: (success: boolean) => void; +} - _onConfirm = () => { +@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog") +export default class ConfirmWipeDeviceDialog extends React.Component { + private onConfirm = (): void => { this.props.onFinished(true); }; - _onDecline = () => { + private onDecline = (): void => { this.props.onFinished(false); }; @@ -55,10 +54,10 @@ export default class ConfirmWipeDeviceDialog extends React.Component {
    ); diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.tsx similarity index 82% rename from src/components/views/dialogs/CreateGroupDialog.js rename to src/components/views/dialogs/CreateGroupDialog.tsx index e6c7a67aca..60e4f5efb8 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.tsx @@ -15,44 +15,51 @@ limitations under the License. */ 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 {replaceableComponent} from "../../../utils/replaceableComponent"; -@replaceableComponent("views.dialogs.CreateGroupDialog") -export default class CreateGroupDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; +interface IProps { + onFinished: (success: boolean) => void; +} - state = { +interface IState { + groupName: string; + groupId: string; + groupIdError: string; + creating: boolean; + createError: Error; +} + +@replaceableComponent("views.dialogs.CreateGroupDialog") +export default class CreateGroupDialog extends React.Component { + public state = { groupName: '', groupId: '', - groupError: null, + groupIdError: '', creating: false, createError: null, }; - _onGroupNameChange = e => { + private onGroupNameChange = (e: React.FormEvent): void => { this.setState({ - groupName: e.target.value, + groupName: e.currentTarget.value, }); }; - _onGroupIdChange = e => { + private onGroupIdChange = (e: React.FormEvent): void => { this.setState({ - groupId: e.target.value, + groupId: e.currentTarget.value, }); }; - _onGroupIdBlur = e => { - this._checkGroupId(); + private onGroupIdBlur = (): void => { + this.checkGroupId(); }; - _checkGroupId(e) { + private checkGroupId() { let error = null; if (!this.state.groupId) { error = _t("Community IDs cannot be empty."); @@ -67,12 +74,12 @@ export default class CreateGroupDialog extends React.Component { return error; } - _onFormSubmit = e => { + private onFormSubmit = (e: React.FormEvent) => { e.preventDefault(); - if (this._checkGroupId()) return; + if (this.checkGroupId()) return; - const profile = {}; + const profile: any = {}; if (this.state.groupName !== '') { profile.name = this.state.groupName; } @@ -121,7 +128,7 @@ export default class CreateGroupDialog extends React.Component { - +
    @@ -129,9 +136,9 @@ export default class CreateGroupDialog extends React.Component {
    @@ -144,10 +151,10 @@ export default class CreateGroupDialog extends React.Component { + diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.js b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx similarity index 94% rename from src/components/views/dialogs/CryptoStoreTooNewDialog.js rename to src/components/views/dialogs/CryptoStoreTooNewDialog.tsx index 6336c635e4..2bdf732bc5 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.js +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx @@ -22,7 +22,11 @@ import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; -export default (props) => { +interface IProps { + onFinished: (success: boolean) => void; +} + +export default (props: IProps) => { const brand = SdkConfig.get().brand; const _onLogoutClicked = () => { @@ -40,7 +44,7 @@ export default (props) => { onFinished: (doLogout) => { if (doLogout) { dis.dispatch({action: 'logout'}); - props.onFinished(); + props.onFinished(true); } }, }); diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.tsx similarity index 87% rename from src/components/views/dialogs/DeactivateAccountDialog.js rename to src/components/views/dialogs/DeactivateAccountDialog.tsx index 4e52549d51..cf88802340 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import Analytics from '../../../Analytics'; @@ -28,8 +27,25 @@ import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/Interactiv import StyledCheckbox from "../elements/StyledCheckbox"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +interface IProps { + onFinished: (success: boolean) => void; +} + +interface IState { + shouldErase: boolean; + errStr: string; + authData: any; // for UIA + authEnabled: boolean; // see usages for information + + // A few strings that are passed to InteractiveAuth for design or are displayed + // next to the InteractiveAuth component. + bodyText: string; + continueText: string; + continueKind: string; +} + @replaceableComponent("views.dialogs.DeactivateAccountDialog") -export default class DeactivateAccountDialog extends React.Component { +export default class DeactivateAccountDialog extends React.Component { constructor(props) { super(props); @@ -46,10 +62,10 @@ export default class DeactivateAccountDialog extends React.Component { continueKind: null, }; - this._initAuth(/* shouldErase= */false); + this.initAuth(/* shouldErase= */false); } - _onStagePhaseChange = (stage, phase) => { + private onStagePhaseChange = (stage: string, phase: string): void => { const dialogAesthetics = { [SSOAuthEntry.PHASE_PREAUTH]: { body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."), @@ -87,19 +103,19 @@ export default class DeactivateAccountDialog extends React.Component { this.setState({bodyText, continueText, continueKind}); }; - _onUIAuthFinished = (success, result, extra) => { + private onUIAuthFinished = (success: boolean, result: Error) => { if (success) return; // great! makeRequest() will be called too. if (result === ERROR_USER_CANCELLED) { - this._onCancel(); + this.onCancel(); return; } - console.error("Error during UI Auth:", {result, extra}); + console.error("Error during UI Auth:", { result }); this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")}); }; - _onUIAuthComplete = (auth) => { + private onUIAuthComplete = (auth: any): void => { MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => { // Deactivation worked - logout & close this dialog Analytics.trackEvent('Account', 'Deactivate Account'); @@ -111,9 +127,9 @@ export default class DeactivateAccountDialog extends React.Component { }); }; - _onEraseFieldChange = (ev) => { + private onEraseFieldChange = (ev: React.FormEvent): void => { this.setState({ - shouldErase: ev.target.checked, + shouldErase: ev.currentTarget.checked, // Disable the auth form because we're going to have to reinitialize the auth // information. We do this because we can't modify the parameters in the UIA @@ -123,14 +139,14 @@ export default class DeactivateAccountDialog extends React.Component { }); // As mentioned above, set up for auth again to get updated UIA session info - this._initAuth(/* shouldErase= */ev.target.checked); + this.initAuth(/* shouldErase= */ev.currentTarget.checked); }; - _onCancel() { + private onCancel(): void { this.props.onFinished(false); } - _initAuth(shouldErase) { + private initAuth(shouldErase: boolean): void { MatrixClientPeg.get().deactivateAccount(null, shouldErase).then(r => { // If we got here, oops. The server didn't require any auth. // Our application lifecycle will catch the error and do the logout bits. @@ -148,7 +164,7 @@ export default class DeactivateAccountDialog extends React.Component { }); } - render() { + public render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); let error = null; @@ -166,9 +182,9 @@ export default class DeactivateAccountDialog extends React.Component { @@ -214,7 +230,7 @@ export default class DeactivateAccountDialog extends React.Component {

    {_t( "Please forget all messages I have sent when my account is deactivated " + @@ -235,7 +251,3 @@ export default class DeactivateAccountDialog extends React.Component { ); } } - -DeactivateAccountDialog.propTypes = { - onFinished: PropTypes.func.isRequired, -}; diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index fdbf6a36fc..2690eb67d7 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -525,11 +525,11 @@ class RoomStateExplorer extends React.PureComponent { diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.tsx similarity index 81% rename from src/components/views/dialogs/ErrorDialog.js rename to src/components/views/dialogs/ErrorDialog.tsx index 5197c68b5a..d50ec7bf36 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.tsx @@ -26,37 +26,37 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {replaceableComponent} from "../../../utils/replaceableComponent"; -@replaceableComponent("views.dialogs.ErrorDialog") -export default class ErrorDialog extends React.Component { - static propTypes = { - title: PropTypes.string, - description: PropTypes.oneOfType([ - PropTypes.element, - PropTypes.string, - ]), - button: PropTypes.string, - focus: PropTypes.bool, - onFinished: PropTypes.func.isRequired, - headerImage: PropTypes.string, - }; +interface IProps { + onFinished: (success: boolean) => void; + title?: string; + description?: React.ReactNode; + button?: string; + focus?: boolean; + headerImage?: string; +} - static defaultProps = { +interface IState { + onFinished: (success: boolean) => void; +} + +@replaceableComponent("views.dialogs.ErrorDialog") +export default class ErrorDialog extends React.Component { + public static defaultProps = { focus: true, title: null, description: null, button: null, }; - onClick = () => { + private onClick = () => { this.props.onFinished(true); }; - render() { + public render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( = ({ matrixClient: cli, event, permalinkCr }); mockEvent.sender = { name: profileInfo.displayname || userId, + rawDisplayName: profileInfo.displayname, userId, getAvatarUrl: (..._) => { return avatarUrlForUser( diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 778744b783..ffca9a88a7 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -153,8 +153,8 @@ class ThreepidMember extends Member { } interface IDMUserTileProps { - member: RoomMember; - onRemove(member: RoomMember): void; + member: Member; + onRemove(member: Member): void; } class DMUserTile extends React.PureComponent { @@ -168,7 +168,7 @@ class DMUserTile extends React.PureComponent { render() { const avatarSize = 20; - const avatar = this.props.member.isEmail + const avatar = (this.props.member as ThreepidMember).isEmail ? { } interface IDMRoomTileProps { - member: RoomMember; + member: Member; lastActiveTs: number; - onToggle(member: RoomMember): void; + onToggle(member: Member): void; highlightWord: string; isSelected: boolean; } @@ -270,7 +270,7 @@ class DMRoomTile extends React.PureComponent { } const avatarSize = 36; - const avatar = this.props.member.isEmail + const avatar = (this.props.member as ThreepidMember).isEmail ? @@ -298,7 +298,7 @@ class DMRoomTile extends React.PureComponent { ); - const caption = this.props.member.isEmail + const caption = (this.props.member as ThreepidMember).isEmail ? _t("Invite by email") : this.highlightName(this.props.member.userId); @@ -334,7 +334,7 @@ interface IInviteDialogProps { } interface IInviteDialogState { - targets: RoomMember[]; // array of Member objects (see interface above) + targets: Member[]; // array of Member objects (see interface above) filterText: string; recents: { user: Member, userId: string }[]; numRecentsShown: number; diff --git a/src/components/views/dialogs/TermsDialog.js b/src/components/views/dialogs/TermsDialog.tsx similarity index 72% rename from src/components/views/dialogs/TermsDialog.js rename to src/components/views/dialogs/TermsDialog.tsx index e8625ec6cb..ace5316323 100644 --- a/src/components/views/dialogs/TermsDialog.js +++ b/src/components/views/dialogs/TermsDialog.tsx @@ -16,22 +16,21 @@ limitations under the License. import url from 'url'; import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t, pickBestLanguage } from '../../../languageHandler'; import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types"; +import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types"; -class TermsCheckbox extends React.PureComponent { - static propTypes = { - onChange: PropTypes.func.isRequired, - url: PropTypes.string.isRequired, - checked: PropTypes.bool.isRequired, - } +interface ITermsCheckboxProps { + onChange: (url: string, checked: boolean) => void; + url: string; + checked: boolean; +} - onChange = (ev) => { - this.props.onChange(this.props.url, ev.target.checked); +class TermsCheckbox extends React.PureComponent { + private onChange = (ev: React.FormEvent): void => { + this.props.onChange(this.props.url, ev.currentTarget.checked); } render() { @@ -42,30 +41,34 @@ class TermsCheckbox extends React.PureComponent { } } +interface ITermsDialogProps { + /** + * Array of [Service, policies] pairs, where policies is the response from the + * /terms endpoint for that service + */ + policiesAndServicePairs: any[], + + /** + * urls that the user has already agreed to + */ + agreedUrls?: string[], + + /** + * Called with: + * * success {bool} True if the user accepted any douments, false if cancelled + * * agreedUrls {string[]} List of agreed URLs + */ + onFinished: (success: boolean, agreedUrls?: string[]) => void, +} + +interface IState { + agreedUrls: any; +} + @replaceableComponent("views.dialogs.TermsDialog") -export default class TermsDialog extends React.PureComponent { - static propTypes = { - /** - * Array of [Service, policies] pairs, where policies is the response from the - * /terms endpoint for that service - */ - policiesAndServicePairs: PropTypes.array.isRequired, - - /** - * urls that the user has already agreed to - */ - agreedUrls: PropTypes.arrayOf(PropTypes.string), - - /** - * Called with: - * * success {bool} True if the user accepted any douments, false if cancelled - * * agreedUrls {string[]} List of agreed URLs - */ - onFinished: PropTypes.func.isRequired, - } - +export default class TermsDialog extends React.PureComponent { constructor(props) { - super(); + super(props); this.state = { // url -> boolean agreedUrls: {}, @@ -75,15 +78,15 @@ export default class TermsDialog extends React.PureComponent { } } - _onCancelClick = () => { + private onCancelClick = (): void => { this.props.onFinished(false); } - _onNextClick = () => { + private onNextClick = (): void => { this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url])); } - _nameForServiceType(serviceType, host) { + private nameForServiceType(serviceType: SERVICE_TYPES, host: string): JSX.Element { switch (serviceType) { case SERVICE_TYPES.IS: return

    {_t("Identity Server")}
    ({host})
    ; @@ -92,7 +95,7 @@ export default class TermsDialog extends React.PureComponent { } } - _summaryForServiceType(serviceType) { + private summaryForServiceType(serviceType: SERVICE_TYPES): JSX.Element { switch (serviceType) { case SERVICE_TYPES.IS: return
    @@ -107,13 +110,13 @@ export default class TermsDialog extends React.PureComponent { } } - _onTermsCheckboxChange = (url, checked) => { + private onTermsCheckboxChange = (url: string, checked: boolean) => { this.setState({ agreedUrls: Object.assign({}, this.state.agreedUrls, { [url]: checked }), }); } - render() { + public render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -128,8 +131,8 @@ export default class TermsDialog extends React.PureComponent { let serviceName; let summary; if (i === 0) { - serviceName = this._nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host); - summary = this._summaryForServiceType( + serviceName = this.nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host); + summary = this.summaryForServiceType( policiesAndService.service.serviceType, ); } @@ -137,12 +140,15 @@ export default class TermsDialog extends React.PureComponent { rows.push( {serviceName} {summary} - {termDoc[termsLang].name} - - + + {termDoc[termsLang].name} + + + + ); @@ -176,7 +182,7 @@ export default class TermsDialog extends React.PureComponent { return ( diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.tsx similarity index 75% rename from src/components/views/dialogs/UserSettingsDialog.js rename to src/components/views/dialogs/UserSettingsDialog.tsx index fe29b85aea..1a62a4ff22 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -16,11 +16,10 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import TabbedView, {Tab} from "../../structures/TabbedView"; import {_t, _td} from "../../../languageHandler"; import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab"; -import SettingsStore from "../../../settings/SettingsStore"; +import SettingsStore, { CallbackFn } from "../../../settings/SettingsStore"; import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab"; import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab"; import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab"; @@ -35,41 +34,49 @@ import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab import {UIFeature} from "../../../settings/UIFeature"; import {replaceableComponent} from "../../../utils/replaceableComponent"; -export const USER_GENERAL_TAB = "USER_GENERAL_TAB"; -export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB"; -export const USER_FLAIR_TAB = "USER_FLAIR_TAB"; -export const USER_NOTIFICATIONS_TAB = "USER_NOTIFICATIONS_TAB"; -export const USER_PREFERENCES_TAB = "USER_PREFERENCES_TAB"; -export const USER_VOICE_TAB = "USER_VOICE_TAB"; -export const USER_SECURITY_TAB = "USER_SECURITY_TAB"; -export const USER_LABS_TAB = "USER_LABS_TAB"; -export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB"; -export const USER_HELP_TAB = "USER_HELP_TAB"; +export enum UserTab { + General = "USER_GENERAL_TAB", + Appearance = "USER_APPEARANCE_TAB", + Flair = "USER_FLAIR_TAB", + Notifications = "USER_NOTIFICATIONS_TAB", + Preferences = "USER_PREFERENCES_TAB", + Voice = "USER_VOICE_TAB", + Security = "USER_SECURITY_TAB", + Labs = "USER_LABS_TAB", + Mjolnir = "USER_MJOLNIR_TAB", + Help = "USER_HELP_TAB", +} + +interface IProps { + onFinished: (success: boolean) => void; + initialTabId?: string; +} + +interface IState { + mjolnirEnabled: boolean; +} @replaceableComponent("views.dialogs.UserSettingsDialog") -export default class UserSettingsDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - initialTabId: PropTypes.string, - }; +export default class UserSettingsDialog extends React.Component { + private mjolnirWatcher: string; - constructor() { - super(); + constructor(props) { + super(props); this.state = { mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"), }; } - componentDidMount(): void { - this._mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this._mjolnirChanged.bind(this)); + public componentDidMount(): void { + this.mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged); } - componentWillUnmount(): void { - SettingsStore.unwatchSetting(this._mjolnirWatcher); + public componentWillUnmount(): void { + SettingsStore.unwatchSetting(this.mjolnirWatcher); } - _mjolnirChanged(settingName, roomId, atLevel, newValue) { + private mjolnirChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => { // We can cheat because we know what levels a feature is tracked at, and how it is tracked this.setState({mjolnirEnabled: newValue}); } @@ -78,33 +85,33 @@ export default class UserSettingsDialog extends React.Component { const tabs = []; tabs.push(new Tab( - USER_GENERAL_TAB, + UserTab.General, _td("General"), "mx_UserSettingsDialog_settingsIcon", , )); tabs.push(new Tab( - USER_APPEARANCE_TAB, + UserTab.Appearance, _td("Appearance"), "mx_UserSettingsDialog_appearanceIcon", , )); if (SettingsStore.getValue(UIFeature.Flair)) { tabs.push(new Tab( - USER_FLAIR_TAB, + UserTab.Flair, _td("Flair"), "mx_UserSettingsDialog_flairIcon", , )); } tabs.push(new Tab( - USER_NOTIFICATIONS_TAB, + UserTab.Notifications, _td("Notifications"), "mx_UserSettingsDialog_bellIcon", , )); tabs.push(new Tab( - USER_PREFERENCES_TAB, + UserTab.Preferences, _td("Preferences"), "mx_UserSettingsDialog_preferencesIcon", , @@ -112,7 +119,7 @@ export default class UserSettingsDialog extends React.Component { if (SettingsStore.getValue(UIFeature.Voip)) { tabs.push(new Tab( - USER_VOICE_TAB, + UserTab.Voice, _td("Voice & Video"), "mx_UserSettingsDialog_voiceIcon", , @@ -120,7 +127,7 @@ export default class UserSettingsDialog extends React.Component { } tabs.push(new Tab( - USER_SECURITY_TAB, + UserTab.Security, _td("Security & Privacy"), "mx_UserSettingsDialog_securityIcon", , @@ -130,7 +137,7 @@ export default class UserSettingsDialog extends React.Component { || SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k)) ) { tabs.push(new Tab( - USER_LABS_TAB, + UserTab.Labs, _td("Labs"), "mx_UserSettingsDialog_labsIcon", , @@ -138,17 +145,17 @@ export default class UserSettingsDialog extends React.Component { } if (this.state.mjolnirEnabled) { tabs.push(new Tab( - USER_MJOLNIR_TAB, + UserTab.Mjolnir, _td("Ignored users"), "mx_UserSettingsDialog_mjolnirIcon", , )); } tabs.push(new Tab( - USER_HELP_TAB, + UserTab.Help, _td("Help & About"), "mx_UserSettingsDialog_helpIcon", - , + this.props.onFinished(true)} />, )); return tabs; diff --git a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx similarity index 83% rename from src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js rename to src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx index e71983b074..6272302a76 100644 --- a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js +++ b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx @@ -15,22 +15,21 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import {_t} from "../../../../languageHandler"; +import { _t } from "../../../../languageHandler"; import * as sdk from "../../../../index"; -import {replaceableComponent} from "../../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../../utils/replaceableComponent"; + +interface IProps { + onFinished: (success: boolean) => void; +} @replaceableComponent("views.dialogs.security.ConfirmDestroyCrossSigningDialog") -export default class ConfirmDestroyCrossSigningDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; - - _onConfirm = () => { +export default class ConfirmDestroyCrossSigningDialog extends React.Component { + private onConfirm = (): void => { this.props.onFinished(true); }; - _onDecline = () => { + private onDecline = (): void => { this.props.onFinished(false); }; @@ -57,10 +56,10 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component {
    ); diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.js b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx similarity index 85% rename from src/components/views/dialogs/security/CreateCrossSigningDialog.js rename to src/components/views/dialogs/security/CreateCrossSigningDialog.tsx index fedcc02f89..840390f6fb 100644 --- a/src/components/views/dialogs/security/CreateCrossSigningDialog.js +++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { MatrixClientPeg } from '../../../../MatrixClientPeg'; import { _t } from '../../../../languageHandler'; import Modal from '../../../../Modal'; @@ -25,7 +24,19 @@ import DialogButtons from '../../elements/DialogButtons'; import BaseDialog from '../BaseDialog'; import Spinner from '../../elements/Spinner'; import InteractiveAuthDialog from '../InteractiveAuthDialog'; -import {replaceableComponent} from "../../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../../utils/replaceableComponent"; + +interface IProps { + accountPassword?: string; + tokenLogin?: boolean; + onFinished?: (success: boolean) => void; +} + +interface IState { + error: Error | null; + canUploadKeysWithPasswordOnly?: boolean; + accountPassword: string; +} /* * Walks the user through the process of creating a cross-signing keys. In most @@ -33,39 +44,32 @@ import {replaceableComponent} from "../../../../utils/replaceableComponent"; * may need to complete some steps to proceed. */ @replaceableComponent("views.dialogs.security.CreateCrossSigningDialog") -export default class CreateCrossSigningDialog extends React.PureComponent { - static propTypes = { - accountPassword: PropTypes.string, - tokenLogin: PropTypes.bool, - }; - - constructor(props) { +export default class CreateCrossSigningDialog extends React.PureComponent { + constructor(props: IProps) { super(props); this.state = { error: null, // Does the server offer a UI auth flow with just m.login.password // for /keys/device_signing/upload? - canUploadKeysWithPasswordOnly: null, - accountPassword: props.accountPassword || "", - }; - - if (this.state.accountPassword) { // If we have an account password in memory, let's simplify and // assume it means password auth is also supported for device // signing key upload as well. This avoids hitting the server to // test auth flows, which may be slow under high load. - this.state.canUploadKeysWithPasswordOnly = true; - } else { - this._queryKeyUploadAuth(); + canUploadKeysWithPasswordOnly: props.accountPassword ? true : null, + accountPassword: props.accountPassword || "", + }; + + if (!this.state.accountPassword) { + this.queryKeyUploadAuth(); } } - componentDidMount() { - this._bootstrapCrossSigning(); + public componentDidMount(): void { + this.bootstrapCrossSigning(); } - async _queryKeyUploadAuth() { + private async queryKeyUploadAuth(): Promise { try { await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {}); // We should never get here: the server should always require @@ -86,7 +90,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent { } } - _doBootstrapUIAuth = async (makeRequest) => { + private doBootstrapUIAuth = async (makeRequest: (authData: any) => void): Promise => { if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { await makeRequest({ type: 'm.login.password', @@ -137,7 +141,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent { } } - _bootstrapCrossSigning = async () => { + private bootstrapCrossSigning = async (): Promise => { this.setState({ error: null, }); @@ -146,13 +150,13 @@ export default class CreateCrossSigningDialog extends React.PureComponent { try { await cli.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: this._doBootstrapUIAuth, + authUploadDeviceSigningKeys: this.doBootstrapUIAuth, }); this.props.onFinished(true); } catch (e) { if (this.props.tokenLogin) { // ignore any failures, we are relying on grace period here - this.props.onFinished(); + this.props.onFinished(false); return; } @@ -161,7 +165,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent { } } - _onCancel = () => { + private onCancel = (): void => { this.props.onFinished(false); } @@ -172,8 +176,8 @@ export default class CreateCrossSigningDialog extends React.PureComponent {

    {_t("Unable to set up keys")}

    ; diff --git a/src/components/views/dialogs/security/SetupEncryptionDialog.js b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx similarity index 72% rename from src/components/views/dialogs/security/SetupEncryptionDialog.js rename to src/components/views/dialogs/security/SetupEncryptionDialog.tsx index 3c15ea9f1d..19c7af01ff 100644 --- a/src/components/views/dialogs/security/SetupEncryptionDialog.js +++ b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx @@ -15,47 +15,52 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import SetupEncryptionBody from '../../../structures/auth/SetupEncryptionBody'; import BaseDialog from '../BaseDialog'; import { _t } from '../../../../languageHandler'; -import { SetupEncryptionStore, PHASE_DONE } from '../../../../stores/SetupEncryptionStore'; +import { SetupEncryptionStore, Phase } from '../../../../stores/SetupEncryptionStore'; import {replaceableComponent} from "../../../../utils/replaceableComponent"; -function iconFromPhase(phase) { - if (phase === PHASE_DONE) { +function iconFromPhase(phase: Phase) { + if (phase === Phase.Done) { return require("../../../../../res/img/e2e/verified.svg"); } else { return require("../../../../../res/img/e2e/warning.svg"); } } -@replaceableComponent("views.dialogs.security.SetupEncryptionDialog") -export default class SetupEncryptionDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; +interface IProps { + onFinished: (success: boolean) => void; +} - constructor() { - super(); +interface IState { + icon: Phase; +} + +@replaceableComponent("views.dialogs.security.SetupEncryptionDialog") +export default class SetupEncryptionDialog extends React.Component { + private store: SetupEncryptionStore; + + constructor(props: IProps) { + super(props); this.store = SetupEncryptionStore.sharedInstance(); this.state = {icon: iconFromPhase(this.store.phase)}; } - componentDidMount() { - this.store.on("update", this._onStoreUpdate); + public componentDidMount() { + this.store.on("update", this.onStoreUpdate); } - componentWillUnmount() { - this.store.removeListener("update", this._onStoreUpdate); + public componentWillUnmount() { + this.store.removeListener("update", this.onStoreUpdate); } - _onStoreUpdate = () => { + private onStoreUpdate = (): void => { this.setState({icon: iconFromPhase(this.store.phase)}); }; - render() { + public render() { return { return res; } - private static getTransitionSequence(events: MatrixEvent[]) { + private static getTransitionSequence(events: IUserEvents[]) { return events.map(MemberEventListSummary.getTransition); } @@ -315,7 +315,7 @@ export default class MemberEventListSummary extends React.Component { * @returns {string?} the transition type given to this event. This defaults to `null` * if a transition is not recognised. */ - private static getTransition(e: MatrixEvent): TransitionType { + private static getTransition(e: IUserEvents): TransitionType { if (e.mxEvent.getType() === 'm.room.third_party_invite') { // Handle 3pid invites the same as invites so they get bundled together if (!isValid3pidInvite(e.mxEvent)) { diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 81ed360b17..ebf4a18aa9 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -297,6 +297,7 @@ export default class ReplyThread extends React.Component { } async getEvent(eventId) { + if (!eventId) return null; const event = this.room.findEventById(eventId); if (event) return event; @@ -392,6 +393,7 @@ export default class ReplyThread extends React.Component { alwaysShowTimestamps={this.props.alwaysShowTimestamps} enableFlair={SettingsStore.getValue(UIFeature.Flair)} replacingEventId={ev.replacingEventId()} + as="div" /> ; }); diff --git a/src/components/views/messages/MVoiceMessageBody.tsx b/src/components/views/messages/MVoiceMessageBody.tsx index d65de7697a..a7e3b1cd86 100644 --- a/src/components/views/messages/MVoiceMessageBody.tsx +++ b/src/components/views/messages/MVoiceMessageBody.tsx @@ -24,6 +24,7 @@ import {_t} from "../../../languageHandler"; import {mediaFromContent} from "../../../customisations/Media"; import {decryptFile} from "../../../utils/DecryptFile"; import RecordingPlayback from "../voice_messages/RecordingPlayback"; +import {IMediaEventContent} from "../../../customisations/models/IMediaEventContent"; interface IProps { mxEvent: MatrixEvent; @@ -45,7 +46,7 @@ export default class MVoiceMessageBody extends React.PureComponent { public render() { - const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice']; + // MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245 + const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice'] + || !!this.props.mxEvent.getContent()['org.matrix.msc3245.voice']; const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages"); if (isVoiceMessage && voiceMessagesEnabled) { return ; diff --git a/src/components/views/messages/MessageTimestamp.js b/src/components/views/messages/MessageTimestamp.tsx similarity index 67% rename from src/components/views/messages/MessageTimestamp.js rename to src/components/views/messages/MessageTimestamp.tsx index a7f350adcd..8b02f6b38e 100644 --- a/src/components/views/messages/MessageTimestamp.js +++ b/src/components/views/messages/MessageTimestamp.tsx @@ -16,20 +16,19 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import {formatFullDate, formatTime, formatFullTime} from '../../../DateUtils'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { formatFullDate, formatTime, formatFullTime } from '../../../DateUtils'; +import { replaceableComponent } from "../../../utils/replaceableComponent"; + +interface IProps { + ts: number; + showTwelveHour?: boolean; + showFullDate?: boolean; + showSeconds?: boolean; +} @replaceableComponent("views.messages.MessageTimestamp") -export default class MessageTimestamp extends React.Component { - static propTypes = { - ts: PropTypes.number.isRequired, - showTwelveHour: PropTypes.bool, - showFullDate: PropTypes.bool, - showSeconds: PropTypes.bool, - }; - - render() { +export default class MessageTimestamp extends React.Component { + public render() { const date = new Date(this.props.ts); let timestamp; if (this.props.showFullDate) { @@ -41,7 +40,11 @@ export default class MessageTimestamp extends React.Component { } return ( - + {timestamp} ); diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx index 805f842fbc..883b2bd8a7 100644 --- a/src/components/views/messages/SenderProfile.tsx +++ b/src/components/views/messages/SenderProfile.tsx @@ -17,10 +17,10 @@ import React from 'react'; import Flair from '../elements/Flair.js'; import FlairStore from '../../../stores/FlairStore'; -import {getUserNameColorClass} from '../../../utils/FormattingUtils'; +import { getUserNameColorClass } from '../../../utils/FormattingUtils'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import MatrixEvent from "matrix-js-sdk/src/models/event"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; interface IProps { mxEvent: MatrixEvent; diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 3adfea6ee6..ebc4ce7ce8 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -16,12 +16,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from 'react'; +import React, { createRef } from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import highlight from 'highlight.js'; import * as HtmlUtils from '../../../HtmlUtils'; -import {formatDate} from '../../../DateUtils'; +import { formatDate } from '../../../DateUtils'; import * as sdk from '../../../index'; import Modal from '../../../Modal'; import dis from '../../../dispatcher/dispatcher'; @@ -29,14 +29,16 @@ import { _t } from '../../../languageHandler'; import * as ContextMenu from '../../structures/ContextMenu'; import SettingsStore from "../../../settings/SettingsStore"; import ReplyThread from "../elements/ReplyThread"; -import {pillifyLinks, unmountPills} from '../../../utils/pillify'; -import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; -import {isPermalinkHost} from "../../../utils/permalinks/Permalinks"; -import {toRightOf} from "../../structures/ContextMenu"; -import {copyPlaintext} from "../../../utils/strings"; +import { pillifyLinks, unmountPills } from '../../../utils/pillify'; +import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; +import { isPermalinkHost } from "../../../utils/permalinks/Permalinks"; +import { toRightOf } from "../../structures/ContextMenu"; +import { copyPlaintext } from "../../../utils/strings"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import UIStore from "../../../stores/UIStore"; +import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; +import { Action } from "../../../dispatcher/actions"; @replaceableComponent("views.messages.TextualBody") export default class TextualBody extends React.Component { @@ -390,9 +392,9 @@ export default class TextualBody extends React.Component { onEmoteSenderClick = event => { const mxEvent = this.props.mxEvent; - dis.dispatch({ - action: 'insert_mention', - user_id: mxEvent.getSender(), + dis.dispatch({ + action: Action.ComposerInsert, + userId: mxEvent.getSender(), }); }; diff --git a/src/components/views/right_panel/EncryptionInfo.tsx b/src/components/views/right_panel/EncryptionInfo.tsx index aa51965ac6..db59a88967 100644 --- a/src/components/views/right_panel/EncryptionInfo.tsx +++ b/src/components/views/right_panel/EncryptionInfo.tsx @@ -17,8 +17,9 @@ limitations under the License. import React from "react"; import * as sdk from "../../../index"; -import {_t} from "../../../languageHandler"; -import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import { _t } from "../../../languageHandler"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { User } from "matrix-js-sdk/src/models/user"; export const PendingActionSpinner = ({text}) => { const Spinner = sdk.getComponent('elements.Spinner'); @@ -31,7 +32,7 @@ export const PendingActionSpinner = ({text}) => { interface IProps { waitingForOtherParty: boolean; waitingForNetwork: boolean; - member: RoomMember; + member: RoomMember | User; onStartVerification: () => Promise; isRoomEncrypted: boolean; inDialog: boolean; @@ -55,7 +56,7 @@ const EncryptionInfo: React.FC = ({ text = _t("Accept on your other login…"); } else { text = _t("Waiting for %(displayName)s to accept…", { - displayName: member.displayName || member.name || member.userId, + displayName: (member as User).displayName || (member as RoomMember).name || member.userId, }); } } else { diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index d6c97f9cf2..03954bad56 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -38,7 +38,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import RoomViewStore from "../../../stores/RoomViewStore"; import MultiInviter from "../../../utils/MultiInviter"; import GroupStore from "../../../stores/GroupStore"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; import { useEventEmitter } from "../../../hooks/useEventEmitter"; import { textualPowerLevel } from '../../../Roles'; @@ -48,7 +48,7 @@ import EncryptionPanel from "./EncryptionPanel"; import { useAsyncMemo } from '../../../hooks/useAsyncMemo'; import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification'; import { Action } from "../../../dispatcher/actions"; -import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog"; +import { UserTab } from "../dialogs/UserSettingsDialog"; import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; import BaseCard from "./BaseCard"; import { E2EStatus } from "../../../utils/ShieldUtils"; @@ -68,6 +68,7 @@ import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; import { mediaFromMxc } from "../../../customisations/Media"; import UIStore from "../../../stores/UIStore"; +import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; export interface IDevice { deviceId: string; @@ -146,7 +147,7 @@ async function openDMForUser(matrixClient: MatrixClient, userId: string) { type SetUpdating = (updating: boolean) => void; -function useHasCrossSigningKeys(cli: MatrixClient, member: RoomMember, canVerify: boolean, setUpdating: SetUpdating) { +function useHasCrossSigningKeys(cli: MatrixClient, member: User, canVerify: boolean, setUpdating: SetUpdating) { return useAsyncMemo(async () => { if (!canVerify) { return undefined; @@ -368,9 +369,9 @@ const UserOptionsSection: React.FC<{ }; const onInsertPillButton = function() { - dis.dispatch({ - action: 'insert_mention', - user_id: member.userId, + dis.dispatch({ + action: Action.ComposerInsert, + userId: member.userId, }); }; @@ -971,7 +972,7 @@ interface IRoomPermissions { canInvite: boolean; } -function useRoomPermissions(cli: MatrixClient, room: Room, user: User): IRoomPermissions { +function useRoomPermissions(cli: MatrixClient, room: Room, user: RoomMember): IRoomPermissions { const [roomPermissions, setRoomPermissions] = useState({ // modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL modifyLevelMax: -1, @@ -1028,7 +1029,7 @@ function useRoomPermissions(cli: MatrixClient, room: Room, user: User): IRoomPer } const PowerLevelSection: React.FC<{ - user: User; + user: RoomMember; room: Room; roomPermissions: IRoomPermissions; powerLevels: IPowerLevelsContent; @@ -1037,7 +1038,7 @@ const PowerLevelSection: React.FC<{ return (); } else { const powerLevelUsersDefault = powerLevels.users_default || 0; - const powerLevel = parseInt(user.powerLevel, 10); + const powerLevel = user.powerLevel; const role = textualPowerLevel(powerLevel, powerLevelUsersDefault); return (
    @@ -1048,13 +1049,13 @@ const PowerLevelSection: React.FC<{ }; const PowerLevelEditor: React.FC<{ - user: User; + user: RoomMember; room: Room; roomPermissions: IRoomPermissions; }> = ({user, room, roomPermissions}) => { const cli = useContext(MatrixClientContext); - const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10)); + const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel); const onPowerChange = useCallback(async (powerLevelStr: string) => { const powerLevel = parseInt(powerLevelStr, 10); setSelectedPowerLevel(powerLevel); @@ -1231,7 +1232,7 @@ const BasicUserInfo: React.FC<{ setPendingUpdateCount(pendingUpdateCount - 1); }, [pendingUpdateCount]); - const roomPermissions = useRoomPermissions(cli, room, member); + const roomPermissions = useRoomPermissions(cli, room, member as RoomMember); const onSynapseDeactivate = useCallback(async () => { const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, { @@ -1275,12 +1276,26 @@ const BasicUserInfo: React.FC<{ ); } + let memberDetails; let adminToolsContainer; - if (room && member.roomId) { + if (room && (member as RoomMember).roomId) { + // hide the Roles section for DMs as it doesn't make sense there + if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) { + memberDetails =
    +

    { _t("Role") }

    + +
    ; + } + adminToolsContainer = ( @@ -1309,20 +1324,6 @@ const BasicUserInfo: React.FC<{ spinner = ; } - let memberDetails; - // hide the Roles section for DMs as it doesn't make sense there - if (room && member.roomId && !DMRoomMap.shared().getUserIdForRoomId(member.roomId)) { - memberDetails =
    -

    { _t("Role") }

    - -
    ; - } - // only display the devices list if our client supports E2E const cryptoEnabled = cli.isCryptoEnabled(); @@ -1349,8 +1350,7 @@ const BasicUserInfo: React.FC<{ const setUpdating = (updating) => { setPendingUpdateCount(count => count + (updating ? 1 : -1)); }; - const hasCrossSigningKeys = - useHasCrossSigningKeys(cli, member, canVerify, setUpdating ); + const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify, setUpdating); const showDeviceListSpinner = devices === undefined; if (canVerify) { @@ -1359,9 +1359,9 @@ const BasicUserInfo: React.FC<{ verifyButton = ( { if (hasCrossSigningKeys) { - verifyUser(member); + verifyUser(member as User); } else { - legacyVerifyUser(member); + legacyVerifyUser(member as User); } }}> {_t("Verify")} @@ -1381,7 +1381,7 @@ const BasicUserInfo: React.FC<{ { dis.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_SECURITY_TAB, + initialTabId: UserTab.Security, }); }}> { _t("Edit devices") } @@ -1409,7 +1409,7 @@ const BasicUserInfo: React.FC<{ @@ -1428,13 +1428,15 @@ const UserInfoHeader: React.FC<{ const cli = useContext(MatrixClientContext); const onMemberAvatarClick = useCallback(() => { - const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl; + const avatarUrl = (member as RoomMember).getMxcAvatarUrl + ? (member as RoomMember).getMxcAvatarUrl() + : (member as User).avatarUrl; if (!avatarUrl) return; const httpUrl = mediaFromMxc(avatarUrl).srcHttp; const params = { src: httpUrl, - name: member.name, + name: (member as RoomMember).name || (member as User).displayName, }; Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true); @@ -1446,13 +1448,13 @@ const UserInfoHeader: React.FC<{
    + urls={(member as User).avatarUrl ? [(member as User).avatarUrl] : undefined} />
    @@ -1469,7 +1471,11 @@ const UserInfoHeader: React.FC<{ presenceCurrentlyActive = member.user.currentlyActive; if (SettingsStore.getValue("feature_custom_status")) { - statusMessage = member.user._unstable_statusMessage; + if ((member as RoomMember).user) { + statusMessage = member.user.unstable_statusMessage; + } else { + statusMessage = (member as unknown as User).unstable_statusMessage; + } } } @@ -1500,7 +1506,7 @@ const UserInfoHeader: React.FC<{ e2eIcon = ; } - const displayName = member.rawDisplayName || member.displayname; + const displayName = (member as RoomMember).rawDisplayName || (member as GroupMember).displayname; return { avatarElement } diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx index ac01c953b9..edfe0e3483 100644 --- a/src/components/views/right_panel/VerificationPanel.tsx +++ b/src/components/views/right_panel/VerificationPanel.tsx @@ -22,6 +22,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import { User } from "matrix-js-sdk/src/models/user"; import {ReciprocateQRCode} from "matrix-js-sdk/src/crypto/verification/QRCode"; import {SAS} from "matrix-js-sdk/src/crypto/verification/SAS"; @@ -51,7 +52,7 @@ enum VerificationPhase { interface IProps { layout: string; request: VerificationRequest; - member: RoomMember; + member: RoomMember | User; phase: VerificationPhase; onClose: () => void; isRoomEncrypted: boolean; @@ -134,7 +135,7 @@ export default class VerificationPanel extends React.PureComponent

    {_t("Verify by scanning")}

    {_t("Ask %(displayName)s to scan your code:", { - displayName: member.displayName || member.name || member.userId, + displayName: (member as User).displayName || (member as RoomMember).name || member.userId, })}

    @@ -205,7 +206,7 @@ export default class VerificationPanel extends React.PureComponent { // This one is the vertical, ie. change height of apps drawer this.setState({ resizingVertical: resizing }); @@ -141,7 +134,10 @@ export default class AppsDrawer extends React.Component { _getAppsHash = (apps) => apps.map(app => app.id).join("~"); componentDidUpdate(prevProps, prevState) { - if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) { + if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) { + // Room has changed, update apps + this._updateApps(); + } else if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) { this._loadResizerPreferences(); } } diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx index 6d2ae39059..74609cca13 100644 --- a/src/components/views/rooms/AuxPanel.tsx +++ b/src/components/views/rooms/AuxPanel.tsx @@ -15,19 +15,18 @@ limitations under the License. */ import React from 'react'; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { Room } from 'matrix-js-sdk/src/models/room' -import dis from "../../../dispatcher/dispatcher"; import AppsDrawer from './AppsDrawer'; import classNames from 'classnames'; import RateLimitedFunc from '../../../ratelimitedfunc'; import SettingsStore from "../../../settings/SettingsStore"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import {UIFeature} from "../../../settings/UIFeature"; +import { UIFeature } from "../../../settings/UIFeature"; import { ResizeNotifier } from "../../../utils/ResizeNotifier"; import CallViewForRoom from '../voip/CallViewForRoom'; -import {objectHasDiff} from "../../../utils/objects"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { objectHasDiff } from "../../../utils/objects"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { // js-sdk room object @@ -69,19 +68,21 @@ export default class AuxPanel extends React.Component { super(props); this.state = { - counters: this._computeCounters(), + counters: this.computeCounters(), }; } componentDidMount() { const cli = MatrixClientPeg.get(); - cli.on("RoomState.events", this._rateLimitedUpdate); + if (SettingsStore.getValue("feature_state_counters")) { + cli.on("RoomState.events", this.rateLimitedUpdate); + } } componentWillUnmount() { const cli = MatrixClientPeg.get(); - if (cli) { - cli.removeListener("RoomState.events", this._rateLimitedUpdate); + if (cli && SettingsStore.getValue("feature_state_counters")) { + cli.removeListener("RoomState.events", this.rateLimitedUpdate); } } @@ -96,23 +97,11 @@ export default class AuxPanel extends React.Component { } } - onConferenceNotificationClick = (ev, type) => { - dis.dispatch({ - action: 'place_call', - type: type, - room_id: this.props.room.roomId, - }); - ev.stopPropagation(); - ev.preventDefault(); - }; - - _rateLimitedUpdate = new RateLimitedFunc(() => { - if (SettingsStore.getValue("feature_state_counters")) { - this.setState({counters: this._computeCounters()}); - } + private rateLimitedUpdate = new RateLimitedFunc(() => { + this.setState({ counters: this.computeCounters() }); }, 500); - _computeCounters() { + private computeCounters() { const counters = []; if (this.props.room && SettingsStore.getValue("feature_state_counters")) { @@ -225,7 +214,7 @@ export default class AuxPanel extends React.Component { } return ( - + { stateViews } { appsDrawer } { callView } diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index e83f066bd0..981ff1b4ae 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -16,38 +16,39 @@ limitations under the License. */ import classNames from 'classnames'; -import React, {createRef, ClipboardEvent} from 'react'; -import {Room} from 'matrix-js-sdk/src/models/room'; +import React, { createRef, ClipboardEvent } from 'react'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; import EditorModel from '../../../editor/model'; import HistoryManager from '../../../editor/history'; -import {Caret, setSelection} from '../../../editor/caret'; +import { Caret, setSelection } from '../../../editor/caret'; import { formatRangeAsQuote, formatRangeAsCode, toggleInlineFormat, replaceRangeAndMoveCaret, } from '../../../editor/operations'; -import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom'; -import Autocomplete, {generateCompletionDomId} from '../rooms/Autocomplete'; -import {getAutoCompleteCreator} from '../../../editor/parts'; -import {parsePlainTextMessage} from '../../../editor/deserialize'; -import {renderModel} from '../../../editor/render'; +import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom'; +import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete'; +import { getAutoCompleteCreator } from '../../../editor/parts'; +import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize'; +import { renderModel } from '../../../editor/render'; import TypingStore from "../../../stores/TypingStore"; import SettingsStore from "../../../settings/SettingsStore"; -import {Key} from "../../../Keyboard"; -import {EMOTICON_TO_EMOJI} from "../../../emoji"; -import {CommandCategories, CommandMap, parseCommandString} from "../../../SlashCommands"; +import { Key } from "../../../Keyboard"; +import { EMOTICON_TO_EMOJI } from "../../../emoji"; +import { CommandCategories, CommandMap, parseCommandString } from "../../../SlashCommands"; import Range from "../../../editor/range"; import MessageComposerFormatBar from "./MessageComposerFormatBar"; import DocumentOffset from "../../../editor/offset"; -import {IDiff} from "../../../editor/diff"; +import { IDiff } from "../../../editor/diff"; import AutocompleteWrapperModel from "../../../editor/autocomplete"; import DocumentPosition from "../../../editor/position"; -import {ICompletion} from "../../../autocomplete/Autocompleter"; +import { ICompletion } from "../../../autocomplete/Autocompleter"; import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; // matches emoticons which follow the start of a line or whitespace const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); @@ -716,4 +717,48 @@ export default class BasicMessageEditor extends React.Component focus() { this.editorRef.current.focus(); } + + public insertMention(userId: string) { + const {model} = this.props; + const {partCreator} = model; + const member = this.props.room.getMember(userId); + const displayName = member ? + member.rawDisplayName : userId; + const caret = this.getCaret(); + const position = model.positionForOffset(caret.offset, caret.atNodeEnd); + // Insert suffix only if the caret is at the start of the composer + const parts = partCreator.createMentionParts(caret.offset === 0, displayName, userId); + model.transform(() => { + const addedLen = model.insert(parts, position); + return model.positionForOffset(caret.offset + addedLen, true); + }); + // refocus on composer, as we just clicked "Mention" + this.focus(); + } + + public insertQuotedMessage(event: MatrixEvent) { + const {model} = this.props; + const {partCreator} = model; + const quoteParts = parseEvent(event, partCreator, {isQuotedMessage: true}); + // add two newlines + quoteParts.push(partCreator.newline()); + quoteParts.push(partCreator.newline()); + model.transform(() => { + const addedLen = model.insert(quoteParts, model.positionForOffset(0)); + return model.positionForOffset(addedLen, true); + }); + // refocus on composer, as we just clicked "Quote" + this.focus(); + } + + public insertPlaintext(text: string) { + const {model} = this.props; + const {partCreator} = model; + const caret = this.getCaret(); + const position = model.positionForOffset(caret.offset, caret.atNodeEnd); + model.transform(() => { + const addedLen = model.insert([partCreator.plain(text)], position); + return model.positionForOffset(caret.offset + addedLen, true); + }); + } } diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index f0980af7ae..914f08eac7 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -16,25 +16,25 @@ limitations under the License. */ import React from 'react'; import * as sdk from '../../../index'; -import {_t, _td} from '../../../languageHandler'; +import { _t, _td } from '../../../languageHandler'; import PropTypes from 'prop-types'; import dis from '../../../dispatcher/dispatcher'; import EditorModel from '../../../editor/model'; -import {getCaretOffsetAndText} from '../../../editor/dom'; -import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize'; -import {findEditableEvent} from '../../../utils/EventUtils'; -import {parseEvent} from '../../../editor/deserialize'; -import {CommandPartCreator} from '../../../editor/parts'; +import { getCaretOffsetAndText } from '../../../editor/dom'; +import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize'; +import { findEditableEvent } from '../../../utils/EventUtils'; +import { parseEvent } from '../../../editor/deserialize'; +import { CommandPartCreator } from '../../../editor/parts'; import EditorStateTransfer from '../../../utils/EditorStateTransfer'; import classNames from 'classnames'; -import {EventStatus} from 'matrix-js-sdk/src/models/event'; +import { EventStatus } from 'matrix-js-sdk/src/models/event'; import BasicMessageComposer from "./BasicMessageComposer"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {CommandCategories, getCommand} from '../../../SlashCommands'; -import {Action} from "../../../dispatcher/actions"; +import { CommandCategories, getCommand } from '../../../SlashCommands'; +import { Action } from "../../../dispatcher/actions"; import CountlyAnalytics from "../../../CountlyAnalytics"; -import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager'; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import SendHistoryManager from '../../../SendHistoryManager'; import Modal from '../../../Modal'; @@ -124,6 +124,7 @@ export default class EditMessageComposer extends React.Component { }; this._createEditorModel(); window.addEventListener("beforeunload", this._saveStoredEditorState); + this.dispatcherRef = dis.register(this.onAction); } _setEditorRef = ref => { @@ -399,6 +400,7 @@ export default class EditMessageComposer extends React.Component { if (this._shouldSaveStoredEditorState) { this._saveStoredEditorState(); } + dis.unregister(this.dispatcherRef); } _createEditorModel() { @@ -443,6 +445,18 @@ export default class EditMessageComposer extends React.Component { }); }; + onAction = payload => { + if (payload.action === "edit_composer_insert" && this._editorRef) { + if (payload.userId) { + this._editorRef.insertMention(payload.userId); + } else if (payload.event) { + this._editorRef.insertQuotedMessage(payload.event); + } else if (payload.text) { + this._editorRef.insertPlaintext(payload.text); + } + } + }; + render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return (
    diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 85b9cac2c4..0099bf73fb 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -46,6 +46,8 @@ import { EditorStateTransfer } from "../../../utils/EditorStateTransfer"; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; import NotificationBadge from "./NotificationBadge"; +import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload"; +import { Action } from '../../../dispatcher/actions'; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', @@ -376,7 +378,7 @@ export default class EventTile extends React.Component { EventType.RoomMessage, EventType.RoomMessageEncrypted, ]; - if (!simpleSendableEvents.includes(this.props.mxEvent.getType())) return false; + if (!simpleSendableEvents.includes(this.props.mxEvent.getType() as EventType)) return false; // Default case return true; @@ -727,9 +729,9 @@ export default class EventTile extends React.Component { onSenderProfileClick = event => { const mxEvent = this.props.mxEvent; - dis.dispatch({ - action: 'insert_mention', - user_id: mxEvent.getSender(), + dis.dispatch({ + action: Action.ComposerInsert, + userId: mxEvent.getSender(), }); }; diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 3671069903..f7d562fca0 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -16,11 +16,11 @@ limitations under the License. import React from 'react'; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; -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 { 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 dis from '../../../dispatcher/dispatcher'; import { ActionPayload } from "../../../dispatcher/payloads"; import Stickerpicker from './Stickerpicker'; @@ -28,19 +28,21 @@ import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalin import ContentMessages from '../../../ContentMessages'; import E2EIcon from './E2EIcon'; import SettingsStore from "../../../settings/SettingsStore"; -import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu"; +import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import ReplyPreview from "./ReplyPreview"; -import {UIFeature} from "../../../settings/UIFeature"; -import {UPDATE_EVENT} from "../../../stores/AsyncStore"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { UIFeature } from "../../../settings/UIFeature"; +import { UPDATE_EVENT } from "../../../stores/AsyncStore"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import VoiceRecordComposerTile from "./VoiceRecordComposerTile"; -import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; -import {RecordingState} from "../../../voice/VoiceRecording"; -import Tooltip, {Alignment} from "../elements/Tooltip"; +import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore"; +import { RecordingState } from "../../../voice/VoiceRecording"; +import Tooltip, { Alignment } from "../elements/Tooltip"; import ResizeNotifier from "../../../utils/ResizeNotifier"; import { E2EStatus } from '../../../utils/ShieldUtils'; import SendMessageComposer from "./SendMessageComposer"; +import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; +import { Action } from "../../../dispatcher/actions"; interface IComposerAvatarProps { me: object; @@ -316,10 +318,10 @@ export default class MessageComposer extends React.Component { } } - addEmoji(emoji) { - dis.dispatch({ - action: "insert_emoji", - emoji, + addEmoji(emoji: string) { + dis.dispatch({ + action: Action.ComposerInsert, + text: emoji, }); } diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.js b/src/components/views/rooms/RoomUpgradeWarningBar.js index a2d4f92d35..66e76903eb 100644 --- a/src/components/views/rooms/RoomUpgradeWarningBar.js +++ b/src/components/views/rooms/RoomUpgradeWarningBar.js @@ -24,7 +24,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {replaceableComponent} from "../../../utils/replaceableComponent"; @replaceableComponent("views.rooms.RoomUpgradeWarningBar") -export default class RoomUpgradeWarningBar extends React.Component { +export default class RoomUpgradeWarningBar extends React.PureComponent { static propTypes = { room: PropTypes.object.isRequired, recommendation: PropTypes.object.isRequired, diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index d60eba00ec..10ef91c689 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -27,27 +27,26 @@ import { startsWith, stripPrefix, } from '../../../editor/serialize'; -import {CommandPartCreator} from '../../../editor/parts'; +import { CommandPartCreator } from '../../../editor/parts'; import BasicMessageComposer from "./BasicMessageComposer"; import ReplyThread from "../elements/ReplyThread"; -import {parseEvent} from '../../../editor/deserialize'; -import {findEditableEvent} from '../../../utils/EventUtils'; +import { findEditableEvent } from '../../../utils/EventUtils'; import SendHistoryManager from "../../../SendHistoryManager"; -import {CommandCategories, getCommand} from '../../../SlashCommands'; +import { CommandCategories, getCommand } from '../../../SlashCommands'; import * as sdk from '../../../index'; import Modal from '../../../Modal'; -import {_t, _td} from '../../../languageHandler'; +import { _t, _td } from '../../../languageHandler'; import ContentMessages from '../../../ContentMessages'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import RateLimitedFunc from '../../../ratelimitedfunc'; -import {Action} from "../../../dispatcher/actions"; -import {containsEmoji} from "../../../effects/utils"; -import {CHAT_EFFECTS} from '../../../effects'; +import { Action } from "../../../dispatcher/actions"; +import { containsEmoji } from "../../../effects/utils"; +import { CHAT_EFFECTS } from '../../../effects'; import CountlyAnalytics from "../../../CountlyAnalytics"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; import EMOJI_REGEX from 'emojibase-regex'; -import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager'; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import SettingsStore from '../../../settings/SettingsStore'; function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { @@ -486,62 +485,18 @@ export default class SendMessageComposer extends React.Component { case Action.FocusComposer: this._editorRef && this._editorRef.focus(); break; - case 'insert_mention': - this._insertMention(payload.user_id); - break; - case 'quote': - this._insertQuotedMessage(payload.event); - break; - case 'insert_emoji': - this._insertEmoji(payload.emoji); + case "send_composer_insert": + if (payload.userId) { + this._editorRef && this._editorRef.insertMention(payload.userId); + } else if (payload.event) { + this._editorRef && this._editorRef.insertQuotedMessage(payload.event); + } else if (payload.text) { + this._editorRef && this._editorRef.insertPlaintext(payload.text); + } break; } }; - _insertMention(userId) { - const {model} = this; - const {partCreator} = model; - const member = this.props.room.getMember(userId); - const displayName = member ? - member.rawDisplayName : userId; - const caret = this._editorRef.getCaret(); - const position = model.positionForOffset(caret.offset, caret.atNodeEnd); - // Insert suffix only if the caret is at the start of the composer - const parts = partCreator.createMentionParts(caret.offset === 0, displayName, userId); - model.transform(() => { - const addedLen = model.insert(parts, position); - return model.positionForOffset(caret.offset + addedLen, true); - }); - // refocus on composer, as we just clicked "Mention" - this._editorRef && this._editorRef.focus(); - } - - _insertQuotedMessage(event) { - const {model} = this; - const {partCreator} = model; - const quoteParts = parseEvent(event, partCreator, {isQuotedMessage: true}); - // add two newlines - quoteParts.push(partCreator.newline()); - quoteParts.push(partCreator.newline()); - model.transform(() => { - const addedLen = model.insert(quoteParts, model.positionForOffset(0)); - return model.positionForOffset(addedLen, true); - }); - // refocus on composer, as we just clicked "Quote" - this._editorRef && this._editorRef.focus(); - } - - _insertEmoji = (emoji) => { - const {model} = this; - const {partCreator} = model; - const caret = this._editorRef.getCaret(); - const position = model.positionForOffset(caret.offset, caret.atNodeEnd); - model.transform(() => { - const addedLen = model.insert([partCreator.plain(emoji)], position); - return model.positionForOffset(caret.offset + addedLen, true); - }); - }; - _onPaste = (event) => { const {clipboardData} = event; // Prioritize text on the clipboard over files as Office on macOS puts a bitmap diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 2102071bf3..20d8c9c5d4 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -77,7 +77,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent Math.round(v * 1024)), }, - "org.matrix.msc2516.voice": {}, // No content, this is a rendering hint + "org.matrix.msc3245.voice": {}, // No content, this is a rendering hint }); await this.disposeRecording(); } diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx index 8d886a191e..25dfe2f151 100644 --- a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx @@ -44,14 +44,11 @@ export default class BridgeSettingsTab extends React.Component { return ; } - static getBridgeStateEvents(roomId: string) { + static getBridgeStateEvents(roomId: string): MatrixEvent[] { const client = MatrixClientPeg.get(); const roomState = client.getRoom(roomId).currentState; - return BRIDGE_EVENT_TYPES.map(typeName => { - const events = roomState.events.get(typeName); - return events ? Array.from(events.values()) : []; - }).flat(1); + return BRIDGE_EVENT_TYPES.map(typeName => roomState.getStateEvents(typeName)).flat(1); } render() { diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 3fa0be478c..beff033001 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -32,7 +32,7 @@ import * as ContextMenu from "../../../../structures/ContextMenu"; import { toRightOf } from "../../../../structures/ContextMenu"; interface IProps { - closeSettingsFn: () => {}; + closeSettingsFn: () => void; } interface IState { diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 6a935ab276..977cd4a9aa 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -29,7 +29,7 @@ import AccessibleButton from "../elements/AccessibleButton"; import {BetaPill} from "../beta/BetaCard"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import {Action} from "../../../dispatcher/actions"; -import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog"; +import { UserTab } from "../dialogs/UserSettingsDialog"; import Field from "../elements/Field"; import withValidation from "../elements/Validation"; import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; @@ -224,7 +224,7 @@ const SpaceCreateMenu = ({ onFinished }) => { onFinished(); defaultDispatcher.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_LABS_TAB, + initialTabId: UserTab.Labs, }); }} /> { body } diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx index 2abdc0641d..a0660318bc 100644 --- a/src/components/views/voip/IncomingCallBox.tsx +++ b/src/components/views/voip/IncomingCallBox.tsx @@ -21,17 +21,20 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg'; import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import { ActionPayload } from '../../../dispatcher/payloads'; -import CallHandler from '../../../CallHandler'; +import CallHandler, { AudioID } from '../../../CallHandler'; import RoomAvatar from '../avatars/RoomAvatar'; import FormButton from '../elements/FormButton'; import { CallState } from 'matrix-js-sdk/src/webrtc/call'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; +import classNames from 'classnames'; interface IProps { } interface IState { incomingCall: any; + silenced: boolean; } @replaceableComponent("views.voip.IncomingCallBox") @@ -44,6 +47,7 @@ export default class IncomingCallBox extends React.Component { this.dispatcherRef = dis.register(this.onAction); this.state = { incomingCall: null, + silenced: false, }; } @@ -58,6 +62,7 @@ export default class IncomingCallBox extends React.Component { if (call && call.state === CallState.Ringing) { this.setState({ incomingCall: call, + silenced: false, // Reset silenced state for new call }); } else { this.setState({ @@ -84,6 +89,13 @@ export default class IncomingCallBox extends React.Component { }); }; + private onSilenceClick: React.MouseEventHandler = (e) => { + e.stopPropagation(); + const newState = !this.state.silenced + this.setState({silenced: newState}); + newState ? CallHandler.sharedInstance().pause(AudioID.Ring) : CallHandler.sharedInstance().play(AudioID.Ring); + } + public render() { if (!this.state.incomingCall) { return null; @@ -107,6 +119,12 @@ export default class IncomingCallBox extends React.Component { } } + const silenceClass = classNames({ + "mx_IncomingCallBox_iconButton": true, + "mx_IncomingCallBox_unSilence": this.state.silenced, + "mx_IncomingCallBox_silence": !this.state.silenced, + }); + return
    {

    {caller}

    {incomingCallText}

    +
    { const partLen = part.text.length; diff --git a/src/hooks/useAccountData.ts b/src/hooks/useAccountData.ts index fe71ed9ecd..0384b3bf77 100644 --- a/src/hooks/useAccountData.ts +++ b/src/hooks/useAccountData.ts @@ -21,11 +21,11 @@ import {Room} from "matrix-js-sdk/src/models/room"; import {useEventEmitter} from "./useEventEmitter"; -const tryGetContent = (ev?: MatrixEvent) => ev ? ev.getContent() : undefined; +const tryGetContent = (ev?: MatrixEvent) => ev ? ev.getContent() : undefined; // Hook to simplify listening to Matrix account data export const useAccountData = (cli: MatrixClient, eventType: string) => { - const [value, setValue] = useState(() => tryGetContent(cli.getAccountData(eventType))); + const [value, setValue] = useState(() => tryGetContent(cli.getAccountData(eventType))); const handler = useCallback((event) => { if (event.getType() !== eventType) return; @@ -38,7 +38,7 @@ export const useAccountData = (cli: MatrixClient, eventType: strin // Hook to simplify listening to Matrix room account data export const useRoomAccountData = (room: Room, eventType: string) => { - const [value, setValue] = useState(() => tryGetContent(room.getAccountData(eventType))); + const [value, setValue] = useState(() => tryGetContent(room.getAccountData(eventType))); const handler = useCallback((event) => { if (event.getType() !== eventType) return; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 17d6f64c46..8c4262fe44 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -905,6 +905,8 @@ "Incoming voice call": "Incoming voice call", "Incoming video call": "Incoming video call", "Incoming call": "Incoming call", + "Sound on": "Sound on", + "Silence call": "Silence call", "Decline": "Decline", "Accept": "Accept", "Pause": "Pause", diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index b6289969bd..c36f96f368 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -300,7 +300,7 @@ export default class EventIndex extends EventEmitter { } private eventToJson(ev: MatrixEvent) { - const jsonEvent = ev.toJSON(); + const jsonEvent: any = ev.toJSON(); const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent; if (ev.isEncrypted()) { diff --git a/src/performance/entry-names.ts b/src/performance/entry-names.ts index effd9506f6..6cb193b1b1 100644 --- a/src/performance/entry-names.ts +++ b/src/performance/entry-names.ts @@ -37,17 +37,17 @@ export enum PerformanceEntryNames { SWITCH_ROOM = "mx_SwithRoom", JUMP_TO_ROOM = "mx_JumpToRoom", - JOIN_ROOM = "mx_JoinRoom", - CREATE_DM = "mx_CreateDM", + JOIN_ROOM = "mx_JoinRoom", // ✅ + CREATE_DM = "mx_CreateDM", // ✅ PEEK_ROOM = "mx_PeekRoom", /** * User */ - VERIFY_E2EE_USER = "mx_VerifyE2EEUser", - LOGIN = "mx_Login", - REGISTER = "mx_Register", + VERIFY_E2EE_USER = "mx_VerifyE2EEUser", // ✅ + LOGIN = "mx_Login", // ✅ + REGISTER = "mx_Register", // ✅ /** * VoIP diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.ts similarity index 73% rename from src/stores/SetupEncryptionStore.js rename to src/stores/SetupEncryptionStore.ts index b768ae69df..88385d0399 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.ts @@ -15,29 +15,42 @@ limitations under the License. */ import EventEmitter from 'events'; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { IKeyBackupVersion } from "matrix-js-sdk/src/crypto/keybackup"; +import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from '../MatrixClientPeg'; import { accessSecretStorage, AccessCancelledError } from '../SecurityManager'; import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -export const PHASE_LOADING = 0; -export const PHASE_INTRO = 1; -export const PHASE_BUSY = 2; -export const PHASE_DONE = 3; //final done stage, but still showing UX -export const PHASE_CONFIRM_SKIP = 4; -export const PHASE_FINISHED = 5; //UX can be closed +export enum Phase { + Loading = 0, + Intro = 1, + Busy = 2, + Done = 3, // final done stage, but still showing UX + ConfirmSkip = 4, + Finished = 5, // UX can be closed +} export class SetupEncryptionStore extends EventEmitter { - static sharedInstance() { - if (!global.mx_SetupEncryptionStore) global.mx_SetupEncryptionStore = new SetupEncryptionStore(); - return global.mx_SetupEncryptionStore; + private started: boolean; + public phase: Phase; + public verificationRequest: VerificationRequest; + public backupInfo: IKeyBackupVersion; + public keyId: string; + public keyInfo: ISecretStorageKeyInfo; + public hasDevicesToVerifyAgainst: boolean; + + public static sharedInstance() { + if (!window.mxSetupEncryptionStore) window.mxSetupEncryptionStore = new SetupEncryptionStore(); + return window.mxSetupEncryptionStore; } - start() { - if (this._started) { + public start(): void { + if (this.started) { return; } - this._started = true; - this.phase = PHASE_LOADING; + this.started = true; + this.phase = Phase.Loading; this.verificationRequest = null; this.backupInfo = null; @@ -48,34 +61,34 @@ export class SetupEncryptionStore extends EventEmitter { const cli = MatrixClientPeg.get(); cli.on("crypto.verification.request", this.onVerificationRequest); - cli.on('userTrustStatusChanged', this._onUserTrustStatusChanged); + cli.on('userTrustStatusChanged', this.onUserTrustStatusChanged); const requestsInProgress = cli.getVerificationRequestsToDeviceInProgress(cli.getUserId()); if (requestsInProgress.length) { // If there are multiple, we take the most recent. Equally if the user sends another request from // another device after this screen has been shown, we'll switch to the new one, so this // generally doesn't support multiple requests. - this._setActiveVerificationRequest(requestsInProgress[requestsInProgress.length - 1]); + this.setActiveVerificationRequest(requestsInProgress[requestsInProgress.length - 1]); } this.fetchKeyInfo(); } - stop() { - if (!this._started) { + public stop(): void { + if (!this.started) { return; } - this._started = false; + this.started = false; if (this.verificationRequest) { this.verificationRequest.off("change", this.onVerificationRequestChange); } if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest); - MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged); + MatrixClientPeg.get().removeListener('userTrustStatusChanged', this.onUserTrustStatusChanged); } } - async fetchKeyInfo() { + public async fetchKeyInfo(): Promise { const cli = MatrixClientPeg.get(); const keys = await cli.isSecretStored('m.cross_signing.master', false); if (keys === null || Object.keys(keys).length === 0) { @@ -97,15 +110,15 @@ export class SetupEncryptionStore extends EventEmitter { if (!this.hasDevicesToVerifyAgainst && !this.keyInfo) { // skip before we can even render anything. - this.phase = PHASE_FINISHED; + this.phase = Phase.Finished; } else { - this.phase = PHASE_INTRO; + this.phase = Phase.Intro; } this.emit("update"); } - async usePassPhrase() { - this.phase = PHASE_BUSY; + public async usePassPhrase(): Promise { + this.phase = Phase.Busy; this.emit("update"); const cli = MatrixClientPeg.get(); try { @@ -120,7 +133,7 @@ export class SetupEncryptionStore extends EventEmitter { // passphase cached for that work. This dialog itself will only wait // on the first trust check, and the key backup restore will happen // in the background. - await new Promise((resolve, reject) => { + await new Promise((resolve: (value?: unknown) => void, reject: (reason?: any) => void) => { accessSecretStorage(async () => { await cli.checkOwnCrossSigningTrust(); resolve(); @@ -134,7 +147,7 @@ export class SetupEncryptionStore extends EventEmitter { }); if (cli.getCrossSigningId()) { - this.phase = PHASE_DONE; + this.phase = Phase.Done; this.emit("update"); } } catch (e) { @@ -142,25 +155,25 @@ export class SetupEncryptionStore extends EventEmitter { console.log(e); } // this will throw if the user hits cancel, so ignore - this.phase = PHASE_INTRO; + this.phase = Phase.Intro; this.emit("update"); } } - _onUserTrustStatusChanged = (userId) => { + private onUserTrustStatusChanged = (userId: string) => { if (userId !== MatrixClientPeg.get().getUserId()) return; const publicKeysTrusted = MatrixClientPeg.get().getCrossSigningId(); if (publicKeysTrusted) { - this.phase = PHASE_DONE; + this.phase = Phase.Done; this.emit("update"); } } - onVerificationRequest = (request) => { - this._setActiveVerificationRequest(request); + public onVerificationRequest = (request: VerificationRequest): void => { + this.setActiveVerificationRequest(request); } - onVerificationRequestChange = () => { + public onVerificationRequestChange = (): void => { if (this.verificationRequest.cancelled) { this.verificationRequest.off("change", this.onVerificationRequestChange); this.verificationRequest = null; @@ -172,34 +185,34 @@ export class SetupEncryptionStore extends EventEmitter { // cross signing to be ready to use, so wait for the user trust status to // change (or change to DONE if it's already ready). const publicKeysTrusted = MatrixClientPeg.get().getCrossSigningId(); - this.phase = publicKeysTrusted ? PHASE_DONE : PHASE_BUSY; + this.phase = publicKeysTrusted ? Phase.Done : Phase.Busy; this.emit("update"); } } - skip() { - this.phase = PHASE_CONFIRM_SKIP; + public skip(): void { + this.phase = Phase.ConfirmSkip; this.emit("update"); } - skipConfirm() { - this.phase = PHASE_FINISHED; + public skipConfirm(): void { + this.phase = Phase.Finished; this.emit("update"); } - returnAfterSkip() { - this.phase = PHASE_INTRO; + public returnAfterSkip(): void { + this.phase = Phase.Intro; this.emit("update"); } - done() { - this.phase = PHASE_FINISHED; + public done(): void { + this.phase = Phase.Finished; this.emit("update"); // async - ask other clients for keys, if necessary MatrixClientPeg.get().crypto.cancelAndResendAllOutgoingKeyRequests(); } - async _setActiveVerificationRequest(request) { + private async setActiveVerificationRequest(request: VerificationRequest): Promise { if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return; if (this.verificationRequest) { diff --git a/src/stores/WidgetEchoStore.ts b/src/stores/WidgetEchoStore.ts index 09120d6108..0b0be50541 100644 --- a/src/stores/WidgetEchoStore.ts +++ b/src/stores/WidgetEchoStore.ts @@ -16,8 +16,8 @@ limitations under the License. import EventEmitter from 'events'; import { IWidget } from 'matrix-widget-api'; -import MatrixEvent from "matrix-js-sdk/src/models/event"; -import {WidgetType} from "../widgets/WidgetType"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { WidgetType } from "../widgets/WidgetType"; /** * Acts as a place to get & set widget state, storing local echo state and diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 397d637125..6dcaf7abd7 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -51,7 +51,7 @@ import ThemeWatcher from "../../settings/watchers/ThemeWatcher"; import {getCustomTheme} from "../../theme"; import CountlyAnalytics from "../../CountlyAnalytics"; import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { MatrixEvent, IEvent } from "matrix-js-sdk/src/models/event"; import { ELEMENT_CLIENT_ID } from "../../identifiers"; import { getUserLanguage } from "../../languageHandler"; @@ -415,7 +415,7 @@ export class StopGapWidget extends EventEmitter { private feedEvent(ev: MatrixEvent) { if (!this.messaging) return; - const raw = ev.event; + const raw = ev.event as IEvent; this.messaging.feedEvent(raw).catch(e => { console.error("Error sending event to widget: ", e); }); diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 25e81c47a2..5218e4a0bc 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -145,7 +145,7 @@ export class StopGapWidgetDriver extends WidgetDriver { return {roomId, eventId: r.event_id}; } - public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise { + public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise { limit = limit > 0 ? Math.min(limit, 25) : 25; // arbitrary choice const client = MatrixClientPeg.get(); @@ -167,9 +167,7 @@ export class StopGapWidgetDriver extends WidgetDriver { return results.map(e => e.event); } - public async readStateEvents( - eventType: string, stateKey: string | undefined, limit: number, - ): Promise { + public async readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise { limit = limit > 0 ? Math.min(limit, 100) : 100; // arbitrary choice const client = MatrixClientPeg.get(); @@ -178,7 +176,7 @@ export class StopGapWidgetDriver extends WidgetDriver { if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client"); const results: MatrixEvent[] = []; - const state = room.currentState.events.get(eventType); + const state: Map = room.currentState.events.get(eventType); if (state) { if (stateKey === "" || !!stateKey) { const forKey = state.get(stateKey); diff --git a/src/toasts/UnverifiedSessionToast.ts b/src/toasts/UnverifiedSessionToast.ts index c856d39d1f..05425b93c0 100644 --- a/src/toasts/UnverifiedSessionToast.ts +++ b/src/toasts/UnverifiedSessionToast.ts @@ -21,7 +21,7 @@ import DeviceListener from '../DeviceListener'; import ToastStore from "../stores/ToastStore"; import GenericToast from "../components/views/toasts/GenericToast"; import { Action } from "../dispatcher/actions"; -import { USER_SECURITY_TAB } from "../components/views/dialogs/UserSettingsDialog"; +import { UserTab } from "../components/views/dialogs/UserSettingsDialog"; function toastKey(deviceId: string) { return "unverified_session_" + deviceId; @@ -34,7 +34,7 @@ export const showToast = async (deviceId: string) => { DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]); dis.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_SECURITY_TAB, + initialTabId: UserTab.Security, }); }; diff --git a/src/utils/DMRoomMap.ts b/src/utils/DMRoomMap.ts index b166674043..3e554f145d 100644 --- a/src/utils/DMRoomMap.ts +++ b/src/utils/DMRoomMap.ts @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClientPeg} from '../MatrixClientPeg'; -import {uniq} from "lodash"; -import {Room} from "matrix-js-sdk/src/models/room"; -import {Event} from "matrix-js-sdk/src/models/event"; -import {MatrixClient} from "matrix-js-sdk/src/client"; +import { uniq } from "lodash"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixClient } from "matrix-js-sdk/src/client"; + +import { MatrixClientPeg } from '../MatrixClientPeg'; /** * Class that takes a Matrix Client and flips the m.direct map @@ -30,15 +30,13 @@ import {MatrixClient} from "matrix-js-sdk/src/client"; export default class DMRoomMap { private static sharedInstance: DMRoomMap; - private matrixClient: MatrixClient; // TODO: convert these to maps private roomToUser: {[key: string]: string} = null; private userToRooms: {[key: string]: string[]} = null; private hasSentOutPatchDirectAccountDataPatch: boolean; - private mDirectEvent: Event; + private mDirectEvent: object; - constructor(matrixClient) { - this.matrixClient = matrixClient; + constructor(private readonly matrixClient: MatrixClient) { // see onAccountData this.hasSentOutPatchDirectAccountDataPatch = false; diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index c67f3bad13..7ff0529363 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -392,7 +392,7 @@ export default class WidgetUtils { } const widgets = client.getAccountData('m.widgets'); if (!widgets) return; - const userWidgets: IWidgetEvent[] = widgets.getContent() || {}; + const userWidgets: Record = widgets.getContent() || {}; Object.entries(userWidgets).forEach(([key, widget]) => { if (widget.content && widget.content.type === "m.integration_manager") { delete userWidgets[key]; diff --git a/test/DecryptionFailureTracker-test.js b/test/DecryptionFailureTracker-test.js index 7a6a42ef55..bc751ba44e 100644 --- a/test/DecryptionFailureTracker-test.js +++ b/test/DecryptionFailureTracker-test.js @@ -30,9 +30,7 @@ function createFailedDecryptionEvent() { const event = new MatrixEvent({ event_id: "event-id-" + Math.random().toString(16).slice(2), }); - event._setClearData( - event._badEncryptedMessage(":("), - ); + event.setClearData(event.badEncryptedMessage(":(")); return event; } @@ -67,7 +65,7 @@ describe('DecryptionFailureTracker', function() { tracker.eventDecrypted(decryptedEvent, err); // Indicate successful decryption: clear data can be anything where the msgtype is not m.bad.encrypted - decryptedEvent._setClearData({}); + decryptedEvent.setClearData({}); tracker.eventDecrypted(decryptedEvent, null); // Pretend "now" is Infinity diff --git a/test/end-to-end-tests/src/scenarios/e2e-encryption.js b/test/end-to-end-tests/src/scenarios/e2e-encryption.js index 20e8af2947..b20874fdaf 100644 --- a/test/end-to-end-tests/src/scenarios/e2e-encryption.js +++ b/test/end-to-end-tests/src/scenarios/e2e-encryption.js @@ -20,9 +20,11 @@ const acceptInvite = require('../usecases/accept-invite'); const {receiveMessage} = require('../usecases/timeline'); const {createDm} = require('../usecases/create-room'); const {checkRoomSettings} = require('../usecases/room-settings'); -const {startSasVerifcation, acceptSasVerification} = require('../usecases/verify'); +const {startSasVerification, acceptSasVerification} = require('../usecases/verify'); const { setupSecureBackup } = require('../usecases/security'); const assert = require('assert'); +const { measureStart, measureStop } = require('../util'); + module.exports = async function e2eEncryptionScenarios(alice, bob) { console.log(" creating an e2e encrypted DM and join through invite:"); @@ -31,12 +33,14 @@ module.exports = async function e2eEncryptionScenarios(alice, bob) { await acceptInvite(alice, 'bob'); // do sas verifcation bob.log.step(`starts SAS verification with ${alice.username}`); - const bobSasPromise = startSasVerifcation(bob, alice.username); + await measureStart(bob, "mx_VerifyE2EEUser"); + const bobSasPromise = startSasVerification(bob, alice.username); const aliceSasPromise = acceptSasVerification(alice, bob.username); // wait in parallel, so they don't deadlock on each other // the logs get a bit messy here, but that's fine enough for debugging (hopefully) const [bobSas, aliceSas] = await Promise.all([bobSasPromise, aliceSasPromise]); assert.deepEqual(bobSas, aliceSas); + await measureStop(bob, "mx_VerifyE2EEUser"); bob.log.done(`done (match for ${bobSas.join(", ")})`); const aliceMessage = "Guess what I just heard?!"; await sendMessage(alice, aliceMessage); diff --git a/test/end-to-end-tests/src/usecases/create-room.js b/test/end-to-end-tests/src/usecases/create-room.js index 3830e3e0da..36b9ed21ec 100644 --- a/test/end-to-end-tests/src/usecases/create-room.js +++ b/test/end-to-end-tests/src/usecases/create-room.js @@ -15,6 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +const { measureStart, measureStop } = require('../util'); + async function openRoomDirectory(session) { const roomDirectoryButton = await session.query('.mx_LeftPanel_exploreButton'); await roomDirectoryButton.click(); @@ -52,6 +54,8 @@ async function createRoom(session, roomName, encrypted=false) { async function createDm(session, invitees) { session.log.step(`creates DM with ${JSON.stringify(invitees)}`); + await measureStart(session, "mx_CreateDM"); + const dmsSublist = await findSublist(session, "people"); const startChatButton = await dmsSublist.$(".mx_RoomSublist_auxButton"); await startChatButton.click(); @@ -76,6 +80,8 @@ async function createDm(session, invitees) { await session.query('.mx_MessageComposer'); session.log.done(); + + await measureStop(session, "mx_CreateDM"); } module.exports = {openRoomDirectory, findSublist, createRoom, createDm}; diff --git a/test/end-to-end-tests/src/usecases/join.js b/test/end-to-end-tests/src/usecases/join.js index 655c0be686..cf0f67be44 100644 --- a/test/end-to-end-tests/src/usecases/join.js +++ b/test/end-to-end-tests/src/usecases/join.js @@ -16,9 +16,12 @@ limitations under the License. */ const {openRoomDirectory} = require('./create-room'); +const { measureStart, measureStop } = require('../util'); + module.exports = async function join(session, roomName) { session.log.step(`joins room "${roomName}"`); + await measureStart(session, "mx_JoinRoom"); await openRoomDirectory(session); const roomInput = await session.query('.mx_DirectorySearchBox input'); await session.replaceInputText(roomInput, roomName); @@ -26,5 +29,6 @@ module.exports = async function join(session, roomName) { const joinFirstLink = await session.query('.mx_RoomDirectory_table .mx_RoomDirectory_join .mx_AccessibleButton'); await joinFirstLink.click(); await session.query('.mx_MessageComposer'); + await measureStop(session, "mx_JoinRoom"); session.log.done(); }; diff --git a/test/end-to-end-tests/src/usecases/verify.js b/test/end-to-end-tests/src/usecases/verify.js index ea5b9961a4..a66c8c1b1c 100644 --- a/test/end-to-end-tests/src/usecases/verify.js +++ b/test/end-to-end-tests/src/usecases/verify.js @@ -74,7 +74,7 @@ async function doSasVerification(session) { return sasCodes; } -module.exports.startSasVerifcation = async function(session, name) { +module.exports.startSasVerification = async function(session, name) { session.log.startGroup("starts verification"); await startVerification(session, name); diff --git a/test/end-to-end-tests/src/util.js b/test/end-to-end-tests/src/util.js index cc7391fa9f..5abb110df4 100644 --- a/test/end-to-end-tests/src/util.js +++ b/test/end-to-end-tests/src/util.js @@ -26,3 +26,15 @@ module.exports.range = function(start, amount, step = 1) { module.exports.delay = function(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }; + +module.exports.measureStart = function(session, name) { + return session.page.evaluate(_name => { + window.mxPerformanceMonitor.start(_name); + }, name); +}; + +module.exports.measureStop = function(session, name) { + return session.page.evaluate(_name => { + window.mxPerformanceMonitor.stop(_name); + }, name); +}; diff --git a/test/end-to-end-tests/start.js b/test/end-to-end-tests/start.js index c1588e848e..04df0c51c0 100644 --- a/test/end-to-end-tests/start.js +++ b/test/end-to-end-tests/start.js @@ -88,6 +88,10 @@ async function runTests() { window.mxPerformanceMonitor.addPerformanceDataCallback({ entryNames: [ window.mxPerformanceEntryNames.REGISTER, + window.mxPerformanceEntryNames.LOGIN, + window.mxPerformanceEntryNames.JOIN_ROOM, + window.mxPerformanceEntryNames.CREATE_DM, + window.mxPerformanceEntryNames.VERIFY_E2EE_USER, ], callback: (events) => { measurements = JSON.stringify(events); diff --git a/yarn.lock b/yarn.lock index 7c232d2aa1..cd4a8b0bd6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6583,9 +6583,9 @@ postcss-value-parser@^4.1.0: integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.35, postcss@^7.0.6: - version "7.0.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" - integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== + version "7.0.36" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.36.tgz#056f8cffa939662a8f5905950c07d5285644dfcb" + integrity sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw== dependencies: chalk "^2.4.2" source-map "^0.6.1"