mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 01:35:49 +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;
|
padding-right: 0;
|
||||||
|
|
||||||
.mx_BaseCard_header {
|
.mx_BaseCard_header {
|
||||||
padding: 6px 8px 6px 0;
|
|
||||||
|
|
||||||
.mx_BaseCard_close,
|
.mx_BaseCard_close,
|
||||||
.mx_BaseCard_back {
|
.mx_BaseCard_back {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_BaseCard_close {
|
.mx_BaseCard_close {
|
||||||
right: -8px;
|
right: -8px;
|
||||||
}
|
}
|
||||||
|
@ -39,6 +36,7 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
span:first-of-type {
|
span:first-of-type {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
@ -49,7 +47,11 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: $secondary-content;
|
color: $primary-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageActionBar_optionsButton {
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ContextualMenu_wrapper {
|
.mx_ContextualMenu_wrapper {
|
||||||
|
@ -178,6 +180,33 @@ limitations under the License.
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
box-sizing: border-box;
|
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 {
|
.mx_ThreadPanel_viewInRoom::before {
|
||||||
|
|
|
@ -687,28 +687,82 @@ $left-gutter: 64px;
|
||||||
padding-left: 11px;
|
padding-left: 11px;
|
||||||
padding-right: 15px;
|
padding-right: 15px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_ThreadInfo_content {
|
.mx_ThreadInfo_content {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding-left: 8px;
|
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 {
|
&::after {
|
||||||
mask-image: url('$(res)/img/element-icons/thread-summary.svg');
|
content: "";
|
||||||
mask-position: center;
|
position: absolute;
|
||||||
height: 16px;
|
left: var(--leftOffset);
|
||||||
min-width: 16px;
|
right: 0;
|
||||||
background-color: $secondary-content;
|
height: 1px;
|
||||||
mask-repeat: no-repeat;
|
bottom: calc(-1 * var(--topOffset));
|
||||||
mask-size: contain;
|
background-color: $quinary-content;
|
||||||
}
|
}
|
||||||
.mx_ThreadInfo_threads-amount {
|
|
||||||
font-weight: 600;
|
&:last-child {
|
||||||
position: relative;
|
&::after {
|
||||||
padding: 0 8px;
|
content: unset;
|
||||||
white-space: nowrap;
|
}
|
||||||
|
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;
|
</ContextMenu> : null;
|
||||||
return <div className="mx_ThreadPanel__header">
|
return <div className="mx_ThreadPanel__header">
|
||||||
<span>{ _t("Threads") }</span>
|
<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}` }
|
{ `${_t('Show:')} ${value.label}` }
|
||||||
</ContextMenuButton>
|
</ContextMenuButton>
|
||||||
{ contextMenu }
|
{ contextMenu }
|
||||||
|
|
|
@ -39,15 +39,8 @@ import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||||
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||||
import ContentMessages from '../../ContentMessages';
|
import ContentMessages from '../../ContentMessages';
|
||||||
import UploadBar from './UploadBar';
|
import UploadBar from './UploadBar';
|
||||||
import { ChevronFace, ContextMenuTooltipButton } from './ContextMenu';
|
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import IconizedContextMenu, {
|
import { ThreadListContextMenu } from '../views/context_menus/ThreadListContextMenu';
|
||||||
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';
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -63,24 +56,8 @@ interface IState {
|
||||||
thread?: Thread;
|
thread?: Thread;
|
||||||
editState?: EditorStateTransfer;
|
editState?: EditorStateTransfer;
|
||||||
replyToEvent?: MatrixEvent;
|
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")
|
@replaceableComponent("structures.ThreadView")
|
||||||
export default class ThreadView extends React.Component<IProps, IState> {
|
export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
static contextType = RoomContext;
|
static contextType = RoomContext;
|
||||||
|
@ -90,12 +67,8 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {};
|
||||||
threadOptionsPosition: null,
|
|
||||||
copyingPhase: CopyingPhase.Idle,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
this.setupThread(this.props.mxEvent);
|
this.setupThread(this.props.mxEvent);
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
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 => {
|
private renderThreadViewHeader = (): JSX.Element => {
|
||||||
return <div className="mx_ThreadPanel__header">
|
return <div className="mx_ThreadPanel__header">
|
||||||
<span>{ _t("Thread") }</span>
|
<span>{ _t("Thread") }</span>
|
||||||
<ContextMenuTooltipButton
|
<ThreadListContextMenu
|
||||||
className="mx_ThreadPanel_button mx_ThreadPanel_OptionsButton"
|
mxEvent={this.props.mxEvent}
|
||||||
onClick={this.onThreadOptionsClick}
|
permalinkCreator={this.props.permalinkCreator} />
|
||||||
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>) }
|
|
||||||
|
|
||||||
</div>;
|
</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 { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { TimelineRenderingType } from "../../../contexts/RoomContext";
|
import { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||||
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||||
|
import Toolbar from '../../../accessibility/Toolbar';
|
||||||
|
import { RovingAccessibleTooltipButton } from '../../../accessibility/roving/RovingAccessibleTooltipButton';
|
||||||
|
import { ThreadListContextMenu } from '../context_menus/ThreadListContextMenu';
|
||||||
|
|
||||||
const eventTileTypes = {
|
const eventTileTypes = {
|
||||||
[EventType.RoomMessage]: 'messages.MessageEvent',
|
[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 {
|
private renderThreadInfo(): React.ReactNode {
|
||||||
if (!SettingsStore.getValue("feature_thread")) {
|
if (!SettingsStore.getValue("feature_thread")) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -569,11 +609,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [lastEvent] = thread.events
|
|
||||||
.filter(event => event.isThreadRelation)
|
|
||||||
.slice(-1);
|
|
||||||
const threadMessagePreview = MessagePreviewStore.instance.generatePreviewForEvent(lastEvent);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="mx_ThreadInfo"
|
className="mx_ThreadInfo"
|
||||||
|
@ -589,14 +624,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
count: thread.length,
|
count: thread.length,
|
||||||
}) }
|
}) }
|
||||||
</span>
|
</span>
|
||||||
{ (threadMessagePreview && lastEvent.sender) && <>
|
{ this.renderThreadLastMessagePreview() }
|
||||||
<MemberAvatar member={lastEvent.sender} width={24} height={24} />
|
|
||||||
<div className="mx_ThreadInfo_content">
|
|
||||||
<span className="mx_ThreadInfo_message-preview">
|
|
||||||
{ threadMessagePreview }
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</> }
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1199,6 +1227,20 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
msgOption = readAvatars;
|
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) {
|
switch (this.props.tileShape) {
|
||||||
case TileShape.Notif: {
|
case TileShape.Notif: {
|
||||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
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: {
|
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());
|
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||||
return React.createElement(this.props.as || "li", {
|
return React.createElement(this.props.as || "li", {
|
||||||
"className": classes,
|
"className": classes,
|
||||||
|
@ -1288,6 +1317,63 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
reactionsRow,
|
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: {
|
case TileShape.FileGrid: {
|
||||||
return React.createElement(this.props.as || "li", {
|
return React.createElement(this.props.as || "li", {
|
||||||
"className": classes,
|
"className": classes,
|
||||||
|
@ -1321,19 +1407,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
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();
|
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
|
// 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.",
|
"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.",
|
"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.",
|
"<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",
|
"This message cannot be decrypted": "This message cannot be decrypted",
|
||||||
"Encrypted by an unverified session": "Encrypted by an unverified session",
|
"Encrypted by an unverified session": "Encrypted by an unverified session",
|
||||||
"Unencrypted": "Unencrypted",
|
"Unencrypted": "Unencrypted",
|
||||||
|
@ -1989,10 +1991,8 @@
|
||||||
"React": "React",
|
"React": "React",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
"Reply": "Reply",
|
"Reply": "Reply",
|
||||||
"Thread": "Thread",
|
|
||||||
"Collapse quotes │ ⇧+click": "Collapse quotes │ ⇧+click",
|
"Collapse quotes │ ⇧+click": "Collapse quotes │ ⇧+click",
|
||||||
"Expand quotes │ ⇧+click": "Expand quotes │ ⇧+click",
|
"Expand quotes │ ⇧+click": "Expand quotes │ ⇧+click",
|
||||||
"Message Actions": "Message Actions",
|
|
||||||
"Download %(text)s": "Download %(text)s",
|
"Download %(text)s": "Download %(text)s",
|
||||||
"Error decrypting attachment": "Error decrypting attachment",
|
"Error decrypting attachment": "Error decrypting attachment",
|
||||||
"Decrypt %(text)s": "Decrypt %(text)s",
|
"Decrypt %(text)s": "Decrypt %(text)s",
|
||||||
|
@ -2733,6 +2733,8 @@
|
||||||
"Move up": "Move up",
|
"Move up": "Move up",
|
||||||
"Move down": "Move down",
|
"Move down": "Move down",
|
||||||
"View Community": "View Community",
|
"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.",
|
"Unable to start audio streaming.": "Unable to start audio streaming.",
|
||||||
"Failed to start livestream": "Failed to start livestream",
|
"Failed to start livestream": "Failed to start livestream",
|
||||||
"Start audio stream": "Start audio stream",
|
"Start audio stream": "Start audio stream",
|
||||||
|
@ -3006,8 +3008,6 @@
|
||||||
"All threads": "All threads",
|
"All threads": "All threads",
|
||||||
"Shows all threads from current room": "Shows all threads from current room",
|
"Shows all threads from current room": "Shows all threads from current room",
|
||||||
"Show:": "Show:",
|
"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 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.",
|
"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",
|
"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
|
Threads
|
||||||
</span>
|
</span>
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
|
className="mx_ThreadPanel_dropdown"
|
||||||
inputRef={
|
inputRef={
|
||||||
Object {
|
Object {
|
||||||
"current": null,
|
"current": null,
|
||||||
|
@ -29,6 +30,7 @@ exports[`ThreadPanel Header expect that My filter for ThreadPanelHeader properly
|
||||||
Threads
|
Threads
|
||||||
</span>
|
</span>
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
|
className="mx_ThreadPanel_dropdown"
|
||||||
inputRef={
|
inputRef={
|
||||||
Object {
|
Object {
|
||||||
"current": null,
|
"current": null,
|
||||||
|
|
Loading…
Reference in a new issue