Apply prettier formatting

This commit is contained in:
Michael Weimann 2022-12-12 12:24:14 +01:00
parent 1cac306093
commit 526645c791
No known key found for this signature in database
GPG key ID: 53F535A266BB9584
1576 changed files with 65385 additions and 62478 deletions

View file

@ -15,15 +15,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import React from "react";
import classNames from "classnames";
import { Resizable } from "re-resizable";
import { Room } from "matrix-js-sdk/src/models/room";
import AppTile from '../elements/AppTile';
import dis from '../../../dispatcher/dispatcher';
import * as ScalarMessaging from '../../../ScalarMessaging';
import WidgetUtils from '../../../utils/WidgetUtils';
import AppTile from "../elements/AppTile";
import dis from "../../../dispatcher/dispatcher";
import * as ScalarMessaging from "../../../ScalarMessaging";
import WidgetUtils from "../../../utils/WidgetUtils";
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import ResizeHandle from "../elements/ResizeHandle";
@ -46,7 +46,7 @@ interface IProps {
interface IState {
// @ts-ignore - TS wants a string key, but we know better
apps: {[id: Container]: IApp[]};
apps: { [id: Container]: IApp[] };
resizingVertical: boolean; // true when changing the height of the apps drawer
resizingHorizontal: boolean; // true when changing the distribution of the width between widgets
resizing: boolean;
@ -117,8 +117,11 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
onResizeStop: () => {
this.resizeContainer.classList.remove("mx_AppsDrawer_resizing");
WidgetLayoutStore.instance.setResizerDistributions(
this.props.room, Container.Top,
this.topApps().slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
this.props.room,
Container.Top,
this.topApps()
.slice(1)
.map((_, i) => this.resizer.forHandleAt(i).size),
);
this.setState({ resizingHorizontal: false });
},
@ -142,7 +145,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
this.loadResizerPreferences();
};
private getAppsHash = (apps: IApp[]): string => apps.map(app => app.id).join("~");
private getAppsHash = (apps: IApp[]): string => apps.map((app) => app.id).join("~");
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
@ -157,13 +160,13 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
const distributors = this.resizer.getDistributors();
// relax all items if they had any overconstrained flexboxes
distributors.forEach(d => d.start());
distributors.forEach(d => d.finish());
distributors.forEach((d) => d.start());
distributors.forEach((d) => d.finish());
};
private loadResizerPreferences = (): void => {
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
if (this.state.apps && (this.topApps().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) {
@ -173,9 +176,9 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
});
} else if (this.state.apps) {
const distributors = this.resizer.getDistributors();
distributors.forEach(d => d.item.clearSize());
distributors.forEach(d => d.start());
distributors.forEach(d => d.finish());
distributors.forEach((d) => d.item.clearSize());
distributors.forEach((d) => d.start());
distributors.forEach((d) => d.finish());
}
};
@ -184,7 +187,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
}
private onAction = (action: ActionPayload): void => {
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
const hideWidgetKey = this.props.room.roomId + "_hide_widget_drawer";
switch (action.action) {
case "appsDrawer":
// Note: these booleans are awkward because localstorage is fundamentally
@ -223,17 +226,19 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
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}
fullWidth={arr.length < 2}
room={this.props.room}
userId={this.props.userId}
creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
pointerEvents={this.isResizing() ? 'none' : undefined}
/>);
return (
<AppTile
key={app.id}
app={app}
fullWidth={arr.length < 2}
room={this.props.room}
userId={this.props.userId}
creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
pointerEvents={this.isResizing() ? "none" : undefined}
/>
);
});
if (apps.length === 0) {
@ -242,10 +247,8 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
let spinner;
if (
apps.length === 0 && WidgetEchoStore.roomHasPendingWidgets(
this.props.room.roomId,
WidgetUtils.getRoomWidgets(this.props.room),
)
apps.length === 0 &&
WidgetEchoStore.roomHasPendingWidgets(this.props.room.roomId, WidgetUtils.getRoomWidgets(this.props.room))
) {
spinner = <Spinner />;
}
@ -258,37 +261,43 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
mx_AppsDrawer_2apps: apps.length === 2,
mx_AppsDrawer_3apps: apps.length === 3,
});
const appContainers =
const appContainers = (
<div className="mx_AppsContainer" ref={this.collectResizer}>
{ apps.map((app, i) => {
{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>;
return (
<React.Fragment key={app.key}>
<ResizeHandle reverse={i > apps.length / 2} />
{app}
</React.Fragment>
);
})}
</div>
);
let drawer;
if (widgetIsMaxmised) {
drawer = appContainers;
} else {
drawer = <PersistentVResizer
room={this.props.room}
minHeight={100}
maxHeight={this.props.maxHeight - 50}
handleClass="mx_AppsContainer_resizerHandle"
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
className="mx_AppsContainer_resizer"
resizeNotifier={this.props.resizeNotifier}>
{ appContainers }
</PersistentVResizer>;
drawer = (
<PersistentVResizer
room={this.props.room}
minHeight={100}
maxHeight={this.props.maxHeight - 50}
handleClass="mx_AppsContainer_resizerHandle"
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
className="mx_AppsContainer_resizer"
resizeNotifier={this.props.resizeNotifier}
>
{appContainers}
</PersistentVResizer>
);
}
return (
<div className={classes}>
{ drawer }
{ spinner }
{drawer}
{spinner}
</div>
);
}
@ -329,33 +338,31 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
defaultHeight = 280;
}
return <Resizable
size={{ height: Math.min(defaultHeight, maxHeight), width: undefined }}
minHeight={minHeight}
maxHeight={maxHeight}
onResizeStart={() => {
resizeNotifier.startResizing();
}}
onResize={() => {
resizeNotifier.notifyTimelineHeightChanged();
}}
onResizeStop={(e, dir, ref, d) => {
let newHeight = defaultHeight + d.height;
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
return (
<Resizable
size={{ height: Math.min(defaultHeight, maxHeight), width: undefined }}
minHeight={minHeight}
maxHeight={maxHeight}
onResizeStart={() => {
resizeNotifier.startResizing();
}}
onResize={() => {
resizeNotifier.notifyTimelineHeightChanged();
}}
onResizeStop={(e, dir, ref, d) => {
let newHeight = defaultHeight + d.height;
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
WidgetLayoutStore.instance.setContainerHeight(
room,
Container.Top,
newHeight,
);
WidgetLayoutStore.instance.setContainerHeight(room, Container.Top, newHeight);
resizeNotifier.stopResizing();
}}
handleWrapperClass={handleWrapperClass}
handleClasses={{ bottom: handleClass }}
className={className}
enable={{ bottom: true }}
>
{ children }
</Resizable>;
resizeNotifier.stopResizing();
}}
handleWrapperClass={handleWrapperClass}
handleClasses={{ bottom: handleClass }}
className={className}
enable={{ bottom: true }}
>
{children}
</Resizable>
);
};

View file

@ -15,14 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef, KeyboardEvent } from 'react';
import classNames from 'classnames';
import React, { createRef, KeyboardEvent } from "react";
import classNames from "classnames";
import { flatMap } from "lodash";
import { Room } from 'matrix-js-sdk/src/models/room';
import { Room } from "matrix-js-sdk/src/models/room";
import Autocompleter, { ICompletion, ISelectionRange, IProviderCompletions } from '../../../autocomplete/Autocompleter';
import Autocompleter, { ICompletion, ISelectionRange, IProviderCompletions } from "../../../autocomplete/Autocompleter";
import SettingsStore from "../../../settings/SettingsStore";
import RoomContext from '../../../contexts/RoomContext';
import RoomContext from "../../../contexts/RoomContext";
const MAX_PROVIDER_MATCHES = 20;
@ -134,15 +134,15 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
}
private processQuery(query: string, selection: ISelectionRange): Promise<void> {
return this.autocompleter.getCompletions(
query, selection, this.state.forceComplete, MAX_PROVIDER_MATCHES,
).then((completions) => {
// Only ever process the completions for the most recent query being processed
if (query !== this.queryRequested) {
return;
}
this.processCompletions(completions);
});
return this.autocompleter
.getCompletions(query, selection, this.state.forceComplete, MAX_PROVIDER_MATCHES)
.then((completions) => {
// Only ever process the completions for the most recent query being processed
if (query !== this.queryRequested) {
return;
}
this.processCompletions(completions);
});
}
private processCompletions(completions: IProviderCompletions[]): void {
@ -154,10 +154,11 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
/* If the currently selected completion is still in the completion list,
try to find it and jump to it. If not, select composer.
*/
const currentSelection = this.state.selectionOffset <= 1 ? null :
this.state.completionList[this.state.selectionOffset - 1].completion;
selectionOffset = completionList.findIndex(
(completion) => completion.completion === currentSelection);
const currentSelection =
this.state.selectionOffset <= 1
? null
: this.state.completionList[this.state.selectionOffset - 1].completion;
selectionOffset = completionList.findIndex((completion) => completion.completion === currentSelection);
if (selectionOffset === -1) {
selectionOffset = 1;
} else {
@ -227,14 +228,17 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
public forceComplete(): Promise<number> {
return new Promise((resolve) => {
this.setState({
forceComplete: true,
hide: false,
}, () => {
this.complete(this.props.query, this.props.selection).then(() => {
resolve(this.countCompletions());
});
});
this.setState(
{
forceComplete: true,
hide: false,
},
() => {
this.complete(this.props.query, this.props.selection).then(() => {
resolve(this.countCompletions());
});
},
);
});
}
@ -278,38 +282,40 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
render() {
let position = 1;
const renderedCompletions = this.state.completions.map((completionResult, i) => {
const completions = completionResult.completions.map((completion, j) => {
const selected = position === this.state.selectionOffset;
const className = classNames('mx_Autocomplete_Completion', { selected });
const componentPosition = position;
position++;
const renderedCompletions = this.state.completions
.map((completionResult, i) => {
const completions = completionResult.completions.map((completion, j) => {
const selected = position === this.state.selectionOffset;
const className = classNames("mx_Autocomplete_Completion", { selected });
const componentPosition = position;
position++;
const onClick = () => {
this.onCompletionClicked(componentPosition);
};
const onClick = () => {
this.onCompletionClicked(componentPosition);
};
return React.cloneElement(completion.component, {
"key": j,
"ref": `completion${componentPosition}`,
"id": generateCompletionDomId(componentPosition - 1), // 0 index the completion IDs
className,
onClick,
"aria-selected": selected,
return React.cloneElement(completion.component, {
"key": j,
"ref": `completion${componentPosition}`,
"id": generateCompletionDomId(componentPosition - 1), // 0 index the completion IDs
className,
onClick,
"aria-selected": selected,
});
});
});
return completions.length > 0 ? (
<div key={i} className="mx_Autocomplete_ProviderSection" role="presentation">
<div className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</div>
{ completionResult.provider.renderCompletions(completions) }
</div>
) : null;
}).filter((completion) => !!completion);
return completions.length > 0 ? (
<div key={i} className="mx_Autocomplete_ProviderSection" role="presentation">
<div className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</div>
{completionResult.provider.renderCompletions(completions)}
</div>
) : null;
})
.filter((completion) => !!completion);
return !this.state.hide && renderedCompletions.length > 0 ? (
<div id="mx_Autocomplete" className="mx_Autocomplete" ref={this.containerRef} role="listbox">
{ renderedCompletions }
{renderedCompletions}
</div>
) : null;
}

View file

@ -14,20 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { lexicographicCompare } from 'matrix-js-sdk/src/utils';
import { Room } from 'matrix-js-sdk/src/models/room';
import { throttle } from 'lodash';
import React from "react";
import { lexicographicCompare } from "matrix-js-sdk/src/utils";
import { Room } from "matrix-js-sdk/src/models/room";
import { throttle } from "lodash";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import AppsDrawer from './AppsDrawer';
import AppsDrawer from "./AppsDrawer";
import SettingsStore from "../../../settings/SettingsStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { UIFeature } from "../../../settings/UIFeature";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import LegacyCallViewForRoom from '../voip/LegacyCallViewForRoom';
import LegacyCallViewForRoom from "../voip/LegacyCallViewForRoom";
import { objectHasDiff } from "../../../utils/objects";
interface IProps {
@ -86,15 +86,19 @@ export default class AuxPanel extends React.Component<IProps, IState> {
}
};
private updateCounters = throttle(() => {
this.setState({ counters: this.computeCounters() });
}, 500, { leading: true, trailing: true });
private updateCounters = throttle(
() => {
this.setState({ counters: this.computeCounters() });
},
500,
{ leading: true, trailing: true },
);
private computeCounters() {
const counters = [];
if (this.props.room && SettingsStore.getValue("feature_state_counters")) {
const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter');
const stateEvs = this.props.room.currentState.getStateEvents("re.jki.counter");
stateEvs.sort((a, b) => lexicographicCompare(a.getStateKey(), b.getStateKey()));
for (const ev of stateEvs) {
@ -132,12 +136,14 @@ export default class AuxPanel extends React.Component<IProps, IState> {
let appsDrawer;
if (SettingsStore.getValue(UIFeature.Widgets)) {
appsDrawer = <AppsDrawer
room={this.props.room}
userId={this.props.userId}
showApps={this.props.showApps}
resizeNotifier={this.props.resizeNotifier}
/>;
appsDrawer = (
<AppsDrawer
room={this.props.room}
userId={this.props.userId}
showApps={this.props.showApps}
resizeNotifier={this.props.resizeNotifier}
/>
);
}
let stateViews = null;
@ -151,12 +157,16 @@ export default class AuxPanel extends React.Component<IProps, IState> {
const severity = counter.severity;
const stateKey = counter.stateKey;
let span = <span>{ title }: { value }</span>;
let span = (
<span>
{title}: {value}
</span>
);
if (link) {
span = (
<a href={link} target="_blank" rel="noreferrer noopener">
{ span }
{span}
</a>
);
}
@ -167,35 +177,31 @@ export default class AuxPanel extends React.Component<IProps, IState> {
data-severity={severity}
key={"x-" + stateKey}
>
{ span }
{span}
</span>
);
counters.push(span);
counters.push(
<span
className="m_RoomView_auxPanel_stateViews_delim"
key={"delim" + idx}
> </span>,
<span className="m_RoomView_auxPanel_stateViews_delim" key={"delim" + idx}>
{" "}
{" "}
</span>,
);
});
if (counters.length > 0) {
counters.pop(); // remove last deliminator
stateViews = (
<div className="m_RoomView_auxPanel_stateViews">
{ counters }
</div>
);
stateViews = <div className="m_RoomView_auxPanel_stateViews">{counters}</div>;
}
}
return (
<AutoHideScrollbar className="mx_RoomView_auxPanel">
{ stateViews }
{ this.props.children }
{ appsDrawer }
{ callView }
{stateViews}
{this.props.children}
{appsDrawer}
{callView}
</AutoHideScrollbar>
);
}

View file

@ -14,23 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import classNames from 'classnames';
import React, { createRef, ClipboardEvent } from 'react';
import { Room } from 'matrix-js-sdk/src/models/room';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import classNames from "classnames";
import React, { createRef, ClipboardEvent } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import EMOTICON_REGEX from "emojibase-regex/emoticon";
import { logger } from "matrix-js-sdk/src/logger";
import EditorModel from '../../../editor/model';
import HistoryManager from '../../../editor/history';
import { Caret, setSelection } from '../../../editor/caret';
import { formatRange, formatRangeAsLink, replaceRangeAndMoveCaret, toggleInlineFormat }
from '../../../editor/operations';
import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom';
import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
import { getAutoCompleteCreator, Part, Type } from '../../../editor/parts';
import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize';
import { renderModel } from '../../../editor/render';
import EditorModel from "../../../editor/model";
import HistoryManager from "../../../editor/history";
import { Caret, setSelection } from "../../../editor/caret";
import {
formatRange,
formatRangeAsLink,
replaceRangeAndMoveCaret,
toggleInlineFormat,
} from "../../../editor/operations";
import { getCaretOffsetAndText, getRangeForSelection } from "../../../editor/dom";
import Autocomplete, { generateCompletionDomId } from "../rooms/Autocomplete";
import { getAutoCompleteCreator, Part, Type } from "../../../editor/parts";
import { parseEvent, parsePlainTextMessage } from "../../../editor/deserialize";
import { renderModel } from "../../../editor/render";
import SettingsStore from "../../../settings/SettingsStore";
import { IS_MAC, Key } from "../../../Keyboard";
import { EMOTICON_TO_EMOJI } from "../../../emoji";
@ -40,19 +44,19 @@ import MessageComposerFormatBar, { Formatting } from "./MessageComposerFormatBar
import DocumentOffset from "../../../editor/offset";
import { IDiff } from "../../../editor/diff";
import AutocompleteWrapperModel from "../../../editor/autocomplete";
import DocumentPosition from '../../../editor/position';
import DocumentPosition from "../../../editor/position";
import { ICompletion } from "../../../autocomplete/Autocompleter";
import { getKeyBindingsManager } from '../../../KeyBindingsManager';
import { ALTERNATE_KEY_NAME, KeyBindingAction } from '../../../accessibility/KeyboardShortcuts';
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { ALTERNATE_KEY_NAME, KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { _t } from "../../../languageHandler";
import { linkify } from '../../../linkify-matrix';
import { SdkContextClass } from '../../../contexts/SDKContext';
import { linkify } from "../../../linkify-matrix";
import { SdkContextClass } from "../../../contexts/SDKContext";
// matches emoticons which follow the start of a line or whitespace
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s|:^$');
export const REGEX_EMOTICON = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')$');
const REGEX_EMOTICON_WHITESPACE = new RegExp("(?:^|\\s)(" + EMOTICON_REGEX.source + ")\\s|:^$");
export const REGEX_EMOTICON = new RegExp("(?:^|\\s)(" + EMOTICON_REGEX.source + ")$");
const SURROUND_WITH_CHARACTERS = ["\"", "_", "`", "'", "*", "~", "$"];
const SURROUND_WITH_CHARACTERS = ['"', "_", "`", "'", "*", "~", "$"];
const SURROUND_WITH_DOUBLE_CHARACTERS = new Map([
["(", ")"],
["[", "]"],
@ -61,10 +65,13 @@ const SURROUND_WITH_DOUBLE_CHARACTERS = new Map([
]);
function ctrlShortcutLabel(key: string, needsShift = false, needsAlt = false): string {
return (IS_MAC ? "⌘" : _t(ALTERNATE_KEY_NAME[Key.CONTROL])) +
(needsShift ? ("+" + _t(ALTERNATE_KEY_NAME[Key.SHIFT])) : "") +
(needsAlt ? ("+" + _t(ALTERNATE_KEY_NAME[Key.ALT])) : "") +
"+" + key;
return (
(IS_MAC ? "⌘" : _t(ALTERNATE_KEY_NAME[Key.CONTROL])) +
(needsShift ? "+" + _t(ALTERNATE_KEY_NAME[Key.SHIFT]) : "") +
(needsAlt ? "+" + _t(ALTERNATE_KEY_NAME[Key.ALT]) : "") +
"+" +
key
);
}
function cloneSelection(selection: Selection): Partial<Selection> {
@ -80,13 +87,15 @@ function cloneSelection(selection: Selection): Partial<Selection> {
}
function selectionEquals(a: Partial<Selection>, b: Selection): boolean {
return a.anchorNode === b.anchorNode &&
return (
a.anchorNode === b.anchorNode &&
a.anchorOffset === b.anchorOffset &&
a.focusNode === b.focusNode &&
a.focusOffset === b.focusOffset &&
a.isCollapsed === b.isCollapsed &&
a.rangeCount === b.rangeCount &&
a.type === b.type;
a.type === b.type
);
}
interface IProps {
@ -140,15 +149,27 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
showVisualBell: false,
};
this.useMarkdownHandle = SettingsStore.watchSetting('MessageComposerInput.useMarkdown', null,
this.configureUseMarkdown);
this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
this.configureEmoticonAutoReplace);
this.useMarkdownHandle = SettingsStore.watchSetting(
"MessageComposerInput.useMarkdown",
null,
this.configureUseMarkdown,
);
this.emoticonSettingHandle = SettingsStore.watchSetting(
"MessageComposerInput.autoReplaceEmoji",
null,
this.configureEmoticonAutoReplace,
);
this.configureEmoticonAutoReplace();
this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null,
this.configureShouldShowPillAvatar);
this.surroundWithHandle = SettingsStore.watchSetting("MessageComposerInput.surroundWith", null,
this.surroundWithSettingChanged);
this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting(
"Pill.shouldShowPillAvatar",
null,
this.configureShouldShowPillAvatar,
);
this.surroundWithHandle = SettingsStore.watchSetting(
"MessageComposerInput.surroundWith",
null,
this.surroundWithSettingChanged,
);
}
public componentDidUpdate(prevProps: IProps) {
@ -208,7 +229,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => {
renderModel(this.editorRef.current, this.props.model);
if (selection) { // set the caret/selection
if (selection) {
// set the caret/selection
try {
setSelection(this.editorRef.current, this.props.model, selection);
} catch (err) {
@ -246,11 +268,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
isTyping = false;
}
}
SdkContextClass.instance.typingStore.setSelfTyping(
this.props.room.roomId,
this.props.threadId,
isTyping,
);
SdkContextClass.instance.typingStore.setSelfTyping(this.props.room.roomId, this.props.threadId, isTyping);
if (this.props.onChange) {
this.props.onChange();
@ -259,7 +277,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
private showPlaceholder(): void {
// escape single quotes
const placeholder = this.props.placeholder.replace(/'/g, '\\\'');
const placeholder = this.props.placeholder.replace(/'/g, "\\'");
this.editorRef.current.style.setProperty("--placeholder", `'${placeholder}'`);
this.editorRef.current.classList.add("mx_BasicMessageComposer_inputEmpty");
}
@ -287,7 +305,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
// however, doing this async seems to break things in Safari for some reason, so browser sniff.
const ua = navigator.userAgent.toLowerCase();
const isSafari = ua.includes('safari/') && !ua.includes('chrome/');
const isSafari = ua.includes("safari/") && !ua.includes("chrome/");
if (isSafari) {
this.onInput({ inputType: "insertCompositionText" });
@ -311,7 +329,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
if (text) {
const { model } = this.props;
const range = getRangeForSelection(this.editorRef.current, model, selection);
const selectedParts = range.parts.map(p => p.serialize());
const selectedParts = range.parts.map((p) => p.serialize());
event.clipboardData.setData("application/x-element-composer", JSON.stringify(selectedParts));
event.clipboardData.setData("text/plain", text); // so plain copy/paste works
if (type === "cut") {
@ -346,7 +364,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
let parts: Part[];
if (partsText) {
const serializedTextParts = JSON.parse(partsText);
parts = serializedTextParts.map(p => partCreator.deserializePart(p));
parts = serializedTextParts.map((p) => partCreator.deserializePart(p));
} else {
parts = parsePlainTextMessage(plainText, partCreator, { shouldEscape: false });
}
@ -593,16 +611,16 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
private async tabCompleteName(): Promise<void> {
try {
await new Promise<void>(resolve => this.setState({ showVisualBell: false }, resolve));
await new Promise<void>((resolve) => this.setState({ showVisualBell: false }, resolve));
const { model } = this.props;
const caret = this.getCaret();
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
const range = model.startRange(position);
range.expandBackwardsWhile((index, offset, part) => {
return part.text[offset] !== " " && part.text[offset] !== "+" && (
part.type === Type.Plain ||
part.type === Type.PillCandidate ||
part.type === Type.Command
return (
part.text[offset] !== " " &&
part.text[offset] !== "+" &&
(part.type === Type.Plain || part.type === Type.PillCandidate || part.type === Type.Command)
);
});
const { partCreator } = model;
@ -664,7 +682,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
};
private transform = (documentPosition: DocumentPosition): void => {
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
const shouldReplace = SettingsStore.getValue("MessageComposerInput.autoReplaceEmoji");
if (shouldReplace) this.replaceEmoticon(documentPosition, REGEX_EMOTICON_WHITESPACE);
};
@ -685,10 +703,12 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
const partCreator = model.partCreator;
// TODO: does this allow us to get rid of EditorStateTransfer?
// not really, but we could not serialize the parts, and just change the autoCompleter
partCreator.setAutoCompleteCreator(getAutoCompleteCreator(
() => this.autocompleteRef.current,
query => new Promise(resolve => this.setState({ query }, resolve)),
));
partCreator.setAutoCompleteCreator(
getAutoCompleteCreator(
() => this.autocompleteRef.current,
(query) => new Promise((resolve) => this.setState({ query }, resolve)),
),
);
// initial render of model
this.updateEditorState(this.getInitialCaretPosition());
// attach input listener by hand so React doesn't proxy the events,
@ -731,23 +751,25 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
if (this.state.autoComplete) {
const query = this.state.query;
const queryLen = query.length;
autoComplete = (<div className="mx_BasicMessageComposer_AutoCompleteWrapper">
<Autocomplete
ref={this.autocompleteRef}
query={query}
onConfirm={this.onAutoCompleteConfirm}
onSelectionChange={this.onAutoCompleteSelectionChange}
selection={{ beginning: true, end: queryLen, start: queryLen }}
room={this.props.room}
/>
</div>);
autoComplete = (
<div className="mx_BasicMessageComposer_AutoCompleteWrapper">
<Autocomplete
ref={this.autocompleteRef}
query={query}
onConfirm={this.onAutoCompleteConfirm}
onSelectionChange={this.onAutoCompleteSelectionChange}
selection={{ beginning: true, end: queryLen, start: queryLen }}
room={this.props.room}
/>
</div>
);
}
const wrapperClasses = classNames("mx_BasicMessageComposer", {
"mx_BasicMessageComposer_input_error": this.state.showVisualBell,
mx_BasicMessageComposer_input_error: this.state.showVisualBell,
});
const classes = classNames("mx_BasicMessageComposer_input", {
"mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar,
"mx_BasicMessageComposer_input_disabled": this.props.disabled,
mx_BasicMessageComposer_input_shouldShowPillAvatar: this.state.showPillAvatar,
mx_BasicMessageComposer_input_disabled: this.props.disabled,
});
const shortcuts = {
@ -765,33 +787,39 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
activeDescendant = generateCompletionDomId(completionIndex);
}
return (<div className={wrapperClasses}>
{ autoComplete }
<MessageComposerFormatBar ref={this.formatBarRef} onAction={this.onFormatAction} shortcuts={shortcuts} />
<div
className={classes}
contentEditable={this.props.disabled ? null : true}
tabIndex={0}
onBlur={this.onBlur}
onFocus={this.onFocus}
onCopy={this.onCopy}
onCut={this.onCut}
onPaste={this.onPaste}
onKeyDown={this.onKeyDown}
ref={this.editorRef}
aria-label={this.props.label}
role="textbox"
aria-multiline="true"
aria-autocomplete="list"
aria-haspopup="listbox"
aria-expanded={hasAutocomplete ? true : undefined}
aria-owns={hasAutocomplete ? "mx_Autocomplete" : undefined}
aria-activedescendant={activeDescendant}
dir="auto"
aria-disabled={this.props.disabled}
data-testid="basicmessagecomposer"
/>
</div>);
return (
<div className={wrapperClasses}>
{autoComplete}
<MessageComposerFormatBar
ref={this.formatBarRef}
onAction={this.onFormatAction}
shortcuts={shortcuts}
/>
<div
className={classes}
contentEditable={this.props.disabled ? null : true}
tabIndex={0}
onBlur={this.onBlur}
onFocus={this.onFocus}
onCopy={this.onCopy}
onCut={this.onCut}
onPaste={this.onPaste}
onKeyDown={this.onKeyDown}
ref={this.editorRef}
aria-label={this.props.label}
role="textbox"
aria-multiline="true"
aria-autocomplete="list"
aria-haspopup="listbox"
aria-expanded={hasAutocomplete ? true : undefined}
aria-owns={hasAutocomplete ? "mx_Autocomplete" : undefined}
aria-activedescendant={activeDescendant}
dir="auto"
aria-disabled={this.props.disabled}
data-testid="basicmessagecomposer"
/>
</div>
);
}
public focus(): void {
@ -803,8 +831,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
const { model } = this.props;
const { partCreator } = model;
const member = this.props.room.getMember(userId);
const displayName = member ?
member.rawDisplayName : userId;
const displayName = member ? member.rawDisplayName : userId;
const caret = this.getCaret();
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
// Insert suffix only if the caret is at the start of the composer

View file

@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ComponentProps, useContext } from 'react';
import classNames from 'classnames';
import React, { ComponentProps, useContext } from "react";
import classNames from "classnames";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { MenuItem } from "../../structures/ContextMenu";
import { OverflowMenuContext } from './MessageComposerButtons';
import { IconizedContextMenuOption } from '../context_menus/IconizedContextMenu';
import { OverflowMenuContext } from "./MessageComposerButtons";
import { IconizedContextMenuOption } from "../context_menus/IconizedContextMenu";
interface ICollapsibleButtonProps extends ComponentProps<typeof MenuItem> {
title: string;
@ -30,18 +30,12 @@ interface ICollapsibleButtonProps extends ComponentProps<typeof MenuItem> {
export const CollapsibleButton = ({ title, children, className, iconClassName, ...props }: ICollapsibleButtonProps) => {
const inOverflowMenu = !!useContext(OverflowMenuContext);
if (inOverflowMenu) {
return <IconizedContextMenuOption
{...props}
iconClassName={iconClassName}
label={title}
/>;
return <IconizedContextMenuOption {...props} iconClassName={iconClassName} label={title} />;
}
return <AccessibleTooltipButton
{...props}
title={title}
className={classNames(className, iconClassName)}
>
{ children }
</AccessibleTooltipButton>;
return (
<AccessibleTooltipButton {...props} title={title} className={classNames(className, iconClassName)}>
{children}
</AccessibleTooltipButton>
);
};

View file

@ -16,9 +16,9 @@ limitations under the License.
*/
import React, { useState } from "react";
import classNames from 'classnames';
import classNames from "classnames";
import { _t, _td } from '../../../languageHandler';
import { _t, _td } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import Tooltip, { Alignment } from "../elements/Tooltip";
import { E2EStatus } from "../../../utils/ShieldUtils";
@ -65,13 +65,16 @@ const E2EIcon: React.FC<IProps> = ({
}) => {
const [hover, setHover] = useState(false);
const classes = classNames({
mx_E2EIcon: true,
mx_E2EIcon_bordered: bordered,
mx_E2EIcon_warning: status === E2EState.Warning,
mx_E2EIcon_normal: status === E2EState.Normal,
mx_E2EIcon_verified: status === E2EState.Verified,
}, className);
const classes = classNames(
{
mx_E2EIcon: true,
mx_E2EIcon_bordered: bordered,
mx_E2EIcon_warning: status === E2EState.Warning,
mx_E2EIcon_normal: status === E2EState.Normal,
mx_E2EIcon_verified: status === E2EState.Verified,
},
className,
);
let e2eTitle;
if (isUser) {
@ -102,14 +105,16 @@ const E2EIcon: React.FC<IProps> = ({
className={classes}
style={style}
>
{ tip }
{tip}
</AccessibleButton>
);
}
return <div onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} className={classes} style={style}>
{ tip }
</div>;
return (
<div onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} className={classes} style={style}>
{tip}
</div>
);
};
export default E2EIcon;

View file

@ -14,34 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef, KeyboardEvent } from 'react';
import classNames from 'classnames';
import { EventStatus, IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { MsgType } from 'matrix-js-sdk/src/@types/event';
import { Room } from 'matrix-js-sdk/src/models/room';
import React, { createRef, KeyboardEvent } from "react";
import classNames from "classnames";
import { EventStatus, IContent, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MsgType } from "matrix-js-sdk/src/@types/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import EditorModel from '../../../editor/model';
import { getCaretOffsetAndText } from '../../../editor/dom';
import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize';
import { findEditableEvent } from '../../../utils/EventUtils';
import { parseEvent } from '../../../editor/deserialize';
import { CommandPartCreator, Part, PartCreator } from '../../../editor/parts';
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import EditorModel from "../../../editor/model";
import { getCaretOffsetAndText } from "../../../editor/dom";
import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from "../../../editor/serialize";
import { findEditableEvent } from "../../../utils/EventUtils";
import { parseEvent } from "../../../editor/deserialize";
import { CommandPartCreator, Part, PartCreator } from "../../../editor/parts";
import EditorStateTransfer from "../../../utils/EditorStateTransfer";
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
import { CommandCategories } from '../../../SlashCommands';
import { CommandCategories } from "../../../SlashCommands";
import { Action } from "../../../dispatcher/actions";
import { getKeyBindingsManager } from '../../../KeyBindingsManager';
import SendHistoryManager from '../../../SendHistoryManager';
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import SendHistoryManager from "../../../SendHistoryManager";
import { ActionPayload } from "../../../dispatcher/payloads";
import AccessibleButton from '../elements/AccessibleButton';
import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
import AccessibleButton from "../elements/AccessibleButton";
import { createRedactEventDialog } from "../dialogs/ConfirmRedactDialog";
import SettingsStore from "../../../settings/SettingsStore";
import { withMatrixClientHOC, MatrixClientProps } from '../../../contexts/MatrixClientContext';
import RoomContext from '../../../contexts/RoomContext';
import { withMatrixClientHOC, MatrixClientProps } from "../../../contexts/MatrixClientContext";
import RoomContext from "../../../contexts/RoomContext";
import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } from "../../../editor/commands";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
@ -60,17 +60,14 @@ function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
function getTextReplyFallback(mxEvent: MatrixEvent): string {
const body = mxEvent.getContent().body;
const lines = body.split("\n").map(l => l.trim());
const lines = body.split("\n").map((l) => l.trim());
if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) {
return `${lines[0]}\n\n`;
}
return "";
}
function createEditContent(
model: EditorModel,
editedEvent: MatrixEvent,
): IContent {
function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IContent {
const isEmote = containsEmote(model);
if (isEmote) {
model = stripEmoteCommand(model);
@ -87,8 +84,8 @@ function createEditContent(
const body = textSerialize(model);
const newContent: IContent = {
"msgtype": isEmote ? MsgType.Emote : MsgType.Text,
"body": body,
msgtype: isEmote ? MsgType.Emote : MsgType.Text,
body: body,
};
const contentBody: IContent = {
msgtype: newContent.msgtype,
@ -109,8 +106,8 @@ function createEditContent(
const relation = {
"m.new_content": newContent,
"m.relates_to": {
"rel_type": "m.replace",
"event_id": editedEvent.getId(),
rel_type: "m.replace",
event_id: editedEvent.getId(),
},
};
@ -256,7 +253,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
if (json) {
try {
const { parts: serializedParts } = JSON.parse(json);
const parts: Part[] = serializedParts.map(p => partCreator.deserializePart(p));
const parts: Part[] = serializedParts.map((p) => partCreator.deserializePart(p));
return parts;
} catch (e) {
logger.error("Error parsing editing state: ", e);
@ -280,9 +277,12 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
private isContentModified(newContent: IContent): boolean {
// if nothing has changed then bail
const oldContent = this.props.editState.getEvent().getContent();
if (oldContent["msgtype"] === newContent["msgtype"] && oldContent["body"] === newContent["body"] &&
if (
oldContent["msgtype"] === newContent["msgtype"] &&
oldContent["body"] === newContent["body"] &&
oldContent["format"] === newContent["format"] &&
oldContent["formatted_body"] === newContent["formatted_body"]) {
oldContent["formatted_body"] === newContent["formatted_body"]
) {
return false;
}
return true;
@ -301,7 +301,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
});
// Replace emoticon at the end of the message
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
if (SettingsStore.getValue("MessageComposerInput.autoReplaceEmoji")) {
const caret = this.editorRef.current?.getCaret();
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
@ -311,7 +311,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
let shouldSend = true;
if (newContent?.body === '') {
if (newContent?.body === "") {
this.cancelPreviousPendingEdit();
createRedactEventDialog({
mxEvent: editedEvent,
@ -339,7 +339,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
} else {
shouldSend = false;
}
} else if (!await shouldSendAnyway(commandText)) {
} else if (!(await shouldSendAnyway(commandText))) {
// if !sendAnyway bail to let the user edit the composer and try again
return;
}
@ -361,10 +361,10 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
private cancelPreviousPendingEdit(): void {
const originalEvent = this.props.editState.getEvent();
const previousEdit = originalEvent.replacingEvent();
if (previousEdit && (
previousEdit.status === EventStatus.QUEUED ||
previousEdit.status === EventStatus.NOT_SENT
)) {
if (
previousEdit &&
(previousEdit.status === EventStatus.QUEUED || previousEdit.status === EventStatus.NOT_SENT)
) {
this.props.mxClient.cancelPendingEvent(previousEdit);
}
}
@ -400,13 +400,15 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
if (editState.hasEditorState()) {
// if restoring state from a previous editor,
// restore serialized parts from the state
parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p));
parts = editState.getSerializedParts().map((p) => partCreator.deserializePart(p));
} else {
// otherwise, either restore serialized parts from localStorage or parse the body of the event
const restoredParts = this.restoreStoredEditorState(partCreator);
parts = restoredParts || parseEvent(editState.getEvent(), partCreator, {
shouldEscape: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
});
parts =
restoredParts ||
parseEvent(editState.getEvent(), partCreator, {
shouldEscape: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
});
isRestored = !!restoredParts;
}
this.model = new EditorModel(parts, partCreator);
@ -445,25 +447,27 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
};
render() {
return (<div className={classNames("mx_EditMessageComposer", this.props.className)} onKeyDown={this.onKeyDown}>
<BasicMessageComposer
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}
/>
<div className="mx_EditMessageComposer_buttons">
<AccessibleButton kind="secondary" onClick={this.cancelEdit}>
{ _t("Cancel") }
</AccessibleButton>
<AccessibleButton kind="primary" onClick={this.sendEdit} disabled={this.state.saveDisabled}>
{ _t("Save") }
</AccessibleButton>
return (
<div className={classNames("mx_EditMessageComposer", this.props.className)} onKeyDown={this.onKeyDown}>
<BasicMessageComposer
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}
/>
<div className="mx_EditMessageComposer_buttons">
<AccessibleButton kind="secondary" onClick={this.cancelEdit}>
{_t("Cancel")}
</AccessibleButton>
<AccessibleButton kind="primary" onClick={this.sendEdit} disabled={this.state.saveDisabled}>
{_t("Save")}
</AccessibleButton>
</div>
</div>
</div>);
);
}
}

View file

@ -35,41 +35,39 @@ export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonP
let contextMenu: React.ReactElement | null = null;
if (menuDisplayed && button.current) {
const position = (
menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect())
);
const position = menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect());
contextMenu = <ContextMenu
{...position}
onFinished={() => {
closeMenu();
overflowMenuCloser?.();
}}
managed={false}
>
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
</ContextMenu>;
contextMenu = (
<ContextMenu
{...position}
onFinished={() => {
closeMenu();
overflowMenuCloser?.();
}}
managed={false}
>
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
</ContextMenu>
);
}
const computedClassName = classNames(
"mx_EmojiButton",
className,
{
"mx_EmojiButton_highlight": menuDisplayed,
},
);
const computedClassName = classNames("mx_EmojiButton", className, {
mx_EmojiButton_highlight: menuDisplayed,
});
// TODO: replace ContextMenuTooltipButton with a unified representation of
// the header buttons and the right panel buttons
return <>
<CollapsibleButton
className={computedClassName}
iconClassName="mx_EmojiButton_icon"
onClick={openMenu}
title={_t("Emoji")}
inputRef={button}
/>
return (
<>
<CollapsibleButton
className={computedClassName}
iconClassName="mx_EmojiButton_icon"
onClick={openMenu}
title={_t("Emoji")}
inputRef={button}
/>
{ contextMenu }
</>;
{contextMenu}
</>
);
}

View file

@ -16,13 +16,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import classNames from "classnames";
import AccessibleButton from '../elements/AccessibleButton';
import { _t, _td } from '../../../languageHandler';
import E2EIcon, { E2EState } from './E2EIcon';
import BaseAvatar from '../avatars/BaseAvatar';
import AccessibleButton from "../elements/AccessibleButton";
import { _t, _td } from "../../../languageHandler";
import E2EIcon, { E2EState } from "./E2EIcon";
import BaseAvatar from "../avatars/BaseAvatar";
import PresenceLabel from "./PresenceLabel";
export enum PowerStatus {
@ -36,28 +36,28 @@ const PowerLabel: Record<PowerStatus, string> = {
};
const PRESENCE_CLASS = {
"offline": "mx_EntityTile_offline",
"online": "mx_EntityTile_online",
"unavailable": "mx_EntityTile_unavailable",
offline: "mx_EntityTile_offline",
online: "mx_EntityTile_online",
unavailable: "mx_EntityTile_unavailable",
};
function presenceClassForMember(presenceState: string, lastActiveAgo: number, showPresence: boolean): string {
if (showPresence === false) {
return 'mx_EntityTile_online_beenactive';
return "mx_EntityTile_online_beenactive";
}
// offline is split into two categories depending on whether we have
// a last_active_ago for them.
if (presenceState === 'offline') {
if (presenceState === "offline") {
if (lastActiveAgo) {
return PRESENCE_CLASS['offline'] + '_beenactive';
return PRESENCE_CLASS["offline"] + "_beenactive";
} else {
return PRESENCE_CLASS['offline'] + '_neveractive';
return PRESENCE_CLASS["offline"] + "_neveractive";
}
} else if (presenceState) {
return PRESENCE_CLASS[presenceState];
} else {
return PRESENCE_CLASS['offline'] + '_neveractive';
return PRESENCE_CLASS["offline"] + "_neveractive";
}
}
@ -105,13 +105,15 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
render() {
const mainClassNames = {
"mx_EntityTile": true,
"mx_EntityTile_noHover": this.props.suppressOnHover,
mx_EntityTile: true,
mx_EntityTile_noHover: this.props.suppressOnHover,
};
if (this.props.className) mainClassNames[this.props.className] = true;
const presenceClass = presenceClassForMember(
this.props.presenceState, this.props.presenceLastActiveAgo, this.props.showPresence,
this.props.presenceState,
this.props.presenceLastActiveAgo,
this.props.showPresence,
);
mainClassNames[presenceClass] = true;
@ -119,39 +121,38 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
const name = this.props.nameJSX || this.props.name;
if (!this.props.suppressOnHover) {
const activeAgo = this.props.presenceLastActiveAgo ?
(Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1;
const activeAgo = this.props.presenceLastActiveAgo
? Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)
: -1;
let presenceLabel = null;
if (this.props.showPresence) {
presenceLabel = <PresenceLabel activeAgo={activeAgo}
currentlyActive={this.props.presenceCurrentlyActive}
presenceState={this.props.presenceState} />;
presenceLabel = (
<PresenceLabel
activeAgo={activeAgo}
currentlyActive={this.props.presenceCurrentlyActive}
presenceState={this.props.presenceState}
/>
);
}
if (this.props.subtextLabel) {
presenceLabel = <span className="mx_EntityTile_subtext">{ this.props.subtextLabel }</span>;
presenceLabel = <span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>;
}
nameEl = (
<div className="mx_EntityTile_details">
<div className="mx_EntityTile_name">
{ name }
</div>
{ presenceLabel }
<div className="mx_EntityTile_name">{name}</div>
{presenceLabel}
</div>
);
} else if (this.props.subtextLabel) {
nameEl = (
<div className="mx_EntityTile_details">
<div className="mx_EntityTile_name">
{ name }
</div>
<span className="mx_EntityTile_subtext">{ this.props.subtextLabel }</span>
<div className="mx_EntityTile_name">{name}</div>
<span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
</div>
);
} else {
nameEl = (
<div className="mx_EntityTile_name">{ name }</div>
);
nameEl = <div className="mx_EntityTile_name">{name}</div>;
}
let inviteButton;
@ -167,7 +168,7 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
const powerStatus = this.props.powerStatus;
if (powerStatus) {
const powerText = _t(PowerLabel[powerStatus]);
powerLabel = <div className="mx_EntityTile_power">{ powerText }</div>;
powerLabel = <div className="mx_EntityTile_power">{powerText}</div>;
}
let e2eIcon;
@ -176,8 +177,9 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
e2eIcon = <E2EIcon status={e2eStatus} isUser={true} bordered={true} />;
}
const av = this.props.avatarJsx ||
<BaseAvatar name={this.props.name} width={36} height={36} aria-hidden="true" />;
const av = this.props.avatarJsx || (
<BaseAvatar name={this.props.name} width={36} height={36} aria-hidden="true" />
);
// The wrapping div is required to make the magic mouse listener work, for some reason.
return (
@ -188,12 +190,12 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
onClick={this.props.onClick}
>
<div className="mx_EntityTile_avatar">
{ av }
{ e2eIcon }
{av}
{e2eIcon}
</div>
{ nameEl }
{ powerLabel }
{ inviteButton }
{nameEl}
{powerLabel}
{inviteButton}
</AccessibleButton>
</div>
);

File diff suppressed because it is too large Load diff

View file

@ -17,10 +17,7 @@ limitations under the License.
import React from "react";
import classNames from "classnames";
import {
RovingAccessibleButton,
RovingAccessibleTooltipButton,
} from "../../../accessibility/RovingTabIndex";
import { RovingAccessibleButton, RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
import NotificationBadge from "./NotificationBadge";
import { NotificationState } from "../../../stores/notifications/NotificationState";
import { ButtonEvent } from "../elements/AccessibleButton";
@ -58,35 +55,30 @@ export default class ExtraTile extends React.Component<IProps, IState> {
public render(): React.ReactElement {
// XXX: We copy classes because it's easier
const classes = classNames({
'mx_ExtraTile': true,
'mx_RoomTile': true,
'mx_RoomTile_selected': this.props.isSelected,
'mx_RoomTile_minimized': this.props.isMinimized,
mx_ExtraTile: true,
mx_RoomTile: true,
mx_RoomTile_selected: this.props.isSelected,
mx_RoomTile_minimized: this.props.isMinimized,
});
let badge;
if (this.props.notificationState) {
badge = (
<NotificationBadge
notification={this.props.notificationState}
forceCount={false}
/>
);
badge = <NotificationBadge notification={this.props.notificationState} forceCount={false} />;
}
let name = this.props.displayName;
if (typeof name !== 'string') name = '';
if (typeof name !== "string") name = "";
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
const nameClasses = classNames({
"mx_RoomTile_title": true,
"mx_RoomTile_titleHasUnreadEvents": this.props.notificationState?.isUnread,
mx_RoomTile_title: true,
mx_RoomTile_titleHasUnreadEvents: this.props.notificationState?.isUnread,
});
let nameContainer = (
<div className="mx_RoomTile_titleContainer">
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
{ name }
{name}
</div>
</div>
);
@ -107,15 +99,11 @@ export default class ExtraTile extends React.Component<IProps, IState> {
role="treeitem"
title={this.props.isMinimized ? name : undefined}
>
<div className="mx_RoomTile_avatarContainer">
{ this.props.avatar }
</div>
<div className="mx_RoomTile_avatarContainer">{this.props.avatar}</div>
<div className="mx_RoomTile_details">
<div className="mx_RoomTile_primaryDetails">
{ nameContainer }
<div className="mx_RoomTile_badgeContainer">
{ badge }
</div>
{nameContainer}
<div className="mx_RoomTile_badgeContainer">{badge}</div>
</div>
</div>
</Button>

View file

@ -37,11 +37,9 @@ const HistoryTile = () => {
subtitle = _t("Encrypted messages before this point are unavailable.");
}
return <EventTileBubble
className="mx_HistoryTile"
title={_t("You can't see earlier messages")}
subtitle={subtitle}
/>;
return (
<EventTileBubble className="mx_HistoryTile" title={_t("You can't see earlier messages")} subtitle={subtitle} />
);
};
export default HistoryTile;

View file

@ -15,10 +15,10 @@ limitations under the License.
*/
import React from "react";
import classNames from 'classnames';
import classNames from "classnames";
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
interface IProps {
numUnreadMessages?: number;
@ -28,21 +28,23 @@ interface IProps {
const JumpToBottomButton: React.FC<IProps> = (props) => {
const className = classNames({
'mx_JumpToBottomButton': true,
'mx_JumpToBottomButton_highlight': props.highlight,
mx_JumpToBottomButton: true,
mx_JumpToBottomButton_highlight: props.highlight,
});
let badge;
if (props.numUnreadMessages) {
badge = (<div className="mx_JumpToBottomButton_badge">{ props.numUnreadMessages }</div>);
badge = <div className="mx_JumpToBottomButton_badge">{props.numUnreadMessages}</div>;
}
return (<div className={className}>
<AccessibleButton
className="mx_JumpToBottomButton_scrollDown"
title={_t("Scroll to most recent messages")}
onClick={props.onScrollToBottomClick}
/>
{ badge }
</div>);
return (
<div className={className}>
<AccessibleButton
className="mx_JumpToBottomButton_scrollDown"
title={_t("Scroll to most recent messages")}
onClick={props.onScrollToBottomClick}
/>
{badge}
</div>
);
};
export default JumpToBottomButton;

View file

@ -40,9 +40,13 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
const [expanded, toggleExpanded] = useStateToggle();
const ts = mxEvent.getTs();
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(async () => {
return fetchPreviews(cli, links, ts);
}, [links, ts], []);
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(
async () => {
return fetchPreviews(cli, links, ts);
},
[links, ts],
[],
);
useEffect(() => {
onHeightChanged();
@ -52,50 +56,55 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
let toggleButton: JSX.Element;
if (previews.length > INITIAL_NUM_PREVIEWS) {
toggleButton = <AccessibleButton onClick={toggleExpanded}>
{ expanded
? _t("Collapse")
: _t("Show %(count)s other previews", { count: previews.length - showPreviews.length }) }
</AccessibleButton>;
toggleButton = (
<AccessibleButton onClick={toggleExpanded}>
{expanded
? _t("Collapse")
: _t("Show %(count)s other previews", { count: previews.length - showPreviews.length })}
</AccessibleButton>
);
}
return <div className="mx_LinkPreviewGroup">
{ showPreviews.map(([link, preview], i) => (
<LinkPreviewWidget key={link} link={link} preview={preview} mxEvent={mxEvent}>
{ i === 0 ? (
<AccessibleButton
className="mx_LinkPreviewGroup_hide"
onClick={onCancelClick}
aria-label={_t("Close preview")}
>
<img
className="mx_filterFlipColor"
alt=""
role="presentation"
src={require("../../../../res/img/cancel.svg").default}
width="18"
height="18"
/>
</AccessibleButton>
): undefined }
</LinkPreviewWidget>
)) }
{ toggleButton }
</div>;
return (
<div className="mx_LinkPreviewGroup">
{showPreviews.map(([link, preview], i) => (
<LinkPreviewWidget key={link} link={link} preview={preview} mxEvent={mxEvent}>
{i === 0 ? (
<AccessibleButton
className="mx_LinkPreviewGroup_hide"
onClick={onCancelClick}
aria-label={_t("Close preview")}
>
<img
className="mx_filterFlipColor"
alt=""
role="presentation"
src={require("../../../../res/img/cancel.svg").default}
width="18"
height="18"
/>
</AccessibleButton>
) : undefined}
</LinkPreviewWidget>
))}
{toggleButton}
</div>
);
};
const fetchPreviews = (cli: MatrixClient, links: string[], ts: number):
Promise<[string, IPreviewUrlResponse][]> => {
return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(async link => {
try {
const preview = await cli.getUrlPreview(link, ts);
if (preview && Object.keys(preview).length > 0) {
return [link, preview];
const fetchPreviews = (cli: MatrixClient, links: string[], ts: number): Promise<[string, IPreviewUrlResponse][]> => {
return Promise.all<[string, IPreviewUrlResponse] | void>(
links.map(async (link) => {
try {
const preview = await cli.getUrlPreview(link, ts);
if (preview && Object.keys(preview).length > 0) {
return [link, preview];
}
} catch (error) {
logger.error("Failed to get URL preview: " + error);
}
} catch (error) {
logger.error("Failed to get URL preview: " + error);
}
})).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
}),
).then((a) => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
};
export default LinkPreviewGroup;

View file

@ -14,19 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ComponentProps, createRef } from 'react';
import { decode } from 'html-entities';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
import React, { ComponentProps, createRef } from "react";
import { decode } from "html-entities";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { IPreviewUrlResponse } from "matrix-js-sdk/src/client";
import { linkifyElement } from '../../../HtmlUtils';
import { linkifyElement } from "../../../HtmlUtils";
import SettingsStore from "../../../settings/SettingsStore";
import Modal from "../../../Modal";
import * as ImageUtils from "../../../ImageUtils";
import { mediaFromMxc } from "../../../customisations/Media";
import ImageView from '../elements/ImageView';
import LinkWithTooltip from '../elements/LinkWithTooltip';
import PlatformPeg from '../../../PlatformPeg';
import ImageView from "../elements/ImageView";
import LinkWithTooltip from "../elements/LinkWithTooltip";
import PlatformPeg from "../../../PlatformPeg";
interface IProps {
link: string;
@ -50,7 +50,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
}
}
private onImageClick = ev => {
private onImageClick = (ev) => {
const p = this.props.preview;
if (ev.button != 0 || ev.metaKey) return;
ev.preventDefault();
@ -98,28 +98,32 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
const imageMaxHeight = 100;
if (image && image.startsWith("mxc://")) {
// We deliberately don't want a square here, so use the source HTTP thumbnail function
image = mediaFromMxc(image).getThumbnailOfSourceHttp(imageMaxWidth, imageMaxHeight, 'scale');
image = mediaFromMxc(image).getThumbnailOfSourceHttp(imageMaxWidth, imageMaxHeight, "scale");
}
let thumbHeight = imageMaxHeight;
if (p["og:image:width"] && p["og:image:height"]) {
thumbHeight = ImageUtils.thumbHeight(
p["og:image:width"], p["og:image:height"],
imageMaxWidth, imageMaxHeight,
p["og:image:width"],
p["og:image:height"],
imageMaxWidth,
imageMaxHeight,
);
}
let img;
if (image) {
img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
<img
ref={this.image}
style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }}
src={image}
onClick={this.onImageClick}
alt=""
/>
</div>;
img = (
<div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
<img
ref={this.image}
style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }}
src={image}
onClick={this.onImageClick}
alt=""
/>
</div>
);
}
// The description includes &-encoded HTML entities, we decode those as React treats the thing as an
@ -127,30 +131,36 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
const description = decode(p["og:description"] || "");
const title = p["og:title"]?.trim() ?? "";
const anchor = <a href={this.props.link} target="_blank" rel="noreferrer noopener">{ title }</a>;
const anchor = (
<a href={this.props.link} target="_blank" rel="noreferrer noopener">
{title}
</a>
);
const needsTooltip = PlatformPeg.get()?.needsUrlTooltips() && this.props.link !== title;
return (
<div className="mx_LinkPreviewWidget">
<div className="mx_LinkPreviewWidget_wrapImageCaption">
{ img }
{img}
<div className="mx_LinkPreviewWidget_caption">
<div className="mx_LinkPreviewWidget_title">
{ needsTooltip ? <LinkWithTooltip
tooltip={new URL(this.props.link, window.location.href).toString()}
>
{ anchor }
</LinkWithTooltip> : anchor }
{ p["og:site_name"] && <span className="mx_LinkPreviewWidget_siteName">
{ (" - " + p["og:site_name"]) }
</span> }
{needsTooltip ? (
<LinkWithTooltip tooltip={new URL(this.props.link, window.location.href).toString()}>
{anchor}
</LinkWithTooltip>
) : (
anchor
)}
{p["og:site_name"] && (
<span className="mx_LinkPreviewWidget_siteName">{" - " + p["og:site_name"]}</span>
)}
</div>
<div className="mx_LinkPreviewWidget_description" ref={this.description}>
{ description }
{description}
</div>
</div>
</div>
{ this.props.children }
{this.props.children}
</div>
);
}

View file

@ -40,21 +40,23 @@ export const LiveContentSummary: FC<Props> = ({ type, text, active, participantC
<span className="mx_LiveContentSummary">
<span
className={classNames("mx_LiveContentSummary_text", {
"mx_LiveContentSummary_text_video": type === LiveContentType.Video,
"mx_LiveContentSummary_text_active": active,
mx_LiveContentSummary_text_video: type === LiveContentType.Video,
mx_LiveContentSummary_text_active: active,
})}
>
{ text }
{text}
</span>
{ participantCount > 0 && <>
{ " • " }
<span
className="mx_LiveContentSummary_participants"
aria-label={_t("%(count)s participants", { count: participantCount })}
>
{ participantCount }
</span>
</> }
{participantCount > 0 && (
<>
{" • "}
<span
className="mx_LiveContentSummary_participants"
aria-label={_t("%(count)s participants", { count: participantCount })}
>
{participantCount}
</span>
</>
)}
</span>
);
@ -62,10 +64,11 @@ interface LiveContentSummaryWithCallProps {
call: Call;
}
export const LiveContentSummaryWithCall: FC<LiveContentSummaryWithCallProps> = ({ call }) =>
export const LiveContentSummaryWithCall: FC<LiveContentSummaryWithCallProps> = ({ call }) => (
<LiveContentSummary
type={LiveContentType.Video}
text={_t("Video")}
active={false}
participantCount={useParticipantCount(call)}
/>;
/>
);

View file

@ -17,35 +17,35 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { Room, RoomEvent } from 'matrix-js-sdk/src/models/room';
import { RoomMember, RoomMemberEvent } from 'matrix-js-sdk/src/models/room-member';
import { RoomState, RoomStateEvent } from 'matrix-js-sdk/src/models/room-state';
import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
import { RoomState, RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { User, UserEvent } from "matrix-js-sdk/src/models/user";
import { throttle } from 'lodash';
import { throttle } from "lodash";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { ClientEvent } from "matrix-js-sdk/src/client";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import { isValid3pidInvite } from "../../../RoomInvite";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import BaseCard from "../right_panel/BaseCard";
import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName";
import TruncatedList from '../elements/TruncatedList';
import TruncatedList from "../elements/TruncatedList";
import Spinner from "../elements/Spinner";
import SearchBox from "../../structures/SearchBox";
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import EntityTile from "./EntityTile";
import MemberTile from "./MemberTile";
import BaseAvatar from '../avatars/BaseAvatar';
import BaseAvatar from "../avatars/BaseAvatar";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import PosthogTrackers from "../../../PosthogTrackers";
import { SDKContext } from '../../../contexts/SDKContext';
import { SDKContext } from "../../../contexts/SDKContext";
const INITIAL_LOAD_NUM_MEMBERS = 30;
const INITIAL_LOAD_NUM_INVITED = 5;
@ -123,10 +123,7 @@ export default class MemberList extends React.Component<IProps, IState> {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.roomId);
return (
room?.canInvite(cli.getUserId()) ||
(room?.isSpaceRoom() && room.getJoinRule() === JoinRule.Public)
);
return room?.canInvite(cli.getUserId()) || (room?.isSpaceRoom() && room.getJoinRule() === JoinRule.Public);
}
private getMembersState(invitedMembers: Array<RoomMember>, joinedMembers: Array<RoomMember>): IState {
@ -190,9 +187,13 @@ export default class MemberList extends React.Component<IProps, IState> {
if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite });
};
private updateList = throttle(() => {
this.updateListNow(false);
}, 500, { leading: true, trailing: true });
private updateList = throttle(
() => {
this.updateListNow(false);
},
500,
{ leading: true, trailing: true },
);
private async updateListNow(showLoadingSpinner: boolean): Promise<void> {
if (!this.mounted) {
@ -202,7 +203,8 @@ export default class MemberList extends React.Component<IProps, IState> {
this.setState({ loading: true });
}
const { joined, invited } = await this.context.memberListStore.loadMemberList(
this.props.roomId, this.props.searchQuery,
this.props.roomId,
this.props.searchQuery,
);
if (!this.mounted) {
return;
@ -229,7 +231,12 @@ export default class MemberList extends React.Component<IProps, IState> {
<EntityTile
className="mx_EntityTile_ellipsis"
avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg").default} name="..." width={36} height={36} />
<BaseAvatar
url={require("../../../../res/img/ellipsis.svg").default}
name="..."
width={36}
height={36}
/>
}
name={text}
presenceState="online"
@ -289,7 +296,7 @@ export default class MemberList extends React.Component<IProps, IState> {
private onPending3pidInviteClick = (inviteEvent: MatrixEvent): void => {
dis.dispatch({
action: 'view_3pid_invite',
action: "view_3pid_invite",
event: inviteEvent,
});
};
@ -302,7 +309,7 @@ export default class MemberList extends React.Component<IProps, IState> {
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (room) {
return room.currentState.getStateEvents("m.room.third_party_invite").filter(function(e) {
return room.currentState.getStateEvents("m.room.third_party_invite").filter(function (e) {
if (!isValid3pidInvite(e)) return false;
// discard all invites which have a m.room.member event since we've
@ -321,12 +328,14 @@ export default class MemberList extends React.Component<IProps, IState> {
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this.showPresence} />;
} else {
// Is a 3pid invite
return <EntityTile
key={m.getStateKey()}
name={m.getContent().display_name}
suppressOnHover={true}
onClick={() => this.onPending3pidInviteClick(m)}
/>;
return (
<EntityTile
key={m.getStateKey()}
name={m.getContent().display_name}
suppressOnHover={true}
onClick={() => this.onPending3pidInviteClick(m)}
/>
);
}
});
}
@ -352,19 +361,18 @@ export default class MemberList extends React.Component<IProps, IState> {
render() {
if (this.state.loading) {
return <BaseCard
className="mx_MemberList"
onClose={this.props.onClose}
>
<Spinner />
</BaseCard>;
return (
<BaseCard className="mx_MemberList" onClose={this.props.onClose}>
<Spinner />
</BaseCard>
);
}
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.roomId);
let inviteButton;
if (room?.getMyMembership() === 'join' && shouldShowComponent(UIComponent.InviteUsers)) {
if (room?.getMyMembership() === "join" && shouldShowComponent(UIComponent.InviteUsers)) {
let inviteButtonText = _t("Invite to this room");
if (room.isSpaceRoom()) {
inviteButtonText = _t("Invite to this space");
@ -376,7 +384,7 @@ export default class MemberList extends React.Component<IProps, IState> {
onClick={this.onInviteButtonClick}
disabled={!this.state.canInvite}
>
<span>{ inviteButtonText }</span>
<span>{inviteButtonText}</span>
</AccessibleButton>
);
}
@ -384,7 +392,7 @@ export default class MemberList extends React.Component<IProps, IState> {
let invitedHeader;
let invitedSection;
if (this.getChildCountInvited() > 0) {
invitedHeader = <h2>{ _t("Invited") }</h2>;
invitedHeader = <h2>{_t("Invited")}</h2>;
invitedSection = (
<TruncatedList
className="mx_MemberList_section mx_MemberList_invited"
@ -399,7 +407,7 @@ export default class MemberList extends React.Component<IProps, IState> {
const footer = (
<SearchBox
className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
placeholder={_t('Filter room members')}
placeholder={_t("Filter room members")}
onSearch={this.onSearchQueryChanged}
initialValue={this.props.searchQuery}
/>
@ -407,45 +415,52 @@ export default class MemberList extends React.Component<IProps, IState> {
let scopeHeader;
if (room?.isSpaceRoom()) {
scopeHeader = <div className="mx_RightPanel_scopeHeader">
<RoomAvatar room={room} height={32} width={32} />
<RoomName room={room} />
</div>;
scopeHeader = (
<div className="mx_RightPanel_scopeHeader">
<RoomAvatar room={room} height={32} width={32} />
<RoomName room={room} />
</div>
);
}
return <BaseCard
className="mx_MemberList"
header={<React.Fragment>
{ scopeHeader }
{ inviteButton }
</React.Fragment>}
footer={footer}
onClose={this.props.onClose}
>
<div className="mx_MemberList_wrapper">
<TruncatedList
className="mx_MemberList_section mx_MemberList_joined"
truncateAt={this.state.truncateAtJoined}
createOverflowElement={this.createOverflowTileJoined}
getChildren={this.getChildrenJoined}
getChildCount={this.getChildCountJoined} />
{ invitedHeader }
{ invitedSection }
</div>
</BaseCard>;
return (
<BaseCard
className="mx_MemberList"
header={
<React.Fragment>
{scopeHeader}
{inviteButton}
</React.Fragment>
}
footer={footer}
onClose={this.props.onClose}
>
<div className="mx_MemberList_wrapper">
<TruncatedList
className="mx_MemberList_section mx_MemberList_joined"
truncateAt={this.state.truncateAtJoined}
createOverflowElement={this.createOverflowTileJoined}
getChildren={this.getChildrenJoined}
getChildCount={this.getChildCountJoined}
/>
{invitedHeader}
{invitedSection}
</div>
</BaseCard>
);
}
private onInviteButtonClick = (ev: ButtonEvent): void => {
PosthogTrackers.trackInteraction("WebRightPanelMemberListInviteButton", ev);
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({ action: 'require_registration' });
dis.dispatch({ action: "require_registration" });
return;
}
// open the room inviter
dis.dispatch({
action: 'view_invite',
action: "view_invite",
roomId: this.props.roomId,
});
};

View file

@ -15,23 +15,23 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { UserTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
import dis from "../../../dispatcher/dispatcher";
import { _t } from '../../../languageHandler';
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { Action } from "../../../dispatcher/actions";
import EntityTile, { PowerStatus } from "./EntityTile";
import MemberAvatar from "./../avatars/MemberAvatar";
import DisambiguatedProfile from "../messages/DisambiguatedProfile";
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
interface IProps {
member: RoomMember;
@ -127,7 +127,7 @@ export default class MemberTile extends React.Component<IProps, IState> {
}
const devices = cli.getStoredDevicesForUser(userId);
const anyDeviceUnverified = devices.some(device => {
const anyDeviceUnverified = devices.some((device) => {
const { deviceId } = device;
// For your own devices, we use the stricter check of cross-signing
// verification to encourage everyone to trust their own devices via
@ -152,14 +152,11 @@ export default class MemberTile extends React.Component<IProps, IState> {
if (
nextProps.member.user &&
(this.userLastModifiedTime === undefined ||
this.userLastModifiedTime < nextProps.member.user.getLastModifiedTime())
this.userLastModifiedTime < nextProps.member.user.getLastModifiedTime())
) {
return true;
}
if (
nextState.isRoomEncrypted !== this.state.isRoomEncrypted ||
nextState.e2eStatus !== this.state.e2eStatus
) {
if (nextState.isRoomEncrypted !== this.state.isRoomEncrypted || nextState.e2eStatus !== this.state.e2eStatus) {
return true;
}
return false;
@ -179,9 +176,9 @@ export default class MemberTile extends React.Component<IProps, IState> {
private getPowerLabel(): string {
return _t("%(userName)s (power %(powerLevelNumber)s)", {
userName: UserIdentifierCustomisations.getDisplayUserIdentifier(
this.props.member.userId, { roomId: this.props.member.roomId },
),
userName: UserIdentifierCustomisations.getDisplayUserIdentifier(this.props.member.userId, {
roomId: this.props.member.roomId,
}),
powerLevelNumber: this.props.member.powerLevel,
}).trim();
}
@ -191,9 +188,7 @@ export default class MemberTile extends React.Component<IProps, IState> {
const name = this.getDisplayName();
const presenceState = member.user ? member.user.presence : null;
const av = (
<MemberAvatar member={member} width={36} height={36} aria-hidden="true" />
);
const av = <MemberAvatar member={member} width={36} height={36} aria-hidden="true" />;
if (member.user) {
this.userLastModifiedTime = member.user.getLastModifiedTime();
@ -221,12 +216,7 @@ export default class MemberTile extends React.Component<IProps, IState> {
e2eStatus = this.state.e2eStatus;
}
const nameJSX = (
<DisambiguatedProfile
member={member}
fallbackName={name || ""}
/>
);
const nameJSX = <DisambiguatedProfile member={member} fallbackName={name || ""} />;
return (
<EntityTile

View file

@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef, ReactNode } from 'react';
import classNames from 'classnames';
import React, { createRef, ReactNode } from "react";
import classNames from "classnames";
import { IEventRelation, 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 { EventType } from 'matrix-js-sdk/src/@types/event';
import { EventType } from "matrix-js-sdk/src/@types/event";
import { Optional } from "matrix-events-sdk";
import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import { ActionPayload } from "../../../dispatcher/payloads";
import Stickerpicker from './Stickerpicker';
import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import E2EIcon from './E2EIcon';
import Stickerpicker from "./Stickerpicker";
import { makeRoomPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import E2EIcon from "./E2EIcon";
import SettingsStore from "../../../settings/SettingsStore";
import { aboveLeftOf } from "../../structures/ContextMenu";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
@ -40,26 +40,26 @@ import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
import { RecordingState } from "../../../audio/VoiceRecording";
import Tooltip, { Alignment } from "../elements/Tooltip";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import { E2EStatus } from '../../../utils/ShieldUtils';
import { E2EStatus } from "../../../utils/ShieldUtils";
import SendMessageComposer, { SendMessageComposer as SendMessageComposerClass } from "./SendMessageComposer";
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { Action } from "../../../dispatcher/actions";
import EditorModel from "../../../editor/model";
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
import RoomContext from '../../../contexts/RoomContext';
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
import RoomContext from "../../../contexts/RoomContext";
import { SettingUpdatedPayload } from "../../../dispatcher/payloads/SettingUpdatedPayload";
import MessageComposerButtons from './MessageComposerButtons';
import { ButtonEvent } from '../elements/AccessibleButton';
import MessageComposerButtons from "./MessageComposerButtons";
import { ButtonEvent } from "../elements/AccessibleButton";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
import { Features } from '../../../settings/Settings';
import { VoiceMessageRecording } from '../../../audio/VoiceMessageRecording';
import { VoiceBroadcastRecordingsStore } from '../../../voice-broadcast';
import { SendWysiwygComposer, sendMessage } from './wysiwyg_composer/';
import { MatrixClientProps, withMatrixClientHOC } from '../../../contexts/MatrixClientContext';
import { htmlToPlainText } from '../../../utils/room/htmlToPlaintext';
import { setUpVoiceBroadcastPreRecording } from '../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording';
import { SdkContextClass } from '../../../contexts/SDKContext';
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
import { Features } from "../../../settings/Settings";
import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording";
import { VoiceBroadcastRecordingsStore } from "../../../voice-broadcast";
import { SendWysiwygComposer, sendMessage } from "./wysiwyg_composer/";
import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext";
import { htmlToPlainText } from "../../../utils/room/htmlToPlaintext";
import { setUpVoiceBroadcastPreRecording } from "../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording";
import { SdkContextClass } from "../../../contexts/SDKContext";
let instanceCount = 0;
@ -73,7 +73,7 @@ function SendButton(props: ISendButtonProps) {
<AccessibleTooltipButton
className="mx_MessageComposer_sendMessage"
onClick={props.onClick}
title={props.title ?? _t('Send message')}
title={props.title ?? _t("Send message")}
data-testid="sendmessagebtn"
/>
);
@ -129,7 +129,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
this.state = {
isComposerEmpty: true,
composerContent: '',
composerContent: "",
haveRecording: false,
recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast
isMenuOpen: false,
@ -139,7 +139,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast),
isWysiwygLabEnabled: SettingsStore.getValue<boolean>("feature_wysiwyg_composer"),
isRichTextEnabled: true,
initialComposerContent: '',
initialComposerContent: "",
};
this.instanceId = instanceCount++;
@ -268,15 +268,15 @@ export class MessageComposer extends React.Component<IProps, IState> {
private onTombstoneClick = (ev) => {
ev.preventDefault();
const replacementRoomId = this.context.tombstone.getContent()['replacement_room'];
const replacementRoomId = this.context.tombstone.getContent()["replacement_room"];
const replacementRoom = MatrixClientPeg.get().getRoom(replacementRoomId);
let createEventId = null;
if (replacementRoom) {
const createEvent = replacementRoom.currentState.getStateEvents(EventType.RoomCreate, '');
const createEvent = replacementRoom.currentState.getStateEvents(EventType.RoomCreate, "");
if (createEvent && createEvent.getId()) createEventId = createEvent.getId();
}
const viaServers = [this.context.tombstone.getSender().split(':').slice(1).join(':')];
const viaServers = [this.context.tombstone.getSender().split(":").slice(1).join(":")];
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
@ -296,19 +296,19 @@ export class MessageComposer extends React.Component<IProps, IState> {
if (this.props.replyToEvent) {
const replyingToThread = this.props.relation?.rel_type === THREAD_RELATION_TYPE.name;
if (replyingToThread && this.props.e2eStatus) {
return _t('Reply to encrypted thread…');
return _t("Reply to encrypted thread…");
} else if (replyingToThread) {
return _t('Reply to thread…');
return _t("Reply to thread…");
} else if (this.props.e2eStatus) {
return _t('Send an encrypted reply…');
return _t("Send an encrypted reply…");
} else {
return _t('Send a reply…');
return _t("Send a reply…");
}
} else {
if (this.props.e2eStatus) {
return _t('Send an encrypted message…');
return _t("Send an encrypted message…");
} else {
return _t('Send a message…');
return _t("Send a message…");
}
}
};
@ -334,11 +334,15 @@ export class MessageComposer extends React.Component<IProps, IState> {
if (this.state.isWysiwygLabEnabled) {
const { permalinkCreator, relation, replyToEvent } = this.props;
sendMessage(this.state.composerContent,
this.state.isRichTextEnabled,
{ mxClient: this.props.mxClient, roomContext: this.context, permalinkCreator, relation, replyToEvent });
sendMessage(this.state.composerContent, this.state.isRichTextEnabled, {
mxClient: this.props.mxClient,
roomContext: this.context,
permalinkCreator,
relation,
replyToEvent,
});
dis.dispatch({ action: Action.ClearAndFocusSendMessageComposer });
this.setState({ composerContent: '', initialComposerContent: '' });
this.setState({ composerContent: "", initialComposerContent: "" });
}
};
@ -356,12 +360,12 @@ export class MessageComposer extends React.Component<IProps, IState> {
};
private onRichTextToggle = () => {
this.setState(state => ({
this.setState((state) => ({
isRichTextEnabled: !state.isRichTextEnabled,
initialComposerContent: !state.isRichTextEnabled ?
state.composerContent :
// TODO when available use rust model plain text
htmlToPlainText(state.composerContent),
initialComposerContent: !state.isRichTextEnabled
? state.composerContent
: // TODO when available use rust model plain text
htmlToPlainText(state.composerContent),
}));
};
@ -432,15 +436,17 @@ export class MessageComposer extends React.Component<IProps, IState> {
contentRect.x,
contentRect.y + heightToRemove,
contentRect.width,
contentRect.height - heightToRemove);
contentRect.height - heightToRemove,
);
return aboveLeftOf(fixedRect);
}
}
public render() {
const hasE2EIcon = Boolean(!this.state.isWysiwygLabEnabled && this.props.e2eStatus);
const e2eIcon = hasE2EIcon &&
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" />;
const e2eIcon = hasE2EIcon && (
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" />
);
const controls: ReactNode[] = [];
const menuPosition = this.getMenuPosition();
@ -449,8 +455,9 @@ export class MessageComposer extends React.Component<IProps, IState> {
let composer: ReactNode;
if (canSendMessages) {
if (this.state.isWysiwygLabEnabled && menuPosition) {
composer =
<SendWysiwygComposer key="controls_input"
composer = (
<SendWysiwygComposer
key="controls_input"
disabled={this.state.haveRecording}
onChange={this.onWysiwygChange}
onSend={this.sendMessage}
@ -459,9 +466,10 @@ export class MessageComposer extends React.Component<IProps, IState> {
e2eStatus={this.props.e2eStatus}
menuPosition={menuPosition}
placeholder={this.renderPlaceholderText()}
/>;
/>
);
} else {
composer =
composer = (
<SendMessageComposer
ref={this.messageComposerInput}
key="controls_input"
@ -473,43 +481,54 @@ export class MessageComposer extends React.Component<IProps, IState> {
onChange={this.onChange}
disabled={this.state.haveRecording}
toggleStickerPickerOpen={this.toggleStickerPickerOpen}
/>;
/>
);
}
controls.push(<VoiceRecordComposerTile
key="controls_voice_record"
ref={this.voiceRecordingButton}
room={this.props.room}
permalinkCreator={this.props.permalinkCreator}
relation={this.props.relation}
replyToEvent={this.props.replyToEvent} />);
controls.push(
<VoiceRecordComposerTile
key="controls_voice_record"
ref={this.voiceRecordingButton}
room={this.props.room}
permalinkCreator={this.props.permalinkCreator}
relation={this.props.relation}
replyToEvent={this.props.replyToEvent}
/>,
);
} else if (this.context.tombstone) {
const replacementRoomId = this.context.tombstone.getContent()['replacement_room'];
const replacementRoomId = this.context.tombstone.getContent()["replacement_room"];
const continuesLink = replacementRoomId ? (
<a href={makeRoomPermalink(replacementRoomId)}
<a
href={makeRoomPermalink(replacementRoomId)}
className="mx_MessageComposer_roomReplaced_link"
onClick={this.onTombstoneClick}
>
{ _t("The conversation continues here.") }
{_t("The conversation continues here.")}
</a>
) : '';
) : (
""
);
controls.push(<div className="mx_MessageComposer_replaced_wrapper" key="room_replaced">
<div className="mx_MessageComposer_replaced_valign">
<img className="mx_MessageComposer_roomReplaced_icon"
src={require("../../../../res/img/room_replaced.svg").default}
/>
<span className="mx_MessageComposer_roomReplaced_header">
{ _t("This room has been replaced and is no longer active.") }
</span><br />
{ continuesLink }
</div>
</div>);
controls.push(
<div className="mx_MessageComposer_replaced_wrapper" key="room_replaced">
<div className="mx_MessageComposer_replaced_valign">
<img
className="mx_MessageComposer_roomReplaced_icon"
src={require("../../../../res/img/room_replaced.svg").default}
/>
<span className="mx_MessageComposer_roomReplaced_header">
{_t("This room has been replaced and is no longer active.")}
</span>
<br />
{continuesLink}
</div>
</div>,
);
} else {
controls.push(
<div key="controls_error" className="mx_MessageComposer_noperm_error">
{ _t('You do not have permission to post to this room') }
{_t("You do not have permission to post to this room")}
</div>,
);
}
@ -517,15 +536,13 @@ export class MessageComposer extends React.Component<IProps, IState> {
let recordingTooltip;
if (this.state.recordingTimeLeftSeconds) {
const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds);
recordingTooltip = <Tooltip
label={_t("%(seconds)ss left", { seconds: secondsLeft })}
alignment={Alignment.Top}
/>;
recordingTooltip = (
<Tooltip label={_t("%(seconds)ss left", { seconds: secondsLeft })} alignment={Alignment.Top} />
);
}
const threadId = this.props.relation?.rel_type === THREAD_RELATION_TYPE.name
? this.props.relation.event_id
: null;
const threadId =
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : null;
controls.push(
<Stickerpicker
@ -549,55 +566,58 @@ export class MessageComposer extends React.Component<IProps, IState> {
return (
<div className={classes} ref={this.ref}>
{ recordingTooltip }
{recordingTooltip}
<div className="mx_MessageComposer_wrapper">
<ReplyPreview
replyToEvent={this.props.replyToEvent}
permalinkCreator={this.props.permalinkCreator} />
permalinkCreator={this.props.permalinkCreator}
/>
<div className="mx_MessageComposer_row">
{ e2eIcon }
{ composer }
{e2eIcon}
{composer}
<div className="mx_MessageComposer_actions">
{ controls }
{ canSendMessages && <MessageComposerButtons
addEmoji={this.addEmoji}
haveRecording={this.state.haveRecording}
isMenuOpen={this.state.isMenuOpen}
isStickerPickerOpen={this.state.isStickerPickerOpen}
menuPosition={menuPosition}
relation={this.props.relation}
onRecordStartEndClick={() => {
this.voiceRecordingButton.current?.onRecordStartEndClick();
if (this.context.narrow) {
{controls}
{canSendMessages && (
<MessageComposerButtons
addEmoji={this.addEmoji}
haveRecording={this.state.haveRecording}
isMenuOpen={this.state.isMenuOpen}
isStickerPickerOpen={this.state.isStickerPickerOpen}
menuPosition={menuPosition}
relation={this.props.relation}
onRecordStartEndClick={() => {
this.voiceRecordingButton.current?.onRecordStartEndClick();
if (this.context.narrow) {
this.toggleButtonMenu();
}
}}
setStickerPickerOpen={this.setStickerPickerOpen}
showLocationButton={!window.electron}
showPollsButton={this.state.showPollsButton}
showStickersButton={this.showStickersButton}
isRichTextEnabled={this.state.isRichTextEnabled}
onComposerModeClick={this.onRichTextToggle}
toggleButtonMenu={this.toggleButtonMenu}
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
onStartVoiceBroadcastClick={() => {
setUpVoiceBroadcastPreRecording(
this.props.room,
MatrixClientPeg.get(),
SdkContextClass.instance.voiceBroadcastPlaybacksStore,
VoiceBroadcastRecordingsStore.instance(),
SdkContextClass.instance.voiceBroadcastPreRecordingStore,
);
this.toggleButtonMenu();
}
}}
setStickerPickerOpen={this.setStickerPickerOpen}
showLocationButton={!window.electron}
showPollsButton={this.state.showPollsButton}
showStickersButton={this.showStickersButton}
isRichTextEnabled={this.state.isRichTextEnabled}
onComposerModeClick={this.onRichTextToggle}
toggleButtonMenu={this.toggleButtonMenu}
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
onStartVoiceBroadcastClick={() => {
setUpVoiceBroadcastPreRecording(
this.props.room,
MatrixClientPeg.get(),
SdkContextClass.instance.voiceBroadcastPlaybacksStore,
VoiceBroadcastRecordingsStore.instance(),
SdkContextClass.instance.voiceBroadcastPreRecordingStore,
);
this.toggleButtonMenu();
}}
/> }
{ showSendButton && (
}}
/>
)}
{showSendButton && (
<SendButton
key="controls_send"
onClick={this.sendMessage}
title={this.state.haveRecording ? _t("Send voice message") : undefined}
/>
) }
)}
</div>
</div>
</div>

View file

@ -14,32 +14,32 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import classNames from 'classnames';
import classNames from "classnames";
import { IEventRelation } from "matrix-js-sdk/src/models/event";
import { M_POLL_START } from "matrix-events-sdk";
import React, { createContext, MouseEventHandler, ReactElement, useContext, useRef } from 'react';
import { Room } from 'matrix-js-sdk/src/models/room';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
import React, { createContext, MouseEventHandler, ReactElement, useContext, useRef } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
import { _t } from '../../../languageHandler';
import { _t } from "../../../languageHandler";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { CollapsibleButton } from './CollapsibleButton';
import { AboveLeftOf } from '../../structures/ContextMenu';
import dis from '../../../dispatcher/dispatcher';
import { CollapsibleButton } from "./CollapsibleButton";
import { AboveLeftOf } from "../../structures/ContextMenu";
import dis from "../../../dispatcher/dispatcher";
import ErrorDialog from "../dialogs/ErrorDialog";
import LocationButton from '../location/LocationButton';
import LocationButton from "../location/LocationButton";
import Modal from "../../../Modal";
import PollCreateDialog from "../elements/PollCreateDialog";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import ContentMessages from '../../../ContentMessages';
import MatrixClientContext from '../../../contexts/MatrixClientContext';
import RoomContext from '../../../contexts/RoomContext';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import ContentMessages from "../../../ContentMessages";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import RoomContext from "../../../contexts/RoomContext";
import { useDispatcher } from "../../../hooks/useDispatcher";
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
import IconizedContextMenu, { IconizedContextMenuOptionList } from '../context_menus/IconizedContextMenu';
import { EmojiButton } from './EmojiButton';
import { useSettingValue } from '../../../hooks/useSettings';
import IconizedContextMenu, { IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
import { EmojiButton } from "./EmojiButton";
import { useSettingValue } from "../../../hooks/useSettings";
interface IProps {
addEmoji: (emoji: string) => boolean;
@ -67,7 +67,7 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
const matrixClient: MatrixClient = useContext(MatrixClientContext);
const { room, roomId, narrow } = useContext(RoomContext);
const isWysiwygLabEnabled = useSettingValue<boolean>('feature_wysiwyg_composer');
const isWysiwygLabEnabled = useSettingValue<boolean>("feature_wysiwyg_composer");
if (props.haveRecording) {
return null;
@ -77,9 +77,15 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
let moreButtons: ReactElement[];
if (narrow) {
mainButtons = [
isWysiwygLabEnabled ?
<ComposerModeButton key="composerModeButton" isRichTextEnabled={props.isRichTextEnabled} onClick={props.onComposerModeClick} /> :
emojiButton(props),
isWysiwygLabEnabled ? (
<ComposerModeButton
key="composerModeButton"
isRichTextEnabled={props.isRichTextEnabled}
onClick={props.onComposerModeClick}
/>
) : (
emojiButton(props)
),
];
moreButtons = [
uploadButton(), // props passed via UploadButtonContext
@ -91,9 +97,15 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
];
} else {
mainButtons = [
isWysiwygLabEnabled ?
<ComposerModeButton key="composerModeButton" isRichTextEnabled={props.isRichTextEnabled} onClick={props.onComposerModeClick} /> :
emojiButton(props),
isWysiwygLabEnabled ? (
<ComposerModeButton
key="composerModeButton"
isRichTextEnabled={props.isRichTextEnabled}
onClick={props.onComposerModeClick}
/>
) : (
emojiButton(props)
),
uploadButton(), // props passed via UploadButtonContext
];
moreButtons = [
@ -114,37 +126,41 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
mx_MessageComposer_closeButtonMenu: props.isMenuOpen,
});
return <UploadButtonContextProvider roomId={roomId} relation={props.relation}>
{ mainButtons }
{ moreButtons.length > 0 && <AccessibleTooltipButton
className={moreOptionsClasses}
onClick={props.toggleButtonMenu}
title={_t("More options")}
/> }
{ props.isMenuOpen && (
<IconizedContextMenu
onFinished={props.toggleButtonMenu}
{...props.menuPosition}
wrapperClassName="mx_MessageComposer_Menu"
compact={true}
>
<OverflowMenuContext.Provider value={props.toggleButtonMenu}>
<IconizedContextMenuOptionList>
{ moreButtons }
</IconizedContextMenuOptionList>
</OverflowMenuContext.Provider>
</IconizedContextMenu>
) }
</UploadButtonContextProvider>;
return (
<UploadButtonContextProvider roomId={roomId} relation={props.relation}>
{mainButtons}
{moreButtons.length > 0 && (
<AccessibleTooltipButton
className={moreOptionsClasses}
onClick={props.toggleButtonMenu}
title={_t("More options")}
/>
)}
{props.isMenuOpen && (
<IconizedContextMenu
onFinished={props.toggleButtonMenu}
{...props.menuPosition}
wrapperClassName="mx_MessageComposer_Menu"
compact={true}
>
<OverflowMenuContext.Provider value={props.toggleButtonMenu}>
<IconizedContextMenuOptionList>{moreButtons}</IconizedContextMenuOptionList>
</OverflowMenuContext.Provider>
</IconizedContextMenu>
)}
</UploadButtonContextProvider>
);
};
function emojiButton(props: IProps): ReactElement {
return <EmojiButton
key="emoji_button"
addEmoji={props.addEmoji}
menuPosition={props.menuPosition}
className="mx_MessageComposer_button"
/>;
return (
<EmojiButton
key="emoji_button"
addEmoji={props.addEmoji}
menuPosition={props.menuPosition}
className="mx_MessageComposer_button"
/>
);
}
function uploadButton(): ReactElement {
@ -167,13 +183,13 @@ const UploadButtonContextProvider: React.FC<IUploadButtonProps> = ({ roomId, rel
const onUploadClick = () => {
if (cli.isGuest()) {
dis.dispatch({ action: 'require_registration' });
dis.dispatch({ action: "require_registration" });
return;
}
uploadInput.current?.click();
};
useDispatcher(dis, payload => {
useDispatcher(dis, (payload) => {
if (roomContext.timelineRenderingType === payload.context && payload.action === "upload_file") {
onUploadClick();
}
@ -195,22 +211,24 @@ const UploadButtonContextProvider: React.FC<IUploadButtonProps> = ({ roomId, rel
// not keeping any state, so reset the value of the form control
// to empty.
// NB. we need to set 'value': the 'files' property is immutable.
ev.target.value = '';
ev.target.value = "";
};
const uploadInputStyle = { display: 'none' };
return <UploadButtonContext.Provider value={onUploadClick}>
{ children }
const uploadInputStyle = { display: "none" };
return (
<UploadButtonContext.Provider value={onUploadClick}>
{children}
<input
ref={uploadInput}
type="file"
style={uploadInputStyle}
multiple
onClick={chromeFileInputFix}
onChange={onUploadFileInputChange}
/>
</UploadButtonContext.Provider>;
<input
ref={uploadInput}
type="file"
style={uploadInputStyle}
multiple
onClick={chromeFileInputFix}
onChange={onUploadFileInputChange}
/>
</UploadButtonContext.Provider>
);
};
// Must be rendered within an UploadButtonContextProvider
@ -223,55 +241,51 @@ const UploadButton = () => {
overflowMenuCloser?.(); // close overflow menu
};
return <CollapsibleButton
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_upload"
onClick={onClick}
title={_t('Attachment')}
/>;
return (
<CollapsibleButton
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_upload"
onClick={onClick}
title={_t("Attachment")}
/>
);
};
function showStickersButton(props: IProps): ReactElement | null {
return (
props.showStickersButton
? <CollapsibleButton
id='stickersButton'
key="controls_stickers"
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_stickers"
onClick={() => props.setStickerPickerOpen(!props.isStickerPickerOpen)}
title={props.isStickerPickerOpen ? _t("Hide stickers") : _t("Sticker")}
/>
: null
);
return props.showStickersButton ? (
<CollapsibleButton
id="stickersButton"
key="controls_stickers"
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_stickers"
onClick={() => props.setStickerPickerOpen(!props.isStickerPickerOpen)}
title={props.isStickerPickerOpen ? _t("Hide stickers") : _t("Sticker")}
/>
) : null;
}
const startVoiceBroadcastButton: React.FC<IProps> = (props: IProps): ReactElement | null => {
return (
props.showVoiceBroadcastButton
? <CollapsibleButton
key="start_voice_broadcast"
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_voiceBroadcast"
onClick={props.onStartVoiceBroadcastClick}
title={_t("Voice broadcast")}
/>
: null
);
return props.showVoiceBroadcastButton ? (
<CollapsibleButton
key="start_voice_broadcast"
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_voiceBroadcast"
onClick={props.onStartVoiceBroadcastClick}
title={_t("Voice broadcast")}
/>
) : null;
};
function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement | null {
// XXX: recording UI does not work well in narrow mode, so hide for now
return (
narrow
? null
: <CollapsibleButton
key="voice_message_send"
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_voiceMessage"
onClick={props.onRecordStartEndClick}
title={_t("Voice Message")}
/>
return narrow ? null : (
<CollapsibleButton
key="voice_message_send"
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_voiceMessage"
onClick={props.onRecordStartEndClick}
title={_t("Voice Message")}
/>
);
}
@ -295,19 +309,13 @@ class PollButton extends React.PureComponent<IPollButtonProps> {
MatrixClientPeg.get().getUserId()!,
);
if (!canSend) {
Modal.createDialog(
ErrorDialog,
{
title: _t("Permission Required"),
description: _t(
"You do not have permission to start polls in this room.",
),
},
);
Modal.createDialog(ErrorDialog, {
title: _t("Permission Required"),
description: _t("You do not have permission to start polls in this room."),
});
} else {
const threadId = this.props.relation?.rel_type === THREAD_RELATION_TYPE.name
? this.props.relation.event_id
: null;
const threadId =
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : null;
Modal.createDialog(
PollCreateDialog,
@ -315,9 +323,9 @@ class PollButton extends React.PureComponent<IPollButtonProps> {
room: this.props.room,
threadId,
},
'mx_CompoundDialog',
"mx_CompoundDialog",
false, // isPriorityModal
true, // isStaticModal
true, // isStaticModal
);
}
};
@ -345,17 +353,15 @@ function showLocationButton(
): ReactElement | null {
const sender = room.getMember(matrixClient.getUserId()!);
return (
props.showLocationButton && sender
? <LocationButton
key="location"
roomId={roomId}
relation={props.relation}
sender={sender}
menuPosition={props.menuPosition}
/>
: null
);
return props.showLocationButton && sender ? (
<LocationButton
key="location"
roomId={roomId}
relation={props.relation}
sender={sender}
menuPosition={props.menuPosition}
/>
) : null;
}
interface WysiwygToggleButtonProps {
@ -366,15 +372,17 @@ interface WysiwygToggleButtonProps {
function ComposerModeButton({ isRichTextEnabled, onClick }: WysiwygToggleButtonProps) {
const title = isRichTextEnabled ? _t("Hide formatting") : _t("Show formatting");
return <CollapsibleButton
className="mx_MessageComposer_button"
iconClassName={classNames({
"mx_MessageComposer_plain_text": isRichTextEnabled,
"mx_MessageComposer_rich_text": !isRichTextEnabled,
})}
onClick={onClick}
title={title}
/>;
return (
<CollapsibleButton
className="mx_MessageComposer_button"
iconClassName={classNames({
mx_MessageComposer_plain_text: isRichTextEnabled,
mx_MessageComposer_rich_text: !isRichTextEnabled,
})}
onClick={onClick}
title={title}
/>
);
}
export default MessageComposerButtons;

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import classNames from 'classnames';
import React, { createRef } from "react";
import classNames from "classnames";
import { _t } from '../../../languageHandler';
import { _t } from "../../../languageHandler";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
export enum Formatting {
@ -48,16 +48,53 @@ export default class MessageComposerFormatBar extends React.PureComponent<IProps
render() {
const classes = classNames("mx_MessageComposerFormatBar", {
"mx_MessageComposerFormatBar_shown": this.state.visible,
mx_MessageComposerFormatBar_shown: this.state.visible,
});
return (<div className={classes} ref={this.formatBarRef}>
<FormatButton label={_t("Bold")} onClick={() => this.props.onAction(Formatting.Bold)} icon="Bold" shortcut={this.props.shortcuts.bold} visible={this.state.visible} />
<FormatButton label={_t("Italics")} onClick={() => this.props.onAction(Formatting.Italics)} icon="Italic" shortcut={this.props.shortcuts.italics} visible={this.state.visible} />
<FormatButton label={_t("Strikethrough")} onClick={() => this.props.onAction(Formatting.Strikethrough)} icon="Strikethrough" visible={this.state.visible} />
<FormatButton label={_t("Code block")} onClick={() => this.props.onAction(Formatting.Code)} icon="Code" shortcut={this.props.shortcuts.code} visible={this.state.visible} />
<FormatButton label={_t("Quote")} onClick={() => this.props.onAction(Formatting.Quote)} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} />
<FormatButton label={_t("Insert link")} onClick={() => this.props.onAction(Formatting.InsertLink)} icon="InsertLink" shortcut={this.props.shortcuts.insert_link} visible={this.state.visible} />
</div>);
return (
<div className={classes} ref={this.formatBarRef}>
<FormatButton
label={_t("Bold")}
onClick={() => this.props.onAction(Formatting.Bold)}
icon="Bold"
shortcut={this.props.shortcuts.bold}
visible={this.state.visible}
/>
<FormatButton
label={_t("Italics")}
onClick={() => this.props.onAction(Formatting.Italics)}
icon="Italic"
shortcut={this.props.shortcuts.italics}
visible={this.state.visible}
/>
<FormatButton
label={_t("Strikethrough")}
onClick={() => this.props.onAction(Formatting.Strikethrough)}
icon="Strikethrough"
visible={this.state.visible}
/>
<FormatButton
label={_t("Code block")}
onClick={() => this.props.onAction(Formatting.Code)}
icon="Code"
shortcut={this.props.shortcuts.code}
visible={this.state.visible}
/>
<FormatButton
label={_t("Quote")}
onClick={() => this.props.onAction(Formatting.Quote)}
icon="Quote"
shortcut={this.props.shortcuts.quote}
visible={this.state.visible}
/>
<FormatButton
label={_t("Insert link")}
onClick={() => this.props.onAction(Formatting.InsertLink)}
icon="InsertLink"
shortcut={this.props.shortcuts.insert_link}
visible={this.state.visible}
/>
</div>
);
}
public showAt(selectionRect: DOMRect): void {
@ -91,18 +128,14 @@ class FormatButton extends React.PureComponent<IFormatButtonProps> {
const className = `mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIcon${this.props.icon}`;
let shortcut;
if (this.props.shortcut) {
shortcut = <div className="mx_MessageComposerFormatBar_tooltipShortcut">
{ this.props.shortcut }
</div>;
shortcut = <div className="mx_MessageComposerFormatBar_tooltipShortcut">{this.props.shortcut}</div>;
}
const tooltip = <div>
<div className="mx_Tooltip_title">
{ this.props.label }
const tooltip = (
<div>
<div className="mx_Tooltip_title">{this.props.label}</div>
<div className="mx_Tooltip_sub">{shortcut}</div>
</div>
<div className="mx_Tooltip_sub">
{ shortcut }
</div>
</div>;
);
// element="button" and type="button" are necessary for the buttons to work on WebKit,
// otherwise the text is deselected before onClick can ever be called
@ -113,7 +146,8 @@ class FormatButton extends React.PureComponent<IFormatButtonProps> {
onClick={this.props.onClick}
title={this.props.label}
tooltip={tooltip}
className={className} />
className={className}
/>
);
}
}

View file

@ -51,9 +51,7 @@ const NewRoomIntro = () => {
const { room, roomId } = useContext(RoomContext);
const isLocalRoom = room instanceof LocalRoom;
const dmPartner = isLocalRoom
? room.targets[0]?.userId
: DMRoomMap.shared().getUserIdForRoomId(roomId);
const dmPartner = isLocalRoom ? room.targets[0]?.userId : DMRoomMap.shared().getUserIdForRoomId(roomId);
let body: JSX.Element;
if (dmPartner) {
@ -62,43 +60,54 @@ const NewRoomIntro = () => {
if (isLocalRoom) {
introMessage = _t("Send your first message to invite <displayName/> to chat");
} else if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) {
} else if (room.getJoinedMemberCount() + room.getInvitedMemberCount() === 2) {
caption = _t("Only the two of you are in this conversation, unless either of you invites anyone to join.");
}
const member = room?.getMember(dmPartner);
const displayName = room?.name || member?.rawDisplayName || dmPartner;
body = <React.Fragment>
<RoomAvatar
room={room}
width={AVATAR_SIZE}
height={AVATAR_SIZE}
onClick={() => {
defaultDispatcher.dispatch<ViewUserPayload>({
action: Action.ViewUser,
// XXX: We should be using a real member object and not assuming what the receiver wants.
member: member || { userId: dmPartner } as User,
});
}}
/>
body = (
<React.Fragment>
<RoomAvatar
room={room}
width={AVATAR_SIZE}
height={AVATAR_SIZE}
onClick={() => {
defaultDispatcher.dispatch<ViewUserPayload>({
action: Action.ViewUser,
// XXX: We should be using a real member object and not assuming what the receiver wants.
member: member || ({ userId: dmPartner } as User),
});
}}
/>
<h2>{ room.name }</h2>
<h2>{room.name}</h2>
<p>{ _t(introMessage, {}, {
displayName: () => <b>{ displayName }</b>,
}) }</p>
{ caption && <p>{ caption }</p> }
</React.Fragment>;
<p>
{_t(
introMessage,
{},
{
displayName: () => <b>{displayName}</b>,
},
)}
</p>
{caption && <p>{caption}</p>}
</React.Fragment>
);
} else {
const inRoom = room && room.getMyMembership() === "join";
const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
const canAddTopic = inRoom && room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getUserId());
const onTopicClick = () => {
defaultDispatcher.dispatch({
action: "open_room_settings",
room_id: roomId,
}, true);
defaultDispatcher.dispatch(
{
action: "open_room_settings",
room_id: roomId,
},
true,
);
// focus the topic field to help the user find it as it'll gain an outline
setImmediate(() => {
window.document.getElementById("profileTopic").focus();
@ -107,15 +116,31 @@ const NewRoomIntro = () => {
let topicText;
if (canAddTopic && topic) {
topicText = _t("Topic: %(topic)s (<a>edit</a>)", { topic }, {
a: sub => <AccessibleButton kind="link_inline" onClick={onTopicClick}>{ sub }</AccessibleButton>,
});
topicText = _t(
"Topic: %(topic)s (<a>edit</a>)",
{ topic },
{
a: (sub) => (
<AccessibleButton kind="link_inline" onClick={onTopicClick}>
{sub}
</AccessibleButton>
),
},
);
} else if (topic) {
topicText = _t("Topic: %(topic)s ", { topic });
} else if (canAddTopic) {
topicText = _t("<a>Add a topic</a> to help people know what it is about.", {}, {
a: sub => <AccessibleButton kind="link_inline" onClick={onTopicClick}>{ sub }</AccessibleButton>,
});
topicText = _t(
"<a>Add a topic</a> to help people know what it is about.",
{},
{
a: (sub) => (
<AccessibleButton kind="link_inline" onClick={onTopicClick}>
{sub}
</AccessibleButton>
),
},
);
}
const creator = room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
@ -140,38 +165,44 @@ const NewRoomIntro = () => {
let buttons;
if (parentSpace && shouldShowComponent(UIComponent.InviteUsers)) {
buttons = <div className="mx_NewRoomIntro_buttons">
<AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary"
onClick={() => {
showSpaceInvite(parentSpace);
}}
>
{ _t("Invite to %(spaceName)s", { spaceName: parentSpace.name }) }
</AccessibleButton>
{ room.canInvite(cli.getUserId()) && <AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary_outline"
onClick={() => {
defaultDispatcher.dispatch({ action: "view_invite", roomId });
}}
>
{ _t("Invite to just this room") }
</AccessibleButton> }
</div>;
buttons = (
<div className="mx_NewRoomIntro_buttons">
<AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary"
onClick={() => {
showSpaceInvite(parentSpace);
}}
>
{_t("Invite to %(spaceName)s", { spaceName: parentSpace.name })}
</AccessibleButton>
{room.canInvite(cli.getUserId()) && (
<AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary_outline"
onClick={() => {
defaultDispatcher.dispatch({ action: "view_invite", roomId });
}}
>
{_t("Invite to just this room")}
</AccessibleButton>
)}
</div>
);
} else if (room.canInvite(cli.getUserId()) && shouldShowComponent(UIComponent.InviteUsers)) {
buttons = <div className="mx_NewRoomIntro_buttons">
<AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary"
onClick={() => {
defaultDispatcher.dispatch({ action: "view_invite", roomId });
}}
>
{ _t("Invite to this room") }
</AccessibleButton>
</div>;
buttons = (
<div className="mx_NewRoomIntro_buttons">
<AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary"
onClick={() => {
defaultDispatcher.dispatch({ action: "view_invite", roomId });
}}
>
{_t("Invite to this room")}
</AccessibleButton>
</div>
);
}
const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;
@ -180,26 +211,37 @@ const NewRoomIntro = () => {
);
if (!avatarUrl) {
avatar = <MiniAvatarUploader
hasAvatar={false}
noAvatarLabel={_t("Add a photo, so people can easily spot your room.")}
setAvatarUrl={url => cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, '')}
>
{ avatar }
</MiniAvatarUploader>;
avatar = (
<MiniAvatarUploader
hasAvatar={false}
noAvatarLabel={_t("Add a photo, so people can easily spot your room.")}
setAvatarUrl={(url) => cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, "")}
>
{avatar}
</MiniAvatarUploader>
);
}
body = <React.Fragment>
{ avatar }
body = (
<React.Fragment>
{avatar}
<h2>{ room.name }</h2>
<h2>{room.name}</h2>
<p>{ createdText } { _t("This is the start of <roomName/>.", {}, {
roomName: () => <b>{ room.name }</b>,
}) }</p>
<p>{ topicText }</p>
{ buttons }
</React.Fragment>;
<p>
{createdText}{" "}
{_t(
"This is the start of <roomName/>.",
{},
{
roomName: () => <b>{room.name}</b>,
},
)}
</p>
<p>{topicText}</p>
{buttons}
</React.Fragment>
);
}
function openRoomSettings(event) {
@ -211,33 +253,40 @@ const NewRoomIntro = () => {
}
const subText = _t(
"Your private messages are normally encrypted, but this room isn't. "+
"Usually this is due to an unsupported device or method being used, " +
"like email invites.",
"Your private messages are normally encrypted, but this room isn't. " +
"Usually this is due to an unsupported device or method being used, " +
"like email invites.",
);
let subButton;
if (room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, MatrixClientPeg.get()) && !isLocalRoom) {
subButton = (
<AccessibleButton kind='link_inline' onClick={openRoomSettings}>{ _t("Enable encryption in settings.") }</AccessibleButton>
<AccessibleButton kind="link_inline" onClick={openRoomSettings}>
{_t("Enable encryption in settings.")}
</AccessibleButton>
);
}
const subtitle = (
<span> { subText } { subButton } </span>
<span>
{" "}
{subText} {subButton}{" "}
</span>
);
return <li className="mx_NewRoomIntro">
{ !hasExpectedEncryptionSettings(cli, room) && (
<EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon_warning"
title={_t("End-to-end encryption isn't enabled")}
subtitle={subtitle}
/>
) }
return (
<li className="mx_NewRoomIntro">
{!hasExpectedEncryptionSettings(cli, room) && (
<EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon_warning"
title={_t("End-to-end encryption isn't enabled")}
subtitle={subtitle}
/>
)}
{ body }
</li>;
{body}
</li>
);
};
export default NewRoomIntro;

View file

@ -65,7 +65,8 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
};
this.countWatcherRef = SettingsStore.watchSetting(
"Notifications.alwaysShowBadgeCounts", this.roomId,
"Notifications.alwaysShowBadgeCounts",
this.roomId,
this.countPreferenceChanged,
);
}
@ -125,16 +126,18 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
tooltip = <Tooltip className="mx_RoleButton_tooltip" label={label} />;
}
return <StatelessNotificationBadge
label={label}
symbol={notification.symbol}
count={notification.count}
color={notification.color}
onClick={onClick}
onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave}
>
{ tooltip }
</StatelessNotificationBadge>;
return (
<StatelessNotificationBadge
label={label}
symbol={notification.symbol}
count={notification.count}
color={notification.color}
onClick={onClick}
onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave}
>
{tooltip}
</StatelessNotificationBadge>
);
}
}

View file

@ -33,11 +33,7 @@ interface Props {
label?: string;
}
export function StatelessNotificationBadge({
symbol,
count,
color,
...props }: Props) {
export function StatelessNotificationBadge({ symbol, count, color, ...props }: Props) {
const hideBold = useSettingValue("feature_hidebold");
// Don't show a badge if we don't need to
@ -54,12 +50,12 @@ export function StatelessNotificationBadge({
}
const classes = classNames({
'mx_NotificationBadge': true,
'mx_NotificationBadge_visible': isEmptyBadge ? true : hasUnreadCount,
'mx_NotificationBadge_highlighted': color >= NotificationColor.Red,
'mx_NotificationBadge_dot': isEmptyBadge,
'mx_NotificationBadge_2char': symbol?.length > 0 && symbol?.length < 3,
'mx_NotificationBadge_3char': symbol?.length > 2,
mx_NotificationBadge: true,
mx_NotificationBadge_visible: isEmptyBadge ? true : hasUnreadCount,
mx_NotificationBadge_highlighted: color >= NotificationColor.Red,
mx_NotificationBadge_dot: isEmptyBadge,
mx_NotificationBadge_2char: symbol?.length > 0 && symbol?.length < 3,
mx_NotificationBadge_3char: symbol?.length > 2,
});
if (props.onClick) {
@ -72,15 +68,15 @@ export function StatelessNotificationBadge({
onMouseOver={props.onMouseOver}
onMouseLeave={props.onMouseLeave}
>
<span className="mx_NotificationBadge_count">{ symbol }</span>
{ props.children }
<span className="mx_NotificationBadge_count">{symbol}</span>
{props.children}
</AccessibleButton>
);
}
return (
<div className={classes}>
<span className="mx_NotificationBadge_count">{ symbol }</span>
<span className="mx_NotificationBadge_count">{symbol}</span>
</div>
);
}

View file

@ -28,9 +28,5 @@ interface Props {
export function UnreadNotificationBadge({ room, threadId }: Props) {
const { symbol, count, color } = useUnreadNotifications(room, threadId);
return <StatelessNotificationBadge
symbol={symbol}
count={count}
color={color}
/>;
return <StatelessNotificationBadge symbol={symbol} count={count} color={color} />;
}

View file

@ -27,8 +27,8 @@ import { Action } from "../../../dispatcher/actions";
import AccessibleButton from "../elements/AccessibleButton";
import MessageEvent from "../messages/MessageEvent";
import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from '../../../languageHandler';
import { formatDate } from '../../../DateUtils';
import { _t } from "../../../languageHandler";
import { formatDate } from "../../../DateUtils";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
@ -78,8 +78,8 @@ export default class PinnedEventTile extends React.Component<IProps> {
try {
await Promise.all(
[M_POLL_RESPONSE.name, M_POLL_RESPONSE.altName, M_POLL_END.name, M_POLL_END.altName]
.map(async eventType => {
[M_POLL_RESPONSE.name, M_POLL_RESPONSE.altName, M_POLL_END.name, M_POLL_END.altName].map(
async (eventType) => {
const relations = new Relations(RelationType.Reference, eventType, room);
relations.setTargetEvent(this.props.event);
@ -91,12 +91,17 @@ export default class PinnedEventTile extends React.Component<IProps> {
let nextBatch: string | undefined;
do {
const page = await this.context.relations(
roomId, eventId, RelationType.Reference, eventType, { from: nextBatch },
roomId,
eventId,
RelationType.Reference,
eventType,
{ from: nextBatch },
);
nextBatch = page.nextBatch;
page.events.forEach(event => relations.addEvent(event));
page.events.forEach((event) => relations.addEvent(event));
} while (nextBatch);
}),
},
),
);
} catch (err) {
logger.error(`Error fetching responses to pinned poll ${eventId} in room ${roomId}`);
@ -119,43 +124,45 @@ export default class PinnedEventTile extends React.Component<IProps> {
);
}
return <div className="mx_PinnedEventTile">
<MemberAvatar
className="mx_PinnedEventTile_senderAvatar"
member={this.props.event.sender}
width={AVATAR_SIZE}
height={AVATAR_SIZE}
fallbackUserId={sender}
/>
<span className={"mx_PinnedEventTile_sender " + getUserNameColorClass(sender)}>
{ this.props.event.sender?.name || sender }
</span>
{ unpinButton }
<div className="mx_PinnedEventTile_message">
<MessageEvent
mxEvent={this.props.event}
getRelationsForEvent={this.getRelationsForEvent}
// @ts-ignore - complaining that className is invalid when it's not
className="mx_PinnedEventTile_body"
maxImageHeight={150}
onHeightChanged={() => {}} // we need to give this, apparently
permalinkCreator={this.props.permalinkCreator}
replacingEventId={this.props.event.replacingEventId()}
return (
<div className="mx_PinnedEventTile">
<MemberAvatar
className="mx_PinnedEventTile_senderAvatar"
member={this.props.event.sender}
width={AVATAR_SIZE}
height={AVATAR_SIZE}
fallbackUserId={sender}
/>
</div>
<div className="mx_PinnedEventTile_footer">
<span className="mx_MessageTimestamp mx_PinnedEventTile_timestamp">
{ formatDate(new Date(this.props.event.getTs())) }
<span className={"mx_PinnedEventTile_sender " + getUserNameColorClass(sender)}>
{this.props.event.sender?.name || sender}
</span>
<AccessibleButton onClick={this.onTileClicked} kind="link">
{ _t("View message") }
</AccessibleButton>
{unpinButton}
<div className="mx_PinnedEventTile_message">
<MessageEvent
mxEvent={this.props.event}
getRelationsForEvent={this.getRelationsForEvent}
// @ts-ignore - complaining that className is invalid when it's not
className="mx_PinnedEventTile_body"
maxImageHeight={150}
onHeightChanged={() => {}} // we need to give this, apparently
permalinkCreator={this.props.permalinkCreator}
replacingEventId={this.props.event.replacingEventId()}
/>
</div>
<div className="mx_PinnedEventTile_footer">
<span className="mx_MessageTimestamp mx_PinnedEventTile_timestamp">
{formatDate(new Date(this.props.event.getTs()))}
</span>
<AccessibleButton onClick={this.onTileClicked} kind="link">
{_t("View message")}
</AccessibleButton>
</div>
</div>
</div>;
);
}
}

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue";
import { _t } from '../../../languageHandler';
import { _t } from "../../../languageHandler";
const BUSY_PRESENCE_NAME = new UnstableValue("busy", "org.matrix.msc3026.busy");
@ -85,7 +85,7 @@ export default class PresenceLabel extends React.Component<IProps> {
render() {
return (
<div className="mx_PresenceLabel">
{ this.getPrettyPresence(this.props.presenceState, this.props.activeAgo, this.props.currentlyActive) }
{this.getPrettyPresence(this.props.presenceState, this.props.activeAgo, this.props.currentlyActive)}
</div>
);
}

View file

@ -83,30 +83,27 @@ export function readReceiptTooltip(members: string[], hasMore: boolean): string
}
}
export function ReadReceiptGroup(
{ readReceipts, readReceiptMap, checkUnmounting, suppressAnimation, isTwelveHour }: Props,
) {
export function ReadReceiptGroup({
readReceipts,
readReceiptMap,
checkUnmounting,
suppressAnimation,
isTwelveHour,
}: Props) {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
// If we are above MAX_READ_AVATARS, well have to remove a few to have space for the +n count.
const hasMore = readReceipts.length > MAX_READ_AVATARS;
const maxAvatars = hasMore
? MAX_READ_AVATARS_PLUS_N
: MAX_READ_AVATARS;
const maxAvatars = hasMore ? MAX_READ_AVATARS_PLUS_N : MAX_READ_AVATARS;
const tooltipMembers: string[] = readReceipts.slice(0, maxAvatars)
.map(it => it.roomMember?.name ?? it.userId);
const tooltipMembers: string[] = readReceipts.slice(0, maxAvatars).map((it) => it.roomMember?.name ?? it.userId);
const tooltipText = readReceiptTooltip(tooltipMembers, hasMore);
const [{ showTooltip, hideTooltip }, tooltip] = useTooltip({
label: (
<>
<div className="mx_Tooltip_title">
{ _t("Seen by %(count)s people", { count: readReceipts.length }) }
</div>
<div className="mx_Tooltip_sub">
{ tooltipText }
</div>
<div className="mx_Tooltip_title">{_t("Seen by %(count)s people", { count: readReceipts.length })}</div>
<div className="mx_Tooltip_sub">{tooltipText}</div>
</>
),
alignment: Alignment.TopRight,
@ -132,42 +129,44 @@ export function ReadReceiptGroup(
);
}
const avatars = readReceipts.map((receipt, index) => {
const { hidden, position } = determineAvatarPosition(index, maxAvatars);
const avatars = readReceipts
.map((receipt, index) => {
const { hidden, position } = determineAvatarPosition(index, maxAvatars);
const userId = receipt.userId;
let readReceiptInfo: IReadReceiptInfo;
const userId = receipt.userId;
let readReceiptInfo: IReadReceiptInfo;
if (readReceiptMap) {
readReceiptInfo = readReceiptMap[userId];
if (!readReceiptInfo) {
readReceiptInfo = {};
readReceiptMap[userId] = readReceiptInfo;
if (readReceiptMap) {
readReceiptInfo = readReceiptMap[userId];
if (!readReceiptInfo) {
readReceiptInfo = {};
readReceiptMap[userId] = readReceiptInfo;
}
}
}
return (
<ReadReceiptMarker
key={userId}
member={receipt.roomMember}
fallbackUserId={userId}
offset={position * READ_AVATAR_OFFSET}
hidden={hidden}
readReceiptInfo={readReceiptInfo}
checkUnmounting={checkUnmounting}
suppressAnimation={suppressAnimation}
timestamp={receipt.ts}
showTwelveHour={isTwelveHour}
/>
);
}).reverse();
return (
<ReadReceiptMarker
key={userId}
member={receipt.roomMember}
fallbackUserId={userId}
offset={position * READ_AVATAR_OFFSET}
hidden={hidden}
readReceiptInfo={readReceiptInfo}
checkUnmounting={checkUnmounting}
suppressAnimation={suppressAnimation}
timestamp={receipt.ts}
showTwelveHour={isTwelveHour}
/>
);
})
.reverse();
let remText: JSX.Element;
const remainder = readReceipts.length - maxAvatars;
if (remainder > 0) {
remText = (
<span className="mx_ReadReceiptGroup_remainder" aria-live="off">
+{ remainder }
+{remainder}
</span>
);
}
@ -176,22 +175,19 @@ export function ReadReceiptGroup(
if (menuDisplayed) {
const buttonRect = button.current.getBoundingClientRect();
contextMenu = (
<ContextMenu
menuClassName="mx_ReadReceiptGroup_popup"
onFinished={closeMenu}
{...aboveLeftOf(buttonRect)}>
<ContextMenu menuClassName="mx_ReadReceiptGroup_popup" onFinished={closeMenu} {...aboveLeftOf(buttonRect)}>
<AutoHideScrollbar>
<SectionHeader className="mx_ReadReceiptGroup_title">
{ _t("Seen by %(count)s people", { count: readReceipts.length }) }
{_t("Seen by %(count)s people", { count: readReceipts.length })}
</SectionHeader>
{ readReceipts.map(receipt => (
{readReceipts.map((receipt) => (
<ReadReceiptPerson
key={receipt.userId}
{...receipt}
isTwelveHour={isTwelveHour}
onAfterClick={closeMenu}
/>
)) }
))}
</AutoHideScrollbar>
</ContextMenu>
);
@ -211,19 +207,21 @@ export function ReadReceiptGroup(
onFocus={showTooltip}
onBlur={hideTooltip}
>
{ remText }
{remText}
<span
className="mx_ReadReceiptGroup_container"
style={{
width: Math.min(maxAvatars, readReceipts.length) * READ_AVATAR_OFFSET +
READ_AVATAR_SIZE - READ_AVATAR_OFFSET,
width:
Math.min(maxAvatars, readReceipts.length) * READ_AVATAR_OFFSET +
READ_AVATAR_SIZE -
READ_AVATAR_OFFSET,
}}
>
{ avatars }
{avatars}
</span>
</AccessibleButton>
{ tooltip }
{ contextMenu }
{tooltip}
{contextMenu}
</div>
</div>
);
@ -240,12 +238,8 @@ function ReadReceiptPerson({ userId, roomMember, ts, isTwelveHour, onAfterClick
tooltipClassName: "mx_ReadReceiptGroup_person--tooltip",
label: (
<>
<div className="mx_Tooltip_title">
{ roomMember?.rawDisplayName ?? userId }
</div>
<div className="mx_Tooltip_sub">
{ userId }
</div>
<div className="mx_Tooltip_title">{roomMember?.rawDisplayName ?? userId}</div>
<div className="mx_Tooltip_sub">{userId}</div>
</>
),
});
@ -260,7 +254,7 @@ function ReadReceiptPerson({ userId, roomMember, ts, isTwelveHour, onAfterClick
// The ViewUser action leads to the RightPanelStore, and RightPanelStoreIPanelState defines the
// member property of IRightPanelCardState as `RoomMember | User`, so were fine for now, but we
// should definitely clean this up later
member: roomMember ?? { userId } as User,
member: roomMember ?? ({ userId } as User),
push: false,
});
onAfterClick?.();
@ -282,12 +276,10 @@ function ReadReceiptPerson({ userId, roomMember, ts, isTwelveHour, onAfterClick
hideTitle
/>
<div className="mx_ReadReceiptGroup_name">
<p>{ roomMember?.name ?? userId }</p>
<p className="mx_ReadReceiptGroup_secondary">
{ formatDate(new Date(ts), isTwelveHour) }
</p>
<p>{roomMember?.name ?? userId}</p>
<p className="mx_ReadReceiptGroup_secondary">{formatDate(new Date(ts), isTwelveHour)}</p>
</div>
{ tooltip }
{tooltip}
</MenuItem>
);
}
@ -301,14 +293,8 @@ function SectionHeader({ className, children }: PropsWithChildren<ISectionHeader
const [onFocus] = useRovingTabIndex(ref);
return (
<h3
className={className}
role="menuitem"
onFocus={onFocus}
tabIndex={-1}
ref={ref}
>
{ children }
<h3 className={className} role="menuitem" onFocus={onFocus} tabIndex={-1} ref={ref}>
{children}
</h3>
);
}

View file

@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef, RefObject } from 'react';
import React, { createRef, RefObject } from "react";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { logger } from "matrix-js-sdk/src/logger";
import NodeAnimator from "../../../NodeAnimator";
import { toPx } from "../../../utils/units";
import { LegacyMemberAvatar as MemberAvatar } from '../avatars/MemberAvatar';
import { LegacyMemberAvatar as MemberAvatar } from "../avatars/MemberAvatar";
import { READ_AVATAR_SIZE } from "./ReadReceiptGroup";
export interface IReadReceiptInfo {
@ -174,10 +174,10 @@ export default class ReadReceiptMarker extends React.PureComponent<IProps, IStat
const newPosition = this.readReceiptPosition(newInfo);
const oldPosition = oldInfo
// start at the old height and in the old h pos
? this.readReceiptPosition(oldInfo)
// treat new RRs as though they were off the top of the screen
: -READ_AVATAR_SIZE;
? // start at the old height and in the old h pos
this.readReceiptPosition(oldInfo)
: // treat new RRs as though they were off the top of the screen
-READ_AVATAR_SIZE;
const startStyles = [];
if (oldInfo?.right) {
@ -204,7 +204,7 @@ export default class ReadReceiptMarker extends React.PureComponent<IProps, IStat
const style = {
right: toPx(this.props.offset),
top: '0px',
top: "0px",
};
return (

View file

@ -33,45 +33,52 @@ 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 => {
return <MenuItem
key={crumb.roomId}
onClick={(ev) => {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: crumb.roomId,
metricsTrigger: "WebVerticalBreadcrumbs",
metricsViaKeyboard: ev.type !== "click",
});
tooltipRef.current?.hideTooltip();
}}
>
{ crumb.isSpaceRoom()
? <RoomAvatar room={crumb} width={24} height={24} />
: <DecoratedRoomAvatar room={crumb} avatarSize={24} tooltipProps={{ tabIndex: -1 }} />
}
<span className="mx_RecentlyViewedButton_entry_label">
<div>{ crumb.name }</div>
<RoomContextDetails className="mx_RecentlyViewedButton_entry_spaces" room={crumb} />
</span>
</MenuItem>;
}) }
const content = (
<div className="mx_RecentlyViewedButton_ContextMenu">
<h4>{_t("Recently viewed")}</h4>
<div>
{crumbs.map((crumb) => {
return (
<MenuItem
key={crumb.roomId}
onClick={(ev) => {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: crumb.roomId,
metricsTrigger: "WebVerticalBreadcrumbs",
metricsViaKeyboard: ev.type !== "click",
});
tooltipRef.current?.hideTooltip();
}}
>
{crumb.isSpaceRoom() ? (
<RoomAvatar room={crumb} width={24} height={24} />
) : (
<DecoratedRoomAvatar room={crumb} avatarSize={24} tooltipProps={{ tabIndex: -1 }} />
)}
<span className="mx_RecentlyViewedButton_entry_label">
<div>{crumb.name}</div>
<RoomContextDetails className="mx_RecentlyViewedButton_entry_spaces" room={crumb} />
</span>
</MenuItem>
);
})}
</div>
</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>;
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

@ -14,19 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import dis from "../../../dispatcher/dispatcher";
import { _t } from "../../../languageHandler";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import ReplyTile from './ReplyTile';
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
import ReplyTile from "./ReplyTile";
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import AccessibleButton from "../elements/AccessibleButton";
function cancelQuoting(context: TimelineRenderingType) {
dis.dispatch({
action: 'reply_to_event',
action: "reply_to_event",
event: null,
context,
});
@ -43,20 +43,19 @@ export default class ReplyPreview extends React.Component<IProps> {
public render(): JSX.Element | null {
if (!this.props.replyToEvent) return null;
return <div className="mx_ReplyPreview">
<div className="mx_ReplyPreview_section">
<div className="mx_ReplyPreview_header">
<span>{ _t('Replying') }</span>
<AccessibleButton
className="mx_ReplyPreview_header_cancel"
onClick={() => cancelQuoting(this.context.timelineRenderingType)}
/>
return (
<div className="mx_ReplyPreview">
<div className="mx_ReplyPreview_section">
<div className="mx_ReplyPreview_header">
<span>{_t("Replying")}</span>
<AccessibleButton
className="mx_ReplyPreview_header_cancel"
onClick={() => cancelQuoting(this.context.timelineRenderingType)}
/>
</div>
<ReplyTile mxEvent={this.props.replyToEvent} permalinkCreator={this.props.permalinkCreator} />
</div>
<ReplyTile
mxEvent={this.props.replyToEvent}
permalinkCreator={this.props.permalinkCreator}
/>
</div>
</div>;
);
}
}

View file

@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import classNames from 'classnames';
import React, { createRef } from "react";
import classNames from "classnames";
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
import { EventType, MsgType } from 'matrix-js-sdk/src/@types/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 { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import SenderProfile from "../messages/SenderProfile";
import MImageReplyBody from "../messages/MImageReplyBody";
import { isVoiceMessage } from '../../../utils/EventUtils';
import { isVoiceMessage } from "../../../utils/EventUtils";
import { getEventDisplayInfo } from "../../../utils/EventRenderingUtils";
import MFileBody from "../messages/MFileBody";
import MemberAvatar from '../avatars/MemberAvatar';
import MemberAvatar from "../avatars/MemberAvatar";
import MVoiceMessageBody from "../messages/MVoiceMessageBody";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { renderReplyTile } from "../../../events/EventTileFactory";
@ -109,17 +109,20 @@ export default class ReplyTile extends React.PureComponent<IProps> {
const msgType = mxEvent.getContent().msgtype;
const evType = mxEvent.getType();
const {
hasRenderer, isInfoMessage, isSeeingThroughMessageHiddenForModeration,
} = getEventDisplayInfo(mxEvent, false /* Replies are never hidden, so this should be fine */);
const { hasRenderer, isInfoMessage, isSeeingThroughMessageHiddenForModeration } = getEventDisplayInfo(
mxEvent,
false /* Replies are never hidden, so this should be fine */,
);
// This shouldn't happen: the caller should check we support this type
// before trying to instantiate us
if (!hasRenderer) {
const { mxEvent } = this.props;
logger.warn(`Event type not supported: type:${mxEvent.getType()} isState:${mxEvent.isState()}`);
return <div className="mx_ReplyTile mx_ReplyTile_info mx_MNoticeBody">
{ _t('This event could not be displayed') }
</div>;
return (
<div className="mx_ReplyTile mx_ReplyTile_info mx_MNoticeBody">
{_t("This event could not be displayed")}
</div>
);
}
const classes = classNames("mx_ReplyTile", {
@ -139,15 +142,8 @@ export default class ReplyTile extends React.PureComponent<IProps> {
if (!hasOwnSender) {
sender = (
<div className="mx_ReplyTile_sender">
<MemberAvatar
member={mxEvent.sender}
fallbackUserId={mxEvent.getSender()}
width={16}
height={16}
/>
<SenderProfile
mxEvent={mxEvent}
/>
<MemberAvatar member={mxEvent.sender} fallbackUserId={mxEvent.getSender()} width={16} height={16} />
<SenderProfile mxEvent={mxEvent} />
</div>
);
}
@ -166,24 +162,27 @@ export default class ReplyTile extends React.PureComponent<IProps> {
return (
<div className={classes}>
<a href={permalink} onClick={this.onClick} ref={this.anchorElement}>
{ sender }
{ renderReplyTile({
...this.props,
{sender}
{renderReplyTile(
{
...this.props,
// overrides
ref: null,
showUrlPreview: false,
overrideBodyTypes: msgtypeOverrides,
overrideEventTypes: evOverrides,
maxImageHeight: 96,
isSeeingThroughMessageHiddenForModeration,
// overrides
ref: null,
showUrlPreview: false,
overrideBodyTypes: msgtypeOverrides,
overrideEventTypes: evOverrides,
maxImageHeight: 96,
isSeeingThroughMessageHiddenForModeration,
// appease TS
highlights: this.props.highlights,
highlightLink: this.props.highlightLink,
onHeightChanged: this.props.onHeightChanged,
permalinkCreator: this.props.permalinkCreator,
}, false /* showHiddenEvents shouldn't be relevant */) }
// appease TS
highlights: this.props.highlights,
highlightLink: this.props.highlightLink,
onHeightChanged: this.props.onHeightChanged,
permalinkCreator: this.props.permalinkCreator,
},
false /* showHiddenEvents shouldn't be relevant */,
)}
</a>
</div>
);

View file

@ -30,8 +30,7 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { ButtonEvent } from "../elements/AccessibleButton";
interface IProps {
}
interface IProps {}
interface IState {
// Both of these control the animation for the breadcrumbs. For details on the
@ -44,7 +43,7 @@ interface IState {
skipFirst: boolean;
}
const RoomBreadcrumbTile = ({ room, onClick }: { room: Room, onClick: (ev: ButtonEvent) => void }) => {
const RoomBreadcrumbTile = ({ room, onClick }: { room: Room; onClick: (ev: ButtonEvent) => void }) => {
const [onFocus, isActive, ref] = useRovingTabIndex();
return (
@ -123,23 +122,16 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
if (tiles.length > 0) {
// NOTE: The CSSTransition timeout MUST match the timeout in our CSS!
return (
<CSSTransition
appear={true}
in={this.state.doAnimation}
timeout={640}
classNames='mx_RoomBreadcrumbs'
>
<Toolbar className='mx_RoomBreadcrumbs' aria-label={_t("Recently visited rooms")}>
{ tiles.slice(this.state.skipFirst ? 1 : 0) }
<CSSTransition appear={true} in={this.state.doAnimation} timeout={640} classNames="mx_RoomBreadcrumbs">
<Toolbar className="mx_RoomBreadcrumbs" aria-label={_t("Recently visited rooms")}>
{tiles.slice(this.state.skipFirst ? 1 : 0)}
</Toolbar>
</CSSTransition>
);
} else {
return (
<div className='mx_RoomBreadcrumbs'>
<div className="mx_RoomBreadcrumbs_placeholder">
{ _t("No recently visited rooms") }
</div>
<div className="mx_RoomBreadcrumbs">
<div className="mx_RoomBreadcrumbs_placeholder">{_t("No recently visited rooms")}</div>
</div>
);
}

View file

@ -27,10 +27,14 @@ type Props<T extends keyof ReactHTML> = HTMLAttributes<T> & {
export function RoomContextDetails<T extends keyof ReactHTML>({ room, component, ...other }: Props<T>) {
const contextDetails = roomContextDetails(room);
if (contextDetails) {
return React.createElement(component ?? "div", {
...other,
"aria-label": contextDetails.ariaLabel,
}, [contextDetails.details]);
return React.createElement(
component ?? "div",
{
...other,
"aria-label": contextDetails.ariaLabel,
},
[contextDetails.details],
);
}
return null;

View file

@ -15,38 +15,38 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { FC, useState, useMemo, useCallback } from 'react';
import classNames from 'classnames';
import { throttle } from 'lodash';
import React, { FC, useState, useMemo, useCallback } from "react";
import classNames from "classnames";
import { throttle } from "lodash";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { ISearchResults } from 'matrix-js-sdk/src/@types/search';
import { ISearchResults } from "matrix-js-sdk/src/@types/search";
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
import type { Room } from "matrix-js-sdk/src/models/room";
import { _t } from '../../../languageHandler';
import { _t } from "../../../languageHandler";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "../dialogs/UserTab";
import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
import E2EIcon from './E2EIcon';
import RoomHeaderButtons from "../right_panel/RoomHeaderButtons";
import E2EIcon from "./E2EIcon";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import RoomTopic from "../elements/RoomTopic";
import RoomName from "../elements/RoomName";
import { E2EStatus } from '../../../utils/ShieldUtils';
import { IOOBData } from '../../../stores/ThreepidInviteStore';
import { SearchScope } from './SearchBar';
import { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
import { E2EStatus } from "../../../utils/ShieldUtils";
import { IOOBData } from "../../../stores/ThreepidInviteStore";
import { SearchScope } from "./SearchBar";
import { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
import RoomContextMenu from "../context_menus/RoomContextMenu";
import { contextMenuBelow } from './RoomTile';
import { RoomNotificationStateStore } from '../../../stores/notifications/RoomNotificationStateStore';
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
import { NotificationStateEvents } from '../../../stores/notifications/NotificationState';
import { contextMenuBelow } from "./RoomTile";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
import RoomContext from "../../../contexts/RoomContext";
import RoomLiveShareWarning from '../beacon/RoomLiveShareWarning';
import RoomLiveShareWarning from "../beacon/RoomLiveShareWarning";
import { BetaPill } from "../beta/BetaCard";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
@ -68,10 +68,10 @@ import IconizedContextMenu, {
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { GroupCallDuration } from "../voip/CallDuration";
import { Alignment } from "../elements/Tooltip";
import RoomCallBanner from '../beacon/RoomCallBanner';
import RoomCallBanner from "../beacon/RoomCallBanner";
class DisabledWithReason {
constructor(public readonly reason: string) { }
constructor(public readonly reason: string) {}
}
interface VoiceCallButtonProps {
@ -93,7 +93,8 @@ const VoiceCallButton: FC<VoiceCallButtonProps> = ({ room, busy, setBusy, behavi
tooltip: behavior.reason,
disabled: true,
};
} else { // behavior === "legacy_or_jitsi"
} else {
// behavior === "legacy_or_jitsi"
return {
onClick: async (ev: ButtonEvent) => {
ev.preventDefault();
@ -106,14 +107,16 @@ const VoiceCallButton: FC<VoiceCallButtonProps> = ({ room, busy, setBusy, behavi
}
}, [behavior, room, setBusy]);
return <AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_voiceCallButton"
onClick={onClick}
title={_t("Voice call")}
tooltip={tooltip ?? _t("Voice call")}
alignment={Alignment.Bottom}
disabled={disabled || busy}
/>;
return (
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_voiceCallButton"
onClick={onClick}
title={_t("Voice call")}
tooltip={tooltip ?? _t("Voice call")}
alignment={Alignment.Bottom}
disabled={disabled || busy}
/>
);
};
interface VideoCallButtonProps {
@ -171,7 +174,8 @@ const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavi
},
disabled: false,
};
} else { // behavior === "jitsi_or_element"
} else {
// behavior === "jitsi_or_element"
return {
onClick: async (ev: ButtonEvent) => {
ev.preventDefault();
@ -182,42 +186,55 @@ const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavi
}
}, [behavior, startLegacyCall, startElementCall, openMenu]);
const onJitsiClick = useCallback(async (ev: ButtonEvent) => {
ev.preventDefault();
closeMenu();
await startLegacyCall();
}, [closeMenu, startLegacyCall]);
const onJitsiClick = useCallback(
async (ev: ButtonEvent) => {
ev.preventDefault();
closeMenu();
await startLegacyCall();
},
[closeMenu, startLegacyCall],
);
const onElementClick = useCallback((ev: ButtonEvent) => {
ev.preventDefault();
closeMenu();
startElementCall();
}, [closeMenu, startElementCall]);
const onElementClick = useCallback(
(ev: ButtonEvent) => {
ev.preventDefault();
closeMenu();
startElementCall();
},
[closeMenu, startElementCall],
);
let menu: JSX.Element | null = null;
if (menuOpen) {
const buttonRect = buttonRef.current!.getBoundingClientRect();
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
menu = <IconizedContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
<IconizedContextMenuOptionList>
<IconizedContextMenuOption label={_t("Video call (Jitsi)")} onClick={onJitsiClick} />
<IconizedContextMenuOption label={_t("Video call (%(brand)s)", { brand })} onClick={onElementClick} />
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
menu = (
<IconizedContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
<IconizedContextMenuOptionList>
<IconizedContextMenuOption label={_t("Video call (Jitsi)")} onClick={onJitsiClick} />
<IconizedContextMenuOption
label={_t("Video call (%(brand)s)", { brand })}
onClick={onElementClick}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
}
return <>
<AccessibleTooltipButton
inputRef={buttonRef}
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
onClick={onClick}
title={_t("Video call")}
tooltip={tooltip ?? _t("Video call")}
alignment={Alignment.Bottom}
disabled={disabled || busy}
/>
{ menu }
</>;
return (
<>
<AccessibleTooltipButton
inputRef={buttonRef}
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
onClick={onClick}
title={_t("Video call")}
tooltip={tooltip ?? _t("Video call")}
alignment={Alignment.Bottom}
disabled={disabled || busy}
/>
{menu}
</>
);
};
interface CallButtonsProps {
@ -243,24 +260,29 @@ const CallButtons: FC<CallButtonsProps> = ({ room }) => {
);
const widgets = useWidgets(room);
const hasJitsiWidget = useMemo(() => widgets.some(widget => WidgetType.JITSI.matches(widget.type)), [widgets]);
const hasJitsiWidget = useMemo(() => widgets.some((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]);
const hasGroupCall = useCall(room.roomId) !== null;
const [functionalMembers, mayEditWidgets, mayCreateElementCalls] = useTypedEventEmitterState(
room,
RoomStateEvent.Update,
useCallback(() => [
getJoinedNonFunctionalMembers(room),
room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client),
room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, room.client),
], [room]),
useCallback(
() => [
getJoinedNonFunctionalMembers(room),
room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client),
room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, room.client),
],
[room],
),
);
const makeVoiceCallButton = (behavior: VoiceCallButtonProps["behavior"]): JSX.Element =>
<VoiceCallButton room={room} busy={busy} setBusy={setBusy} behavior={behavior} />;
const makeVideoCallButton = (behavior: VideoCallButtonProps["behavior"]): JSX.Element =>
<VideoCallButton room={room} busy={busy} setBusy={setBusy} behavior={behavior} />;
const makeVoiceCallButton = (behavior: VoiceCallButtonProps["behavior"]): JSX.Element => (
<VoiceCallButton room={room} busy={busy} setBusy={setBusy} behavior={behavior} />
);
const makeVideoCallButton = (behavior: VideoCallButtonProps["behavior"]): JSX.Element => (
<VideoCallButton room={room} busy={busy} setBusy={setBusy} behavior={behavior} />
);
if (isVideoRoom || !showButtons) {
return null;
@ -276,54 +298,72 @@ const CallButtons: FC<CallButtonsProps> = ({ room }) => {
);
}
} else if (hasLegacyCall || hasJitsiWidget || hasGroupCall) {
return <>
{ makeVoiceCallButton(new DisabledWithReason(_t("Ongoing call"))) }
{ makeVideoCallButton(new DisabledWithReason(_t("Ongoing call"))) }
</>;
return (
<>
{makeVoiceCallButton(new DisabledWithReason(_t("Ongoing call")))}
{makeVideoCallButton(new DisabledWithReason(_t("Ongoing call")))}
</>
);
} else if (functionalMembers.length <= 1) {
return <>
{ makeVoiceCallButton(new DisabledWithReason(_t("There's no one here to call"))) }
{ makeVideoCallButton(new DisabledWithReason(_t("There's no one here to call"))) }
</>;
return (
<>
{makeVoiceCallButton(new DisabledWithReason(_t("There's no one here to call")))}
{makeVideoCallButton(new DisabledWithReason(_t("There's no one here to call")))}
</>
);
} else if (functionalMembers.length === 2) {
return <>
{ makeVoiceCallButton("legacy_or_jitsi") }
{ makeVideoCallButton("legacy_or_jitsi") }
</>;
return (
<>
{makeVoiceCallButton("legacy_or_jitsi")}
{makeVideoCallButton("legacy_or_jitsi")}
</>
);
} else if (mayEditWidgets) {
return <>
{ makeVoiceCallButton("legacy_or_jitsi") }
{ makeVideoCallButton(mayCreateElementCalls ? "jitsi_or_element" : "legacy_or_jitsi") }
</>;
return (
<>
{makeVoiceCallButton("legacy_or_jitsi")}
{makeVideoCallButton(mayCreateElementCalls ? "jitsi_or_element" : "legacy_or_jitsi")}
</>
);
} else {
const videoCallBehavior = mayCreateElementCalls
? "element"
: new DisabledWithReason(_t("You do not have permission to start video calls"));
return <>
{ makeVoiceCallButton(new DisabledWithReason(_t("You do not have permission to start voice calls"))) }
{ makeVideoCallButton(videoCallBehavior) }
</>;
return (
<>
{makeVoiceCallButton(new DisabledWithReason(_t("You do not have permission to start voice calls")))}
{makeVideoCallButton(videoCallBehavior)}
</>
);
}
} else if (hasLegacyCall || hasJitsiWidget) {
return <>
{ makeVoiceCallButton(new DisabledWithReason(_t("Ongoing call"))) }
{ makeVideoCallButton(new DisabledWithReason(_t("Ongoing call"))) }
</>;
return (
<>
{makeVoiceCallButton(new DisabledWithReason(_t("Ongoing call")))}
{makeVideoCallButton(new DisabledWithReason(_t("Ongoing call")))}
</>
);
} else if (functionalMembers.length <= 1) {
return <>
{ makeVoiceCallButton(new DisabledWithReason(_t("There's no one here to call"))) }
{ makeVideoCallButton(new DisabledWithReason(_t("There's no one here to call"))) }
</>;
return (
<>
{makeVoiceCallButton(new DisabledWithReason(_t("There's no one here to call")))}
{makeVideoCallButton(new DisabledWithReason(_t("There's no one here to call")))}
</>
);
} else if (functionalMembers.length === 2 || mayEditWidgets) {
return <>
{ makeVoiceCallButton("legacy_or_jitsi") }
{ makeVideoCallButton("legacy_or_jitsi") }
</>;
return (
<>
{makeVoiceCallButton("legacy_or_jitsi")}
{makeVideoCallButton("legacy_or_jitsi")}
</>
);
} else {
return <>
{ makeVoiceCallButton(new DisabledWithReason(_t("You do not have permission to start voice calls"))) }
{ makeVideoCallButton(new DisabledWithReason(_t("You do not have permission to start video calls"))) }
</>;
return (
<>
{makeVoiceCallButton(new DisabledWithReason(_t("You do not have permission to start voice calls")))}
{makeVideoCallButton(new DisabledWithReason(_t("You do not have permission to start video calls")))}
</>
);
}
};
@ -335,62 +375,75 @@ const CallLayoutSelector: FC<CallLayoutSelectorProps> = ({ call }) => {
const layout = useLayout(call);
const [menuOpen, buttonRef, openMenu, closeMenu] = useContextMenu();
const onClick = useCallback((ev: ButtonEvent) => {
ev.preventDefault();
openMenu();
}, [openMenu]);
const onClick = useCallback(
(ev: ButtonEvent) => {
ev.preventDefault();
openMenu();
},
[openMenu],
);
const onFreedomClick = useCallback((ev: ButtonEvent) => {
ev.preventDefault();
closeMenu();
call.setLayout(Layout.Tile);
}, [closeMenu, call]);
const onFreedomClick = useCallback(
(ev: ButtonEvent) => {
ev.preventDefault();
closeMenu();
call.setLayout(Layout.Tile);
},
[closeMenu, call],
);
const onSpotlightClick = useCallback((ev: ButtonEvent) => {
ev.preventDefault();
closeMenu();
call.setLayout(Layout.Spotlight);
}, [closeMenu, call]);
const onSpotlightClick = useCallback(
(ev: ButtonEvent) => {
ev.preventDefault();
closeMenu();
call.setLayout(Layout.Spotlight);
},
[closeMenu, call],
);
let menu: JSX.Element | null = null;
if (menuOpen) {
const buttonRect = buttonRef.current!.getBoundingClientRect();
menu = <IconizedContextMenu
className="mx_RoomHeader_layoutMenu"
{...aboveLeftOf(buttonRect)}
onFinished={closeMenu}
>
<IconizedContextMenuOptionList>
<IconizedContextMenuRadio
iconClassName="mx_RoomHeader_freedomIcon"
label={_t("Freedom")}
active={layout === Layout.Tile}
onClick={onFreedomClick}
/>
<IconizedContextMenuRadio
iconClassName="mx_RoomHeader_spotlightIcon"
label={_t("Spotlight")}
active={layout === Layout.Spotlight}
onClick={onSpotlightClick}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
menu = (
<IconizedContextMenu
className="mx_RoomHeader_layoutMenu"
{...aboveLeftOf(buttonRect)}
onFinished={closeMenu}
>
<IconizedContextMenuOptionList>
<IconizedContextMenuRadio
iconClassName="mx_RoomHeader_freedomIcon"
label={_t("Freedom")}
active={layout === Layout.Tile}
onClick={onFreedomClick}
/>
<IconizedContextMenuRadio
iconClassName="mx_RoomHeader_spotlightIcon"
label={_t("Spotlight")}
active={layout === Layout.Spotlight}
onClick={onSpotlightClick}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
}
return <>
<AccessibleTooltipButton
inputRef={buttonRef}
className={classNames("mx_RoomHeader_button", {
"mx_RoomHeader_layoutButton--freedom": layout === Layout.Tile,
"mx_RoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
})}
onClick={onClick}
title={_t("Change layout")}
alignment={Alignment.Bottom}
key="layout"
/>
{ menu }
</>;
return (
<>
<AccessibleTooltipButton
inputRef={buttonRef}
className={classNames("mx_RoomHeader_button", {
"mx_RoomHeader_layoutButton--freedom": layout === Layout.Tile,
"mx_RoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
})}
onClick={onClick}
title={_t("Change layout")}
alignment={Alignment.Bottom}
key="layout"
/>
{menu}
</>
);
};
export interface ISearchInfo {
@ -479,9 +532,13 @@ export default class RoomHeader extends React.Component<IProps, IState> {
this.forceUpdate();
};
private rateLimitedUpdate = throttle(() => {
this.forceUpdate();
}, 500, { leading: true, trailing: true });
private rateLimitedUpdate = throttle(
() => {
this.forceUpdate();
},
500,
{ leading: true, trailing: true },
);
private onContextMenuOpenClick = (ev: ButtonEvent) => {
ev.preventDefault();
@ -516,76 +573,90 @@ export default class RoomHeader extends React.Component<IProps, IState> {
}
if (!this.props.viewingCall && this.props.onForgetClick) {
startButtons.push(<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_forgetButton"
onClick={this.props.onForgetClick}
title={_t("Forget room")}
alignment={Alignment.Bottom}
key="forget"
/>);
startButtons.push(
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_forgetButton"
onClick={this.props.onForgetClick}
title={_t("Forget room")}
alignment={Alignment.Bottom}
key="forget"
/>,
);
}
if (!this.props.viewingCall && this.props.onAppsClick) {
startButtons.push(<AccessibleTooltipButton
className={classNames("mx_RoomHeader_button mx_RoomHeader_appsButton", {
mx_RoomHeader_appsButton_highlight: this.props.appsShown,
})}
onClick={this.props.onAppsClick}
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")}
alignment={Alignment.Bottom}
key="apps"
/>);
startButtons.push(
<AccessibleTooltipButton
className={classNames("mx_RoomHeader_button mx_RoomHeader_appsButton", {
mx_RoomHeader_appsButton_highlight: this.props.appsShown,
})}
onClick={this.props.onAppsClick}
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")}
alignment={Alignment.Bottom}
key="apps"
/>,
);
}
if (!this.props.viewingCall && this.props.onSearchClick && this.props.inRoom) {
startButtons.push(<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_searchButton"
onClick={this.props.onSearchClick}
title={_t("Search")}
alignment={Alignment.Bottom}
key="search"
/>);
startButtons.push(
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_searchButton"
onClick={this.props.onSearchClick}
title={_t("Search")}
alignment={Alignment.Bottom}
key="search"
/>,
);
}
if (this.props.onInviteClick && (!this.props.viewingCall || isVideoRoom) && this.props.inRoom) {
startButtons.push(<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_inviteButton"
onClick={this.props.onInviteClick}
title={_t("Invite")}
alignment={Alignment.Bottom}
key="invite"
/>);
startButtons.push(
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_inviteButton"
onClick={this.props.onInviteClick}
title={_t("Invite")}
alignment={Alignment.Bottom}
key="invite"
/>,
);
}
const endButtons: JSX.Element[] = [];
if (this.props.viewingCall && !isVideoRoom) {
if (this.props.activeCall === null) {
endButtons.push(<AccessibleButton
className="mx_RoomHeader_button mx_RoomHeader_closeButton"
onClick={this.onHideCallClick}
title={_t("Close call")}
key="close"
/>);
endButtons.push(
<AccessibleButton
className="mx_RoomHeader_button mx_RoomHeader_closeButton"
onClick={this.onHideCallClick}
title={_t("Close call")}
key="close"
/>,
);
} else {
endButtons.push(<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_minimiseButton"
onClick={this.onHideCallClick}
title={_t("View chat timeline")}
alignment={Alignment.Bottom}
key="minimise"
/>);
endButtons.push(
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_minimiseButton"
onClick={this.onHideCallClick}
title={_t("View chat timeline")}
alignment={Alignment.Bottom}
key="minimise"
/>,
);
}
}
return <>
{ startButtons }
<RoomHeaderButtons
room={this.props.room}
excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons}
/>
{ endButtons }
</>;
return (
<>
{startButtons}
<RoomHeaderButtons
room={this.props.room}
excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons}
/>
{endButtons}
</>
);
}
private renderName(oobName: string) {
@ -605,22 +676,26 @@ export default class RoomHeader extends React.Component<IProps, IState> {
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
if (members) {
if (members.length === 1 && members[0].userId === this.client.credentials.userId) {
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
const nameEvent = this.props.room.currentState.getStateEvents("m.room.name", "");
if (!nameEvent || !nameEvent.getContent().name) {
settingsHint = true;
}
}
}
const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
const roomName = <RoomName room={this.props.room}>
{ (name) => {
const roomName = name || oobName;
return <div dir="auto" className={textClasses} title={roomName} role="heading" aria-level={1}>
{ roomName }
</div>;
} }
</RoomName>;
const textClasses = classNames("mx_RoomHeader_nametext", { mx_RoomHeader_settingsHint: settingsHint });
const roomName = (
<RoomName room={this.props.room}>
{(name) => {
const roomName = name || oobName;
return (
<div dir="auto" className={textClasses} title={roomName} role="heading" aria-level={1}>
{roomName}
</div>
);
}}
</RoomName>
);
if (this.props.enableRoomOptionsMenu) {
return (
@ -631,16 +706,14 @@ export default class RoomHeader extends React.Component<IProps, IState> {
title={_t("Room options")}
alignment={Alignment.Bottom}
>
{ roomName }
{ this.props.room && <div className="mx_RoomHeader_chevron" /> }
{ contextMenu }
{roomName}
{this.props.room && <div className="mx_RoomHeader_chevron" />}
{contextMenu}
</ContextMenuTooltipButton>
);
}
return <div className="mx_RoomHeader_name mx_RoomHeader_name--textonly">
{ roomName }
</div>;
return <div className="mx_RoomHeader_name mx_RoomHeader_name--textonly">{roomName}</div>;
}
public render() {
@ -648,27 +721,25 @@ export default class RoomHeader extends React.Component<IProps, IState> {
let roomAvatar: JSX.Element | null = null;
if (this.props.room) {
roomAvatar = <DecoratedRoomAvatar
room={this.props.room}
avatarSize={24}
oobData={this.props.oobData}
viewAvatarOnClick={true}
/>;
roomAvatar = (
<DecoratedRoomAvatar
room={this.props.room}
avatarSize={24}
oobData={this.props.oobData}
viewAvatarOnClick={true}
/>
);
}
const icon = this.props.viewingCall
? <div className="mx_RoomHeader_icon mx_RoomHeader_icon_video" />
: this.props.e2eStatus
? <E2EIcon
className="mx_RoomHeader_icon"
status={this.props.e2eStatus}
tooltipAlignment={Alignment.Bottom}
/>
// If we're expecting an E2EE status to come in, but it hasn't
// yet been loaded, insert a blank div to reserve space
: this.client.isRoomEncrypted(this.props.room.roomId) && this.client.isCryptoEnabled()
? <div className="mx_RoomHeader_icon" />
: null;
const icon = this.props.viewingCall ? (
<div className="mx_RoomHeader_icon mx_RoomHeader_icon_video" />
) : this.props.e2eStatus ? (
<E2EIcon className="mx_RoomHeader_icon" status={this.props.e2eStatus} tooltipAlignment={Alignment.Bottom} />
) : // If we're expecting an E2EE status to come in, but it hasn't
// yet been loaded, insert a blank div to reserve space
this.client.isRoomEncrypted(this.props.room.roomId) && this.client.isCryptoEnabled() ? (
<div className="mx_RoomHeader_icon" />
) : null;
const buttons = this.props.showButtons ? this.renderButtons(isVideoRoom) : null;
@ -679,17 +750,17 @@ export default class RoomHeader extends React.Component<IProps, IState> {
className="mx_RoomHeader_wrapper"
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
>
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
{ icon }
<div className="mx_RoomHeader_avatar">{roomAvatar}</div>
{icon}
<div className="mx_RoomHeader_name mx_RoomHeader_name--textonly mx_RoomHeader_name--small">
{ _t("Video call") }
{_t("Video call")}
</div>
{ this.props.activeCall instanceof ElementCall && (
{this.props.activeCall instanceof ElementCall && (
<GroupCallDuration groupCall={this.props.activeCall.groupCall} />
) }
{ /* Empty topic element to fill out space */ }
)}
{/* Empty topic element to fill out space */}
<div className="mx_RoomHeader_topic" />
{ buttons }
{buttons}
</div>
</header>
);
@ -700,9 +771,12 @@ export default class RoomHeader extends React.Component<IProps, IState> {
// don't display the search count until the search completes and
// gives us a valid (possibly zero) searchCount.
if (typeof this.props.searchInfo?.count === "number") {
searchStatus = <div className="mx_RoomHeader_searchStatus">&nbsp;
{ _t("(~%(count)s results)", { count: this.props.searchInfo.count }) }
</div>;
searchStatus = (
<div className="mx_RoomHeader_searchStatus">
&nbsp;
{_t("(~%(count)s results)", { count: this.props.searchInfo.count })}
</div>
);
}
let oobName = _t("Join Room");
@ -712,15 +786,13 @@ export default class RoomHeader extends React.Component<IProps, IState> {
const name = this.renderName(oobName);
const topicElement = <RoomTopic
room={this.props.room}
className="mx_RoomHeader_topic"
/>;
const topicElement = <RoomTopic room={this.props.room} className="mx_RoomHeader_topic" />;
const viewLabs = () => defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
});
const viewLabs = () =>
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
});
const betaPill = isVideoRoom ? (
<BetaPill onClick={viewLabs} tooltipTitle={_t("Video rooms are a beta feature")} />
) : null;
@ -731,15 +803,15 @@ export default class RoomHeader extends React.Component<IProps, IState> {
className="mx_RoomHeader_wrapper"
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
>
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
{ icon }
{ name }
{ searchStatus }
{ topicElement }
{ betaPill }
{ buttons }
<div className="mx_RoomHeader_avatar">{roomAvatar}</div>
{icon}
{name}
{searchStatus}
{topicElement}
{betaPill}
{buttons}
</div>
{ !isVideoRoom && <RoomCallBanner roomId={this.props.room.roomId} /> }
{!isVideoRoom && <RoomCallBanner roomId={this.props.room.roomId} />}
<RoomLiveShareWarning roomId={this.props.room.roomId} />
</header>
);

View file

@ -41,7 +41,7 @@ const RoomInfoLine: FC<IProps> = ({ room }) => {
return null;
}
}, [room]);
const joinRule = useRoomState(room, state => state.getJoinRule());
const joinRule = useRoomState(room, (state) => state.getJoinRule());
const membership = useMyRoomMembership(room);
const memberCount = useRoomMemberCount(room);
@ -64,27 +64,31 @@ const RoomInfoLine: FC<IProps> = ({ room }) => {
let members: JSX.Element;
if (membership === "invite" && summary) {
// Don't trust local state and instead use the summary API
members = <span className="mx_RoomInfoLine_members">
{ _t("%(count)s members", { count: summary.num_joined_members }) }
</span>;
} else if (memberCount && summary !== undefined) { // summary is not still loading
const viewMembers = () => RightPanelStore.instance.setCard({
phase: room.isSpaceRoom() ? RightPanelPhases.SpaceMemberList : RightPanelPhases.RoomMemberList,
});
members = (
<span className="mx_RoomInfoLine_members">
{_t("%(count)s members", { count: summary.num_joined_members })}
</span>
);
} else if (memberCount && summary !== undefined) {
// summary is not still loading
const viewMembers = () =>
RightPanelStore.instance.setCard({
phase: room.isSpaceRoom() ? RightPanelPhases.SpaceMemberList : RightPanelPhases.RoomMemberList,
});
members = <AccessibleButton
kind="link"
className="mx_RoomInfoLine_members"
onClick={viewMembers}
>
{ _t("%(count)s members", { count: memberCount }) }
</AccessibleButton>;
members = (
<AccessibleButton kind="link" className="mx_RoomInfoLine_members" onClick={viewMembers}>
{_t("%(count)s members", { count: memberCount })}
</AccessibleButton>
);
}
return <div className={`mx_RoomInfoLine ${iconClass}`}>
{ roomType }
{ members }
</div>;
return (
<div className={`mx_RoomInfoLine ${iconClass}`}>
{roomType}
{members}
</div>
);
};
export default RoomInfoLine;

View file

@ -92,10 +92,7 @@ export const TAG_ORDER: TagID[] = [
DefaultTagID.Suggested,
DefaultTagID.Archived,
];
const ALWAYS_VISIBLE_TAGS: TagID[] = [
DefaultTagID.DM,
DefaultTagID.Untagged,
];
const ALWAYS_VISIBLE_TAGS: TagID[] = [DefaultTagID.DM, DefaultTagID.Untagged];
interface ITagAesthetics {
sectionLabel: string;
@ -133,62 +130,78 @@ const DmAuxButton = ({ tabIndex, dispatcher = defaultDispatcher }: IAuxButtonPro
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" });
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateChatItem", e);
}}
/> }
{ 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>;
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" });
PosthogTrackers.trackInteraction(
"WebRoomListRoomsSublistPlusMenuCreateChatItem",
e,
);
}}
/>
)}
{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
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={openMenu}
onClick={(e) => {
dispatcher.dispatch({ action: "view_create_chat" });
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateChatItem", e);
}}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={_t("Add people")}
title={_t("Add people")}
isExpanded={menuDisplayed}
inputRef={handle}
aria-label={_t("Start chat")}
title={_t("Start chat")}
/>
{ contextMenu }
</>;
} else if (!activeSpace && showCreateRooms) {
return <AccessibleTooltipButton
tabIndex={tabIndex}
onClick={(e) => {
dispatcher.dispatch({ action: 'view_create_chat' });
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateChatItem", e);
}}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={_t("Start chat")}
title={_t("Start chat")}
/>;
);
}
return null;
@ -206,28 +219,30 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
let contextMenuContent: JSX.Element | null = null;
if (menuDisplayed && activeSpace) {
const canAddRooms = activeSpace.currentState.maySendStateEvent(EventType.SpaceChild,
MatrixClientPeg.get().getUserId());
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<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: activeSpace.roomId,
metricsTrigger: undefined, // other
});
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuExploreRoomsItem", e);
}}
/>
{
showCreateRoom
? (<>
contextMenuContent = (
<IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Explore rooms")}
iconClassName="mx_RoomList_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: activeSpace.roomId,
metricsTrigger: undefined, // other
});
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuExploreRoomsItem", e);
}}
/>
{showCreateRoom ? (
<>
<IconizedContextMenuOption
label={_t("New room")}
iconClassName="mx_RoomList_iconNewRoom"
@ -239,10 +254,13 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to create new rooms in this space")}
tooltip={
canAddRooms
? undefined
: _t("You do not have permissions to create new rooms in this space")
}
/>
{ videoRoomsEnabled && (
{videoRoomsEnabled && (
<IconizedContextMenuOption
label={_t("New video room")}
iconClassName="mx_RoomList_iconNewVideoRoom"
@ -256,12 +274,15 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to create new rooms in this space")}
tooltip={
canAddRooms
? undefined
: _t("You do not have permissions to create new rooms in this space")
}
>
<BetaPill />
</IconizedContextMenuOption>
) }
)}
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomList_iconAddExistingRoom"
@ -272,80 +293,91 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
showAddExistingRooms(activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to add rooms to this space")}
tooltip={
canAddRooms ? undefined : _t("You do not have permissions to add rooms to this space")
}
/>
</>)
: null
}
</IconizedContextMenuOptionList>;
</>
) : null}
</IconizedContextMenuOptionList>
);
} else if (menuDisplayed) {
contextMenuContent = <IconizedContextMenuOptionList first>
{ showCreateRoom && <>
contextMenuContent = (
<IconizedContextMenuOptionList first>
{showCreateRoom && (
<>
<IconizedContextMenuOption
label={_t("New room")}
iconClassName="mx_RoomList_iconNewRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
defaultDispatcher.dispatch({ action: "view_create_room" });
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e);
}}
/>
{videoRoomsEnabled && (
<IconizedContextMenuOption
label={_t("New video room")}
iconClassName="mx_RoomList_iconNewVideoRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
defaultDispatcher.dispatch({
action: "view_create_room",
type: elementCallVideoRoomsEnabled
? RoomType.UnstableCall
: RoomType.ElementVideo,
});
}}
>
<BetaPill />
</IconizedContextMenuOption>
)}
</>
)}
<IconizedContextMenuOption
label={_t("New room")}
iconClassName="mx_RoomList_iconNewRoom"
label={_t("Explore public rooms")}
iconClassName="mx_RoomList_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
defaultDispatcher.dispatch({ action: "view_create_room" });
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e);
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuExploreRoomsItem", e);
defaultDispatcher.fire(Action.ViewRoomDirectory);
}}
/>
{ videoRoomsEnabled && (
<IconizedContextMenuOption
label={_t("New video room")}
iconClassName="mx_RoomList_iconNewVideoRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
defaultDispatcher.dispatch({
action: "view_create_room",
type: elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
});
}}
>
<BetaPill />
</IconizedContextMenuOption>
) }
</> }
<IconizedContextMenuOption
label={_t("Explore public rooms")}
iconClassName="mx_RoomList_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuExploreRoomsItem", e);
defaultDispatcher.fire(Action.ViewRoomDirectory);
}}
/>
</IconizedContextMenuOptionList>;
</IconizedContextMenuOptionList>
);
}
let contextMenu: JSX.Element | null = null;
if (menuDisplayed) {
contextMenu = <IconizedContextMenu {...auxButtonContextMenuPosition(handle)} onFinished={closeMenu} compact>
{ contextMenuContent }
</IconizedContextMenu>;
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={_t("Add room")}
title={_t("Add room")}
isExpanded={menuDisplayed}
inputRef={handle}
/>
return (
<>
<ContextMenuTooltipButton
tabIndex={tabIndex}
onClick={openMenu}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={_t("Add room")}
title={_t("Add room")}
isExpanded={menuDisplayed}
inputRef={handle}
/>
{ contextMenu }
</>;
{contextMenu}
</>
);
};
const TAG_AESTHETICS: ITagAestheticsMap = {
@ -424,10 +456,13 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
SdkContextClass.instance.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SpaceStore.instance.on(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
this.favouriteMessageWatcher =
SettingsStore.watchSetting("feature_favourite_messages", null, (...[,,, value]) => {
this.favouriteMessageWatcher = SettingsStore.watchSetting(
"feature_favourite_messages",
null,
(...[, , , value]) => {
this.setState({ feature_favourite_messages: value });
});
},
);
this.updateLists(); // trigger the first update
}
@ -467,12 +502,12 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
private getRoomDelta = (roomId: string, delta: number, unread = false) => {
const lists = RoomListStore.instance.orderedLists;
const rooms: Room[] = [];
TAG_ORDER.forEach(t => {
TAG_ORDER.forEach((t) => {
let listRooms = lists[t];
if (unread) {
// filter to only notification rooms (and our current active room so we can index properly)
listRooms = listRooms.filter(r => {
listRooms = listRooms.filter((r) => {
const state = RoomNotificationStateStore.instance.getRoomState(r);
return state.room.roomId === roomId || state.isUnread;
});
@ -481,7 +516,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
rooms.push(...listRooms);
});
const currentIndex = rooms.findIndex(r => r.roomId === roomId);
const currentIndex = rooms.findIndex((r) => r.roomId === roomId);
// use slice to account for looping around the start
const [room] = rooms.slice((currentIndex + delta) % rooms.length);
return room;
@ -525,7 +560,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
};
private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
return this.state.suggestedRooms.map(room => {
return this.state.suggestedRooms.map((room) => {
const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Empty room");
const avatar = (
<RoomAvatar
@ -573,7 +608,8 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
width={32}
height={32}
resizeMethod="crop"
/>);
/>
);
return [
<ExtraTile
@ -589,45 +625,44 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
private renderSublists(): React.ReactElement[] {
// show a skeleton UI if the user is in no rooms and they are not filtering and have no suggested rooms
const showSkeleton = !this.state.suggestedRooms?.length &&
Object.values(RoomListStore.instance.orderedLists).every(list => !list?.length);
const showSkeleton =
!this.state.suggestedRooms?.length &&
Object.values(RoomListStore.instance.orderedLists).every((list) => !list?.length);
return TAG_ORDER
.map(orderedTagId => {
let extraTiles = null;
if (orderedTagId === DefaultTagID.Suggested) {
extraTiles = this.renderSuggestedRooms();
} else if (this.state.feature_favourite_messages && orderedTagId === DefaultTagID.SavedItems) {
extraTiles = this.renderFavoriteMessagesList();
}
return TAG_ORDER.map((orderedTagId) => {
let extraTiles = null;
if (orderedTagId === DefaultTagID.Suggested) {
extraTiles = this.renderSuggestedRooms();
} else if (this.state.feature_favourite_messages && orderedTagId === DefaultTagID.SavedItems) {
extraTiles = this.renderFavoriteMessagesList();
}
const aesthetics = TAG_AESTHETICS[orderedTagId];
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
const aesthetics = 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) ||
(
!isMetaSpace(this.props.activeSpace) &&
orderedTagId === DefaultTagID.DM &&
!SettingsStore.getValue("Spaces.showPeopleInSpace", this.props.activeSpace)
)
) {
alwaysVisible = false;
}
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) ||
(!isMetaSpace(this.props.activeSpace) &&
orderedTagId === DefaultTagID.DM &&
!SettingsStore.getValue("Spaces.showPeopleInSpace", this.props.activeSpace))
) {
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
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
key={`sublist-${orderedTagId}`}
tagId={orderedTagId}
forRooms={true}
@ -641,22 +676,23 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
alwaysVisible={alwaysVisible}
onListCollapse={this.props.onListCollapse}
forceExpanded={forceExpanded}
/>;
});
/>
);
});
}
public focus(): void {
// focus the first focusable element in this aria treeview widget
const treeItems = this.treeRef.current?.querySelectorAll<HTMLElement>('[role="treeitem"]');
if (!treeItems) return;
[...treeItems].find(e => e.offsetParent !== null)?.focus();
[...treeItems].find((e) => e.offsetParent !== null)?.focus();
}
public render() {
const sublists = this.renderSublists();
return (
<RovingTabIndexProvider handleHomeEnd handleUpDown onKeyDown={this.props.onKeyDown}>
{ ({ onKeyDownHandler }) => (
{({ onKeyDownHandler }) => (
<div
onFocus={this.props.onFocus}
onBlur={this.props.onBlur}
@ -666,9 +702,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
aria-label={_t("Rooms")}
ref={this.treeRef}
>
{ sublists }
{sublists}
</div>
) }
)}
</RovingTabIndexProvider>
);
}

View file

@ -86,7 +86,7 @@ const usePendingActions = (): Map<PendingActionType, Set<string>> => {
}
};
useDispatcher(defaultDispatcher, payload => {
useDispatcher(defaultDispatcher, (payload) => {
switch (payload.action) {
case Action.JoinRoom:
addAction(PendingActionType.JoinRoom, payload.roomId);
@ -103,9 +103,7 @@ const usePendingActions = (): Map<PendingActionType, Set<string>> => {
break;
}
});
useTypedEventEmitter(cli, ClientEvent.Room, (room: Room) =>
removeAction(PendingActionType.JoinRoom, room.roomId),
);
useTypedEventEmitter(cli, ClientEvent.Room, (room: Room) => removeAction(PendingActionType.JoinRoom, room.roomId));
return actions;
};
@ -151,8 +149,10 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
const canCreateRooms = shouldShowComponent(UIComponent.CreateRooms);
const canCreateSpaces = shouldShowComponent(UIComponent.CreateSpaces);
const hasPermissionToAddSpaceChild =
activeSpace?.currentState?.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
const hasPermissionToAddSpaceChild = activeSpace?.currentState?.maySendStateEvent(
EventType.SpaceChild,
cli.getUserId(),
);
const canAddSubRooms = hasPermissionToAddSpaceChild && canCreateRooms;
const canAddSubSpaces = hasPermissionToAddSpaceChild && canCreateSpaces;
@ -170,159 +170,170 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
ContextMenuComponent = HomeButtonContextMenu;
}
contextMenu = <ContextMenuComponent
{...contextMenuBelow(mainMenuHandle.current.getBoundingClientRect())}
space={activeSpace}
onFinished={closeMainMenu}
hideHeader={true}
/>;
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();
}}
/>;
inviteOption = (
<IconizedContextMenuOption
label={_t("Invite")}
iconClassName="mx_RoomListHeader_iconInvite"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showSpaceInvite(activeSpace);
closePlusMenu();
}}
/>
);
}
let newRoomOptions: JSX.Element;
if (activeSpace?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId())) {
newRoomOptions = <>
<IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconNewRoom"
label={_t("New room")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showCreateNewRoom(activeSpace);
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
closePlusMenu();
}}
/>
{ videoRoomsEnabled && (
newRoomOptions = (
<>
<IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
label={_t("New video room")}
iconClassName="mx_RoomListHeader_iconNewRoom"
label={_t("New room")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showCreateNewRoom(
activeSpace,
elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
);
showCreateNewRoom(activeSpace);
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
closePlusMenu();
}}
>
<BetaPill />
</IconizedContextMenuOption>
) }
</>;
/>
{videoRoomsEnabled && (
<IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
label={_t("New video room")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showCreateNewRoom(
activeSpace,
elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
);
closePlusMenu();
}}
>
<BetaPill />
</IconizedContextMenuOption>
)}
</>
);
}
contextMenu = <IconizedContextMenu
{...contextMenuBelow(plusMenuHandle.current.getBoundingClientRect())}
onFinished={closePlusMenu}
compact
>
<IconizedContextMenuOptionList first>
{ inviteOption }
{ newRoomOptions }
<IconizedContextMenuOption
label={_t("Explore rooms")}
iconClassName="mx_RoomListHeader_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: activeSpace.roomId,
metricsTrigger: undefined, // other
});
closePlusMenu();
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuExploreRoomsItem", e);
}}
/>
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomListHeader_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showAddExistingRooms(activeSpace);
closePlusMenu();
}}
disabled={!canAddSubRooms}
tooltip={!canAddSubRooms && _t("You do not have permissions to add rooms to this space")}
/>
{ canCreateSpaces && <IconizedContextMenuOption
label={_t("Add space")}
iconClassName="mx_RoomListHeader_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showCreateNewSubspace(activeSpace);
closePlusMenu();
}}
disabled={!canAddSubSpaces}
tooltip={!canAddSubSpaces && _t("You do not have permissions to add spaces to this space")}
>
<BetaPill />
</IconizedContextMenuOption>
}
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
contextMenu = (
<IconizedContextMenu
{...contextMenuBelow(plusMenuHandle.current.getBoundingClientRect())}
onFinished={closePlusMenu}
compact
>
<IconizedContextMenuOptionList first>
{inviteOption}
{newRoomOptions}
<IconizedContextMenuOption
label={_t("Explore rooms")}
iconClassName="mx_RoomListHeader_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: activeSpace.roomId,
metricsTrigger: undefined, // other
});
closePlusMenu();
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuExploreRoomsItem", e);
}}
/>
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomListHeader_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showAddExistingRooms(activeSpace);
closePlusMenu();
}}
disabled={!canAddSubRooms}
tooltip={!canAddSubRooms && _t("You do not have permissions to add rooms to this space")}
/>
{canCreateSpaces && (
<IconizedContextMenuOption
label={_t("Add space")}
iconClassName="mx_RoomListHeader_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showCreateNewSubspace(activeSpace);
closePlusMenu();
}}
disabled={!canAddSubSpaces}
tooltip={!canAddSubSpaces && _t("You do not have permissions to add spaces to this space")}
>
<BetaPill />
</IconizedContextMenuOption>
)}
</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
} else if (plusMenuDisplayed) {
let newRoomOpts: JSX.Element;
let joinRoomOpt: JSX.Element;
if (canCreateRooms) {
newRoomOpts = <>
<IconizedContextMenuOption
label={_t("Start new chat")}
iconClassName="mx_RoomListHeader_iconStartChat"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "view_create_chat" });
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e);
closePlusMenu();
}}
/>
<IconizedContextMenuOption
label={_t("New room")}
iconClassName="mx_RoomListHeader_iconNewRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "view_create_room" });
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
closePlusMenu();
}}
/>
{ videoRoomsEnabled && (
newRoomOpts = (
<>
<IconizedContextMenuOption
label={_t("New video room")}
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
label={_t("Start new chat")}
iconClassName="mx_RoomListHeader_iconStartChat"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({
action: "view_create_room",
type: elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
});
defaultDispatcher.dispatch({ action: "view_create_chat" });
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e);
closePlusMenu();
}}
>
<BetaPill />
</IconizedContextMenuOption>
) }
</>;
/>
<IconizedContextMenuOption
label={_t("New room")}
iconClassName="mx_RoomListHeader_iconNewRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "view_create_room" });
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
closePlusMenu();
}}
/>
{videoRoomsEnabled && (
<IconizedContextMenuOption
label={_t("New video room")}
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({
action: "view_create_room",
type: elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
});
closePlusMenu();
}}
>
<BetaPill />
</IconizedContextMenuOption>
)}
</>
);
}
if (canExploreRooms) {
joinRoomOpt = (
@ -340,16 +351,18 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
);
}
contextMenu = <IconizedContextMenu
{...contextMenuBelow(plusMenuHandle.current.getBoundingClientRect())}
onFinished={closePlusMenu}
compact
>
<IconizedContextMenuOptionList first>
{ newRoomOpts }
{ joinRoomOpt }
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
contextMenu = (
<IconizedContextMenu
{...contextMenuBelow(plusMenuHandle.current.getBoundingClientRect())}
onFinished={closePlusMenu}
compact
>
<IconizedContextMenuOptionList first>
{newRoomOpts}
{joinRoomOpt}
</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
}
let title: string;
@ -371,36 +384,46 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
})
.join("\n");
let contextMenuButton: JSX.Element = <div className="mx_RoomListHeader_contextLessTitle">{ title }</div>;
let contextMenuButton: JSX.Element = <div className="mx_RoomListHeader_contextLessTitle">{title}</div>;
if (canShowMainMenu) {
contextMenuButton = <ContextMenuTooltipButton
inputRef={mainMenuHandle}
onClick={openMainMenu}
isExpanded={mainMenuDisplayed}
className="mx_RoomListHeader_contextMenuButton"
title={activeSpace
? _t("%(spaceName)s menu", { spaceName: spaceName ?? activeSpace.name })
: _t("Home options")}
>
{ title }
</ContextMenuTooltipButton>;
contextMenuButton = (
<ContextMenuTooltipButton
inputRef={mainMenuHandle}
onClick={openMainMenu}
isExpanded={mainMenuDisplayed}
className="mx_RoomListHeader_contextMenuButton"
title={
activeSpace
? _t("%(spaceName)s menu", { spaceName: spaceName ?? activeSpace.name })
: _t("Home options")
}
>
{title}
</ContextMenuTooltipButton>
);
}
return <div className="mx_RoomListHeader">
{ contextMenuButton }
{ pendingActionSummary ?
<TooltipTarget label={pendingActionSummary}><InlineSpinner /></TooltipTarget> :
null }
{ canShowPlusMenu && <ContextMenuTooltipButton
inputRef={plusMenuHandle}
onClick={openPlusMenu}
isExpanded={plusMenuDisplayed}
className="mx_RoomListHeader_plusButton"
title={_t("Add")}
/> }
return (
<div className="mx_RoomListHeader">
{contextMenuButton}
{pendingActionSummary ? (
<TooltipTarget label={pendingActionSummary}>
<InlineSpinner />
</TooltipTarget>
) : null}
{canShowPlusMenu && (
<ContextMenuTooltipButton
inputRef={plusMenuHandle}
onClick={openPlusMenu}
isExpanded={plusMenuDisplayed}
className="mx_RoomListHeader_plusButton"
title={_t("Add")}
/>
)}
{ contextMenu }
</div>;
{contextMenu}
</div>
);
};
export default RoomListHeader;

View file

@ -20,17 +20,14 @@ import { MatrixError } from "matrix-js-sdk/src/http-api";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { IJoinRuleEventContent, JoinRule } from "matrix-js-sdk/src/@types/partials";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import classNames from 'classnames';
import {
RoomPreviewOpts,
RoomViewLifecycle,
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
import classNames from "classnames";
import { RoomPreviewOpts, RoomViewLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import IdentityAuthClient from '../../../IdentityAuthClient';
import IdentityAuthClient from "../../../IdentityAuthClient";
import InviteReason from "../elements/InviteReason";
import { IOOBData } from "../../../stores/ThreepidInviteStore";
import Spinner from "../elements/Spinner";
@ -135,7 +132,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
// Gather the account 3PIDs
const account3pids = await MatrixClientPeg.get().getThreePids();
this.setState({
accountEmails: account3pids.threepids.filter(b => b.medium === 'email').map(b => b.address),
accountEmails: account3pids.threepids.filter((b) => b.medium === "email").map((b) => b.address),
});
// If we have an IS connected, use that to lookup the email and
// check the bound MXID.
@ -146,7 +143,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken();
const result = await MatrixClientPeg.get().lookupThreePid(
'email',
"email",
this.props.invitedEmail,
identityAccessToken,
);
@ -187,10 +184,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
if (this.props.invitedEmail) {
if (this.state.threePidFetchError) {
return MessageCase.OtherThreePIDError;
} else if (
this.state.accountEmails &&
!this.state.accountEmails.includes(this.props.invitedEmail)
) {
} else if (this.state.accountEmails && !this.state.accountEmails.includes(this.props.invitedEmail)) {
return MessageCase.InvitedEmailNotFoundInAccount;
} else if (!MatrixClientPeg.get().getIdentityServerUrl()) {
return MessageCase.InvitedEmailNoIdentityServer;
@ -200,7 +194,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
}
return MessageCase.Invite;
} else if (this.props.error) {
if ((this.props.error as MatrixError).errcode == 'M_NOT_FOUND') {
if ((this.props.error as MatrixError).errcode == "M_NOT_FOUND") {
return MessageCase.RoomNotFound;
} else {
return MessageCase.OtherError;
@ -210,23 +204,21 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
}
}
private getKickOrBanInfo(): { memberName?: string, reason?: string } {
private getKickOrBanInfo(): { memberName?: string; reason?: string } {
const myMember = this.getMyMember();
if (!myMember) {
return {};
}
const kickerMember = this.props.room.currentState.getMember(
myMember.events.member.getSender(),
);
const memberName = kickerMember ?
kickerMember.name : myMember.events.member.getSender();
const kickerMember = this.props.room.currentState.getMember(myMember.events.member.getSender());
const memberName = kickerMember ? kickerMember.name : myMember.events.member.getSender();
const reason = myMember.events.member.getContent().reason;
return { memberName, reason };
}
private joinRule(): JoinRule {
return this.props.room?.currentState
.getStateEvents(EventType.RoomJoinRules, "")?.getContent<IJoinRuleEventContent>().join_rule;
.getStateEvents(EventType.RoomJoinRules, "")
?.getContent<IJoinRuleEventContent>().join_rule;
}
private getMyMember(): RoomMember {
@ -257,9 +249,9 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
return memberContent.membership === "invite" && memberContent.is_direct;
}
private makeScreenAfterLogin(): { screen: string, params: Record<string, any> } {
private makeScreenAfterLogin(): { screen: string; params: Record<string, any> } {
return {
screen: 'room',
screen: "room",
params: {
email: this.props.invitedEmail,
signurl: this.props.signUrl,
@ -271,11 +263,11 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
}
private onLoginClick = () => {
dis.dispatch({ action: 'start_login', screenAfterLogin: this.makeScreenAfterLogin() });
dis.dispatch({ action: "start_login", screenAfterLogin: this.makeScreenAfterLogin() });
};
private onRegisterClick = () => {
dis.dispatch({ action: 'start_registration', screenAfterLogin: this.makeScreenAfterLogin() });
dis.dispatch({ action: "start_registration", screenAfterLogin: this.makeScreenAfterLogin() });
};
render() {
@ -319,8 +311,11 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
case MessageCase.NotLoggedIn: {
const opts: RoomPreviewOpts = { canJoin: false };
if (this.props.room?.roomId) {
ModuleRunner.instance
.invoke(RoomViewLifecycle.PreviewRoomNotLoggedIn, opts, this.props.room.roomId);
ModuleRunner.instance.invoke(
RoomViewLifecycle.PreviewRoomNotLoggedIn,
opts,
this.props.room.roomId,
);
}
if (opts.canJoin) {
title = _t("Join the room to participate");
@ -341,7 +336,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
footer = (
<div>
<Spinner w={20} h={20} />
{ _t("Loading preview") }
{_t("Loading preview")}
</div>
);
}
@ -350,8 +345,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
case MessageCase.Kicked: {
const { memberName, reason } = this.getKickOrBanInfo();
if (roomName) {
title = _t("You were removed from %(roomName)s by %(memberName)s",
{ memberName, roomName });
title = _t("You were removed from %(roomName)s by %(memberName)s", { memberName, roomName });
} else {
title = _t("You were removed by %(memberName)s", { memberName });
}
@ -398,15 +392,12 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
const joinRule = this.joinRule();
const errCodeMessage = _t(
"An error (%(errcode)s) was returned while trying to validate your " +
"invite. You could try to pass this information on to the person who invited you.",
"invite. You could try to pass this information on to the person who invited you.",
{ errcode: this.state.threePidFetchError.errcode || _t("unknown error code") },
);
switch (joinRule) {
case "invite":
subTitle = [
_t("You can only join it with a working invite."),
errCodeMessage,
];
subTitle = [_t("You can only join it with a working invite."), errCodeMessage];
primaryActionLabel = _t("Try to join anyway");
primaryActionHandler = this.props.onJoinClick;
break;
@ -427,22 +418,20 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
if (roomName) {
title = _t(
"This invite to %(roomName)s was sent to %(email)s which is not " +
"associated with your account",
"associated with your account",
{
roomName,
email: this.props.invitedEmail,
},
);
} else {
title = _t(
"This invite was sent to %(email)s which is not associated with your account",
{ email: this.props.invitedEmail },
);
title = _t("This invite was sent to %(email)s which is not associated with your account", {
email: this.props.invitedEmail,
});
}
subTitle = _t(
"Link this email with your account in Settings to receive invites " +
"directly in %(brand)s.",
"Link this email with your account in Settings to receive invites " + "directly in %(brand)s.",
{ brand },
);
primaryActionLabel = _t("Join the discussion");
@ -451,42 +440,32 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
}
case MessageCase.InvitedEmailNoIdentityServer: {
if (roomName) {
title = _t(
"This invite to %(roomName)s was sent to %(email)s",
{
roomName,
email: this.props.invitedEmail,
},
);
title = _t("This invite to %(roomName)s was sent to %(email)s", {
roomName,
email: this.props.invitedEmail,
});
} else {
title = _t("This invite was sent to %(email)s", { email: this.props.invitedEmail });
}
subTitle = _t(
"Use an identity server in Settings to receive invites directly in %(brand)s.",
{ brand },
);
subTitle = _t("Use an identity server in Settings to receive invites directly in %(brand)s.", {
brand,
});
primaryActionLabel = _t("Join the discussion");
primaryActionHandler = this.props.onJoinClick;
break;
}
case MessageCase.InvitedEmailMismatch: {
if (roomName) {
title = _t(
"This invite to %(roomName)s was sent to %(email)s",
{
roomName,
email: this.props.invitedEmail,
},
);
title = _t("This invite to %(roomName)s was sent to %(email)s", {
roomName,
email: this.props.invitedEmail,
});
} else {
title = _t("This invite was sent to %(email)s", { email: this.props.invitedEmail });
}
subTitle = _t(
"Share this email in Settings to receive invites directly in %(brand)s.",
{ brand },
);
subTitle = _t("Share this email in Settings to receive invites directly in %(brand)s.", { brand });
primaryActionLabel = _t("Join the discussion");
primaryActionHandler = this.props.onJoinClick;
break;
@ -497,29 +476,24 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
const inviteMember = this.getInviteMember();
let inviterElement;
if (inviteMember) {
inviterElement = <span>
<span className="mx_RoomPreviewBar_inviter">
{ inviteMember.rawDisplayName }
</span> ({ inviteMember.userId })
</span>;
inviterElement = (
<span>
<span className="mx_RoomPreviewBar_inviter">{inviteMember.rawDisplayName}</span> (
{inviteMember.userId})
</span>
);
} else {
inviterElement = (<span className="mx_RoomPreviewBar_inviter">{ this.props.inviterName }</span>);
inviterElement = <span className="mx_RoomPreviewBar_inviter">{this.props.inviterName}</span>;
}
const isDM = this.isDMInvite();
if (isDM) {
title = _t("Do you want to chat with %(user)s?", { user: inviteMember.name });
subTitle = [
avatar,
_t("<userName/> wants to chat", {}, { userName: () => inviterElement }),
];
subTitle = [avatar, _t("<userName/> wants to chat", {}, { userName: () => inviterElement })];
primaryActionLabel = _t("Start chatting");
} else {
title = _t("Do you want to join %(roomName)s?", { roomName });
subTitle = [
avatar,
_t("<userName/> invited you", {}, { userName: () => inviterElement }),
];
subTitle = [avatar, _t("<userName/> invited you", {}, { userName: () => inviterElement })];
primaryActionLabel = _t("Accept");
}
@ -527,10 +501,12 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
const memberEventContent = this.props.room.currentState.getMember(myUserId).events.member.getContent();
if (memberEventContent.reason) {
reasonElement = <InviteReason
reason={memberEventContent.reason}
htmlReason={memberEventContent[MemberEventHtmlReasonField]}
/>;
reasonElement = (
<InviteReason
reason={memberEventContent.reason}
htmlReason={memberEventContent[MemberEventHtmlReasonField]}
/>
);
}
primaryActionHandler = this.props.onJoinClick;
@ -540,7 +516,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
if (this.props.onRejectAndIgnoreClick) {
extraComponents.push(
<AccessibleButton kind="secondary" onClick={this.props.onRejectAndIgnoreClick} key="ignore">
{ _t("Reject & Ignore user") }
{_t("Reject & Ignore user")}
</AccessibleButton>,
);
}
@ -577,13 +553,20 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
_t("Try again later, or ask a room or space admin to check if you have access."),
_t(
"%(errcode)s was returned while trying to access the room or space. " +
"If you think you're seeing this message in error, please " +
"<issueLink>submit a bug report</issueLink>.",
"If you think you're seeing this message in error, please " +
"<issueLink>submit a bug report</issueLink>.",
{ errcode: this.props.error.errcode },
{ issueLink: label => <a
href="https://github.com/vector-im/element-web/issues/new/choose"
target="_blank"
rel="noreferrer noopener">{ label }</a> },
{
issueLink: (label) => (
<a
href="https://github.com/vector-im/element-web/issues/new/choose"
target="_blank"
rel="noreferrer noopener"
>
{label}
</a>
),
},
),
];
break;
@ -595,21 +578,26 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
if (!Array.isArray(subTitle)) {
subTitle = [subTitle];
}
subTitleElements = subTitle.map((t, i) => <p key={`subTitle${i}`}>{ t }</p>);
subTitleElements = subTitle.map((t, i) => <p key={`subTitle${i}`}>{t}</p>);
}
let titleElement;
if (showSpinner) {
titleElement = <h3 className="mx_RoomPreviewBar_spinnerTitle"><Spinner />{ title }</h3>;
titleElement = (
<h3 className="mx_RoomPreviewBar_spinnerTitle">
<Spinner />
{title}
</h3>
);
} else {
titleElement = <h3>{ title }</h3>;
titleElement = <h3>{title}</h3>;
}
let primaryButton;
if (primaryActionHandler) {
primaryButton = (
<AccessibleButton kind="primary" onClick={primaryActionHandler}>
{ primaryActionLabel }
{primaryActionLabel}
</AccessibleButton>
);
}
@ -618,7 +606,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
if (secondaryActionHandler) {
secondaryButton = (
<AccessibleButton kind="secondary" onClick={secondaryActionHandler}>
{ secondaryActionLabel }
{secondaryActionLabel}
</AccessibleButton>
);
}
@ -626,36 +614,34 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
const isPanel = this.props.canPreview;
const classes = classNames("mx_RoomPreviewBar", "dark-panel", `mx_RoomPreviewBar_${messageCase}`, {
"mx_RoomPreviewBar_panel": isPanel,
"mx_RoomPreviewBar_dialog": !isPanel,
mx_RoomPreviewBar_panel: isPanel,
mx_RoomPreviewBar_dialog: !isPanel,
});
// ensure correct tab order for both views
const actions = isPanel
? <>
{ secondaryButton }
{ extraComponents }
{ primaryButton }
const actions = isPanel ? (
<>
{secondaryButton}
{extraComponents}
{primaryButton}
</>
: <>
{ primaryButton }
{ extraComponents }
{ secondaryButton }
</>;
) : (
<>
{primaryButton}
{extraComponents}
{secondaryButton}
</>
);
return (
<div className={classes}>
<div className="mx_RoomPreviewBar_message">
{ titleElement }
{ subTitleElements }
</div>
{ reasonElement }
<div className="mx_RoomPreviewBar_actions">
{ actions }
</div>
<div className="mx_RoomPreviewBar_footer">
{ footer }
{titleElement}
{subTitleElements}
</div>
{reasonElement}
<div className="mx_RoomPreviewBar_actions">{actions}</div>
<div className="mx_RoomPreviewBar_footer">{footer}</div>
</div>
);
}

View file

@ -54,7 +54,7 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
const isVideoRoom = room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom());
const myMembership = useMyRoomMembership(room);
useDispatcher(defaultDispatcher, payload => {
useDispatcher(defaultDispatcher, (payload) => {
if (payload.action === Action.JoinRoomError && payload.roomId === room.roomId) {
setBusy(false); // stop the spinner, join failed
}
@ -62,14 +62,15 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
const [busy, setBusy] = useState(false);
const joinRule = useRoomState(room, state => state.getJoinRule());
const cannotJoin = getEffectiveMembership(myMembership) === EffectiveMembership.Leave
&& joinRule !== JoinRule.Public;
const joinRule = useRoomState(room, (state) => state.getJoinRule());
const cannotJoin =
getEffectiveMembership(myMembership) === EffectiveMembership.Leave && joinRule !== JoinRule.Public;
const viewLabs = () => defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
});
const viewLabs = () =>
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
});
let inviterSection: JSX.Element | null = null;
let joinButtons: JSX.Element;
@ -84,7 +85,7 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
});
}}
>
{ _t("Leave") }
{_t("Leave")}
</AccessibleButton>
);
} else if (myMembership === "invite") {
@ -93,41 +94,47 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
if (inviteSender) {
const inviter = room.getMember(inviteSender);
inviterSection = <div className="mx_RoomPreviewCard_inviter">
<MemberAvatar member={inviter} fallbackUserId={inviteSender} width={32} height={32} />
<div>
<div className="mx_RoomPreviewCard_inviter_name">
{ _t("<inviter/> invites you", {}, {
inviter: () => <b>{ inviter?.name || inviteSender }</b>,
}) }
inviterSection = (
<div className="mx_RoomPreviewCard_inviter">
<MemberAvatar member={inviter} fallbackUserId={inviteSender} width={32} height={32} />
<div>
<div className="mx_RoomPreviewCard_inviter_name">
{_t(
"<inviter/> invites you",
{},
{
inviter: () => <b>{inviter?.name || inviteSender}</b>,
},
)}
</div>
{inviter ? <div className="mx_RoomPreviewCard_inviter_mxid">{inviteSender}</div> : null}
</div>
{ inviter ? <div className="mx_RoomPreviewCard_inviter_mxid">
{ inviteSender }
</div> : null }
</div>
</div>;
);
}
joinButtons = <>
<AccessibleButton
kind="secondary"
onClick={() => {
setBusy(true);
onRejectButtonClicked();
}}
>
{ _t("Reject") }
</AccessibleButton>
<AccessibleButton
kind="primary"
onClick={() => {
setBusy(true);
onJoinButtonClicked();
}}
>
{ _t("Accept") }
</AccessibleButton>
</>;
joinButtons = (
<>
<AccessibleButton
kind="secondary"
onClick={() => {
setBusy(true);
onRejectButtonClicked();
}}
>
{_t("Reject")}
</AccessibleButton>
<AccessibleButton
kind="primary"
onClick={() => {
setBusy(true);
onJoinButtonClicked();
}}
>
{_t("Accept")}
</AccessibleButton>
</>
);
} else {
joinButtons = (
<AccessibleButton
@ -141,7 +148,7 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
}}
disabled={cannotJoin}
>
{ _t("Join") }
{_t("Join")}
</AccessibleButton>
);
}
@ -152,11 +159,13 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
let avatarRow: JSX.Element;
if (isVideoRoom) {
avatarRow = <>
<RoomAvatar room={room} height={50} width={50} viewAvatarOnClick />
<div className="mx_RoomPreviewCard_video" />
<BetaPill onClick={viewLabs} tooltipTitle={_t("Video rooms are a beta feature")} />
</>;
avatarRow = (
<>
<RoomAvatar room={room} height={50} width={50} viewAvatarOnClick />
<div className="mx_RoomPreviewCard_video" />
<BetaPill onClick={viewLabs} tooltipTitle={_t("Video rooms are a beta feature")} />
</>
);
} else if (room.isSpaceRoom()) {
avatarRow = <RoomAvatar room={room} height={80} width={80} viewAvatarOnClick />;
} else {
@ -169,33 +178,32 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
roomName: room.name,
});
} else if (isVideoRoom && !videoRoomsEnabled) {
notice = myMembership === "join"
? _t("To view, please enable video rooms in Labs first")
: _t("To join, please enable video rooms in Labs first");
notice =
myMembership === "join"
? _t("To view, please enable video rooms in Labs first")
: _t("To join, please enable video rooms in Labs first");
joinButtons = <AccessibleButton kind="primary" onClick={viewLabs}>
{ _t("Show Labs settings") }
</AccessibleButton>;
joinButtons = (
<AccessibleButton kind="primary" onClick={viewLabs}>
{_t("Show Labs settings")}
</AccessibleButton>
);
}
return <div className="mx_RoomPreviewCard">
{ inviterSection }
<div className="mx_RoomPreviewCard_avatar">
{ avatarRow }
return (
<div className="mx_RoomPreviewCard">
{inviterSection}
<div className="mx_RoomPreviewCard_avatar">{avatarRow}</div>
<h1 className="mx_RoomPreviewCard_name">
<RoomName room={room} />
</h1>
<RoomInfoLine room={room} />
<RoomTopic room={room} className="mx_RoomPreviewCard_topic" />
{room.getJoinRule() === "public" && <RoomFacePile room={room} />}
{notice ? <div className="mx_RoomPreviewCard_notice">{notice}</div> : null}
<div className="mx_RoomPreviewCard_joinButtons">{joinButtons}</div>
</div>
<h1 className="mx_RoomPreviewCard_name">
<RoomName room={room} />
</h1>
<RoomInfoLine room={room} />
<RoomTopic room={room} className="mx_RoomPreviewCard_topic" />
{ room.getJoinRule() === "public" && <RoomFacePile room={room} /> }
{ notice ? <div className="mx_RoomPreviewCard_notice">
{ notice }
</div> : null }
<div className="mx_RoomPreviewCard_joinButtons">
{ joinButtons }
</div>
</div>;
);
};
export default RoomPreviewCard;

View file

@ -17,7 +17,7 @@ limitations under the License.
*/
import { Room } from "matrix-js-sdk/src/models/room";
import classNames from 'classnames';
import classNames from "classnames";
import { Dispatcher } from "flux";
import { Enable, Resizable } from "re-resizable";
import { Direction } from "re-resizable/lib/resizer";
@ -198,8 +198,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
}
// Do the same check used on props for state, without the rooms we're going to no-op
const prevStateNoRooms = objectExcluding(this.state, ['rooms']);
const nextStateNoRooms = objectExcluding(nextState, ['rooms']);
const prevStateNoRooms = objectExcluding(this.state, ["rooms"]);
const nextStateNoRooms = objectExcluding(nextState, ["rooms"]);
if (objectHasDiff(prevStateNoRooms, nextStateNoRooms)) {
return true;
}
@ -343,9 +343,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(this.props.tagId);
const count = RoomListStore.instance.getCount(this.props.tagId);
await SlidingSyncManager.instance.ensureListRegistered(slidingSyncIndex, {
ranges: [
[0, count],
],
ranges: [[0, count]],
});
}
// read number of visible tiles before we mutate it
@ -448,12 +446,12 @@ export default class RoomSublist extends React.Component<IProps, IState> {
const listScrollTop = Math.round(list.scrollTop);
const isAtTop = listScrollTop <= Math.round(HEADER_HEIGHT);
const isAtBottom = listScrollTop >= Math.round(list.scrollHeight - list.offsetHeight);
const isStickyTop = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyTop');
const isStickyBottom = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyBottom');
const isStickyTop = possibleSticky.classList.contains("mx_RoomSublist_headerContainer_stickyTop");
const isStickyBottom = possibleSticky.classList.contains("mx_RoomSublist_headerContainer_stickyBottom");
if ((isStickyBottom && !isAtBottom) || (isStickyTop && !isAtTop)) {
// is sticky - jump to list
sublist.scrollIntoView({ behavior: 'smooth' });
sublist.scrollIntoView({ behavior: "smooth" });
} else {
// on screen - toggle collapse
const isExpanded = this.state.isExpanded;
@ -461,7 +459,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
// if the bottom list is collapsed then scroll it in so it doesn't expand off screen
if (!isExpanded && isStickyBottom) {
setImmediate(() => {
sublist.scrollIntoView({ behavior: 'smooth' });
sublist.scrollIntoView({ behavior: "smooth" });
});
}
}
@ -532,13 +530,15 @@ export default class RoomSublist extends React.Component<IProps, IState> {
}
for (const room of visibleRooms) {
tiles.push(<RoomTile
room={room}
key={`room-${room.roomId}`}
showMessagePreview={this.layout.showPreviews}
isMinimized={this.props.isMinimized}
tag={this.props.tagId}
/>);
tiles.push(
<RoomTile
room={room}
key={`room-${room.roomId}`}
showMessagePreview={this.layout.showPreviews}
isMinimized={this.props.isMinimized}
tag={this.props.tagId}
/>,
);
}
}
@ -569,9 +569,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(this.props.tagId);
const slidingList = SlidingSyncManager.instance.slidingSync.getList(slidingSyncIndex);
isAlphabetical = slidingList.sort[0] === "by_name";
isUnreadFirst = (
slidingList.sort[0] === "by_notification_level"
);
isUnreadFirst = slidingList.sort[0] === "by_notification_level";
}
// Invites don't get some nonsense options, so only add them if we have to.
@ -581,20 +579,20 @@ export default class RoomSublist extends React.Component<IProps, IState> {
<React.Fragment>
<hr />
<div>
<div className='mx_RoomSublist_contextMenu_title'>{ _t("Appearance") }</div>
<div className="mx_RoomSublist_contextMenu_title">{_t("Appearance")}</div>
<StyledMenuItemCheckbox
onClose={this.onCloseMenu}
onChange={this.onUnreadFirstChanged}
checked={isUnreadFirst}
>
{ _t("Show rooms with unread messages first") }
{_t("Show rooms with unread messages first")}
</StyledMenuItemCheckbox>
<StyledMenuItemCheckbox
onClose={this.onCloseMenu}
onChange={this.onMessagePreviewChanged}
checked={this.layout.showPreviews}
>
{ _t("Show previews of messages") }
{_t("Show previews of messages")}
</StyledMenuItemCheckbox>
</div>
</React.Fragment>
@ -610,14 +608,14 @@ export default class RoomSublist extends React.Component<IProps, IState> {
>
<div className="mx_RoomSublist_contextMenu">
<div>
<div className='mx_RoomSublist_contextMenu_title'>{ _t("Sort by") }</div>
<div className="mx_RoomSublist_contextMenu_title">{_t("Sort by")}</div>
<StyledMenuItemRadio
onClose={this.onCloseMenu}
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
checked={!isAlphabetical}
name={`mx_${this.props.tagId}_sortBy`}
>
{ _t("Activity") }
{_t("Activity")}
</StyledMenuItemRadio>
<StyledMenuItemRadio
onClose={this.onCloseMenu}
@ -625,10 +623,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
checked={isAlphabetical}
name={`mx_${this.props.tagId}_sortBy`}
>
{ _t("A-Z") }
{_t("A-Z")}
</StyledMenuItemRadio>
</div>
{ otherSections }
{otherSections}
</div>
</ContextMenu>
);
@ -642,7 +640,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
title={_t("List options")}
isExpanded={!!this.state.contextMenuPosition}
/>
{ contextMenu }
{contextMenu}
</React.Fragment>
);
}
@ -650,7 +648,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
private renderHeader(): React.ReactElement {
return (
<RovingTabIndexWrapper inputRef={this.headerButton}>
{ ({ onFocus, isActive, ref }) => {
{({ onFocus, isActive, ref }) => {
const tabIndex = isActive ? 0 : -1;
let ariaLabel = _t("Jump to first unread room.");
@ -676,20 +674,16 @@ export default class RoomSublist extends React.Component<IProps, IState> {
}
const collapseClasses = classNames({
'mx_RoomSublist_collapseBtn': true,
'mx_RoomSublist_collapseBtn_collapsed': !this.state.isExpanded && !this.props.forceExpanded,
mx_RoomSublist_collapseBtn: true,
mx_RoomSublist_collapseBtn_collapsed: !this.state.isExpanded && !this.props.forceExpanded,
});
const classes = classNames({
'mx_RoomSublist_headerContainer': true,
'mx_RoomSublist_headerContainer_withAux': !!addRoomButton,
mx_RoomSublist_headerContainer: true,
mx_RoomSublist_headerContainer_withAux: !!addRoomButton,
});
const badgeContainer = (
<div className="mx_RoomSublist_badgeContainer">
{ badge }
</div>
);
const badgeContainer = <div className="mx_RoomSublist_badgeContainer">{badge}</div>;
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
if (this.props.isMinimized) {
@ -723,18 +717,18 @@ export default class RoomSublist extends React.Component<IProps, IState> {
title={this.props.isMinimized ? this.props.label : undefined}
>
<span className={collapseClasses} />
<span>{ this.props.label }</span>
<span>{this.props.label}</span>
</Button>
{ this.renderMenu() }
{ this.props.isMinimized ? null : badgeContainer }
{ this.props.isMinimized ? null : addRoomButton }
{this.renderMenu()}
{this.props.isMinimized ? null : badgeContainer}
{this.props.isMinimized ? null : addRoomButton}
</div>
</div>
{ this.props.isMinimized ? badgeContainer : null }
{ this.props.isMinimized ? addRoomButton : null }
{this.props.isMinimized ? badgeContainer : null}
{this.props.isMinimized ? addRoomButton : null}
</div>
);
} }
}}
</RovingTabIndexWrapper>
);
}
@ -749,21 +743,23 @@ export default class RoomSublist extends React.Component<IProps, IState> {
const visibleTiles = this.renderVisibleTiles();
const hidden = !this.state.rooms.length && !this.props.extraTiles?.length && this.props.alwaysVisible !== true;
const classes = classNames({
'mx_RoomSublist': true,
'mx_RoomSublist_hasMenuOpen': !!this.state.contextMenuPosition,
'mx_RoomSublist_minimized': this.props.isMinimized,
'mx_RoomSublist_hidden': hidden,
mx_RoomSublist: true,
mx_RoomSublist_hasMenuOpen: !!this.state.contextMenuPosition,
mx_RoomSublist_minimized: this.props.isMinimized,
mx_RoomSublist_hidden: hidden,
});
let content = null;
if (this.state.roomsLoading) {
content = <div className="mx_RoomSublist_skeletonUI" />;
} else 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 }
content = (
<div className="mx_RoomSublist_resizeBox mx_RoomSublist_resizeBox_forceExpanded">
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
{visibleTiles}
</div>
</div>
</div>;
);
} else if (visibleTiles.length > 0) {
const layout = this.layout; // to shorten calls
@ -773,18 +769,17 @@ export default class RoomSublist extends React.Component<IProps, IState> {
const minTilesPx = layout.tilesToPixelsWithPadding(minTiles, minHeightPadding);
const maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
const showMoreBtnClasses = classNames({
'mx_RoomSublist_showNButton': true,
mx_RoomSublist_showNButton: true,
});
// If we're hiding rooms, show a 'show more' button to the user. This button
// floats above the resize handle, if we have one present. If the user has all
// tiles visible, it becomes 'show less'.
let showNButton = null;
const hasMoreSlidingSync = (
this.slidingSyncMode && (RoomListStore.instance.getCount(this.props.tagId) > this.state.rooms.length)
);
const hasMoreSlidingSync =
this.slidingSyncMode && RoomListStore.instance.getCount(this.props.tagId) > this.state.rooms.length;
if ((maxTilesPx > this.state.height) || hasMoreSlidingSync) {
if (maxTilesPx > this.state.height || hasMoreSlidingSync) {
// the height of all the tiles is greater than the section height: we need a 'show more' button
const nonPaddedHeight = this.state.height - RESIZE_HANDLE_HEIGHT - SHOW_N_BUTTON_HEIGHT;
const amountFullyShown = Math.floor(nonPaddedHeight / this.layout.tileHeight);
@ -793,11 +788,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
numMissing = RoomListStore.instance.getCount(this.props.tagId) - amountFullyShown;
}
const label = _t("Show %(count)s more", { count: numMissing });
let showMoreText = (
<span className='mx_RoomSublist_showNButtonText'>
{ label }
</span>
);
let showMoreText = <span className="mx_RoomSublist_showNButtonText">{label}</span>;
if (this.props.isMinimized) showMoreText = null;
showNButton = (
<RovingAccessibleButton
@ -806,20 +797,16 @@ export default class RoomSublist extends React.Component<IProps, IState> {
className={showMoreBtnClasses}
aria-label={label}
>
<span className='mx_RoomSublist_showMoreButtonChevron mx_RoomSublist_showNButtonChevron'>
{ /* set by CSS masking */ }
<span className="mx_RoomSublist_showMoreButtonChevron mx_RoomSublist_showNButtonChevron">
{/* set by CSS masking */}
</span>
{ showMoreText }
{showMoreText}
</RovingAccessibleButton>
);
} else if (this.numTiles > this.layout.defaultVisibleTiles) {
// we have all tiles visible - add a button to show less
const label = _t("Show less");
let showLessText = (
<span className='mx_RoomSublist_showNButtonText'>
{ label }
</span>
);
let showLessText = <span className="mx_RoomSublist_showNButtonText">{label}</span>;
if (this.props.isMinimized) showLessText = null;
showNButton = (
<RovingAccessibleButton
@ -828,10 +815,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
className={showMoreBtnClasses}
aria-label={label}
>
<span className='mx_RoomSublist_showLessButtonChevron mx_RoomSublist_showNButtonChevron'>
{ /* set by CSS masking */ }
<span className="mx_RoomSublist_showLessButtonChevron mx_RoomSublist_showNButtonChevron">
{/* set by CSS masking */}
</span>
{ showLessText }
{showLessText}
</RovingAccessibleButton>
);
}
@ -863,8 +850,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
// only mathematically 7 possible).
const handleWrapperClasses = classNames({
'mx_RoomSublist_resizerHandles': true,
'mx_RoomSublist_resizerHandles_showNButton': !!showNButton,
mx_RoomSublist_resizerHandles: true,
mx_RoomSublist_resizerHandles_showNButton: !!showNButton,
});
content = (
@ -882,9 +869,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
enable={handles}
>
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
{ visibleTiles }
{visibleTiles}
</div>
{ showNButton }
{showNButton}
</Resizable>
</React.Fragment>
);
@ -901,8 +888,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
aria-label={this.props.label}
onKeyDown={this.onKeyDown}
>
{ this.renderHeader() }
{ content }
{this.renderHeader()}
{content}
</div>
);
}

View file

@ -22,7 +22,7 @@ import classNames from "classnames";
import type { Call } from "../../../models/Call";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
import defaultDispatcher from '../../../dispatcher/dispatcher';
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { _t } from "../../../languageHandler";
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
@ -181,7 +181,8 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
}
private onAction = (payload: ActionPayload) => {
if (payload.action === Action.ViewRoom &&
if (
payload.action === Action.ViewRoom &&
payload.room_id === this.props.room.roomId &&
payload.show_room_tile
) {
@ -223,8 +224,9 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
ev.stopPropagation();
const action = getKeyBindingsManager().getAccessibilityAction(ev);
const clearSearch = ([KeyBindingAction.Enter, KeyBindingAction.Space] as Array<string | undefined>)
.includes(action);
const clearSearch = ([KeyBindingAction.Enter, KeyBindingAction.Space] as Array<string | undefined>).includes(
action,
);
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
@ -279,8 +281,11 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
};
private renderNotificationsMenu(isActive: boolean): React.ReactElement | null {
if (MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Archived ||
!this.showContextMenu || this.props.isMinimized
if (
MatrixClientPeg.get().isGuest() ||
this.props.tag === DefaultTagID.Archived ||
!this.showContextMenu ||
this.props.isMinimized
) {
// the menu makes no sense in these cases so do not show one
return null;
@ -309,13 +314,13 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
isExpanded={!!this.state.notificationsMenuPosition}
tabIndex={isActive ? 0 : -1}
/>
{ this.state.notificationsMenuPosition && (
{this.state.notificationsMenuPosition && (
<RoomNotificationContextMenu
{...contextMenuBelow(this.state.notificationsMenuPosition)}
onFinished={this.onCloseNotificationsMenu}
room={this.props.room}
/>
) }
)}
</React.Fragment>
);
}
@ -330,39 +335,39 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
title={_t("Room options")}
isExpanded={!!this.state.generalMenuPosition}
/>
{ this.state.generalMenuPosition && (
{this.state.generalMenuPosition && (
<RoomGeneralContextMenu
{...contextMenuBelow(this.state.generalMenuPosition)}
onFinished={this.onCloseGeneralMenu}
room={this.props.room}
onPostFavoriteClick={(ev: ButtonEvent) => PosthogTrackers.trackInteraction(
"WebRoomListRoomTileContextMenuFavouriteToggle", ev,
)}
onPostInviteClick={(ev: ButtonEvent) => PosthogTrackers.trackInteraction(
"WebRoomListRoomTileContextMenuInviteItem", ev,
)}
onPostSettingsClick={(ev: ButtonEvent) => PosthogTrackers.trackInteraction(
"WebRoomListRoomTileContextMenuSettingsItem", ev,
)}
onPostLeaveClick={(ev: ButtonEvent) => PosthogTrackers.trackInteraction(
"WebRoomListRoomTileContextMenuLeaveItem", ev,
)}
onPostFavoriteClick={(ev: ButtonEvent) =>
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuFavouriteToggle", ev)
}
onPostInviteClick={(ev: ButtonEvent) =>
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuInviteItem", ev)
}
onPostSettingsClick={(ev: ButtonEvent) =>
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuSettingsItem", ev)
}
onPostLeaveClick={(ev: ButtonEvent) =>
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuLeaveItem", ev)
}
/>
) }
)}
</React.Fragment>
);
}
public render(): React.ReactElement {
const classes = classNames({
'mx_RoomTile': true,
'mx_RoomTile_selected': this.state.selected,
'mx_RoomTile_hasMenuOpen': !!(this.state.generalMenuPosition || this.state.notificationsMenuPosition),
'mx_RoomTile_minimized': this.props.isMinimized,
mx_RoomTile: true,
mx_RoomTile_selected: this.state.selected,
mx_RoomTile_hasMenuOpen: !!(this.state.generalMenuPosition || this.state.notificationsMenuPosition),
mx_RoomTile_minimized: this.props.isMinimized,
});
let name = this.props.room.name;
if (typeof name !== 'string') name = '';
if (typeof name !== "string") name = "";
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
let badge: React.ReactNode;
@ -395,25 +400,23 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
id={messagePreviewId(this.props.room.roomId)}
title={this.state.messagePreview}
>
{ this.state.messagePreview }
{this.state.messagePreview}
</div>
);
}
const titleClasses = classNames({
"mx_RoomTile_title": true,
"mx_RoomTile_titleWithSubtitle": !!subtitle,
"mx_RoomTile_titleHasUnreadEvents": this.notificationState.isUnread,
mx_RoomTile_title: true,
mx_RoomTile_titleWithSubtitle: !!subtitle,
mx_RoomTile_titleHasUnreadEvents: this.notificationState.isUnread,
});
const titleContainer = this.props.isMinimized ? null : (
<div className="mx_RoomTile_titleContainer">
<div title={name} className={titleClasses} tabIndex={-1}>
<span dir="auto">
{ name }
</span>
<span dir="auto">{name}</span>
</div>
{ subtitle }
{subtitle}
</div>
);
@ -422,13 +425,17 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
if (this.props.tag === DefaultTagID.Invite) {
// append nothing
} else if (this.notificationState.hasMentions) {
ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
count: this.notificationState.count,
});
ariaLabel +=
" " +
_t("%(count)s unread messages including mentions.", {
count: this.notificationState.count,
});
} else if (this.notificationState.hasUnreadCount) {
ariaLabel += " " + _t("%(count)s unread messages.", {
count: this.notificationState.count,
});
ariaLabel +=
" " +
_t("%(count)s unread messages.", {
count: this.notificationState.count,
});
} else if (this.notificationState.isUnread) {
ariaLabel += " " + _t("Unread messages.");
}
@ -450,7 +457,7 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
return (
<React.Fragment>
<RovingTabIndexWrapper inputRef={this.roomTileRef}>
{ ({ onFocus, isActive, ref }) =>
{({ onFocus, isActive, ref }) => (
<Button
{...props}
onFocus={onFocus}
@ -470,12 +477,12 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
displayBadge={this.props.isMinimized}
tooltipProps={{ tabIndex: isActive ? 0 : -1 }}
/>
{ titleContainer }
{ badge }
{ this.renderGeneralMenu() }
{ this.renderNotificationsMenu(isActive) }
{titleContainer}
{badge}
{this.renderGeneralMenu()}
{this.renderNotificationsMenu(isActive)}
</Button>
}
)}
</RovingTabIndexWrapper>
</React.Fragment>
);

View file

@ -46,10 +46,12 @@ export const RoomTileCallSummary: FC<Props> = ({ call }) => {
break;
}
return <LiveContentSummary
type={LiveContentType.Video}
text={text}
active={active}
participantCount={useParticipantCount(call)}
/>;
return (
<LiveContentSummary
type={LiveContentType.Video}
text={text}
active={active}
participantCount={useParticipantCount(call)}
/>
);
};

View file

@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
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 { RoomStateEvent } from 'matrix-js-sdk/src/models/room-state';
import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import RoomUpgradeDialog from '../dialogs/RoomUpgradeDialog';
import AccessibleButton from '../elements/AccessibleButton';
import Modal from "../../../Modal";
import { _t } from "../../../languageHandler";
import RoomUpgradeDialog from "../dialogs/RoomUpgradeDialog";
import AccessibleButton from "../elements/AccessibleButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps {
@ -74,26 +74,27 @@ export default class RoomUpgradeWarningBar extends React.PureComponent<IProps, I
<div>
<div className="mx_RoomUpgradeWarningBar_body">
<p>
{ _t(
{_t(
"Upgrading this room will shut down the current instance of the room and create " +
"an upgraded room with the same name.",
) }
"an upgraded room with the same name.",
)}
</p>
<p>
{ _t(
{_t(
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members " +
"to the new version of the room.</i> We'll post a link to the new room in the old " +
"version of the room - room members will have to click this link to join the new room.",
{}, {
"b": (sub) => <b>{ sub }</b>,
"i": (sub) => <i>{ sub }</i>,
"to the new version of the room.</i> We'll post a link to the new room in the old " +
"version of the room - room members will have to click this link to join the new room.",
{},
{
b: (sub) => <b>{sub}</b>,
i: (sub) => <i>{sub}</i>,
},
) }
)}
</p>
</div>
<p className="mx_RoomUpgradeWarningBar_upgradelink">
<AccessibleButton onClick={this.onUpgradeClick}>
{ _t("Upgrade this room to the recommended room version") }
{_t("Upgrade this room to the recommended room version")}
</AccessibleButton>
</p>
</div>
@ -102,9 +103,7 @@ export default class RoomUpgradeWarningBar extends React.PureComponent<IProps, I
if (this.state.upgraded) {
doUpgradeWarnings = (
<div className="mx_RoomUpgradeWarningBar_body">
<p>
{ _t("This room has already been upgraded.") }
</p>
<p>{_t("This room has already been upgraded.")}</p>
</div>
);
}
@ -113,19 +112,19 @@ export default class RoomUpgradeWarningBar extends React.PureComponent<IProps, I
<div className="mx_RoomUpgradeWarningBar">
<div className="mx_RoomUpgradeWarningBar_wrapped">
<div className="mx_RoomUpgradeWarningBar_header">
{ _t(
{_t(
"This room is running room version <roomVersion />, which this homeserver has " +
"marked as <i>unstable</i>.",
"marked as <i>unstable</i>.",
{},
{
"roomVersion": () => <code>{ this.props.room.getVersion() }</code>,
"i": (sub) => <i>{ sub }</i>,
roomVersion: () => <code>{this.props.room.getVersion()}</code>,
i: (sub) => <i>{sub}</i>,
},
) }
)}
</div>
{ doUpgradeWarnings }
{doUpgradeWarnings}
<div className="mx_RoomUpgradeWarningBar_small">
{ _t("Only room administrators will see this warning") }
{_t("Only room administrators will see this warning")}
</div>
</div>
</div>

View file

@ -15,12 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef, RefObject } from 'react';
import React, { createRef, RefObject } from "react";
import classNames from "classnames";
import AccessibleButton from "../elements/AccessibleButton";
import { _t } from '../../../languageHandler';
import { PosthogScreenTracker } from '../../../PosthogTrackers';
import { _t } from "../../../languageHandler";
import { PosthogScreenTracker } from "../../../PosthogTrackers";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import SearchWarning, { WarningKind } from "../elements/SearchWarning";
@ -104,7 +104,7 @@ export default class SearchBar extends React.Component<IProps, IState> {
aria-checked={this.state.scope === SearchScope.Room}
role="radio"
>
{ _t("This Room") }
{_t("This Room")}
</AccessibleButton>
<AccessibleButton
className={allRoomsClasses}
@ -112,7 +112,7 @@ export default class SearchBar extends React.Component<IProps, IState> {
aria-checked={this.state.scope === SearchScope.All}
role="radio"
>
{ _t("All Rooms") }
{_t("All Rooms")}
</AccessibleButton>
</div>
<div className="mx_SearchBar_input mx_textinput">

View file

@ -21,7 +21,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import SettingsStore from "../../../settings/SettingsStore";
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import DateSeparator from "../messages/DateSeparator";
import EventTile from "./EventTile";
import { shouldFormContinuation } from "../../structures/MessagePanel";
@ -73,7 +73,7 @@ export default class SearchResultTile extends React.Component<IProps> {
for (let j = 0; j < timeline.length; j++) {
const mxEv = timeline[j];
let highlights;
const contextual = (j != result.context.getOurEventIndex());
const contextual = j != result.context.getOurEventIndex();
if (!contextual) {
highlights = this.props.searchHighlights;
}
@ -82,7 +82,8 @@ export default class SearchResultTile extends React.Component<IProps> {
// do we need a date separator since the last event?
const prevEv = timeline[j - 1];
// is this a continuation of the previous message?
const continuation = prevEv &&
const continuation =
prevEv &&
!wantsDateSeparator(prevEv.getDate(), mxEv.getDate()) &&
shouldFormContinuation(
prevEv,
@ -96,7 +97,7 @@ export default class SearchResultTile extends React.Component<IProps> {
const nextEv = timeline[j + 1];
if (nextEv) {
const willWantDateSeparator = wantsDateSeparator(mxEv.getDate(), nextEv.getDate());
lastInSection = (
lastInSection =
willWantDateSeparator ||
mxEv.getSender() !== nextEv.getSender() ||
!shouldFormContinuation(
@ -105,8 +106,7 @@ export default class SearchResultTile extends React.Component<IProps> {
this.context?.showHiddenEvents,
threadsEnabled,
TimelineRenderingType.Search,
)
);
);
}
ret.push(
@ -129,8 +129,10 @@ export default class SearchResultTile extends React.Component<IProps> {
}
}
return <li data-scroll-tokens={eventId}>
<ol>{ ret }</ol>
</li>;
return (
<li data-scroll-tokens={eventId}>
<ol>{ret}</ol>
</li>
);
}
}

View file

@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ClipboardEvent, createRef, KeyboardEvent } from 'react';
import EMOJI_REGEX from 'emojibase-regex';
import { IContent, MatrixEvent, IEventRelation } from 'matrix-js-sdk/src/models/event';
import { DebouncedFunc, throttle } from 'lodash';
import React, { ClipboardEvent, createRef, KeyboardEvent } from "react";
import EMOJI_REGEX from "emojibase-regex";
import { IContent, MatrixEvent, IEventRelation } from "matrix-js-sdk/src/models/event";
import { DebouncedFunc, throttle } from "lodash";
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
import { logger } from "matrix-js-sdk/src/logger";
import { Room } from 'matrix-js-sdk/src/models/room';
import { Room } from "matrix-js-sdk/src/models/room";
import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
import dis from '../../../dispatcher/dispatcher';
import EditorModel from '../../../editor/model';
import dis from "../../../dispatcher/dispatcher";
import EditorModel from "../../../editor/model";
import {
containsEmote,
htmlSerializeIfNeeded,
@ -34,37 +34,37 @@ import {
stripPrefix,
textSerialize,
unescapeMessage,
} from '../../../editor/serialize';
} from "../../../editor/serialize";
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
import { CommandPartCreator, Part, PartCreator, SerializedPart } from '../../../editor/parts';
import { findEditableEvent } from '../../../utils/EventUtils';
import { CommandPartCreator, Part, PartCreator, SerializedPart } from "../../../editor/parts";
import { findEditableEvent } from "../../../utils/EventUtils";
import SendHistoryManager from "../../../SendHistoryManager";
import { CommandCategories } from '../../../SlashCommands';
import ContentMessages from '../../../ContentMessages';
import { CommandCategories } from "../../../SlashCommands";
import ContentMessages from "../../../ContentMessages";
import { withMatrixClientHOC, MatrixClientProps } from "../../../contexts/MatrixClientContext";
import { Action } from "../../../dispatcher/actions";
import { containsEmoji } from "../../../effects/utils";
import { CHAT_EFFECTS } from '../../../effects';
import { CHAT_EFFECTS } from "../../../effects";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { getKeyBindingsManager } from '../../../KeyBindingsManager';
import SettingsStore from '../../../settings/SettingsStore';
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import SettingsStore from "../../../settings/SettingsStore";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { ActionPayload } from "../../../dispatcher/payloads";
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import DocumentPosition from "../../../editor/position";
import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } from "../../../editor/commands";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { PosthogAnalytics } from "../../../PosthogAnalytics";
import { addReplyToMessageContent } from '../../../utils/Reply';
import { doMaybeLocalRoomAction } from '../../../utils/local-room';
import { addReplyToMessageContent } from "../../../utils/Reply";
import { doMaybeLocalRoomAction } from "../../../utils/local-room";
// Merges favouring the given relation
export function attachRelation(content: IContent, relation?: IEventRelation): void {
if (relation) {
content['m.relates_to'] = {
...(content['m.relates_to'] || {}),
content["m.relates_to"] = {
...(content["m.relates_to"] || {}),
...relation,
};
}
@ -124,8 +124,7 @@ export function isQuickReaction(model: EditorModel): boolean {
const hasShortcut = text.startsWith("+") || text.startsWith("+ ");
const emojiMatch = text.match(EMOJI_REGEX);
if (hasShortcut && emojiMatch && emojiMatch.length == 1) {
return emojiMatch[0] === text.substring(1) ||
emojiMatch[0] === text.substring(2);
return emojiMatch[0] === text.substring(1) || emojiMatch[0] === text.substring(2);
}
}
return false;
@ -163,9 +162,13 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
this.context = context; // otherwise React will only set it prior to render due to type def above
if (this.props.mxClient.isCryptoEnabled() && this.props.mxClient.isRoomEncrypted(this.props.room.roomId)) {
this.prepareToEncrypt = throttle(() => {
this.props.mxClient.prepareToEncrypt(this.props.room);
}, 60000, { leading: true, trailing: false });
this.prepareToEncrypt = throttle(
() => {
this.props.mxClient.prepareToEncrypt(this.props.room);
},
60000,
{ leading: true, trailing: false },
);
}
window.addEventListener("beforeunload", this.saveStoredEditorState);
@ -174,14 +177,14 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
const parts = this.restoreStoredEditorState(partCreator) || [];
this.model = new EditorModel(parts, partCreator);
this.dispatcherRef = dis.register(this.onAction);
this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_cider_history_');
this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, "mx_cider_history_");
}
public componentDidUpdate(prevProps: ISendMessageComposerProps): void {
const replyingToThread = this.props.relation?.key === THREAD_RELATION_TYPE.name;
const differentEventTarget = this.props.relation?.event_id !== prevProps.relation?.event_id;
const threadChanged = replyingToThread && (differentEventTarget);
const threadChanged = replyingToThread && differentEventTarget;
if (threadChanged) {
const partCreator = new CommandPartCreator(this.props.room, this.props.mxClient);
const parts = this.restoreStoredEditorState(partCreator) || [];
@ -223,9 +226,9 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
case KeyBindingAction.EditPrevMessage:
// selection must be collapsed and caret at start
if (this.editorRef.current?.isSelectionCollapsed() && this.editorRef.current?.isCaretAtStart()) {
const events =
this.context.liveTimeline.getEvents()
.concat(replyingToThread ? [] : this.props.room.getPendingEvents());
const events = this.context.liveTimeline
.getEvents()
.concat(replyingToThread ? [] : this.props.room.getPendingEvents());
const editEvent = findEditableEvent({
events,
isForward: false,
@ -243,7 +246,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
break;
case KeyBindingAction.CancelReplyOrEdit:
dis.dispatch({
action: 'reply_to_event',
action: "reply_to_event",
event: null,
context: this.context.timelineRenderingType,
});
@ -275,7 +278,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
}
const { parts, replyEventId } = this.sendHistoryManager.getItem(delta);
dis.dispatch({
action: 'reply_to_event',
action: "reply_to_event",
event: replyEventId ? this.props.room.findEventById(replyEventId) : null,
context: this.context.timelineRenderingType,
});
@ -295,23 +298,26 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
let shouldReact = true;
const lastMessage = events[i];
const userId = MatrixClientPeg.get().getUserId();
const messageReactions = this.props.room.relations
.getChildEventsForEvent(lastMessage.getId(), RelationType.Annotation, EventType.Reaction);
const messageReactions = this.props.room.relations.getChildEventsForEvent(
lastMessage.getId(),
RelationType.Annotation,
EventType.Reaction,
);
// if we have already sent this reaction, don't redact but don't re-send
if (messageReactions) {
const myReactionEvents = messageReactions.getAnnotationsBySender()[userId] || [];
const myReactionKeys = [...myReactionEvents]
.filter(event => !event.isRedacted())
.map(event => event.getRelation().key);
.filter((event) => !event.isRedacted())
.map((event) => event.getRelation().key);
shouldReact = !myReactionKeys.includes(reaction);
}
if (shouldReact) {
MatrixClientPeg.get().sendEvent(lastMessage.getRoomId(), EventType.Reaction, {
"m.relates_to": {
"rel_type": RelationType.Annotation,
"event_id": lastMessage.getId(),
"key": reaction,
rel_type: RelationType.Annotation,
event_id: lastMessage.getId(),
key: reaction,
},
});
dis.dispatch({ action: "message_sent" });
@ -341,7 +347,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
PosthogAnalytics.instance.trackEvent<ComposerEvent>(posthogEvent);
// Replace emoticon at the end of the message
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
if (SettingsStore.getValue("MessageComposerInput.autoReplaceEmoji")) {
const indexOfLastPart = model.parts.length - 1;
const positionInLastPart = model.parts[indexOfLastPart].text.length;
this.editorRef.current?.replaceEmoticon(
@ -357,9 +363,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
if (!containsEmote(model) && isSlashCommand(this.model)) {
const [cmd, args, commandText] = getSlashCommand(this.model);
if (cmd) {
const threadId = this.props.relation?.rel_type === THREAD_RELATION_TYPE.name
? this.props.relation?.event_id
: null;
const threadId =
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation?.event_id : null;
let commandSuccessful: boolean;
[content, commandSuccessful] = await runSlashCommand(cmd, args, this.props.room.roomId, threadId);
@ -379,7 +384,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
} else {
shouldSend = false;
}
} else if (!await shouldSendAnyway(commandText)) {
} else if (!(await shouldSendAnyway(commandText))) {
// if !sendAnyway bail to let the user edit the composer and try again
return;
}
@ -408,9 +413,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
decorateStartSendingTime(content);
}
const threadId = this.props.relation?.rel_type === THREAD_RELATION_TYPE.name
? this.props.relation.event_id
: null;
const threadId =
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : null;
const prom = doMaybeLocalRoomAction(
roomId,
@ -421,7 +425,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
// Clear reply_to_event as we put the message into the queue
// if the send fails, retry will handle resending.
dis.dispatch({
action: 'reply_to_event',
action: "reply_to_event",
event: null,
context: this.context.timelineRenderingType,
});
@ -438,7 +442,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
}
});
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
prom.then(resp => {
prom.then((resp) => {
sendRoundTripMetric(this.props.mxClient, roomId, resp.event_id);
});
}
@ -486,10 +490,10 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
if (json) {
try {
const { parts: serializedParts, replyEventId } = JSON.parse(json);
const parts: Part[] = serializedParts.map(p => partCreator.deserializePart(p));
const parts: Part[] = serializedParts.map((p) => partCreator.deserializePart(p));
if (replyEventId) {
dis.dispatch({
action: 'reply_to_event',
action: "reply_to_event",
event: this.props.room.findEventById(replyEventId),
context: this.context.timelineRenderingType,
});
@ -521,7 +525,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
if (this.props.disabled) return;
switch (payload.action) {
case 'reply_to_event':
case "reply_to_event":
case Action.FocusSendMessageComposer:
if ((payload.context ?? TimelineRenderingType.Room) === this.context.timelineRenderingType) {
this.editorRef.current?.focus();
@ -569,9 +573,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
};
render() {
const threadId = this.props.relation?.rel_type === THREAD_RELATION_TYPE.name
? this.props.relation.event_id
: null;
const threadId =
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : null;
return (
<div className="mx_SendMessageComposer" onClick={this.focusComposer} onKeyDown={this.onKeyDown}>
<BasicMessageComposer

View file

@ -14,27 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { Room, RoomEvent } from 'matrix-js-sdk/src/models/room';
import React from "react";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import { _t, _td } from '../../../languageHandler';
import AppTile from '../elements/AppTile';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import AccessibleButton from '../elements/AccessibleButton';
import WidgetUtils, { IWidgetEvent } from '../../../utils/WidgetUtils';
import { _t, _td } from "../../../languageHandler";
import AppTile from "../elements/AppTile";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import AccessibleButton from "../elements/AccessibleButton";
import WidgetUtils, { IWidgetEvent } from "../../../utils/WidgetUtils";
import PersistedElement from "../elements/PersistedElement";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import ContextMenu, { ChevronFace } from "../../structures/ContextMenu";
import { WidgetType } from "../../../widgets/WidgetType";
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
import { ActionPayload } from '../../../dispatcher/payloads';
import ScalarAuthClient from '../../../ScalarAuthClient';
import { ActionPayload } from "../../../dispatcher/payloads";
import ScalarAuthClient from "../../../ScalarAuthClient";
import GenericElementContextMenu from "../context_menus/GenericElementContextMenu";
import { IApp } from "../../../stores/WidgetStore";
import RightPanelStore from '../../../stores/right-panel/RightPanelStore';
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
// We sit in a context menu, so this should be given to the context menu.
@ -87,12 +87,15 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
// TODO: Pick the right manager for the widget
if (IntegrationManagers.sharedInstance().hasManager()) {
this.scalarClient = IntegrationManagers.sharedInstance().getPrimaryManager().getScalarClient();
return this.scalarClient.connect().then(() => {
this.forceUpdate();
return this.scalarClient;
}).catch((e) => {
this.imError(_td("Failed to connect to integration manager"), e);
});
return this.scalarClient
.connect()
.then(() => {
this.forceUpdate();
return this.scalarClient;
})
.catch((e) => {
this.imError(_td("Failed to connect to integration manager"), e);
});
} else {
IntegrationManagers.sharedInstance().openNoManagerDialog();
}
@ -100,32 +103,37 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
private removeStickerpickerWidgets = async (): Promise<void> => {
const scalarClient = await this.acquireScalarClient();
logger.log('Removing Stickerpicker widgets');
logger.log("Removing Stickerpicker widgets");
if (this.state.widgetId) {
if (scalarClient) {
scalarClient.disableWidgetAssets(WidgetType.STICKERPICKER, this.state.widgetId).then(() => {
logger.log('Assets disabled');
}).catch(() => {
logger.error('Failed to disable assets');
});
scalarClient
.disableWidgetAssets(WidgetType.STICKERPICKER, this.state.widgetId)
.then(() => {
logger.log("Assets disabled");
})
.catch(() => {
logger.error("Failed to disable assets");
});
} else {
logger.error("Cannot disable assets: no scalar client");
}
} else {
logger.warn('No widget ID specified, not disabling assets');
logger.warn("No widget ID specified, not disabling assets");
}
this.props.setStickerPickerOpen(false);
WidgetUtils.removeStickerpickerWidgets().then(() => {
this.forceUpdate();
}).catch((e) => {
logger.error('Failed to remove sticker picker widget', e);
});
WidgetUtils.removeStickerpickerWidgets()
.then(() => {
this.forceUpdate();
})
.catch((e) => {
logger.error("Failed to remove sticker picker widget", e);
});
};
public componentDidMount(): void {
// Close the sticker picker when the window resizes
window.addEventListener('resize', this.onResize);
window.addEventListener("resize", this.onResize);
this.dispatcherRef = dis.register(this.onAction);
@ -141,7 +149,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
const client = MatrixClientPeg.get();
if (client) client.removeListener(RoomEvent.AccountData, this.updateWidget);
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
window.removeEventListener('resize', this.onResize);
window.removeEventListener("resize", this.onResize);
if (this.dispatcherRef) {
dis.unregister(this.dispatcherRef);
}
@ -211,10 +219,9 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
private defaultStickerpickerContent(): JSX.Element {
return (
<AccessibleButton onClick={this.launchManageIntegrations}
className='mx_Stickers_contentPlaceholder'>
<p>{ _t("You don't currently have any stickerpacks enabled") }</p>
<p className='mx_Stickers_addLink'>{ _t("Add some now") }</p>
<AccessibleButton onClick={this.launchManageIntegrations} className="mx_Stickers_contentPlaceholder">
<p>{_t("You don't currently have any stickerpacks enabled")}</p>
<p className="mx_Stickers_addLink">{_t("Add some now")}</p>
<img src={require("../../../../res/img/stickerpack-placeholder.png")} alt="" />
</AccessibleButton>
);
@ -223,7 +230,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
private errorStickerpickerContent(): JSX.Element {
return (
<div style={{ textAlign: "center" }} className="error">
<p> { this.state.imError } </p>
<p> {this.state.imError} </p>
</div>
);
}
@ -234,7 +241,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
WidgetUtils.calcWidgetUid(this.state.stickerpickerWidget.id, null),
);
if (messaging && visible !== this.prevSentVisibility) {
messaging.updateVisibility(visible).catch(err => {
messaging.updateVisibility(visible).catch((err) => {
logger.error("Error updating widget visibility: ", err);
});
this.prevSentVisibility = visible;
@ -277,12 +284,12 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
};
stickersContent = (
<div className='mx_Stickers_content_container'>
<div className="mx_Stickers_content_container">
<div
id='stickersContent'
className='mx_Stickers_content'
id="stickersContent"
className="mx_Stickers_content"
style={{
border: 'none',
border: "none",
height: this.popoverHeight,
width: this.popoverWidth,
}}
@ -339,28 +346,28 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
*/
private launchManageIntegrations = (): void => {
// noinspection JSIgnoredPromiseFromCall
IntegrationManagers.sharedInstance().getPrimaryManager().open(
this.props.room,
`type_${WidgetType.STICKERPICKER.preferred}`,
this.state.widgetId,
);
IntegrationManagers.sharedInstance()
.getPrimaryManager()
.open(this.props.room, `type_${WidgetType.STICKERPICKER.preferred}`, this.state.widgetId);
};
public render(): JSX.Element {
if (!this.props.isStickerPickerOpen) return null;
return <ContextMenu
chevronFace={ChevronFace.Bottom}
menuWidth={this.popoverWidth}
menuHeight={this.popoverHeight}
onFinished={this.onFinished}
menuPaddingTop={0}
menuPaddingLeft={0}
menuPaddingRight={0}
zIndex={STICKERPICKER_Z_INDEX}
{...this.props.menuPosition}
>
<GenericElementContextMenu element={this.getStickerpickerContent()} onResize={this.onFinished} />
</ContextMenu>;
return (
<ContextMenu
chevronFace={ChevronFace.Bottom}
menuWidth={this.popoverWidth}
menuHeight={this.popoverHeight}
onFinished={this.onFinished}
menuPaddingTop={0}
menuPaddingLeft={0}
menuPaddingRight={0}
zIndex={STICKERPICKER_Z_INDEX}
{...this.props.menuPosition}
>
<GenericElementContextMenu element={this.getStickerpickerContent()} onResize={this.onFinished} />
</ContextMenu>
);
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
@ -28,8 +28,8 @@ import Modal from "../../../Modal";
import { isValid3pidInvite } from "../../../RoomInvite";
import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName";
import ErrorDialog from '../dialogs/ErrorDialog';
import AccessibleButton from '../elements/AccessibleButton';
import ErrorDialog from "../dialogs/ErrorDialog";
import AccessibleButton from "../elements/AccessibleButton";
interface IProps {
event: MatrixEvent;
@ -55,7 +55,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
const powerLevels = this.room.currentState.getStateEvents("m.room.power_levels", "");
let kickLevel = powerLevels ? powerLevels.getContent().kick : 50;
if (typeof(kickLevel) !== 'number') kickLevel = 50;
if (typeof kickLevel !== "number") kickLevel = 50;
const sender = this.room.getMember(this.props.event.getSender());
@ -86,7 +86,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
const isInvited = isValid3pidInvite(ev);
const newState = { invited: isInvited };
if (newDisplayName) newState['displayName'] = newDisplayName;
if (newDisplayName) newState["displayName"] = newDisplayName;
this.setState(newState);
}
};
@ -99,7 +99,8 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
};
onKickClick = () => {
MatrixClientPeg.get().sendStateEvent(this.state.roomId, "m.room.third_party_invite", {}, this.state.stateKey)
MatrixClientPeg.get()
.sendStateEvent(this.state.roomId, "m.room.third_party_invite", {}, this.state.stateKey)
.catch((err) => {
logger.error(err);
@ -110,7 +111,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
title: _t("Failed to revoke invite"),
description: _t(
"Could not revoke the invite. The server may be experiencing a temporary problem or " +
"you do not have sufficient permissions to revoke the invite.",
"you do not have sufficient permissions to revoke the invite.",
),
});
});
@ -124,10 +125,10 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
if (this.state.canKick && this.state.invited) {
adminTools = (
<div className="mx_MemberInfo_container">
<h3>{ _t("Admin Tools") }</h3>
<h3>{_t("Admin Tools")}</h3>
<div className="mx_MemberInfo_buttons">
<AccessibleButton className="mx_MemberInfo_field" onClick={this.onKickClick}>
{ _t("Revoke invite") }
{_t("Revoke invite")}
</AccessibleButton>
</div>
</div>
@ -136,31 +137,30 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
let scopeHeader;
if (this.room.isSpaceRoom()) {
scopeHeader = <div className="mx_RightPanel_scopeHeader">
<RoomAvatar room={this.room} height={32} width={32} />
<RoomName room={this.room} />
</div>;
scopeHeader = (
<div className="mx_RightPanel_scopeHeader">
<RoomAvatar room={this.room} height={32} width={32} />
<RoomName room={this.room} />
</div>
);
}
// We shamelessly rip off the MemberInfo styles here.
return (
<div className="mx_MemberInfo" role="tabpanel">
{ scopeHeader }
{scopeHeader}
<div className="mx_MemberInfo_name">
<AccessibleButton className="mx_MemberInfo_cancel"
onClick={this.onCancel}
title={_t('Close')}
/>
<h2>{ this.state.displayName }</h2>
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel} title={_t("Close")} />
<h2>{this.state.displayName}</h2>
</div>
<div className="mx_MemberInfo_container">
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">
{ _t("Invited by %(sender)s", { sender: this.state.senderName }) }
{_t("Invited by %(sender)s", { sender: this.state.senderName })}
</div>
</div>
</div>
{ adminTools }
{adminTools}
</div>
);
}

View file

@ -62,9 +62,7 @@ const ThreadSummary = ({ mxEvent, thread, ...props }: IProps) => {
}}
aria-label={_t("Open thread")}
>
<span className="mx_ThreadSummary_replies_amount">
{ countSection }
</span>
<span className="mx_ThreadSummary_replies_amount">{countSection}</span>
<ThreadMessagePreview thread={thread} showDisplayname={!roomContext.narrow} />
<div className="mx_ThreadSummary_chevron" />
</AccessibleButton>
@ -99,23 +97,23 @@ export const ThreadMessagePreview = ({ thread, showDisplayname = false }: IPrevi
return null;
}
return <>
<MemberAvatar
member={lastReply.sender}
fallbackUserId={lastReply.getSender()}
width={24}
height={24}
className="mx_ThreadSummary_avatar"
/>
{ showDisplayname && <div className="mx_ThreadSummary_sender">
{ lastReply.sender?.name ?? lastReply.getSender() }
</div> }
<div className="mx_ThreadSummary_content" title={preview}>
<span className="mx_ThreadSummary_message-preview">
{ preview }
</span>
</div>
</>;
return (
<>
<MemberAvatar
member={lastReply.sender}
fallbackUserId={lastReply.getSender()}
width={24}
height={24}
className="mx_ThreadSummary_avatar"
/>
{showDisplayname && (
<div className="mx_ThreadSummary_sender">{lastReply.sender?.name ?? lastReply.getSender()}</div>
)}
<div className="mx_ThreadSummary_content" title={preview}>
<span className="mx_ThreadSummary_message-preview">{preview}</span>
</div>
</>
);
};
export default ThreadSummary;

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
interface IProps {
onScrollUpClick?: (e: React.MouseEvent) => void;
@ -30,12 +30,12 @@ export default class TopUnreadMessagesBar extends React.PureComponent<IProps> {
<div className="mx_TopUnreadMessagesBar">
<AccessibleButton
className="mx_TopUnreadMessagesBar_scrollUp"
title={_t('Jump to first unread message.')}
title={_t("Jump to first unread message.")}
onClick={this.props.onScrollUpClick}
/>
<AccessibleButton
className="mx_TopUnreadMessagesBar_markAsRead"
title={_t('Mark all as read')}
title={_t("Mark all as read")}
onClick={this.props.onCloseClick}
/>
</div>

View file

@ -128,7 +128,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
Math.round(this.state.recorder.durationSeconds * 1000),
this.state.recorder.contentLength,
upload.encrypted,
this.state.recorder.getPlayback().thumbnailWaveform.map(v => Math.round(v * 1024)),
this.state.recorder.getPlayback().thumbnailWaveform.map((v) => Math.round(v * 1024)),
);
attachRelation(content, relation);
@ -140,15 +140,14 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
// Clear reply_to_event as we put the message into the queue
// if the send fails, retry will handle resending.
defaultDispatcher.dispatch({
action: 'reply_to_event',
action: "reply_to_event",
event: null,
context: this.context.timelineRenderingType,
});
}
doMaybeLocalRoomAction(
this.props.room.roomId,
(actualRoomId: string) => MatrixClientPeg.get().sendMessage(actualRoomId, content),
doMaybeLocalRoomAction(this.props.room.roomId, (actualRoomId: string) =>
MatrixClientPeg.get().sendMessage(actualRoomId, content),
);
} catch (e) {
logger.error("Error sending voice message:", e);
@ -181,11 +180,15 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
const accessError = () => {
Modal.createDialog(ErrorDialog, {
title: _t("Unable to access your microphone"),
description: <>
<p>{ _t(
"We were unable to access your microphone. Please check your browser settings and try again.",
) }</p>
</>,
description: (
<>
<p>
{_t(
"We were unable to access your microphone. Please check your browser settings and try again.",
)}
</p>
</>
),
});
};
@ -196,11 +199,15 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
if (!devices?.[MediaDeviceKindEnum.AudioInput]?.length) {
Modal.createDialog(ErrorDialog, {
title: _t("No microphone found"),
description: <>
<p>{ _t(
"We didn't find a microphone on your device. Please check your settings and try again.",
) }</p>
</>,
description: (
<>
<p>
{_t(
"We didn't find a microphone on your device. Please check your settings and try again.",
)}
</p>
</>
),
});
return;
}
@ -247,17 +254,16 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
if (!this.state.recorder) return null; // no recorder means we're not recording: no waveform
if (this.state.recordingPhase !== RecordingState.Started) {
return <RecordingPlayback
playback={this.state.recorder.getPlayback()}
layout={PlaybackLayout.Composer}
/>;
return <RecordingPlayback playback={this.state.recorder.getPlayback()} layout={PlaybackLayout.Composer} />;
}
// only other UI is the recording-in-progress UI
return <div className="mx_MediaBody mx_VoiceMessagePrimaryContainer mx_VoiceRecordComposerTile_recording">
<LiveRecordingClock recorder={this.state.recorder} />
<LiveRecordingWaveform recorder={this.state.recorder} />
</div>;
return (
<div className="mx_MediaBody mx_VoiceMessagePrimaryContainer mx_VoiceRecordComposerTile_recording">
<LiveRecordingClock recorder={this.state.recorder} />
<LiveRecordingWaveform recorder={this.state.recorder} />
</div>
);
}
public render(): ReactNode {
@ -271,46 +277,56 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
tooltip = _t("Stop recording");
}
stopBtn = <AccessibleTooltipButton
className="mx_VoiceRecordComposerTile_stop"
onClick={this.onRecordStartEndClick}
title={tooltip}
/>;
stopBtn = (
<AccessibleTooltipButton
className="mx_VoiceRecordComposerTile_stop"
onClick={this.onRecordStartEndClick}
title={tooltip}
/>
);
if (this.state.recorder && !this.state.recorder?.isRecording) {
stopBtn = null;
}
}
if (this.state.recorder && this.state.recordingPhase !== RecordingState.Uploading) {
deleteButton = <AccessibleTooltipButton
className='mx_VoiceRecordComposerTile_delete'
title={_t("Delete")}
onClick={this.onCancel}
/>;
deleteButton = (
<AccessibleTooltipButton
className="mx_VoiceRecordComposerTile_delete"
title={_t("Delete")}
onClick={this.onCancel}
/>
);
}
let uploadIndicator;
if (this.state.recordingPhase === RecordingState.Uploading) {
uploadIndicator = <span className='mx_VoiceRecordComposerTile_uploadingState'>
<InlineSpinner w={16} h={16} />
</span>;
} else if (this.state.didUploadFail && this.state.recordingPhase === RecordingState.Ended) {
uploadIndicator = <span className='mx_VoiceRecordComposerTile_failedState'>
<span className='mx_VoiceRecordComposerTile_uploadState_badge'>
{ /* Need to stick the badge in a span to ensure it doesn't create a block component */ }
<NotificationBadge
notification={StaticNotificationState.forSymbol("!", NotificationColor.Red)}
/>
uploadIndicator = (
<span className="mx_VoiceRecordComposerTile_uploadingState">
<InlineSpinner w={16} h={16} />
</span>
<span className='text-warning'>{ _t("Failed to send") }</span>
</span>;
);
} else if (this.state.didUploadFail && this.state.recordingPhase === RecordingState.Ended) {
uploadIndicator = (
<span className="mx_VoiceRecordComposerTile_failedState">
<span className="mx_VoiceRecordComposerTile_uploadState_badge">
{/* Need to stick the badge in a span to ensure it doesn't create a block component */}
<NotificationBadge
notification={StaticNotificationState.forSymbol("!", NotificationColor.Red)}
/>
</span>
<span className="text-warning">{_t("Failed to send")}</span>
</span>
);
}
return (<>
{ uploadIndicator }
{ deleteButton }
{ stopBtn }
{ this.renderWaveformArea() }
</>);
return (
<>
{uploadIndicator}
{deleteButton}
{stopBtn}
{this.renderWaveformArea()}
</>
);
}
}

View file

@ -15,16 +15,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { compare } from "matrix-js-sdk/src/utils";
import * as WhoIsTyping from '../../../WhoIsTyping';
import Timer from '../../../utils/Timer';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import MemberAvatar from '../avatars/MemberAvatar';
import * as WhoIsTyping from "../../../WhoIsTyping";
import Timer from "../../../utils/Timer";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import MemberAvatar from "../avatars/MemberAvatar";
interface IProps {
// the room this statusbar is representing.
@ -139,7 +139,9 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
timer.start();
timer.finished().then(
() => this.removeUserTimer(m.userId), // on elapsed
() => {/* aborted */},
() => {
/* aborted */
},
);
}
return delayedStopTypingTimers;
@ -189,7 +191,7 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
if (othersCount > 0) {
avatars.push(
<span className="mx_WhoIsTypingTile_remainingAvatarPlaceholder" key="others">
+{ othersCount }
+{othersCount}
</span>,
);
}
@ -199,8 +201,9 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
render() {
let usersTyping = this.state.usersTyping;
const stoppedUsersOnTimer = Object.keys(this.state.delayedStopTypingTimers)
.map((userId) => this.props.room.getMember(userId));
const stoppedUsersOnTimer = Object.keys(this.state.delayedStopTypingTimers).map((userId) =>
this.props.room.getMember(userId),
);
// append the users that have been reported not typing anymore
// but have a timeout timer running so they can disappear
// when a message comes in
@ -209,10 +212,7 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
// moved to delayedStopTypingTimers
usersTyping.sort((a, b) => compare(a.name, b.name));
const typingString = WhoIsTyping.whoIsTypingString(
usersTyping,
this.props.whoIsTypingLimit,
);
const typingString = WhoIsTyping.whoIsTypingString(usersTyping, this.props.whoIsTypingLimit);
if (!typingString) {
return null;
}
@ -220,11 +220,9 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
return (
<li className="mx_WhoIsTypingTile" aria-atomic="true">
<div className="mx_WhoIsTypingTile_avatars">
{ this.renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
</div>
<div className="mx_WhoIsTypingTile_label">
{ typingString }
{this.renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit)}
</div>
<div className="mx_WhoIsTypingTile_label">{typingString}</div>
</li>
);
}

View file

@ -14,26 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { forwardRef, RefObject } from 'react';
import classNames from 'classnames';
import React, { forwardRef, RefObject } from "react";
import classNames from "classnames";
import EditorStateTransfer from '../../../../utils/EditorStateTransfer';
import { WysiwygComposer } from './components/WysiwygComposer';
import { EditionButtons } from './components/EditionButtons';
import { useWysiwygEditActionHandler } from './hooks/useWysiwygEditActionHandler';
import { useEditing } from './hooks/useEditing';
import { useInitialContent } from './hooks/useInitialContent';
import EditorStateTransfer from "../../../../utils/EditorStateTransfer";
import { WysiwygComposer } from "./components/WysiwygComposer";
import { EditionButtons } from "./components/EditionButtons";
import { useWysiwygEditActionHandler } from "./hooks/useWysiwygEditActionHandler";
import { useEditing } from "./hooks/useEditing";
import { useInitialContent } from "./hooks/useInitialContent";
interface ContentProps {
disabled: boolean;
}
const Content = forwardRef<HTMLElement, ContentProps>(
function Content({ disabled }: ContentProps, forwardRef: RefObject<HTMLElement>) {
useWysiwygEditActionHandler(disabled, forwardRef);
return null;
},
);
const Content = forwardRef<HTMLElement, ContentProps>(function Content(
{ disabled }: ContentProps,
forwardRef: RefObject<HTMLElement>,
) {
useWysiwygEditActionHandler(disabled, forwardRef);
return null;
});
interface EditWysiwygComposerProps {
disabled?: boolean;
@ -48,17 +49,26 @@ export function EditWysiwygComposer({ editorStateTransfer, className, ...props }
const { editMessage, endEditing, onChange, isSaveDisabled } = useEditing(editorStateTransfer, initialContent);
return isReady && <WysiwygComposer
className={classNames("mx_EditWysiwygComposer", className)}
initialContent={initialContent}
onChange={onChange}
onSend={editMessage}
{...props}>
{ (ref) => (
<>
<Content disabled={props.disabled} ref={ref} />
<EditionButtons onCancelClick={endEditing} onSaveClick={editMessage} isSaveDisabled={isSaveDisabled} />
</>)
}
</WysiwygComposer>;
return (
isReady && (
<WysiwygComposer
className={classNames("mx_EditWysiwygComposer", className)}
initialContent={initialContent}
onChange={onChange}
onSend={editMessage}
{...props}
>
{(ref) => (
<>
<Content disabled={props.disabled} ref={ref} />
<EditionButtons
onCancelClick={endEditing}
onSaveClick={editMessage}
isSaveDisabled={isSaveDisabled}
/>
</>
)}
</WysiwygComposer>
)
);
}

View file

@ -14,31 +14,29 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ForwardedRef, forwardRef, MutableRefObject } from 'react';
import React, { ForwardedRef, forwardRef, MutableRefObject } from "react";
import { useWysiwygSendActionHandler } from './hooks/useWysiwygSendActionHandler';
import { WysiwygComposer } from './components/WysiwygComposer';
import { PlainTextComposer } from './components/PlainTextComposer';
import { ComposerFunctions } from './types';
import { E2EStatus } from '../../../../utils/ShieldUtils';
import E2EIcon from '../E2EIcon';
import { AboveLeftOf } from '../../../structures/ContextMenu';
import { Emoji } from './components/Emoji';
import { useWysiwygSendActionHandler } from "./hooks/useWysiwygSendActionHandler";
import { WysiwygComposer } from "./components/WysiwygComposer";
import { PlainTextComposer } from "./components/PlainTextComposer";
import { ComposerFunctions } from "./types";
import { E2EStatus } from "../../../../utils/ShieldUtils";
import E2EIcon from "../E2EIcon";
import { AboveLeftOf } from "../../../structures/ContextMenu";
import { Emoji } from "./components/Emoji";
interface ContentProps {
disabled?: boolean;
composerFunctions: ComposerFunctions;
}
const Content = forwardRef<HTMLElement, ContentProps>(
function Content(
{ disabled = false, composerFunctions }: ContentProps,
forwardRef: ForwardedRef<HTMLElement>,
) {
useWysiwygSendActionHandler(disabled, forwardRef as MutableRefObject<HTMLElement>, composerFunctions);
return null;
},
);
const Content = forwardRef<HTMLElement, ContentProps>(function Content(
{ disabled = false, composerFunctions }: ContentProps,
forwardRef: ForwardedRef<HTMLElement>,
) {
useWysiwygSendActionHandler(disabled, forwardRef as MutableRefObject<HTMLElement>, composerFunctions);
return null;
});
interface SendWysiwygComposerProps {
initialContent?: string;
@ -51,19 +49,26 @@ interface SendWysiwygComposerProps {
menuPosition: AboveLeftOf;
}
export function SendWysiwygComposer(
{ isRichTextEnabled, e2eStatus, menuPosition, ...props }: SendWysiwygComposerProps) {
export function SendWysiwygComposer({
isRichTextEnabled,
e2eStatus,
menuPosition,
...props
}: SendWysiwygComposerProps) {
const Composer = isRichTextEnabled ? WysiwygComposer : PlainTextComposer;
return <Composer
className="mx_SendWysiwygComposer"
leftComponent={e2eStatus && <E2EIcon status={e2eStatus} />}
rightComponent={(selectPreviousSelection) =>
<Emoji menuPosition={menuPosition} selectPreviousSelection={selectPreviousSelection} />}
{...props}
>
{ (ref, composerFunctions) => (
<Content disabled={props.disabled} ref={ref} composerFunctions={composerFunctions} />
) }
</Composer>;
return (
<Composer
className="mx_SendWysiwygComposer"
leftComponent={e2eStatus && <E2EIcon status={e2eStatus} />}
rightComponent={(selectPreviousSelection) => (
<Emoji menuPosition={menuPosition} selectPreviousSelection={selectPreviousSelection} />
)}
{...props}
>
{(ref, composerFunctions) => (
<Content disabled={props.disabled} ref={ref} composerFunctions={composerFunctions} />
)}
</Composer>
);
}

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { MouseEventHandler } from 'react';
import React, { MouseEventHandler } from "react";
import { _t } from '../../../../../languageHandler';
import AccessibleButton from '../../../elements/AccessibleButton';
import { _t } from "../../../../../languageHandler";
import AccessibleButton from "../../../elements/AccessibleButton";
interface EditionButtonsProps {
onCancelClick: MouseEventHandler<HTMLButtonElement>;
@ -26,12 +26,14 @@ interface EditionButtonsProps {
}
export function EditionButtons({ onCancelClick, onSaveClick, isSaveDisabled = false }: EditionButtonsProps) {
return <div className="mx_EditWysiwygComposer_buttons">
<AccessibleButton kind="secondary" onClick={onCancelClick}>
{ _t("Cancel") }
</AccessibleButton>
<AccessibleButton kind="primary" onClick={onSaveClick} disabled={isSaveDisabled}>
{ _t("Save") }
</AccessibleButton>
</div>;
return (
<div className="mx_EditWysiwygComposer_buttons">
<AccessibleButton kind="secondary" onClick={onCancelClick}>
{_t("Cancel")}
</AccessibleButton>
<AccessibleButton kind="primary" onClick={onSaveClick} disabled={isSaveDisabled}>
{_t("Save")}
</AccessibleButton>
</div>
);
}

View file

@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import classNames from 'classnames';
import React, { CSSProperties, forwardRef, memo, MutableRefObject, ReactNode } from 'react';
import classNames from "classnames";
import React, { CSSProperties, forwardRef, memo, MutableRefObject, ReactNode } from "react";
import { useIsExpanded } from '../hooks/useIsExpanded';
import { useSelection } from '../hooks/useSelection';
import { useIsExpanded } from "../hooks/useIsExpanded";
import { useSelection } from "../hooks/useSelection";
const HEIGHT_BREAKING_POINT = 20;
@ -30,40 +30,41 @@ interface EditorProps {
}
export const Editor = memo(
forwardRef<HTMLDivElement, EditorProps>(
function Editor({ disabled, placeholder, leftComponent, rightComponent }: EditorProps, ref,
) {
const isExpanded = useIsExpanded(ref as MutableRefObject<HTMLDivElement | null>, HEIGHT_BREAKING_POINT);
const { onFocus, onBlur, selectPreviousSelection, onInput } = useSelection();
forwardRef<HTMLDivElement, EditorProps>(function Editor(
{ disabled, placeholder, leftComponent, rightComponent }: EditorProps,
ref,
) {
const isExpanded = useIsExpanded(ref as MutableRefObject<HTMLDivElement | null>, HEIGHT_BREAKING_POINT);
const { onFocus, onBlur, selectPreviousSelection, onInput } = useSelection();
return <div
return (
<div
data-testid="WysiwygComposerEditor"
className="mx_WysiwygComposer_Editor"
data-is-expanded={isExpanded}
>
{ leftComponent }
{leftComponent}
<div className="mx_WysiwygComposer_Editor_container">
<div className={classNames("mx_WysiwygComposer_Editor_content",
{
"mx_WysiwygComposer_Editor_content_placeholder": Boolean(placeholder),
},
)}
style={{ "--placeholder": `"${placeholder}"` } as CSSProperties}
ref={ref}
contentEditable={!disabled}
role="textbox"
aria-multiline="true"
aria-autocomplete="list"
aria-haspopup="listbox"
dir="auto"
aria-disabled={disabled}
onFocus={onFocus}
onBlur={onBlur}
onInput={onInput}
<div
className={classNames("mx_WysiwygComposer_Editor_content", {
mx_WysiwygComposer_Editor_content_placeholder: Boolean(placeholder),
})}
style={{ "--placeholder": `"${placeholder}"` } as CSSProperties}
ref={ref}
contentEditable={!disabled}
role="textbox"
aria-multiline="true"
aria-autocomplete="list"
aria-haspopup="listbox"
dir="auto"
aria-disabled={disabled}
onFocus={onFocus}
onBlur={onBlur}
onInput={onInput}
/>
</div>
{ rightComponent?.(selectPreviousSelection) }
</div>;
},
),
{rightComponent?.(selectPreviousSelection)}
</div>
);
}),
);

View file

@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { AboveLeftOf } from "../../../../structures/ContextMenu";
import { EmojiButton } from "../../EmojiButton";
import dis from '../../../../../dispatcher/dispatcher';
import dis from "../../../../../dispatcher/dispatcher";
import { ComposerInsertPayload } from "../../../../../dispatcher/payloads/ComposerInsertPayload";
import { Action } from "../../../../../dispatcher/actions";
import { useRoomContext } from "../../../../../contexts/RoomContext";
@ -31,15 +31,18 @@ interface EmojiProps {
export function Emoji({ selectPreviousSelection, menuPosition }: EmojiProps) {
const roomContext = useRoomContext();
return <EmojiButton menuPosition={menuPosition}
addEmoji={(emoji) => {
selectPreviousSelection();
dis.dispatch<ComposerInsertPayload>({
action: Action.ComposerInsert,
text: emoji,
timelineRenderingType: roomContext.timelineRenderingType,
});
return true;
}}
/>;
return (
<EmojiButton
menuPosition={menuPosition}
addEmoji={(emoji) => {
selectPreviousSelection();
dis.dispatch<ComposerInsertPayload>({
action: Action.ComposerInsert,
text: emoji,
timelineRenderingType: roomContext.timelineRenderingType,
});
return true;
}}
/>
);
}

View file

@ -18,11 +18,11 @@ import React, { MouseEventHandler, ReactNode } from "react";
import { FormattingFunctions, AllActionStates } from "@matrix-org/matrix-wysiwyg";
import classNames from "classnames";
import { Icon as BoldIcon } from '../../../../../../res/img/element-icons/room/composer/bold.svg';
import { Icon as ItalicIcon } from '../../../../../../res/img/element-icons/room/composer/italic.svg';
import { Icon as UnderlineIcon } from '../../../../../../res/img/element-icons/room/composer/underline.svg';
import { Icon as StrikeThroughIcon } from '../../../../../../res/img/element-icons/room/composer/strikethrough.svg';
import { Icon as InlineCodeIcon } from '../../../../../../res/img/element-icons/room/composer/inline_code.svg';
import { Icon as BoldIcon } from "../../../../../../res/img/element-icons/room/composer/bold.svg";
import { Icon as ItalicIcon } from "../../../../../../res/img/element-icons/room/composer/italic.svg";
import { Icon as UnderlineIcon } from "../../../../../../res/img/element-icons/room/composer/underline.svg";
import { Icon as StrikeThroughIcon } from "../../../../../../res/img/element-icons/room/composer/strikethrough.svg";
import { Icon as InlineCodeIcon } from "../../../../../../res/img/element-icons/room/composer/inline_code.svg";
import AccessibleTooltipButton from "../../../elements/AccessibleTooltipButton";
import { Alignment } from "../../../elements/Tooltip";
import { KeyboardShortcut } from "../../../settings/KeyboardShortcut";
@ -36,10 +36,14 @@ interface TooltipProps {
}
function Tooltip({ label, keyCombo }: TooltipProps) {
return <div className="mx_FormattingButtons_Tooltip">
{ label }
{ keyCombo && <KeyboardShortcut value={keyCombo} className="mx_FormattingButtons_Tooltip_KeyboardShortcut" /> }
</div>;
return (
<div className="mx_FormattingButtons_Tooltip">
{label}
{keyCombo && (
<KeyboardShortcut value={keyCombo} className="mx_FormattingButtons_Tooltip_KeyboardShortcut" />
)}
</div>
);
}
interface ButtonProps extends TooltipProps {
@ -49,20 +53,21 @@ interface ButtonProps extends TooltipProps {
}
function Button({ label, keyCombo, onClick, isActive, icon }: ButtonProps) {
return <AccessibleTooltipButton
element="button"
onClick={onClick as (e: ButtonEvent) => void}
title={label}
className={
classNames('mx_FormattingButtons_Button', {
'mx_FormattingButtons_active': isActive,
'mx_FormattingButtons_Button_hover': !isActive,
return (
<AccessibleTooltipButton
element="button"
onClick={onClick as (e: ButtonEvent) => void}
title={label}
className={classNames("mx_FormattingButtons_Button", {
mx_FormattingButtons_active: isActive,
mx_FormattingButtons_Button_hover: !isActive,
})}
tooltip={keyCombo && <Tooltip label={label} keyCombo={keyCombo} />}
alignment={Alignment.Top}
>
{ icon }
</AccessibleTooltipButton>;
tooltip={keyCombo && <Tooltip label={label} keyCombo={keyCombo} />}
alignment={Alignment.Top}
>
{icon}
</AccessibleTooltipButton>
);
}
interface FormattingButtonsProps {
@ -71,11 +76,42 @@ interface FormattingButtonsProps {
}
export function FormattingButtons({ composer, actionStates }: FormattingButtonsProps) {
return <div className="mx_FormattingButtons">
<Button isActive={actionStates.bold === 'reversed'} label={_td("Bold")} keyCombo={{ ctrlOrCmdKey: true, key: 'b' }} onClick={() => composer.bold()} icon={<BoldIcon className="mx_FormattingButtons_Icon" />} />
<Button isActive={actionStates.italic === 'reversed'} label={_td('Italic')} keyCombo={{ ctrlOrCmdKey: true, key: 'i' }} onClick={() => composer.italic()} icon={<ItalicIcon className="mx_FormattingButtons_Icon" />} />
<Button isActive={actionStates.underline === 'reversed'} label={_td('Underline')} keyCombo={{ ctrlOrCmdKey: true, key: 'u' }} onClick={() => composer.underline()} icon={<UnderlineIcon className="mx_FormattingButtons_Icon" />} />
<Button isActive={actionStates.strikeThrough === 'reversed'} label={_td('Strikethrough')} onClick={() => composer.strikeThrough()} icon={<StrikeThroughIcon className="mx_FormattingButtons_Icon" />} />
<Button isActive={actionStates.inlineCode === 'reversed'} label={_td('Code')} keyCombo={{ ctrlOrCmdKey: true, key: 'e' }} onClick={() => composer.inlineCode()} icon={<InlineCodeIcon className="mx_FormattingButtons_Icon" />} />
</div>;
return (
<div className="mx_FormattingButtons">
<Button
isActive={actionStates.bold === "reversed"}
label={_td("Bold")}
keyCombo={{ ctrlOrCmdKey: true, key: "b" }}
onClick={() => composer.bold()}
icon={<BoldIcon className="mx_FormattingButtons_Icon" />}
/>
<Button
isActive={actionStates.italic === "reversed"}
label={_td("Italic")}
keyCombo={{ ctrlOrCmdKey: true, key: "i" }}
onClick={() => composer.italic()}
icon={<ItalicIcon className="mx_FormattingButtons_Icon" />}
/>
<Button
isActive={actionStates.underline === "reversed"}
label={_td("Underline")}
keyCombo={{ ctrlOrCmdKey: true, key: "u" }}
onClick={() => composer.underline()}
icon={<UnderlineIcon className="mx_FormattingButtons_Icon" />}
/>
<Button
isActive={actionStates.strikeThrough === "reversed"}
label={_td("Strikethrough")}
onClick={() => composer.strikeThrough()}
icon={<StrikeThroughIcon className="mx_FormattingButtons_Icon" />}
/>
<Button
isActive={actionStates.inlineCode === "reversed"}
label={_td("Code")}
keyCombo={{ ctrlOrCmdKey: true, key: "e" }}
onClick={() => composer.inlineCode()}
icon={<InlineCodeIcon className="mx_FormattingButtons_Icon" />}
/>
</div>
);
}

View file

@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import classNames from 'classnames';
import React, { MutableRefObject, ReactNode } from 'react';
import classNames from "classnames";
import React, { MutableRefObject, ReactNode } from "react";
import { useComposerFunctions } from '../hooks/useComposerFunctions';
import { useIsFocused } from '../hooks/useIsFocused';
import { usePlainTextInitialization } from '../hooks/usePlainTextInitialization';
import { usePlainTextListeners } from '../hooks/usePlainTextListeners';
import { useSetCursorPosition } from '../hooks/useSetCursorPosition';
import { ComposerFunctions } from '../types';
import { useComposerFunctions } from "../hooks/useComposerFunctions";
import { useIsFocused } from "../hooks/useIsFocused";
import { usePlainTextInitialization } from "../hooks/usePlainTextInitialization";
import { usePlainTextListeners } from "../hooks/usePlainTextListeners";
import { useSetCursorPosition } from "../hooks/useSetCursorPosition";
import { ComposerFunctions } from "../types";
import { Editor } from "./Editor";
interface PlainTextComposerProps {
@ -33,13 +33,8 @@ interface PlainTextComposerProps {
initialContent?: string;
className?: string;
leftComponent?: ReactNode;
rightComponent?: (
selectPreviousSelection: () => void
) => ReactNode;
children?: (
ref: MutableRefObject<HTMLDivElement | null>,
composerFunctions: ComposerFunctions,
) => ReactNode;
rightComponent?: (selectPreviousSelection: () => void) => ReactNode;
children?: (ref: MutableRefObject<HTMLDivElement | null>, composerFunctions: ComposerFunctions) => ReactNode;
}
export function PlainTextComposer({
@ -52,26 +47,36 @@ export function PlainTextComposer({
initialContent,
leftComponent,
rightComponent,
}: PlainTextComposerProps,
) {
const { ref, onInput, onPaste, onKeyDown, content, setContent } =
usePlainTextListeners(initialContent, onChange, onSend);
}: PlainTextComposerProps) {
const { ref, onInput, onPaste, onKeyDown, content, setContent } = usePlainTextListeners(
initialContent,
onChange,
onSend,
);
const composerFunctions = useComposerFunctions(ref, setContent);
usePlainTextInitialization(initialContent, ref);
useSetCursorPosition(disabled, ref);
const { isFocused, onFocus } = useIsFocused();
const computedPlaceholder = !content && placeholder || undefined;
const computedPlaceholder = (!content && placeholder) || undefined;
return <div
data-testid="PlainTextComposer"
className={classNames(className, { [`${className}-focused`]: isFocused })}
onFocus={onFocus}
onBlur={onFocus}
onInput={onInput}
onPaste={onPaste}
onKeyDown={onKeyDown}
>
<Editor ref={ref} disabled={disabled} leftComponent={leftComponent} rightComponent={rightComponent} placeholder={computedPlaceholder} />
{ children?.(ref, composerFunctions) }
</div>;
return (
<div
data-testid="PlainTextComposer"
className={classNames(className, { [`${className}-focused`]: isFocused })}
onFocus={onFocus}
onBlur={onFocus}
onInput={onInput}
onPaste={onPaste}
onKeyDown={onKeyDown}
>
<Editor
ref={ref}
disabled={disabled}
leftComponent={leftComponent}
rightComponent={rightComponent}
placeholder={computedPlaceholder}
/>
{children?.(ref, composerFunctions)}
</div>
);
}

View file

@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { memo, MutableRefObject, ReactNode, useEffect } from 'react';
import React, { memo, MutableRefObject, ReactNode, useEffect } from "react";
import { useWysiwyg, FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
import classNames from 'classnames';
import classNames from "classnames";
import { FormattingButtons } from './FormattingButtons';
import { Editor } from './Editor';
import { useInputEventProcessor } from '../hooks/useInputEventProcessor';
import { useSetCursorPosition } from '../hooks/useSetCursorPosition';
import { useIsFocused } from '../hooks/useIsFocused';
import { FormattingButtons } from "./FormattingButtons";
import { Editor } from "./Editor";
import { useInputEventProcessor } from "../hooks/useInputEventProcessor";
import { useSetCursorPosition } from "../hooks/useSetCursorPosition";
import { useIsFocused } from "../hooks/useIsFocused";
interface WysiwygComposerProps {
disabled?: boolean;
@ -32,32 +32,24 @@ interface WysiwygComposerProps {
initialContent?: string;
className?: string;
leftComponent?: ReactNode;
rightComponent?: (
selectPreviousSelection: () => void
) => ReactNode;
children?: (
ref: MutableRefObject<HTMLDivElement | null>,
wysiwyg: FormattingFunctions,
) => ReactNode;
rightComponent?: (selectPreviousSelection: () => void) => ReactNode;
children?: (ref: MutableRefObject<HTMLDivElement | null>, wysiwyg: FormattingFunctions) => ReactNode;
}
export const WysiwygComposer = memo(function WysiwygComposer(
{
disabled = false,
onChange,
onSend,
placeholder,
initialContent,
className,
leftComponent,
rightComponent,
children,
}: WysiwygComposerProps,
) {
export const WysiwygComposer = memo(function WysiwygComposer({
disabled = false,
onChange,
onSend,
placeholder,
initialContent,
className,
leftComponent,
rightComponent,
children,
}: WysiwygComposerProps) {
const inputEventProcessor = useInputEventProcessor(onSend);
const { ref, isWysiwygReady, content, actionStates, wysiwyg } =
useWysiwyg({ initialContent, inputEventProcessor });
const { ref, isWysiwygReady, content, actionStates, wysiwyg } = useWysiwyg({ initialContent, inputEventProcessor });
useEffect(() => {
if (!disabled && content !== null) {
@ -69,13 +61,24 @@ export const WysiwygComposer = memo(function WysiwygComposer(
useSetCursorPosition(!isReady, ref);
const { isFocused, onFocus } = useIsFocused();
const computedPlaceholder = !content && placeholder || undefined;
const computedPlaceholder = (!content && placeholder) || undefined;
return (
<div data-testid="WysiwygComposer" className={classNames(className, { [`${className}-focused`]: isFocused })} onFocus={onFocus} onBlur={onFocus}>
<div
data-testid="WysiwygComposer"
className={classNames(className, { [`${className}-focused`]: isFocused })}
onFocus={onFocus}
onBlur={onFocus}
>
<FormattingButtons composer={wysiwyg} actionStates={actionStates} />
<Editor ref={ref} disabled={!isReady} leftComponent={leftComponent} rightComponent={rightComponent} placeholder={computedPlaceholder} />
{ children?.(ref, wysiwyg) }
<Editor
ref={ref}
disabled={!isReady}
leftComponent={leftComponent}
rightComponent={rightComponent}
placeholder={computedPlaceholder}
/>
{children?.(ref, wysiwyg)}
</div>
);
});

View file

@ -19,27 +19,30 @@ import { RefObject, useMemo } from "react";
import { setSelection } from "../utils/selection";
export function useComposerFunctions(ref: RefObject<HTMLDivElement>, setContent: (content: string) => void) {
return useMemo(() => ({
clear: () => {
if (ref.current) {
ref.current.innerHTML = '';
}
},
insertText: (text: string) => {
const selection = document.getSelection();
return useMemo(
() => ({
clear: () => {
if (ref.current) {
ref.current.innerHTML = "";
}
},
insertText: (text: string) => {
const selection = document.getSelection();
if (ref.current && selection) {
const content = ref.current.innerHTML;
const { anchorOffset, focusOffset } = selection;
ref.current.innerHTML = `${content.slice(0, anchorOffset)}${text}${content.slice(focusOffset)}`;
setSelection({
anchorNode: ref.current.firstChild,
anchorOffset: anchorOffset + text.length,
focusNode: ref.current.firstChild,
focusOffset: focusOffset + text.length,
});
setContent(ref.current.innerHTML);
}
},
}), [ref, setContent]);
if (ref.current && selection) {
const content = ref.current.innerHTML;
const { anchorOffset, focusOffset } = selection;
ref.current.innerHTML = `${content.slice(0, anchorOffset)}${text}${content.slice(focusOffset)}`;
setSelection({
anchorNode: ref.current.firstChild,
anchorOffset: anchorOffset + text.length,
focusNode: ref.current.firstChild,
focusOffset: focusOffset + text.length,
});
setContent(ref.current.innerHTML);
}
},
}),
[ref, setContent],
);
}

View file

@ -28,14 +28,17 @@ export function useEditing(editorStateTransfer: EditorStateTransfer, initialCont
const [isSaveDisabled, setIsSaveDisabled] = useState(true);
const [content, setContent] = useState(initialContent);
const onChange = useCallback((_content: string) => {
setContent(_content);
setIsSaveDisabled(_isSaveDisabled => _isSaveDisabled && _content === initialContent);
}, [initialContent]);
const onChange = useCallback(
(_content: string) => {
setContent(_content);
setIsSaveDisabled((_isSaveDisabled) => _isSaveDisabled && _content === initialContent);
},
[initialContent],
);
const editMessageMemoized = useCallback(() =>
content !== undefined && editMessage(content, { roomContext, mxClient, editorStateTransfer }),
[content, roomContext, mxClient, editorStateTransfer],
const editMessageMemoized = useCallback(
() => content !== undefined && editMessage(content, { roomContext, mxClient, editorStateTransfer }),
[content, roomContext, mxClient, editorStateTransfer],
);
const endEditingMemoized = useCallback(() => endEditing(roomContext), [roomContext]);

View file

@ -25,7 +25,12 @@ import SettingsStore from "../../../../../settings/SettingsStore";
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
function getFormattedContent(editorStateTransfer: EditorStateTransfer): string {
return editorStateTransfer.getEvent().getContent().formatted_body?.replace(/<mx-reply>.*<\/mx-reply>/, '') || '';
return (
editorStateTransfer
.getEvent()
.getContent()
.formatted_body?.replace(/<mx-reply>.*<\/mx-reply>/, "") || ""
);
}
function parseEditorStateTransfer(
@ -39,13 +44,13 @@ function parseEditorStateTransfer(
if (editorStateTransfer.hasEditorState()) {
// if restoring state from a previous editor,
// restore serialized parts from the state
parts = editorStateTransfer.getSerializedParts().map(p => partCreator.deserializePart(p));
parts = editorStateTransfer.getSerializedParts().map((p) => partCreator.deserializePart(p));
} else {
// otherwise, either restore serialized parts from localStorage or parse the body of the event
// TODO local storage
// const restoredParts = this.restoreStoredEditorState(partCreator);
if (editorStateTransfer.getEvent().getContent().format === 'org.matrix.custom.html') {
if (editorStateTransfer.getEvent().getContent().format === "org.matrix.custom.html") {
return getFormattedContent(editorStateTransfer);
}
@ -54,7 +59,7 @@ function parseEditorStateTransfer(
});
}
return parts.reduce((content, part) => content + part.text, '');
return parts.reduce((content, part) => content + part.text, "");
// Todo local storage
// this.saveStoredEditorState();
}

View file

@ -21,20 +21,19 @@ import { useSettingValue } from "../../../../../hooks/useSettings";
export function useInputEventProcessor(onSend: () => void) {
const isCtrlEnter = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
return useCallback((event: WysiwygInputEvent) => {
if (event instanceof ClipboardEvent) {
return useCallback(
(event: WysiwygInputEvent) => {
if (event instanceof ClipboardEvent) {
return event;
}
if ((event.inputType === "insertParagraph" && !isCtrlEnter) || event.inputType === "sendMessage") {
onSend();
return null;
}
return event;
}
if (
(event.inputType === 'insertParagraph' && !isCtrlEnter) ||
event.inputType === 'sendMessage'
) {
onSend();
return null;
}
return event;
}
, [isCtrlEnter, onSend]);
},
[isCtrlEnter, onSend],
);
}

View file

@ -21,7 +21,7 @@ export function useIsExpanded(ref: MutableRefObject<HTMLElement | null>, breakin
useEffect(() => {
if (ref.current) {
const editor = ref.current;
const resizeObserver = new ResizeObserver(entries => {
const resizeObserver = new ResizeObserver((entries) => {
requestAnimationFrame(() => {
const height = entries[0]?.contentBoxSize?.[0].blockSize;
setIsExpanded(height >= breakingPoint);

View file

@ -21,16 +21,19 @@ export function useIsFocused() {
const timeoutIDRef = useRef<number>();
useEffect(() => () => clearTimeout(timeoutIDRef.current), [timeoutIDRef]);
const onFocus = useCallback((event: FocusEvent<HTMLElement>) => {
clearTimeout(timeoutIDRef.current);
if (event.type === 'focus') {
setIsFocused(true);
} else {
// To avoid a blink when we switch mode between plain text and rich text mode
// We delay the unfocused action
timeoutIDRef.current = window.setTimeout(() => setIsFocused(false), 100);
}
}, [setIsFocused, timeoutIDRef]);
const onFocus = useCallback(
(event: FocusEvent<HTMLElement>) => {
clearTimeout(timeoutIDRef.current);
if (event.type === "focus") {
setIsFocused(true);
} else {
// To avoid a blink when we switch mode between plain text and rich text mode
// We delay the unfocused action
timeoutIDRef.current = window.setTimeout(() => setIsFocused(false), 100);
}
},
[setIsFocused, timeoutIDRef],
);
return { isFocused, onFocus };
}

View file

@ -16,7 +16,7 @@ limitations under the License.
import { RefObject, useEffect } from "react";
export function usePlainTextInitialization(initialContent = '', ref: RefObject<HTMLElement>) {
export function usePlainTextInitialization(initialContent = "", ref: RefObject<HTMLElement>) {
useEffect(() => {
if (ref.current) {
ref.current.innerText = initialContent;

View file

@ -29,32 +29,41 @@ export function usePlainTextListeners(
) {
const ref = useRef<HTMLDivElement | null>(null);
const [content, setContent] = useState<string | undefined>(initialContent);
const send = useCallback((() => {
const send = useCallback(() => {
if (ref.current) {
ref.current.innerHTML = '';
ref.current.innerHTML = "";
}
onSend?.();
}), [ref, onSend]);
}, [ref, onSend]);
const setText = useCallback((text: string) => {
setContent(text);
onChange?.(text);
}, [onChange]);
const setText = useCallback(
(text: string) => {
setContent(text);
onChange?.(text);
},
[onChange],
);
const onInput = useCallback((event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
if (isDivElement(event.target)) {
setText(event.target.innerHTML);
}
}, [setText]);
const onInput = useCallback(
(event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
if (isDivElement(event.target)) {
setText(event.target.innerHTML);
}
},
[setText],
);
const isCtrlEnter = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
const onKeyDown = useCallback((event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' && !event.shiftKey && (!isCtrlEnter || (isCtrlEnter && event.ctrlKey))) {
event.preventDefault();
event.stopPropagation();
send();
}
}, [isCtrlEnter, send]);
const onKeyDown = useCallback(
(event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === "Enter" && !event.shiftKey && (!isCtrlEnter || (isCtrlEnter && event.ctrlKey))) {
event.preventDefault();
event.stopPropagation();
send();
}
},
[isCtrlEnter, send],
);
return { ref, onInput, onPaste: onInput, onKeyDown, content, setContent: setText };
}

View file

@ -19,7 +19,7 @@ import { MutableRefObject, useCallback, useEffect, useRef } from "react";
import useFocus from "../../../../../hooks/useFocus";
import { setSelection } from "../utils/selection";
type SubSelection = Pick<Selection, 'anchorNode' | 'anchorOffset' | 'focusNode' | 'focusOffset'>;
type SubSelection = Pick<Selection, "anchorNode" | "anchorOffset" | "focusNode" | "focusOffset">;
function setSelectionRef(selectionRef: MutableRefObject<SubSelection>) {
const selection = document.getSelection();
@ -49,10 +49,10 @@ export function useSelection() {
}
if (isFocused) {
document.addEventListener('selectionchange', onSelectionChange);
document.addEventListener("selectionchange", onSelectionChange);
}
return () => document.removeEventListener('selectionchange', onSelectionChange);
return () => document.removeEventListener("selectionchange", onSelectionChange);
}, [isFocused]);
const onInput = useCallback(() => {

View file

@ -23,26 +23,26 @@ import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/R
import { useDispatcher } from "../../../../../hooks/useDispatcher";
import { focusComposer } from "./utils";
export function useWysiwygEditActionHandler(
disabled: boolean,
composerElement: RefObject<HTMLElement>,
) {
export function useWysiwygEditActionHandler(disabled: boolean, composerElement: RefObject<HTMLElement>) {
const roomContext = useRoomContext();
const timeoutId = useRef<number | null>(null);
const handler = useCallback((payload: ActionPayload) => {
// don't let the user into the composer if it is disabled - all of these branches lead
// to the cursor being in the composer
if (disabled || !composerElement.current) return;
const handler = useCallback(
(payload: ActionPayload) => {
// don't let the user into the composer if it is disabled - all of these branches lead
// to the cursor being in the composer
if (disabled || !composerElement.current) return;
const context = payload.context ?? TimelineRenderingType.Room;
const context = payload.context ?? TimelineRenderingType.Room;
switch (payload.action) {
case Action.FocusEditMessageComposer:
focusComposer(composerElement, context, roomContext, timeoutId);
break;
}
}, [disabled, composerElement, timeoutId, roomContext]);
switch (payload.action) {
case Action.FocusEditMessageComposer:
focusComposer(composerElement, context, roomContext, timeoutId);
break;
}
},
[disabled, composerElement, timeoutId, roomContext],
);
useDispatcher(defaultDispatcher, handler);
}

View file

@ -33,36 +33,39 @@ export function useWysiwygSendActionHandler(
const roomContext = useRoomContext();
const timeoutId = useRef<number | null>(null);
const handler = useCallback((payload: ActionPayload) => {
// don't let the user into the composer if it is disabled - all of these branches lead
// to the cursor being in the composer
if (disabled || !composerElement?.current) return;
const handler = useCallback(
(payload: ActionPayload) => {
// don't let the user into the composer if it is disabled - all of these branches lead
// to the cursor being in the composer
if (disabled || !composerElement?.current) return;
const context = payload.context ?? TimelineRenderingType.Room;
const context = payload.context ?? TimelineRenderingType.Room;
switch (payload.action) {
case "reply_to_event":
case Action.FocusSendMessageComposer:
focusComposer(composerElement, context, roomContext, timeoutId);
break;
case Action.ClearAndFocusSendMessageComposer:
composerFunctions.clear();
focusComposer(composerElement, context, roomContext, timeoutId);
break;
case Action.ComposerInsert:
if (payload.timelineRenderingType !== roomContext.timelineRenderingType) break;
if (payload.composerType !== ComposerType.Send) break;
switch (payload.action) {
case "reply_to_event":
case Action.FocusSendMessageComposer:
focusComposer(composerElement, context, roomContext, timeoutId);
break;
case Action.ClearAndFocusSendMessageComposer:
composerFunctions.clear();
focusComposer(composerElement, context, roomContext, timeoutId);
break;
case Action.ComposerInsert:
if (payload.timelineRenderingType !== roomContext.timelineRenderingType) break;
if (payload.composerType !== ComposerType.Send) break;
if (payload.userId) {
// TODO insert mention - see SendMessageComposer
} else if (payload.event) {
// TODO insert quote message - see SendMessageComposer
} else if (payload.text) {
composerFunctions.insertText(payload.text);
}
break;
}
}, [disabled, composerElement, composerFunctions, timeoutId, roomContext]);
if (payload.userId) {
// TODO insert mention - see SendMessageComposer
} else if (payload.event) {
// TODO insert quote message - see SendMessageComposer
} else if (payload.text) {
composerFunctions.insertText(payload.text);
}
break;
}
},
[disabled, composerElement, composerFunctions, timeoutId, roomContext],
);
useDispatcher(defaultDispatcher, handler);
}

View file

@ -37,10 +37,7 @@ export function focusComposer(
if (timeoutId.current) {
clearTimeout(timeoutId.current);
}
timeoutId.current = window.setTimeout(
() => composerElement.current?.focus(),
200,
);
timeoutId.current = window.setTimeout(() => composerElement.current?.focus(), 200);
}
}

View file

@ -14,6 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export { SendWysiwygComposer } from './SendWysiwygComposer';
export { EditWysiwygComposer } from './EditWysiwygComposer';
export { sendMessage } from './utils/message';
export { SendWysiwygComposer } from "./SendWysiwygComposer";
export { EditWysiwygComposer } from "./EditWysiwygComposer";
export { sendMessage } from "./utils/message";

View file

@ -25,8 +25,8 @@ import { htmlToPlainText } from "../../../../../utils/room/htmlToPlaintext";
// Merges favouring the given relation
function attachRelation(content: IContent, relation?: IEventRelation): void {
if (relation) {
content['m.relates_to'] = {
...(content['m.relates_to'] || {}),
content["m.relates_to"] = {
...(content["m.relates_to"] || {}),
...relation,
};
}
@ -44,10 +44,10 @@ function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
function getTextReplyFallback(mxEvent: MatrixEvent): string {
const body = mxEvent.getContent().body;
if (typeof body !== 'string') {
if (typeof body !== "string") {
return "";
}
const lines = body.split("\n").map(l => l.trim());
const lines = body.split("\n").map((l) => l.trim());
if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) {
return `${lines[0]}\n\n`;
}
@ -65,8 +65,13 @@ interface CreateMessageContentParams {
export function createMessageContent(
message: string,
isHTML: boolean,
{ relation, replyToEvent, permalinkCreator, includeReplyLegacyFallback = true, editedEvent }:
CreateMessageContentParams,
{
relation,
replyToEvent,
permalinkCreator,
includeReplyLegacyFallback = true,
editedEvent,
}: CreateMessageContentParams,
): IContent {
// TODO emote ?
@ -86,9 +91,9 @@ export function createMessageContent(
// const body = textSerialize(model);
// TODO remove this ugly hack for replace br tag
const body = isHTML && htmlToPlainText(message) || message.replace(/<br>/g, '\n');
const bodyPrefix = isReplyAndEditing && getTextReplyFallback(editedEvent) || '';
const formattedBodyPrefix = isReplyAndEditing && getHtmlReplyFallback(editedEvent) || '';
const body = (isHTML && htmlToPlainText(message)) || message.replace(/<br>/g, "\n");
const bodyPrefix = (isReplyAndEditing && getTextReplyFallback(editedEvent)) || "";
const formattedBodyPrefix = (isReplyAndEditing && getHtmlReplyFallback(editedEvent)) || "";
const content: IContent = {
// TODO emote
@ -100,12 +105,11 @@ export function createMessageContent(
// TODO markdown support
const isMarkdownEnabled = SettingsStore.getValue<boolean>("MessageComposerInput.useMarkdown");
const formattedBody =
isHTML ?
message :
isMarkdownEnabled ?
htmlSerializeFromMdIfNeeded(message, { forceHTML: isReply }) :
null;
const formattedBody = isHTML
? message
: isMarkdownEnabled
? htmlSerializeFromMdIfNeeded(message, { forceHTML: isReply })
: null;
if (formattedBody) {
content.format = "org.matrix.custom.html";
@ -113,20 +117,18 @@ export function createMessageContent(
}
if (isEditing) {
content['m.new_content'] = {
"msgtype": content.msgtype,
"body": body,
content["m.new_content"] = {
msgtype: content.msgtype,
body: body,
};
if (formattedBody) {
content['m.new_content'].format = "org.matrix.custom.html";
content['m.new_content']['formatted_body'] = formattedBody;
content["m.new_content"].format = "org.matrix.custom.html";
content["m.new_content"]["formatted_body"] = formattedBody;
}
}
const newRelation = isEditing ?
{ ...relation, 'rel_type': 'm.replace', 'event_id': editedEvent.getId() }
: relation;
const newRelation = isEditing ? { ...relation, rel_type: "m.replace", event_id: editedEvent.getId() } : relation;
attachRelation(content, newRelation);

View file

@ -17,7 +17,7 @@ limitations under the License.
import { EventStatus, MatrixClient } from "matrix-js-sdk/src/matrix";
import { IRoomState } from "../../../../structures/RoomView";
import dis from '../../../../../dispatcher/dispatcher';
import dis from "../../../../../dispatcher/dispatcher";
import { Action } from "../../../../../dispatcher/actions";
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
@ -41,10 +41,7 @@ export function endEditing(roomContext: IRoomState) {
export function cancelPreviousPendingEdit(mxClient: MatrixClient, editorStateTransfer: EditorStateTransfer) {
const originalEvent = editorStateTransfer.getEvent();
const previousEdit = originalEvent.replacingEvent();
if (previousEdit && (
previousEdit.status === EventStatus.QUEUED ||
previousEdit.status === EventStatus.NOT_SENT
)) {
if (previousEdit && (previousEdit.status === EventStatus.QUEUED || previousEdit.status === EventStatus.NOT_SENT)) {
mxClient.cancelPendingEvent(previousEdit);
}
}

View file

@ -21,9 +21,12 @@ import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
export function isContentModified(newContent: IContent, editorStateTransfer: EditorStateTransfer): boolean {
// if nothing has changed then bail
const oldContent = editorStateTransfer.getEvent().getContent();
if (oldContent["msgtype"] === newContent["msgtype"] && oldContent["body"] === newContent["body"] &&
if (
oldContent["msgtype"] === newContent["msgtype"] &&
oldContent["body"] === newContent["body"] &&
oldContent["format"] === newContent["format"] &&
oldContent["formatted_body"] === newContent["formatted_body"]) {
oldContent["formatted_body"] === newContent["formatted_body"]
) {
return false;
}
return true;

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
import { IContent, IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { IContent, IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { ISendEventResponse, MatrixClient } from "matrix-js-sdk/src/matrix";
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
@ -27,7 +27,7 @@ import { doMaybeLocalRoomAction } from "../../../../../utils/local-room";
import { CHAT_EFFECTS } from "../../../../../effects";
import { containsEmoji } from "../../../../../effects/utils";
import { IRoomState } from "../../../../structures/RoomView";
import dis from '../../../../../dispatcher/dispatcher';
import dis from "../../../../../dispatcher/dispatcher";
import { createRedactEventDialog } from "../../../dialogs/ConfirmRedactDialog";
import { endEditing, cancelPreviousPendingEdit } from "./editing";
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
@ -43,11 +43,7 @@ interface SendMessageParams {
includeReplyLegacyFallback?: boolean;
}
export function sendMessage(
message: string,
isHTML: boolean,
{ roomContext, mxClient, ...params }: SendMessageParams,
) {
export function sendMessage(message: string, isHTML: boolean, { roomContext, mxClient, ...params }: SendMessageParams) {
const { relation, replyToEvent } = params;
const { room } = roomContext;
const { roomId } = room;
@ -76,11 +72,7 @@ export function sendMessage(
// TODO quick reaction
if (!content) {
content = createMessageContent(
message,
isHTML,
params,
);
content = createMessageContent(message, isHTML, params);
}
// don't bother sending an empty message
@ -92,9 +84,7 @@ export function sendMessage(
decorateStartSendingTime(content);
}
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name
? relation.event_id
: null;
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null;
const prom = doMaybeLocalRoomAction(
roomId,
@ -106,7 +96,7 @@ export function sendMessage(
// Clear reply_to_event as we put the message into the queue
// if the send fails, retry will handle resending.
dis.dispatch({
action: 'reply_to_event',
action: "reply_to_event",
event: null,
context: roomContext.timelineRenderingType,
});
@ -124,7 +114,7 @@ export function sendMessage(
}
});
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
prom.then(resp => {
prom.then((resp) => {
sendRoundTripMetric(mxClient, roomId, resp.event_id);
});
}
@ -149,10 +139,7 @@ interface EditMessageParams {
editorStateTransfer: EditorStateTransfer;
}
export function editMessage(
html: string,
{ roomContext, mxClient, editorStateTransfer }: EditMessageParams,
) {
export function editMessage(html: string, { roomContext, mxClient, editorStateTransfer }: EditMessageParams) {
const editedEvent = editorStateTransfer.getEvent();
PosthogAnalytics.instance.trackEvent<ComposerEvent>({
@ -174,7 +161,7 @@ export function editMessage(
const shouldSend = true;
if (newContent?.body === '') {
if (newContent?.body === "") {
cancelPreviousPendingEdit(mxClient, editorStateTransfer);
createRedactEventDialog({
mxEvent: editedEvent,

View file

@ -14,9 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export function setSelection(selection:
Pick<Selection, 'anchorNode' | 'anchorOffset' | 'focusNode' | 'focusOffset'>,
) {
export function setSelection(selection: Pick<Selection, "anchorNode" | "anchorOffset" | "focusNode" | "focusOffset">) {
if (selection.anchorNode && selection.focusNode) {
const range = new Range();
range.setStart(selection.anchorNode, selection.anchorOffset);
@ -26,4 +24,3 @@ export function setSelection(selection:
document.getSelection()?.addRange(range);
}
}