feat: support for video events
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -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()}`, [])
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
16
src/components/Note/PictureNote.tsx
Normal file
16
src/components/Note/PictureNote.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
src/components/Note/VideoNote.tsx
Normal file
18
src/components/Note/VideoNote.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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': 'تذكر اختياري',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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': 'انتخاب من را به خاطر بسپار',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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': '選択を記憶',
|
||||||
|
|||||||
@@ -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': '내 선택 기억하기',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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': 'Запомнить мой выбор',
|
||||||
|
|||||||
@@ -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': 'จำการเลือกของฉัน',
|
||||||
|
|||||||
@@ -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': '记住我的选择',
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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[][]) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
2
src/types/index.d.ts
vendored
2
src/types/index.d.ts
vendored
@@ -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 }
|
||||||
|
|||||||
Reference in New Issue
Block a user