diff --git a/src/App.vue b/src/App.vue index 57bdaedd00f..ac50396e425 100644 --- a/src/App.vue +++ b/src/App.vue @@ -38,7 +38,7 @@ import RightSidebar from './components/RightSidebar/RightSidebar.vue' import SettingsDialog from './components/SettingsDialog/SettingsDialog.vue' import { useActiveSession } from './composables/useActiveSession.js' -import { useDocumentVisibility } from './composables/useDocumentVisibility.ts' +import { useDocumentTitle } from './composables/useDocumentTitle.ts' import { useHashCheck } from './composables/useHashCheck.js' import { useIsInCall } from './composables/useIsInCall.js' import { useSessionIssueHandler } from './composables/useSessionIssueHandler.js' @@ -66,6 +66,7 @@ export default { }, setup() { + useDocumentTitle() // Add provided value to check if we're in the main app or plugin provide('Talk:isMainApp', true) @@ -74,7 +75,6 @@ export default { isLeavingAfterSessionIssue: useSessionIssueHandler(), isMobile: useIsMobile(), isNextcloudTalkHashDirty: useHashCheck(), - isDocumentVisible: useDocumentVisibility(), supportSessionState: useActiveSession(), federationStore: useFederationStore(), callViewStore: useCallViewStore(), @@ -84,8 +84,6 @@ export default { data() { return { - savedLastMessageMap: {}, - defaultPageTitle: false, loading: false, isRefreshingCurrentConversation: false, recordingConsentGiven: false, @@ -106,57 +104,6 @@ export default { return !this.isLeavingAfterSessionIssue && this.isInCall }, - /** - * Keeps a list for all last message ids - * - * @return {object} Map with token => lastMessageId - */ - lastMessageMap() { - const conversationList = this.$store.getters.conversationsList - if (conversationList.length === 0) { - return {} - } - - const lastMessage = {} - conversationList.forEach(conversation => { - lastMessage[conversation.token] = 0 - if (conversation.lastMessage) { - const currentActorIsAuthor = conversation.lastMessage.actorType === this.$store.getters.getActorType() - && conversation.lastMessage.actorId === this.$store.getters.getActorId() - if (currentActorIsAuthor) { - // Set a special value when the actor is the author so we can skip it. - // Can't use 0 though because hidden commands result in 0 - // and they would hide other previously posted new messages - lastMessage[conversation.token] = -1 - } else { - lastMessage[conversation.token] = Math.max( - conversation.lastMessage && conversation.lastMessage.id ? conversation.lastMessage.id : 0, - this.$store.getters.getLastKnownMessageId(conversation.token) ? this.$store.getters.getLastKnownMessageId(conversation.token) : 0, - ) - } - } - }) - return lastMessage - }, - - /** - * @return {boolean} Returns true, if - * - a conversation is newly added to lastMessageMap - * - a conversation has a different last message id then previously - */ - atLeastOneLastMessageIdChanged() { - let modified = false - Object.keys(this.lastMessageMap).forEach(token => { - if (!this.savedLastMessageMap[token] // Conversation is new - || (this.savedLastMessageMap[token] !== this.lastMessageMap[token] // Last message changed - && this.lastMessageMap[token] !== -1)) { // But is not from the current user - modified = true - } - }) - - return modified - }, - /** * The current conversation token * @@ -187,14 +134,6 @@ export default { }, watch: { - atLeastOneLastMessageIdChanged() { - if (this.isDocumentVisible) { - return - } - - this.setPageTitle(this.getConversationName(this.token), this.atLeastOneLastMessageIdChanged) - }, - token(newValue, oldValue) { const shouldShowSidebar = BrowserStorage.getItem('sidebarOpen') !== 'false' // Collapse the sidebar if it's a one to one conversation @@ -221,22 +160,6 @@ export default { } } }, - - isDocumentVisible(value) { - if (value) { - // Remove the potential "*" marker for unread chat messages - let title = this.getConversationName(this.token) - if (window.document.title.indexOf(t('spreed', 'Duplicate session')) === 0) { - title = t('spreed', 'Duplicate session') - } - this.setPageTitle(title, false) - } else { - // Copy the last message map to the saved version, - // this will be our reference to check if any chat got a new - // message since the last visit - this.savedLastMessageMap = this.lastMessageMap - } - }, }, beforeCreate() { @@ -398,11 +321,6 @@ export default { * the store. */ EventBus.once('conversations-received', () => { - if (this.$route.name === 'conversation') { - // Adjust the page title once the conversation list is loaded - this.setPageTitle(this.getConversationName(this.token), false) - } - if (!getCurrentUser()) { // Set the current actor/participant for guests const conversation = this.$store.getters.conversation(this.token) @@ -441,7 +359,7 @@ export default { if (this.warnLeaving && !to.params?.skipLeaveWarning) { OC.dialogs.confirmDestructive( t('spreed', 'Navigating away from the page will leave the call in {conversation}', { - conversation: this.getConversationName(this.token), + conversation: this.currentConversation?.displayName ?? '', }), t('spreed', 'Leave call'), { @@ -463,19 +381,6 @@ export default { } }) - Router.afterEach((to) => { - /** - * Change the page title only after the route was changed - */ - if (to.name === 'conversation') { - // Page title - const nextConversationName = this.getConversationName(to.params.token) - this.setPageTitle(nextConversationName) - } else if (to.name === 'notfound') { - this.setPageTitle('') - } - }) - if (getCurrentUser()) { console.debug('Setting current user') this.$store.dispatch('setCurrentUser', getCurrentUser()) @@ -626,38 +531,6 @@ export default { this.fetchSingleConversation(this.token) }, - /** - * Set the page title to the conversation name - * - * @param {string} title Prefix for the page title e.g. conversation name - * @param {boolean} showAsterix Prefix for the page title e.g. conversation name - */ - setPageTitle(title, showAsterix) { - if (this.defaultPageTitle === false) { - // On the first load we store the current page title "Talk - Nextcloud", - // so we can append it every time again - this.defaultPageTitle = window.document.title - // Coming from a "Duplicate session - Talk - …" page? - if (this.defaultPageTitle.indexOf(' - ' + t('spreed', 'Talk') + ' - ') !== -1) { - this.defaultPageTitle = this.defaultPageTitle.substring(this.defaultPageTitle.indexOf(' - ' + t('spreed', 'Talk') + ' - ') + 3) - } - // When a conversation is opened directly, the "Talk - " part is - // missing from the title - if (!IS_DESKTOP && this.defaultPageTitle.indexOf(t('spreed', 'Talk') + ' - ') !== 0) { - this.defaultPageTitle = t('spreed', 'Talk') + ' - ' + this.defaultPageTitle - } - } - - let newTitle = this.defaultPageTitle - if (title !== '') { - newTitle = `${title} - ${newTitle}` - } - if (showAsterix && !newTitle.startsWith('* ')) { - newTitle = '* ' + newTitle - } - window.document.title = newTitle - }, - onResize() { this.windowHeight = window.innerHeight - document.getElementById('header').clientHeight }, @@ -670,20 +543,6 @@ export default { event.preventDefault() }, - /** - * Get a conversation's name. - * - * @param {string} token The conversation's token - * @return {string} The conversation's name - */ - getConversationName(token) { - if (!this.$store.getters.conversation(token)) { - return '' - } - - return this.$store.getters.conversation(token).displayName - }, - async fetchSingleConversation(token) { if (this.isRefreshingCurrentConversation) { return diff --git a/src/composables/useDocumentTitle.ts b/src/composables/useDocumentTitle.ts new file mode 100644 index 00000000000..cbb9d5c6b8e --- /dev/null +++ b/src/composables/useDocumentTitle.ts @@ -0,0 +1,162 @@ +/* + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { computed, ref, watch } from 'vue' +import type { ComputedRef } from 'vue' +import type { Route } from 'vue-router' + +import { t } from '@nextcloud/l10n' + +import { useDocumentVisibility } from './useDocumentVisibility.ts' +import { useStore } from './useStore.js' +import Router from '../router/router.js' +import { EventBus } from '../services/EventBus.ts' +import type { Conversation } from '../types/index.ts' + +/** + * Composable to check whether the page is visible. + */ +export function useDocumentTitle() { + const store = useStore() + const isDocumentVisible = useDocumentVisibility() + + const defaultPageTitle = ref(getDefaultPageTitle()) + const showAsterisk = ref(false) + const savedLastMessageMap = ref>({}) + + const conversationList = computed(() => store.getters.conversationsList) + const actorId = computed(() => store.getters.getActorId()) + const actorType = computed(() => store.getters.getActorType()) + + watch(conversationList, (newValue) => { + if (isDocumentVisible.value || document.title.startsWith('* ') + || !Object.keys(savedLastMessageMap.value).length) { + return + } + + const newLastMessageMap = getLastMessageMap(newValue) + /** + * @return {boolean} Returns true, if + * - a conversation is newly added to lastMessageMap + * - a conversation has a different last message id then previously + */ + const shouldShowAsterisk = Object.keys(newLastMessageMap).some(token => { + return !savedLastMessageMap.value[token] // Conversation is new + || (savedLastMessageMap.value[token] !== newLastMessageMap[token] // Last message changed + && newLastMessageMap[token] !== -1) // But is not from the current user + }) + if (shouldShowAsterisk) { + showAsterisk.value = true + setPageTitleFromRoute(Router.currentRoute) + } + }) + + watch(isDocumentVisible, () => { + if (isDocumentVisible.value) { + // Remove asterisk for unread chat messages + showAsterisk.value = false + setPageTitleFromRoute(Router.currentRoute) + } else { + // Copy the last message map to the saved version, + // this will be our reference to check if any chat got a new + // message since the last visit + savedLastMessageMap.value = getLastMessageMap(conversationList.value) + } + }) + + /** + * Adjust the page title to the conversation name once conversationsList is loaded + */ + EventBus.once('conversations-received', () => { + setPageTitleFromRoute(Router.currentRoute) + }) + + /** + * Change the page title after the route was changed + */ + Router.afterEach((to) => setPageTitleFromRoute(to)) + + /** + * Get a list for all last message ids + * + * @param conversationList array of conversations + */ + function getLastMessageMap(conversationList: Conversation[]): Record { + if (conversationList.length === 0) { + return {} + } + + return conversationList.reduce((acc: Record, conversation: Conversation) => { + const { token, lastMessage } = conversation + // Default to 0 for messages without valid lastMessage + if (!lastMessage || Array.isArray(lastMessage)) { + acc[token] = 0 + return acc + } + + if (lastMessage.actorId === actorId.value && lastMessage.actorType === actorType.value) { + // Set a special value when the actor is the author so we can skip it. + // Can't use 0 though because hidden commands result in 0, + // and they would hide other previously posted new messages + acc[token] = -1 + } else { + // @ts-expect-error: Property 'id' does not exist on type ChatProxyMessage + const lastMessageId = lastMessage.id ?? 0 + const lastKnownMessageId = store.getters.getLastKnownMessageId(token) ?? 0 + acc[token] = Math.max(lastMessageId, lastKnownMessageId) + } + return acc + }, {}) + } + + /** + * + * @param route current web route + */ + function setPageTitleFromRoute(route: Route) { + switch (route.name) { + case 'conversation': + setPageTitle(store.getters.conversation(route.params.token)?.displayName ?? '') + break + case 'duplicatesession': + setPageTitle(t('spreed', 'Duplicate session')) + break + default: + setPageTitle('') + } + } + + /** + * Set the page title to the conversation name + * + * @param title Prefix for the page title e.g. conversation name + */ + function setPageTitle(title: string) { + const newTitle = title ? `${title} - ${defaultPageTitle.value}` : defaultPageTitle.value + document.title = (showAsterisk.value && !newTitle.startsWith('* ')) + ? '* ' + newTitle + : newTitle + } + + /** + * Get the default page title of Talk page like "Talk - Nextcloud", to append it every time again + */ + function getDefaultPageTitle() { + // Do nothing on Desktop + if (IS_DESKTOP) { + return document.title + } + const appNamePrefix = t('spreed', 'Talk') + ' - ' + // If coming from a "… - Talk - …" page, keep only "Talk - …" part + if (document.title.includes(' - ' + appNamePrefix)) { + return document.title.substring(document.title.indexOf(' - ' + appNamePrefix) + 3) + } + // When a conversation is opened directly, "Talk - " might be missing from the title + if (!document.title.startsWith(appNamePrefix)) { + return appNamePrefix + document.title + } + return document.title + } +} diff --git a/src/views/SessionConflictView.vue b/src/views/SessionConflictView.vue index b418ad9164c..213367239cf 100644 --- a/src/views/SessionConflictView.vue +++ b/src/views/SessionConflictView.vue @@ -13,30 +13,10 @@ -