diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index b019fe7e01..0efa2d01a1 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -222,7 +222,6 @@ limitations under the License. .mx_RoomView_MessageList li { clear: both; - contain: content; } li.mx_RoomView_myReadMarker_container { @@ -239,6 +238,7 @@ hr.mx_RoomView_myReadMarker { position: relative; top: -1px; z-index: 1; + will-change: width; transition: width 400ms easeinsine 1s, opacity 400ms easeinsine 1s; width: 99%; opacity: 1; diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index b6b901757c..cf61ce569d 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -115,8 +115,7 @@ $irc-line-height: $font-18px; .mx_EventTile_line { .mx_EventTile_e2eIcon, .mx_TextualEvent, - .mx_MTextBody, - .mx_ReplyThread_wrapper_empty { + .mx_MTextBody { display: inline-block; } } @@ -177,16 +176,13 @@ $irc-line-height: $font-18px; .mx_SenderProfile_hover { background-color: $primary-bg-color; overflow: hidden; + display: flex; - > span { - display: flex; - - > .mx_SenderProfile_name { - overflow: hidden; - text-overflow: ellipsis; - min-width: var(--name-width); - text-align: end; - } + > .mx_SenderProfile_name { + overflow: hidden; + text-overflow: ellipsis; + min-width: var(--name-width); + text-align: end; } } diff --git a/res/css/views/rooms/_RoomBreadcrumbs.scss b/res/css/views/rooms/_RoomBreadcrumbs.scss index 6512797401..152b0a45cd 100644 --- a/res/css/views/rooms/_RoomBreadcrumbs.scss +++ b/res/css/views/rooms/_RoomBreadcrumbs.scss @@ -32,14 +32,14 @@ limitations under the License. // first triggering the enter state with the newest breadcrumb off screen (-40px) then // sliding it into view. &.mx_RoomBreadcrumbs-enter { - margin-left: -40px; // 32px for the avatar, 8px for the margin + transform: translateX(-40px); // 32px for the avatar, 8px for the margin } &.mx_RoomBreadcrumbs-enter-active { - margin-left: 0; + transform: translateX(0); // Timing function is as-requested by design. // NOTE: The transition time MUST match the value passed to CSSTransition! - transition: margin-left 640ms cubic-bezier(0.66, 0.02, 0.36, 1); + transition: transform 640ms cubic-bezier(0.66, 0.02, 0.36, 1); } .mx_RoomBreadcrumbs_placeholder { diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 421fbfe39b..03146e0325 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -19,7 +19,7 @@ limitations under the License. margin-bottom: 4px; padding: 4px; - contain: strict; + contain: content; // Not strict as it will break when resizing a sublist vertically height: 40px; box-sizing: border-box; diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index a05d3a25c8..0d87451b5f 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -264,7 +264,7 @@ export default class CallHandler extends EventEmitter { } public getSupportsVirtualRooms() { - return this.supportsPstnProtocol; + return this.supportsSipNativeVirtual; } public pstnLookup(phoneNumber: string): Promise { @@ -521,7 +521,9 @@ export default class CallHandler extends EventEmitter { let newNativeAssertedIdentity = newAssertedIdentity; if (newAssertedIdentity) { const response = await this.sipNativeLookup(newAssertedIdentity); - if (response.length) newNativeAssertedIdentity = response[0].userid; + if (response.length && response[0].fields.lookup_success) { + newNativeAssertedIdentity = response[0].userid; + } } console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`); @@ -862,9 +864,43 @@ export default class CallHandler extends EventEmitter { }); break; } + case Action.DialNumber: + this.dialNumber(payload.number); + break; } } + private async dialNumber(number: string) { + const results = await this.pstnLookup(number); + if (!results || results.length === 0 || !results[0].userid) { + Modal.createTrackedDialog('', '', ErrorDialog, { + title: _t("Unable to look up phone number"), + description: _t("There was an error looking up the phone number"), + }); + return; + } + const userId = results[0].userid; + + // Now check to see if this is a virtual user, in which case we should find the + // native user + let nativeUserId; + if (this.getSupportsVirtualRooms()) { + const nativeLookupResults = await this.sipNativeLookup(userId); + const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success; + nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId; + console.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId); + } else { + nativeUserId = userId; + } + + const roomId = await ensureDMExists(MatrixClientPeg.get(), nativeUserId); + + dis.dispatch({ + action: 'view_room', + room_id: roomId, + }); + } + setActiveCallRoomId(activeCallRoomId: string) { logger.info("Setting call in room " + activeCallRoomId + " active"); diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index e5bed2e812..d576a5434c 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -33,7 +33,7 @@ export default class VoipUserMapper { private async userToVirtualUser(userId: string): Promise { const results = await CallHandler.sharedInstance().sipVirtualLookup(userId); - if (results.length === 0) return null; + if (results.length === 0 || !results[0].fields.lookup_success) return null; return results[0].userid; } @@ -82,14 +82,14 @@ export default class VoipUserMapper { return Boolean(claimedNativeRoomId); } - public async onNewInvitedRoom(invitedRoom: Room) { + public async onNewInvitedRoom(invitedRoom: Room): Promise { if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return; const inviterId = invitedRoom.getDMInviter(); console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId); if (result.length === 0) { - return true; + return; } if (result[0].fields.is_virtual) { diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 53a6d4d92b..6709fef814 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -648,39 +648,37 @@ export default class MessagePanel extends React.Component { // use txnId as key if available so that we don't remount during sending ret.push( -
  • - - - -
  • , + + + , ); return ret; @@ -782,7 +780,7 @@ export default class MessagePanel extends React.Component { } _collectEventNode = (eventId, node) => { - this.eventNodes[eventId] = node; + this.eventNodes[eventId] = node?.ref?.current; } // once dynamic content in the events load, make the scrollPanel check the diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index ec72eae69f..4390d21783 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -79,8 +79,9 @@ import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutS import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager'; import { objectHasDiff } from "../../utils/objects"; import SpaceRoomView from "./SpaceRoomView"; -import { IOpts } from "../../createRoom"; -import { replaceableComponent } from "../../utils/replaceableComponent"; +import { IOpts } from "../../createRoom +import { replaceableComponent } from "../../utils/replaceableComponent +import { omit } from 'lodash'; import UIStore from "../../stores/UIStore"; const DEBUG = false; @@ -173,6 +174,7 @@ export interface IState { statusBarVisible: boolean; // We load this later by asking the js-sdk to suggest a version for us. // This object is the result of Room#getRecommendedVersion() + upgradeRecommendation?: { version: string; needsUpgrade: boolean; @@ -524,7 +526,20 @@ export default class RoomView extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - return (objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, 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 hasStateDiff = + objectHasDiff(state, newState) || + (newUpgradeRecommendation.needsUpgrade === true) + + return hasPropsDiff || hasStateDiff; } componentDidUpdate() { @@ -818,7 +833,7 @@ export default class RoomView extends React.Component { }; private onEvent = (ev) => { - if (ev.isBeingDecrypted() || ev.isDecryptionFailure() || ev.shouldAttemptDecryption()) return; + if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; this.handleEffects(ev); }; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index c826199533..6300c7532e 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -36,7 +36,6 @@ import shouldHideEvent from '../../shouldHideEvent'; import EditorStateTransfer from '../../utils/EditorStateTransfer'; import {haveTileForEvent} from "../views/rooms/EventTile"; import {UIFeature} from "../../settings/UIFeature"; -import {objectHasDiff} from "../../utils/objects"; import {replaceableComponent} from "../../utils/replaceableComponent"; import { arrayFastClone } from "../../utils/arrays"; @@ -270,30 +269,6 @@ class TimelinePanel extends React.Component { } } - shouldComponentUpdate(nextProps, nextState) { - if (objectHasDiff(this.props, nextProps)) { - if (DEBUG) { - console.group("Timeline.shouldComponentUpdate: props change"); - console.log("props before:", this.props); - console.log("props after:", nextProps); - console.groupEnd(); - } - return true; - } - - if (objectHasDiff(this.state, nextState)) { - if (DEBUG) { - console.group("Timeline.shouldComponentUpdate: state change"); - console.log("state before:", this.state); - console.log("state after:", nextState); - console.groupEnd(); - } - return true; - } - - return false; - } - componentWillUnmount() { // set a boolean to say we've been unmounted, which any pending // promises can use to throw away their results. diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index 1fd3e3419f..273c8a079f 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -55,6 +55,7 @@ export default class ToastContainer extends React.Component<{}, IState> { const totalCount = this.state.toasts.length; const isStacked = totalCount > 1; let toast; + let containerClasses; if (totalCount !== 0) { const topToast = this.state.toasts[0]; const {title, icon, key, component, className, props} = topToast; @@ -79,16 +80,17 @@ export default class ToastContainer extends React.Component<{}, IState> {
    {React.createElement(component, toastProps)}
    ); + + containerClasses = classNames("mx_ToastContainer", { + "mx_ToastContainer_stacked": isStacked, + }); } - - const containerClasses = classNames("mx_ToastContainer", { - "mx_ToastContainer_stacked": isStacked, - }); - - return ( -
    - {toast} -
    - ); + return toast + ? ( +
    + {toast} +
    + ) + : null; } } diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index 3bb264fb3e..c98a7c3156 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -73,7 +73,7 @@ export default class AccessibleTooltipButton extends React.PureComponent :
    ; + /> : null; return ( ; + return null; } const avatars = this.state.profiles.map((profile, index) => { return ; diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 870803995d..bbced5328f 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -214,7 +214,7 @@ export default class ReplyThread extends React.Component { static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) { if (!ReplyThread.getParentEventId(parentEv)) { - return
    ; + return null; } return 0) { + loadedEv = await this.getNextEvent(events[0]); + } + this.setState({ - loadedEv: null, + loadedEv, events, - }, this.loadNextEvent); + }); dis.fire(Action.FocusComposer); } diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index bd10526799..8f10954370 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -31,21 +31,23 @@ export default class SenderProfile extends React.Component { static contextType = MatrixClientContext; - state = { - userGroups: null, - relatedGroups: [], - }; + constructor(props) { + super(props); + const senderId = this.props.mxEvent.getSender(); + this.state = { + userGroups: FlairStore.cachedPublicisedGroups(senderId) || [], + relatedGroups: [], + }; + } componentDidMount() { this.unmounted = false; this._updateRelatedGroups(); - FlairStore.getPublicisedGroupsCached( - this.context, this.props.mxEvent.getSender(), - ).then((userGroups) => { - if (this.unmounted) return; - this.setState({userGroups}); - }); + if (this.state.userGroups.length === 0) { + this.getPublicisedGroups(); + } + this.context.on('RoomState.events', this.onRoomStateEvents); } @@ -55,6 +57,15 @@ export default class SenderProfile extends React.Component { this.context.removeListener('RoomState.events', this.onRoomStateEvents); } + async getPublicisedGroups() { + if (!this.unmounted) { + const userGroups = await FlairStore.getPublicisedGroupsCached( + this.context, this.props.mxEvent.getSender(), + ); + this.setState({userGroups}); + } + } + onRoomStateEvents = event => { if (event.getType() === 'm.room.related_groups' && event.getRoomId() === this.props.mxEvent.getRoomId() @@ -93,10 +104,10 @@ export default class SenderProfile extends React.Component { const {msgtype} = mxEvent.getContent(); if (msgtype === 'm.emote') { - return ; // emote message must include the name so don't duplicate it + return null; // emote message must include the name so don't duplicate it } - let flair =
    ; + let flair = null; if (this.props.enableFlair) { const displayedGroups = this._getDisplayedGroups( this.state.userGroups, this.state.relatedGroups, @@ -110,19 +121,12 @@ export default class SenderProfile extends React.Component { const nameElem = name || ''; - // Name + flair - const nameFlair = - - { nameElem } - - { flair } - ; - return ( -
    -
    - { nameFlair } -
    +
    + + { nameElem } + + { flair }
    ); } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 67df5a84ba..d3057fed62 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -277,6 +277,12 @@ interface IProps { // Helper to build permalinks for the room permalinkCreator?: RoomPermalinkCreator; + + // Symbol of the root node + as?: string + + // whether or not to always show timestamps + alwaysShowTimestamps?: boolean } interface IState { @@ -291,12 +297,15 @@ interface IState { previouslyRequestedKeys: boolean; // The Relations model from the JS SDK for reactions to `mxEvent` reactions: Relations; + + hover: boolean; } @replaceableComponent("views.rooms.EventTile") export default class EventTile extends React.Component { private suppressReadReceiptAnimation: boolean; private isListeningForReceipts: boolean; + private ref: React.RefObject; private tile = React.createRef(); private replyThread = React.createRef(); @@ -322,6 +331,8 @@ export default class EventTile extends React.Component { previouslyRequestedKeys: false, // The Relations model from the JS SDK for reactions to `mxEvent` reactions: this.getReactions(), + + hover: false, }; // don't do RR animations until we are mounted @@ -333,6 +344,8 @@ export default class EventTile extends React.Component { // to determine if we've already subscribed and use a combination of other flags to find // out if we should even be subscribed at all. this.isListeningForReceipts = false; + + this.ref = React.createRef(); } /** @@ -631,7 +644,7 @@ export default class EventTile extends React.Component { // return early if there are no read receipts if (!this.props.readReceipts || this.props.readReceipts.length === 0) { - return (); + return null; } const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); @@ -640,6 +653,11 @@ export default class EventTile extends React.Component { let left = 0; const receipts = this.props.readReceipts || []; + + if (receipts.length === 0) { + return null; + } + for (let i = 0; i < receipts.length; ++i) { const receipt = receipts[i]; @@ -690,10 +708,14 @@ export default class EventTile extends React.Component { } } - return - { remText } - { avatars } - ; + return ( +
    + + { remText } + { avatars } + +
    + ) } onSenderProfileClick = event => { @@ -953,7 +975,8 @@ export default class EventTile extends React.Component { onFocusChange={this.onActionBarFocusChange} /> : undefined; - const timestamp = this.props.mxEvent.getTs() ? + const showTimestamp = this.props.mxEvent.getTs() && (this.props.alwaysShowTimestamps || this.state.hover); + const timestamp = showTimestamp ? : null; const keyRequestHelpText = @@ -1016,11 +1039,7 @@ export default class EventTile extends React.Component { let msgOption; if (this.props.showReadReceipts) { const readAvatars = this.getReadAvatars(); - msgOption = ( -
    - { readAvatars } -
    - ); + msgOption = readAvatars; } switch (this.props.tileShape) { @@ -1124,11 +1143,20 @@ export default class EventTile extends React.Component { // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return ( -
    - { ircTimestamp } - { sender } - { ircPadlock } -
    + React.createElement(this.props.as || "div", { + "ref": this.ref, + "className": classes, + "tabIndex": -1, + "aria-live": ariaLive, + "aria-atomic": "true", + "data-scroll-tokens": this.props["data-scroll-tokens"], + "onMouseEnter": () => this.setState({ hover: true }), + "onMouseLeave": () => this.setState({ hover: false }), + }, [ + ircTimestamp, + sender, + ircPadlock, +
    { groupTimestamp } { groupPadlock } { thread } @@ -1145,16 +1173,12 @@ export default class EventTile extends React.Component { { keyRequestInfo } { reactionsRow } { actionBar } -
    - {msgOption} - { - // The avatar goes after the event tile as it's absolutely positioned to be over the - // event tile line, so needs to be later in the DOM so it appears on top (this avoids - // the need for further z-indexing chaos) - } - { avatar } -
    - ); +
    , + msgOption, + avatar, + + ]) + ) } } } diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index b2a66f6670..9aedb38654 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -62,13 +62,11 @@ export default class SimpleRoomHeader extends React.Component { } return ( -
    -
    -
    - { icon } - { this.props.title } - { cancelButton } -
    +
    +
    + { icon } + { this.props.title } + { cancelButton }
    ); diff --git a/src/components/views/rooms/WhoIsTypingTile.tsx b/src/components/views/rooms/WhoIsTypingTile.tsx index c5d5929546..3a1d2051b4 100644 --- a/src/components/views/rooms/WhoIsTypingTile.tsx +++ b/src/components/views/rooms/WhoIsTypingTile.tsx @@ -215,7 +215,7 @@ export default class WhoIsTypingTile extends React.Component { this.props.whoIsTypingLimit, ); if (!typingString) { - return (
    ); + return null; } return ( diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx index cdd5bc6641..8c0af5e81a 100644 --- a/src/components/views/voip/DialPadModal.tsx +++ b/src/components/views/voip/DialPadModal.tsx @@ -15,17 +15,14 @@ limitations under the License. */ import * as React from "react"; -import { ensureDMExists } from "../../../createRoom"; import { _t } from "../../../languageHandler"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import AccessibleButton from "../elements/AccessibleButton"; import Field from "../elements/Field"; import DialPad from './DialPad'; import dis from '../../../dispatcher/dispatcher'; -import Modal from "../../../Modal"; -import ErrorDialog from "../../views/dialogs/ErrorDialog"; -import CallHandler from "../../../CallHandler"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { DialNumberPayload } from "../../../dispatcher/payloads/DialNumberPayload"; +import { Action } from "../../../dispatcher/actions"; interface IProps { onFinished: (boolean) => void; @@ -67,21 +64,11 @@ export default class DialpadModal extends React.PureComponent { } onDialPress = async () => { - const results = await CallHandler.sharedInstance().pstnLookup(this.state.value); - if (!results || results.length === 0 || !results[0].userid) { - Modal.createTrackedDialog('', '', ErrorDialog, { - title: _t("Unable to look up phone number"), - description: _t("There was an error looking up the phone number"), - }); - } - const userId = results[0].userid; - - const roomId = await ensureDMExists(MatrixClientPeg.get(), userId); - - dis.dispatch({ - action: 'view_room', - room_id: roomId, - }); + const payload: DialNumberPayload = { + action: Action.DialNumber, + number: this.state.value, + }; + dis.dispatch(payload); this.props.onFinished(true); } diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 9fc0b54eea..300eed2b98 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -100,6 +100,12 @@ export enum Action { */ OpenDialPad = "open_dial_pad", + /** + * Dial the phone number in the payload + * payload: DialNumberPayload + */ + DialNumber = "dial_number", + /** * Fired when CallHandler has checked for PSTN protocol support * payload: none diff --git a/src/dispatcher/payloads/DialNumberPayload.ts b/src/dispatcher/payloads/DialNumberPayload.ts new file mode 100644 index 0000000000..1b591b9f6b --- /dev/null +++ b/src/dispatcher/payloads/DialNumberPayload.ts @@ -0,0 +1,23 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { ActionPayload } from "../payloads"; +import { Action } from "../actions"; + +export interface DialNumberPayload extends ActionPayload { + action: Action.DialNumber; + number: string; +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ef9943507a..9e85ea28c8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -63,6 +63,8 @@ "Already in call": "Already in call", "You're already in a call with this person.": "You're already in a call with this person.", "You cannot place a call with yourself.": "You cannot place a call with yourself.", + "Unable to look up phone number": "Unable to look up phone number", + "There was an error looking up the phone number": "There was an error looking up the phone number", "Call in Progress": "Call in Progress", "A call is currently being placed!": "A call is currently being placed!", "Permission Required": "Permission Required", @@ -898,8 +900,6 @@ "Fill Screen": "Fill Screen", "Return to call": "Return to call", "%(name)s on hold": "%(name)s on hold", - "Unable to look up phone number": "Unable to look up phone number", - "There was an error looking up the phone number": "There was an error looking up the phone number", "Dial pad": "Dial pad", "Unknown caller": "Unknown caller", "Incoming voice call": "Incoming voice call", diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js index 53d07d0452..23254b98ab 100644 --- a/src/stores/FlairStore.js +++ b/src/stores/FlairStore.js @@ -65,6 +65,10 @@ class FlairStore extends EventEmitter { delete this._userGroups[userId]; } + cachedPublicisedGroups(userId) { + return this._userGroups[userId]; + } + getPublicisedGroupsCached(matrixClient, userId) { if (this._userGroups[userId]) { return Promise.resolve(this._userGroups[userId]); diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts index 1e3f92e788..12316ac01c 100644 --- a/test/CallHandler-test.ts +++ b/test/CallHandler-test.ts @@ -23,8 +23,10 @@ import dis from '../src/dispatcher/dispatcher'; import { CallEvent, CallState } from 'matrix-js-sdk/src/webrtc/call'; import DMRoomMap from '../src/utils/DMRoomMap'; import EventEmitter from 'events'; -import { Action } from '../src/dispatcher/actions'; import SdkConfig from '../src/SdkConfig'; +import { ActionPayload } from '../src/dispatcher/payloads'; +import { Actions } from '../src/notifications/types'; +import { Action } from '../src/dispatcher/actions'; const REAL_ROOM_ID = '$room1:example.org'; const MAPPED_ROOM_ID = '$room2:example.org'; @@ -75,6 +77,18 @@ class FakeCall extends EventEmitter { } } +function untilDispatch(waitForAction: string): Promise { + let dispatchHandle; + return new Promise(resolve => { + dispatchHandle = dis.register(payload => { + if (payload.action === waitForAction) { + dis.unregister(dispatchHandle); + resolve(payload); + } + }); + }); +} + describe('CallHandler', () => { let dmRoomMap; let callHandler; @@ -94,6 +108,21 @@ describe('CallHandler', () => { callHandler = new CallHandler(); callHandler.start(); + const realRoom = mkStubDM(REAL_ROOM_ID, '@user1:example.org'); + const mappedRoom = mkStubDM(MAPPED_ROOM_ID, '@user2:example.org'); + const mappedRoom2 = mkStubDM(MAPPED_ROOM_ID_2, '@user3:example.org'); + + MatrixClientPeg.get().getRoom = roomId => { + switch (roomId) { + case REAL_ROOM_ID: + return realRoom; + case MAPPED_ROOM_ID: + return mappedRoom; + case MAPPED_ROOM_ID_2: + return mappedRoom2; + } + }; + dmRoomMap = { getUserIdForRoomId: roomId => { if (roomId === REAL_ROOM_ID) { @@ -134,38 +163,34 @@ describe('CallHandler', () => { SdkConfig.unset(); }); + it('should look up the correct user and open the room when a phone number is dialled', async () => { + MatrixClientPeg.get().getThirdpartyUser = jest.fn().mockResolvedValue([{ + userid: '@user2:example.org', + protocol: "im.vector.protocol.sip_native", + fields: { + is_native: true, + lookup_success: true, + }, + }]); + + dis.dispatch({ + action: Action.DialNumber, + number: '01818118181', + }, true); + + const viewRoomPayload = await untilDispatch('view_room'); + expect(viewRoomPayload.room_id).toEqual(MAPPED_ROOM_ID); + }); + it('should move calls between rooms when remote asserted identity changes', async () => { - const realRoom = mkStubDM(REAL_ROOM_ID, '@user1:example.org'); - const mappedRoom = mkStubDM(MAPPED_ROOM_ID, '@user2:example.org'); - const mappedRoom2 = mkStubDM(MAPPED_ROOM_ID_2, '@user3:example.org'); - - MatrixClientPeg.get().getRoom = roomId => { - switch (roomId) { - case REAL_ROOM_ID: - return realRoom; - case MAPPED_ROOM_ID: - return mappedRoom; - case MAPPED_ROOM_ID_2: - return mappedRoom2; - } - }; - dis.dispatch({ action: 'place_call', type: PlaceCallType.Voice, room_id: REAL_ROOM_ID, }, true); - let dispatchHandle; // wait for the call to be set up - await new Promise(resolve => { - dispatchHandle = dis.register(payload => { - if (payload.action === 'call_state') { - resolve(); - } - }); - }); - dis.unregister(dispatchHandle); + await untilDispatch('call_state'); // should start off in the actual room ID it's in at the protocol level expect(callHandler.getCallForRoom(REAL_ROOM_ID)).toBe(fakeCall); diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index dc70e3f7f6..5b466b4bb0 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -309,7 +309,7 @@ describe('MessagePanel', function() { const rm = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_RoomView_myReadMarker_container'); // it should follow the
  • which wraps the event tile for event 4 - const eventContainer = ReactDOM.findDOMNode(tiles[4]).parentNode; + const eventContainer = ReactDOM.findDOMNode(tiles[4]); expect(rm.previousSibling).toEqual(eventContainer); }); @@ -365,7 +365,7 @@ describe('MessagePanel', function() { const tiles = TestUtils.scryRenderedComponentsWithType( mp, sdk.getComponent('rooms.EventTile')); const tileContainers = tiles.map(function(t) { - return ReactDOM.findDOMNode(t).parentNode; + return ReactDOM.findDOMNode(t); }); // find the
  • which wraps the read marker @@ -460,7 +460,7 @@ describe('MessagePanel', function() { />, ); const Dates = res.find(sdk.getComponent('messages.DateSeparator')); - + expect(Dates.length).toEqual(1); }); });