feat: support for video events

This commit is contained in:
codytseng
2025-08-24 16:24:35 +08:00
parent d6a5a82cf8
commit 6b88da3f03
28 changed files with 116 additions and 72 deletions

View File

@@ -9,11 +9,11 @@ import {
EmbeddedWebsocketUrlParser, EmbeddedWebsocketUrlParser,
parseContent parseContent
} from '@/lib/content-parser' } from '@/lib/content-parser'
import { getImageInfosFromEvent } from '@/lib/event' import { getImetaInfosFromEvent } from '@/lib/event'
import { getEmojiInfosFromEmojiTags, getImageInfoFromImetaTag } from '@/lib/tag' import { getEmojiInfosFromEmojiTags, getImetaInfoFromImetaTag } from '@/lib/tag'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import mediaUpload from '@/services/media-upload.service' import mediaUpload from '@/services/media-upload.service'
import { TImageInfo } from '@/types' import { TImetaInfo } from '@/types'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { memo } from 'react' import { memo } from 'react'
import { import {
@@ -46,30 +46,30 @@ const Content = memo(
EmbeddedEmojiParser EmbeddedEmojiParser
]) ])
const imageInfos = event ? getImageInfosFromEvent(event) : [] const imetaInfos = event ? getImetaInfosFromEvent(event) : []
const allImages = nodes const allImages = nodes
.map((node) => { .map((node) => {
if (node.type === 'image') { if (node.type === 'image') {
const imageInfo = imageInfos.find((image) => image.url === node.data) const imageInfo = imetaInfos.find((image) => image.url === node.data)
if (imageInfo) { if (imageInfo) {
return imageInfo return imageInfo
} }
const tag = mediaUpload.getImetaTagByUrl(node.data) const tag = mediaUpload.getImetaTagByUrl(node.data)
return tag return tag
? getImageInfoFromImetaTag(tag, event?.pubkey) ? getImetaInfoFromImetaTag(tag, event?.pubkey)
: { url: node.data, pubkey: event?.pubkey } : { url: node.data, pubkey: event?.pubkey }
} }
if (node.type === 'images') { if (node.type === 'images') {
const urls = Array.isArray(node.data) ? node.data : [node.data] const urls = Array.isArray(node.data) ? node.data : [node.data]
return urls.map((url) => { return urls.map((url) => {
const imageInfo = imageInfos.find((image) => image.url === url) const imageInfo = imetaInfos.find((image) => image.url === url)
return imageInfo ?? { url, pubkey: event?.pubkey } return imageInfo ?? { url, pubkey: event?.pubkey }
}) })
} }
return null return null
}) })
.filter(Boolean) .filter(Boolean)
.flat() as TImageInfo[] .flat() as TImetaInfo[]
let imageIndex = 0 let imageIndex = 0
const emojiInfos = getEmojiInfosFromEmojiTags(event?.tags) const emojiInfos = getEmojiInfosFromEmojiTags(event?.tags)

View File

@@ -1,7 +1,7 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import client from '@/services/client.service' import client from '@/services/client.service'
import { TImageInfo } from '@/types' import { TImetaInfo } from '@/types'
import { getHashFromURL } from 'blossom-client-sdk' import { getHashFromURL } from 'blossom-client-sdk'
import { decode } from 'blurhash' import { decode } from 'blurhash'
import { ImageOff } from 'lucide-react' import { ImageOff } from 'lucide-react'
@@ -20,7 +20,7 @@ export default function Image({
wrapper?: string wrapper?: string
errorPlaceholder?: string errorPlaceholder?: string
} }
image: TImageInfo image: TImetaInfo
alt?: string alt?: string
hideIfError?: boolean hideIfError?: boolean
errorPlaceholder?: React.ReactNode errorPlaceholder?: React.ReactNode

View File

@@ -1,7 +1,7 @@
import { randomString } from '@/lib/random' import { randomString } from '@/lib/random'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import modalManager from '@/services/modal-manager.service' import modalManager from '@/services/modal-manager.service'
import { TImageInfo } from '@/types' import { TImetaInfo } from '@/types'
import { ReactNode, useEffect, useMemo, useState } from 'react' import { ReactNode, useEffect, useMemo, useState } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import Lightbox from 'yet-another-react-lightbox' import Lightbox from 'yet-another-react-lightbox'
@@ -15,7 +15,7 @@ export default function ImageGallery({
end = images.length end = images.length
}: { }: {
className?: string className?: string
images: TImageInfo[] images: TImetaInfo[]
start?: number start?: number
end?: number end?: number
}) { }) {

View File

@@ -1,7 +1,7 @@
import { randomString } from '@/lib/random' import { randomString } from '@/lib/random'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import modalManager from '@/services/modal-manager.service' import modalManager from '@/services/modal-manager.service'
import { TImageInfo } from '@/types' import { TImetaInfo } from '@/types'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import Lightbox from 'yet-another-react-lightbox' import Lightbox from 'yet-another-react-lightbox'
@@ -12,7 +12,7 @@ export default function ImageWithLightbox({
image, image,
className className
}: { }: {
image: TImageInfo image: TImetaInfo
className?: string className?: string
}) { }) {
const id = useMemo(() => `image-with-lightbox-${randomString()}`, []) const id = useMemo(() => `image-with-lightbox-${randomString()}`, [])

View File

@@ -3,23 +3,24 @@ import { Checkbox } from '@/components/ui/checkbox'
import { Drawer, DrawerContent, DrawerHeader, DrawerTrigger } from '@/components/ui/drawer' import { Drawer, DrawerContent, DrawerHeader, DrawerTrigger } from '@/components/ui/drawer'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { DEFAULT_SHOW_KINDS, ExtendedKind } from '@/constants' import { ExtendedKind, SUPPORTED_KINDS } from '@/constants'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useKindFilter } from '@/providers/KindFilterProvider' import { useKindFilter } from '@/providers/KindFilterProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { ListFilter } from 'lucide-react' import { ListFilter } from 'lucide-react'
import { kinds } from 'nostr-tools' import { kinds } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
const SUPPORTED_KINDS = [ const KIND_FILTER_OPTIONS = [
{ kindGroup: [kinds.ShortTextNote, ExtendedKind.COMMENT], label: 'Posts' }, { kindGroup: [kinds.ShortTextNote, ExtendedKind.COMMENT], label: 'Posts' },
{ kindGroup: [kinds.Repost], label: 'Reposts' }, { kindGroup: [kinds.Repost], label: 'Reposts' },
{ kindGroup: [kinds.LongFormArticle], label: 'Articles' }, { kindGroup: [kinds.LongFormArticle], label: 'Articles' },
{ kindGroup: [kinds.Highlights], label: 'Highlights' }, { kindGroup: [kinds.Highlights], label: 'Highlights' },
{ kindGroup: [ExtendedKind.POLL], label: 'Polls' }, { kindGroup: [ExtendedKind.POLL], label: 'Polls' },
{ kindGroup: [ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT], label: 'Voice Posts' }, { kindGroup: [ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT], label: 'Voice Posts' },
{ kindGroup: [ExtendedKind.PICTURE], label: 'Photo Posts' } { kindGroup: [ExtendedKind.PICTURE], label: 'Photo Posts' },
{ kindGroup: [ExtendedKind.VIDEO, ExtendedKind.SHORT_VIDEO], label: 'Video Posts' }
] ]
export default function KindFilter({ export default function KindFilter({
@@ -35,9 +36,6 @@ export default function KindFilter({
const { updateShowKinds } = useKindFilter() const { updateShowKinds } = useKindFilter()
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds) const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
const [isPersistent, setIsPersistent] = useState(false) const [isPersistent, setIsPersistent] = useState(false)
const isFilterApplied = useMemo(() => {
return showKinds.length !== DEFAULT_SHOW_KINDS.length
}, [showKinds])
useEffect(() => { useEffect(() => {
setTemporaryShowKinds(showKinds) setTemporaryShowKinds(showKinds)
@@ -74,7 +72,7 @@ export default function KindFilter({
<Button <Button
variant="ghost" variant="ghost"
size="titlebar-icon" size="titlebar-icon"
className={cn('mr-1', !isFilterApplied && 'text-muted-foreground')} className="mr-1"
onClick={() => { onClick={() => {
if (isSmallScreen) { if (isSmallScreen) {
setOpen(true) setOpen(true)
@@ -88,7 +86,7 @@ export default function KindFilter({
const content = ( const content = (
<div> <div>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{SUPPORTED_KINDS.map(({ kindGroup, label }) => { {KIND_FILTER_OPTIONS.map(({ kindGroup, label }) => {
const checked = kindGroup.every((k) => temporaryShowKinds.includes(k)) const checked = kindGroup.every((k) => temporaryShowKinds.includes(k))
return ( return (
<div <div
@@ -118,7 +116,7 @@ export default function KindFilter({
<Button <Button
variant="secondary" variant="secondary"
onClick={() => { onClick={() => {
setTemporaryShowKinds(DEFAULT_SHOW_KINDS) setTemporaryShowKinds(SUPPORTED_KINDS)
}} }}
className="flex-1" className="flex-1"
> >

View File

@@ -0,0 +1,16 @@
import { getImetaInfosFromEvent } from '@/lib/event'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import Content from '../Content'
import ImageGallery from '../ImageGallery'
export default function PictureNote({ event, className }: { event: Event; className?: string }) {
const imageInfos = useMemo(() => getImetaInfosFromEvent(event), [event])
return (
<div className={className}>
<Content event={event} />
{imageInfos.length > 0 && <ImageGallery images={imageInfos} />}
</div>
)
}

View File

@@ -0,0 +1,18 @@
import { getImetaInfosFromEvent } from '@/lib/event'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import Content from '../Content'
import MediaPlayer from '../MediaPlayer'
export default function VideoNote({ event, className }: { event: Event; className?: string }) {
const videoInfos = useMemo(() => getImetaInfosFromEvent(event), [event])
return (
<div className={className}>
<Content event={event} />
{videoInfos.map((video) => (
<MediaPlayer src={video.url} key={video.url} className="mt-2" />
))}
</div>
)
}

View File

@@ -1,12 +1,6 @@
import { useSecondaryPage } from '@/PageManager' import { useSecondaryPage } from '@/PageManager'
import { ExtendedKind } from '@/constants' import { ExtendedKind, SUPPORTED_KINDS } from '@/constants'
import { import { getParentBech32Id, getUsingClient, isNsfwEvent } from '@/lib/event'
getImageInfosFromEvent,
getParentBech32Id,
getUsingClient,
isNsfwEvent,
isPictureEvent
} from '@/lib/event'
import { toNote } from '@/lib/link' import { toNote } from '@/lib/link'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
@@ -16,7 +10,6 @@ import { useMemo, useState } from 'react'
import AudioPlayer from '../AudioPlayer' import AudioPlayer from '../AudioPlayer'
import Content from '../Content' import Content from '../Content'
import { FormattedTimestamp } from '../FormattedTimestamp' import { FormattedTimestamp } from '../FormattedTimestamp'
import ImageGallery from '../ImageGallery'
import Nip05 from '../Nip05' import Nip05 from '../Nip05'
import NoteOptions from '../NoteOptions' import NoteOptions from '../NoteOptions'
import ParentNotePreview from '../ParentNotePreview' import ParentNotePreview from '../ParentNotePreview'
@@ -32,8 +25,10 @@ import LongFormArticle from './LongFormArticle'
import LongFormArticlePreview from './LongFormArticlePreview' import LongFormArticlePreview from './LongFormArticlePreview'
import MutedNote from './MutedNote' import MutedNote from './MutedNote'
import NsfwNote from './NsfwNote' import NsfwNote from './NsfwNote'
import PictureNote from './PictureNote'
import Poll from './Poll' import Poll from './Poll'
import UnknownNote from './UnknownNote' import UnknownNote from './UnknownNote'
import VideoNote from './VideoNote'
export default function Note({ export default function Note({
event, event,
@@ -56,10 +51,6 @@ export default function Note({
() => (hideParentNotePreview ? undefined : getParentBech32Id(event)), () => (hideParentNotePreview ? undefined : getParentBech32Id(event)),
[event, hideParentNotePreview] [event, hideParentNotePreview]
) )
const imageInfos = useMemo(
() => (isPictureEvent(event) ? getImageInfosFromEvent(event) : []),
[event]
)
const usingClient = useMemo(() => getUsingClient(event), [event]) const usingClient = useMemo(() => getUsingClient(event), [event])
const { defaultShowNsfw } = useContentPolicy() const { defaultShowNsfw } = useContentPolicy()
const [showNsfw, setShowNsfw] = useState(false) const [showNsfw, setShowNsfw] = useState(false)
@@ -67,21 +58,7 @@ export default function Note({
const [showMuted, setShowMuted] = useState(false) const [showMuted, setShowMuted] = useState(false)
let content: React.ReactNode let content: React.ReactNode
if ( if (!SUPPORTED_KINDS.includes(event.kind)) {
![
kinds.ShortTextNote,
kinds.Highlights,
kinds.LongFormArticle,
kinds.LiveEvent,
kinds.CommunityDefinition,
ExtendedKind.GROUP_METADATA,
ExtendedKind.PICTURE,
ExtendedKind.COMMENT,
ExtendedKind.POLL,
ExtendedKind.VOICE,
ExtendedKind.VOICE_COMMENT
].includes(event.kind)
) {
content = <UnknownNote className="mt-2" event={event} /> content = <UnknownNote className="mt-2" event={event} />
} else if (mutePubkeys.includes(event.pubkey) && !showMuted) { } else if (mutePubkeys.includes(event.pubkey) && !showMuted) {
content = <MutedNote show={() => setShowMuted(true)} /> content = <MutedNote show={() => setShowMuted(true)} />
@@ -110,6 +87,10 @@ export default function Note({
) )
} else if (event.kind === ExtendedKind.VOICE || event.kind === ExtendedKind.VOICE_COMMENT) { } else if (event.kind === ExtendedKind.VOICE || event.kind === ExtendedKind.VOICE_COMMENT) {
content = <AudioPlayer className="mt-2" src={event.content} /> content = <AudioPlayer className="mt-2" src={event.content} />
} else if (event.kind === ExtendedKind.PICTURE) {
content = <PictureNote className="mt-2" event={event} />
} else if (event.kind === ExtendedKind.VIDEO || event.kind === ExtendedKind.SHORT_VIDEO) {
content = <VideoNote className="mt-2" event={event} />
} else { } else {
content = <Content className="mt-2" event={event} /> content = <Content className="mt-2" event={event} />
} }
@@ -159,7 +140,6 @@ export default function Note({
)} )}
<IValue event={event} className="mt-2" /> <IValue event={event} className="mt-2" />
{content} {content}
{imageInfos.length > 0 && <ImageGallery images={imageInfos} />}
</div> </div>
) )
} }

View File

@@ -39,6 +39,7 @@ export const StorageKey = {
DEFAULT_SHOW_NSFW: 'defaultShowNsfw', DEFAULT_SHOW_NSFW: 'defaultShowNsfw',
DISMISSED_TOO_MANY_RELAYS_ALERT: 'dismissedTooManyRelaysAlert', DISMISSED_TOO_MANY_RELAYS_ALERT: 'dismissedTooManyRelaysAlert',
SHOW_KINDS: 'showKinds', SHOW_KINDS: 'showKinds',
SHOW_KINDS_VERSION: 'showKindsVersion',
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated
@@ -67,6 +68,8 @@ export const GROUP_METADATA_EVENT_KIND = 39000
export const ExtendedKind = { export const ExtendedKind = {
PICTURE: 20, PICTURE: 20,
VIDEO: 21,
SHORT_VIDEO: 22,
POLL: 1068, POLL: 1068,
POLL_RESPONSE: 1018, POLL_RESPONSE: 1018,
COMMENT: 1111, COMMENT: 1111,
@@ -77,10 +80,12 @@ export const ExtendedKind = {
GROUP_METADATA: 39000 GROUP_METADATA: 39000
} }
export const DEFAULT_SHOW_KINDS = [ export const SUPPORTED_KINDS = [
kinds.ShortTextNote, kinds.ShortTextNote,
kinds.Repost, kinds.Repost,
ExtendedKind.PICTURE, ExtendedKind.PICTURE,
ExtendedKind.VIDEO,
ExtendedKind.SHORT_VIDEO,
ExtendedKind.POLL, ExtendedKind.POLL,
ExtendedKind.COMMENT, ExtendedKind.COMMENT,
ExtendedKind.VOICE, ExtendedKind.VOICE,

View File

@@ -360,6 +360,7 @@ export default {
Polls: 'الاستطلاعات', Polls: 'الاستطلاعات',
'Voice Posts': 'المشاركات الصوتية', 'Voice Posts': 'المشاركات الصوتية',
'Photo Posts': 'مشاركات الصور', 'Photo Posts': 'مشاركات الصور',
'Video Posts': 'مشاركات الفيديو',
'Select All': 'تحديد الكل', 'Select All': 'تحديد الكل',
'Clear All': 'مسح الكل', 'Clear All': 'مسح الكل',
'Remember my choice': 'تذكر اختياري', 'Remember my choice': 'تذكر اختياري',

View File

@@ -367,6 +367,7 @@ export default {
Polls: 'Umfragen', Polls: 'Umfragen',
'Voice Posts': 'Sprachbeiträge', 'Voice Posts': 'Sprachbeiträge',
'Photo Posts': 'Fotobeiträge', 'Photo Posts': 'Fotobeiträge',
'Video Posts': 'Videobeiträge',
'Select All': 'Alle auswählen', 'Select All': 'Alle auswählen',
'Clear All': 'Alle löschen', 'Clear All': 'Alle löschen',
'Remember my choice': 'Meine Auswahl merken', 'Remember my choice': 'Meine Auswahl merken',

View File

@@ -361,6 +361,7 @@ export default {
Polls: 'Polls', Polls: 'Polls',
'Voice Posts': 'Voice Posts', 'Voice Posts': 'Voice Posts',
'Photo Posts': 'Photo Posts', 'Photo Posts': 'Photo Posts',
'Video Posts': 'Video Posts',
'Select All': 'Select All', 'Select All': 'Select All',
'Clear All': 'Clear All', 'Clear All': 'Clear All',
'Remember my choice': 'Remember my choice', 'Remember my choice': 'Remember my choice',

View File

@@ -366,6 +366,7 @@ export default {
Polls: 'Encuestas', Polls: 'Encuestas',
'Voice Posts': 'Publicaciones de voz', 'Voice Posts': 'Publicaciones de voz',
'Photo Posts': 'Publicaciones de fotos', 'Photo Posts': 'Publicaciones de fotos',
'Video Posts': 'Publicaciones de video',
'Select All': 'Seleccionar todo', 'Select All': 'Seleccionar todo',
'Clear All': 'Limpiar todo', 'Clear All': 'Limpiar todo',
'Remember my choice': 'Recordar mi elección', 'Remember my choice': 'Recordar mi elección',

View File

@@ -361,6 +361,7 @@ export default {
Polls: 'نظرسنجی‌ها', Polls: 'نظرسنجی‌ها',
'Voice Posts': 'پست‌های صوتی', 'Voice Posts': 'پست‌های صوتی',
'Photo Posts': 'پست‌های عکس', 'Photo Posts': 'پست‌های عکس',
'Video Posts': 'پست‌های ویدیو',
'Select All': 'انتخاب همه', 'Select All': 'انتخاب همه',
'Clear All': 'پاک کردن همه', 'Clear All': 'پاک کردن همه',
'Remember my choice': 'انتخاب من را به خاطر بسپار', 'Remember my choice': 'انتخاب من را به خاطر بسپار',

View File

@@ -366,6 +366,7 @@ export default {
Polls: 'Sondages', Polls: 'Sondages',
'Voice Posts': 'Publications vocales', 'Voice Posts': 'Publications vocales',
'Photo Posts': 'Publications photo', 'Photo Posts': 'Publications photo',
'Video Posts': 'Publications vidéo',
'Select All': 'Tout sélectionner', 'Select All': 'Tout sélectionner',
'Clear All': 'Tout effacer', 'Clear All': 'Tout effacer',
'Remember my choice': 'Se souvenir de mon choix', 'Remember my choice': 'Se souvenir de mon choix',

View File

@@ -365,6 +365,7 @@ export default {
Polls: 'Sondaggi', Polls: 'Sondaggi',
'Voice Posts': 'Post vocali', 'Voice Posts': 'Post vocali',
'Photo Posts': 'Post foto', 'Photo Posts': 'Post foto',
'Video Posts': 'Post video',
'Select All': 'Seleziona tutto', 'Select All': 'Seleziona tutto',
'Clear All': 'Cancella tutto', 'Clear All': 'Cancella tutto',
'Remember my choice': 'Ricorda la mia scelta', 'Remember my choice': 'Ricorda la mia scelta',

View File

@@ -363,6 +363,7 @@ export default {
Polls: '投票', Polls: '投票',
'Voice Posts': '音声投稿', 'Voice Posts': '音声投稿',
'Photo Posts': '写真投稿', 'Photo Posts': '写真投稿',
'Video Posts': 'ビデオ投稿',
'Select All': 'すべて選択', 'Select All': 'すべて選択',
'Clear All': 'すべてクリア', 'Clear All': 'すべてクリア',
'Remember my choice': '選択を記憶', 'Remember my choice': '選択を記憶',

View File

@@ -362,6 +362,7 @@ export default {
Polls: '투표', Polls: '투표',
'Voice Posts': '음성 게시물', 'Voice Posts': '음성 게시물',
'Photo Posts': '사진 게시물', 'Photo Posts': '사진 게시물',
'Video Posts': '비디오 게시물',
'Select All': '모두 선택', 'Select All': '모두 선택',
'Clear All': '모두 지우기', 'Clear All': '모두 지우기',
'Remember my choice': '내 선택 기억하기', 'Remember my choice': '내 선택 기억하기',

View File

@@ -365,6 +365,7 @@ export default {
Polls: 'Ankiety', Polls: 'Ankiety',
'Voice Posts': 'Posty głosowe', 'Voice Posts': 'Posty głosowe',
'Photo Posts': 'Posty ze zdjęciami', 'Photo Posts': 'Posty ze zdjęciami',
'Video Posts': 'Posty wideo',
'Select All': 'Zaznacz wszystko', 'Select All': 'Zaznacz wszystko',
'Clear All': 'Wyczyść wszystko', 'Clear All': 'Wyczyść wszystko',
'Remember my choice': 'Zapamiętaj mój wybór', 'Remember my choice': 'Zapamiętaj mój wybór',

View File

@@ -364,6 +364,7 @@ export default {
Polls: 'Enquetes', Polls: 'Enquetes',
'Voice Posts': 'Áudios', 'Voice Posts': 'Áudios',
'Photo Posts': 'Fotos', 'Photo Posts': 'Fotos',
'Video Posts': 'Vídeos',
'Select All': 'Selecionar tudo', 'Select All': 'Selecionar tudo',
'Clear All': 'Limpar tudo', 'Clear All': 'Limpar tudo',
'Remember my choice': 'Lembrar minha escolha', 'Remember my choice': 'Lembrar minha escolha',

View File

@@ -363,8 +363,9 @@ export default {
Articles: 'Artigos', Articles: 'Artigos',
Highlights: 'Destaques', Highlights: 'Destaques',
Polls: 'Inquéritos', Polls: 'Inquéritos',
'Voice Posts': 'Publicações de voz', 'Voice Posts': 'Áudios',
'Photo Posts': 'Publicações de foto', 'Photo Posts': 'Fotos',
'Video Posts': 'Vídeos',
'Select All': 'Seleccionar tudo', 'Select All': 'Seleccionar tudo',
'Clear All': 'Limpar tudo', 'Clear All': 'Limpar tudo',
'Remember my choice': 'Lembrar a minha escolha', 'Remember my choice': 'Lembrar a minha escolha',

View File

@@ -365,6 +365,7 @@ export default {
Polls: 'Опросы', Polls: 'Опросы',
'Voice Posts': 'Голосовые посты', 'Voice Posts': 'Голосовые посты',
'Photo Posts': 'Фото посты', 'Photo Posts': 'Фото посты',
'Video Posts': 'Видео посты',
'Select All': 'Выбрать все', 'Select All': 'Выбрать все',
'Clear All': 'Очистить все', 'Clear All': 'Очистить все',
'Remember my choice': 'Запомнить мой выбор', 'Remember my choice': 'Запомнить мой выбор',

View File

@@ -359,6 +359,7 @@ export default {
Polls: 'โพล', Polls: 'โพล',
'Voice Posts': 'โพสต์เสียง', 'Voice Posts': 'โพสต์เสียง',
'Photo Posts': 'โพสต์รูปภาพ', 'Photo Posts': 'โพสต์รูปภาพ',
'Video Posts': 'โพสต์วิดีโอ',
'Select All': 'เลือกทั้งหมด', 'Select All': 'เลือกทั้งหมด',
'Clear All': 'ล้างทั้งหมด', 'Clear All': 'ล้างทั้งหมด',
'Remember my choice': 'จำการเลือกของฉัน', 'Remember my choice': 'จำการเลือกของฉัน',

View File

@@ -357,6 +357,7 @@ export default {
Polls: '投票', Polls: '投票',
'Voice Posts': '语音帖子', 'Voice Posts': '语音帖子',
'Photo Posts': '图片帖子', 'Photo Posts': '图片帖子',
'Video Posts': '视频帖子',
'Select All': '全选', 'Select All': '全选',
'Clear All': '清空', 'Clear All': '清空',
'Remember my choice': '记住我的选择', 'Remember my choice': '记住我的选择',

View File

@@ -1,10 +1,10 @@
import { EMBEDDED_MENTION_REGEX, ExtendedKind } from '@/constants' import { EMBEDDED_MENTION_REGEX, ExtendedKind } from '@/constants'
import client from '@/services/client.service' import client from '@/services/client.service'
import { TImageInfo } from '@/types' import { TImetaInfo } from '@/types'
import { LRUCache } from 'lru-cache' import { LRUCache } from 'lru-cache'
import { Event, kinds, nip19 } from 'nostr-tools' import { Event, kinds, nip19 } from 'nostr-tools'
import { import {
getImageInfoFromImetaTag, getImetaInfoFromImetaTag,
generateBech32IdFromATag, generateBech32IdFromATag,
generateBech32IdFromETag, generateBech32IdFromETag,
tagNameEquals tagNameEquals
@@ -171,15 +171,15 @@ export function getUsingClient(event: Event) {
return event.tags.find(tagNameEquals('client'))?.[1] return event.tags.find(tagNameEquals('client'))?.[1]
} }
export function getImageInfosFromEvent(event: Event) { export function getImetaInfosFromEvent(event: Event) {
const images: TImageInfo[] = [] const imeta: TImetaInfo[] = []
event.tags.forEach((tag) => { event.tags.forEach((tag) => {
const imageInfo = getImageInfoFromImetaTag(tag, event.pubkey) const imageInfo = getImetaInfoFromImetaTag(tag, event.pubkey)
if (imageInfo) { if (imageInfo) {
images.push(imageInfo) imeta.push(imageInfo)
} }
}) })
return images return imeta
} }
export function getEmbeddedNoteBech32Ids(event: Event) { export function getEmbeddedNoteBech32Ids(event: Event) {

View File

@@ -1,4 +1,4 @@
import { TEmoji, TImageInfo } from '@/types' import { TEmoji, TImetaInfo } from '@/types'
import { isBlurhashValid } from 'blurhash' import { isBlurhashValid } from 'blurhash'
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
import { isValidPubkey } from './pubkey' import { isValidPubkey } from './pubkey'
@@ -46,19 +46,19 @@ export function generateBech32IdFromATag(tag: string[]) {
} }
} }
export function getImageInfoFromImetaTag(tag: string[], pubkey?: string): TImageInfo | null { export function getImetaInfoFromImetaTag(tag: string[], pubkey?: string): TImetaInfo | null {
if (tag[0] !== 'imeta') return null if (tag[0] !== 'imeta') return null
const urlItem = tag.find((item) => item.startsWith('url ')) const urlItem = tag.find((item) => item.startsWith('url '))
const url = urlItem?.slice(4) const url = urlItem?.slice(4)
if (!url) return null if (!url) return null
const image: TImageInfo = { url, pubkey } const imeta: TImetaInfo = { url, pubkey }
const blurHashItem = tag.find((item) => item.startsWith('blurhash ')) const blurHashItem = tag.find((item) => item.startsWith('blurhash '))
const blurHash = blurHashItem?.slice(9) const blurHash = blurHashItem?.slice(9)
if (blurHash) { if (blurHash) {
const validRes = isBlurhashValid(blurHash) const validRes = isBlurhashValid(blurHash)
if (validRes.result) { if (validRes.result) {
image.blurHash = blurHash imeta.blurHash = blurHash
} }
} }
const dimItem = tag.find((item) => item.startsWith('dim ')) const dimItem = tag.find((item) => item.startsWith('dim '))
@@ -66,10 +66,10 @@ export function getImageInfoFromImetaTag(tag: string[], pubkey?: string): TImage
if (dim) { if (dim) {
const [width, height] = dim.split('x').map(Number) const [width, height] = dim.split('x').map(Number)
if (width && height) { if (width && height) {
image.dim = { width, height } imeta.dim = { width, height }
} }
} }
return image return imeta
} }
export function getPubkeysFromPTags(tags: string[][]) { export function getPubkeysFromPTags(tags: string[][]) {

View File

@@ -1,4 +1,4 @@
import { DEFAULT_NIP_96_SERVICE, DEFAULT_SHOW_KINDS, StorageKey } from '@/constants' import { DEFAULT_NIP_96_SERVICE, ExtendedKind, SUPPORTED_KINDS, StorageKey } from '@/constants'
import { isSameAccount } from '@/lib/account' import { isSameAccount } from '@/lib/account'
import { randomString } from '@/lib/random' import { randomString } from '@/lib/random'
import { import {
@@ -142,7 +142,19 @@ class LocalStorageService {
window.localStorage.getItem(StorageKey.DISMISSED_TOO_MANY_RELAYS_ALERT) === 'true' window.localStorage.getItem(StorageKey.DISMISSED_TOO_MANY_RELAYS_ALERT) === 'true'
const showKindsStr = window.localStorage.getItem(StorageKey.SHOW_KINDS) const showKindsStr = window.localStorage.getItem(StorageKey.SHOW_KINDS)
this.showKinds = showKindsStr ? JSON.parse(showKindsStr) : DEFAULT_SHOW_KINDS if (!showKindsStr) {
this.showKinds = SUPPORTED_KINDS
} else {
const showKindsVersionStr = window.localStorage.getItem(StorageKey.SHOW_KINDS_VERSION)
const showKindsVersion = showKindsVersionStr ? parseInt(showKindsVersionStr) : 0
const showKinds = JSON.parse(showKindsStr) as number[]
if (showKindsVersion < 1) {
showKinds.push(ExtendedKind.VIDEO, ExtendedKind.SHORT_VIDEO)
}
this.showKinds = showKinds
}
window.localStorage.setItem(StorageKey.SHOW_KINDS, JSON.stringify(this.showKinds))
window.localStorage.setItem(StorageKey.SHOW_KINDS_VERSION, '1')
// Clean up deprecated data // Clean up deprecated data
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP) window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)

View File

@@ -109,7 +109,7 @@ export type TFeedInfo = { feedType: TFeedType; id?: string }
export type TLanguage = 'en' | 'zh' | 'pl' export type TLanguage = 'en' | 'zh' | 'pl'
export type TImageInfo = { export type TImetaInfo = {
url: string url: string
blurHash?: string blurHash?: string
dim?: { width: number; height: number } dim?: { width: number; height: number }