Skip to content

Commit a731d79

Browse files
committed
[IMP] mail: PWA shows discuss badge counter on app icon
This commit adds a badge counter with global discuss counter on the PWA app icon, which helps a lot in knowing how many chat notifications are pending in the PWA when the Odoo app is not open. Task-5136354 closes odoo#235003 X-original-commit: c04c4cb Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
1 parent b094553 commit a731d79

File tree

10 files changed

+166
-26
lines changed

10 files changed

+166
-26
lines changed

addons/mail/__manifest__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
# depends on BS variables, can't be loaded in assets_primary or assets_secondary
147147
'mail/static/src/scss/variables/derived_variables.scss',
148148
'mail/static/src/scss/*.scss',
149+
'mail/static/lib/idb-keyval/idb-keyval.js',
149150
'mail/static/lib/selfie_segmentation/selfie_segmentation.js',
150151
'mail/static/src/js/**/*',
151152
'mail/static/src/model/**/*',
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// idb-keyval.js 3.2.0
2+
// https://github.com/jakearchibald/idb-keyval
3+
// Copyright 2016, Jake Archibald
4+
// Licensed under the Apache License, Version 2.0
5+
6+
var idbKeyval = (function (exports) {
7+
'use strict';
8+
9+
class Store {
10+
constructor(dbName = 'keyval-store', storeName = 'keyval') {
11+
this.storeName = storeName;
12+
this._dbp = new Promise((resolve, reject) => {
13+
const openreq = indexedDB.open(dbName, 1);
14+
openreq.onerror = () => reject(openreq.error);
15+
openreq.onsuccess = () => resolve(openreq.result);
16+
// First time setup: create an empty object store
17+
openreq.onupgradeneeded = () => {
18+
openreq.result.createObjectStore(storeName);
19+
};
20+
});
21+
}
22+
_withIDBStore(type, callback) {
23+
return this._dbp.then(db => new Promise((resolve, reject) => {
24+
const transaction = db.transaction(this.storeName, type);
25+
transaction.oncomplete = () => resolve();
26+
transaction.onabort = transaction.onerror = () => reject(transaction.error);
27+
callback(transaction.objectStore(this.storeName));
28+
}));
29+
}
30+
}
31+
let store;
32+
function getDefaultStore() {
33+
if (!store)
34+
store = new Store();
35+
return store;
36+
}
37+
function get(key, store = getDefaultStore()) {
38+
let req;
39+
return store._withIDBStore('readonly', store => {
40+
req = store.get(key);
41+
}).then(() => req.result);
42+
}
43+
function set(key, value, store = getDefaultStore()) {
44+
return store._withIDBStore('readwrite', store => {
45+
store.put(value, key);
46+
});
47+
}
48+
function del(key, store = getDefaultStore()) {
49+
return store._withIDBStore('readwrite', store => {
50+
store.delete(key);
51+
});
52+
}
53+
function clear(store = getDefaultStore()) {
54+
return store._withIDBStore('readwrite', store => {
55+
store.clear();
56+
});
57+
}
58+
function keys(store = getDefaultStore()) {
59+
const keys = [];
60+
return store._withIDBStore('readonly', store => {
61+
// This would be store.getAllKeys(), but it isn't supported by Edge or Safari.
62+
// And openKeyCursor isn't supported by Safari.
63+
(store.openKeyCursor || store.openCursor).call(store).onsuccess = function () {
64+
if (!this.result)
65+
return;
66+
keys.push(this.result.key);
67+
this.result.continue();
68+
};
69+
}).then(() => keys);
70+
}
71+
72+
exports.Store = Store;
73+
exports.get = get;
74+
exports.set = set;
75+
exports.del = del;
76+
exports.clear = clear;
77+
exports.keys = keys;
78+
79+
return exports;
80+
81+
}({}));

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -437,15 +437,18 @@ export class Store extends BaseStore {
437437
});
438438
}
439439
}
440-
if (
441-
type === "notification-displayed" &&
442-
["mail.thread", "discuss.channel"].includes(payload.model)
443-
) {
444-
this.env.services["mail.out_of_focus"]._playSound();
440+
if (type === "notification-displayed") {
441+
this.onPushNotificationDisplayed(payload);
445442
}
446443
});
447444
}
448445

446+
onPushNotificationDisplayed(payload) {
447+
if (["mail.thread", "discuss.channel"].includes(payload.model)) {
448+
this.env.services["mail.out_of_focus"]._playSound();
449+
}
450+
}
451+
449452
/**
450453
* Search and fetch for a partner with a given user or partner id.
451454
* @param {Object} param0

addons/mail/static/src/core/public_web/out_of_focus_service_patch.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ patch(OutOfFocusService.prototype, {
77
this.titleService = services.title;
88
this.counter = 0;
99
this.contributingMessageLocalIds = new Set();
10-
env.bus.addEventListener("window_focus", () => this.clearUnreadMessage());
10+
env.bus.addEventListener("window_focus", () => this.onWindowFocus());
1111
},
1212
clearUnreadMessage() {
1313
this.counter = 0;
@@ -23,5 +23,8 @@ patch(OutOfFocusService.prototype, {
2323
this.titleService.setCounters({ discuss: this.counter });
2424
super.notify(...arguments);
2525
},
26+
onWindowFocus() {
27+
this.clearUnreadMessage();
28+
},
2629
});
2730
outOfFocusService.dependencies = [...outOfFocusService.dependencies, "title"];

addons/mail/static/src/core/web/messaging_menu_patch.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ patch(MessagingMenu.prototype, {
167167
},
168168
get counter() {
169169
let value =
170-
this.store.inbox.counter +
170+
this.store.globalCounter +
171171
this.store.failures.reduce((acc, f) => acc + parseInt(f.notifications.length), 0);
172172
if (this.canPromptToInstall) {
173173
value++;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { OutOfFocusService } from "@mail/core/common/out_of_focus_service";
2+
import { patch } from "@web/core/utils/patch";
3+
4+
patch(OutOfFocusService.prototype, {
5+
onWindowFocus() {
6+
super.onWindowFocus();
7+
this.store.updateAppBadge();
8+
},
9+
});

addons/mail/static/src/core/web/store_service_patch.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ import { _t } from "@web/core/l10n/translation";
55

66
import { patch } from "@web/core/utils/patch";
77

8+
const unread_store = (() => {
9+
if (!window.idbKeyval) {
10+
return undefined;
11+
}
12+
return new window.idbKeyval.Store("odoo-mail-unread-db", "odoo-mail-unread-store");
13+
})();
14+
815
/** @type {import("models").Store} */
916
const StorePatch = {
1017
setup() {
@@ -26,17 +33,33 @@ const StorePatch = {
2633
return getSortId(g1) - getSortId(g2);
2734
},
2835
});
36+
this.globalCounter = fields.Attr(0, {
37+
compute() {
38+
return this.computeGlobalCounter();
39+
},
40+
onUpdate() {
41+
this.updateAppBadge();
42+
},
43+
eager: true,
44+
});
2945
this.inbox = fields.One("mail.thread");
3046
this.starred = fields.One("mail.thread");
3147
this.history = fields.One("mail.thread");
3248
},
49+
computeGlobalCounter() {
50+
return this.inbox?.counter ?? 0;
51+
},
3352
async initialize() {
3453
await Promise.all([
3554
this.fetchStoreData("failures"),
3655
this.fetchStoreData("systray_get_activities"),
3756
super.initialize(...arguments),
3857
]);
3958
},
59+
onPushNotificationDisplayed() {
60+
super.onPushNotificationDisplayed(...arguments);
61+
this.updateAppBadge();
62+
},
4063
onStarted() {
4164
super.onStarted(...arguments);
4265
this.inbox = {
@@ -102,6 +125,12 @@ const StorePatch = {
102125
)
103126
);
104127
},
128+
updateAppBadge() {
129+
if (unread_store) {
130+
window.idbKeyval.set("unread", this.globalCounter, unread_store);
131+
Promise.resolve(navigator.setAppBadge?.(this.globalCounter)).catch(() => {}); // FIXME: Illegal invocation error in HOOT
132+
}
133+
},
105134
/**
106135
* @param {object} param0
107136
* @param {{ type: "INSERT"|"DELETE"|"RELOAD_CHATTER", payload: Partial<import("models").Activity> }} param0.data

addons/mail/static/src/discuss/core/web/messaging_menu_patch.js

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,4 @@ patch(MessagingMenu.prototype, {
2121
this.dropdown.close();
2222
}
2323
},
24-
get counter() {
25-
const count = super.counter;
26-
const channelsContribution =
27-
this.store.channels.status !== "fetched"
28-
? this.store.initChannelsUnreadCounter
29-
: Object.values(this.store["discuss.channel"].records).filter(
30-
(channel) =>
31-
channel.displayToSelf &&
32-
!channel.self_member_id?.mute_until_dt &&
33-
(channel.self_member_id?.message_unread_counter ||
34-
channel.message_needaction_counter)
35-
).length;
36-
// Needactions are already counted in the super call, but we want to discard them for channel so that there is only +1 per channel.
37-
const channelsNeedactionCounter = Object.values(
38-
this.store["discuss.channel"].records
39-
).reduce((acc, channel) => acc + channel.message_needaction_counter, 0);
40-
return count + channelsContribution - channelsNeedactionCounter;
41-
},
4224
});

addons/mail/static/src/discuss/core/web/store_service_patch.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,26 @@ const StorePatch = {
1010
super.setup(...arguments);
1111
this.initChannelsUnreadCounter = 0;
1212
},
13+
computeGlobalCounter() {
14+
if (!this["discuss.channel"]) {
15+
return super.computeGlobalCounter();
16+
}
17+
const channelsContribution =
18+
this.channels.status !== "fetched"
19+
? this.initChannelsUnreadCounter
20+
: Object.values(this.store["discuss.channel"].records).filter(
21+
(channel) =>
22+
channel.displayToSelf &&
23+
!channel.self_member_id?.mute_until_dt &&
24+
(channel.self_member_id?.message_unread_counter ||
25+
channel.message_needaction_counter)
26+
).length;
27+
// Needactions are already counted in the super call, but we want to discard them for channel so that there is only +1 per channel.
28+
const channelsNeedactionCounter = Object.values(
29+
this.store["discuss.channel"].records
30+
).reduce((acc, channel) => acc + channel.message_needaction_counter, 0);
31+
return super.computeGlobalCounter() + channelsContribution + channelsNeedactionCounter;
32+
},
1333
/** @returns {import("models").Thread[]} */
1434
getSelfImportantChannels() {
1535
return this.getSelfRecentChannels().filter((channel) => channel.importantCounter > 0);

addons/mail/static/src/service_worker.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/* eslint-env serviceworker */
22
/* eslint-disable no-restricted-globals */
3+
/* global idbKeyval */
4+
importScripts("/mail/static/lib/idb-keyval/idb-keyval.js");
35

46
const MESSAGE_TYPE = { POST_RTC_LOGS: "POST_RTC_LOGS" };
57
const PUSH_NOTIFICATION_TYPE = {
@@ -11,8 +13,10 @@ const PUSH_NOTIFICATION_ACTION = {
1113
DECLINE: "DECLINE",
1214
};
1315

16+
const { Store, set, get } = idbKeyval;
1417
const LOG_AGE_LIMIT = 24 * 60 * 60 * 1000; // 24h
1518
let db;
19+
const unread_store = new Store("odoo-mail-unread-db", "odoo-mail-unread-store");
1620
let interactionSinceCleanupCount = 0;
1721

1822
async function openDatabase() {
@@ -229,6 +233,13 @@ self.addEventListener("message", ({ data }) => {
229233
}
230234
});
231235

236+
async function incrementUnread() {
237+
const oldCounter = (await get("unread", unread_store)) ?? 0;
238+
const newCounter = oldCounter + 1;
239+
set("unread", newCounter, unread_store);
240+
navigator.setAppBadge?.(newCounter);
241+
}
242+
232243
async function handlePushEvent(notification) {
233244
const { model, res_id } = notification.options?.data || {};
234245
const correlationId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
@@ -252,7 +263,8 @@ async function handlePushEvent(notification) {
252263
})
253264
);
254265
});
255-
timeoutId = setTimeout(() => {
266+
timeoutId = setTimeout(async () => {
267+
await incrementUnread();
256268
self.clients.matchAll({ includeUncontrolled: true, type: "window" }).then((clients) => {
257269
clients.forEach((client) =>
258270
client.postMessage({

0 commit comments

Comments
 (0)