Overlay virtual room call events into main timeline (#9626)
* super WIP POC for merging virtual room events into main timeline * remove some debugs * c * add some todos * remove hardcoded fake virtual user * insert overlay events into main timeline without resorting main tl events * remove more debugs * add extra tick to roomview tests * RoomView test case for virtual room * test case for merged timeline * make overlay event filter generic * remove TODOs from LegacyCallEventGrouper * tidy comments * remove some newlines * test timelinepanel room timeline event handling * use newState.roomId * fix strict errors in RoomView * fix strict errors in TimelinePanel * add type * pr tweaks * strict errors * more strict fix * strict error whackamole * update ROomView tests to use rtlpull/28788/head^2
parent
1b6d753cfe
commit
6150b86421
|
@ -79,7 +79,7 @@ export default class VoipUserMapper {
|
||||||
return findDMForUser(MatrixClientPeg.get(), virtualUser);
|
return findDMForUser(MatrixClientPeg.get(), virtualUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
public nativeRoomForVirtualRoom(roomId: string): string {
|
public nativeRoomForVirtualRoom(roomId: string): string | null {
|
||||||
const cachedNativeRoomId = this.virtualToNativeRoomIdCache.get(roomId);
|
const cachedNativeRoomId = this.virtualToNativeRoomIdCache.get(roomId);
|
||||||
if (cachedNativeRoomId) {
|
if (cachedNativeRoomId) {
|
||||||
logger.log(
|
logger.log(
|
||||||
|
|
|
@ -44,13 +44,18 @@ export enum CustomCallState {
|
||||||
Missed = "missed",
|
Missed = "missed",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isCallEventType = (eventType: string): boolean =>
|
||||||
|
eventType.startsWith("m.call.") || eventType.startsWith("org.matrix.call.");
|
||||||
|
|
||||||
|
export const isCallEvent = (event: MatrixEvent): boolean => isCallEventType(event.getType());
|
||||||
|
|
||||||
export function buildLegacyCallEventGroupers(
|
export function buildLegacyCallEventGroupers(
|
||||||
callEventGroupers: Map<string, LegacyCallEventGrouper>,
|
callEventGroupers: Map<string, LegacyCallEventGrouper>,
|
||||||
events?: MatrixEvent[],
|
events?: MatrixEvent[],
|
||||||
): Map<string, LegacyCallEventGrouper> {
|
): Map<string, LegacyCallEventGrouper> {
|
||||||
const newCallEventGroupers = new Map();
|
const newCallEventGroupers = new Map();
|
||||||
events?.forEach(ev => {
|
events?.forEach(ev => {
|
||||||
if (!ev.getType().startsWith("m.call.") && !ev.getType().startsWith("org.matrix.call.")) {
|
if (!isCallEvent(ev)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -110,6 +110,8 @@ import { CallStore, CallStoreEvent } from "../../stores/CallStore";
|
||||||
import { Call } from "../../models/Call";
|
import { Call } from "../../models/Call";
|
||||||
import { RoomSearchView } from './RoomSearchView';
|
import { RoomSearchView } from './RoomSearchView';
|
||||||
import eventSearch from "../../Searching";
|
import eventSearch from "../../Searching";
|
||||||
|
import VoipUserMapper from '../../VoipUserMapper';
|
||||||
|
import { isCallEvent } from './LegacyCallEventGrouper';
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
let debuglog = function(msg: string) {};
|
let debuglog = function(msg: string) {};
|
||||||
|
@ -144,6 +146,7 @@ enum MainSplitContentType {
|
||||||
}
|
}
|
||||||
export interface IRoomState {
|
export interface IRoomState {
|
||||||
room?: Room;
|
room?: Room;
|
||||||
|
virtualRoom?: Room;
|
||||||
roomId?: string;
|
roomId?: string;
|
||||||
roomAlias?: string;
|
roomAlias?: string;
|
||||||
roomLoading: boolean;
|
roomLoading: boolean;
|
||||||
|
@ -654,7 +657,11 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
// NB: This does assume that the roomID will not change for the lifetime of
|
// NB: This does assume that the roomID will not change for the lifetime of
|
||||||
// the RoomView instance
|
// the RoomView instance
|
||||||
if (initial) {
|
if (initial) {
|
||||||
newState.room = this.context.client.getRoom(newState.roomId);
|
const virtualRoom = newState.roomId ?
|
||||||
|
await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(newState.roomId) : undefined;
|
||||||
|
|
||||||
|
newState.room = this.context.client!.getRoom(newState.roomId) || undefined;
|
||||||
|
newState.virtualRoom = virtualRoom || undefined;
|
||||||
if (newState.room) {
|
if (newState.room) {
|
||||||
newState.showApps = this.shouldShowApps(newState.room);
|
newState.showApps = this.shouldShowApps(newState.room);
|
||||||
this.onRoomLoaded(newState.room);
|
this.onRoomLoaded(newState.room);
|
||||||
|
@ -1264,7 +1271,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRoom = (room: Room) => {
|
private onRoom = async (room: Room) => {
|
||||||
if (!room || room.roomId !== this.state.roomId) {
|
if (!room || room.roomId !== this.state.roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1277,8 +1284,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const virtualRoom = await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(room.roomId);
|
||||||
this.setState({
|
this.setState({
|
||||||
room: room,
|
room: room,
|
||||||
|
virtualRoom: virtualRoom || undefined,
|
||||||
}, () => {
|
}, () => {
|
||||||
this.onRoomLoaded(room);
|
this.onRoomLoaded(room);
|
||||||
});
|
});
|
||||||
|
@ -1286,7 +1295,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
|
|
||||||
private onDeviceVerificationChanged = (userId: string) => {
|
private onDeviceVerificationChanged = (userId: string) => {
|
||||||
const room = this.state.room;
|
const room = this.state.room;
|
||||||
if (!room.currentState.getMember(userId)) {
|
if (!room?.currentState.getMember(userId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.updateE2EStatus(room);
|
this.updateE2EStatus(room);
|
||||||
|
@ -2093,7 +2102,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
hideMessagePanel = true;
|
hideMessagePanel = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let highlightedEventId = null;
|
let highlightedEventId: string | undefined;
|
||||||
if (this.state.isInitialEventHighlighted) {
|
if (this.state.isInitialEventHighlighted) {
|
||||||
highlightedEventId = this.state.initialEventId;
|
highlightedEventId = this.state.initialEventId;
|
||||||
}
|
}
|
||||||
|
@ -2102,6 +2111,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
<TimelinePanel
|
<TimelinePanel
|
||||||
ref={this.gatherTimelinePanelRef}
|
ref={this.gatherTimelinePanelRef}
|
||||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||||
|
overlayTimelineSet={this.state.virtualRoom?.getUnfilteredTimelineSet()}
|
||||||
|
overlayTimelineSetFilter={isCallEvent}
|
||||||
showReadReceipts={this.state.showReadReceipts}
|
showReadReceipts={this.state.showReadReceipts}
|
||||||
manageReadReceipts={!this.state.isPeeking}
|
manageReadReceipts={!this.state.isPeeking}
|
||||||
sendReadReceiptOnLoad={!this.state.wasContextSwitch}
|
sendReadReceiptOnLoad={!this.state.wasContextSwitch}
|
||||||
|
|
|
@ -76,6 +76,14 @@ interface IProps {
|
||||||
// a timeline representing. If it has a room, we maintain RRs etc for
|
// a timeline representing. If it has a room, we maintain RRs etc for
|
||||||
// that room.
|
// that room.
|
||||||
timelineSet: EventTimelineSet;
|
timelineSet: EventTimelineSet;
|
||||||
|
// overlay events from a second timelineset on the main timeline
|
||||||
|
// added to support virtual rooms
|
||||||
|
// events from the overlay timeline set will be added by localTimestamp
|
||||||
|
// into the main timeline
|
||||||
|
// back paging not yet supported
|
||||||
|
overlayTimelineSet?: EventTimelineSet;
|
||||||
|
// filter events from overlay timeline
|
||||||
|
overlayTimelineSetFilter?: (event: MatrixEvent) => boolean;
|
||||||
showReadReceipts?: boolean;
|
showReadReceipts?: boolean;
|
||||||
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
||||||
manageReadReceipts?: boolean;
|
manageReadReceipts?: boolean;
|
||||||
|
@ -236,14 +244,15 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
private readonly messagePanel = createRef<MessagePanel>();
|
private readonly messagePanel = createRef<MessagePanel>();
|
||||||
private readonly dispatcherRef: string;
|
private readonly dispatcherRef: string;
|
||||||
private timelineWindow?: TimelineWindow;
|
private timelineWindow?: TimelineWindow;
|
||||||
|
private overlayTimelineWindow?: TimelineWindow;
|
||||||
private unmounted = false;
|
private unmounted = false;
|
||||||
private readReceiptActivityTimer: Timer;
|
private readReceiptActivityTimer: Timer | null = null;
|
||||||
private readMarkerActivityTimer: Timer;
|
private readMarkerActivityTimer: Timer | null = null;
|
||||||
|
|
||||||
// A map of <callId, LegacyCallEventGrouper>
|
// A map of <callId, LegacyCallEventGrouper>
|
||||||
private callEventGroupers = new Map<string, LegacyCallEventGrouper>();
|
private callEventGroupers = new Map<string, LegacyCallEventGrouper>();
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
||||||
|
@ -642,7 +651,12 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
data: IRoomTimelineData,
|
data: IRoomTimelineData,
|
||||||
): void => {
|
): void => {
|
||||||
// ignore events for other timeline sets
|
// ignore events for other timeline sets
|
||||||
if (data.timeline.getTimelineSet() !== this.props.timelineSet) return;
|
if (
|
||||||
|
data.timeline.getTimelineSet() !== this.props.timelineSet
|
||||||
|
&& data.timeline.getTimelineSet() !== this.props.overlayTimelineSet
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!Thread.hasServerSideSupport && this.context.timelineRenderingType === TimelineRenderingType.Thread) {
|
if (!Thread.hasServerSideSupport && this.context.timelineRenderingType === TimelineRenderingType.Thread) {
|
||||||
if (toStartOfTimeline && !this.state.canBackPaginate) {
|
if (toStartOfTimeline && !this.state.canBackPaginate) {
|
||||||
|
@ -680,21 +694,27 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
// timeline window.
|
// timeline window.
|
||||||
//
|
//
|
||||||
// see https://github.com/vector-im/vector-web/issues/1035
|
// see https://github.com/vector-im/vector-web/issues/1035
|
||||||
this.timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => {
|
this.timelineWindow!.paginate(EventTimeline.FORWARDS, 1, false)
|
||||||
if (this.unmounted) { return; }
|
.then(() => {
|
||||||
|
if (this.overlayTimelineWindow) {
|
||||||
|
return this.overlayTimelineWindow.paginate(EventTimeline.FORWARDS, 1, false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (this.unmounted) { return; }
|
||||||
|
|
||||||
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
|
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
|
||||||
this.buildLegacyCallEventGroupers(events);
|
this.buildLegacyCallEventGroupers(events);
|
||||||
const lastLiveEvent = liveEvents[liveEvents.length - 1];
|
const lastLiveEvent = liveEvents[liveEvents.length - 1];
|
||||||
|
|
||||||
const updatedState: Partial<IState> = {
|
const updatedState: Partial<IState> = {
|
||||||
events,
|
events,
|
||||||
liveEvents,
|
liveEvents,
|
||||||
firstVisibleEventIndex,
|
firstVisibleEventIndex,
|
||||||
};
|
};
|
||||||
|
|
||||||
let callRMUpdated;
|
let callRMUpdated = false;
|
||||||
if (this.props.manageReadMarkers) {
|
if (this.props.manageReadMarkers) {
|
||||||
// when a new event arrives when the user is not watching the
|
// when a new event arrives when the user is not watching the
|
||||||
// window, but the window is in its auto-scroll mode, make sure the
|
// window, but the window is in its auto-scroll mode, make sure the
|
||||||
// read marker is visible.
|
// read marker is visible.
|
||||||
|
@ -703,28 +723,28 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
// read-marker when a remote echo of an event we have just sent takes
|
// read-marker when a remote echo of an event we have just sent takes
|
||||||
// more than the timeout on userActiveRecently.
|
// more than the timeout on userActiveRecently.
|
||||||
//
|
//
|
||||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
callRMUpdated = false;
|
callRMUpdated = false;
|
||||||
if (ev.getSender() !== myUserId && !UserActivity.sharedInstance().userActiveRecently()) {
|
if (ev.getSender() !== myUserId && !UserActivity.sharedInstance().userActiveRecently()) {
|
||||||
updatedState.readMarkerVisible = true;
|
updatedState.readMarkerVisible = true;
|
||||||
} else if (lastLiveEvent && this.getReadMarkerPosition() === 0) {
|
} else if (lastLiveEvent && this.getReadMarkerPosition() === 0) {
|
||||||
// we know we're stuckAtBottom, so we can advance the RM
|
// we know we're stuckAtBottom, so we can advance the RM
|
||||||
// immediately, to save a later render cycle
|
// immediately, to save a later render cycle
|
||||||
|
|
||||||
this.setReadMarker(lastLiveEvent.getId(), lastLiveEvent.getTs(), true);
|
this.setReadMarker(lastLiveEvent.getId() ?? null, lastLiveEvent.getTs(), true);
|
||||||
updatedState.readMarkerVisible = false;
|
updatedState.readMarkerVisible = false;
|
||||||
updatedState.readMarkerEventId = lastLiveEvent.getId();
|
updatedState.readMarkerEventId = lastLiveEvent.getId();
|
||||||
callRMUpdated = true;
|
callRMUpdated = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
this.setState<null>(updatedState, () => {
|
this.setState(updatedState as IState, () => {
|
||||||
this.messagePanel.current?.updateTimelineMinHeight();
|
this.messagePanel.current?.updateTimelineMinHeight();
|
||||||
if (callRMUpdated) {
|
if (callRMUpdated) {
|
||||||
this.props.onReadMarkerUpdated?.();
|
this.props.onReadMarkerUpdated?.();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRoomTimelineReset = (room: Room, timelineSet: EventTimelineSet): void => {
|
private onRoomTimelineReset = (room: Room, timelineSet: EventTimelineSet): void => {
|
||||||
|
@ -735,7 +755,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public canResetTimeline = () => this.messagePanel?.current.isAtBottom();
|
public canResetTimeline = () => this.messagePanel?.current?.isAtBottom();
|
||||||
|
|
||||||
private onRoomRedaction = (ev: MatrixEvent, room: Room): void => {
|
private onRoomRedaction = (ev: MatrixEvent, room: Room): void => {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
@ -1337,6 +1357,9 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number, scrollIntoView = true): void {
|
private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number, scrollIntoView = true): void {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
this.timelineWindow = new TimelineWindow(cli, this.props.timelineSet, { windowLimit: this.props.timelineCap });
|
this.timelineWindow = new TimelineWindow(cli, this.props.timelineSet, { windowLimit: this.props.timelineCap });
|
||||||
|
this.overlayTimelineWindow = this.props.overlayTimelineSet
|
||||||
|
? new TimelineWindow(cli, this.props.overlayTimelineSet, { windowLimit: this.props.timelineCap })
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const onLoaded = () => {
|
const onLoaded = () => {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
@ -1351,8 +1374,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
this.advanceReadMarkerPastMyEvents();
|
this.advanceReadMarkerPastMyEvents();
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
canBackPaginate: this.timelineWindow.canPaginate(EventTimeline.BACKWARDS),
|
canBackPaginate: !!this.timelineWindow?.canPaginate(EventTimeline.BACKWARDS),
|
||||||
canForwardPaginate: this.timelineWindow.canPaginate(EventTimeline.FORWARDS),
|
canForwardPaginate: !!this.timelineWindow?.canPaginate(EventTimeline.FORWARDS),
|
||||||
timelineLoading: false,
|
timelineLoading: false,
|
||||||
}, () => {
|
}, () => {
|
||||||
// initialise the scroll state of the message panel
|
// initialise the scroll state of the message panel
|
||||||
|
@ -1433,12 +1456,19 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
// if we've got an eventId, and the timeline exists, we can skip
|
// if we've got an eventId, and the timeline exists, we can skip
|
||||||
// the promise tick.
|
// the promise tick.
|
||||||
this.timelineWindow.load(eventId, INITIAL_SIZE);
|
this.timelineWindow.load(eventId, INITIAL_SIZE);
|
||||||
|
this.overlayTimelineWindow?.load(undefined, INITIAL_SIZE);
|
||||||
// in this branch this method will happen in sync time
|
// in this branch this method will happen in sync time
|
||||||
onLoaded();
|
onLoaded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prom = this.timelineWindow.load(eventId, INITIAL_SIZE);
|
const prom = this.timelineWindow.load(eventId, INITIAL_SIZE).then(async () => {
|
||||||
|
if (this.overlayTimelineWindow) {
|
||||||
|
// @TODO(kerrya) use timestampToEvent to load the overlay timeline
|
||||||
|
// with more correct position when main TL eventId is truthy
|
||||||
|
await this.overlayTimelineWindow.load(undefined, INITIAL_SIZE);
|
||||||
|
}
|
||||||
|
});
|
||||||
this.buildLegacyCallEventGroupers();
|
this.buildLegacyCallEventGroupers();
|
||||||
this.setState({
|
this.setState({
|
||||||
events: [],
|
events: [],
|
||||||
|
@ -1471,7 +1501,23 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// get the list of events from the timeline window and the pending event list
|
// get the list of events from the timeline window and the pending event list
|
||||||
private getEvents(): Pick<IState, "events" | "liveEvents" | "firstVisibleEventIndex"> {
|
private getEvents(): Pick<IState, "events" | "liveEvents" | "firstVisibleEventIndex"> {
|
||||||
const events: MatrixEvent[] = this.timelineWindow.getEvents();
|
const mainEvents: MatrixEvent[] = this.timelineWindow?.getEvents() || [];
|
||||||
|
const eventFilter = this.props.overlayTimelineSetFilter || Boolean;
|
||||||
|
const overlayEvents = this.overlayTimelineWindow?.getEvents().filter(eventFilter) || [];
|
||||||
|
|
||||||
|
// maintain the main timeline event order as returned from the HS
|
||||||
|
// merge overlay events at approximately the right position based on local timestamp
|
||||||
|
const events = overlayEvents.reduce((acc: MatrixEvent[], overlayEvent: MatrixEvent) => {
|
||||||
|
// find the first main tl event with a later timestamp
|
||||||
|
const index = acc.findIndex(event => event.localTimestamp > overlayEvent.localTimestamp);
|
||||||
|
// insert overlay event into timeline at approximately the right place
|
||||||
|
if (index > -1) {
|
||||||
|
acc.splice(index, 0, overlayEvent);
|
||||||
|
} else {
|
||||||
|
acc.push(overlayEvent);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [...mainEvents]);
|
||||||
|
|
||||||
// `arrayFastClone` performs a shallow copy of the array
|
// `arrayFastClone` performs a shallow copy of the array
|
||||||
// we want the last event to be decrypted first but displayed last
|
// we want the last event to be decrypted first but displayed last
|
||||||
|
@ -1483,20 +1529,20 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
client.decryptEventIfNeeded(event);
|
client.decryptEventIfNeeded(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstVisibleEventIndex = this.checkForPreJoinUISI(events);
|
const firstVisibleEventIndex = this.checkForPreJoinUISI(mainEvents);
|
||||||
|
|
||||||
// Hold onto the live events separately. The read receipt and read marker
|
// Hold onto the live events separately. The read receipt and read marker
|
||||||
// should use this list, so that they don't advance into pending events.
|
// should use this list, so that they don't advance into pending events.
|
||||||
const liveEvents = [...events];
|
const liveEvents = [...events];
|
||||||
|
|
||||||
// if we're at the end of the live timeline, append the pending events
|
// if we're at the end of the live timeline, append the pending events
|
||||||
if (!this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
if (!this.timelineWindow?.canPaginate(EventTimeline.FORWARDS)) {
|
||||||
const pendingEvents = this.props.timelineSet.getPendingEvents();
|
const pendingEvents = this.props.timelineSet.getPendingEvents();
|
||||||
events.push(...pendingEvents.filter(event => {
|
events.push(...pendingEvents.filter(event => {
|
||||||
const {
|
const {
|
||||||
shouldLiveInRoom,
|
shouldLiveInRoom,
|
||||||
threadId,
|
threadId,
|
||||||
} = this.props.timelineSet.room.eventShouldLiveIn(event, pendingEvents);
|
} = this.props.timelineSet.room!.eventShouldLiveIn(event, pendingEvents);
|
||||||
|
|
||||||
if (this.context.timelineRenderingType === TimelineRenderingType.Thread) {
|
if (this.context.timelineRenderingType === TimelineRenderingType.Thread) {
|
||||||
return threadId === this.context.threadId;
|
return threadId === this.context.threadId;
|
||||||
|
|
|
@ -17,15 +17,21 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
// eslint-disable-next-line deprecate/import
|
// eslint-disable-next-line deprecate/import
|
||||||
import { mount, ReactWrapper } from "enzyme";
|
import { mount, ReactWrapper } from "enzyme";
|
||||||
import { act } from "react-dom/test-utils";
|
|
||||||
import { mocked, MockedObject } from "jest-mock";
|
import { mocked, MockedObject } from "jest-mock";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { EventType } from "matrix-js-sdk/src/matrix";
|
import { EventType } from "matrix-js-sdk/src/matrix";
|
||||||
import { MEGOLM_ALGORITHM } from "matrix-js-sdk/src/crypto/olmlib";
|
import { MEGOLM_ALGORITHM } from "matrix-js-sdk/src/crypto/olmlib";
|
||||||
|
import { fireEvent, render } from "@testing-library/react";
|
||||||
|
|
||||||
import { stubClient, mockPlatformPeg, unmockPlatformPeg, wrapInMatrixClientContext } from "../../test-utils";
|
import {
|
||||||
|
stubClient,
|
||||||
|
mockPlatformPeg,
|
||||||
|
unmockPlatformPeg,
|
||||||
|
wrapInMatrixClientContext,
|
||||||
|
flushPromises,
|
||||||
|
} from "../../test-utils";
|
||||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||||
import { Action } from "../../../src/dispatcher/actions";
|
import { Action } from "../../../src/dispatcher/actions";
|
||||||
import { defaultDispatcher } from "../../../src/dispatcher/dispatcher";
|
import { defaultDispatcher } from "../../../src/dispatcher/dispatcher";
|
||||||
|
@ -42,6 +48,7 @@ import { DirectoryMember } from "../../../src/utils/direct-messages";
|
||||||
import { createDmLocalRoom } from "../../../src/utils/dm/createDmLocalRoom";
|
import { createDmLocalRoom } from "../../../src/utils/dm/createDmLocalRoom";
|
||||||
import { UPDATE_EVENT } from "../../../src/stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../src/stores/AsyncStore";
|
||||||
import { SdkContextClass, SDKContext } from "../../../src/contexts/SDKContext";
|
import { SdkContextClass, SDKContext } from "../../../src/contexts/SDKContext";
|
||||||
|
import VoipUserMapper from "../../../src/VoipUserMapper";
|
||||||
|
|
||||||
const RoomView = wrapInMatrixClientContext(_RoomView);
|
const RoomView = wrapInMatrixClientContext(_RoomView);
|
||||||
|
|
||||||
|
@ -67,6 +74,8 @@ describe("RoomView", () => {
|
||||||
stores = new SdkContextClass();
|
stores = new SdkContextClass();
|
||||||
stores.client = cli;
|
stores.client = cli;
|
||||||
stores.rightPanelStore.useUnitTestClient(cli);
|
stores.rightPanelStore.useUnitTestClient(cli);
|
||||||
|
|
||||||
|
jest.spyOn(VoipUserMapper.sharedInstance(), 'getVirtualRoomForRoom').mockResolvedValue(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
@ -89,7 +98,7 @@ describe("RoomView", () => {
|
||||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||||
action: Action.ViewRoom,
|
action: Action.ViewRoom,
|
||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
metricsTrigger: null,
|
metricsTrigger: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await switchedRoom;
|
await switchedRoom;
|
||||||
|
@ -98,16 +107,52 @@ describe("RoomView", () => {
|
||||||
const roomView = mount(
|
const roomView = mount(
|
||||||
<SDKContext.Provider value={stores}>
|
<SDKContext.Provider value={stores}>
|
||||||
<RoomView
|
<RoomView
|
||||||
threepidInvite={null}
|
// threepidInvite should be optional on RoomView props
|
||||||
oobData={null}
|
// it is treated as optional in RoomView
|
||||||
|
threepidInvite={undefined as any}
|
||||||
resizeNotifier={new ResizeNotifier()}
|
resizeNotifier={new ResizeNotifier()}
|
||||||
justCreatedOpts={null}
|
|
||||||
forceTimeline={false}
|
forceTimeline={false}
|
||||||
onRegistered={null}
|
|
||||||
/>
|
/>
|
||||||
</SDKContext.Provider>,
|
</SDKContext.Provider>,
|
||||||
);
|
);
|
||||||
await act(() => Promise.resolve()); // Allow state to settle
|
await flushPromises();
|
||||||
|
return roomView;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderRoomView = async (): Promise<ReturnType<typeof render>> => {
|
||||||
|
if (stores.roomViewStore.getRoomId() !== room.roomId) {
|
||||||
|
const switchedRoom = new Promise<void>(resolve => {
|
||||||
|
const subFn = () => {
|
||||||
|
if (stores.roomViewStore.getRoomId()) {
|
||||||
|
stores.roomViewStore.off(UPDATE_EVENT, subFn);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
stores.roomViewStore.on(UPDATE_EVENT, subFn);
|
||||||
|
});
|
||||||
|
|
||||||
|
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||||
|
action: Action.ViewRoom,
|
||||||
|
room_id: room.roomId,
|
||||||
|
metricsTrigger: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
await switchedRoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomView = render(
|
||||||
|
<SDKContext.Provider value={stores}>
|
||||||
|
<RoomView
|
||||||
|
// threepidInvite should be optional on RoomView props
|
||||||
|
// it is treated as optional in RoomView
|
||||||
|
threepidInvite={undefined as any}
|
||||||
|
resizeNotifier={new ResizeNotifier()}
|
||||||
|
forceTimeline={false}
|
||||||
|
onRegistered={jest.fn()}
|
||||||
|
/>
|
||||||
|
</SDKContext.Provider>,
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
return roomView;
|
return roomView;
|
||||||
};
|
};
|
||||||
const getRoomViewInstance = async (): Promise<_RoomView> =>
|
const getRoomViewInstance = async (): Promise<_RoomView> =>
|
||||||
|
@ -137,7 +182,7 @@ describe("RoomView", () => {
|
||||||
// and fake an encryption event into the room to prompt it to re-check
|
// and fake an encryption event into the room to prompt it to re-check
|
||||||
room.addLiveEvents([new MatrixEvent({
|
room.addLiveEvents([new MatrixEvent({
|
||||||
type: "m.room.encryption",
|
type: "m.room.encryption",
|
||||||
sender: cli.getUserId(),
|
sender: cli.getUserId()!,
|
||||||
content: {},
|
content: {},
|
||||||
event_id: "someid",
|
event_id: "someid",
|
||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
|
@ -155,6 +200,26 @@ describe("RoomView", () => {
|
||||||
expect(roomViewInstance.state.liveTimeline).not.toEqual(oldTimeline);
|
expect(roomViewInstance.state.liveTimeline).not.toEqual(oldTimeline);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('with virtual rooms', () => {
|
||||||
|
it("checks for a virtual room on initial load", async () => {
|
||||||
|
const { container } = await renderRoomView();
|
||||||
|
expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledWith(room.roomId);
|
||||||
|
|
||||||
|
// quick check that rendered without error
|
||||||
|
expect(container.querySelector('.mx_ErrorBoundary')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("checks for a virtual room on room event", async () => {
|
||||||
|
await renderRoomView();
|
||||||
|
expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledWith(room.roomId);
|
||||||
|
|
||||||
|
cli.emit(ClientEvent.Room, room);
|
||||||
|
|
||||||
|
// called again after room event
|
||||||
|
expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("video rooms", () => {
|
describe("video rooms", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// Make it a video room
|
// Make it a video room
|
||||||
|
@ -178,7 +243,6 @@ describe("RoomView", () => {
|
||||||
|
|
||||||
describe("for a local room", () => {
|
describe("for a local room", () => {
|
||||||
let localRoom: LocalRoom;
|
let localRoom: LocalRoom;
|
||||||
let roomView: ReactWrapper;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
localRoom = room = await createDmLocalRoom(cli, [new DirectoryMember({ user_id: "@user:example.com" })]);
|
localRoom = room = await createDmLocalRoom(cli, [new DirectoryMember({ user_id: "@user:example.com" })]);
|
||||||
|
@ -186,15 +250,15 @@ describe("RoomView", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should remove the room from the store on unmount", async () => {
|
it("should remove the room from the store on unmount", async () => {
|
||||||
roomView = await mountRoomView();
|
const { unmount } = await renderRoomView();
|
||||||
roomView.unmount();
|
unmount();
|
||||||
expect(cli.store.removeRoom).toHaveBeenCalledWith(room.roomId);
|
expect(cli.store.removeRoom).toHaveBeenCalledWith(room.roomId);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("in state NEW", () => {
|
describe("in state NEW", () => {
|
||||||
it("should match the snapshot", async () => {
|
it("should match the snapshot", async () => {
|
||||||
roomView = await mountRoomView();
|
const { container } = await renderRoomView();
|
||||||
expect(roomView.html()).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("that is encrypted", () => {
|
describe("that is encrypted", () => {
|
||||||
|
@ -208,8 +272,8 @@ describe("RoomView", () => {
|
||||||
content: {
|
content: {
|
||||||
algorithm: MEGOLM_ALGORITHM,
|
algorithm: MEGOLM_ALGORITHM,
|
||||||
},
|
},
|
||||||
user_id: cli.getUserId(),
|
user_id: cli.getUserId()!,
|
||||||
sender: cli.getUserId(),
|
sender: cli.getUserId()!,
|
||||||
state_key: "",
|
state_key: "",
|
||||||
room_id: localRoom.roomId,
|
room_id: localRoom.roomId,
|
||||||
origin_server_ts: Date.now(),
|
origin_server_ts: Date.now(),
|
||||||
|
@ -218,33 +282,32 @@ describe("RoomView", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should match the snapshot", async () => {
|
it("should match the snapshot", async () => {
|
||||||
const roomView = await mountRoomView();
|
const { container } = await renderRoomView();
|
||||||
expect(roomView.html()).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("in state CREATING should match the snapshot", async () => {
|
it("in state CREATING should match the snapshot", async () => {
|
||||||
localRoom.state = LocalRoomState.CREATING;
|
localRoom.state = LocalRoomState.CREATING;
|
||||||
roomView = await mountRoomView();
|
const { container } = await renderRoomView();
|
||||||
expect(roomView.html()).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("in state ERROR", () => {
|
describe("in state ERROR", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
localRoom.state = LocalRoomState.ERROR;
|
localRoom.state = LocalRoomState.ERROR;
|
||||||
roomView = await mountRoomView();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should match the snapshot", async () => {
|
it("should match the snapshot", async () => {
|
||||||
expect(roomView.html()).toMatchSnapshot();
|
const { container } = await renderRoomView();
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clicking retry should set the room state to new dispatch a local room event", () => {
|
it("clicking retry should set the room state to new dispatch a local room event", async () => {
|
||||||
jest.spyOn(defaultDispatcher, "dispatch");
|
jest.spyOn(defaultDispatcher, "dispatch");
|
||||||
roomView.findWhere((w: ReactWrapper) => {
|
const { getByText } = await renderRoomView();
|
||||||
return w.hasClass("mx_RoomStatusBar_unsentRetry") && w.text() === "Retry";
|
fireEvent.click(getByText('Retry'));
|
||||||
}).first().simulate("click");
|
|
||||||
expect(localRoom.state).toBe(LocalRoomState.NEW);
|
expect(localRoom.state).toBe(LocalRoomState.NEW);
|
||||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
|
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
|
||||||
action: "local_room_event",
|
action: "local_room_event",
|
||||||
|
|
|
@ -26,6 +26,8 @@ import {
|
||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
PendingEventOrdering,
|
PendingEventOrdering,
|
||||||
Room,
|
Room,
|
||||||
|
RoomEvent,
|
||||||
|
TimelineWindow,
|
||||||
} from 'matrix-js-sdk/src/matrix';
|
} from 'matrix-js-sdk/src/matrix';
|
||||||
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
|
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
|
||||||
import {
|
import {
|
||||||
|
@ -41,7 +43,8 @@ import TimelinePanel from '../../../src/components/structures/TimelinePanel';
|
||||||
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
||||||
import { MatrixClientPeg } from '../../../src/MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../src/MatrixClientPeg';
|
||||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||||
import { mkRoom, stubClient } from "../../test-utils";
|
import { isCallEvent } from '../../../src/components/structures/LegacyCallEventGrouper';
|
||||||
|
import { flushPromises, mkRoom, stubClient } from "../../test-utils";
|
||||||
|
|
||||||
const newReceipt = (eventId: string, userId: string, readTs: number, fullyReadTs: number): MatrixEvent => {
|
const newReceipt = (eventId: string, userId: string, readTs: number, fullyReadTs: number): MatrixEvent => {
|
||||||
const receiptContent = {
|
const receiptContent = {
|
||||||
|
@ -80,7 +83,7 @@ const mockEvents = (room: Room, count = 2): MatrixEvent[] => {
|
||||||
for (let index = 0; index < count; index++) {
|
for (let index = 0; index < count; index++) {
|
||||||
events.push(new MatrixEvent({
|
events.push(new MatrixEvent({
|
||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
event_id: `event_${index}`,
|
event_id: `${room.roomId}_event_${index}`,
|
||||||
type: EventType.RoomMessage,
|
type: EventType.RoomMessage,
|
||||||
user_id: "userId",
|
user_id: "userId",
|
||||||
content: MessageEvent.from(`Event${index}`).serialize().content,
|
content: MessageEvent.from(`Event${index}`).serialize().content,
|
||||||
|
@ -90,6 +93,13 @@ const mockEvents = (room: Room, count = 2): MatrixEvent[] => {
|
||||||
return events;
|
return events;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setupTestData = (): [MatrixClient, Room, MatrixEvent[]] => {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const room = mkRoom(client, "roomId");
|
||||||
|
const events = mockEvents(room);
|
||||||
|
return [client, room, events];
|
||||||
|
};
|
||||||
|
|
||||||
describe('TimelinePanel', () => {
|
describe('TimelinePanel', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
stubClient();
|
stubClient();
|
||||||
|
@ -155,9 +165,7 @@ describe('TimelinePanel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends public read receipt when enabled", () => {
|
it("sends public read receipt when enabled", () => {
|
||||||
const client = MatrixClientPeg.get();
|
const [client, room, events] = setupTestData();
|
||||||
const room = mkRoom(client, "roomId");
|
|
||||||
const events = mockEvents(room);
|
|
||||||
|
|
||||||
const getValueCopy = SettingsStore.getValue;
|
const getValueCopy = SettingsStore.getValue;
|
||||||
SettingsStore.getValue = jest.fn().mockImplementation((name: string) => {
|
SettingsStore.getValue = jest.fn().mockImplementation((name: string) => {
|
||||||
|
@ -170,9 +178,7 @@ describe('TimelinePanel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not send public read receipt when enabled", () => {
|
it("does not send public read receipt when enabled", () => {
|
||||||
const client = MatrixClientPeg.get();
|
const [client, room, events] = setupTestData();
|
||||||
const room = mkRoom(client, "roomId");
|
|
||||||
const events = mockEvents(room);
|
|
||||||
|
|
||||||
const getValueCopy = SettingsStore.getValue;
|
const getValueCopy = SettingsStore.getValue;
|
||||||
SettingsStore.getValue = jest.fn().mockImplementation((name: string) => {
|
SettingsStore.getValue = jest.fn().mockImplementation((name: string) => {
|
||||||
|
@ -202,6 +208,146 @@ describe('TimelinePanel', () => {
|
||||||
expect(props.onEventScrolledIntoView).toHaveBeenCalledWith(events[1].getId());
|
expect(props.onEventScrolledIntoView).toHaveBeenCalledWith(events[1].getId());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('onRoomTimeline', () => {
|
||||||
|
it('ignores events for other timelines', () => {
|
||||||
|
const [client, room, events] = setupTestData();
|
||||||
|
|
||||||
|
const otherTimelineSet = { room: room as Room } as EventTimelineSet;
|
||||||
|
const otherTimeline = new EventTimeline(otherTimelineSet);
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
...getProps(room, events),
|
||||||
|
onEventScrolledIntoView: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const paginateSpy = jest.spyOn(TimelineWindow.prototype, 'paginate').mockClear();
|
||||||
|
|
||||||
|
render(<TimelinePanel {...props} />);
|
||||||
|
|
||||||
|
const event = new MatrixEvent({ type: RoomEvent.Timeline });
|
||||||
|
const data = { timeline: otherTimeline, liveEvent: true };
|
||||||
|
client.emit(RoomEvent.Timeline, event, room, false, false, data);
|
||||||
|
|
||||||
|
expect(paginateSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores timeline updates without a live event', () => {
|
||||||
|
const [client, room, events] = setupTestData();
|
||||||
|
|
||||||
|
const props = getProps(room, events);
|
||||||
|
|
||||||
|
const paginateSpy = jest.spyOn(TimelineWindow.prototype, 'paginate').mockClear();
|
||||||
|
|
||||||
|
render(<TimelinePanel {...props} />);
|
||||||
|
|
||||||
|
const event = new MatrixEvent({ type: RoomEvent.Timeline });
|
||||||
|
const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: false };
|
||||||
|
client.emit(RoomEvent.Timeline, event, room, false, false, data);
|
||||||
|
|
||||||
|
expect(paginateSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores timeline where toStartOfTimeline is true', () => {
|
||||||
|
const [client, room, events] = setupTestData();
|
||||||
|
|
||||||
|
const props = getProps(room, events);
|
||||||
|
|
||||||
|
const paginateSpy = jest.spyOn(TimelineWindow.prototype, 'paginate').mockClear();
|
||||||
|
|
||||||
|
render(<TimelinePanel {...props} />);
|
||||||
|
|
||||||
|
const event = new MatrixEvent({ type: RoomEvent.Timeline });
|
||||||
|
const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: false };
|
||||||
|
const toStartOfTimeline = true;
|
||||||
|
client.emit(RoomEvent.Timeline, event, room, toStartOfTimeline, false, data);
|
||||||
|
|
||||||
|
expect(paginateSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('advances the timeline window', () => {
|
||||||
|
const [client, room, events] = setupTestData();
|
||||||
|
|
||||||
|
const props = getProps(room, events);
|
||||||
|
|
||||||
|
const paginateSpy = jest.spyOn(TimelineWindow.prototype, 'paginate').mockClear();
|
||||||
|
|
||||||
|
render(<TimelinePanel {...props} />);
|
||||||
|
|
||||||
|
const event = new MatrixEvent({ type: RoomEvent.Timeline });
|
||||||
|
const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: true };
|
||||||
|
client.emit(RoomEvent.Timeline, event, room, false, false, data);
|
||||||
|
|
||||||
|
expect(paginateSpy).toHaveBeenCalledWith(EventTimeline.FORWARDS, 1, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('advances the overlay timeline window', async () => {
|
||||||
|
const [client, room, events] = setupTestData();
|
||||||
|
|
||||||
|
const virtualRoom = mkRoom(client, "virtualRoomId");
|
||||||
|
const virtualEvents = mockEvents(virtualRoom);
|
||||||
|
const { timelineSet: overlayTimelineSet } = getProps(virtualRoom, virtualEvents);
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
...getProps(room, events),
|
||||||
|
overlayTimelineSet,
|
||||||
|
};
|
||||||
|
|
||||||
|
const paginateSpy = jest.spyOn(TimelineWindow.prototype, 'paginate').mockClear();
|
||||||
|
|
||||||
|
render(<TimelinePanel {...props} />);
|
||||||
|
|
||||||
|
const event = new MatrixEvent({ type: RoomEvent.Timeline });
|
||||||
|
const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: true };
|
||||||
|
client.emit(RoomEvent.Timeline, event, room, false, false, data);
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(paginateSpy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with overlayTimeline', () => {
|
||||||
|
it('renders merged timeline', () => {
|
||||||
|
const [client, room, events] = setupTestData();
|
||||||
|
const virtualRoom = mkRoom(client, "virtualRoomId");
|
||||||
|
const virtualCallInvite = new MatrixEvent({
|
||||||
|
type: 'm.call.invite',
|
||||||
|
room_id: virtualRoom.roomId,
|
||||||
|
event_id: `virtualCallEvent1`,
|
||||||
|
});
|
||||||
|
const virtualCallMetaEvent = new MatrixEvent({
|
||||||
|
type: 'org.matrix.call.sdp_stream_metadata_changed',
|
||||||
|
room_id: virtualRoom.roomId,
|
||||||
|
event_id: `virtualCallEvent2`,
|
||||||
|
});
|
||||||
|
const virtualEvents = [
|
||||||
|
virtualCallInvite,
|
||||||
|
...mockEvents(virtualRoom),
|
||||||
|
virtualCallMetaEvent,
|
||||||
|
];
|
||||||
|
const { timelineSet: overlayTimelineSet } = getProps(virtualRoom, virtualEvents);
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
...getProps(room, events),
|
||||||
|
overlayTimelineSet,
|
||||||
|
overlayTimelineSetFilter: isCallEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { container } = render(<TimelinePanel {...props} />);
|
||||||
|
|
||||||
|
const eventTiles = container.querySelectorAll('.mx_EventTile');
|
||||||
|
const eventTileIds = [...eventTiles].map(tileElement => tileElement.getAttribute('data-event-id'));
|
||||||
|
expect(eventTileIds).toEqual([
|
||||||
|
// main timeline events are included
|
||||||
|
events[1].getId(),
|
||||||
|
events[0].getId(),
|
||||||
|
// virtual timeline call event is included
|
||||||
|
virtualCallInvite.getId(),
|
||||||
|
// virtual call event has no tile renderer => not rendered
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("when a thread updates", () => {
|
describe("when a thread updates", () => {
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
let room: Room;
|
let room: Room;
|
||||||
|
|
|
@ -1,9 +1,824 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`RoomView for a local room in state CREATING should match the snapshot 1`] = `"<div class="mx_RoomView mx_RoomView--local"><header class="mx_RoomHeader light-panel"><div class="mx_RoomHeader_wrapper"><div class="mx_RoomHeader_avatar"><div class="mx_DecoratedRoomAvatar"><span class="mx_BaseAvatar" role="presentation"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 24px; height: 24px;" aria-hidden="true" data-testid="avatar-img"></span></div></div><div class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon"></div><div class="mx_RoomHeader_name mx_RoomHeader_name--textonly"><div dir="auto" class="mx_RoomHeader_nametext" title="@user:example.com" role="heading" aria-level="1">@user:example.com</div></div><div class="mx_RoomHeader_topic mx_RoomTopic" dir="auto"><div tabindex="0"><div><span dir="auto"></span></div></div></div></div></header><div class="mx_RoomView_body"><div class="mx_LargeLoader"><div class="mx_Spinner"><div class="mx_Spinner_icon" style="width: 45px; height: 45px;" aria-label="Loading..." role="progressbar" data-testid="spinner"></div></div><div class="mx_LargeLoader_text">We're creating a room with @user:example.com</div></div></div></div>"`;
|
exports[`RoomView for a local room in state CREATING should match the snapshot 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_RoomView mx_RoomView--local"
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
class="mx_RoomHeader light-panel"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomHeader_wrapper"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomHeader_avatar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_DecoratedRoomAvatar"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_BaseAvatar"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;"
|
||||||
|
>
|
||||||
|
U
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_image"
|
||||||
|
data-testid="avatar-img"
|
||||||
|
src="data:image/png;base64,00"
|
||||||
|
style="width: 24px; height: 24px;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="mx_RoomHeader_name mx_RoomHeader_name--textonly"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-level="1"
|
||||||
|
class="mx_RoomHeader_nametext"
|
||||||
|
dir="auto"
|
||||||
|
role="heading"
|
||||||
|
title="@user:example.com"
|
||||||
|
>
|
||||||
|
@user:example.com
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_RoomHeader_topic mx_RoomTopic"
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
dir="auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
class="mx_RoomView_body"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LargeLoader"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Spinner"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Loading..."
|
||||||
|
class="mx_Spinner_icon"
|
||||||
|
data-testid="spinner"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: 45px; height: 45px;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LargeLoader_text"
|
||||||
|
>
|
||||||
|
We're creating a room with @user:example.com
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`RoomView for a local room in state ERROR should match the snapshot 1`] = `"<div class="mx_RoomView mx_RoomView--local"><header class="mx_RoomHeader light-panel"><div class="mx_RoomHeader_wrapper"><div class="mx_RoomHeader_avatar"><div class="mx_DecoratedRoomAvatar"><span class="mx_BaseAvatar" role="presentation"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 24px; height: 24px;" aria-hidden="true" data-testid="avatar-img"></span></div></div><div class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon"></div><div class="mx_RoomHeader_name mx_RoomHeader_name--textonly"><div dir="auto" class="mx_RoomHeader_nametext" title="@user:example.com" role="heading" aria-level="1">@user:example.com</div></div><div class="mx_RoomHeader_topic mx_RoomTopic" dir="auto"><div tabindex="0"><div><span dir="auto"></span></div></div></div></div></header><main class="mx_RoomView_body"><div class="mx_RoomView_timeline"><div class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel" tabindex="-1"><div class="mx_RoomView_messageListWrapper"><ol class="mx_RoomView_MessageList" aria-live="polite" style="height: 400px;"><li class="mx_NewRoomIntro"><div class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning"><div class="mx_EventTileBubble_title">End-to-end encryption isn't enabled</div><div class="mx_EventTileBubble_subtitle"><span> Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. </span></div></div><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 33.800000000000004px; width: 52px; line-height: 52px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 52px; height: 52px;" aria-hidden="true" data-testid="avatar-img"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages"><div role="alert"><div class="mx_RoomStatusBar_unsentBadge"><div class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_highlighted mx_NotificationBadge_2char"><span class="mx_NotificationBadge_count">!</span></div></div><div><div class="mx_RoomStatusBar_unsentTitle">Some of your messages have not been sent</div></div><div class="mx_RoomStatusBar_unsentButtonBar"><div role="button" tabindex="0" class="mx_AccessibleButton mx_RoomStatusBar_unsentRetry">Retry</div></div></div></div></main></div>"`;
|
exports[`RoomView for a local room in state ERROR should match the snapshot 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_RoomView mx_RoomView--local"
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
class="mx_RoomHeader light-panel"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomHeader_wrapper"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomHeader_avatar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_DecoratedRoomAvatar"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_BaseAvatar"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;"
|
||||||
|
>
|
||||||
|
U
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_image"
|
||||||
|
data-testid="avatar-img"
|
||||||
|
src="data:image/png;base64,00"
|
||||||
|
style="width: 24px; height: 24px;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="mx_RoomHeader_name mx_RoomHeader_name--textonly"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-level="1"
|
||||||
|
class="mx_RoomHeader_nametext"
|
||||||
|
dir="auto"
|
||||||
|
role="heading"
|
||||||
|
title="@user:example.com"
|
||||||
|
>
|
||||||
|
@user:example.com
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_RoomHeader_topic mx_RoomTopic"
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
dir="auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main
|
||||||
|
class="mx_RoomView_body"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomView_timeline"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomView_messageListWrapper"
|
||||||
|
>
|
||||||
|
<ol
|
||||||
|
aria-live="polite"
|
||||||
|
class="mx_RoomView_MessageList"
|
||||||
|
style="height: 400px;"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="mx_NewRoomIntro"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_EventTileBubble_title"
|
||||||
|
>
|
||||||
|
End-to-end encryption isn't enabled
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_EventTileBubble_subtitle"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
|
||||||
|
Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.
|
||||||
|
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
aria-label="Avatar"
|
||||||
|
aria-live="off"
|
||||||
|
class="mx_AccessibleButton mx_BaseAvatar"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 33.800000000000004px; width: 52px; line-height: 52px;"
|
||||||
|
>
|
||||||
|
U
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_image"
|
||||||
|
data-testid="avatar-img"
|
||||||
|
src="data:image/png;base64,00"
|
||||||
|
style="width: 52px; height: 52px;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<h2>
|
||||||
|
@user:example.com
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
Send your first message to invite
|
||||||
|
<b>
|
||||||
|
@user:example.com
|
||||||
|
</b>
|
||||||
|
to chat
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomStatusBar_unsentBadge"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_highlighted mx_NotificationBadge_2char"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_NotificationBadge_count"
|
||||||
|
>
|
||||||
|
!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_RoomStatusBar_unsentTitle"
|
||||||
|
>
|
||||||
|
Some of your messages have not been sent
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_RoomStatusBar_unsentButtonBar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_RoomStatusBar_unsentRetry"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`RoomView for a local room in state NEW should match the snapshot 1`] = `"<div class="mx_RoomView mx_RoomView--local"><header class="mx_RoomHeader light-panel"><div class="mx_RoomHeader_wrapper"><div class="mx_RoomHeader_avatar"><div class="mx_DecoratedRoomAvatar"><span class="mx_BaseAvatar" role="presentation"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 24px; height: 24px;" aria-hidden="true" data-testid="avatar-img"></span></div></div><div class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon"></div><div class="mx_RoomHeader_name mx_RoomHeader_name--textonly"><div dir="auto" class="mx_RoomHeader_nametext" title="@user:example.com" role="heading" aria-level="1">@user:example.com</div></div><div class="mx_RoomHeader_topic mx_RoomTopic" dir="auto"><div tabindex="0"><div><span dir="auto"></span></div></div></div></div></header><main class="mx_RoomView_body"><div class="mx_RoomView_timeline"><div class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel" tabindex="-1"><div class="mx_RoomView_messageListWrapper"><ol class="mx_RoomView_MessageList" aria-live="polite" style="height: 400px;"><li class="mx_NewRoomIntro"><div class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning"><div class="mx_EventTileBubble_title">End-to-end encryption isn't enabled</div><div class="mx_EventTileBubble_subtitle"><span> Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. </span></div></div><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 33.800000000000004px; width: 52px; line-height: 52px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 52px; height: 52px;" aria-hidden="true" data-testid="avatar-img"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class="mx_MessageComposer"><div class="mx_MessageComposer_wrapper"><div class="mx_MessageComposer_row"><div class="mx_SendMessageComposer"><div class="mx_BasicMessageComposer"><div class="mx_MessageComposerFormatBar"><button type="button" aria-label="Bold" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold"></button><button type="button" aria-label="Italics" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic"></button><button type="button" aria-label="Strikethrough" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough"></button><button type="button" aria-label="Code block" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode"></button><button type="button" aria-label="Quote" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote"></button><button type="button" aria-label="Insert link" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink"></button></div><div class="mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty" contenteditable="true" tabindex="0" aria-label="Send a message…" role="textbox" aria-multiline="true" aria-autocomplete="list" aria-haspopup="listbox" dir="auto" aria-disabled="false" data-testid="basicmessagecomposer" style="--placeholder: 'Send a message…';"><div><br></div></div></div></div><div class="mx_MessageComposer_actions"><div aria-label="Emoji" role="button" tabindex="0" class="mx_AccessibleButton mx_EmojiButton mx_MessageComposer_button mx_EmojiButton_icon"></div><div aria-label="Attachment" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload"></div><div aria-label="More options" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"></div><input type="file" style="display: none;" multiple=""></div></div></div></div></main></div>"`;
|
exports[`RoomView for a local room in state NEW should match the snapshot 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_RoomView mx_RoomView--local"
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
class="mx_RoomHeader light-panel"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomHeader_wrapper"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomHeader_avatar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_DecoratedRoomAvatar"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_BaseAvatar"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;"
|
||||||
|
>
|
||||||
|
U
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_image"
|
||||||
|
data-testid="avatar-img"
|
||||||
|
src="data:image/png;base64,00"
|
||||||
|
style="width: 24px; height: 24px;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="mx_RoomHeader_name mx_RoomHeader_name--textonly"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-level="1"
|
||||||
|
class="mx_RoomHeader_nametext"
|
||||||
|
dir="auto"
|
||||||
|
role="heading"
|
||||||
|
title="@user:example.com"
|
||||||
|
>
|
||||||
|
@user:example.com
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_RoomHeader_topic mx_RoomTopic"
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
dir="auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main
|
||||||
|
class="mx_RoomView_body"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomView_timeline"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomView_messageListWrapper"
|
||||||
|
>
|
||||||
|
<ol
|
||||||
|
aria-live="polite"
|
||||||
|
class="mx_RoomView_MessageList"
|
||||||
|
style="height: 400px;"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="mx_NewRoomIntro"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_EventTileBubble_title"
|
||||||
|
>
|
||||||
|
End-to-end encryption isn't enabled
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_EventTileBubble_subtitle"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
|
||||||
|
Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.
|
||||||
|
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
aria-label="Avatar"
|
||||||
|
aria-live="off"
|
||||||
|
class="mx_AccessibleButton mx_BaseAvatar"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 33.800000000000004px; width: 52px; line-height: 52px;"
|
||||||
|
>
|
||||||
|
U
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_image"
|
||||||
|
data-testid="avatar-img"
|
||||||
|
src="data:image/png;base64,00"
|
||||||
|
style="width: 52px; height: 52px;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<h2>
|
||||||
|
@user:example.com
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
Send your first message to invite
|
||||||
|
<b>
|
||||||
|
@user:example.com
|
||||||
|
</b>
|
||||||
|
to chat
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_MessageComposer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_MessageComposer_wrapper"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_MessageComposer_row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_SendMessageComposer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_BasicMessageComposer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_MessageComposerFormatBar"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="Bold"
|
||||||
|
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label="Italics"
|
||||||
|
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label="Strikethrough"
|
||||||
|
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label="Code block"
|
||||||
|
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label="Quote"
|
||||||
|
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label="Insert link"
|
||||||
|
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-disabled="false"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-label="Send a message…"
|
||||||
|
aria-multiline="true"
|
||||||
|
class="mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty"
|
||||||
|
contenteditable="true"
|
||||||
|
data-testid="basicmessagecomposer"
|
||||||
|
dir="auto"
|
||||||
|
role="textbox"
|
||||||
|
style="--placeholder: 'Send a message…';"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_MessageComposer_actions"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Emoji"
|
||||||
|
class="mx_AccessibleButton mx_EmojiButton mx_MessageComposer_button mx_EmojiButton_icon"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-label="Attachment"
|
||||||
|
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-label="More options"
|
||||||
|
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
multiple=""
|
||||||
|
style="display: none;"
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`RoomView for a local room in state NEW that is encrypted should match the snapshot 1`] = `"<div class="mx_RoomView mx_RoomView--local"><header class="mx_RoomHeader light-panel"><div class="mx_RoomHeader_wrapper"><div class="mx_RoomHeader_avatar"><div class="mx_DecoratedRoomAvatar"><span class="mx_BaseAvatar" role="presentation"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 24px; height: 24px;" aria-hidden="true" data-testid="avatar-img"></span></div></div><div class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon"></div><div class="mx_RoomHeader_name mx_RoomHeader_name--textonly"><div dir="auto" class="mx_RoomHeader_nametext" title="@user:example.com" role="heading" aria-level="1">@user:example.com</div></div><div class="mx_RoomHeader_topic mx_RoomTopic" dir="auto"><div tabindex="0"><div><span dir="auto"></span></div></div></div></div></header><main class="mx_RoomView_body"><div class="mx_RoomView_timeline"><div class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel" tabindex="-1"><div class="mx_RoomView_messageListWrapper"><ol class="mx_RoomView_MessageList" aria-live="polite" style="height: 400px;"><div class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon"><div class="mx_EventTileBubble_title">Encryption enabled</div><div class="mx_EventTileBubble_subtitle">Messages in this chat will be end-to-end encrypted.</div></div><li class="mx_NewRoomIntro"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 33.800000000000004px; width: 52px; line-height: 52px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" style="width: 52px; height: 52px;" aria-hidden="true" data-testid="avatar-img"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class="mx_MessageComposer"><div class="mx_MessageComposer_wrapper"><div class="mx_MessageComposer_row"><div class="mx_SendMessageComposer"><div class="mx_BasicMessageComposer"><div class="mx_MessageComposerFormatBar"><button type="button" aria-label="Bold" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold"></button><button type="button" aria-label="Italics" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic"></button><button type="button" aria-label="Strikethrough" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough"></button><button type="button" aria-label="Code block" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode"></button><button type="button" aria-label="Quote" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote"></button><button type="button" aria-label="Insert link" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink"></button></div><div class="mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty" contenteditable="true" tabindex="0" aria-label="Send a message…" role="textbox" aria-multiline="true" aria-autocomplete="list" aria-haspopup="listbox" dir="auto" aria-disabled="false" data-testid="basicmessagecomposer" style="--placeholder: 'Send a message…';"><div><br></div></div></div></div><div class="mx_MessageComposer_actions"><div aria-label="Emoji" role="button" tabindex="0" class="mx_AccessibleButton mx_EmojiButton mx_MessageComposer_button mx_EmojiButton_icon"></div><div aria-label="Attachment" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload"></div><div aria-label="More options" role="button" tabindex="0" class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"></div><input type="file" style="display: none;" multiple=""></div></div></div></div></main></div>"`;
|
exports[`RoomView for a local room in state NEW that is encrypted should match the snapshot 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_RoomView mx_RoomView--local"
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
class="mx_RoomHeader light-panel"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomHeader_wrapper"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomHeader_avatar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_DecoratedRoomAvatar"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_BaseAvatar"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;"
|
||||||
|
>
|
||||||
|
U
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_image"
|
||||||
|
data-testid="avatar-img"
|
||||||
|
src="data:image/png;base64,00"
|
||||||
|
style="width: 24px; height: 24px;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="mx_RoomHeader_name mx_RoomHeader_name--textonly"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-level="1"
|
||||||
|
class="mx_RoomHeader_nametext"
|
||||||
|
dir="auto"
|
||||||
|
role="heading"
|
||||||
|
title="@user:example.com"
|
||||||
|
>
|
||||||
|
@user:example.com
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_RoomHeader_topic mx_RoomTopic"
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
dir="auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main
|
||||||
|
class="mx_RoomView_body"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomView_timeline"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomView_messageListWrapper"
|
||||||
|
>
|
||||||
|
<ol
|
||||||
|
aria-live="polite"
|
||||||
|
class="mx_RoomView_MessageList"
|
||||||
|
style="height: 400px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_EventTileBubble_title"
|
||||||
|
>
|
||||||
|
Encryption enabled
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_EventTileBubble_subtitle"
|
||||||
|
>
|
||||||
|
Messages in this chat will be end-to-end encrypted.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<li
|
||||||
|
class="mx_NewRoomIntro"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-label="Avatar"
|
||||||
|
aria-live="off"
|
||||||
|
class="mx_AccessibleButton mx_BaseAvatar"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 33.800000000000004px; width: 52px; line-height: 52px;"
|
||||||
|
>
|
||||||
|
U
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_image"
|
||||||
|
data-testid="avatar-img"
|
||||||
|
src="data:image/png;base64,00"
|
||||||
|
style="width: 52px; height: 52px;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<h2>
|
||||||
|
@user:example.com
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
Send your first message to invite
|
||||||
|
<b>
|
||||||
|
@user:example.com
|
||||||
|
</b>
|
||||||
|
to chat
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_MessageComposer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_MessageComposer_wrapper"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_MessageComposer_row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_SendMessageComposer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_BasicMessageComposer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_MessageComposerFormatBar"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="Bold"
|
||||||
|
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label="Italics"
|
||||||
|
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label="Strikethrough"
|
||||||
|
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label="Code block"
|
||||||
|
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label="Quote"
|
||||||
|
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label="Insert link"
|
||||||
|
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-disabled="false"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-label="Send a message…"
|
||||||
|
aria-multiline="true"
|
||||||
|
class="mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty"
|
||||||
|
contenteditable="true"
|
||||||
|
data-testid="basicmessagecomposer"
|
||||||
|
dir="auto"
|
||||||
|
role="textbox"
|
||||||
|
style="--placeholder: 'Send a message…';"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_MessageComposer_actions"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Emoji"
|
||||||
|
class="mx_AccessibleButton mx_EmojiButton mx_MessageComposer_button mx_EmojiButton_icon"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-label="Attachment"
|
||||||
|
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-label="More options"
|
||||||
|
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
multiple=""
|
||||||
|
style="display: none;"
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
Loading…
Reference in New Issue