Skip to content

Commit aeabf3b

Browse files
authored
Show message type prefix in thread root & reply previews (#28361)
* Extract EventPreview from PinnedMessageBanner Signed-off-by: Michael Telatynski <[email protected]> * Show message type prefix in thread root previews Signed-off-by: Michael Telatynski <[email protected]> * Show message type prefix in thread reply preview Signed-off-by: Michael Telatynski <[email protected]> * Update tests Signed-off-by: Michael Telatynski <[email protected]> * Iterate Signed-off-by: Michael Telatynski <[email protected]> --------- Signed-off-by: Michael Telatynski <[email protected]>
1 parent c9d9c42 commit aeabf3b

File tree

11 files changed

+215
-154
lines changed

11 files changed

+215
-154
lines changed

res/css/_components.pcss

+1
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@
282282
@import "./views/rooms/_EmojiButton.pcss";
283283
@import "./views/rooms/_EntityTile.pcss";
284284
@import "./views/rooms/_EventBubbleTile.pcss";
285+
@import "./views/rooms/_EventPreview.pcss";
285286
@import "./views/rooms/_EventTile.pcss";
286287
@import "./views/rooms/_HistoryTile.pcss";
287288
@import "./views/rooms/_IRCLayout.pcss";
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
* Copyright 2024 The Matrix.org Foundation C.I.C.
4+
*
5+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
6+
* Please see LICENSE files in the repository root for full details.
7+
*/
8+
9+
.mx_EventPreview {
10+
font: var(--cpd-font-body-sm-regular);
11+
overflow: hidden;
12+
text-overflow: ellipsis;
13+
white-space: nowrap;
14+
15+
.mx_EventPreview_prefix {
16+
font: var(--cpd-font-body-sm-semibold);
17+
}
18+
}

res/css/views/rooms/_PinnedMessageBanner.pcss

-8
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,7 @@
8181

8282
.mx_PinnedMessageBanner_message {
8383
grid-area: message;
84-
font: var(--cpd-font-body-sm-regular);
8584
line-height: 20px;
86-
overflow: hidden;
87-
text-overflow: ellipsis;
88-
white-space: nowrap;
89-
90-
.mx_PinnedMessageBanner_prefix {
91-
font: var(--cpd-font-body-sm-semibold);
92-
}
9385
}
9486

9587
.mx_PinnedMessageBanner_redactedMessage {
+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
* Copyright 2024 The Matrix.org Foundation C.I.C.
4+
*
5+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
6+
* Please see LICENSE files in the repository root for full details.
7+
*/
8+
9+
import React, { HTMLProps, JSX, useContext, useState } from "react";
10+
import { IContent, M_POLL_START, MatrixEvent, MatrixEventEvent, MsgType } from "matrix-js-sdk/src/matrix";
11+
import classNames from "classnames";
12+
13+
import { _t } from "../../../languageHandler";
14+
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
15+
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
16+
import MatrixClientContext from "../../../contexts/MatrixClientContext";
17+
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter.ts";
18+
19+
/**
20+
* The props for the {@link EventPreview} component.
21+
*/
22+
interface Props extends HTMLProps<HTMLSpanElement> {
23+
/**
24+
* The event to display the preview for
25+
*/
26+
mxEvent: MatrixEvent;
27+
}
28+
29+
/**
30+
* A component that displays a preview for the given event.
31+
* Wraps both `useEventPreview` & `EventPreviewTile`.
32+
*/
33+
export function EventPreview({ mxEvent, className, ...props }: Props): JSX.Element | null {
34+
const preview = useEventPreview(mxEvent);
35+
if (!preview) return null;
36+
37+
return <EventPreviewTile {...props} preview={preview} className={className} />;
38+
}
39+
40+
/**
41+
* The props for the {@link EventPreviewTile} component.
42+
*/
43+
interface EventPreviewTileProps extends HTMLProps<HTMLSpanElement> {
44+
/**
45+
* The preview to display
46+
*/
47+
preview: Preview;
48+
}
49+
50+
/**
51+
* A component that displays a preview given the output from `useEventPreview`.
52+
*/
53+
export function EventPreviewTile({
54+
preview: [preview, prefix],
55+
className,
56+
...props
57+
}: EventPreviewTileProps): JSX.Element | null {
58+
const classes = classNames("mx_EventPreview", className);
59+
if (!prefix)
60+
return (
61+
<span {...props} className={classes} title={preview}>
62+
{preview}
63+
</span>
64+
);
65+
66+
return (
67+
<span {...props} className={classes}>
68+
{_t(
69+
"event_preview|preview",
70+
{
71+
prefix,
72+
preview,
73+
},
74+
{
75+
bold: (sub) => <span className="mx_EventPreview_prefix">{sub}</span>,
76+
},
77+
)}
78+
</span>
79+
);
80+
}
81+
82+
type Preview = [preview: string, prefix: string | null];
83+
84+
/**
85+
* Hooks to generate a preview for the event.
86+
* @param mxEvent
87+
*/
88+
export function useEventPreview(mxEvent: MatrixEvent | undefined): Preview | null {
89+
const cli = useContext(MatrixClientContext);
90+
// track the content as a means to regenerate the preview upon edits & decryption
91+
const [content, setContent] = useState<IContent | undefined>(mxEvent?.getContent());
92+
useTypedEventEmitter(mxEvent ?? undefined, MatrixEventEvent.Replaced, () => {
93+
setContent(mxEvent!.getContent());
94+
});
95+
const awaitDecryption = mxEvent?.shouldAttemptDecryption() || mxEvent?.isBeingDecrypted();
96+
useTypedEventEmitter(awaitDecryption ? (mxEvent ?? undefined) : undefined, MatrixEventEvent.Decrypted, () => {
97+
setContent(mxEvent!.getContent());
98+
});
99+
100+
return useAsyncMemo(
101+
async () => {
102+
if (!mxEvent || mxEvent.isRedacted() || mxEvent.isDecryptionFailure()) return null;
103+
await cli.decryptEventIfNeeded(mxEvent);
104+
return [
105+
MessagePreviewStore.instance.generatePreviewForEvent(mxEvent),
106+
getPreviewPrefix(mxEvent.getType(), content?.msgtype as MsgType),
107+
];
108+
},
109+
[mxEvent, content],
110+
null,
111+
);
112+
}
113+
114+
/**
115+
* Get the prefix for the preview based on the type and the message type.
116+
* @param type
117+
* @param msgType
118+
*/
119+
function getPreviewPrefix(type: string, msgType: MsgType): string | null {
120+
switch (type) {
121+
case M_POLL_START.name:
122+
return _t("event_preview|prefix|poll");
123+
default:
124+
}
125+
126+
switch (msgType) {
127+
case MsgType.Audio:
128+
return _t("event_preview|prefix|audio");
129+
case MsgType.Image:
130+
return _t("event_preview|prefix|image");
131+
case MsgType.Video:
132+
return _t("event_preview|prefix|video");
133+
case MsgType.File:
134+
return _t("event_preview|prefix|file");
135+
default:
136+
return null;
137+
}
138+
}

src/components/views/rooms/EventTile.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ import { IReadReceiptPosition } from "./ReadReceiptMarker";
6161
import MessageActionBar from "../messages/MessageActionBar";
6262
import ReactionsRow from "../messages/ReactionsRow";
6363
import { getEventDisplayInfo } from "../../../utils/EventRenderingUtils";
64-
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
6564
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
6665
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
6766
import { ButtonEvent } from "../elements/AccessibleButton";
@@ -83,6 +82,7 @@ import { EventTileThreadToolbar } from "./EventTile/EventTileThreadToolbar";
8382
import { getLateEventInfo } from "../../structures/grouper/LateEventGrouper";
8483
import PinningUtils from "../../../utils/PinningUtils";
8584
import { PinnedMessageBadge } from "../messages/PinnedMessageBadge";
85+
import { EventPreview } from "./EventPreview";
8686

8787
export type GetRelationsForEvent = (
8888
eventId: string,
@@ -1341,7 +1341,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
13411341
) : this.props.mxEvent.isDecryptionFailure() ? (
13421342
<DecryptionFailureBody mxEvent={this.props.mxEvent} />
13431343
) : (
1344-
MessagePreviewStore.instance.generatePreviewForEvent(this.props.mxEvent)
1344+
<EventPreview mxEvent={this.props.mxEvent} />
13451345
)}
13461346
</div>
13471347
{this.renderThreadPanelSummary()}

src/components/views/rooms/PinnedMessageBanner.tsx

+8-82
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
* Please see LICENSE files in the repository root for full details.
77
*/
88

9-
import React, { JSX, useEffect, useMemo, useState } from "react";
9+
import React, { JSX, useEffect, useState } from "react";
1010
import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin-solid";
1111
import { Button } from "@vector-im/compound-web";
12-
import { M_POLL_START, MatrixEvent, MsgType, Room } from "matrix-js-sdk/src/matrix";
12+
import { Room } from "matrix-js-sdk/src/matrix";
1313
import classNames from "classnames";
1414

1515
import { usePinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents";
@@ -19,12 +19,12 @@ import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePha
1919
import { useEventEmitter } from "../../../hooks/useEventEmitter";
2020
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
2121
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
22-
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
2322
import dis from "../../../dispatcher/dispatcher";
2423
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
2524
import { Action } from "../../../dispatcher/actions";
2625
import MessageEvent from "../messages/MessageEvent";
2726
import PosthogTrackers from "../../../PosthogTrackers.ts";
27+
import { EventPreview } from "./EventPreview.tsx";
2828

2929
/**
3030
* The props for the {@link PinnedMessageBanner} component.
@@ -105,7 +105,11 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan
105105
)}
106106
</div>
107107
)}
108-
<EventPreview pinnedEvent={pinnedEvent} />
108+
<EventPreview
109+
mxEvent={pinnedEvent}
110+
className="mx_PinnedMessageBanner_message"
111+
data-testid="banner-message"
112+
/>
109113
{/* In case of redacted event, we want to display the nice sentence of the message event like in the timeline or in the pinned message list */}
110114
{shouldUseMessageEvent && (
111115
<div className="mx_PinnedMessageBanner_redactedMessage">
@@ -124,84 +128,6 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan
124128
);
125129
}
126130

127-
/**
128-
* The props for the {@link EventPreview} component.
129-
*/
130-
interface EventPreviewProps {
131-
/**
132-
* The pinned event to display the preview for
133-
*/
134-
pinnedEvent: MatrixEvent;
135-
}
136-
137-
/**
138-
* A component that displays a preview for the pinned event.
139-
*/
140-
function EventPreview({ pinnedEvent }: EventPreviewProps): JSX.Element | null {
141-
const preview = useEventPreview(pinnedEvent);
142-
if (!preview) return null;
143-
144-
const prefix = getPreviewPrefix(pinnedEvent.getType(), pinnedEvent.getContent().msgtype as MsgType);
145-
if (!prefix)
146-
return (
147-
<span className="mx_PinnedMessageBanner_message" data-testid="banner-message">
148-
{preview}
149-
</span>
150-
);
151-
152-
return (
153-
<span className="mx_PinnedMessageBanner_message" data-testid="banner-message">
154-
{_t(
155-
"room|pinned_message_banner|preview",
156-
{
157-
prefix,
158-
preview,
159-
},
160-
{
161-
bold: (sub) => <span className="mx_PinnedMessageBanner_prefix">{sub}</span>,
162-
},
163-
)}
164-
</span>
165-
);
166-
}
167-
168-
/**
169-
* Hooks to generate a preview for the pinned event.
170-
* @param pinnedEvent
171-
*/
172-
function useEventPreview(pinnedEvent: MatrixEvent | null): string | null {
173-
return useMemo(() => {
174-
if (!pinnedEvent || pinnedEvent.isRedacted() || pinnedEvent.isDecryptionFailure()) return null;
175-
return MessagePreviewStore.instance.generatePreviewForEvent(pinnedEvent);
176-
}, [pinnedEvent]);
177-
}
178-
179-
/**
180-
* Get the prefix for the preview based on the type and the message type.
181-
* @param type
182-
* @param msgType
183-
*/
184-
function getPreviewPrefix(type: string, msgType: MsgType): string | null {
185-
switch (type) {
186-
case M_POLL_START.name:
187-
return _t("room|pinned_message_banner|prefix|poll");
188-
default:
189-
}
190-
191-
switch (msgType) {
192-
case MsgType.Audio:
193-
return _t("room|pinned_message_banner|prefix|audio");
194-
case MsgType.Image:
195-
return _t("room|pinned_message_banner|prefix|image");
196-
case MsgType.Video:
197-
return _t("room|pinned_message_banner|prefix|video");
198-
case MsgType.File:
199-
return _t("room|pinned_message_banner|prefix|file");
200-
default:
201-
return null;
202-
}
203-
}
204-
205131
const MAX_INDICATORS = 3;
206132

207133
/**

0 commit comments

Comments
 (0)