Merge branch 'develop' into travis/remove-skinning

This commit is contained in:
Travis Ralston 2022-03-31 19:25:43 -06:00
commit 97efdf7094
54 changed files with 1559 additions and 431 deletions

View file

@ -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) {

View file

@ -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>

View file

@ -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> }

View file

@ -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;

View file

@ -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 });
};

View file

@ -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>
);
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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>
);

View file

@ -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") {

View file

@ -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;
}

View file

@ -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 },

View file

@ -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;
}

View file

@ -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">

View file

@ -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

View file

@ -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>
);
}