Skip to content

Commit

Permalink
Merge pull request #11943 from nextcloud/fix/noid/refactor-chat-scrol…
Browse files Browse the repository at this point in the history
…ling

Follow-up: chat scrolling refactoring
  • Loading branch information
DorraJaouad authored Apr 23, 2024
2 parents 76acc8d + a731695 commit 8f7b181
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 113 deletions.
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)
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)
})

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
// 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 }) {
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
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

0 comments on commit 8f7b181

Please sign in to comment.