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"
+ );
+ });
+});