diff --git a/src/components/ClientSelect/index.tsx b/src/components/ClientSelect/index.tsx new file mode 100644 index 00000000..697dcd54 --- /dev/null +++ b/src/components/ClientSelect/index.tsx @@ -0,0 +1,242 @@ +import { Button, ButtonProps } from '@/components/ui/button' +import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog' +import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer' +import { Separator } from '@/components/ui/separator' +import { ExtendedKind } from '@/constants' +import { getReplaceableEventIdentifier, getSharableEventId } from '@/lib/event' +import { toChachiChat } from '@/lib/link' +import { useScreenSize } from '@/providers/ScreenSizeProvider' +import clientService from '@/services/client.service' +import { ExternalLink } from 'lucide-react' +import { Event, kinds, nip19 } from 'nostr-tools' +import { Dispatch, SetStateAction, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' + +const clients: Record string }> = { + nosta: { + name: 'Nosta', + getUrl: (id: string) => `https://nosta.me/${id}` + }, + snort: { + name: 'Snort', + getUrl: (id: string) => `https://snort.social/${id}` + }, + olas: { + name: 'Olas', + getUrl: (id: string) => `https://olas.app/e/${id}` + }, + primal: { + name: 'Primal', + getUrl: (id: string) => `https://primal.net/e/${id}` + }, + nostrudel: { + name: 'Nostrudel', + getUrl: (id: string) => `https://nostrudel.ninja/l/${id}` + }, + nostter: { + name: 'Nostter', + getUrl: (id: string) => `https://nostter.app/${id}` + }, + coracle: { + name: 'Coracle', + getUrl: (id: string) => `https://coracle.social/${id}` + }, + iris: { + name: 'Iris', + getUrl: (id: string) => `https://iris.to/${id}` + }, + lumilumi: { + name: 'Lumilumi', + getUrl: (id: string) => `https://lumilumi.app/${id}` + }, + zapStream: { + name: 'zap.stream', + getUrl: (id: string) => `https://zap.stream/${id}` + }, + yakihonne: { + name: 'YakiHonne', + getUrl: (id: string) => `https://yakihonne.com/${id}` + }, + habla: { + name: 'Habla', + getUrl: (id: string) => `https://habla.news/a/${id}` + }, + pareto: { + name: 'Pareto', + getUrl: (id: string) => `https://pareto.space/a/${id}` + }, + njump: { + name: 'Njump', + getUrl: (id: string) => `https://njump.me/${id}` + } +} + +export default function ClientSelect({ + event, + originalNoteId, + ...props +}: ButtonProps & { + event?: Event + originalNoteId?: string +}) { + const { isSmallScreen } = useScreenSize() + const [open, setOpen] = useState(false) + const { t } = useTranslation() + + const supportedClients = useMemo(() => { + let kind: number | undefined + if (event) { + kind = event.kind + } else if (originalNoteId) { + try { + const pointer = nip19.decode(originalNoteId) + if (pointer.type === 'naddr') { + kind = pointer.data.kind + } + } catch (error) { + console.error('Failed to decode NIP-19 pointer:', error) + return ['njump'] + } + } + if (!kind) { + return ['njump'] + } + + switch (kind) { + case kinds.LongFormArticle: + case kinds.DraftLong: + return ['yakihonne', 'coracle', 'habla', 'lumilumi', 'pareto', 'njump'] + case kinds.LiveEvent: + return ['zapStream', 'nostrudel', 'njump'] + case kinds.Date: + case kinds.Time: + return ['coracle', 'njump'] + case kinds.CommunityDefinition: + return ['coracle', 'snort', 'njump'] + default: + return ['njump'] + } + }, [event]) + + if (!originalNoteId && !event) { + return null + } + + const content = ( +
+ {event?.kind === ExtendedKind.GROUP_METADATA ? ( + + ) : ( + supportedClients.map((clientId) => { + const client = clients[clientId] + if (!client) return null + + return ( + setOpen(false)} + href={client.getUrl(originalNoteId ?? getSharableEventId(event!))} + name={client.name} + /> + ) + }) + )} + + +
+ ) + + if (isSmallScreen) { + return ( +
e.stopPropagation()}> + + + + + {content} + +
+ ) + } + + return ( +
e.stopPropagation()}> + + + + + e.preventDefault()}> + {content} + + +
+ ) +} + +function RelayBasedGroupChatSelector({ + event, + originalNoteId, + setOpen +}: { + event: Event + setOpen: Dispatch> + originalNoteId?: string +}) { + const { relay, id } = useMemo(() => { + let relay: string | undefined + if (originalNoteId) { + const pointer = nip19.decode(originalNoteId) + if (pointer.type === 'naddr' && pointer.data.relays?.length) { + relay = pointer.data.relays[0] + } + } + if (!relay) { + relay = clientService.getEventHint(event.id) + } + + return { relay, id: getReplaceableEventIdentifier(event) } + }, [event, originalNoteId]) + + return ( + setOpen(false)} + href={toChachiChat(relay, id)} + name="Chachi Chat" + /> + ) +} + +function ClientSelectItem({ + onClick, + href, + name +}: { + onClick: () => void + href: string + name: string +}) { + return ( + + ) +} diff --git a/src/components/ContentPreview/CommunityDefinitionPreview.tsx b/src/components/ContentPreview/CommunityDefinitionPreview.tsx new file mode 100644 index 00000000..ca3ab8ff --- /dev/null +++ b/src/components/ContentPreview/CommunityDefinitionPreview.tsx @@ -0,0 +1,24 @@ +import { getCommunityDefinition } from '@/lib/event' +import { cn } from '@/lib/utils' +import { Event } from 'nostr-tools' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +export default function CommunityDefinitionPreview({ + event, + className, + onClick +}: { + event: Event + className?: string + onClick?: React.MouseEventHandler | undefined +}) { + const { t } = useTranslation() + const metadata = useMemo(() => getCommunityDefinition(event), [event]) + + return ( +
+ [{t('Community')}] {metadata.name} +
+ ) +} diff --git a/src/components/ContentPreview/GroupMetadataPreview.tsx b/src/components/ContentPreview/GroupMetadataPreview.tsx new file mode 100644 index 00000000..e953978f --- /dev/null +++ b/src/components/ContentPreview/GroupMetadataPreview.tsx @@ -0,0 +1,24 @@ +import { getGroupMetadata } from '@/lib/event' +import { cn } from '@/lib/utils' +import { Event } from 'nostr-tools' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +export default function GroupMetadataPreview({ + event, + className, + onClick +}: { + event: Event + className?: string + onClick?: React.MouseEventHandler | undefined +}) { + const { t } = useTranslation() + const metadata = useMemo(() => getGroupMetadata(event), [event]) + + return ( +
+ [{t('Group')}] {metadata.name} +
+ ) +} diff --git a/src/components/ContentPreview/LiveEventPreview.tsx b/src/components/ContentPreview/LiveEventPreview.tsx new file mode 100644 index 00000000..648c0c4c --- /dev/null +++ b/src/components/ContentPreview/LiveEventPreview.tsx @@ -0,0 +1,24 @@ +import { getLiveEventMetadata } from '@/lib/event' +import { cn } from '@/lib/utils' +import { Event } from 'nostr-tools' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +export default function LiveEventPreview({ + event, + className, + onClick +}: { + event: Event + className?: string + onClick?: React.MouseEventHandler | undefined +}) { + const { t } = useTranslation() + const metadata = useMemo(() => getLiveEventMetadata(event), [event]) + + return ( +
+ [{t('Live event')}] {metadata.title} +
+ ) +} diff --git a/src/components/ContentPreview/LongFormArticlePreview.tsx b/src/components/ContentPreview/LongFormArticlePreview.tsx new file mode 100644 index 00000000..18b3dbe9 --- /dev/null +++ b/src/components/ContentPreview/LongFormArticlePreview.tsx @@ -0,0 +1,24 @@ +import { getLongFormArticleMetadata } from '@/lib/event' +import { cn } from '@/lib/utils' +import { Event } from 'nostr-tools' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +export default function LongFormArticlePreview({ + event, + className, + onClick +}: { + event: Event + className?: string + onClick?: React.MouseEventHandler | undefined +}) { + const { t } = useTranslation() + const metadata = useMemo(() => getLongFormArticleMetadata(event), [event]) + + return ( +
+ [{t('Article')}] {metadata.title} +
+ ) +} diff --git a/src/components/ContentPreview/NormalContentPreview.tsx b/src/components/ContentPreview/NormalContentPreview.tsx new file mode 100644 index 00000000..02c72cf1 --- /dev/null +++ b/src/components/ContentPreview/NormalContentPreview.tsx @@ -0,0 +1,68 @@ +import { useTranslatedEvent } from '@/hooks' +import { + EmbeddedEmojiParser, + EmbeddedEventParser, + EmbeddedImageParser, + EmbeddedMentionParser, + EmbeddedVideoParser, + parseContent +} from '@/lib/content-parser' +import { extractEmojiInfosFromTags } from '@/lib/event' +import { cn } from '@/lib/utils' +import { Event } from 'nostr-tools' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { EmbeddedMentionText } from '../Embedded' +import Emoji from '../Emoji' + +export default function NormalContentPreview({ + event, + className, + onClick +}: { + event: Event + className?: string + onClick?: React.MouseEventHandler | undefined +}) { + const { t } = useTranslation() + const translatedEvent = useTranslatedEvent(event?.id) + const nodes = useMemo(() => { + return parseContent(event.content, [ + EmbeddedImageParser, + EmbeddedVideoParser, + EmbeddedEventParser, + EmbeddedMentionParser, + EmbeddedEmojiParser + ]) + }, [event, translatedEvent]) + + const emojiInfos = extractEmojiInfosFromTags(event?.tags) + + return ( +
+ {nodes.map((node, index) => { + if (node.type === 'text') { + return node.data + } + if (node.type === 'image' || node.type === 'images') { + return index > 0 ? ` [${t('image')}]` : `[${t('image')}]` + } + if (node.type === 'video') { + return index > 0 ? ` [${t('video')}]` : `[${t('video')}]` + } + if (node.type === 'event') { + return index > 0 ? ` [${t('note')}]` : `[${t('note')}]` + } + if (node.type === 'mention') { + return + } + if (node.type === 'emoji') { + const shortcode = node.data.split(':')[1] + const emoji = emojiInfos.find((e) => e.shortcode === shortcode) + if (!emoji) return node.data + return + } + })} +
+ ) +} diff --git a/src/components/ContentPreview/index.tsx b/src/components/ContentPreview/index.tsx index f8923e5e..03ececb9 100644 --- a/src/components/ContentPreview/index.tsx +++ b/src/components/ContentPreview/index.tsx @@ -1,19 +1,14 @@ -import { useTranslatedEvent } from '@/hooks' -import { - EmbeddedEmojiParser, - EmbeddedEventParser, - EmbeddedImageParser, - EmbeddedMentionParser, - EmbeddedVideoParser, - parseContent -} from '@/lib/content-parser' -import { extractEmojiInfosFromTags } from '@/lib/event' +import { ExtendedKind } from '@/constants' import { cn } from '@/lib/utils' +import { useMuteList } from '@/providers/MuteListProvider' import { Event, kinds } from 'nostr-tools' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { EmbeddedMentionText } from '../Embedded' -import Emoji from '../Emoji' +import CommunityDefinitionPreview from './CommunityDefinitionPreview' +import GroupMetadataPreview from './GroupMetadataPreview' +import LiveEventPreview from './LiveEventPreview' +import LongFormArticlePreview from './LongFormArticlePreview' +import NormalContentPreview from './NormalContentPreview' export default function ContentPreview({ event, @@ -25,56 +20,49 @@ export default function ContentPreview({ onClick?: React.MouseEventHandler | undefined }) { const { t } = useTranslation() - const translatedEvent = useTranslatedEvent(event?.id) - const nodes = useMemo(() => { - if (!event) return [{ type: 'text', data: `[${t('Note not found')}]` }] + const { mutePubkeys } = useMuteList() + const isMuted = useMemo( + () => (event ? mutePubkeys.includes(event.pubkey) : false), + [mutePubkeys, event] + ) - if (event.kind === kinds.Highlights) return [] + if (!event) { + return
{`[${t('Note not found')}]`}
+ } - return parseContent(translatedEvent?.content ?? event.content, [ - EmbeddedImageParser, - EmbeddedVideoParser, - EmbeddedEventParser, - EmbeddedMentionParser, - EmbeddedEmojiParser - ]) - }, [event, translatedEvent]) - - if (event?.kind === kinds.Highlights) { + if (isMuted) { return ( -
- {event.content} +
[{t('This user has been muted')}]
+ ) + } + + if ([kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.PICTURE].includes(event.kind)) { + return + } + + if (event.kind === kinds.Highlights) { + return ( +
+ [{t('Highlight')}] {event.content}
) } - const emojiInfos = extractEmojiInfosFromTags(event?.tags) + if (event.kind === kinds.LongFormArticle) { + return + } - return ( -
- {nodes.map((node, index) => { - if (node.type === 'text') { - return node.data - } - if (node.type === 'image' || node.type === 'images') { - return index > 0 ? ` [${t('image')}]` : `[${t('image')}]` - } - if (node.type === 'video') { - return index > 0 ? ` [${t('video')}]` : `[${t('video')}]` - } - if (node.type === 'event') { - return index > 0 ? ` [${t('note')}]` : `[${t('note')}]` - } - if (node.type === 'mention') { - return - } - if (node.type === 'emoji') { - const shortcode = node.data.split(':')[1] - const emoji = emojiInfos.find((e) => e.shortcode === shortcode) - if (!emoji) return node.data - return - } - })} -
- ) + if (event.kind === ExtendedKind.GROUP_METADATA) { + return + } + + if (event.kind === kinds.CommunityDefinition) { + return + } + + if (event.kind === kinds.LiveEvent) { + return + } + + return
[{t('Cannot handle event of kind k', { k: event.kind })}]
} diff --git a/src/components/Embedded/EmbeddedNote.tsx b/src/components/Embedded/EmbeddedNote.tsx index f6eb24fe..6d532d54 100644 --- a/src/components/Embedded/EmbeddedNote.tsx +++ b/src/components/Embedded/EmbeddedNote.tsx @@ -1,11 +1,9 @@ -import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { useFetchEvent } from '@/hooks' import { cn } from '@/lib/utils' -import { Check, Copy } from 'lucide-react' -import { useState } from 'react' import { useTranslation } from 'react-i18next' -import GenericNoteCard from '../NoteCard/GenericNoteCard' +import ClientSelect from '../ClientSelect' +import MainNoteCard from '../NoteCard/MainNoteCard' export function EmbeddedNote({ noteId, className }: { noteId: string; className?: string }) { const { event, isFetching } = useFetchEvent(noteId) @@ -19,7 +17,7 @@ export function EmbeddedNote({ noteId, className }: { noteId: string; className? } return ( -
{t('Sorry! The note cannot be found 😔')}
- +
) diff --git a/src/components/Note/CommunityDefinition.tsx b/src/components/Note/CommunityDefinition.tsx new file mode 100644 index 00000000..54bc17ca --- /dev/null +++ b/src/components/Note/CommunityDefinition.tsx @@ -0,0 +1,42 @@ +import { getCommunityDefinition } from '@/lib/event' +import { Event } from 'nostr-tools' +import { useMemo } from 'react' +import ClientSelect from '../ClientSelect' +import Image from '../Image' + +export default function CommunityDefinition({ + event, + className +}: { + event: Event + className?: string +}) { + const metadata = useMemo(() => getCommunityDefinition(event), [event]) + + const communityNameComponent = ( +
{metadata.name}
+ ) + + const communityDescriptionComponent = metadata.description && ( +
{metadata.description}
+ ) + + return ( +
+
+ {metadata.image && ( + + )} +
+ {communityNameComponent} + {communityDescriptionComponent} +
+
+ +
+ ) +} diff --git a/src/components/Note/GroupMetadata.tsx b/src/components/Note/GroupMetadata.tsx new file mode 100644 index 00000000..d0631281 --- /dev/null +++ b/src/components/Note/GroupMetadata.tsx @@ -0,0 +1,49 @@ +import { getGroupMetadata } from '@/lib/event' +import { Event } from 'nostr-tools' +import { useMemo } from 'react' +import ClientSelect from '../ClientSelect' +import Image from '../Image' + +export default function GroupMetadata({ + event, + originalNoteId, + className +}: { + event: Event + originalNoteId?: string + className?: string +}) { + const metadata = useMemo(() => getGroupMetadata(event), [event]) + + const groupNameComponent = ( +
{metadata.name}
+ ) + + const groupAboutComponent = metadata.about && ( +
{metadata.about}
+ ) + + return ( +
+
+ {metadata.picture && ( + + )} +
+ {groupNameComponent} + {groupAboutComponent} +
+
+ +
+ ) +} diff --git a/src/components/Note/Highlight.tsx b/src/components/Note/Highlight.tsx index 49ff16bd..42427d24 100644 --- a/src/components/Note/Highlight.tsx +++ b/src/components/Note/Highlight.tsx @@ -1,5 +1,5 @@ import { useFetchEvent, useTranslatedEvent } from '@/hooks' -import { createFakeEvent, isSupportedKind } from '@/lib/event' +import { createFakeEvent } from '@/lib/event' import { toNjump, toNote } from '@/lib/link' import { isValidPubkey } from '@/lib/pubkey' import { generateEventIdFromATag } from '@/lib/tag' @@ -110,7 +110,7 @@ function HighlightSource({ event }: { event: Event }) {
{t('From')}
{pubkey && } - {referenceEvent && isSupportedKind(referenceEvent.kind) ? ( + {referenceEvent ? ( getLiveEventMetadata(event), [event]) + + const liveStatusComponent = + metadata.status && + (metadata.status === 'live' ? ( + live + ) : metadata.status === 'ended' ? ( + ended + ) : ( + {metadata.status} + )) + + const titleComponent =
{metadata.title}
+ + const summaryComponent = metadata.summary && ( +
{metadata.summary}
+ ) + + const tagsComponent = metadata.tags.length > 0 && ( +
+ {metadata.tags.map((tag) => ( + + {tag} + + ))} +
+ ) + + if (isSmallScreen) { + return ( +
+ {metadata.image && ( + + )} +
+ {titleComponent} + {liveStatusComponent} + {summaryComponent} + {tagsComponent} + +
+
+ ) + } + + return ( +
+
+ {metadata.image && ( + + )} +
+ {titleComponent} + {liveStatusComponent} + {summaryComponent} + {tagsComponent} +
+
+ +
+ ) +} diff --git a/src/components/Note/LongFormArticle.tsx b/src/components/Note/LongFormArticle.tsx new file mode 100644 index 00000000..de8eacf3 --- /dev/null +++ b/src/components/Note/LongFormArticle.tsx @@ -0,0 +1,74 @@ +import { Badge } from '@/components/ui/badge' +import { getLongFormArticleMetadata } from '@/lib/event' +import { useScreenSize } from '@/providers/ScreenSizeProvider' +import { Event } from 'nostr-tools' +import { useMemo } from 'react' +import ClientSelect from '../ClientSelect' +import Image from '../Image' + +export default function LongFormArticle({ + event, + className +}: { + event: Event + className?: string +}) { + const { isSmallScreen } = useScreenSize() + const metadata = useMemo(() => getLongFormArticleMetadata(event), [event]) + + const titleComponent =
{metadata.title}
+ + const tagsComponent = metadata.tags.length > 0 && ( +
+ {metadata.tags.map((tag) => ( + + {tag} + + ))} +
+ ) + + const summaryComponent = metadata.summary && ( +
{metadata.summary}
+ ) + + if (isSmallScreen) { + return ( +
+ {metadata.image && ( + + )} +
+ {titleComponent} + {summaryComponent} + {tagsComponent} + +
+
+ ) + } + + return ( +
+
+ {metadata.image && ( + + )} +
+ {titleComponent} + {summaryComponent} + {tagsComponent} +
+
+ +
+ ) +} diff --git a/src/components/Note/MutedNote.tsx b/src/components/Note/MutedNote.tsx new file mode 100644 index 00000000..bf8c131a --- /dev/null +++ b/src/components/Note/MutedNote.tsx @@ -0,0 +1,23 @@ +import { Button } from '@/components/ui/button' +import { Eye } from 'lucide-react' +import { useTranslation } from 'react-i18next' + +export default function MutedNote({ show }: { show: () => void }) { + const { t } = useTranslation() + + return ( +
+
{t('This user has been muted')}
+ +
+ ) +} diff --git a/src/components/Note/UnknownNote.tsx b/src/components/Note/UnknownNote.tsx index dc516fac..46297466 100644 --- a/src/components/Note/UnknownNote.tsx +++ b/src/components/Note/UnknownNote.tsx @@ -1,10 +1,7 @@ -import { Button } from '@/components/ui/button' -import { getSharableEventId } from '@/lib/event' -import { toNjump } from '@/lib/link' import { cn } from '@/lib/utils' -import { ExternalLink } from 'lucide-react' import { Event } from 'nostr-tools' import { useTranslation } from 'react-i18next' +import ClientSelect from '../ClientSelect' export function UnknownNote({ event, className }: { event: Event; className?: string }) { const { t } = useTranslation() @@ -17,16 +14,7 @@ export function UnknownNote({ event, className }: { event: Event; className?: st )} >
{t('Cannot handle event of kind k', { k: event.kind })}
- +
) } diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index 8c3c950b..33dca6d5 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -1,13 +1,14 @@ import { useSecondaryPage } from '@/PageManager' +import { ExtendedKind } from '@/constants' import { extractImageInfosFromEventTags, getParentEventId, getUsingClient, isNsfwEvent, - isPictureEvent, - isSupportedKind + isPictureEvent } from '@/lib/event' import { toNote } from '@/lib/link' +import { useMuteList } from '@/providers/MuteListProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { Event, kinds } from 'nostr-tools' import { useMemo, useState } from 'react' @@ -20,18 +21,25 @@ import ParentNotePreview from '../ParentNotePreview' import TranslateButton from '../TranslateButton' import UserAvatar from '../UserAvatar' import Username from '../Username' +import CommunityDefinition from './CommunityDefinition' +import GroupMetadata from './GroupMetadata' import Highlight from './Highlight' import IValue from './IValue' +import LiveEvent from './LiveEvent' +import LongFormArticle from './LongFormArticle' +import MutedNote from './MutedNote' import NsfwNote from './NsfwNote' import { UnknownNote } from './UnknownNote' export default function Note({ event, + originalNoteId, size = 'normal', className, hideParentNotePreview = false }: { event: Event + originalNoteId?: string size?: 'normal' | 'small' className?: string hideParentNotePreview?: boolean @@ -48,14 +56,37 @@ export default function Note({ ) const usingClient = useMemo(() => getUsingClient(event), [event]) const [showNsfw, setShowNsfw] = useState(false) + const { mutePubkeys } = useMuteList() + const [showMuted, setShowMuted] = useState(false) let content: React.ReactNode - if (!isSupportedKind(event.kind)) { + if ( + ![ + kinds.ShortTextNote, + kinds.Highlights, + kinds.LongFormArticle, + kinds.LiveEvent, + kinds.CommunityDefinition, + ExtendedKind.GROUP_METADATA, + ExtendedKind.PICTURE, + ExtendedKind.COMMENT + ].includes(event.kind) + ) { content = + } else if (mutePubkeys.includes(event.pubkey) && !showMuted) { + content = setShowMuted(true)} /> } else if (isNsfwEvent(event) && !showNsfw) { content = setShowNsfw(true)} /> } else if (event.kind === kinds.Highlights) { content = + } else if (event.kind === kinds.LongFormArticle) { + content = + } else if (event.kind === kinds.LiveEvent) { + content = + } else if (event.kind === ExtendedKind.GROUP_METADATA) { + content = + } else if (event.kind === kinds.CommunityDefinition) { + content = } else { content = } diff --git a/src/components/NoteCard/GenericNoteCard.tsx b/src/components/NoteCard/GenericNoteCard.tsx deleted file mode 100644 index 14f1631a..00000000 --- a/src/components/NoteCard/GenericNoteCard.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { ExtendedKind } from '@/constants' -import { isSupportedKind } from '@/lib/event' -import { useMuteList } from '@/providers/MuteListProvider' -import { Event, kinds } from 'nostr-tools' -import { useState } from 'react' -import GroupMetadataCard from './GroupMetadataCard' -import LiveEventCard from './LiveEventCard' -import LongFormArticleCard from './LongFormArticleCard' -import MainNoteCard from './MainNoteCard' -import MutedNoteCard from './MutedNoteCard' -import UnknownNoteCard from './UnknownNoteCard' - -export default function GenericNoteCard({ - event, - className, - reposter, - embedded, - originalNoteId -}: { - event: Event - className?: string - reposter?: string - embedded?: boolean - originalNoteId?: string -}) { - const [showMuted, setShowMuted] = useState(false) - const { mutePubkeys } = useMuteList() - - if (mutePubkeys.includes(event.pubkey) && !showMuted) { - return ( - setShowMuted(true)} - /> - ) - } - - if (isSupportedKind(event.kind)) { - return ( - - ) - } - if (event.kind === kinds.LongFormArticle) { - return ( - - ) - } - if (event.kind === kinds.LiveEvent) { - return ( - - ) - } - if (event.kind === ExtendedKind.GROUP_METADATA) { - return ( - - ) - } - return ( - - ) -} diff --git a/src/components/NoteCard/GroupMetadataCard.tsx b/src/components/NoteCard/GroupMetadataCard.tsx deleted file mode 100644 index baaafb0f..00000000 --- a/src/components/NoteCard/GroupMetadataCard.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { Separator } from '@/components/ui/separator' -import { getSharableEventId } from '@/lib/event' -import { toChachiChat } from '@/lib/link' -import { simplifyUrl } from '@/lib/url' -import { cn } from '@/lib/utils' -import { useScreenSize } from '@/providers/ScreenSizeProvider' -import client from '@/services/client.service' -import { Check, Copy, ExternalLink } from 'lucide-react' -import { Event, nip19 } from 'nostr-tools' -import { useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { FormattedTimestamp } from '../FormattedTimestamp' -import Image from '../Image' -import UserAvatar from '../UserAvatar' -import Username from '../Username' -import RepostDescription from './RepostDescription' - -export default function GroupMetadataCard({ - event, - className, - originalNoteId, - embedded = false, - reposter -}: { - event: Event - className?: string - originalNoteId?: string - embedded?: boolean - reposter?: string -}) { - const { t } = useTranslation() - const { isSmallScreen } = useScreenSize() - const [isCopied, setIsCopied] = useState(false) - const metadata = useMemo(() => { - let d: string | undefined - let name: string | undefined - let about: string | undefined - let picture: string | undefined - let relay: string | undefined - const tags = new Set() - - if (originalNoteId) { - const pointer = nip19.decode(originalNoteId) - if (pointer.type === 'naddr' && pointer.data.relays?.length) { - relay = pointer.data.relays[0] - } - } - if (!relay) { - relay = client.getEventHint(event.id) - } - - event.tags.forEach(([tagName, tagValue]) => { - if (tagName === 'name') { - name = tagValue - } else if (tagName === 'about') { - about = tagValue - } else if (tagName === 'picture') { - picture = tagValue - } else if (tagName === 't' && tagValue) { - tags.add(tagValue.toLocaleLowerCase()) - } else if (tagName === 'd') { - d = tagValue - } - }) - - if (!name) { - name = d ?? 'no name' - } - - return { d, name, about, picture, tags: Array.from(tags), relay } - }, [event, originalNoteId]) - - return ( -
-
- -
- -
- -
- -
-
-
-
- {metadata.picture && ( - - )} -
-
{metadata.name}
- {metadata.about && ( -
{metadata.about}
- )} - {metadata.tags.length > 0 && ( -
- {metadata.tags.map((tag) => ( - - {tag} - - ))} -
- )} - {(!metadata.relay || !metadata.d) && ( - - )} -
-
-
- {!embedded && } - {!isSmallScreen && metadata.relay && metadata.d && ( -
{ - e.stopPropagation() - window.open(toChachiChat(simplifyUrl(metadata.relay), metadata.d!), '_blank') - }} - > -
- {t('Open in a', { a: 'Chachi' })} -
-
- )} -
- ) -} diff --git a/src/components/NoteCard/LiveEventCard.tsx b/src/components/NoteCard/LiveEventCard.tsx deleted file mode 100644 index 91e2cf51..00000000 --- a/src/components/NoteCard/LiveEventCard.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { Badge } from '@/components/ui/badge' -import { Separator } from '@/components/ui/separator' -import { toZapStreamLiveEvent } from '@/lib/link' -import { tagNameEquals } from '@/lib/tag' -import { cn } from '@/lib/utils' -import { useScreenSize } from '@/providers/ScreenSizeProvider' -import { ExternalLink } from 'lucide-react' -import { Event } from 'nostr-tools' -import { useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { FormattedTimestamp } from '../FormattedTimestamp' -import Image from '../Image' -import UserAvatar from '../UserAvatar' -import Username from '../Username' -import RepostDescription from './RepostDescription' - -export default function LiveEventCard({ - event, - className, - embedded = false, - reposter -}: { - event: Event - className?: string - embedded?: boolean - reposter?: string -}) { - const { t } = useTranslation() - const { isSmallScreen } = useScreenSize() - const metadata = useMemo(() => { - let title: string | undefined - let summary: string | undefined - let image: string | undefined - let status: string | undefined - const tags = new Set() - - event.tags.forEach(([tagName, tagValue]) => { - if (tagName === 'title') { - title = tagValue - } else if (tagName === 'summary') { - summary = tagValue - } else if (tagName === 'image') { - image = tagValue - } else if (tagName === 'status') { - status = tagValue - } else if (tagName === 't' && tagValue && tags.size < 6) { - tags.add(tagValue.toLocaleLowerCase()) - } - }) - - if (!title) { - title = event.tags.find(tagNameEquals('d'))?.[1] ?? 'no title' - } - - return { title, summary, image, status, tags: Array.from(tags) } - }, [event]) - - const liveStatusComponent = - metadata.status && - (metadata.status === 'live' ? ( - live - ) : metadata.status === 'ended' ? ( - ended - ) : ( - {metadata.status} - )) - - const userInfoComponent = ( -
- -
-
- - {liveStatusComponent} -
-
- -
-
-
- ) - - const titleComponent =
{metadata.title}
- - const summaryComponent = metadata.summary && ( -
{metadata.summary}
- ) - - const tagsComponent = metadata.tags.length > 0 && ( -
- {metadata.tags.map((tag) => ( - - {tag} - - ))} -
- ) - - const handleClick = (e: React.MouseEvent) => { - e.stopPropagation() - window.open(toZapStreamLiveEvent(event), '_blank') - } - - if (isSmallScreen) { - return ( -
-
- - {userInfoComponent} - {metadata.image && ( - - )} -
- {titleComponent} - {summaryComponent} - {tagsComponent} -
-
- {!embedded && } -
- ) - } - - return ( -
-
-
- - {userInfoComponent} -
- {titleComponent} - {summaryComponent} - {tagsComponent} -
-
- {metadata.image && ( - - )} -
- {!embedded && } -
-
- {t('Open in a', { a: 'Zap Stream' })} -
-
-
- ) -} diff --git a/src/components/NoteCard/LongFormArticleCard.tsx b/src/components/NoteCard/LongFormArticleCard.tsx deleted file mode 100644 index 33fb4032..00000000 --- a/src/components/NoteCard/LongFormArticleCard.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { Badge } from '@/components/ui/badge' -import { Separator } from '@/components/ui/separator' -import { toHablaLongFormArticle } from '@/lib/link' -import { tagNameEquals } from '@/lib/tag' -import { cn } from '@/lib/utils' -import { useScreenSize } from '@/providers/ScreenSizeProvider' -import { ExternalLink } from 'lucide-react' -import { Event } from 'nostr-tools' -import { useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import Image from '../Image' -import UserAvatar from '../UserAvatar' -import Username from '../Username' -import RepostDescription from './RepostDescription' - -export default function LongFormArticleCard({ - event, - className, - embedded = false, - reposter -}: { - event: Event - className?: string - embedded?: boolean - reposter?: string -}) { - const { t } = useTranslation() - const { isSmallScreen } = useScreenSize() - const metadata = useMemo(() => { - let title: string | undefined - let summary: string | undefined - let image: string | undefined - let publishDateString: string | undefined - const tags = new Set() - - event.tags.forEach(([tagName, tagValue]) => { - if (tagName === 'title') { - title = tagValue - } else if (tagName === 'summary') { - summary = tagValue - } else if (tagName === 'image') { - image = tagValue - } else if (tagName === 'published_at') { - try { - const publishedAt = parseInt(tagValue) - publishDateString = !isNaN(publishedAt) - ? new Date(publishedAt * 1000).toLocaleString() - : undefined - } catch { - // ignore - } - } else if (tagName === 't' && tagValue && tags.size < 6) { - tags.add(tagValue.toLocaleLowerCase()) - } - }) - - if (!title) { - title = event.tags.find(tagNameEquals('d'))?.[1] ?? 'no title' - } - - return { title, summary, image, publishDateString, tags: Array.from(tags) } - }, [event]) - - const userInfoComponent = ( -
- -
- - {metadata.publishDateString && ( -
{metadata.publishDateString}
- )} -
-
- ) - - const titleComponent =
{metadata.title}
- - const tagsComponent = metadata.tags.length > 0 && ( -
- {metadata.tags.map((tag) => ( - - {tag} - - ))} -
- ) - - const summaryComponent = metadata.summary && ( -
{metadata.summary}
- ) - - const handleClick = (e: React.MouseEvent) => { - e.stopPropagation() - window.open(toHablaLongFormArticle(event), '_blank') - } - - if (isSmallScreen) { - return ( -
-
- - {userInfoComponent} - {metadata.image && ( - - )} -
- {titleComponent} - {tagsComponent} - {summaryComponent} -
-
- {!embedded && } -
- ) - } - - return ( -
-
-
- - {userInfoComponent} -
- {titleComponent} - {tagsComponent} - {summaryComponent} -
-
- {metadata.image && ( - - )} -
- {!embedded && } -
-
- {t('Open in a', { a: 'Habla' })} -
-
-
- ) -} diff --git a/src/components/NoteCard/MainNoteCard.tsx b/src/components/NoteCard/MainNoteCard.tsx index 6730393d..3b0252c1 100644 --- a/src/components/NoteCard/MainNoteCard.tsx +++ b/src/components/NoteCard/MainNoteCard.tsx @@ -11,12 +11,14 @@ export default function MainNoteCard({ event, className, reposter, - embedded + embedded, + originalNoteId }: { event: Event className?: string reposter?: string embedded?: boolean + originalNoteId?: string }) { const { push } = useSecondaryPage() @@ -25,7 +27,7 @@ export default function MainNoteCard({ className={className} onClick={(e) => { e.stopPropagation() - push(toNote(event)) + push(toNote(originalNoteId ?? event)) }} >
@@ -35,6 +37,7 @@ export default function MainNoteCard({ className={embedded ? '' : 'px-4'} size={embedded ? 'small' : 'normal'} event={event} + originalNoteId={originalNoteId} /> {!embedded && } diff --git a/src/components/NoteCard/MutedNoteCard.tsx b/src/components/NoteCard/MutedNoteCard.tsx deleted file mode 100644 index 633648c2..00000000 --- a/src/components/NoteCard/MutedNoteCard.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Button } from '@/components/ui/button' -import { Separator } from '@/components/ui/separator' -import { cn } from '@/lib/utils' -import { Eye } from 'lucide-react' -import { Event } from 'nostr-tools' -import { useTranslation } from 'react-i18next' -import { FormattedTimestamp } from '../FormattedTimestamp' -import UserAvatar from '../UserAvatar' -import Username from '../Username' -import RepostDescription from './RepostDescription' - -export default function MutedNoteCard({ - event, - show, - reposter, - embedded, - className -}: { - event: Event - show: () => void - reposter?: string - embedded?: boolean - className?: string -}) { - const { t } = useTranslation() - - return ( -
-
- -
- -
- -
- -
-
-
-
-
{t('This user has been muted')}
- -
-
- {!embedded && } -
- ) -} diff --git a/src/components/NoteCard/RepostNoteCard.tsx b/src/components/NoteCard/RepostNoteCard.tsx index 36f43518..ccf5582d 100644 --- a/src/components/NoteCard/RepostNoteCard.tsx +++ b/src/components/NoteCard/RepostNoteCard.tsx @@ -3,7 +3,7 @@ import { useMuteList } from '@/providers/MuteListProvider' import client from '@/services/client.service' import { Event, kinds, nip19, verifyEvent } from 'nostr-tools' import { useEffect, useState } from 'react' -import GenericNoteCard from './GenericNoteCard' +import MainNoteCard from './MainNoteCard' export default function RepostNoteCard({ event, @@ -61,5 +61,5 @@ export default function RepostNoteCard({ return null } - return + return } diff --git a/src/components/NoteCard/UnknownNoteCard.tsx b/src/components/NoteCard/UnknownNoteCard.tsx deleted file mode 100644 index 106e9735..00000000 --- a/src/components/NoteCard/UnknownNoteCard.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Separator } from '@/components/ui/separator' -import { cn } from '@/lib/utils' -import { Event } from 'nostr-tools' -import { FormattedTimestamp } from '../FormattedTimestamp' -import { UnknownNote } from '../Note/UnknownNote' -import UserAvatar from '../UserAvatar' -import Username from '../Username' -import RepostDescription from './RepostDescription' - -export default function UnknownNoteCard({ - event, - className, - embedded = false, - reposter -}: { - event: Event - className?: string - embedded?: boolean - reposter?: string -}) { - return ( -
-
- -
- -
- -
- -
-
-
- -
- {!embedded && } -
- ) -} diff --git a/src/components/NoteCard/index.tsx b/src/components/NoteCard/index.tsx index df210cac..1edabf30 100644 --- a/src/components/NoteCard/index.tsx +++ b/src/components/NoteCard/index.tsx @@ -2,7 +2,7 @@ import { Skeleton } from '@/components/ui/skeleton' import { useMuteList } from '@/providers/MuteListProvider' import { Event, kinds } from 'nostr-tools' import { useTranslation } from 'react-i18next' -import GenericNoteCard from './GenericNoteCard' +import MainNoteCard from './MainNoteCard' import RepostNoteCard from './RepostNoteCard' export default function NoteCard({ @@ -24,7 +24,7 @@ export default function NoteCard({ ) } - return + return } export function NoteCardLoadingSkeleton({ isPictures }: { isPictures: boolean }) { diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index 1b4c25b2..8ebf758b 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -115,7 +115,13 @@ export default function NoteList({ subRequests.push({ urls: myRelayList.write.concat(BIG_RELAY_URLS).slice(0, 5), filter: { - kinds: [kinds.ShortTextNote, kinds.Repost, kinds.Highlights, ExtendedKind.COMMENT], + kinds: [ + kinds.ShortTextNote, + kinds.Repost, + kinds.Highlights, + ExtendedKind.COMMENT, + kinds.LongFormArticle + ], authors: [pubkey], '#p': [author], limit: LIMIT @@ -124,7 +130,13 @@ export default function NoteList({ subRequests.push({ urls: targetRelayList.write.concat(BIG_RELAY_URLS).slice(0, 5), filter: { - kinds: [kinds.ShortTextNote, kinds.Repost, kinds.Highlights, ExtendedKind.COMMENT], + kinds: [ + kinds.ShortTextNote, + kinds.Repost, + kinds.Highlights, + ExtendedKind.COMMENT, + kinds.LongFormArticle + ], authors: [author], '#p': [pubkey], limit: LIMIT @@ -140,7 +152,13 @@ export default function NoteList({ kinds: filterType === 'pictures' ? [ExtendedKind.PICTURE] - : [kinds.ShortTextNote, kinds.Repost, kinds.Highlights, ExtendedKind.COMMENT], + : [ + kinds.ShortTextNote, + kinds.Repost, + kinds.Highlights, + ExtendedKind.COMMENT, + kinds.LongFormArticle + ], limit: areAlgoRelays ? ALGO_LIMIT : LIMIT } if (relayUrls.length === 0 && (_filter.authors?.length || author)) { diff --git a/src/components/NotificationList/NotificationItem/ReactionNotification.tsx b/src/components/NotificationList/NotificationItem/ReactionNotification.tsx index 84bb8577..40605d0e 100644 --- a/src/components/NotificationList/NotificationItem/ReactionNotification.tsx +++ b/src/components/NotificationList/NotificationItem/ReactionNotification.tsx @@ -1,6 +1,5 @@ import Image from '@/components/Image' import { useFetchEvent } from '@/hooks' -import { isSupportedKind } from '@/lib/event' import { toNote } from '@/lib/link' import { tagNameEquals } from '@/lib/tag' import { cn } from '@/lib/utils' @@ -54,7 +53,7 @@ export function ReactionNotification({ return notification.content }, [notification]) - if (!event || !eventId || !isSupportedKind(event.kind)) { + if (!event || !eventId) { return null } diff --git a/src/components/ParentNotePreview/index.tsx b/src/components/ParentNotePreview/index.tsx index fcc68747..fb50bbe5 100644 --- a/src/components/ParentNotePreview/index.tsx +++ b/src/components/ParentNotePreview/index.tsx @@ -1,9 +1,6 @@ import { Skeleton } from '@/components/ui/skeleton' import { useFetchEvent } from '@/hooks' -import { isSupportedKind } from '@/lib/event' import { cn } from '@/lib/utils' -import { useMuteList } from '@/providers/MuteListProvider' -import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import ContentPreview from '../ContentPreview' import UserAvatar from '../UserAvatar' @@ -18,21 +15,15 @@ export default function ParentNotePreview({ onClick?: React.MouseEventHandler | undefined }) { const { t } = useTranslation() - const { mutePubkeys } = useMuteList() const { event, isFetching } = useFetchEvent(eventId) - const isMuted = useMemo( - () => (event ? mutePubkeys.includes(event.pubkey) : false), - [mutePubkeys, event] - ) if (isFetching) { return (
{t('reply to')}
@@ -43,37 +34,18 @@ export default function ParentNotePreview({ ) } - if (!event) { - return ( -
-
{t('reply to')}
-
{`[${t('Note not found')}]`}
-
- ) - } - return (
{t('reply to')}
{event && } - {isMuted ? ( -
[{t('This user has been muted')}]
- ) : !isSupportedKind(event.kind) ? ( -
[{t('Cannot handle event of kind k', { k: event.kind })}]
- ) : ( - - )} +
) } diff --git a/src/components/TranslateButton/index.tsx b/src/components/TranslateButton/index.tsx index 04dcd72d..c86f575d 100644 --- a/src/components/TranslateButton/index.tsx +++ b/src/components/TranslateButton/index.tsx @@ -1,11 +1,11 @@ +import { ExtendedKind } from '@/constants' import { useTranslatedEvent } from '@/hooks' -import { isSupportedKind } from '@/lib/event' import { toTranslation } from '@/lib/link' import { cn, detectLanguage } from '@/lib/utils' import { useSecondaryPage } from '@/PageManager' import { useTranslationService } from '@/providers/TranslationServiceProvider' import { Languages, Loader } from 'lucide-react' -import { Event } from 'nostr-tools' +import { Event, kinds } from 'nostr-tools' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' @@ -22,7 +22,13 @@ export default function TranslateButton({ const { translateEvent, showOriginalEvent } = useTranslationService() const [translating, setTranslating] = useState(false) const translatedEvent = useTranslatedEvent(event.id) - const supported = useMemo(() => isSupportedKind(event.kind), [event]) + const supported = useMemo( + () => + [kinds.ShortTextNote, kinds.Highlights, ExtendedKind.COMMENT, ExtendedKind.PICTURE].includes( + event.kind + ), + [event] + ) const needTranslation = useMemo(() => { const detected = detectLanguage(event.content) diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index 6b78ccdc..ead95a1c 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -280,6 +280,11 @@ export default { Translate: 'ترجمة', 'Show original': 'عرض الأصل', Website: 'الموقع الإلكتروني', - 'Hide untrusted notes': 'إخفاء الملاحظات غير الموثوقة' + 'Hide untrusted notes': 'إخفاء الملاحظات غير الموثوقة', + 'Open in another client': 'فتح في عميل آخر', + Community: 'المجتمع', + Group: 'المجموعة', + 'Live event': 'حدث مباشر', + Article: 'مقالة' } } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 33201577..ccfadf20 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -287,6 +287,11 @@ export default { Translate: 'Übersetzen', 'Show original': 'Original anzeigen', Website: 'Website', - 'Hide untrusted notes': 'Untrusted Notizen ausblenden' + 'Hide untrusted notes': 'Untrusted Notizen ausblenden', + 'Open in another client': 'In anderem Client öffnen', + Community: 'Community', + Group: 'Gruppe', + 'Live event': 'Live-Event', + Article: 'Artikel' } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 4a9bac49..8072c40f 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -280,6 +280,11 @@ export default { Translate: 'Translate', 'Show original': 'Show original', Website: 'Website', - 'Hide untrusted notes': 'Hide untrusted notes' + 'Hide untrusted notes': 'Hide untrusted notes', + 'Open in another client': 'Open in another client', + Community: 'Community', + Group: 'Group', + 'Live event': 'Live event', + Article: 'Article' } } diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index e2ddaac8..41dabf5d 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -285,6 +285,11 @@ export default { Translate: 'Traducir', 'Show original': 'Mostrar original', Website: 'Sitio web', - 'Hide untrusted notes': 'Ocultar notas no confiables' + 'Hide untrusted notes': 'Ocultar notas no confiables', + 'Open in another client': 'Abrir en otro cliente', + Community: 'Comunidad', + Group: 'Grupo', + 'Live event': 'Evento en vivo', + Article: 'Artículo' } } diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index b7039334..816029c2 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -285,6 +285,11 @@ export default { Translate: 'Traduire', 'Show original': 'Afficher l’original', Website: 'Site Web', - 'Hide untrusted notes': 'Cacher les notes non fiables' + 'Hide untrusted notes': 'Cacher les notes non fiables', + 'Open in another client': 'Ouvrir dans un autre client', + Community: 'Communauté', + Group: 'Groupe', + 'Live event': 'Événement en direct', + Article: 'Article' } } diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index 8c738b86..0dc1d9dd 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -284,6 +284,11 @@ export default { Translate: 'Traduci', 'Show original': 'Mostra originale', Website: 'Sito web', - 'Hide untrusted notes': 'Nascondi note non fidate' + 'Hide untrusted notes': 'Nascondi note non fidate', + 'Open in another client': 'Apri in un altro client', + Community: 'Comunità', + Group: 'Gruppo', + 'Live event': 'Evento dal vivo', + Article: 'Articolo' } } diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index 4d8672be..5ba4e0fe 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -282,6 +282,11 @@ export default { Translate: '翻訳', 'Show original': '原文を表示', Website: 'ウェブサイト', - 'Hide untrusted notes': '信頼されていないノートを非表示' + 'Hide untrusted notes': '信頼されていないノートを非表示', + 'Open in another client': '別のクライアントで開く', + Community: 'コミュニティ', + Group: 'グループ', + 'Live event': 'ライブイベント', + Article: '記事' } } diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index ca2779e1..13089e8e 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -282,6 +282,11 @@ export default { Translate: '번역', 'Show original': '원본 보기', Website: '웹사이트', - 'Hide untrusted notes': '신뢰하지 않는 노트 숨기기' + 'Hide untrusted notes': '신뢰하지 않는 노트 숨기기', + 'Open in another client': '다른 클라이언트에서 열기', + Community: '커뮤니티', + Group: '그룹', + 'Live event': '라이브 이벤트', + Article: '기사' } } diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index ae94c677..4f4eb2b3 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -283,6 +283,11 @@ export default { Translate: 'Przetłumacz', 'Show original': 'Pokaż oryginał', Website: 'Strona internetowa', - 'Hide untrusted notes': 'Ukryj wpisy od nieznanych użytkowników' + 'Hide untrusted notes': 'Ukryj wpisy od nieznanych użytkowników', + 'Open in another client': 'Otwórz w innym kliencie', + Community: 'Społeczność', + Group: 'Grupa', + 'Live event': 'Wydarzenie na żywo', + Article: 'Artykuł' } } diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index 38f52b58..523d37d7 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -283,6 +283,11 @@ export default { Translate: 'Traduzir', 'Show original': 'Mostrar original', Website: 'Website', - 'Hide untrusted notes': 'Ocultar notas não confiáveis' + 'Hide untrusted notes': 'Ocultar notas não confiáveis', + 'Open in another client': 'Abrir em outro cliente', + Community: 'Comunidade', + Group: 'Grupo', + 'Live event': 'Evento ao vivo', + Article: 'Artigo' } } diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index 0ba853c7..291c536b 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -284,6 +284,11 @@ export default { Translate: 'Traduzir', 'Show original': 'Mostrar original', Website: 'Website', - 'Hide untrusted notes': 'Esconder notas não confiáveis' + 'Hide untrusted notes': 'Esconder notas não confiáveis', + 'Open in another client': 'Abrir em outro cliente', + Community: 'Comunidade', + Group: 'Grupo', + 'Live event': 'Evento ao vivo', + Article: 'Artigo' } } diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 167a6d4e..148df2ae 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -285,6 +285,11 @@ export default { Translate: 'Перевести', 'Show original': 'Показать оригинал', Website: 'Веб-сайт', - 'Hide untrusted notes': 'Скрыть недоверенные заметки' + 'Hide untrusted notes': 'Скрыть недоверенные заметки', + 'Open in another client': 'Открыть в другом клиенте', + Community: 'Сообщество', + Group: 'Группа', + 'Live event': 'Живое событие', + Article: 'Статья' } } diff --git a/src/i18n/locales/th.ts b/src/i18n/locales/th.ts index 23880bf3..28d6f9a1 100644 --- a/src/i18n/locales/th.ts +++ b/src/i18n/locales/th.ts @@ -279,6 +279,11 @@ export default { Translate: 'แปล', 'Show original': 'แสดงต้นฉบับ', Website: 'เว็บไซต์', - 'Hide untrusted notes': 'ซ่อนโน้ตที่ไม่น่าเชื่อถือ' + 'Hide untrusted notes': 'ซ่อนโน้ตที่ไม่น่าเชื่อถือ', + 'Open in another client': 'เปิดในไคลเอนต์อื่น', + Community: 'ชุมชน', + Group: 'กลุ่ม', + 'Live event': 'เหตุการณ์สด', + Article: 'บทความ' } } diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index 8160e670..a0495791 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -280,6 +280,11 @@ export default { Translate: '翻译', 'Show original': '显示原文', Website: '网站', - 'Hide untrusted notes': '隐藏不受信任的笔记' + 'Hide untrusted notes': '隐藏不受信任的笔记', + 'Open in another client': '在其他客户端打开', + Community: '社区', + Group: '群组', + 'Live event': '直播', + Article: '文章' } } diff --git a/src/lib/event.ts b/src/lib/event.ts index 9720d0c6..d83596f1 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -67,15 +67,6 @@ export function isProtectedEvent(event: Event) { return event.tags.some(([tagName]) => tagName === '-') } -export function isSupportedKind(kind: number) { - return [ - kinds.ShortTextNote, - kinds.Highlights, - ExtendedKind.PICTURE, - ExtendedKind.COMMENT - ].includes(kind) -} - export function getParentEventTag(event?: Event) { if (!event || ![kinds.ShortTextNote, ExtendedKind.COMMENT].includes(event.kind)) return undefined @@ -609,3 +600,106 @@ export function createFakeEvent(event: Partial): Event { ...event } } + +export function getLongFormArticleMetadata(event: Event) { + let title: string | undefined + let summary: string | undefined + let image: string | undefined + const tags = new Set() + + event.tags.forEach(([tagName, tagValue]) => { + if (tagName === 'title') { + title = tagValue + } else if (tagName === 'summary') { + summary = tagValue + } else if (tagName === 'image') { + image = tagValue + } else if (tagName === 't' && tagValue && tags.size < 6) { + tags.add(tagValue.toLocaleLowerCase()) + } + }) + + if (!title) { + title = event.tags.find(tagNameEquals('d'))?.[1] ?? 'no title' + } + + return { title, summary, image, tags: Array.from(tags) } +} + +export function getLiveEventMetadata(event: Event) { + let title: string | undefined + let summary: string | undefined + let image: string | undefined + let status: string | undefined + const tags = new Set() + + event.tags.forEach(([tagName, tagValue]) => { + if (tagName === 'title') { + title = tagValue + } else if (tagName === 'summary') { + summary = tagValue + } else if (tagName === 'image') { + image = tagValue + } else if (tagName === 'status') { + status = tagValue + } else if (tagName === 't' && tagValue && tags.size < 6) { + tags.add(tagValue.toLocaleLowerCase()) + } + }) + + if (!title) { + title = event.tags.find(tagNameEquals('d'))?.[1] ?? 'no title' + } + + return { title, summary, image, status, tags: Array.from(tags) } +} + +export function getGroupMetadata(event: Event) { + let d: string | undefined + let name: string | undefined + let about: string | undefined + let picture: string | undefined + const tags = new Set() + + event.tags.forEach(([tagName, tagValue]) => { + if (tagName === 'name') { + name = tagValue + } else if (tagName === 'about') { + about = tagValue + } else if (tagName === 'picture') { + picture = tagValue + } else if (tagName === 't' && tagValue) { + tags.add(tagValue.toLocaleLowerCase()) + } else if (tagName === 'd') { + d = tagValue + } + }) + + if (!name) { + name = d ?? 'no name' + } + + return { d, name, about, picture, tags: Array.from(tags) } +} + +export function getCommunityDefinition(event: Event) { + let name: string | undefined + let description: string | undefined + let image: string | undefined + + event.tags.forEach(([tagName, tagValue]) => { + if (tagName === 'name') { + name = tagValue + } else if (tagName === 'description') { + description = tagValue + } else if (tagName === 'image') { + image = tagValue + } + }) + + if (!name) { + name = event.tags.find(tagNameEquals('d'))?.[1] ?? 'no name' + } + + return { name, description, image } +} diff --git a/src/lib/link.ts b/src/lib/link.ts index 1df09fe1..78ca19e3 100644 --- a/src/lib/link.ts +++ b/src/lib/link.ts @@ -65,5 +65,7 @@ export const toHablaLongFormArticle = (event: Event) => { export const toZapStreamLiveEvent = (event: Event) => { return `https://zap.stream/${getSharableEventId(event)}` } -export const toChachiChat = (relay: string, d: string) => `https://chachi.chat/${relay}/${d}` +export const toChachiChat = (relay: string, d: string) => { + return `https://chachi.chat/${relay.replace(/^wss?:\/\//, '').replace(/\/$/, '')}/${d}` +} export const toNjump = (id: string) => `https://njump.me/${id}` diff --git a/src/pages/secondary/NotePage/index.tsx b/src/pages/secondary/NotePage/index.tsx index 0a6cc8bf..01509891 100644 --- a/src/pages/secondary/NotePage/index.tsx +++ b/src/pages/secondary/NotePage/index.tsx @@ -11,10 +11,10 @@ import { Skeleton } from '@/components/ui/skeleton' import { ExtendedKind } from '@/constants' import { useFetchEvent } from '@/hooks' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' -import { getParentEventId, getRootEventId, isPictureEvent, isSupportedKind } from '@/lib/event' +import { getParentEventId, getRootEventId, isPictureEvent } from '@/lib/event' import { toNote, toNoteList } from '@/lib/link' import { tagNameEquals } from '@/lib/tag' -import { useMuteList } from '@/providers/MuteListProvider' +import { cn } from '@/lib/utils' import { forwardRef, useMemo } from 'react' import { useTranslation } from 'react-i18next' import NotFoundPage from '../NotFoundPage' @@ -81,6 +81,7 @@ const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref event={event} className="select-text" hideParentNotePreview + originalNoteId={id} />
@@ -109,19 +110,14 @@ function ExternalRoot({ value }: { value: string }) { } function ParentNote({ eventId }: { eventId?: string }) { - const { t } = useTranslation() const { push } = useSecondaryPage() - const { mutePubkeys } = useMuteList() const { event, isFetching } = useFetchEvent(eventId) if (!eventId) return null if (isFetching) { return (
- push(toNote(eventId))} - > +
@@ -132,54 +128,19 @@ function ParentNote({ eventId }: { eventId?: string }) { ) } - if (!event) { - return ( -
- - [{t('Note not found')}] - -
-
- ) - } - - if (mutePubkeys.includes(event.pubkey)) { - return ( -
- push(toNote(eventId))} - > - -
[{t('This user has been muted')}]
-
-
-
- ) - } - - if (!isSupportedKind(event.kind)) { - return ( -
- push(toNote(eventId))} - > - -
[{t('Cannot handle event of kind k', { k: event.kind })}]
-
-
-
- ) - } - return (
push(toNote(eventId))} + className={cn( + 'flex space-x-1 p-1 items-center clickable text-sm text-muted-foreground', + event && 'hover:text-foreground' + )} + onClick={() => { + if (!event) return + push(toNote(eventId)) + }} > - + {event && }