feat: add setting for notification list style

This commit is contained in:
codytseng
2025-09-06 13:49:13 +08:00
parent 71994be407
commit fc138609a1
24 changed files with 257 additions and 29 deletions

View File

@@ -16,6 +16,7 @@ import { ReplyProvider } from '@/providers/ReplyProvider'
import { ScreenSizeProvider } from '@/providers/ScreenSizeProvider'
import { ThemeProvider } from '@/providers/ThemeProvider'
import { TranslationServiceProvider } from '@/providers/TranslationServiceProvider'
import { UserPreferencesProvider } from '@/providers/UserPreferencesProvider'
import { UserTrustProvider } from '@/providers/UserTrustProvider'
import { ZapProvider } from '@/providers/ZapProvider'
import { PageManager } from './PageManager'
@@ -38,8 +39,10 @@ export default function App(): JSX.Element {
<ReplyProvider>
<MediaUploadServiceProvider>
<KindFilterProvider>
<PageManager />
<Toaster />
<UserPreferencesProvider>
<PageManager />
<Toaster />
</UserPreferencesProvider>
</KindFilterProvider>
</MediaUploadServiceProvider>
</ReplyProvider>

View File

@@ -1,8 +1,10 @@
import ParentNotePreview from '@/components/ParentNotePreview'
import { NOTIFICATION_LIST_STYLE } from '@/constants'
import { getEmbeddedPubkeys, getParentBech32Id } from '@/lib/event'
import { toNote } from '@/lib/link'
import { useSecondaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { AtSign, MessageCircle, Quote } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
@@ -19,6 +21,7 @@ export function MentionNotification({
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { pubkey } = useNostr()
const { notificationListStyle } = useUserPreferences()
const isMention = useMemo(() => {
if (!pubkey) return false
const mentions = getEmbeddedPubkeys(notification)
@@ -42,6 +45,7 @@ export function MentionNotification({
sentAt={notification.created_at}
targetEvent={notification}
middle={
notificationListStyle === NOTIFICATION_LIST_STYLE.DETAILED &&
parentEventId && (
<ParentNotePreview
eventId={parentEventId}

View File

@@ -4,11 +4,13 @@ import NoteStats from '@/components/NoteStats'
import { Skeleton } from '@/components/ui/skeleton'
import UserAvatar from '@/components/UserAvatar'
import Username from '@/components/Username'
import { NOTIFICATION_LIST_STYLE } from '@/constants'
import { toNote, toProfile } from '@/lib/link'
import { cn } from '@/lib/utils'
import { useSecondaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider'
import { useNotification } from '@/providers/NotificationProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { NostrEvent } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@@ -38,22 +40,52 @@ export default function Notification({
const { push } = useSecondaryPage()
const { pubkey } = useNostr()
const { isNotificationRead, markNotificationAsRead } = useNotification()
const { notificationListStyle } = useUserPreferences()
const unread = useMemo(
() => isNew && !isNotificationRead(notificationId),
[isNew, isNotificationRead, notificationId]
)
const handleClick = () => {
markNotificationAsRead(notificationId)
if (targetEvent) {
push(toNote(targetEvent.id))
} else if (pubkey) {
push(toProfile(pubkey))
}
}
if (notificationListStyle === NOTIFICATION_LIST_STYLE.COMPACT) {
return (
<div
className="flex items-center justify-between cursor-pointer py-2 px-4"
onClick={handleClick}
>
<div className="flex gap-2 items-center flex-1 w-0">
<UserAvatar userId={sender} size="small" />
{icon}
{middle}
{targetEvent && (
<ContentPreview
className={cn(
'truncate flex-1 w-0',
unread ? 'font-semibold' : 'text-muted-foreground'
)}
event={targetEvent}
/>
)}
</div>
<div className="text-muted-foreground shrink-0">
<FormattedTimestamp timestamp={sentAt} short />
</div>
</div>
)
}
return (
<div
className="clickable flex items-start gap-2 cursor-pointer py-2 px-4 border-b"
onClick={() => {
markNotificationAsRead(notificationId)
if (targetEvent) {
push(toNote(targetEvent.id))
} else if (pubkey) {
push(toProfile(pubkey))
}
}}
onClick={handleClick}
>
<div className="flex gap-2 items-center mt-1.5">
{icon}
@@ -95,6 +127,17 @@ export default function Notification({
}
export function NotificationSkeleton() {
const { notificationListStyle } = useUserPreferences()
if (notificationListStyle === NOTIFICATION_LIST_STYLE.COMPACT) {
return (
<div className="flex gap-2 items-center h-11 py-2 px-4">
<Skeleton className="w-7 h-7 rounded-full" />
<Skeleton className="h-6 flex-1 w-0" />
</div>
)
}
return (
<div className="flex items-start gap-2 cursor-pointer py-2 px-4">
<div className="flex gap-2 items-center mt-1.5">

View File

@@ -1,8 +1,9 @@
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { BIG_RELAY_URLS, ExtendedKind, NOTIFICATION_LIST_STYLE } from '@/constants'
import { compareEvents } from '@/lib/event'
import { usePrimaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider'
import { useNotification } from '@/providers/NotificationProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import client from '@/services/client.service'
import noteStatsService from '@/services/note-stats.service'
@@ -34,6 +35,7 @@ const NotificationList = forwardRef((_, ref) => {
const { pubkey } = useNostr()
const { hideUntrustedNotifications, isUserTrusted } = useUserTrust()
const { getNotificationsSeenAt } = useNotification()
const { notificationListStyle } = useUserPreferences()
const [notificationType, setNotificationType] = useState<TNotificationType>('all')
const [lastReadTime, setLastReadTime] = useState(0)
const [refreshCount, setRefreshCount] = useState(0)
@@ -262,7 +264,7 @@ const NotificationList = forwardRef((_, ref) => {
}}
pullingContent=""
>
<div>
<div className={notificationListStyle === NOTIFICATION_LIST_STYLE.COMPACT ? 'pt-2' : ''}>
{visibleNotifications.map((notification) => (
<NotificationItem
key={notification.id}

View File

@@ -41,6 +41,7 @@ export const StorageKey = {
SHOW_KINDS: 'showKinds',
SHOW_KINDS_VERSION: 'showKindsVersion',
HIDE_CONTENT_MENTIONING_MUTED_USERS: 'hideContentMentioningMutedUsers',
NOTIFICATION_LIST_STYLE: 'notificationListStyle',
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated
@@ -136,3 +137,8 @@ export const POLL_TYPE = {
MULTIPLE_CHOICE: 'multiplechoice',
SINGLE_CHOICE: 'singlechoice'
} as const
export const NOTIFICATION_LIST_STYLE = {
COMPACT: 'compact',
DETAILED: 'detailed'
} as const

View File

@@ -392,6 +392,11 @@ export default {
profanity: 'ألفاظ نابية',
illegal: 'محتوى غير قانوني',
spam: 'رسائل مزعجة',
other: 'أخرى'
other: 'أخرى',
'Notification list style': 'نمط قائمة الإشعارات',
'See extra info for each notification': 'عرض معلومات إضافية لكل إشعار',
'See more notifications at a glance': 'رؤية المزيد من الإشعارات بنظرة سريعة',
Detailed: 'تفصيلي',
Compact: 'مضغوط'
}
}

View File

@@ -401,6 +401,12 @@ export default {
profanity: 'Obszönität',
illegal: 'Illegaler Inhalt',
spam: 'Spam',
other: 'Sonstiges'
other: 'Sonstiges',
'Notification list style': 'Benachrichtigungslistenstil',
'See extra info for each notification':
'Zusätzliche Informationen für jede Benachrichtigung anzeigen',
'See more notifications at a glance': 'Mehr Benachrichtigungen auf einen Blick sehen',
Detailed: 'Detailliert',
Compact: 'Kompakt'
}
}

View File

@@ -391,6 +391,11 @@ export default {
profanity: 'Profanity',
illegal: 'Illegal content',
spam: 'Spam',
other: 'Other'
other: 'Other',
'Notification list style': 'Notification list style',
'See extra info for each notification': 'See extra info for each notification',
'See more notifications at a glance': 'See more notifications at a glance',
Detailed: 'Detailed',
Compact: 'Compact'
}
}

View File

@@ -397,6 +397,11 @@ export default {
profanity: 'Blasfemia',
illegal: 'Contenido ilegal',
spam: 'Spam',
other: 'Otro'
other: 'Otro',
'Notification list style': 'Estilo de lista de notificaciones',
'See extra info for each notification': 'Ver información adicional para cada notificación',
'See more notifications at a glance': 'Ver más notificaciones de un vistazo',
Detailed: 'Detallado',
Compact: 'Compacto'
}
}

View File

@@ -393,6 +393,11 @@ export default {
profanity: 'فحاشی',
illegal: 'محتوای غیرقانونی',
spam: 'اسپم',
other: 'سایر'
other: 'سایر',
'Notification list style': 'سبک فهرست اعلان‌ها',
'See extra info for each notification': 'مشاهده اطلاعات اضافی برای هر اعلان',
'See more notifications at a glance': 'مشاهده اعلان‌های بیشتر در یک نگاه',
Detailed: 'تفصیلی',
Compact: 'فشرده'
}
}

View File

@@ -401,6 +401,12 @@ export default {
profanity: 'Blasphème',
illegal: 'Contenu illégal',
spam: 'Spam',
other: 'Autre'
other: 'Autre',
'Notification list style': 'Style de liste de notifications',
'See extra info for each notification':
'Voir des infos supplémentaires pour chaque notification',
'See more notifications at a glance': "Voir plus de notifications en un coup d'œil",
Detailed: 'Détaillé',
Compact: 'Compact'
}
}

View File

@@ -397,6 +397,11 @@ export default {
profanity: 'Blasfemia',
illegal: 'Contenuto illegale',
spam: 'Spam',
other: 'Altro'
other: 'Altro',
'Notification list style': 'Stile elenco notifiche',
'See extra info for each notification': 'Visualizza informazioni extra per ogni notifica',
'See more notifications at a glance': "Visualizza più notifiche a colpo d'occhio",
Detailed: 'Dettagliato',
Compact: 'Compatto'
}
}

View File

@@ -394,6 +394,11 @@ export default {
profanity: '冒涜的な内容',
illegal: '違法コンテンツ',
spam: 'スパム',
other: 'その他'
other: 'その他',
'Notification list style': '通知リストスタイル',
'See extra info for each notification': '各通知の詳細情報を表示',
'See more notifications at a glance': '一目でより多くの通知を確認',
Detailed: '詳細',
Compact: 'コンパクト'
}
}

View File

@@ -394,6 +394,11 @@ export default {
profanity: '욕설',
illegal: '불법 콘텐츠',
spam: '스팸',
other: '기타'
other: '기타',
'Notification list style': '알림 목록 스타일',
'See extra info for each notification': '각 알림의 추가 정보 보기',
'See more notifications at a glance': '한눈에 더 많은 알림 보기',
Detailed: '상세',
Compact: '간단'
}
}

View File

@@ -398,6 +398,11 @@ export default {
profanity: 'Wulgaryzmy',
illegal: 'Nielegalna treść',
spam: 'Spam',
other: 'Inne'
other: 'Inne',
'Notification list style': 'Styl listy powiadomień',
'See extra info for each notification': 'Zobacz dodatkowe informacje dla każdego powiadomienia',
'See more notifications at a glance': 'Zobacz więcej powiadomień na pierwszy rzut oka',
Detailed: 'Szczegółowy',
Compact: 'Zwięzły'
}
}

View File

@@ -394,6 +394,11 @@ export default {
profanity: 'Blasfêmia',
illegal: 'Conteúdo ilegal',
spam: 'Spam',
other: 'Outro'
other: 'Outro',
'Notification list style': 'Estilo da lista de notificações',
'See extra info for each notification': 'Ver informações extras para cada notificação',
'See more notifications at a glance': 'Ver mais notificações rapidamente',
Detailed: 'Detalhado',
Compact: 'Compacto'
}
}

View File

@@ -397,6 +397,11 @@ export default {
profanity: 'Blasfémia',
illegal: 'Conteúdo ilegal',
spam: 'Spam',
other: 'Outro'
other: 'Outro',
'Notification list style': 'Estilo da lista de notificações',
'See extra info for each notification': 'Ver informações extra para cada notificação',
'See more notifications at a glance': 'Ver mais notificações rapidamente',
Detailed: 'Detalhado',
Compact: 'Compacto'
}
}

View File

@@ -398,6 +398,12 @@ export default {
profanity: 'Ненормативная лексика',
illegal: 'Незаконный контент',
spam: 'Спам',
other: 'Другое'
other: 'Другое',
'Notification list style': 'Стиль списка уведомлений',
'See extra info for each notification':
'Просмотреть дополнительную информацию для каждого уведомления',
'See more notifications at a glance': 'Увидеть больше уведомлений с первого взгляда',
Detailed: 'Подробный',
Compact: 'Компактный'
}
}

View File

@@ -389,6 +389,11 @@ export default {
profanity: 'คำหยาบคาย',
illegal: 'เนื้อหาผิดกฎหมาย',
spam: 'สแปม',
other: 'อื่นๆ'
other: 'อื่นๆ',
'Notification list style': 'รูปแบบรายการการแจ้งเตือน',
'See extra info for each notification': 'ดูข้อมูลเพิ่มเติมสำหรับการแจ้งเตือนแต่ละรายการ',
'See more notifications at a glance': 'ดูการแจ้งเตือนเพิ่มเติมในแวบเดียว',
Detailed: 'รายละเอียด',
Compact: 'กะทัดรัด'
}
}

View File

@@ -387,6 +387,11 @@ export default {
profanity: '亵渎言论',
illegal: '违法内容',
spam: '垃圾信息',
other: '其他'
other: '其他',
'Notification list style': '通知列表样式',
'See extra info for each notification': '查看每条通知的详细信息',
'See more notifications at a glance': '一目了然地查看更多通知',
Detailed: '详细',
Compact: '紧凑'
}
}

View File

@@ -1,11 +1,13 @@
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import { NOTIFICATION_LIST_STYLE } from '@/constants'
import { LocalizedLanguageNames, TLanguage } from '@/i18n'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { cn } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useTheme } from '@/providers/ThemeProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import { SelectValue } from '@radix-ui/react-select'
import { ExternalLink } from 'lucide-react'
@@ -25,6 +27,7 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
setHideContentMentioningMutedUsers
} = useContentPolicy()
const { hideUntrustedNotes, updateHideUntrustedNotes } = useUserTrust()
const { notificationListStyle, updateNotificationListStyle } = useUserPreferences()
const handleLanguageChange = (value: TLanguage) => {
i18n.changeLanguage(value)
@@ -66,6 +69,29 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
</SelectContent>
</Select>
</SettingItem>
<SettingItem>
<Label htmlFor="notification-list-style" className="text-base font-normal">
<div>{t('Notification list style')}</div>
<div className="text-muted-foreground">
{notificationListStyle === NOTIFICATION_LIST_STYLE.DETAILED
? t('See extra info for each notification')
: t('See more notifications at a glance')}
</div>
</Label>
<Select
defaultValue={NOTIFICATION_LIST_STYLE.DETAILED}
value={notificationListStyle}
onValueChange={updateNotificationListStyle}
>
<SelectTrigger id="notification-list-style" className="w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={NOTIFICATION_LIST_STYLE.DETAILED}>{t('Detailed')}</SelectItem>
<SelectItem value={NOTIFICATION_LIST_STYLE.COMPACT}>{t('Compact')}</SelectItem>
</SelectContent>
</Select>
</SettingItem>
<SettingItem>
<Label htmlFor="autoplay" className="text-base font-normal">
<div>{t('Autoplay')}</div>

View File

@@ -0,0 +1,40 @@
import storage from '@/services/local-storage.service'
import { TNotificationStyle } from '@/types'
import { createContext, useContext, useState } from 'react'
type TUserPreferencesContext = {
notificationListStyle: TNotificationStyle
updateNotificationListStyle: (style: TNotificationStyle) => void
}
const UserPreferencesContext = createContext<TUserPreferencesContext | undefined>(undefined)
export const useUserPreferences = () => {
const context = useContext(UserPreferencesContext)
if (!context) {
throw new Error('useUserPreferences must be used within a UserPreferencesProvider')
}
return context
}
export function UserPreferencesProvider({ children }: { children: React.ReactNode }) {
const [notificationListStyle, setNotificationListStyle] = useState(
storage.getNotificationListStyle()
)
const updateNotificationListStyle = (style: TNotificationStyle) => {
setNotificationListStyle(style)
storage.setNotificationListStyle(style)
}
return (
<UserPreferencesContext.Provider
value={{
notificationListStyle,
updateNotificationListStyle
}}
>
{children}
</UserPreferencesContext.Provider>
)
}

View File

@@ -1,4 +1,10 @@
import { DEFAULT_NIP_96_SERVICE, ExtendedKind, SUPPORTED_KINDS, StorageKey } from '@/constants'
import {
DEFAULT_NIP_96_SERVICE,
ExtendedKind,
NOTIFICATION_LIST_STYLE,
SUPPORTED_KINDS,
StorageKey
} from '@/constants'
import { isSameAccount } from '@/lib/account'
import { randomString } from '@/lib/random'
import {
@@ -7,6 +13,7 @@ import {
TFeedInfo,
TMediaUploadServiceConfig,
TNoteListMode,
TNotificationStyle,
TRelaySet,
TThemeSetting,
TTranslationServiceConfig
@@ -36,6 +43,7 @@ class LocalStorageService {
private dismissedTooManyRelaysAlert: boolean = false
private showKinds: number[] = []
private hideContentMentioningMutedUsers: boolean = false
private notificationListStyle: TNotificationStyle = NOTIFICATION_LIST_STYLE.DETAILED
constructor() {
if (!LocalStorageService.instance) {
@@ -160,6 +168,12 @@ class LocalStorageService {
this.hideContentMentioningMutedUsers =
window.localStorage.getItem(StorageKey.HIDE_CONTENT_MENTIONING_MUTED_USERS) === 'true'
this.notificationListStyle =
window.localStorage.getItem(StorageKey.NOTIFICATION_LIST_STYLE) ===
NOTIFICATION_LIST_STYLE.COMPACT
? NOTIFICATION_LIST_STYLE.COMPACT
: NOTIFICATION_LIST_STYLE.DETAILED
// Clean up deprecated data
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
@@ -410,6 +424,15 @@ class LocalStorageService {
this.hideContentMentioningMutedUsers = hide
window.localStorage.setItem(StorageKey.HIDE_CONTENT_MENTIONING_MUTED_USERS, hide.toString())
}
getNotificationListStyle() {
return this.notificationListStyle
}
setNotificationListStyle(style: TNotificationStyle) {
this.notificationListStyle = style
window.localStorage.setItem(StorageKey.NOTIFICATION_LIST_STYLE, style)
}
}
const instance = new LocalStorageService()

View File

@@ -1,5 +1,5 @@
import { Event, VerifiedEvent, Filter } from 'nostr-tools'
import { POLL_TYPE } from './constants'
import { NOTIFICATION_LIST_STYLE, POLL_TYPE } from '../constants'
export type TSubRequestFilter = Omit<Filter, 'since' | 'until'> & { limit: number }
@@ -182,3 +182,6 @@ export type TSearchParams = {
search: string
input?: string
}
export type TNotificationStyle =
(typeof NOTIFICATION_LIST_STYLE)[keyof typeof NOTIFICATION_LIST_STYLE]