Plugin
FindReply
Jumps to the earliest reply to a message in a channel (lets you follow past conversations more easily).
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import { addButton, removeButton } from "@api/MessagePopover";8
import { disableStyle, enableStyle } from "@api/Styles";9
import { Devs } from "@utils/constants";10
import definePlugin, { OptionType } from "@utils/types";11
import { findByPropsLazy } from "@webpack";12
import { ChannelStore, MessageStore, ReactDOM, Toasts } from "@webpack/common";13
import Message from "discord-types/general/Message";14
import { Root } from "react-dom/client";15
16
import ReplyNavigator from "./ReplyNavigator";17
import styles from "./styles.css?managed";18
19
20
export const jumper: any = findByPropsLazy("jumpToMessage");21
const FindReplyIcon = () => {22
return <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" width="18" height="18">23
<path24
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
};27
let root: Root | null = null;28
let element: HTMLDivElement | null = null;29
let madeComponent = false;30
31
function 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 sorting37
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
57
export 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.MESSAGE87
});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: "Couldn039;t find a reply.",100
type: Toasts.Type.FAILURE101
});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: false119
},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: false125
},126
hideButtonIfNoReply: {127
type: OptionType.BOOLEAN,128
description: "Hides the button if there are no replies to the message",129
default: true,130
restartNeeded: true131
}132
}133
});134