Add thread information in pinned message list (#12902)

This commit is contained in:
Florian Duros 2024-08-21 11:02:35 +02:00 committed by GitHub
parent 3d80eff65b
commit a7e907e0e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 171 additions and 4 deletions

View file

@ -37,5 +37,28 @@ limitations under the License.
white-space: nowrap; white-space: nowrap;
} }
} }
.mx_PinnedEventTile_thread {
display: flex;
gap: var(--cpd-space-2x);
font: var(--cpd-font-body-sm-regular);
svg {
width: 20px;
fill: var(--cpd-color-icon-tertiary);
}
span {
display: flex;
color: var(--cpd-color-text-secondary);
}
button {
background: transparent;
border: none;
cursor: pointer;
text-decoration: underline;
}
}
} }
} }

View file

@ -23,6 +23,7 @@ import { Icon as UnpinIcon } from "@vector-im/compound-design-tokens/icons/unpin
import { Icon as ForwardIcon } from "@vector-im/compound-design-tokens/icons/forward.svg"; import { Icon as ForwardIcon } from "@vector-im/compound-design-tokens/icons/forward.svg";
import { Icon as TriggerIcon } from "@vector-im/compound-design-tokens/icons/overflow-horizontal.svg"; import { Icon as TriggerIcon } from "@vector-im/compound-design-tokens/icons/overflow-horizontal.svg";
import { Icon as DeleteIcon } from "@vector-im/compound-design-tokens/icons/delete.svg"; import { Icon as DeleteIcon } from "@vector-im/compound-design-tokens/icons/delete.svg";
import { Icon as ThreadIcon } from "@vector-im/compound-design-tokens/icons/threads.svg";
import classNames from "classnames"; import classNames from "classnames";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
@ -39,6 +40,7 @@ import { isContentActionable } from "../../../utils/EventUtils";
import { getForwardableEvent } from "../../../events"; import { getForwardableEvent } from "../../../events";
import { OpenForwardDialogPayload } from "../../../dispatcher/payloads/OpenForwardDialogPayload"; import { OpenForwardDialogPayload } from "../../../dispatcher/payloads/OpenForwardDialogPayload";
import { createRedactEventDialog } from "../dialogs/ConfirmRedactDialog"; import { createRedactEventDialog } from "../dialogs/ConfirmRedactDialog";
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
const AVATAR_SIZE = "32px"; const AVATAR_SIZE = "32px";
@ -69,6 +71,9 @@ export function PinnedEventTile({ event, room, permalinkCreator }: PinnedEventTi
throw new Error("Pinned event unexpectedly has no sender"); throw new Error("Pinned event unexpectedly has no sender");
} }
const isInThread = Boolean(event.threadRootId);
const displayThreadInfo = !event.isThreadRoot && isInThread;
return ( return (
<div className="mx_PinnedEventTile" role="listitem"> <div className="mx_PinnedEventTile" role="listitem">
<div> <div>
@ -97,6 +102,36 @@ export function PinnedEventTile({ event, room, permalinkCreator }: PinnedEventTi
permalinkCreator={permalinkCreator} permalinkCreator={permalinkCreator}
replacingEventId={event.replacingEventId()} replacingEventId={event.replacingEventId()}
/> />
{displayThreadInfo && (
<div className="mx_PinnedEventTile_thread">
<ThreadIcon />
{_t(
"right_panel|pinned_messages|reply_thread",
{},
{
link: (sub) => (
<button
type="button"
onClick={() => {
if (!event.threadRootId) return;
const rootEvent = room.findEventById(event.threadRootId);
if (!rootEvent) return;
dis.dispatch<ShowThreadPayload>({
action: Action.ShowThread,
rootEvent: rootEvent,
push: true,
});
}}
>
{sub}
</button>
),
},
)}
</div>
)}
</div> </div>
</div> </div>
); );

View file

@ -1850,6 +1850,7 @@
"other": "You can only pin up to %(count)s widgets" "other": "You can only pin up to %(count)s widgets"
}, },
"menu": "Open menu", "menu": "Open menu",
"reply_thread": "Reply to a <link>thread message</link>",
"title": "Pinned messages", "title": "Pinned messages",
"unpin_all": { "unpin_all": {
"button": "Unpin all messages", "button": "Unpin all messages",

View file

@ -97,6 +97,36 @@ describe("<PinnedEventTile />", () => {
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
it("should render pinned event with thread info", async () => {
const event = makePinEvent({
content: {
"body": "First pinned message",
"msgtype": "m.text",
"m.relates_to": {
"event_id": "$threadRootEventId",
"is_falling_back": true,
"m.in_reply_to": {
event_id: "$$threadRootEventId",
},
"rel_type": "m.thread",
},
},
});
const threadRootEvent = makePinEvent({ event_id: "$threadRootEventId" });
jest.spyOn(room, "findEventById").mockReturnValue(threadRootEvent);
const { container } = renderComponent(event);
expect(container).toMatchSnapshot();
await userEvent.click(screen.getByRole("button", { name: "thread message" }));
// Check that the thread is opened
expect(dis.dispatch).toHaveBeenCalledWith({
action: Action.ShowThread,
rootEvent: threadRootEvent,
push: true,
});
});
it("should render the menu without unpin and delete", async () => { it("should render the menu without unpin and delete", async () => {
jest.spyOn(room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "mayClientSendStateEvent").mockReturnValue( jest.spyOn(room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "mayClientSendStateEvent").mockReturnValue(
false, false,

View file

@ -65,10 +65,88 @@ exports[`<PinnedEventTile /> should render pinned event 1`] = `
</div> </div>
`; `;
exports[`<PinnedEventTile /> should render pinned event with thread info 1`] = `
<div>
<div
class="mx_PinnedEventTile"
role="listitem"
>
<div>
<span
class="_avatar_mcap2_17 mx_BaseAvatar mx_PinnedEventTile_senderAvatar _avatar-imageless_mcap2_61"
data-color="2"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
>
a
</span>
</div>
<div
class="mx_PinnedEventTile_wrapper"
>
<div
class="mx_PinnedEventTile_top"
>
<span
class="_typography_yh5dq_162 _font-body-md-semibold_yh5dq_64 mx_PinnedEventTile_sender mx_Username_color2"
>
@alice:server.org
</span>
<button
aria-disabled="false"
aria-expanded="false"
aria-haspopup="menu"
aria-label="Open menu"
class="_icon-button_bh2qc_17"
data-state="closed"
id="radix-2"
role="button"
style="--cpd-icon-button-size: 24px;"
tabindex="0"
type="button"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<div />
</div>
</button>
</div>
<div
class="mx_MTextBody mx_EventTile_content"
>
<div
class="mx_EventTile_body translate"
dir="auto"
>
First pinned message
</div>
</div>
<div
class="mx_PinnedEventTile_thread"
>
<div />
<span>
Reply to a
<button
type="button"
>
thread message
</button>
</span>
</div>
</div>
</div>
</div>
`;
exports[`<PinnedEventTile /> should render the menu with all the options 1`] = ` exports[`<PinnedEventTile /> should render the menu with all the options 1`] = `
<div <div
aria-label="Open menu" aria-label="Open menu"
aria-labelledby="radix-6" aria-labelledby="radix-8"
aria-orientation="vertical" aria-orientation="vertical"
class="_menu_1x5h1_17" class="_menu_1x5h1_17"
data-align="start" data-align="start"
@ -77,7 +155,7 @@ exports[`<PinnedEventTile /> should render the menu with all the options 1`] = `
data-side="right" data-side="right"
data-state="open" data-state="open"
dir="ltr" dir="ltr"
id="radix-7" id="radix-9"
role="menu" role="menu"
style="outline: none; --radix-dropdown-menu-content-transform-origin: var(--radix-popper-transform-origin); --radix-dropdown-menu-content-available-width: var(--radix-popper-available-width); --radix-dropdown-menu-content-available-height: var(--radix-popper-available-height); --radix-dropdown-menu-trigger-width: var(--radix-popper-anchor-width); --radix-dropdown-menu-trigger-height: var(--radix-popper-anchor-height); pointer-events: auto;" style="outline: none; --radix-dropdown-menu-content-transform-origin: var(--radix-popper-transform-origin); --radix-dropdown-menu-content-available-width: var(--radix-popper-available-width); --radix-dropdown-menu-content-available-height: var(--radix-popper-available-height); --radix-dropdown-menu-trigger-width: var(--radix-popper-anchor-width); --radix-dropdown-menu-trigger-height: var(--radix-popper-anchor-height); pointer-events: auto;"
tabindex="-1" tabindex="-1"
@ -226,7 +304,7 @@ exports[`<PinnedEventTile /> should render the menu with all the options 1`] = `
exports[`<PinnedEventTile /> should render the menu without unpin and delete 1`] = ` exports[`<PinnedEventTile /> should render the menu without unpin and delete 1`] = `
<div <div
aria-label="Open menu" aria-label="Open menu"
aria-labelledby="radix-2" aria-labelledby="radix-4"
aria-orientation="vertical" aria-orientation="vertical"
class="_menu_1x5h1_17" class="_menu_1x5h1_17"
data-align="start" data-align="start"
@ -235,7 +313,7 @@ exports[`<PinnedEventTile /> should render the menu without unpin and delete 1`]
data-side="right" data-side="right"
data-state="open" data-state="open"
dir="ltr" dir="ltr"
id="radix-3" id="radix-5"
role="menu" role="menu"
style="outline: none; --radix-dropdown-menu-content-transform-origin: var(--radix-popper-transform-origin); --radix-dropdown-menu-content-available-width: var(--radix-popper-available-width); --radix-dropdown-menu-content-available-height: var(--radix-popper-available-height); --radix-dropdown-menu-trigger-width: var(--radix-popper-anchor-width); --radix-dropdown-menu-trigger-height: var(--radix-popper-anchor-height); pointer-events: auto;" style="outline: none; --radix-dropdown-menu-content-transform-origin: var(--radix-popper-transform-origin); --radix-dropdown-menu-content-available-width: var(--radix-popper-available-width); --radix-dropdown-menu-content-available-height: var(--radix-popper-available-height); --radix-dropdown-menu-trigger-width: var(--radix-popper-anchor-width); --radix-dropdown-menu-trigger-height: var(--radix-popper-anchor-height); pointer-events: auto;"
tabindex="-1" tabindex="-1"