Skip to content

Commit 7d86c4d

Browse files
committed
request tab leadership to fetch conversations and update talk-hash every 30 seconds only from one source
Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
1 parent 1f05ddf commit 7d86c4d

8 files changed

Lines changed: 192 additions & 9 deletions

File tree

src/App.vue

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ import Router from './router/router.js'
6464
import BrowserStorage from './services/BrowserStorage.js'
6565
import { EventBus } from './services/EventBus.js'
6666
import { leaveConversationSync } from './services/participantsService.js'
67+
import { talkBroadcastChannel } from './services/talkBroadcastChannel.js'
68+
import { requestTabLeadership } from './utils/requestTabLeadership.js'
6769
import { signalingKill } from './utils/webrtc/index.js'
6870
6971
// Styles
@@ -101,6 +103,7 @@ export default {
101103
defaultPageTitle: false,
102104
loading: false,
103105
isRefreshingCurrentConversation: false,
106+
isCurrentTabLeader: false,
104107
}
105108
},
106109
@@ -442,6 +445,31 @@ export default {
442445
} else {
443446
console.debug('Can not set current user because it\'s a guest')
444447
}
448+
449+
requestTabLeadership().then(() => {
450+
this.isCurrentTabLeader = true
451+
})
452+
453+
talkBroadcastChannel.addEventListener('message', (event) => {
454+
if (!this.isCurrentTabLeader) {
455+
switch (event.data.message) {
456+
case 'talk:update-conversations' : {
457+
this.$store.dispatch('patchConversations', {
458+
conversations: event.data.conversations,
459+
withRemoving: event.data.withRemoving,
460+
})
461+
break
462+
}
463+
case 'update-nextcloud-talk-hash': {
464+
this.$store.dispatch('setNextcloudTalkHash', event.data.hash)
465+
break
466+
}
467+
default: {
468+
break
469+
}
470+
}
471+
}
472+
})
445473
},
446474
447475
async mounted() {

src/components/LeftSidebar/LeftSidebar.spec.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { searchPossibleConversations, searchListedConversations } from '../../se
1414
import { EventBus } from '../../services/EventBus.js'
1515
import storeConfig from '../../store/storeConfig.js'
1616
import { findNcListItems, findNcActionButton } from '../../test-helpers.js'
17+
import { requestTabLeadership } from '../../utils/requestTabLeadership.js'
1718

1819
jest.mock('@nextcloud/initial-state', () => ({
1920
loadState: jest.fn(),
@@ -131,6 +132,8 @@ describe('LeftSidebar.vue', () => {
131132

132133
const wrapper = mountComponent()
133134

135+
await requestTabLeadership()
136+
134137
expect(fetchConversationsAction).toHaveBeenCalledWith(expect.anything(), expect.anything())
135138
expect(conversationsListMock).toHaveBeenCalled()
136139

@@ -157,6 +160,9 @@ describe('LeftSidebar.vue', () => {
157160

158161
test('re-fetches conversations every 30 seconds', async () => {
159162
const wrapper = mountComponent()
163+
164+
await requestTabLeadership()
165+
160166
expect(wrapper.exists()).toBeTruthy()
161167
expect(fetchConversationsAction).toHaveBeenCalled()
162168

@@ -175,6 +181,9 @@ describe('LeftSidebar.vue', () => {
175181

176182
test('re-fetches conversations when receiving bus event', async () => {
177183
const wrapper = mountComponent()
184+
185+
await requestTabLeadership()
186+
178187
expect(wrapper.exists()).toBeTruthy()
179188
expect(fetchConversationsAction).toHaveBeenCalled()
180189

@@ -303,6 +312,8 @@ describe('LeftSidebar.vue', () => {
303312

304313
const wrapper = mountComponent()
305314

315+
await requestTabLeadership()
316+
306317
expect(fetchConversationsAction).toHaveBeenCalledWith(expect.anything(), expect.anything())
307318
expect(conversationsListMock).toHaveBeenCalled()
308319

src/components/LeftSidebar/LeftSidebar.vue

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,9 @@ import {
261261
searchListedConversations,
262262
} from '../../services/conversationsService.js'
263263
import { EventBus } from '../../services/EventBus.js'
264+
import { talkBroadcastChannel } from '../../services/talkBroadcastChannel.js'
264265
import CancelableRequest from '../../utils/cancelableRequest.js'
266+
import { requestTabLeadership } from '../../utils/requestTabLeadership.js'
265267
266268
export default {
267269
@@ -331,6 +333,8 @@ export default {
331333
preventFindingUnread: false,
332334
roomListModifiedBefore: 0,
333335
forceFullRoomListRefreshAfterXLoops: 0,
336+
isFetchingConversations: false,
337+
isCurrentTabLeader: false,
334338
isFocused: false,
335339
isFiltered: null,
336340
}
@@ -414,17 +418,32 @@ export default {
414418
// before updated ones come from server
415419
this.restoreConversations()
416420
417-
this.fetchConversations()
418-
},
419-
420-
mounted() {
421-
// Refreshes the conversations every 30 seconds
422-
this.refreshTimer = window.setInterval(() => {
423-
if (!this.isFetchingConversations) {
421+
requestTabLeadership().then(() => {
422+
this.isCurrentTabLeader = true
423+
this.fetchConversations()
424+
// Refreshes the conversations list every 30 seconds
425+
this.refreshTimer = window.setInterval(() => {
424426
this.fetchConversations()
427+
}, 30000)
428+
})
429+
430+
talkBroadcastChannel.addEventListener('message', (event) => {
431+
if (this.isCurrentTabLeader) {
432+
switch (event.data.message) {
433+
case 'talk:reset-modified-before-count' : {
434+
this.roomListModifiedBefore = 0
435+
this.debounceFetchConversations()
436+
break
437+
}
438+
default: {
439+
break
440+
}
441+
}
425442
}
426-
}, 30000)
443+
})
444+
},
427445
446+
mounted() {
428447
EventBus.$on('should-refresh-conversations', this.handleShouldRefreshConversations)
429448
EventBus.$once('conversations-received', this.handleUnreadMention)
430449
EventBus.$on('route-change', this.onRouteChange)
@@ -622,7 +641,13 @@ export default {
622641
*/
623642
async handleShouldRefreshConversations(options) {
624643
if (options?.all === true) {
625-
this.roomListModifiedBefore = 0
644+
if (this.isCurrentTabLeader) {
645+
this.roomListModifiedBefore = 0
646+
} else {
647+
// Force leader tab to do a full fetch
648+
talkBroadcastChannel.postMessage({ message: 'talk:reset-modified-before-count' })
649+
return
650+
}
626651
} else if (options?.token && options?.properties) {
627652
await this.$store.dispatch('setConversationProperties', {
628653
token: options.token,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
*
3+
* @copyright Copyright (c) 2023 Maksim Sukharev <antreesy.web@gmail.com>
4+
*
5+
* @author Maksim Sukharev <antreesy.web@gmail.com>
6+
*
7+
* @license AGPL-3.0-or-later
8+
*
9+
* This program is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU Affero General Public License as
11+
* published by the Free Software Foundation, either version 3 of the
12+
* License, or (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU Affero General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU Affero General Public License
20+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
*
22+
*/
23+
24+
/**
25+
* Broadcast channel to send messages between active tabs.
26+
*/
27+
const talkBroadcastChannel = new BroadcastChannel('nextcloud:talk')
28+
29+
export {
30+
talkBroadcastChannel,
31+
}

src/store/conversationsStore.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
startCallRecording,
6464
stopCallRecording,
6565
} from '../services/recordingService.js'
66+
import { talkBroadcastChannel } from '../services/talkBroadcastChannel.js'
6667

6768
const DUMMY_CONVERSATION = {
6869
token: '',
@@ -766,6 +767,12 @@ const actions = {
766767

767768
dispatch('cacheConversations')
768769

770+
// Inform other tabs about successful fetch
771+
talkBroadcastChannel.postMessage({
772+
message: 'talk:update-conversations',
773+
conversations: response.data.ocs.data,
774+
withRemoving: modifiedSince === 0,
775+
})
769776
return response
770777
} catch (error) {
771778
if (error?.response) {

src/store/talkHashStore.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
import { showError, TOAST_PERMANENT_TIMEOUT } from '@nextcloud/dialogs'
2424

25+
import { talkBroadcastChannel } from '../services/talkBroadcastChannel.js'
26+
2527
const state = {
2628
initialNextcloudTalkHash: '',
2729
isNextcloudTalkHashDirty: false,
@@ -94,6 +96,12 @@ const actions = {
9496
}
9597

9698
context.dispatch('setNextcloudTalkHash', newTalkCacheBusterHash)
99+
100+
// Inform other tabs about changed hash
101+
talkBroadcastChannel.postMessage({
102+
message: 'update-nextcloud-talk-hash',
103+
hash: newTalkCacheBusterHash,
104+
})
97105
},
98106

99107
checkMaintenanceMode(context, response) {

src/test-setup.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ function myArrayBuffer() {
9898

9999
global.Blob.prototype.arrayBuffer = Blob.prototype.arrayBuffer || myArrayBuffer
100100

101+
global.BroadcastChannel = jest.fn(() => ({
102+
postMessage: jest.fn(),
103+
addEventListener: jest.fn(),
104+
}))
105+
101106
const originalConsoleError = console.error
102107
console.error = function(error) {
103108
if (error?.message?.includes('Could not parse CSS stylesheet')) {

src/utils/requestTabLeadership.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @copyright Copyright (c) 2023 Maksim Sukharev <antreesy.web@gmail.com>
3+
*
4+
* @author Maksim Sukharev <antreesy.web@gmail.com>
5+
* @author Grigorii Shartsev <me@shgk.me>
6+
*
7+
* @license AGPL-3.0-or-later
8+
*
9+
* This program is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU Affero General Public License as
11+
* published by the Free Software Foundation, either version 3 of the
12+
* License, or (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU Affero General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU Affero General Public License
20+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
*
22+
*/
23+
24+
/**
25+
* Assign the first window, which requested that, a 'fetching leader'.
26+
* It will fetch conversations with defined interval and cache to BrowserStorage
27+
* Other will update list with defined interval from the BrowserStorage
28+
* Once 'leader' is closed, next requested tab will be assigned, and so on
29+
*/
30+
function lockTabLeadership() {
31+
return new Promise((resolve) => {
32+
// Locks are supported only with HTTPS protocol,
33+
// so we don't lock anything for another cases
34+
if (navigator.locks === undefined) {
35+
resolve()
36+
return
37+
}
38+
39+
window.navigator.locks.request('talk:leader', () => {
40+
// resolve a promise for the first requested tab
41+
resolve()
42+
43+
// return an infinity promise, resource is blocked until 'leader' tab is closed
44+
return new Promise(() => {
45+
})
46+
})
47+
})
48+
}
49+
50+
const createPromiseSingleton = (promiseFn) => {
51+
let promise = null
52+
53+
return (...args) => {
54+
if (!promise) {
55+
promise = promiseFn(...args)
56+
promise.then(() => {
57+
promise = null
58+
})
59+
}
60+
return promise
61+
}
62+
}
63+
64+
const requestTabLeadership = createPromiseSingleton(lockTabLeadership)
65+
66+
export {
67+
requestTabLeadership,
68+
}

0 commit comments

Comments
 (0)