From db890f970868300604c890d21a84c8b40d74a764 Mon Sep 17 00:00:00 2001 From: "Jan C. Borchardt" <925062+jancborchardt@users.noreply.github.com> Date: Wed, 4 Nov 2026 12:00:04 +0100 Subject: [PATCH] fix(notifications): open conversation from notification action without page reload Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: Jan C. Borchardt <925062+jancborchardt@users.noreply.github.com> --- .../useInterceptNotifications.spec.js | 67 +++++++++++++++++++ src/composables/useInterceptNotifications.ts | 4 +- 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 src/composables/__tests__/useInterceptNotifications.spec.js diff --git a/src/composables/__tests__/useInterceptNotifications.spec.js b/src/composables/__tests__/useInterceptNotifications.spec.js new file mode 100644 index 00000000000..bca8243a71a --- /dev/null +++ b/src/composables/__tests__/useInterceptNotifications.spec.js @@ -0,0 +1,67 @@ +/*! + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { emit } from '@nextcloud/event-bus' +import { mount } from '@vue/test-utils' +import { afterAll, afterEach, describe, expect, test, vi } from 'vitest' +import { useInterceptNotifications } from '../useInterceptNotifications.ts' + +const router = { push: vi.fn(() => new Promise(() => {})) } + +vi.mock('vue-router', () => ({ useRouter: () => router })) +vi.mock('vuex', () => ({ useStore: () => ({}) })) +vi.mock('../../stores/federation.ts', () => ({ useFederationStore: () => ({}) })) + +describe('useInterceptNotifications', () => { + const wrapper = mount({ + setup() { + useInterceptNotifications() + return () => null + }, + }) + + afterAll(() => { + wrapper.unmount() + }) + + afterEach(() => { + router.push.mockClear() + }) + + test('cancels a conversation link synchronously, before navigation finishes', () => { + const event = { + cancelAction: false, + notification: { app: 'spreed' }, + action: { type: 'WEB', url: 'https://cloud.example.com/index.php/call/abc123#message_42' }, + } + emit('notifications:action:execute', event) + + expect(event.cancelAction).toBe(true) + expect(router.push).toHaveBeenCalledWith('/call/abc123#message_42') + }) + + test('keeps default behavior for links without a conversation', () => { + const event = { + cancelAction: false, + notification: { app: 'spreed' }, + action: { type: 'WEB', url: 'https://cloud.example.com/index.php/settings/admin/talk' }, + } + emit('notifications:action:execute', event) + + expect(event.cancelAction).toBe(false) + expect(router.push).not.toHaveBeenCalled() + }) + + test('ignores notifications of other apps', () => { + const event = { + cancelAction: false, + notification: { app: 'files' }, + action: { type: 'WEB', url: 'https://cloud.example.com/index.php/call/abc123' }, + } + emit('notifications:action:execute', event) + + expect(event.cancelAction).toBe(false) + }) +}) diff --git a/src/composables/useInterceptNotifications.ts b/src/composables/useInterceptNotifications.ts index 0a9c3b9a8e8..87262e3bf7e 100644 --- a/src/composables/useInterceptNotifications.ts +++ b/src/composables/useInterceptNotifications.ts @@ -61,9 +61,9 @@ export function useInterceptNotifications() { } const conversationRouteLocationRaw = event.action.url.substring(conversationRouteIndex) - await router.push(conversationRouteLocationRaw) - + // Must be set before the first await, the notifications app checks it right after emitting event.cancelAction = true + await router.push(conversationRouteLocationRaw) break } case 'POST': {