Merge remote-tracking branch 'upstream/develop' into feature/image-view-load-anim/18186
This commit is contained in:
commit
7022ab4f8a
144 changed files with 4658 additions and 2660 deletions
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { Resizable } from "re-resizable";
|
||||
|
||||
|
@ -26,8 +25,6 @@ import * as sdk from '../../../index';
|
|||
import * as ScalarMessaging from '../../../ScalarMessaging';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import ResizeHandle from "../elements/ResizeHandle";
|
||||
import Resizer from "../../../resizer/resizer";
|
||||
|
@ -37,60 +34,74 @@ import { clamp, percentageOf, percentageWithin } from "../../../utils/numbers";
|
|||
import { useStateCallback } from "../../../hooks/useStateCallback";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { IApp } from "../../../stores/WidgetStore";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
|
||||
interface IProps {
|
||||
userId: string;
|
||||
room: Room;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
showApps?: boolean; // Should apps be rendered
|
||||
maxHeight: number;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
apps: IApp[];
|
||||
resizingVertical: boolean; // true when changing the height of the apps drawer
|
||||
resizingHorizontal: boolean; // true when chagning the distribution of the width between widgets
|
||||
resizing: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.AppsDrawer")
|
||||
export default class AppsDrawer extends React.Component {
|
||||
static propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
resizeNotifier: PropTypes.instanceOf(ResizeNotifier).isRequired,
|
||||
showApps: PropTypes.bool, // Should apps be rendered
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||
private resizeContainer: HTMLDivElement;
|
||||
private resizer: Resizer;
|
||||
private dispatcherRef: string;
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
showApps: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
apps: this._getApps(),
|
||||
resizingVertical: false, // true when changing the height of the apps drawer
|
||||
resizingHorizontal: false, // true when chagning the distribution of the width between widgets
|
||||
apps: this.getApps(),
|
||||
resizingVertical: false,
|
||||
resizingHorizontal: false,
|
||||
resizing: false,
|
||||
};
|
||||
|
||||
this._resizeContainer = null;
|
||||
this.resizer = this._createResizer();
|
||||
this.resizer = this.createResizer();
|
||||
|
||||
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
ScalarMessaging.startListening();
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
ScalarMessaging.stopListening();
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps);
|
||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||
if (this._resizeContainer) {
|
||||
if (this.resizeContainer) {
|
||||
this.resizer.detach();
|
||||
}
|
||||
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
|
||||
}
|
||||
|
||||
onIsResizing = (resizing) => {
|
||||
private onIsResizing = (resizing: boolean): void => {
|
||||
// This one is the vertical, ie. change height of apps drawer
|
||||
this.setState({ resizingVertical: resizing });
|
||||
if (!resizing) {
|
||||
this._relaxResizer();
|
||||
this.relaxResizer();
|
||||
}
|
||||
};
|
||||
|
||||
_createResizer() {
|
||||
private createResizer(): Resizer {
|
||||
// This is the horizontal one, changing the distribution of the width between the app tiles
|
||||
// (ie. a vertical resize handle because, the handle itself is vertical...)
|
||||
const classNames = {
|
||||
|
@ -100,11 +111,11 @@ export default class AppsDrawer extends React.Component {
|
|||
};
|
||||
const collapseConfig = {
|
||||
onResizeStart: () => {
|
||||
this._resizeContainer.classList.add("mx_AppsDrawer_resizing");
|
||||
this.resizeContainer.classList.add("mx_AppsDrawer_resizing");
|
||||
this.setState({ resizingHorizontal: true });
|
||||
},
|
||||
onResizeStop: () => {
|
||||
this._resizeContainer.classList.remove("mx_AppsDrawer_resizing");
|
||||
this.resizeContainer.classList.remove("mx_AppsDrawer_resizing");
|
||||
WidgetLayoutStore.instance.setResizerDistributions(
|
||||
this.props.room, Container.Top,
|
||||
this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
||||
|
@ -113,13 +124,13 @@ export default class AppsDrawer extends React.Component {
|
|||
},
|
||||
};
|
||||
// pass a truthy container for now, we won't call attach until we update it
|
||||
const resizer = new Resizer({}, PercentageDistributor, collapseConfig);
|
||||
const resizer = new Resizer(null, PercentageDistributor, collapseConfig);
|
||||
resizer.setClassNames(classNames);
|
||||
return resizer;
|
||||
}
|
||||
|
||||
_collectResizer = (ref) => {
|
||||
if (this._resizeContainer) {
|
||||
private collectResizer = (ref: HTMLDivElement): void => {
|
||||
if (this.resizeContainer) {
|
||||
this.resizer.detach();
|
||||
}
|
||||
|
||||
|
@ -127,22 +138,22 @@ export default class AppsDrawer extends React.Component {
|
|||
this.resizer.container = ref;
|
||||
this.resizer.attach();
|
||||
}
|
||||
this._resizeContainer = ref;
|
||||
this._loadResizerPreferences();
|
||||
this.resizeContainer = ref;
|
||||
this.loadResizerPreferences();
|
||||
};
|
||||
|
||||
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
|
||||
private getAppsHash = (apps: IApp[]): string => apps.map(app => app.id).join("~");
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
|
||||
// Room has changed, update apps
|
||||
this._updateApps();
|
||||
} else if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
|
||||
this._loadResizerPreferences();
|
||||
this.updateApps();
|
||||
} else if (this.getAppsHash(this.state.apps) !== this.getAppsHash(prevState.apps)) {
|
||||
this.loadResizerPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
_relaxResizer = () => {
|
||||
private relaxResizer = (): void => {
|
||||
const distributors = this.resizer.getDistributors();
|
||||
|
||||
// relax all items if they had any overconstrained flexboxes
|
||||
|
@ -150,7 +161,7 @@ export default class AppsDrawer extends React.Component {
|
|||
distributors.forEach(d => d.finish());
|
||||
};
|
||||
|
||||
_loadResizerPreferences = () => {
|
||||
private loadResizerPreferences = (): void => {
|
||||
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
|
||||
if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
|
||||
distributions.forEach((size, i) => {
|
||||
|
@ -168,11 +179,11 @@ export default class AppsDrawer extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
isResizing() {
|
||||
private isResizing(): boolean {
|
||||
return this.state.resizingVertical || this.state.resizingHorizontal;
|
||||
}
|
||||
|
||||
onAction = (action) => {
|
||||
private onAction = (action: ActionPayload): void => {
|
||||
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
||||
switch (action.action) {
|
||||
case 'appsDrawer':
|
||||
|
@ -190,23 +201,15 @@ export default class AppsDrawer extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_getApps = () => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
|
||||
private getApps = (): IApp[] => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
|
||||
|
||||
_updateApps = () => {
|
||||
private updateApps = (): void => {
|
||||
this.setState({
|
||||
apps: this._getApps(),
|
||||
apps: this.getApps(),
|
||||
});
|
||||
};
|
||||
|
||||
_launchManageIntegrations() {
|
||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||
IntegrationManagers.sharedInstance().openAll();
|
||||
} else {
|
||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
if (!this.props.showApps) return <div />;
|
||||
|
||||
const apps = this.state.apps.map((app, index, arr) => {
|
||||
|
@ -257,7 +260,7 @@ export default class AppsDrawer extends React.Component {
|
|||
className="mx_AppsContainer_resizer"
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
<div className="mx_AppsContainer" ref={this._collectResizer}>
|
||||
<div className="mx_AppsContainer" ref={this.collectResizer}>
|
||||
{ apps.map((app, i) => {
|
||||
if (i < 1) return app;
|
||||
return <React.Fragment key={app.key}>
|
||||
|
@ -273,7 +276,18 @@ export default class AppsDrawer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
const PersistentVResizer = ({
|
||||
interface IPersistentResizerProps {
|
||||
room: Room;
|
||||
minHeight: number;
|
||||
maxHeight: number;
|
||||
className: string;
|
||||
handleWrapperClass: string;
|
||||
handleClass: string;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
|
||||
room,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
|
@ -303,7 +317,7 @@ const PersistentVResizer = ({
|
|||
});
|
||||
|
||||
return <Resizable
|
||||
size={{ height: Math.min(height, maxHeight) }}
|
||||
size={{ height: Math.min(height, maxHeight), width: null }}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
onResizeStart={() => {
|
|
@ -50,7 +50,8 @@ import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
// matches emoticons which follow the start of a line or whitespace
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s|:^$');
|
||||
export const REGEX_EMOTICON = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')$');
|
||||
|
||||
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
||||
|
||||
|
@ -161,7 +162,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
}
|
||||
}
|
||||
|
||||
private replaceEmoticon = (caretPosition: DocumentPosition): number => {
|
||||
public replaceEmoticon(caretPosition: DocumentPosition, regex: RegExp): number {
|
||||
const { model } = this.props;
|
||||
const range = model.startRange(caretPosition);
|
||||
// expand range max 8 characters backwards from caretPosition,
|
||||
|
@ -170,9 +171,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
range.expandBackwardsWhile((index, offset) => {
|
||||
const part = model.parts[index];
|
||||
n -= 1;
|
||||
return n >= 0 && (part.type === Type.Plain || part.type === Type.PillCandidate);
|
||||
return n >= 0 && [Type.Plain, Type.PillCandidate, Type.Newline].includes(part.type);
|
||||
});
|
||||
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text);
|
||||
const emoticonMatch = regex.exec(range.text);
|
||||
if (emoticonMatch) {
|
||||
const query = emoticonMatch[1].replace("-", "");
|
||||
// try both exact match and lower-case, this means that xd won't match xD but :P will match :p
|
||||
|
@ -180,18 +181,25 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
|
||||
if (data) {
|
||||
const { partCreator } = model;
|
||||
const hasPrecedingSpace = emoticonMatch[0][0] === " ";
|
||||
const firstMatch = emoticonMatch[0];
|
||||
const moveStart = firstMatch[0] === " " ? 1 : 0;
|
||||
|
||||
// we need the range to only comprise of the emoticon
|
||||
// because we'll replace the whole range with an emoji,
|
||||
// so move the start forward to the start of the emoticon.
|
||||
// Take + 1 because index is reported without the possible preceding space.
|
||||
range.moveStart(emoticonMatch.index + (hasPrecedingSpace ? 1 : 0));
|
||||
range.moveStartForwards(emoticonMatch.index + moveStart);
|
||||
// If the end is a trailing space/newline move end backwards, so that we don't replace it
|
||||
if (["\n", " "].includes(firstMatch[firstMatch.length - 1])) {
|
||||
range.moveEndBackwards(1);
|
||||
}
|
||||
|
||||
// this returns the amount of added/removed characters during the replace
|
||||
// so the caret position can be adjusted.
|
||||
return range.replace([partCreator.plain(data.unicode + " ")]);
|
||||
return range.replace([partCreator.plain(data.unicode)]);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => {
|
||||
renderModel(this.editorRef.current, this.props.model);
|
||||
|
@ -607,8 +615,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
};
|
||||
|
||||
private configureEmoticonAutoReplace = (): void => {
|
||||
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
|
||||
this.props.model.setTransformCallback(shouldReplace ? this.replaceEmoticon : null);
|
||||
this.props.model.setTransformCallback(this.transform);
|
||||
};
|
||||
|
||||
private configureShouldShowPillAvatar = (): void => {
|
||||
|
@ -621,6 +628,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
this.setState({ surroundWith });
|
||||
};
|
||||
|
||||
private transform = (documentPosition: DocumentPosition): void => {
|
||||
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
|
||||
if (shouldReplace) this.replaceEmoticon(documentPosition, REGEX_EMOTICON_WHITESPACE);
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("selectionchange", this.onSelectionChange);
|
||||
this.editorRef.current.removeEventListener("input", this.onInput, true);
|
||||
|
|
|
@ -16,41 +16,51 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
|
||||
export const E2E_STATE = {
|
||||
VERIFIED: "verified",
|
||||
WARNING: "warning",
|
||||
UNKNOWN: "unknown",
|
||||
NORMAL: "normal",
|
||||
UNAUTHENTICATED: "unauthenticated",
|
||||
export enum E2EState {
|
||||
Verified = "verified",
|
||||
Warning = "warning",
|
||||
Unknown = "unknown",
|
||||
Normal = "normal",
|
||||
Unauthenticated = "unauthenticated",
|
||||
}
|
||||
|
||||
const crossSigningUserTitles: { [key in E2EState]?: string } = {
|
||||
[E2EState.Warning]: _td("This user has not verified all of their sessions."),
|
||||
[E2EState.Normal]: _td("You have not verified this user."),
|
||||
[E2EState.Verified]: _td("You have verified this user. This user has verified all of their sessions."),
|
||||
};
|
||||
const crossSigningRoomTitles: { [key in E2EState]?: string } = {
|
||||
[E2EState.Warning]: _td("Someone is using an unknown session"),
|
||||
[E2EState.Normal]: _td("This room is end-to-end encrypted"),
|
||||
[E2EState.Verified]: _td("Everyone in this room is verified"),
|
||||
};
|
||||
|
||||
const crossSigningUserTitles = {
|
||||
[E2E_STATE.WARNING]: _td("This user has not verified all of their sessions."),
|
||||
[E2E_STATE.NORMAL]: _td("You have not verified this user."),
|
||||
[E2E_STATE.VERIFIED]: _td("You have verified this user. This user has verified all of their sessions."),
|
||||
};
|
||||
const crossSigningRoomTitles = {
|
||||
[E2E_STATE.WARNING]: _td("Someone is using an unknown session"),
|
||||
[E2E_STATE.NORMAL]: _td("This room is end-to-end encrypted"),
|
||||
[E2E_STATE.VERIFIED]: _td("Everyone in this room is verified"),
|
||||
};
|
||||
interface IProps {
|
||||
isUser?: boolean;
|
||||
status?: E2EState | E2EStatus;
|
||||
className?: string;
|
||||
size?: number;
|
||||
onClick?: () => void;
|
||||
hideTooltip?: boolean;
|
||||
bordered?: boolean;
|
||||
}
|
||||
|
||||
const E2EIcon = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => {
|
||||
const E2EIcon: React.FC<IProps> = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => {
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
const classes = classNames({
|
||||
mx_E2EIcon: true,
|
||||
mx_E2EIcon_bordered: bordered,
|
||||
mx_E2EIcon_warning: status === E2E_STATE.WARNING,
|
||||
mx_E2EIcon_normal: status === E2E_STATE.NORMAL,
|
||||
mx_E2EIcon_verified: status === E2E_STATE.VERIFIED,
|
||||
mx_E2EIcon_warning: status === E2EState.Warning,
|
||||
mx_E2EIcon_normal: status === E2EState.Normal,
|
||||
mx_E2EIcon_verified: status === E2EState.Verified,
|
||||
}, className);
|
||||
|
||||
let e2eTitle;
|
||||
|
@ -92,12 +102,4 @@ const E2EIcon = ({ isUser, status, className, size, onClick, hideTooltip, border
|
|||
</div>;
|
||||
};
|
||||
|
||||
E2EIcon.propTypes = {
|
||||
isUser: PropTypes.bool,
|
||||
status: PropTypes.oneOf(Object.values(E2E_STATE)),
|
||||
className: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default E2EIcon;
|
|
@ -27,7 +27,7 @@ import { findEditableEvent } from '../../../utils/EventUtils';
|
|||
import { parseEvent } from '../../../editor/deserialize';
|
||||
import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
|
@ -42,6 +42,7 @@ import ErrorDialog from "../dialogs/ErrorDialog";
|
|||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||
const html = mxEvent.getContent().formatted_body;
|
||||
|
@ -315,6 +316,14 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
private sendEdit = async (): Promise<void> => {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
const editedEvent = this.props.editState.getEvent();
|
||||
|
||||
// Replace emoticon at the end of the message
|
||||
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);
|
||||
}
|
||||
|
||||
const editContent = createEditContent(this.model, editedEvent);
|
||||
const newContent = editContent["m.new_content"];
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import React from 'react';
|
|||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { _td } from '../../../languageHandler';
|
||||
import classNames from "classnames";
|
||||
import E2EIcon from './E2EIcon';
|
||||
import E2EIcon, { E2EState } from './E2EIcon';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import PresenceLabel from "./PresenceLabel";
|
||||
|
@ -75,7 +75,7 @@ interface IProps {
|
|||
suppressOnHover?: boolean;
|
||||
showPresence?: boolean;
|
||||
subtextLabel?: string;
|
||||
e2eStatus?: string;
|
||||
e2eStatus?: E2EState;
|
||||
powerStatus?: PowerStatus;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
|||
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Relations } from "matrix-js-sdk/src/models/relations";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { Thread } from 'matrix-js-sdk/src/models/thread';
|
||||
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -33,7 +33,7 @@ import { formatTime } from "../../../DateUtils";
|
|||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { ALL_RULE_TYPES } from "../../../mjolnir/BanList";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { E2E_STATE } from "./E2EIcon";
|
||||
import { E2EState } from "./E2EIcon";
|
||||
import { toRem } from "../../../utils/units";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
|
@ -464,8 +464,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
if (SettingsStore.getValue("feature_thread")) {
|
||||
this.props.mxEvent.once("Thread.ready", this.updateThread);
|
||||
this.props.mxEvent.on("Thread.update", this.updateThread);
|
||||
this.props.mxEvent.once(ThreadEvent.Ready, this.updateThread);
|
||||
this.props.mxEvent.on(ThreadEvent.Update, this.updateThread);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -521,7 +521,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
|
||||
const thread = this.state.thread;
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
if (!thread || this.props.showThreadInfo === false) {
|
||||
if (!thread || this.props.showThreadInfo === false || thread.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -605,7 +605,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
if (encryptionInfo.mismatchedSender) {
|
||||
// something definitely wrong is going on here
|
||||
this.setState({
|
||||
verified: E2E_STATE.WARNING,
|
||||
verified: E2EState.Warning,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
@ -613,7 +613,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
if (!userTrust.isCrossSigningVerified()) {
|
||||
// user is not verified, so default to everything is normal
|
||||
this.setState({
|
||||
verified: E2E_STATE.NORMAL,
|
||||
verified: E2EState.Normal,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
@ -623,27 +623,27 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
);
|
||||
if (!eventSenderTrust) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.UNKNOWN,
|
||||
verified: E2EState.Unknown,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eventSenderTrust.isVerified()) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.WARNING,
|
||||
verified: E2EState.Warning,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
if (!encryptionInfo.authenticated) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.UNAUTHENTICATED,
|
||||
verified: E2EState.Unauthenticated,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
verified: E2E_STATE.VERIFIED,
|
||||
verified: E2EState.Verified,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
}
|
||||
|
||||
|
@ -850,13 +850,13 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
|
||||
// event is encrypted, display padlock corresponding to whether or not it is verified
|
||||
if (ev.isEncrypted()) {
|
||||
if (this.state.verified === E2E_STATE.NORMAL) {
|
||||
if (this.state.verified === E2EState.Normal) {
|
||||
return; // no icon if we've not even cross-signed the user
|
||||
} else if (this.state.verified === E2E_STATE.VERIFIED) {
|
||||
} else if (this.state.verified === E2EState.Verified) {
|
||||
return; // no icon for verified
|
||||
} else if (this.state.verified === E2E_STATE.UNAUTHENTICATED) {
|
||||
} else if (this.state.verified === E2EState.Unauthenticated) {
|
||||
return (<E2ePadlockUnauthenticated />);
|
||||
} else if (this.state.verified === E2E_STATE.UNKNOWN) {
|
||||
} else if (this.state.verified === E2EState.Unknown) {
|
||||
return (<E2ePadlockUnknown />);
|
||||
} else {
|
||||
return (<E2ePadlockUnverified />);
|
||||
|
@ -961,9 +961,9 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
mx_EventTile_lastInSection: this.props.lastInSection,
|
||||
mx_EventTile_contextual: this.props.contextual,
|
||||
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
||||
mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2E_STATE.VERIFIED,
|
||||
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2E_STATE.WARNING,
|
||||
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN,
|
||||
mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2EState.Verified,
|
||||
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2EState.Warning,
|
||||
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2EState.Unknown,
|
||||
mx_EventTile_bad: isEncryptionFailure,
|
||||
mx_EventTile_emote: msgtype === 'm.emote',
|
||||
mx_EventTile_noSender: this.props.hideSender,
|
||||
|
@ -1192,14 +1192,19 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
default: {
|
||||
const thread = ReplyThread.makeThread(
|
||||
this.props.mxEvent,
|
||||
this.props.onHeightChanged,
|
||||
this.props.permalinkCreator,
|
||||
this.replyThread,
|
||||
this.props.layout,
|
||||
this.props.alwaysShowTimestamps || this.state.hover,
|
||||
);
|
||||
let thread;
|
||||
// When the "showHiddenEventsInTimeline" lab is enabled,
|
||||
// avoid showing replies for hidden events (events without tiles)
|
||||
if (haveTileForEvent(this.props.mxEvent)) {
|
||||
thread = ReplyThread.makeThread(
|
||||
this.props.mxEvent,
|
||||
this.props.onHeightChanged,
|
||||
this.props.permalinkCreator,
|
||||
this.replyThread,
|
||||
this.props.layout,
|
||||
this.props.alwaysShowTimestamps || this.state.hover,
|
||||
);
|
||||
}
|
||||
|
||||
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
ContextMenu,
|
||||
useContextMenu,
|
||||
MenuItem,
|
||||
AboveLeftOf,
|
||||
} from "../../structures/ContextMenu";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import ReplyPreview from "./ReplyPreview";
|
||||
|
@ -56,7 +57,7 @@ let instanceCount = 0;
|
|||
const NARROW_MODE_BREAKPOINT = 500;
|
||||
|
||||
interface IComposerAvatarProps {
|
||||
me: object;
|
||||
me: RoomMember;
|
||||
}
|
||||
|
||||
function ComposerAvatar(props: IComposerAvatarProps) {
|
||||
|
@ -511,7 +512,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
null,
|
||||
];
|
||||
|
||||
let menuPosition;
|
||||
let menuPosition: AboveLeftOf | undefined;
|
||||
if (this.ref.current) {
|
||||
const contentRect = this.ref.current.getBoundingClientRect();
|
||||
menuPosition = aboveLeftOf(contentRect);
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { MouseEvent } from "react";
|
||||
import classNames from "classnames";
|
||||
import { formatCount } from "../../../utils/FormattingUtils";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
@ -22,6 +22,9 @@ import AccessibleButton from "../elements/AccessibleButton";
|
|||
import { XOR } from "../../../@types/common";
|
||||
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||
|
||||
interface IProps {
|
||||
notification: NotificationState;
|
||||
|
@ -39,6 +42,7 @@ interface IProps {
|
|||
}
|
||||
|
||||
interface IClickableProps extends IProps, React.InputHTMLAttributes<Element> {
|
||||
showUnsentTooltip?: boolean;
|
||||
/**
|
||||
* If specified will return an AccessibleButton instead of a div.
|
||||
*/
|
||||
|
@ -47,6 +51,7 @@ interface IClickableProps extends IProps, React.InputHTMLAttributes<Element> {
|
|||
|
||||
interface IState {
|
||||
showCounts: boolean; // whether or not to show counts. Independent of props.forceCount
|
||||
showTooltip: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.NotificationBadge")
|
||||
|
@ -59,6 +64,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
|||
|
||||
this.state = {
|
||||
showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId),
|
||||
showTooltip: false,
|
||||
};
|
||||
|
||||
this.countWatcherRef = SettingsStore.watchSetting(
|
||||
|
@ -93,9 +99,22 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
|||
this.forceUpdate(); // notification state changed - update
|
||||
};
|
||||
|
||||
private onMouseOver = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
this.setState({
|
||||
showTooltip: true,
|
||||
});
|
||||
};
|
||||
|
||||
private onMouseLeave = () => {
|
||||
this.setState({
|
||||
showTooltip: false,
|
||||
});
|
||||
};
|
||||
|
||||
public render(): React.ReactElement {
|
||||
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
||||
const { notification, forceCount, roomId, onClick, ...props } = this.props;
|
||||
const { notification, showUnsentTooltip, forceCount, roomId, onClick, ...props } = this.props;
|
||||
|
||||
// Don't show a badge if we don't need to
|
||||
if (notification.isIdle) return null;
|
||||
|
@ -124,9 +143,24 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
|||
});
|
||||
|
||||
if (onClick) {
|
||||
let label: string;
|
||||
let tooltip: JSX.Element;
|
||||
if (showUnsentTooltip && this.state.showTooltip && notification.color === NotificationColor.Unsent) {
|
||||
label = _t("Message didn't send. Click for info.");
|
||||
tooltip = <Tooltip className="mx_RoleButton_tooltip" label={label} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton {...props} className={classes} onClick={onClick}>
|
||||
<AccessibleButton
|
||||
aria-label={label}
|
||||
{...props}
|
||||
className={classes}
|
||||
onClick={onClick}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
<span className="mx_NotificationBadge_count">{ symbol }</span>
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -547,7 +547,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
const unfilteredHistorical = unfilteredLists[DefaultTagID.Archived] || [];
|
||||
const unfilteredFavourite = unfilteredLists[DefaultTagID.Favourite] || [];
|
||||
// show a prompt to join/create rooms if the user is in 0 rooms and no historical
|
||||
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1 && unfilteredFavourite < 1) {
|
||||
if (unfilteredRooms.length < 1 && unfilteredHistorical.length < 1 && unfilteredFavourite.length < 1) {
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{ _t("Use the + to make a new room or explore existing ones below") }</div>
|
||||
<AccessibleButton
|
||||
|
|
|
@ -14,8 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
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 * as sdk from '../../../index';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -27,91 +32,102 @@ import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore
|
|||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import InviteReason from "../elements/InviteReason";
|
||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
const MemberEventHtmlReasonField = "io.element.html_reason";
|
||||
|
||||
const MessageCase = Object.freeze({
|
||||
NotLoggedIn: "NotLoggedIn",
|
||||
Joining: "Joining",
|
||||
Loading: "Loading",
|
||||
Rejecting: "Rejecting",
|
||||
Kicked: "Kicked",
|
||||
Banned: "Banned",
|
||||
OtherThreePIDError: "OtherThreePIDError",
|
||||
InvitedEmailNotFoundInAccount: "InvitedEmailNotFoundInAccount",
|
||||
InvitedEmailNoIdentityServer: "InvitedEmailNoIdentityServer",
|
||||
InvitedEmailMismatch: "InvitedEmailMismatch",
|
||||
Invite: "Invite",
|
||||
ViewingRoom: "ViewingRoom",
|
||||
RoomNotFound: "RoomNotFound",
|
||||
OtherError: "OtherError",
|
||||
});
|
||||
enum MessageCase {
|
||||
NotLoggedIn = "NotLoggedIn",
|
||||
Joining = "Joining",
|
||||
Loading = "Loading",
|
||||
Rejecting = "Rejecting",
|
||||
Kicked = "Kicked",
|
||||
Banned = "Banned",
|
||||
OtherThreePIDError = "OtherThreePIDError",
|
||||
InvitedEmailNotFoundInAccount = "InvitedEmailNotFoundInAccount",
|
||||
InvitedEmailNoIdentityServer = "InvitedEmailNoIdentityServer",
|
||||
InvitedEmailMismatch = "InvitedEmailMismatch",
|
||||
Invite = "Invite",
|
||||
ViewingRoom = "ViewingRoom",
|
||||
RoomNotFound = "RoomNotFound",
|
||||
OtherError = "OtherError",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
// if inviterName is specified, the preview bar will shown an invite to the room.
|
||||
// You should also specify onRejectClick if specifying inviterName
|
||||
inviterName?: string;
|
||||
|
||||
// If invited by 3rd party invite, the email address the invite was sent to
|
||||
invitedEmail?: string;
|
||||
|
||||
// For third party invites, information passed about the room out-of-band
|
||||
oobData?: IOOBData;
|
||||
|
||||
// For third party invites, a URL for a 3pid invite signing service
|
||||
signUrl?: string;
|
||||
|
||||
// A standard client/server API error object. If supplied, indicates that the
|
||||
// caller was unable to fetch details about the room for the given reason.
|
||||
error?: MatrixError;
|
||||
|
||||
canPreview?: boolean;
|
||||
previewLoading?: boolean;
|
||||
room?: Room;
|
||||
|
||||
loading?: boolean;
|
||||
joining?: boolean;
|
||||
rejecting?: boolean;
|
||||
// The alias that was used to access this room, if appropriate
|
||||
// If given, this will be how the room is referred to (eg.
|
||||
// in error messages).
|
||||
roomAlias?: string;
|
||||
|
||||
onJoinClick?(): void;
|
||||
onRejectClick?(): void;
|
||||
onRejectAndIgnoreClick?(): void;
|
||||
onForgetClick?(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
busy: boolean;
|
||||
accountEmails?: string[];
|
||||
invitedEmailMxid?: string;
|
||||
threePidFetchError?: MatrixError;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.RoomPreviewBar")
|
||||
export default class RoomPreviewBar extends React.Component {
|
||||
static propTypes = {
|
||||
onJoinClick: PropTypes.func,
|
||||
onRejectClick: PropTypes.func,
|
||||
onRejectAndIgnoreClick: PropTypes.func,
|
||||
onForgetClick: PropTypes.func,
|
||||
// if inviterName is specified, the preview bar will shown an invite to the room.
|
||||
// You should also specify onRejectClick if specifiying inviterName
|
||||
inviterName: PropTypes.string,
|
||||
|
||||
// If invited by 3rd party invite, the email address the invite was sent to
|
||||
invitedEmail: PropTypes.string,
|
||||
|
||||
// For third party invites, information passed about the room out-of-band
|
||||
oobData: PropTypes.object,
|
||||
|
||||
// For third party invites, a URL for a 3pid invite signing service
|
||||
signUrl: PropTypes.string,
|
||||
|
||||
// A standard client/server API error object. If supplied, indicates that the
|
||||
// caller was unable to fetch details about the room for the given reason.
|
||||
error: PropTypes.object,
|
||||
|
||||
canPreview: PropTypes.bool,
|
||||
previewLoading: PropTypes.bool,
|
||||
room: PropTypes.object,
|
||||
|
||||
// When a spinner is present, a spinnerState can be specified to indicate the
|
||||
// purpose of the spinner.
|
||||
spinner: PropTypes.bool,
|
||||
spinnerState: PropTypes.oneOf(["joining"]),
|
||||
loading: PropTypes.bool,
|
||||
joining: PropTypes.bool,
|
||||
rejecting: PropTypes.bool,
|
||||
// The alias that was used to access this room, if appropriate
|
||||
// If given, this will be how the room is referred to (eg.
|
||||
// in error messages).
|
||||
roomAlias: PropTypes.string,
|
||||
};
|
||||
|
||||
export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||
static defaultProps = {
|
||||
onJoinClick() {},
|
||||
};
|
||||
|
||||
state = {
|
||||
busy: false,
|
||||
};
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
busy: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._checkInvitedEmail();
|
||||
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this._onCommunityUpdate);
|
||||
this.checkInvitedEmail();
|
||||
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this.onCommunityUpdate);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.props.invitedEmail !== prevProps.invitedEmail || this.props.inviterName !== prevProps.inviterName) {
|
||||
this._checkInvitedEmail();
|
||||
this.checkInvitedEmail();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this._onCommunityUpdate);
|
||||
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this.onCommunityUpdate);
|
||||
}
|
||||
|
||||
async _checkInvitedEmail() {
|
||||
private async checkInvitedEmail() {
|
||||
// If this is an invite and we've been told what email address was
|
||||
// invited, fetch the user's account emails and discovery bindings so we
|
||||
// can check them against the email that was invited.
|
||||
|
@ -121,8 +137,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
// 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,21 +161,21 @@ export default class RoomPreviewBar extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_onCommunityUpdate = (roomId) => {
|
||||
private onCommunityUpdate = (roomId: string): void => {
|
||||
if (this.props.room && this.props.room.roomId !== roomId) {
|
||||
return;
|
||||
}
|
||||
this.forceUpdate(); // we have nothing to update
|
||||
};
|
||||
|
||||
_getMessageCase() {
|
||||
private getMessageCase(): MessageCase {
|
||||
const isGuest = MatrixClientPeg.get().isGuest();
|
||||
|
||||
if (isGuest) {
|
||||
return MessageCase.NotLoggedIn;
|
||||
}
|
||||
|
||||
const myMember = this._getMyMember();
|
||||
const myMember = this.getMyMember();
|
||||
|
||||
if (myMember) {
|
||||
if (myMember.isKicked()) {
|
||||
|
@ -195,7 +210,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
}
|
||||
return MessageCase.Invite;
|
||||
} else if (this.props.error) {
|
||||
if (this.props.error.errcode == 'M_NOT_FOUND') {
|
||||
if ((this.props.error as MatrixError).errcode == 'M_NOT_FOUND') {
|
||||
return MessageCase.RoomNotFound;
|
||||
} else {
|
||||
return MessageCase.OtherError;
|
||||
|
@ -205,8 +220,8 @@ export default class RoomPreviewBar extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_getKickOrBanInfo() {
|
||||
const myMember = this._getMyMember();
|
||||
private getKickOrBanInfo(): { memberName?: string, reason?: string } {
|
||||
const myMember = this.getMyMember();
|
||||
if (!myMember) {
|
||||
return {};
|
||||
}
|
||||
|
@ -219,24 +234,19 @@ export default class RoomPreviewBar extends React.Component {
|
|||
return { memberName, reason };
|
||||
}
|
||||
|
||||
_joinRule() {
|
||||
const room = this.props.room;
|
||||
if (room) {
|
||||
const joinRules = room.currentState.getStateEvents('m.room.join_rules', '');
|
||||
if (joinRules) {
|
||||
return joinRules.getContent().join_rule;
|
||||
}
|
||||
}
|
||||
private joinRule(): JoinRule {
|
||||
return this.props.room?.currentState
|
||||
.getStateEvents(EventType.RoomJoinRules, "")?.getContent<IJoinRuleEventContent>().join_rule;
|
||||
}
|
||||
|
||||
_communityProfile() {
|
||||
private communityProfile(): { displayName?: string, avatarMxc?: string } {
|
||||
if (this.props.room) return CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
|
||||
return { displayName: null, avatarMxc: null };
|
||||
}
|
||||
|
||||
_roomName(atStart = false) {
|
||||
private roomName(atStart = false): string {
|
||||
let name = this.props.room ? this.props.room.name : this.props.roomAlias;
|
||||
const profile = this._communityProfile();
|
||||
const profile = this.communityProfile();
|
||||
if (profile.displayName) name = profile.displayName;
|
||||
if (name) {
|
||||
return name;
|
||||
|
@ -247,14 +257,11 @@ export default class RoomPreviewBar extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_getMyMember() {
|
||||
return (
|
||||
this.props.room &&
|
||||
this.props.room.getMember(MatrixClientPeg.get().getUserId())
|
||||
);
|
||||
private getMyMember(): RoomMember {
|
||||
return this.props.room?.getMember(MatrixClientPeg.get().getUserId());
|
||||
}
|
||||
|
||||
_getInviteMember() {
|
||||
private getInviteMember(): RoomMember {
|
||||
const { room } = this.props;
|
||||
if (!room) {
|
||||
return;
|
||||
|
@ -268,8 +275,8 @@ export default class RoomPreviewBar extends React.Component {
|
|||
return room.currentState.getMember(inviterUserId);
|
||||
}
|
||||
|
||||
_isDMInvite() {
|
||||
const myMember = this._getMyMember();
|
||||
private isDMInvite(): boolean {
|
||||
const myMember = this.getMyMember();
|
||||
if (!myMember) {
|
||||
return false;
|
||||
}
|
||||
|
@ -278,7 +285,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
return memberContent.membership === "invite" && memberContent.is_direct;
|
||||
}
|
||||
|
||||
_makeScreenAfterLogin() {
|
||||
private makeScreenAfterLogin(): { screen: string, params: Record<string, any> } {
|
||||
return {
|
||||
screen: 'room',
|
||||
params: {
|
||||
|
@ -291,18 +298,16 @@ export default class RoomPreviewBar extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
onLoginClick = () => {
|
||||
dis.dispatch({ action: 'start_login', screenAfterLogin: this._makeScreenAfterLogin() });
|
||||
private onLoginClick = () => {
|
||||
dis.dispatch({ action: 'start_login', screenAfterLogin: this.makeScreenAfterLogin() });
|
||||
};
|
||||
|
||||
onRegisterClick = () => {
|
||||
dis.dispatch({ action: 'start_registration', screenAfterLogin: this._makeScreenAfterLogin() });
|
||||
private onRegisterClick = () => {
|
||||
dis.dispatch({ action: 'start_registration', screenAfterLogin: this.makeScreenAfterLogin() });
|
||||
};
|
||||
|
||||
render() {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let showSpinner = false;
|
||||
let title;
|
||||
|
@ -315,10 +320,10 @@ export default class RoomPreviewBar extends React.Component {
|
|||
let footer;
|
||||
const extraComponents = [];
|
||||
|
||||
const messageCase = this._getMessageCase();
|
||||
const messageCase = this.getMessageCase();
|
||||
switch (messageCase) {
|
||||
case MessageCase.Joining: {
|
||||
title = _t("Joining room …");
|
||||
title = this.props.oobData?.roomType === RoomType.Space ? _t("Joining space …") : _t("Joining room …");
|
||||
showSpinner = true;
|
||||
break;
|
||||
}
|
||||
|
@ -349,12 +354,12 @@ export default class RoomPreviewBar extends React.Component {
|
|||
break;
|
||||
}
|
||||
case MessageCase.Kicked: {
|
||||
const { memberName, reason } = this._getKickOrBanInfo();
|
||||
const { memberName, reason } = this.getKickOrBanInfo();
|
||||
title = _t("You were kicked from %(roomName)s by %(memberName)s",
|
||||
{ memberName, roomName: this._roomName() });
|
||||
{ memberName, roomName: this.roomName() });
|
||||
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
||||
|
||||
if (this._joinRule() === "invite") {
|
||||
if (this.joinRule() === "invite") {
|
||||
primaryActionLabel = _t("Forget this room");
|
||||
primaryActionHandler = this.props.onForgetClick;
|
||||
} else {
|
||||
|
@ -366,9 +371,9 @@ export default class RoomPreviewBar extends React.Component {
|
|||
break;
|
||||
}
|
||||
case MessageCase.Banned: {
|
||||
const { memberName, reason } = this._getKickOrBanInfo();
|
||||
const { memberName, reason } = this.getKickOrBanInfo();
|
||||
title = _t("You were banned from %(roomName)s by %(memberName)s",
|
||||
{ memberName, roomName: this._roomName() });
|
||||
{ memberName, roomName: this.roomName() });
|
||||
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
||||
primaryActionLabel = _t("Forget this room");
|
||||
primaryActionHandler = this.props.onForgetClick;
|
||||
|
@ -376,8 +381,8 @@ export default class RoomPreviewBar extends React.Component {
|
|||
}
|
||||
case MessageCase.OtherThreePIDError: {
|
||||
title = _t("Something went wrong with your invite to %(roomName)s",
|
||||
{ roomName: this._roomName() });
|
||||
const joinRule = this._joinRule();
|
||||
{ roomName: this.roomName() });
|
||||
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 a room admin.",
|
||||
|
@ -410,7 +415,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
"This invite to %(roomName)s was sent to %(email)s which is not " +
|
||||
"associated with your account",
|
||||
{
|
||||
roomName: this._roomName(),
|
||||
roomName: this.roomName(),
|
||||
email: this.props.invitedEmail,
|
||||
},
|
||||
);
|
||||
|
@ -427,7 +432,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
title = _t(
|
||||
"This invite to %(roomName)s was sent to %(email)s",
|
||||
{
|
||||
roomName: this._roomName(),
|
||||
roomName: this.roomName(),
|
||||
email: this.props.invitedEmail,
|
||||
},
|
||||
);
|
||||
|
@ -443,7 +448,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
title = _t(
|
||||
"This invite to %(roomName)s was sent to %(email)s",
|
||||
{
|
||||
roomName: this._roomName(),
|
||||
roomName: this.roomName(),
|
||||
email: this.props.invitedEmail,
|
||||
},
|
||||
);
|
||||
|
@ -458,11 +463,11 @@ export default class RoomPreviewBar extends React.Component {
|
|||
case MessageCase.Invite: {
|
||||
const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar");
|
||||
const oobData = Object.assign({}, this.props.oobData, {
|
||||
avatarUrl: this._communityProfile().avatarMxc,
|
||||
avatarUrl: this.communityProfile().avatarMxc,
|
||||
});
|
||||
const avatar = <RoomAvatar room={this.props.room} oobData={oobData} />;
|
||||
|
||||
const inviteMember = this._getInviteMember();
|
||||
const inviteMember = this.getInviteMember();
|
||||
let inviterElement;
|
||||
if (inviteMember) {
|
||||
inviterElement = <span>
|
||||
|
@ -474,7 +479,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
inviterElement = (<span className="mx_RoomPreviewBar_inviter">{ this.props.inviterName }</span>);
|
||||
}
|
||||
|
||||
const isDM = this._isDMInvite();
|
||||
const isDM = this.isDMInvite();
|
||||
if (isDM) {
|
||||
title = _t("Do you want to chat with %(user)s?",
|
||||
{ user: inviteMember.name });
|
||||
|
@ -485,7 +490,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
primaryActionLabel = _t("Start chatting");
|
||||
} else {
|
||||
title = _t("Do you want to join %(roomName)s?",
|
||||
{ roomName: this._roomName() });
|
||||
{ roomName: this.roomName() });
|
||||
subTitle = [
|
||||
avatar,
|
||||
_t("<userName/> invited you", {}, { userName: () => inviterElement }),
|
||||
|
@ -519,22 +524,22 @@ export default class RoomPreviewBar extends React.Component {
|
|||
case MessageCase.ViewingRoom: {
|
||||
if (this.props.canPreview) {
|
||||
title = _t("You're previewing %(roomName)s. Want to join it?",
|
||||
{ roomName: this._roomName() });
|
||||
{ roomName: this.roomName() });
|
||||
} else {
|
||||
title = _t("%(roomName)s can't be previewed. Do you want to join it?",
|
||||
{ roomName: this._roomName(true) });
|
||||
{ roomName: this.roomName(true) });
|
||||
}
|
||||
primaryActionLabel = _t("Join the discussion");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
}
|
||||
case MessageCase.RoomNotFound: {
|
||||
title = _t("%(roomName)s does not exist.", { roomName: this._roomName(true) });
|
||||
title = _t("%(roomName)s does not exist.", { roomName: this.roomName(true) });
|
||||
subTitle = _t("This room doesn't exist. Are you sure you're at the right place?");
|
||||
break;
|
||||
}
|
||||
case MessageCase.OtherError: {
|
||||
title = _t("%(roomName)s is not accessible at this time.", { roomName: this._roomName(true) });
|
||||
title = _t("%(roomName)s is not accessible at this time.", { roomName: this.roomName(true) });
|
||||
subTitle = [
|
||||
_t("Try again later, or ask a room admin to check if you have access."),
|
||||
_t(
|
|
@ -670,6 +670,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
onClick={this.onBadgeClick}
|
||||
tabIndex={tabIndex}
|
||||
aria-label={ariaLabel}
|
||||
showUnsentTooltip={true}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
|
||||
import React, { createRef } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import classNames from "classnames";
|
||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||
|
@ -51,8 +50,6 @@ import IconizedContextMenu, {
|
|||
} from "../context_menus/IconizedContextMenu";
|
||||
import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { getUnsentMessages } from "../../structures/RoomStatusBar";
|
||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -68,7 +65,6 @@ interface IState {
|
|||
notificationsMenuPosition: PartialDOMRect;
|
||||
generalMenuPosition: PartialDOMRect;
|
||||
messagePreview?: string;
|
||||
hasUnsentEvents: boolean;
|
||||
}
|
||||
|
||||
const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomId}`;
|
||||
|
@ -95,7 +91,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
|
||||
notificationsMenuPosition: null,
|
||||
generalMenuPosition: null,
|
||||
hasUnsentEvents: this.countUnsentEvents() > 0,
|
||||
|
||||
// generatePreview() will return nothing if the user has previews disabled
|
||||
messagePreview: "",
|
||||
|
@ -106,11 +101,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||
}
|
||||
|
||||
private countUnsentEvents(): number {
|
||||
return getUnsentMessages(this.props.room).length;
|
||||
}
|
||||
|
||||
private onRoomNameUpdate = (room) => {
|
||||
private onRoomNameUpdate = (room: Room) => {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
|
@ -118,11 +109,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.forceUpdate(); // notification state changed - update
|
||||
};
|
||||
|
||||
private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
|
||||
if (room?.roomId !== this.props.room.roomId) return;
|
||||
this.setState({ hasUnsentEvents: this.countUnsentEvents() > 0 });
|
||||
};
|
||||
|
||||
private onRoomPropertyUpdate = (property: CachedRoomKey) => {
|
||||
if (property === CachedRoomKey.NotificationVolume) this.onNotificationUpdate();
|
||||
// else ignore - not important for this tile
|
||||
|
@ -178,12 +164,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
);
|
||||
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||
this.roomProps.on("Room.name", this.onRoomNameUpdate);
|
||||
this.props.room?.on("Room.name", this.onRoomNameUpdate);
|
||||
CommunityPrototypeStore.instance.on(
|
||||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -208,7 +193,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
MatrixClientPeg.get()?.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
|
@ -587,30 +571,17 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
/>;
|
||||
|
||||
let badge: React.ReactNode;
|
||||
if (!this.props.isMinimized) {
|
||||
if (!this.props.isMinimized && this.notificationState) {
|
||||
// aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
|
||||
if (this.state.hasUnsentEvents) {
|
||||
// hardcode the badge to a danger state when there's unsent messages
|
||||
badge = (
|
||||
<div className="mx_RoomTile_badgeContainer" aria-hidden="true">
|
||||
<NotificationBadge
|
||||
notification={StaticNotificationState.RED_EXCLAMATION}
|
||||
forceCount={false}
|
||||
roomId={this.props.room.roomId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (this.notificationState) {
|
||||
badge = (
|
||||
<div className="mx_RoomTile_badgeContainer" aria-hidden="true">
|
||||
<NotificationBadge
|
||||
notification={this.notificationState}
|
||||
forceCount={false}
|
||||
roomId={this.props.room.roomId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
badge = (
|
||||
<div className="mx_RoomTile_badgeContainer" aria-hidden="true">
|
||||
<NotificationBadge
|
||||
notification={this.notificationState}
|
||||
forceCount={false}
|
||||
roomId={this.props.room.roomId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let messagePreview = null;
|
||||
|
|
|
@ -31,8 +31,8 @@ import {
|
|||
textSerialize,
|
||||
unescapeMessage,
|
||||
} from '../../../editor/serialize';
|
||||
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||
import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import { findEditableEvent } from '../../../utils/EventUtils';
|
||||
import SendHistoryManager from "../../../SendHistoryManager";
|
||||
|
@ -347,15 +347,24 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
public async sendMessage(): Promise<void> {
|
||||
if (this.model.isEmpty) {
|
||||
const model = this.model;
|
||||
|
||||
if (model.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace emoticon at the end of the message
|
||||
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||
const caret = this.editorRef.current?.getCaret();
|
||||
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
||||
}
|
||||
|
||||
const replyToEvent = this.props.replyToEvent;
|
||||
let shouldSend = true;
|
||||
let content;
|
||||
|
||||
if (!containsEmote(this.model) && this.isSlashCommand()) {
|
||||
if (!containsEmote(model) && this.isSlashCommand()) {
|
||||
const [cmd, args, commandText] = this.getSlashCommand();
|
||||
if (cmd) {
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
|
@ -400,7 +409,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
}
|
||||
}
|
||||
|
||||
if (isQuickReaction(this.model)) {
|
||||
if (isQuickReaction(model)) {
|
||||
shouldSend = false;
|
||||
this.sendQuickReaction();
|
||||
}
|
||||
|
@ -410,7 +419,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
const { roomId } = this.props.room;
|
||||
if (!content) {
|
||||
content = createMessageContent(
|
||||
this.model,
|
||||
model,
|
||||
replyToEvent,
|
||||
this.props.replyInThread,
|
||||
this.props.permalinkCreator,
|
||||
|
@ -446,9 +455,9 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
||||
}
|
||||
|
||||
this.sendHistoryManager.save(this.model, replyToEvent);
|
||||
this.sendHistoryManager.save(model, replyToEvent);
|
||||
// clear composer
|
||||
this.model.reset([]);
|
||||
model.reset([]);
|
||||
this.editorRef.current?.clearUndoHistory();
|
||||
this.editorRef.current?.focus();
|
||||
this.clearStoredEditorState();
|
||||
|
|
|
@ -32,6 +32,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import GenericElementContextMenu from "../context_menus/GenericElementContextMenu";
|
||||
import { IApp } from "../../../stores/WidgetStore";
|
||||
|
||||
// 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.
|
||||
|
@ -256,12 +257,16 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack");
|
||||
|
||||
// FIXME: could this use the same code as other apps?
|
||||
const stickerApp = {
|
||||
const stickerApp: IApp = {
|
||||
id: stickerpickerWidget.id,
|
||||
url: stickerpickerWidget.content.url,
|
||||
name: stickerpickerWidget.content.name,
|
||||
type: stickerpickerWidget.content.type,
|
||||
data: stickerpickerWidget.content.data,
|
||||
roomId: stickerpickerWidget.content.roomId,
|
||||
eventId: stickerpickerWidget.content.eventId,
|
||||
avatar_url: stickerpickerWidget.content.avatar_url,
|
||||
creatorUserId: stickerpickerWidget.content.creatorUserId,
|
||||
};
|
||||
|
||||
stickersContent = (
|
||||
|
@ -287,9 +292,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
onEditClick={this.launchManageIntegrations}
|
||||
onDeleteClick={this.removeStickerpickerWidgets}
|
||||
showTitle={false}
|
||||
showCancel={false}
|
||||
showPopout={false}
|
||||
onMinimiseClick={this.onHideStickersClick}
|
||||
handleMinimisePointerEvents={true}
|
||||
userWidget={true}
|
||||
/>
|
||||
|
@ -345,16 +348,6 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Trigger hiding of the sticker picker overlay
|
||||
* @param {Event} ev Event that triggered the function call
|
||||
*/
|
||||
private onHideStickersClick = (ev: React.MouseEvent): void => {
|
||||
if (this.props.showStickers) {
|
||||
this.props.setShowStickers(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when the window is resized
|
||||
*/
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue