Skip to content

Commit 12aa6aa

Browse files
committed
[IMP] mail, im_livechat: easy local storage field + upgrade
Before this commit, having a record field saved in local storage and synced among browser tabs was cumbersome. Roughly this was defined as follow: ```js class Settings extends Record { static new(...args) { const settings = super.new(...args); settings.onStorage = settings.onStorage.bind(settings); browser.addEventListener("storage", onStorage); return settings; } delete() { super.delete(); browser.removeEventListener("storage", onStorage); } onStorage(ev) { if (ev.key === USE_BLUR_KEY) { this.useBlur = ev.newValue === "true"; } } useBlur = fields.Attr(false, { compute: () => browser.localStorage.getItem(USE_BLUR_KEY) === "true", onUpdate: () => this.useBlur ? browser.localStorage.seItem(USE_BLUR_KEY, "true") : browser.localStorage.removeItem(USE_BLUR_KEY), }); } ``` This commit simplifies drastically how to define fields that are saved and synced to local storage: ```js class Settings extends Record { useBlur = fields.Attr(false, { localStorage: true }); } ``` The write in local storage is made only when the field value differs from default: - if value is same as default value, then remove from local storage - if value is different than default value, write current value in local storage This commit also introduces local storage upgrade scripts. Since local storage keys and values are expected to change but we want to keep user preferences, these scripts help upgrade old local storage values to newer ones. To define an upgrade script: ```js import { addUpgrade } from "./upgrade_helpers"; addUpgrade({ version: NEW_VERSION, key: OLD_KEY, upgrade: ({ value: oldValue }) => ({ key: NEW_KEY, value: NEW_VALUE, }), }); ``` This upgrades `OLD_KEY` local storage entry to `NEW_KEY` local storage entry with `NEW_VALUE` with `NEW_VERSION`. `NEW_VERSION` matches the targeted odoo version, as a string, in the format `MAJOR.MINOR`, e.g. "19.1" and "20.0". Upgrades are executed from oldest to newest version. Range is based on version in local storage at bottom and curent odoo version at the top. For example, let's say we have upgrades for 19.1, 19.3 and 20.0: - current odoo version is 19.1 and local storage has no version, then upgrade using 19.1 scripts. - current odoo version is 20.0 and local storage has version 19.2, then upgrade using 19.3 scripts then 20.0 scripts. - current odoo version is 17.0 and local storage has version 20.0, don't upgrade. Part of Task-5003012
1 parent 2bb3960 commit 12aa6aa

File tree

25 files changed

+684
-207
lines changed

25 files changed

+684
-207
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { upgrade_19_1 } from "@mail/core/common/upgrade/upgrade_19_1";
2+
3+
upgrade_19_1.add("discuss_sidebar_category_folded_im_livechat.category_default", {
4+
key: "DiscussAppCategory,im_livechat.category_default:is_open",
5+
value: false,
6+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers";
2+
3+
import { DiscussAppCategory } from "@mail/discuss/core/public_web/discuss_app/discuss_app_category_model";
4+
import { makeRecordFieldLocalId } from "@mail/model/misc";
5+
import { toRawValue } from "@mail/utils/common/local_storage";
6+
import { contains, openDiscuss, start, startServer } from "@mail/../tests/mail_test_helpers";
7+
8+
import { beforeEach, describe, expect, test } from "@odoo/hoot";
9+
10+
import { Command, serverState } from "@web/../tests/web_test_helpers";
11+
12+
describe.current.tags("desktop");
13+
defineLivechatModels();
14+
15+
beforeEach(() => {
16+
serverState.serverVersion = [99, 9]; // high version so following upgrades keep good working of feature
17+
});
18+
19+
test("category 'Livechat' is folded", async () => {
20+
const pyEnv = await startServer();
21+
const guestId = pyEnv["mail.guest"].create({ name: "Visitor" });
22+
pyEnv["discuss.channel"].create({
23+
channel_member_ids: [
24+
Command.create({
25+
livechat_member_type: "agent",
26+
partner_id: serverState.partnerId,
27+
}),
28+
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
29+
],
30+
channel_type: "livechat",
31+
livechat_operator_id: serverState.partnerId,
32+
});
33+
localStorage.setItem("discuss_sidebar_category_folded_im_livechat.category_default", "true");
34+
await start();
35+
await openDiscuss();
36+
await contains(".o-mail-DiscussSidebarCategory:contains('Livechat') .oi.oi-chevron-right");
37+
const defaultLivechat_is_open = makeRecordFieldLocalId(
38+
DiscussAppCategory.localId("im_livechat.category_default"),
39+
"is_open"
40+
);
41+
expect(localStorage.getItem(defaultLivechat_is_open)).toBe(toRawValue(false));
42+
expect(
43+
localStorage.getItem("discuss_sidebar_category_folded_im_livechat.category_default")
44+
).toBe(null);
45+
});

addons/mail/static/src/core/common/settings_model.js

Lines changed: 3 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,9 @@ import { fields, Record } from "@mail/model/export";
55
import { debounce } from "@web/core/utils/timing";
66
import { rpc } from "@web/core/network/rpc";
77

8-
const MESSAGE_SOUND = "mail.user_setting.message_sound";
9-
108
export class Settings extends Record {
119
id;
1210

13-
static new() {
14-
const record = super.new(...arguments);
15-
record.onStorage = record.onStorage.bind(record);
16-
browser.addEventListener("storage", record.onStorage);
17-
return record;
18-
}
19-
2011
setup() {
2112
super.setup();
2213
this.saveVoiceThresholdDebounce = debounce(() => {
@@ -30,11 +21,6 @@ export class Settings extends Record {
3021
this._loadLocalSettings();
3122
}
3223

33-
delete() {
34-
browser.removeEventListener("storage", this.onStorage);
35-
super.delete(...arguments);
36-
}
37-
3824
// Notification settings
3925
/**
4026
* @type {"mentions"|"all"|"no_notif"}
@@ -44,33 +30,8 @@ export class Settings extends Record {
4430
return this.channel_notifications === false ? "mentions" : this.channel_notifications;
4531
},
4632
});
47-
messageSound = fields.Attr(true, {
48-
compute() {
49-
return browser.localStorage.getItem(MESSAGE_SOUND) !== "false";
50-
},
51-
/** @this {import("models").Settings} */
52-
onUpdate() {
53-
if (this.messageSound) {
54-
browser.localStorage.removeItem(MESSAGE_SOUND);
55-
} else {
56-
browser.localStorage.setItem(MESSAGE_SOUND, "false");
57-
}
58-
},
59-
});
60-
useCallAutoFocus = fields.Attr(true, {
61-
/** @this {import("models").Settings} */
62-
compute() {
63-
return !browser.localStorage.getItem("mail_user_setting_disable_call_auto_focus");
64-
},
65-
/** @this {import("models").Settings} */
66-
onUpdate() {
67-
if (this.useCallAutoFocus) {
68-
browser.localStorage.removeItem("mail_user_setting_disable_call_auto_focus");
69-
return;
70-
}
71-
browser.localStorage.setItem("mail_user_setting_disable_call_auto_focus", "true");
72-
},
73-
});
33+
messageSound = fields.Attr(true, { localStorage: true });
34+
useCallAutoFocus = fields.Attr(true, { localStorage: true });
7435

7536
// Voice settings
7637
// DeviceId of the audio input selected by the user
@@ -91,19 +52,7 @@ export class Settings extends Record {
9152
backgroundBlurAmount = 10;
9253
edgeBlurAmount = 10;
9354
showOnlyVideo = false;
94-
useBlur = fields.Attr(false, {
95-
compute() {
96-
return browser.localStorage.getItem("mail_user_setting_use_blur") === "true";
97-
},
98-
/** @this {import("models").Settings} */
99-
onUpdate() {
100-
if (this.useBlur) {
101-
browser.localStorage.setItem("mail_user_setting_use_blur", "true");
102-
} else {
103-
browser.localStorage.removeItem("mail_user_setting_use_blur");
104-
}
105-
},
106-
});
55+
useBlur = fields.Attr(false, { localStorage: true });
10756
blurPerformanceWarning = fields.Attr(false, {
10857
compute() {
10958
const rtc = this.store.rtc;
@@ -388,9 +337,6 @@ export class Settings extends Record {
388337
this.backgroundBlurAmount = backgroundBlurAmount ? parseInt(backgroundBlurAmount) : 10;
389338
const edgeBlurAmount = browser.localStorage.getItem("mail_user_setting_edge_blur_amount");
390339
this.edgeBlurAmount = edgeBlurAmount ? parseInt(edgeBlurAmount) : 10;
391-
this.useCallAutoFocus = !browser.localStorage.getItem(
392-
"mail_user_setting_disable_call_auto_focus"
393-
);
394340
}
395341
/**
396342
* @private
@@ -425,14 +371,6 @@ export class Settings extends Record {
425371
{ guest_id: guestId }
426372
);
427373
}
428-
onStorage(ev) {
429-
if (ev.key === MESSAGE_SOUND) {
430-
this.messageSound = ev.newValue !== "false";
431-
}
432-
if (ev.key === "mail_user_setting_use_blur") {
433-
this.useBlur = ev.newValue === "true";
434-
}
435-
}
436374
/**
437375
* @private
438376
*/

addons/mail/static/src/core/common/store_service.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,7 @@ export class Store extends BaseStore {
730730
Store.register();
731731

732732
export const storeService = {
733-
dependencies: ["bus_service", "im_status", "ui", "popover"],
733+
dependencies: ["bus_service", "im_status", "ui", "popover", "discuss.upgrade"],
734734
/**
735735
* @param {import("@web/env").OdooEnv} env
736736
* @param {import("services").ServiceFactories} services
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { addUpgrade } from "@mail/core/common/upgrade/upgrade_helpers";
2+
3+
/** @typedef {import("@mail/core/common/upgrade/upgrade_helpers").UpgradeFnParam} UpgradeFnParam */
4+
/** @typedef {import("@mail/core/common/upgrade/upgrade_helpers").UpgradeFnReturn} UpgradeFnReturn */
5+
6+
export const upgrade_19_1 = {
7+
/**
8+
* @param {string} key
9+
* @param {((param: UpgradeFnParam) => UpgradeFnReturn)|UpgradeFnReturn} upgrade
10+
* @returns {UpgradeFnReturn}
11+
*/
12+
add(key, upgrade) {
13+
return addUpgrade({ key, version: "19.1", upgrade });
14+
},
15+
};
16+
17+
upgrade_19_1.add("mail.user_setting.message_sound", {
18+
key: "Settings,undefined:messageSound",
19+
value: false,
20+
});
21+
22+
upgrade_19_1.add("mail_user_setting_disable_call_auto_focus", {
23+
key: "Settings,undefined:useCallAutoFocus",
24+
value: false,
25+
});
26+
27+
upgrade_19_1.add("mail_user_setting_use_blur", {
28+
key: "Settings,undefined:useBlur",
29+
value: true,
30+
});
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { getCurrentLocalStorageVersion, LocalStorageEntry } from "@mail/utils/common/local_storage";
2+
import { parseVersion } from "@mail/utils/common/misc";
3+
import { registry } from "@web/core/registry";
4+
5+
/**
6+
* @typedef {Object} UpgradeData
7+
* @property {string} version
8+
* @property {string} key
9+
* @property {((param: UpgradeFnParam) => UpgradeFnReturn)|UpgradeFnReturn} upgrade
10+
*/
11+
/**
12+
* @typedef {Object} UpgradeDataWithoutVersion
13+
* @property {string} key
14+
* @property {((param: UpgradeFnParam) => UpgradeFnReturn)|UpgradeFnReturn} upgrade
15+
*/
16+
/**
17+
* @typedef {Object} UpgradeFnParam
18+
* @property {string} key
19+
* @property {any} value
20+
*/
21+
/**
22+
* @typedef {Object} UpgradeFnReturn
23+
* @property {string} key
24+
* @property {any} value
25+
*/
26+
27+
/**
28+
* Register a `key` and `upgrade` function for a `version` of local storage.
29+
*
30+
* When there's request to upgrade a local storage
31+
*
32+
* Example:
33+
* - we have versions 19.1, 19.3, and 20.0.
34+
* - define upgrade function with 19.1 is to upgrade to 19.1
35+
* - define ugrade function with 20.0 is to upgrade to 20.0
36+
*
37+
* Upgrades are applied in sequence, i.e.:
38+
* - 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.
39+
* - 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.
40+
* - 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.
41+
* - if version is 20.0 and local storage data are in version 20.0, this will not upgrade data.
42+
*
43+
* @param {UpgradeData} param0
44+
*/
45+
export function addUpgrade({ version, key, upgrade }) {
46+
/** @type {Map<string, Function[]>} */
47+
const map = getUpgradeMap();
48+
if (!map.has(version)) {
49+
map.set(version, new Map());
50+
}
51+
map.get(version).set(key, { version, key, upgrade });
52+
}
53+
54+
/** @param {string} version */
55+
export function upgradeFrom(version) {
56+
const orderedUpgradeList = Array.from(getUpgradeMap().entries())
57+
.filter(
58+
([v]) =>
59+
!parseVersion(v).isLowerThan(version) &&
60+
!parseVersion(getCurrentLocalStorageVersion()).isLowerThan(v)
61+
)
62+
.sort(([v1], [v2]) => (parseVersion(v1).isLowerThan(v2) ? -1 : 1));
63+
for (const [, keyMap] of orderedUpgradeList) {
64+
for (const upgradeData of keyMap.values()) {
65+
applyUpgrade(upgradeData);
66+
}
67+
}
68+
}
69+
70+
const upgradeRegistry = registry.category("discuss.upgrade");
71+
upgradeRegistry.add(null, new Map());
72+
73+
/**
74+
* A Map of version numbers to a Map of keys and upgrade functions.
75+
* Basically:
76+
*
77+
* Map: {
78+
* "19.1": {
79+
* key_1: upgradeData_1,
80+
* key_2: upgradeData_2,
81+
* },
82+
* "19.2": {
83+
* key_3: upgradeData_3,
84+
* key_4: upgradeData_4,
85+
* ...
86+
* },
87+
* ...
88+
* }
89+
*
90+
* To upgrade a key in a given version, find the key in version
91+
* and applyUpgrade using upgradeData to upgrade with new key, value and version.
92+
*
93+
* @return {Map<string, Map<string, UpgradeData>>}
94+
*/
95+
function getUpgradeMap() {
96+
return upgradeRegistry.get(null);
97+
}
98+
99+
/**
100+
* Upgrade local storage using `upgrade` data.
101+
* i.e. call `upgradeData.upgrade` to get new key and value.
102+
*
103+
* @param {UpgradeData} upgradeData
104+
*/
105+
function applyUpgrade(upgradeData) {
106+
const oldEntry = new LocalStorageEntry(upgradeData.key);
107+
const parsed = oldEntry.parse();
108+
const oldValue = parsed ?? oldEntry.get();
109+
if (
110+
oldValue === null ||
111+
(parsed && parsed.version && parseVersion(upgradeData.version).isLowerThan(parsed.version))
112+
) {
113+
return; // could not upgrade (cannot parse or more recent version)
114+
}
115+
const { key, value } =
116+
typeof upgradeData.upgrade === "function"
117+
? upgradeData.upgrade({ key: upgradeData.key, value: oldValue })
118+
: upgradeData.upgrade;
119+
oldEntry.remove();
120+
const newEntry = new LocalStorageEntry(key);
121+
newEntry.set(value, upgradeData.version);
122+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { registry } from "@web/core/registry";
2+
import { upgradeFrom } from "./upgrade_helpers";
3+
import { getCurrentLocalStorageVersion, LocalStorageEntry } from "@mail/utils/common/local_storage";
4+
import { parseVersion } from "@mail/utils/common/misc";
5+
6+
export const discussUpgradeService = {
7+
dependencies: [],
8+
start() {
9+
const lse = new LocalStorageEntry("discuss.upgrade.version");
10+
const parsed = lse.parse();
11+
const oldVersion = parsed?.version ?? "1.0";
12+
const currentVersion = getCurrentLocalStorageVersion();
13+
lse.set(true, currentVersion);
14+
if (parseVersion(oldVersion).isLowerThan(currentVersion)) {
15+
upgradeFrom(oldVersion);
16+
}
17+
},
18+
};
19+
20+
registry.category("services").add("discuss.upgrade", discussUpgradeService);

0 commit comments

Comments
 (0)