Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve chat channel unread marker handling #9006

Merged
merged 9 commits into from
Jul 12, 2022
8 changes: 4 additions & 4 deletions resources/assets/lib/chat/chat-state-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,10 @@ export default class ChatStateStore implements DispatchListener {
// noticeable when navigating via ?sendto= on existing channel.
if (this.selected === channelId) return;

// mark the channel being switched away from as read.
if (this.selectedChannel != null) {
this.channelStore.markAsRead(this.selectedChannel.channelId);
}
// Mark the channel being switched away from as read.
// Marking as read is done here to avoid constantly sending mark-as-read requests
// while receiving messages when autoScroll is enabled on the channel.
this.selectedChannel?.sendMarkAsRead();

this.selected = channelId;

Expand Down
15 changes: 8 additions & 7 deletions resources/assets/lib/chat/conversation-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,10 @@ export default class ConversationView extends React.Component<Props> {
disposeOnUnmount(
this,
reaction(() => core.windowFocusObserver.hasFocus, (value) => {
if (value) {
this.maybeMarkAsRead();
// mark as read when regaining focus and at the bottom to the channel.
if (value && this.currentChannel?.uiState.autoScroll) {
this.currentChannel.moveMarkAsReadMarker();
this.currentChannel.sendMarkAsRead();
}
}),
);
Expand Down Expand Up @@ -264,18 +266,17 @@ export default class ConversationView extends React.Component<Props> {

this.currentChannel.uiState.autoScroll = chatView.scrollTop + chatView.clientHeight >= chatView.scrollHeight;
this.currentChannel.uiState.scrollY = chatView.scrollTop;
// keep marker at the end when autoScrolling but only if window has focus.
if (this.currentChannel.uiState.autoScroll && core.windowFocusObserver.hasFocus) {
this.currentChannel.moveMarkAsReadMarker();
}
};

private loadEarlierMessages = () => {
if (this.currentChannel == null) return;
core.dataStore.channelStore.loadChannelEarlierMessages(this.currentChannel.channelId);
};

private maybeMarkAsRead() {
if (this.currentChannel == null) return;
core.dataStore.channelStore.markAsRead(this.currentChannel.channelId);
}

private renderCannotSendMessage() {
if (this.currentChannel == null) {
// this shouldn't happen...
Expand Down
28 changes: 25 additions & 3 deletions resources/assets/lib/models/chat/channel.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

import { getChannel, getMessages } from 'chat/chat-api';
import { markAsRead, getChannel, getMessages } from 'chat/chat-api';
import ChannelJson, { ChannelType, SupportedChannelType, supportedTypeLookup } from 'interfaces/chat/channel-json';
import MessageJson from 'interfaces/chat/message-json';
import { minBy, sortBy } from 'lodash';
Expand Down Expand Up @@ -32,6 +32,8 @@ export default class Channel {
};
@observable userIds: number[] = [];

private markAsReadLastSent = 0;
private markAsReadTimeout: number | null = null;
@observable private messagesMap = new Map<number | string, Message>();
private serverLastMessageId?: number;
@observable private usersLoaded = false;
Expand Down Expand Up @@ -162,7 +164,7 @@ export default class Channel {
@action
addSendingMessage(message: Message) {
this.messagesMap.set(message.messageId, message);
this.markAsRead();
this.moveMarkAsReadMarker();
}

@action
Expand Down Expand Up @@ -229,7 +231,7 @@ export default class Channel {
}

@action
markAsRead() {
moveMarkAsReadMarker() {
this.setLastReadId(this.lastMessageId);
}

Expand All @@ -242,6 +244,25 @@ export default class Channel {
}
}

@action
sendMarkAsRead() {
const lastReadId = this.lastReadId ?? 0;
if (this.markAsReadLastSent >= lastReadId || this.markAsReadTimeout != null) return;

// TODO: check if can just replace with debounce?
const currentTimeout = window.setTimeout(action(() => {
// allow next debounce to be queued again
if (this.markAsReadTimeout === currentTimeout) {
this.markAsReadTimeout = null;
}

this.markAsReadLastSent = lastReadId;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this check if LastSent >= lastReadId as well? updateWithJson may update the value outside this function (and the same check when setting the value in there?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure the delay is even needed, thinking about it again...

markAsRead(this.channelId, lastReadId);
}), 1000);

this.markAsReadTimeout = currentTimeout;
}

@action
setInputText(message: string) {
this.inputText = message;
Expand All @@ -260,6 +281,7 @@ export default class Channel {
if (json.current_user_attributes != null) {
this.canMessageError = json.current_user_attributes.can_message_error;
this.setLastReadId(json.current_user_attributes.last_read_id);
this.markAsReadLastSent = json.current_user_attributes.last_read_id;
}
}

Expand Down
37 changes: 1 addition & 36 deletions resources/assets/lib/stores/channel-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ChatNewConversationAdded } from 'actions/chat-new-conversation-added';
import ChatUpdateSilences from 'actions/chat-update-silences';
import DispatcherAction from 'actions/dispatcher-action';
import { dispatch, dispatchListener } from 'app-dispatcher';
import { markAsRead as apiMarkAsRead, newConversation, partChannel as apiPartChannel, sendMessage } from 'chat/chat-api';
import { newConversation, partChannel as apiPartChannel, sendMessage } from 'chat/chat-api';
import MessageNewEvent from 'chat/message-new-event';
import DispatchListener from 'dispatch-listener';
import ChannelJson, { filterSupportedChannelTypes, SupportedChannelType, supportedChannelTypes } from 'interfaces/chat/channel-json';
Expand Down Expand Up @@ -54,8 +54,6 @@ export default class ChannelStore implements DispatchListener {
@observable channels = observable.map<number, Channel>();
lastReceivedMessageId = 0;

private markingAsRead: Partial<Record<number, number>> = {};

@computed
get groupedChannels() {
const grouped = makeEmptyGroupedChannels();
Expand Down Expand Up @@ -135,39 +133,6 @@ export default class ChannelStore implements DispatchListener {
this.get(channelId)?.loadEarlierMessages();
}

@action
markAsRead(channelId: number) {
const channel = this.get(channelId);

if (channel == null || !channel.isUnread || !channel.uiState.autoScroll) {
return;
}

if (this.markingAsRead[channelId] != null) {
return;
}

channel.markAsRead();

const currentTimeout = window.setTimeout(action(() => {
// allow next debounce to be queued again
if (this.markingAsRead[channelId] === currentTimeout) {
delete this.markingAsRead[channelId];
}

// TODO: need to mark again in case the marker has moved?

// We don't need to send mark-as-read for our own messages, as the cursor is automatically bumped forward server-side when sending messages.
if (channel.lastMessage?.sender.id === core.currentUser?.id) {
return;
}

apiMarkAsRead(channel.channelId, channel.lastMessageId);
}), 1000);

this.markingAsRead[channelId] = currentTimeout;
}

@action
partChannel(channelId: number, remote = true) {
if (channelId > 0 && remote) {
Expand Down