diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/server/discord/DiscordEventHandler.java b/core/src/main/java/com/xujiayao/discord_mc_chat/server/discord/DiscordEventHandler.java index f8d00c43..cbf173d6 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/server/discord/DiscordEventHandler.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/server/discord/DiscordEventHandler.java @@ -47,7 +47,20 @@ public class DiscordEventHandler extends ListenerAdapter { private static final ConcurrentHashMap messageCache = new ConcurrentHashMap<>(); private static final int MAX_CACHE_SIZE = 200; - private record CachedMessage(String authorName, String authorRoleColor, String contentRaw, Message contextMessage) {} + /** + * Cached Discord message metadata for reply/edit/delete context rendering. + *

+ * Stores the already-rendered one-line reply preview so fallback rendering keeps the + * same formatting pipeline (including webhook message formatting) without retaining full + * JDA {@link Message} objects in memory. + * {@code replySegments} may be null when a preview cannot be produced for a message. + * + * @param authorName Cached effective name of the message author. + * @param authorRoleColor Cached role color of the message author. + * @param contentRaw Cached raw content for fallback rebuilding. + * @param replySegments Cached rendered reply preview, nullable. + */ + private record CachedMessage(String authorName, String authorRoleColor, String contentRaw, List replySegments) {} /** * Resolves the OP Level credential for a Discord user based on config mappings. @@ -380,9 +393,12 @@ public class DiscordEventHandler extends ListenerAdapter { replySegments = DiscordMessageParser.buildReplySegments( cachedRef.authorName(), cachedRef.authorRoleColor(), - cachedRef.contextMessage(), + null, cachedRef.contentRaw() ); + if (cachedRef.replySegments() != null && !cachedRef.replySegments().isEmpty()) { + replySegments = cachedRef.replySegments(); + } } } @@ -495,12 +511,15 @@ public class DiscordEventHandler extends ListenerAdapter { CachedMessage cached = messageCache.get(message.getId()); List replySegments = null; if (cached != null && cached.contentRaw() != null) { - replySegments = DiscordMessageParser.buildReplySegments( - cached.authorName(), - cached.authorRoleColor(), - cached.contextMessage(), - cached.contentRaw() - ); + replySegments = cached.replySegments(); + if (replySegments == null || replySegments.isEmpty()) { + replySegments = DiscordMessageParser.buildReplySegments( + cached.authorName(), + cached.authorRoleColor(), + null, + cached.contentRaw() + ); + } } // Build edit notification segments @@ -552,9 +571,12 @@ public class DiscordEventHandler extends ListenerAdapter { packet.replySegments = DiscordMessageParser.buildReplySegments( cached.authorName(), cached.authorRoleColor(), - cached.contextMessage(), + null, cached.contentRaw() ); + if (cached.replySegments() != null && !cached.replySegments().isEmpty()) { + packet.replySegments = cached.replySegments(); + } logDiscordEventForConsole(packet); NetworkManager.broadcastToClients(packet); } @@ -575,7 +597,8 @@ public class DiscordEventHandler extends ListenerAdapter { Member member = message.getMember(); String name = member != null ? member.getEffectiveName() : message.getAuthor().getName(); String roleColor = DiscordMessageParser.getRoleColorHex(member); - messageCache.put(message.getId(), new CachedMessage(name, roleColor, message.getContentRaw(), message)); + List replySegments = DiscordMessageParser.buildReplySegments(message); + messageCache.put(message.getId(), new CachedMessage(name, roleColor, message.getContentRaw(), replySegments)); } private static void logDiscordEventForConsole(DiscordEventPacket packet) { diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/server/discord/DiscordMessageParser.java b/core/src/main/java/com/xujiayao/discord_mc_chat/server/discord/DiscordMessageParser.java index 16d80052..02c7f266 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/server/discord/DiscordMessageParser.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/server/discord/DiscordMessageParser.java @@ -92,9 +92,13 @@ private static final Pattern BOLD_ITALIC_URL_PATTERN = Pattern.compile("\\*\\*\\ private static final Pattern BOLD_URL_PATTERN = Pattern.compile("\\*\\*(https?://[^\\s*|~`<>)\\]]+)\\*\\*"); private static final Pattern SPOILER_ITALIC_URL_PATTERN = Pattern.compile("\\|\\|\\*(https?://[^\\s*|~`<>)\\]]+)\\*\\|\\|"); private static final Pattern SPOILER_URL_PATTERN = Pattern.compile("\\|\\|(https?://[^\\s*|~`<>)\\]]+)\\|\\|"); +// Matches spoiler-wrapped user mentions: ||<@123>|| / ||<@!123>|| private static final Pattern SPOILER_USER_MENTION_PATTERN = Pattern.compile("\\|\\|<@!?(\\d+)>\\|\\|"); +// Matches spoiler-wrapped role mentions: ||<@&123>|| private static final Pattern SPOILER_ROLE_MENTION_PATTERN = Pattern.compile("\\|\\|<@&(\\d+)>\\|\\|"); +// Matches spoiler-wrapped channel mentions: ||<#123>|| private static final Pattern SPOILER_CHANNEL_MENTION_PATTERN = Pattern.compile("\\|\\|<#(\\d+)>\\|\\|"); +// Matches spoiler-wrapped @everyone/@here tokens: ||@everyone|| / ||@here|| private static final Pattern SPOILER_EVERYONE_HERE_PATTERN = Pattern.compile("\\|\\|@(everyone|here)\\|\\|"); private static final Pattern SPOILER_CONTENT_PATTERN = Pattern.compile("\\|\\|(.+?)\\|\\|"); private static final Pattern LINK_TOKEN_PATTERN = Pattern.compile("\\[([^]]+)]\\((https?://[^)]+)\\)"); @@ -106,6 +110,9 @@ private static final int REPLY_TRUNCATE_LIMIT_NARROW = 40; private static final int MAIN_TRUNCATE_LIMIT_WIDE = 200; private static final int MAIN_TRUNCATE_LIMIT_NARROW = 400; private static final String URL_COLOR = "#3366CC"; +private static final String ATTACHMENT_LABEL_PREFIX = " buildAttachmentSegments(String type, String fileName, String url, boolean spoiler) { List segments = new ArrayList<>(); -TextSegment prefix = new TextSegment("", false, URL_COLOR); +TextSegment suffix = new TextSegment(LABEL_SUFFIX, false, URL_COLOR); applyLinkStyle(prefix, url); applyLinkStyle(fileNameSegment, url); @@ -1508,9 +1511,9 @@ return segments; private static List buildEmbedSegments(String title, String url, boolean spoiler) { List segments = new ArrayList<>(); String color = url == null ? "yellow" : URL_COLOR; -TextSegment prefix = new TextSegment("", false, color); +TextSegment suffix = new TextSegment(LABEL_SUFFIX, false, color); if (url != null) { applyLinkStyle(prefix, url); @@ -1532,8 +1535,20 @@ return segments; private static void applyLinkStyle(TextSegment segment, String url) { segment.underlined = true; segment.clickUrl = url; +if (segment.hoverText == null) { segment.hoverText = I18nManager.getDmccTranslation("discord.message_parser.click_to_open_link"); } +} + +private static void applySpoilerStyle(TextSegment segment) { +segment.obfuscated = true; +// Obfuscated Minecraft text is unreadable in chat, so we keep original plain text as hover preview. +segment.hoverText = segment.text; +} + +private static String colorOrDefault(String color) { +return color != null ? color : "white"; +} private static String truncateMainRaw(String raw) { String lineLimited = applyMainLineLimit(raw);