FEATURE: Scroll to first message when clicking date in chat (#21926)
This PR adds a new parameter to fetch chat messages: `target_date`. It can be used to fetch messages by a specific date string. Note that it does not need to be the `created_at` date of an existing message, it can be any date. Similar to `target_message_id`, it retrieves an array of past and future messages following the query limits.
This commit is contained in:
parent
cee15d4177
commit
e51bbfa4e8
|
@ -68,14 +68,14 @@ class Chat::Api::ChannelsController < Chat::ApiController
|
|||
end
|
||||
|
||||
def show
|
||||
if params[:target_message_id].present? || params[:include_messages].present? ||
|
||||
params[:fetch_from_last_read].present?
|
||||
if should_build_channel_view?
|
||||
with_service(
|
||||
Chat::ChannelViewBuilder,
|
||||
**params.permit(
|
||||
:channel_id,
|
||||
:target_message_id,
|
||||
:thread_id,
|
||||
:target_date,
|
||||
:page_size,
|
||||
:direction,
|
||||
:fetch_from_last_read,
|
||||
|
@ -83,6 +83,7 @@ class Chat::Api::ChannelsController < Chat::ApiController
|
|||
:channel_id,
|
||||
:target_message_id,
|
||||
:thread_id,
|
||||
:target_date,
|
||||
:page_size,
|
||||
:direction,
|
||||
:fetch_from_last_read,
|
||||
|
@ -160,4 +161,9 @@ class Chat::Api::ChannelsController < Chat::ApiController
|
|||
permitted_params += CATEGORY_CHANNEL_EDITABLE_PARAMS if channel.category_channel?
|
||||
params.require(:channel).permit(*permitted_params)
|
||||
end
|
||||
|
||||
def should_build_channel_view?
|
||||
params[:target_message_id].present? || params[:target_date].present? ||
|
||||
params[:include_messages].present? || params[:fetch_from_last_read].present?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
module Chat
|
||||
# Queries messages for a specific channel. This can be used in two modes:
|
||||
#
|
||||
# 1. Query messages around a target_message_id. This is used when a user
|
||||
# needs to jump to the middle of a messages stream or load around their
|
||||
# last read message ID. There is no pagination or direction here, just
|
||||
# a limit on past and future messages.
|
||||
# 1. Query messages around a target_message_id or target_date. This is used
|
||||
# when a user needs to jump to the middle of a messages stream or load
|
||||
# around a target. There is no pagination or direction
|
||||
# here, just a limit on past and future messages.
|
||||
# 2. Query messages with paginations and direction. This is used for normal
|
||||
# scrolling of the messages stream of a channel.
|
||||
#
|
||||
|
@ -28,6 +28,7 @@ module Chat
|
|||
# @param thread_id [Integer] (optional) The thread ID to filter messages by.
|
||||
# @param target_message_id [Integer] (optional) The message ID to query around.
|
||||
# It is assumed that the caller already checked if this exists.
|
||||
# @param target_date [String] (optional) The date to query around.
|
||||
# @param include_thread_messages [Boolean] (optional) Whether to include messages
|
||||
# that are linked to a thread.
|
||||
# @param page_size [Integer] (optional) The number of messages to fetch when not
|
||||
|
@ -42,7 +43,8 @@ module Chat
|
|||
target_message_id: nil,
|
||||
include_thread_messages: false,
|
||||
page_size: PAST_MESSAGE_LIMIT + FUTURE_MESSAGE_LIMIT,
|
||||
direction: nil
|
||||
direction: nil,
|
||||
target_date: nil
|
||||
)
|
||||
messages = base_query(channel: channel)
|
||||
messages = messages.with_deleted if guardian.can_moderate_chat?(channel.chatable)
|
||||
|
@ -61,7 +63,11 @@ module Chat
|
|||
if target_message_id.present? && direction.blank?
|
||||
query_around_target(target_message_id, channel, messages)
|
||||
else
|
||||
query_paginated_messages(direction, page_size, channel, messages, target_message_id)
|
||||
if target_date.present?
|
||||
query_by_date(target_date, channel, messages)
|
||||
else
|
||||
query_paginated_messages(direction, page_size, channel, messages, target_message_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -151,5 +157,32 @@ module Chat
|
|||
can_load_more_future: can_load_more_future,
|
||||
}
|
||||
end
|
||||
|
||||
def self.query_by_date(target_date, channel, messages)
|
||||
past_messages =
|
||||
messages
|
||||
.where("created_at <= ?", target_date)
|
||||
.order(created_at: :desc)
|
||||
.limit(PAST_MESSAGE_LIMIT)
|
||||
.to_a
|
||||
|
||||
future_messages =
|
||||
messages
|
||||
.where("created_at > ?", target_date)
|
||||
.order(created_at: :asc)
|
||||
.limit(FUTURE_MESSAGE_LIMIT)
|
||||
.to_a
|
||||
|
||||
can_load_more_past = past_messages.size == PAST_MESSAGE_LIMIT
|
||||
can_load_more_future = future_messages.size == FUTURE_MESSAGE_LIMIT
|
||||
|
||||
{
|
||||
past_messages: past_messages,
|
||||
future_messages: future_messages,
|
||||
target_date: target_date,
|
||||
can_load_more_past: can_load_more_past,
|
||||
can_load_more_future: can_load_more_future,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -51,6 +51,7 @@ module Chat
|
|||
attribute :direction, :string # (optional)
|
||||
attribute :page_size, :integer # (optional)
|
||||
attribute :fetch_from_last_read, :boolean # (optional)
|
||||
attribute :target_date, :string # (optional)
|
||||
|
||||
validates :channel_id, presence: true
|
||||
validates :direction,
|
||||
|
@ -128,16 +129,17 @@ module Chat
|
|||
include_thread_messages: include_thread_messages,
|
||||
page_size: contract.page_size,
|
||||
direction: contract.direction,
|
||||
target_date: contract.target_date,
|
||||
)
|
||||
|
||||
context.can_load_more_past = messages_data[:can_load_more_past]
|
||||
context.can_load_more_future = messages_data[:can_load_more_future]
|
||||
|
||||
if !messages_data[:target_message]
|
||||
if !messages_data[:target_message] && !messages_data[:target_date]
|
||||
context.messages = messages_data[:messages]
|
||||
else
|
||||
messages_data[:target_message] = (
|
||||
if !include_thread_messages && messages_data[:target_message].thread_reply?
|
||||
if !include_thread_messages && messages_data[:target_message]&.thread_reply?
|
||||
[]
|
||||
else
|
||||
[messages_data[:target_message]]
|
||||
|
@ -148,7 +150,7 @@ module Chat
|
|||
messages_data[:past_messages].reverse,
|
||||
messages_data[:target_message],
|
||||
messages_data[:future_messages],
|
||||
].reduce([], :concat)
|
||||
].reduce([], :concat).compact
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -36,12 +36,12 @@
|
|||
class="chat-messages-container"
|
||||
{{chat/on-resize this.didResizePane (hash delay=100 immediate=true)}}
|
||||
>
|
||||
|
||||
{{#if this.loadedOnce}}
|
||||
{{#each @channel.messages key="id" as |message|}}
|
||||
<ChatMessage
|
||||
@message={{message}}
|
||||
@resendStagedMessage={{this.resendStagedMessage}}
|
||||
@fetchMessagesByDate={{this.fetchMessagesByDate}}
|
||||
@context="channel"
|
||||
/>
|
||||
{{/each}}
|
||||
|
|
|
@ -183,6 +183,8 @@ export default class ChatLivePane extends Component {
|
|||
if (this.requestedTargetMessageId) {
|
||||
findArgs.targetMessageId = this.requestedTargetMessageId;
|
||||
scrollToMessageId = this.requestedTargetMessageId;
|
||||
} else if (this.requestedTargetDate) {
|
||||
findArgs.targetDate = this.requestedTargetDate;
|
||||
} else if (fetchingFromLastRead) {
|
||||
findArgs.fetchFromLastRead = true;
|
||||
scrollToMessageId =
|
||||
|
@ -230,6 +232,15 @@ export default class ChatLivePane extends Component {
|
|||
highlight: true,
|
||||
});
|
||||
return;
|
||||
} else if (this.requestedTargetDate) {
|
||||
const message = this.args.channel?.findFirstMessageOfDay(
|
||||
this.requestedTargetDate
|
||||
);
|
||||
|
||||
this.scrollToMessage(message.id, {
|
||||
highlight: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -240,7 +251,6 @@ export default class ChatLivePane extends Component {
|
|||
this.scrollToMessage(scrollToMessageId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.scrollToBottom();
|
||||
})
|
||||
.catch(this._handleErrors)
|
||||
|
@ -251,6 +261,7 @@ export default class ChatLivePane extends Component {
|
|||
|
||||
this.loadedOnce = true;
|
||||
this.requestedTargetMessageId = null;
|
||||
this.requestedTargetDate = null;
|
||||
this.loadingMorePast = false;
|
||||
this.debounceFillPaneAttempt();
|
||||
this.updateLastReadMessage();
|
||||
|
@ -364,6 +375,16 @@ export default class ChatLivePane extends Component {
|
|||
}
|
||||
|
||||
@bind
|
||||
fetchMessagesByDate(date) {
|
||||
const message = this.args.channel?.findFirstMessageOfDay(date);
|
||||
if (message.firstOfResults && this.args.channel?.canLoadMorePast) {
|
||||
this.requestedTargetDate = date;
|
||||
this.debounceFetchMessages();
|
||||
} else {
|
||||
this.highlightOrFetchMessage(message.id);
|
||||
}
|
||||
}
|
||||
|
||||
fillPaneAttempt() {
|
||||
if (this._selfDeleted) {
|
||||
return;
|
||||
|
@ -392,9 +413,7 @@ export default class ChatLivePane extends Component {
|
|||
let foundFirstNew = false;
|
||||
|
||||
result.chat_messages.forEach((messageData, index) => {
|
||||
if (index === 0) {
|
||||
messageData.firstOfResults = true;
|
||||
}
|
||||
messageData.firstOfResults = index === 0;
|
||||
|
||||
if (this.currentUser.ignored_users) {
|
||||
// If a message has been hidden it is because the current user is ignoring
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
{{#if @message.firstMessageOfTheDayAt}}
|
||||
{{#if @message.formattedFirstMessageDate}}
|
||||
<div
|
||||
class={{concat-class
|
||||
"chat-message-separator-date"
|
||||
(if @message.newest "with-last-visit")
|
||||
}}
|
||||
role="button"
|
||||
{{on
|
||||
"click"
|
||||
(fn @fetchMessagesByDate @message.firstMessageOfTheDayAt)
|
||||
passive=true
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="chat-message-separator__text-container"
|
||||
{{chat/track-message-separator-date}}
|
||||
>
|
||||
<span class="chat-message-separator__text">
|
||||
<span>{{@message.firstMessageOfTheDayAt}}</span>
|
||||
{{@message.formattedFirstMessageDate}}
|
||||
|
||||
{{#if @message.newest}}
|
||||
<span class="chat-message-separator__last-visit">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{{#if (and @message.newest (not @message.firstMessageOfTheDayAt))}}
|
||||
{{#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">
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
{{#if this.shouldRender}}
|
||||
{{#if (eq @context "channel")}}
|
||||
<ChatMessageSeparatorDate @message={{@message}} />
|
||||
<ChatMessageSeparatorDate
|
||||
@fetchMessagesByDate={{@fetchMessagesByDate}}
|
||||
@message={{@message}}
|
||||
/>
|
||||
<ChatMessageSeparatorNew @message={{@message}} />
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -35,6 +35,13 @@ export default class ChatMessagesManager {
|
|||
);
|
||||
}
|
||||
|
||||
findFirstMessageOfDay(messageDate) {
|
||||
const targetDay = new Date(messageDate).toDateString();
|
||||
return this.messages.find(
|
||||
(message) => new Date(message.createdAt).toDateString() === targetDay
|
||||
);
|
||||
}
|
||||
|
||||
removeMessage(message) {
|
||||
return this.messages.removeObject(message);
|
||||
}
|
||||
|
|
|
@ -157,6 +157,10 @@ export default class ChatChannel {
|
|||
return this.messagesManager.findMessage(id);
|
||||
}
|
||||
|
||||
findFirstMessageOfDay(date) {
|
||||
return this.messagesManager.findFirstMessageOfDay(date);
|
||||
}
|
||||
|
||||
addMessages(messages) {
|
||||
this.messagesManager.addMessages(messages);
|
||||
}
|
||||
|
|
|
@ -194,7 +194,7 @@ export default class ChatMessage {
|
|||
|
||||
get firstMessageOfTheDayAt() {
|
||||
if (!this.previousMessage) {
|
||||
return this.#calendarDate(this.createdAt);
|
||||
return this.#startOfDay(this.createdAt);
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -203,7 +203,13 @@ export default class ChatMessage {
|
|||
new Date(this.createdAt)
|
||||
)
|
||||
) {
|
||||
return this.#calendarDate(this.createdAt);
|
||||
return this.#startOfDay(this.createdAt);
|
||||
}
|
||||
}
|
||||
|
||||
get formattedFirstMessageDate() {
|
||||
if (this.firstMessageOfTheDayAt) {
|
||||
return this.#calendarDate(this.firstMessageOfTheDayAt);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -363,4 +369,8 @@ export default class ChatMessage {
|
|||
a.getDate() === b.getDate()
|
||||
);
|
||||
}
|
||||
|
||||
#startOfDay(date) {
|
||||
return moment(date).startOf("day").format();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,10 @@ export default class ChatApi extends Service {
|
|||
if (data.threadId) {
|
||||
args.thread_id = data.threadId;
|
||||
}
|
||||
|
||||
if (data.targetDate) {
|
||||
args.target_date = data.targetDate;
|
||||
}
|
||||
}
|
||||
|
||||
return this.#getRequest(`/channels/${channelId}`, args).then((result) => {
|
||||
|
|
|
@ -69,6 +69,10 @@
|
|||
border-radius: 4px;
|
||||
color: var(--primary-800);
|
||||
background: var(--primary-50);
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--secondary-high);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-separator__last-visit {
|
||||
|
@ -94,6 +98,30 @@
|
|||
padding: 0.25rem 0.5rem;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
|
||||
.no-touch & {
|
||||
&:hover {
|
||||
border: 1px solid var(--secondary-high);
|
||||
border-radius: 4px;
|
||||
color: var(--primary-800);
|
||||
background: var(--primary-50);
|
||||
}
|
||||
}
|
||||
|
||||
.touch & {
|
||||
&:active {
|
||||
border: 1px solid var(--secondary-high);
|
||||
border-radius: 4px;
|
||||
color: var(--primary-800);
|
||||
background: var(--primary-50);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
& + .chat-message-separator__line-container {
|
||||
|
|
|
@ -9,6 +9,7 @@ RSpec.describe Chat::MessagesQuery do
|
|||
let(:page_size) { nil }
|
||||
let(:direction) { nil }
|
||||
let(:target_message_id) { nil }
|
||||
let(:target_date) { nil }
|
||||
let(:options) do
|
||||
{
|
||||
thread_id: thread_id,
|
||||
|
@ -16,6 +17,7 @@ RSpec.describe Chat::MessagesQuery do
|
|||
page_size: page_size,
|
||||
direction: direction,
|
||||
target_message_id: target_message_id,
|
||||
target_date: target_date,
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -118,6 +120,20 @@ RSpec.describe Chat::MessagesQuery do
|
|||
end
|
||||
end
|
||||
|
||||
context "when target_date provided" do
|
||||
let(:target_date) { 1.day.ago }
|
||||
|
||||
it "queries messages in the channel and finds the past and future messages" do
|
||||
expect(subject).to eq(
|
||||
past_messages: [message_1],
|
||||
future_messages: [message_2, message_3],
|
||||
target_date: target_date,
|
||||
can_load_more_past: false,
|
||||
can_load_more_future: false,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when target_message_id not provided" do
|
||||
it "queries messages in the channel" do
|
||||
expect(subject).to eq(
|
||||
|
|
|
@ -21,6 +21,7 @@ RSpec.describe Chat::ChannelViewBuilder do
|
|||
let(:direction) { nil }
|
||||
let(:thread_id) { nil }
|
||||
let(:fetch_from_last_read) { nil }
|
||||
let(:target_date) { nil }
|
||||
let(:params) do
|
||||
{
|
||||
guardian: guardian,
|
||||
|
@ -30,6 +31,7 @@ RSpec.describe Chat::ChannelViewBuilder do
|
|||
page_size: page_size,
|
||||
direction: direction,
|
||||
thread_id: thread_id,
|
||||
target_date: target_date,
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -54,6 +56,7 @@ RSpec.describe Chat::ChannelViewBuilder do
|
|||
include_thread_messages: true,
|
||||
page_size: page_size,
|
||||
direction: direction,
|
||||
target_date: target_date,
|
||||
)
|
||||
.returns({ messages: [] })
|
||||
subject
|
||||
|
@ -337,5 +340,24 @@ RSpec.describe Chat::ChannelViewBuilder do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when target_date provided" do
|
||||
fab!(:past_message) do
|
||||
msg = Fabricate(:chat_message, chat_channel: channel)
|
||||
msg.update!(created_at: 3.days.ago)
|
||||
msg
|
||||
end
|
||||
fab!(:future_message) do
|
||||
msg = Fabricate(:chat_message, chat_channel: channel)
|
||||
msg.update!(created_at: 1.days.ago)
|
||||
msg
|
||||
end
|
||||
|
||||
let(:target_date) { 2.days.ago }
|
||||
|
||||
it "includes past and future messages" do
|
||||
expect(subject.view.chat_messages).to eq([past_message, future_message])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,9 +11,12 @@ module(
|
|||
|
||||
test("first message of the day", async function (assert) {
|
||||
this.set("date", moment().format("LLL"));
|
||||
this.set("message", { firstMessageOfTheDayAt: this.date });
|
||||
this.set("message", { formattedFirstMessageDate: this.date });
|
||||
this.set("fetchMessagesByDate", () => {});
|
||||
|
||||
await render(hbs`<ChatMessageSeparatorDate @message={{this.message}} />`);
|
||||
await render(
|
||||
hbs`<ChatMessageSeparatorDate @message={{this.message}} @fetchMessagesByDate={{this.fetchMessagesByDate}} />`
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
query(".chat-message-separator-date").innerText.trim(),
|
||||
|
|
Loading…
Reference in New Issue