feat: add support for publishing highlights
This commit is contained in:
@@ -15,7 +15,7 @@ import { cn } from '@/lib/utils'
|
||||
import mediaUpload from '@/services/media-upload.service'
|
||||
import { TImetaInfo } from '@/types'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
EmbeddedHashtag,
|
||||
EmbeddedLNInvoice,
|
||||
@@ -25,8 +25,10 @@ import {
|
||||
} from '../Embedded'
|
||||
import Emoji from '../Emoji'
|
||||
import ExternalLink from '../ExternalLink'
|
||||
import HighlightButton from '../HighlightButton'
|
||||
import ImageGallery from '../ImageGallery'
|
||||
import MediaPlayer from '../MediaPlayer'
|
||||
import PostEditor from '../PostEditor'
|
||||
import WebPreview from '../WebPreview'
|
||||
import XEmbeddedPost from '../XEmbeddedPost'
|
||||
import YoutubeEmbeddedPlayer from '../YoutubeEmbeddedPlayer'
|
||||
@@ -35,13 +37,18 @@ export default function Content({
|
||||
event,
|
||||
content,
|
||||
className,
|
||||
mustLoadMedia
|
||||
mustLoadMedia,
|
||||
enableHighlight = false
|
||||
}: {
|
||||
event?: Event
|
||||
content?: string
|
||||
className?: string
|
||||
mustLoadMedia?: boolean
|
||||
enableHighlight?: boolean
|
||||
}) {
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
const [showHighlightEditor, setShowHighlightEditor] = useState(false)
|
||||
const [selectedText, setSelectedText] = useState('')
|
||||
const translatedEvent = useTranslatedEvent(event?.id)
|
||||
const { nodes, allImages, lastNormalUrl, emojiInfos } = useMemo(() => {
|
||||
const _content = translatedEvent?.content ?? event?.content ?? content
|
||||
@@ -95,81 +102,99 @@ export default function Content({
|
||||
return null
|
||||
}
|
||||
|
||||
const handleHighlight = (text: string) => {
|
||||
setSelectedText(text)
|
||||
setShowHighlightEditor(true)
|
||||
}
|
||||
|
||||
let imageIndex = 0
|
||||
return (
|
||||
<div className={cn('text-wrap break-words whitespace-pre-wrap', className)}>
|
||||
{nodes.map((node, index) => {
|
||||
if (node.type === 'text') {
|
||||
return node.data
|
||||
}
|
||||
if (node.type === 'image' || node.type === 'images') {
|
||||
const start = imageIndex
|
||||
const end = imageIndex + (Array.isArray(node.data) ? node.data.length : 1)
|
||||
imageIndex = end
|
||||
return (
|
||||
<ImageGallery
|
||||
className="mt-2"
|
||||
key={index}
|
||||
images={allImages}
|
||||
start={start}
|
||||
end={end}
|
||||
mustLoad={mustLoadMedia}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (node.type === 'media') {
|
||||
return (
|
||||
<MediaPlayer className="mt-2" key={index} src={node.data} mustLoad={mustLoadMedia} />
|
||||
)
|
||||
}
|
||||
if (node.type === 'url') {
|
||||
return <ExternalLink url={node.data} key={index} />
|
||||
}
|
||||
if (node.type === 'invoice') {
|
||||
return <EmbeddedLNInvoice invoice={node.data} key={index} className="mt-2" />
|
||||
}
|
||||
if (node.type === 'websocket-url') {
|
||||
return <EmbeddedWebsocketUrl url={node.data} key={index} />
|
||||
}
|
||||
if (node.type === 'event') {
|
||||
const id = node.data.split(':')[1]
|
||||
return <EmbeddedNote key={index} noteId={id} className="mt-2" />
|
||||
}
|
||||
if (node.type === 'mention') {
|
||||
return <EmbeddedMention key={index} userId={node.data.split(':')[1]} />
|
||||
}
|
||||
if (node.type === 'hashtag') {
|
||||
return <EmbeddedHashtag hashtag={node.data} key={index} />
|
||||
}
|
||||
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 classNames={{ img: 'mb-1' }} emoji={emoji} key={index} />
|
||||
}
|
||||
if (node.type === 'youtube') {
|
||||
return (
|
||||
<YoutubeEmbeddedPlayer
|
||||
key={index}
|
||||
url={node.data}
|
||||
className="mt-2"
|
||||
mustLoad={mustLoadMedia}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (node.type === 'x-post') {
|
||||
return (
|
||||
<XEmbeddedPost
|
||||
key={index}
|
||||
url={node.data}
|
||||
className="mt-2"
|
||||
mustLoad={mustLoadMedia}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
{lastNormalUrl && <WebPreview className="mt-2" url={lastNormalUrl} />}
|
||||
</div>
|
||||
<>
|
||||
<div ref={contentRef} className={cn('text-wrap break-words whitespace-pre-wrap', className)}>
|
||||
{nodes.map((node, index) => {
|
||||
if (node.type === 'text') {
|
||||
return node.data
|
||||
}
|
||||
if (node.type === 'image' || node.type === 'images') {
|
||||
const start = imageIndex
|
||||
const end = imageIndex + (Array.isArray(node.data) ? node.data.length : 1)
|
||||
imageIndex = end
|
||||
return (
|
||||
<ImageGallery
|
||||
className="mt-2"
|
||||
key={index}
|
||||
images={allImages}
|
||||
start={start}
|
||||
end={end}
|
||||
mustLoad={mustLoadMedia}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (node.type === 'media') {
|
||||
return (
|
||||
<MediaPlayer className="mt-2" key={index} src={node.data} mustLoad={mustLoadMedia} />
|
||||
)
|
||||
}
|
||||
if (node.type === 'url') {
|
||||
return <ExternalLink url={node.data} key={index} />
|
||||
}
|
||||
if (node.type === 'invoice') {
|
||||
return <EmbeddedLNInvoice invoice={node.data} key={index} className="mt-2" />
|
||||
}
|
||||
if (node.type === 'websocket-url') {
|
||||
return <EmbeddedWebsocketUrl url={node.data} key={index} />
|
||||
}
|
||||
if (node.type === 'event') {
|
||||
const id = node.data.split(':')[1]
|
||||
return <EmbeddedNote key={index} noteId={id} className="mt-2" />
|
||||
}
|
||||
if (node.type === 'mention') {
|
||||
return <EmbeddedMention key={index} userId={node.data.split(':')[1]} />
|
||||
}
|
||||
if (node.type === 'hashtag') {
|
||||
return <EmbeddedHashtag hashtag={node.data} key={index} />
|
||||
}
|
||||
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 classNames={{ img: 'mb-1' }} emoji={emoji} key={index} />
|
||||
}
|
||||
if (node.type === 'youtube') {
|
||||
return (
|
||||
<YoutubeEmbeddedPlayer
|
||||
key={index}
|
||||
url={node.data}
|
||||
className="mt-2"
|
||||
mustLoad={mustLoadMedia}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (node.type === 'x-post') {
|
||||
return (
|
||||
<XEmbeddedPost
|
||||
key={index}
|
||||
url={node.data}
|
||||
className="mt-2"
|
||||
mustLoad={mustLoadMedia}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
{lastNormalUrl && <WebPreview className="mt-2" url={lastNormalUrl} />}
|
||||
</div>
|
||||
{enableHighlight && (
|
||||
<HighlightButton onHighlight={handleHighlight} containerRef={contentRef} />
|
||||
)}
|
||||
{enableHighlight && (
|
||||
<PostEditor
|
||||
highlightedText={selectedText}
|
||||
parentStuff={event}
|
||||
open={showHighlightEditor}
|
||||
setOpen={setShowHighlightEditor}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
115
src/components/HighlightButton/index.tsx
Normal file
115
src/components/HighlightButton/index.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Highlighter } from 'lucide-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface HighlightButtonProps {
|
||||
onHighlight: (selectedText: string) => void
|
||||
containerRef?: React.RefObject<HTMLElement>
|
||||
}
|
||||
|
||||
export default function HighlightButton({ onHighlight, containerRef }: HighlightButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
const [position, setPosition] = useState<{ top: number; left: number } | null>(null)
|
||||
const [selectedText, setSelectedText] = useState('')
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleSelectionEnd = () => {
|
||||
// Use a small delay to ensure selection is complete
|
||||
setTimeout(() => {
|
||||
const selection = window.getSelection()
|
||||
const text = selection?.toString().trim()
|
||||
|
||||
if (!text || text.length === 0) {
|
||||
setPosition(null)
|
||||
setSelectedText('')
|
||||
return
|
||||
}
|
||||
|
||||
// Check if selection is within the container (if provided)
|
||||
if (containerRef?.current) {
|
||||
const range = selection?.getRangeAt(0)
|
||||
if (range && !containerRef.current.contains(range.commonAncestorContainer)) {
|
||||
setPosition(null)
|
||||
setSelectedText('')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const range = selection?.getRangeAt(0)
|
||||
if (!range) return
|
||||
|
||||
// Get the bounding rect of the entire selection
|
||||
const rect = range.getBoundingClientRect()
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop
|
||||
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft
|
||||
|
||||
// Position button above the selection area, centered horizontally
|
||||
setPosition({
|
||||
top: rect.top + scrollTop - 48, // 48px above the selection
|
||||
left: rect.left + scrollLeft + rect.width / 2 // Center of the selection
|
||||
})
|
||||
setSelectedText(text)
|
||||
}, 10)
|
||||
}
|
||||
|
||||
// Only listen to mouseup and touchend (when user finishes selection)
|
||||
document.addEventListener('mouseup', handleSelectionEnd)
|
||||
document.addEventListener('touchend', handleSelectionEnd)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mouseup', handleSelectionEnd)
|
||||
document.removeEventListener('touchend', handleSelectionEnd)
|
||||
}
|
||||
}, [containerRef])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (buttonRef.current && !buttonRef.current.contains(event.target as Node)) {
|
||||
const selection = window.getSelection()
|
||||
if (!selection?.toString().trim()) {
|
||||
setPosition(null)
|
||||
setSelectedText('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!position || !selectedText) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed z-50 animate-in fade-in-0 slide-in-from-bottom-4 duration-200"
|
||||
style={{
|
||||
top: `${position.top}px`,
|
||||
left: `${position.left}px`
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
size="sm"
|
||||
variant="default"
|
||||
className="shadow-lg gap-2 -translate-x-1/2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onHighlight(selectedText)
|
||||
// Clear selection after highlighting
|
||||
window.getSelection()?.removeAllRanges()
|
||||
setPosition(null)
|
||||
setSelectedText('')
|
||||
}}
|
||||
>
|
||||
<Highlighter className="h-4 w-4" />
|
||||
{t('Highlight')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export default function Highlight({ event, className }: { event: Event; classNam
|
||||
|
||||
return (
|
||||
<div className={cn('text-wrap break-words whitespace-pre-wrap space-y-4', className)}>
|
||||
{comment && <Content event={createFakeEvent({ content: comment })} />}
|
||||
{comment && <Content event={createFakeEvent({ content: comment, tags: event.tags })} />}
|
||||
<div className="flex gap-4">
|
||||
<div className="w-1 flex-shrink-0 my-1 bg-primary/60 rounded-md" />
|
||||
<div className="italic whitespace-pre-line">
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { SecondaryPageLink, useSecondaryPage } from '@/PageManager'
|
||||
import ImageWithLightbox from '@/components/ImageWithLightbox'
|
||||
import HighlightButton from '@/components/HighlightButton'
|
||||
import PostEditor from '@/components/PostEditor'
|
||||
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
|
||||
import { toNote, toNoteList, toProfile } from '@/lib/link'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import Markdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import NostrNode from './NostrNode'
|
||||
@@ -20,6 +22,14 @@ export default function LongFormArticle({
|
||||
}) {
|
||||
const { push } = useSecondaryPage()
|
||||
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
const [showHighlightEditor, setShowHighlightEditor] = useState(false)
|
||||
const [selectedText, setSelectedText] = useState('')
|
||||
|
||||
const handleHighlight = (text: string) => {
|
||||
setSelectedText(text)
|
||||
setShowHighlightEditor(true)
|
||||
}
|
||||
|
||||
const components = useMemo(
|
||||
() =>
|
||||
@@ -74,54 +84,64 @@ export default function LongFormArticle({
|
||||
/>
|
||||
)
|
||||
}) as Components,
|
||||
[]
|
||||
[event.pubkey]
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`prose prose-zinc max-w-none dark:prose-invert break-words overflow-wrap-anywhere ${className || ''}`}
|
||||
>
|
||||
<h1 className="break-words">{metadata.title}</h1>
|
||||
{metadata.summary && (
|
||||
<blockquote>
|
||||
<p className="break-words">{metadata.summary}</p>
|
||||
</blockquote>
|
||||
)}
|
||||
{metadata.image && (
|
||||
<ImageWithLightbox
|
||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||
className="w-full aspect-[3/1] object-cover my-0"
|
||||
/>
|
||||
)}
|
||||
<Markdown
|
||||
remarkPlugins={[remarkGfm, remarkNostr]}
|
||||
urlTransform={(url) => {
|
||||
if (url.startsWith('nostr:')) {
|
||||
return url.slice(6) // Remove 'nostr:' prefix for rendering
|
||||
}
|
||||
return url
|
||||
}}
|
||||
components={components}
|
||||
<>
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={`prose prose-zinc max-w-none dark:prose-invert break-words overflow-wrap-anywhere ${className || ''}`}
|
||||
>
|
||||
{event.content}
|
||||
</Markdown>
|
||||
{metadata.tags.length > 0 && (
|
||||
<div className="flex gap-2 flex-wrap pb-2">
|
||||
{metadata.tags.map((tag) => (
|
||||
<div
|
||||
key={tag}
|
||||
title={tag}
|
||||
className="flex items-center rounded-full px-3 bg-muted text-muted-foreground max-w-44 cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
push(toNoteList({ hashtag: tag, kinds: [kinds.LongFormArticle] }))
|
||||
}}
|
||||
>
|
||||
#<span className="truncate">{tag}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h1 className="break-words">{metadata.title}</h1>
|
||||
{metadata.summary && (
|
||||
<blockquote>
|
||||
<p className="break-words">{metadata.summary}</p>
|
||||
</blockquote>
|
||||
)}
|
||||
{metadata.image && (
|
||||
<ImageWithLightbox
|
||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||
className="w-full aspect-[3/1] object-cover my-0"
|
||||
/>
|
||||
)}
|
||||
<Markdown
|
||||
remarkPlugins={[remarkGfm, remarkNostr]}
|
||||
urlTransform={(url) => {
|
||||
if (url.startsWith('nostr:')) {
|
||||
return url.slice(6) // Remove 'nostr:' prefix for rendering
|
||||
}
|
||||
return url
|
||||
}}
|
||||
components={components}
|
||||
>
|
||||
{event.content}
|
||||
</Markdown>
|
||||
{metadata.tags.length > 0 && (
|
||||
<div className="flex gap-2 flex-wrap pb-2">
|
||||
{metadata.tags.map((tag) => (
|
||||
<div
|
||||
key={tag}
|
||||
title={tag}
|
||||
className="flex items-center rounded-full px-3 bg-muted text-muted-foreground max-w-44 cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
push(toNoteList({ hashtag: tag, kinds: [kinds.LongFormArticle] }))
|
||||
}}
|
||||
>
|
||||
#<span className="truncate">{tag}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<HighlightButton onHighlight={handleHighlight} containerRef={contentRef} />
|
||||
<PostEditor
|
||||
highlightedText={selectedText}
|
||||
parentStuff={event}
|
||||
open={showHighlightEditor}
|
||||
setOpen={setShowHighlightEditor}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ export default function Note({
|
||||
} else if (event.kind === ExtendedKind.FOLLOW_PACK) {
|
||||
content = <FollowPack className="mt-2" event={event} />
|
||||
} else {
|
||||
content = <Content className="mt-2" event={event} />
|
||||
content = <Content className="mt-2" event={event} enableHighlight />
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Highlighter } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Notification from './Notification'
|
||||
|
||||
export function HighlightNotification({
|
||||
notification,
|
||||
isNew = false
|
||||
}: {
|
||||
notification: Event
|
||||
isNew?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Notification
|
||||
notificationId={notification.id}
|
||||
icon={<Highlighter size={24} className="text-orange-400" />}
|
||||
sender={notification.pubkey}
|
||||
sentAt={notification.created_at}
|
||||
targetEvent={notification}
|
||||
description={t('highlighted your note')}
|
||||
isNew={isNew}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import { HighlightNotification } from './HighlightNotification'
|
||||
import { MentionNotification } from './MentionNotification'
|
||||
import { PollResponseNotification } from './PollResponseNotification'
|
||||
import { ReactionNotification } from './ReactionNotification'
|
||||
@@ -60,5 +61,8 @@ export function NotificationItem({
|
||||
if (notification.kind === ExtendedKind.POLL_RESPONSE) {
|
||||
return <PollResponseNotification notification={notification} isNew={isNew} />
|
||||
}
|
||||
if (notification.kind === kinds.Highlights) {
|
||||
return <HighlightNotification notification={notification} isNew={isNew} />
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ const NotificationList = forwardRef((_, ref) => {
|
||||
case 'mentions':
|
||||
return [
|
||||
kinds.ShortTextNote,
|
||||
kinds.Highlights,
|
||||
ExtendedKind.COMMENT,
|
||||
ExtendedKind.VOICE_COMMENT,
|
||||
ExtendedKind.POLL
|
||||
@@ -70,6 +71,7 @@ const NotificationList = forwardRef((_, ref) => {
|
||||
kinds.GenericRepost,
|
||||
kinds.Reaction,
|
||||
kinds.Zap,
|
||||
kinds.Highlights,
|
||||
ExtendedKind.COMMENT,
|
||||
ExtendedKind.POLL_RESPONSE,
|
||||
ExtendedKind.VOICE_COMMENT,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import Note from '@/components/Note'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { BIG_RELAY_URLS } from '@/constants'
|
||||
import {
|
||||
createCommentDraftEvent,
|
||||
createHighlightDraftEvent,
|
||||
createPollDraftEvent,
|
||||
createShortTextNoteDraftEvent,
|
||||
deleteDraftEventCache
|
||||
@@ -24,18 +26,19 @@ import PostOptions from './PostOptions'
|
||||
import PostRelaySelector from './PostRelaySelector'
|
||||
import PostTextarea, { TPostTextareaHandle } from './PostTextarea'
|
||||
import Uploader from './Uploader'
|
||||
import { BIG_RELAY_URLS } from '@/constants'
|
||||
|
||||
export default function PostContent({
|
||||
defaultContent = '',
|
||||
parentStuff,
|
||||
close,
|
||||
openFrom
|
||||
openFrom,
|
||||
highlightedText
|
||||
}: {
|
||||
defaultContent?: string
|
||||
parentStuff?: Event | string
|
||||
close: () => void
|
||||
openFrom?: string[]
|
||||
highlightedText?: string
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { pubkey, publish, checkLogin } = useNostr()
|
||||
@@ -68,7 +71,7 @@ export default function PostContent({
|
||||
const canPost = useMemo(() => {
|
||||
return (
|
||||
!!pubkey &&
|
||||
!!text &&
|
||||
(!!text || !!highlightedText) &&
|
||||
!posting &&
|
||||
!uploadProgresses.length &&
|
||||
(!isPoll || pollCreateData.options.filter((option) => !!option.trim()).length >= 2) &&
|
||||
@@ -77,6 +80,7 @@ export default function PostContent({
|
||||
}, [
|
||||
pubkey,
|
||||
text,
|
||||
highlightedText,
|
||||
posting,
|
||||
uploadProgresses,
|
||||
isPoll,
|
||||
@@ -123,30 +127,23 @@ export default function PostContent({
|
||||
const post = async (e?: React.MouseEvent) => {
|
||||
e?.stopPropagation()
|
||||
checkLogin(async () => {
|
||||
if (!canPost || postingRef.current) return
|
||||
if (!canPost || !pubkey || postingRef.current) return
|
||||
|
||||
postingRef.current = true
|
||||
setPosting(true)
|
||||
try {
|
||||
const draftEvent =
|
||||
parentStuff &&
|
||||
(typeof parentStuff === 'string' || parentStuff.kind !== kinds.ShortTextNote)
|
||||
? await createCommentDraftEvent(text, parentStuff, mentions, {
|
||||
addClientTag,
|
||||
protectedEvent: isProtectedEvent,
|
||||
isNsfw
|
||||
})
|
||||
: isPoll
|
||||
? await createPollDraftEvent(pubkey!, text, mentions, pollCreateData, {
|
||||
addClientTag,
|
||||
isNsfw
|
||||
})
|
||||
: await createShortTextNoteDraftEvent(text, mentions, {
|
||||
parentEvent,
|
||||
addClientTag,
|
||||
protectedEvent: isProtectedEvent,
|
||||
isNsfw
|
||||
})
|
||||
const draftEvent = await createDraftEvent({
|
||||
parentStuff,
|
||||
highlightedText,
|
||||
text,
|
||||
mentions,
|
||||
isPoll,
|
||||
pollCreateData,
|
||||
pubkey,
|
||||
addClientTag,
|
||||
isProtectedEvent,
|
||||
isNsfw
|
||||
})
|
||||
|
||||
const _additionalRelayUrls = [...additionalRelayUrls]
|
||||
if (parentStuff && typeof parentStuff === 'string') {
|
||||
@@ -205,7 +202,14 @@ export default function PostContent({
|
||||
{parentEvent && (
|
||||
<ScrollArea className="flex max-h-48 flex-col overflow-y-auto rounded-lg border bg-muted/40">
|
||||
<div className="p-2 sm:p-3 pointer-events-none">
|
||||
<Note size="small" event={parentEvent} hideParentNotePreview />
|
||||
{highlightedText ? (
|
||||
<div className="flex gap-4">
|
||||
<div className="w-1 flex-shrink-0 my-1 bg-primary/60 rounded-md" />
|
||||
<div className="italic whitespace-pre-line">{highlightedText}</div>
|
||||
</div>
|
||||
) : (
|
||||
<Note size="small" event={parentEvent} hideParentNotePreview />
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
@@ -220,6 +224,7 @@ export default function PostContent({
|
||||
onUploadStart={handleUploadStart}
|
||||
onUploadProgress={handleUploadProgress}
|
||||
onUploadEnd={handleUploadEnd}
|
||||
placeholder={highlightedText ? t('Write your thoughts about this highlight...') : undefined}
|
||||
/>
|
||||
{isPoll && (
|
||||
<PollEditor
|
||||
@@ -332,7 +337,7 @@ export default function PostContent({
|
||||
</Button>
|
||||
<Button type="submit" disabled={!canPost} onClick={post}>
|
||||
{posting && <LoaderCircle className="animate-spin" />}
|
||||
{parentStuff ? t('Reply') : t('Post')}
|
||||
{parentStuff ? (highlightedText ? t('Publish Highlight') : t('Reply')) : t('Post')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -366,3 +371,62 @@ export default function PostContent({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
async function createDraftEvent({
|
||||
parentStuff,
|
||||
text,
|
||||
mentions,
|
||||
isPoll,
|
||||
pollCreateData,
|
||||
pubkey,
|
||||
addClientTag,
|
||||
isProtectedEvent,
|
||||
isNsfw,
|
||||
highlightedText
|
||||
}: {
|
||||
parentStuff: Event | string | undefined
|
||||
text: string
|
||||
mentions: string[]
|
||||
isPoll: boolean
|
||||
pollCreateData: TPollCreateData
|
||||
pubkey: string
|
||||
addClientTag: boolean
|
||||
isProtectedEvent: boolean
|
||||
isNsfw: boolean
|
||||
highlightedText?: string
|
||||
}) {
|
||||
const { parentEvent, externalContent } =
|
||||
typeof parentStuff === 'string'
|
||||
? { parentEvent: undefined, externalContent: parentStuff }
|
||||
: { parentEvent: parentStuff, externalContent: undefined }
|
||||
|
||||
if (highlightedText && parentEvent) {
|
||||
return createHighlightDraftEvent(highlightedText, text, parentEvent, mentions, {
|
||||
addClientTag,
|
||||
protectedEvent: isProtectedEvent,
|
||||
isNsfw
|
||||
})
|
||||
}
|
||||
|
||||
if (parentStuff && (externalContent || parentEvent?.kind !== kinds.ShortTextNote)) {
|
||||
return await createCommentDraftEvent(text, parentStuff, mentions, {
|
||||
addClientTag,
|
||||
protectedEvent: isProtectedEvent,
|
||||
isNsfw
|
||||
})
|
||||
}
|
||||
|
||||
if (isPoll) {
|
||||
return await createPollDraftEvent(pubkey, text, mentions, pollCreateData, {
|
||||
addClientTag,
|
||||
isNsfw
|
||||
})
|
||||
}
|
||||
|
||||
return await createShortTextNoteDraftEvent(text, mentions, {
|
||||
parentEvent,
|
||||
addClientTag,
|
||||
protectedEvent: isProtectedEvent,
|
||||
isNsfw
|
||||
})
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ const PostTextarea = forwardRef<
|
||||
onUploadStart?: (file: File, cancel: () => void) => void
|
||||
onUploadProgress?: (file: File, progress: number) => void
|
||||
onUploadEnd?: (file: File) => void
|
||||
placeholder?: string
|
||||
}
|
||||
>(
|
||||
(
|
||||
@@ -52,7 +53,8 @@ const PostTextarea = forwardRef<
|
||||
className,
|
||||
onUploadStart,
|
||||
onUploadProgress,
|
||||
onUploadEnd
|
||||
onUploadEnd,
|
||||
placeholder
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
@@ -67,6 +69,7 @@ const PostTextarea = forwardRef<
|
||||
HardBreak,
|
||||
Placeholder.configure({
|
||||
placeholder:
|
||||
placeholder ??
|
||||
t('Write something...') + ' (' + t('Paste or drop media files to upload') + ')'
|
||||
}),
|
||||
Emoji.configure({
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import postEditor from '@/services/post-editor.service'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { Dispatch, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PostContent from './PostContent'
|
||||
import Title from './Title'
|
||||
|
||||
@@ -25,14 +26,17 @@ export default function PostEditor({
|
||||
parentStuff,
|
||||
open,
|
||||
setOpen,
|
||||
openFrom
|
||||
openFrom,
|
||||
highlightedText
|
||||
}: {
|
||||
defaultContent?: string
|
||||
parentStuff?: Event | string
|
||||
open: boolean
|
||||
setOpen: Dispatch<boolean>
|
||||
openFrom?: string[]
|
||||
highlightedText?: string
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
|
||||
const content = useMemo(() => {
|
||||
@@ -42,9 +46,10 @@ export default function PostEditor({
|
||||
parentStuff={parentStuff}
|
||||
close={() => setOpen(false)}
|
||||
openFrom={openFrom}
|
||||
highlightedText={highlightedText}
|
||||
/>
|
||||
)
|
||||
}, [])
|
||||
}, [highlightedText])
|
||||
|
||||
if (isSmallScreen) {
|
||||
return (
|
||||
@@ -64,7 +69,7 @@ export default function PostEditor({
|
||||
<div className="space-y-4 px-2 py-6">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-start">
|
||||
<Title parentStuff={parentStuff} />
|
||||
{highlightedText ? t('Create Highlight') : <Title parentStuff={parentStuff} />}
|
||||
</SheetTitle>
|
||||
<SheetDescription className="hidden" />
|
||||
</SheetHeader>
|
||||
@@ -92,7 +97,7 @@ export default function PostEditor({
|
||||
<div className="space-y-4 px-2 py-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Title parentStuff={parentStuff} />
|
||||
{highlightedText ? t('Create Highlight') : <Title parentStuff={parentStuff} />}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="hidden" />
|
||||
</DialogHeader>
|
||||
|
||||
@@ -384,6 +384,7 @@ export default {
|
||||
'reacted to your note': 'تفاعل مع ملاحظتك',
|
||||
'reposted your note': 'أعاد نشر ملاحظتك',
|
||||
'zapped your note': 'زاب ملاحظتك',
|
||||
'highlighted your note': 'أبرز ملاحظتك',
|
||||
'zapped you': 'زابك',
|
||||
'Mark as read': 'تعليم كمقروء',
|
||||
Report: 'تبليغ',
|
||||
@@ -583,6 +584,9 @@ export default {
|
||||
'Special Follow': 'متابعة خاصة',
|
||||
'Unfollow Special': 'إلغاء المتابعة الخاصة',
|
||||
'Personal Feeds': 'التدفقات الشخصية',
|
||||
'Relay Feeds': 'تدفقات الترحيل'
|
||||
'Relay Feeds': 'تدفقات الترحيل',
|
||||
'Create Highlight': 'إنشاء تمييز',
|
||||
'Write your thoughts about this highlight...': 'اكتب أفكارك حول هذا التمييز...',
|
||||
'Publish Highlight': 'نشر التمييز'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@ export default {
|
||||
'reacted to your note': 'hat auf Ihre Notiz reagiert',
|
||||
'reposted your note': 'hat Ihre Notiz geteilt',
|
||||
'zapped your note': 'hat Ihre Notiz gezappt',
|
||||
'highlighted your note': 'hat Ihre Notiz hervorgehoben',
|
||||
'zapped you': 'hat Sie gezappt',
|
||||
'Mark as read': 'Als gelesen markieren',
|
||||
Report: 'Melden',
|
||||
@@ -599,6 +600,10 @@ export default {
|
||||
'Special Follow': 'Besonders Folgen',
|
||||
'Unfollow Special': 'Besonders Entfolgen',
|
||||
'Personal Feeds': 'Persönliche Feeds',
|
||||
'Relay Feeds': 'Relay-Feeds'
|
||||
'Relay Feeds': 'Relay-Feeds',
|
||||
'Create Highlight': 'Markierung Erstellen',
|
||||
'Write your thoughts about this highlight...':
|
||||
'Schreiben Sie Ihre Gedanken zu dieser Markierung...',
|
||||
'Publish Highlight': 'Markierung Veröffentlichen'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,6 +383,7 @@ export default {
|
||||
'reacted to your note': 'reacted to your note',
|
||||
'reposted your note': 'reposted your note',
|
||||
'zapped your note': 'zapped your note',
|
||||
'highlighted your note': 'highlighted your note',
|
||||
'zapped you': 'zapped you',
|
||||
'Mark as read': 'Mark as read',
|
||||
Report: 'Report',
|
||||
@@ -586,6 +587,9 @@ export default {
|
||||
'Special Follow': 'Special Follow',
|
||||
'Unfollow Special': 'Unfollow Special',
|
||||
'Personal Feeds': 'Personal Feeds',
|
||||
'Relay Feeds': 'Relay Feeds'
|
||||
'Relay Feeds': 'Relay Feeds',
|
||||
'Create Highlight': 'Create Highlight',
|
||||
'Write your thoughts about this highlight...': 'Write your thoughts about this highlight...',
|
||||
'Publish Highlight': 'Publish Highlight'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,6 +389,7 @@ export default {
|
||||
'reacted to your note': 'reaccionó a tu nota',
|
||||
'reposted your note': 'reposteó tu nota',
|
||||
'zapped your note': 'zappeó tu nota',
|
||||
'highlighted your note': 'destacó tu nota',
|
||||
'zapped you': 'te zappeó',
|
||||
'Mark as read': 'Marcar como leído',
|
||||
Report: 'Reportar',
|
||||
@@ -595,6 +596,10 @@ export default {
|
||||
'Special Follow': 'Seguir Especial',
|
||||
'Unfollow Special': 'Dejar de Seguir Especial',
|
||||
'Personal Feeds': 'Feeds Personales',
|
||||
'Relay Feeds': 'Feeds de Relays'
|
||||
'Relay Feeds': 'Feeds de Relays',
|
||||
'Create Highlight': 'Crear Resaltado',
|
||||
'Write your thoughts about this highlight...':
|
||||
'Escribe tus pensamientos sobre este resaltado...',
|
||||
'Publish Highlight': 'Publicar Resaltado'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,6 +385,7 @@ export default {
|
||||
'reacted to your note': 'به یادداشت شما واکنش نشان داد',
|
||||
'reposted your note': 'یادداشت شما را بازنشر کرد',
|
||||
'zapped your note': 'یادداشت شما را زپ کرد',
|
||||
'highlighted your note': 'یادداشت شما را برجسته کرد',
|
||||
'zapped you': 'شما را زپ کرد',
|
||||
'Mark as read': 'علامتگذاری به عنوان خوانده شده',
|
||||
Report: 'گزارش',
|
||||
@@ -589,6 +590,9 @@ export default {
|
||||
'Special Follow': 'دنبال کردن ویژه',
|
||||
'Unfollow Special': 'لغو دنبال کردن ویژه',
|
||||
'Personal Feeds': 'فیدهای شخصی',
|
||||
'Relay Feeds': 'فیدهای رله'
|
||||
'Relay Feeds': 'فیدهای رله',
|
||||
'Create Highlight': 'ایجاد برجستهسازی',
|
||||
'Write your thoughts about this highlight...': 'نظرات خود را درباره این برجستهسازی بنویسید...',
|
||||
'Publish Highlight': 'انتشار برجستهسازی'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@ export default {
|
||||
'reacted to your note': 'a réagi à votre note',
|
||||
'reposted your note': 'a repartagé votre note',
|
||||
'zapped your note': 'a zappé votre note',
|
||||
'highlighted your note': 'a mis en évidence votre note',
|
||||
'zapped you': 'vous a zappé',
|
||||
'Mark as read': 'Marquer comme lu',
|
||||
Report: 'Signaler',
|
||||
@@ -598,6 +599,9 @@ export default {
|
||||
'Special Follow': 'Suivre Spécial',
|
||||
'Unfollow Special': 'Ne Plus Suivre Spécial',
|
||||
'Personal Feeds': 'Flux Personnels',
|
||||
'Relay Feeds': 'Flux de Relais'
|
||||
'Relay Feeds': 'Flux de Relais',
|
||||
'Create Highlight': 'Créer un Surlignage',
|
||||
'Write your thoughts about this highlight...': 'Écrivez vos pensées sur ce surlignage...',
|
||||
'Publish Highlight': 'Publier le Surlignage'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,6 +388,7 @@ export default {
|
||||
'reacted to your note': 'ने आपके नोट पर प्रतिक्रिया दी',
|
||||
'reposted your note': 'ने आपके नोट को रीपोस्ट किया',
|
||||
'zapped your note': 'ने आपके नोट को जैप किया',
|
||||
'highlighted your note': 'ने आपके नोट को हाइलाइट किया',
|
||||
'zapped you': 'ने आपको जैप किया',
|
||||
'Mark as read': 'पढ़ा हुआ मार्क करें',
|
||||
Report: 'रिपोर्ट करें',
|
||||
@@ -590,6 +591,9 @@ export default {
|
||||
'Special Follow': 'विशेष फ़ॉलो',
|
||||
'Unfollow Special': 'विशेष अनफ़ॉलो',
|
||||
'Personal Feeds': 'व्यक्तिगत फ़ीड',
|
||||
'Relay Feeds': 'रिले फ़ीड'
|
||||
'Relay Feeds': 'रिले फ़ीड',
|
||||
'Create Highlight': 'हाइलाइट बनाएं',
|
||||
'Write your thoughts about this highlight...': 'इस हाइलाइट के बारे में अपने विचार लिखें...',
|
||||
'Publish Highlight': 'हाइलाइट प्रकाशित करें'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,6 +385,7 @@ export default {
|
||||
'reacted to your note': 'reagált a posztodra',
|
||||
'reposted your note': 'újraposztolta a posztodat',
|
||||
'zapped your note': 'zappolta a posztodat',
|
||||
'highlighted your note': 'kiemelte a posztodat',
|
||||
'zapped you': 'zappolt téged',
|
||||
'Mark as read': 'Megjelölés olvasottként',
|
||||
Report: 'Jelentés',
|
||||
@@ -584,6 +585,9 @@ export default {
|
||||
'Special Follow': 'Különleges Követés',
|
||||
'Unfollow Special': 'Különleges Követés Megszüntetése',
|
||||
'Personal Feeds': 'Személyes Feedek',
|
||||
'Relay Feeds': 'Relay Feedek'
|
||||
'Relay Feeds': 'Relay Feedek',
|
||||
'Create Highlight': 'Kiemelés Létrehozása',
|
||||
'Write your thoughts about this highlight...': 'Írd le a gondolataidat erről a kiemelésről...',
|
||||
'Publish Highlight': 'Kiemelés Közzététele'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,6 +389,7 @@ export default {
|
||||
'reacted to your note': 'ha reagito alla tua nota',
|
||||
'reposted your note': 'ha ricondiviso la tua nota',
|
||||
'zapped your note': 'ha zappato la tua nota',
|
||||
'highlighted your note': 'ha evidenziato la tua nota',
|
||||
'zapped you': 'ti ha zappato',
|
||||
'Mark as read': 'Segna come letto',
|
||||
Report: 'Segnala',
|
||||
@@ -594,6 +595,10 @@ export default {
|
||||
'Special Follow': 'Segui Speciale',
|
||||
'Unfollow Special': 'Smetti di Seguire Speciale',
|
||||
'Personal Feeds': 'Feed Personali',
|
||||
'Relay Feeds': 'Feed di Relay'
|
||||
'Relay Feeds': 'Feed di Relay',
|
||||
'Create Highlight': 'Crea Evidenziazione',
|
||||
'Write your thoughts about this highlight...':
|
||||
'Scrivi i tuoi pensieri su questa evidenziazione...',
|
||||
'Publish Highlight': 'Pubblica Evidenziazione'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,6 +386,7 @@ export default {
|
||||
'reacted to your note': 'あなたのノートにリアクションしました',
|
||||
'reposted your note': 'あなたのノートをリポストしました',
|
||||
'zapped your note': 'あなたのノートにザップしました',
|
||||
'highlighted your note': 'あなたのノートをハイライトしました',
|
||||
'zapped you': 'あなたにザップしました',
|
||||
'Mark as read': '既読にする',
|
||||
Report: '報告',
|
||||
@@ -589,6 +590,10 @@ export default {
|
||||
'Special Follow': '特別フォロー',
|
||||
'Unfollow Special': '特別フォロー解除',
|
||||
'Personal Feeds': '個人フィード',
|
||||
'Relay Feeds': 'リレーフィード'
|
||||
'Relay Feeds': 'リレーフィード',
|
||||
'Create Highlight': 'ハイライトを作成',
|
||||
'Write your thoughts about this highlight...':
|
||||
'このハイライトについての考えを書いてください...',
|
||||
'Publish Highlight': 'ハイライトを公開'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,6 +386,7 @@ export default {
|
||||
'reacted to your note': '당신의 노트에 반응했습니다',
|
||||
'reposted your note': '당신의 노트를 리포스트했습니다',
|
||||
'zapped your note': '당신의 노트를 잽했습니다',
|
||||
'highlighted your note': '당신의 노트를 하이라이트했습니다',
|
||||
'zapped you': '당신을 잽했습니다',
|
||||
'Mark as read': '읽음으로 표시',
|
||||
Report: '신고',
|
||||
@@ -588,6 +589,9 @@ export default {
|
||||
'Special Follow': '특별 팔로우',
|
||||
'Unfollow Special': '특별 팔로우 해제',
|
||||
'Personal Feeds': '개인 피드',
|
||||
'Relay Feeds': '릴레이 피드'
|
||||
'Relay Feeds': '릴레이 피드',
|
||||
'Create Highlight': '하이라이트 만들기',
|
||||
'Write your thoughts about this highlight...': '이 하이라이트에 대한 생각을 작성하세요...',
|
||||
'Publish Highlight': '하이라이트 게시'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,6 +390,7 @@ export default {
|
||||
'reacted to your note': 'zareagował na twój wpis',
|
||||
'reposted your note': 'repostował twój wpis',
|
||||
'zapped your note': 'zappował twój wpis',
|
||||
'highlighted your note': 'wyróżnił twój wpis',
|
||||
'zapped you': 'zappował cię',
|
||||
'Mark as read': 'Oznacz jako przeczytane',
|
||||
Report: 'Zgłoś',
|
||||
@@ -595,6 +596,10 @@ export default {
|
||||
'Special Follow': 'Specjalne Śledzenie',
|
||||
'Unfollow Special': 'Cofnij Specjalne Śledzenie',
|
||||
'Personal Feeds': 'Osobiste Kanały',
|
||||
'Relay Feeds': 'Kanały Przekaźników'
|
||||
'Relay Feeds': 'Kanały Przekaźników',
|
||||
'Create Highlight': 'Utwórz Podświetlenie',
|
||||
'Write your thoughts about this highlight...':
|
||||
'Napisz swoje przemyślenia na temat tego podświetlenia...',
|
||||
'Publish Highlight': 'Opublikuj Podświetlenie'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,6 +386,7 @@ export default {
|
||||
'reacted to your note': 'reagiu à sua nota',
|
||||
'reposted your note': 'republicou sua nota',
|
||||
'zapped your note': 'zappeou sua nota',
|
||||
'highlighted your note': 'destacou sua nota',
|
||||
'zapped you': 'zappeou você',
|
||||
'Mark as read': 'Marcar como lida',
|
||||
Report: 'Denunciar',
|
||||
@@ -590,6 +591,10 @@ export default {
|
||||
'Special Follow': 'Favoritos',
|
||||
'Unfollow Special': 'Desfavoritar',
|
||||
'Personal Feeds': 'Meus feeds',
|
||||
'Relay Feeds': 'Feeds de relays'
|
||||
'Relay Feeds': 'Feeds de relays',
|
||||
'Create Highlight': 'Criar Destaque',
|
||||
'Write your thoughts about this highlight...':
|
||||
'Escreva seus pensamentos sobre este destaque...',
|
||||
'Publish Highlight': 'Publicar Destaque'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,6 +389,7 @@ export default {
|
||||
'reacted to your note': 'reagiu à sua nota',
|
||||
'reposted your note': 'republicou a sua nota',
|
||||
'zapped your note': 'zappeou a sua nota',
|
||||
'highlighted your note': 'destacou a sua nota',
|
||||
'zapped you': 'zappeou-o',
|
||||
'Mark as read': 'Marcar como lida',
|
||||
Report: 'Denunciar',
|
||||
@@ -593,6 +594,10 @@ export default {
|
||||
'Special Follow': 'Seguir Especial',
|
||||
'Unfollow Special': 'Deixar de Seguir Especial',
|
||||
'Personal Feeds': 'Feeds Pessoais',
|
||||
'Relay Feeds': 'Feeds de Relays'
|
||||
'Relay Feeds': 'Feeds de Relays',
|
||||
'Create Highlight': 'Criar Destaque',
|
||||
'Write your thoughts about this highlight...':
|
||||
'Escreva os seus pensamentos sobre este destaque...',
|
||||
'Publish Highlight': 'Publicar Destaque'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,6 +390,7 @@ export default {
|
||||
'reacted to your note': 'отреагировал на вашу заметку',
|
||||
'reposted your note': 'репостнул вашу заметку',
|
||||
'zapped your note': 'заппил вашу заметку',
|
||||
'highlighted your note': 'выделил вашу заметку',
|
||||
'zapped you': 'заппил вас',
|
||||
'Mark as read': 'Отметить как прочитанное',
|
||||
Report: 'Пожаловаться',
|
||||
@@ -595,6 +596,9 @@ export default {
|
||||
'Special Follow': 'Особая Подписка',
|
||||
'Unfollow Special': 'Отменить Особую Подписку',
|
||||
'Personal Feeds': 'Личные Ленты',
|
||||
'Relay Feeds': 'Ленты Релеев'
|
||||
'Relay Feeds': 'Ленты Релеев',
|
||||
'Create Highlight': 'Создать Выделение',
|
||||
'Write your thoughts about this highlight...': 'Напишите свои мысли об этом выделении...',
|
||||
'Publish Highlight': 'Опубликовать Выделение'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,6 +382,7 @@ export default {
|
||||
'reacted to your note': 'ได้แสดงปฏิกิริยาต่อโน้ตของคุณ',
|
||||
'reposted your note': 'ได้รีโพสต์โน้ตของคุณ',
|
||||
'zapped your note': 'ได้แซปโน้ตของคุณ',
|
||||
'highlighted your note': 'ได้ไฮไลต์โน้ตของคุณ',
|
||||
'zapped you': 'ได้แซปคุณ',
|
||||
'Mark as read': 'ทำเครื่องหมายว่าอ่านแล้ว',
|
||||
Report: 'รายงาน',
|
||||
@@ -582,6 +583,9 @@ export default {
|
||||
'Special Follow': 'ติดตามพิเศษ',
|
||||
'Unfollow Special': 'ยกเลิกติดตามพิเศษ',
|
||||
'Personal Feeds': 'ฟีดส่วนตัว',
|
||||
'Relay Feeds': 'ฟีดรีเลย์'
|
||||
'Relay Feeds': 'ฟีดรีเลย์',
|
||||
'Create Highlight': 'สร้างไฮไลท์',
|
||||
'Write your thoughts about this highlight...': 'เขียนความคิดของคุณเกี่ยวกับไฮไลท์นี้...',
|
||||
'Publish Highlight': 'เผยแพร่ไฮไลท์'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,6 +380,7 @@ export default {
|
||||
'reacted to your note': '对您的笔记做出了反应',
|
||||
'reposted your note': '转发了您的笔记',
|
||||
'zapped your note': '打闪了您的笔记',
|
||||
'highlighted your note': '高亮了您的笔记',
|
||||
'zapped you': '给您打闪',
|
||||
'Mark as read': '标记为已读',
|
||||
Report: '举报',
|
||||
@@ -575,6 +576,9 @@ export default {
|
||||
'Special Follow': '特别关注',
|
||||
'Unfollow Special': '取消特别关注',
|
||||
'Personal Feeds': '个人订阅',
|
||||
'Relay Feeds': '中继订阅'
|
||||
'Relay Feeds': '中继订阅',
|
||||
'Create Highlight': '创建高亮',
|
||||
'Write your thoughts about this highlight...': '写下你对这段高亮的想法...',
|
||||
'Publish Highlight': '发布高亮'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,6 +307,74 @@ export async function createCommentDraftEvent(
|
||||
return setDraftEventCache(baseDraft)
|
||||
}
|
||||
|
||||
// https://github.com/nostr-protocol/nips/blob/master/84.md
|
||||
export function createHighlightDraftEvent(
|
||||
highlightedText: string,
|
||||
comment: string = '',
|
||||
sourceEvent: Event,
|
||||
mentions: string[],
|
||||
options: {
|
||||
addClientTag?: boolean
|
||||
protectedEvent?: boolean
|
||||
isNsfw?: boolean
|
||||
} = {}
|
||||
): TDraftEvent {
|
||||
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(comment)
|
||||
const quoteTags = extractQuoteTags(comment)
|
||||
const hashtags = extractHashtags(transformedEmojisContent)
|
||||
|
||||
const tags = emojiTags.concat(hashtags.map((hashtag) => buildTTag(hashtag)))
|
||||
|
||||
// imeta tags
|
||||
const images = extractImagesFromContent(transformedEmojisContent)
|
||||
if (images && images.length) {
|
||||
tags.push(...generateImetaTags(images))
|
||||
}
|
||||
|
||||
// q tags
|
||||
tags.push(...quoteTags)
|
||||
|
||||
// p tags
|
||||
tags.push(
|
||||
...mentions
|
||||
.filter((pubkey) => pubkey !== sourceEvent.pubkey)
|
||||
.map((pubkey) => ['p', pubkey, '', 'mention'])
|
||||
)
|
||||
|
||||
// Add comment tag if comment exists
|
||||
if (transformedEmojisContent) {
|
||||
tags.push(['comment', transformedEmojisContent])
|
||||
}
|
||||
|
||||
// Add source reference
|
||||
const hint = client.getEventHint(sourceEvent.id)
|
||||
if (isReplaceableEvent(sourceEvent.kind)) {
|
||||
tags.push(['a', getReplaceableCoordinateFromEvent(sourceEvent), hint, 'source'])
|
||||
} else {
|
||||
tags.push(['e', sourceEvent.id, hint, 'source'])
|
||||
}
|
||||
tags.push(['p', sourceEvent.pubkey, '', 'author'])
|
||||
|
||||
if (options.addClientTag) {
|
||||
tags.push(buildClientTag())
|
||||
}
|
||||
|
||||
if (options.isNsfw) {
|
||||
tags.push(buildNsfwTag())
|
||||
}
|
||||
|
||||
if (options.protectedEvent) {
|
||||
tags.push(buildProtectedTag())
|
||||
}
|
||||
|
||||
const baseDraft = {
|
||||
kind: kinds.Highlights,
|
||||
content: highlightedText,
|
||||
tags
|
||||
}
|
||||
return setDraftEventCache(baseDraft)
|
||||
}
|
||||
|
||||
export function createRelayListDraftEvent(mailboxRelays: TMailboxRelay[]): TDraftEvent {
|
||||
return {
|
||||
kind: kinds.RelayList,
|
||||
|
||||
Reference in New Issue
Block a user