feat: filter notifications by type (#2371)

Co-authored-by: Xabi <xabi.rn@gmail.com>
Co-authored-by: userquin <userquin@gmail.com>
This commit is contained in:
Ayo Ayco 2023-09-06 11:13:16 +02:00 committed by GitHub
parent e9c5de577e
commit 907d9999dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 171 additions and 31 deletions

View file

@ -1,6 +1,8 @@
<script setup lang="ts">
import type { RouteLocationRaw } from 'vue-router'
const { t } = useI18n()
export interface CommonRouteTabOption {
to: RouteLocationRaw
display: string
@ -8,9 +10,17 @@ export interface CommonRouteTabOption {
name?: string
icon?: string
hide?: boolean
match?: boolean
}
const { options, command, replace, preventScrollTop = false } = $defineProps<{
export interface CommonRouteTabMoreOption {
options: CommonRouteTabOption[]
icon?: string
tooltip?: string
match?: boolean
}
const { options, command, replace, preventScrollTop = false, moreOptions } = $defineProps<{
options: CommonRouteTabOption[]
moreOptions?: CommonRouteTabMoreOption
command?: boolean
replace?: boolean
preventScrollTop?: boolean
@ -21,7 +31,6 @@ const router = useRouter()
useCommands(() => command
? options.map(tab => ({
scope: 'Tabs',
name: tab.display,
icon: tab.icon ?? 'i-ri:file-list-2-line',
onActivate: () => router.replace(tab.to),
@ -51,5 +60,43 @@ useCommands(() => command
<span ws-nowrap mxa sm:px2 sm:py3 py2 text-center text-secondary-light op50>{{ option.display }}</span>
</div>
</template>
<template v-if="moreOptions?.options?.length">
<CommonDropdown placement="bottom" flex cursor-pointer mx-1.25rem>
<CommonTooltip placement="top" :content="moreOptions.tooltip || t('action.more')">
<button
cursor-pointer
flex
gap-1
w-12
rounded
hover:bg-active
btn-action-icon
op75
px4
group
:aria-label="t('action.more')"
:class="moreOptions.match ? 'text-primary' : 'text-secondary'"
>
<span v-if="moreOptions.icon" :class="moreOptions.icon" text-sm me--1 block />
<span i-ri:arrow-down-s-line text-sm me--1 block />
</button>
</CommonTooltip>
<template #popper>
<NuxtLink
v-for="(option, index) in moreOptions.options.filter(item => !item.hide)"
:key="option?.name || index"
:to="option.to"
>
<CommonDropdownItem>
<span flex="~ row" gap-x-4 items-center :class="option.match ? 'text-primary' : ''">
<span v-if="option.icon" :class="[option.icon, option.match ? 'text-primary' : 'text.secondary']" text-md me--1 block />
<span v-else block>&#160;</span>
<span>{{ option.display }}</span>
</span>
</CommonDropdownItem>
</NuxtLink>
</template>
</commondropdown>
</template>
</div>
</template>

View file

@ -31,7 +31,7 @@ const { notification } = defineProps<{
</template>
<template v-else-if="notification.type === 'admin.sign_up'">
<div flex p3 items-center bg-shaded>
<div i-ri:admin-fill me-1 color-purple />
<div i-ri:user-add-fill me-1 color-purple />
<AccountDisplayName
:account="notification.account"
text-purple me-1 font-bold line-clamp-1 ws-pre-wrap break-all
@ -58,7 +58,7 @@ const { notification } = defineProps<{
</template>
<template v-else-if="notification.type === 'follow_request'">
<div flex ms-4 items-center class="-top-2.5" absolute inset-ie-2 px-2>
<div i-ri:user-follow-fill text-xl me-1 />
<div i-ri:user-shared-fill text-xl me-1 />
<AccountInlineInfo :account="notification.account" me1 />
</div>
<!-- TODO: accept request -->

View file

@ -1,12 +0,0 @@
<script setup lang="ts">
// Default limit is 20 notifications, and servers are normally caped to 30
const paginator = useMastoClient().v1.notifications.list({ limit: 30, types: ['mention'] })
const stream = $(useStreaming(client => client.v1.stream.streamUser()))
const { clearNotifications } = useNotifications()
onActivated(clearNotifications)
</script>
<template>
<NotificationPaginator v-bind="{ paginator, stream }" />
</template>

View file

@ -1,6 +1,14 @@
<script setup lang="ts">
import type { mastodon } from 'masto'
const { filter } = defineProps<{
filter?: mastodon.v1.NotificationType
}>()
const options = { limit: 30, types: filter ? [filter] : [] }
// Default limit is 20 notifications, and servers are normally caped to 30
const paginator = useMastoClient().v1.notifications.list({ limit: 30 })
const paginator = useMastoClient().v1.notifications.list(options)
const stream = useStreaming(client => client.v1.stream.streamUser())
const { clearNotifications } = useNotifications()

View file

@ -0,0 +1,20 @@
import type { mastodon } from 'masto'
import { NOTIFICATION_FILTER_TYPES } from '~/constants'
/**
* Typeguard to check if an object is a valid notification filter
* @param obj the object to be checked
* @returns boolean and assigns type to object if true
*/
export function isNotificationFilter(obj: unknown): obj is mastodon.v1.NotificationType {
return !!obj && NOTIFICATION_FILTER_TYPES.includes(obj as unknown as mastodon.v1.NotificationType)
}
/**
* Typeguard to check if an object is a valid notification
* @param obj the object to be checked
* @returns boolean and assigns type to object if true
*/
export function isNotification(obj: unknown): obj is mastodon.v1.NotificationType {
return !!obj && ['mention', ...NOTIFICATION_FILTER_TYPES].includes(obj as unknown as mastodon.v1.NotificationType)
}

View file

@ -1,3 +1,5 @@
import type { mastodon } from 'masto'
export const APP_NAME = 'Elk'
export const DEFAULT_POST_CHARS_LIMIT = 500
@ -22,3 +24,5 @@ export const STORAGE_KEY_NOTIFICATION_POLICY = 'elk-notification-policy'
export const STORAGE_KEY_PWA_HIDE_INSTALL = 'elk-pwa-hide-install'
export const HANDLED_MASTO_URLS = /^(https?:\/\/)?([\w\d-]+\.)+\w+\/(@[@\w\d-\.]+)(\/objects)?(\/\d+)?$/
export const NOTIFICATION_FILTER_TYPES: mastodon.v1.NotificationType[] = ['status', 'reblog', 'follow', 'follow_request', 'favourite', 'poll', 'update', 'admin.sign_up', 'admin.report']

View file

@ -605,8 +605,20 @@
"list": "List",
"media": "Media",
"news": "News",
"notifications_admin": {
"report": "Report",
"sign_up": "Sign-Up"
},
"notifications_all": "All",
"notifications_favourite": "Favourite",
"notifications_follow": "Follow",
"notifications_follow_request": "Follow request",
"notifications_mention": "Mention",
"notifications_more_tooltip": "Filter notifications by type",
"notifications_poll": "Poll",
"notifications_reblog": "Boost",
"notifications_status": "Status",
"notifications_update": "Update",
"posts": "Posts",
"posts_with_replies": "Posts & Replies"
},

View file

@ -1,10 +1,16 @@
<script setup lang="ts">
import type { CommonRouteTabOption } from '~/components/common/CommonRouteTabs.vue'
import type { mastodon } from 'masto'
import { NOTIFICATION_FILTER_TYPES } from '~/constants'
import type {
CommonRouteTabMoreOption,
CommonRouteTabOption,
} from '~/components/common/CommonRouteTabs.vue'
definePageMeta({
middleware: 'auth',
})
const route = useRoute()
const { t } = useI18n()
const pwaEnabled = useAppConfig().pwaEnabled
@ -20,6 +26,47 @@ const tabs = $computed<CommonRouteTabOption[]>(() => [
display: isHydrated.value ? t('tab.notifications_mention') : '',
},
])
const filter = $computed<mastodon.v1.NotificationType | undefined>(() => {
if (!isHydrated.value)
return undefined
const rawFilter = route.params?.filter
const actualFilter = Array.isArray(rawFilter) ? rawFilter[0] : rawFilter
if (isNotificationFilter(actualFilter))
return actualFilter
})
const filterIconMap: Record<mastodon.v1.NotificationType, string> = {
'mention': 'i-ri:at-line',
'status': 'i-ri:account-pin-circle-line',
'reblog': 'i-ri:repeat-fill',
'follow': 'i-ri:user-follow-line',
'follow_request': 'i-ri:user-shared-line',
'favourite': 'i-ri:heart-3-line',
'poll': 'i-ri:chat-poll-line',
'update': 'i-ri:edit-2-line',
'admin.sign_up': 'i-ri:user-add-line',
'admin.report': 'i-ri:flag-line',
}
const filterText = $computed(() => (`${t('tab.notifications_more_tooltip')}${filter ? `: ${t(`tab.notifications_${filter}`)}` : ''}`))
const notificationFilterRoutes = $computed<CommonRouteTabOption[]>(() => NOTIFICATION_FILTER_TYPES.map(
name => ({
name,
to: `/notifications/${name}`,
display: isHydrated.value ? t(`tab.notifications_${name}`) : '',
icon: filterIconMap[name],
match: name === filter,
}),
))
const moreOptions = $computed<CommonRouteTabMoreOption>(() => ({
options: notificationFilterRoutes,
icon: 'i-ri:filter-2-line',
tooltip: filterText,
match: !!filter,
}))
</script>
<template>
@ -27,7 +74,7 @@ const tabs = $computed<CommonRouteTabOption[]>(() => [
<template #title>
<NuxtLink to="/notifications" timeline-title-style flex items-center gap-2 @click="$scrollToTop">
<div i-ri:notification-4-line />
<span>{{ t('nav.notifications') }}</span>
<span>{{ isHydrated ? t('nav.notifications') : '' }}</span>
</NuxtLink>
</template>
@ -35,7 +82,7 @@ const tabs = $computed<CommonRouteTabOption[]>(() => [
<NuxtLink
flex rounded-4 p1
hover:bg-active cursor-pointer transition-100
:title="t('settings.notifications.show_btn')"
:title="isHydrated ? t('settings.notifications.show_btn') : ''"
to="/settings/notifications"
>
<span aria-hidden="true" i-ri:notification-badge-line />
@ -43,7 +90,7 @@ const tabs = $computed<CommonRouteTabOption[]>(() => [
</template>
<template #header>
<CommonRouteTabs replace :options="tabs" />
<CommonRouteTabs replace :options="tabs" :more-options="moreOptions" />
</template>
<slot>

View file

@ -0,0 +1,24 @@
<script setup lang="ts">
import type { mastodon } from 'masto'
const route = useRoute()
const { t } = useI18n()
const filter = $computed<mastodon.v1.NotificationType | undefined>(() => {
if (!isHydrated.value)
return undefined
const rawFilter = route.params?.filter
const actualFilter = Array.isArray(rawFilter) ? rawFilter[0] : rawFilter
if (isNotification(actualFilter))
return actualFilter
})
useHydratedHead({
title: () => `${t(`tab.notifications_${filter ?? 'all'}`)} | ${t('nav.notifications')}`,
})
</script>
<template>
<TimelineNotifications v-if="isHydrated" :filter="filter" />
</template>

View file

@ -1,10 +0,0 @@
<script setup lang="ts">
const { t } = useI18n()
useHydratedHead({
title: () => `${t('tab.notifications_mention')} | ${t('nav.notifications')}`,
})
</script>
<template>
<TimelineMentions v-if="isHydrated" />
</template>