From 4c09954eff638c0f9afe8698e9ee27a3df2e1a7a Mon Sep 17 00:00:00 2001 From: michal-skraburski Date: Fri, 28 Aug 2026 15:17:52 +0100 Subject: [PATCH 1/5] feat: add a quote reaction + text hint for removing quote embeds --- src/features/quote/embed.ts | 30 +++++++++++++++++++++++++++++- src/features/quote/index.ts | 3 ++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/features/quote/embed.ts b/src/features/quote/embed.ts index d8929b0..c8f9c6a 100644 --- a/src/features/quote/embed.ts +++ b/src/features/quote/embed.ts @@ -17,6 +17,15 @@ import { const EMBED_DESC_LIMIT = 4096; const FIELD_VALUE_LIMIT = 1024; const JUMP_BUTTON_LABEL = 'Jump to message'; +export const DELETE_HINT_EMOJI = '❌'; +const DELETE_HINT = `React with ${DELETE_HINT_EMOJI} to delete`; +const DELETE_HINT_LINE = `-# ${DELETE_HINT}`; + +const applyDeleteHint = (embed: EmbedBuilder): void => { + if (!embed.data.footer?.text) { + embed.setFooter({ text: DELETE_HINT }); + } +}; type OriginalQuoteInfo = { authorMention: string; @@ -130,6 +139,17 @@ export const createQuoteEmbed = ({ components.push(attributionLine); } + const hasDeleteHint = components.some( + (component) => + component.type === ComponentType.TextDisplay && + (component as { content: string }).content === DELETE_HINT_LINE + ); + if (!hasDeleteHint) { + components.push( + new TextDisplayBuilder().setContent(DELETE_HINT_LINE).toJSON() + ); + } + return { allowedMentions: { parse: [] }, components, @@ -152,12 +172,14 @@ export const createQuoteEmbed = ({ // Find an existing "Quoted by" field, if quotedMessage is itself a quote. let existingField: APIEmbedField | null = null; + let existingFieldEmbed: EmbedBuilder | null = null; for (const embed of embeds) { const found = embed.data.fields?.find( (field) => /^quoted by$/i.test(field.name) && parseQuoteLine(field.value) ); if (found) { existingField = found; + existingFieldEmbed = embed; break; } } @@ -188,6 +210,9 @@ export const createQuoteEmbed = ({ // Already a quote: swap the field's value in place, keep everything // else (original author/description/image/timestamp) untouched. existingField.value = quotedByField.value; + if (existingFieldEmbed) { + applyDeleteHint(existingFieldEmbed); + } } else { // First-time quote: build the wrapper/annotation. const authorOptions = { @@ -195,8 +220,11 @@ export const createQuoteEmbed = ({ iconURL: quotedMessage.author.displayAvatarURL({ size: 64 }), }; - const stampAsQuote = (embed: EmbedBuilder) => + const stampAsQuote = (embed: EmbedBuilder) => { embed.setAuthor(authorOptions).addFields(quotedByField).setTimestamp(); + applyDeleteHint(embed); + return embed; + }; const hasContent = quotedMessage.content.length > 0; const hasEmbeds = embeds.length > 0; diff --git a/src/features/quote/index.ts b/src/features/quote/index.ts index dceaa45..68ad3ad 100644 --- a/src/features/quote/index.ts +++ b/src/features/quote/index.ts @@ -1,7 +1,7 @@ import { Client, Events, type Message } from 'discord.js'; import { createEvent } from '@/common/events/create-event.js'; import { UserBotMessagesService } from '@/services/user-bot-messages/user-bot-messages-service.js'; -import { createQuoteEmbed } from './embed.js'; +import { createQuoteEmbed, DELETE_HINT_EMOJI } from './embed.js'; export const quoteEvent = createEvent( { @@ -81,6 +81,7 @@ export const quoteEvent = createEvent( ? { ...options, reply: { messageReference: referenceMessageId } } : options ); + void sentMessage.react(DELETE_HINT_EMOJI); void UserBotMessagesService.addUserBotMessage({ messageId: sentMessage.id, userId: message.author.id, From 0e59cf06aadbc31e4be453c313c237473f9e77af Mon Sep 17 00:00:00 2001 From: michal-skraburski Date: Fri, 28 Aug 2026 15:57:48 +0100 Subject: [PATCH 2/5] fix: remove auto-react off of quote embeds --- src/features/quote/embed.ts | 3 +-- src/features/quote/index.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/features/quote/embed.ts b/src/features/quote/embed.ts index c8f9c6a..493ef31 100644 --- a/src/features/quote/embed.ts +++ b/src/features/quote/embed.ts @@ -17,8 +17,7 @@ import { const EMBED_DESC_LIMIT = 4096; const FIELD_VALUE_LIMIT = 1024; const JUMP_BUTTON_LABEL = 'Jump to message'; -export const DELETE_HINT_EMOJI = '❌'; -const DELETE_HINT = `React with ${DELETE_HINT_EMOJI} to delete`; +const DELETE_HINT = `React with ❌ to delete`; const DELETE_HINT_LINE = `-# ${DELETE_HINT}`; const applyDeleteHint = (embed: EmbedBuilder): void => { diff --git a/src/features/quote/index.ts b/src/features/quote/index.ts index 68ad3ad..dceaa45 100644 --- a/src/features/quote/index.ts +++ b/src/features/quote/index.ts @@ -1,7 +1,7 @@ import { Client, Events, type Message } from 'discord.js'; import { createEvent } from '@/common/events/create-event.js'; import { UserBotMessagesService } from '@/services/user-bot-messages/user-bot-messages-service.js'; -import { createQuoteEmbed, DELETE_HINT_EMOJI } from './embed.js'; +import { createQuoteEmbed } from './embed.js'; export const quoteEvent = createEvent( { @@ -81,7 +81,6 @@ export const quoteEvent = createEvent( ? { ...options, reply: { messageReference: referenceMessageId } } : options ); - void sentMessage.react(DELETE_HINT_EMOJI); void UserBotMessagesService.addUserBotMessage({ messageId: sentMessage.id, userId: message.author.id, From f1b14cea7461bc6d840ce5afd6cc8a456383af76 Mon Sep 17 00:00:00 2001 From: michal-skraburski Date: Fri, 28 Aug 2026 16:34:55 +0100 Subject: [PATCH 3/5] fix: change footer to append, add wastebin, coauthor: hmd-ali/cake --- src/features/quote/embed.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/features/quote/embed.ts b/src/features/quote/embed.ts index 493ef31..f5404f4 100644 --- a/src/features/quote/embed.ts +++ b/src/features/quote/embed.ts @@ -17,12 +17,14 @@ import { const EMBED_DESC_LIMIT = 4096; const FIELD_VALUE_LIMIT = 1024; const JUMP_BUTTON_LABEL = 'Jump to message'; -const DELETE_HINT = `React with ❌ to delete`; +const DELETE_HINT = `React with ❌/🗑️ to delete`; const DELETE_HINT_LINE = `-# ${DELETE_HINT}`; const applyDeleteHint = (embed: EmbedBuilder): void => { if (!embed.data.footer?.text) { embed.setFooter({ text: DELETE_HINT }); + } else if (!embed.data.footer.text.includes(DELETE_HINT)) { + embed.setFooter({ text: `${embed.data.footer.text} | ${DELETE_HINT}` }); } }; From b78e9e7217efafb75d7386a9dbac14901e0209a2 Mon Sep 17 00:00:00 2001 From: michal-skraburski Date: Fri, 28 Aug 2026 17:23:16 +0100 Subject: [PATCH 4/5] fix: quote now refers to reactions feat. emojis --- src/features/quote/embed.ts | 5 ++++- src/features/reactions/remove-user-bot-message.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/features/quote/embed.ts b/src/features/quote/embed.ts index f5404f4..1b0a97d 100644 --- a/src/features/quote/embed.ts +++ b/src/features/quote/embed.ts @@ -14,10 +14,13 @@ import { type User, } from 'discord.js'; +import { DELETE_EMOJIS } from '../reactions/remove-user-bot-message.js'; + const EMBED_DESC_LIMIT = 4096; const FIELD_VALUE_LIMIT = 1024; const JUMP_BUTTON_LABEL = 'Jump to message'; -const DELETE_HINT = `React with ❌/🗑️ to delete`; +const EMOJIS = DELETE_EMOJIS.join('/'); +const DELETE_HINT = `React with ${EMOJIS} to delete`; const DELETE_HINT_LINE = `-# ${DELETE_HINT}`; const applyDeleteHint = (embed: EmbedBuilder): void => { diff --git a/src/features/reactions/remove-user-bot-message.ts b/src/features/reactions/remove-user-bot-message.ts index ccab885..6935768 100644 --- a/src/features/reactions/remove-user-bot-message.ts +++ b/src/features/reactions/remove-user-bot-message.ts @@ -1,7 +1,7 @@ import type { ClientEvents, Events, GuildMember } from 'discord.js'; import { UserBotMessagesService } from '@/services/user-bot-messages/user-bot-messages-service.js'; -const DELETE_EMOJIS = ['🗑️', '❌']; +export const DELETE_EMOJIS = ['🗑️', '❌']; export const removeUserBotMessage: ( ...args: ClientEvents[Events.MessageReactionAdd] From 444d6e466ce84725f880b9925f8cc5447f2ec1b4 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 30 Aug 2026 14:03:21 +0300 Subject: [PATCH 5/5] refactor: remove `as` and introducde a type predicate --- src/features/quote/embed.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/features/quote/embed.ts b/src/features/quote/embed.ts index 1b0a97d..92ddd75 100644 --- a/src/features/quote/embed.ts +++ b/src/features/quote/embed.ts @@ -2,6 +2,8 @@ import { clampText } from '@/util/text.js'; import { ActionRowBuilder, type APIEmbedField, + type APIMessageTopLevelComponent, + type APITextDisplayComponent, ButtonBuilder, ButtonStyle, ComponentType, @@ -23,6 +25,11 @@ const EMOJIS = DELETE_EMOJIS.join('/'); const DELETE_HINT = `React with ${EMOJIS} to delete`; const DELETE_HINT_LINE = `-# ${DELETE_HINT}`; +const isTextDisplayComponent = ( + component: APIMessageTopLevelComponent +): component is APITextDisplayComponent => + component.type === ComponentType.TextDisplay; + const applyDeleteHint = (embed: EmbedBuilder): void => { if (!embed.data.footer?.text) { embed.setFooter({ text: DELETE_HINT }); @@ -117,9 +124,10 @@ export const createQuoteEmbed = ({ const existingLineIndex = components.findIndex( (component) => component.type === ComponentType.TextDisplay ); + const existingComponent = components[existingLineIndex]; const existingContent = - existingLineIndex !== -1 - ? (components[existingLineIndex] as { content: string }).content + existingComponent && isTextDisplayComponent(existingComponent) + ? existingComponent.content : null; const parsed = @@ -145,8 +153,8 @@ export const createQuoteEmbed = ({ const hasDeleteHint = components.some( (component) => - component.type === ComponentType.TextDisplay && - (component as { content: string }).content === DELETE_HINT_LINE + isTextDisplayComponent(component) && + component.content === DELETE_HINT_LINE ); if (!hasDeleteHint) { components.push(