Merge branch 'develop' into travis/remove-skinning
This commit is contained in:
commit
97efdf7094
54 changed files with 1559 additions and 431 deletions
|
@ -53,6 +53,7 @@ import { Action } from '../../dispatcher/actions';
|
|||
import { getEventDisplayInfo } from "../../utils/EventRenderingUtils";
|
||||
import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker";
|
||||
import { haveRendererForEvent } from "../../events/EventTileFactory";
|
||||
import { editorRoomKey } from "../../Editing";
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
||||
|
@ -306,9 +307,10 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
|
||||
const pendingEditItem = this.pendingEditItem;
|
||||
if (!this.props.editState && this.props.room && pendingEditItem) {
|
||||
const event = this.props.room.findEventById(pendingEditItem);
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: this.props.room.findEventById(pendingEditItem),
|
||||
event: !event?.isRedacted() ? event : null,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
|
@ -612,13 +614,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
if (!this.props.room) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
return localStorage.getItem(`mx_edit_room_${this.props.room.roomId}_${this.context.timelineRenderingType}`);
|
||||
return localStorage.getItem(editorRoomKey(this.props.room.roomId, this.context.timelineRenderingType));
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getEventTiles(): ReactNode[] {
|
||||
let i;
|
||||
|
||||
|
@ -721,10 +725,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
): ReactNode[] {
|
||||
const ret = [];
|
||||
|
||||
const isEditing = this.props.editState &&
|
||||
this.props.editState.getEvent().getId() === mxEv.getId();
|
||||
// local echoes have a fake date, which could even be yesterday. Treat them
|
||||
// as 'today' for the date separators.
|
||||
const isEditing = this.props.editState?.getEvent().getId() === mxEv.getId();
|
||||
// local echoes have a fake date, which could even be yesterday. Treat them as 'today' for the date separators.
|
||||
let ts1 = mxEv.getTs();
|
||||
let eventDate = mxEv.getDate();
|
||||
if (mxEv.status) {
|
||||
|
|
|
@ -364,7 +364,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
this.checkWidgets(this.state.room);
|
||||
};
|
||||
|
||||
private checkWidgets = (room) => {
|
||||
private checkWidgets = (room: Room): void => {
|
||||
this.setState({
|
||||
hasPinnedWidgets: WidgetLayoutStore.instance.hasPinnedWidgets(room),
|
||||
mainSplitContentType: this.getMainSplitContentType(room),
|
||||
|
@ -372,7 +372,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
});
|
||||
};
|
||||
|
||||
private getMainSplitContentType = (room) => {
|
||||
private getMainSplitContentType = (room: Room) => {
|
||||
if (SettingsStore.getValue("feature_voice_rooms") && room.isCallRoom()) {
|
||||
return MainSplitContentType.Video;
|
||||
}
|
||||
|
@ -1981,11 +1981,11 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
);
|
||||
|
||||
let messageComposer; let searchInfo;
|
||||
const canSpeak = (
|
||||
const showComposer = (
|
||||
// joined and not showing search results
|
||||
myMembership === 'join' && !this.state.searchResults
|
||||
);
|
||||
if (canSpeak) {
|
||||
if (showComposer) {
|
||||
messageComposer =
|
||||
<MessageComposer
|
||||
room={this.state.room}
|
||||
|
@ -2101,10 +2101,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
const showChatEffects = SettingsStore.getValue('showChatEffects');
|
||||
|
||||
let mainSplitBody;
|
||||
let mainSplitBody: React.ReactFragment;
|
||||
let mainSplitContentClassName: string;
|
||||
// Decide what to show in the main split
|
||||
switch (this.state.mainSplitContentType) {
|
||||
case MainSplitContentType.Timeline:
|
||||
mainSplitContentClassName = "mx_MainSplit_timeline";
|
||||
mainSplitBody = <>
|
||||
<Measured
|
||||
sensor={this.roomViewBody.current}
|
||||
|
@ -2124,16 +2126,21 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
</>;
|
||||
break;
|
||||
case MainSplitContentType.MaximisedWidget:
|
||||
mainSplitBody = <AppsDrawer
|
||||
room={this.state.room}
|
||||
userId={this.context.credentials.userId}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showApps={true}
|
||||
/>;
|
||||
mainSplitContentClassName = "mx_MainSplit_maximisedWidget";
|
||||
mainSplitBody = <>
|
||||
<AppsDrawer
|
||||
room={this.state.room}
|
||||
userId={this.context.credentials.userId}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showApps={true}
|
||||
/>
|
||||
{ previewBar }
|
||||
</>;
|
||||
break;
|
||||
case MainSplitContentType.Video: {
|
||||
const app = getVoiceChannel(this.state.room.roomId);
|
||||
if (!app) break;
|
||||
mainSplitContentClassName = "mx_MainSplit_video";
|
||||
mainSplitBody = <AppTile
|
||||
app={app}
|
||||
room={this.state.room}
|
||||
|
@ -2145,6 +2152,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
/>;
|
||||
}
|
||||
}
|
||||
const mainSplitContentClasses = classNames("mx_RoomView_body", mainSplitContentClassName);
|
||||
|
||||
let excludedRightPanelPhaseButtons = [RightPanelPhases.Timeline];
|
||||
let onAppsClick = this.onAppsClick;
|
||||
let onForgetClick = this.onForgetClick;
|
||||
|
@ -2160,6 +2169,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
onForgetClick = null;
|
||||
onSearchClick = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RoomContext.Provider value={this.state}>
|
||||
<main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
|
||||
|
@ -2181,7 +2191,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons}
|
||||
/>
|
||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||
<div className="mx_RoomView_body" ref={this.roomViewBody} data-layout={this.state.layout}>
|
||||
<div className={mainSplitContentClasses} ref={this.roomViewBody} data-layout={this.state.layout}>
|
||||
{ mainSplitBody }
|
||||
</div>
|
||||
</MainSplit>
|
||||
|
|
|
@ -50,6 +50,7 @@ import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
|||
import Measured from '../views/elements/Measured';
|
||||
import PosthogTrackers from "../../PosthogTrackers";
|
||||
import { ButtonEvent } from "../views/elements/AccessibleButton";
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -104,9 +105,19 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
public componentWillUnmount(): void {
|
||||
this.teardownThread();
|
||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const roomId = this.props.mxEvent.getRoomId();
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
room.removeListener(ThreadEvent.New, this.onNewThread);
|
||||
SettingsStore.unwatchSetting(this.layoutWatcherRef);
|
||||
|
||||
const hasRoomChanged = RoomViewStore.getRoomId() !== roomId;
|
||||
if (this.props.isInitialEventHighlighted && !hasRoomChanged) {
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.props.room.roomId,
|
||||
metricsTrigger: undefined, // room doesn't change
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps) {
|
||||
|
@ -204,7 +215,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onScroll = (): void => {
|
||||
private resetHighlightedEvent = (): void => {
|
||||
if (this.props.initialEvent && this.props.isInitialEventHighlighted) {
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
|
@ -361,7 +372,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
editState={this.state.editState}
|
||||
eventId={this.props.initialEvent?.getId()}
|
||||
highlightedEventId={highlightedEventId}
|
||||
onUserScroll={this.onScroll}
|
||||
onUserScroll={this.resetHighlightedEvent}
|
||||
onPaginationRequest={this.onPaginationRequest}
|
||||
/>
|
||||
</div> }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, ReactNode, RefObject } from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
import PlayPauseButton from "./PlayPauseButton";
|
||||
import { formatBytes } from "../../../utils/FormattingUtils";
|
||||
|
@ -23,40 +23,8 @@ import { _t } from "../../../languageHandler";
|
|||
import SeekBar from "./SeekBar";
|
||||
import PlaybackClock from "./PlaybackClock";
|
||||
import AudioPlayerBase from "./AudioPlayerBase";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
|
||||
export default class AudioPlayer extends AudioPlayerBase {
|
||||
private playPauseRef: RefObject<PlayPauseButton> = createRef();
|
||||
private seekRef: RefObject<SeekBar> = createRef();
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
let handled = true;
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||
|
||||
switch (action) {
|
||||
case KeyBindingAction.Space:
|
||||
this.playPauseRef.current?.toggleState();
|
||||
break;
|
||||
case KeyBindingAction.ArrowLeft:
|
||||
this.seekRef.current?.left();
|
||||
break;
|
||||
case KeyBindingAction.ArrowRight:
|
||||
this.seekRef.current?.right();
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// stopPropagation() prevents the FocusComposer catch-all from triggering,
|
||||
// but we need to do it on key down instead of press (even though the user
|
||||
// interaction is typically on press).
|
||||
if (handled) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
protected renderFileSize(): string {
|
||||
const bytes = this.props.playback.sizeBytes;
|
||||
if (!bytes) return null;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,14 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from "react";
|
||||
import React, { createRef, ReactNode, RefObject } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { Playback, PlaybackState } from "../../../audio/Playback";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import SeekBar from "./SeekBar";
|
||||
import PlayPauseButton from "./PlayPauseButton";
|
||||
|
||||
interface IProps {
|
||||
export interface IProps {
|
||||
// Playback instance to render. Cannot change during component lifecycle: create
|
||||
// an all-new component instead.
|
||||
playback: Playback;
|
||||
|
@ -34,8 +38,11 @@ interface IState {
|
|||
error?: boolean;
|
||||
}
|
||||
|
||||
export default abstract class AudioPlayerBase extends React.PureComponent<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
export default abstract class AudioPlayerBase<T extends IProps = IProps> extends React.PureComponent<T, IState> {
|
||||
protected seekRef: RefObject<SeekBar> = createRef();
|
||||
protected playPauseRef: RefObject<PlayPauseButton> = createRef();
|
||||
|
||||
constructor(props: T) {
|
||||
super(props);
|
||||
|
||||
// Playback instances can be reused in the composer
|
||||
|
@ -54,6 +61,33 @@ export default abstract class AudioPlayerBase extends React.PureComponent<IProps
|
|||
});
|
||||
}
|
||||
|
||||
protected onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
let handled = true;
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||
|
||||
switch (action) {
|
||||
case KeyBindingAction.Space:
|
||||
this.playPauseRef.current?.toggleState();
|
||||
break;
|
||||
case KeyBindingAction.ArrowLeft:
|
||||
this.seekRef.current?.left();
|
||||
break;
|
||||
case KeyBindingAction.ArrowRight:
|
||||
this.seekRef.current?.right();
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// stopPropagation() prevents the FocusComposer catch-all from triggering,
|
||||
// but we need to do it on key down instead of press (even though the user
|
||||
// interaction is typically on press).
|
||||
if (handled) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
private onPlaybackUpdate = (ev: PlaybackState) => {
|
||||
this.setState({ playbackPhase: ev });
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -18,28 +18,49 @@ import React, { ReactNode } from "react";
|
|||
|
||||
import PlayPauseButton from "./PlayPauseButton";
|
||||
import PlaybackClock from "./PlaybackClock";
|
||||
import AudioPlayerBase, { IProps as IAudioPlayerBaseProps } from "./AudioPlayerBase";
|
||||
import SeekBar from "./SeekBar";
|
||||
import PlaybackWaveform from "./PlaybackWaveform";
|
||||
import AudioPlayerBase from "./AudioPlayerBase";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
|
||||
export default class RecordingPlayback extends AudioPlayerBase {
|
||||
static contextType = RoomContext;
|
||||
public context!: React.ContextType<typeof RoomContext>;
|
||||
interface IProps extends IAudioPlayerBaseProps {
|
||||
/**
|
||||
* When true, use a waveform instead of a seek bar
|
||||
*/
|
||||
withWaveform?: boolean;
|
||||
}
|
||||
|
||||
private get isWaveformable(): boolean {
|
||||
return this.context.timelineRenderingType !== TimelineRenderingType.Notification
|
||||
&& this.context.timelineRenderingType !== TimelineRenderingType.File
|
||||
&& this.context.timelineRenderingType !== TimelineRenderingType.Pinned;
|
||||
export default class RecordingPlayback extends AudioPlayerBase<IProps> {
|
||||
// This component is rendered in two ways: the composer and timeline. They have different
|
||||
// rendering properties (specifically the difference of a waveform or not).
|
||||
|
||||
private renderWaveformLook(): ReactNode {
|
||||
return <>
|
||||
<PlaybackClock playback={this.props.playback} />
|
||||
<PlaybackWaveform playback={this.props.playback} />
|
||||
</>;
|
||||
}
|
||||
|
||||
private renderSeekableLook(): ReactNode {
|
||||
return <>
|
||||
<SeekBar
|
||||
playback={this.props.playback}
|
||||
tabIndex={-1} // prevent tabbing into the bar
|
||||
playbackPhase={this.state.playbackPhase}
|
||||
ref={this.seekRef}
|
||||
/>
|
||||
<PlaybackClock playback={this.props.playback} />
|
||||
</>;
|
||||
}
|
||||
|
||||
protected renderComponent(): ReactNode {
|
||||
const shapeClass = !this.isWaveformable ? 'mx_VoiceMessagePrimaryContainer_noWaveform' : '';
|
||||
|
||||
return (
|
||||
<div className={'mx_MediaBody mx_VoiceMessagePrimaryContainer ' + shapeClass}>
|
||||
<PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} />
|
||||
<PlaybackClock playback={this.props.playback} />
|
||||
{ this.isWaveformable && <PlaybackWaveform playback={this.props.playback} /> }
|
||||
<div className="mx_MediaBody mx_VoiceMessagePrimaryContainer" onKeyDown={this.onKeyDown}>
|
||||
<PlayPauseButton
|
||||
playback={this.props.playback}
|
||||
playbackPhase={this.state.playbackPhase}
|
||||
ref={this.playPauseRef}
|
||||
/>
|
||||
{ this.props.withWaveform ? this.renderWaveformLook() : this.renderSeekableLook() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,11 +21,31 @@ import { useEventEmitterState } from '../../../hooks/useEventEmitter';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../stores/OwnBeaconStore';
|
||||
import { Icon as LiveLocationIcon } from '../../../../res/img/location/live-location.svg';
|
||||
import { ViewRoomPayload } from '../../../dispatcher/payloads/ViewRoomPayload';
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import dispatcher from '../../../dispatcher/dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
interface Props {
|
||||
isMinimized?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose the most relevant beacon
|
||||
* and get its roomId
|
||||
*/
|
||||
const chooseBestBeaconRoomId = (liveBeaconIds, errorBeaconIds): string | undefined => {
|
||||
// both lists are ordered by creation timestamp in store
|
||||
// so select latest beacon
|
||||
const beaconId = errorBeaconIds?.[0] ?? liveBeaconIds?.[0];
|
||||
if (!beaconId) {
|
||||
return undefined;
|
||||
}
|
||||
const beacon = OwnBeaconStore.instance.getBeaconById(beaconId);
|
||||
|
||||
return beacon?.roomId;
|
||||
};
|
||||
|
||||
const LeftPanelLiveShareWarning: React.FC<Props> = ({ isMinimized }) => {
|
||||
const isMonitoringLiveLocation = useEventEmitterState(
|
||||
OwnBeaconStore.instance,
|
||||
|
@ -33,18 +53,48 @@ const LeftPanelLiveShareWarning: React.FC<Props> = ({ isMinimized }) => {
|
|||
() => OwnBeaconStore.instance.isMonitoringLiveLocation,
|
||||
);
|
||||
|
||||
const beaconIdsWithWireError = useEventEmitterState(
|
||||
OwnBeaconStore.instance,
|
||||
OwnBeaconStoreEvent.WireError,
|
||||
() => OwnBeaconStore.instance.getLiveBeaconIdsWithWireError(),
|
||||
);
|
||||
|
||||
const liveBeaconIds = useEventEmitterState(
|
||||
OwnBeaconStore.instance,
|
||||
OwnBeaconStoreEvent.LivenessChange,
|
||||
() => OwnBeaconStore.instance.getLiveBeaconIds(),
|
||||
);
|
||||
|
||||
const hasWireErrors = !!beaconIdsWithWireError.length;
|
||||
|
||||
if (!isMonitoringLiveLocation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div
|
||||
const relevantBeaconRoomId = chooseBestBeaconRoomId(liveBeaconIds, beaconIdsWithWireError);
|
||||
|
||||
const onWarningClick = relevantBeaconRoomId ? () => {
|
||||
dispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: relevantBeaconRoomId,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
} : undefined;
|
||||
|
||||
const label = hasWireErrors ?
|
||||
_t('An error occured whilst sharing your live location') :
|
||||
_t('You are sharing your live location');
|
||||
|
||||
return <AccessibleButton
|
||||
className={classNames('mx_LeftPanelLiveShareWarning', {
|
||||
'mx_LeftPanelLiveShareWarning__minimized': isMinimized,
|
||||
'mx_LeftPanelLiveShareWarning__error': hasWireErrors,
|
||||
})}
|
||||
title={isMinimized ? _t('You are sharing your live location') : undefined}
|
||||
title={isMinimized ? label : undefined}
|
||||
onClick={onWarningClick}
|
||||
>
|
||||
{ isMinimized ? <LiveLocationIcon height={10} /> : _t('You are sharing your live location') }
|
||||
</div>;
|
||||
{ isMinimized ? <LiveLocationIcon height={10} /> : label }
|
||||
</AccessibleButton>;
|
||||
};
|
||||
|
||||
export default LeftPanelLiveShareWarning;
|
||||
|
|
|
@ -18,19 +18,16 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||
import classNames from 'classnames';
|
||||
import { Room, Beacon } from 'matrix-js-sdk/src/matrix';
|
||||
|
||||
import { formatDuration } from '../../../DateUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
|
||||
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../stores/OwnBeaconStore';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import StyledLiveBeaconIcon from './StyledLiveBeaconIcon';
|
||||
import { formatDuration } from '../../../DateUtils';
|
||||
import { getBeaconMsUntilExpiry, sortBeaconsByLatestExpiry } from '../../../utils/beacon';
|
||||
import Spinner from '../elements/Spinner';
|
||||
import { useInterval } from '../../../hooks/useTimeout';
|
||||
|
||||
interface Props {
|
||||
roomId: Room['roomId'];
|
||||
}
|
||||
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../stores/OwnBeaconStore';
|
||||
import { getBeaconMsUntilExpiry, sortBeaconsByLatestExpiry } from '../../../utils/beacon';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Spinner from '../elements/Spinner';
|
||||
import StyledLiveBeaconIcon from './StyledLiveBeaconIcon';
|
||||
import { Icon as CloseIcon } from '../../../../res/img/image-view/close.svg';
|
||||
|
||||
const MINUTE_MS = 60000;
|
||||
const HOUR_MS = MINUTE_MS * 60;
|
||||
|
@ -72,33 +69,28 @@ const useMsRemaining = (beacon: Beacon): number => {
|
|||
type LiveBeaconsState = {
|
||||
beacon?: Beacon;
|
||||
onStopSharing?: () => void;
|
||||
onResetWireError?: () => void;
|
||||
stoppingInProgress?: boolean;
|
||||
hasStopSharingError?: boolean;
|
||||
hasWireError?: boolean;
|
||||
};
|
||||
const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
|
||||
const useLiveBeacons = (liveBeaconIds: string[], roomId: string): LiveBeaconsState => {
|
||||
const [stoppingInProgress, setStoppingInProgress] = useState(false);
|
||||
const [error, setError] = useState<Error>();
|
||||
|
||||
// do we have an active geolocation.watchPosition
|
||||
const isMonitoringLiveLocation = useEventEmitterState(
|
||||
const hasWireError = useEventEmitterState(
|
||||
OwnBeaconStore.instance,
|
||||
OwnBeaconStoreEvent.MonitoringLivePosition,
|
||||
() => OwnBeaconStore.instance.isMonitoringLiveLocation,
|
||||
);
|
||||
|
||||
const liveBeaconIds = useEventEmitterState(
|
||||
OwnBeaconStore.instance,
|
||||
OwnBeaconStoreEvent.LivenessChange,
|
||||
() => OwnBeaconStore.instance.getLiveBeaconIds(roomId),
|
||||
OwnBeaconStoreEvent.WireError,
|
||||
() =>
|
||||
OwnBeaconStore.instance.hasWireErrors(roomId),
|
||||
);
|
||||
|
||||
// reset stopping in progress on change in live ids
|
||||
useEffect(() => {
|
||||
setStoppingInProgress(false);
|
||||
setError(undefined);
|
||||
}, [liveBeaconIds]);
|
||||
|
||||
if (!isMonitoringLiveLocation || !liveBeaconIds?.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// select the beacon with latest expiry to display expiry time
|
||||
const beacon = liveBeaconIds.map(beaconId => OwnBeaconStore.instance.getBeaconById(beaconId))
|
||||
.sort(sortBeaconsByLatestExpiry)
|
||||
|
@ -112,11 +104,23 @@ const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
|
|||
// only clear loading in case of error
|
||||
// to avoid flash of not-loading state
|
||||
// after beacons have been stopped but we wait for sync
|
||||
setError(error);
|
||||
setStoppingInProgress(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { onStopSharing, beacon, stoppingInProgress };
|
||||
const onResetWireError = () => {
|
||||
liveBeaconIds.map(beaconId => OwnBeaconStore.instance.resetWireError(beaconId));
|
||||
};
|
||||
|
||||
return {
|
||||
onStopSharing,
|
||||
onResetWireError,
|
||||
beacon,
|
||||
stoppingInProgress,
|
||||
hasWireError,
|
||||
hasStopSharingError: !!error,
|
||||
};
|
||||
};
|
||||
|
||||
const LiveTimeRemaining: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
|
||||
|
@ -131,39 +135,103 @@ const LiveTimeRemaining: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
|
|||
>{ liveTimeRemaining }</span>;
|
||||
};
|
||||
|
||||
const RoomLiveShareWarning: React.FC<Props> = ({ roomId }) => {
|
||||
const getLabel = (hasWireError: boolean, hasStopSharingError: boolean): string => {
|
||||
if (hasWireError) {
|
||||
return _t('An error occured whilst sharing your live location, please try again');
|
||||
}
|
||||
if (hasStopSharingError) {
|
||||
return _t('An error occurred while stopping your live location, please try again');
|
||||
}
|
||||
return _t('You are sharing your live location');
|
||||
};
|
||||
|
||||
interface RoomLiveShareWarningInnerProps {
|
||||
liveBeaconIds: string[];
|
||||
roomId: Room['roomId'];
|
||||
}
|
||||
const RoomLiveShareWarningInner: React.FC<RoomLiveShareWarningInnerProps> = ({ liveBeaconIds, roomId }) => {
|
||||
const {
|
||||
onStopSharing,
|
||||
onResetWireError,
|
||||
beacon,
|
||||
stoppingInProgress,
|
||||
} = useLiveBeacons(roomId);
|
||||
hasStopSharingError,
|
||||
hasWireError,
|
||||
} = useLiveBeacons(liveBeaconIds, roomId);
|
||||
|
||||
if (!beacon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasError = hasStopSharingError || hasWireError;
|
||||
|
||||
const onButtonClick = () => {
|
||||
if (hasWireError) {
|
||||
onResetWireError();
|
||||
} else {
|
||||
onStopSharing();
|
||||
}
|
||||
};
|
||||
|
||||
return <div
|
||||
className={classNames('mx_RoomLiveShareWarning')}
|
||||
>
|
||||
<StyledLiveBeaconIcon className="mx_RoomLiveShareWarning_icon" />
|
||||
<StyledLiveBeaconIcon className="mx_RoomLiveShareWarning_icon" withError={hasError} />
|
||||
|
||||
<span className="mx_RoomLiveShareWarning_label">
|
||||
{ _t('You are sharing your live location') }
|
||||
{ getLabel(hasWireError, hasStopSharingError) }
|
||||
</span>
|
||||
|
||||
{ stoppingInProgress ?
|
||||
<span className='mx_RoomLiveShareWarning_spinner'><Spinner h={16} w={16} /></span> :
|
||||
<LiveTimeRemaining beacon={beacon} />
|
||||
{ stoppingInProgress &&
|
||||
<span className='mx_RoomLiveShareWarning_spinner'><Spinner h={16} w={16} /></span>
|
||||
}
|
||||
{ !stoppingInProgress && !hasError && <LiveTimeRemaining beacon={beacon} /> }
|
||||
|
||||
<AccessibleButton
|
||||
data-test-id='room-live-share-stop-sharing'
|
||||
onClick={onStopSharing}
|
||||
data-test-id='room-live-share-primary-button'
|
||||
onClick={onButtonClick}
|
||||
kind='danger'
|
||||
element='button'
|
||||
disabled={stoppingInProgress}
|
||||
>
|
||||
{ _t('Stop sharing') }
|
||||
{ hasError ? _t('Retry') : _t('Stop sharing') }
|
||||
</AccessibleButton>
|
||||
{ hasWireError && <AccessibleButton
|
||||
data-test-id='room-live-share-wire-error-close-button'
|
||||
title={_t('Stop sharing and close')}
|
||||
element='button'
|
||||
className='mx_RoomLiveShareWarning_closeButton'
|
||||
onClick={onStopSharing}
|
||||
>
|
||||
<CloseIcon className='mx_RoomLiveShareWarning_closeButtonIcon' />
|
||||
</AccessibleButton> }
|
||||
</div>;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
roomId: Room['roomId'];
|
||||
}
|
||||
const RoomLiveShareWarning: React.FC<Props> = ({ roomId }) => {
|
||||
// do we have an active geolocation.watchPosition
|
||||
const isMonitoringLiveLocation = useEventEmitterState(
|
||||
OwnBeaconStore.instance,
|
||||
OwnBeaconStoreEvent.MonitoringLivePosition,
|
||||
() => OwnBeaconStore.instance.isMonitoringLiveLocation,
|
||||
);
|
||||
|
||||
const liveBeaconIds = useEventEmitterState(
|
||||
OwnBeaconStore.instance,
|
||||
OwnBeaconStoreEvent.LivenessChange,
|
||||
() => OwnBeaconStore.instance.getLiveBeaconIds(roomId),
|
||||
);
|
||||
|
||||
if (!isMonitoringLiveLocation || !liveBeaconIds.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// split into outer/inner to avoid watching various parts of live beacon state
|
||||
// when there are none
|
||||
return <RoomLiveShareWarningInner liveBeaconIds={liveBeaconIds} roomId={roomId} />;
|
||||
};
|
||||
|
||||
export default RoomLiveShareWarning;
|
||||
|
|
|
@ -19,10 +19,14 @@ import classNames from 'classnames';
|
|||
|
||||
import { Icon as LiveLocationIcon } from '../../../../res/img/location/live-location.svg';
|
||||
|
||||
const StyledLiveBeaconIcon: React.FC<React.SVGProps<SVGSVGElement>> = ({ className, ...props }) =>
|
||||
interface Props extends React.SVGProps<SVGSVGElement> {
|
||||
// use error styling when true
|
||||
withError?: boolean;
|
||||
}
|
||||
const StyledLiveBeaconIcon: React.FC<Props> = ({ className, withError, ...props }) =>
|
||||
<LiveLocationIcon
|
||||
{...props}
|
||||
className={classNames('mx_StyledLiveBeaconIcon', className)}
|
||||
className={classNames('mx_StyledLiveBeaconIcon', className, { 'mx_StyledLiveBeaconIcon_error': withError })}
|
||||
/>;
|
||||
|
||||
export default StyledLiveBeaconIcon;
|
||||
|
|
|
@ -219,6 +219,9 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
|
||||
const isUploading = ContentMessages.sharedInstance().getCurrentUploads(this.props.composerRelation).length > 0;
|
||||
|
||||
const myMembership = this.props.room.getMyMembership();
|
||||
const showComposer = myMembership === "join";
|
||||
|
||||
return (
|
||||
<RoomContext.Provider value={{
|
||||
...this.context,
|
||||
|
@ -268,15 +271,17 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
<UploadBar room={this.props.room} relation={this.props.composerRelation} />
|
||||
) }
|
||||
|
||||
<MessageComposer
|
||||
room={this.props.room}
|
||||
relation={this.props.composerRelation}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
replyToEvent={this.state.replyToEvent}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>
|
||||
{ showComposer && (
|
||||
<MessageComposer
|
||||
room={this.props.room}
|
||||
relation={this.props.composerRelation}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
replyToEvent={this.state.replyToEvent}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>
|
||||
) }
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
|
|
|
@ -430,8 +430,7 @@ const UserOptionsSection: React.FC<{
|
|||
const roomId = member && member.roomId ? member.roomId : RoomViewStore.instance.getRoomId();
|
||||
const onInviteUserButton = async (ev: ButtonEvent) => {
|
||||
try {
|
||||
// We use a MultiInviter to re-use the invite logic, even though
|
||||
// we're only inviting one user.
|
||||
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user.
|
||||
const inviter = new MultiInviter(roomId);
|
||||
await inviter.invite([member.userId]).then(() => {
|
||||
if (inviter.getCompletionState(member.userId) !== "invited") {
|
||||
|
|
|
@ -46,6 +46,7 @@ import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload
|
|||
import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } from "../../../editor/commands";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import { PosthogAnalytics } from "../../../PosthogAnalytics";
|
||||
import { editorRoomKey, editorStateKey } from "../../../Editing";
|
||||
|
||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||
const html = mxEvent.getContent().formatted_body;
|
||||
|
@ -222,11 +223,11 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
}
|
||||
|
||||
private get editorRoomKey(): string {
|
||||
return `mx_edit_room_${this.getRoom().roomId}_${this.context.timelineRenderingType}`;
|
||||
return editorRoomKey(this.props.editState.getEvent().getRoomId(), this.context.timelineRenderingType);
|
||||
}
|
||||
|
||||
private get editorStateKey(): string {
|
||||
return `mx_edit_state_${this.props.editState.getEvent().getId()}`;
|
||||
return editorStateKey(this.props.editState.getEvent().getId());
|
||||
}
|
||||
|
||||
private get events(): MatrixEvent[] {
|
||||
|
@ -314,6 +315,9 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
this.cancelPreviousPendingEdit();
|
||||
createRedactEventDialog({
|
||||
mxEvent: editedEvent,
|
||||
onCloseDialog: () => {
|
||||
this.cancelEdit();
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -225,17 +225,6 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
.getStateEvents(EventType.RoomJoinRules, "")?.getContent<IJoinRuleEventContent>().join_rule;
|
||||
}
|
||||
|
||||
private roomName(atStart = false): string {
|
||||
const name = this.props.room ? this.props.room.name : this.props.roomAlias;
|
||||
if (name) {
|
||||
return name;
|
||||
} else if (atStart) {
|
||||
return _t("This room");
|
||||
} else {
|
||||
return _t("this room");
|
||||
}
|
||||
}
|
||||
|
||||
private getMyMember(): RoomMember {
|
||||
return this.props.room?.getMember(MatrixClientPeg.get().getUserId());
|
||||
}
|
||||
|
@ -287,6 +276,8 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
|
||||
render() {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const roomName = this.props.room?.name ?? this.props.roomAlias ?? "";
|
||||
const isSpace = this.props.room?.isSpaceRoom() ?? this.props.oobData?.roomType === RoomType.Space;
|
||||
|
||||
let showSpinner = false;
|
||||
let title;
|
||||
|
@ -302,7 +293,12 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
const messageCase = this.getMessageCase();
|
||||
switch (messageCase) {
|
||||
case MessageCase.Joining: {
|
||||
title = this.props.oobData?.roomType === RoomType.Space ? _t("Joining space …") : _t("Joining room …");
|
||||
if (this.props.oobData?.roomType || isSpace) {
|
||||
title = isSpace ? _t("Joining space …") : _t("Joining room …");
|
||||
} else {
|
||||
title = _t("Joining …");
|
||||
}
|
||||
|
||||
showSpinner = true;
|
||||
break;
|
||||
}
|
||||
|
@ -328,7 +324,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
footer = (
|
||||
<div>
|
||||
<Spinner w={20} h={20} />
|
||||
{ _t("Loading room preview") }
|
||||
{ _t("Loading preview") }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -336,37 +332,56 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
}
|
||||
case MessageCase.Kicked: {
|
||||
const { memberName, reason } = this.getKickOrBanInfo();
|
||||
title = _t("You were removed from %(roomName)s by %(memberName)s",
|
||||
{ memberName, roomName: this.roomName() });
|
||||
if (roomName) {
|
||||
title = _t("You were removed from %(roomName)s by %(memberName)s",
|
||||
{ memberName, roomName });
|
||||
} else {
|
||||
title = _t("You were removed by %(memberName)s", { memberName });
|
||||
}
|
||||
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
||||
|
||||
if (this.joinRule() === "invite") {
|
||||
primaryActionLabel = _t("Forget this room");
|
||||
primaryActionHandler = this.props.onForgetClick;
|
||||
if (isSpace) {
|
||||
primaryActionLabel = _t("Forget this space");
|
||||
} else {
|
||||
primaryActionLabel = _t("Forget this room");
|
||||
}
|
||||
primaryActionHandler = this.props.onForgetClick;
|
||||
|
||||
if (this.joinRule() !== JoinRule.Invite) {
|
||||
secondaryActionLabel = primaryActionLabel;
|
||||
secondaryActionHandler = primaryActionHandler;
|
||||
|
||||
primaryActionLabel = _t("Re-join");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
secondaryActionLabel = _t("Forget this room");
|
||||
secondaryActionHandler = this.props.onForgetClick;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageCase.Banned: {
|
||||
const { memberName, reason } = this.getKickOrBanInfo();
|
||||
title = _t("You were banned from %(roomName)s by %(memberName)s",
|
||||
{ memberName, roomName: this.roomName() });
|
||||
if (roomName) {
|
||||
title = _t("You were banned from %(roomName)s by %(memberName)s", { memberName, roomName });
|
||||
} else {
|
||||
title = _t("You were banned by %(memberName)s", { memberName });
|
||||
}
|
||||
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
||||
primaryActionLabel = _t("Forget this room");
|
||||
if (isSpace) {
|
||||
primaryActionLabel = _t("Forget this space");
|
||||
} else {
|
||||
primaryActionLabel = _t("Forget this room");
|
||||
}
|
||||
primaryActionHandler = this.props.onForgetClick;
|
||||
break;
|
||||
}
|
||||
case MessageCase.OtherThreePIDError: {
|
||||
title = _t("Something went wrong with your invite to %(roomName)s",
|
||||
{ roomName: this.roomName() });
|
||||
if (roomName) {
|
||||
title = _t("Something went wrong with your invite to %(roomName)s", { roomName });
|
||||
} else {
|
||||
title = _t("Something went wrong with your invite.");
|
||||
}
|
||||
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.",
|
||||
"invite. You could try to pass this information on to the person who invited you.",
|
||||
{ errcode: this.state.threePidFetchError.errcode || _t("unknown error code") },
|
||||
);
|
||||
switch (joinRule) {
|
||||
|
@ -379,7 +394,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
case "public":
|
||||
subTitle = _t("You can still join it because this is a public room.");
|
||||
subTitle = _t("You can still join here.");
|
||||
primaryActionLabel = _t("Join the discussion");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
|
@ -392,14 +407,22 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
case MessageCase.InvitedEmailNotFoundInAccount: {
|
||||
title = _t(
|
||||
"This invite to %(roomName)s was sent to %(email)s which is not " +
|
||||
"associated with your account",
|
||||
{
|
||||
roomName: this.roomName(),
|
||||
email: this.props.invitedEmail,
|
||||
},
|
||||
);
|
||||
if (roomName) {
|
||||
title = _t(
|
||||
"This invite to %(roomName)s was sent to %(email)s which is not " +
|
||||
"associated with your account",
|
||||
{
|
||||
roomName,
|
||||
email: this.props.invitedEmail,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
title = _t(
|
||||
"This invite was sent to %(email)s which is not associated with your account",
|
||||
{ email: this.props.invitedEmail },
|
||||
);
|
||||
}
|
||||
|
||||
subTitle = _t(
|
||||
"Link this email with your account in Settings to receive invites " +
|
||||
"directly in %(brand)s.",
|
||||
|
@ -410,13 +433,18 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
case MessageCase.InvitedEmailNoIdentityServer: {
|
||||
title = _t(
|
||||
"This invite to %(roomName)s was sent to %(email)s",
|
||||
{
|
||||
roomName: this.roomName(),
|
||||
email: this.props.invitedEmail,
|
||||
},
|
||||
);
|
||||
if (roomName) {
|
||||
title = _t(
|
||||
"This invite to %(roomName)s was sent to %(email)s",
|
||||
{
|
||||
roomName,
|
||||
email: this.props.invitedEmail,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
title = _t("This invite was sent to %(email)s", { email: this.props.invitedEmail });
|
||||
}
|
||||
|
||||
subTitle = _t(
|
||||
"Use an identity server in Settings to receive invites directly in %(brand)s.",
|
||||
{ brand },
|
||||
|
@ -426,13 +454,18 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
case MessageCase.InvitedEmailMismatch: {
|
||||
title = _t(
|
||||
"This invite to %(roomName)s was sent to %(email)s",
|
||||
{
|
||||
roomName: this.roomName(),
|
||||
email: this.props.invitedEmail,
|
||||
},
|
||||
);
|
||||
if (roomName) {
|
||||
title = _t(
|
||||
"This invite to %(roomName)s was sent to %(email)s",
|
||||
{
|
||||
roomName,
|
||||
email: this.props.invitedEmail,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
title = _t("This invite was sent to %(email)s", { email: this.props.invitedEmail });
|
||||
}
|
||||
|
||||
subTitle = _t(
|
||||
"Share this email in Settings to receive invites directly in %(brand)s.",
|
||||
{ brand },
|
||||
|
@ -458,16 +491,14 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
|
||||
const isDM = this.isDMInvite();
|
||||
if (isDM) {
|
||||
title = _t("Do you want to chat with %(user)s?",
|
||||
{ user: inviteMember.name });
|
||||
title = _t("Do you want to chat with %(user)s?", { user: inviteMember.name });
|
||||
subTitle = [
|
||||
avatar,
|
||||
_t("<userName/> wants to chat", {}, { userName: () => inviterElement }),
|
||||
];
|
||||
primaryActionLabel = _t("Start chatting");
|
||||
} else {
|
||||
title = _t("Do you want to join %(roomName)s?",
|
||||
{ roomName: this.roomName() });
|
||||
title = _t("Do you want to join %(roomName)s?", { roomName });
|
||||
subTitle = [
|
||||
avatar,
|
||||
_t("<userName/> invited you", {}, { userName: () => inviterElement }),
|
||||
|
@ -500,27 +531,35 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
}
|
||||
case MessageCase.ViewingRoom: {
|
||||
if (this.props.canPreview) {
|
||||
title = _t("You're previewing %(roomName)s. Want to join it?",
|
||||
{ roomName: this.roomName() });
|
||||
title = _t("You're previewing %(roomName)s. Want to join it?", { roomName });
|
||||
} else if (roomName) {
|
||||
title = _t("%(roomName)s can't be previewed. Do you want to join it?", { roomName });
|
||||
} else {
|
||||
title = _t("%(roomName)s can't be previewed. Do you want to join it?",
|
||||
{ roomName: this.roomName(true) });
|
||||
title = _t("There's no preview, would you like to join?");
|
||||
}
|
||||
primaryActionLabel = _t("Join the discussion");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
}
|
||||
case MessageCase.RoomNotFound: {
|
||||
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?");
|
||||
if (roomName) {
|
||||
title = _t("%(roomName)s does not exist.", { roomName });
|
||||
} else {
|
||||
title = _t("This room or space does not exist.");
|
||||
}
|
||||
subTitle = _t("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) });
|
||||
if (roomName) {
|
||||
title = _t("%(roomName)s is not accessible at this time.", { roomName });
|
||||
} else {
|
||||
title = _t("This room or space is not accessible at this time.");
|
||||
}
|
||||
subTitle = [
|
||||
_t("Try again later, or ask a room admin to check if you have access."),
|
||||
_t("Try again later, or ask a room or space admin to check if you have access."),
|
||||
_t(
|
||||
"%(errcode)s was returned while trying to access the room. " +
|
||||
"%(errcode)s was returned while trying to access the room or space. " +
|
||||
"If you think you're seeing this message in error, please " +
|
||||
"<issueLink>submit a bug report</issueLink>.",
|
||||
{ errcode: this.props.error.errcode },
|
||||
|
|
|
@ -59,14 +59,12 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
|||
import { PosthogAnalytics } from "../../../PosthogAnalytics";
|
||||
import { addReplyToMessageContent } from '../../../utils/Reply';
|
||||
|
||||
export function attachRelation(
|
||||
content: IContent,
|
||||
relation?: IEventRelation,
|
||||
): void {
|
||||
// Merges favouring the given relation
|
||||
export function attachRelation(content: IContent, relation?: IEventRelation): void {
|
||||
if (relation) {
|
||||
content['m.relates_to'] = {
|
||||
...relation, // the composer can have a default
|
||||
...content['m.relates_to'],
|
||||
...(content['m.relates_to'] || {}),
|
||||
...relation,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -99,6 +97,7 @@ export function createMessageContent(
|
|||
content.formatted_body = formattedBody;
|
||||
}
|
||||
|
||||
attachRelation(content, relation);
|
||||
if (replyToEvent) {
|
||||
addReplyToMessageContent(content, replyToEvent, {
|
||||
permalinkCreator,
|
||||
|
@ -106,13 +105,6 @@ export function createMessageContent(
|
|||
});
|
||||
}
|
||||
|
||||
if (relation) {
|
||||
content['m.relates_to'] = {
|
||||
...relation,
|
||||
...content['m.relates_to'],
|
||||
};
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
|
|
|
@ -90,17 +90,16 @@ export const ThreadMessagePreview = ({ thread, showDisplayname = false }: IPrevi
|
|||
}, [lastReply, replacingEventId]);
|
||||
if (!preview) return null;
|
||||
|
||||
const sender = thread.roomState.getSentinelMember(lastReply.getSender());
|
||||
return <>
|
||||
<MemberAvatar
|
||||
member={sender}
|
||||
member={lastReply.sender}
|
||||
fallbackUserId={lastReply.getSender()}
|
||||
width={24}
|
||||
height={24}
|
||||
className="mx_ThreadInfo_avatar"
|
||||
/>
|
||||
{ showDisplayname && <div className="mx_ThreadInfo_sender">
|
||||
{ sender?.name ?? lastReply.getSender() }
|
||||
{ lastReply.sender?.name ?? lastReply.getSender() }
|
||||
</div> }
|
||||
<div className="mx_ThreadInfo_content">
|
||||
<span className="mx_ThreadInfo_message-preview">
|
||||
|
|
|
@ -231,7 +231,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
if (!this.state.recorder) return null; // no recorder means we're not recording: no waveform
|
||||
|
||||
if (this.state.recordingPhase !== RecordingState.Started) {
|
||||
return <RecordingPlayback playback={this.state.recorder.getPlayback()} />;
|
||||
return <RecordingPlayback playback={this.state.recorder.getPlayback()} withWaveform={true} />;
|
||||
}
|
||||
|
||||
// only other UI is the recording-in-progress UI
|
||||
|
|
|
@ -97,6 +97,7 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
|
|||
render() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
const isSpace = room.isSpaceRoom();
|
||||
|
||||
let unfederatableSection;
|
||||
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, '');
|
||||
|
@ -120,7 +121,9 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
|
|||
) }
|
||||
</p>
|
||||
<AccessibleButton onClick={this.upgradeRoom} kind='primary'>
|
||||
{ _t("Upgrade this room to the recommended room version") }
|
||||
{ isSpace
|
||||
? _t("Upgrade this space to the recommended room version")
|
||||
: _t("Upgrade this room to the recommended room version") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -128,12 +131,16 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
|
|||
|
||||
let oldRoomLink;
|
||||
if (this.state.oldRoomId) {
|
||||
let name = _t("this room");
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
if (room && room.name) name = room.name;
|
||||
let copy: string;
|
||||
if (isSpace) {
|
||||
copy = _t("View older version of %(spaceName)s.", { spaceName: room.name });
|
||||
} else {
|
||||
copy = _t("View older messages in %(roomName)s.", { roomName: room.name });
|
||||
}
|
||||
|
||||
oldRoomLink = (
|
||||
<AccessibleButton element='a' onClick={this.onOldRoomClicked}>
|
||||
{ _t("View older messages in %(roomName)s.", { roomName: name }) }
|
||||
{ copy }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue