Skip to content

Commit 4e6efa5

Browse files
authored
Merge pull request #19307 from nextcloud/backport/19239/stable35
[stable35] fix(sounds): fall back when the server has no play-sounds capability
2 parents 7f7d904 + cac92a9 commit 4e6efa5

2 files changed

Lines changed: 152 additions & 10 deletions

File tree

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { getCurrentUser } from '@nextcloud/auth'
7+
import axios from '@nextcloud/axios'
8+
import { generateOcsUrl } from '@nextcloud/router'
9+
import { createPinia, setActivePinia } from 'pinia'
10+
import { beforeEach, describe, expect, it, vi } from 'vitest'
11+
import BrowserStorage from '../../services/BrowserStorage.js'
12+
import { getTalkConfig } from '../../services/CapabilitiesManager.ts'
13+
14+
vi.mock('@nextcloud/auth', () => ({
15+
getCurrentUser: vi.fn(),
16+
}))
17+
vi.mock('@nextcloud/axios', () => ({
18+
default: {
19+
post: vi.fn(() => Promise.resolve()),
20+
},
21+
}))
22+
vi.mock('../../services/BrowserStorage.js', () => ({
23+
default: {
24+
getItem: vi.fn(),
25+
setItem: vi.fn(),
26+
},
27+
}))
28+
vi.mock('../../services/CapabilitiesManager.ts', () => ({
29+
getTalkConfig: vi.fn(),
30+
}))
31+
32+
/**
33+
* The initial value is computed when the module is loaded, so load it fresh for every test
34+
*/
35+
async function loadSoundsStore() {
36+
vi.resetModules()
37+
const { useSoundsStore } = await import('../sounds.js')
38+
setActivePinia(createPinia())
39+
return useSoundsStore()
40+
}
41+
42+
describe('soundsStore', () => {
43+
beforeEach(() => {
44+
vi.clearAllMocks()
45+
getCurrentUser.mockReturnValue({ uid: 'alice' })
46+
getTalkConfig.mockReturnValue(undefined)
47+
BrowserStorage.getItem.mockReturnValue(null)
48+
})
49+
50+
describe('initial value for users', () => {
51+
it('takes the value from the capabilities on Talk 24+', async () => {
52+
getTalkConfig.mockReturnValue(false)
53+
const store = await loadSoundsStore()
54+
expect(store.shouldPlaySounds).toBe(false)
55+
expect(getTalkConfig).toHaveBeenCalledWith('local', 'call', 'play-sounds')
56+
})
57+
58+
it('prefers the capabilities over browser storage', async () => {
59+
getTalkConfig.mockReturnValue(true)
60+
BrowserStorage.getItem.mockReturnValue('no')
61+
const store = await loadSoundsStore()
62+
expect(store.shouldPlaySounds).toBe(true)
63+
})
64+
65+
it('falls back to browser storage when the server has no capability', async () => {
66+
BrowserStorage.getItem.mockReturnValue('no')
67+
const store = await loadSoundsStore()
68+
expect(store.shouldPlaySounds).toBe(false)
69+
})
70+
71+
it('defaults to enabled without capability or storage', async () => {
72+
const store = await loadSoundsStore()
73+
expect(store.shouldPlaySounds).toBe(true)
74+
})
75+
})
76+
77+
describe('initial value for guests', () => {
78+
beforeEach(() => {
79+
getCurrentUser.mockReturnValue(null)
80+
})
81+
82+
it('prefers browser storage over the capabilities', async () => {
83+
getTalkConfig.mockReturnValue(true)
84+
BrowserStorage.getItem.mockReturnValue('no')
85+
const store = await loadSoundsStore()
86+
expect(store.shouldPlaySounds).toBe(false)
87+
})
88+
89+
it('takes the value from the capabilities without storage', async () => {
90+
getTalkConfig.mockReturnValue(false)
91+
const store = await loadSoundsStore()
92+
expect(store.shouldPlaySounds).toBe(false)
93+
})
94+
95+
it('defaults to enabled on older servers', async () => {
96+
const store = await loadSoundsStore()
97+
expect(store.shouldPlaySounds).toBe(true)
98+
})
99+
})
100+
101+
describe('setShouldPlaySounds', () => {
102+
it('saves on the server only when the capability exists', async () => {
103+
getTalkConfig.mockReturnValue(true)
104+
const store = await loadSoundsStore()
105+
await store.setShouldPlaySounds(false)
106+
expect(axios.post).toHaveBeenCalledWith(
107+
generateOcsUrl('apps/spreed/api/v1/settings/user'),
108+
{ key: 'play_sounds', value: 'no' },
109+
)
110+
expect(BrowserStorage.setItem).not.toHaveBeenCalled()
111+
expect(store.shouldPlaySounds).toBe(false)
112+
})
113+
114+
it('saves to browser storage instead on older servers, since they cannot hand it back', async () => {
115+
const store = await loadSoundsStore()
116+
await store.setShouldPlaySounds(false)
117+
expect(axios.post).not.toHaveBeenCalled()
118+
expect(BrowserStorage.setItem).toHaveBeenCalledWith('play_sounds', 'no')
119+
expect(store.shouldPlaySounds).toBe(false)
120+
})
121+
122+
it('saves guests to browser storage', async () => {
123+
getCurrentUser.mockReturnValue(null)
124+
const store = await loadSoundsStore()
125+
await store.setShouldPlaySounds(true)
126+
expect(axios.post).not.toHaveBeenCalled()
127+
expect(BrowserStorage.setItem).toHaveBeenCalledWith('play_sounds', 'yes')
128+
})
129+
})
130+
})

src/stores/sounds.js

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,32 @@ import { getTalkConfig } from '../services/CapabilitiesManager.ts'
1111
import { setPlaySounds } from '../services/settingsService.ts'
1212

1313
const hasUserAccount = Boolean(getCurrentUser()?.uid)
14+
const playSoundsCapability = getTalkConfig('local', 'call', 'play-sounds')
15+
const hasPlaySoundsCapability = playSoundsCapability !== undefined
16+
1417
/**
15-
* Get play sounds option (from server for user or from browser storage for guest)
18+
* Get play sounds option. A guest keeps whatever this browser remembered, otherwise the
19+
* capability decides. Servers before Talk 24 don't hand the value out at all, so fall back
20+
* to the browser and then to enabled.
21+
*
22+
* @return {boolean}
1623
*/
17-
let shouldPlaySounds = false
18-
if (hasUserAccount) {
19-
shouldPlaySounds = getTalkConfig('local', 'call', 'play-sounds')
20-
} else {
21-
if (BrowserStorage.getItem('play_sounds')) {
22-
shouldPlaySounds = BrowserStorage.getItem('play_sounds') !== 'no'
23-
} else {
24-
shouldPlaySounds = getTalkConfig('local', 'call', 'play-sounds')
24+
function getInitialShouldPlaySounds() {
25+
const fromStorage = BrowserStorage.getItem('play_sounds')
26+
27+
if (!hasUserAccount && fromStorage) {
28+
return fromStorage !== 'no'
29+
}
30+
31+
if (hasPlaySoundsCapability) {
32+
return playSoundsCapability
2533
}
34+
35+
return fromStorage ? fromStorage !== 'no' : true
2636
}
2737

38+
const shouldPlaySounds = getInitialShouldPlaySounds()
39+
2840
/**
2941
* Preferred version is the .ogg, with .flac fallback if .ogg is not supported (Safari)
3042
*/
@@ -55,7 +67,7 @@ export const useSoundsStore = defineStore('sounds', {
5567
* @param {boolean} value whether sounds should be played
5668
*/
5769
async setShouldPlaySounds(value) {
58-
await setPlaySounds(hasUserAccount, value ? 'yes' : 'no')
70+
await setPlaySounds(hasUserAccount && hasPlaySoundsCapability, value ? 'yes' : 'no')
5971
this.shouldPlaySounds = value
6072
},
6173

0 commit comments

Comments
 (0)