feat: support for follow packs

This commit is contained in:
codytseng
2025-11-27 00:02:13 +08:00
parent cdab9aa19e
commit b21855c294
27 changed files with 384 additions and 24 deletions

View File

@@ -0,0 +1,22 @@
import { getFollowPackInfoFromEvent } from '@/lib/event-metadata'
import { cn } from '@/lib/utils'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
export default function FollowPackPreview({
event,
className
}: {
event: Event
className?: string
}) {
const { t } = useTranslation()
const { title } = useMemo(() => getFollowPackInfoFromEvent(event), [event])
return (
<div className={cn('truncate', className)}>
[{t('Follow Pack')}] <span className="italic pr-0.5">{title}</span>
</div>
)
}

View File

@@ -8,6 +8,7 @@ import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import CommunityDefinitionPreview from './CommunityDefinitionPreview'
import EmojiPackPreview from './EmojiPackPreview'
import FollowPackPreview from './FollowPackPreview'
import GroupMetadataPreview from './GroupMetadataPreview'
import HighlightPreview from './HighlightPreview'
import LiveEventPreview from './LiveEventPreview'
@@ -105,5 +106,9 @@ export default function ContentPreview({
return <EmojiPackPreview event={event} className={className} />
}
if (event.kind === ExtendedKind.FOLLOW_PACK) {
return <FollowPackPreview event={event} className={className} />
}
return <div className={className}>[{t('Cannot handle event of kind k', { k: event.kind })}]</div>
}

View File

@@ -0,0 +1,55 @@
import { Button } from '@/components/ui/button'
import { getFollowPackInfoFromEvent } from '@/lib/event-metadata'
import { toFollowPack } from '@/lib/link'
import { useSecondaryPage } from '@/PageManager'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Image from '../Image'
export default function FollowPack({ event, className }: { event: Event; className?: string }) {
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { title, description, image, pubkeys } = useMemo(
() => getFollowPackInfoFromEvent(event),
[event]
)
const handleViewDetails = (e: React.MouseEvent) => {
e.stopPropagation()
push(toFollowPack(event))
}
return (
<div className={className}>
<div className="flex items-start gap-2 mb-2">
{image && (
<Image
image={{ url: image, pubkey: event.pubkey }}
className="w-24 h-20 object-cover rounded-lg"
classNames={{
wrapper: 'w-24 h-20 flex-shrink-0',
errorPlaceholder: 'w-24 h-20'
}}
hideIfError
/>
)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h3 className="text-xl font-semibold mb-1 truncate">{title}</h3>
<span className="text-xs text-muted-foreground shrink-0">
{t('n users', { count: pubkeys.length })}
</span>
</div>
{description && (
<p className="text-sm text-muted-foreground line-clamp-2">{description}</p>
)}
</div>
</div>
<Button onClick={handleViewDetails} variant="outline" className="w-full">
{t('View Details')}
</Button>
</div>
)
}

View File

@@ -21,6 +21,7 @@ import UserAvatar from '../UserAvatar'
import Username from '../Username'
import CommunityDefinition from './CommunityDefinition'
import EmojiPack from './EmojiPack'
import FollowPack from './FollowPack'
import GroupMetadata from './GroupMetadata'
import Highlight from './Highlight'
import LiveEvent from './LiveEvent'
@@ -109,6 +110,8 @@ export default function Note({
content = <RelayReview className="mt-2" event={event} />
} else if (event.kind === kinds.Emojisets) {
content = <EmojiPack className="mt-2" event={event} />
} else if (event.kind === ExtendedKind.FOLLOW_PACK) {
content = <FollowPack className="mt-2" event={event} />
} else {
content = <Content className="mt-2" event={event} />
}

View File

@@ -81,13 +81,14 @@ export const ExtendedKind = {
VOICE_COMMENT: 1244,
FAVORITE_RELAYS: 10012,
BLOSSOM_SERVER_LIST: 10063,
FOLLOW_PACK: 39089,
RELAY_REVIEW: 31987,
GROUP_METADATA: 39000,
ADDRESSABLE_NORMAL_VIDEO: 34235,
ADDRESSABLE_SHORT_VIDEO: 34236
}
export const SUPPORTED_KINDS = [
export const ALLOWED_FILTER_KINDS = [
kinds.ShortTextNote,
kinds.Repost,
kinds.GenericRepost,
@@ -100,12 +101,17 @@ export const SUPPORTED_KINDS = [
ExtendedKind.VOICE_COMMENT,
kinds.Highlights,
kinds.LongFormArticle,
ExtendedKind.RELAY_REVIEW,
kinds.Emojisets,
ExtendedKind.ADDRESSABLE_NORMAL_VIDEO,
ExtendedKind.ADDRESSABLE_SHORT_VIDEO
]
export const SUPPORTED_KINDS = [
...ALLOWED_FILTER_KINDS,
ExtendedKind.RELAY_REVIEW,
kinds.Emojisets,
ExtendedKind.FOLLOW_PACK
]
export const URL_REGEX =
/https?:\/\/[\w\p{L}\p{N}\p{M}&.\-/?=#@%+_:!~*]+[^\s.,;:'")\]}!?"']/giu
export const WS_URL_REGEX =

View File

@@ -552,6 +552,13 @@ export default {
Highlight: 'تسليط الضوء',
'Optimal relays and {{count}} other relays': 'المرحلات المثلى و {{count}} مرحلات أخرى',
'Likely spam account (Trust score: {{percentile}}%)': 'حساب مشبوه للغاية (درجة الثقة: {{percentile}}%)',
'Suspicious account (Trust score: {{percentile}}%)': 'حساب مشبوه (درجة الثقة: {{percentile}}%)'
'Suspicious account (Trust score: {{percentile}}%)': 'حساب مشبوه (درجة الثقة: {{percentile}}%)',
'n users': '{{count}} مستخدمين',
'View Details': 'عرض التفاصيل',
'Follow Pack Not Found': 'لم يتم العثور على حزمة المتابعة',
'Follow pack not found': 'لم يتم العثور على حزمة المتابعة',
Users: 'المستخدمون',
Feed: 'التغذية',
'Follow Pack': 'حزمة المتابعة'
}
}

View File

@@ -568,6 +568,13 @@ export default {
Highlight: 'Hervorheben',
'Optimal relays and {{count}} other relays': 'Optimale Relays und {{count}} andere Relays',
'Likely spam account (Trust score: {{percentile}}%)': 'Wahrscheinlich Spam-Konto (Vertrauenswert: {{percentile}}%)',
'Suspicious account (Trust score: {{percentile}}%)': 'Verdächtiges Konto (Vertrauenswert: {{percentile}}%)'
'Suspicious account (Trust score: {{percentile}}%)': 'Verdächtiges Konto (Vertrauenswert: {{percentile}}%)',
'n users': '{{count}} Benutzer',
'View Details': 'Details anzeigen',
'Follow Pack Not Found': 'Follow-Pack nicht gefunden',
'Follow pack not found': 'Follow-Pack nicht gefunden',
Users: 'Benutzer',
Feed: 'Feed',
'Follow Pack': 'Follow-Pack'
}
}

View File

@@ -555,6 +555,15 @@ export default {
'Likely spam account (Trust score: {{percentile}}%)':
'Likely spam account (Trust score: {{percentile}}%)',
'Suspicious account (Trust score: {{percentile}}%)':
'Suspicious account (Trust score: {{percentile}}%)'
'Suspicious account (Trust score: {{percentile}}%)',
'n users': '{{count}} users',
'n users_one': '{{count}} user',
'n users_other': '{{count}} users',
'View Details': 'View Details',
'Follow Pack Not Found': 'Follow Pack Not Found',
'Follow pack not found': 'Follow pack not found',
Users: 'Users',
Feed: 'Feed',
'Follow Pack': 'Follow Pack'
}
}

View File

@@ -563,6 +563,13 @@ export default {
Highlight: 'Destacado',
'Optimal relays and {{count}} other relays': 'Relays óptimos y {{count}} otros relays',
'Likely spam account (Trust score: {{percentile}}%)': 'Probablemente cuenta spam (Puntuación de confianza: {{percentile}}%)',
'Suspicious account (Trust score: {{percentile}}%)': 'Cuenta sospechosa (Puntuación de confianza: {{percentile}}%)'
'Suspicious account (Trust score: {{percentile}}%)': 'Cuenta sospechosa (Puntuación de confianza: {{percentile}}%)',
'n users': '{{count}} usuarios',
'View Details': 'Ver detalles',
'Follow Pack Not Found': 'Paquete de seguimiento no encontrado',
'Follow pack not found': 'Paquete de seguimiento no encontrado',
Users: 'Usuarios',
Feed: 'Feed',
'Follow Pack': 'Paquete de Seguimiento'
}
}

View File

@@ -557,6 +557,13 @@ export default {
Highlight: 'برجسته‌سازی',
'Optimal relays and {{count}} other relays': 'رله‌های بهینه و {{count}} رله دیگر',
'Likely spam account (Trust score: {{percentile}}%)': 'احتمالاً حساب هرزنامه (امتیاز اعتماد: {{percentile}}%)',
'Suspicious account (Trust score: {{percentile}}%)': 'حساب مشکوک (امتیاز اعتماد: {{percentile}}%)'
'Suspicious account (Trust score: {{percentile}}%)': 'حساب مشکوک (امتیاز اعتماد: {{percentile}}%)',
'n users': '{{count}} کاربر',
'View Details': 'مشاهده جزئیات',
'Follow Pack Not Found': 'بسته دنبال‌کننده یافت نشد',
'Follow pack not found': 'بسته دنبال‌کننده یافت نشد',
Users: 'کاربران',
Feed: 'فید',
'Follow Pack': 'بسته دنبال‌کننده'
}
}

View File

@@ -566,6 +566,13 @@ export default {
Highlight: 'Surligner',
'Optimal relays and {{count}} other relays': 'Relais optimaux et {{count}} autres relais',
'Likely spam account (Trust score: {{percentile}}%)': 'Compte probablement spam (Score de confiance: {{percentile}}%)',
'Suspicious account (Trust score: {{percentile}}%)': 'Compte suspect (Score de confiance: {{percentile}}%)'
'Suspicious account (Trust score: {{percentile}}%)': 'Compte suspect (Score de confiance: {{percentile}}%)',
'n users': '{{count}} utilisateurs',
'View Details': 'Voir les détails',
'Follow Pack Not Found': 'Pack de suivi introuvable',
'Follow pack not found': 'Pack de suivi introuvable',
Users: 'Utilisateurs',
Feed: 'Flux',
'Follow Pack': 'Pack de Suivi'
}
}

View File

@@ -558,6 +558,13 @@ export default {
Highlight: 'हाइलाइट',
'Optimal relays and {{count}} other relays': 'इष्टतम रिले और {{count}} अन्य रिले',
'Likely spam account (Trust score: {{percentile}}%)': 'संभावित स्पैम खाता (विश्वास स्कोर: {{percentile}}%)',
'Suspicious account (Trust score: {{percentile}}%)': 'संदिग्ध खाता (विश्वास स्कोर: {{percentile}}%)'
'Suspicious account (Trust score: {{percentile}}%)': 'संदिग्ध खाता (विश्वास स्कोर: {{percentile}}%)',
'n users': '{{count}} उपयोगकर्ता',
'View Details': 'विवरण देखें',
'Follow Pack Not Found': 'फॉलो पैक नहीं मिला',
'Follow pack not found': 'फॉलो पैक नहीं मिला',
Users: 'उपयोगकर्ता',
Feed: 'फ़ीड',
'Follow Pack': 'फॉलो पैक'
}
}

View File

@@ -553,6 +553,13 @@ export default {
Highlight: 'Kiemelés',
'Optimal relays and {{count}} other relays': 'Optimális relay-ek és {{count}} másik relay',
'Likely spam account (Trust score: {{percentile}}%)': 'Valószínűleg spam fiók (Megbízhatósági pontszám: {{percentile}}%)',
'Suspicious account (Trust score: {{percentile}}%)': 'Gyanús fiók (Megbízhatósági pontszám: {{percentile}}%)'
'Suspicious account (Trust score: {{percentile}}%)': 'Gyanús fiók (Megbízhatósági pontszám: {{percentile}}%)',
'n users': '{{count}} felhasználó',
'View Details': 'Részletek megtekintése',
'Follow Pack Not Found': 'Követési csomag nem található',
'Follow pack not found': 'Követési csomag nem található',
Users: 'Felhasználók',
Feed: 'Hírfolyam',
'Follow Pack': 'Követési Csomag'
}
}

View File

@@ -562,6 +562,13 @@ export default {
Highlight: 'Evidenzia',
'Optimal relays and {{count}} other relays': 'Relay ottimali e {{count}} altri relay',
'Likely spam account (Trust score: {{percentile}}%)': 'Probabile account spam (Punteggio di fiducia: {{percentile}}%)',
'Suspicious account (Trust score: {{percentile}}%)': 'Account sospetto (Punteggio di fiducia: {{percentile}}%)'
'Suspicious account (Trust score: {{percentile}}%)': 'Account sospetto (Punteggio di fiducia: {{percentile}}%)',
'n users': '{{count}} utenti',
'View Details': 'Visualizza dettagli',
'Follow Pack Not Found': 'Pacchetto di follow non trovato',
'Follow pack not found': 'Pacchetto di follow non trovato',
Users: 'Utenti',
Feed: 'Feed',
'Follow Pack': 'Pacchetto di Follow'
}
}

View File

@@ -557,6 +557,13 @@ export default {
Highlight: 'ハイライト',
'Optimal relays and {{count}} other relays': '最適なリレーと他の{{count}}個のリレー',
'Likely spam account (Trust score: {{percentile}}%)': 'スパムの可能性が高いアカウント(信頼スコア:{{percentile}}%',
'Suspicious account (Trust score: {{percentile}}%)': '疑わしいアカウント(信頼スコア:{{percentile}}%'
'Suspicious account (Trust score: {{percentile}}%)': '疑わしいアカウント(信頼スコア:{{percentile}}%',
'n users': '{{count}}人のユーザー',
'View Details': '詳細を表示',
'Follow Pack Not Found': 'フォローパックが見つかりません',
'Follow pack not found': 'フォローパックが見つかりません',
Users: 'ユーザー',
Feed: 'フィード',
'Follow Pack': 'フォローパック'
}
}

View File

@@ -557,6 +557,13 @@ export default {
Highlight: '하이라이트',
'Optimal relays and {{count}} other relays': '최적 릴레이 및 기타 {{count}}개 릴레이',
'Likely spam account (Trust score: {{percentile}}%)': '스팸 계정 가능성 높음 (신뢰 점수: {{percentile}}%)',
'Suspicious account (Trust score: {{percentile}}%)': '의심스러운 계정 (신뢰 점수: {{percentile}}%)'
'Suspicious account (Trust score: {{percentile}}%)': '의심스러운 계정 (신뢰 점수: {{percentile}}%)',
'n users': '{{count}}명의 사용자',
'View Details': '세부 정보 보기',
'Follow Pack Not Found': '팔로우 팩을 찾을 수 없음',
'Follow pack not found': '팔로우 팩을 찾을 수 없습니다',
Users: '사용자',
Feed: '피드',
'Follow Pack': '팔로우 팩'
}
}

View File

@@ -563,6 +563,13 @@ export default {
Highlight: 'Podświetl',
'Optimal relays and {{count}} other relays': 'Optymalne przekaźniki i {{count}} innych przekaźników',
'Likely spam account (Trust score: {{percentile}}%)': 'Prawdopodobnie konto spamowe (Wynik zaufania: {{percentile}}%)',
'Suspicious account (Trust score: {{percentile}}%)': 'Podejrzane konto (Wynik zaufania: {{percentile}}%)'
'Suspicious account (Trust score: {{percentile}}%)': 'Podejrzane konto (Wynik zaufania: {{percentile}}%)',
'n users': '{{count}} użytkowników',
'View Details': 'Zobacz szczegóły',
'Follow Pack Not Found': 'Nie znaleziono pakietu obserwowanych',
'Follow pack not found': 'Nie znaleziono pakietu obserwowanych',
Users: 'Użytkownicy',
Feed: 'Kanał',
'Follow Pack': 'Pakiet Obserwowanych'
}
}

View File

@@ -558,6 +558,13 @@ export default {
Highlight: 'Marcação',
'Optimal relays and {{count}} other relays': 'Relays ideais e {{count}} outros relays',
'Likely spam account (Trust score: {{percentile}}%)': 'Provável conta de spam (Pontuação de confiança: {{percentile}}%)',
'Suspicious account (Trust score: {{percentile}}%)': 'Conta suspeita (Pontuação de confiança: {{percentile}}%)'
'Suspicious account (Trust score: {{percentile}}%)': 'Conta suspeita (Pontuação de confiança: {{percentile}}%)',
'n users': '{{count}} usuários',
'View Details': 'Ver detalhes',
'Follow Pack Not Found': 'Pacote de seguir não encontrado',
'Follow pack not found': 'Pacote de seguir não encontrado',
Users: 'Usuários',
Feed: 'Feed',
'Follow Pack': 'Pacote de Seguir'
}
}

View File

@@ -561,6 +561,13 @@ export default {
Highlight: 'Destacar',
'Optimal relays and {{count}} other relays': 'Relays ideais e {{count}} outros relays',
'Likely spam account (Trust score: {{percentile}}%)': 'Provável conta de spam (Pontuação de confiança: {{percentile}}%)',
'Suspicious account (Trust score: {{percentile}}%)': 'Conta suspeita (Pontuação de confiança: {{percentile}}%)'
'Suspicious account (Trust score: {{percentile}}%)': 'Conta suspeita (Pontuação de confiança: {{percentile}}%)',
'n users': '{{count}} utilizadores',
'View Details': 'Ver detalhes',
'Follow Pack Not Found': 'Pacote de seguir não encontrado',
'Follow pack not found': 'Pacote de seguir não encontrado',
Users: 'Utilizadores',
Feed: 'Feed',
'Follow Pack': 'Pacote de Seguir'
}
}

View File

@@ -563,6 +563,13 @@ export default {
Highlight: 'Выделить',
'Optimal relays and {{count}} other relays': 'Оптимальные релеи и {{count}} других релеев',
'Likely spam account (Trust score: {{percentile}}%)': 'Вероятно спам-аккаунт (Оценка доверия: {{percentile}}%)',
'Suspicious account (Trust score: {{percentile}}%)': 'Подозрительный аккаунт (Оценка доверия: {{percentile}}%)'
'Suspicious account (Trust score: {{percentile}}%)': 'Подозрительный аккаунт (Оценка доверия: {{percentile}}%)',
'n users': '{{count}} пользователей',
'View Details': 'Посмотреть детали',
'Follow Pack Not Found': 'Пакет подписок не найден',
'Follow pack not found': 'Пакет подписок не найден',
Users: 'Пользователи',
Feed: 'Лента',
'Follow Pack': 'Пакет Подписок'
}
}

View File

@@ -550,6 +550,13 @@ export default {
Highlight: 'ไฮไลต์',
'Optimal relays and {{count}} other relays': 'รีเลย์ที่เหมาะสมและรีเลย์อื่น {{count}} รายการ',
'Likely spam account (Trust score: {{percentile}}%)': 'น่าจะเป็นบัญชีสแปม (คะแนนความน่าเชื่อถือ: {{percentile}}%)',
'Suspicious account (Trust score: {{percentile}}%)': 'บัญชีที่น่าสงสัย (คะแนนความน่าเชื่อถือ: {{percentile}}%)'
'Suspicious account (Trust score: {{percentile}}%)': 'บัญชีที่น่าสงสัย (คะแนนความน่าเชื่อถือ: {{percentile}}%)',
'n users': '{{count}} ผู้ใช้',
'View Details': 'ดูรายละเอียด',
'Follow Pack Not Found': 'ไม่พบแพ็คการติดตาม',
'Follow pack not found': 'ไม่พบแพ็คการติดตาม',
Users: 'ผู้ใช้',
Feed: 'ฟีด',
'Follow Pack': 'แพ็คการติดตาม'
}
}

View File

@@ -545,6 +545,13 @@ export default {
Highlight: '高亮',
'Optimal relays and {{count}} other relays': '最佳中继器和其他 {{count}} 个中继器',
'Likely spam account (Trust score: {{percentile}}%)': '疑似垃圾账号(信任分数:{{percentile}}%',
'Suspicious account (Trust score: {{percentile}}%)': '可疑账号(信任分数:{{percentile}}%'
'Suspicious account (Trust score: {{percentile}}%)': '可疑账号(信任分数:{{percentile}}%',
'n users': '{{count}} 位用户',
'View Details': '查看详情',
'Follow Pack Not Found': '未找到关注包',
'Follow pack not found': '未找到关注包',
Users: '用户',
Feed: '动态',
'Follow Pack': '关注包'
}
}

View File

@@ -4,7 +4,7 @@ import { Event, kinds } from 'nostr-tools'
import { buildATag } from './draft-event'
import { getReplaceableEventIdentifier } from './event'
import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning'
import { formatPubkey, pubkeyToNpub } from './pubkey'
import { formatPubkey, isValidPubkey, pubkeyToNpub } from './pubkey'
import { generateBech32IdFromETag, getEmojiInfosFromEmojiTags, tagNameEquals } from './tag'
import { isOnionUrl, isWebsocketUrl, normalizeHttpUrl, normalizeUrl } from './url'
@@ -403,3 +403,28 @@ export function getPinnedEventHexIdSetFromPinListEvent(event?: Event | null): Se
.slice(0, MAX_PINNED_NOTES) ?? []
)
}
export function getFollowPackInfoFromEvent(event: Event) {
let title: string | undefined
let description: string | undefined
let image: string | undefined
const pubkeys: string[] = []
event.tags.forEach(([tagName, tagValue]) => {
if (tagName === 'title') {
title = tagValue
} else if (tagName === 'description') {
description = tagValue
} else if (tagName === 'image') {
image = tagValue
} else if (tagName === 'p' && isValidPubkey(tagValue)) {
pubkeys.push(tagValue)
}
})
if (!title) {
title = event.tags.find(tagNameEquals('d'))?.[1] ?? 'Untitled Follow Pack'
}
return { title, description, image, pubkeys }
}

View File

@@ -77,6 +77,11 @@ export const toRelayReviews = (url: string) => `/relays/${encodeURIComponent(url
export const toMuteList = () => '/mutes'
export const toRizful = () => '/rizful'
export const toBookmarks = () => '/bookmarks'
export const toFollowPack = (eventOrId: Event | string) => {
if (typeof eventOrId === 'string') return `/follow-packs/${eventOrId}`
const naddr = getNoteBech32Id(eventOrId)
return `/follow-packs/${naddr}`
}
export const toChachiChat = (relay: string, d: string) => {
return `https://chachi.chat/${relay.replace(/^wss?:\/\//, '').replace(/\/$/, '')}/${d}`

View File

@@ -0,0 +1,116 @@
import ImageWithLightbox from '@/components/ImageWithLightbox'
import NormalFeed from '@/components/NormalFeed'
import ProfileList from '@/components/ProfileList'
import { Skeleton } from '@/components/ui/skeleton'
import { useFetchEvent } from '@/hooks/useFetchEvent'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { getFollowPackInfoFromEvent } from '@/lib/event-metadata'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service'
import { TFeedSubRequest } from '@/types'
import { forwardRef, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
const FollowPackPage = forwardRef(({ id, index }: { id?: string; index?: number }, ref) => {
const { t } = useTranslation()
const [tab, setTab] = useState<'users' | 'feed'>('users')
const { event, isFetching } = useFetchEvent(id)
const { title, description, image, pubkeys } = useMemo(() => {
if (!event) return { title: '', description: '', image: '', pubkeys: [] }
return getFollowPackInfoFromEvent(event)
}, [event])
if (isFetching) {
return (
<SecondaryPageLayout ref={ref} index={index} title={t('Follow Pack')}>
<div className="px-4 py-3 space-y-2">
<Skeleton className="h-48 w-full" />
<Skeleton className="h-7 py-1 w-full" />
</div>
</SecondaryPageLayout>
)
}
if (!event) {
return (
<SecondaryPageLayout ref={ref} index={index} title={t('Follow Pack')}>
<div className="p-4 text-center text-muted-foreground">{t('Follow pack not found')}</div>
</SecondaryPageLayout>
)
}
return (
<SecondaryPageLayout ref={ref} index={index} title={t('Follow Pack')} displayScrollToTopButton>
<div>
{/* Header */}
<div className="px-4 pt-3 space-y-2">
{image && (
<ImageWithLightbox
image={{ url: image, pubkey: event.pubkey }}
className="w-full h-48 object-cover rounded-lg"
classNames={{
wrapper: 'w-full h-48 border-none'
}}
/>
)}
<div className="flex items-center gap-2">
<h3 className="text-2xl font-semibold mb-1 truncate">{title}</h3>
<span className="text-xs text-muted-foreground shrink-0">
{t('n users', { count: pubkeys.length })}
</span>
</div>
{description && (
<p className="text-sm text-muted-foreground whitespace-pre-wrap">{description}</p>
)}
<div className="inline-flex items-center rounded-lg border bg-muted/50">
<button
onClick={() => setTab('users')}
className={cn(
'px-3 py-1.5 text-sm font-medium rounded-l-lg transition-colors',
tab === 'users'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
)}
>
{t('Users')}
</button>
<button
onClick={() => setTab('feed')}
className={cn(
'px-3 py-1.5 text-sm font-medium rounded-r-lg transition-colors',
tab === 'feed'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
)}
>
{t('Feed')}
</button>
</div>
</div>
{/* Content */}
{tab === 'users' && <ProfileList pubkeys={pubkeys} />}
{tab === 'feed' && pubkeys.length > 0 && <Feed pubkeys={pubkeys} />}
</div>
</SecondaryPageLayout>
)
})
FollowPackPage.displayName = 'FollowPackPage'
export default FollowPackPage
function Feed({ pubkeys }: { pubkeys: string[] }) {
const { pubkey: myPubkey } = useNostr()
const [subRequests, setSubRequests] = useState<TFeedSubRequest[]>([])
useEffect(() => {
client.generateSubRequestsForPubkeys(pubkeys, myPubkey).then(setSubRequests)
}, [pubkeys, myPubkey])
return <NormalFeed subRequests={subRequests} />
}

View File

@@ -3,6 +3,7 @@ import BookmarkPage from '@/pages/secondary/BookmarkPage'
import EmojiPackSettingsPage from '@/pages/secondary/EmojiPackSettingsPage'
import ExternalContentPage from '@/pages/secondary/ExternalContentPage'
import FollowingListPage from '@/pages/secondary/FollowingListPage'
import FollowPackPage from '@/pages/secondary/FollowPackPage'
import GeneralSettingsPage from '@/pages/secondary/GeneralSettingsPage'
import MuteListPage from '@/pages/secondary/MuteListPage'
import NoteListPage from '@/pages/secondary/NoteListPage'
@@ -48,7 +49,8 @@ const SECONDARY_ROUTE_CONFIGS = [
{ path: '/profile-editor', element: <ProfileEditorPage /> },
{ path: '/mutes', element: <MuteListPage /> },
{ path: '/rizful', element: <RizfulPage /> },
{ path: '/bookmarks', element: <BookmarkPage /> }
{ path: '/bookmarks', element: <BookmarkPage /> },
{ path: '/follow-packs/:id', element: <FollowPackPage /> }
]
export const SECONDARY_ROUTES = SECONDARY_ROUTE_CONFIGS.map(({ path, element }) => ({

View File

@@ -1,10 +1,10 @@
import {
ALLOWED_FILTER_KINDS,
DEFAULT_FAVICON_URL_TEMPLATE,
DEFAULT_NIP_96_SERVICE,
ExtendedKind,
MEDIA_AUTO_LOAD_POLICY,
NOTIFICATION_LIST_STYLE,
SUPPORTED_KINDS,
StorageKey,
TPrimaryColor
} from '@/constants'
@@ -165,7 +165,7 @@ class LocalStorageService {
const showKindsStr = window.localStorage.getItem(StorageKey.SHOW_KINDS)
if (!showKindsStr) {
this.showKinds = SUPPORTED_KINDS
this.showKinds = ALLOWED_FILTER_KINDS
} else {
const showKindsVersionStr = window.localStorage.getItem(StorageKey.SHOW_KINDS_VERSION)
const showKindsVersion = showKindsVersionStr ? parseInt(showKindsVersionStr) : 0