FEATURE: save/retrieve scroll position in chat channel (#25336)
Note this is only saved on each tab session.
This commit is contained in:
parent
2014f1a0b7
commit
9365d8b544
|
@ -63,6 +63,7 @@ export default class ChatChannel extends Component {
|
||||||
@service chatDraftsManager;
|
@service chatDraftsManager;
|
||||||
@service chatEmojiPickerManager;
|
@service chatEmojiPickerManager;
|
||||||
@service chatStateManager;
|
@service chatStateManager;
|
||||||
|
@service chatChannelScrollPositions;
|
||||||
@service("chat-channel-composer") composer;
|
@service("chat-channel-composer") composer;
|
||||||
@service("chat-channel-pane") pane;
|
@service("chat-channel-pane") pane;
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
|
@ -96,6 +97,10 @@ export default class ChatChannel extends Component {
|
||||||
return this.args.channel.currentUserMembership;
|
return this.args.channel.currentUserMembership;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasSavedScrollPosition() {
|
||||||
|
return !!this.chatChannelScrollPositions.get(this.args.channel.id);
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setScrollable(element) {
|
setScrollable(element) {
|
||||||
this.scrollable = element;
|
this.scrollable = element;
|
||||||
|
@ -158,6 +163,10 @@ export default class ChatChannel extends Component {
|
||||||
|
|
||||||
if (this.args.targetMessageId) {
|
if (this.args.targetMessageId) {
|
||||||
this.debounceHighlightOrFetchMessage(this.args.targetMessageId);
|
this.debounceHighlightOrFetchMessage(this.args.targetMessageId);
|
||||||
|
} else if (this.chatChannelScrollPositions.get(this.args.channel.id)) {
|
||||||
|
this.debounceHighlightOrFetchMessage(
|
||||||
|
this.chatChannelScrollPositions.get(this.args.channel.id)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.fetchMessages({ fetch_from_last_read: true });
|
this.fetchMessages({ fetch_from_last_read: true });
|
||||||
}
|
}
|
||||||
|
@ -192,7 +201,10 @@ export default class ChatChannel extends Component {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (findArgs.target_message_id) {
|
if (findArgs.target_message_id) {
|
||||||
this.scrollToMessageId(findArgs.target_message_id, { highlight: true });
|
this.scrollToMessageId(findArgs.target_message_id, {
|
||||||
|
highlight: true,
|
||||||
|
position: findArgs.position,
|
||||||
|
});
|
||||||
} else if (findArgs.fetch_from_last_read) {
|
} else if (findArgs.fetch_from_last_read) {
|
||||||
const lastReadMessageId = this.currentUserMembership?.lastReadMessageId;
|
const lastReadMessageId = this.currentUserMembership?.lastReadMessageId;
|
||||||
this.scrollToMessageId(lastReadMessageId);
|
this.scrollToMessageId(lastReadMessageId);
|
||||||
|
@ -379,7 +391,7 @@ export default class ChatChannel extends Component {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.fetchMessages({ target_message_id: messageId });
|
this.fetchMessages({ target_message_id: messageId, position: "end" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -484,6 +496,12 @@ export default class ChatChannel extends Component {
|
||||||
|
|
||||||
if (state.atBottom) {
|
if (state.atBottom) {
|
||||||
this.fetchMoreMessages({ direction: FUTURE });
|
this.fetchMoreMessages({ direction: FUTURE });
|
||||||
|
this.chatChannelScrollPositions.remove(this.args.channel.id);
|
||||||
|
} else {
|
||||||
|
this.chatChannelScrollPositions.set(
|
||||||
|
this.args.channel.id,
|
||||||
|
state.lastVisibleId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -670,6 +688,7 @@ export default class ChatChannel extends Component {
|
||||||
"chat-channel"
|
"chat-channel"
|
||||||
(if this.messagesLoader.loading "loading")
|
(if this.messagesLoader.loading "loading")
|
||||||
(if this.pane.sending "chat-channel--sending")
|
(if this.pane.sending "chat-channel--sending")
|
||||||
|
(if this.hasSavedScrollPosition "chat-channel--saved-scroll-position")
|
||||||
(unless this.messagesLoader.fetchedOnce "chat-channel--not-loaded-once")
|
(unless this.messagesLoader.fetchedOnce "chat-channel--not-loaded-once")
|
||||||
}}
|
}}
|
||||||
{{willDestroy this.teardown}}
|
{{willDestroy this.teardown}}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { cancel, throttle } from "@ember/runloop";
|
||||||
import Modifier from "ember-modifier";
|
import Modifier from "ember-modifier";
|
||||||
import discourseLater from "discourse-common/lib/later";
|
import discourseLater from "discourse-common/lib/later";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
import { checkMessageBottomVisibility } from "discourse/plugins/chat/discourse/lib/check-message-visibility";
|
||||||
|
|
||||||
const UP = "up";
|
const UP = "up";
|
||||||
const DOWN = "down";
|
const DOWN = "down";
|
||||||
|
@ -49,7 +50,11 @@ export default class ChatScrollableList extends Modifier {
|
||||||
cancel(this.scrollTimer);
|
cancel(this.scrollTimer);
|
||||||
this.throttleTimer = throttle(this, this.computeScroll, 50, true);
|
this.throttleTimer = throttle(this, this.computeScroll, 50, true);
|
||||||
this.scrollTimer = discourseLater(() => {
|
this.scrollTimer = discourseLater(() => {
|
||||||
this.options.onScrollEnd?.(this.computeState());
|
this.options.onScrollEnd?.(
|
||||||
|
Object.assign(this.computeState(), {
|
||||||
|
lastVisibleId: this.computeFirstVisibleMessageId(),
|
||||||
|
})
|
||||||
|
);
|
||||||
}, this.options.delay || 250);
|
}, this.options.delay || 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,4 +132,23 @@ export default class ChatScrollableList extends Modifier {
|
||||||
|
|
||||||
return this.element.scrollTop < this.lastScrollTop ? UP : DOWN;
|
return this.element.scrollTop < this.lastScrollTop ? UP : DOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
computeFirstVisibleMessageId() {
|
||||||
|
let firstVisibleMessage;
|
||||||
|
const messages = this.element.querySelectorAll(
|
||||||
|
":scope .chat-messages-container > [data-id]"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
const message = messages[i];
|
||||||
|
|
||||||
|
if (checkMessageBottomVisibility(this.element, message)) {
|
||||||
|
firstVisibleMessage = message;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = firstVisibleMessage?.dataset?.id;
|
||||||
|
return id ? parseInt(id, 10) : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import Service from "@ember/service";
|
||||||
|
import { TrackedMap } from "@ember-compat/tracked-built-ins";
|
||||||
|
|
||||||
|
export default class ChatChannelScrollPositions extends Service {
|
||||||
|
@tracked positions = new TrackedMap();
|
||||||
|
|
||||||
|
add(channelId, position) {
|
||||||
|
this.positions.set(channelId, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(channelId) {
|
||||||
|
if (this.positions.has(channelId)) {
|
||||||
|
this.positions.delete(channelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -358,4 +358,24 @@ RSpec.describe "Chat channel", type: :system do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when navigating from one channel to another" do
|
||||||
|
fab!(:channel_2) { Fabricate(:chat_channel) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
channel_2.add(current_user)
|
||||||
|
Fabricate.times(50, :chat_message, chat_channel: channel_1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "remembers the scroll position" do
|
||||||
|
chat_page.visit_channel(channel_1, message_id: channel_1.chat_messages[2].id)
|
||||||
|
|
||||||
|
expect(channel_page).to have_css(".chat-channel--saved-scroll-position")
|
||||||
|
|
||||||
|
sidebar_page.open_channel(channel_2)
|
||||||
|
sidebar_page.open_channel(channel_1)
|
||||||
|
|
||||||
|
expect(channel_page.messages).to have_message(id: channel_1.chat_messages[2].id)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue