Merge branch 'develop' into t3chguy/fix/6606

pull/21833/head
Michael Telatynski 2021-06-03 19:33:09 +01:00 committed by GitHub
commit 93010d34fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 317 additions and 228 deletions

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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;

View File

@ -264,7 +264,7 @@ export default class CallHandler extends EventEmitter {
}
public getSupportsVirtualRooms() {
return this.supportsPstnProtocol;
return this.supportsSipNativeVirtual;
}
public pstnLookup(phoneNumber: string): Promise<ThirdpartyLookupResponse[]> {
@ -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");

View File

@ -33,7 +33,7 @@ export default class VoipUserMapper {
private async userToVirtualUser(userId: string): Promise<string> {
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<void> {
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) {

View File

@ -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(
<li
key={mxEv.getTxnId() || eventId}
ref={this._collectEventNode.bind(this, eventId)}
data-scroll-tokens={scrollToken}
>
<TileErrorBoundary mxEvent={mxEv}>
<EventTile
mxEvent={mxEv}
continuation={continuation}
isRedacted={mxEv.isRedacted()}
replacingEventId={mxEv.replacingEventId()}
editState={isEditing && this.props.editState}
onHeightChanged={this._onHeightChanged}
readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap}
showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.getAssociatedStatus()}
tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
permalinkCreator={this.props.permalinkCreator}
last={last}
lastInSection={willWantDateSeparator}
lastSuccessful={isLastSuccessful}
isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions}
layout={this.props.layout}
enableFlair={this.props.enableFlair}
showReadReceipts={this.props.showReadReceipts}
/>
</TileErrorBoundary>
</li>,
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
<EventTile
as="li"
data-scroll-tokens={scrollToken}
ref={this._collectEventNode.bind(this, eventId)}
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
mxEvent={mxEv}
continuation={continuation}
isRedacted={mxEv.isRedacted()}
replacingEventId={mxEv.replacingEventId()}
editState={isEditing && this.props.editState}
onHeightChanged={this._onHeightChanged}
readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap}
showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.getAssociatedStatus()}
tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
permalinkCreator={this.props.permalinkCreator}
last={last}
lastInSection={willWantDateSeparator}
lastSuccessful={isLastSuccessful}
isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions}
layout={this.props.layout}
enableFlair={this.props.enableFlair}
showReadReceipts={this.props.showReadReceipts}
/>
</TileErrorBoundary>,
);
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

View File

@ -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<IProps, IState> {
}
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<IProps, IState> {
};
private onEvent = (ev) => {
if (ev.isBeingDecrypted() || ev.isDecryptionFailure() || ev.shouldAttemptDecryption()) return;
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
this.handleEffects(ev);
};

View File

@ -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.

View File

@ -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> {
</div>
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
</div>);
containerClasses = classNames("mx_ToastContainer", {
"mx_ToastContainer_stacked": isStacked,
});
}
const containerClasses = classNames("mx_ToastContainer", {
"mx_ToastContainer_stacked": isStacked,
});
return (
<div className={containerClasses} role="alert">
{toast}
</div>
);
return toast
? (
<div className={containerClasses} role="alert">
{toast}
</div>
)
: null;
}
}

View File

@ -73,7 +73,7 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
label={tooltip || title}
yOffset={yOffset}
/> : <div />;
/> : null;
return (
<AccessibleButton
{...props}

View File

@ -116,7 +116,7 @@ export default class Flair extends React.Component {
render() {
if (this.state.profiles.length === 0) {
return <span className="mx_Flair" />;
return null;
}
const avatars = this.state.profiles.map((profile, index) => {
return <FlairAvatar key={index} groupProfile={profile} />;

View File

@ -214,7 +214,7 @@ export default class ReplyThread extends React.Component {
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) {
if (!ReplyThread.getParentEventId(parentEv)) {
return <div className="mx_ReplyThread_wrapper_empty" />;
return null;
}
return <ReplyThread
parentEv={parentEv}
@ -269,36 +269,27 @@ export default class ReplyThread extends React.Component {
const {parentEv} = this.props;
// at time of making this component we checked that props.parentEv has a parentEventId
const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));
if (this.unmounted) return;
if (ev) {
const loadedEv = await this.getNextEvent(ev);
this.setState({
events: [ev],
}, this.loadNextEvent);
loadedEv,
loading: false,
});
} else {
this.setState({err: true});
}
}
async loadNextEvent() {
if (this.unmounted) return;
const ev = this.state.events[0];
const inReplyToEventId = ReplyThread.getParentEventId(ev);
if (!inReplyToEventId) {
this.setState({
loading: false,
});
return;
}
const loadedEv = await this.getEvent(inReplyToEventId);
if (this.unmounted) return;
if (loadedEv) {
this.setState({loadedEv});
} else {
this.setState({err: true});
async getNextEvent(ev) {
try {
const inReplyToEventId = ReplyThread.getParentEventId(ev);
return await this.getEvent(inReplyToEventId);
} catch (e) {
return null;
}
}
@ -326,13 +317,18 @@ export default class ReplyThread extends React.Component {
this.initialize();
}
onQuoteClick() {
async onQuoteClick() {
const events = [this.state.loadedEv, ...this.state.events];
let loadedEv = null;
if (events.length > 0) {
loadedEv = await this.getNextEvent(events[0]);
}
this.setState({
loadedEv: null,
loadedEv,
events,
}, this.loadNextEvent);
});
dis.fire(Action.FocusComposer);
}

View File

@ -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 <span />; // 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 = <div />;
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 = <span>
<span className={`mx_SenderProfile_name ${colorClass}`}>
{ nameElem }
</span>
{ flair }
</span>;
return (
<div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
<div className="mx_SenderProfile_hover">
{ nameFlair }
</div>
<div className="mx_SenderProfile mx_SenderProfile_hover" dir="auto" onClick={this.props.onClick}>
<span className={`mx_SenderProfile_name ${colorClass}`}>
{ nameElem }
</span>
{ flair }
</div>
);
}

View File

@ -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<IProps, IState> {
private suppressReadReceiptAnimation: boolean;
private isListeningForReceipts: boolean;
private ref: React.RefObject<unknown>;
private tile = React.createRef();
private replyThread = React.createRef();
@ -322,6 +331,8 @@ export default class EventTile extends React.Component<IProps, IState> {
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<IProps, IState> {
// 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<IProps, IState> {
// return early if there are no read receipts
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
return (<span className="mx_EventTile_readAvatars" />);
return null;
}
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
@ -640,6 +653,11 @@ export default class EventTile extends React.Component<IProps, IState> {
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<IProps, IState> {
}
}
return <span className="mx_EventTile_readAvatars">
{ remText }
{ avatars }
</span>;
return (
<div className="mx_EventTile_msgOption">
<span className="mx_EventTile_readAvatars">
{ remText }
{ avatars }
</span>
</div>
)
}
onSenderProfileClick = event => {
@ -953,7 +975,8 @@ export default class EventTile extends React.Component<IProps, IState> {
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 ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
const keyRequestHelpText =
@ -1016,11 +1039,7 @@ export default class EventTile extends React.Component<IProps, IState> {
let msgOption;
if (this.props.showReadReceipts) {
const readAvatars = this.getReadAvatars();
msgOption = (
<div className="mx_EventTile_msgOption">
{ readAvatars }
</div>
);
msgOption = readAvatars;
}
switch (this.props.tileShape) {
@ -1124,11 +1143,20 @@ export default class EventTile extends React.Component<IProps, IState> {
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return (
<div className={classes} tabIndex={-1} aria-live={ariaLive} aria-atomic="true">
{ ircTimestamp }
{ sender }
{ ircPadlock }
<div className="mx_EventTile_line">
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,
<div className="mx_EventTile_line" key="mx_EventTile_line">
{ groupTimestamp }
{ groupPadlock }
{ thread }
@ -1145,16 +1173,12 @@ export default class EventTile extends React.Component<IProps, IState> {
{ keyRequestInfo }
{ reactionsRow }
{ actionBar }
</div>
{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 }
</div>
);
</div>,
msgOption,
avatar,
])
)
}
}
}

View File

@ -62,13 +62,11 @@ export default class SimpleRoomHeader extends React.Component {
}
return (
<div className="mx_RoomHeader" >
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_simpleHeader">
{ icon }
{ this.props.title }
{ cancelButton }
</div>
<div className="mx_RoomHeader mx_RoomHeader_wrapper" >
<div className="mx_RoomHeader_simpleHeader">
{ icon }
{ this.props.title }
{ cancelButton }
</div>
</div>
);

View File

@ -215,7 +215,7 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
this.props.whoIsTypingLimit,
);
if (!typingString) {
return (<div className="mx_WhoIsTypingTile_empty" />);
return null;
}
return (

View File

@ -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<IProps, IState> {
}
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);
}

View File

@ -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

View File

@ -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;
}

View File

@ -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",

View File

@ -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]);

View File

@ -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<ActionPayload> {
let dispatchHandle;
return new Promise<ActionPayload>(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<void>(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);

View File

@ -309,7 +309,7 @@ describe('MessagePanel', function() {
const rm = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_RoomView_myReadMarker_container');
// it should follow the <li> 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 <li> 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);
});
});