forked from elk-zone/elk
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: pwa with push notifications (elk-zone#337)
- Loading branch information
Showing
48 changed files
with
3,002 additions
and
113 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
MOCK_USER='{"user":{"server":"universeodon.com","token":"BLMfvYGgiEPgLpiunVS0JYxxqzga3S58C60DDwu1jvw","account":{"id":"109424142224653388","username":"elkdev","acct":"[email protected]","displayName":"Elk Dev Team","locked":false,"bot":false,"discoverable":null,"group":false,"createdAt":"2022-11-28T00:00:00.000Z","note":"","url":"https://universeodon.com/@elkdev","avatar":"https://universeodon.com/avatars/original/missing.png","avatarStatic":"https://universeodon.com/avatars/original/missing.png","header":"https://universeodon.com/headers/original/missing.png","headerStatic":"https://universeodon.com/headers/original/missing.png","followersCount":0,"followingCount":0,"statusesCount":0,"lastStatusAt":null,"noindex":false,"source":{"privacy":"public","sensitive":false,"language":null,"note":"","fields":[],"followRequestsCount":0},"emojis":[],"fields":[],"role":{"id":"-99","name":"","permissions":"65536","color":"","highlighted":false}}},"server":{"109424142224653388":{"uri":"universeodon.com","title":"Universeodon","shortDescription":"Be one with the fediverse.","description":"","email":"[email protected]","version":"4.0.2","urls":{"streamingApi":"wss://universeodon.com"},"stats":{"userCount":57026,"statusCount":283364,"domainCount":11515},"thumbnail":"https://media.universeodon.com/site_uploads/files/000/000/003/@1x/9de6fc1bbd150b05.png","languages":["en"],"registrations":true,"approvalRequired":false,"invitesEnabled":true,"configuration":{"accounts":{"maxFeaturedTags":10},"statuses":{"maxCharacters":500,"maxMediaAttachments":4,"charactersReservedPerUrl":23},"mediaAttachments":{"supportedMimeTypes":["image/jpeg","image/png","image/gif","image/heic","image/heif","image/webp","image/avif","video/webm","video/mp4","video/quicktime","video/ogg","audio/wave","audio/wav","audio/x-wav","audio/x-pn-wave","audio/vnd.wave","audio/ogg","audio/vorbis","audio/mpeg","audio/mp3","audio/webm","audio/flac","audio/aac","audio/m4a","audio/x-m4a","audio/mp4","audio/3gpp","video/x-ms-asf"],"imageSizeLimit":10485760,"imageMatrixLimit":16777216,"videoSizeLimit":41943040,"videoFrameRateLimit":60,"videoMatrixLimit":2304000},"polls":{"maxOptions":4,"maxCharactersPerOption":50,"minExpiration":300,"maxExpiration":2629746}},"contactAccount":{"id":"109287809647205395","username":"supernovae","acct":"supernovae","displayName":"Supernovae","locked":false,"bot":false,"discoverable":true,"group":false,"createdAt":"2022-11-04T00:00:00.000Z","note":"","url":"https://universeodon.com/@supernovae","avatar":"https://media.universeodon.com/accounts/avatars/109/287/809/647/205/395/original/551eafba585d19e5.jpg","avatarStatic":"https://media.universeodon.com/accounts/avatars/109/287/809/647/205/395/original/551eafba585d19e5.jpg","header":"https://media.universeodon.com/accounts/headers/109/287/809/647/205/395/original/5de388c5945925c5.jpg","headerStatic":"https://media.universeodon.com/accounts/headers/109/287/809/647/205/395/original/5de388c5945925c5.jpg","followersCount":6387,"followingCount":305,"statusesCount":1753,"lastStatusAt":"2022-11-28","noindex":false,"emojis":[],"fields":[]},"rules":[]}}}' | ||
MOCK_USER='{"user":{"server":"universeodon.com","token":"yZcpj0FmnsEkUvBiXSCb_KQnccl2IU0kx9TfDbcxPJY","vapidKey":"BJwtUVlyCabpMnLI6HOyu-qMfJswxEq_c8pgRymxjTN_vCzMWfGrRHrwNczj9LIokAHtxh6Ziw1Kq7_ERDoriz0=","account":{"id":"109424142224653388","username":"elkdev","acct":"[email protected]","displayName":"Elk Dev Team","locked":false,"bot":false,"discoverable":null,"group":false,"createdAt":"2022-11-28T00:00:00.000Z","note":"","url":"https://universeodon.com/@elkdev","avatar":"https://universeodon.com/avatars/original/missing.png","avatarStatic":"https://universeodon.com/avatars/original/missing.png","header":"https://universeodon.com/headers/original/missing.png","headerStatic":"https://universeodon.com/headers/original/missing.png","followersCount":3,"followingCount":4,"statusesCount":20,"lastStatusAt":"2022-12-13","noindex":false,"source":{"privacy":"public","sensitive":false,"language":null,"note":"","fields":[],"followRequestsCount":0},"emojis":[],"fields":[],"role":{"id":"-99","name":"","permissions":"65536","color":"","highlighted":false}}},"server":{"109424142224653388":{"uri":"universeodon.com","title":"Universeodon","shortDescription":"Be one with the fediverse.","description":"","email":"[email protected]","version":"4.0.2","urls":{"streamingApi":"wss://universeodon.com"},"stats":{"userCount":57026,"statusCount":283364,"domainCount":11515},"thumbnail":"https://media.universeodon.com/site_uploads/files/000/000/003/@1x/9de6fc1bbd150b05.png","languages":["en"],"registrations":true,"approvalRequired":false,"invitesEnabled":true,"configuration":{"accounts":{"maxFeaturedTags":10},"statuses":{"maxCharacters":500,"maxMediaAttachments":4,"charactersReservedPerUrl":23},"mediaAttachments":{"supportedMimeTypes":["image/jpeg","image/png","image/gif","image/heic","image/heif","image/webp","image/avif","video/webm","video/mp4","video/quicktime","video/ogg","audio/wave","audio/wav","audio/x-wav","audio/x-pn-wave","audio/vnd.wave","audio/ogg","audio/vorbis","audio/mpeg","audio/mp3","audio/webm","audio/flac","audio/aac","audio/m4a","audio/x-m4a","audio/mp4","audio/3gpp","video/x-ms-asf"],"imageSizeLimit":10485760,"imageMatrixLimit":16777216,"videoSizeLimit":41943040,"videoFrameRateLimit":60,"videoMatrixLimit":2304000},"polls":{"maxOptions":4,"maxCharactersPerOption":50,"minExpiration":300,"maxExpiration":2629746}},"contactAccount":{"id":"109287809647205395","username":"supernovae","acct":"supernovae","displayName":"Supernovae","locked":false,"bot":false,"discoverable":true,"group":false,"createdAt":"2022-11-04T00:00:00.000Z","note":"","url":"https://universeodon.com/@supernovae","avatar":"https://media.universeodon.com/accounts/avatars/109/287/809/647/205/395/original/551eafba585d19e5.jpg","avatarStatic":"https://media.universeodon.com/accounts/avatars/109/287/809/647/205/395/original/551eafba585d19e5.jpg","header":"https://media.universeodon.com/accounts/headers/109/287/809/647/205/395/original/5de388c5945925c5.jpg","headerStatic":"https://media.universeodon.com/accounts/headers/109/287/809/647/205/395/original/5de388c5945925c5.jpg","followersCount":6387,"followingCount":305,"statusesCount":1753,"lastStatusAt":"2022-11-28","noindex":false,"emojis":[],"fields":[]},"rules":[]}}}' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,5 @@ | |
*.png | ||
*.ico | ||
*.toml | ||
https-dev-config/localhost.crt | ||
https-dev-config/localhost.key |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ dist | |
.DS_Store | ||
.idea/ | ||
.vite-inspect | ||
.netlify/ | ||
|
||
public/shiki | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
<script setup> | ||
import { usePWA } from '~/composables/pwa' | ||
const { close, needRefresh, updateServiceWorker } = usePWA() | ||
</script> | ||
|
||
<!-- TODO: remove shadow on mobile and position it above the bottom nav --> | ||
<template> | ||
<div | ||
v-if="needRefresh" | ||
role="alertdialog" | ||
aria-labelledby="pwa-toast-title" | ||
aria-describedby="pwa-toast-description" | ||
animate animate-back-in-up md:animate-back-in-right | ||
z11 | ||
fixed | ||
bottom-14 md:bottom-0 right-0 | ||
m-2 p-4 | ||
bg-base border="~ base" | ||
rounded | ||
text-left | ||
shadow | ||
> | ||
<h2 id="pwa-toast-title" sr-only> | ||
{{ $t('pwa.title') }} | ||
</h2> | ||
<div id="pwa-toast-message"> | ||
{{ $t('pwa.message') }} | ||
</div> | ||
<div m-t4 flex="~ colum" gap-x="4"> | ||
<button type="button" btn-solid text-sm px-2 py-1 text-center @click="updateServiceWorker()"> | ||
{{ $t('pwa.reload') }} | ||
</button> | ||
<button type="button" btn-outline px-2 py-1 text-sm text-center @click="close"> | ||
{{ $t('pwa.close') }} | ||
</button> | ||
</div> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
<script setup lang="ts"> | ||
defineProps<{ | ||
label: string | ||
hover?: boolean | ||
}>() | ||
const { modelValue } = defineModel<{ | ||
modelValue: boolean | ||
}>() | ||
</script> | ||
|
||
<template> | ||
<label | ||
class="common-checkbox flex items-center cursor-pointer py-1 text-md w-full gap-y-1" | ||
:class="hover ? 'hover:bg-active ml--2 pl-4' : null" | ||
@click.prevent="modelValue = !modelValue" | ||
> | ||
<span | ||
:class="modelValue ? 'i-ri:checkbox-line' : 'i-ri:checkbox-blank-line'" | ||
aria-hidden="true" | ||
/> | ||
<input | ||
v-model="modelValue" | ||
type="checkbox" | ||
sr-only | ||
> | ||
<span ml-2 pointer-events-none>{{ label }}</span> | ||
</label> | ||
</template> | ||
|
||
<style> | ||
.common-checkbox:focus-within { | ||
outline: none; | ||
border-bottom: 1px solid var(--c-text-base); | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
<script setup lang="ts"> | ||
defineProps<{ | ||
label: string | ||
value: any | ||
hover?: boolean | ||
}>() | ||
const { modelValue } = defineModel<{ | ||
modelValue: any | ||
}>() | ||
</script> | ||
|
||
<template> | ||
<label | ||
class="common-radio flex items-center cursor-pointer py-1 text-md w-full gap-y-1" | ||
:class="hover ? 'hover:bg-active ml--2 pl-4' : null" | ||
@click.prevent="modelValue = value" | ||
> | ||
<span | ||
:class="modelValue === value ? 'i-ri:radio-button-line' : 'i-ri:checkbox-blank-circle-line'" | ||
aria-hidden="true" | ||
/> | ||
<input | ||
v-model="modelValue" | ||
type="radio" | ||
:value="value" | ||
sr-only | ||
> | ||
<span ml-2 pointer-events-none>{{ label }}</span> | ||
</label> | ||
</template> | ||
|
||
<style> | ||
.common-radio:focus-within { | ||
outline: none; | ||
border-bottom: 1px solid var(--c-text-base); | ||
} | ||
</style> |
42 changes: 42 additions & 0 deletions
42
components/notification/NotificationEnablePushNotification.client.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<script setup lang="ts"> | ||
defineProps<{ | ||
withHeader?: boolean | ||
busy?: boolean | ||
animate?: boolean | ||
}>() | ||
defineEmits(['hide', 'subscribe']) | ||
</script> | ||
|
||
<template> | ||
<div flex="~ col" role="alert" aria-labelledby="notifications-warning" :class="withHeader ? 'border-b border-base' : null"> | ||
<header v-if="withHeader" flex items-center pb-2> | ||
<h2 id="notifications-warning" text-md font-bold w-full> | ||
{{ $t('notification.settings.warning.enable_title') }} | ||
</h2> | ||
<button | ||
flex rounded-4 | ||
type="button" | ||
:title="$t('notification.settings.warning.enable_close')" | ||
hover:bg-active cursor-pointer transition-100 | ||
:disabled="busy" | ||
@click="$emit('hide')" | ||
> | ||
<span aria-hidden="true" i-ri:close-circle-line /> | ||
</button> | ||
</header> | ||
<p> | ||
{{ $t(withHeader ? 'notification.settings.warning.enable_description' : 'notification.settings.warning.enable_description_short') }} | ||
</p> | ||
<button | ||
btn-outline rounded-full font-bold py4 flex="~ gap2 center" m5 | ||
type="button" | ||
:class="busy ? 'border-transparent' : null" | ||
:disabled="busy" | ||
@click="$emit('subscribe')" | ||
> | ||
<span aria-hidden="true" :class="busy && animate ? 'i-ri:loader-2-fill animate-spin' : 'i-ri:check-line'" /> | ||
{{ $t('notification.settings.warning.enable_desktop') }} | ||
</button> | ||
</div> | ||
</template> |
185 changes: 185 additions & 0 deletions
185
components/notification/NotificationPreferences.client.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
<script setup lang="ts"> | ||
import { usePushManager } from '~/composables/push-notifications/usePushManager' | ||
defineProps<{ show: boolean }>() | ||
let busy = $ref<boolean>(false) | ||
let animateSave = $ref<boolean>(false) | ||
let animateSubscription = $ref<boolean>(false) | ||
let animateRemoveSubscription = $ref<boolean>(false) | ||
const { | ||
pushNotificationData, | ||
saveEnabled, | ||
undoChanges, | ||
hiddenNotification, | ||
isSubscribed, | ||
isSupported, | ||
notificationPermission, | ||
updateSubscription, | ||
subscribe, | ||
unsubscribe, | ||
} = usePushManager() | ||
const pwaEnabled = useRuntimeConfig().public.pwaEnabled | ||
const hideNotification = () => { | ||
const key = currentUser.value?.account?.acct | ||
if (key) | ||
hiddenNotification.value[key] = true | ||
} | ||
const showWarning = $computed(() => { | ||
if (!pwaEnabled) | ||
return false | ||
return isSupported | ||
&& (!isSubscribed.value || !notificationPermission.value || notificationPermission.value === 'prompt') | ||
&& !(hiddenNotification.value[currentUser.value?.account?.acct ?? ''] === true) | ||
}) | ||
const saveSettings = async () => { | ||
if (busy) | ||
return | ||
busy = true | ||
await nextTick() | ||
animateSave = true | ||
try { | ||
const subscription = await updateSubscription() | ||
// todo: handle error | ||
} | ||
finally { | ||
busy = false | ||
animateSave = false | ||
} | ||
} | ||
const doSubscribe = async () => { | ||
if (busy) | ||
return | ||
busy = true | ||
await nextTick() | ||
animateSubscription = true | ||
try { | ||
const subscription = await subscribe() | ||
// todo: apply some logic based on the result: subscription === 'subscribed' | ||
// todo: maybe throwing an error instead just a literal to show a dialog with the error | ||
// todo: handle error | ||
} | ||
finally { | ||
busy = false | ||
animateSubscription = false | ||
} | ||
} | ||
const removeSubscription = async () => { | ||
if (busy) | ||
return | ||
busy = true | ||
await nextTick() | ||
animateRemoveSubscription = true | ||
try { | ||
await unsubscribe() | ||
} | ||
finally { | ||
busy = false | ||
animateRemoveSubscription = false | ||
} | ||
} | ||
onActivated(() => (busy = false)) | ||
</script> | ||
|
||
<template> | ||
<div v-if="pwaEnabled && (showWarning || show)"> | ||
<Transition name="slide-down"> | ||
<div v-if="show" flex="~ col" border="b base" px5 py4> | ||
<header flex items-center pb-2> | ||
<h2 id="notifications-title" text-md font-bold w-full> | ||
{{ $t('notification.settings.title') }} | ||
</h2> | ||
</header> | ||
<template v-if="isSupported"> | ||
<div v-if="isSubscribed" flex="~ col"> | ||
<form flex="~ col" gap-y-2 @submit.prevent="saveSettings"> | ||
<fieldset flex="~ col" gap-y-1 py-1> | ||
<legend>{{ $t('notification.settings.alerts.title') }}</legend> | ||
<CommonCheckbox v-model="pushNotificationData.follow" hover :label="$t('notification.settings.alerts.follow')" /> | ||
<CommonCheckbox v-model="pushNotificationData.favourite" hover :label="$t('notification.settings.alerts.favourite')" /> | ||
<CommonCheckbox v-model="pushNotificationData.reblog" hover :label="$t('notification.settings.alerts.reblog')" /> | ||
<CommonCheckbox v-model="pushNotificationData.mention" hover :label="$t('notification.settings.alerts.mention')" /> | ||
<CommonCheckbox v-model="pushNotificationData.poll" hover :label="$t('notification.settings.alerts.poll')" /> | ||
</fieldset> | ||
<fieldset flex="~ col" gap-y-1 py-1> | ||
<legend>{{ $t('notification.settings.policy.title') }}</legend> | ||
<CommonRadio v-model="pushNotificationData.policy" hover value="all" :label="$t('notification.settings.policy.all')" /> | ||
<CommonRadio v-model="pushNotificationData.policy" hover value="followed" :label="$t('notification.settings.policy.followed')" /> | ||
<CommonRadio v-model="pushNotificationData.policy" hover value="follower" :label="$t('notification.settings.policy.follower')" /> | ||
<CommonRadio v-model="pushNotificationData.policy" hover value="none" :label="$t('notification.settings.policy.none')" /> | ||
</fieldset> | ||
<div flex="~ col" gap-y-4 py-1 sm="~ justify-between flex-row"> | ||
<button | ||
btn-solid font-bold py2 full-w sm-wa flex="~ gap2 center" | ||
:class="busy || !saveEnabled ? 'border-transparent' : null" | ||
:disabled="busy || !saveEnabled" | ||
> | ||
<span :class="busy && animateSave ? 'i-ri:loader-2-fill animate-spin' : 'i-ri:save-2-fill'" /> | ||
{{ $t('notification.settings.save_settings') }} | ||
</button> | ||
<button | ||
btn-outline font-bold py2 full-w sm-wa flex="~ gap2 center" | ||
type="button" | ||
:class="busy || !saveEnabled ? 'border-transparent' : null" | ||
:disabled="busy || !saveEnabled" | ||
@click="undoChanges" | ||
> | ||
<span aria-hidden="true" class="i-material-symbols:undo-rounded" /> | ||
{{ $t('notification.settings.undo_settings') }} | ||
</button> | ||
</div> | ||
</form> | ||
<form flex="~ col" mt-4 @submit.prevent="removeSubscription"> | ||
<span border="b base 2px" class="bg-$c-text-secondary" /> | ||
<button | ||
btn-outline rounded-full font-bold py-4 flex="~ gap2 center" m5 | ||
:class="busy ? 'border-transparent' : null" | ||
:disabled="busy" | ||
> | ||
<span aria-hidden="true" :class="busy && animateRemoveSubscription ? 'i-ri:loader-2-fill animate-spin' : 'i-material-symbols:cancel-rounded'" /> | ||
{{ $t('notification.settings.unsubscribe') }} | ||
</button> | ||
</form> | ||
</div> | ||
<template v-else> | ||
<p v-if="showWarning" role="alert" aria-labelledby="notifications-title"> | ||
{{ $t('notification.settings.unsubscribed_with_warning') }} | ||
</p> | ||
<NotificationEnablePushNotification | ||
v-else | ||
:animate="animateSubscription" | ||
:busy="busy" | ||
@hide="hideNotification" | ||
@subscribe="doSubscribe" | ||
/> | ||
</template> | ||
</template> | ||
<p v-else role="alert" aria-labelledby="notifications-unsupported"> | ||
{{ $t('notification.settings.unsupported') }} | ||
</p> | ||
</div> | ||
</Transition> | ||
<NotificationEnablePushNotification | ||
v-if="showWarning" | ||
with-header | ||
px5 | ||
py4 | ||
:animate="animateSubscription" | ||
:busy="busy" | ||
@hide="hideNotification" | ||
@subscribe="doSubscribe" | ||
/> | ||
</div> | ||
</template> |
Oops, something went wrong.