mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 09:17:08 +08:00
FEATURE: implements dates separators for threads (#25335)
This commit creates a shared implementation of the dates computation and moves all the logic (new messages since last visit and dates separator into one single component <ChatMessageSeparator />). The frontend tests have been removed and only a single system spec has been added for threads as everything is sharing the same implementation and the existing channel specs should catch any regression.
This commit is contained in:
parent
d5d0bab19d
commit
2014f1a0b7
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if @message.formattedFirstMessageDate}}
|
||||
<div
|
||||
class={{concatClass
|
||||
"chat-message-separator-date"
|
||||
(if @message.newest "with-last-visit")
|
||||
}}
|
||||
role="button"
|
||||
{{on "click" this.onDateClick passive=true}}
|
||||
>
|
||||
<div
|
||||
class="chat-message-separator__text-container"
|
||||
{{trackMessageSeparatorDate}}
|
||||
>
|
||||
<span class="chat-message-separator__text">
|
||||
{{@message.formattedFirstMessageDate}}
|
||||
|
||||
{{#if @message.newest}}
|
||||
<span class="chat-message-separator__last-visit">
|
||||
<span
|
||||
class="chat-message-separator__last-visit-separator"
|
||||
>-</span>
|
||||
{{i18n "chat.last_visit"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-message-separator__line-container">
|
||||
<div class="chat-message-separator__line"></div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -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 = <template>
|
||||
{{#if (and @message.newest (not @message.formattedFirstMessageDate))}}
|
||||
<div class="chat-message-separator-new">
|
||||
<div class="chat-message-separator__text-container">
|
||||
<span class="chat-message-separator__text">
|
||||
{{i18n "chat.last_visit"}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="chat-message-separator__line-container">
|
||||
<div class="chat-message-separator__line"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>;
|
||||
|
||||
export default ChatMessageSeparatorNew;
|
|
@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.formattedFirstMessageDate}}
|
||||
<div
|
||||
class={{concatClass
|
||||
"chat-message-separator-date"
|
||||
(if @message.newest "with-last-visit")
|
||||
}}
|
||||
role="button"
|
||||
{{on "click" this.onDateClick passive=true}}
|
||||
>
|
||||
<div class="chat-message-separator__text-container" {{this.track}}>
|
||||
<span class="chat-message-separator__text">
|
||||
{{this.formattedFirstMessageDate}}
|
||||
|
||||
{{#if @message.newest}}
|
||||
<span class="chat-message-separator__last-visit">
|
||||
<span
|
||||
class="chat-message-separator__last-visit-separator"
|
||||
>-</span>
|
||||
{{i18n "chat.last_visit"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-message-separator__line-container">
|
||||
<div class="chat-message-separator__line"></div>
|
||||
</div>
|
||||
{{else if @message.newest}}
|
||||
<div class="chat-message-separator-new">
|
||||
<div class="chat-message-separator__text-container">
|
||||
<span class="chat-message-separator__text">
|
||||
{{i18n "chat.last_visit"}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="chat-message-separator__line-container">
|
||||
<div class="chat-message-separator__line"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -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>
|
||||
{{! template-lint-disable no-invalid-interactive }}
|
||||
{{#if this.shouldRender}}
|
||||
{{#if (eq @context "channel")}}
|
||||
<ChatMessageSeparatorDate
|
||||
@fetchMessagesByDate={{@fetchMessagesByDate}}
|
||||
@message={{@message}}
|
||||
/>
|
||||
<ChatMessageSeparatorNew @message={{@message}} />
|
||||
{{/if}}
|
||||
<ChatMessageSeparator
|
||||
@fetchMessagesByDate={{@fetchMessagesByDate}}
|
||||
@message={{@message}}
|
||||
/>
|
||||
|
||||
<div
|
||||
class={{concatClass
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
stackingContextFix,
|
||||
} from "discourse/plugins/chat/discourse/lib/chat-ios-hacks";
|
||||
import ChatMessagesLoader from "discourse/plugins/chat/discourse/lib/chat-messages-loader";
|
||||
import DatesSeparatorsPositioner from "discourse/plugins/chat/discourse/lib/dates-separators-positioner";
|
||||
import {
|
||||
scrollListToBottom,
|
||||
scrollListToMessage,
|
||||
|
@ -118,6 +119,7 @@ export default class ChatThread extends Component {
|
|||
}
|
||||
|
||||
bodyScrollFix();
|
||||
DatesSeparatorsPositioner.apply(this.scrollable);
|
||||
|
||||
this.needsArrow =
|
||||
(this.messagesLoader.fetchedOnce &&
|
||||
|
@ -191,6 +193,7 @@ export default class ChatThread extends Component {
|
|||
this._ignoreNextScroll = true;
|
||||
this.debounceFillPaneAttempt();
|
||||
this.debounceUpdateLastReadMessage();
|
||||
DatesSeparatorsPositioner.apply(this.scrollable);
|
||||
}
|
||||
|
||||
async fetchMessages(findArgs = {}) {
|
||||
|
@ -362,6 +365,41 @@ export default class ChatThread extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
fetchMessagesByDate(date) {
|
||||
if (this.messagesLoader.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = this.messagesManager.findFirstMessageOfDay(new Date(date));
|
||||
if (message.firstOfResults && this.messagesLoader.canLoadMorePast) {
|
||||
this.fetchMessages({ target_date: date, direction: FUTURE });
|
||||
} else {
|
||||
this.highlightOrFetchMessage(message.id, { position: "center" });
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
highlightOrFetchMessage(messageId, options = {}) {
|
||||
const message = this.messagesManager.findMessage(messageId);
|
||||
if (message) {
|
||||
this.scrollToMessageId(
|
||||
message.id,
|
||||
Object.assign(
|
||||
{
|
||||
highlight: true,
|
||||
position: "start",
|
||||
autoExpand: true,
|
||||
behavior: this.capabilities.isIOS ? "smooth" : null,
|
||||
},
|
||||
options
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this.fetchMessages({ target_message_id: messageId });
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
resetComposerMessage() {
|
||||
this.args.thread.draft = ChatMessage.createDraftMessage(
|
||||
|
@ -510,6 +548,7 @@ export default class ChatThread extends Component {
|
|||
@message={{message}}
|
||||
@disableMouseEvents={{this.isScrolling}}
|
||||
@resendStagedMessage={{this.resendStagedMessage}}
|
||||
@fetchMessagesByDate={{this.fetchMessagesByDate}}
|
||||
@context="thread"
|
||||
/>
|
||||
{{/each}}
|
||||
|
|
|
@ -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";
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`<ChatMessageSeparatorDate @message={{this.message}} @fetchMessagesByDate={{this.fetchMessagesByDate}} />`
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
query(".chat-message-separator-date").innerText.trim(),
|
||||
this.date
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
|
@ -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`<ChatMessageSeparatorNew @message={{this.message}} />`);
|
||||
|
||||
assert.strictEqual(
|
||||
query(".chat-message-separator-new").innerText.trim(),
|
||||
I18n.t("chat.last_visit")
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
Loading…
Reference in New Issue
Block a user