From b6e65328762cad82b9e933b81524ccc29112c2cd Mon Sep 17 00:00:00 2001 From: Ali-Alammori Date: Sun, 21 Jun 2026 14:36:29 +0000 Subject: [PATCH 1/3] Feat: Open topic chat window after topic creation and selection --- clefincode_chat/__init__.py | 2 +- .../js/components/erpnext_chat_space.js | 30 ++++++++++++++++--- setup.py | 2 +- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/clefincode_chat/__init__.py b/clefincode_chat/__init__.py index 19626ee..83e1ae1 100644 --- a/clefincode_chat/__init__.py +++ b/clefincode_chat/__init__.py @@ -1 +1 @@ -__version__ = '1.3.908' \ No newline at end of file +__version__ = '1.3.909' \ No newline at end of file diff --git a/clefincode_chat/public/js/components/erpnext_chat_space.js b/clefincode_chat/public/js/components/erpnext_chat_space.js index 1bad36c..b9f6b9c 100644 --- a/clefincode_chat/public/js/components/erpnext_chat_space.js +++ b/clefincode_chat/public/js/components/erpnext_chat_space.js @@ -1011,6 +1011,10 @@ async openCreateTopicFromPlusDialog() { }); await this.loadChannelTopicsForSelect?.(); + await this.openTopicChatWindow(topicName, topicSubject, { + topic_color: color + }); + } } }); @@ -4395,7 +4399,7 @@ if (!me.chat_topic_space) { me.renderTopicSelectList(query); }); - me.$chat_space.on("click", ".topic-select-main", async function (e) { +me.$chat_space.on("click", ".topic-select-main", async function (e) { e.preventDefault(); e.stopPropagation(); @@ -4418,8 +4422,11 @@ if (!me.chat_topic_space) { me.closeTopicSelectPopup?.(); me.closeAllTopicsView?.(); -}); + await me.openTopicChatWindow(topicName, topicSubject, { + topic_color: topicColor + }); +}); me.$chat_space.on("click", ".topic-select-open", async function (e) { e.preventDefault(); e.stopPropagation(); @@ -7765,16 +7772,26 @@ if (!is_deleted && type !== "info-message") { await this.send_add_document_message(mention_doctypes, chat_room); } else { - let results = await create_chat_topic( + let results = await create_chat_topic( mention_doctypes, chat_room, this.last_active_sub_channel ); + const createdTopicName = results?.[0]?.chat_topic || results?.[0]?.name; + const createdTopicSubject = mention_doctypes?.[0]?.docname || createdTopicName; + const createdTopicColor = results?.[0]?.topic_color || null; + await this.send_set_topic_message( - mention_doctypes[0].docname, + createdTopicSubject, chat_room ); + + if (createdTopicName) { + await this.openTopicChatWindow(createdTopicName, createdTopicSubject, { + topic_color: createdTopicColor + }); + } } return; } @@ -7959,6 +7976,11 @@ if (!is_deleted && type !== "info-message") { mention_doctypes[0].docname, chat_room ); + if (createdTopicName) { + await this.openTopicChatWindow(createdTopicName, this.activeMessageTopicSubject, { + topic_color: createdTopicColor + }); + } return; } diff --git a/setup.py b/setup.py index 00298c1..8b7d48f 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="clefincode_chat", - version='1.3.908', + version='1.3.909', description="ERPNext & Frappe Business Chat: A self-hosted communication solution.", author="ClefinCode L.L.C-FZ", author_email="info@clefincode.com", From 9b6699ae1c49b86a4f6fa6e8e5bc18972a79f495 Mon Sep 17 00:00:00 2001 From: Ali-Alammori Date: Mon, 22 Jun 2026 14:06:25 +0000 Subject: [PATCH 2/3] Fix: route replies to topics without auto-selecting them Replying to a topic-linked message now opens the topic window, prepares the reply there after the window is ready, and resumes the reply after reopening closed topics. Selecting or creating topics from the plus menu now only opens the topic window and no longer marks future main-chat messages with that topic. --- clefincode_chat/__init__.py | 2 +- .../public/js/components/erpnext_chat_info.js | 110 +++++++++ .../js/components/erpnext_chat_space.js | 227 ++++++++++++++---- setup.py | 2 +- 4 files changed, 295 insertions(+), 46 deletions(-) diff --git a/clefincode_chat/__init__.py b/clefincode_chat/__init__.py index 83e1ae1..6bb6ad1 100644 --- a/clefincode_chat/__init__.py +++ b/clefincode_chat/__init__.py @@ -1 +1 @@ -__version__ = '1.3.909' \ No newline at end of file +__version__ = '1.3.910' \ No newline at end of file diff --git a/clefincode_chat/public/js/components/erpnext_chat_info.js b/clefincode_chat/public/js/components/erpnext_chat_info.js index fa380e5..bfa2eec 100644 --- a/clefincode_chat/public/js/components/erpnext_chat_info.js +++ b/clefincode_chat/public/js/components/erpnext_chat_info.js @@ -737,6 +737,16 @@ save_all_contacts(dialog) { ${frappe.utils.escape_html(topicStatus)} + +
+ +
`; @@ -839,6 +849,106 @@ save_all_contacts(dialog) { }); + // Close topic + this.$chat_info.find(".close-topic").on("click", async () => { + const topicName = + this.chat_space.chat_topic_space || + this.chat_space.chat_topic || + this.chat_space.profile?.chat_topic; + + if (!topicName) { + frappe.msgprint({ + title: __("Error"), + message: __("No topic found."), + indicator: "red" + }); + return; + } + + try { + await frappe.call({ + method: "clefincode_chat.api.api_1_3_4.api.update_chat_topic_info", + args: { + chat_topic: topicName, + topic_status: "Closed" + } + }); + + this.applyUpdatedTopicInfo( + topicName, + this.chat_space.chat_topic_subject || + this.chat_space.chat_topic_space_subject || + this.chat_space.alternative_subject || + topicName, + this.chat_space.activeMessageTopicColor || + this.chat_space.topicColorMap?.get(topicName) || + this.chat_space.getTopicColor?.(topicName), + this.chat_space.is_private ?? + this.chat_space.profile?.is_private ?? + 0, + "Closed" + ); + + this.chat_space.topic_status = "Closed"; + this.chat_space.chat_topic_status = "Closed"; + + if (this.chat_space.profile) { + this.chat_space.profile.topic_status = "Closed"; + } + + if (this.chat_space.topicDetailsCache?.has(topicName)) { + const cached = this.chat_space.topicDetailsCache.get(topicName); + if (cached) { + cached.topic_status = "Closed"; + cached.status = "Closed"; + } + } + + if (this.chat_space.messageCache) { + this.chat_space.messageCache.forEach((cached, key) => { + if ( + cached.chat_topic === topicName || + cached.topic === topicName || + cached.chat_topic_space === topicName + ) { + cached.topic_status = "Closed"; + cached.chat_topic_status = "Closed"; + cached.status = "Closed"; + } + }); + } + + const $topicElements = this.chat_space.$chat_space_container + ? this.chat_space.$chat_space_container.find( + `[data-topic-name="${topicName}"]` + ) + : this.chat_space.$chat_space + ? this.chat_space.$chat_space.find( + `[data-topic-name="${topicName}"]` + ) + : $(); + + $topicElements.attr("data-topic-status", "Closed"); + + this.chat_space.refreshTopicNavBar?.(); + this.chat_space.buildTopicMetaMap?.(); + this.chat_space.applyTopicVisibility?.(); + + this.$chat_info.find(".close-topic").prop("disabled", true).text(__("Closed")); + $(".close-chat-window").click(); + frappe.show_alert({ + message: __("Topic closed"), + indicator: "green" + }); + } catch (e) { + console.error("Failed to close topic", e); + frappe.msgprint({ + title: __("Error"), + message: __("Failed to close topic."), + indicator: "red" + }); + } + }); this.$chat_info.find(".back-to-chat-info").on("click", function () { me.chat_space.$wrapper.find(".chat-info").show(); diff --git a/clefincode_chat/public/js/components/erpnext_chat_space.js b/clefincode_chat/public/js/components/erpnext_chat_space.js index b9f6b9c..5d15e82 100644 --- a/clefincode_chat/public/js/components/erpnext_chat_space.js +++ b/clefincode_chat/public/js/components/erpnext_chat_space.js @@ -90,6 +90,7 @@ export default class ChatSpace { this.topic_read_only = Boolean(opts.topic_read_only); this.chat_topic_status = opts.chat_topic_status || null; this.reply_to_message_name = null; + this.pendingReplyAfterReopen = null; this.topicInactiveNoticeDismissed = false; this.$topicInactiveNotice = null; this.pendingReplies = []; @@ -182,7 +183,7 @@ export default class ChatSpace { this.initial_message_scroll_done = false; this.messageCache = new Map(); - this.setup(); + this.ready = this.setup(); } async fetchTopicColorFromDB(topicName) { if (!topicName) return null; @@ -554,7 +555,21 @@ async openTopicChatWindow(topicName, topicSubject = null, opts = {}) { if (check_if_chat_window_open(topicKey, "topic")) { $(`.expand-chat-window[data-id|='${topicKey}']`).click(); - return; + + let existingInstance = null; + $(".chat-window").each(function () { + const inst = $(this).find(".chat-space").data("chat-space-instance"); + if (inst && String(inst.chat_topic_space || inst.chat_topic || "") === topicKey) { + existingInstance = inst; + return false; + } + }); + + if (existingInstance?.ready) { + await existingInstance.ready; + } + + return existingInstance; } const chat_window = new ChatWindow({ @@ -563,7 +578,7 @@ async openTopicChatWindow(topicName, topicSubject = null, opts = {}) { } }); - new ChatSpace({ + const topicChatSpace = new ChatSpace({ $wrapper: chat_window.$chat_window, profile: { ...this.profile, @@ -596,6 +611,12 @@ async openTopicChatWindow(topicName, topicSubject = null, opts = {}) { chat_topic_status: ctx.topic_status, original_room_type: ctx.room_type || this.profile.room_type }); + + if (topicChatSpace.ready) { + await topicChatSpace.ready; + } + + return topicChatSpace; } toggleTopicMessages(topicName) { if (!topicName) return; @@ -999,21 +1020,32 @@ async openCreateTopicFromPlusDialog() { latestDate: "" }); - this.selectMessageTopic(topicName, topicSubject, { scroll: false, color, topic_color: color }); - await this.saveUserActiveChatTopic(topicName); - setTimeout(() => { - this.updatePlusTopicButton(); - }, 50); + // this.selectMessageTopic(topicName, topicSubject, { scroll: false, color, topic_color: color }); + // await this.saveUserActiveChatTopic(topicName); + // setTimeout(() => { + // this.updatePlusTopicButton(); + // }, 50); - frappe.show_alert({ - message: __("Topic selected for new messages"), - indicator: "green" - }); + // frappe.show_alert({ + // message: __("Topic selected for new messages"), + // indicator: "green" + // }); + - await this.loadChannelTopicsForSelect?.(); + await this.loadChannelTopicsForSelect?.(); await this.openTopicChatWindow(topicName, topicSubject, { topic_color: color }); + this.activeMessageTopic = null; + this.activeMessageTopicSubject = null; + this.activeMessageTopicColor = null; + this.chat_topic = null; + + this.clearMessageTopic(false); + this.updateActiveTopicButton(null); + this.updatePlusTopicButton(); + + await this.clearUserActiveChatTopic(topicName); } } @@ -2091,6 +2123,54 @@ if (original.is_deleted) { text }; } +async prepareReplyToMessage(messageName) { + if (!messageName) return; + + if (this.ready) { + await this.ready; + } + + this.reply_to_message_name = messageName; + + const snippet = await this.makeReplySnippet(messageName, 120); + const text = snippet?.text || "[Attachment]"; + + let $host = this.$chat_space.children(".reply-preview-host"); + + if (!$host.length) { + $host = $('
'); + + if (this.$chat_actions && this.$chat_actions.length) { + this.$chat_actions.before($host); + } else { + this.$chat_space.append($host); + } + } + + $host.html(` +
+ + + +
+ `); + + $host.find(".reply-preview__text").text(text); + + setTimeout(() => { + if (this.type_message_input?.quill) { + this.type_message_input.quill.focus(); + this.type_message_input.quill.setSelection( + this.type_message_input.quill.getLength(), + 0 + ); + return; + } + + const $editor = this.$chat_actions?.find(".type-message .ql-editor"); + if ($editor?.length) $editor.trigger("focus"); + }, 0); +} async jumpToMessage(messageName, maxTries = 50) { const limit = this.messages_limit || 10; @@ -3560,6 +3640,15 @@ this.$chat_space.off("click.reopenTopic", ".reopen-topic-btn") await me.setup_actions(); me.setup_events(); + if (me.pendingReplyAfterReopen) { + const pendingMessageName = me.pendingReplyAfterReopen; + me.pendingReplyAfterReopen = null; + + setTimeout(async () => { + await me.prepareReplyToMessage(pendingMessageName); + }, 150); + } + frappe.show_alert({ message: __("Topic reopened"), indicator: "green" @@ -3767,40 +3856,89 @@ this.$wrapper.off("click.chatMenuActions", ".reply-action") me.closeMessageActionMenu(); const messageName = $(this).data("message-name"); - me.reply_to_message_name = messageName; + if (!messageName) return; + + const isAlreadyInTopic = + me.is_topic_window || + me.profile?.room_type === "Topic" || + me.chat_topic_space; - const snippet = await me.makeReplySnippet(messageName, 120); - const text = snippet?.text || "[Attachment]"; + let cached = me.messageCache.get(messageName) || {}; - let $host = me.$chat_space.children(".reply-preview-host"); - if (!$host.length) { - $host = $('
'); - me.$chat_actions.before($host); + if (!cached.chat_topic && !cached.topic && !cached.topic_name) { + try { + const msg = await me.fetch_single_message(messageName); + + if (msg) { + cached = { + ...cached, + ...msg, + chat_topic: msg.chat_topic || msg.topic || msg.topic_name || cached.chat_topic || null, + chat_topic_subject: + msg.chat_topic_subject || + msg.topic_subject || + msg.chat_topic_title || + msg.subject || + cached.chat_topic_subject || + null, + topic_color: + msg.topic_color || + msg.chat_topic_color || + cached.topic_color || + null + }; + + me.messageCache.set(messageName, cached); + } + } catch (err) { + console.warn("Failed to fetch message before reply", err); + } } - $host.html(` -
- - - -
- `); + const linkedTopic = await me.getLinkedTopicInfo(cached); - $host.find(".reply-preview__text").text(text); + if (linkedTopic?.name && !isAlreadyInTopic) { + const topicSpace = await me.openTopicChatWindow( + linkedTopic.name, + linkedTopic.subject || linkedTopic.name, + { + topic_color: + linkedTopic.topic_color || + linkedTopic.color || + cached.topic_color || + null + } + ); - setTimeout(() => { - if (me.type_message_input?.quill) { - me.type_message_input.quill.focus(); - me.type_message_input.quill.setSelection( - me.type_message_input.quill.getLength(), - 0 - ); + if (topicSpace) { + const isClosed = + String( + topicSpace.chat_topic_status || + topicSpace.profile?.chat_topic_status || + "" + ).toLowerCase() === "closed"; + + const isReadOnly = + topicSpace.topic_read_only === true || + topicSpace.topic_write_mode === false; + + if (isClosed || isReadOnly) { + topicSpace.pendingReplyAfterReopen = messageName; + + frappe.show_alert({ + message: __("Reopen the topic to reply"), + indicator: "orange" + }); + + return; + } + + await topicSpace.prepareReplyToMessage(messageName); return; } + } - const $editor = me.$chat_actions?.find(".type-message .ql-editor"); - if ($editor?.length) $editor.trigger("focus"); - }, 0); + await me.prepareReplyToMessage(messageName); }); this.$wrapper.off("click.chatMenuActions", ".react-action") @@ -4370,6 +4508,7 @@ if (!me.chat_topic_space) { e.stopPropagation(); me.closePlusMenu(); await me.openCreateTopicFromPlusDialog(); + }); me.$chat_space.on("click", ".chat-read-more-btn", function (e) { @@ -4412,13 +4551,13 @@ me.$chat_space.on("click", ".topic-select-main", async function (e) { topicColor = await me.getTopicColorFromSource(topicName, null, { forceRefresh: true }); - me.selectMessageTopic(topicName, topicSubject, { - scroll: false, - color: topicColor, - topic_color: topicColor - }); + // me.selectMessageTopic(topicName, topicSubject, { + // scroll: false, + // color: topicColor, + // topic_color: topicColor + // }); - await me.saveUserActiveChatTopic(topicName); + // await me.saveUserActiveChatTopic(topicName); me.closeTopicSelectPopup?.(); me.closeAllTopicsView?.(); diff --git a/setup.py b/setup.py index 8b7d48f..f4738bb 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="clefincode_chat", - version='1.3.909', + version='1.3.910', description="ERPNext & Frappe Business Chat: A self-hosted communication solution.", author="ClefinCode L.L.C-FZ", author_email="info@clefincode.com", From 6b991e0d21d105f93c9b22a9294e2faa8e66478a Mon Sep 17 00:00:00 2001 From: Ali-Alammori Date: Tue, 23 Jun 2026 08:44:11 +0000 Subject: [PATCH 3/3] Fix: topic inactive notice now follows last rendered message --- .../js/components/erpnext_chat_space.js | 97 ++++++++++++++++--- .../js/components/erpnext_chat_utils.js | 2 + 2 files changed, 86 insertions(+), 13 deletions(-) diff --git a/clefincode_chat/public/js/components/erpnext_chat_space.js b/clefincode_chat/public/js/components/erpnext_chat_space.js index 5d15e82..d9fd1fc 100644 --- a/clefincode_chat/public/js/components/erpnext_chat_space.js +++ b/clefincode_chat/public/js/components/erpnext_chat_space.js @@ -577,6 +577,9 @@ async openTopicChatWindow(topicName, topicSubject = null, opts = {}) { topic: topicKey } }); + chat_window.$chat_window + .attr("data-topic", topicKey) + .data("topic", topicKey); const topicChatSpace = new ChatSpace({ $wrapper: chat_window.$chat_window, @@ -2208,6 +2211,8 @@ async jumpToMessage(messageName, maxTries = 50) { this.applyTopicVisibility(); } + this.checkAndShowTopicInactiveNotice?.(); + $msg = this.$chat_space.find(`#msg-${messageName}`); @@ -2827,8 +2832,8 @@ async fetch_single_message(messageName) { await this.setup_messages(res.results || []); await this.setup_actions(); await this.applySavedActiveTopic(); - this.checkAndShowTopicInactiveNotice(); this.render(); + this.checkAndShowTopicInactiveNotice?.(); } catch (error) { console.log(error); } @@ -5288,32 +5293,96 @@ isDedicatedTopicContext() { } checkAndShowTopicInactiveNotice() { - if (this.isDedicatedTopicContext()) return; - if (this.topicInactiveNoticeDismissed) return; + if ( + this.isDedicatedTopicContext?.() || + this.is_topic_window || + this.profile?.room_type === "Topic" || + this.chat_topic_space + ) { + this.hideTopicInactiveNotice?.(); + return; + } + + if (!this.$chat_space_container || !this.$chat_space_container.length) { + return; + } - const lastMsg = this.prevMessage; - if (!lastMsg) return; + const $lastMsg = this.$chat_space_container + .find("[data-message-name]") + .filter((_, el) => { + const $el = $(el); + const messageName = $el.attr("data-message-name") || $el.data("message-name"); + const cached = this.messageCache?.get(messageName) || {}; - const topicName = lastMsg.chat_topic || lastMsg.topic || null; - if (!topicName) return; + if (Number(cached.is_deleted || 0) === 1) return false; + + const messageType = String(cached.message_type || "").toLowerCase(); + if (messageType === "information") return false; + + const templateType = String(cached.message_template_type || "").toLowerCase(); + if ( + templateType === "set topic" || + templateType === "set-topic" || + templateType === "settopic" || + templateType === "remove topic" || + templateType === "remove-topic" || + templateType === "removetopic" || + templateType === "close topic" || + templateType === "close-topic" || + templateType === "closetopic" + ) { + return false; + } + + return true; + }) + .last(); + + if (!$lastMsg.length) { + this.hideTopicInactiveNotice?.(); + return; + } - if (this.activeMessageTopic && String(this.activeMessageTopic) === String(topicName)) return; + const messageName = $lastMsg.attr("data-message-name") || $lastMsg.data("message-name"); + const cached = this.messageCache?.get(messageName) || {}; + + const topicName = + $lastMsg.attr("data-topic-name") || + cached.chat_topic || + cached.topic || + cached.topic_name || + null; + + if (!topicName) { + this.hideTopicInactiveNotice?.(); + return; + } + + const topicSubject = + $lastMsg.attr("data-topic-subject") || + cached.chat_topic_subject || + cached.topic_subject || + cached.chat_topic_title || + topicName; - const topicSubject = lastMsg.chat_topic_subject || lastMsg.topic_subject || topicName; this.showTopicInactiveNotice(topicSubject); } showTopicInactiveNotice(topicSubject) { + this.hideTopicInactiveNotice?.(); + const $notice = $(` -
- ${__('No topic selected ')} -
-`); +
+ ${__("No topic selected")} +
+ `); + if (this.$chat_actions && this.$chat_actions.length) { this.$chat_actions.before($notice); } else { this.$chat_space.append($notice); } + this.$topicInactiveNotice = $notice; } @@ -8917,6 +8986,7 @@ async fetchTemplateSuggestions(textValue) { this.fetchAndRenderReactions(res.message_name); } this.prevMessage = res; + this.checkAndShowTopicInactiveNotice?.(); } openMessageActionMenu({ $trigger, messageName, isMyMessage, isTextOnly }) { @@ -9805,6 +9875,7 @@ async rebuildMessage(messageName) { me.buildTopicMetaMap(); me.applyTopicVisibility(); } + me.checkAndShowTopicInactiveNotice?.(); me.resolvePendingReplies(); me.hydrateReactionsForMessages(res.results); if (res.results.length != 0) { diff --git a/clefincode_chat/public/js/components/erpnext_chat_utils.js b/clefincode_chat/public/js/components/erpnext_chat_utils.js index 75a8a3a..035d3a8 100644 --- a/clefincode_chat/public/js/components/erpnext_chat_utils.js +++ b/clefincode_chat/public/js/components/erpnext_chat_utils.js @@ -166,12 +166,14 @@ function set_notification_count(type) { function check_if_chat_window_open(element, data) { let open_chat_windows = $(".chat-window"); + console.log(open_chat_windows); let open_window_exist = false; if (open_chat_windows.length > 0) { open_chat_windows.each(function () { let open_chat_window = $(this).data(data); if (element == open_chat_window) { open_window_exist = true; + return false; } }); }