From c2a20b69c13c25a1f8be9fd04cd679fb10535ffd Mon Sep 17 00:00:00 2001 From: fcamblor Date: Sun, 25 Apr 2021 15:14:36 +0200 Subject: [PATCH 01/26] added idb dependency for promisified indexeddb management + provided a first implementation for center appointment subscriptions --- package-lock.json | 5 ++++ package.json | 1 + src/storage/DB.ts | 59 ++++++++++++++++++++++++++++++++++++++++ src/vmd-app.component.ts | 3 ++ 4 files changed, 68 insertions(+) create mode 100644 src/storage/DB.ts diff --git a/package-lock.json b/package-lock.json index 7a8a640c5..9ee873bb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -183,6 +183,11 @@ "function-bind": "^1.1.1" } }, + "idb": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/idb/-/idb-6.0.0.tgz", + "integrity": "sha512-+M367poGtpzAylX4pwcrZIa7cFQLfNkAOlMMLN2kw/2jGfJP6h+TB/unQNSVYwNtP8XqkLYrfuiVnxLQNP1tjA==" + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", diff --git a/package.json b/package.json index 0eb34df80..6aaf29990 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "bootstrap": "5.0.0-beta3", + "idb": "6.0.0", "leaflet": "1.7.1", "leaflet.markercluster": "1.4.1", "lit-element": "2.4.0", diff --git a/src/storage/DB.ts b/src/storage/DB.ts new file mode 100644 index 000000000..bce1309fc --- /dev/null +++ b/src/storage/DB.ts @@ -0,0 +1,59 @@ +import {IDBPDatabase, openDB} from "idb"; +import {Commune, Departement, LieuAvecDistance} from "../state/State"; + + +export type Subscription = { + ts: number; + departement: Departement; + commune?: Commune; + lieu: LieuAvecDistance; + notificationUrl : string; +}; + +export class DB { + + public static readonly INSTANCE = new DB(); + + private db: IDBPDatabase|undefined = undefined; + + private constructor() { + } + + public async initialize(): Promise { + this.db = await openDB('vite-ma-dose', 1, { + upgrade(db, oldVersion, newVersion, transaction) { + switch(oldVersion) { + case 0: + db.createObjectStore('subscriptions', { keyPath: 'ts' }) + break; + } + }, + blocked() { + console.error("openDB blocked") + }, + blocking() { + console.warn("Blocking openDB") + }, + terminated() { + console.info("Terminated openDB") + }, + }); + } + + async subscribeToCenterAppointments(subscription: Subscription) { + if(!this.db) { + throw new Error("DB not initialized !"); + } + + const id = await this.db.transaction(["subscriptions"], "readwrite").objectStore("subscriptions").add(subscription); + console.info(`Created subscription with id ${id}`); + } + + async fetchAllSubscriptions(): Promise { + if(!this.db) { + throw new Error("DB not initialized !"); + } + + return await this.db.transaction(["subscriptions"]).objectStore("subscriptions").getAll(); + } +} diff --git a/src/vmd-app.component.ts b/src/vmd-app.component.ts index 69b3b6386..4d6bf3ced 100644 --- a/src/vmd-app.component.ts +++ b/src/vmd-app.component.ts @@ -2,6 +2,7 @@ import {LitElement, html, customElement, property, css, unsafeCSS} from 'lit-ele import {Router, SlottedTemplateResultFactory} from "./routing/Router"; import globalCss from './styles/global.scss' import {TemplateResult} from "lit-html"; +import {DB} from "./storage/DB"; @customElement('vmd-app') export class VmdAppComponent extends LitElement { @@ -25,6 +26,8 @@ export class VmdAppComponent extends LitElement { Router.installRoutes((viewTemplateResult, path) => { this.viewTemplateResult = viewTemplateResult; }) + + DB.INSTANCE.initialize() } render() { From 4b66108914097f20f4b53a9ebce5f1670addf941 Mon Sep 17 00:00:00 2001 From: fcamblor Date: Sun, 25 Apr 2021 15:20:04 +0200 Subject: [PATCH 02/26] introduced basic service worker implementation --- public/sw.js | 75 +++++++++++++++++++++++++++++++++++++ src/utils/ServiceWorkers.ts | 61 ++++++++++++++++++++++++++++++ src/vmd-app.component.ts | 5 ++- 3 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 public/sw.js create mode 100644 src/utils/ServiceWorkers.ts diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 000000000..6696d4cb7 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,75 @@ +importScripts( + 'https://cdn.jsdelivr.net/npm/idb@6.0.0/build/iife/index-min.js' +) + +let getVersionPort = undefined; + +// self.addEventListener('install', function(event) { +// console.log('Service Worker activating...'); +// event.waitUntil(self.skipWaiting()); // Activate worker immediately +// }); + +self.addEventListener('activate', function(event) { + console.log('Service Worker activating...'); + event.waitUntil( + DB.INSTANCE.initialize().then(function() { + console.log('DB created !'); + return self.clients.claim(); + }) + ); +}); + +self.addEventListener('sync', function(event) { + console.log("sync event", event); +}); +self.addEventListener("message", function(event) { + if (event.data && event.data.type === 'INIT_PORT') { + getVersionPort = event.ports[0]; + } +}); + +class DB { + static _INSTANCE = new DB(); + static instance() { + return DB._INSTANCE.db(); + } + + dbResolver; + dbPromise; + + constructor() { + var _this = this; + this.dbPromise = new Promise(function(resolve) { + _this.dbResolver = resolve; + }); + } + + db() { + return this.dbPromise; + } + + initialize() { + var _this = this; + idb.openDB('vite-ma-dose', 1, { + upgrade(db, oldVersion, newVersion, transaction) { + switch(oldVersion) { + case 0: + db.createObjectStore('subscriptions', { keyPath: 'ts' }) + break; + } + }, + blocked() { + console.error("openDB blocked") + }, + blocking() { + console.warn("Blocking openDB") + }, + terminated() { + console.info("Terminated openDB") + }, + }).then(function(db) { + _this.dbResolver(db); + }); + return this.dbPromise; + } +} diff --git a/src/utils/ServiceWorkers.ts b/src/utils/ServiceWorkers.ts new file mode 100644 index 000000000..52c9dd7fc --- /dev/null +++ b/src/utils/ServiceWorkers.ts @@ -0,0 +1,61 @@ +import {Router} from "../routing/Router"; +import {DB} from "../storage/DB"; + + +export class ServiceWorkers { + + public static readonly INSTANCE = new ServiceWorkers(); + + private messageChannel = new MessageChannel(); + + private constructor() { + } + + async startup() { + // Registering background synchronization + if (!navigator.serviceWorker){ + console.info("Service Worker not supported") + return false; + } + + // Waiting for 'load' event + // see https://developers.google.com/web/fundamentals/primers/service-workers/registration#improving_the_boilerplate + await new Promise((resolve, reject) => window.addEventListener('load', resolve)); + + const serviceWorkerRegistration = await navigator.serviceWorker.register(`${Router.basePath}sw.js`); + + navigator.serviceWorker.addEventListener('controllerchange', () => { + console.log("controllerchange called !"); + }); + + // Cases : + // - navigator.serviceWorker.controller is undefined : this occurs the first time the sw is installed + // in that case, we should look for updatefound + statechange=activated events to resolve controller + // - navigator.serviceWorker.controller is defined, we should play with is, but don't forget to register a + // controller change event in case a new version of the sw is deployed + + const swInitializer = await new Promise<{controller: ServiceWorker, updated: boolean}>((resolve, reject) => { + if(navigator.serviceWorker.controller) { + resolve({controller: navigator.serviceWorker.controller, updated: false}); + } + + serviceWorkerRegistration.addEventListener('updatefound', () => { + const newWorker = serviceWorkerRegistration.installing!; + newWorker.addEventListener('statechange', (event) => { + if(newWorker.state === 'activated') { + resolve({ controller: navigator.serviceWorker.controller!, updated: true}); + } + }) + }); + }) + + swInitializer.controller.postMessage({ + type: 'INIT_PORT' + }, [this.messageChannel.port2]) + this.messageChannel.port1.onmessage = function (event) { + console.log(event.data.payload); + } + + return true; + } +} diff --git a/src/vmd-app.component.ts b/src/vmd-app.component.ts index 4d6bf3ced..9b52e2619 100644 --- a/src/vmd-app.component.ts +++ b/src/vmd-app.component.ts @@ -2,6 +2,7 @@ import {LitElement, html, customElement, property, css, unsafeCSS} from 'lit-ele import {Router, SlottedTemplateResultFactory} from "./routing/Router"; import globalCss from './styles/global.scss' import {TemplateResult} from "lit-html"; +import {ServiceWorkers} from "./utils/ServiceWorkers"; import {DB} from "./storage/DB"; @customElement('vmd-app') @@ -27,7 +28,9 @@ export class VmdAppComponent extends LitElement { this.viewTemplateResult = viewTemplateResult; }) - DB.INSTANCE.initialize() + DB.INSTANCE.initialize().then(() => { + ServiceWorkers.INSTANCE.startup(); + }) } render() { From afe6a29e375464944dfc72ff7fb34143508f24a9 Mon Sep 17 00:00:00 2001 From: fcamblor Date: Sun, 25 Apr 2021 15:23:00 +0200 Subject: [PATCH 03/26] introduced PushNotifications utility class, communicating push notifications grants to the service worker when they are changing --- public/sw.js | 6 +++++ src/utils/ServiceWorkers.ts | 54 +++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/public/sw.js b/public/sw.js index 6696d4cb7..d4148b50c 100644 --- a/public/sw.js +++ b/public/sw.js @@ -3,6 +3,7 @@ importScripts( ) let getVersionPort = undefined; +let pushNotificationsGranted=false; // self.addEventListener('install', function(event) { // console.log('Service Worker activating...'); @@ -26,6 +27,11 @@ self.addEventListener("message", function(event) { if (event.data && event.data.type === 'INIT_PORT') { getVersionPort = event.ports[0]; } + + if (event.data && event.data.type === 'UPDATE_PUSH_NOTIF_GRANT') { + pushNotificationsGranted = event.data.granted; + console.info("Update push notification granted to ["+pushNotificationsGranted+"]"); + } }); class DB { diff --git a/src/utils/ServiceWorkers.ts b/src/utils/ServiceWorkers.ts index 52c9dd7fc..9f8841477 100644 --- a/src/utils/ServiceWorkers.ts +++ b/src/utils/ServiceWorkers.ts @@ -59,3 +59,57 @@ export class ServiceWorkers { return true; } } + +type PushNotificationGrantResult = 'granted'|'denied'|'not_available'; +export class PushNotifications { + + public static readonly INSTANCE = new PushNotifications(); + + private constructor() { + } + + public async ensureGranted(): Promise<{granted:true}|{granted:false,type:'not_granted',status:string}|{granted:false,type:'error',error:any}> { + return new Promise(async (resolve) => { + try { + const result = await PushNotifications.askPermission(); + + await this.pushNotificationGrantToServiceWorker(); + + if(result === 'granted'){ + resolve({ granted: true }); + } else { + resolve({ granted: false, type: 'not_granted', status: result }); + } + } catch(e) { + resolve({ granted: false, type: 'error', error: e }); + } + }) + } + + async pushNotificationGrantToServiceWorker() { + await navigator.serviceWorker.controller!.postMessage({ + type: 'UPDATE_PUSH_NOTIF_GRANT', + granted: Notification.permission === 'granted' + }); + } + + private static async askPermission(): Promise { + if(!Notification) { + console.info("Notification not supported !") + return 'not_available'; + } + + const permissionResult = await new Promise((resolve, reject) => { + const permissionResult = Notification.requestPermission((result) => resolve(result)); + if (permissionResult) { + permissionResult.then(resolve, reject); + } + }); + + if (permissionResult !== 'granted') { + console.error(`We weren't granted permission.`) + return 'denied'; + } + return 'granted'; + } +} From be49fe657f73413e8003f3c16a55ec0a9c39050e Mon Sep 17 00:00:00 2001 From: fcamblor Date: Sun, 25 Apr 2021 19:50:53 +0200 Subject: [PATCH 04/26] introduced a button allowing to subscribe to center new appointments --- public/assets/images/png/vmd-badge.png | Bin 0 -> 14838 bytes public/assets/images/png/vmd-icon.png | Bin 0 -> 6693 bytes public/sw.js | 86 +++++++++++++++++- .../vmd-appointment-card.component.ts | 9 ++ src/utils/ServiceWorkers.ts | 18 ++++ src/views/vmd-rdv.view.ts | 24 ++++- 6 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 public/assets/images/png/vmd-badge.png create mode 100644 public/assets/images/png/vmd-icon.png diff --git a/public/assets/images/png/vmd-badge.png b/public/assets/images/png/vmd-badge.png new file mode 100644 index 0000000000000000000000000000000000000000..7ec9126978c23d5e26bed6c83d96413d227daccc GIT binary patch literal 14838 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 literal 0 HcmV?d00001 diff --git a/public/assets/images/png/vmd-icon.png b/public/assets/images/png/vmd-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9dfbac6efb27ea1048a629e848b37c2175616300 GIT binary patch literal 6693 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 literal 0 HcmV?d00001 diff --git a/public/sw.js b/public/sw.js index d4148b50c..a10cbd800 100644 --- a/public/sw.js +++ b/public/sw.js @@ -2,6 +2,11 @@ importScripts( 'https://cdn.jsdelivr.net/npm/idb@6.0.0/build/iife/index-min.js' ) +const USE_RAW_GITHUB = false +const VMD_BASE_URL = USE_RAW_GITHUB + ? "https://raw.githubusercontent.com/CovidTrackerFr/vitemadose/data-auto/data/output" + : "https://vitemadose.gitlab.io/vitemadose" + let getVersionPort = undefined; let pushNotificationsGranted=false; @@ -13,7 +18,7 @@ let pushNotificationsGranted=false; self.addEventListener('activate', function(event) { console.log('Service Worker activating...'); event.waitUntil( - DB.INSTANCE.initialize().then(function() { + DB.initialize().then(function() { console.log('DB created !'); return self.clients.claim(); }) @@ -22,6 +27,9 @@ self.addEventListener('activate', function(event) { self.addEventListener('sync', function(event) { console.log("sync event", event); + if (event.tag === 'check-subscriptions') { + checkSubscriptions(); + } }); self.addEventListener("message", function(event) { if (event.data && event.data.type === 'INIT_PORT') { @@ -34,6 +42,76 @@ self.addEventListener("message", function(event) { } }); +function checkSubscriptions() { + if(!pushNotificationsGranted) { + console.info("Push notifications not granted, skipping any sync event !") + return; + } + + return DB.instance().then(function(db) { + return db.transaction(["subscriptions"]).objectStore("subscriptions").getAll().then(function(results) { + if (!results.length) { + // No subscription declared, skipping + return; + } + + return rechercherAbonnementsAvecRdvDispos(results); + }).then(function(abonnementsAvecRvdDispos) { + return Promise.all(abonnementsAvecRvdDispos.map(function(abonnementAvecRvdDispos) { + return Promise.all([ + db.transaction(["subscriptions"], "readwrite").objectStore("subscriptions").delete(abonnementAvecRvdDispos.subscription.ts), + self.registration.showNotification( + "ViteMaDose - "+abonnementAvecRvdDispos.appointment_count+" créneaux trouvés", + { + lang: 'fr-FR', + body: abonnementAvecRvdDispos.appointment_count+" créneaux de vaccination trouvés sur " + + abonnementAvecRvdDispos.subscription.lieu.nom+" ("+abonnementAvecRvdDispos.subscription.departement.code_departement+")", + badge: 'https://deca76fefa39.ngrok.io/assets/images/png/vmd-badge.png', + icon: 'https://deca76fefa39.ngrok.io/assets/images/favicon/android-chrome-512x512.png', + // That's too big.. the icon above is enough + // image: 'https://deca76fefa39.ngrok.io/assets/images/favicon/android-chrome-512x512.png', + data: { + notificationUrl: abonnementAvecRvdDispos.subscription.notificationUrl, + departement: abonnementAvecRvdDispos.subscription.departement, + commune: abonnementAvecRvdDispos.subscription.commune + }, + }) + ]); + })); + }); + }) +} + +function rechercherAbonnementsAvecRdvDispos(subscriptions) { + const subscriptionsByCodesDepartement = subscriptions.reduce(function(codeDepartements, subscription) { + codeDepartements.set(subscription.departement.code_departement, codeDepartements.get(subscription.departement.code_departement) || []); + codeDepartements.get(subscription.departement.code_departement).push(subscription); + return codeDepartements; + }, new Map()) + + return Promise.all(Array.from(subscriptionsByCodesDepartement.keys()).map(function(codeDepartement) { + return fetch(VMD_BASE_URL + "/" + codeDepartement + ".json") + .then(function(resp) { return resp.json(); }) + .then(function(centres) { return { centres: centres, codeDepartement: codeDepartement }; }) + })).then(function(results) { + return results.reduce(function(abosAvecRdvDispos, centresParDepartement) { + return subscriptionsByCodesDepartement.get(centresParDepartement.codeDepartement).reduce(function(abosAvecRdvDispos, subscription) { + const centre = centresParDepartement.centres.centres_disponibles.find(function(l) { + // To be improved ??? With a levenshtein distance or something like this ? (in case lieu has been renamed a bit) + return subscription.lieu.nom === l.nom; + }); + if(centre && centre.appointment_count) { + abosAvecRdvDispos.push({ + subscription: subscription, + appointment_count: centre.appointment_count + }); + } + return abosAvecRdvDispos; + }, abosAvecRdvDispos); + }, []); + }); +} + class DB { static _INSTANCE = new DB(); static instance() { @@ -54,8 +132,8 @@ class DB { return this.dbPromise; } - initialize() { - var _this = this; + static initialize() { + var _this = DB._INSTANCE; idb.openDB('vite-ma-dose', 1, { upgrade(db, oldVersion, newVersion, transaction) { switch(oldVersion) { @@ -76,6 +154,6 @@ class DB { }).then(function(db) { _this.dbResolver(db); }); - return this.dbPromise; + return _this.dbPromise; } } diff --git a/src/components/vmd-appointment-card.component.ts b/src/components/vmd-appointment-card.component.ts index e395c5cc8..6894c4b70 100644 --- a/src/components/vmd-appointment-card.component.ts +++ b/src/components/vmd-appointment-card.component.ts @@ -134,6 +134,9 @@ export class VmdAppointmentCardComponent extends LitElement { + `:html``} @@ -142,6 +145,12 @@ export class VmdAppointmentCardComponent extends LitElement { } } + subscribe() { + this.dispatchEvent(new CustomEvent('abonnement-clique', { + detail: { lieu: this.lieu } + })); + } + connectedCallback() { super.connectedCallback(); // console.log("connected callback") diff --git a/src/utils/ServiceWorkers.ts b/src/utils/ServiceWorkers.ts index 9f8841477..2fa435990 100644 --- a/src/utils/ServiceWorkers.ts +++ b/src/utils/ServiceWorkers.ts @@ -56,6 +56,24 @@ export class ServiceWorkers { console.log(event.data.payload); } + // If at least 1 subscription defined, ensuring that push notifications are still granted + // - if user subscribed to some push notifs, we should update sw's push notification granted + // flag with what is currently in place + // - otherwise, no need to ask any permission until user clicks on subscription button + const allSubscriptions = await DB.INSTANCE.fetchAllSubscriptions() + if(allSubscriptions.length) { + if (Notification.permission === 'default') { + await PushNotifications.INSTANCE.ensureGranted(); + } else if(Notification.permission === 'denied') { + // TODO: CHANGE THIS TO A STICKY FOOTER ERROR ??? + console.error("User subscribed to some center updated, but denied PUSH Notifications") + } else if(Notification.permission === 'granted') { + await PushNotifications.INSTANCE.pushNotificationGrantToServiceWorker(); + } + } + + await serviceWorkerRegistration.sync.register("check-subscriptions"); + return true; } } diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index 6bdd725a4..c0e5cffc7 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -12,7 +12,8 @@ import { Departement, libelleUrlPathDeCommune, libelleUrlPathDuDepartement, - Lieu, LieuxAvecDistanceParDepartement, + Lieu, + LieuxAvecDistanceParDepartement, LieuxParDepartement, State, TRIS_CENTRE @@ -21,7 +22,9 @@ import {Dates} from "../utils/Dates"; import {Strings} from "../utils/Strings"; import { AutocompleteTriggered, - CommuneSelected, DepartementSelected, VmdCommuneOrDepartmentSelectorComponent, + CommuneSelected, + DepartementSelected, + VmdCommuneOrDepartmentSelectorComponent, VmdCommuneSelectorComponent } from "../components/vmd-commune-selector.component"; import {DEPARTEMENTS_LIMITROPHES} from "../utils/Departements"; @@ -29,6 +32,8 @@ import {ValueStrCustomEvent} from "../components/vmd-selector.component"; import {TemplateResult} from "lit-html"; import {Analytics} from "../utils/Analytics"; import {LieuCliqueCustomEvent} from "../components/vmd-appointment-card.component"; +import {PushNotifications} from "../utils/ServiceWorkers"; +import {DB} from "../storage/DB"; const MAX_DISTANCE_CENTRE_IN_KM = 100; @@ -237,6 +242,7 @@ export abstract class AbstractVmdRdvView extends LitElement { .rdvPossible="${false}" @prise-rdv-cliquee="${(event: LieuCliqueCustomEvent) => this.prendreRdv(event.detail.lieu)}" @verification-rdv-cliquee="${(event: LieuCliqueCustomEvent) => this.verifierRdv(event.detail.lieu)}" + @abonnement-clique="${(event: LieuCliqueCustomEvent) => this.sabonnerAuCentre(event.detail.lieu) }" >`; })} ` : html``} @@ -245,6 +251,20 @@ export abstract class AbstractVmdRdvView extends LitElement { `; } + async sabonnerAuCentre(lieu: Lieu) { + const outcome = await PushNotifications.INSTANCE.ensureGranted(); + if(outcome.granted) { + DB.INSTANCE.subscribeToCenterAppointments({ + ts: Date.now(), + departement: this.departementSelectionne!, + commune: this.communeSelectionnee, + // TODO: distance should be available once #117 will be merged + lieu: {...lieu, distance: undefined}, + notificationUrl: window.location.href + }); + } + } + onCommuneAutocompleteLoaded(autocompletes: string[]): Promise { return Promise.resolve(); } From 1e1eb42ed4215602d98ef2c428e8215dc18c7644 Mon Sep 17 00:00:00 2001 From: fcamblor Date: Sun, 25 Apr 2021 21:49:45 +0200 Subject: [PATCH 05/26] replaced es2017 lib to es2018 in order to fix error where AsyncIterableIterator weren't found : Cannot find name 'AsyncIterableIterator'. Do you need to change your target library? Try changing the compiler option to 'es2018' or later. --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index c8ade5a91..000b75812 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "ES5", "module": "esnext", - "lib": ["es2017", "dom", "dom.iterable"], + "lib": ["es2018", "dom", "dom.iterable"], "types": ["vite/client"], "declaration": true, "emitDeclarationOnly": true, From a7fb9f516bfab49051cad4ef27ffeacd58db31e6 Mon Sep 17 00:00:00 2001 From: fcamblor Date: Sun, 25 Apr 2021 22:46:39 +0200 Subject: [PATCH 06/26] implemented unsubscribe + provided some icon indicator on subscribe/unsubsribe button --- icons-src/config.json | 40 ++++++++++-- icons-src/custom-svg/eye-slash-solid.svg | 1 + icons-src/custom-svg/eye-solid.svg | 1 + public/assets/fonts/fontello/fontello.eot | Bin 7732 -> 8376 bytes public/assets/fonts/fontello/fontello.svg | 4 ++ public/assets/fonts/fontello/fontello.ttf | Bin 7564 -> 8208 bytes public/assets/fonts/fontello/fontello.woff | Bin 4512 -> 4976 bytes public/assets/fonts/fontello/fontello.woff2 | Bin 3712 -> 4156 bytes .../vmd-appointment-card.component.ts | 24 +++++-- src/state/State.ts | 6 ++ src/storage/DB.ts | 14 +++- src/styles/fontello-icons.scss | 2 + src/views/vmd-rdv.view.ts | 61 +++++++++++++----- 13 files changed, 126 insertions(+), 27 deletions(-) create mode 100644 icons-src/custom-svg/eye-slash-solid.svg create mode 100644 icons-src/custom-svg/eye-solid.svg diff --git a/icons-src/config.json b/icons-src/config.json index f581c342e..ce66b5346 100644 --- a/icons-src/config.json +++ b/icons-src/config.json @@ -6,12 +6,6 @@ "units_per_em": 1000, "ascent": 850, "glyphs": [ - { - "uid": "fc455b9530e0b37facc289f42a120fdd", - "css": "commerical-building", - "code": 59409, - "src": "maki" - }, { "uid": "7707c5b47c0a2b29afbfb10cdbab3e28", "css": "calendar-x-fill", @@ -109,6 +103,40 @@ "search": [ "arrow-up-right" ] + }, + { + "uid": "fc455b9530e0b37facc289f42a120fdd", + "css": "commerical-building", + "code": 59409, + "src": "maki" + }, + { + "uid": "5b954c88f6386ce35c5a88ffbc9ca6b6", + "css": "eye-solid", + "code": 59400, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M1118.2 471.5C1012.3 264.8 802.6 125 562.5 125S112.7 264.9 6.8 471.5A63.2 63.2 0 0 0 6.8 528.5C112.7 735.2 322.4 875 562.5 875S1012.3 735.1 1118.2 528.5A63.2 63.2 0 0 0 1118.2 471.5ZM562.5 781.3A281.3 281.3 0 1 1 843.8 500 281.1 281.1 0 0 1 562.5 781.3ZM562.5 312.5A186.2 186.2 0 0 0 513.1 319.9 93.5 93.5 0 0 1 382.4 450.6 187.1 187.1 0 1 0 562.5 312.5Z", + "width": 1125 + }, + "search": [ + "eye-solid" + ] + }, + { + "uid": "6c04e21c48bdcd8977470cc230f821e1", + "css": "eye-slash-solid", + "code": 59401, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M625 781.3C476.9 781.3 356.9 666.6 345.9 521.3L141 362.9C114.1 396.7 89.3 432.4 69.3 471.5A63.2 63.2 0 0 0 69.3 528.5C175.2 735.2 384.9 875 625 875 677.6 875 728.3 867.2 777.1 854.6L675.8 776.2A281.5 281.5 0 0 1 625 781.3ZM1237.9 894.7L1022 727.9A647 647 0 0 0 1180.7 528.5 63.2 63.2 0 0 0 1180.7 471.5C1074.8 264.8 865.1 125 625 125A601.9 601.9 0 0 0 337.3 198.6L88.8 6.6A31.3 31.3 0 0 0 44.9 12.1L6.6 61.4A31.3 31.3 0 0 0 12.1 105.3L1161.2 993.4A31.3 31.3 0 0 0 1205.1 987.9L1243.4 938.6A31.3 31.3 0 0 0 1237.9 894.7ZM879.1 617.4L802.3 558A185.1 185.1 0 0 0 812.5 500 185.1 185.1 0 0 0 575.6 319.9 93.1 93.1 0 0 1 593.8 375 91.1 91.1 0 0 1 590.7 394.5L447 283.4A277.9 277.9 0 0 1 625 218.8 281.1 281.1 0 0 1 906.3 500C906.3 542.2 895.9 581.6 879.1 617.4Z", + "width": 1250 + }, + "search": [ + "eye-slash-solid" + ] } ] } \ No newline at end of file diff --git a/icons-src/custom-svg/eye-slash-solid.svg b/icons-src/custom-svg/eye-slash-solid.svg new file mode 100644 index 000000000..e19a3c593 --- /dev/null +++ b/icons-src/custom-svg/eye-slash-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons-src/custom-svg/eye-solid.svg b/icons-src/custom-svg/eye-solid.svg new file mode 100644 index 000000000..b7d90c466 --- /dev/null +++ b/icons-src/custom-svg/eye-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/fonts/fontello/fontello.eot b/public/assets/fonts/fontello/fontello.eot index 8642cf1fbd85aec982ce31e233555f34047dc8bf..8ca28da8a4f7e92a7f3f5883580d46828085d162 100644 GIT binary patch delta 1205 zcmY*YZA@EL7=GV#&b_xEv;|xGF|m|_y-6v>GHB;0P>~UsEOV0#mnAL*y0+NTj#gxI zuEswQV=|W-qX{g_viLFmhavni(d?fI#zYr2`e$Z|{jnIMahW=)@4aO|JU8dQ&-1?T zb8^me&;2yM{gk}z18DYLmZPF;_4fMZ#2b~vtpNBe0Gyc1CyPI9e0G8OhctKRmdn(w zjh5%>Wj#|&=V!xzohP0l+LTU~ij;3AuIUZw+_^K3>z5b+C%thjlS{Knqr?OyLb<|R(tO^sO}tFpolmY5(QAE&_?yIii^+Uy zXT$9w{s{ovDHclQt()PivEtpXRRM~?brhm# zq6032fHBG&=%G#57drqm|)rriV?ZO0qVbq6uSj32tgbs6XX58OcFSy zpx_c0(rX|wF0qTW78k&?GBQMVk^;jd5IGj^@wgAfdg9@Dr{5j&gdTMmWOp})Q4RPN zD=MSK=&k6+MCnexyYtg2Rl1?;fS=;>(M+}$S;o{PkGog z-_H;3@%L8moAwz325u6sHo)$sBPK+3d?xq^e=Be*qT6@q(90871|yn1>j%0+j-%4j z2h(MunsJ9eCwYX_57rt0y@PGu(9wbBCXd%-EVOrMK28l)Q>GuM*Ke+y%Ne!o7eyQp13y5{;Illbxa0)Y6L>i>v4`j4?!)PuSDw%HdFDZOQv{km71 zfZy>7-eYmL#=ZP~aZp^-rDmAi{MKybE;C(3jshi=Q9%P5(S&Afs{UquF>FkoOZAir mx$L}6N4aDv)8O{GLO!2b%FZQoJ+m)mbMx87^yXRHP3wPeZ~XfJ delta 509 zcmdntxW$IeM2>->M|L8c8B5Iu>9mOsUG<*t7#J7>7#JABl5-Oa0v)f40r@L{_*QaR zi2_g@2>7mmh_r(A+$6o9RY2wwAm&L=EG__w3jo4i&MX3dK z+pM=SFvxtEynr#9@yz5ujOl?{42(co28I*{W-v46HytI|Ik&15CV(oFKo00LSEO%;B7ZK#f2Ql=Ne< zXPbPK=ekmw^oE*vew(ih{LC*v3h&HnlZMg%fBa8kemi*|uY?ry3n1q=^INDZK`J1C zbMgmX1qCKx{D6T6!zadhjNh1Sm^zr{nA^b0IT<`Q+wgHR3v)6uF)}l + + + + diff --git a/public/assets/fonts/fontello/fontello.ttf b/public/assets/fonts/fontello/fontello.ttf index 87fcd979526df733e04b93181eaa268c3507a2fe..02a9f848ad08a86b7a5bdd0d0a4a50f1e9728a7e 100644 GIT binary patch delta 1186 zcmY*YOKe+36uo!mef)Qf9e+x25-0dc$%}*C#4fdySRsv^;;OVn4OJy7$98Q8J2tT~ zX(=`8rdd>KP^1c^ra}nuDeNLeY$_pj#UgA_1qe2vN_Y{m08s@2HrLPTf_a*E&$;)W z(ad@CZbmobo4&@$UI2Uv0M4aLsmc%A_pcKFlx8bkuMJS1hV2!4SuRy_rRBh1SBU3{ znsTXXh4MYbJ9=HNc=?j!qgxDslU_fU&!#fZoJ-gNO#4(1=P98J3NHhghlvm8OSSdU zrF#1@B@&cSi{*66c*XoB@fvY=DYaffujMxJo5TlJQ>EZDlQ6xwaeK1?aE6dvF2&*jmI~st!m+fC_L8sT2)#zB&jnM|mARwCASO)HiMI zHX&&0x=7O@)g9m@jMT?TC4vMpW%b%R(vFQW>;aS3y`jAU3|l_FG^nH1Xi_gU_1#G- zXxh3H3_I8PjMU%&?O%l~dk8L2AqETa`H68R3Ye5p;35~qcR*xZWY=gdE&wk`$Pn3i z3Jg;~aJrlhS&}6#$xaJXBri_001B^8-Yb;$ODp##WtreA!Ote;f4@o2&iHA}%)a*o zB#*cqQgE)%I+9txIOJc(w{qJfsyecrGxwYGU6?obx4!$IN|WP5Y{-3cLCJBv@`A8X zU7PPX$lI-UUUiJNwD9FHVh`^KD2VuM*AP_%K0XlX7o`81Nw3js(J6Dh{xi-io zezv^=ApAo0zlBHIZ*f@Ef^lo#FrW%qt)<0&JFF$(cf5;_S&VIPFaKCLE_|p-jj*tz z8ui>|q>IQ=KoKRBQHOdopb<@53Cn8%efDy8tXeJ>GFA;0Q`LNz+tcMzDZ5rkr;20C QZxo7|!fI~kYs&-6e=$?^761SM delta 522 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 Gs5<~PQOOMe literal 3712 zcmV-`4uA1?Pew8T0RR9101kiv4*&oF03D0~01hVr0RR9100000000000000000000 z0000SR0dW6gIWj<37iZO2nvo2j6(|)00A}vBm*P_AO(d@2Z1#VfgBs88?zBMY#b0~ z{5B^0FAi*Dd4nw(Mpkw#*)tgX=uYj8>B1mM2-CNJE}N_!{!03~?vD?A9wEsrJWqE2 z{jXhBU5z7M40KP*oAR$`h7;tk@?qNB$9_NtR8t?Ew5 zNzfZMw#c7S>bFB~hh=1&s{b`j>3=KjSm|d6;8p>vfMfzl2wbncnp;V3MR)E^mr^P! z?Ep$c9>WwlrT5_-Hy!}PSO5=WS%x>(0j=u@Z~cP!s@+~=fc{7X2Ik!004phqH^-skuqMo0fRyC@+{_x`j>J52h?@-E%0>v`^)d?O@YGERPJis;l&5} zv7(aE8)lBPLvP|;xCD@LnF$?aO653KA2d8kkJJR2hLKE*Ig_f`x0#t4RA?82nt9pf zp0oP1@c!|{QW+d)=vg{FsF1Y~Ua?TTVxf7(!tjcP!z&h+S1jD2mCR1?tp7}Z<3l<>YStcJ53vEZyUn9k^m;(;95XR~t+*l`Ejd&WJ z_Zi6n>()ZZ+nZZQ5u~oZlU+6|U|(E^v+V<~CNzd$AF{Wee!~>+QdzsYmy>0mMRb)A zcV438I8(p{e(X*tKPK;ye2pfkZ&y^kMLI~v-46JWq5~vj^V?7@@QEmAa&ehk=L<0T z6BLWM<3kO_)(%B}9N_BW1`9D(v>59v4K7_ot#9-Yt`fP{0%)^3w|_?x(-zY31FSbe zNvX|^e@l!}Z(1ks%;y~+NTI2o#KAXpfi&V2iB!CpI(ur@m`M*|1JZSY3})L!E~wvz zI%9oBCW^*=GKwJC1+#ehAvN*v@(+cVrl-B3tlY3gfj;i#<`HX0sMwsW1?*zsG#qW|v zjA4xB!Yxj1VzK*m>{6*8M_?x7#p=@C!fND9h<&S*+m9~NHzP;IeKW<;l^mmqAT-u< z_tBOG8}n*s5#k%92oq&6oT!7(-V|0RZdu8#lmDiwNtnWx^{z95SfXsO1?;jY;TqCr z>N55j$0`nB0#iu9OzU2)MAs10oC6cyFo`6bqQd&S*BO(=1hn`PrGqj6^QQ2&COMug zPSb2koqF@m$<_$R9|ElHwtBwXlH-Hxjp!<%Tn+De(`DP^RY7EF!@Ny8{@WhG)Op}< zUQ&v*c6&Lg&t2=`x!u5g%A9*{z5NBVZ7}yyF9c)mSnow(+{ZAL(Q7xy5w;LEI(lBy z;$n(X(=R-|vl7>+w+x*Nn|gJ27y1{z1TE;Z2*vRHSF9qiG)Jz&4({!)^HHVLXBBB_ zHDm4}df85-rQVV|vAgFSVI31%KUfGG7}Lf~Sc*+dX!BqpY+)SUif_O|#Wq7|`w$`Q zFqn2`fnb**w0npU_89E$6^F%a9}+7gvHeKy0XmkkgECHs1`FXZWABJcSY#YUX&l2M z<2Xv=1WMy1O5+qt<21^iGiXfso_BVrkSH?GVZM9b#cc}<;ukIkve^p@?*o3*9Pb3JSN)DcmuqaMzr|J%#8#7ka=&Kg5h_JYr7c zQF9uPnbUY&A$o!fJ;|lvDg4HEYVRGpI+>o{l*~JybKJ%2qPN!jYwA_;;&+qx?ggFQ*dW%|8FPdgCZMmA=D3g=NMbBPt@>mx zsUk;b4wiuDk9(rjZDn(&lu#B=FssVo4mMMBfF%Ulimr_o0Kf(93_&F@0A(l0K=udo ze98e1x&Lmf{=pJZSvT8VnU#`S-vy~4Rv%JRvMV92XDXl-Xem4CAT6&UFRkl?kZOp} z+qhMYM7(v+&`kYe{ld!OtwTb;3k4qgL#kBl^XJPxK3MP2J`H(|zEm7DN=k9WMoZGt@# z^`eYZBJ=EVI~29U?dKaYh(&A*@*=OqwFrr@coK2vW7EEXR2%1{!bS|e13t$%SjA_e zz-Y*29&t6e{}ge;T@hHoz==1ExC|REmrHP#n2U1@C1#SJT5K*U-$=t^Ug42E@}ft7 z-LLt?-ACT2sjq#5BA=+KJ^#k9u1vrG?Q|c|)Xh#U)b(NEm-Aunue1Mtb?z^-eYwBP zz54H+#H2R9^6EiO`+-K@Pr3tdY6oEEW4)+d+`PwiMJKRh%gy|ACQJWCqqxiydc(JH zO@FT)qSn%biWXUg7A0EUMX867uobiyLDrT^{r$$)c;rW;eXYi`vTEgr^Xs-n?M4vc zELM5SdUqYI1Z71X^~G~tZ+jxBU@yMiU0WNqaqcmXB<_2Al$zKX9vaBzat-)dk1a`WN7=*isjB z#YZdZ3`++q)6UYm3fFNZ=2)k~YRfS#IwnS9Mwv>5f~&196$|wx3Z9+g(fXM9p7MO3 zipzAg(o(-?8xybD3n!vs6pf zxGlQA5I|r{?Z%UPQxkvlQn~{0>oEzlEhiWsMR%21hAeCbhQsB5jbb8s!jUleWnBjtXdZ z*+901CAt5BaFeR_rVV^!!3$K{W4nldK|mAS-4j$-u% z(h=#yUY|DfA=W3?pv!@N+oy1P0|!UA#A(Z%r>uK8xrIaU-Z5O?L%q86aA2u-n#(I= z@(-#VT;Q_PeI&ehSV8Z+Lg_+F(#g)bBoCojX< zndRa~4xQ;NpS+k#xEHQnKrB0y`~HuDV+SttI?s(=5m@;pKi!Z%67bpWC*; +export type ActionAbonnement = "unsubscribe"|"subscribe"; +export type AbonnementCliqueContext = LieuCliqueContext & {action: ActionAbonnement}; +export type AbonnementCliqueCustomEvent = CustomEvent; + @customElement('vmd-appointment-card') export class VmdAppointmentCardComponent extends LitElement { @@ -25,6 +29,7 @@ export class VmdAppointmentCardComponent extends LitElement { @property({type: Number, attribute: false}) distance!: number; /* dunno why, but boolean string is not properly converted to boolean when using attributes */ @property({type: Boolean, attribute: false }) rdvPossible!: boolean; + @property({type: Boolean, attribute: false}) watching!: boolean; private get estCliquable() { return !!this.lieu.url; @@ -135,7 +140,15 @@ export class VmdAppointmentCardComponent extends LitElement { Vérifier le centre de vaccination `:html``} @@ -145,9 +158,12 @@ export class VmdAppointmentCardComponent extends LitElement { } } - subscribe() { - this.dispatchEvent(new CustomEvent('abonnement-clique', { - detail: { lieu: this.lieu } + changeSubscription() { + this.dispatchEvent(new CustomEvent('abonnement-clique', { + detail: { + lieu: this.lieu, + action: this.watching?'unsubscribe':'subscribe' + } })); } diff --git a/src/state/State.ts b/src/state/State.ts index 952a71d0c..08359c127 100644 --- a/src/state/State.ts +++ b/src/state/State.ts @@ -85,6 +85,12 @@ export type Lieu = { type: TypeLieu; vaccine_type: string }; + +export function sameLieu(l1: Lieu, l2: Lieu) { + // TODO: do a better matching ? (based on an id on lieu ?) + return l1.nom === l2.nom; +} + function transformLieu(rawLieu: any): Lieu { return { ...rawLieu, diff --git a/src/storage/DB.ts b/src/storage/DB.ts index bce1309fc..d30280ae1 100644 --- a/src/storage/DB.ts +++ b/src/storage/DB.ts @@ -1,5 +1,5 @@ import {IDBPDatabase, openDB} from "idb"; -import {Commune, Departement, LieuAvecDistance} from "../state/State"; +import {Commune, Departement, Lieu, LieuAvecDistance, sameLieu} from "../state/State"; export type Subscription = { @@ -56,4 +56,16 @@ export class DB { return await this.db.transaction(["subscriptions"]).objectStore("subscriptions").getAll(); } + + async unsubscribeToCenterAppointments(lieu: Lieu) { + if(!this.db) { + throw new Error("DB not initialized !"); + } + + const subscriptions = await this.fetchAllSubscriptions(); + const subscriptionToDelete = await subscriptions.find(s => sameLieu(s.lieu, lieu)) + if(subscriptionToDelete) { + await this.db.transaction(["subscriptions"], "readwrite").objectStore("subscriptions").delete(subscriptionToDelete.ts); + } + } } diff --git a/src/styles/fontello-icons.scss b/src/styles/fontello-icons.scss index d55897f39..d1fc5afd0 100644 --- a/src/styles/fontello-icons.scss +++ b/src/styles/fontello-icons.scss @@ -62,4 +62,6 @@ .vmdicon-telephone-fill:before { content: '\e804'; } /* '' */ .vmdicon-geo-alt-fill:before { content: '\e805'; } /* '' */ .vmdicon-syringe:before { content: '\e806'; } /* '' */ +.vmdicon-eye-solid:before { content: '\e808'; } /* '' */ +.vmdicon-eye-slash-solid:before { content: '\e809'; } /* '' */ .vmdicon-commerical-building:before { content: '\e811'; } /* '' */ diff --git a/src/views/vmd-rdv.view.ts b/src/views/vmd-rdv.view.ts index c0e5cffc7..f3335be70 100644 --- a/src/views/vmd-rdv.view.ts +++ b/src/views/vmd-rdv.view.ts @@ -14,7 +14,7 @@ import { libelleUrlPathDuDepartement, Lieu, LieuxAvecDistanceParDepartement, - LieuxParDepartement, + LieuxParDepartement, sameLieu, State, TRIS_CENTRE } from "../state/State"; @@ -31,9 +31,12 @@ import {DEPARTEMENTS_LIMITROPHES} from "../utils/Departements"; import {ValueStrCustomEvent} from "../components/vmd-selector.component"; import {TemplateResult} from "lit-html"; import {Analytics} from "../utils/Analytics"; -import {LieuCliqueCustomEvent} from "../components/vmd-appointment-card.component"; +import { + AbonnementCliqueCustomEvent, ActionAbonnement, + LieuCliqueCustomEvent +} from "../components/vmd-appointment-card.component"; import {PushNotifications} from "../utils/ServiceWorkers"; -import {DB} from "../storage/DB"; +import {DB, Subscription} from "../storage/DB"; const MAX_DISTANCE_CENTRE_IN_KM = 100; @@ -58,6 +61,9 @@ export abstract class AbstractVmdRdvView extends LitElement { @property({type: Array, attribute: false}) lieuxParDepartementAffiches: LieuxAvecDistanceParDepartement | undefined = undefined; @property({type: Boolean, attribute: false}) searchInProgress: boolean = false; + @property({type: Array, attribute: false}) abonnements: Subscription[] = []; + protected refreshAbonnementsIntervalId: number|undefined = undefined; + protected derniereCommuneSelectionnee: Commune|undefined = undefined; @@ -220,6 +226,7 @@ export abstract class AbstractVmdRdvView extends LitElement { .lieu="${lieu}" .rdvPossible="${true}" .distance="${lieu.distance}" + .watching="${this.abonnementSur(lieu)}" @prise-rdv-cliquee="${(event: LieuCliqueCustomEvent) => this.prendreRdv(event.detail.lieu)}" @verification-rdv-cliquee="${(event: LieuCliqueCustomEvent) => this.verifierRdv(event.detail.lieu)}" />`; @@ -240,9 +247,10 @@ export abstract class AbstractVmdRdvView extends LitElement { style="--list-index: ${index}" .lieu="${lieu}" .rdvPossible="${false}" + .watching="${this.abonnementSur(lieu)}" @prise-rdv-cliquee="${(event: LieuCliqueCustomEvent) => this.prendreRdv(event.detail.lieu)}" @verification-rdv-cliquee="${(event: LieuCliqueCustomEvent) => this.verifierRdv(event.detail.lieu)}" - @abonnement-clique="${(event: LieuCliqueCustomEvent) => this.sabonnerAuCentre(event.detail.lieu) }" + @abonnement-clique="${(event: AbonnementCliqueCustomEvent) => this.changerAbonnementAuLieu(event.detail.lieu, event.detail.action) }" >`; })} ` : html``} @@ -251,17 +259,23 @@ export abstract class AbstractVmdRdvView extends LitElement { `; } - async sabonnerAuCentre(lieu: Lieu) { - const outcome = await PushNotifications.INSTANCE.ensureGranted(); - if(outcome.granted) { - DB.INSTANCE.subscribeToCenterAppointments({ - ts: Date.now(), - departement: this.departementSelectionne!, - commune: this.communeSelectionnee, - // TODO: distance should be available once #117 will be merged - lieu: {...lieu, distance: undefined}, - notificationUrl: window.location.href - }); + async changerAbonnementAuLieu(lieu: Lieu, action: ActionAbonnement) { + if(action === 'subscribe') { + const outcome = await PushNotifications.INSTANCE.ensureGranted(); + if(outcome.granted) { + await DB.INSTANCE.subscribeToCenterAppointments({ + ts: Date.now(), + departement: this.departementSelectionne!, + commune: this.communeSelectionnee, + // TODO: distance should be available once #117 will be merged + lieu: {...lieu, distance: undefined}, + notificationUrl: window.location.href + }); + await this.refreshAbonnements(); + } + } else if(action === 'unsubscribe') { + await DB.INSTANCE.unsubscribeToCenterAppointments(lieu); + await this.refreshAbonnements(); } } @@ -287,9 +301,23 @@ export abstract class AbstractVmdRdvView extends LitElement { ]) await this.onceStartupPromiseResolved(); + + await this.refreshAbonnements(); + this.refreshAbonnementsIntervalId = setInterval(async() => { + await this.refreshAbonnements(); + }, 60000); + await this.refreshLieux(); } + async refreshAbonnements() { + this.abonnements = await DB.INSTANCE.fetchAllSubscriptions(); + } + + abonnementSur(lieu: Lieu) { + return !!this.abonnements.find(a => sameLieu(a.lieu, lieu)); + } + preventRafraichissementLieux(): boolean { // overridable return false; @@ -337,7 +365,8 @@ export abstract class AbstractVmdRdvView extends LitElement { disconnectedCallback() { super.disconnectedCallback(); - // console.log("disconnected callback") + + clearInterval(this.refreshAbonnementsIntervalId); } _onRefreshPageWhenValidParams(): "return"|"continue" { From 8f2eff449e70ee780dc03a48e994798ad85c6309 Mon Sep 17 00:00:00 2001 From: fcamblor Date: Sun, 25 Apr 2021 23:50:05 +0200 Subject: [PATCH 07/26] variabilized serviceworker root url --- public/sw.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/public/sw.js b/public/sw.js index a10cbd800..8686aacc6 100644 --- a/public/sw.js +++ b/public/sw.js @@ -9,11 +9,13 @@ const VMD_BASE_URL = USE_RAW_GITHUB let getVersionPort = undefined; let pushNotificationsGranted=false; +let clientRootUrl = undefined; -// self.addEventListener('install', function(event) { -// console.log('Service Worker activating...'); -// event.waitUntil(self.skipWaiting()); // Activate worker immediately -// }); +self.addEventListener('install', function(event) { + console.log('Service Worker activating...'); + clientRootUrl = event.target.location.href.replace("sw.js",""); + // event.waitUntil(self.skipWaiting()); // Activate worker immediately +}); self.addEventListener('activate', function(event) { console.log('Service Worker activating...'); @@ -66,10 +68,10 @@ function checkSubscriptions() { lang: 'fr-FR', body: abonnementAvecRvdDispos.appointment_count+" créneaux de vaccination trouvés sur " + abonnementAvecRvdDispos.subscription.lieu.nom+" ("+abonnementAvecRvdDispos.subscription.departement.code_departement+")", - badge: 'https://deca76fefa39.ngrok.io/assets/images/png/vmd-badge.png', - icon: 'https://deca76fefa39.ngrok.io/assets/images/favicon/android-chrome-512x512.png', + badge: clientRootUrl+'assets/images/png/vmd-badge.png', + icon: clientRootUrl+'assets/images/favicon/android-chrome-512x512.png', // That's too big.. the icon above is enough - // image: 'https://deca76fefa39.ngrok.io/assets/images/favicon/android-chrome-512x512.png', + // image: clientRootUrl+'assets/images/favicon/android-chrome-512x512.png', data: { notificationUrl: abonnementAvecRvdDispos.subscription.notificationUrl, departement: abonnementAvecRvdDispos.subscription.departement, From 05f9e7ed29b02c9ac95a06ed1572c291a379f0f6 Mon Sep 17 00:00:00 2001 From: fcamblor Date: Mon, 26 Apr 2021 00:48:05 +0200 Subject: [PATCH 08/26] improved indexeddb versioning management, not handling it into serviceworker (as we're sure to migrate DB in app before sw) --- public/sw.js | 19 +------------------ src/storage/DB.ts | 6 ++---- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/public/sw.js b/public/sw.js index 8686aacc6..25e17076c 100644 --- a/public/sw.js +++ b/public/sw.js @@ -136,24 +136,7 @@ class DB { static initialize() { var _this = DB._INSTANCE; - idb.openDB('vite-ma-dose', 1, { - upgrade(db, oldVersion, newVersion, transaction) { - switch(oldVersion) { - case 0: - db.createObjectStore('subscriptions', { keyPath: 'ts' }) - break; - } - }, - blocked() { - console.error("openDB blocked") - }, - blocking() { - console.warn("Blocking openDB") - }, - terminated() { - console.info("Terminated openDB") - }, - }).then(function(db) { + idb.openDB('vite-ma-dose', 1).then(function(db) { _this.dbResolver(db); }); return _this.dbPromise; diff --git a/src/storage/DB.ts b/src/storage/DB.ts index d30280ae1..097b0f277 100644 --- a/src/storage/DB.ts +++ b/src/storage/DB.ts @@ -22,10 +22,8 @@ export class DB { public async initialize(): Promise { this.db = await openDB('vite-ma-dose', 1, { upgrade(db, oldVersion, newVersion, transaction) { - switch(oldVersion) { - case 0: - db.createObjectStore('subscriptions', { keyPath: 'ts' }) - break; + if(oldVersion < 1) { + db.createObjectStore('subscriptions', { keyPath: 'ts' }) } }, blocked() { From 2f1673a952bea498756f284d43f87285f339434f Mon Sep 17 00:00:00 2001 From: fcamblor Date: Mon, 26 Apr 2021 00:49:18 +0200 Subject: [PATCH 09/26] introduced debug mode, allowing to force appointments for subscriptions once enabled. click 5 times on 'vite ma dose' footer to enable/disable this debug mode --- public/sw.js | 13 ++++++++++++- src/storage/DB.ts | 22 +++++++++++++++++++++- src/vmd-app.component.ts | 11 ++++++++++- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/public/sw.js b/public/sw.js index 25e17076c..49d22f91c 100644 --- a/public/sw.js +++ b/public/sw.js @@ -96,6 +96,12 @@ function rechercherAbonnementsAvecRdvDispos(subscriptions) { .then(function(resp) { return resp.json(); }) .then(function(centres) { return { centres: centres, codeDepartement: codeDepartement }; }) })).then(function(results) { + return DB.instance().then(function(db) { + return db.transaction(['debug']).objectStore("debug").count() + }).then(function(count) { return { results: results, debugEnabled: count !== 0 }; }); + }).then(function(_) { + const results = _.results; + const debugEnabled = _.debugEnabled; return results.reduce(function(abosAvecRdvDispos, centresParDepartement) { return subscriptionsByCodesDepartement.get(centresParDepartement.codeDepartement).reduce(function(abosAvecRdvDispos, subscription) { const centre = centresParDepartement.centres.centres_disponibles.find(function(l) { @@ -107,6 +113,11 @@ function rechercherAbonnementsAvecRdvDispos(subscriptions) { subscription: subscription, appointment_count: centre.appointment_count }); + } else if(debugEnabled) { + abosAvecRdvDispos.push({ + subscription: subscription, + appointment_count: Math.round(Math.random()*100) + }); } return abosAvecRdvDispos; }, abosAvecRdvDispos); @@ -136,7 +147,7 @@ class DB { static initialize() { var _this = DB._INSTANCE; - idb.openDB('vite-ma-dose', 1).then(function(db) { + idb.openDB('vite-ma-dose', 2).then(function(db) { _this.dbResolver(db); }); return _this.dbPromise; diff --git a/src/storage/DB.ts b/src/storage/DB.ts index 097b0f277..1963f9eed 100644 --- a/src/storage/DB.ts +++ b/src/storage/DB.ts @@ -20,11 +20,14 @@ export class DB { } public async initialize(): Promise { - this.db = await openDB('vite-ma-dose', 1, { + this.db = await openDB('vite-ma-dose', 2, { upgrade(db, oldVersion, newVersion, transaction) { if(oldVersion < 1) { db.createObjectStore('subscriptions', { keyPath: 'ts' }) } + if(oldVersion < 2) { + db.createObjectStore('debug', {keyPath: 'name'}) + } }, blocked() { console.error("openDB blocked") @@ -66,4 +69,21 @@ export class DB { await this.db.transaction(["subscriptions"], "readwrite").objectStore("subscriptions").delete(subscriptionToDelete.ts); } } + + async switchDebugMode() { + if(!this.db) { + throw new Error("DB not initialized !"); + } + + const debugs = await this.db.transaction(["debug"]).objectStore("debug").getAll() + const debugStoreExists = (debugs.length!==0); + + if(debugStoreExists) { + await this.db.transaction(["debug"], "readwrite").objectStore("debug").delete(debugs[0].name); + alert("Switched to debugMode=disabled") + } else { + await this.db.transaction(["debug"], "readwrite").objectStore("debug").add({name: 'enabled'}); + alert("Switched to debugMode=enabled") + } + } } diff --git a/src/vmd-app.component.ts b/src/vmd-app.component.ts index 9b52e2619..7b11f9048 100644 --- a/src/vmd-app.component.ts +++ b/src/vmd-app.component.ts @@ -60,7 +60,7 @@ export class VmdAppComponent extends LitElement {