mirror of
https://github.com/element-hq/element-web
synced 2024-11-26 19:26:04 +03:00
Design thread list tiles according to mockups (#7078)
This commit is contained in:
parent
2a20d9a7df
commit
38750202ee
8 changed files with 332 additions and 181 deletions
|
@ -21,13 +21,10 @@ limitations under the License.
|
|||
padding-right: 0;
|
||||
|
||||
.mx_BaseCard_header {
|
||||
padding: 6px 8px 6px 0;
|
||||
|
||||
.mx_BaseCard_close,
|
||||
.mx_BaseCard_back {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.mx_BaseCard_close {
|
||||
right: -8px;
|
||||
}
|
||||
|
@ -39,6 +36,7 @@ limitations under the License.
|
|||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span:first-of-type {
|
||||
font-weight: 600;
|
||||
|
@ -49,7 +47,11 @@ limitations under the License.
|
|||
|
||||
.mx_AccessibleButton {
|
||||
font-size: 12px;
|
||||
color: $secondary-content;
|
||||
color: $primary-content;
|
||||
}
|
||||
|
||||
.mx_MessageActionBar_optionsButton {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mx_ContextualMenu_wrapper {
|
||||
|
@ -178,6 +180,33 @@ limitations under the License.
|
|||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mx_ThreadPanel_dropdown {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
line-height: 1.5;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mx_ThreadPanel_dropdown:hover,
|
||||
.mx_ThreadPanel_dropdown[aria-expanded=true] {
|
||||
background: $quinary-content;
|
||||
}
|
||||
|
||||
.mx_ThreadPanel_dropdown::before {
|
||||
content: "";
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: currentColor;
|
||||
mask-image: url("$(res)/img/feather-customised/chevron-down.svg");
|
||||
mask-size: 100%;
|
||||
mask-repeat: no-repeat;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.mx_ThreadPanel_dropdown[aria-expanded=true]::before {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ThreadPanel_viewInRoom::before {
|
||||
|
|
|
@ -687,28 +687,82 @@ $left-gutter: 64px;
|
|||
padding-left: 11px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ThreadInfo_content {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding-left: 8px;
|
||||
.mx_ThreadInfo_content {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.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;
|
||||
padding: 0 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mx_EventTile[data-shape=thread_list] {
|
||||
--topOffset: 24px;
|
||||
--leftOffset: 46px;
|
||||
|
||||
margin: var(--topOffset) 0;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: $system;
|
||||
}
|
||||
|
||||
.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;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: var(--leftOffset);
|
||||
right: 0;
|
||||
height: 1px;
|
||||
bottom: calc(-1 * var(--topOffset));
|
||||
background-color: $quinary-content;
|
||||
}
|
||||
.mx_ThreadInfo_threads-amount {
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
padding: 0 8px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-child {
|
||||
&::after {
|
||||
content: unset;
|
||||
}
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
padding-top: 0;
|
||||
|
||||
.mx_EventTile_avatar {
|
||||
top: -4px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.mx_SenderProfile {
|
||||
margin-left: var(--leftOffset) !important;
|
||||
}
|
||||
|
||||
.mx_EventTile_line {
|
||||
padding-left: var(--leftOffset) !important;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.mx_MessageTimestamp {
|
||||
right: 0;
|
||||
left: auto;
|
||||
top: -23px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -167,7 +167,7 @@ export const ThreadPanelHeader = ({ filterOption, setFilterOption }: {
|
|||
</ContextMenu> : null;
|
||||
return <div className="mx_ThreadPanel__header">
|
||||
<span>{ _t("Threads") }</span>
|
||||
<ContextMenuButton inputRef={button} isExpanded={menuDisplayed} onClick={() => menuDisplayed ? closeMenu() : openMenu()}>
|
||||
<ContextMenuButton className="mx_ThreadPanel_dropdown" inputRef={button} isExpanded={menuDisplayed} onClick={() => menuDisplayed ? closeMenu() : openMenu()}>
|
||||
{ `${_t('Show:')} ${value.label}` }
|
||||
</ContextMenuButton>
|
||||
{ contextMenu }
|
||||
|
|
|
@ -39,15 +39,8 @@ import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
|||
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||
import ContentMessages from '../../ContentMessages';
|
||||
import UploadBar from './UploadBar';
|
||||
import { ChevronFace, ContextMenuTooltipButton } from './ContextMenu';
|
||||
import { _t } from '../../languageHandler';
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from '../views/context_menus/IconizedContextMenu';
|
||||
import { ButtonEvent } from '../views/elements/AccessibleButton';
|
||||
import { copyPlaintext } from '../../utils/strings';
|
||||
import { sleep } from 'matrix-js-sdk/src/utils';
|
||||
import { ThreadListContextMenu } from '../views/context_menus/ThreadListContextMenu';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -63,24 +56,8 @@ interface IState {
|
|||
thread?: Thread;
|
||||
editState?: EditorStateTransfer;
|
||||
replyToEvent?: MatrixEvent;
|
||||
threadOptionsPosition: DOMRect | null;
|
||||
copyingPhase: CopyingPhase;
|
||||
}
|
||||
|
||||
enum CopyingPhase {
|
||||
Idle,
|
||||
Copying,
|
||||
Failed,
|
||||
}
|
||||
|
||||
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 chevronFace = ChevronFace.None;
|
||||
return { left, top, chevronFace };
|
||||
};
|
||||
|
||||
@replaceableComponent("structures.ThreadView")
|
||||
export default class ThreadView extends React.Component<IProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
|
@ -90,12 +67,8 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
threadOptionsPosition: null,
|
||||
copyingPhase: CopyingPhase.Idle,
|
||||
};
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.setupThread(this.props.mxEvent);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -210,95 +183,12 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onThreadOptionsClick = (ev: ButtonEvent): void => {
|
||||
if (this.isThreadOptionsVisible) {
|
||||
this.closeThreadOptions();
|
||||
} else {
|
||||
const position = ev.currentTarget.getBoundingClientRect();
|
||||
this.setState({
|
||||
threadOptionsPosition: position,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private closeThreadOptions = (): void => {
|
||||
this.setState({
|
||||
threadOptionsPosition: null,
|
||||
});
|
||||
};
|
||||
|
||||
private get isThreadOptionsVisible(): boolean {
|
||||
return !!this.state.threadOptionsPosition;
|
||||
}
|
||||
|
||||
private viewInRoom = (evt: ButtonEvent): void => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
event_id: this.props.mxEvent.getId(),
|
||||
highlighted: true,
|
||||
room_id: this.props.mxEvent.getRoomId(),
|
||||
});
|
||||
this.closeThreadOptions();
|
||||
};
|
||||
|
||||
private copyLinkToThread = async (evt: ButtonEvent): Promise<void> => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const matrixToUrl = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
||||
|
||||
this.setState({
|
||||
copyingPhase: CopyingPhase.Copying,
|
||||
});
|
||||
|
||||
const hasSuccessfullyCopied = await copyPlaintext(matrixToUrl);
|
||||
|
||||
if (hasSuccessfullyCopied) {
|
||||
await sleep(500);
|
||||
} else {
|
||||
this.setState({ copyingPhase: CopyingPhase.Failed });
|
||||
await sleep(2500);
|
||||
}
|
||||
|
||||
this.setState({ copyingPhase: CopyingPhase.Idle });
|
||||
|
||||
if (hasSuccessfullyCopied) {
|
||||
this.closeThreadOptions();
|
||||
}
|
||||
};
|
||||
|
||||
private renderThreadViewHeader = (): JSX.Element => {
|
||||
return <div className="mx_ThreadPanel__header">
|
||||
<span>{ _t("Thread") }</span>
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_ThreadPanel_button mx_ThreadPanel_OptionsButton"
|
||||
onClick={this.onThreadOptionsClick}
|
||||
title={_t("Thread options")}
|
||||
isExpanded={this.isThreadOptionsVisible}
|
||||
/>
|
||||
{ this.isThreadOptionsVisible && (<IconizedContextMenu
|
||||
onFinished={this.closeThreadOptions}
|
||||
className="mx_RoomTile_contextMenu"
|
||||
compact
|
||||
rightAligned
|
||||
{...contextMenuBelow(this.state.threadOptionsPosition)}
|
||||
>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOption
|
||||
onClick={(e) => this.viewInRoom(e)}
|
||||
label={_t("View in room")}
|
||||
iconClassName="mx_ThreadPanel_viewInRoom"
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
onClick={(e) => this.copyLinkToThread(e)}
|
||||
label={_t("Copy link to thread")}
|
||||
iconClassName="mx_ThreadPanel_copyLinkToThread"
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>) }
|
||||
|
||||
<ThreadListContextMenu
|
||||
mxEvent={this.props.mxEvent}
|
||||
permalinkCreator={this.props.permalinkCreator} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
|
103
src/components/views/context_menus/ThreadListContextMenu.tsx
Normal file
103
src/components/views/context_menus/ThreadListContextMenu.tsx
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
Copyright 2021 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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import { copyPlaintext } from "../../../utils/strings";
|
||||
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
}
|
||||
|
||||
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 chevronFace = ChevronFace.None;
|
||||
return { left, top, chevronFace };
|
||||
};
|
||||
|
||||
export const ThreadListContextMenu: React.FC<IProps> = ({ mxEvent, permalinkCreator }) => {
|
||||
const [optionsPosition, setOptionsPosition] = useState(null);
|
||||
const closeThreadOptions = useCallback(() => {
|
||||
setOptionsPosition(null);
|
||||
}, []);
|
||||
|
||||
const viewInRoom = useCallback((evt: ButtonEvent): void => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
event_id: mxEvent.getId(),
|
||||
highlighted: true,
|
||||
room_id: mxEvent.getRoomId(),
|
||||
});
|
||||
closeThreadOptions();
|
||||
}, [mxEvent, closeThreadOptions]);
|
||||
|
||||
const copyLinkToThread = useCallback(async (evt: ButtonEvent) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId());
|
||||
await copyPlaintext(matrixToUrl);
|
||||
closeThreadOptions();
|
||||
}, [mxEvent, closeThreadOptions, permalinkCreator]);
|
||||
|
||||
const toggleOptionsMenu = useCallback((ev: ButtonEvent): void => {
|
||||
if (!!optionsPosition) {
|
||||
closeThreadOptions();
|
||||
} else {
|
||||
const position = ev.currentTarget.getBoundingClientRect();
|
||||
setOptionsPosition(position);
|
||||
}
|
||||
}, [closeThreadOptions, optionsPosition]);
|
||||
|
||||
return <React.Fragment>
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
|
||||
onClick={toggleOptionsMenu}
|
||||
title={_t("Thread options")}
|
||||
isExpanded={!!optionsPosition}
|
||||
/>
|
||||
{ !!optionsPosition && (<IconizedContextMenu
|
||||
onFinished={closeThreadOptions}
|
||||
className="mx_RoomTile_contextMenu"
|
||||
compact
|
||||
rightAligned
|
||||
{...contextMenuBelow(optionsPosition)}
|
||||
>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOption
|
||||
onClick={(e) => viewInRoom(e)}
|
||||
label={_t("View in room")}
|
||||
iconClassName="mx_ThreadPanel_viewInRoom"
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
onClick={(e) => copyLinkToThread(e)}
|
||||
label={_t("Copy link to thread")}
|
||||
iconClassName="mx_ThreadPanel_copyLinkToThread"
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>) }
|
||||
</React.Fragment>;
|
||||
};
|
|
@ -64,6 +64,9 @@ import { MessagePreviewStore } from '../../../stores/room-list/MessagePreviewSto
|
|||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||
import Toolbar from '../../../accessibility/Toolbar';
|
||||
import { RovingAccessibleTooltipButton } from '../../../accessibility/roving/RovingAccessibleTooltipButton';
|
||||
import { ThreadListContextMenu } from '../context_menus/ThreadListContextMenu';
|
||||
|
||||
const eventTileTypes = {
|
||||
[EventType.RoomMessage]: 'messages.MessageEvent',
|
||||
|
@ -547,6 +550,43 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private renderThreadLastMessagePreview(): JSX.Element | null {
|
||||
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.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [lastEvent] = thread.events
|
||||
.filter(event => event.isThreadRelation)
|
||||
.slice(-1);
|
||||
const threadMessagePreview = MessagePreviewStore.instance.generatePreviewForEvent(lastEvent);
|
||||
|
||||
if (!threadMessagePreview || !lastEvent.sender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>
|
||||
<MemberAvatar member={lastEvent.sender} style={{ float: "left", width: "24px", height: "24px" }} />
|
||||
<div className="mx_ThreadInfo_content">
|
||||
<span className="mx_ThreadInfo_message-preview">
|
||||
{ threadMessagePreview }
|
||||
</span>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
private renderThreadInfo(): React.ReactNode {
|
||||
if (!SettingsStore.getValue("feature_thread")) {
|
||||
return null;
|
||||
|
@ -569,11 +609,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
return null;
|
||||
}
|
||||
|
||||
const [lastEvent] = thread.events
|
||||
.filter(event => event.isThreadRelation)
|
||||
.slice(-1);
|
||||
const threadMessagePreview = MessagePreviewStore.instance.generatePreviewForEvent(lastEvent);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="mx_ThreadInfo"
|
||||
|
@ -589,14 +624,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
count: thread.length,
|
||||
}) }
|
||||
</span>
|
||||
{ (threadMessagePreview && lastEvent.sender) && <>
|
||||
<MemberAvatar member={lastEvent.sender} width={24} height={24} />
|
||||
<div className="mx_ThreadInfo_content">
|
||||
<span className="mx_ThreadInfo_message-preview">
|
||||
{ threadMessagePreview }
|
||||
</span>
|
||||
</div>
|
||||
</> }
|
||||
{ this.renderThreadLastMessagePreview() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1199,6 +1227,20 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
msgOption = readAvatars;
|
||||
}
|
||||
|
||||
const replyChain = haveTileForEvent(this.props.mxEvent) &&
|
||||
ReplyChain.hasReply(this.props.mxEvent) ? (
|
||||
<ReplyChain
|
||||
parentEv={this.props.mxEvent}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
ref={this.replyChain}
|
||||
forExport={this.props.forExport}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
layout={this.props.layout}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
|
||||
isQuoteExpanded={isQuoteExpanded}
|
||||
setQuoteExpanded={this.setQuoteExpanded}
|
||||
/>) : null;
|
||||
|
||||
switch (this.props.tileShape) {
|
||||
case TileShape.Notif: {
|
||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||
|
@ -1235,19 +1277,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
]);
|
||||
}
|
||||
case TileShape.Thread: {
|
||||
const replyChain = haveTileForEvent(this.props.mxEvent) &&
|
||||
ReplyChain.hasReply(this.props.mxEvent) ? (
|
||||
<ReplyChain
|
||||
parentEv={this.props.mxEvent}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
ref={this.replyChain}
|
||||
forExport={this.props.forExport}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
layout={this.props.layout}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
|
||||
isQuoteExpanded={isQuoteExpanded}
|
||||
setQuoteExpanded={this.setQuoteExpanded}
|
||||
/>) : null;
|
||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||
return React.createElement(this.props.as || "li", {
|
||||
"className": classes,
|
||||
|
@ -1288,6 +1317,63 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
reactionsRow,
|
||||
]);
|
||||
}
|
||||
case TileShape.ThreadPanel: {
|
||||
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
|
||||
|
||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||
return (
|
||||
React.createElement(this.props.as || "li", {
|
||||
"ref": this.ref,
|
||||
"className": classes,
|
||||
"tabIndex": -1,
|
||||
"aria-live": ariaLive,
|
||||
"aria-atomic": "true",
|
||||
"data-scroll-tokens": scrollToken,
|
||||
"data-layout": this.props.layout,
|
||||
"data-shape": this.props.tileShape,
|
||||
"data-self": isOwnEvent,
|
||||
"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">
|
||||
{ linkedTimestamp }
|
||||
{ this.renderE2EPadlock() }
|
||||
{ replyChain }
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
forExport={this.props.forExport}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
editState={this.props.editState}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
callEventGrouper={this.props.callEventGrouper}
|
||||
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() }
|
||||
</div>
|
||||
{ msgOption }
|
||||
</>)
|
||||
);
|
||||
}
|
||||
case TileShape.FileGrid: {
|
||||
return React.createElement(this.props.as || "li", {
|
||||
"className": classes,
|
||||
|
@ -1321,19 +1407,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
default: {
|
||||
const replyChain = haveTileForEvent(this.props.mxEvent) &&
|
||||
ReplyChain.hasReply(this.props.mxEvent) ? (
|
||||
<ReplyChain
|
||||
parentEv={this.props.mxEvent}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
ref={this.replyChain}
|
||||
forExport={this.props.forExport}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
layout={this.props.layout}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
|
||||
isQuoteExpanded={isQuoteExpanded}
|
||||
setQuoteExpanded={this.setQuoteExpanded}
|
||||
/>) : null;
|
||||
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
|
||||
|
||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||
|
|
|
@ -1571,6 +1571,8 @@
|
|||
"If your other sessions do not have the key for this message you will not be able to decrypt them.": "If your other sessions do not have the key for this message you will not be able to decrypt them.",
|
||||
"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",
|
||||
"This message cannot be decrypted": "This message cannot be decrypted",
|
||||
"Encrypted by an unverified session": "Encrypted by an unverified session",
|
||||
"Unencrypted": "Unencrypted",
|
||||
|
@ -1989,10 +1991,8 @@
|
|||
"React": "React",
|
||||
"Edit": "Edit",
|
||||
"Reply": "Reply",
|
||||
"Thread": "Thread",
|
||||
"Collapse quotes │ ⇧+click": "Collapse quotes │ ⇧+click",
|
||||
"Expand quotes │ ⇧+click": "Expand quotes │ ⇧+click",
|
||||
"Message Actions": "Message Actions",
|
||||
"Download %(text)s": "Download %(text)s",
|
||||
"Error decrypting attachment": "Error decrypting attachment",
|
||||
"Decrypt %(text)s": "Decrypt %(text)s",
|
||||
|
@ -2733,6 +2733,8 @@
|
|||
"Move up": "Move up",
|
||||
"Move down": "Move down",
|
||||
"View Community": "View Community",
|
||||
"Thread options": "Thread options",
|
||||
"Copy link to thread": "Copy link to thread",
|
||||
"Unable to start audio streaming.": "Unable to start audio streaming.",
|
||||
"Failed to start livestream": "Failed to start livestream",
|
||||
"Start audio stream": "Start audio stream",
|
||||
|
@ -3006,8 +3008,6 @@
|
|||
"All threads": "All threads",
|
||||
"Shows all threads from current room": "Shows all threads from current room",
|
||||
"Show:": "Show:",
|
||||
"Thread options": "Thread options",
|
||||
"Copy link to thread": "Copy link to 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",
|
||||
|
|
|
@ -8,6 +8,7 @@ exports[`ThreadPanel Header expect that All filter for ThreadPanelHeader properl
|
|||
Threads
|
||||
</span>
|
||||
<ContextMenuButton
|
||||
className="mx_ThreadPanel_dropdown"
|
||||
inputRef={
|
||||
Object {
|
||||
"current": null,
|
||||
|
@ -29,6 +30,7 @@ exports[`ThreadPanel Header expect that My filter for ThreadPanelHeader properly
|
|||
Threads
|
||||
</span>
|
||||
<ContextMenuButton
|
||||
className="mx_ThreadPanel_dropdown"
|
||||
inputRef={
|
||||
Object {
|
||||
"current": null,
|
||||
|
|
Loading…
Reference in a new issue