diff --git a/addons/im_livechat/static/src/core/public_web/upgrade_19_1.js b/addons/im_livechat/static/src/core/public_web/upgrade_19_1.js new file mode 100644 index 0000000000000..6b7560cc44973 --- /dev/null +++ b/addons/im_livechat/static/src/core/public_web/upgrade_19_1.js @@ -0,0 +1,6 @@ +import { upgrade_19_1 } from "@mail/core/common/upgrade/upgrade_19_1"; + +upgrade_19_1.add("discuss_sidebar_category_folded_im_livechat.category_default", { + key: "DiscussAppCategory,im_livechat.category_default:is_open", + value: false, +}); diff --git a/addons/im_livechat/static/tests/upgrade_19_1.test.js b/addons/im_livechat/static/tests/upgrade_19_1.test.js new file mode 100644 index 0000000000000..a71624c00df8a --- /dev/null +++ b/addons/im_livechat/static/tests/upgrade_19_1.test.js @@ -0,0 +1,45 @@ +import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers"; + +import { DiscussAppCategory } from "@mail/discuss/core/public_web/discuss_app/discuss_app_category_model"; +import { makeRecordFieldLocalId } from "@mail/model/misc"; +import { toRawValue } from "@mail/utils/common/local_storage"; +import { contains, openDiscuss, start, startServer } from "@mail/../tests/mail_test_helpers"; + +import { beforeEach, describe, expect, test } from "@odoo/hoot"; + +import { Command, serverState } from "@web/../tests/web_test_helpers"; + +describe.current.tags("desktop"); +defineLivechatModels(); + +beforeEach(() => { + serverState.serverVersion = [99, 9]; // high version so following upgrades keep good working of feature +}); + +test("category 'Livechat' is folded", async () => { + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ name: "Visitor" }); + pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ + livechat_member_type: "agent", + partner_id: serverState.partnerId, + }), + Command.create({ guest_id: guestId, livechat_member_type: "visitor" }), + ], + channel_type: "livechat", + livechat_operator_id: serverState.partnerId, + }); + localStorage.setItem("discuss_sidebar_category_folded_im_livechat.category_default", "true"); + await start(); + await openDiscuss(); + await contains(".o-mail-DiscussSidebarCategory:contains('Livechat') .oi.oi-chevron-right"); + const defaultLivechat_is_open = makeRecordFieldLocalId( + DiscussAppCategory.localId("im_livechat.category_default"), + "is_open" + ); + expect(localStorage.getItem(defaultLivechat_is_open)).toBe(toRawValue(false)); + expect( + localStorage.getItem("discuss_sidebar_category_folded_im_livechat.category_default") + ).toBe(null); +}); diff --git a/addons/mail/static/src/core/common/settings_model.js b/addons/mail/static/src/core/common/settings_model.js index 5cb29f23da3c4..ff72d95de38b1 100644 --- a/addons/mail/static/src/core/common/settings_model.js +++ b/addons/mail/static/src/core/common/settings_model.js @@ -5,18 +5,9 @@ import { fields, Record } from "@mail/model/export"; import { debounce } from "@web/core/utils/timing"; import { rpc } from "@web/core/network/rpc"; -const MESSAGE_SOUND = "mail.user_setting.message_sound"; - export class Settings extends Record { id; - static new() { - const record = super.new(...arguments); - record.onStorage = record.onStorage.bind(record); - browser.addEventListener("storage", record.onStorage); - return record; - } - setup() { super.setup(); this.saveVoiceThresholdDebounce = debounce(() => { @@ -30,11 +21,6 @@ export class Settings extends Record { this._loadLocalSettings(); } - delete() { - browser.removeEventListener("storage", this.onStorage); - super.delete(...arguments); - } - // Notification settings /** * @type {"mentions"|"all"|"no_notif"} @@ -44,33 +30,8 @@ export class Settings extends Record { return this.channel_notifications === false ? "mentions" : this.channel_notifications; }, }); - messageSound = fields.Attr(true, { - compute() { - return browser.localStorage.getItem(MESSAGE_SOUND) !== "false"; - }, - /** @this {import("models").Settings} */ - onUpdate() { - if (this.messageSound) { - browser.localStorage.removeItem(MESSAGE_SOUND); - } else { - browser.localStorage.setItem(MESSAGE_SOUND, "false"); - } - }, - }); - useCallAutoFocus = fields.Attr(true, { - /** @this {import("models").Settings} */ - compute() { - return !browser.localStorage.getItem("mail_user_setting_disable_call_auto_focus"); - }, - /** @this {import("models").Settings} */ - onUpdate() { - if (this.useCallAutoFocus) { - browser.localStorage.removeItem("mail_user_setting_disable_call_auto_focus"); - return; - } - browser.localStorage.setItem("mail_user_setting_disable_call_auto_focus", "true"); - }, - }); + messageSound = fields.Attr(true, { localStorage: true }); + useCallAutoFocus = fields.Attr(true, { localStorage: true }); // Voice settings // DeviceId of the audio input selected by the user @@ -91,19 +52,7 @@ export class Settings extends Record { backgroundBlurAmount = 10; edgeBlurAmount = 10; showOnlyVideo = false; - useBlur = fields.Attr(false, { - compute() { - return browser.localStorage.getItem("mail_user_setting_use_blur") === "true"; - }, - /** @this {import("models").Settings} */ - onUpdate() { - if (this.useBlur) { - browser.localStorage.setItem("mail_user_setting_use_blur", "true"); - } else { - browser.localStorage.removeItem("mail_user_setting_use_blur"); - } - }, - }); + useBlur = fields.Attr(false, { localStorage: true }); blurPerformanceWarning = fields.Attr(false, { compute() { const rtc = this.store.rtc; @@ -388,9 +337,6 @@ export class Settings extends Record { this.backgroundBlurAmount = backgroundBlurAmount ? parseInt(backgroundBlurAmount) : 10; const edgeBlurAmount = browser.localStorage.getItem("mail_user_setting_edge_blur_amount"); this.edgeBlurAmount = edgeBlurAmount ? parseInt(edgeBlurAmount) : 10; - this.useCallAutoFocus = !browser.localStorage.getItem( - "mail_user_setting_disable_call_auto_focus" - ); } /** * @private @@ -425,14 +371,6 @@ export class Settings extends Record { { guest_id: guestId } ); } - onStorage(ev) { - if (ev.key === MESSAGE_SOUND) { - this.messageSound = ev.newValue !== "false"; - } - if (ev.key === "mail_user_setting_use_blur") { - this.useBlur = ev.newValue === "true"; - } - } /** * @private */ diff --git a/addons/mail/static/src/core/common/store_service.js b/addons/mail/static/src/core/common/store_service.js index 5caca0ca753b3..772521f246fd0 100644 --- a/addons/mail/static/src/core/common/store_service.js +++ b/addons/mail/static/src/core/common/store_service.js @@ -730,7 +730,7 @@ export class Store extends BaseStore { Store.register(); export const storeService = { - dependencies: ["bus_service", "im_status", "ui", "popover"], + dependencies: ["bus_service", "im_status", "ui", "popover", "discuss.upgrade"], /** * @param {import("@web/env").OdooEnv} env * @param {import("services").ServiceFactories} services diff --git a/addons/mail/static/src/core/common/upgrade/upgrade_19_1.js b/addons/mail/static/src/core/common/upgrade/upgrade_19_1.js new file mode 100644 index 0000000000000..d5134d7bc850e --- /dev/null +++ b/addons/mail/static/src/core/common/upgrade/upgrade_19_1.js @@ -0,0 +1,30 @@ +import { addUpgrade } from "@mail/core/common/upgrade/upgrade_helpers"; + +/** @typedef {import("@mail/core/common/upgrade/upgrade_helpers").UpgradeFnParam} UpgradeFnParam */ +/** @typedef {import("@mail/core/common/upgrade/upgrade_helpers").UpgradeFnReturn} UpgradeFnReturn */ + +export const upgrade_19_1 = { + /** + * @param {string} key + * @param {((param: UpgradeFnParam) => UpgradeFnReturn)|UpgradeFnReturn} upgrade + * @returns {UpgradeFnReturn} + */ + add(key, upgrade) { + return addUpgrade({ key, version: "19.1", upgrade }); + }, +}; + +upgrade_19_1.add("mail.user_setting.message_sound", { + key: "Settings,undefined:messageSound", + value: false, +}); + +upgrade_19_1.add("mail_user_setting_disable_call_auto_focus", { + key: "Settings,undefined:useCallAutoFocus", + value: false, +}); + +upgrade_19_1.add("mail_user_setting_use_blur", { + key: "Settings,undefined:useBlur", + value: true, +}); diff --git a/addons/mail/static/src/core/common/upgrade/upgrade_helpers.js b/addons/mail/static/src/core/common/upgrade/upgrade_helpers.js new file mode 100644 index 0000000000000..945d5777e79e4 --- /dev/null +++ b/addons/mail/static/src/core/common/upgrade/upgrade_helpers.js @@ -0,0 +1,122 @@ +import { getCurrentLocalStorageVersion, LocalStorageEntry } from "@mail/utils/common/local_storage"; +import { parseVersion } from "@mail/utils/common/misc"; +import { registry } from "@web/core/registry"; + +/** + * @typedef {Object} UpgradeData + * @property {string} version + * @property {string} key + * @property {((param: UpgradeFnParam) => UpgradeFnReturn)|UpgradeFnReturn} upgrade + */ +/** + * @typedef {Object} UpgradeDataWithoutVersion + * @property {string} key + * @property {((param: UpgradeFnParam) => UpgradeFnReturn)|UpgradeFnReturn} upgrade + */ +/** + * @typedef {Object} UpgradeFnParam + * @property {string} key + * @property {any} value + */ +/** + * @typedef {Object} UpgradeFnReturn + * @property {string} key + * @property {any} value + */ + +/** + * Register a `key` and `upgrade` function for a `version` of local storage. + * + * When there's request to upgrade a local storage + * + * Example: + * - we have versions 19.1, 19.3, and 20.0. + * - define upgrade function with 19.1 is to upgrade to 19.1 + * - define ugrade function with 20.0 is to upgrade to 20.0 + * + * Upgrades are applied in sequence, i.e.: + * - if version is 20.0 and local storage data are in version 0, this will apply upgrade to 19.1, then to 19.3 and finally to 20.0. + * - if version is 20.0 and local storage data are in version 19.1, this will apply upgrade to 19.3 and finally to 20.0. + * - if version is 20.0 and local storage data are in version 19.2, this will apply upgrade to 19.3 and finally to 20.0. + * - if version is 20.0 and local storage data are in version 20.0, this will not upgrade data. + * + * @param {UpgradeData} param0 + */ +export function addUpgrade({ version, key, upgrade }) { + /** @type {Map} */ + const map = getUpgradeMap(); + if (!map.has(version)) { + map.set(version, new Map()); + } + map.get(version).set(key, { version, key, upgrade }); +} + +/** @param {string} version */ +export function upgradeFrom(version) { + const orderedUpgradeList = Array.from(getUpgradeMap().entries()) + .filter( + ([v]) => + !parseVersion(v).isLowerThan(version) && + !parseVersion(getCurrentLocalStorageVersion()).isLowerThan(v) + ) + .sort(([v1], [v2]) => (parseVersion(v1).isLowerThan(v2) ? -1 : 1)); + for (const [, keyMap] of orderedUpgradeList) { + for (const upgradeData of keyMap.values()) { + applyUpgrade(upgradeData); + } + } +} + +const upgradeRegistry = registry.category("discuss.upgrade"); +upgradeRegistry.add(null, new Map()); + +/** + * A Map of version numbers to a Map of keys and upgrade functions. + * Basically: + * + * Map: { + * "19.1": { + * key_1: upgradeData_1, + * key_2: upgradeData_2, + * }, + * "19.2": { + * key_3: upgradeData_3, + * key_4: upgradeData_4, + * ... + * }, + * ... + * } + * + * To upgrade a key in a given version, find the key in version + * and applyUpgrade using upgradeData to upgrade with new key, value and version. + * + * @return {Map>} + */ +function getUpgradeMap() { + return upgradeRegistry.get(null); +} + +/** + * Upgrade local storage using `upgrade` data. + * i.e. call `upgradeData.upgrade` to get new key and value. + * + * @param {UpgradeData} upgradeData + */ +function applyUpgrade(upgradeData) { + const oldEntry = new LocalStorageEntry(upgradeData.key); + const parsed = oldEntry.parse(); + const oldValue = parsed ?? oldEntry.get(); + if ( + oldValue === null || + (parsed && parsed.version && parseVersion(upgradeData.version).isLowerThan(parsed.version)) + ) { + return; // could not upgrade (cannot parse or more recent version) + } + const { key, value } = + typeof upgradeData.upgrade === "function" + ? upgradeData.upgrade({ key: upgradeData.key, value: oldValue }) + : upgradeData.upgrade; + oldEntry.remove(); + const newEntry = new LocalStorageEntry(key); + newEntry.set(value, upgradeData.version); +} diff --git a/addons/mail/static/src/core/common/upgrade/upgrade_service.js b/addons/mail/static/src/core/common/upgrade/upgrade_service.js new file mode 100644 index 0000000000000..a39062d9b686c --- /dev/null +++ b/addons/mail/static/src/core/common/upgrade/upgrade_service.js @@ -0,0 +1,20 @@ +import { registry } from "@web/core/registry"; +import { upgradeFrom } from "./upgrade_helpers"; +import { getCurrentLocalStorageVersion, LocalStorageEntry } from "@mail/utils/common/local_storage"; +import { parseVersion } from "@mail/utils/common/misc"; + +export const discussUpgradeService = { + dependencies: [], + start() { + const lse = new LocalStorageEntry("discuss.upgrade.version"); + const parsed = lse.parse(); + const oldVersion = parsed?.version ?? "1.0"; + const currentVersion = getCurrentLocalStorageVersion(); + lse.set(true, currentVersion); + if (parseVersion(oldVersion).isLowerThan(currentVersion)) { + upgradeFrom(oldVersion); + } + }, +}; + +registry.category("services").add("discuss.upgrade", discussUpgradeService); diff --git a/addons/mail/static/src/core/public_web/discuss_app/discuss_app_model.js b/addons/mail/static/src/core/public_web/discuss_app/discuss_app_model.js index 637fa17329106..79efb4f591c8b 100644 --- a/addons/mail/static/src/core/public_web/discuss_app/discuss_app_model.js +++ b/addons/mail/static/src/core/public_web/discuss_app/discuss_app_model.js @@ -1,9 +1,4 @@ import { fields, Record } from "@mail/model/export"; -import { browser } from "@web/core/browser/browser"; - -const NO_MEMBERS_DEFAULT_OPEN_LS = "mail.user_setting.no_members_default_open"; -export const DISCUSS_SIDEBAR_COMPACT_LS = "mail.user_setting.discuss_sidebar_compact"; -export const LAST_DISCUSS_ACTIVE_ID_LS = "mail.user_setting.discuss_last_active_id"; export class DiscussApp extends Record { INSPECTOR_WIDTH = 300; @@ -12,49 +7,9 @@ export class DiscussApp extends Record { activeTab = "notification"; searchTerm = ""; isActive = false; - isMemberPanelOpenByDefault = fields.Attr(true, { - compute() { - return browser.localStorage.getItem(NO_MEMBERS_DEFAULT_OPEN_LS) !== "true"; - }, - /** @this {import("models").DiscussApp} */ - onUpdate() { - if (this.isMemberPanelOpenByDefault) { - browser.localStorage.removeItem(NO_MEMBERS_DEFAULT_OPEN_LS); - } else { - browser.localStorage.setItem(NO_MEMBERS_DEFAULT_OPEN_LS, "true"); - } - }, - }); - isSidebarCompact = fields.Attr(false, { - compute() { - return browser.localStorage.getItem(DISCUSS_SIDEBAR_COMPACT_LS) === "true"; - }, - /** @this {import("models").DiscussApp} */ - onUpdate() { - if (this.isSidebarCompact) { - browser.localStorage.setItem( - DISCUSS_SIDEBAR_COMPACT_LS, - this.isSidebarCompact.toString() - ); - } else { - browser.localStorage.removeItem(DISCUSS_SIDEBAR_COMPACT_LS); - } - }, - }); - lastActiveId = fields.Attr(undefined, { - /** @this {import("models").DiscussApp} */ - compute() { - return browser.localStorage.getItem(LAST_DISCUSS_ACTIVE_ID_LS) ?? undefined; - }, - /** @this {import("models").DiscussApp} */ - onUpdate() { - if (this.lastActiveId) { - browser.localStorage.setItem(LAST_DISCUSS_ACTIVE_ID_LS, this.lastActiveId); - } else { - browser.localStorage.removeItem(LAST_DISCUSS_ACTIVE_ID_LS); - } - }, - }); + isMemberPanelOpenByDefault = fields.Attr(true, { localStorage: true }); + isSidebarCompact = fields.Attr(false, { localStorage: true }); + lastActiveId = fields.Attr(undefined, { localStorage: true }); thread = fields.One("mail.thread", { /** @this {import("models").DiscussApp} */ onUpdate() { @@ -62,27 +17,6 @@ export class DiscussApp extends Record { }, }); hasRestoredThread = false; - - static new() { - const record = super.new(...arguments); - record.onStorage = record.onStorage.bind(record); - browser.addEventListener("storage", record.onStorage); - return record; - } - - delete() { - browser.removeEventListener("storage", this.onStorage); - super.delete(...arguments); - } - - onStorage(ev) { - if (ev.key === DISCUSS_SIDEBAR_COMPACT_LS) { - this.isSidebarCompact = ev.newValue === "true"; - } - if (ev.key === NO_MEMBERS_DEFAULT_OPEN_LS) { - this.isMemberPanelOpenByDefault = ev.newValue !== "true"; - } - } } DiscussApp.register(); diff --git a/addons/mail/static/src/core/public_web/discuss_app/upgrade_19_1.js b/addons/mail/static/src/core/public_web/discuss_app/upgrade_19_1.js new file mode 100644 index 0000000000000..26dd8d5fc2d53 --- /dev/null +++ b/addons/mail/static/src/core/public_web/discuss_app/upgrade_19_1.js @@ -0,0 +1,26 @@ +import { upgrade_19_1 } from "@mail/core/common/upgrade/upgrade_19_1"; + +upgrade_19_1.add("mail.user_setting.no_members_default_open", { + key: "DiscussApp,undefined:isMemberPanelOpenByDefault", + value: false, +}); + +upgrade_19_1.add("mail.user_setting.discuss_sidebar_compact", { + key: "DiscussApp,undefined:isSidebarCompact", + value: true, +}); + +upgrade_19_1.add("mail.user_setting.discuss_last_active_id", ({ value }) => ({ + key: "DiscussApp,undefined:lastActiveId", + value, +})); + +upgrade_19_1.add("discuss_sidebar_category_folded_channels", { + key: "DiscussAppCategory,channels:is_open", + value: false, +}); + +upgrade_19_1.add("discuss_sidebar_category_folded_chats", { + key: "DiscussAppCategory,chats:is_open", + value: false, +}); diff --git a/addons/mail/static/src/discuss/call/common/call_settings.xml b/addons/mail/static/src/discuss/call/common/call_settings.xml index 1477cb8a168d6..ab455181520c8 100644 --- a/addons/mail/static/src/discuss/call/common/call_settings.xml +++ b/addons/mail/static/src/discuss/call/common/call_settings.xml @@ -99,7 +99,7 @@ -
+
Blur video background diff --git a/addons/mail/static/src/discuss/core/public_web/discuss_app/discuss_app_category_model.js b/addons/mail/static/src/discuss/core/public_web/discuss_app/discuss_app_category_model.js index eb7a2a6a2be43..b136edc138e1f 100644 --- a/addons/mail/static/src/discuss/core/public_web/discuss_app/discuss_app_category_model.js +++ b/addons/mail/static/src/discuss/core/public_web/discuss_app/discuss_app_category_model.js @@ -1,30 +1,9 @@ import { compareDatetime } from "@mail/utils/common/misc"; import { fields, Record } from "@mail/model/export"; -import { browser } from "@web/core/browser/browser"; - -export const DISCUSS_SIDEBAR_CATEGORY_FOLDED_LS = "discuss_sidebar_category_folded_"; export class DiscussAppCategory extends Record { static id = "id"; - static new() { - const record = super.new(...arguments); - record.onStorage = record.onStorage.bind(record); - browser.addEventListener("storage", record.onStorage); - return record; - } - - delete() { - browser.removeEventListener("storage", this.onStorage); - super.delete(...arguments); - } - - onStorage(ev) { - if (ev.key === `${DISCUSS_SIDEBAR_CATEGORY_FOLDED_LS}${this.id}`) { - this.is_open = ev.newValue !== "true"; - } - } - /** * @param {import("models").Thread} t1 * @param {import("models").Thread} t2 @@ -70,26 +49,7 @@ export class DiscussAppCategory extends Record { /** @type {number} */ sequence; - is_open = fields.Attr(false, { - /** @this {import("models").DiscussApp} */ - compute() { - return !( - browser.localStorage.getItem(`${DISCUSS_SIDEBAR_CATEGORY_FOLDED_LS}${this.id}`) ?? - false - ); - }, - /** @this {import("models").DiscussApp} */ - onUpdate() { - if (!this.is_open) { - browser.localStorage.setItem( - `${DISCUSS_SIDEBAR_CATEGORY_FOLDED_LS}${this.id}`, - true - ); - } else { - browser.localStorage.removeItem(`${DISCUSS_SIDEBAR_CATEGORY_FOLDED_LS}${this.id}`); - } - }, - }); + is_open = fields.Attr(true, { localStorage: true }); threads = fields.Many("mail.thread", { sort(t1, t2) { diff --git a/addons/mail/static/src/model/make_store.js b/addons/mail/static/src/model/make_store.js index 5b7bdb40b9672..67b687e101376 100644 --- a/addons/mail/static/src/model/make_store.js +++ b/addons/mail/static/src/model/make_store.js @@ -166,9 +166,6 @@ export function makeStore(env, { localRegistry } = {}) { store = record; Record.store = store; } - for (const name of Model._.fields.keys()) { - record._.prepareField(record, name, recordProxy); - } return recordProxy; } }, diff --git a/addons/mail/static/src/model/misc.js b/addons/mail/static/src/model/misc.js index 2a54b90af810e..b64b7bb5903c1 100644 --- a/addons/mail/static/src/model/misc.js +++ b/addons/mail/static/src/model/misc.js @@ -203,4 +203,8 @@ export const fields = { }, }; +export function makeRecordFieldLocalId(recordLocalId, fieldName) { + return `${recordLocalId}:${fieldName}`; +} + export const technicalKeysOnRecords = ["_", "_proxy", "_proxyInternal", "_raw", "env", "Model"]; diff --git a/addons/mail/static/src/model/model_internal.js b/addons/mail/static/src/model/model_internal.js index 77cc814aa3f81..4e9918bc32b78 100644 --- a/addons/mail/static/src/model/model_internal.js +++ b/addons/mail/static/src/model/model_internal.js @@ -1,4 +1,7 @@ +import { toRaw } from "@odoo/owl"; import { ATTR_SYM, MANY_SYM, ONE_SYM } from "./misc"; +import { parseVersion } from "@mail/utils/common/misc"; +import { getCurrentLocalStorageVersion } from "@mail/utils/common/local_storage"; export class ModelInternal { /** @type {Map} */ @@ -13,7 +16,7 @@ export class ModelInternal { fieldsHtml = new Map(); /** @type {Map} */ fieldsTargetModel = new Map(); - /** @type {Map any>} */ + /** @type {Map Function[]>} */ fieldsCompute = new Map(); /** @type {Map} */ fieldsEager = new Map(); @@ -23,12 +26,14 @@ export class ModelInternal { fieldsOnAdd = new Map(); /** @type {Map void>} */ fieldsOnDelete = new Map(); - /** @type {Map void>} */ + /** @type {Map void>>} */ fieldsOnUpdate = new Map(); /** @type {Map number>} */ fieldsSort = new Map(); /** @type {Map} */ fieldsType = new Map(); + /** @type {Set} */ + fieldsLocalStorage = new Set(); /** * Set of field names on the current model that are _inherits fields. * @@ -59,6 +64,43 @@ export class ModelInternal { if (data[MANY_SYM]) { this.fieldsMany.set(fieldName, true); } + if (data.localStorage) { + if (data.compute) { + throw new Error( + `The field "${fieldName}" cannot have "localStorage" and "compute" at the same time. "localStorage" is implicitly a computed field` + ); + } + this.fieldsLocalStorage.add(fieldName); + this.fieldsCompute.set( + fieldName, + /** @this {import("./record").Record}*/ + function fieldLocalStorageCompute() { + const record = toRaw(this)._raw; + const lse = record._.fieldsLocalStorage.get(fieldName); + const parsed = lse.parse(); + const currentOdooVersion = getCurrentLocalStorageVersion(); + if (!parsed || parseVersion(currentOdooVersion).isLowerThan(parsed.version)) { + lse.remove(); + return this[fieldName]; + } + return parsed.value; + } + ); + + this.registerOnUpdate( + fieldName, + /** @this {import("./record").Record}*/ + function fieldLocalStorageOnChange(value) { + const record = toRaw(this)._raw; + const lse = record._.fieldsLocalStorage.get(fieldName); + if (value === record._.fieldsDefault.get(fieldName)) { + lse.remove(); + } else { + lse.set(value); + } + } + ); + } for (const key in data) { const value = data[key]; switch (key) { @@ -101,7 +143,7 @@ export class ModelInternal { break; } case "onUpdate": { - this.fieldsOnUpdate.set(fieldName, value); + this.registerOnUpdate(fieldName, value); break; } case "type": { @@ -111,4 +153,12 @@ export class ModelInternal { } } } + registerOnUpdate(fieldName, onUpdate) { + let onUpdateList = this.fieldsOnUpdate.get(fieldName); + if (!onUpdateList) { + onUpdateList = []; + this.fieldsOnUpdate.set(fieldName, onUpdateList); + } + onUpdateList.push(onUpdate); + } } diff --git a/addons/mail/static/src/model/record.js b/addons/mail/static/src/model/record.js index f53437153ee87..28da984369ffb 100644 --- a/addons/mail/static/src/model/record.js +++ b/addons/mail/static/src/model/record.js @@ -192,6 +192,9 @@ export class Record { const recordProxy = new Model.Class(); const record = toRaw(recordProxy)._raw; Object.assign(record._, { localId: Model.localId(ids) }); + for (const name of Model._.fields.keys()) { + record._.prepareField(record, name, recordProxy); + } Object.assign(recordProxy, { ...ids }); Model.records[record.localId] = recordProxy; if (record.Model.getName() === "Store") { diff --git a/addons/mail/static/src/model/record_internal.js b/addons/mail/static/src/model/record_internal.js index cbb43231e1d3a..f43b3892ba81e 100644 --- a/addons/mail/static/src/model/record_internal.js +++ b/addons/mail/static/src/model/record_internal.js @@ -2,10 +2,17 @@ /** @typedef {import("./record_list").RecordList} RecordList */ import { onChange } from "@mail/utils/common/misc"; -import { IS_DELETED_SYM, IS_RECORD_SYM, isFieldDefinition, isRelation } from "./misc"; +import { + IS_DELETED_SYM, + IS_RECORD_SYM, + isFieldDefinition, + isRelation, + makeRecordFieldLocalId, +} from "./misc"; import { RecordList } from "./record_list"; import { reactive, toRaw } from "@odoo/owl"; import { RecordUses } from "./record_uses"; +import { LocalStorageEntry } from "@mail/utils/common/local_storage"; export class RecordInternal { [IS_RECORD_SYM] = true; @@ -53,16 +60,26 @@ export class RecordInternal { fieldsComputeOnNeed = new Map(); /** @type {Map void>} */ fieldsOnUpdateObserves = new Map(); - /** @type {Map} */ + /** @type {Map} */ fieldsSortProxy2 = new Map(); - /** @type {Map} */ + /** @type {Map} */ fieldsComputeProxy2 = new Map(); + /** @type {Map} */ + fieldsDefault = new Map(); uses = new RecordUses(); updatingAttrs = new Map(); proxyUsed = new Map(); /** @type {string} */ localId; gettingField = false; + /** + * For fields that use local storage, this map contains the "ls" object that eases interactions on the related + * local storage entry. For instance, instead of having to write `browser.localStorage.setItem(EXACT_LOCAL_STORAGE_ENTRY_OF_FIELD, value)`, + * this "ls" object allow to just write the equivalent expression with `ls.set(value)` + * + * @type {Map} + */ + fieldsLocalStorage = new Map(); /** * @param {Record} record @@ -93,6 +110,17 @@ export class RecordInternal { ? record[fieldName].default : record[fieldName]; } + this.fieldsDefault.set(fieldName, record[fieldName]); + // register local storage fields + for (const lsFieldName of Model._.fieldsLocalStorage) { + const { localStorageKeyToRecordFields } = record.store._; + const localStorageKey = makeRecordFieldLocalId(record.localId, lsFieldName); + if (!localStorageKeyToRecordFields.has(localStorageKey)) { + localStorageKeyToRecordFields.set(localStorageKey, new Map()); + } + localStorageKeyToRecordFields.get(localStorageKey).set(record, lsFieldName); + this.fieldsLocalStorage.set(lsFieldName, new LocalStorageEntry(localStorageKey)); + } if (Model._.fieldsCompute.get(fieldName)) { if (!Model._.fieldsEager.get(fieldName)) { onChange(recordProxy, fieldName, () => { @@ -256,7 +284,9 @@ export class RecordInternal { * need reactive (observe is called separately). */ try { - Model._.fieldsOnUpdate.get(fieldName).call(record._proxyInternal); + Model._.fieldsOnUpdate + .get(fieldName) + .forEach((fn) => fn.call(record._proxyInternal, record._proxyInternal[fieldName])); } catch (err) { store.handleError(err); } diff --git a/addons/mail/static/src/model/store.js b/addons/mail/static/src/model/store.js index f699606824cfd..f052613ad710e 100644 --- a/addons/mail/static/src/model/store.js +++ b/addons/mail/static/src/model/store.js @@ -161,6 +161,15 @@ export class Store extends Record { } } } + for (const lsFieldName of record.Model._.fieldsLocalStorage) { + const { localStorageKeyToRecordFields } = record.store._; + const key = record._.fieldsLocalStorage.get(lsFieldName).key; + const lsKeyMap = localStorageKeyToRecordFields.get(key); + lsKeyMap.delete(record); + if (lsKeyMap.size === 0) { + localStorageKeyToRecordFields.delete(key); + } + } deletingRecordsByLocalId.set(record.localId, record); this.recordByLocalId.delete(record.localId); this._.ADD_QUEUE("hard_delete", toRaw(record)); diff --git a/addons/mail/static/src/model/store_internal.js b/addons/mail/static/src/model/store_internal.js index 5f5ca6ca1f4e5..598baa90f076f 100644 --- a/addons/mail/static/src/model/store_internal.js +++ b/addons/mail/static/src/model/store_internal.js @@ -5,9 +5,14 @@ import { htmlEscape, markup, toRaw } from "@odoo/owl"; import { RecordInternal } from "./record_internal"; import { deserializeDate, deserializeDateTime } from "@web/core/l10n/dates"; import { IS_DELETED_SYM, isCommand, isMany } from "./misc"; +import { browser } from "@web/core/browser/browser"; +import { getCurrentLocalStorageVersion, parseRawValue } from "@mail/utils/common/local_storage"; const Markup = markup().constructor; +/** @typedef {string} LocalStorageKey */ +/** @typedef {string} FieldName */ + export class StoreInternal extends RecordInternal { /** @type {Map>} */ FC_QUEUE = new Map(); // field-computes @@ -27,6 +32,37 @@ export class StoreInternal extends RecordInternal { RHD_QUEUE = new Map(); // record-hard-deletes ERRORS = []; UPDATE = 0; + /** + * Map of local storage keys of fields synced with local storage to the record and field name. + * + * @type {Map>} + */ + localStorageKeyToRecordFields = new Map(); + + constructor() { + super(...arguments); + this.onStorage = this.onStorage.bind(this); + browser.addEventListener("storage", this.onStorage); + } + + onStorage(ev) { + const entryMap = this.localStorageKeyToRecordFields.get(ev.key); + if (!entryMap) { + return; + } + for (const [record, fieldName] of entryMap.entries()) { + if (ev.newValue === null) { + record._proxy[fieldName] = record._.fieldsDefault.get(fieldName); + } else { + const parsed = parseRawValue(ev.newValue); + if (!parsed || parsed.version !== getCurrentLocalStorageVersion()) { + record._proxy[fieldName] = record._.fieldsDefault.get(fieldName); + } else { + record._proxy[fieldName] = parsed.value; + } + } + } + } /** * @param {"compute"|"sort"|"onAdd"|"onDelete"|"onUpdate"|"hard_delete"} type @@ -194,6 +230,15 @@ export class StoreInternal extends RecordInternal { */ updateFields(record, vals) { for (const [fieldName, value] of Object.entries(vals)) { + if (record.Model._.fieldsLocalStorage.has(fieldName)) { + // should immediately write in local storage, for immediately correct next compute + const lse = record._.fieldsLocalStorage.get(fieldName); + if (value === record._.fieldsDefault.get(fieldName)) { + lse.remove(); + } else { + lse.set(value); + } + } if (!record.Model._.fields.get(fieldName) || record.Model._.fieldsAttr.get(fieldName)) { this.updateAttr(record, fieldName, value); } else { diff --git a/addons/mail/static/src/utils/common/local_storage.js b/addons/mail/static/src/utils/common/local_storage.js new file mode 100644 index 0000000000000..b951305090880 --- /dev/null +++ b/addons/mail/static/src/utils/common/local_storage.js @@ -0,0 +1,63 @@ +import { browser } from "@web/core/browser/browser"; +import { session } from "@web/session"; + +const LOCAL_STORAGE_SUBVERSION = 0; + +/** + * @typedef {Object} VersionedValue + * @property {any} value + * @property {string} version + */ + +export function getCurrentLocalStorageVersion() { + const [major, minor] = session.server_version_info ?? "1.0"; + return [major, minor, LOCAL_STORAGE_SUBVERSION].join("."); +} + +/** + * Utility class to simplify interaction on local storage with constant local storage key and with versioning. + * When a value is set, this is done as `{ value, version }`. + * Note: The object syntax is necessary to properly handle types, like "false" vs false. + */ +export class LocalStorageEntry { + /** @type {string} */ + key; + constructor(key) { + this.key = key; + } + get() { + return browser.localStorage.getItem(this.key); + } + parse() { + return parseRawValue(this.get()); + } + set(value, version = getCurrentLocalStorageVersion()) { + const parsed = this.parse(); + if (parsed && parsed.value === value && parsed.version === version) { + return; + } + browser.localStorage.setItem(this.key, toRawValue(value, version)); + } + remove() { + if (this.get() === null) { + return; + } + browser.localStorage.removeItem(this.key); + } +} + +export function toRawValue(value, version = getCurrentLocalStorageVersion()) { + return JSON.stringify({ value, version }); +} + +/** + * @param {string} rawValue + * @returns {VersionedValue} + */ +export function parseRawValue(rawValue) { + try { + return JSON.parse(rawValue); + } catch { + // noop + } +} diff --git a/addons/mail/static/tests/core/common/upgrade_19_1.test.js b/addons/mail/static/tests/core/common/upgrade_19_1.test.js new file mode 100644 index 0000000000000..382089b2fbed4 --- /dev/null +++ b/addons/mail/static/tests/core/common/upgrade_19_1.test.js @@ -0,0 +1,144 @@ +import { beforeEach, describe, expect, test } from "@odoo/hoot"; +import { + click, + contains, + defineMailModels, + mockGetMedia, + openDiscuss, + patchUiSize, + SIZES, + start, + startServer, +} from "@mail/../tests/mail_test_helpers"; +import { makeRecordFieldLocalId } from "@mail/model/misc"; +import { toRawValue } from "@mail/utils/common/local_storage"; +import { Settings } from "@mail/core/common/settings_model"; +import { DiscussApp } from "@mail/core/public_web/discuss_app/discuss_app_model"; +import { DiscussAppCategory } from "@mail/discuss/core/public_web/discuss_app/discuss_app_category_model"; +import { getService, serverState } from "@web/../tests/web_test_helpers"; + +describe.current.tags("desktop"); +defineMailModels(); + +beforeEach(() => { + serverState.serverVersion = [99, 9]; // high version so following upgrades keep good working of feature +}); + +test("message sound is 'off'", async () => { + localStorage.setItem("mail.user_setting.message_sound", "false"); + await start(); + getService("action").doAction({ + tag: "mail.discuss_notification_settings_action", + type: "ir.actions.client", + }); + await contains("label:has(h5:contains('Message sound')) input:not(:checked)"); + const messageSoundKey = makeRecordFieldLocalId(Settings.localId(), "messageSound"); + expect(localStorage.getItem(messageSoundKey)).toBe(toRawValue(false)); + expect(localStorage.getItem("mail.user_setting.message_sound")).toBe(null); +}); + +test("use blur is 'on'", async () => { + const pyEnv = await startServer(); + const channelId = pyEnv["discuss.channel"].create({ name: "test" }); + localStorage.setItem("mail_user_setting_use_blur", "true"); + patchUiSize({ size: SIZES.SM }); + await start(); + await openDiscuss(channelId); + // dropdown requires an extra delay before click (because handler is registered in useEffect) + await contains("[title='Open Actions Menu']"); + await click("[title='Open Actions Menu']"); + await click(".o-dropdown-item", { text: "Call Settings" }); + await contains(".o-discuss-CallSettings"); + await contains( + ".o-discuss-CallSettings-item:has(label:contains('Blur video background')) input:checked" + ); + const useBlurKey = makeRecordFieldLocalId(Settings.localId(), "useBlur"); + expect(localStorage.getItem(useBlurKey)).toBe(toRawValue(true)); + expect(localStorage.getItem("mail_user_setting_use_blur")).toBe(null); +}); + +test("member default open is 'off'", async () => { + const pyEnv = await startServer(); + const channelId = pyEnv["discuss.channel"].create({ name: "test" }); + localStorage.setItem("mail.user_setting.no_members_default_open", "true"); + await start(); + await openDiscuss(channelId); + await contains(".o-mail-Thread:contains('Welcome to #test')"); + await contains(".o-mail-ActionList-button[title='Members']"); + await contains(".o-mail-ActionList-button[title='Members']:not(.active)"); + const isMemberPanelOpenByDefaultKey = makeRecordFieldLocalId( + DiscussApp.localId(), + "isMemberPanelOpenByDefault" + ); + expect(localStorage.getItem(isMemberPanelOpenByDefaultKey)).toBe(toRawValue(false)); + expect(localStorage.getItem("mail.user_setting.no_members_default_open")).toBe(null); + await click(".o-mail-ActionList-button[title='Members']"); + await contains(".o-mail-ActionList-button[title='Members'].active"); // just to validate .active is correct selector + expect(localStorage.getItem(isMemberPanelOpenByDefaultKey)).toBe(null); +}); + +test("sidebar compact is 'on'", async () => { + localStorage.setItem("mail.user_setting.discuss_sidebar_compact", "true"); + await start(); + await openDiscuss(); + await contains(".o-mail-DiscussSidebar.o-compact"); + const isSidebarCompact = makeRecordFieldLocalId(DiscussApp.localId(), "isSidebarCompact"); + expect(localStorage.getItem(isSidebarCompact)).toBe(toRawValue(true)); + expect(localStorage.getItem("mail.user_setting.discuss_sidebar_compact")).toBe(null); +}); + +test("category 'Channels' is folded", async () => { + localStorage.setItem("discuss_sidebar_category_folded_channels", "true"); + await start(); + await openDiscuss(); + await contains(".o-mail-DiscussSidebarCategory:contains('Channels') .oi.oi-chevron-right"); + const channels_is_open = makeRecordFieldLocalId( + DiscussAppCategory.localId("channels"), + "is_open" + ); + expect(localStorage.getItem(channels_is_open)).toBe(toRawValue(false)); + expect(localStorage.getItem("discuss_sidebar_category_folded_channels")).toBe(null); +}); + +test("category 'Direct messages' is folded", async () => { + localStorage.setItem("discuss_sidebar_category_folded_chats", "true"); + await start(); + await openDiscuss(); + await contains( + ".o-mail-DiscussSidebarCategory:contains('Direct messages') .oi.oi-chevron-right" + ); + const chats_is_open = makeRecordFieldLocalId(DiscussAppCategory.localId("chats"), "is_open"); + expect(localStorage.getItem(chats_is_open)).toBe(toRawValue(false)); + expect(localStorage.getItem("discuss_sidebar_category_folded_chats")).toBe(null); +}); + +test("last active id of discuss app", async () => { + const pyEnv = await startServer(); + const channelId = pyEnv["discuss.channel"].create({ name: "test" }); + localStorage.setItem( + "mail.user_setting.discuss_last_active_id", + `discuss.channel_${channelId}` + ); + await start(); + await openDiscuss(); + await contains(".o-mail-Thread:contains('Welcome to #test')"); + const lastActiveId = makeRecordFieldLocalId(DiscussApp.localId(), "lastActiveId"); + expect(localStorage.getItem(lastActiveId)).toBe(toRawValue(`discuss.channel_${channelId}`)); + expect(localStorage.getItem("mail.user_setting.discuss_last_active_id")).toBe(null); +}); + +test("call auto focus is 'off", async () => { + mockGetMedia(); + const pyEnv = await startServer(); + const channelId = pyEnv["discuss.channel"].create({ name: "test" }); + localStorage.setItem("mail_user_setting_disable_call_auto_focus", "true"); + await start(); + await openDiscuss(channelId); + await click("[title='Start Call']"); + await click(".o-discuss-CallActionList [title='More']"); + await contains(".o-dropdown-item:contains('Autofocus speaker')"); + // correct local storage values + const useCallAutoFocusKey = makeRecordFieldLocalId(Settings.localId(), "useCallAutoFocus"); + expect(localStorage.getItem(useCallAutoFocusKey)).toBe(toRawValue(false)); + expect(localStorage.getItem("mail_user_setting_disable_call_auto_focus")).toBe(null); +}); diff --git a/addons/mail/static/tests/core/record.test.js b/addons/mail/static/tests/core/record.test.js index 75619caef7782..d1ac16f77a329 100644 --- a/addons/mail/static/tests/core/record.test.js +++ b/addons/mail/static/tests/core/record.test.js @@ -1,10 +1,11 @@ +import { toRawValue } from "@mail/utils/common/local_storage"; import { defineMailModels, start as start2 } from "@mail/../tests/mail_test_helpers"; import { afterEach, beforeEach, describe, expect, test } from "@odoo/hoot"; import { markup, reactive, toRaw } from "@odoo/owl"; import { mockService } from "@web/../tests/web_test_helpers"; import { Record, Store, makeStore } from "@mail/model/export"; -import { AND, fields } from "@mail/model/misc"; +import { AND, fields, makeRecordFieldLocalId } from "@mail/model/misc"; import { serializeDateTime } from "@web/core/l10n/dates"; import { registry } from "@web/core/registry"; @@ -1465,3 +1466,33 @@ test("accessing fields through empty _inherits parent returns empty values", asy expect(user.name).toBe(undefined); expect(user.partners).toHaveLength(0); }); + +test("Fields with { localStorage: true } are saved in local storage", async () => { + (class Message extends Record { + static id = "id"; + id; + body = fields.Attr("", { localStorage: true }); + }).register(localRegistry); + const store = await start(); + const message = store.Message.insert(1); + const bodyLocalId = makeRecordFieldLocalId(message.localId, "body"); + expect(localStorage.getItem(bodyLocalId)).toBe(null); + message.body = "test"; + expect(localStorage.getItem(bodyLocalId)).toBe(toRawValue("test")); + message.body = "test2"; + expect(localStorage.getItem(bodyLocalId)).toBe(toRawValue("test2")); +}); + +test("Fields with { localStorage: true } are restored from local storage", async () => { + class Message extends Record { + static id = "id"; + id; + body = fields.Attr("", { localStorage: true }); + } + Message.register(localRegistry); + const bodyLocalId = makeRecordFieldLocalId(Message.localId(1), "body"); + localStorage.setItem(bodyLocalId, toRawValue("test")); + const store = await start(); + const message = store.Message.insert(1); + expect(message.body).toBe("test"); +}); diff --git a/addons/mail/static/tests/discuss/call/call_settings_menu.test.js b/addons/mail/static/tests/discuss/call/call_settings_menu.test.js index e9a9060044eaf..2609e23e61519 100644 --- a/addons/mail/static/tests/discuss/call/call_settings_menu.test.js +++ b/addons/mail/static/tests/discuss/call/call_settings_menu.test.js @@ -9,6 +9,9 @@ import { start, startServer, } from "@mail/../tests/mail_test_helpers"; +import { toRawValue } from "@mail/utils/common/local_storage"; +import { Settings } from "@mail/core/common/settings_model"; +import { makeRecordFieldLocalId } from "@mail/model/misc"; import { describe, test, expect } from "@odoo/hoot"; import { advanceTime } from "@odoo/hoot-mock"; import { patchWithCleanup } from "@web/../tests/web_test_helpers"; @@ -94,7 +97,8 @@ test("local storage for call settings", async () => { localStorage.setItem("mail_user_setting_background_blur_amount", "3"); localStorage.setItem("mail_user_setting_edge_blur_amount", "5"); localStorage.setItem("mail_user_setting_show_only_video", "true"); - localStorage.setItem("mail_user_setting_use_blur", "true"); + const useBlurLocalStorageKey = makeRecordFieldLocalId(Settings.localId(), "useBlur"); + localStorage.setItem(useBlurLocalStorageKey, toRawValue(true)); patchWithCleanup(localStorage, { setItem(key, value) { if (key.startsWith("mail_user_setting")) { @@ -118,12 +122,9 @@ test("local storage for call settings", async () => { // testing save to local storage await click("input[title='Show video participants only']"); - await expect.waitForSteps([ - "mail_user_setting_use_blur: true", - "mail_user_setting_show_only_video: false", - ]); + await expect.waitForSteps(["mail_user_setting_show_only_video: false"]); await click("input[title='Blur video background']"); - expect(localStorage.getItem("mail_user_setting_use_blur")).toBe(null); + expect(localStorage.getItem(useBlurLocalStorageKey)).toBe(null); await editInput(document.body, ".o-Discuss-CallSettings-thresholdInput", 0.3); await advanceTime(2000); // threshold setting debounce timer await expect.waitForSteps(["mail_user_setting_voice_threshold: 0.3"]); diff --git a/addons/mail/static/tests/discuss_app/discuss.test.js b/addons/mail/static/tests/discuss_app/discuss.test.js index d72b7df99c636..0c6043163c93d 100644 --- a/addons/mail/static/tests/discuss_app/discuss.test.js +++ b/addons/mail/static/tests/discuss_app/discuss.test.js @@ -1,7 +1,7 @@ import { waitUntilSubscribe } from "@bus/../tests/bus_test_helpers"; import { OutOfFocusService } from "@mail/core/common/out_of_focus_service"; -import { LAST_DISCUSS_ACTIVE_ID_LS } from "@mail/core/public_web/discuss_app/discuss_app_model"; +import { DiscussApp } from "@mail/core/public_web/discuss_app/discuss_app_model"; import { click, contains, @@ -39,6 +39,9 @@ import { serverState, withUser, } from "@web/../tests/web_test_helpers"; +import { makeRecordFieldLocalId } from "@mail/model/misc"; +import { Settings } from "@mail/core/common/settings_model"; +import { toRawValue } from "@mail/utils/common/local_storage"; describe.current.tags("desktop"); defineMailModels(); @@ -597,7 +600,11 @@ test("sidebar: Inbox should have icon", async () => { test("last discuss conversation is remembered", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General" }); - browser.localStorage.setItem(LAST_DISCUSS_ACTIVE_ID_LS, "discuss.channel_" + channelId); + const LAST_DISCUSS_ACTIVE_ID_LS = makeRecordFieldLocalId(DiscussApp.localId(), "lastActiveId"); + browser.localStorage.setItem( + LAST_DISCUSS_ACTIVE_ID_LS, + toRawValue(`${"discuss.channel_" + channelId}`) + ); await start(); await openDiscuss(); await contains('[role="heading"]', { text: "Welcome to #General!" }); @@ -622,6 +629,7 @@ test("channel deletion fallbacks to no conversation selected", async () => { test("sidebar: change active", async () => { const pyEnv = await startServer(); pyEnv["res.users"].write(serverState.userId, { notification_type: "inbox" }); + const LAST_DISCUSS_ACTIVE_ID_LS = makeRecordFieldLocalId(DiscussApp.localId(), "lastActiveId"); await start(); await openDiscuss("mail.box_inbox"); await contains("button.o-active", { text: "Inbox" }); @@ -629,7 +637,9 @@ test("sidebar: change active", async () => { await click("button", { text: "Starred messages" }); await contains("button:not(.o-active)", { text: "Inbox" }); await contains("button.o-active", { text: "Starred messages" }); - expect(browser.localStorage.getItem(LAST_DISCUSS_ACTIVE_ID_LS)).toBe("mail.box_starred"); + expect(browser.localStorage.getItem(LAST_DISCUSS_ACTIVE_ID_LS)).toBe( + toRawValue("mail.box_starred") + ); }); test("sidebar: basic channel rendering", async () => { @@ -1531,7 +1541,8 @@ test("message sound on receiving new message based on user preferences", async ( await waitFor(".o-mail-ChatBubble .badge:contains(1)", { timeout: 3000 }); await expect.waitForSteps(["sound:new-message"]); // simulate message sound settings turned off - browser.localStorage.setItem("mail.user_setting.message_sound", false); + const MESSAGE_SOUND_LS = makeRecordFieldLocalId(Settings.localId(), "messageSound"); + browser.localStorage.setItem(MESSAGE_SOUND_LS, toRawValue(false)); await animationFrame(); await withUser(userId, () => rpc("/mail/message/post", { @@ -1546,8 +1557,7 @@ test("message sound on receiving new message based on user preferences", async ( await waitFor(".o-mail-ChatBubble .badge:contains(2)", { timeout: 3000 }); expect.verifySteps([]); // simulate message sound settings turned on - browser.localStorage.setItem("mail.user_setting.message_sound", null); - browser.localStorage.removeItem("mail.user_setting.message_sound"); + browser.localStorage.removeItem(MESSAGE_SOUND_LS); await withUser(userId, () => rpc("/mail/message/post", { post_data: { diff --git a/addons/mail/static/tests/discuss_app/sidebar.test.js b/addons/mail/static/tests/discuss_app/sidebar.test.js index cc986682d3261..c1a09e3312ae1 100644 --- a/addons/mail/static/tests/discuss_app/sidebar.test.js +++ b/addons/mail/static/tests/discuss_app/sidebar.test.js @@ -14,7 +14,9 @@ import { triggerHotkey, waitStoreFetch, } from "@mail/../tests/mail_test_helpers"; -import { DISCUSS_SIDEBAR_COMPACT_LS } from "@mail/core/public_web/discuss_app/discuss_app_model"; +import { toRawValue } from "@mail/utils/common/local_storage"; +import { DiscussApp } from "@mail/core/public_web/discuss_app/discuss_app_model"; +import { makeRecordFieldLocalId } from "@mail/model/misc"; import { describe, expect, test } from "@odoo/hoot"; import { animationFrame, drag, press, queryFirst } from "@odoo/hoot-dom"; import { Deferred, mockDate } from "@odoo/hoot-mock"; @@ -1021,7 +1023,11 @@ test("Can make sidebar smaller", async () => { }); test("Sidebar compact is locally persistent (saved in local storage)", async () => { - browser.localStorage.setItem(DISCUSS_SIDEBAR_COMPACT_LS, true); + const DISCUSS_SIDEBAR_COMPACT_LS = makeRecordFieldLocalId( + DiscussApp.localId(), + "isSidebarCompact" + ); + browser.localStorage.setItem(DISCUSS_SIDEBAR_COMPACT_LS, toRawValue(true)); await start(); await openDiscuss(); await contains(".o-mail-DiscussSidebar.o-compact"); @@ -1038,7 +1044,7 @@ test("Sidebar compact is locally persistent (saved in local storage)", async () position: { x: 0 }, }); await contains(".o-mail-DiscussSidebar.o-compact"); - expect(browser.localStorage.getItem(DISCUSS_SIDEBAR_COMPACT_LS)).toBe("true"); + expect(browser.localStorage.getItem(DISCUSS_SIDEBAR_COMPACT_LS)).toBe(toRawValue(true)); }); test("Sidebar compact is crosstab synced", async () => { diff --git a/addons/mail/static/tests/mail_test_helpers.js b/addons/mail/static/tests/mail_test_helpers.js index 1f1cbbd517fd3..f334cafd7752a 100644 --- a/addons/mail/static/tests/mail_test_helpers.js +++ b/addons/mail/static/tests/mail_test_helpers.js @@ -78,7 +78,9 @@ import { ResUsersSettingsVolumes } from "./mock_server/mock_models/res_users_set import { Network } from "@mail/discuss/call/common/rtc_service"; import { UPDATE_EVENT } from "@mail/discuss/call/common/peer_to_peer"; import { SoundEffects } from "@mail/core/common/sound_effects_service"; -import { DISCUSS_SIDEBAR_CATEGORY_FOLDED_LS } from "@mail/discuss/core/public_web/discuss_app/discuss_app_category_model"; +import { DiscussAppCategory } from "@mail/discuss/core/public_web/discuss_app/discuss_app_category_model"; +import { makeRecordFieldLocalId } from "@mail/model/misc"; +import { LocalStorageEntry } from "@mail/utils/common/local_storage"; export * from "./mail_test_helpers_contains"; @@ -771,15 +773,19 @@ export function setupChatHub({ opened = [], folded = [] } = {}) { } export function setDiscussSidebarCategoryFoldState(categoryId, val) { + const localId = DiscussAppCategory.localId(categoryId); + const lse = new LocalStorageEntry(makeRecordFieldLocalId(localId, "is_open")); if (val) { - localStorage.setItem(`${DISCUSS_SIDEBAR_CATEGORY_FOLDED_LS}${categoryId}`, val); + lse.set(!val); } else { - localStorage.removeItem(`${DISCUSS_SIDEBAR_CATEGORY_FOLDED_LS}${categoryId}`); + lse.remove(); } } export function isDiscussSidebarCategoryFolded(categoryId) { - return localStorage.getItem(`${DISCUSS_SIDEBAR_CATEGORY_FOLDED_LS}${categoryId}`) === "true"; + const localId = DiscussAppCategory.localId(categoryId); + const lse = new LocalStorageEntry(makeRecordFieldLocalId(localId, "is_open")); + return !(lse.parse()?.value ?? true); } export function assertChatHub({ opened = [], folded = [] }) {