-
Notifications
You must be signed in to change notification settings - Fork 31
Message Reply Feature #531
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
f0ed13c
91b73e3
19f2e75
13e4c25
df14889
218b554
12f6b97
1eaaf42
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -188,6 +188,7 @@ const commandsinfo = new Map([ | |
| ], | ||
| ["unhideemote", { desc: "Unhide a hidden emote." }], | ||
| ["spoiler", { desc: "Wraps a message in the spoiler tags `||`." }], | ||
| ["reply", { desc: "Reply to a user's most recent message." }], | ||
| ]); | ||
| const banstruct = { | ||
| id: 0, | ||
|
|
@@ -263,6 +264,7 @@ class Chat { | |
| this.source.on("NAMES", data => this.onNAMES(data)); | ||
| this.source.on("QUIT", data => this.onQUIT(data)); | ||
| this.source.on("MSG", data => this.onMSG(data)); | ||
| this.source.on("MSGREPLY", data => this.onREPLY(data)); | ||
| this.source.on("MUTE", data => this.onMUTE(data)); | ||
| this.source.on("UNMUTE", data => this.onUNMUTE(data)); | ||
| this.source.on("BAN", data => this.onBAN(data)); | ||
|
|
@@ -317,6 +319,7 @@ class Chat { | |
| this.cmdHIDEEMOTE(data, "UNHIDEEMOTE") | ||
| ); | ||
| this.control.on("SPOILER", data => this.cmdSPOILER(data)) | ||
| this.control.on("REPLY", data => this.cmdREPLY(data)) | ||
|
|
||
| notificationSound.loadConfig(); | ||
| } | ||
|
|
@@ -441,6 +444,25 @@ class Chat { | |
| e.stopPropagation(); | ||
| if (!this.authenticated) { | ||
| this.loginscrn.show(); | ||
| return; | ||
| } | ||
| if (this.input.hasClass("invalid-msg-warning")) return; | ||
|
|
||
| const text = this.input.val().toString().trim(); | ||
| if (!text) return; | ||
|
|
||
| const prevMessageId = $("#chat-reply-banner").data("replyTo"); | ||
| const prevText = $("#chat-reply-banner").data("prevText"); | ||
| const targetUser = $("#chat-reply-banner").data("targetUser"); | ||
|
|
||
| if (prevMessageId && prevText && targetUser) { | ||
| // Build the MSGREPLY payload | ||
| this.source.emit("MSGREPLY", { data: text, nick: this.user.nick, target: targetUser.nick, prev: prevText, prevMessageId: prevMessageId }); | ||
|
|
||
| // Clear banner state | ||
| $("#chat-reply-banner").hide().removeData("replyTo").removeData("prevText").removeData("targetUser"); | ||
| $("#chat-reply-user").text(""); | ||
| this.input.val("").trigger("input"); | ||
| } else { | ||
| // don't do anything if the message is marked invalid client-side (currently only when the message is too long) | ||
| if (!this.input.hasClass("invalid-msg-warning")) { | ||
|
|
@@ -541,6 +563,33 @@ class Chat { | |
| } | ||
| }); | ||
|
|
||
| // Right click a reply to scroll to the orginal message | ||
| this.ui.on("contextmenu", ".msg-reply-preview", function (e) { | ||
| e.preventDefault(); | ||
|
|
||
| const preview = $(this); // the reply preview div | ||
| const targetId = preview.data("reply-to"); // uses data-reply-to attr | ||
|
|
||
| if (targetId) { | ||
| const original = $(`[data-msg-id="${targetId}"]`); | ||
|
|
||
| if (original.length) { | ||
| original[0].scrollIntoView({ behavior: "smooth", block: "center" }); | ||
|
|
||
| // temporary highlight | ||
| original.addClass("msg-highlight-reply"); | ||
| setTimeout(() => original.removeClass("msg-highlight-reply"), 1500); | ||
| } else { | ||
| console.log("Original message not found in DOM:", targetId); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| // Cancel a repl to someone | ||
| this.ui.on("click", ".chat-reply-cancel", () => { | ||
| $("#chat-reply-banner").hide().removeData("replyTo").removeData("prevText").removeData("targetUser"); | ||
| }); | ||
|
|
||
| // Visibility | ||
| document.addEventListener( | ||
| "visibilitychange", | ||
|
|
@@ -951,21 +1000,35 @@ class Chat { | |
| win.lastmessage.user && | ||
| win.lastmessage.user.username.toLowerCase() === | ||
| message.user.username.toLowerCase(); | ||
| // get mentions from message | ||
| message.mentioned = Chat.extractNicks(message.message).reduce((m, a) => { | ||
| // Gets mentions from message and a reply mentions | ||
| message.mentioned = [ | ||
| ...Chat.extractNicks(message.message), | ||
| ...(message.prevMessage ? Chat.extractNicks(message.prevMessage) : []) | ||
| ].reduce((m, a) => { | ||
| const user = this.users.get(a.toLowerCase()); | ||
| return user ? [...m, user.nick] : m; | ||
| if (user && !m.includes(user.nick)) { | ||
| m.push(user.nick); | ||
| } | ||
| return m; | ||
| }, []); | ||
| // add replytarget nick if present | ||
| if (message.replytarget?.nick && !message.mentioned.includes(message.replytarget.nick)) { | ||
| message.mentioned.push(message.replytarget.nick); | ||
| } | ||
|
|
||
| // set tagged state | ||
| message.tag = this.taggednicks.get(message.user.nick.toLowerCase()); | ||
| // set highlighted state if this is not the current users message or a bot, as well as other highlight criteria | ||
| message.highlighted = | ||
| !message.isown && | ||
| !message.user.hasFeature(UserFeatures.BOT) && | ||
| // Check current user nick against msg.message (if highlight setting is on) | ||
| ((this.regexhighlightself && | ||
| this.settings.get("highlight") && | ||
| this.regexhighlightself.test(message.message)) || | ||
| ( | ||
| // Highlight if the replytarget is the current user | ||
| (message.replytarget?.username === this.user.username) || | ||
| // Check current user nick against msg.message (if highlight setting is on) | ||
| (this.regexhighlightself && | ||
| this.settings.get("highlight") && | ||
| this.regexhighlightself.test(message.message)) || | ||
| // Check /highlight nicks against msg.nick | ||
| (this.regexhighlightnicks && | ||
| this.regexhighlightnicks.test(message.user.username)) || | ||
|
|
@@ -1248,6 +1311,21 @@ class Chat { | |
| } | ||
| } | ||
|
|
||
| onREPLY(data) { | ||
| const user = this.users.get(data.nick.toLowerCase()); | ||
| const target = this.users.get(data.target.toLowerCase()); | ||
|
|
||
| MessageBuilder.reply( | ||
| data.data, // current message | ||
| user, // sender user object | ||
| target, // target user object | ||
| data.prev, // previoustext | ||
| data.prevMessageId, // previoustext | ||
| data.messageId, // optional message ID | ||
| data.timestamp // optional timestamp | ||
| ).into(this); | ||
| } | ||
|
|
||
| onMUTE(data) { | ||
| // data.data is the nick which has been banned, no info about duration | ||
| if (this.user.username.toLowerCase() === data.data.toLowerCase()) { | ||
|
|
@@ -1986,6 +2064,58 @@ class Chat { | |
| this.control.emit("SEND", `|| ${data.join(' ')} ||`) | ||
| } | ||
|
|
||
| findLastMessageByUser({ nick, id }, win = this.getActiveWindow()) { | ||
| const children = win.getlines(); | ||
|
|
||
| for (let i = children.length - 1; i >= 0; i--) { | ||
| const line = $(children[i]); | ||
| const msg = line.data("message"); | ||
|
|
||
| if (!msg) continue; | ||
|
|
||
| // Match by ID first | ||
| if (id && msg.id === id) { | ||
| return msg; | ||
| } | ||
|
|
||
| // Match by nickname | ||
| if (nick && msg.user && msg.user.nick.toLowerCase() === nick.toLowerCase()) { | ||
| return msg; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
|
|
||
| cmdREPLY(parts) { | ||
| if (!parts[0] || !nickregex.test(parts[0])) { | ||
| MessageBuilder.error("Usage: /reply <nick> <message>").into(this); | ||
| return; | ||
| } | ||
|
|
||
| const targetNick = parts[0]; | ||
| const replyText = parts.slice(1).join(" "); | ||
|
|
||
| if (!replyText) { | ||
| MessageBuilder.error("Reply message cannot be empty").into(this); | ||
| return; | ||
| } | ||
|
|
||
| // Find the last message from that user in the active window | ||
| const prevMessage = this.findLastMessageByUser({ nick: targetNick }); | ||
| if (!prevMessage) { | ||
| MessageBuilder.error(`No recent message found from ${targetNick}`).into(this); | ||
| return; | ||
| } | ||
|
|
||
| this.source.emit("MSGREPLY", { data: replyText, nick: this.user.nick, target: targetNick, prev: prevMessage.message, prevMessageId: prevMessage.msgid }); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does the server handle this command?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wasn't able to fully get the chat WebSocket server to connect to the gui for a full on production test but from testing with the mock server I was able to emit the reply like a message would and get the client to store it in its dom and render / show it in chat |
||
|
|
||
| // Add to input history | ||
| this.inputhistory.add(`/reply ${targetNick} ${replyText}`); | ||
| } | ||
|
|
||
|
|
||
| openConversation(nick) { | ||
| const normalized = nick.toLowerCase(); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: reply