14 changed files with 209 additions and 93 deletions
@ -0,0 +1,63 @@ |
|||||||
|
import { |
||||||
|
EmbeddedEmojiParser, |
||||||
|
EmbeddedEventParser, |
||||||
|
EmbeddedImageParser, |
||||||
|
EmbeddedMentionParser, |
||||||
|
EmbeddedVideoParser, |
||||||
|
parseContent |
||||||
|
} from '@/lib/content-parser' |
||||||
|
import { cn } from '@/lib/utils' |
||||||
|
import { TEmoji } from '@/types' |
||||||
|
import { useMemo } from 'react' |
||||||
|
import { useTranslation } from 'react-i18next' |
||||||
|
import { EmbeddedMentionText } from '../Embedded' |
||||||
|
import Emoji from '../Emoji' |
||||||
|
|
||||||
|
export default function Content({ |
||||||
|
content, |
||||||
|
className, |
||||||
|
emojiInfos |
||||||
|
}: { |
||||||
|
content: string |
||||||
|
className?: string |
||||||
|
emojiInfos?: TEmoji[] |
||||||
|
}) { |
||||||
|
const { t } = useTranslation() |
||||||
|
const nodes = useMemo(() => { |
||||||
|
return parseContent(content, [ |
||||||
|
EmbeddedImageParser, |
||||||
|
EmbeddedVideoParser, |
||||||
|
EmbeddedEventParser, |
||||||
|
EmbeddedMentionParser, |
||||||
|
EmbeddedEmojiParser |
||||||
|
]) |
||||||
|
}, [content]) |
||||||
|
|
||||||
|
return ( |
||||||
|
<span className={cn('pointer-events-none', className)}> |
||||||
|
{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 <EmbeddedMentionText key={index} userId={node.data.split(':')[1]} /> |
||||||
|
} |
||||||
|
if (node.type === 'emoji') { |
||||||
|
const shortcode = node.data.split(':')[1] |
||||||
|
const emoji = emojiInfos?.find((e) => e.shortcode === shortcode) |
||||||
|
if (!emoji) return node.data |
||||||
|
return <Emoji key={index} emoji={emoji} /> |
||||||
|
} |
||||||
|
})} |
||||||
|
</span> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,30 @@ |
|||||||
|
import { useTranslatedEvent } from '@/hooks' |
||||||
|
import { getEmojiInfosFromEmojiTags } from '@/lib/tag' |
||||||
|
import { cn } from '@/lib/utils' |
||||||
|
import { Event } from 'nostr-tools' |
||||||
|
import { useMemo } from 'react' |
||||||
|
import { useTranslation } from 'react-i18next' |
||||||
|
import Content from './Content' |
||||||
|
|
||||||
|
export default function HighlightPreview({ |
||||||
|
event, |
||||||
|
className |
||||||
|
}: { |
||||||
|
event: Event |
||||||
|
className?: string |
||||||
|
}) { |
||||||
|
const { t } = useTranslation() |
||||||
|
const translatedEvent = useTranslatedEvent(event.id) |
||||||
|
const emojiInfos = useMemo(() => getEmojiInfosFromEmojiTags(event.tags), [event]) |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={cn('pointer-events-none', className)}> |
||||||
|
[{t('Highlight')}]{' '} |
||||||
|
<Content |
||||||
|
content={translatedEvent?.content ?? event.content} |
||||||
|
emojiInfos={emojiInfos} |
||||||
|
className="italic pr-0.5" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
@ -1,68 +1,24 @@ |
|||||||
import { useTranslatedEvent } from '@/hooks' |
import { useTranslatedEvent } from '@/hooks' |
||||||
import { |
|
||||||
EmbeddedEmojiParser, |
|
||||||
EmbeddedEventParser, |
|
||||||
EmbeddedImageParser, |
|
||||||
EmbeddedMentionParser, |
|
||||||
EmbeddedVideoParser, |
|
||||||
parseContent |
|
||||||
} from '@/lib/content-parser' |
|
||||||
import { getEmojiInfosFromEmojiTags } from '@/lib/tag' |
import { getEmojiInfosFromEmojiTags } from '@/lib/tag' |
||||||
import { cn } from '@/lib/utils' |
|
||||||
import { Event } from 'nostr-tools' |
import { Event } from 'nostr-tools' |
||||||
import { useMemo } from 'react' |
import { useMemo } from 'react' |
||||||
import { useTranslation } from 'react-i18next' |
import Content from './Content' |
||||||
import { EmbeddedMentionText } from '../Embedded' |
|
||||||
import Emoji from '../Emoji' |
|
||||||
|
|
||||||
export default function NormalContentPreview({ |
export default function NormalContentPreview({ |
||||||
event, |
event, |
||||||
className, |
className |
||||||
onClick |
|
||||||
}: { |
}: { |
||||||
event: Event |
event: Event |
||||||
className?: string |
className?: string |
||||||
onClick?: React.MouseEventHandler<HTMLDivElement> | undefined |
|
||||||
}) { |
}) { |
||||||
const { t } = useTranslation() |
|
||||||
const translatedEvent = useTranslatedEvent(event?.id) |
const translatedEvent = useTranslatedEvent(event?.id) |
||||||
const nodes = useMemo(() => { |
const emojiInfos = useMemo(() => getEmojiInfosFromEmojiTags(event?.tags), [event]) |
||||||
return parseContent(event.content, [ |
|
||||||
EmbeddedImageParser, |
|
||||||
EmbeddedVideoParser, |
|
||||||
EmbeddedEventParser, |
|
||||||
EmbeddedMentionParser, |
|
||||||
EmbeddedEmojiParser |
|
||||||
]) |
|
||||||
}, [event, translatedEvent]) |
|
||||||
|
|
||||||
const emojiInfos = getEmojiInfosFromEmojiTags(event?.tags) |
|
||||||
|
|
||||||
return ( |
return ( |
||||||
<div className={cn('pointer-events-none', className)} onClick={onClick}> |
<Content |
||||||
{nodes.map((node, index) => { |
content={translatedEvent?.content ?? event.content} |
||||||
if (node.type === 'text') { |
className={className} |
||||||
return node.data |
emojiInfos={emojiInfos} |
||||||
} |
/> |
||||||
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 <EmbeddedMentionText key={index} userId={node.data.split(':')[1]} /> |
|
||||||
} |
|
||||||
if (node.type === 'emoji') { |
|
||||||
const shortcode = node.data.split(':')[1] |
|
||||||
const emoji = emojiInfos.find((e) => e.shortcode === shortcode) |
|
||||||
if (!emoji) return node.data |
|
||||||
return <Emoji key={index} emoji={emoji} /> |
|
||||||
} |
|
||||||
})} |
|
||||||
</div> |
|
||||||
) |
) |
||||||
} |
} |
||||||
|
|||||||
@ -0,0 +1,24 @@ |
|||||||
|
import { useTranslatedEvent } from '@/hooks' |
||||||
|
import { getEmojiInfosFromEmojiTags } from '@/lib/tag' |
||||||
|
import { cn } from '@/lib/utils' |
||||||
|
import { Event } from 'nostr-tools' |
||||||
|
import { useMemo } from 'react' |
||||||
|
import { useTranslation } from 'react-i18next' |
||||||
|
import Content from './Content' |
||||||
|
|
||||||
|
export default function PollPreview({ event, className }: { event: Event; className?: string }) { |
||||||
|
const { t } = useTranslation() |
||||||
|
const translatedEvent = useTranslatedEvent(event.id) |
||||||
|
const emojiInfos = useMemo(() => getEmojiInfosFromEmojiTags(event.tags), [event]) |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={cn('pointer-events-none', className)}> |
||||||
|
[{t('Poll')}]{' '} |
||||||
|
<Content |
||||||
|
content={translatedEvent?.content ?? event.content} |
||||||
|
emojiInfos={emojiInfos} |
||||||
|
className="italic pr-0.5" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,47 @@ |
|||||||
|
import { useFetchEvent } from '@/hooks' |
||||||
|
import { toNote } from '@/lib/link' |
||||||
|
import { generateBech32IdFromETag, tagNameEquals } from '@/lib/tag' |
||||||
|
import { cn } from '@/lib/utils' |
||||||
|
import { useSecondaryPage } from '@/PageManager' |
||||||
|
import { Vote } from 'lucide-react' |
||||||
|
import { Event } from 'nostr-tools' |
||||||
|
import { useMemo } from 'react' |
||||||
|
import ContentPreview from '../../ContentPreview' |
||||||
|
import { FormattedTimestamp } from '../../FormattedTimestamp' |
||||||
|
import UserAvatar from '../../UserAvatar' |
||||||
|
|
||||||
|
export function PollResponseNotification({ |
||||||
|
notification, |
||||||
|
isNew = false |
||||||
|
}: { |
||||||
|
notification: Event |
||||||
|
isNew?: boolean |
||||||
|
}) { |
||||||
|
const { push } = useSecondaryPage() |
||||||
|
const eventId = useMemo(() => { |
||||||
|
const eTag = notification.tags.find(tagNameEquals('e')) |
||||||
|
return eTag ? generateBech32IdFromETag(eTag) : undefined |
||||||
|
}, [notification]) |
||||||
|
const { event: pollEvent } = useFetchEvent(eventId) |
||||||
|
|
||||||
|
if (!pollEvent) { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
className="flex gap-2 items-center cursor-pointer py-2" |
||||||
|
onClick={() => push(toNote(pollEvent))} |
||||||
|
> |
||||||
|
<UserAvatar userId={notification.pubkey} size="small" /> |
||||||
|
<Vote size={24} className="text-violet-400" /> |
||||||
|
<ContentPreview |
||||||
|
className={cn('truncate flex-1 w-0', isNew ? 'font-semibold' : 'text-muted-foreground')} |
||||||
|
event={pollEvent} |
||||||
|
/> |
||||||
|
<div className="text-muted-foreground"> |
||||||
|
<FormattedTimestamp timestamp={notification.created_at} short /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
Loading…
Reference in new issue