Merge branch 'develop' into sort-imports

Signed-off-by: Aaron Raimist <aaron@raim.ist>
This commit is contained in:
Aaron Raimist 2021-12-09 08:34:20 +00:00
commit 7b94e13a84
642 changed files with 30052 additions and 8035 deletions

View file

@ -22,7 +22,6 @@ import { Room } from "matrix-js-sdk/src/models/room";
import AppTile from '../elements/AppTile';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import * as ScalarMessaging from '../../../ScalarMessaging';
import WidgetUtils from '../../../utils/WidgetUtils';
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
@ -37,6 +36,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import UIStore from "../../../stores/UIStore";
import { IApp } from "../../../stores/WidgetStore";
import { ActionPayload } from "../../../dispatcher/payloads";
import Spinner from "../elements/Spinner";
interface IProps {
userId: string;
@ -47,7 +47,8 @@ interface IProps {
}
interface IState {
apps: IApp[];
// @ts-ignore - TS wants a string key, but we know better
apps: {[id: Container]: IApp[]};
resizingVertical: boolean; // true when changing the height of the apps drawer
resizingHorizontal: boolean; // true when chagning the distribution of the width between widgets
resizing: boolean;
@ -118,7 +119,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
this.resizeContainer.classList.remove("mx_AppsDrawer_resizing");
WidgetLayoutStore.instance.setResizerDistributions(
this.props.room, Container.Top,
this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
this.topApps().slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
);
this.setState({ resizingHorizontal: false });
},
@ -148,7 +149,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
// Room has changed, update apps
this.updateApps();
} else if (this.getAppsHash(this.state.apps) !== this.getAppsHash(prevState.apps)) {
} else if (this.getAppsHash(this.topApps()) !== this.getAppsHash(prevState.apps[Container.Top])) {
this.loadResizerPreferences();
}
}
@ -163,7 +164,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
private loadResizerPreferences = (): void => {
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
if (this.state.apps && (this.topApps().length - 1) === distributions.length) {
distributions.forEach((size, i) => {
const distributor = this.resizer.forHandleAt(i);
if (distributor) {
@ -200,8 +201,16 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
break;
}
};
private getApps = (): IApp[] => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
// @ts-ignore - TS wants a string key, but we know better
private getApps = (): { [id: Container]: IApp[] } => {
// @ts-ignore
const appsDict: { [id: Container]: IApp[] } = {};
appsDict[Container.Top] = WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
appsDict[Container.Center] = WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Center);
return appsDict;
};
private topApps = (): IApp[] => this.state.apps[Container.Top];
private centerApps = (): IApp[] => this.state.apps[Container.Center];
private updateApps = (): void => {
this.setState({
@ -211,8 +220,9 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
public render(): JSX.Element {
if (!this.props.showApps) return <div />;
const apps = this.state.apps.map((app, index, arr) => {
const widgetIsMaxmised: boolean = this.centerApps().length > 0;
const appsToDisplay = widgetIsMaxmised ? this.centerApps() : this.topApps();
const apps = appsToDisplay.map((app, index, arr) => {
return (<AppTile
key={app.id}
app={app}
@ -237,39 +247,47 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
WidgetUtils.getRoomWidgets(this.props.room),
)
) {
const Loader = sdk.getComponent("elements.Spinner");
spinner = <Loader />;
spinner = <Spinner />;
}
const classes = classNames({
mx_AppsDrawer: true,
mx_AppsDrawer_maximise: widgetIsMaxmised,
mx_AppsDrawer_fullWidth: apps.length < 2,
mx_AppsDrawer_resizing: this.state.resizing,
mx_AppsDrawer_2apps: apps.length === 2,
mx_AppsDrawer_3apps: apps.length === 3,
});
const appConatiners =
<div className="mx_AppsContainer" ref={this.collectResizer}>
{ apps.map((app, i) => {
if (i < 1) return app;
return <React.Fragment key={app.key}>
<ResizeHandle reverse={i > apps.length / 2} />
{ app }
</React.Fragment>;
}) }
</div>;
let drawer;
if (widgetIsMaxmised) {
drawer = appConatiners;
} else {
drawer = <PersistentVResizer
room={this.props.room}
minHeight={100}
maxHeight={(this.props.maxHeight || !widgetIsMaxmised) ? this.props.maxHeight - 50 : undefined}
handleClass="mx_AppsContainer_resizerHandle"
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
className="mx_AppsContainer_resizer"
resizeNotifier={this.props.resizeNotifier}>
{ appConatiners }
</PersistentVResizer>;
}
return (
<div className={classes}>
<PersistentVResizer
room={this.props.room}
minHeight={100}
maxHeight={this.props.maxHeight ? this.props.maxHeight - 50 : undefined}
handleClass="mx_AppsContainer_resizerHandle"
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
className="mx_AppsContainer_resizer"
resizeNotifier={this.props.resizeNotifier}
>
<div className="mx_AppsContainer" ref={this.collectResizer}>
{ apps.map((app, i) => {
if (i < 1) return app;
return <React.Fragment key={app.key}>
<ResizeHandle reverse={i > apps.length / 2} />
{ app }
</React.Fragment>;
}) }
</div>
</PersistentVResizer>
{ drawer }
{ spinner }
</div>
);

View file

@ -23,6 +23,7 @@ import { Room } from 'matrix-js-sdk/src/models/room';
import Autocompleter, { ICompletion, ISelectionRange, IProviderCompletions } from '../../../autocomplete/Autocompleter';
import SettingsStore from "../../../settings/SettingsStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import RoomContext from '../../../contexts/RoomContext';
const MAX_PROVIDER_MATCHES = 20;
@ -56,11 +57,11 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
debounceCompletionsRequest: number;
private containerRef = createRef<HTMLDivElement>();
public static contextType = RoomContext;
constructor(props) {
super(props);
this.autocompleter = new Autocompleter(props.room);
this.state = {
// list of completionResults, each containing completions
completions: [],
@ -81,6 +82,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
}
componentDidMount() {
this.autocompleter = new Autocompleter(this.props.room, this.context.timelineRenderingType);
this.applyNewProps();
}

View file

@ -121,6 +121,7 @@ export default class AuxPanel extends React.Component<IProps, IState> {
<CallViewForRoom
roomId={this.props.room.roomId}
resizeNotifier={this.props.resizeNotifier}
showApps={this.props.showApps}
/>
);

View file

@ -94,6 +94,7 @@ function selectionEquals(a: Partial<Selection>, b: Selection): boolean {
interface IProps {
model: EditorModel;
room: Room;
threadId: string;
placeholder?: string;
label?: string;
initialCaret?: DocumentOffset;
@ -242,7 +243,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
isTyping = false;
}
}
TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, isTyping);
TypingStore.sharedInstance().setSelfTyping(
this.props.room.roomId,
this.props.threadId,
isTyping,
);
if (this.props.onChange) {
this.props.onChange();

View file

@ -206,7 +206,10 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
event: null,
timelineRenderingType: this.context.timelineRenderingType,
});
dis.fire(Action.FocusSendMessageComposer);
dis.dispatch({
action: Action.FocusSendMessageComposer,
context: this.context.timelineRenderingType,
});
}
event.preventDefault();
break;
@ -236,7 +239,10 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
event: null,
timelineRenderingType: this.context.timelineRenderingType,
});
dis.fire(Action.FocusSendMessageComposer);
dis.dispatch({
action: Action.FocusSendMessageComposer,
context: this.context.timelineRenderingType,
});
};
private get shouldSaveStoredEditorState(): boolean {
@ -314,7 +320,9 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
}
private async runSlashCommand(cmd: Command, args: string, roomId: string): Promise<void> {
const result = cmd.run(roomId, args);
const threadId = this.props.editState?.getEvent()?.getThread()?.id || null;
const result = cmd.run(roomId, threadId, args);
let messageContent;
let error = result.error;
if (result.promise) {
@ -417,7 +425,11 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
}
if (shouldSend) {
this.cancelPreviousPendingEdit();
const prom = this.props.mxClient.sendMessage(roomId, editContent);
const event = this.props.editState.getEvent();
const threadId = event.threadRootId || null;
const prom = this.props.mxClient.sendMessage(roomId, threadId, editContent);
this.clearStoredEditorState();
dis.dispatch({ action: "message_sent" });
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
@ -430,7 +442,10 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
event: null,
timelineRenderingType: this.context.timelineRenderingType,
});
dis.fire(Action.FocusSendMessageComposer);
dis.dispatch({
action: Action.FocusSendMessageComposer,
context: this.context.timelineRenderingType,
});
};
private cancelPreviousPendingEdit(): void {
@ -523,6 +538,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
ref={this.editorRef}
model={this.model}
room={this.getRoom()}
threadId={this.props.editState?.getEvent()?.getThread()?.id}
initialCaret={this.props.editState.getCaret()}
label={_t("Edit message")}
onChange={this.onChange}

View file

@ -17,7 +17,7 @@ limitations under the License.
import React, { createRef } from 'react';
import classNames from "classnames";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Relations } from "matrix-js-sdk/src/models/relations";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
@ -29,7 +29,7 @@ import { _t } from '../../../languageHandler';
import { hasText } from "../../../TextForEvent";
import * as sdk from "../../../index";
import dis from '../../../dispatcher/dispatcher';
import { Layout } from "../../../settings/Layout";
import { Layout } from "../../../settings/enums/Layout";
import { formatTime } from "../../../DateUtils";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { ALL_RULE_TYPES } from "../../../mjolnir/BanList";
@ -63,10 +63,15 @@ import { dispatchShowThreadEvent } from '../../../dispatcher/dispatch-actions/th
import { MessagePreviewStore } from '../../../stores/room-list/MessagePreviewStore';
import { TimelineRenderingType } from "../../../contexts/RoomContext";
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import Toolbar from '../../../accessibility/Toolbar';
import { POLL_START_EVENT_TYPE } from '../../../polls/consts';
import { RovingAccessibleTooltipButton } from '../../../accessibility/roving/RovingAccessibleTooltipButton';
import ThreadListContextMenu from '../context_menus/ThreadListContextMenu';
const eventTileTypes = {
[EventType.RoomMessage]: 'messages.MessageEvent',
[EventType.Sticker]: 'messages.MessageEvent',
[POLL_START_EVENT_TYPE.name]: 'messages.MessageEvent',
[EventType.KeyVerificationCancel]: 'messages.MKeyVerificationConclusion',
[EventType.KeyVerificationDone]: 'messages.MKeyVerificationConclusion',
[EventType.CallInvite]: 'messages.CallEvent',
@ -124,7 +129,7 @@ export function getHandlerTile(ev) {
// not even when showing hidden events
if (type === "m.room.message") {
const content = ev.getContent();
if (content && content.msgtype === "m.key.verification.request") {
if (content && content.msgtype === MsgType.KeyVerificationRequest) {
const client = MatrixClientPeg.get();
const me = client && client.getUserId();
if (ev.getSender() !== me && content.to !== me) {
@ -546,7 +551,7 @@ export default class EventTile extends React.Component<IProps, IState> {
}
};
private renderThreadInfo(): React.ReactNode {
private get thread(): Thread | null {
if (!SettingsStore.getValue("feature_thread")) {
return null;
}
@ -560,19 +565,55 @@ export default class EventTile extends React.Component<IProps, IState> {
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const thread = room?.threads.get(this.props.mxEvent.getId());
if (thread && !thread.ready) {
thread.addEvent(this.props.mxEvent, true);
}
if (!thread || this.props.showThreadInfo === false || thread.length === 0) {
if (!thread || thread.length === 0) {
return null;
}
const [lastEvent] = thread.events
return thread;
}
private renderThreadPanelSummary(): JSX.Element | null {
if (!this.thread) {
return null;
}
return <div className="mx_ThreadPanel_replies">
<span className="mx_ThreadPanel_repliesSummary">
{ this.thread.length }
</span>
{ this.renderThreadLastMessagePreview() }
</div>;
}
private renderThreadLastMessagePreview(): JSX.Element | null {
if (!this.thread) {
return null;
}
const [lastEvent] = this.thread.events
.filter(event => event.isThreadRelation)
.slice(-1);
const threadMessagePreview = MessagePreviewStore.instance.generatePreviewForEvent(lastEvent);
if (!threadMessagePreview || !lastEvent.sender) {
return null;
}
return <>
<MemberAvatar member={lastEvent.sender} width={24} height={24} className="mx_ThreadInfo_avatar" />
<div className="mx_ThreadInfo_content">
<span className="mx_ThreadInfo_message-preview">
{ threadMessagePreview }
</span>
</div>
</>;
}
private renderThreadInfo(): React.ReactNode {
if (!this.thread) {
return null;
}
return (
<div
className="mx_ThreadInfo"
@ -582,20 +623,12 @@ export default class EventTile extends React.Component<IProps, IState> {
);
}}
>
<span className="mx_ThreadInfo_thread-icon" />
<span className="mx_ThreadInfo_threads-amount">
{ _t("%(count)s reply", {
count: thread.length,
count: this.thread.length,
}) }
</span>
{ (threadMessagePreview && lastEvent.sender) && <>
<MemberAvatar member={lastEvent.sender} width={24} height={24} />
<div className="mx_ThreadInfo_content">
<span className="mx_ThreadInfo_message-preview">
{ threadMessagePreview }
</span>
</div>
</> }
{ this.renderThreadLastMessagePreview() }
</div>
);
}
@ -842,7 +875,8 @@ export default class EventTile extends React.Component<IProps, IState> {
if (remainder > 0) {
remText = <span className="mx_EventTile_readAvatarRemainder"
onClick={this.toggleAllReadAvatars}
style={{ right: "calc(" + toRem(-left) + " + " + receiptOffset + "px)" }}>{ remainder }+
style={{ right: "calc(" + toRem(-left) + " + " + receiptOffset + "px)" }}
aria-live="off">{ remainder }+
</span>;
}
}
@ -884,7 +918,7 @@ export default class EventTile extends React.Component<IProps, IState> {
// matrix.to, but also for it to enable routing within Element when clicked.
e.preventDefault();
dis.dispatch({
action: 'view_room',
action: Action.ViewRoom,
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
@ -955,7 +989,7 @@ export default class EventTile extends React.Component<IProps, IState> {
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
};
private onReactionsCreated = (relationType, eventType) => {
private onReactionsCreated = (relationType: string, eventType: string) => {
if (relationType !== "m.annotation" || eventType !== "m.reaction") {
return;
}
@ -1031,6 +1065,7 @@ export default class EventTile extends React.Component<IProps, IState> {
mx_EventTile_bad: isEncryptionFailure,
mx_EventTile_emote: msgtype === 'm.emote',
mx_EventTile_noSender: this.props.hideSender,
mx_EventTile_clamp: this.props.tileShape === TileShape.ThreadPanel,
});
// If the tile is in the Sending state, don't speak the message.
@ -1121,6 +1156,7 @@ export default class EventTile extends React.Component<IProps, IState> {
onFocusChange={this.onActionBarFocusChange}
isQuoteExpanded={isQuoteExpanded}
toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)}
getRelationsForEvent={this.props.getRelationsForEvent}
/> : undefined;
const showTimestamp = this.props.mxEvent.getTs()
@ -1129,8 +1165,20 @@ export default class EventTile extends React.Component<IProps, IState> {
|| this.state.hover
|| this.state.actionBarFocused);
const timestamp = showTimestamp ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const thread = room?.findThreadForEvent?.(this.props.mxEvent);
// Thread panel shows the timestamp of the last reply in that thread
const ts = this.props.tileShape !== TileShape.ThreadPanel
? this.props.mxEvent.getTs()
: thread?.lastReply.getTs();
const timestamp = showTimestamp && ts ?
<MessageTimestamp
showRelative={this.props.tileShape === TileShape.ThreadPanel}
showTwelveHour={this.props.isTwelveHour}
ts={ts}
/> : null;
const keyRequestHelpText =
<div className="mx_EventTile_keyRequestInfo_tooltip_contents">
@ -1193,6 +1241,20 @@ export default class EventTile extends React.Component<IProps, IState> {
msgOption = readAvatars;
}
const replyChain = haveTileForEvent(this.props.mxEvent) &&
ReplyChain.hasReply(this.props.mxEvent) ? (
<ReplyChain
parentEv={this.props.mxEvent}
onHeightChanged={this.props.onHeightChanged}
ref={this.replyChain}
forExport={this.props.forExport}
permalinkCreator={this.props.permalinkCreator}
layout={this.props.layout}
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
isQuoteExpanded={isQuoteExpanded}
setQuoteExpanded={this.setQuoteExpanded}
/>) : null;
switch (this.props.tileShape) {
case TileShape.Notif: {
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
@ -1224,31 +1286,22 @@ export default class EventTile extends React.Component<IProps, IState> {
onHeightChanged={this.props.onHeightChanged}
tileShape={this.props.tileShape}
editState={this.props.editState}
getRelationsForEvent={this.props.getRelationsForEvent}
/>
</div>,
]);
}
case TileShape.Thread: {
const replyChain = haveTileForEvent(this.props.mxEvent) &&
ReplyChain.hasReply(this.props.mxEvent) ? (
<ReplyChain
parentEv={this.props.mxEvent}
onHeightChanged={this.props.onHeightChanged}
ref={this.replyChain}
forExport={this.props.forExport}
permalinkCreator={this.props.permalinkCreator}
layout={this.props.layout}
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
isQuoteExpanded={isQuoteExpanded}
setQuoteExpanded={this.setQuoteExpanded}
/>) : null;
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
return React.createElement(this.props.as || "li", {
"ref": this.ref,
"className": classes,
"aria-live": ariaLive,
"aria-atomic": true,
"data-scroll-tokens": scrollToken,
"data-has-reply": !!replyChain,
"onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }),
}, [
<div className="mx_EventTile_roomName" key="mx_EventTile_roomName">
<RoomAvatar room={room} width={28} height={28} />
@ -1260,7 +1313,6 @@ export default class EventTile extends React.Component<IProps, IState> {
{ avatar }
<a href={permalink} onClick={this.onPermalinkClicked}>
{ sender }
{ timestamp }
</a>
</div>,
<div className={lineClasses} key="mx_EventTile_line">
@ -1274,12 +1326,65 @@ export default class EventTile extends React.Component<IProps, IState> {
tileShape={this.props.tileShape}
editState={this.props.editState}
replacingEventId={this.props.replacingEventId}
getRelationsForEvent={this.props.getRelationsForEvent}
/>
{ actionBar }
{ timestamp }
</div>,
reactionsRow,
]);
}
case TileShape.ThreadPanel: {
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return (
React.createElement(this.props.as || "li", {
"ref": this.ref,
"className": classes,
"tabIndex": -1,
"aria-live": ariaLive,
"aria-atomic": "true",
"data-scroll-tokens": scrollToken,
"data-layout": this.props.layout,
"data-shape": this.props.tileShape,
"data-self": isOwnEvent,
"data-has-reply": !!replyChain,
"onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }),
}, <>
{ sender }
{ avatar }
<div
className={lineClasses}
onClick={() => dispatchShowThreadEvent(this.props.mxEvent)}
key="mx_EventTile_line"
>
{ linkedTimestamp }
{ this.renderE2EPadlock() }
<div className="mx_EventTile_body">
{ MessagePreviewStore.instance.generatePreviewForEvent(this.props.mxEvent) }
</div>
{ this.renderThreadPanelSummary() }
</div>
<Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off">
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_threadButton"
title={_t("Reply in thread")}
onClick={() => dispatchShowThreadEvent(this.props.mxEvent)}
key="thread"
/>
<ThreadListContextMenu
mxEvent={this.props.mxEvent}
permalinkCreator={this.props.permalinkCreator}
onMenuToggle={this.onActionBarFocusChange}
/>
</Toolbar>
{ msgOption }
</>)
);
}
case TileShape.FileGrid: {
return React.createElement(this.props.as || "li", {
"className": classes,
@ -1296,6 +1401,7 @@ export default class EventTile extends React.Component<IProps, IState> {
tileShape={this.props.tileShape}
onHeightChanged={this.props.onHeightChanged}
editState={this.props.editState}
getRelationsForEvent={this.props.getRelationsForEvent}
/>
</div>,
<a
@ -1313,19 +1419,6 @@ export default class EventTile extends React.Component<IProps, IState> {
}
default: {
const replyChain = haveTileForEvent(this.props.mxEvent) &&
ReplyChain.hasReply(this.props.mxEvent) ? (
<ReplyChain
parentEv={this.props.mxEvent}
onHeightChanged={this.props.onHeightChanged}
ref={this.replyChain}
forExport={this.props.forExport}
permalinkCreator={this.props.permalinkCreator}
layout={this.props.layout}
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
isQuoteExpanded={isQuoteExpanded}
setQuoteExpanded={this.setQuoteExpanded}
/>) : null;
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
@ -1362,13 +1455,19 @@ export default class EventTile extends React.Component<IProps, IState> {
permalinkCreator={this.props.permalinkCreator}
onHeightChanged={this.props.onHeightChanged}
callEventGrouper={this.props.callEventGrouper}
getRelationsForEvent={this.props.getRelationsForEvent}
/>
{ keyRequestInfo }
{ actionBar }
{ this.props.layout === Layout.IRC && (reactionsRow) }
{ this.props.layout === Layout.IRC && <>
{ reactionsRow }
{ this.renderThreadInfo() }
</> }
</div>
{ this.renderThreadInfo() }
{ this.props.layout !== Layout.IRC && (reactionsRow) }
{ this.props.layout !== Layout.IRC && <>
{ reactionsRow }
{ this.renderThreadInfo() }
</> }
{ msgOption }
</>)
);
@ -1403,25 +1502,25 @@ export function haveTileForEvent(e: MatrixEvent, showHiddenEvents?: boolean) {
function E2ePadlockUndecryptable(props) {
return (
<E2ePadlock title={_t("This message cannot be decrypted")} icon="undecryptable" {...props} />
<E2ePadlock title={_t("This message cannot be decrypted")} icon={E2ePadlockIcon.Warning} {...props} />
);
}
function E2ePadlockUnverified(props) {
return (
<E2ePadlock title={_t("Encrypted by an unverified session")} icon="unverified" {...props} />
<E2ePadlock title={_t("Encrypted by an unverified session")} icon={E2ePadlockIcon.Warning} {...props} />
);
}
function E2ePadlockUnencrypted(props) {
return (
<E2ePadlock title={_t("Unencrypted")} icon="unencrypted" {...props} />
<E2ePadlock title={_t("Unencrypted")} icon={E2ePadlockIcon.Warning} {...props} />
);
}
function E2ePadlockUnknown(props) {
return (
<E2ePadlock title={_t("Encrypted by a deleted session")} icon="unknown" {...props} />
<E2ePadlock title={_t("Encrypted by a deleted session")} icon={E2ePadlockIcon.Normal} {...props} />
);
}
@ -1429,14 +1528,19 @@ function E2ePadlockUnauthenticated(props) {
return (
<E2ePadlock
title={_t("The authenticity of this encrypted message can't be guaranteed on this device.")}
icon="unauthenticated"
icon={E2ePadlockIcon.Normal}
{...props}
/>
);
}
enum E2ePadlockIcon {
Normal = "normal",
Warning = "warning",
}
interface IE2ePadlockProps {
icon: string;
icon: E2ePadlockIcon;
title: string;
}
@ -1445,7 +1549,7 @@ interface IE2ePadlockState {
}
class E2ePadlock extends React.Component<IE2ePadlockProps, IE2ePadlockState> {
constructor(props) {
constructor(props: IE2ePadlockProps) {
super(props);
this.state = {
@ -1453,15 +1557,15 @@ class E2ePadlock extends React.Component<IE2ePadlockProps, IE2ePadlockState> {
};
}
onHoverStart = () => {
private onHoverStart = (): void => {
this.setState({ hover: true });
};
onHoverEnd = () => {
private onHoverEnd = (): void => {
this.setState({ hover: false });
};
render() {
public render(): JSX.Element {
let tooltip = null;
if (this.state.hover) {
tooltip = <Tooltip className="mx_EventTile_e2eIcon_tooltip" label={this.props.title} />;

View file

@ -18,15 +18,6 @@ limitations under the License.
*/
import React from 'react';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { Room } from 'matrix-js-sdk/src/models/room';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import { RoomState } from 'matrix-js-sdk/src/models/room-state';
import { User } from "matrix-js-sdk/src/models/user";
import { throttle } from 'lodash';
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import dis from '../../../dispatcher/dispatcher';
@ -39,6 +30,11 @@ import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import SettingsStore from "../../../settings/SettingsStore";
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { Room } from 'matrix-js-sdk/src/models/room';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import { RoomState } from 'matrix-js-sdk/src/models/room-state';
import { User } from "matrix-js-sdk/src/models/user";
import TruncatedList from '../elements/TruncatedList';
import Spinner from "../elements/Spinner";
import SearchBox from "../../structures/SearchBox";
@ -46,11 +42,11 @@ import AccessibleButton from '../elements/AccessibleButton';
import EntityTile from "./EntityTile";
import MemberTile from "./MemberTile";
import BaseAvatar from '../avatars/BaseAvatar';
import SpaceStore from "../../../stores/SpaceStore";
import { throttle } from 'lodash';
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
const getSearchQueryLSKey = (roomId: string) => `mx_MemberList_searchQuarry_${roomId}`;
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
const INITIAL_LOAD_NUM_MEMBERS = 30;
const INITIAL_LOAD_NUM_INVITED = 5;
@ -62,7 +58,9 @@ const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g;
interface IProps {
roomId: string;
searchQuery: string;
onClose(): void;
onSearchQueryChanged: (query: string) => void;
}
interface IState {
@ -73,7 +71,6 @@ interface IState {
canInvite: boolean;
truncateAtJoined: number;
truncateAtInvited: number;
searchQuery: string;
}
@replaceableComponent("views.rooms.MemberList")
@ -182,27 +179,19 @@ export default class MemberList extends React.Component<IProps, IState> {
}
private getMembersState(members: Array<RoomMember>): IState {
let searchQuery;
try {
searchQuery = window.localStorage.getItem(getSearchQueryLSKey(this.props.roomId));
} catch (error) {
logger.warn("Failed to get last the MemberList search query", error);
}
// set the state after determining showPresence to make sure it's
// taken into account while rendering
return {
loading: false,
members: members,
filteredJoinedMembers: this.filterMembers(members, 'join', searchQuery),
filteredInvitedMembers: this.filterMembers(members, 'invite', searchQuery),
filteredJoinedMembers: this.filterMembers(members, 'join', this.props.searchQuery),
filteredInvitedMembers: this.filterMembers(members, 'invite', this.props.searchQuery),
canInvite: this.canInvite,
// ideally we'd size this to the page height, but
// in practice I find that a little constraining
truncateAtJoined: INITIAL_LOAD_NUM_MEMBERS,
truncateAtInvited: INITIAL_LOAD_NUM_INVITED,
searchQuery: searchQuery ?? "",
};
}
@ -266,8 +255,8 @@ export default class MemberList extends React.Component<IProps, IState> {
this.setState({
loading: false,
members: members,
filteredJoinedMembers: this.filterMembers(members, 'join', this.state.searchQuery),
filteredInvitedMembers: this.filterMembers(members, 'invite', this.state.searchQuery),
filteredJoinedMembers: this.filterMembers(members, 'join', this.props.searchQuery),
filteredInvitedMembers: this.filterMembers(members, 'invite', this.props.searchQuery),
});
}
@ -432,14 +421,8 @@ export default class MemberList extends React.Component<IProps, IState> {
};
private onSearchQueryChanged = (searchQuery: string): void => {
try {
window.localStorage.setItem(getSearchQueryLSKey(this.props.roomId), searchQuery);
} catch (error) {
logger.warn("Failed to set the last MemberList search query", error);
}
this.props.onSearchQueryChanged(searchQuery);
this.setState({
searchQuery,
filteredJoinedMembers: this.filterMembers(this.state.members, 'join', searchQuery),
filteredInvitedMembers: this.filterMembers(this.state.members, 'invite', searchQuery),
});
@ -579,7 +562,7 @@ export default class MemberList extends React.Component<IProps, IState> {
className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
placeholder={_t('Filter room members')}
onSearch={this.onSearchQueryChanged}
initialValue={this.state.searchQuery}
initialValue={this.props.searchQuery}
/>
);

View file

@ -15,13 +15,11 @@ limitations under the License.
*/
import React, { createRef } from 'react';
import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { MatrixEvent, IEventRelation } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { RelationType } from 'matrix-js-sdk/src/@types/event';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import { ActionPayload } from "../../../dispatcher/payloads";
import Stickerpicker from './Stickerpicker';
@ -29,16 +27,14 @@ import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalin
import ContentMessages from '../../../ContentMessages';
import E2EIcon from './E2EIcon';
import SettingsStore from "../../../settings/SettingsStore";
import {
import ContextMenu, {
aboveLeftOf,
ContextMenu,
useContextMenu,
MenuItem,
AboveLeftOf,
} from "../../structures/ContextMenu";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import ReplyPreview from "./ReplyPreview";
import { UIFeature } from "../../../settings/UIFeature";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
@ -52,25 +48,22 @@ import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInse
import { Action } from "../../../dispatcher/actions";
import EditorModel from "../../../editor/model";
import EmojiPicker from '../emojipicker/EmojiPicker';
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
import LocationPicker from '../location/LocationPicker';
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
import Modal from "../../../Modal";
import InfoDialog from "../dialogs/InfoDialog";
import { RelationType } from 'matrix-js-sdk/src/@types/event';
import RoomContext from '../../../contexts/RoomContext';
import { POLL_START_EVENT_TYPE } from "../../../polls/consts";
import ErrorDialog from "../dialogs/ErrorDialog";
import PollCreateDialog from "../elements/PollCreateDialog";
import { MsgType } from "matrix-js-sdk/src/@types/event";
import { logger } from "matrix-js-sdk/src/logger";
import LocationShareType from "../location/LocationShareType";
import { SettingUpdatedPayload } from "../../../dispatcher/payloads/SettingUpdatedPayload";
let instanceCount = 0;
const NARROW_MODE_BREAKPOINT = 500;
interface IComposerAvatarProps {
me: RoomMember;
}
function ComposerAvatar(props: IComposerAvatarProps) {
return <div className="mx_MessageComposer_avatar">
<MemberStatusMessageAvatar member={props.me} width={24} height={24} />
</div>;
}
interface ISendButtonProps {
onClick: () => void;
title?: string; // defaults to something generic
@ -88,7 +81,7 @@ function SendButton(props: ISendButtonProps) {
interface IEmojiButtonProps {
addEmoji: (unicode: string) => boolean;
menuPosition: any; // TODO: Types
menuPosition: AboveLeftOf;
narrowMode: boolean;
}
@ -125,8 +118,49 @@ const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition, narr
</React.Fragment>;
};
interface ILocationButtonProps {
room: Room;
shareLocation: (uri: string, ts: number, type: LocationShareType, description: string) => boolean;
menuPosition: AboveLeftOf;
narrowMode: boolean;
}
const LocationButton: React.FC<ILocationButtonProps> = ({ shareLocation, menuPosition, narrowMode }) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
let contextMenu;
if (menuDisplayed) {
const position = menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect());
contextMenu = <ContextMenu {...position} onFinished={closeMenu} managed={false}>
<LocationPicker onChoose={shareLocation} onFinished={closeMenu} />
</ContextMenu>;
}
const className = classNames(
"mx_MessageComposer_button",
"mx_MessageComposer_location",
{
"mx_MessageComposer_button_highlight": menuDisplayed,
},
);
// TODO: replace ContextMenuTooltipButton with a unified representation of
// the header buttons and the right panel buttons
return <React.Fragment>
<AccessibleTooltipButton
className={className}
onClick={openMenu}
title={!narrowMode && _t('Share location')}
label={narrowMode ? _t('Share location') : null}
/>
{ contextMenu }
</React.Fragment>;
};
interface IUploadButtonProps {
roomId: string;
relation?: IEventRelation | null;
}
class UploadButton extends React.Component<IUploadButtonProps> {
@ -168,7 +202,7 @@ class UploadButton extends React.Component<IUploadButtonProps> {
}
ContentMessages.sharedInstance().sendContentListToRoom(
tfiles, this.props.roomId, MatrixClientPeg.get(),
tfiles, this.props.roomId, this.props.relation, MatrixClientPeg.get(),
);
// This is the onChange handler for a file form control, but we're
@ -198,18 +232,34 @@ class UploadButton extends React.Component<IUploadButtonProps> {
}
}
// TODO: [polls] Make this component actually do something
class PollButton extends React.PureComponent {
interface IPollButtonProps {
room: Room;
}
class PollButton extends React.PureComponent<IPollButtonProps> {
private onCreateClick = () => {
Modal.createTrackedDialog('Polls', 'Not Yet Implemented', InfoDialog, {
// XXX: Deliberately not translated given this dialog is meant to be replaced and we don't
// want to clutter the language files with short-lived strings.
title: "Polls are currently in development",
description: "" +
"Thanks for testing polls! We haven't quite gotten a chance to write the feature yet " +
"though. Check back later for updates.",
hasCloseButton: true,
});
const canSend = this.props.room.currentState.maySendEvent(
POLL_START_EVENT_TYPE.name,
MatrixClientPeg.get().getUserId(),
);
if (!canSend) {
Modal.createTrackedDialog('Polls', 'permissions error: cannot start', ErrorDialog, {
title: _t("Permission Required"),
description: _t("You do not have permission to start polls in this room."),
});
} else {
Modal.createTrackedDialog(
'Polls',
'create',
PollCreateDialog,
{
room: this.props.room,
},
'mx_CompoundDialog',
false, // isPriorityModal
true, // isStaticModal
);
}
};
render() {
@ -243,6 +293,8 @@ interface IState {
narrowMode?: boolean;
isMenuOpen: boolean;
showStickers: boolean;
showStickersButton: boolean;
showPollsButton: boolean;
}
@replaceableComponent("views.rooms.MessageComposer")
@ -272,9 +324,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
recordingTimeLeftSeconds: null, // when set to a number, shows a toast
isMenuOpen: false,
showStickers: false,
showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"),
showPollsButton: SettingsStore.getValue("feature_polls"),
};
this.instanceId = instanceCount++;
SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null);
SettingsStore.monitorSetting("feature_polls", null);
}
componentDidMount() {
@ -297,14 +354,39 @@ export default class MessageComposer extends React.Component<IProps, IState> {
};
private onAction = (payload: ActionPayload) => {
if (payload.action === 'reply_to_event' && payload.context === this.context.timelineRenderingType) {
// add a timeout for the reply preview to be rendered, so
// that the ScrollPanel listening to the resizeNotifier can
// correctly measure it's new height and scroll down to keep
// at the bottom if it already is
setTimeout(() => {
this.props.resizeNotifier.notifyTimelineHeightChanged();
}, 100);
switch (payload.action) {
case "reply_to_event":
if (payload.context === this.context.timelineRenderingType) {
// add a timeout for the reply preview to be rendered, so
// that the ScrollPanel listening to the resizeNotifier can
// correctly measure it's new height and scroll down to keep
// at the bottom if it already is
setTimeout(() => {
this.props.resizeNotifier.notifyTimelineHeightChanged();
}, 100);
}
break;
case Action.SettingUpdated: {
const settingUpdatedPayload = payload as SettingUpdatedPayload;
switch (settingUpdatedPayload.settingName) {
case "MessageComposerInput.showStickersButton": {
const showStickersButton = SettingsStore.getValue("MessageComposerInput.showStickersButton");
if (this.state.showStickersButton !== showStickersButton) {
this.setState({ showStickersButton });
}
break;
}
case "feature_polls": {
const showPollsButton = SettingsStore.getValue("feature_polls");
if (this.state.showPollsButton !== showPollsButton) {
this.setState({ showPollsButton });
}
break;
}
}
}
}
};
@ -360,9 +442,9 @@ export default class MessageComposer extends React.Component<IProps, IState> {
if (createEvent && createEvent.getId()) createEventId = createEvent.getId();
}
const viaServers = [this.state.tombstone.getSender().split(':').splice(1).join(':')];
const viaServers = [this.state.tombstone.getSender().split(':').slice(1).join(':')];
dis.dispatch({
action: 'view_room',
action: Action.ViewRoom,
highlighted: true,
event_id: createEventId,
room_id: replacementRoomId,
@ -409,6 +491,25 @@ export default class MessageComposer extends React.Component<IProps, IState> {
return true;
};
private shareLocation = (uri: string, ts: number, type: LocationShareType, description: string): boolean => {
if (!uri) return false;
try {
const text = `${description ? description : 'Location'} at ${uri} as of ${new Date(ts).toISOString()}`;
// noinspection ES6MissingAwait - we don't care if it fails, it'll get queued.
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
"body": text,
"msgtype": MsgType.Location,
"geo_uri": uri,
"org.matrix.msc3488.location": { uri, description },
"org.matrix.msc3488.ts": ts,
// TODO: MSC1767 fallbacks for text & thumbnail
});
} catch (e) {
logger.error("Error sending location:", e);
}
return true;
};
private sendMessage = async () => {
if (this.state.haveRecording && this.voiceRecordingButton.current) {
// There shouldn't be any text message to send when a voice recording is active, so
@ -446,9 +547,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
};
private shouldShowStickerPicker = (): boolean => {
return SettingsStore.getValue(UIFeature.Widgets)
&& SettingsStore.getValue("MessageComposerInput.showStickersButton")
&& !this.state.haveRecording;
return this.state.showStickersButton && !this.state.haveRecording;
};
private showStickers = (showStickers: boolean) => {
@ -462,16 +561,33 @@ export default class MessageComposer extends React.Component<IProps, IState> {
};
private renderButtons(menuPosition): JSX.Element | JSX.Element[] {
let uploadButtonIndex = 0;
const buttons: JSX.Element[] = [];
if (!this.state.haveRecording) {
if (SettingsStore.getValue("feature_polls")) {
if (this.state.showPollsButton) {
buttons.push(
<PollButton key="polls" />,
<PollButton key="polls" room={this.props.room} />,
);
}
uploadButtonIndex = buttons.length;
buttons.push(
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
<UploadButton
key="controls_upload"
roomId={this.props.room.roomId}
relation={this.props.relation}
/>,
);
if (SettingsStore.getValue("feature_location_share")) {
buttons.push(
<LocationButton
key="location"
room={this.props.room}
shareLocation={this.shareLocation}
menuPosition={menuPosition}
narrowMode={this.state.narrowMode}
/>,
);
}
buttons.push(
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} menuPosition={menuPosition} narrowMode={this.state.narrowMode} />,
);
@ -514,7 +630,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
});
return <>
{ buttons[0] }
{ buttons[uploadButtonIndex] }
<AccessibleTooltipButton
className={classnames}
onClick={this.toggleButtonMenu}
@ -544,7 +660,6 @@ export default class MessageComposer extends React.Component<IProps, IState> {
render() {
const controls = [
this.state.me && !this.props.compact ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
this.props.e2eStatus ?
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" /> :
null,
@ -615,9 +730,15 @@ export default class MessageComposer extends React.Component<IProps, IState> {
yOffset={-50}
/>;
}
const threadId = this.props.relation?.rel_type === RelationType.Thread
? this.props.relation.event_id
: null;
controls.push(
<Stickerpicker
room={this.props.room}
threadId={threadId}
showStickers={this.state.showStickers}
setShowStickers={this.showStickers}
menuPosition={menuPosition}
@ -631,6 +752,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
"mx_MessageComposer": true,
"mx_GroupLayout": true,
"mx_MessageComposer--compact": this.props.compact,
"mx_MessageComposer_e2eStatus": this.props.e2eStatus != undefined,
});
return (

View file

@ -31,7 +31,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import dis from "../../../dispatcher/dispatcher";
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
import { Action } from "../../../dispatcher/actions";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { showSpaceInvite } from "../../../utils/space";
import { privateShouldBeEncrypted } from "../../../createRoom";
import EventTileBubble from "../messages/EventTileBubble";
@ -126,12 +126,12 @@ const NewRoomIntro = () => {
});
}
let parentSpace;
let parentSpace: Room;
if (
SpaceStore.instance.activeSpace?.canInvite(cli.getUserId()) &&
SpaceStore.instance.activeSpaceRoom?.canInvite(cli.getUserId()) &&
SpaceStore.instance.getSpaceFilteredRoomIds(SpaceStore.instance.activeSpace).has(room.roomId)
) {
parentSpace = SpaceStore.instance.activeSpace;
parentSpace = SpaceStore.instance.activeSpaceRoom;
}
let buttons;

View file

@ -21,7 +21,7 @@ import { formatCount } from "../../../utils/FormattingUtils";
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import { XOR } from "../../../@types/common";
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
import { NotificationState, NotificationStateEvents } from "../../../stores/notifications/NotificationState";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Tooltip from "../elements/Tooltip";
import { _t } from "../../../languageHandler";
@ -61,7 +61,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
constructor(props: IProps) {
super(props);
this.props.notification.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.props.notification.on(NotificationStateEvents.Update, this.onNotificationUpdate);
this.state = {
showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId),
@ -81,15 +81,15 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
public componentWillUnmount() {
SettingsStore.unwatchSetting(this.countWatcherRef);
this.props.notification.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.props.notification.off(NotificationStateEvents.Update, this.onNotificationUpdate);
}
public componentDidUpdate(prevProps: Readonly<IProps>) {
if (prevProps.notification) {
prevProps.notification.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
prevProps.notification.off(NotificationStateEvents.Update, this.onNotificationUpdate);
}
this.props.notification.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.props.notification.on(NotificationStateEvents.Update, this.onNotificationUpdate);
}
private countPreferenceChanged = () => {

View file

@ -20,6 +20,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import AccessibleButton from "../elements/AccessibleButton";
import MessageEvent from "../messages/MessageEvent";
import MemberAvatar from "../avatars/MemberAvatar";
@ -45,7 +46,7 @@ export default class PinnedEventTile extends React.Component<IProps> {
private onTileClicked = () => {
dis.dispatch({
action: 'view_room',
action: Action.ViewRoom,
event_id: this.props.event.getId(),
highlighted: true,
room_id: this.props.event.getRoomId(),

View file

@ -204,6 +204,7 @@ export default class ReadReceiptMarker extends React.PureComponent<IProps, IStat
member={this.props.member}
fallbackUserId={this.props.fallbackUserId}
aria-hidden="true"
aria-live="off"
width={14}
height={14}
resizeMethod="crop"

View file

@ -0,0 +1,73 @@
/*
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 React, { useRef } from "react";
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { MenuItem } from "../../structures/ContextMenu";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import { _t } from "../../../languageHandler";
import RoomAvatar from "../avatars/RoomAvatar";
import dis from "../../../dispatcher/dispatcher";
import InteractiveTooltip, { Direction } from "../elements/InteractiveTooltip";
import { roomContextDetailsText } from "../../../Rooms";
const RecentlyViewedButton = () => {
const tooltipRef = useRef<InteractiveTooltip>();
const crumbs = useEventEmitterState(BreadcrumbsStore.instance, UPDATE_EVENT, () => BreadcrumbsStore.instance.rooms);
const content = <div className="mx_RecentlyViewedButton_ContextMenu">
<h4>{ _t("Recently viewed") }</h4>
<div>
{ crumbs.map(crumb => {
const contextDetails = roomContextDetailsText(crumb);
return <MenuItem
key={crumb.roomId}
onClick={() => {
dis.dispatch({
action: "view_room",
room_id: crumb.roomId,
});
tooltipRef.current?.hideTooltip();
}}
>
<RoomAvatar room={crumb} width={24} height={24} />
<span className="mx_RecentlyViewedButton_entry_label">
<div>{ crumb.name }</div>
{ contextDetails && <div className="mx_RecentlyViewedButton_entry_spaces">
{ contextDetails }
</div> }
</span>
</MenuItem>;
}) }
</div>
</div>;
return <InteractiveTooltip content={content} direction={Direction.Right} ref={tooltipRef}>
{ ({ ref, onMouseOver }) => (
<span
className="mx_LeftPanel_recentsButton"
title={_t("Recently viewed")}
ref={ref}
onMouseOver={onMouseOver}
/>
) }
</InteractiveTooltip>;
};
export default RecentlyViewedButton;

View file

@ -16,21 +16,22 @@ limitations under the License.
import React, { createRef } from 'react';
import classNames from 'classnames';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType, MsgType } from 'matrix-js-sdk/src/@types/event';
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import { Action } from '../../../dispatcher/actions';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import SenderProfile from "../messages/SenderProfile";
import MImageReplyBody from "../messages/MImageReplyBody";
import * as sdk from '../../../index';
import { EventType, MsgType } from 'matrix-js-sdk/src/@types/event';
import { replaceableComponent } from '../../../utils/replaceableComponent';
import { getEventDisplayInfo, isVoiceMessage } from '../../../utils/EventUtils';
import MFileBody from "../messages/MFileBody";
import MVoiceMessageBody from "../messages/MVoiceMessageBody";
import { logger } from "matrix-js-sdk/src/logger";
interface IProps {
mxEvent: MatrixEvent;
permalinkCreator?: RoomPermalinkCreator;
@ -90,7 +91,7 @@ export default class ReplyTile extends React.PureComponent<IProps> {
this.props.toggleExpandedQuote();
} else {
dis.dispatch({
action: 'view_room',
action: Action.ViewRoom,
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),

View file

@ -19,6 +19,7 @@ import { Room } from 'matrix-js-sdk/src';
import classNames from 'classnames';
import dis from '../../../dispatcher/dispatcher';
import { Action } from '../../../dispatcher/actions';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import RoomDetailRow from "./RoomDetailRow";
@ -39,7 +40,7 @@ export default class RoomDetailList extends React.Component<IProps> {
private onDetailsClick = (ev: React.MouseEvent, room: Room): void => {
dis.dispatch({
action: 'view_room',
action: Action.ViewRoom,
room_id: room.roomId,
room_alias: room.getCanonicalAlias() || (room.getAltAliases() || [])[0],
});

View file

@ -17,11 +17,9 @@ limitations under the License.
import React from 'react';
import classNames from 'classnames';
import { throttle } from 'lodash';
import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
import E2EIcon from './E2EIcon';
@ -29,13 +27,19 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import RoomTopic from "../elements/RoomTopic";
import RoomName from "../elements/RoomName";
import { PlaceCallType } from "../../../CallHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Modal from '../../../Modal';
import InfoDialog from "../dialogs/InfoDialog";
import { throttle } from 'lodash';
import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src';
import { E2EStatus } from '../../../utils/ShieldUtils';
import { IOOBData } from '../../../stores/ThreepidInviteStore';
import { SearchScope } from './SearchBar';
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { ContextMenuTooltipButton } from '../../structures/ContextMenu';
import RoomContextMenu from "../context_menus/RoomContextMenu";
import { contextMenuBelow } from './RoomTile';
import { RoomNotificationStateStore } from '../../../stores/notifications/RoomNotificationStateStore';
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
import { NotificationStateEvents } from '../../../stores/notifications/NotificationState';
export interface ISearchInfo {
searchTerm: string;
@ -47,23 +51,35 @@ interface IProps {
room: Room;
oobData?: IOOBData;
inRoom: boolean;
onSettingsClick: () => void;
onSearchClick: () => void;
onForgetClick: () => void;
onCallPlaced: (type: PlaceCallType) => void;
onCallPlaced: (type: CallType) => void;
onAppsClick: () => void;
e2eStatus: E2EStatus;
appsShown: boolean;
searchInfo: ISearchInfo;
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
}
interface IState {
contextMenuPosition?: DOMRect;
}
@replaceableComponent("views.rooms.RoomHeader")
export default class RoomHeader extends React.Component<IProps> {
export default class RoomHeader extends React.Component<IProps, IState> {
static defaultProps = {
editing: false,
inRoom: false,
excludedRightPanelPhaseButtons: [],
};
constructor(props, context) {
super(props, context);
const notiStore = RoomNotificationStateStore.instance.getRoomState(props.room);
notiStore.on(NotificationStateEvents.Update, this.onNotificationUpdate);
this.state = {};
}
public componentDidMount() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this.onRoomStateEvents);
@ -74,6 +90,8 @@ export default class RoomHeader extends React.Component<IProps> {
if (cli) {
cli.removeListener("RoomState.events", this.onRoomStateEvents);
}
const notiStore = RoomNotificationStateStore.instance.getRoomState(this.props.room);
notiStore.removeListener(NotificationStateEvents.Update, this.onNotificationUpdate);
}
private onRoomStateEvents = (event: MatrixEvent, state: RoomState) => {
@ -85,17 +103,24 @@ export default class RoomHeader extends React.Component<IProps> {
this.rateLimitedUpdate();
};
private onNotificationUpdate = () => {
this.forceUpdate();
};
private rateLimitedUpdate = throttle(() => {
this.forceUpdate();
}, 500, { leading: true, trailing: true });
private displayInfoDialogAboutScreensharing() {
Modal.createDialog(InfoDialog, {
title: _t("Screen sharing is here!"),
description: _t("You can now share your screen by pressing the \"screen share\" " +
"button during a call. You can even do this in audio calls if both sides support it!"),
});
}
private onContextMenuOpenClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target as HTMLButtonElement;
this.setState({ contextMenuPosition: target.getBoundingClientRect() });
};
private onContextMenuCloseClick = () => {
this.setState({ contextMenuPosition: null });
};
public render() {
let searchStatus = null;
@ -127,17 +152,35 @@ export default class RoomHeader extends React.Component<IProps> {
oobName = this.props.oobData.name;
}
let contextMenu: JSX.Element;
if (this.state.contextMenuPosition && this.props.room) {
contextMenu = (
<RoomContextMenu
{...contextMenuBelow(this.state.contextMenuPosition)}
room={this.props.room}
onFinished={this.onContextMenuCloseClick}
/>
);
}
const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
const name =
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
const name = (
<ContextMenuTooltipButton
className="mx_RoomHeader_name"
onClick={this.onContextMenuOpenClick}
isExpanded={!!this.state.contextMenuPosition}
title={_t("Room options")}
>
<RoomName room={this.props.room}>
{ (name) => {
const roomName = name || oobName;
return <div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>;
} }
</RoomName>
{ searchStatus }
</div>;
{ this.props.room && <div className="mx_RoomHeader_chevron" /> }
{ contextMenu }
</ContextMenuTooltipButton>
);
const topicElement = <RoomTopic room={this.props.room}>
{ (topic, ref) => <div className="mx_RoomHeader_topic" ref={ref} title={topic} dir="auto">
@ -149,7 +192,7 @@ export default class RoomHeader extends React.Component<IProps> {
if (this.props.room) {
roomAvatar = <DecoratedRoomAvatar
room={this.props.room}
avatarSize={32}
avatarSize={24}
oobData={this.props.oobData}
viewAvatarOnClick={true}
/>;
@ -160,14 +203,13 @@ export default class RoomHeader extends React.Component<IProps> {
if (this.props.inRoom && SettingsStore.getValue("showCallButtonsInComposer")) {
const voiceCallButton = <AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_voiceCallButton"
onClick={() => this.props.onCallPlaced(PlaceCallType.Voice)}
onClick={() => this.props.onCallPlaced(CallType.Voice)}
title={_t("Voice call")}
key="voice"
/>;
const videoCallButton = <AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
onClick={(ev: React.MouseEvent<Element>) => ev.shiftKey ?
this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)}
onClick={() => this.props.onCallPlaced(CallType.Video)}
title={_t("Video call")}
key="video"
/>;
@ -219,9 +261,10 @@ export default class RoomHeader extends React.Component<IProps> {
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
{ name }
{ searchStatus }
{ topicElement }
{ rightRow }
<RoomHeaderButtons room={this.props.room} />
<RoomHeaderButtons room={this.props.room} excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons} />
</div>
</div>
);

View file

@ -14,23 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef, ReactComponentElement } from "react";
import { Dispatcher } from "flux";
import React, { ComponentType, createRef, ReactComponentElement, RefObject } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import * as fbEmitter from "fbemitter";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { _t, _td } from "../../../languageHandler";
import { RovingTabIndexProvider, IState as IRovingTabIndexState } from "../../../accessibility/RovingTabIndex";
import { IState as IRovingTabIndexState, RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import RoomViewStore from "../../../stores/RoomViewStore";
import { ITagMap } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, isCustomTag, TagID } from "../../../stores/room-list/models";
import dis from "../../../dispatcher/dispatcher";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import RoomSublist from "./RoomSublist";
import RoomSublist, { IAuxButtonProps } from "./RoomSublist";
import { ActionPayload } from "../../../dispatcher/payloads";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import GroupAvatar from "../avatars/GroupAvatar";
@ -42,16 +39,29 @@ import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNo
import CustomRoomTagStore from "../../../stores/CustomRoomTagStore";
import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays";
import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
import AccessibleButton from "../elements/AccessibleButton";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import SpaceStore, { ISuggestedRoom, SUGGESTED_ROOMS } from "../../../stores/SpaceStore";
import { showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import {
ISuggestedRoom,
MetaSpace,
SpaceKey,
UPDATE_SUGGESTED_ROOMS,
UPDATE_SELECTED_SPACE,
} from "../../../stores/spaces";
import { shouldShowSpaceInvite, showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import RoomAvatar from "../avatars/RoomAvatar";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps {
onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void;
@ -61,7 +71,7 @@ interface IProps {
onListCollapse?: (isExpanded: boolean) => void;
resizeNotifier: ResizeNotifier;
isMinimized: boolean;
activeSpace: Room;
activeSpace: SpaceKey;
}
interface IState {
@ -93,9 +103,7 @@ const ALWAYS_VISIBLE_TAGS: TagID[] = [
interface ITagAesthetics {
sectionLabel: string;
sectionLabelRaw?: string;
addRoomLabel?: string;
onAddRoom?: (dispatcher?: Dispatcher<ActionPayload>) => void;
addRoomContextMenu?: (onFinished: () => void) => React.ReactNode;
AuxButtonComponent?: ComponentType<IAuxButtonProps>;
isInvite: boolean;
defaultHidden: boolean;
}
@ -105,6 +113,194 @@ interface ITagAestheticsMap {
[tagId: TagID]: ITagAesthetics;
}
const auxButtonContextMenuPosition = (handle: RefObject<HTMLDivElement>) => {
const rect = handle.current.getBoundingClientRect();
return {
chevronFace: ChevronFace.None,
left: rect.left - 7,
top: rect.top + rect.height,
};
};
const DmAuxButton = ({ tabIndex, dispatcher = defaultDispatcher }: IAuxButtonProps) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
const activeSpace = useEventEmitterState<Room>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
return SpaceStore.instance.activeSpaceRoom;
});
const showCreateRooms = shouldShowComponent(UIComponent.CreateRooms);
const showInviteUsers = shouldShowComponent(UIComponent.InviteUsers);
if (activeSpace && (showCreateRooms || showInviteUsers)) {
let contextMenu: JSX.Element;
if (menuDisplayed) {
const canInvite = shouldShowSpaceInvite(activeSpace);
contextMenu = <IconizedContextMenu {...auxButtonContextMenuPosition(handle)} onFinished={closeMenu} compact>
<IconizedContextMenuOptionList first>
{ showCreateRooms && <IconizedContextMenuOption
label={_t("Start new chat")}
iconClassName="mx_RoomList_iconStartChat"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
defaultDispatcher.dispatch({ action: "view_create_chat" });
}}
/> }
{ showInviteUsers && <IconizedContextMenuOption
label={_t("Invite to space")}
iconClassName="mx_RoomList_iconInvite"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
showSpaceInvite(activeSpace);
}}
disabled={!canInvite}
tooltip={canInvite ? undefined
: _t("You do not have permissions to invite people to this space")}
/> }
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
}
return <>
<ContextMenuTooltipButton
tabIndex={tabIndex}
onClick={openMenu}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={_t("Add people")}
title={_t("Add people")}
isExpanded={menuDisplayed}
inputRef={handle}
/>
{ contextMenu }
</>;
} else if (!activeSpace && showCreateRooms) {
return <AccessibleTooltipButton
tabIndex={tabIndex}
onClick={() => dispatcher.dispatch({ action: 'view_create_chat' })}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={_t("Start chat")}
title={_t("Start chat")}
/>;
}
};
const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
const activeSpace = useEventEmitterState<Room>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
return SpaceStore.instance.activeSpaceRoom;
});
const showCreateRoom = shouldShowComponent(UIComponent.CreateRooms);
let contextMenuContent: JSX.Element;
if (menuDisplayed && activeSpace) {
const canAddRooms = activeSpace.currentState.maySendStateEvent(EventType.SpaceChild,
MatrixClientPeg.get().getUserId());
contextMenuContent = <IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Explore rooms")}
iconClassName="mx_RoomList_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
defaultDispatcher.dispatch({
action: "view_room",
room_id: activeSpace.roomId,
});
}}
/>
{
showCreateRoom
? (<>
<IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomList_iconCreateNewRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
showCreateNewRoom(activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to create new rooms in this space")}
/>
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomList_iconAddExistingRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
showAddExistingRooms(activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to add rooms to this space")}
/>
</>)
: null
}
</IconizedContextMenuOptionList>;
} else if (menuDisplayed) {
contextMenuContent = <IconizedContextMenuOptionList first>
{ showCreateRoom && <IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomList_iconCreateNewRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
defaultDispatcher.dispatch({ action: "view_create_room" });
}}
/> }
<IconizedContextMenuOption
label={CommunityPrototypeStore.instance.getSelectedCommunityId()
? _t("Explore community rooms")
: _t("Explore public rooms")}
iconClassName="mx_RoomList_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
defaultDispatcher.fire(Action.ViewRoomDirectory);
}}
/>
</IconizedContextMenuOptionList>;
}
let contextMenu: JSX.Element;
if (menuDisplayed) {
contextMenu = <IconizedContextMenu {...auxButtonContextMenuPosition(handle)} onFinished={closeMenu} compact>
{ contextMenuContent }
</IconizedContextMenu>;
}
return <>
<ContextMenuTooltipButton
tabIndex={tabIndex}
onClick={openMenu}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={_td("Add room")}
title={_td("Add room")}
isExpanded={menuDisplayed}
inputRef={handle}
/>
{ contextMenu }
</>;
};
const TAG_AESTHETICS: ITagAestheticsMap = {
[DefaultTagID.Invite]: {
sectionLabel: _td("Invites"),
@ -120,92 +316,13 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
sectionLabel: _td("People"),
isInvite: false,
defaultHidden: false,
addRoomLabel: _td("Start chat"),
onAddRoom: (dispatcher?: Dispatcher<ActionPayload>) => {
(dispatcher || defaultDispatcher).dispatch({ action: 'view_create_chat' });
},
AuxButtonComponent: DmAuxButton,
},
[DefaultTagID.Untagged]: {
sectionLabel: _td("Rooms"),
isInvite: false,
defaultHidden: false,
addRoomLabel: _td("Add room"),
addRoomContextMenu: (onFinished: () => void) => {
if (SpaceStore.instance.activeSpace) {
const canAddRooms = SpaceStore.instance.activeSpace.currentState.maySendStateEvent(EventType.SpaceChild,
MatrixClientPeg.get().getUserId());
return <IconizedContextMenuOptionList first>
{
shouldShowComponent(UIComponent.CreateRooms)
? (<>
<IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
showCreateNewRoom(SpaceStore.instance.activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to create new rooms in this space")}
/>
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomList_iconHash"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
showAddExistingRooms(SpaceStore.instance.activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to add rooms to this space")}
/>
</>)
: null
}
<IconizedContextMenuOption
label={_t("Explore rooms")}
iconClassName="mx_RoomList_iconBrowse"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
defaultDispatcher.fire(Action.ViewRoomDirectory);
}}
/>
</IconizedContextMenuOptionList>;
}
return <IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
defaultDispatcher.dispatch({ action: "view_create_room" });
}}
/>
<IconizedContextMenuOption
label={CommunityPrototypeStore.instance.getSelectedCommunityId()
? _t("Explore community rooms")
: _t("Explore public rooms")}
iconClassName="mx_RoomList_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
defaultDispatcher.fire(Action.ViewRoomDirectory);
}}
/>
</IconizedContextMenuOptionList>;
},
AuxButtonComponent: UntaggedAuxButton,
},
[DefaultTagID.LowPriority]: {
sectionLabel: _td("Low priority"),
@ -251,6 +368,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
private roomStoreToken: fbEmitter.EventSubscription;
private treeRef = createRef<HTMLDivElement>();
static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
constructor(props: IProps) {
super(props);
@ -264,14 +384,14 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
public componentDidMount(): void {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
SpaceStore.instance.on(SUGGESTED_ROOMS, this.updateSuggestedRooms);
SpaceStore.instance.on(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
this.updateLists(); // trigger the first update
}
public componentWillUnmount() {
SpaceStore.instance.off(SUGGESTED_ROOMS, this.updateSuggestedRooms);
SpaceStore.instance.off(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
defaultDispatcher.unregister(this.dispatcherRef);
if (this.customTagStoreRef) this.customTagStoreRef.remove();
@ -290,8 +410,8 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
const currentRoomId = RoomViewStore.getRoomId();
const room = this.getRoomDelta(currentRoomId, viewRoomDeltaPayload.delta, viewRoomDeltaPayload.unread);
if (room) {
dis.dispatch({
action: 'view_room',
defaultDispatcher.dispatch({
action: Action.ViewRoom,
room_id: room.roomId,
show_room_tile: true, // to make sure the room gets scrolled into view
});
@ -369,17 +489,19 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
private onStartChat = () => {
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
dis.dispatch({ action: "view_create_chat", initialText });
defaultDispatcher.dispatch({ action: "view_create_chat", initialText });
};
private onExplore = () => {
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
};
private onSpaceInviteClick = () => {
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
showSpaceInvite(this.props.activeSpace, initialText);
if (this.props.activeSpace[0] === "!") {
defaultDispatcher.dispatch({
action: "view_room",
room_id: SpaceStore.instance.activeSpace,
});
} else {
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
defaultDispatcher.dispatch({ action: Action.ViewRoomDirectory, initialText });
}
};
private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
@ -485,6 +607,23 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
: TAG_AESTHETICS[orderedTagId];
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
let alwaysVisible = ALWAYS_VISIBLE_TAGS.includes(orderedTagId);
if (
(this.props.activeSpace === MetaSpace.Favourites && orderedTagId !== DefaultTagID.Favourite) ||
(this.props.activeSpace === MetaSpace.People && orderedTagId !== DefaultTagID.DM) ||
(this.props.activeSpace === MetaSpace.Orphans && orderedTagId === DefaultTagID.DM)
) {
alwaysVisible = false;
}
let forceExpanded = false;
if (
(this.props.activeSpace === MetaSpace.Favourites && orderedTagId === DefaultTagID.Favourite) ||
(this.props.activeSpace === MetaSpace.People && orderedTagId === DefaultTagID.DM)
) {
forceExpanded = true;
}
// The cost of mounting/unmounting this component offsets the cost
// of keeping it in the DOM and hiding it when it is not required
return <RoomSublist
@ -493,15 +632,14 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
forRooms={true}
startAsHidden={aesthetics.defaultHidden}
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
onAddRoom={aesthetics.onAddRoom}
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
addRoomContextMenu={aesthetics.addRoomContextMenu}
AuxButtonComponent={aesthetics.AuxButtonComponent}
isMinimized={this.props.isMinimized}
showSkeleton={showSkeleton}
extraTiles={extraTiles}
resizeNotifier={this.props.resizeNotifier}
alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)}
alwaysVisible={alwaysVisible}
onListCollapse={this.props.onListCollapse}
forceExpanded={forceExpanded}
/>;
});
}
@ -513,14 +651,11 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
}
public render() {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
let explorePrompt: JSX.Element;
if (!this.props.isMinimized) {
if (this.state.isNameFiltering) {
explorePrompt = <div className="mx_RoomList_explorePrompt">
<div>{ _t("Can't see what youre looking for?") }</div>
<div>{ _t("Can't see what you're looking for?") }</div>
<AccessibleButton
className="mx_RoomList_explorePrompt_startChat"
kind="link"
@ -533,59 +668,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
kind="link"
onClick={this.onExplore}
>
{ this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
{ this.props.activeSpace[0] === "!" ? _t("Explore rooms") : _t("Explore all public rooms") }
</AccessibleButton>
</div>;
} else if (
this.props.activeSpace?.canInvite(userId) ||
this.props.activeSpace?.getMyMembership() === "join" ||
this.props.activeSpace?.getJoinRule() === JoinRule.Public
) {
const spaceName = this.props.activeSpace.name;
const canInvite = this.props.activeSpace?.canInvite(userId) ||
this.props.activeSpace?.getJoinRule() === JoinRule.Public;
explorePrompt = <div className="mx_RoomList_explorePrompt">
<div>{ _t("Quick actions") }</div>
{ canInvite && <AccessibleTooltipButton
className="mx_RoomList_explorePrompt_spaceInvite"
onClick={this.onSpaceInviteClick}
title={_t("Invite to %(spaceName)s", { spaceName })}
>
{ _t("Invite people") }
</AccessibleTooltipButton> }
{ this.props.activeSpace?.getMyMembership() === "join" && <AccessibleTooltipButton
className="mx_RoomList_explorePrompt_spaceExplore"
onClick={this.onExplore}
title={_t("Explore %(spaceName)s", { spaceName })}
>
{ _t("Explore rooms") }
</AccessibleTooltipButton> }
</div>;
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
const unfilteredLists = RoomListStore.instance.unfilteredLists;
const unfilteredRooms = unfilteredLists[DefaultTagID.Untagged] || [];
const unfilteredHistorical = unfilteredLists[DefaultTagID.Archived] || [];
const unfilteredFavourite = unfilteredLists[DefaultTagID.Favourite] || [];
// show a prompt to join/create rooms if the user is in 0 rooms and no historical
if (unfilteredRooms.length < 1 && unfilteredHistorical.length < 1 && unfilteredFavourite.length < 1) {
explorePrompt = <div className="mx_RoomList_explorePrompt">
<div>{ _t("Use the + to make a new room or explore existing ones below") }</div>
<AccessibleButton
className="mx_RoomList_explorePrompt_startChat"
kind="link"
onClick={this.onStartChat}
>
{ _t("Start a new chat") }
</AccessibleButton>
<AccessibleButton
className="mx_RoomList_explorePrompt_explore"
kind="link"
onClick={this.onExplore}
>
{ _t("Explore all public rooms") }
</AccessibleButton>
</div>;
}
}
}

View file

@ -0,0 +1,370 @@
/*
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 React, { useContext, useEffect, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from "../../../languageHandler";
import { useEventEmitter, useEventEmitterState } from "../../../hooks/useEventEmitter";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
import SpaceContextMenu from "../context_menus/SpaceContextMenu";
import { HomeButtonContextMenu } from "../spaces/SpacePanel";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import dis from "../../../dispatcher/dispatcher";
import { shouldShowSpaceInvite, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import { ButtonEvent } from "../elements/AccessibleButton";
import Modal from "../../../Modal";
import EditCommunityPrototypeDialog from "../dialogs/EditCommunityPrototypeDialog";
import { Action } from "../../../dispatcher/actions";
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import ErrorDialog from "../dialogs/ErrorDialog";
import { showCommunityInviteDialog } from "../../../RoomInvite";
import { useDispatcher } from "../../../hooks/useDispatcher";
import InlineSpinner from "../elements/InlineSpinner";
import TooltipButton from "../elements/TooltipButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import {
getMetaSpaceName,
MetaSpace,
SpaceKey,
UPDATE_HOME_BEHAVIOUR,
UPDATE_SELECTED_SPACE,
} from "../../../stores/spaces";
const contextMenuBelow = (elementRect: DOMRect) => {
// align the context menu's icons with the icon which opened the context menu
const left = elementRect.left + window.pageXOffset;
const top = elementRect.bottom + window.pageYOffset + 12;
const chevronFace = ChevronFace.None;
return { left, top, chevronFace };
};
const PrototypeCommunityContextMenu = (props) => {
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
let settingsOption;
if (CommunityPrototypeStore.instance.isAdminOf(communityId)) {
const onCommunitySettingsClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog('Edit Community', '', EditCommunityPrototypeDialog, {
communityId: CommunityPrototypeStore.instance.getSelectedCommunityId(),
});
props.onFinished();
};
settingsOption = (
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("Settings")}
aria-label={_t("Community settings")}
onClick={onCommunitySettingsClick}
/>
);
}
const onCommunityMembersClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
// We'd ideally just pop open a right panel with the member list, but the current
// way the right panel is structured makes this exceedingly difficult. Instead, we'll
// switch to the general room and open the member list there as it should be in sync
// anyways.
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
if (chat) {
dis.dispatch({
action: 'view_room',
room_id: chat.roomId,
}, true);
dis.dispatch({ action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList });
} else {
// "This should never happen" clauses go here for the prototype.
Modal.createTrackedDialog('Failed to find general chat', '', ErrorDialog, {
title: _t('Failed to find the general chat for this community'),
description: _t("Failed to find the general chat for this community"),
});
}
props.onFinished();
};
return <IconizedContextMenu {...props} compact>
<IconizedContextMenuOptionList first>
{ settingsOption }
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconMembers"
label={_t("Members")}
onClick={onCommunityMembersClick}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
};
const useJoiningRooms = (): Set<string> => {
const cli = useContext(MatrixClientContext);
const [joiningRooms, setJoiningRooms] = useState(new Set<string>());
useDispatcher(defaultDispatcher, payload => {
switch (payload.action) {
case Action.JoinRoom:
setJoiningRooms(new Set(joiningRooms.add(payload.roomId)));
break;
case Action.JoinRoomReady:
case Action.JoinRoomError:
if (joiningRooms.delete(payload.roomId)) {
setJoiningRooms(new Set(joiningRooms));
}
break;
}
});
useEventEmitter(cli, "Room", (room: Room) => {
if (joiningRooms.delete(room.roomId)) {
setJoiningRooms(new Set(joiningRooms));
}
});
return joiningRooms;
};
interface IProps {
spacePanelDisabled: boolean;
onVisibilityChange?(): void;
}
const RoomListHeader = ({ spacePanelDisabled, onVisibilityChange }: IProps) => {
const cli = useContext(MatrixClientContext);
const [mainMenuDisplayed, mainMenuHandle, openMainMenu, closeMainMenu] = useContextMenu<HTMLDivElement>();
const [plusMenuDisplayed, plusMenuHandle, openPlusMenu, closePlusMenu] = useContextMenu<HTMLDivElement>();
const [spaceKey, activeSpace] = useEventEmitterState<[SpaceKey, Room | null]>(
SpaceStore.instance,
UPDATE_SELECTED_SPACE,
() => [SpaceStore.instance.activeSpace, SpaceStore.instance.activeSpaceRoom],
);
const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
return SpaceStore.instance.allRoomsInHome;
});
const joiningRooms = useJoiningRooms();
const count = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () => {
if (RoomListStore.instance.getFirstNameFilterCondition()) {
return Object.values(RoomListStore.instance.orderedLists).flat(1).length;
} else {
return null;
}
});
useEffect(() => {
if (onVisibilityChange) {
onVisibilityChange();
}
}, [count, onVisibilityChange]);
if (typeof count === "number") {
return <div className="mx_LeftPanel_roomListFilterCount">
{ _t("%(count)s results", { count }) }
</div>;
} else if (spacePanelDisabled) {
return null;
}
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
let contextMenu: JSX.Element;
if (mainMenuDisplayed) {
let ContextMenuComponent;
if (activeSpace) {
ContextMenuComponent = SpaceContextMenu;
} else if (communityId) {
ContextMenuComponent = PrototypeCommunityContextMenu;
} else {
ContextMenuComponent = HomeButtonContextMenu;
}
contextMenu = <ContextMenuComponent
{...contextMenuBelow(mainMenuHandle.current.getBoundingClientRect())}
space={activeSpace}
onFinished={closeMainMenu}
hideHeader={true}
/>;
} else if (plusMenuDisplayed && activeSpace) {
let inviteOption: JSX.Element;
if (shouldShowSpaceInvite(activeSpace)) {
inviteOption = <IconizedContextMenuOption
label={_t("Invite")}
iconClassName="mx_RoomListHeader_iconInvite"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showSpaceInvite(activeSpace);
closePlusMenu();
}}
/>;
} else if (CommunityPrototypeStore.instance.canInviteTo(communityId)) {
inviteOption = <IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconInvite"
label={_t("Invite")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
closePlusMenu();
}}
/>;
}
let createNewRoomOption: JSX.Element;
if (activeSpace?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId())) {
createNewRoomOption = <IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconCreateRoom"
label={_t("Create new room")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showCreateNewRoom(activeSpace);
closePlusMenu();
}}
/>;
}
contextMenu = <IconizedContextMenu
{...contextMenuBelow(plusMenuHandle.current.getBoundingClientRect())}
onFinished={closePlusMenu}
compact
>
<IconizedContextMenuOptionList first>
{ inviteOption }
<IconizedContextMenuOption
label={_t("Start new chat")}
iconClassName="mx_RoomListHeader_iconStartChat"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "view_create_chat" });
closePlusMenu();
}}
/>
{ createNewRoomOption }
<IconizedContextMenuOption
label={_t("Explore rooms")}
iconClassName="mx_RoomListHeader_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: Action.ViewRoomDirectory });
closePlusMenu();
}}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
} else if (plusMenuDisplayed) {
contextMenu = <IconizedContextMenu
{...contextMenuBelow(plusMenuHandle.current.getBoundingClientRect())}
onFinished={closePlusMenu}
compact
>
<IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Start new chat")}
iconClassName="mx_RoomListHeader_iconStartChat"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "view_create_chat" });
closePlusMenu();
}}
/>
<IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomListHeader_iconCreateRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "view_create_room" });
closePlusMenu();
}}
/>
<IconizedContextMenuOption
label={_t("Join public room")}
iconClassName="mx_RoomListHeader_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: Action.ViewRoomDirectory });
closePlusMenu();
}}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
}
let title: string;
if (activeSpace) {
title = activeSpace.name;
} else if (communityId) {
title = CommunityPrototypeStore.instance.getSelectedCommunityName();
} else {
title = getMetaSpaceName(spaceKey as MetaSpace, allRoomsInHome);
}
let pendingRoomJoinSpinner;
if (joiningRooms.size) {
pendingRoomJoinSpinner = <InlineSpinner>
<TooltipButton helpText={_t(
"Currently joining %(count)s rooms",
{ count: joiningRooms.size },
)} />
</InlineSpinner>;
}
let contextMenuButton: JSX.Element = <div className="mx_RoomListHeader_contextLessTitle">{ title }</div>;
if (activeSpace || spaceKey === MetaSpace.Home) {
contextMenuButton = <ContextMenuTooltipButton
inputRef={mainMenuHandle}
onClick={openMainMenu}
isExpanded={mainMenuDisplayed}
className="mx_RoomListHeader_contextMenuButton"
title={activeSpace
? _t("%(spaceName)s menu", { spaceName: activeSpace.name })
: _t("Home options")}
>
{ title }
</ContextMenuTooltipButton>;
}
return <div className="mx_RoomListHeader">
{ contextMenuButton }
{ pendingRoomJoinSpinner }
<ContextMenuTooltipButton
inputRef={plusMenuHandle}
onClick={openPlusMenu}
isExpanded={plusMenuDisplayed}
className="mx_RoomListHeader_plusButton"
title={_t("Add")}
/>
{ contextMenu }
</div>;
};
export default RoomListHeader;

View file

@ -1,55 +0,0 @@
/*
Copyright 2020 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 React, { useEffect, useState } from "react";
import { _t } from "../../../languageHandler";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import { useEventEmitter } from "../../../hooks/useEventEmitter";
import SpaceStore from "../../../stores/SpaceStore";
interface IProps {
onVisibilityChange?: () => void;
}
const RoomListNumResults: React.FC<IProps> = ({ onVisibilityChange }) => {
const [count, setCount] = useState<number>(null);
useEventEmitter(RoomListStore.instance, LISTS_UPDATE_EVENT, () => {
if (RoomListStore.instance.getFirstNameFilterCondition()) {
const numRooms = Object.values(RoomListStore.instance.orderedLists).flat(1).length;
setCount(numRooms);
} else {
setCount(null);
}
});
useEffect(() => {
if (onVisibilityChange) {
onVisibilityChange();
}
}, [count, onVisibilityChange]);
if (typeof count !== "number") return null;
return <div className="mx_LeftPanel_roomListFilterCount">
{ SpaceStore.instance.spacePanelSpaces.length
? _t("%(count)s results in all spaces", { count })
: _t("%(count)s results", { count })
}
</div>;
};
export default RoomListNumResults;

View file

@ -22,7 +22,6 @@ import { IJoinRuleEventContent, JoinRule } from "matrix-js-sdk/src/@types/partia
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import classNames from 'classnames';
import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
@ -37,6 +36,7 @@ import Spinner from "../elements/Spinner";
import AccessibleButton from "../elements/AccessibleButton";
import { UIFeature } from "../../../settings/UIFeature";
import SettingsStore from "../../../settings/SettingsStore";
import RoomAvatar from "../avatars/RoomAvatar";
const MemberEventHtmlReasonField = "io.element.html_reason";
@ -465,7 +465,6 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
break;
}
case MessageCase.Invite: {
const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar");
const oobData = Object.assign({}, this.props.oobData, {
avatarUrl: this.communityProfile().avatarMxc,
});

View file

@ -17,7 +17,7 @@ limitations under the License.
*/
import * as React from "react";
import { createRef, ReactComponentElement } from "react";
import { ComponentType, createRef, ReactComponentElement } from "react";
import { normalize } from "matrix-js-sdk/src/utils";
import { Room } from "matrix-js-sdk/src/models/room";
import classNames from 'classnames';
@ -29,9 +29,8 @@ import { _t } from "../../../languageHandler";
import AccessibleButton from "../../views/elements/AccessibleButton";
import RoomTile from "./RoomTile";
import { ListLayout } from "../../../stores/room-list/ListLayout";
import {
import ContextMenu, {
ChevronFace,
ContextMenu,
ContextMenuTooltipButton,
StyledMenuItemCheckbox,
StyledMenuItemRadio,
@ -41,6 +40,7 @@ import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorith
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import dis from "../../../dispatcher/dispatcher";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import NotificationBadge from "./NotificationBadge";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { Key } from "../../../Keyboard";
@ -53,11 +53,9 @@ import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
import { objectExcluding, objectHasDiff } from "../../../utils/objects";
import ExtraTile from "./ExtraTile";
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
import IconizedContextMenu from "../context_menus/IconizedContextMenu";
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import { Dispatcher } from "flux";
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
@ -68,22 +66,24 @@ const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT;
// HACK: We really shouldn't have to do this.
polyfillTouchEvent();
export interface IAuxButtonProps {
tabIndex: number;
dispatcher?: Dispatcher<ActionPayload>;
}
interface IProps {
forRooms: boolean;
startAsHidden: boolean;
label: string;
onAddRoom?: () => void;
addRoomContextMenu?: (onFinished: () => void) => React.ReactNode;
addRoomLabel: string;
AuxButtonComponent?: ComponentType<IAuxButtonProps>;
isMinimized: boolean;
tagId: TagID;
showSkeleton?: boolean;
alwaysVisible?: boolean;
forceExpanded?: boolean;
resizeNotifier: ResizeNotifier;
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
onListCollapse?: (isExpanded: boolean) => void;
// TODO: Account for https://github.com/vector-im/element-web/issues/14179
}
// TODO: Use re-resizer's NumberSize when it is exposed as the type
@ -96,7 +96,6 @@ type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">;
interface IState {
contextMenuPosition: PartialDOMRect;
addRoomContextMenuPosition: PartialDOMRect;
isResizing: boolean;
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
height: number;
@ -124,7 +123,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
this.notificationState = RoomNotificationStateStore.instance.getListState(this.props.tagId);
this.state = {
contextMenuPosition: null,
addRoomContextMenuPosition: null,
isResizing: false,
isExpanded: this.isBeingFiltered ? this.isBeingFiltered : !this.layout.isCollapsed,
height: 0, // to be fixed in a moment, we need `rooms` to calculate this.
@ -314,11 +312,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
}
};
private onAddRoom = (e) => {
e.stopPropagation();
if (this.props.onAddRoom) this.props.onAddRoom();
};
private applyHeightChange(newHeight: number) {
const heightInTiles = Math.ceil(this.layout.pixelsToTiles(newHeight - this.padding));
this.layout.visibleTiles = Math.min(this.numTiles, heightInTiles);
@ -396,21 +389,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
});
};
private onAddRoomContextMenu = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target as HTMLButtonElement;
this.setState({ addRoomContextMenuPosition: target.getBoundingClientRect() });
};
private onCloseMenu = () => {
this.setState({ contextMenuPosition: null });
};
private onCloseAddRoomMenu = () => {
this.setState({ addRoomContextMenuPosition: null });
};
private onUnreadFirstChanged = () => {
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance;
@ -445,7 +427,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
if (room) {
dis.dispatch({
action: 'view_room',
action: Action.ViewRoom,
room_id: room.roomId,
show_room_tile: true, // to make sure the room gets scrolled into view
});
@ -480,6 +462,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
};
private toggleCollapsed = () => {
if (this.props.forceExpanded) return;
this.layout.isCollapsed = this.state.isExpanded;
this.setState({ isExpanded: !this.layout.isCollapsed });
if (this.props.onListCollapse) {
@ -528,7 +511,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
};
private renderVisibleTiles(): React.ReactElement[] {
if (!this.state.isExpanded) {
if (!this.state.isExpanded && !this.props.forceExpanded) {
// don't waste time on rendering
return [];
}
@ -536,7 +519,11 @@ export default class RoomSublist extends React.Component<IProps, IState> {
const tiles: React.ReactElement[] = [];
if (this.state.rooms) {
const visibleRooms = this.state.rooms.slice(0, this.numVisibleTiles);
let visibleRooms = this.state.rooms;
if (!this.props.forceExpanded) {
visibleRooms = visibleRooms.slice(0, this.numVisibleTiles);
}
for (const room of visibleRooms) {
tiles.push(<RoomTile
room={room}
@ -557,7 +544,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
// to avoid spending cycles on slicing. It's generally fine to do this though
// as users are unlikely to have more than a handful of tiles when the extra
// tiles are used.
if (tiles.length > this.numVisibleTiles) {
if (tiles.length > this.numVisibleTiles && !this.props.forceExpanded) {
return tiles.slice(0, this.numVisibleTiles);
}
@ -628,18 +615,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
</div>
</ContextMenu>
);
} else if (this.state.addRoomContextMenuPosition) {
contextMenu = (
<IconizedContextMenu
chevronFace={ChevronFace.None}
left={this.state.addRoomContextMenuPosition.left - 7} // center align with the handle
top={this.state.addRoomContextMenuPosition.top + this.state.addRoomContextMenuPosition.height}
onFinished={this.onCloseAddRoomMenu}
compact
>
{ this.props.addRoomContextMenu(this.onCloseAddRoomMenu) }
</IconizedContextMenu>
);
}
return (
@ -678,30 +653,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
);
let addRoomButton = null;
if (!!this.props.onAddRoom && shouldShowComponent(UIComponent.CreateRooms)) {
addRoomButton = (
<AccessibleTooltipButton
tabIndex={tabIndex}
onClick={this.onAddRoom}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={this.props.addRoomLabel || _t("Add room")}
title={this.props.addRoomLabel}
/>
);
} else if (this.props.addRoomContextMenu) {
// We assume that shouldShowComponent() is checked by the context menu itself.
addRoomButton = (
<ContextMenuTooltipButton
tabIndex={tabIndex}
onClick={this.onAddRoomContextMenu}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={this.props.addRoomLabel || _t("Add room")}
title={this.props.addRoomLabel}
isExpanded={!!this.state.addRoomContextMenuPosition}
/>
);
if (this.props.AuxButtonComponent) {
const AuxButtonComponent = this.props.AuxButtonComponent;
addRoomButton = <AuxButtonComponent tabIndex={tabIndex} />;
}
const collapseClasses = classNames({
@ -784,7 +738,13 @@ export default class RoomSublist extends React.Component<IProps, IState> {
});
let content = null;
if (visibleTiles.length > 0) {
if (visibleTiles.length > 0 && this.props.forceExpanded) {
content = <div className="mx_RoomSublist_resizeBox mx_RoomSublist_resizeBox_forceExpanded">
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
{ visibleTiles }
</div>
</div>;
} else if (visibleTiles.length > 0) {
const layout = this.layout; // to shorten calls
const minTiles = Math.min(layout.minVisibleTiles, this.numTiles);

View file

@ -24,6 +24,7 @@ import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
import dis from '../../../dispatcher/dispatcher';
import defaultDispatcher from '../../../dispatcher/dispatcher';
import { Action } from "../../../dispatcher/actions";
import { Key } from "../../../Keyboard";
import ActiveRoomObserver from "../../../ActiveRoomObserver";
import { _t } from "../../../languageHandler";
@ -38,7 +39,7 @@ import RoomListStore from "../../../stores/room-list/RoomListStore";
import RoomListActions from "../../../actions/RoomListActions";
import { ActionPayload } from "../../../dispatcher/payloads";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
import { NotificationState, NotificationStateEvents } from "../../../stores/notifications/NotificationState";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
import { CachedRoomKey, RoomEchoChamber } from "../../../stores/local-echo/RoomEchoChamber";
@ -70,7 +71,7 @@ interface IState {
const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomId}`;
const contextMenuBelow = (elementRect: PartialDOMRect) => {
export const contextMenuBelow = (elementRect: PartialDOMRect) => {
// align the context menu's icons with the icon which opened the context menu
const left = elementRect.left + window.pageXOffset - 9;
const top = elementRect.bottom + window.pageYOffset + 17;
@ -163,7 +164,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
this.onRoomPreviewChanged,
);
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.notificationState.on(NotificationStateEvents.Update, this.onNotificationUpdate);
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
this.props.room?.on("Room.name", this.onRoomNameUpdate);
CommunityPrototypeStore.instance.on(
@ -187,9 +188,8 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
}
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
defaultDispatcher.unregister(this.dispatcherRef);
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate);
this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
this.roomProps.off("Room.name", this.onRoomNameUpdate);
CommunityPrototypeStore.instance.off(
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
this.onCommunityUpdate,
@ -236,7 +236,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: 'view_room',
action: Action.ViewRoom,
show_room_tile: true, // make sure the room is visible in the list
room_id: this.props.room.roomId,
clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)),

View file

@ -21,10 +21,10 @@ import { RoomState } from 'matrix-js-sdk/src/models/room-state';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import RoomUpgradeDialog from '../dialogs/RoomUpgradeDialog';
import AccessibleButton from '../elements/AccessibleButton';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps {
room: Room;
@ -36,18 +36,24 @@ interface IState {
@replaceableComponent("views.rooms.RoomUpgradeWarningBar")
export default class RoomUpgradeWarningBar extends React.PureComponent<IProps, IState> {
public componentDidMount(): void {
const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", "");
this.setState({ upgraded: tombstone && tombstone.getContent().replacement_room });
static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
MatrixClientPeg.get().on("RoomState.events", this.onStateEvents);
constructor(props, context) {
super(props, context);
const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", "");
this.state = {
upgraded: tombstone?.getContent().replacement_room,
};
}
public componentDidMount(): void {
this.context.on("RoomState.events", this.onStateEvents);
}
public componentWillUnmount(): void {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this.onStateEvents);
}
this.context.removeListener("RoomState.events", this.onStateEvents);
}
private onStateEvents = (event: MatrixEvent, state: RoomState): void => {

View file

@ -56,7 +56,7 @@ import ErrorDialog from "../dialogs/ErrorDialog";
import QuestionDialog from "../dialogs/QuestionDialog";
import { ActionPayload } from "../../../dispatcher/payloads";
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
import RoomContext from '../../../contexts/RoomContext';
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
import DocumentPosition from "../../../editor/position";
import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
@ -64,7 +64,6 @@ function addReplyToMessageContent(
content: IContent,
replyToEvent: MatrixEvent,
permalinkCreator: RoomPermalinkCreator,
relation?: IEventRelation,
): void {
const replyContent = ReplyChain.makeReplyMixIn(replyToEvent);
Object.assign(content, replyContent);
@ -78,7 +77,12 @@ function addReplyToMessageContent(
}
content.body = nestedReply.body + content.body;
}
}
export function attachRelation(
content: IContent,
relation?: IEventRelation,
): void {
if (relation) {
content['m.relates_to'] = {
...relation, // the composer can have a default
@ -346,7 +350,11 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
}
private async runSlashCommand(cmd: Command, args: string): Promise<void> {
const result = cmd.run(this.props.room.roomId, args);
const threadId = this.props.relation?.rel_type === RelationType.Thread
? this.props.relation?.event_id
: null;
const result = cmd.run(this.props.room.roomId, threadId, args);
let messageContent;
let error = result.error;
if (result.promise) {
@ -417,9 +425,9 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
content,
replyToEvent,
this.props.permalinkCreator,
this.props.relation,
);
}
attachRelation(content, this.props.relation);
} else {
this.runSlashCommand(cmd, args);
shouldSend = false;
@ -475,7 +483,11 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
decorateStartSendingTime(content);
}
const prom = this.props.mxClient.sendMessage(roomId, content);
const threadId = this.props.relation?.rel_type === RelationType.Thread
? this.props.relation.event_id
: null;
const prom = this.props.mxClient.sendMessage(roomId, threadId, content);
if (replyToEvent) {
// Clear reply_to_event as we put the message into the queue
// if the send fails, retry will handle resending.
@ -488,7 +500,12 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
dis.dispatch({ action: "message_sent" });
CHAT_EFFECTS.forEach((effect) => {
if (containsEmoji(content, effect.emojis)) {
dis.dispatch({ action: `effects.${effect.command}` });
// For initial threads launch, chat effects are disabled
// see #19731
const isNotThread = this.props.relation?.rel_type !== RelationType.Thread;
if (!SettingsStore.getValue("feature_thread") || isNotThread) {
dis.dispatch({ action: `effects.${effect.command}` });
}
}
});
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
@ -588,7 +605,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
switch (payload.action) {
case 'reply_to_event':
case Action.FocusSendMessageComposer:
if (payload.context === this.context.timelineRenderingType) {
if ((payload.context ?? TimelineRenderingType.Room) === this.context.timelineRenderingType) {
this.editorRef.current?.focus();
}
break;
@ -615,7 +632,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
// it puts the filename in as text/plain which we want to ignore.
if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) {
ContentMessages.sharedInstance().sendContentListToRoom(
Array.from(clipboardData.files), this.props.room.roomId, this.props.mxClient,
Array.from(clipboardData.files), this.props.room.roomId, this.props.relation, this.props.mxClient,
);
return true; // to skip internal onPaste handler
}
@ -630,6 +647,9 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
};
render() {
const threadId = this.props.relation?.rel_type === RelationType.Thread
? this.props.relation.event_id
: null;
return (
<div className="mx_SendMessageComposer" onClick={this.focusComposer} onKeyDown={this.onKeyDown}>
<BasicMessageComposer
@ -637,6 +657,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
ref={this.editorRef}
model={this.model}
room={this.props.room}
threadId={threadId}
label={this.props.placeholder}
placeholder={this.props.placeholder}
onPaste={this.onPaste}

View file

@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { Room } from 'matrix-js-sdk/src/models/room';
import { logger } from "matrix-js-sdk/src/logger";
@ -26,7 +27,7 @@ import WidgetUtils, { IWidgetEvent } from '../../../utils/WidgetUtils';
import PersistedElement from "../elements/PersistedElement";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import { ChevronFace, ContextMenu } from "../../structures/ContextMenu";
import ContextMenu, { ChevronFace } from "../../structures/ContextMenu";
import { WidgetType } from "../../../widgets/WidgetType";
import { Action } from "../../../dispatcher/actions";
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
@ -45,6 +46,7 @@ const PERSISTED_ELEMENT_KEY = "stickerPicker";
interface IProps {
room: Room;
threadId?: string | null;
showStickers: boolean;
menuPosition?: any;
setShowStickers: (showStickers: boolean) => void;
@ -61,6 +63,10 @@ interface IState {
@replaceableComponent("views.rooms.Stickerpicker")
export default class Stickerpicker extends React.PureComponent<IProps, IState> {
static defaultProps = {
threadId: null,
};
static currentWidget;
private dispatcherRef: string;
@ -286,6 +292,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
<AppTile
app={stickerApp}
room={this.props.room}
threadId={this.props.threadId}
fullWidth={true}
userId={MatrixClientPeg.get().credentials.userId}
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}

View file

@ -29,7 +29,7 @@ import RoomName from "../elements/RoomName";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import ErrorDialog from '../dialogs/ErrorDialog';
import AccessibleButton from '../elements/AccessibleButton';
import SpaceStore from "../../../stores/SpaceStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
interface IProps {
event: MatrixEvent;

View file

@ -183,6 +183,7 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
height={24}
resizeMethod="crop"
viewUserOnClick={true}
aria-live="off"
/>
);
});