Plugin

FindReply

Jumps to the earliest reply to a message in a channel (lets you follow past conversations more easily).

index.tsx
Download

Source

src/plugins/findReply/index.tsx
1/*
2 * FiveCord — a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7import { addButton, removeButton } from "@api/MessagePopover";
8import { disableStyle, enableStyle } from "@api/Styles";
9import { Devs } from "@utils/constants";
10import definePlugin, { OptionType } from "@utils/types";
11import { findByPropsLazy } from "@webpack";
12import { ChannelStore, MessageStore, ReactDOM, Toasts } from "@webpack/common";
13import Message from "discord-types/general/Message";
14import { Root } from "react-dom/client";
15
16import ReplyNavigator from "./ReplyNavigator";
17import styles from "./styles.css?managed";
18
19
20export const jumper: any = findByPropsLazy("jumpToMessage");
21const FindReplyIcon = () => {
22 return <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" width="18" height="18">
23 <path
24 d="M 7 3 L 7 11 C 7 11 7 12 6 12 L 5 12 C 4 12 4 12 4.983 13.115 L 8.164 17.036 C 9 18 9 18 9.844 17.018 L 12.991 13.277 C 14 12 14 12 13.006 11.985 L 12 12 C 12 12 11 12 11 11 L 11 3 C 11 2 11 2 10 2 L 8 2 C 7 2 7 2 7 3" />
25 </svg>;
26};
27let root: Root | null = null;
28let element: HTMLDivElement | null = null;
29let madeComponent = false;
30
31function findReplies(message: Message) {
32 const messages: Array<Message & {
33 deleted?: boolean;
34 }> = [...MessageStore.getMessages(message.channel_id)?._array ?? []].filter(m => !m.deleted).sort((a, b) => {
35 return a.timestamp.toString().localeCompare(b.timestamp.toString());
36 }); class="ts-cmt">// Need to deep copy Message array when sorting
37 const found: Message[] = [];
38 for (const other of messages) {
39 if (other.timestamp.toString().localeCompare(message.timestamp.toString()) <= 0) continue;
40 if (other.messageReference?.message_id === message.id) {
41 found.push(other);
42 }
43 if (FiveCord.Settings.plugins.FindReply.includePings) {
44 if (other.content?.includes(`<@${message.author.id}>`)) {
45 found.push(other);
46 }
47 }
48 if (FiveCord.Settings.plugins.FindReply.includeAuthor) {
49 if (messages.find(m => m.id === other.messageReference?.message_id)?.author.id === message.author.id) {
50 found.push(other);
51 }
52 }
53 }
54 return found;
55}
56
57export default definePlugin({
58 name: "FindReply",
59 description: "Jumps to the earliest reply to a message in a channel (lets you follow past conversations more easily).",
60 authors: [Devs.FiveCord],
61 start() {
62 enableStyle(styles);
63 addButton("vc-findreply", message => {
64 if (!message.id) return null;
65 const replies = findReplies(message);
66 if (FiveCord.Settings.plugins.FindReply.hideButtonIfNoReply && !replies.length) return null;
67 return {
68 label: "Jump to Reply",
69 icon: FindReplyIcon,
70 message,
71 channel: ChannelStore.getChannel(message.channel_id),
72 onClick: async () => {
73 if (replies.length) {
74 const channelId = replies[0].channel_id;
75 const messageId = replies[0].id;
76 jumper.jumpToMessage({
77 channelId,
78 messageId,
79 flash: true,
80 jumpType: "INSTANT"
81 });
82 if (replies.length > 1) {
83 Toasts.show({
84 id: Toasts.genId(),
85 message: "Use the bottom panel to navigate between replies.",
86 type: Toasts.Type.MESSAGE
87 });
88 if (!madeComponent) {
89 madeComponent = true;
90 element = document.createElement("div");
91 document.querySelector("[class^=base__]")!.appendChild(element);
92 root = ReactDOM.createRoot(element);
93 }
94 root!.render(<ReplyNavigator replies={replies} />);
95 }
96 } else {
97 Toasts.show({
98 id: Toasts.genId(),
99 message: "Couldn&#039;t find a reply.",
100 type: Toasts.Type.FAILURE
101 });
102 }
103 }
104 };
105 });
106 },
107 stop() {
108 removeButton("vc-findreply");
109 root && root.unmount();
110 element?.remove();
111 disableStyle(styles);
112 },
113 options: {
114 includePings: {
115 type: OptionType.BOOLEAN,
116 description: "Will also search for messages that @ the author directly",
117 default: false,
118 restartNeeded: false
119 },
120 includeAuthor: {
121 type: OptionType.BOOLEAN,
122 description: "Will also search for messages that reply to the author in general, not just that exact message",
123 default: false,
124 restartNeeded: false
125 },
126 hideButtonIfNoReply: {
127 type: OptionType.BOOLEAN,
128 description: "Hides the button if there are no replies to the message",
129 default: true,
130 restartNeeded: true
131 }
132 }
133});
134