feat: show and stream new notifications (#282)
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>zio/stable
parent
0f06653636
commit
585b9e0229
|
@ -4,11 +4,12 @@ import { DynamicScroller } from 'vue-virtual-scroller'
|
||||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||||
import type { Paginator, WsEvents } from 'masto'
|
import type { Paginator, WsEvents } from 'masto'
|
||||||
|
|
||||||
const { paginator, stream, keyProp = 'id', virtualScroller = false } = defineProps<{
|
const { paginator, stream, keyProp = 'id', virtualScroller = false, eventType = 'update' } = defineProps<{
|
||||||
paginator: Paginator<any, any[]>
|
paginator: Paginator<any, any[]>
|
||||||
keyProp?: string
|
keyProp?: string
|
||||||
virtualScroller?: boolean
|
virtualScroller?: boolean
|
||||||
stream?: WsEvents
|
stream?: WsEvents
|
||||||
|
eventType?: 'notification' | 'update'
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
|
@ -23,7 +24,7 @@ defineSlots<{
|
||||||
loading: {}
|
loading: {}
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { items, prevItems, update, state, endAnchor, error } = usePaginator(paginator, stream)
|
const { items, prevItems, update, state, endAnchor, error } = usePaginator(paginator, stream, eventType)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -1,8 +1,21 @@
|
||||||
|
<script setup>
|
||||||
|
const { notifications } = useNotifications()
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav px3 py4 flex="~ col gap2" text-lg>
|
<nav px3 py4 flex="~ col gap2" text-lg>
|
||||||
<template v-if="currentUser">
|
<template v-if="currentUser">
|
||||||
<NavSideItem :text="$t('nav_side.home')" to="/home" icon="i-ri:home-5-line" />
|
<NavSideItem :text="$t('nav_side.home')" to="/home" icon="i-ri:home-5-line" />
|
||||||
<NavSideItem :text="$t('nav_side.notifications')" to="/notifications" icon="i-ri:notification-4-line" />
|
<NavSideItem :text="$t('nav_side.notifications')" to="/notifications" icon="i-ri:notification-4-line">
|
||||||
|
<template #icon>
|
||||||
|
<div flex relative>
|
||||||
|
<div class="i-ri:notification-4-line" />
|
||||||
|
<div v-if="notifications" class="top-[-0.3rem] right-[-0.3rem]" absolute font-bold rounded-full h-4 w-4 text-xs bg-primary text-inverted flex items-center justify-center>
|
||||||
|
{{ notifications < 10 ? notifications : '•' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NavSideItem>
|
||||||
</template>
|
</template>
|
||||||
<NavSideItem :text="$t('nav_side.explore')" to="/explore" icon="i-ri:hashtag" />
|
<NavSideItem :text="$t('nav_side.explore')" to="/explore" icon="i-ri:hashtag" />
|
||||||
<NavSideItem :text="$t('nav_side.local')" to="/public/local" icon="i-ri:group-2-line " />
|
<NavSideItem :text="$t('nav_side.local')" to="/public/local" icon="i-ri:group-2-line " />
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Notification, Paginator } from 'masto'
|
import type { Notification, Paginator, WsEvents } from 'masto'
|
||||||
import type { GroupedNotifications } from '~/types'
|
import type { GroupedNotifications } from '~/types'
|
||||||
|
|
||||||
const { paginator } = defineProps<{
|
const { paginator, stream } = defineProps<{
|
||||||
paginator: Paginator<any, Notification[]>
|
paginator: Paginator<any, Notification[]>
|
||||||
|
stream?: WsEvents
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function groupItems(items: Notification[]): (Notification | GroupedNotifications)[] {
|
function groupItems(items: Notification[]): (Notification | GroupedNotifications)[] {
|
||||||
|
@ -41,10 +42,17 @@ function groupItems(items: Notification[]): (Notification | GroupedNotifications
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { clearNotifications } = useNotifications()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CommonPaginator :paginator="paginator">
|
<CommonPaginator :paginator="paginator" :stream="stream" event-type="notification">
|
||||||
|
<template #updater="{ number, update }">
|
||||||
|
<button py-4 border="b base" flex="~ col" p-3 w-full text-primary font-bold @click="() => { update(); clearNotifications() }">
|
||||||
|
{{ $t('timeline.show_new_items', [number]) }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
<template #items="{ items }">
|
<template #items="{ items }">
|
||||||
<template v-for="item of groupItems(items)" :key="item.id">
|
<template v-for="item of groupItems(items)" :key="item.id">
|
||||||
<NotificationGroupedFollow
|
<NotificationGroupedFollow
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { Paginator, WsEvents } from 'masto'
|
||||||
import { useDeactivated } from './lifecycle'
|
import { useDeactivated } from './lifecycle'
|
||||||
import type { PaginatorState } from '~/types'
|
import type { PaginatorState } from '~/types'
|
||||||
|
|
||||||
export function usePaginator<T>(paginator: Paginator<any, T[]>, stream?: WsEvents) {
|
export function usePaginator<T>(paginator: Paginator<any, T[]>, stream?: WsEvents, eventType: 'notification' | 'update' = 'update') {
|
||||||
const state = ref<PaginatorState>('idle')
|
const state = ref<PaginatorState>('idle')
|
||||||
const items = ref<T[]>([])
|
const items = ref<T[]>([])
|
||||||
const nextItems = ref<T[]>([])
|
const nextItems = ref<T[]>([])
|
||||||
|
@ -19,7 +19,7 @@ export function usePaginator<T>(paginator: Paginator<any, T[]>, stream?: WsEvent
|
||||||
prevItems.value = []
|
prevItems.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
stream?.on('update', (status) => {
|
stream?.on(eventType, (status) => {
|
||||||
prevItems.value.unshift(status as any)
|
prevItems.value.unshift(status as any)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { login as loginMasto } from 'masto'
|
import { login as loginMasto } from 'masto'
|
||||||
import type { AccountCredentials, Instance } from 'masto'
|
import type { AccountCredentials, Instance, WsEvents } from 'masto'
|
||||||
import { clearUserDrafts } from './statusDrafts'
|
import { clearUserDrafts } from './statusDrafts'
|
||||||
import type { UserLogin } from '~/types'
|
import type { UserLogin } from '~/types'
|
||||||
import { DEFAULT_POST_CHARS_LIMIT, DEFAULT_SERVER, STORAGE_KEY_CURRENT_USER, STORAGE_KEY_SERVERS, STORAGE_KEY_USERS } from '~/constants'
|
import { DEFAULT_POST_CHARS_LIMIT, DEFAULT_SERVER, STORAGE_KEY_CURRENT_USER, STORAGE_KEY_SERVERS, STORAGE_KEY_USERS } from '~/constants'
|
||||||
|
@ -97,6 +97,43 @@ export async function signout() {
|
||||||
await loginTo(currentUser.value)
|
await loginTo(currentUser.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notifications = reactive<Record<string, undefined | [Promise<WsEvents>, number]>>({})
|
||||||
|
|
||||||
|
export const useNotifications = () => {
|
||||||
|
const id = currentUser.value?.account.id
|
||||||
|
|
||||||
|
const clearNotifications = () => {
|
||||||
|
if (!id || !notifications[id])
|
||||||
|
return
|
||||||
|
notifications[id]![1] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect(): Promise<void> {
|
||||||
|
if (!id || notifications[id])
|
||||||
|
return
|
||||||
|
|
||||||
|
const masto = useMasto()
|
||||||
|
const stream = masto.stream.streamUser()
|
||||||
|
notifications[id] = [stream, 0]
|
||||||
|
;(await stream).on('notification', () => {
|
||||||
|
if (notifications[id])
|
||||||
|
notifications[id]![1]++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect(): void {
|
||||||
|
if (!id || !notifications[id])
|
||||||
|
return
|
||||||
|
notifications[id]![0].then(stream => stream.disconnect())
|
||||||
|
notifications[id] = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(currentUser, disconnect)
|
||||||
|
connect()
|
||||||
|
|
||||||
|
return { notifications: computed(() => id ? notifications[id]?.[1] ?? 0 : 0), disconnect, clearNotifications }
|
||||||
|
}
|
||||||
|
|
||||||
export function checkLogin() {
|
export function checkLogin() {
|
||||||
if (!currentUser.value) {
|
if (!currentUser.value) {
|
||||||
openSigninDialog()
|
openSigninDialog()
|
||||||
|
|
|
@ -8,6 +8,11 @@ const { t } = useI18n()
|
||||||
const paginatorAll = useMasto().notifications.getIterator()
|
const paginatorAll = useMasto().notifications.getIterator()
|
||||||
const paginatorMention = useMasto().notifications.getIterator({ types: ['mention'] })
|
const paginatorMention = useMasto().notifications.getIterator({ types: ['mention'] })
|
||||||
|
|
||||||
|
const { clearNotifications } = useNotifications()
|
||||||
|
onActivated(clearNotifications)
|
||||||
|
|
||||||
|
const stream = await useMasto().stream.streamUser()
|
||||||
|
|
||||||
const tabs = $computed(() => [
|
const tabs = $computed(() => [
|
||||||
{
|
{
|
||||||
name: 'all',
|
name: 'all',
|
||||||
|
@ -43,7 +48,7 @@ useHeadFixed({
|
||||||
<CommonTabs v-model="tab" :options="tabs" />
|
<CommonTabs v-model="tab" :options="tabs" />
|
||||||
</template>
|
</template>
|
||||||
<slot>
|
<slot>
|
||||||
<NotificationPaginator :key="tab" :paginator="paginator" />
|
<NotificationPaginator :key="tab" v-bind="{ paginator, stream }" />
|
||||||
</slot>
|
</slot>
|
||||||
</MainContent>
|
</MainContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Reference in New Issue