From e2aeb5fc23cc0460a3edef7abf17442a8d2d414e Mon Sep 17 00:00:00 2001 From: tsm-odoo Date: Fri, 15 Jul 2022 09:18:02 +0000 Subject: [PATCH] [IMP] bus, *: introduce multi tab service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *: calendar, survey. In order to ease the PR introducing websockets in Odoo, introduce the multi service. Indeed, the cross tab bus won't be necessary anymore but there will still be a need to elect a main tab: some actions should only be triggered once. To achieve this, the code electing the main tab has been split with the one handling the longpolling. The crosstab bus now relies on the multi tab service to known whether or not the current tab is the main tab. task-2053917 closes odoo/odoo#96174 Related: odoo/enterprise#29573 Signed-off-by: Sébastien Theys (seb) --- addons/bus/static/src/crosstab_bus.js | 157 +------------ addons/bus/static/src/services/bus_service.js | 6 +- .../legacy/make_multi_tab_to_legacy_env.js | 14 ++ .../static/src/services/multi_tab_service.js | 213 ++++++++++++++++++ .../bus/static/tests/assets_watchdog_tests.js | 2 + addons/bus/static/tests/bus_tests.js | 20 +- .../tests/calendar_notification_tests.js | 2 + addons/im_livechat/__manifest__.py | 1 + addons/mail/__manifest__.py | 1 + .../static/tests/helpers/webclient_setup.js | 5 + addons/survey/static/src/js/survey_form.js | 10 +- 11 files changed, 268 insertions(+), 163 deletions(-) create mode 100644 addons/bus/static/src/services/legacy/make_multi_tab_to_legacy_env.js create mode 100644 addons/bus/static/src/services/multi_tab_service.js diff --git a/addons/bus/static/src/crosstab_bus.js b/addons/bus/static/src/crosstab_bus.js index 50e6ba5aeb5..1574e245428 100644 --- a/addons/bus/static/src/crosstab_bus.js +++ b/addons/bus/static/src/crosstab_bus.js @@ -9,34 +9,24 @@ import { Longpolling } from '@bus/longpolling_bus'; * CrossTab * * This is an extension of the longpolling bus with browser cross-tab synchronization. - * It uses a Master/Slaves with Leader Election architecture: - * - a single tab handles longpolling. + * It uses the multiTab service. + * - only the main tab handles longpolling. * - tabs are synchronized by means of the local storage. * * localStorage used keys are: * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.channels : shared public channel list to listen during the poll * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.options : shared options * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.notification : the received notifications from the last poll - * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.tab_list : list of opened tab ids - * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.tab_master : generated id of the master tab * * trigger: * - notification : when a notification is receive from the long polling - * - become_master : when this tab became the master - * - no_longer_master : when this tab is not longer the master (the user swith tab) */ export class CrossTab extends Longpolling { constructor(env, services) { super(env, services); - // constants - this.TAB_HEARTBEAT_PERIOD = 10000; // 10 seconds - this.MASTER_TAB_HEARTBEAT_PERIOD = 1500; // 1.5 seconds - this.HEARTBEAT_OUT_OF_DATE_PERIOD = 5000; // 5 seconds - this.HEARTBEAT_KILL_OLD_PERIOD = 15000; // 15 seconds this.LOCAL_STORAGE_PREFIX = 'bus'; // properties - this._isMasterTab = false; this._isRegistered = false; var now = new Date().getTime(); @@ -49,7 +39,11 @@ export class CrossTab extends Longpolling { this._callLocalStorage('removeItem', 'last'); } this._lastNotificationID = this._callLocalStorage('getItem', 'last', 0); + this._registerWindowUnload(); browser.addEventListener('storage', this._onStorage.bind(this)); + + env.bus.addEventListener('no_longer_main_tab', () => this.stopPolling()); + env.bus.addEventListener('become_main_tab', () => this.startPolling()); } //-------------------------------------------------------------------------- @@ -85,49 +79,24 @@ export class CrossTab extends Longpolling { return this._id; } - /** - * Tells whether this bus is related to the master tab. - * - * @returns {boolean} - */ - isMasterTab() { - return this._isMasterTab; - } - /** * Use the local storage to share the long polling from the master tab. * * @override */ startPolling() { - if (this._isActive === null) { - this._heartbeat = this._heartbeat.bind(this); - } if (!this._isRegistered) { this._isRegistered = true; - var peers = this._callLocalStorage('getItem', 'peers', {}); - peers[this._id] = new Date().getTime(); - this._callLocalStorage('setItem', 'peers', peers); - - this._registerWindowUnload(); - - if (!this._callLocalStorage('getItem', 'master')) { - this._startElection(); - } - - this._heartbeat(); - - if (this._isMasterTab) { + if (this.env.services['multiTab'].isOnMainTab()) { this._callLocalStorage('setItem', 'options', this._options); } else { this._options = this._callLocalStorage('getItem', 'options', this._options); } this._updateChannels(); - return; // startPolling will be called again on tab registration } - if (this._isMasterTab) { + if (this.env.services['multiTab'].isOnMainTab()) { super.startPolling(); } } @@ -180,57 +149,6 @@ export class CrossTab extends Longpolling { return this.LOCAL_STORAGE_PREFIX + '.' + this._sanitizedOrigin + '.' + key; } - /** - * Check all the time (according to the constants) if the tab is the master tab and - * check if it is active. Use the local storage for this checks. - * - * @private - * @see _startElection method - */ - _heartbeat() { - var now = new Date().getTime(); - var heartbeatValue = parseInt(this._callLocalStorage('getItem', 'heartbeat', 0)); - var peers = this._callLocalStorage('getItem', 'peers', {}); - - if ((heartbeatValue + this.HEARTBEAT_OUT_OF_DATE_PERIOD) < now) { - // Heartbeat is out of date. Electing new master - this._startElection(); - heartbeatValue = parseInt(this._callLocalStorage('getItem', 'heartbeat', 0)); - } - - if (this._isMasterTab) { - //walk through all peers and kill old - var cleanedPeers = {}; - for (var peerName in peers) { - if (peers[peerName] + this.HEARTBEAT_KILL_OLD_PERIOD > now) { - cleanedPeers[peerName] = peers[peerName]; - } - } - - if (heartbeatValue !== this.lastHeartbeat) { - // someone else is also master... - // it should not happen, except in some race condition situation. - this._isMasterTab = false; - this.lastHeartbeat = 0; - peers[this._id] = now; - this._callLocalStorage('setItem', 'peers', peers); - this.stopPolling(); - this.trigger('no_longer_master'); - } else { - this.lastHeartbeat = now; - this._callLocalStorage('setItem', 'heartbeat', now); - this._callLocalStorage('setItem', 'peers', cleanedPeers); - } - } else { - //update own heartbeat - peers[this._id] = now; - this._callLocalStorage('setItem', 'peers', peers); - } - - var hbPeriod = this._isMasterTab ? this.MASTER_TAB_HEARTBEAT_PERIOD : this.TAB_HEARTBEAT_PERIOD; - this._heartbeatTimeout = browser.setTimeout(this._heartbeat.bind(this), hbPeriod); - } - /** * @private */ @@ -238,45 +156,6 @@ export class CrossTab extends Longpolling { browser.addEventListener('unload', this._onUnload.bind(this)); } - /** - * Check with the local storage if the current tab is the master tab. - * If this tab became the master, trigger 'become_master' event - * - * @private - */ - _startElection() { - if (this._isMasterTab) { - return; - } - //check who's next - var now = new Date().getTime(); - var peers = this._callLocalStorage('getItem', 'peers', {}); - var heartbeatKillOld = now - this.HEARTBEAT_KILL_OLD_PERIOD; - var newMaster; - for (var peerName in peers) { - //check for dead peers - if (peers[peerName] < heartbeatKillOld) { - continue; - } - newMaster = peerName; - break; - } - - if (newMaster === this._id) { - //we're next in queue. Electing as master - this.lastHeartbeat = now; - this._callLocalStorage('setItem', 'heartbeat', this.lastHeartbeat); - this._callLocalStorage('setItem', 'master', true); - this._isMasterTab = true; - this.startPolling(); - this.trigger('become_master'); - - //removing master peer from queue - delete peers[newMaster]; - this._callLocalStorage('setItem', 'peers', peers); - } - } - /** * Update localstorage channels of with the channels of this tab. * @@ -284,7 +163,7 @@ export class CrossTab extends Longpolling { * @return {boolean} true if the aggregated channels has changed. */ _updateChannels() { - const currentPeerIds = new Set(Object.keys(this._callLocalStorage('getItem', 'peers') || {})); + const currentPeerIds = new Set(this.env.services['multiTab'].getAllTabIds()); const peerChannels = this._callLocalStorage('getItem', 'channels') || {}; const peerChannelsBefore = JSON.stringify(peerChannels); peerChannels[this._id] = Array.from(this._currentTabChannels); @@ -329,7 +208,7 @@ export class CrossTab extends Longpolling { */ _onPoll(notifications) { var notifs = super._onPoll(notifications); - if (this._isMasterTab && notifs.length) { + if (this.env.services['multiTab'].isOnMainTab() && notifs.length) { this._callLocalStorage('setItem', 'last', this._lastNotificationID); this._callLocalStorage('setItem', 'last_ts', new Date().getTime()); this._callLocalStorage('setItem', 'notification', notifs); @@ -351,18 +230,13 @@ export class CrossTab extends Longpolling { var value = JSON.parse(e.newValue); var key = e.key; - if (this._isRegistered && key === this._generateKey('master') && !value) { - //master was unloaded - this._startElection(); - } - // last notification id changed if (key === this._generateKey('last')) { this._lastNotificationID = value || 0; } // notifications changed else if (key === this._generateKey('notification')) { - if (!this._isMasterTab) { + if (!this.env.services['multiTab'].isOnMainTab()) { this.trigger("notification", value); } } @@ -384,16 +258,7 @@ export class CrossTab extends Longpolling { * @private */ _onUnload() { - // unload peer - var peers = this._callLocalStorage('getItem', 'peers') || {}; - delete peers[this._id]; - this._callLocalStorage('setItem', 'peers', peers); this._currentTabChannels.clear(); this._updateChannels(); - - // unload master - if (this._isMasterTab) { - this._callLocalStorage('removeItem', 'master'); - } } } diff --git a/addons/bus/static/src/services/bus_service.js b/addons/bus/static/src/services/bus_service.js index d0497f15cb8..1a94dd10b2b 100644 --- a/addons/bus/static/src/services/bus_service.js +++ b/addons/bus/static/src/services/bus_service.js @@ -28,7 +28,7 @@ export class BusService extends CrossTab { */ sendNotification(options, callback) { if (window.Notification && Notification.permission === "granted") { - if (this.isMasterTab()) { + if (this.env.services['multiTab'].isOnMainTab()) { try { this._sendNativeNotification(options.title, options.message, callback); } catch (error) { @@ -45,7 +45,7 @@ export class BusService extends CrossTab { } } else { this.env.services['notification'].add(options.message, options); - if (this.isMasterTab()) { + if (this.env.services['multiTab'].isOnMainTab()) { this._beep(); } } @@ -121,7 +121,7 @@ export class BusService extends CrossTab { } export const busService = { - dependencies: ['notification', 'presence', 'rpc'], + dependencies: ['notification', 'presence', 'rpc', 'multiTab'], start(env, services) { return new BusService(env, services); }, diff --git a/addons/bus/static/src/services/legacy/make_multi_tab_to_legacy_env.js b/addons/bus/static/src/services/legacy/make_multi_tab_to_legacy_env.js new file mode 100644 index 00000000000..7cdac6ae64a --- /dev/null +++ b/addons/bus/static/src/services/legacy/make_multi_tab_to_legacy_env.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { registry } from '@web/core/registry'; + +export function makeMultiTabToLegacyEnv(legacyEnv) { + return { + dependencies: ['multiTab'], + start(_, { multiTab }) { + legacyEnv.services['multiTab'] = multiTab; + }, + }; +} + +registry.category('wowlToLegacyServiceMappers').add('multi_tab_to_legacy_env', makeMultiTabToLegacyEnv); diff --git a/addons/bus/static/src/services/multi_tab_service.js b/addons/bus/static/src/services/multi_tab_service.js new file mode 100644 index 00000000000..170d3bce543 --- /dev/null +++ b/addons/bus/static/src/services/multi_tab_service.js @@ -0,0 +1,213 @@ +/** @odoo-module **/ + +import { registry } from '@web/core/registry'; +import { browser } from '@web/core/browser/browser'; +import session from 'web.session'; + +/** + * This class uses a Master/Slaves with Leader Election architecture in + * order to keep track of the main tab. Tabs are synchronized thanks to the + * localStorage. + * + * localStorage used keys are: + * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.lastPresenceByTab: + * mapping of tab ids to their last recorded presence. + * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.main : id of the current + * main tab. + * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.heartbeat : last main tab + * heartbeat time. + * + * trigger on env.bus: + * - become_main_tab : when this tab became the main. + * - no_longer_main_tab : when this tab is no longer the main. + */ +export class MultiTab { + constructor(env) { + this.env = env; + + // CONSTANTS + this.TAB_HEARTBEAT_PERIOD = 10000; // 10 seconds + this.MAIN_TAB_HEARTBEAT_PERIOD = 1500; // 1.5 seconds + this.HEARTBEAT_OUT_OF_DATE_PERIOD = 5000; // 5 seconds + this.HEARTBEAT_KILL_OLD_PERIOD = 15000; // 15 seconds + this.LOCAL_STORAGE_PREFIX = 'multiTabService'; + + // PROPERTIES + this._isOnMainTab = false; + this._sanitizedOrigin = session.origin.replace(/:\/{0,2}/g, '_'); + + const now = new Date().getTime(); + this._id = _.uniqueId(this.LOCAL_STORAGE_PREFIX) + ':' + now; + browser.addEventListener('unload', this._onUnload.bind(this)); + browser.addEventListener('storage', this._onStorage.bind(this)); + // REGISTER THIS TAB + const lastPresenceByTab = this._callLocalStorage('getItem', 'lastPresenceByTab', {}); + lastPresenceByTab[this._id] = now; + this._callLocalStorage('setItem', 'lastPresenceByTab', lastPresenceByTab); + + if (!this._callLocalStorage('getItem', 'main')) { + this._startElection(); + } + this._heartbeat(); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + isOnMainTab() { + return this._isOnMainTab; + } + + getAllTabIds() { + return Object.keys(this._callLocalStorage('getItem', 'lastPresenceByTab', {})); + } + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Call browser localStorage. + * + * @private + * @param {string} method (getItem, setItem, removeItem) + * @param {string} key + * @param {any} param + * @returns Result of the called method, parsed. + */ + _callLocalStorage(method, key, param) { + if (method === 'setItem') { + param = JSON.stringify(param); + } + const result = browser.localStorage[method](this._generateKey(key), param); + if (method === 'getItem') { + return result ? JSON.parse(result) : param; + } + } + + /** + * Generates localStorage keys prefixed by LOCAL_STORAGE_PREFIX and + * the sanitized origin, to prevent keys from conflicting when + * several multi tab services co-exist. + * + * @private + * @param {string} key + * @returns Key prefixed with the origin. + */ + _generateKey(key) { + return this.LOCAL_STORAGE_PREFIX + '.' + this._sanitizedOrigin + '.' + key; + } + + /** + * Check all the time (according to the constants) if the tab is the main tab and + * check if it is active. Use the local storage for this checks. + * + * @private + * @see _startElection method + */ + _heartbeat() { + const now = new Date().getTime(); + let heartbeatValue = this._callLocalStorage('getItem', 'heartbeat', 0); + const lastPresenceByTab = this._callLocalStorage('getItem', 'lastPresenceByTab', {}); + if (heartbeatValue + this.HEARTBEAT_OUT_OF_DATE_PERIOD < now) { + // Heartbeat is out of date. Electing new main. + this._startElection(); + heartbeatValue = this._callLocalStorage('getItem', 'heartbeat', 0); + } + if (this._isOnMainTab) { + // Walk through all tabs and kill old ones. + const cleanedTabs = {}; + for (const [tabId, lastPresence] of Object.entries(lastPresenceByTab)) { + if (lastPresence + this.HEARTBEAT_KILL_OLD_PERIOD > now) { + cleanedTabs[tabId] = lastPresence; + } + } + if (heartbeatValue !== this.lastHeartbeat) { + // Someone else is also main... + // It should not happen, except in some race condition situation. + this._isOnMainTab = false; + this.lastHeartbeat = 0; + lastPresenceByTab[this._id] = now; + this._callLocalStorage('setItem', 'lastPresenceByTab', lastPresenceByTab); + this.env.bus.trigger('no_longer_main_tab'); + } else { + this.lastHeartbeat = now; + this._callLocalStorage('setItem', 'heartbeat', now); + this._callLocalStorage('setItem', 'lastPresenceByTab', cleanedTabs); + } + } else { + // Update own heartbeat. + lastPresenceByTab[this._id] = now; + this._callLocalStorage('setItem', 'lastPresenceByTab', lastPresenceByTab); + } + const hbPeriod = this._isOnMainTab ? this.MAIN_TAB_HEARTBEAT_PERIOD : this.TAB_HEARTBEAT_PERIOD; + this._heartbeatTimeout = browser.setTimeout(this._heartbeat.bind(this), hbPeriod); + } + + /** + * Check with the local storage if the current tab is the main tab. + * If this tab became the main, trigger 'become_main_tab' event. + * + * @private + */ + _startElection() { + if (this._isOnMainTab) { + return; + } + // Check who's next. + const now = new Date().getTime(); + const lastPresenceByTab = this._callLocalStorage('getItem', 'lastPresenceByTab', {}); + const heartbeatKillOld = now - this.HEARTBEAT_KILL_OLD_PERIOD; + let newMain; + for (const [tab, lastPresence] of Object.entries(lastPresenceByTab)) { + // Check for dead tabs. + if (lastPresence < heartbeatKillOld) { + continue; + } + newMain = tab; + break; + } + if (newMain === this._id) { + // We're next in queue. Electing as main. + this.lastHeartbeat = now; + this._callLocalStorage('setItem', 'heartbeat', this.lastHeartbeat); + this._callLocalStorage('setItem', 'main', true); + this._isOnMainTab = true; + this.env.bus.trigger('become_main_tab'); + // Removing main peer from queue. + delete lastPresenceByTab[newMain]; + this._callLocalStorage('setItem', 'lastPresenceByTab', lastPresenceByTab); + } + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + _onStorage({ key, newValue }) { + if (key === this._generateKey('main') && !newValue) { + // Main was unloaded. + this._startElection(); + } + } + + _onUnload() { + const lastPresenceByTab = this._callLocalStorage('getItem', 'lastPresenceByTab', {}); + delete lastPresenceByTab[this._id]; + this._callLocalStorage('setItem', 'lastPresenceByTab', lastPresenceByTab); + + // Unload main. + if (this._isOnMainTab) { + this._callLocalStorage('removeItem', 'main'); + } + } +} + +export const multiTabService = { + start(env) { + return new MultiTab(env); + }, +}; + +registry.category('services').add('multiTab', multiTabService); diff --git a/addons/bus/static/tests/assets_watchdog_tests.js b/addons/bus/static/tests/assets_watchdog_tests.js index 853794191f5..5189d8c39e9 100644 --- a/addons/bus/static/tests/assets_watchdog_tests.js +++ b/addons/bus/static/tests/assets_watchdog_tests.js @@ -2,6 +2,7 @@ import { busService } from "@bus/services/bus_service"; import { presenceService } from "@bus/services/presence_service"; +import { multiTabService } from "@bus/services/multi_tab_service"; import { createWebClient } from "@web/../tests/webclient/helpers"; import { assetsWatchdogService } from "@bus/services/assets_watchdog_service"; @@ -17,6 +18,7 @@ QUnit.module("Bus Assets WatchDog", (hooks) => { serviceRegistry.add("assetsWatchdog", assetsWatchdogService); serviceRegistry.add("bus_service", busService); serviceRegistry.add("presence", presenceService); + serviceRegistry.add("multiTab", multiTabService); patchWithCleanup(browser, { setTimeout(fn) { return this._super(fn, 0); diff --git a/addons/bus/static/tests/bus_tests.js b/addons/bus/static/tests/bus_tests.js index 97ac8c15e80..7f22e6361d4 100644 --- a/addons/bus/static/tests/bus_tests.js +++ b/addons/bus/static/tests/bus_tests.js @@ -3,6 +3,7 @@ odoo.define('web.bus_tests', function (require) { var { busService } = require('@bus/services/bus_service'); const { presenceService } = require('@bus/services/presence_service'); +const { multiTabService } = require('@bus/services/multi_tab_service'); var { CrossTab } = require('@bus/crosstab_bus'); var testUtils = require('web.test_utils'); @@ -14,17 +15,18 @@ const { makeTestEnv } = require('@web/../tests/helpers/mock_env'); QUnit.module('Bus', { beforeEach: function () { - const customBusService = { - ...busService, + const customMultiTabService = { + ...multiTabService, start() { - const originalBusService = busService.start(...arguments); - originalBusService.TAB_HEARTBEAT_PERIOD = 10; - originalBusService.MASTER_TAB_HEARTBEAT_PERIOD = 1; - return originalBusService; + const originalMultiTabService = multiTabService.start(...arguments); + originalMultiTabService.TAB_HEARTBEAT_PERIOD = 10; + originalMultiTabService.MAIN_TAB_HEARTBEAT_PERIOD = 1; + return originalMultiTabService; }, }; - registry.category('services').add('bus_service', customBusService); + registry.category('services').add('bus_service', busService); registry.category('services').add('presence', presenceService); + registry.category('services').add('multiTab', customMultiTabService); }, }, function () { QUnit.test('notifications received from the longpolling channel', async function (assert) { @@ -194,7 +196,7 @@ QUnit.module('Bus', { ]); }); - QUnit.test('cross tab bus elect new master on master unload', async function (assert) { + QUnit.test('multi tab service elects new master on master unload', async function (assert) { assert.expect(8); // master @@ -240,7 +242,7 @@ QUnit.module('Bus', { await testUtils.nextTick(); // simulate unloading master - masterEnv.services['bus_service']._onUnload(); + masterEnv.services['multiTab']._onUnload(); pollPromiseSlave.resolve([{ id: 2, diff --git a/addons/calendar/static/tests/calendar_notification_tests.js b/addons/calendar/static/tests/calendar_notification_tests.js index 9bd7af0618e..0bc10ea58a3 100644 --- a/addons/calendar/static/tests/calendar_notification_tests.js +++ b/addons/calendar/static/tests/calendar_notification_tests.js @@ -2,6 +2,7 @@ import { busService } from "@bus/services/bus_service"; import { presenceService } from "@bus/services/presence_service"; +import { multiTabService } from "@bus/services/multi_tab_service"; import { createWebClient } from "@web/../tests/webclient/helpers"; import { calendarNotificationService } from "@calendar/js/services/calendar_notification_service"; @@ -20,6 +21,7 @@ QUnit.module("Calendar Notification", (hooks) => { serviceRegistry.add("calendarNotification", calendarNotificationService); serviceRegistry.add("bus_service", busService); serviceRegistry.add("presence", presenceService); + serviceRegistry.add("multiTab", multiTabService); patchWithCleanup(browser, { setTimeout(fn) { this._super(fn, 0); diff --git a/addons/im_livechat/__manifest__.py b/addons/im_livechat/__manifest__.py index 36d6b6baf3c..2fa02db7da9 100644 --- a/addons/im_livechat/__manifest__.py +++ b/addons/im_livechat/__manifest__.py @@ -185,6 +185,7 @@ Help your customers with this chat, and analyse their feedback. 'bus/static/src/longpolling_bus.js', 'bus/static/src/crosstab_bus.js', 'bus/static/src/services/bus_service.js', + 'bus/static/src/services/multi_tab_service.js', 'mail/static/src/js/utils.js', 'im_livechat/static/src/legacy/public_livechat_history_tracking.js', 'im_livechat/static/src/legacy/public_livechat_chatbot.js', diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index 1af73dac56b..adadf33d654 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -98,6 +98,7 @@ 'bus/static/src/*.js', 'bus/static/src/services/bus_service.js', 'bus/static/src/services/presence_service.js', + 'bus/static/src/services/multi_tab_service.js', 'bus/static/src/services/legacy/make_bus_service_to_legacy_env.js', 'web/static/lib/luxon/luxon.js', 'web/static/src/core/**/*', diff --git a/addons/mail/static/tests/helpers/webclient_setup.js b/addons/mail/static/tests/helpers/webclient_setup.js index c17ad188947..974ccd7f57d 100644 --- a/addons/mail/static/tests/helpers/webclient_setup.js +++ b/addons/mail/static/tests/helpers/webclient_setup.js @@ -1,7 +1,9 @@ /** @odoo-module **/ import { busService } from '@bus/services/bus_service'; +import { multiTabService } from '@bus/services/multi_tab_service'; import { makeBusServiceToLegacyEnv } from '@bus/services/legacy/make_bus_service_to_legacy_env'; +import { makeMultiTabToLegacyEnv } from '@bus/services/legacy/make_multi_tab_to_legacy_env'; import { makeFakePresenceService } from '@bus/../tests/helpers/mock_services'; import { ChatWindowManagerContainer } from '@mail/components/chat_window_manager_container/chat_window_manager_container'; @@ -86,6 +88,7 @@ function setupMessagingServiceRegistries({ }; const customBusService = { + ...busService, start() { const originalService = busService.start(...arguments); Object.assign(originalService, { @@ -106,6 +109,7 @@ function setupMessagingServiceRegistries({ isOdooFocused: () => true, }), systrayService, + multiTab: multiTabService, ...services, }; @@ -114,6 +118,7 @@ function setupMessagingServiceRegistries({ }); registry.category('wowlToLegacyServiceMappers').add('bus_service_to_legacy_env', makeBusServiceToLegacyEnv); + registry.category('wowlToLegacyServiceMappers').add('multi_tab_to_legacy_env', makeMultiTabToLegacyEnv); registry.category('wowlToLegacyServiceMappers').add('messaging_service_to_legacy_env', makeMessagingToLegacyEnv); } diff --git a/addons/survey/static/src/js/survey_form.js b/addons/survey/static/src/js/survey_form.js index d20f0da0462..2890badd11f 100644 --- a/addons/survey/static/src/js/survey_form.js +++ b/addons/survey/static/src/js/survey_form.js @@ -934,10 +934,10 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa this.call('bus_service', 'addChannel', this.options.surveyToken); this.call('bus_service', 'startPolling'); - if (!this._checkIsMasterTab()) { + if (!this._checkisOnMainTab()) { this.shouldReloadMasterTab = true; this.masterTabCheckInterval = setInterval(function () { - if (self._checkIsMasterTab()) { + if (self._checkisOnMainTab()) { clearInterval(self.masterTabCheckInterval); } }, 2000); @@ -1077,10 +1077,10 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa * * @private */ - _checkIsMasterTab: function () { - var isMasterTab = this.call('bus_service', 'isMasterTab'); + _checkisOnMainTab: function () { + var isOnMainTab = this.call('multiTab', 'isOnMainTab'); var $errorModal = this.$('#MasterTabErrorModal'); - if (isMasterTab) { + if (isOnMainTab) { // Force reload the page when survey is ready to be followed, to force restart long polling if (this.shouldReloadMasterTab) { window.location.reload();