Fixes following threads design implementation review (#7100)

This commit is contained in:
Germain 2021-11-11 11:00:18 +00:00 committed by GitHub
parent b8edebecc9
commit 1de9630e44
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 280 additions and 115 deletions

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { MatrixEvent } from "matrix-js-sdk/src";
import { ButtonEvent } from "../elements/AccessibleButton";
import dis from '../../../dispatcher/dispatcher';
@ -27,17 +27,18 @@ import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOpti
interface IProps {
mxEvent: MatrixEvent;
permalinkCreator: RoomPermalinkCreator;
onMenuToggle?: (open: boolean) => void;
}
const contextMenuBelow = (elementRect: DOMRect) => {
// align the context menu's icons with the icon which opened the context menu
const left = elementRect.left + window.pageXOffset + elementRect.width;
const top = elementRect.bottom + window.pageYOffset + 17;
const top = elementRect.bottom + window.pageYOffset;
const chevronFace = ChevronFace.None;
return { left, top, chevronFace };
};
export const ThreadListContextMenu: React.FC<IProps> = ({ mxEvent, permalinkCreator }) => {
const ThreadListContextMenu: React.FC<IProps> = ({ mxEvent, permalinkCreator, onMenuToggle }) => {
const [optionsPosition, setOptionsPosition] = useState(null);
const closeThreadOptions = useCallback(() => {
setOptionsPosition(null);
@ -72,6 +73,12 @@ export const ThreadListContextMenu: React.FC<IProps> = ({ mxEvent, permalinkCrea
}
}, [closeThreadOptions, optionsPosition]);
useEffect(() => {
if (onMenuToggle) {
onMenuToggle(!!optionsPosition);
}
}, [optionsPosition, onMenuToggle]);
return <React.Fragment>
<ContextMenuTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"

View file

@ -294,7 +294,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
&& this.context.timelineRenderingType !== TimelineRenderingType.Thread) && (
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_threadButton"
title={_t("Thread")}
title={_t("Reply in thread")}
onClick={this.onThreadClick}
key="thread"
/>
@ -327,7 +327,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
) {
toolbarOpts.unshift(<RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_threadButton"
title={_t("Thread")}
title={_t("Reply in thread")}
onClick={this.onThreadClick}
key="thread"
/>);

View file

@ -31,6 +31,7 @@ interface IProps {
className?: string;
withoutScrollContainer?: boolean;
previousPhase?: RightPanelPhases;
previousPhaseLabel?: string;
closeLabel?: string;
onClose?(): void;
refireParams?;
@ -56,6 +57,7 @@ const BaseCard: React.FC<IProps> = ({
footer,
withoutScrollContainer,
previousPhase,
previousPhaseLabel,
children,
refireParams,
}) => {
@ -68,7 +70,8 @@ const BaseCard: React.FC<IProps> = ({
refireParams: refireParams,
});
};
backButton = <AccessibleButton className="mx_BaseCard_back" onClick={onBackClick} title={_t("Back")} />;
const label = previousPhaseLabel ?? _t("Back");
backButton = <AccessibleButton className="mx_BaseCard_back" onClick={onBackClick} title={label} />;
}
let closeButton;

View file

@ -33,6 +33,7 @@ import { useSettingValue } from "../../../hooks/useSettings";
import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard';
import { dispatchShowThreadsPanelEvent } from "../../../dispatcher/dispatch-actions/threads";
import SettingsStore from "../../../settings/SettingsStore";
import dis from "../../../dispatcher/dispatcher";
const ROOM_INFO_PHASES = [
RightPanelPhases.RoomSummary,
@ -72,6 +73,11 @@ interface IProps {
@replaceableComponent("views.right_panel.RoomHeaderButtons")
export default class RoomHeaderButtons extends HeaderButtons<IProps> {
private static readonly THREAD_PHASES = [
RightPanelPhases.ThreadPanel,
RightPanelPhases.ThreadView,
];
constructor(props: IProps) {
super(props, HeaderKind.Room);
}
@ -117,6 +123,17 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
this.setPhase(RightPanelPhases.PinnedMessages);
};
private onThreadsPanelClicked = () => {
if (RoomHeaderButtons.THREAD_PHASES.includes(this.state.phase)) {
dis.dispatch({
action: Action.ToggleRightPanel,
type: "room",
});
} else {
dispatchShowThreadsPanelEvent();
}
};
public renderButtons() {
return <>
<PinnedMessagesHeaderButton
@ -127,11 +144,8 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
{ SettingsStore.getValue("feature_thread") && <HeaderButton
name="threadsButton"
title={_t("Threads")}
onClick={dispatchShowThreadsPanelEvent}
isHighlighted={this.isPhase([
RightPanelPhases.ThreadPanel,
RightPanelPhases.ThreadView,
])}
onClick={this.onThreadsPanelClicked}
isHighlighted={this.isPhase(RoomHeaderButtons.THREAD_PHASES)}
analytics={['Right Panel', 'Threads List Button', 'click']}
/> }
<HeaderButton

View file

@ -48,7 +48,6 @@ import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widget
import RoomName from "../elements/RoomName";
import UIStore from "../../../stores/UIStore";
import ExportDialog from "../dialogs/ExportDialog";
import { dispatchShowThreadsPanelEvent } from "../../../dispatcher/dispatch-actions/threads";
interface IProps {
room: Room;
@ -284,11 +283,6 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
<Button className="mx_RoomSummaryCard_icon_export" onClick={onRoomExportClick}>
{ _t("Export chat") }
</Button>
{ SettingsStore.getValue("feature_thread") && (
<Button className="mx_RoomSummaryCard_icon_threads" onClick={dispatchShowThreadsPanelEvent}>
{ _t("Show threads") }
</Button>
) }
<Button className="mx_RoomSummaryCard_icon_share" onClick={onShareRoomClick}>
{ _t("Share room") }
</Button>

View file

@ -67,7 +67,7 @@ import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import Toolbar from '../../../accessibility/Toolbar';
import { POLL_START_EVENT_TYPE } from '../../../polls/consts';
import { RovingAccessibleTooltipButton } from '../../../accessibility/roving/RovingAccessibleTooltipButton';
import { ThreadListContextMenu } from '../context_menus/ThreadListContextMenu';
import ThreadListContextMenu from '../context_menus/ThreadListContextMenu';
const eventTileTypes = {
[EventType.RoomMessage]: 'messages.MessageEvent',
@ -552,7 +552,7 @@ export default class EventTile extends React.Component<IProps, IState> {
}
};
private renderThreadLastMessagePreview(): JSX.Element | null {
private get thread(): Thread | null {
if (!SettingsStore.getValue("feature_thread")) {
return null;
}
@ -570,7 +570,28 @@ export default class EventTile extends React.Component<IProps, IState> {
return null;
}
const [lastEvent] = thread.events
return thread;
}
private renderThreadPanelSummary(): JSX.Element | null {
if (!this.thread) {
return null;
}
return <div className="mx_ThreadPanel_replies">
<span className="mx_ThreadPanel_repliesSummary">
{ this.thread.length }
</span>
{ this.renderThreadLastMessagePreview() }
</div>;
}
private renderThreadLastMessagePreview(): JSX.Element | null {
if (!this.thread) {
return null;
}
const [lastEvent] = this.thread.events
.filter(event => event.isThreadRelation)
.slice(-1);
const threadMessagePreview = MessagePreviewStore.instance.generatePreviewForEvent(lastEvent);
@ -590,24 +611,7 @@ export default class EventTile extends React.Component<IProps, IState> {
}
private renderThreadInfo(): React.ReactNode {
if (!SettingsStore.getValue("feature_thread")) {
return null;
}
/**
* Accessing the threads value through the room due to a race condition
* that will be solved when there are proper backend support for threads
* We currently have no reliable way to discover than an event is a thread
* when we are at the sync stage
*/
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const thread = room?.threads.get(this.props.mxEvent.getId());
if (thread && !thread.ready) {
thread.addEvent(this.props.mxEvent, true);
}
if (!thread || this.props.showThreadInfo === false || thread.length === 0) {
if (!this.thread) {
return null;
}
@ -620,10 +624,9 @@ export default class EventTile extends React.Component<IProps, IState> {
);
}}
>
<span className="mx_ThreadInfo_thread-icon" />
<span className="mx_ThreadInfo_threads-amount">
{ _t("%(count)s reply", {
count: thread.length,
count: this.thread.length,
}) }
</span>
{ this.renderThreadLastMessagePreview() }
@ -1063,6 +1066,7 @@ export default class EventTile extends React.Component<IProps, IState> {
mx_EventTile_bad: isEncryptionFailure,
mx_EventTile_emote: msgtype === 'm.emote',
mx_EventTile_noSender: this.props.hideSender,
mx_EventTile_clamp: this.props.tileShape === TileShape.ThreadPanel,
});
// If the tile is in the Sending state, don't speak the message.
@ -1161,11 +1165,16 @@ export default class EventTile extends React.Component<IProps, IState> {
|| this.state.hover
|| this.state.actionBarFocused);
// Thread panel shows the timestamp of the last reply in that thread
const ts = this.props.tileShape !== TileShape.ThreadPanel
? this.props.mxEvent.getTs()
: this.props.mxEvent.getThread().lastReply.getTs();
const timestamp = showTimestamp ?
<MessageTimestamp
showRelative={this.props.tileShape === TileShape.ThreadPanel}
showTwelveHour={this.props.isTwelveHour}
ts={this.props.mxEvent.getTs()}
ts={ts}
/> : null;
const keyRequestHelpText =
@ -1337,11 +1346,15 @@ export default class EventTile extends React.Component<IProps, IState> {
"data-has-reply": !!replyChain,
"onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }),
"onClick": () => dispatchShowThreadEvent(this.props.mxEvent),
}, <>
{ sender }
{ avatar }
<div className={lineClasses} key="mx_EventTile_line">
<div
className={lineClasses}
onClick={() => dispatchShowThreadEvent(this.props.mxEvent)}
key="mx_EventTile_line"
>
{ linkedTimestamp }
{ this.renderE2EPadlock() }
{ replyChain }
@ -1359,19 +1372,21 @@ export default class EventTile extends React.Component<IProps, IState> {
tileShape={this.props.tileShape}
/>
{ keyRequestInfo }
<Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off">
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_threadButton"
title={_t("Thread")}
onClick={() => dispatchShowThreadEvent(this.props.mxEvent)}
key="thread"
/>
<ThreadListContextMenu
mxEvent={this.props.mxEvent}
permalinkCreator={this.props.permalinkCreator} />
</Toolbar>
{ this.renderThreadLastMessagePreview() }
{ this.renderThreadPanelSummary() }
</div>
<Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off">
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_threadButton"
title={_t("Reply in thread")}
onClick={() => dispatchShowThreadEvent(this.props.mxEvent)}
key="thread"
/>
<ThreadListContextMenu
mxEvent={this.props.mxEvent}
permalinkCreator={this.props.permalinkCreator}
onMenuToggle={this.onActionBarFocusChange}
/>
</Toolbar>
{ msgOption }
</>)
);