Local Notifications

This commit is contained in:
2024-03-31 17:49:31 +08:00
parent c3bfb2069c
commit 43aad8c2d2
13 changed files with 154 additions and 57 deletions

View File

@@ -1,8 +1,8 @@
<template>
<v-menu eager :close-on-content-click="false">
<template #activator="{ props }">
<v-btn v-bind="props" icon rounded="circle" size="small" variant="text" :loading="loading">
<v-badge v-if="pagination.total > 0" color="error" :content="pagination.total">
<v-btn v-bind="props" icon size="small" variant="text" :loading="loading">
<v-badge v-if="notify.total > 0" color="error" :content="notify.total">
<v-icon icon="mdi-bell" />
</v-badge>
@@ -10,20 +10,19 @@
</v-btn>
</template>
<v-list v-if="notifications.length <= 0" class="w-[380px]" density="compact">
<v-list v-if="notify.notifications.length <= 0" class="w-[380px]" density="compact">
<v-list-item>
<v-alert class="text-sm" variant="tonal" type="info">You are done! There is no unread notifications for
you.</v-alert>
<v-alert class="text-sm" variant="tonal" type="info">You are done! There is no unread notifications for you.</v-alert>
</v-list-item>
</v-list>
<v-list v-else class="w-[380px]" density="compact" lines="three">
<v-list-item v-for="item in notifications">
<v-list-item v-for="(item, idx) in notify.notifications">
<template #title>{{ item.subject }}</template>
<template #subtitle>{{ item.content }}</template>
<template #append>
<v-btn icon="mdi-check" size="x-small" variant="text" :disabled="loading" @click="markAsRead(item)" />
<v-btn icon="mdi-check" size="x-small" variant="text" :disabled="loading" @click="markAsRead(item, idx)" />
</template>
<div class="flex text-xs gap-1">
@@ -40,50 +39,32 @@
<script setup lang="ts">
import { request } from "@/scripts/request"
import { getAtk } from "@/stores/userinfo"
import { reactive, ref } from "vue"
import { computed, onMounted, onUnmounted, ref } from "vue";
import { useNotifications } from "@/stores/notifications";
const loading = ref(false)
const notify = useNotifications()
const error = ref<string | null>(null)
const submitting = ref(false)
const loading = computed(() => notify.loading || submitting.value)
const notifications = ref<any[]>([])
const pagination = reactive({ page: 1, pageSize: 25, total: 0 })
async function readNotifications() {
loading.value = true
const res = await request(
"identity",
"/api/notifications?" +
new URLSearchParams({
take: pagination.pageSize.toString(),
offset: ((pagination.page - 1) * pagination.pageSize).toString()
}),
{
headers: { Authorization: `Bearer ${await getAtk()}` }
}
)
if (res.status === 200) {
const data = await res.json()
notifications.value = data["data"]
pagination.total = data["count"]
}
loading.value = false
}
readNotifications()
async function markAsRead(item: any) {
loading.value = true
const res = await request("identity", `/api/notifications/${item.id}/read`, {
async function markAsRead(item: any, idx: number) {
submitting.value = true
const res = await request(`/api/notifications/${item.id}/read`, {
method: "PUT",
headers: { Authorization: `Bearer ${await getAtk()}` }
headers: { Authorization: `Bearer ${getAtk()}` },
})
if (res.status !== 200) {
error.value = await res.text()
} else {
await readNotifications()
notify.remove(idx)
error.value = null
}
loading.value = false
submitting.value = false
}
notify.list()
onMounted(() => notify.connect())
onUnmounted(() => notify.disconnect())
</script>

View File

@@ -14,7 +14,6 @@
<v-carousel-item v-for="(item, idx) in attachments">
<img
v-if="item.type === 1"
loading="lazy"
decoding="async"
class="cursor-zoom-in content-visibility-auto w-full h-full object-contain"
:src="getUrl(item)"

View File

@@ -0,0 +1,81 @@
import { defineStore } from "pinia"
import { ref } from "vue"
import { checkLoggedIn, getAtk } from "@/stores/userinfo"
import { buildRequestUrl, request } from "@/scripts/request"
import { LocalNotifications } from "@capacitor/local-notifications"
import { Capacitor } from "@capacitor/core"
export const useNotifications = defineStore("notifications", () => {
let socket: WebSocket
const loading = ref(false)
const notifications = ref<any[]>([])
const total = ref(0)
async function list() {
loading.value = true
const res = await request(
"identity",
"/api/notifications?" +
new URLSearchParams({
take: (25).toString(),
offset: (0).toString()
}),
{
headers: { Authorization: `Bearer ${await getAtk()}` }
}
)
if (res.status === 200) {
const data = await res.json()
notifications.value = data["data"]
total.value = data["count"]
}
loading.value = false
}
function remove(idx: number) {
notifications.value.splice(idx, 1)
total.value--
}
async function connect() {
if (!(await checkLoggedIn())) return
const uri = buildRequestUrl("identity", "/api/notifications/listen").replace("http", "ws")
socket = new WebSocket(uri + `?tk=${await getAtk() as string}`)
socket.addEventListener("open", (event) => {
console.log("[NOTIFICATIONS] The listen websocket has been established... ", event.type)
})
socket.addEventListener("close", (event) => {
console.warn("[NOTIFICATIONS] The listen websocket is disconnected... ", event.reason, event.code)
})
socket.addEventListener("message", (event) => {
const data = JSON.parse(event.data)
notifications.value.push(data)
total.value++
if (Capacitor.getPlatform() === "web") {
new Notification(data["id"], {
body: data["subject"]
})
} else {
LocalNotifications.schedule({
notifications: [
{ id: data["id"], title: data["subject"], body: data["subject"] }
]
}).then((res) => console.log(res))
}
})
await Notification.requestPermission()
}
function disconnect() {
socket.close()
}
return { loading, notifications, total, list, remove, connect, disconnect }
})

View File

@@ -4,7 +4,7 @@
<post-list v-model:posts="posts" :loader="readMore" />
</div>
<div class="aside md:sticky top-0 w-full h-fit md:min-w-[280px] md:max-w-[320px] max-md:order-first">
<div class="aside w-full h-full md:min-w-[320px] md:max-w-[320px] max-md:order-first">
<v-card title="Categories">
<v-list density="compact">
<v-list-item title="All" prepend-icon="mdi-apps" active></v-list-item>
@@ -16,8 +16,8 @@
<script setup lang="ts">
import PostList from "@/components/posts/PostList.vue"
import { reactive, ref } from "vue"
import { request } from "@/scripts/request"
import { reactive, ref } from "vue"
const error = ref<string | null>(null)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })