diff --git a/plugins/chat/app/services/chat/publisher.rb b/plugins/chat/app/services/chat/publisher.rb index 08b78083fd5..5f3c8b58b72 100644 --- a/plugins/chat/app/services/chat/publisher.rb +++ b/plugins/chat/app/services/chat/publisher.rb @@ -475,6 +475,12 @@ module Chat ) end + def self.publish_notice(user_id:, channel_id:, text_content:) + payload = { type: "notice", text_content: text_content, channel_id: channel_id } + + MessageBus.publish("/chat/#{channel_id}", payload, user_ids: [user_id]) + end + private def self.permissions(chat_channel) diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel.hbs index 63b05abccbf..a7150008dc2 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel.hbs @@ -21,7 +21,7 @@ @displayed={{this.includeHeader}} /> - + diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-notices.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-notices.hbs new file mode 100644 index 00000000000..0587c135ccc --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/components/chat-notices.hbs @@ -0,0 +1,17 @@ +
+ + + {{#each this.noticesForChannel as |notice|}} +
+

+ {{notice.textContent}} +

+ + +
+ {{/each}} +
\ No newline at end of file diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-notices.js b/plugins/chat/assets/javascripts/discourse/components/chat-notices.js new file mode 100644 index 00000000000..80b535a3cca --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/components/chat-notices.js @@ -0,0 +1,18 @@ +import Component from "@glimmer/component"; +import { inject as service } from "@ember/service"; +import { action } from "@ember/object"; + +export default class ChatNotices extends Component { + @service("chat-channel-pane-subscriptions-manager") subscriptionsManager; + + get noticesForChannel() { + return this.subscriptionsManager.notices.filter( + (notice) => notice.channelId === this.args.channel.id + ); + } + + @action + clearNotice(notice) { + this.subscriptionsManager.clearNotice(notice); + } +} diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-notice.js b/plugins/chat/assets/javascripts/discourse/models/chat-notice.js new file mode 100644 index 00000000000..0960846d16e --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/models/chat-notice.js @@ -0,0 +1,15 @@ +import { tracked } from "@glimmer/tracking"; + +export default class ChatNotice { + static create(args = {}) { + return new ChatNotice(args); + } + + @tracked channelId; + @tracked textContent; + + constructor(args = {}) { + this.channelId = args.channel_id; + this.textContent = args.text_content; + } +} diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-channel-pane-subscriptions-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-channel-pane-subscriptions-manager.js index 6604c7e1c0d..b381b2c1a47 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-channel-pane-subscriptions-manager.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-channel-pane-subscriptions-manager.js @@ -1,11 +1,16 @@ import { inject as service } from "@ember/service"; +import { tracked } from "@glimmer/tracking"; +import { TrackedArray } from "@ember-compat/tracked-built-ins"; import ChatPaneBaseSubscriptionsManager from "./chat-pane-base-subscriptions-manager"; import ChatThreadPreview from "../models/chat-thread-preview"; +import ChatNotice from "../models/chat-notice"; export default class ChatChannelPaneSubscriptionsManager extends ChatPaneBaseSubscriptionsManager { @service chat; @service currentUser; + @tracked notices = new TrackedArray(); + get messageBusChannel() { return `/chat/${this.model.id}`; } @@ -18,6 +23,17 @@ export default class ChatChannelPaneSubscriptionsManager extends ChatPaneBaseSub return; } + handleNotice(data) { + this.notices.push(ChatNotice.create(data)); + } + + clearNotice(notice) { + const index = this.notices.indexOf(notice); + if (index > -1) { + this.notices.splice(index, 1); + } + } + handleThreadOriginalMessageUpdate(data) { const message = this.messagesManager.findMessage(data.original_message_id); if (message) { diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-pane-base-subscriptions-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-pane-base-subscriptions-manager.js index f9f453d25cc..67ebfb55f42 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-pane-base-subscriptions-manager.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-pane-base-subscriptions-manager.js @@ -116,6 +116,9 @@ export default class ChatPaneBaseSubscriptionsManager extends Service { case "update_thread_original_message": this.handleThreadOriginalMessageUpdate(busData); break; + case "notice": + this.handleNotice(busData); + break; } } @@ -248,6 +251,10 @@ export default class ChatPaneBaseSubscriptionsManager extends Service { throw "not implemented"; } + handleNotice() { + throw "not implemented"; + } + _afterDeleteMessage() { throw "not implemented"; } diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-thread-pane-subscriptions-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-thread-pane-subscriptions-manager.js index 131bebea108..08f146a1519 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-thread-pane-subscriptions-manager.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-thread-pane-subscriptions-manager.js @@ -32,6 +32,11 @@ export default class ChatThreadPaneSubscriptionsManager extends ChatPaneBaseSubs return; } + // NOTE: We don't yet handle notices inside of threads so do nothing. + handleNotice() { + return; + } + _afterDeleteMessage(targetMsg, data) { if (this.model.currentUserMembership?.lastReadMessageId === targetMsg.id) { this.model.currentUserMembership.lastReadMessageId = diff --git a/plugins/chat/assets/stylesheets/common/chat-notices.scss b/plugins/chat/assets/stylesheets/common/chat-notices.scss new file mode 100644 index 00000000000..77f266b81cf --- /dev/null +++ b/plugins/chat/assets/stylesheets/common/chat-notices.scss @@ -0,0 +1,42 @@ +.chat-notices { + display: flex; + flex-direction: column; + gap: 0.5em; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + z-index: 10; + min-width: 280px; + + &__notice, + .chat-retention-reminder { + display: flex; + align-items: center; + justify-content: space-between; + background: var(--tertiary-low); + padding: 0.5em 0 0.5em 1em; + color: var(--primary); + padding: 0.5em 0 0.5em 1em; + } + + .btn-flat { + margin: 0 0.25em; + color: var(--primary-medium); + + &:hover, + &:focus { + background-color: transparent; + .d-icon { + color: var(--primary); + } + } + .d-icon { + color: var(--primary-medium); + } + } +} + +.full-page-chat .chat-notices { + top: 4rem; +} diff --git a/plugins/chat/assets/stylesheets/common/chat-retention-reminder.scss b/plugins/chat/assets/stylesheets/common/chat-retention-reminder.scss deleted file mode 100644 index d1aeaaf3a91..00000000000 --- a/plugins/chat/assets/stylesheets/common/chat-retention-reminder.scss +++ /dev/null @@ -1,35 +0,0 @@ -.chat-retention-reminder { - display: flex; - position: absolute; - top: 0; - left: 50%; - transform: translateX(-50%); - align-items: center; - justify-content: space-between; - background: var(--tertiary-low); - padding: 0.5em 0 0.5em 1em; - font-size: var(--font-down-1); - color: var(--primary); - z-index: 10; - min-width: 280px; - - .btn-flat.dismiss-btn { - margin-left: 0.25em; - color: var(--primary-medium); - - &:hover, - &:focus { - background-color: transparent; - .d-icon { - color: var(--primary); - } - } - .d-icon { - color: var(--primary-medium); - } - } -} - -.full-page-chat .chat-retention-reminder { - top: 4rem; -} diff --git a/plugins/chat/assets/stylesheets/common/index.scss b/plugins/chat/assets/stylesheets/common/index.scss index 2d721919ed9..462b89eb073 100644 --- a/plugins/chat/assets/stylesheets/common/index.scss +++ b/plugins/chat/assets/stylesheets/common/index.scss @@ -28,10 +28,10 @@ @import "chat-message-separator"; @import "chat-message-thread-indicator"; @import "chat-message"; +@import "chat-notices"; @import "chat-onebox"; @import "chat-reply"; @import "chat-replying-indicator"; -@import "chat-retention-reminder"; @import "chat-selection-manager"; @import "chat-side-panel"; @import "chat-skeleton"; diff --git a/plugins/chat/test/javascripts/components/chat-notices-test.js b/plugins/chat/test/javascripts/components/chat-notices-test.js new file mode 100644 index 00000000000..8181eb03c5a --- /dev/null +++ b/plugins/chat/test/javascripts/components/chat-notices-test.js @@ -0,0 +1,65 @@ +import { setupRenderingTest } from "discourse/tests/helpers/component-test"; +import hbs from "htmlbars-inline-precompile"; +import fabricators from "discourse/plugins/chat/discourse/lib/fabricators"; +import { query, queryAll } from "discourse/tests/helpers/qunit-helpers"; +import { module, test } from "qunit"; +import { click, render } from "@ember/test-helpers"; + +module("Discourse Chat | Component | chat-notice", function (hooks) { + setupRenderingTest(hooks); + + test("displays all notices for a channel", async function (assert) { + this.channel = fabricators.channel(); + this.manager = this.container.lookup( + "service:chatChannelPaneSubscriptionsManager" + ); + this.manager.handleNotice({ + channel_id: this.channel.id, + text_content: "hello", + }); + this.manager.handleNotice({ + channel_id: this.channel.id, + text_content: "goodbye", + }); + this.manager.handleNotice({ + channel_id: this.channel.id + 1, + text_content: "N/A", + }); + + await render(hbs``); + + const notices = queryAll(".chat-notices .chat-notices__notice"); + + assert.strictEqual(notices.length, 2, "Two notices are rendered"); + + assert.true(notices[0].innerText.includes("hello")); + assert.true(notices[1].innerText.includes("goodbye")); + }); + + test("Notices can be cleared", async function (assert) { + this.channel = fabricators.channel(); + this.manager = this.container.lookup( + "service:chatChannelPaneSubscriptionsManager" + ); + this.manager.handleNotice({ + channel_id: this.channel.id, + text_content: "hello", + }); + + await render(hbs``); + + assert.strictEqual( + queryAll(".chat-notices .chat-notices__notice").length, + 1, + "Notice is present" + ); + + await click(query(".chat-notices__notice__clear"), "Clear the notice"); + + assert.strictEqual( + queryAll(".chat-notices .chat-notices__notice").length, + 0, + "Notice was cleared" + ); + }); +});