Skip to content

Commit 2d5267d

Browse files
User: Add notifications push support in webbrowsers - refs #3255
Author: @christianbeeznest
1 parent 835949e commit 2d5267d

File tree

17 files changed

+1432
-12
lines changed

17 files changed

+1432
-12
lines changed

assets/vue/App.vue

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,5 +186,16 @@ watch(
186186
onMounted(async () => {
187187
mejsLoader()
188188
await securityStore.checkSession()
189+
190+
if ("serviceWorker" in navigator) {
191+
navigator.serviceWorker
192+
.register("/service-worker.js")
193+
.then((registration) => {
194+
console.log("[PWA] Service Worker registered with scope:", registration.scope)
195+
})
196+
.catch((error) => {
197+
console.error("[PWA] Service Worker registration failed:", error)
198+
})
199+
}
189200
})
190201
</script>

assets/vue/components/social/UserProfileCard.vue

Lines changed: 108 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,65 @@
9696
</div>
9797

9898
<Divider />
99+
<div
100+
v-if="pushEnabled"
101+
class="mt-4 w-full text-center"
102+
>
103+
<p
104+
v-if="loading || isSubscribed === null"
105+
class="text-gray-500 text-sm"
106+
>
107+
<i class="mdi mdi-loading mdi-spin mr-2"></i>
108+
{{ t("Checking push subscription...") }}
109+
</p>
110+
111+
<div v-else>
112+
<template v-if="isSubscribed">
113+
<div class="flex flex-col items-center text-green-700">
114+
<i class="mdi mdi-bell-ring-outline text-4xl mb-2"></i>
115+
<p class="text-sm font-semibold">
116+
{{ t("You're subscribed to push notifications in this browser.") }}
117+
</p>
118+
<BaseButton
119+
:label="t('Unsubscribe')"
120+
class="mt-2"
121+
icon="bell-off"
122+
type="danger"
123+
size="small"
124+
@click="handleUnsubscribe"
125+
:loading="loading"
126+
/>
127+
<div
128+
v-if="showDetails"
129+
class="mt-2 bg-gray-100 rounded p-2 text-gray-800 text-xs break-all max-w-full"
130+
>
131+
<strong>{{ t("Endpoint") }}:</strong>
132+
<br />
133+
{{ subscriptionInfo?.endpoint }}
134+
</div>
135+
</div>
136+
</template>
137+
138+
<template v-else>
139+
<div class="flex flex-col items-center text-red-700">
140+
<i class="mdi mdi-bell-off-outline text-4xl mb-2"></i>
141+
<p class="text-sm">
142+
{{ t("Push notifications are not enabled in this browser.") }}
143+
</p>
144+
<BaseButton
145+
:label="t('Enable notifications')"
146+
class="mt-2"
147+
icon="bell"
148+
type="primary"
149+
size="small"
150+
@click="handleSubscribe"
151+
:loading="loading"
152+
/>
153+
</div>
154+
</template>
155+
</div>
156+
</div>
157+
99158
<BaseButton
100159
v-if="isCurrentUser || securityStore.isAdmin"
101160
:label="t('Edit profile')"
@@ -117,14 +176,15 @@
117176
</template>
118177

119178
<script setup>
120-
import { computed, inject, ref, watchEffect } from "vue"
179+
import { computed, inject, onMounted, ref, watchEffect } from "vue"
121180
import BaseCard from "../basecomponents/BaseCard.vue"
122181
import BaseButton from "../basecomponents/BaseButton.vue"
123182
import { useI18n } from "vue-i18n"
124183
import Divider from "primevue/divider"
125184
import axios from "axios"
126185
import { useSecurityStore } from "../../store/securityStore"
127186
import BaseUserAvatar from "../basecomponents/BaseUserAvatar.vue"
187+
import { usePushSubscription } from "../../composables/usePushSubscription"
128188
129189
const { t } = useI18n()
130190
const securityStore = useSecurityStore()
@@ -138,17 +198,31 @@ const showFullProfile = computed(() => isCurrentUser.value || securityStore.isAd
138198
const languageInfo = ref(null)
139199
const vCardUserLink = ref("")
140200
const visibility = ref({})
141-
watchEffect(() => {
142-
if (user.value && user.value.id) {
143-
fetchUserProfile(user.value.id)
144-
}
145-
})
146201
147-
const editProfile = () => {
202+
const {
203+
isSubscribed,
204+
subscriptionInfo,
205+
subscribe,
206+
unsubscribe,
207+
loading,
208+
checkSubscription,
209+
loadVapidKey,
210+
vapidPublicKey,
211+
pushEnabled,
212+
registerServiceWorker,
213+
} = usePushSubscription()
214+
215+
const showDetails = ref(false)
216+
217+
function toggleDetails() {
218+
showDetails.value = !showDetails.value
219+
}
220+
221+
function editProfile() {
148222
window.location = "/account/edit"
149223
}
150224
151-
const changePassword = () => {
225+
function changePassword() {
152226
window.location = "/account/change-password"
153227
}
154228
@@ -170,8 +244,33 @@ async function fetchUserProfile(userId) {
170244
171245
function flagIconExists(code) {
172246
const mdiFlagIcons = ["us", "fr", "de", "es", "it", "pl"]
173-
return mdiFlagIcons.includes(code.toLowerCase())
247+
return mdiFlagIcons.includes(code?.toLowerCase())
174248
}
175249
176250
function chatWith(userId, completeName, isOnline, avatarSmall) {}
251+
252+
watchEffect(async () => {
253+
if (user.value && user.value.id) {
254+
fetchUserProfile(user.value.id)
255+
loadVapidKey()
256+
await registerServiceWorker()
257+
await checkSubscription(user.value.id)
258+
}
259+
})
260+
261+
async function handleSubscribe() {
262+
if (user.value?.id) {
263+
await subscribe(user.value.id)
264+
} else {
265+
console.error("[Push] No user id for subscription.")
266+
}
267+
}
268+
269+
async function handleUnsubscribe() {
270+
if (user.value?.id) {
271+
await unsubscribe(user.value.id)
272+
} else {
273+
console.error("[Push] No user id for unsubscription.")
274+
}
275+
}
177276
</script>

0 commit comments

Comments
 (0)