diff --git a/plugins/chat/app/queries/chat/messages_query.rb b/plugins/chat/app/queries/chat/messages_query.rb index 55b34889faf..b96b2be24cd 100644 --- a/plugins/chat/app/queries/chat/messages_query.rb +++ b/plugins/chat/app/queries/chat/messages_query.rb @@ -161,14 +161,14 @@ module Chat def self.query_by_date(target_date, channel, messages) past_messages = messages - .where("created_at <= ?", target_date) + .where("created_at <= ?", target_date.to_time.utc) .order(created_at: :desc) .limit(PAST_MESSAGE_LIMIT) .to_a future_messages = messages - .where("created_at > ?", target_date) + .where("created_at > ?", target_date.to_time.utc) .order(created_at: :asc) .limit(FUTURE_MESSAGE_LIMIT) .to_a diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-channel.gjs index 5f583126460..44e7274c2fe 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel.gjs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel.gjs @@ -34,6 +34,7 @@ import { checkMessageBottomVisibility, checkMessageTopVisibility, } from "discourse/plugins/chat/discourse/lib/check-message-visibility"; +import DatesSeparatorsPositioner from "discourse/plugins/chat/discourse/lib/dates-separators-positioner"; import { scrollListToBottom, scrollListToMessage, @@ -111,7 +112,7 @@ export default class ChatChannel extends Component { @action didResizePane() { this.debounceFillPaneAttempt(); - this.computeDatesSeparators(); + DatesSeparatorsPositioner.apply(this.scrollable); } @action @@ -443,13 +444,14 @@ export default class ChatChannel extends Component { @action onScroll(state) { - bodyScrollFix(); - next(() => { if (this.#flushIgnoreNextScroll()) { return; } + bodyScrollFix(); + DatesSeparatorsPositioner.apply(this.scrollable); + this.needsArrow = (this.messagesLoader.fetchedOnce && this.messagesLoader.canLoadMoreFuture) || @@ -641,50 +643,6 @@ export default class ChatChannel extends Component { return; } - @bind - computeDatesSeparators() { - schedule("afterRender", () => { - const dates = [ - ...this.scrollable.querySelectorAll(".chat-message-separator-date"), - ].reverse(); - const height = this.scrollable.querySelector( - ".chat-messages-container" - ).clientHeight; - - dates - .map((date, index) => { - const item = { bottom: 0, date }; - const line = date.nextElementSibling; - - if (index > 0) { - const prevDate = dates[index - 1]; - const prevLine = prevDate.nextElementSibling; - item.bottom = height - prevLine.offsetTop; - } - - if (dates.length === 1) { - item.height = height; - } else { - if (index === 0) { - item.height = height - line.offsetTop; - } else { - const prevDate = dates[index - 1]; - const prevLine = prevDate.nextElementSibling; - item.height = - height - line.offsetTop - (height - prevLine.offsetTop); - } - } - - return item; - }) - // group all writes at the end - .forEach((item) => { - item.date.style.bottom = item.bottom + "px"; - item.date.style.height = item.height + "px"; - }); - }); - } - #cancelHandlers() { cancel(this._debouncedHighlightOrFetchMessageHandler); cancel(this._debouncedUpdateLastReadMessageHandler); diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-separator-date.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-message-separator-date.gjs deleted file mode 100644 index 1850c39b817..00000000000 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message-separator-date.gjs +++ /dev/null @@ -1,50 +0,0 @@ -import Component from "@glimmer/component"; -import { on } from "@ember/modifier"; -import { action } from "@ember/object"; -import concatClass from "discourse/helpers/concat-class"; -import i18n from "discourse-common/helpers/i18n"; -import trackMessageSeparatorDate from "../modifiers/chat/track-message-separator-date"; - -export default class ChatMessageSeparatorDate extends Component { - @action - onDateClick() { - return this.args.fetchMessagesByDate?.( - this.args.message.firstMessageOfTheDayAt - ); - } - - - {{#if @message.formattedFirstMessageDate}} - - - - {{@message.formattedFirstMessageDate}} - - {{#if @message.newest}} - - - - {{i18n "chat.last_visit"}} - - {{/if}} - - - - - - - - {{/if}} - -} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-separator-new.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-message-separator-new.gjs deleted file mode 100644 index 0ff7142457e..00000000000 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message-separator-new.gjs +++ /dev/null @@ -1,21 +0,0 @@ -import i18n from "discourse-common/helpers/i18n"; -import and from "truth-helpers/helpers/and"; -import not from "truth-helpers/helpers/not"; - -const ChatMessageSeparatorNew = - {{#if (and @message.newest (not @message.formattedFirstMessageDate))}} - - - - {{i18n "chat.last_visit"}} - - - - - - - - {{/if}} -; - -export default ChatMessageSeparatorNew; diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-separator.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-message-separator.gjs new file mode 100644 index 00000000000..04e4aa78b69 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message-separator.gjs @@ -0,0 +1,130 @@ +import Component from "@glimmer/component"; +import { cached } from "@glimmer/tracking"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; +import { modifier } from "ember-modifier"; +import concatClass from "discourse/helpers/concat-class"; +import i18n from "discourse-common/helpers/i18n"; +import I18n from "I18n"; + +const IS_PINNED_CLASS = "is-pinned"; + +export default class ChatMessageSeparator extends Component { + track = modifier((element) => { + const intersectionObserver = new IntersectionObserver( + ([entry]) => { + if ( + entry.isIntersecting && + entry.intersectionRatio < 1 && + entry.boundingClientRect.y < entry.intersectionRect.y + ) { + entry.target.classList.add(IS_PINNED_CLASS); + } else { + entry.target.classList.remove(IS_PINNED_CLASS); + } + }, + { threshold: [0, 1] } + ); + + intersectionObserver.observe(element); + + return () => { + intersectionObserver?.disconnect(); + }; + }); + + @action + onDateClick() { + return this.args.fetchMessagesByDate?.(this.firstMessageOfTheDayAt); + } + + @cached + get firstMessageOfTheDayAt() { + const message = this.args.message; + + if (!message.previousMessage) { + return this.#startOfDay(message.createdAt); + } + + if ( + !this.#areDatesOnSameDay( + message.previousMessage.createdAt, + message.createdAt + ) + ) { + return this.#startOfDay(message.createdAt); + } + } + + @cached + get formattedFirstMessageDate() { + if (this.firstMessageOfTheDayAt) { + return this.#calendarDate(this.firstMessageOfTheDayAt); + } + } + + #areDatesOnSameDay(a, b) { + return ( + a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate() + ); + } + + #startOfDay(date) { + return moment(date).startOf("day").format(); + } + + #calendarDate(date) { + return moment(date).calendar(moment(), { + sameDay: `[${I18n.t("chat.chat_message_separator.today")}]`, + lastDay: `[${I18n.t("chat.chat_message_separator.yesterday")}]`, + lastWeek: "LL", + sameElse: "LL", + }); + } + + + {{#if this.formattedFirstMessageDate}} + + + + {{this.formattedFirstMessageDate}} + + {{#if @message.newest}} + + - + {{i18n "chat.last_visit"}} + + {{/if}} + + + + + + + + {{else if @message.newest}} + + + + {{i18n "chat.last_visit"}} + + + + + + + + {{/if}} + +} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-message.gjs index 8404d275af9..c2f0d4cbb67 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message.gjs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message.gjs @@ -26,8 +26,7 @@ import ChatMessageInfo from "discourse/plugins/chat/discourse/components/chat/me import ChatMessageLeftGutter from "discourse/plugins/chat/discourse/components/chat/message/left-gutter"; import ChatMessageInReplyToIndicator from "discourse/plugins/chat/discourse/components/chat-message-in-reply-to-indicator"; import ChatMessageReaction from "discourse/plugins/chat/discourse/components/chat-message-reaction"; -import ChatMessageSeparatorDate from "discourse/plugins/chat/discourse/components/chat-message-separator-date"; -import ChatMessageSeparatorNew from "discourse/plugins/chat/discourse/components/chat-message-separator-new"; +import ChatMessageSeparator from "discourse/plugins/chat/discourse/components/chat-message-separator"; import ChatMessageText from "discourse/plugins/chat/discourse/components/chat-message-text"; import ChatMessageThreadIndicator from "discourse/plugins/chat/discourse/components/chat-message-thread-indicator"; import ChatMessageInteractor from "discourse/plugins/chat/discourse/lib/chat-message-interactor"; @@ -494,13 +493,10 @@ export default class ChatMessage extends Component { {{! template-lint-disable no-invalid-interactive }} {{#if this.shouldRender}} - {{#if (eq @context "channel")}} - - - {{/if}} + {{/each}} diff --git a/plugins/chat/assets/javascripts/discourse/lib/dates-separators-positioner.js b/plugins/chat/assets/javascripts/discourse/lib/dates-separators-positioner.js new file mode 100644 index 00000000000..79d22d23fb5 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/lib/dates-separators-positioner.js @@ -0,0 +1,46 @@ +import { schedule } from "@ember/runloop"; + +export default class DatesSeparatorsPositioner { + static apply(list) { + schedule("afterRender", () => { + const dates = [ + ...list.querySelectorAll(".chat-message-separator-date"), + ].reverse(); + const height = list.querySelector( + ".chat-messages-container" + ).clientHeight; + + dates + .map((date, index) => { + const item = { bottom: 0, date }; + const line = date.nextElementSibling; + + if (index > 0) { + const prevDate = dates[index - 1]; + const prevLine = prevDate.nextElementSibling; + item.bottom = height - prevLine.offsetTop; + } + + if (dates.length === 1) { + item.height = height; + } else { + if (index === 0) { + item.height = height - line.offsetTop; + } else { + const prevDate = dates[index - 1]; + const prevLine = prevDate.nextElementSibling; + item.height = + height - line.offsetTop - (height - prevLine.offsetTop); + } + } + + return item; + }) + // group all writes at the end + .forEach((item) => { + item.date.style.bottom = item.bottom + "px"; + item.date.style.height = item.height + "px"; + }); + }); + } +} diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-message.js b/plugins/chat/assets/javascripts/discourse/models/chat-message.js index df0df7aedd0..1e20cb906f7 100644 --- a/plugins/chat/assets/javascripts/discourse/models/chat-message.js +++ b/plugins/chat/assets/javascripts/discourse/models/chat-message.js @@ -5,7 +5,6 @@ import Bookmark from "discourse/models/bookmark"; import User from "discourse/models/user"; import { getOwnerWithFallback } from "discourse-common/lib/get-owner"; import discourseLater from "discourse-common/lib/later"; -import I18n from "discourse-i18n"; import transformAutolinks from "discourse/plugins/chat/discourse/lib/transform-auto-links"; import ChatMessageReaction from "discourse/plugins/chat/discourse/models/chat-message-reaction"; @@ -161,35 +160,6 @@ export default class ChatMessage { return this.channel.currentUserMembership?.lastReadMessageId >= this.id; } - @cached - get firstMessageOfTheDayAt() { - if (!this.previousMessage) { - return this.#startOfDay(this.createdAt); - } - - if ( - !this.#areDatesOnSameDay(this.previousMessage.createdAt, this.createdAt) - ) { - return this.#startOfDay(this.createdAt); - } - } - - @cached - get formattedFirstMessageDate() { - if (this.firstMessageOfTheDayAt) { - return this.#calendarDate(this.firstMessageOfTheDayAt); - } - } - - #calendarDate(date) { - return moment(date).calendar(moment(), { - sameDay: `[${I18n.t("chat.chat_message_separator.today")}]`, - lastDay: `[${I18n.t("chat.chat_message_separator.yesterday")}]`, - lastWeek: "LL", - sameElse: "LL", - }); - } - @cached get index() { return this.manager?.messages?.indexOf(this); @@ -370,16 +340,4 @@ export default class ChatMessage { return User.create(user); } - - #areDatesOnSameDay(a, b) { - return ( - a.getFullYear() === b.getFullYear() && - a.getMonth() === b.getMonth() && - a.getDate() === b.getDate() - ); - } - - #startOfDay(date) { - return moment(date).startOf("day").format(); - } } diff --git a/plugins/chat/spec/system/single_thread_spec.rb b/plugins/chat/spec/system/single_thread_spec.rb index dea9cbb6891..c69afe94f6d 100644 --- a/plugins/chat/spec/system/single_thread_spec.rb +++ b/plugins/chat/spec/system/single_thread_spec.rb @@ -195,5 +195,17 @@ describe "Single thread in side panel", type: :system do expect(side_panel).to have_open_thread(thread) end end + + context "when messages are separated by a day" do + before do + Fabricate(:chat_message, chat_channel: channel, thread: thread, created_at: 2.days.ago) + end + + it "shows a date separator" do + chat_page.visit_thread(thread) + + expect(page).to have_selector(".chat-thread .chat-message-separator__text", text: "Today") + end + end end end diff --git a/plugins/chat/test/javascripts/components/chat-message-separator-date-test.js b/plugins/chat/test/javascripts/components/chat-message-separator-date-test.js deleted file mode 100644 index 3cec6e4b180..00000000000 --- a/plugins/chat/test/javascripts/components/chat-message-separator-date-test.js +++ /dev/null @@ -1,27 +0,0 @@ -import { render } from "@ember/test-helpers"; -import hbs from "htmlbars-inline-precompile"; -import { module, test } from "qunit"; -import { setupRenderingTest } from "discourse/tests/helpers/component-test"; -import { query } from "discourse/tests/helpers/qunit-helpers"; - -module( - "Discourse Chat | Component | chat-message-separator-date", - function (hooks) { - setupRenderingTest(hooks); - - test("first message of the day", async function (assert) { - this.set("date", moment().format("LLL")); - this.set("message", { formattedFirstMessageDate: this.date }); - this.set("fetchMessagesByDate", () => {}); - - await render( - hbs`` - ); - - assert.strictEqual( - query(".chat-message-separator-date").innerText.trim(), - this.date - ); - }); - } -); diff --git a/plugins/chat/test/javascripts/components/chat-message-separator-new-test.js b/plugins/chat/test/javascripts/components/chat-message-separator-new-test.js deleted file mode 100644 index 0f0d8a4fcb2..00000000000 --- a/plugins/chat/test/javascripts/components/chat-message-separator-new-test.js +++ /dev/null @@ -1,24 +0,0 @@ -import { render } from "@ember/test-helpers"; -import hbs from "htmlbars-inline-precompile"; -import { module, test } from "qunit"; -import { setupRenderingTest } from "discourse/tests/helpers/component-test"; -import { query } from "discourse/tests/helpers/qunit-helpers"; -import I18n from "discourse-i18n"; - -module( - "Discourse Chat | Component | chat-message-separator-new", - function (hooks) { - setupRenderingTest(hooks); - - test("newest message", async function (assert) { - this.set("message", { newest: true }); - - await render(hbs``); - - assert.strictEqual( - query(".chat-message-separator-new").innerText.trim(), - I18n.t("chat.last_visit") - ); - }); - } -);