FIX: ensures last read is updated on new message (#26772)

This commit is contained in:
Joffrey JAFFEUX 2024-04-26 18:27:39 +02:00 committed by GitHub
parent 6c7e46a32f
commit fb40f50865
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 54 additions and 49 deletions

View File

@ -20,6 +20,7 @@ import i18n from "discourse-common/helpers/i18n";
import discourseDebounce from "discourse-common/lib/debounce"; import discourseDebounce from "discourse-common/lib/debounce";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import ChatChannelStatus from "discourse/plugins/chat/discourse/components/chat-channel-status"; import ChatChannelStatus from "discourse/plugins/chat/discourse/components/chat-channel-status";
import firstVisibleMessageId from "discourse/plugins/chat/discourse/helpers/first-visible-message-id";
import ChatChannelSubscriptionManager from "discourse/plugins/chat/discourse/lib/chat-channel-subscription-manager"; import ChatChannelSubscriptionManager from "discourse/plugins/chat/discourse/lib/chat-channel-subscription-manager";
import { import {
FUTURE, FUTURE,
@ -412,27 +413,27 @@ export default class ChatChannel extends Component {
return; return;
} }
if (!this.lastFullyVisibleMessageId) { const firstFullyVisibleMessageId = firstVisibleMessageId(this.scroller);
if (!firstFullyVisibleMessageId) {
return; return;
} }
let lastUnreadVisibleMessage = this.messagesManager.findMessage( let firstMessage = this.messagesManager.findMessage(
this.lastFullyVisibleMessageId firstFullyVisibleMessageId
); );
if (!firstMessage) {
if (!lastUnreadVisibleMessage) {
return; return;
} }
const lastReadId = const lastReadId =
this.args.channel.currentUserMembership?.lastReadMessageId; this.args.channel.currentUserMembership?.lastReadMessageId;
if (lastReadId >= lastUnreadVisibleMessage.id) { if (lastReadId >= firstMessage.id) {
return; return;
} }
return this.chatApi.markChannelAsRead( return this.chatApi.markChannelAsRead(
this.args.channel.id, this.args.channel.id,
lastUnreadVisibleMessage.id firstMessage.id
); );
} }
@ -460,7 +461,6 @@ export default class ChatChannel extends Component {
(state.distanceToBottom.pixels > 250 && !state.atBottom); (state.distanceToBottom.pixels > 250 && !state.atBottom);
this.isScrolling = true; this.isScrolling = true;
this.debouncedUpdateLastReadMessage(); this.debouncedUpdateLastReadMessage();
this.lastFullyVisibleMessageId = state.lastVisibleId;
if ( if (
state.atTop || state.atTop ||
@ -484,7 +484,6 @@ export default class ChatChannel extends Component {
(state.distanceToBottom.pixels > 250 && !state.atBottom); (state.distanceToBottom.pixels > 250 && !state.atBottom);
this.isScrolling = false; this.isScrolling = false;
this.atBottom = state.atBottom; this.atBottom = state.atBottom;
this.lastFullyVisibleMessageId = state.lastVisibleId;
if (state.atBottom) { if (state.atBottom) {
this.fetchMoreMessages({ direction: FUTURE }); this.fetchMoreMessages({ direction: FUTURE });
@ -492,7 +491,7 @@ export default class ChatChannel extends Component {
} else { } else {
this.chatChannelScrollPositions.set( this.chatChannelScrollPositions.set(
this.args.channel.id, this.args.channel.id,
state.lastVisibleId state.firstVisibleId
); );
} }
} }

View File

@ -12,6 +12,7 @@ import { resetIdle } from "discourse/lib/desktop-notifications";
import { NotificationLevels } from "discourse/lib/notification-levels"; import { NotificationLevels } from "discourse/lib/notification-levels";
import discourseDebounce from "discourse-common/lib/debounce"; import discourseDebounce from "discourse-common/lib/debounce";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import firstVisibleMessageId from "discourse/plugins/chat/discourse/helpers/first-visible-message-id";
import ChatChannelThreadSubscriptionManager from "discourse/plugins/chat/discourse/lib/chat-channel-thread-subscription-manager"; import ChatChannelThreadSubscriptionManager from "discourse/plugins/chat/discourse/lib/chat-channel-thread-subscription-manager";
import { import {
FUTURE, FUTURE,
@ -123,7 +124,6 @@ export default class ChatThread extends Component {
(state.distanceToBottom.pixels > 250 && !state.atBottom); (state.distanceToBottom.pixels > 250 && !state.atBottom);
this.isScrolling = true; this.isScrolling = true;
this.debounceUpdateLastReadMessage(); this.debounceUpdateLastReadMessage();
this.lastFullyVisibleMessageId = state.lastVisibleId;
if ( if (
state.atTop || state.atTop ||
@ -148,7 +148,6 @@ export default class ChatThread extends Component {
this.resetIdle(); this.resetIdle();
this.atBottom = state.atBottom; this.atBottom = state.atBottom;
this.args.setFullTitle?.(state.atTop); this.args.setFullTitle?.(state.atTop);
this.lastFullyVisibleMessageId = state.lastVisibleId;
if (state.atBottom) { if (state.atBottom) {
this.fetchMoreMessages({ direction: FUTURE }); this.fetchMoreMessages({ direction: FUTURE });
@ -169,27 +168,27 @@ export default class ChatThread extends Component {
return; return;
} }
if (!this.lastFullyVisibleMessageId) { const firstFullyVisibleMessageId = firstVisibleMessageId(this.scroller);
if (!firstFullyVisibleMessageId) {
return; return;
} }
const lastUnreadVisibleMessage = this.messagesManager.findMessage( const firstMessage = this.messagesManager.findMessage(
this.lastFullyVisibleMessageId firstFullyVisibleMessageId
); );
if (!firstMessage) {
if (!lastUnreadVisibleMessage) {
return; return;
} }
const lastReadId = this.args.thread.currentUserMembership.lastReadMessageId; const lastReadId = this.args.thread.currentUserMembership.lastReadMessageId;
if (lastReadId >= lastUnreadVisibleMessage.id) { if (lastReadId >= firstMessage.id) {
return; return;
} }
return this.chatApi.markThreadAsRead( return this.chatApi.markThreadAsRead(
this.args.thread.channel.id, this.args.thread.channel.id,
this.args.thread.id, this.args.thread.id,
lastUnreadVisibleMessage.id firstMessage.id
); );
} }

View File

@ -0,0 +1,20 @@
import { checkMessageBottomVisibility } from "discourse/plugins/chat/discourse/lib/check-message-visibility";
export default function firstVisibleMessageId(container) {
let _found;
const messages = container.querySelectorAll(
":scope .chat-messages-container > [data-id]"
);
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i];
if (checkMessageBottomVisibility(container, message)) {
_found = message;
break;
}
}
const id = _found?.dataset?.id;
return id ? parseInt(id, 10) : null;
}

View File

@ -3,7 +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"; import firstVisibleMessageId from "discourse/plugins/chat/discourse/helpers/first-visible-message-id";
const UP = "up"; const UP = "up";
const DOWN = "down"; const DOWN = "down";
@ -54,7 +54,7 @@ export default class ChatScrollableList extends Modifier {
this.scrollTimer = discourseLater(() => { this.scrollTimer = discourseLater(() => {
this.options.onScrollEnd?.( this.options.onScrollEnd?.(
Object.assign(this.computeState(), { Object.assign(this.computeState(), {
lastVisibleId: this.computeFirstVisibleMessageId(), firstVisibleId: firstVisibleMessageId(this.element),
}) })
); );
}, this.options.delay || 250); }, this.options.delay || 250);
@ -134,23 +134,4 @@ 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;
}
} }

View File

@ -3,7 +3,7 @@
RSpec.describe "Update last read", type: :system do RSpec.describe "Update last read", type: :system do
fab!(:current_user) { Fabricate(:user) } fab!(:current_user) { Fabricate(:user) }
fab!(:channel_1) { Fabricate(:chat_channel) } fab!(:channel_1) { Fabricate(:chat_channel) }
fab!(:first_unread) { Fabricate(:chat_message, chat_channel: channel_1) } fab!(:messages) { Fabricate.times(15, :chat_message, chat_channel: channel_1) }
let(:chat_page) { PageObjects::Pages::Chat.new } let(:chat_page) { PageObjects::Pages::Chat.new }
let(:channel_page) { PageObjects::Pages::ChatChannel.new } let(:channel_page) { PageObjects::Pages::ChatChannel.new }
@ -12,21 +12,27 @@ RSpec.describe "Update last read", type: :system do
before do before do
chat_system_bootstrap chat_system_bootstrap
channel_1.add(current_user) channel_1.add(current_user)
membership.update!(last_read_message_id: first_unread.id)
Fabricate.times(25, :chat_message, chat_channel: channel_1) membership.update!(last_read_message_id: messages.last.id)
sign_in(current_user) sign_in(current_user)
end end
context "when the full message is visible" do context "when the full message is visible" do
xit "marks it as read" do it "marks it as read" do
last_message = Fabricate(:chat_message, chat_channel: channel_1) last_message = Fabricate(:chat_message, chat_channel: channel_1)
chat_page.visit_channel(channel_1) chat_page.visit_channel(channel_1)
try_until_success do try_until_success { expect(membership.reload.last_read_message_id).to eq(last_message.id) }
page.execute_script("document.querySelector('.chat-messages-scroller').scrollTo(0, 1)") end
page.execute_script("document.querySelector('.chat-messages-scroller').scrollTo(0, 0)") end
expect(membership.reload.last_read_message_id).to eq(last_message.id)
end context "when receiving a messages" do
it "marks it as read" do
chat_page.visit_channel(channel_1)
last_message = Fabricate(:chat_message, chat_channel: channel_1, use_service: true)
try_until_success { expect(membership.reload.last_read_message_id).to eq(last_message.id) }
end end
end end