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

@ -38,7 +38,6 @@ limitations under the License.
position: absolute;
font-size: $font-14px;
z-index: 5001;
contain: content;
}
.mx_ContextualMenu_right {

View file

@ -22,7 +22,7 @@ limitations under the License.
display: flex;
flex-direction: column;
border-radius: 8px;
padding: 4px 0;
padding: 8px 0;
box-sizing: border-box;
height: 100%;
contain: strict;

View file

@ -22,7 +22,7 @@ limitations under the License.
flex: 1;
.mx_BaseCard_header {
margin: 8px 0;
margin: 4px 0;
> h2 {
margin: 0 44px;
@ -40,13 +40,13 @@ limitations under the License.
width: 20px;
margin: 12px;
top: 0;
border-radius: 10px;
border-radius: 50%;
&::before {
content: "";
position: absolute;
height: 20px;
width: 20px;
height: inherit;
width: inherit;
top: 0;
left: 0;
mask-repeat: no-repeat;

View file

@ -18,21 +18,29 @@ limitations under the License.
display: flex;
flex-direction: column;
padding-right: 0;
.mx_BaseCard_header {
margin-bottom: 12px;
.mx_BaseCard_close,
.mx_BaseCard_back {
margin-top: 15px;
width: 24px;
height: 24px;
}
.mx_BaseCard_back {
left: -4px;
}
.mx_BaseCard_close {
right: -8px;
right: -4px;
}
}
.mx_ThreadPanel__header {
.mx_BaseCard_back ~ .mx_ThreadPanel__header {
width: calc(100% - 60px);
margin-left: 30px;
}
.mx_ThreadPanel__header {
width: calc(100% - 30px);
height: 24px;
display: flex;
flex: 1;
justify-content: space-between;
@ -47,13 +55,23 @@ limitations under the License.
.mx_AccessibleButton {
font-size: 12px;
color: $primary-content;
color: $secondary-content;
}
.mx_MessageActionBar_optionsButton {
position: relative;
}
.mx_MessageActionBar_maskButton {
--size: 24px;
width: var(--size);
height: var(--size);
&::after {
mask-size: var(--size);
mask-image: url("$(res)/img/element-icons/message/overflow-large.svg");
}
}
.mx_ContextualMenu_wrapper {
// It's added here due to some weird error if I pass it directly in the style, even though it's a numeric value, so it's being passed 0 instead.
// The error: react_devtools_backend.js:2526 Warning: `NaN` is an invalid value for the `top` css style property.
@ -70,6 +88,25 @@ limitations under the License.
font-size: 12px;
color: $secondary-content;
padding-top: 10px;
padding-bottom: 10px;
border: 1px solid $quinary-content;
box-shadow: 0px 1px 3px rgba(23, 25, 28, 0.05);
}
.mx_ContextualMenu_chevron_top {
left: auto;
right: 22px;
border-bottom-color: $quinary-content;
&::after {
content: "";
border: inherit;
border-bottom-color: $background;
position: absolute;
top: 1px;
left: -8px;
}
}
.mx_ThreadPanel_Header_FilterOptionItem {
@ -77,31 +114,33 @@ limitations under the License.
flex-grow: 1;
justify-content: space-between;
flex-direction: column;
overflow: visible;
width: 100%;
padding: 20px;
padding-left: 30px;
padding: 10px 20px 10px 30px;
position: relative;
&:hover {
background-color: $event-selected-color;
}
&[aria-selected="true"] {
&::before {
:first-child {
margin-left: -20px;
}
:first-child::before {
content: "";
width: 12px;
height: 12px;
grid-column: 1;
grid-row: 1;
margin-right: 8px;
mask-image: url("$(res)/img/feather-customised/check.svg");
mask-size: 100%;
mask-repeat: no-repeat;
position: absolute;
top: 22px;
left: 10px;
background-color: $primary-content;
display: inline-block;
vertical-align: middle;
}
}
:last-child {
color: $secondary-content;
}
}
}
@ -131,24 +170,20 @@ limitations under the License.
}
.mx_AutoHideScrollbar {
border-radius: 8px;
}
.mx_RoomView_messageListWrapper {
background: #fff;
background-color: $background;
padding: 8px;
border-radius: inherit;
border-radius: 8px;
width: calc(100% - 16px);
padding-right: 16px;
}
.mx_ScrollPanel {
.mx_RoomView_MessageList {
padding: 0;
}
.mx_RoomView_MessageList {
padding-left: 12px;
padding-right: 0;
}
.mx_EventTile, .mx_EventListSummary {
// Account for scrollbar when hovering
width: calc(100% - 3px);
margin: 0 2px;
padding-top: 0;
@ -170,19 +205,28 @@ limitations under the License.
.mx_DateSeparator {
display: none;
}
&.mx_EventTile_clamp:hover {
cursor: pointer;
}
}
.mx_EventTile:not([data-layout=bubble]) {
.mx_EventTile_e2eIcon {
left: 8px;
}
}
.mx_MessageComposer {
background-color: $background;
border-radius: 8px;
margin-top: 8px;
width: calc(100% - 8px);
padding: 0 8px;
box-sizing: border-box;
}
.mx_ThreadPanel_dropdown {
padding: 4px 8px;
padding: 3px 8px;
border-radius: 4px;
line-height: 1.5;
user-select: none;
@ -207,6 +251,36 @@ limitations under the License.
.mx_ThreadPanel_dropdown[aria-expanded=true]::before {
transform: rotate(180deg);
}
.mx_MessageTimestamp {
font-size: $font-12px;
}
}
.mx_ThreadPanel_replies {
margin-top: 8px;
}
.mx_ThreadPanel_repliesSummary {
&::before {
content: "";
mask-image: url('$(res)/img/element-icons/thread-summary.svg');
mask-position: center;
display: inline-block;
height: 18px;
min-width: 18px;
background-color: currentColor;
mask-repeat: no-repeat;
mask-size: contain;
margin-right: 8px;
vertical-align: middle;
}
color: $secondary-content;
font-weight: 600;
float: left;
margin-right: 12px;
font-size: $font-12px;
}
.mx_ThreadPanel_viewInRoom::before {

View file

@ -460,6 +460,16 @@ $left-gutter: 64px;
}
}
.mx_EventTile_clamp {
.mx_EventTile_body {
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
}
}
.mx_EventTile_content .markdown-body {
font-family: inherit !important;
white-space: normal !important;
@ -663,7 +673,10 @@ $left-gutter: 64px;
}
.mx_ThreadInfo {
height: 35px;
min-width: 267px;
max-width: min(calc(100% - 64px), 600px);
width: auto;
height: 40px;
position: relative;
background-color: $system;
padding-left: 12px;
@ -671,13 +684,13 @@ $left-gutter: 64px;
align-items: center;
border-radius: 8px;
padding-right: 16px;
padding-top: 8px;
padding-bottom: 8px;
margin-top: 8px;
font-size: $font-12px;
color: $secondary-content;
box-sizing: border-box;
justify-content: flex-start;
clear: both;
overflow: hidden;
&:hover {
cursor: pointer;
@ -687,6 +700,44 @@ $left-gutter: 64px;
padding-left: 11px;
padding-right: 15px;
}
&::before {
content: "";
mask-image: url('$(res)/img/element-icons/thread-summary.svg');
mask-position: center;
height: 18px;
min-width: 18px;
background-color: $secondary-content;
mask-repeat: no-repeat;
mask-size: contain;
}
&::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 60px;
padding: 0 10px;
font-size: 15px;
line-height: 39px;
box-sizing: border-box;
text-align: right;
font-weight: 600;
background: linear-gradient(270deg, $system 52.6%, transparent 100%);
opacity: 0;
transform: translateX(20px);
transition: all .1s ease-in-out;
}
&:hover::after {
opacity: 1;
transform: translateX(0);
}
}
.mx_ThreadInfo_content {
@ -703,15 +754,6 @@ $left-gutter: 64px;
float: left;
}
.mx_ThreadInfo_thread-icon {
mask-image: url('$(res)/img/element-icons/thread-summary.svg');
mask-position: center;
height: 16px;
min-width: 16px;
background-color: $secondary-content;
mask-repeat: no-repeat;
mask-size: contain;
}
.mx_ThreadInfo_threads-amount {
font-weight: 600;
position: relative;
@ -720,10 +762,10 @@ $left-gutter: 64px;
}
.mx_EventTile[data-shape=thread_list] {
--topOffset: 24px;
--topOffset: 20px;
--leftOffset: 46px;
margin: var(--topOffset) 0;
margin: var(--topOffset) 16px var(--topOffset) 0;
border-radius: 8px;
&:hover {
@ -819,6 +861,7 @@ $left-gutter: 64px;
left: auto;
right: 2px !important;
top: 1px !important;
font-size: 1rem;
}
.mx_ReactionsRow {
@ -830,7 +873,8 @@ $left-gutter: 64px;
}
}
.mx_EventTile_content {
.mx_EventTile_content,
.mx_RedactedBody {
margin-left: 36px;
margin-right: 50px;
}

View file

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.66699 12C6.66699 13.1046 5.77156 14 4.66699 14C3.56242 14 2.66699 13.1046 2.66699 12C2.66699 10.8954 3.56242 10 4.66699 10C5.77156 10 6.66699 10.8954 6.66699 12Z" fill="#17191C"/>
<path d="M14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12Z" fill="#17191C"/>
<path d="M19.333 14C20.4376 14 21.333 13.1046 21.333 12C21.333 10.8954 20.4376 10 19.333 10C18.2284 10 17.333 10.8954 17.333 12C17.333 13.1046 18.2284 14 19.333 14Z" fill="#17191C"/>
</svg>

After

Width:  |  Height:  |  Size: 625 B

View file

@ -355,7 +355,9 @@ export default class RightPanel extends React.Component<IProps, IState> {
panel = <ThreadPanel
roomId={roomId}
resizeNotifier={this.props.resizeNotifier}
onClose={this.onClose} />;
onClose={this.onClose}
permalinkCreator={this.props.permalinkCreator}
/>;
break;
case RightPanelPhases.RoomSummary:

View file

@ -20,24 +20,24 @@ import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set';
import { Room } from 'matrix-js-sdk/src/models/room';
import BaseCard from "../views/right_panel/BaseCard";
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import ResizeNotifier from '../../utils/ResizeNotifier';
import MatrixClientContext from '../../contexts/MatrixClientContext';
import { _t } from '../../languageHandler';
import { ContextMenuButton } from '../../accessibility/context_menu/ContextMenuButton';
import ContextMenu, { useContextMenu } from './ContextMenu';
import ContextMenu, { ChevronFace, useContextMenu } from './ContextMenu';
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
import TimelinePanel from './TimelinePanel';
import { Layout } from '../../settings/Layout';
import { useEventEmitter } from '../../hooks/useEventEmitter';
import AccessibleButton from '../views/elements/AccessibleButton';
import { TileShape } from '../views/rooms/EventTile';
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
interface IProps {
roomId: string;
onClose: () => void;
resizeNotifier: ResizeNotifier;
permalinkCreator: RoomPermalinkCreator;
}
export enum ThreadFilterType {
@ -162,7 +162,13 @@ export const ThreadPanelHeader = ({ filterOption, setFilterOption }: {
}}
isSelected={opt === value}
/>);
const contextMenu = menuDisplayed ? <ContextMenu top={0} right={25} onFinished={closeMenu} managed={false}>
const contextMenu = menuDisplayed ? <ContextMenu
top={0}
right={25}
onFinished={closeMenu}
managed={false}
chevronFace={ChevronFace.Top}
>
{ contextMenuOptions }
</ContextMenu> : null;
return <div className="mx_ThreadPanel__header">
@ -174,7 +180,7 @@ export const ThreadPanelHeader = ({ filterOption, setFilterOption }: {
</div>;
};
const ThreadPanel: React.FC<IProps> = ({ roomId, onClose }) => {
const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) => {
const mxClient = useContext(MatrixClientContext);
const roomContext = useContext(RoomContext);
const room = mxClient.getRoom(roomId);
@ -200,7 +206,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose }) => {
header={<ThreadPanelHeader filterOption={filterOption} setFilterOption={setFilterOption} />}
className="mx_ThreadPanel"
onClose={onClose}
previousPhase={RightPanelPhases.RoomSummary}
withoutScrollContainer={true}
>
<TimelinePanel
ref={ref}
@ -218,6 +224,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose }) => {
showReactions={true}
className="mx_RoomView_messagePanel mx_GroupLayout"
membersLoaded={true}
permalinkCreator={permalinkCreator}
tileShape={TileShape.ThreadPanel}
/>
</BaseCard>

View file

@ -40,7 +40,7 @@ import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
import ContentMessages from '../../ContentMessages';
import UploadBar from './UploadBar';
import { _t } from '../../languageHandler';
import { ThreadListContextMenu } from '../views/context_menus/ThreadListContextMenu';
import ThreadListContextMenu from '../views/context_menus/ThreadListContextMenu';
interface IProps {
room: Room;
@ -214,6 +214,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
className="mx_ThreadView mx_ThreadPanel"
onClose={this.props.onClose}
previousPhase={RightPanelPhases.ThreadPanel}
previousPhaseLabel={_t("All threads")}
withoutScrollContainer={true}
header={this.renderThreadViewHeader()}
>

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

View file

@ -1573,7 +1573,7 @@
"Key request sent.": "Key request sent.",
"<requestLink>Re-request encryption keys</requestLink> from your other sessions.": "<requestLink>Re-request encryption keys</requestLink> from your other sessions.",
"Message Actions": "Message Actions",
"Thread": "Thread",
"Reply in thread": "Reply in thread",
"This message cannot be decrypted": "This message cannot be decrypted",
"Encrypted by an unverified session": "Encrypted by an unverified session",
"Unencrypted": "Unencrypted",
@ -1864,7 +1864,6 @@
"%(count)s people|one": "%(count)s person",
"Show files": "Show files",
"Export chat": "Export chat",
"Show threads": "Show threads",
"Share room": "Share room",
"Room settings": "Room settings",
"Trusted": "Trusted",
@ -3012,6 +3011,7 @@
"All threads": "All threads",
"Shows all threads from current room": "Shows all threads from current room",
"Show:": "Show:",
"Thread": "Thread",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
"Failed to load timeline position": "Failed to load timeline position",