Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web push notifications #107

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:16.20.1-alpine3.17 as build-tools
FROM node:16.20.2-alpine3.18 as build-tools
LABEL maintainer="kernoeb <[email protected]>"

RUN apk add --no-cache curl bash
Expand Down Expand Up @@ -48,7 +48,7 @@ RUN ls node_modules/node-libcurl/lib/binding/
# Clean node_modules, one of the heaviest object in the universe
RUN clean-modules --yes --exclude "**/*.mustache"

FROM node:16.20.1-alpine3.17 as app
FROM node:16.20.2-alpine3.18 as app

RUN apk --no-cache add dumb-init curl bash

Expand Down
134 changes: 134 additions & 0 deletions components/DialogSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,31 @@
@change="$emit('change_settings', $event)"
>
<v-subheader>{{ $config.i18n.ui }}</v-subheader>

<v-list-item inactive style="cursor:pointer;" :disabled="loadingSubscription || notifications === undefined">
<v-list-item-action v-if="loadingSubscription">
<v-progress-circular
indeterminate
size="20"
color="primary"
/>
</v-list-item-action>
<v-list-item-action v-else>
<v-checkbox
v-model="checkedNotifications"
:disabled="loadingSubscription || notifications === undefined"
:indeterminate-icon="mdiCheckboxBlankOutline"
:off-icon="mdiCheckboxBlankOutline"
:on-icon="mdiCheckboxMarked"
/>
</v-list-item-action>

<v-list-item-content @click="subscriptions()">
<v-list-item-title>Activer les notifications des favoris</v-list-item-title>
<v-list-item-subtitle>Web Push notifications</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>

<v-list-item inactive style="cursor:pointer;">
<v-list-item-action>
<v-checkbox
Expand Down Expand Up @@ -305,6 +330,8 @@ export default {
colorOthers: '#eddd6e',

fullDark: false,
notifications: undefined,
loadingSubscription: false,
mergeDuplicates: true,
highlightTeacher: false
}
Expand Down Expand Up @@ -334,6 +361,14 @@ export default {
this.switchMergeDuplicates()
}
},
checkedNotifications: {
get () {
return this.notifications
},
set () {
this.subscriptions()
}
},
checkedHighlightTeacher: {
get () {
return this.highlightTeacher
Expand Down Expand Up @@ -363,6 +398,8 @@ export default {
}
},
mounted () {
console.log('Mounted settings')

if (this.$cookies.get('blocklist') !== undefined) {
try {
const tmp = JSON.parse(this.$cookies.get('blocklist', { parseJSON: false }))
Expand Down Expand Up @@ -391,8 +428,105 @@ export default {
} catch (err) {
this.highlightTeacher = false
}

// eslint-disable-next-line nuxt/no-env-in-hooks
if (process.client) {
setTimeout(async () => {
if ('serviceWorker' in navigator) {
console.log('Service worker exists')
await navigator.serviceWorker.register('/notifications-worker.js', { scope: '/' })
console.log('Service worker ready')
this.notifications = !!(navigator.serviceWorker.ready && await navigator.serviceWorker.ready.then(registration => registration.pushManager.getSubscription()))
console.log('Is subscribed', this.notifications)
}
}, 10)
}
},
methods: {
urlBase64ToUint8Array (base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4)
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/')

const rawData = window.atob(base64)
const outputArray = new Uint8Array(rawData.length)

for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
},
async subscriptions () {
console.log('Running push')
if (!('serviceWorker' in navigator)) return console.log('Service workers are not supported by this browser')

this.loadingSubscription = true
try {
const registration = await navigator.serviceWorker.ready

// if already subscribed, unsubscribe
if (this.notifications) {
console.log('Unregistering push')
const subscription = await registration.pushManager.getSubscription()
await subscription.unsubscribe()

await this.$axios.$post('/api/v1/subscriptions/unsubscribe', subscription, {
headers: {
'content-type': 'application/json'
}
})

localStorage.removeItem('subscription')

console.log('Unregistered push')
this.notifications = false
this.loadingSubscription = false
return
}

// ask for permission
if (Notification.permission !== 'granted') await Notification.requestPermission()

console.log('Registering push')
const publicVapidKey = this.$config.publicVapidKey
if (!publicVapidKey) {
this.loadingSubscription = false
return console.log('Vapid key is not defined')
}

const subscription = (await registration.pushManager
.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlBase64ToUint8Array(publicVapidKey)
})).toJSON()

console.log('Registered push')

try {
subscription.plannings = this.$cookies.get('favorites', { parseJSON: false })?.split(',') || []
} catch (err) {
subscription.plannings = []
}

localStorage.setItem('subscription', JSON.stringify(subscription))

console.log('Sending push')
await this.$axios.$post('/api/v1/subscriptions/subscribe', subscription, {
headers: {
'content-type': 'application/json'
}
})

this.notifications = true

console.log('Sent push')
} catch (err) {
alert(JSON.stringify(err, ['message', 'arguments', 'type', 'name']))
console.error(err)
}
this.loadingSubscription = false
},
forceFullMode () {
this.fullDark = !this.fullDark
this.$cookies.set('fullDark', this.fullDark, { maxAge: 2147483646 })
Expand Down
13 changes: 12 additions & 1 deletion components/SelectPlanning.vue
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,6 @@ export default {
const final = [...new Set(tmp)].filter(v => !!v)
this.favorites = final
this.$cookies.set('favorites', final.join(','), { maxAge: 2147483646 })
this.getNames()
this.$nextTick(() => {
this.refreshFavorites()
})
Expand Down Expand Up @@ -455,6 +454,18 @@ export default {

this.getNames()

try {
const subscription = JSON.parse(localStorage.getItem('subscription') || '{}')
if (subscription && subscription.endpoint) {
const plannings = [...this.favorites || []]
this.$axios.$put('/api/v1/subscriptions/update-plannings ', { subscription, plannings }).catch((err) => {
console.log('Error updating favorites for notifications', err)
})
}
} catch (err) {
console.log('Error updating favorites for notifications', err)
}

if (this.$cookies?.get('favorites') === '') this.$cookies.remove('favorites')
if (this.$cookies?.get('groupFavorites') === '') this.$cookies.remove('groupFavorites')
} catch (err) {}
Expand Down
2 changes: 0 additions & 2 deletions config/custom-environment-variables.json

This file was deleted.

49 changes: 0 additions & 49 deletions config/default.json

This file was deleted.

2 changes: 0 additions & 2 deletions config/development.json

This file was deleted.

2 changes: 0 additions & 2 deletions config/production.json

This file was deleted.

2 changes: 1 addition & 1 deletion jobs/updatePlannings.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const { fetchAndGetJSON } = require('../server/util/utils')
const { Planning } = require('../server/models/planning')

mongoose.connect(`mongodb://${process.env.MONGODB_URL || 'localhost:27017'}/planningsup`).then(async (v) => {
logger.info('BREE Mongo initialized !')
logger.info('BREE UpdatePlanning - Mongo initialized !')

const instance = axios.create({
timeout: 5000,
Expand Down
54 changes: 52 additions & 2 deletions nuxt.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import colors from 'vuetify/es5/util/colors'
import fr from 'vuetify/es5/locale/fr'
import minifyTheme from 'minify-css-string'

console.log(process.env)

const { NODE_ENV = 'production' } = process.env
const isDev = NODE_ENV === 'development'

Expand Down Expand Up @@ -102,8 +105,6 @@ export default {
'@nuxtjs/component-cache',
// https://saintplay.github.io/vue-swatches/
'vue-swatches/nuxt',
// https://github.com/Djancyp/nuxt-config#readme
'nuxt-json-config',
[
'@dansmaculotte/nuxt-security',
{
Expand Down Expand Up @@ -167,12 +168,61 @@ export default {
enableAutoPageviews: true,
enableAutoOutboundTracking: true
},

publicRuntimeConfig: {
plausible: {
domain: DOMAIN,
apiHost: 'https://' + PLAUSIBLE_DOMAIN,
enableAutoPageviews: true,
enableAutoOutboundTracking: true
},
publicVapidKey: process.env.PUBLIC_VAPID_KEY,
name: 'PlanningSup',
i18n: {
week: 'Semaine',
weeks: 'Semaines',
day: 'Jour',
month: 'Mois',
today: "Aujourd'hui",
error1: 'Bon y a eu un soucis.',
error2: 'Revient plus tard bg.',
chooseEdt: 'Plannings',
changeEdt: "Changer d'EDT",
changeTheme: 'Changer le thème',
offline: 'Hors connexion',
donate: 'Faire un don',
projectPage: 'Code source',
settings: 'Paramètres',
lightThemeMsg: 'Activer le thème clair',
lightThemeDesc: 'Idéal pour perdre la vue',
blocklist: 'Liste noire',
blocklistDesc: 'Cache les cours contenant le(s) mot(s)',
ui: 'Interface',
error_db: "Le serveur de l'université choisie est indisponible (ou en galère), voici une version sauvegardée datant du ",
error_db_only: 'Planning temporairement indisponible',
error_db_one: 'Au moins un planning temporairement indisponible',
error_saved: 'version sauvegardée du',
error_saved2: 'version sauvegardée',
error_db2: "Le serveur de l'université choisie est indisponible (ou en galère), voici une version sauvegardée.",
error_db_all: "Oups, aucun planning n'est disponible, désolé !",
close: 'Fermer',
distance: 'DISTANCIEL',
mode: 'Mode',
contact: 'Me contacter',
multiplePlannings: 'Multiples plannings',
reset: 'Tout désélectionner',
selection: 'Sélection',
searchPlanning: 'Rechercher un planning',
selectedPlannings: 'plannings sélectionnés',
colors: 'Couleurs des cours',
others: 'Autres',
amphi: 'Amphis',
types: {
td: 'Travaux dirigés',
tp: 'Travaux pratiques',
amphi: 'Amphithéâtre',
other: 'Les cours random'
}
}
},

Expand Down
Loading