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

Follow-up: chat scrolling refactoring #11943

Merged
merged 4 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 54 additions & 92 deletions src/components/MessagesList/MessagesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ import uniqueId from 'lodash/uniqueId.js'
import Message from 'vue-material-design-icons/Message.vue'

import Axios from '@nextcloud/axios'
import { getCapabilities } from '@nextcloud/capabilities'
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import moment from '@nextcloud/moment'

Expand Down Expand Up @@ -241,7 +240,7 @@ export default {
return false
}

return !!this.$store.getters.findParticipant(this.token, this.$store.getters.getParticipantIdentifier())
return !!this.$store.getters.findParticipant(this.token, this.conversation)
},

isInLobby() {
Expand Down Expand Up @@ -613,36 +612,37 @@ export default {
let isFocused = null
if (focusMessageId) {
// scroll to message in URL anchor
isFocused = this.focusMessage(focusMessageId, false)
this.focusMessage(focusMessageId)
return
}

if (!isFocused && this.visualLastReadMessageId) {
if (this.visualLastReadMessageId) {
// scroll to last read message if visible in the current pages
isFocused = this.focusMessage(this.visualLastReadMessageId, false, false)
}

// TODO: in case the element is not in a page but does exist in the DB,
// we need to scroll up / down to the page where it would exist after
// loading said pages

if (!isFocused) {
// if no anchor was present or the message to focus on did not exist,
// scroll to bottom
this.scrollToBottom({ force: true })
// Safeguard: scroll to before last read message
const fallbackLastReadMessageId = this.$store.getters.getFirstDisplayableMessageIdBeforeReadMarker(this.token, this.visualLastReadMessageId)
if (fallbackLastReadMessageId) {
isFocused = this.focusMessage(fallbackLastReadMessageId, false, false)
this.$store.dispatch('setVisualLastReadMessageId', {
token: this.token,
id: fallbackLastReadMessageId,
})
} else {
// This is an ultimate safeguard in case the fallback message is not found too
// scroll to bottom
this.scrollToBottom({ force: true, smooth: true })
}
}

// if no scrollbars, clear read marker directly as scrolling is not possible for the user to clear it
// also clear in case lastReadMessage is zero which is due to an older bug
if (this.visualLastReadMessageId === 0
|| (this.$refs.scroller && this.$refs.scroller.scrollHeight <= this.$refs.scroller.offsetHeight)) {
// clear after a delay, unless scrolling can resume in-between
this.debounceUpdateReadMarkerPosition()
}
// Update read marker in all cases except when the message is from URL anchor
this.debounceUpdateReadMarkerPosition()
},

async handleStartGettingMessagesPreconditions() {
if (this.token && this.isParticipant && !this.isInLobby) {

// prevent sticky mode before we have loaded anything
this.isInitialisingMessages = true
const focusMessageId = this.getMessageIdFromHash()
Expand All @@ -653,70 +653,27 @@ export default {
})

if (this.$store.getters.getFirstKnownMessageId(this.token) === null) {
let startingMessageId = 0
// first time load, initialize important properties
if (focusMessageId === null) {
// Start from unread marker
this.$store.dispatch('setFirstKnownMessageId', {
token: this.token,
id: this.conversation.lastReadMessage,
})
startingMessageId = this.conversation.lastReadMessage
this.$store.dispatch('setLastKnownMessageId', {
token: this.token,
id: this.conversation.lastReadMessage,
})
} else {
// Start from message hash
this.$store.dispatch('setFirstKnownMessageId', {
token: this.token,
id: focusMessageId,
})
startingMessageId = focusMessageId
this.$store.dispatch('setLastKnownMessageId', {
token: this.token,
id: focusMessageId,
})
}
// Start from message hash or unread marker
const startingMessageId = focusMessageId !== null ? focusMessageId : this.conversation.lastReadMessage
// First time load, initialize important properties
this.$store.dispatch('setFirstKnownMessageId', { token: this.token, id: startingMessageId })
this.$store.dispatch('setLastKnownMessageId', { token: this.token, id: startingMessageId })

// Get chat messages before last read message and after it
await this.getMessageContext(startingMessageId)
DorraJaouad marked this conversation as resolved.
Show resolved Hide resolved
const startingMessageFound = this.focusMessage(startingMessageId, false, focusMessageId !== null)

if (!startingMessageFound) {
const fallbackStartingMessageId = this.$store.getters.getFirstDisplayableMessageIdBeforeReadMarker(this.token, startingMessageId)
this.$store.dispatch('setVisualLastReadMessageId', {
token: this.token,
id: fallbackStartingMessageId,
})
this.focusMessage(fallbackStartingMessageId, false, false)
}
}

let hasScrolled = false
if (focusMessageId === null) {
// if lookForNewMessages will long poll instead of returning existing messages,
// scroll right away to avoid delays
if (!this.hasMoreMessagesToLoad) {
hasScrolled = true
this.$nextTick(() => {
this.scrollToFocusedMessage(focusMessageId)
})
}
}
this.$nextTick(() => {
// basically scrolling to either the last read message or the message in the URL anchor
// and there is a fallback to scroll to the bottom if the message is not found
this.scrollToFocusedMessage(focusMessageId)
})
DorraJaouad marked this conversation as resolved.
Show resolved Hide resolved

this.isInitialisingMessages = false

// get new messages
await this.lookForNewMessages()

if (focusMessageId === null) {
// don't scroll if lookForNewMessages was polling as we don't want
// to scroll back to the read marker after receiving new messages later
if (!hasScrolled) {
this.scrollToFocusedMessage(focusMessageId)
}
}
} else {
this.$store.dispatch('cancelLookForNewMessages', { requestId: this.chatIdentifier })
}
Expand Down Expand Up @@ -749,6 +706,12 @@ export default {
if (Axios.isCancel(exception)) {
console.debug('The request has been canceled', exception)
}

if (exception?.response?.status === 304 && exception?.response?.data === '') {
// 304 - Not modified
// Empty chat, no messages to load
this.$store.dispatch('loadedMessagesOfConversation', { token: this.token })
}
}
this.loadingOldMessages = false
},
Expand Down Expand Up @@ -1082,7 +1045,7 @@ export default {
*/
scrollToBottom(options = {}) {
this.$nextTick(() => {
if (!this.$refs.scroller) {
if (!this.$refs.scroller || this.isFocusingMessage) {
return
}

Expand All @@ -1105,7 +1068,6 @@ export default {
newTop = this.$refs.scroller.scrollHeight
this.setChatScrolledToBottom(true)
}

this.$refs.scroller.scrollTo({
top: newTop,
behavior: options?.smooth ? 'smooth' : 'auto',
Expand All @@ -1124,33 +1086,33 @@ export default {
focusMessage(messageId, smooth = true, highlightAnimation = true) {
const element = document.getElementById(`message_${messageId}`)
if (!element) {
// Message id doesn't exist
// TODO: in some cases might need to trigger a scroll up if this is an older message
DorraJaouad marked this conversation as resolved.
Show resolved Hide resolved
// https://github.com/nextcloud/spreed/pull/10084
console.warn('Message to focus not found in DOM', messageId)
return false
return false // element not found
}

console.debug('Scrolling to a focused message programmatically')
this.isFocusingMessage = true

this.$nextTick(async () => {
// FIXME: this doesn't wait for the smooth scroll to end
element.scrollIntoView({
behavior: smooth ? 'smooth' : 'auto',
block: 'center',
inline: 'nearest',
})
if (this.$refs.scroller && !smooth) {
// scroll the viewport slightly further to make sure the element is about 1/3 from the top
this.$refs.scroller.scrollTop += this.$refs.scroller.offsetHeight / 4
}
if (highlightAnimation) {
EventBus.emit('highlight-message', messageId)
}
this.isFocusingMessage = false
await this.handleScroll()
element.scrollIntoView({
behavior: smooth ? 'smooth' : 'auto',
block: 'center',
inline: 'nearest',
})

return true
if (this.$refs.scroller && !smooth) {
// scroll the viewport slightly further to make sure the element is about 1/3 from the top
this.$refs.scroller.scrollTop += this.$refs.scroller.offsetHeight / 4
}

if (highlightAnimation) {
EventBus.emit('highlight-message', messageId)
}
this.isFocusingMessage = false

return true // element found
},

/**
Expand Down
4 changes: 4 additions & 0 deletions src/store/messagesStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -1410,6 +1410,10 @@ const actions = {
async easeMessageList(context, { token }) {
context.commit('easeMessageList', { token })
},

loadedMessagesOfConversation(context, { token }) {
Antreesy marked this conversation as resolved.
Show resolved Hide resolved
context.commit('loadedMessagesOfConversation', { token })
}
}

export default { state, mutations, getters, actions }
29 changes: 8 additions & 21 deletions src/store/participantsStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,29 +230,16 @@ const getters = {
}

if (participantIdentifier.attendeeId) {
if (state.attendees[token][participantIdentifier.attendeeId]) {
return state.attendees[token][participantIdentifier.attendeeId]
}
return null
}

let foundAttendee = null
Object.keys(state.attendees[token]).forEach((attendeeId) => {
if (participantIdentifier.actorType && participantIdentifier.actorId
&& state.attendees[token][attendeeId].actorType === participantIdentifier.actorType
&& state.attendees[token][attendeeId].actorId === participantIdentifier.actorId) {
foundAttendee = attendeeId
}
if (participantIdentifier.sessionId && state.attendees[token][attendeeId].sessionIds.includes(participantIdentifier.sessionId)) {
foundAttendee = attendeeId
}
})

if (!foundAttendee) {
return null
return state.attendees[token][participantIdentifier.attendeeId] ?? null
}

return state.attendees[token][foundAttendee]
// Fallback, sometimes actorId and actorType are set before the attendeeId
DorraJaouad marked this conversation as resolved.
Show resolved Hide resolved
return Object.entries(state.attendees[token]).find(([attendeeId, attendee]) => {
return (participantIdentifier.actorType && participantIdentifier.actorId
&& attendee.actorType === participantIdentifier.actorType
&& attendee.actorId === participantIdentifier.actorId)
|| (participantIdentifier.sessionId && attendee.sessionIds.includes(participantIdentifier.sessionId))
})?.[1] ?? null
},
getPeer: (state) => (token, sessionId, userId) => {
if (state.peers[token]) {
Expand Down
Loading