Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 0bae79d

Browse files
authored
Improve Thread View UI (#7063)
1 parent 351c426 commit 0bae79d

File tree

10 files changed

+221
-41
lines changed

10 files changed

+221
-41
lines changed

res/css/structures/_ContextualMenu.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,11 @@ limitations under the License.
116116
border-top: 8px solid $menu-bg-color;
117117
border-right: 8px solid transparent;
118118
}
119+
120+
.mx_ContextualMenu_rightAligned {
121+
transform: translateX(-100%);
122+
}
123+
124+
.mx_ContextualMenu_bottomAligned {
125+
transform: translateY(-100%);
126+
}

res/css/views/right_panel/_ThreadPanel.scss

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,24 @@ limitations under the License.
1818
display: flex;
1919
flex-direction: column;
2020

21+
padding-right: 0;
22+
2123
.mx_BaseCard_header {
22-
padding: 6px 0;
24+
padding: 6px 8px 6px 0;
2325

24-
.mx_BaseCard_close {
26+
.mx_BaseCard_close,
27+
.mx_BaseCard_back {
2528
margin-top: 15px;
2629
}
27-
}
2830

29-
.mx_AccessibleButton.mx_BaseCard_back {
30-
display: none;
31+
.mx_BaseCard_close {
32+
right: -8px;
33+
}
3134
}
3235

33-
&__header {
34-
width: calc(100% - 40px);
36+
.mx_ThreadPanel__header {
37+
width: calc(100% - 60px);
38+
margin-left: 30px;
3539
display: flex;
3640
flex: 1;
3741
justify-content: space-between;
@@ -99,11 +103,39 @@ limitations under the License.
99103
}
100104
}
101105

106+
.mx_ThreadPanel_button {
107+
width: 20px;
108+
height: 20px;
109+
margin-top: -3px;
110+
margin-bottom: auto;
111+
position: relative;
112+
113+
&::before {
114+
top: 2px;
115+
left: 2px;
116+
content: '';
117+
width: 16px;
118+
height: 16px;
119+
position: absolute;
120+
mask-position: center;
121+
mask-size: contain;
122+
mask-repeat: no-repeat;
123+
background: $primary-content;
124+
}
125+
126+
&.mx_ThreadPanel_OptionsButton::before {
127+
mask-image: url('$(res)/img/element-icons/context-menu.svg');
128+
}
129+
}
130+
131+
.mx_AutoHideScrollbar {
132+
border-radius: 8px;
133+
}
134+
102135
.mx_RoomView_messageListWrapper {
103136
background-color: $background;
104-
border-radius: 8px;
105-
padding-top: 8px;
106-
padding-bottom: 12px;
137+
padding: 8px;
138+
border-radius: inherit;
107139
}
108140

109141
.mx_ScrollPanel {
@@ -116,18 +148,7 @@ limitations under the License.
116148
// Account for scrollbar when hovering
117149
width: calc(100% - 3px);
118150
margin: 0 2px;
119-
120-
.mx_MessageTimestamp {
121-
// We need to add !important here due to some enormous selectors overriding it anyways
122-
// See: _EventTile.scss:241
123-
left: unset !important;
124-
right: 0 !important;
125-
top: 16px;
126-
}
127-
128-
.mx_EventTile_line.mx_EventTile_line {
129-
position: unset;
130-
}
151+
padding-top: 0;
131152

132153
.mx_ThreadInfo {
133154
position: relative;
@@ -148,4 +169,21 @@ limitations under the License.
148169
display: none;
149170
}
150171
}
172+
173+
.mx_MessageComposer {
174+
background-color: $background;
175+
border-radius: 8px;
176+
margin-top: 8px;
177+
width: calc(100% - 8px);
178+
padding: 0 8px;
179+
box-sizing: border-box;
180+
}
181+
}
182+
183+
.mx_ThreadPanel_viewInRoom::before {
184+
mask-image: url('$(res)/img/element-icons/view-in-room.svg');
185+
}
186+
187+
.mx_ThreadPanel_copyLinkToThread::before {
188+
mask-image: url('$(res)/img/element-icons/link.svg');
151189
}

res/css/views/rooms/_EventTile.scss

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -716,19 +716,10 @@ $left-gutter: 64px;
716716
display: flex;
717717
flex-direction: column;
718718

719-
.mx_ScrollPanel {
720-
margin-top: 20px;
721-
722-
.mx_RoomView_MessageList {
723-
padding: 0;
724-
}
725-
}
726-
727719
.mx_EventTile_senderDetails {
728720
display: flex;
729721
align-items: center;
730722
gap: 6px;
731-
margin-bottom: 6px;
732723

733724
a {
734725
flex: 1;
@@ -761,22 +752,28 @@ $left-gutter: 64px;
761752
width: 100%;
762753
display: flex;
763754
flex-direction: column;
764-
margin-top: 0;
765-
padding-bottom: 5px;
766-
margin-bottom: 5px;
755+
padding-top: 0;
767756

768757
.mx_MessageTimestamp {
769758
left: auto;
770-
right: 0;
759+
right: 2px !important;
760+
top: 1px !important;
771761
}
772762

773763
.mx_ReactionsRow {
774764
order: 999;
775765
padding-left: 0;
776766
padding-right: 0;
767+
margin-left: 36px;
768+
margin-right: 50px;
777769
}
778770
}
779771

772+
.mx_EventTile_content {
773+
margin-left: 36px;
774+
margin-right: 50px;
775+
}
776+
780777
.mx_MessageComposer_sendMessage {
781778
margin-right: 0;
782779
}

res/css/views/rooms/_MessageComposer.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,12 @@ limitations under the License.
390390
padding: 0 0 0 25px;
391391
}
392392

393+
&:not(.mx_MessageComposer_e2eStatus) {
394+
.mx_MessageComposer_wrapper {
395+
padding: 0;
396+
}
397+
}
398+
393399
.mx_MessageComposer_button:last-child {
394400
margin-right: 0;
395401
}

src/components/structures/ContextMenu.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export interface IPosition {
4949
bottom?: number;
5050
left?: number;
5151
right?: number;
52+
rightAligned?: boolean;
53+
bottomAligned?: boolean;
5254
}
5355

5456
export enum ChevronFace {
@@ -346,6 +348,8 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
346348
'mx_ContextualMenu_withChevron_right': chevronFace === ChevronFace.Right,
347349
'mx_ContextualMenu_withChevron_top': chevronFace === ChevronFace.Top,
348350
'mx_ContextualMenu_withChevron_bottom': chevronFace === ChevronFace.Bottom,
351+
'mx_ContextualMenu_rightAligned': this.props.rightAligned === true,
352+
'mx_ContextualMenu_bottomAligned': this.props.bottomAligned === true,
349353
});
350354

351355
const menuStyle: CSSProperties = {};

src/components/structures/ThreadView.tsx

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ import { MatrixClientPeg } from '../../MatrixClientPeg';
3737
import { E2EStatus } from '../../utils/ShieldUtils';
3838
import EditorStateTransfer from '../../utils/EditorStateTransfer';
3939
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
40+
import { ChevronFace, ContextMenuTooltipButton } from './ContextMenu';
41+
import { _t } from '../../languageHandler';
42+
import IconizedContextMenu, {
43+
IconizedContextMenuOption,
44+
IconizedContextMenuOptionList,
45+
} from '../views/context_menus/IconizedContextMenu';
46+
import { ButtonEvent } from '../views/elements/AccessibleButton';
47+
import { copyPlaintext } from '../../utils/strings';
48+
import { sleep } from 'matrix-js-sdk/src/utils';
4049

4150
interface IProps {
4251
room: Room;
@@ -48,13 +57,28 @@ interface IProps {
4857
initialEvent?: MatrixEvent;
4958
initialEventHighlighted?: boolean;
5059
}
51-
5260
interface IState {
5361
thread?: Thread;
5462
editState?: EditorStateTransfer;
5563
replyToEvent?: MatrixEvent;
64+
threadOptionsPosition: DOMRect | null;
65+
copyingPhase: CopyingPhase;
66+
}
67+
68+
enum CopyingPhase {
69+
Idle,
70+
Copying,
71+
Failed,
5672
}
5773

74+
const contextMenuBelow = (elementRect: DOMRect) => {
75+
// align the context menu's icons with the icon which opened the context menu
76+
const left = elementRect.left + window.pageXOffset + elementRect.width;
77+
const top = elementRect.bottom + window.pageYOffset + 17;
78+
const chevronFace = ChevronFace.None;
79+
return { left, top, chevronFace };
80+
};
81+
5882
@replaceableComponent("structures.ThreadView")
5983
export default class ThreadView extends React.Component<IProps, IState> {
6084
static contextType = RoomContext;
@@ -64,7 +88,10 @@ export default class ThreadView extends React.Component<IProps, IState> {
6488

6589
constructor(props: IProps) {
6690
super(props);
67-
this.state = {};
91+
this.state = {
92+
threadOptionsPosition: null,
93+
copyingPhase: CopyingPhase.Idle,
94+
};
6895
}
6996

7097
public componentDidMount(): void {
@@ -181,6 +208,98 @@ export default class ThreadView extends React.Component<IProps, IState> {
181208
}
182209
};
183210

211+
private onThreadOptionsClick = (ev: ButtonEvent): void => {
212+
if (this.isThreadOptionsVisible) {
213+
this.closeThreadOptions();
214+
} else {
215+
const position = ev.currentTarget.getBoundingClientRect();
216+
this.setState({
217+
threadOptionsPosition: position,
218+
});
219+
}
220+
};
221+
222+
private closeThreadOptions = (): void => {
223+
this.setState({
224+
threadOptionsPosition: null,
225+
});
226+
};
227+
228+
private get isThreadOptionsVisible(): boolean {
229+
return !!this.state.threadOptionsPosition;
230+
}
231+
232+
private viewInRoom = (evt: ButtonEvent): void => {
233+
evt.preventDefault();
234+
evt.stopPropagation();
235+
dis.dispatch({
236+
action: 'view_room',
237+
event_id: this.props.mxEvent.getId(),
238+
highlighted: true,
239+
room_id: this.props.mxEvent.getRoomId(),
240+
});
241+
this.closeThreadOptions();
242+
};
243+
244+
private copyLinkToThread = async (evt: ButtonEvent): Promise<void> => {
245+
evt.preventDefault();
246+
evt.stopPropagation();
247+
248+
const matrixToUrl = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
249+
250+
this.setState({
251+
copyingPhase: CopyingPhase.Copying,
252+
});
253+
254+
const hasSuccessfullyCopied = await copyPlaintext(matrixToUrl);
255+
256+
if (hasSuccessfullyCopied) {
257+
await sleep(500);
258+
} else {
259+
this.setState({ copyingPhase: CopyingPhase.Failed });
260+
await sleep(2500);
261+
}
262+
263+
this.setState({ copyingPhase: CopyingPhase.Idle });
264+
265+
if (hasSuccessfullyCopied) {
266+
this.closeThreadOptions();
267+
}
268+
};
269+
270+
private renderThreadViewHeader = (): JSX.Element => {
271+
return <div className="mx_ThreadPanel__header">
272+
<span>{ _t("Thread") }</span>
273+
<ContextMenuTooltipButton
274+
className="mx_ThreadPanel_button mx_ThreadPanel_OptionsButton"
275+
onClick={this.onThreadOptionsClick}
276+
title={_t("Thread options")}
277+
isExpanded={this.isThreadOptionsVisible}
278+
/>
279+
{ this.isThreadOptionsVisible && (<IconizedContextMenu
280+
onFinished={this.closeThreadOptions}
281+
className="mx_RoomTile_contextMenu"
282+
compact
283+
rightAligned
284+
{...contextMenuBelow(this.state.threadOptionsPosition)}
285+
>
286+
<IconizedContextMenuOptionList>
287+
<IconizedContextMenuOption
288+
onClick={(e) => this.viewInRoom(e)}
289+
label={_t("View in room")}
290+
iconClassName="mx_ThreadPanel_viewInRoom"
291+
/>
292+
<IconizedContextMenuOption
293+
onClick={(e) => this.copyLinkToThread(e)}
294+
label={_t("Copy link to thread")}
295+
iconClassName="mx_ThreadPanel_copyLinkToThread"
296+
/>
297+
</IconizedContextMenuOptionList>
298+
</IconizedContextMenu>) }
299+
300+
</div>;
301+
};
302+
184303
public render(): JSX.Element {
185304
const highlightedEventId = this.props.initialEventHighlighted
186305
? this.props.initialEvent?.getId()
@@ -193,10 +312,11 @@ export default class ThreadView extends React.Component<IProps, IState> {
193312
}}>
194313

195314
<BaseCard
196-
className="mx_ThreadView"
315+
className="mx_ThreadView mx_ThreadPanel"
197316
onClose={this.props.onClose}
198317
previousPhase={RightPanelPhases.ThreadPanel}
199318
withoutScrollContainer={true}
319+
header={this.renderThreadViewHeader()}
200320
>
201321
{ this.state.thread && (
202322
<TimelinePanel
@@ -209,7 +329,6 @@ export default class ThreadView extends React.Component<IProps, IState> {
209329
showUrlPreview={true}
210330
tileShape={TileShape.Thread}
211331
empty={<div>empty</div>}
212-
alwaysShowTimestamps={true}
213332
layout={Layout.Group}
214333
hideThreadedMessages={false}
215334
hidden={false}

0 commit comments

Comments
 (0)