mirror of
https://github.com/element-hq/element-web
synced 2024-11-22 09:15:41 +03:00
Fixes following threads design implementation review (#7100)
This commit is contained in:
parent
b8edebecc9
commit
1de9630e44
16 changed files with 280 additions and 115 deletions
|
@ -38,7 +38,6 @@ limitations under the License.
|
|||
position: absolute;
|
||||
font-size: $font-14px;
|
||||
z-index: 5001;
|
||||
contain: content;
|
||||
}
|
||||
|
||||
.mx_ContextualMenu_right {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
5
res/img/element-icons/message/overflow-large.svg
Normal file
5
res/img/element-icons/message/overflow-large.svg
Normal 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 |
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()}
|
||||
>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
/>);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }
|
||||
</>)
|
||||
);
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue