Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/19276
This commit is contained in:
commit
4ac29a431c
57 changed files with 2396 additions and 1035 deletions
|
@ -226,6 +226,11 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onClick = (ev: React.MouseEvent) => {
|
||||
// Don't allow clicks to escape the context menu wrapper
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
// don't let keyboard handling escape the context menu
|
||||
ev.stopPropagation();
|
||||
|
@ -383,6 +388,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
|
||||
style={{ ...position, ...wrapperStyle }}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onClick={this.onClick}
|
||||
onContextMenu={this.onContextMenuPreventBubbling}
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -48,6 +48,8 @@ import Spinner from "../views/elements/Spinner";
|
|||
import TileErrorBoundary from '../views/messages/TileErrorBoundary';
|
||||
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
||||
import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import { Action } from '../../dispatcher/actions';
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
||||
|
@ -287,6 +289,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
ghostReadMarkers,
|
||||
});
|
||||
}
|
||||
|
||||
const pendingEditItem = this.pendingEditItem;
|
||||
if (!this.props.editState && this.props.room && pendingEditItem) {
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: this.props.room.findEventById(pendingEditItem),
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private calculateRoomMembersCount = (): void => {
|
||||
|
@ -550,10 +561,14 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
return { nextEvent, nextTile };
|
||||
}
|
||||
|
||||
private get roomHasPendingEdit(): string {
|
||||
return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
|
||||
private get pendingEditItem(): string | undefined {
|
||||
try {
|
||||
return localStorage.getItem(`mx_edit_room_${this.props.room.roomId}_${this.context.timelineRenderingType}`);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getEventTiles(): ReactNode[] {
|
||||
this.eventNodes = {};
|
||||
|
||||
|
@ -663,13 +678,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
if (!this.props.editState && this.roomHasPendingEdit) {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "edit_event",
|
||||
event: this.props.room.findEventById(this.roomHasPendingEdit),
|
||||
});
|
||||
}
|
||||
|
||||
if (grouper) {
|
||||
ret.push(...grouper.getTiles());
|
||||
}
|
||||
|
|
|
@ -48,8 +48,8 @@ import { Layout } from "../../settings/Layout";
|
|||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import MatrixClientContext, { withMatrixClientHOC, MatrixClientProps } from "../../contexts/MatrixClientContext";
|
||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
|
@ -91,6 +91,7 @@ import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
|
|||
import SpaceStore from "../../stores/SpaceStore";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -102,7 +103,7 @@ if (DEBUG) {
|
|||
debuglog = logger.log.bind(console);
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
interface IRoomProps extends MatrixClientProps {
|
||||
threepidInvite: IThreepidInvite;
|
||||
oobData?: IOOBData;
|
||||
|
||||
|
@ -113,7 +114,7 @@ interface IProps {
|
|||
onRegistered?(credentials: IMatrixClientCreds): void;
|
||||
}
|
||||
|
||||
export interface IState {
|
||||
export interface IRoomState {
|
||||
room?: Room;
|
||||
roomId?: string;
|
||||
roomAlias?: string;
|
||||
|
@ -187,10 +188,12 @@ export interface IState {
|
|||
// if it did we don't want the room to be marked as read as soon as it is loaded.
|
||||
wasContextSwitch?: boolean;
|
||||
editState?: EditorStateTransfer;
|
||||
timelineRenderingType: TimelineRenderingType;
|
||||
liveTimeline?: EventTimeline;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RoomView")
|
||||
export default class RoomView extends React.Component<IProps, IState> {
|
||||
export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
private readonly dispatcherRef: string;
|
||||
private readonly roomStoreToken: EventSubscription;
|
||||
private readonly rightPanelStoreToken: EventSubscription;
|
||||
|
@ -247,6 +250,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
showDisplaynameChanges: true,
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
dragCounter: 0,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
liveTimeline: undefined,
|
||||
};
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -336,7 +341,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
const roomId = RoomViewStore.getRoomId();
|
||||
|
||||
const newState: Pick<IState, any> = {
|
||||
const newState: Pick<IRoomState, any> = {
|
||||
roomId,
|
||||
roomAlias: RoomViewStore.getRoomAlias(),
|
||||
roomLoading: RoomViewStore.isRoomLoading(),
|
||||
|
@ -808,7 +813,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.onSearchClick();
|
||||
break;
|
||||
|
||||
case "edit_event": {
|
||||
case Action.EditEvent: {
|
||||
// Quit early if we're trying to edit events in wrong rendering context
|
||||
if (payload.timelineRenderingType !== this.state.timelineRenderingType) return;
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({ editState }, () => {
|
||||
if (payload.event) {
|
||||
|
@ -932,6 +939,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.updateE2EStatus(room);
|
||||
this.updatePermissions(room);
|
||||
this.checkWidgets(room);
|
||||
|
||||
this.setState({
|
||||
liveTimeline: room.getLiveTimeline(),
|
||||
});
|
||||
};
|
||||
|
||||
private async calculateRecommendedVersion(room: Room) {
|
||||
|
@ -2086,3 +2097,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const RoomViewWithMatrixClient = withMatrixClientHOC(RoomView);
|
||||
export default RoomViewWithMatrixClient;
|
||||
|
|
|
@ -34,6 +34,8 @@ import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPan
|
|||
import { Action } from '../../dispatcher/actions';
|
||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -47,10 +49,14 @@ interface IProps {
|
|||
interface IState {
|
||||
replyToEvent?: MatrixEvent;
|
||||
thread?: Thread;
|
||||
editState?: EditorStateTransfer;
|
||||
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.ThreadView")
|
||||
export default class ThreadView extends React.Component<IProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
|
||||
private dispatcherRef: string;
|
||||
private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef();
|
||||
|
||||
|
@ -90,6 +96,23 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
this.setupThread(payload.event);
|
||||
}
|
||||
}
|
||||
switch (payload.action) {
|
||||
case Action.EditEvent: {
|
||||
// Quit early if it's not a thread context
|
||||
if (payload.timelineRenderingType !== TimelineRenderingType.Thread) return;
|
||||
// Quit early if that's not a thread event
|
||||
if (payload.event && !payload.event.getThread()) return;
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({ editState }, () => {
|
||||
if (payload.event) {
|
||||
this.timelinePanelRef.current?.scrollToEventIfNeeded(payload.event.getId());
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private setupThread = (mxEv: MatrixEvent) => {
|
||||
|
@ -124,44 +147,53 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<BaseCard
|
||||
className="mx_ThreadView"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer={true}
|
||||
>
|
||||
{ this.state.thread && (
|
||||
<TimelinePanel
|
||||
ref={this.timelinePanelRef}
|
||||
showReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadMarkers={false} // No RM support in thread's MVP
|
||||
sendReadReceiptOnLoad={false} // No RR support in thread's MVP
|
||||
timelineSet={this.state?.thread?.timelineSet}
|
||||
showUrlPreview={true}
|
||||
tileShape={TileShape.Thread}
|
||||
empty={<div>empty</div>}
|
||||
alwaysShowTimestamps={true}
|
||||
layout={Layout.Group}
|
||||
hideThreadedMessages={false}
|
||||
hidden={false}
|
||||
showReactions={true}
|
||||
className="mx_RoomView_messagePanel mx_GroupLayout"
|
||||
<RoomContext.Provider value={{
|
||||
...this.context,
|
||||
timelineRenderingType: TimelineRenderingType.Thread,
|
||||
liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(),
|
||||
}}>
|
||||
|
||||
<BaseCard
|
||||
className="mx_ThreadView"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer={true}
|
||||
>
|
||||
{ this.state.thread && (
|
||||
<TimelinePanel
|
||||
ref={this.timelinePanelRef}
|
||||
showReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadMarkers={false} // No RM support in thread's MVP
|
||||
sendReadReceiptOnLoad={false} // No RR support in thread's MVP
|
||||
timelineSet={this.state?.thread?.timelineSet}
|
||||
showUrlPreview={true}
|
||||
tileShape={TileShape.Thread}
|
||||
empty={<div>empty</div>}
|
||||
alwaysShowTimestamps={true}
|
||||
layout={Layout.Group}
|
||||
hideThreadedMessages={false}
|
||||
hidden={false}
|
||||
showReactions={true}
|
||||
className="mx_RoomView_messagePanel mx_GroupLayout"
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
membersLoaded={true}
|
||||
editState={this.state.editState}
|
||||
/>
|
||||
) }
|
||||
|
||||
{ this.state?.thread?.timelineSet && (<MessageComposer
|
||||
room={this.props.room}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
replyInThread={true}
|
||||
replyToEvent={this.state?.thread?.replyToEvent}
|
||||
showReplyPreview={false}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
membersLoaded={true}
|
||||
/>
|
||||
) }
|
||||
<MessageComposer
|
||||
room={this.props.room}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
replyInThread={true}
|
||||
replyToEvent={this.state?.thread?.replyToEvent}
|
||||
showReplyPreview={false}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>
|
||||
</BaseCard>
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>) }
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import * as sdk from '../../../index';
|
|||
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
|
||||
import SetupEncryptionBody from "./SetupEncryptionBody";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AccessibleButton from '../../views/elements/AccessibleButton';
|
||||
|
||||
interface IProps {
|
||||
onFinished: () => void;
|
||||
|
@ -27,6 +28,7 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
lostKeys: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.auth.CompleteSecurity")
|
||||
|
@ -36,12 +38,17 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
|||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.on("update", this.onStoreUpdate);
|
||||
store.start();
|
||||
this.state = { phase: store.phase };
|
||||
this.state = { phase: store.phase, lostKeys: store.lostKeys() };
|
||||
}
|
||||
|
||||
private onStoreUpdate = (): void => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
this.setState({ phase: store.phase });
|
||||
this.setState({ phase: store.phase, lostKeys: store.lostKeys() });
|
||||
};
|
||||
|
||||
private onSkipClick = (): void => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.skip();
|
||||
};
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
|
@ -53,15 +60,20 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
|||
public render() {
|
||||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
|
||||
const { phase } = this.state;
|
||||
const { phase, lostKeys } = this.state;
|
||||
let icon;
|
||||
let title;
|
||||
|
||||
if (phase === Phase.Loading) {
|
||||
return null;
|
||||
} else if (phase === Phase.Intro) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Verify this login");
|
||||
if (lostKeys) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Unable to verify this login");
|
||||
} else {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Verify this login");
|
||||
}
|
||||
} else if (phase === Phase.Done) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
|
||||
title = _t("Session verified");
|
||||
|
@ -71,16 +83,29 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
|||
} else if (phase === Phase.Busy) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Verify this login");
|
||||
} else if (phase === Phase.ConfirmReset) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Really reset verification keys?");
|
||||
} else if (phase === Phase.Finished) {
|
||||
// SetupEncryptionBody will take care of calling onFinished, we don't need to do anything
|
||||
} else {
|
||||
throw new Error(`Unknown phase ${phase}`);
|
||||
}
|
||||
|
||||
let skipButton;
|
||||
if (phase === Phase.Intro || phase === Phase.ConfirmReset) {
|
||||
skipButton = (
|
||||
<AccessibleButton onClick={this.onSkipClick} className="mx_CompleteSecurity_skip" aria-label={_t("Skip verification for now")} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<CompleteSecurityBody>
|
||||
<h2 className="mx_CompleteSecurity_header">
|
||||
{ icon }
|
||||
{ title }
|
||||
{ skipButton }
|
||||
</h2>
|
||||
<div className="mx_CompleteSecurity_body">
|
||||
<SetupEncryptionBody onFinished={this.props.onFinished} />
|
||||
|
|
|
@ -46,6 +46,7 @@ interface IState {
|
|||
phase: Phase;
|
||||
verificationRequest: VerificationRequest;
|
||||
backupInfo: IKeyBackupInfo;
|
||||
lostKeys: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.auth.SetupEncryptionBody")
|
||||
|
@ -62,6 +63,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
// Because of the latter, it lives in the state.
|
||||
verificationRequest: store.verificationRequest,
|
||||
backupInfo: store.backupInfo,
|
||||
lostKeys: store.lostKeys(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -75,6 +77,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
phase: store.phase,
|
||||
verificationRequest: store.verificationRequest,
|
||||
backupInfo: store.backupInfo,
|
||||
lostKeys: store.lostKeys(),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -105,11 +108,6 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
});
|
||||
};
|
||||
|
||||
private onSkipClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.skip();
|
||||
};
|
||||
|
||||
private onSkipConfirmClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.skipConfirm();
|
||||
|
@ -120,6 +118,22 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
store.returnAfterSkip();
|
||||
};
|
||||
|
||||
private onResetClick = (ev: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
ev.preventDefault();
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.reset();
|
||||
};
|
||||
|
||||
private onResetConfirmClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.resetConfirm();
|
||||
};
|
||||
|
||||
private onResetBackClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.returnAfterReset();
|
||||
};
|
||||
|
||||
private onDoneClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.done();
|
||||
|
@ -132,6 +146,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
public render() {
|
||||
const {
|
||||
phase,
|
||||
lostKeys,
|
||||
} = this.state;
|
||||
|
||||
if (this.state.verificationRequest) {
|
||||
|
@ -143,43 +158,67 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
isRoomEncrypted={false}
|
||||
/>;
|
||||
} else if (phase === Phase.Intro) {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
let recoveryKeyPrompt;
|
||||
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
||||
recoveryKeyPrompt = _t("Use Security Key or Phrase");
|
||||
} else if (store.keyInfo) {
|
||||
recoveryKeyPrompt = _t("Use Security Key");
|
||||
}
|
||||
if (lostKeys) {
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"It looks like you don't have a Security Key or any other devices you can " +
|
||||
"verify against. This device will not be able to access old encrypted messages. " +
|
||||
"In order to verify your identity on this device, you'll need to reset " +
|
||||
"your verification keys.",
|
||||
) }</p>
|
||||
|
||||
let useRecoveryKeyButton;
|
||||
if (recoveryKeyPrompt) {
|
||||
useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this.onUsePassphraseClick}>
|
||||
{ recoveryKeyPrompt }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let verifyButton;
|
||||
if (store.hasDevicesToVerifyAgainst) {
|
||||
verifyButton = <AccessibleButton kind="primary" onClick={this.onVerifyClick}>
|
||||
{ _t("Use another login") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"Verify your identity to access encrypted messages and prove your identity to others.",
|
||||
) }</p>
|
||||
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
{ verifyButton }
|
||||
{ useRecoveryKeyButton }
|
||||
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
|
||||
{ _t("Skip") }
|
||||
</AccessibleButton>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton kind="primary" onClick={this.onResetConfirmClick}>
|
||||
{ _t("Proceed with reset") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
} else {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
let recoveryKeyPrompt;
|
||||
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
||||
recoveryKeyPrompt = _t("Verify with Security Key or Phrase");
|
||||
} else if (store.keyInfo) {
|
||||
recoveryKeyPrompt = _t("Verify with Security Key");
|
||||
}
|
||||
|
||||
let useRecoveryKeyButton;
|
||||
if (recoveryKeyPrompt) {
|
||||
useRecoveryKeyButton = <AccessibleButton kind="primary" onClick={this.onUsePassphraseClick}>
|
||||
{ recoveryKeyPrompt }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let verifyButton;
|
||||
if (store.hasDevicesToVerifyAgainst) {
|
||||
verifyButton = <AccessibleButton kind="primary" onClick={this.onVerifyClick}>
|
||||
{ _t("Verify with another login") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"Verify your identity to access encrypted messages and prove your identity to others.",
|
||||
) }</p>
|
||||
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
{ verifyButton }
|
||||
{ useRecoveryKeyButton }
|
||||
</div>
|
||||
<div className="mx_SetupEncryptionBody_reset">
|
||||
{ _t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, {
|
||||
a: (sub) => <a
|
||||
href=""
|
||||
onClick={this.onResetClick}
|
||||
className="mx_SetupEncryptionBody_reset_link">{ sub }</a>,
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else if (phase === Phase.Done) {
|
||||
let message;
|
||||
if (this.state.backupInfo) {
|
||||
|
@ -215,14 +254,13 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
) }</p>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
className="warning"
|
||||
kind="secondary"
|
||||
kind="danger_outline"
|
||||
onClick={this.onSkipConfirmClick}
|
||||
>
|
||||
{ _t("Skip") }
|
||||
{ _t("I'll verify later") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
kind="primary"
|
||||
onClick={this.onSkipBackClick}
|
||||
>
|
||||
{ _t("Go Back") }
|
||||
|
@ -230,6 +268,30 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === Phase.ConfirmReset) {
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"Resetting your verification keys cannot be undone. After resetting, " +
|
||||
"you won't have access to old encrypted messages, and any friends who " +
|
||||
"have previously verified you will see security warnings until you " +
|
||||
"re-verify with them.",
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"Please only proceed if you're sure you've lost all of your other " +
|
||||
"devices and your security key.",
|
||||
) }</p>
|
||||
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton kind="danger_outline" onClick={this.onResetConfirmClick}>
|
||||
{ _t("Proceed with reset") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={this.onResetBackClick}>
|
||||
{ _t("Go Back") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === Phase.Busy || phase === Phase.Loading) {
|
||||
return <Spinner />;
|
||||
} else {
|
||||
|
|
|
@ -39,6 +39,8 @@ import dis from "../../../dispatcher/dispatcher";
|
|||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "./UserSettingsDialog";
|
||||
import TagOrderActions from "../../../actions/TagOrderActions";
|
||||
import { inviteUsersToRoom } from "../../../RoomInvite";
|
||||
import ProgressBar from "../elements/ProgressBar";
|
||||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
|
@ -90,10 +92,22 @@ export interface IGroupSummary {
|
|||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
enum Progress {
|
||||
NotStarted,
|
||||
ValidatingInputs,
|
||||
FetchingData,
|
||||
CreatingSpace,
|
||||
InvitingUsers,
|
||||
// anything beyond here is inviting user n - 4
|
||||
}
|
||||
|
||||
const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, groupId, onFinished }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const [progress, setProgress] = useState(Progress.NotStarted);
|
||||
const [numInvites, setNumInvites] = useState(0);
|
||||
const busy = progress > 0;
|
||||
|
||||
const [avatar, setAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||
const [name, setName] = useState("");
|
||||
|
@ -122,30 +136,34 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
|||
if (busy) return;
|
||||
|
||||
setError(null);
|
||||
setBusy(true);
|
||||
setProgress(Progress.ValidatingInputs);
|
||||
|
||||
// require & validate the space name field
|
||||
if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
|
||||
setBusy(false);
|
||||
setProgress(0);
|
||||
spaceNameField.current.focus();
|
||||
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
// validate the space name alias field but do not require it
|
||||
if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
|
||||
setBusy(false);
|
||||
setProgress(0);
|
||||
spaceAliasField.current.focus();
|
||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setProgress(Progress.FetchingData);
|
||||
|
||||
const [rooms, members, invitedMembers] = await Promise.all([
|
||||
cli.getGroupRooms(groupId).then(parseRoomsResponse) as Promise<IGroupRoom[]>,
|
||||
cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||
cli.getGroupInvitedUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||
]);
|
||||
|
||||
setNumInvites(members.length + invitedMembers.length);
|
||||
|
||||
const viaMap = new Map<string, string[]>();
|
||||
for (const { roomId, canonicalAlias } of rooms) {
|
||||
const room = cli.getRoom(roomId);
|
||||
|
@ -167,6 +185,8 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
|||
}
|
||||
}
|
||||
|
||||
setProgress(Progress.CreatingSpace);
|
||||
|
||||
const spaceAvatar = avatar !== undefined ? avatar : groupSummary.profile.avatar_url;
|
||||
const roomId = await createSpace(name, joinRule === JoinRule.Public, alias, topic, spaceAvatar, {
|
||||
creation_content: {
|
||||
|
@ -179,11 +199,16 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
|||
via: viaMap.get(roomId) || [],
|
||||
},
|
||||
})),
|
||||
invite: [...members, ...invitedMembers].map(m => m.userId).filter(m => m !== cli.getUserId()),
|
||||
// we do not specify the inviters here because Synapse applies a limit and this may cause it to trip
|
||||
}, {
|
||||
andView: false,
|
||||
});
|
||||
|
||||
setProgress(Progress.InvitingUsers);
|
||||
|
||||
const userIds = [...members, ...invitedMembers].map(m => m.userId).filter(m => m !== cli.getUserId());
|
||||
await inviteUsersToRoom(roomId, userIds, () => setProgress(p => p + 1));
|
||||
|
||||
// eagerly remove it from the community panel
|
||||
dis.dispatch(TagOrderActions.removeTag(cli, groupId));
|
||||
|
||||
|
@ -250,7 +275,7 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
|||
setError(e);
|
||||
}
|
||||
|
||||
setBusy(false);
|
||||
setProgress(Progress.NotStarted);
|
||||
};
|
||||
|
||||
let footer;
|
||||
|
@ -267,13 +292,41 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
|
|||
{ _t("Retry") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
} else if (busy) {
|
||||
let description: string;
|
||||
switch (progress) {
|
||||
case Progress.ValidatingInputs:
|
||||
case Progress.FetchingData:
|
||||
description = _t("Fetching data...");
|
||||
break;
|
||||
case Progress.CreatingSpace:
|
||||
description = _t("Creating Space...");
|
||||
break;
|
||||
case Progress.InvitingUsers:
|
||||
default:
|
||||
description = _t("Adding rooms... (%(progress)s out of %(count)s)", {
|
||||
count: numInvites,
|
||||
progress,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
footer = <span>
|
||||
<ProgressBar
|
||||
value={progress > Progress.FetchingData ? progress : 0}
|
||||
max={numInvites + Progress.InvitingUsers}
|
||||
/>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_progressText">
|
||||
{ description }
|
||||
</div>
|
||||
</span>;
|
||||
} else {
|
||||
footer = <>
|
||||
<AccessibleButton kind="primary_outline" disabled={busy} onClick={() => onFinished()}>
|
||||
<AccessibleButton kind="primary_outline" onClick={() => onFinished()}>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" disabled={busy} onClick={onCreateSpaceClick}>
|
||||
{ busy ? _t("Creating...") : _t("Create Space") }
|
||||
<AccessibleButton kind="primary" onClick={onCreateSpaceClick}>
|
||||
{ _t("Create Space") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import { Action } from '../../../dispatcher/actions';
|
|||
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
|
||||
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
|
||||
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import Toolbar from "../../../accessibility/Toolbar";
|
||||
import { RovingAccessibleTooltipButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
@ -128,11 +128,6 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
|
|||
</React.Fragment>;
|
||||
};
|
||||
|
||||
export enum ActionBarRenderingContext {
|
||||
Room,
|
||||
Thread
|
||||
}
|
||||
|
||||
interface IMessageActionBarProps {
|
||||
mxEvent: MatrixEvent;
|
||||
reactions?: Relations;
|
||||
|
@ -142,7 +137,6 @@ interface IMessageActionBarProps {
|
|||
permalinkCreator?: RoomPermalinkCreator;
|
||||
onFocusChange?: (menuDisplayed: boolean) => void;
|
||||
toggleThreadExpanded: () => void;
|
||||
renderingContext?: ActionBarRenderingContext;
|
||||
isQuoteExpanded?: boolean;
|
||||
}
|
||||
|
||||
|
@ -150,10 +144,6 @@ interface IMessageActionBarProps {
|
|||
export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
|
||||
public static contextType = RoomContext;
|
||||
|
||||
public static defaultProps = {
|
||||
renderingContext: ActionBarRenderingContext.Room,
|
||||
};
|
||||
|
||||
public componentDidMount(): void {
|
||||
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
|
||||
this.props.mxEvent.on("Event.status", this.onSent);
|
||||
|
@ -217,8 +207,9 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
|
||||
private onEditClick = (ev: React.MouseEvent): void => {
|
||||
dis.dispatch({
|
||||
action: 'edit_event',
|
||||
action: Action.EditEvent,
|
||||
event: this.props.mxEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -298,7 +289,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
// Like the resend button, the react and reply buttons need to appear before the edit.
|
||||
// The only catch is we do the reply button first so that we can make sure the react
|
||||
// button is the very first button without having to do length checks for `splice()`.
|
||||
if (this.context.canReply && this.props.renderingContext === ActionBarRenderingContext.Room) {
|
||||
if (this.context.canReply && this.context.timelineRenderingType === TimelineRenderingType.Room) {
|
||||
toolbarOpts.splice(0, 0, <>
|
||||
<RovingAccessibleTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
||||
|
|
|
@ -49,16 +49,18 @@ const EncryptionInfo: React.FC<IProps> = ({
|
|||
isSelfVerification,
|
||||
}: IProps) => {
|
||||
let content: JSX.Element;
|
||||
if (waitingForOtherParty || waitingForNetwork) {
|
||||
if (waitingForOtherParty && isSelfVerification) {
|
||||
content = (
|
||||
<div>
|
||||
{ _t("To proceed, please accept the verification request on your other login.") }
|
||||
</div>
|
||||
);
|
||||
} else if (waitingForOtherParty || waitingForNetwork) {
|
||||
let text: string;
|
||||
if (waitingForOtherParty) {
|
||||
if (isSelfVerification) {
|
||||
text = _t("Accept on your other login…");
|
||||
} else {
|
||||
text = _t("Waiting for %(displayName)s to accept…", {
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
});
|
||||
}
|
||||
text = _t("Waiting for %(displayName)s to accept…", {
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
});
|
||||
} else {
|
||||
text = _t("Accepting…");
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ import { parseEvent } from '../../../editor/deserialize';
|
|||
import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
@ -36,7 +35,7 @@ import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindin
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import SendHistoryManager from '../../../SendHistoryManager';
|
||||
import Modal from '../../../Modal';
|
||||
import { MsgType } from 'matrix-js-sdk/src/@types/event';
|
||||
import { MsgType, UNSTABLE_ELEMENT_REPLY_IN_THREAD } from 'matrix-js-sdk/src/@types/event';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
|
@ -46,6 +45,8 @@ import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { withMatrixClientHOC, MatrixClientProps } from '../../../contexts/MatrixClientContext';
|
||||
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
|
||||
|
||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||
const html = mxEvent.getContent().formatted_body;
|
||||
|
@ -66,7 +67,11 @@ function getTextReplyFallback(mxEvent: MatrixEvent): string {
|
|||
return "";
|
||||
}
|
||||
|
||||
function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IContent {
|
||||
function createEditContent(
|
||||
model: EditorModel,
|
||||
editedEvent: MatrixEvent,
|
||||
renderingContext?: TimelineRenderingType,
|
||||
): IContent {
|
||||
const isEmote = containsEmote(model);
|
||||
if (isEmote) {
|
||||
model = stripEmoteCommand(model);
|
||||
|
@ -99,41 +104,49 @@ function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IConte
|
|||
contentBody.formatted_body = `${htmlPrefix} * ${formattedBody}`;
|
||||
}
|
||||
|
||||
return Object.assign({
|
||||
const relation = {
|
||||
"m.new_content": newContent,
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.replace",
|
||||
"event_id": editedEvent.getId(),
|
||||
},
|
||||
}, contentBody);
|
||||
};
|
||||
|
||||
if (renderingContext === TimelineRenderingType.Thread) {
|
||||
relation['m.relates_to'][UNSTABLE_ELEMENT_REPLY_IN_THREAD.name] = true;
|
||||
}
|
||||
|
||||
return Object.assign(relation, contentBody);
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
interface IEditMessageComposerProps extends MatrixClientProps {
|
||||
editState: EditorStateTransfer;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
saveDisabled: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.EditMessageComposer")
|
||||
export default class EditMessageComposer extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
context!: React.ContextType<typeof MatrixClientContext>;
|
||||
class EditMessageComposer extends React.Component<IEditMessageComposerProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
context!: React.ContextType<typeof RoomContext>;
|
||||
|
||||
private readonly editorRef = createRef<BasicMessageComposer>();
|
||||
private readonly dispatcherRef: string;
|
||||
private model: EditorModel = null;
|
||||
|
||||
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
constructor(props: IEditMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
|
||||
super(props);
|
||||
this.context = context; // otherwise React will only set it prior to render due to type def above
|
||||
|
||||
const isRestored = this.createEditorModel();
|
||||
const ev = this.props.editState.getEvent();
|
||||
|
||||
const renderingContext = this.context.timelineRenderingType;
|
||||
const editContent = createEditContent(this.model, ev, renderingContext);
|
||||
this.state = {
|
||||
saveDisabled: !isRestored || !this.isContentModified(createEditContent(this.model, ev)["m.new_content"]),
|
||||
saveDisabled: !isRestored || !this.isContentModified(editContent["m.new_content"]),
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", this.saveStoredEditorState);
|
||||
|
@ -141,7 +154,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
private getRoom(): Room {
|
||||
return this.context.getRoom(this.props.editState.getEvent().getRoomId());
|
||||
return this.props.mxClient.getRoom(this.props.editState.getEvent().getRoomId());
|
||||
}
|
||||
|
||||
private onKeyDown = (event: KeyboardEvent): void => {
|
||||
|
@ -162,10 +175,17 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtStart()) {
|
||||
return;
|
||||
}
|
||||
const previousEvent = findEditableEvent(this.getRoom(), false,
|
||||
this.props.editState.getEvent().getId());
|
||||
const previousEvent = findEditableEvent({
|
||||
events: this.events,
|
||||
isForward: false,
|
||||
fromEventId: this.props.editState.getEvent().getId(),
|
||||
});
|
||||
if (previousEvent) {
|
||||
dis.dispatch({ action: 'edit_event', event: previousEvent });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: previousEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
|
@ -174,12 +194,24 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtEnd()) {
|
||||
return;
|
||||
}
|
||||
const nextEvent = findEditableEvent(this.getRoom(), true, this.props.editState.getEvent().getId());
|
||||
const nextEvent = findEditableEvent({
|
||||
events: this.events,
|
||||
isForward: true,
|
||||
fromEventId: this.props.editState.getEvent().getId(),
|
||||
});
|
||||
if (nextEvent) {
|
||||
dis.dispatch({ action: 'edit_event', event: nextEvent });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: nextEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
} else {
|
||||
this.clearStoredEditorState();
|
||||
dis.dispatch({ action: 'edit_event', event: null });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: null,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
}
|
||||
event.preventDefault();
|
||||
|
@ -189,16 +221,27 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
};
|
||||
|
||||
private get editorRoomKey(): string {
|
||||
return `mx_edit_room_${this.getRoom().roomId}`;
|
||||
return `mx_edit_room_${this.getRoom().roomId}_${this.context.timelineRenderingType}`;
|
||||
}
|
||||
|
||||
private get editorStateKey(): string {
|
||||
return `mx_edit_state_${this.props.editState.getEvent().getId()}`;
|
||||
}
|
||||
|
||||
private get events(): MatrixEvent[] {
|
||||
const liveTimelineEvents = this.context.liveTimeline.getEvents();
|
||||
const pendingEvents = this.getRoom().getPendingEvents();
|
||||
const isInThread = Boolean(this.props.editState.getEvent().getThread());
|
||||
return liveTimelineEvents.concat(isInThread ? [] : pendingEvents);
|
||||
}
|
||||
|
||||
private cancelEdit = (): void => {
|
||||
this.clearStoredEditorState();
|
||||
dis.dispatch({ action: "edit_event", event: null });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: null,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
};
|
||||
|
||||
|
@ -326,8 +369,8 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
||||
}
|
||||
|
||||
const editContent = createEditContent(this.model, editedEvent);
|
||||
const renderingContext = this.context.timelineRenderingType;
|
||||
const editContent = createEditContent(this.model, editedEvent, renderingContext);
|
||||
const newContent = editContent["m.new_content"];
|
||||
|
||||
let shouldSend = true;
|
||||
|
@ -381,7 +424,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
}
|
||||
if (shouldSend) {
|
||||
this.cancelPreviousPendingEdit();
|
||||
const prom = this.context.sendMessage(roomId, editContent);
|
||||
const prom = this.props.mxClient.sendMessage(roomId, editContent);
|
||||
this.clearStoredEditorState();
|
||||
dis.dispatch({ action: "message_sent" });
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
||||
|
@ -389,7 +432,11 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
// close the event editing and focus composer
|
||||
dis.dispatch({ action: "edit_event", event: null });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: null,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
};
|
||||
|
||||
|
@ -400,7 +447,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
previousEdit.status === EventStatus.QUEUED ||
|
||||
previousEdit.status === EventStatus.NOT_SENT
|
||||
)) {
|
||||
this.context.cancelPendingEvent(previousEdit);
|
||||
this.props.mxClient.cancelPendingEvent(previousEdit);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -428,7 +475,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
private createEditorModel(): boolean {
|
||||
const { editState } = this.props;
|
||||
const room = this.getRoom();
|
||||
const partCreator = new CommandPartCreator(room, this.context);
|
||||
const partCreator = new CommandPartCreator(room, this.props.mxClient);
|
||||
|
||||
let parts;
|
||||
let isRestored = false;
|
||||
|
@ -493,3 +540,6 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
const EditMessageComposerWithMatrixClient = withMatrixClientHOC(EditMessageComposer);
|
||||
export default EditMessageComposerWithMatrixClient;
|
||||
|
|
|
@ -53,7 +53,7 @@ import SenderProfile from '../messages/SenderProfile';
|
|||
import MessageTimestamp from '../messages/MessageTimestamp';
|
||||
import TooltipButton from '../elements/TooltipButton';
|
||||
import ReadReceiptMarker from "./ReadReceiptMarker";
|
||||
import MessageActionBar, { ActionBarRenderingContext } from "../messages/MessageActionBar";
|
||||
import MessageActionBar from "../messages/MessageActionBar";
|
||||
import ReactionsRow from '../messages/ReactionsRow';
|
||||
import { getEventDisplayInfo } from '../../../utils/EventUtils';
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
|
@ -1063,9 +1063,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
const showMessageActionBar = !isEditing && !this.props.forExport;
|
||||
const renderingContext = this.props.tileShape === TileShape.Thread
|
||||
? ActionBarRenderingContext.Thread
|
||||
: ActionBarRenderingContext.Room;
|
||||
const actionBar = showMessageActionBar ? <MessageActionBar
|
||||
mxEvent={this.props.mxEvent}
|
||||
reactions={this.state.reactions}
|
||||
|
@ -1073,7 +1070,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
getTile={this.getTile}
|
||||
getReplyThread={this.getReplyThread}
|
||||
onFocusChange={this.onActionBarFocusChange}
|
||||
renderingContext={renderingContext}
|
||||
isQuoteExpanded={isQuoteExpanded}
|
||||
toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)}
|
||||
/> : undefined;
|
||||
|
@ -1178,6 +1174,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
showUrlPreview={this.props.showUrlPreview}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
tileShape={this.props.tileShape}
|
||||
editState={this.props.editState}
|
||||
/>
|
||||
</div>,
|
||||
]);
|
||||
|
@ -1211,6 +1208,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
showUrlPreview={this.props.showUrlPreview}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
tileShape={this.props.tileShape}
|
||||
editState={this.props.editState}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
/>
|
||||
{ actionBar }
|
||||
</div>,
|
||||
|
@ -1231,6 +1230,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
editState={this.props.editState}
|
||||
/>
|
||||
</div>,
|
||||
<a
|
||||
|
|
|
@ -45,7 +45,7 @@ import { RecordingState } from "../../../audio/VoiceRecording";
|
|||
import Tooltip, { Alignment } from "../elements/Tooltip";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import { E2EStatus } from '../../../utils/ShieldUtils';
|
||||
import SendMessageComposer from "./SendMessageComposer";
|
||||
import SendMessageComposer, { SendMessageComposer as SendMessageComposerClass } from "./SendMessageComposer";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import EditorModel from "../../../editor/model";
|
||||
|
@ -219,8 +219,8 @@ interface IState {
|
|||
@replaceableComponent("views.rooms.MessageComposer")
|
||||
export default class MessageComposer extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private messageComposerInput: SendMessageComposer;
|
||||
private voiceRecordingButton: VoiceRecordComposerTile;
|
||||
private messageComposerInput = createRef<SendMessageComposerClass>();
|
||||
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
|
||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||
private instanceId: number;
|
||||
|
||||
|
@ -378,14 +378,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private sendMessage = async () => {
|
||||
if (this.state.haveRecording && this.voiceRecordingButton) {
|
||||
if (this.state.haveRecording && this.voiceRecordingButton.current) {
|
||||
// There shouldn't be any text message to send when a voice recording is active, so
|
||||
// just send out the voice recording.
|
||||
await this.voiceRecordingButton.send();
|
||||
await this.voiceRecordingButton.current?.send();
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageComposerInput.sendMessage();
|
||||
this.messageComposerInput.current?.sendMessage();
|
||||
};
|
||||
|
||||
private onChange = (model: EditorModel) => {
|
||||
|
@ -460,7 +460,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
buttons.push(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
|
||||
onClick={() => this.voiceRecordingButton?.onRecordStartEndClick()}
|
||||
onClick={() => this.voiceRecordingButton.current?.onRecordStartEndClick()}
|
||||
title={_t("Send voice message")}
|
||||
/>,
|
||||
);
|
||||
|
@ -521,7 +521,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
if (!this.state.tombstone && this.state.canSendMessages) {
|
||||
controls.push(
|
||||
<SendMessageComposer
|
||||
ref={(c) => this.messageComposerInput = c}
|
||||
ref={this.messageComposerInput}
|
||||
key="controls_input"
|
||||
room={this.props.room}
|
||||
placeholder={this.renderPlaceholderText()}
|
||||
|
@ -535,7 +535,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
controls.push(<VoiceRecordComposerTile
|
||||
key="controls_voice_record"
|
||||
ref={c => this.voiceRecordingButton = c}
|
||||
ref={this.voiceRecordingButton}
|
||||
room={this.props.room} />);
|
||||
} else if (this.state.tombstone) {
|
||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||
|
|
|
@ -35,6 +35,8 @@ import InviteReason from "../elements/InviteReason";
|
|||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const MemberEventHtmlReasonField = "io.element.html_reason";
|
||||
|
||||
|
@ -339,8 +341,10 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
}
|
||||
case MessageCase.NotLoggedIn: {
|
||||
title = _t("Join the conversation with an account");
|
||||
primaryActionLabel = _t("Sign Up");
|
||||
primaryActionHandler = this.onRegisterClick;
|
||||
if (SettingsStore.getValue(UIFeature.Registration)) {
|
||||
primaryActionLabel = _t("Sign Up");
|
||||
primaryActionHandler = this.onRegisterClick;
|
||||
}
|
||||
secondaryActionLabel = _t("Sign In");
|
||||
secondaryActionHandler = this.onLoginClick;
|
||||
if (this.props.previewLoading) {
|
||||
|
|
|
@ -19,6 +19,7 @@ import EMOJI_REGEX from 'emojibase-regex';
|
|||
import { IContent, MatrixEvent } 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 dis from '../../../dispatcher/dispatcher';
|
||||
import EditorModel from '../../../editor/model';
|
||||
|
@ -40,7 +41,7 @@ import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
|||
import Modal from '../../../Modal';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { withMatrixClientHOC, MatrixClientProps } from "../../../contexts/MatrixClientContext";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { containsEmoji } from "../../../effects/utils";
|
||||
import { CHAT_EFFECTS } from '../../../effects';
|
||||
|
@ -55,8 +56,7 @@ import ErrorDialog from "../dialogs/ErrorDialog";
|
|||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import RoomContext from '../../../contexts/RoomContext';
|
||||
|
||||
function addReplyToMessageContent(
|
||||
content: IContent,
|
||||
|
@ -130,7 +130,7 @@ export function isQuickReaction(model: EditorModel): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
interface ISendMessageComposerProps extends MatrixClientProps {
|
||||
room: Room;
|
||||
placeholder?: string;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
|
@ -141,10 +141,8 @@ interface IProps {
|
|||
}
|
||||
|
||||
@replaceableComponent("views.rooms.SendMessageComposer")
|
||||
export default class SendMessageComposer extends React.Component<IProps> {
|
||||
static contextType = MatrixClientContext;
|
||||
context!: React.ContextType<typeof MatrixClientContext>;
|
||||
|
||||
export class SendMessageComposer extends React.Component<ISendMessageComposerProps> {
|
||||
static contextType = RoomContext;
|
||||
private readonly prepareToEncrypt?: DebouncedFunc<() => void>;
|
||||
private readonly editorRef = createRef<BasicMessageComposer>();
|
||||
private model: EditorModel = null;
|
||||
|
@ -152,26 +150,25 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
private dispatcherRef: string;
|
||||
private sendHistoryManager: SendHistoryManager;
|
||||
|
||||
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
constructor(props: ISendMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
|
||||
super(props);
|
||||
this.context = context; // otherwise React will only set it prior to render due to type def above
|
||||
if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) {
|
||||
if (this.props.mxClient.isCryptoEnabled() && this.props.mxClient.isRoomEncrypted(this.props.room.roomId)) {
|
||||
this.prepareToEncrypt = throttle(() => {
|
||||
this.context.prepareToEncrypt(this.props.room);
|
||||
this.props.mxClient.prepareToEncrypt(this.props.room);
|
||||
}, 60000, { leading: true, trailing: false });
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", this.saveStoredEditorState);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IProps): void {
|
||||
public componentDidUpdate(prevProps: ISendMessageComposerProps): void {
|
||||
const replyToEventChanged = this.props.replyInThread && (this.props.replyToEvent !== prevProps.replyToEvent);
|
||||
if (replyToEventChanged) {
|
||||
this.model.reset([]);
|
||||
}
|
||||
|
||||
if (this.props.replyInThread && this.props.replyToEvent && (!prevProps.replyToEvent || replyToEventChanged)) {
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.context);
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.props.mxClient);
|
||||
const parts = this.restoreStoredEditorState(partCreator) || [];
|
||||
this.model.reset(parts);
|
||||
this.editorRef.current?.focus();
|
||||
|
@ -202,13 +199,20 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
case MessageComposerAction.EditPrevMessage:
|
||||
// selection must be collapsed and caret at start
|
||||
if (this.editorRef.current?.isSelectionCollapsed() && this.editorRef.current?.isCaretAtStart()) {
|
||||
const editEvent = findEditableEvent(this.props.room, false);
|
||||
const events =
|
||||
this.context.liveTimeline.getEvents()
|
||||
.concat(this.props.replyInThread ? [] : this.props.room.getPendingEvents());
|
||||
const editEvent = findEditableEvent({
|
||||
events,
|
||||
isForward: false,
|
||||
});
|
||||
if (editEvent) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
event.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'edit_event',
|
||||
action: Action.EditEvent,
|
||||
event: editEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -275,7 +279,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
private sendQuickReaction(): void {
|
||||
const timeline = this.props.room.getLiveTimeline();
|
||||
const timeline = this.context.liveTimeline();
|
||||
const events = timeline.getEvents();
|
||||
const reaction = this.model.parts[1].text;
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
|
@ -448,7 +452,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
decorateStartSendingTime(content);
|
||||
}
|
||||
|
||||
const prom = this.context.sendMessage(roomId, content);
|
||||
const prom = this.props.mxClient.sendMessage(roomId, content);
|
||||
if (replyToEvent) {
|
||||
// Clear reply_to_event as we put the message into the queue
|
||||
// if the send fails, retry will handle resending.
|
||||
|
@ -465,7 +469,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
});
|
||||
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
||||
prom.then(resp => {
|
||||
sendRoundTripMetric(this.context, roomId, resp.event_id);
|
||||
sendRoundTripMetric(this.props.mxClient, roomId, resp.event_id);
|
||||
});
|
||||
}
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
||||
|
@ -490,7 +494,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount() { // eslint-disable-line
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.context);
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.props.mxClient);
|
||||
const parts = this.restoreStoredEditorState(partCreator) || [];
|
||||
this.model = new EditorModel(parts, partCreator);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -577,7 +581,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
// it puts the filename in as text/plain which we want to ignore.
|
||||
if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) {
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
Array.from(clipboardData.files), this.props.room.roomId, this.context,
|
||||
Array.from(clipboardData.files), this.props.room.roomId, this.props.mxClient,
|
||||
);
|
||||
return true; // to skip internal onPaste handler
|
||||
}
|
||||
|
@ -608,3 +612,6 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SendMessageComposerWithMatrixClient = withMatrixClientHOC(SendMessageComposer);
|
||||
export default SendMessageComposerWithMatrixClient;
|
||||
|
|
|
@ -121,24 +121,24 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
let confirm;
|
||||
if (this.state.pending || this.state.cancelling) {
|
||||
if (this.state.pending && this.props.isSelf) {
|
||||
let text;
|
||||
// device shouldn't be null in this situation but it can be, eg. if the device is
|
||||
// logged out during verification
|
||||
if (this.props.device) {
|
||||
text = _t("Waiting for you to verify on your other session, %(deviceName)s (%(deviceId)s)…", {
|
||||
deviceName: this.props.device ? this.props.device.getDisplayName() : '',
|
||||
deviceId: this.props.device ? this.props.device.deviceId : '',
|
||||
});
|
||||
} else {
|
||||
text = _t("Waiting for you to verify on your other session…");
|
||||
}
|
||||
confirm = <p>{ text }</p>;
|
||||
} else if (this.state.pending || this.state.cancelling) {
|
||||
let text;
|
||||
if (this.state.pending) {
|
||||
if (this.props.isSelf) {
|
||||
// device shouldn't be null in this situation but it can be, eg. if the device is
|
||||
// logged out during verification
|
||||
if (this.props.device) {
|
||||
text = _t("Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…", {
|
||||
deviceName: this.props.device ? this.props.device.getDisplayName() : '',
|
||||
deviceId: this.props.device ? this.props.device.deviceId : '',
|
||||
});
|
||||
} else {
|
||||
text = _t("Waiting for your other session to verify…");
|
||||
}
|
||||
} else {
|
||||
const { displayName } = this.props;
|
||||
text = _t("Waiting for %(displayName)s to verify…", { displayName });
|
||||
}
|
||||
const { displayName } = this.props;
|
||||
text = _t("Waiting for %(displayName)s to verify…", { displayName });
|
||||
} else {
|
||||
text = _t("Cancelling…");
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue