elk/components/notification/NotificationPaginator.vue

198 lines
5.9 KiB
Vue
Raw Normal View History

2022-11-15 21:21:54 +00:00
<script setup lang="ts">
// @ts-expect-error missing types
import { DynamicScrollerItem } from 'vue-virtual-scroller'
import type { Paginator, WsEvents, mastodon } from 'masto'
import type { GroupedAccountLike, NotificationSlot } from '~/types'
2022-11-15 21:21:54 +00:00
const { paginator, stream } = defineProps<{
paginator: Paginator<mastodon.v1.Notification[], mastodon.v1.ListNotificationsParams>
stream?: Promise<WsEvents>
2022-11-15 21:21:54 +00:00
}>()
2022-11-30 00:47:54 +00:00
const virtualScroller = false // TODO: fix flickering issue with virtual scroll
2022-12-11 22:40:40 +00:00
const groupCapacity = Number.MAX_VALUE // No limit
// Group by type (and status when applicable)
2023-01-08 06:21:09 +00:00
const groupId = (item: mastodon.v1.Notification): string => {
2022-12-11 22:40:40 +00:00
// If the update is related to an status, group notifications from the same account (boost + favorite the same status)
const id = item.status
? {
status: item.status?.id,
type: (item.type === 'reblog' || item.type === 'favourite') ? 'like' : item.type,
}
: {
type: item.type,
}
return JSON.stringify(id)
}
function groupItems(items: mastodon.v1.Notification[]): NotificationSlot[] {
2022-12-11 22:40:40 +00:00
const results: NotificationSlot[] = []
2022-11-30 00:47:54 +00:00
let id = 0
2022-12-11 22:40:40 +00:00
let currentGroupId = ''
2023-01-08 06:21:09 +00:00
let currentGroup: mastodon.v1.Notification[] = []
2022-12-11 22:40:40 +00:00
const processGroup = () => {
if (currentGroup.length === 0)
return
const group = currentGroup
currentGroup = []
2022-11-30 00:47:54 +00:00
2022-12-11 22:40:40 +00:00
// Only group follow notifications when there are too many in a row
// This normally happens when you transfer an account, if not, show
// a big profile card for each follow
if (group[0].type === 'follow') {
let groups: mastodon.v1.Notification[] = []
function newGroup() {
if (groups.length > 0) {
results.push({
id: `grouped-${id++}`,
type: 'grouped-follow',
items: groups,
})
groups = []
}
}
for (const item of group) {
const hasHeader = !item.account.header.endsWith('/original/missing.png')
if (hasHeader && (item.account.followersCount > 250 || (group.length === 1 && item.account.followersCount > 25))) {
newGroup()
results.push(item)
}
else {
groups.push(item)
}
}
newGroup()
2022-12-11 22:40:40 +00:00
return
2022-11-30 00:47:54 +00:00
}
2022-12-11 22:40:40 +00:00
const { status } = group[0]
if (status && group.length > 1 && (group[0].type === 'reblog' || group[0].type === 'favourite')) {
// All notifications in these group are reblogs or favourites of the same status
const likes: GroupedAccountLike[] = []
for (const notification of group) {
let like = likes.find(like => like.account.id === notification.account.id)
if (!like) {
like = { account: notification.account }
likes.push(like)
}
like[notification.type === 'reblog' ? 'reblog' : 'favourite'] = notification
}
likes.sort((a, b) => a.reblog ? !b.reblog || (a.favourite && !b.favourite) ? -1 : 0 : 0)
2022-11-30 00:47:54 +00:00
results.push({
id: `grouped-${id++}`,
2022-12-11 22:40:40 +00:00
type: 'grouped-reblogs-and-favourites',
status,
likes,
2022-11-30 00:47:54 +00:00
})
2022-12-11 22:40:40 +00:00
return
2022-11-30 00:47:54 +00:00
}
2022-12-11 22:40:40 +00:00
results.push(...group)
2022-11-30 00:47:54 +00:00
}
for (const item of items) {
2022-12-11 22:40:40 +00:00
const itemId = groupId(item)
// Finalize group if it already has too many notifications
if (currentGroupId !== itemId || currentGroup.length >= groupCapacity)
processGroup()
2022-11-30 00:47:54 +00:00
2022-12-11 22:40:40 +00:00
currentGroup.push(item)
currentGroupId = itemId
}
// Finalize remaining groups
processGroup()
2022-11-30 00:47:54 +00:00
return results
}
function preprocess(items: NotificationSlot[]): NotificationSlot[] {
const flattenedNotifications: mastodon.v1.Notification[] = []
for (const item of items) {
if (item.type === 'grouped-reblogs-and-favourites') {
const group = item
for (const like of group.likes) {
if (like.reblog)
flattenedNotifications.push(like.reblog)
if (like.favourite)
flattenedNotifications.push(like.favourite)
}
}
else if (item.type === 'grouped-follow') {
flattenedNotifications.push(...item.items)
}
else {
flattenedNotifications.push(item)
}
}
2023-01-21 19:35:52 +00:00
2023-01-21 19:46:05 +00:00
return groupItems(flattenedNotifications)
}
const { clearNotifications } = useNotifications()
const { formatNumber } = useHumanReadableNumber()
2022-11-15 21:21:54 +00:00
</script>
<template>
<CommonPaginator
:paginator="paginator"
:preprocess="preprocess"
:stream="stream"
:eager="3"
:virtual-scroller="virtualScroller"
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, { named: { v: formatNumber(number) } }) }}
</button>
</template>
<template #default="{ item, active }">
<template v-if="virtualScroller">
<DynamicScrollerItem :item="item" :active="active" tag="div">
<NotificationGroupedFollow
v-if="item.type === 'grouped-follow'"
:items="item"
border="b base"
/>
<NotificationGroupedLikes
v-else-if="item.type === 'grouped-reblogs-and-favourites'"
:group="item"
border="b base"
/>
<NotificationCard
v-else
:notification="item"
hover:bg-active
border="b base"
/>
</DynamicScrollerItem>
</template>
<template v-else>
2022-11-30 00:47:54 +00:00
<NotificationGroupedFollow
v-if="item.type === 'grouped-follow'"
:items="item"
border="b base"
2022-11-30 00:47:54 +00:00
/>
2022-12-11 22:40:40 +00:00
<NotificationGroupedLikes
v-else-if="item.type === 'grouped-reblogs-and-favourites'"
:group="item"
2022-12-11 22:40:40 +00:00
border="b base"
/>
2022-11-30 00:47:54 +00:00
<NotificationCard
v-else
:notification="item"
2022-11-30 00:47:54 +00:00
hover:bg-active
border="b base"
2022-11-30 00:47:54 +00:00
/>
</template>
2022-11-16 16:11:08 +00:00
</template>
</CommonPaginator>
2022-11-15 21:21:54 +00:00
</template>