diff --git a/src/components/ReplyNote/index.tsx b/src/components/ReplyNote/index.tsx index c0b82d1..7851575 100644 --- a/src/components/ReplyNote/index.tsx +++ b/src/components/ReplyNote/index.tsx @@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { isMentioningMutedUsers } from '@/lib/event' import { toNote } from '@/lib/link' +import { cn } from '@/lib/utils' import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useMuteList } from '@/providers/MuteListProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' @@ -15,9 +16,10 @@ import Content from '../Content' import { FormattedTimestamp } from '../FormattedTimestamp' import Nip05 from '../Nip05' import NoteOptions from '../NoteOptions' -import StuffStats from '../StuffStats' import ParentNotePreview from '../ParentNotePreview' +import StuffStats from '../StuffStats' import TranslateButton from '../TranslateButton' +import TrustScoreBadge from '../TrustScoreBadge' import UserAvatar from '../UserAvatar' import Username from '../Username' @@ -25,12 +27,14 @@ export default function ReplyNote({ event, parentEventId, onClickParent = () => {}, - highlight = false + highlight = false, + className = '' }: { event: Event parentEventId?: string onClickParent?: () => void highlight?: boolean + className?: string }) { const { t } = useTranslation() const { isSmallScreen } = useScreenSize() @@ -53,7 +57,11 @@ export default function ReplyNote({ return (
push(toNote(event))} > @@ -68,6 +76,7 @@ export default function ReplyNote({ className="text-sm font-semibold text-muted-foreground hover:text-foreground truncate" skeletonClassName="h-3" /> +
diff --git a/src/components/ReplyNoteList/SubReplies.tsx b/src/components/ReplyNoteList/SubReplies.tsx new file mode 100644 index 0000000..8cbb785 --- /dev/null +++ b/src/components/ReplyNoteList/SubReplies.tsx @@ -0,0 +1,152 @@ +import { useSecondaryPage } from '@/PageManager' +import { Separator } from '@/components/ui/separator' +import { getEventKey, getKeyFromTag, getParentTag, isMentioningMutedUsers } from '@/lib/event' +import { toNote } from '@/lib/link' +import { generateBech32IdFromETag } from '@/lib/tag' +import { useContentPolicy } from '@/providers/ContentPolicyProvider' +import { useMuteList } from '@/providers/MuteListProvider' +import { useReply } from '@/providers/ReplyProvider' +import { useUserTrust } from '@/providers/UserTrustProvider' +import { ChevronDown, ChevronUp } from 'lucide-react' +import { NostrEvent } from 'nostr-tools' +import { useCallback, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import ReplyNote from '../ReplyNote' +import { cn } from '@/lib/utils' + +export default function SubReplies({ parentKey }: { parentKey: string }) { + const { t } = useTranslation() + const { push } = useSecondaryPage() + const { repliesMap } = useReply() + const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() + const { mutePubkeySet } = useMuteList() + const { hideContentMentioningMutedUsers } = useContentPolicy() + const [isExpanded, setIsExpanded] = useState(false) + const replies = useMemo(() => { + const replyKeySet = new Set() + const replyEvents: NostrEvent[] = [] + + let parentKeys = [parentKey] + while (parentKeys.length > 0) { + const events = parentKeys.flatMap((key) => repliesMap.get(key)?.events || []) + events.forEach((evt) => { + const key = getEventKey(evt) + if (replyKeySet.has(key)) return + if (mutePubkeySet.has(evt.pubkey)) return + if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet)) return + if (hideUntrustedInteractions && !isUserTrusted(evt.pubkey)) { + const replyKey = getEventKey(evt) + const repliesForThisReply = repliesMap.get(replyKey) + // If the reply is not trusted and there are no trusted replies for this reply, skip rendering + if ( + !repliesForThisReply || + repliesForThisReply.events.every((evt) => !isUserTrusted(evt.pubkey)) + ) { + return + } + } + + replyKeySet.add(key) + replyEvents.push(evt) + }) + parentKeys = events.map((evt) => getEventKey(evt)) + } + return replyEvents.sort((a, b) => a.created_at - b.created_at) + }, [ + parentKey, + repliesMap, + mutePubkeySet, + hideContentMentioningMutedUsers, + hideUntrustedInteractions + ]) + const [highlightReplyKey, setHighlightReplyKey] = useState(undefined) + const replyRefs = useRef>({}) + + const highlightReply = useCallback((key: string, eventId?: string, scrollTo = true) => { + let found = false + if (scrollTo) { + const ref = replyRefs.current[key] + if (ref) { + found = true + ref.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) + } + } + if (!found) { + if (eventId) push(toNote(eventId)) + return + } + + setHighlightReplyKey(key) + setTimeout(() => { + setHighlightReplyKey((pre) => (pre === key ? undefined : pre)) + }, 1500) + }, []) + + if (replies.length === 0) return + + return ( +
+ {replies.length > 1 && ( + + )} + {(isExpanded || replies.length === 1) && ( +
+ {replies.map((reply, index) => { + const currentReplyKey = getEventKey(reply) + const _parentTag = getParentTag(reply) + if (_parentTag?.type !== 'e') return null + const _parentKey = _parentTag ? getKeyFromTag(_parentTag.tag) : undefined + const _parentEventId = generateBech32IdFromETag(_parentTag.tag) + return ( +
(replyRefs.current[currentReplyKey] = el)} + key={currentReplyKey} + className="scroll-mt-12 flex" + > +
+ { + if (!_parentKey) return + highlightReply(_parentKey, _parentEventId) + }} + highlight={highlightReplyKey === currentReplyKey} + /> +
+ ) + })} +
+ )} +
+ ) +} diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx index 2659cb7..90386d8 100644 --- a/src/components/ReplyNoteList/index.tsx +++ b/src/components/ReplyNoteList/index.tsx @@ -2,16 +2,13 @@ import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { useStuff } from '@/hooks/useStuff' import { getEventKey, - getKeyFromTag, - getParentTag, getReplaceableCoordinateFromEvent, getRootTag, isMentioningMutedUsers, isProtectedEvent, isReplaceableEvent } from '@/lib/event' -import { toNote } from '@/lib/link' -import { generateBech32IdFromATag, generateBech32IdFromETag } from '@/lib/tag' +import { generateBech32IdFromETag } from '@/lib/tag' import { useSecondaryPage } from '@/PageManager' import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useMuteList } from '@/providers/MuteListProvider' @@ -23,6 +20,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { LoadingBar } from '../LoadingBar' import ReplyNote, { ReplyNoteSkeleton } from '../ReplyNote' +import SubReplies from './SubReplies' type TRootInfo = | { type: 'E'; id: string; pubkey: string } @@ -40,7 +38,7 @@ export default function ReplyNoteList({ index?: number }) { const { t } = useTranslation() - const { push, currentIndex } = useSecondaryPage() + const { currentIndex } = useSecondaryPage() const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() const { mutePubkeySet } = useMuteList() const { hideContentMentioningMutedUsers } = useContentPolicy() @@ -49,30 +47,40 @@ export default function ReplyNoteList({ const { event, externalContent, stuffKey } = useStuff(stuff) const replies = useMemo(() => { const replyKeySet = new Set() - const replyEvents: NEvent[] = [] + const replyEvents = (repliesMap.get(stuffKey)?.events || []).filter((evt) => { + const key = getEventKey(evt) + if (replyKeySet.has(key)) return false + if (mutePubkeySet.has(evt.pubkey)) return false + if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet)) { + return false + } + if (hideUntrustedInteractions && !isUserTrusted(evt.pubkey)) { + const replyKey = getEventKey(evt) + const repliesForThisReply = repliesMap.get(replyKey) + // If the reply is not trusted and there are no trusted replies for this reply, skip rendering + if ( + !repliesForThisReply || + repliesForThisReply.events.every((evt) => !isUserTrusted(evt.pubkey)) + ) { + return false + } + } - let parentKeys = [stuffKey] - while (parentKeys.length > 0) { - const events = parentKeys.flatMap((key) => repliesMap.get(key)?.events || []) - events.forEach((evt) => { - const key = getEventKey(evt) - if (replyKeySet.has(key)) return - if (mutePubkeySet.has(evt.pubkey)) return - if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet)) return - - replyKeySet.add(key) - replyEvents.push(evt) - }) - parentKeys = events.map((evt) => getEventKey(evt)) - } + replyKeySet.add(key) + return true + }) return replyEvents.sort((a, b) => a.created_at - b.created_at) - }, [stuffKey, repliesMap]) + }, [ + stuffKey, + repliesMap, + mutePubkeySet, + hideContentMentioningMutedUsers, + hideUntrustedInteractions + ]) const [timelineKey, setTimelineKey] = useState(undefined) const [until, setUntil] = useState(undefined) const [loading, setLoading] = useState(false) const [showCount, setShowCount] = useState(SHOW_COUNT) - const [highlightReplyKey, setHighlightReplyKey] = useState(undefined) - const replyRefs = useRef>({}) const bottomRef = useRef(null) useEffect(() => { @@ -252,26 +260,6 @@ export default function ReplyNoteList({ setLoading(false) }, [loading, until, timelineKey]) - const highlightReply = useCallback((key: string, eventId?: string, scrollTo = true) => { - let found = false - if (scrollTo) { - const ref = replyRefs.current[key] - if (ref) { - found = true - ref.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) - } - } - if (!found) { - if (eventId) push(toNote(eventId)) - return - } - - setHighlightReplyKey(key) - setTimeout(() => { - setHighlightReplyKey((pre) => (pre === key ? undefined : pre)) - }, 1500) - }, []) - return (
{loading && } @@ -285,44 +273,11 @@ export default function ReplyNoteList({ )}
{replies.slice(0, showCount).map((reply) => { - if (hideUntrustedInteractions && !isUserTrusted(reply.pubkey)) { - const replyKey = getEventKey(reply) - const repliesForThisReply = repliesMap.get(replyKey) - // If the reply is not trusted and there are no trusted replies for this reply, skip rendering - if ( - !repliesForThisReply || - repliesForThisReply.events.every((evt) => !isUserTrusted(evt.pubkey)) - ) { - return null - } - } - - const rootKey = event ? getEventKey(event) : externalContent! - const currentReplyKey = getEventKey(reply) - const parentTag = getParentTag(reply) - const parentKey = parentTag ? getKeyFromTag(parentTag.tag) : undefined - const parentEventId = parentTag - ? parentTag.type === 'e' - ? generateBech32IdFromETag(parentTag.tag) - : parentTag.type === 'a' - ? generateBech32IdFromATag(parentTag.tag) - : undefined - : undefined + const key = getEventKey(reply) return ( -
(replyRefs.current[currentReplyKey] = el)} - key={currentReplyKey} - className="scroll-mt-12" - > - { - if (!parentKey) return - highlightReply(parentKey, parentEventId) - }} - highlight={highlightReplyKey === currentReplyKey} - /> +
+ +
) })} diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index d09616e..e945fd5 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -587,6 +587,8 @@ export default { 'Relay Feeds': 'تدفقات الترحيل', 'Create Highlight': 'إنشاء تمييز', 'Write your thoughts about this highlight...': 'اكتب أفكارك حول هذا التمييز...', - 'Publish Highlight': 'نشر التمييز' + 'Publish Highlight': 'نشر التمييز', + 'Show replies': 'إظهار الردود', + 'Hide replies': 'إخفاء الردود' } } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 8d4abed..da185bf 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -604,6 +604,8 @@ export default { 'Create Highlight': 'Markierung Erstellen', 'Write your thoughts about this highlight...': 'Schreiben Sie Ihre Gedanken zu dieser Markierung...', - 'Publish Highlight': 'Markierung Veröffentlichen' + 'Publish Highlight': 'Markierung Veröffentlichen', + 'Show replies': 'Antworten anzeigen', + 'Hide replies': 'Antworten ausblenden' } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 7270639..92bd32e 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -590,6 +590,8 @@ export default { 'Relay Feeds': 'Relay Feeds', 'Create Highlight': 'Create Highlight', 'Write your thoughts about this highlight...': 'Write your thoughts about this highlight...', - 'Publish Highlight': 'Publish Highlight' + 'Publish Highlight': 'Publish Highlight', + 'Show replies': 'Show replies', + 'Hide replies': 'Hide replies' } } diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 095cb87..996b11f 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -600,6 +600,8 @@ export default { 'Create Highlight': 'Crear Resaltado', 'Write your thoughts about this highlight...': 'Escribe tus pensamientos sobre este resaltado...', - 'Publish Highlight': 'Publicar Resaltado' + 'Publish Highlight': 'Publicar Resaltado', + 'Show replies': 'Mostrar respuestas', + 'Hide replies': 'Ocultar respuestas' } } diff --git a/src/i18n/locales/fa.ts b/src/i18n/locales/fa.ts index 78a8d84..7722315 100644 --- a/src/i18n/locales/fa.ts +++ b/src/i18n/locales/fa.ts @@ -593,6 +593,8 @@ export default { 'Relay Feeds': 'فیدهای رله', 'Create Highlight': 'ایجاد برجسته‌سازی', 'Write your thoughts about this highlight...': 'نظرات خود را درباره این برجسته‌سازی بنویسید...', - 'Publish Highlight': 'انتشار برجسته‌سازی' + 'Publish Highlight': 'انتشار برجسته‌سازی', + 'Show replies': 'نمایش پاسخ‌ها', + 'Hide replies': 'پنهان کردن پاسخ‌ها' } } diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 9d5031b..ea87bb6 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -602,6 +602,8 @@ export default { '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' + 'Publish Highlight': 'Publier le Surlignage', + 'Show replies': 'Afficher les réponses', + 'Hide replies': 'Masquer les réponses' } } diff --git a/src/i18n/locales/hi.ts b/src/i18n/locales/hi.ts index a3a2933..4c05d3f 100644 --- a/src/i18n/locales/hi.ts +++ b/src/i18n/locales/hi.ts @@ -594,6 +594,8 @@ export default { 'Relay Feeds': 'रिले फ़ीड', 'Create Highlight': 'हाइलाइट बनाएं', 'Write your thoughts about this highlight...': 'इस हाइलाइट के बारे में अपने विचार लिखें...', - 'Publish Highlight': 'हाइलाइट प्रकाशित करें' + 'Publish Highlight': 'हाइलाइट प्रकाशित करें', + 'Show replies': 'जवाब दिखाएं', + 'Hide replies': 'जवाब छुपाएं' } } diff --git a/src/i18n/locales/hu.ts b/src/i18n/locales/hu.ts index b531180..f6cae2a 100644 --- a/src/i18n/locales/hu.ts +++ b/src/i18n/locales/hu.ts @@ -588,6 +588,8 @@ export default { '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' + 'Publish Highlight': 'Kiemelés Közzététele', + 'Show replies': 'Válaszok megjelenítése', + 'Hide replies': 'Válaszok elrejtése' } } diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index 84e2310..50494d5 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -599,6 +599,8 @@ export default { 'Create Highlight': 'Crea Evidenziazione', 'Write your thoughts about this highlight...': 'Scrivi i tuoi pensieri su questa evidenziazione...', - 'Publish Highlight': 'Pubblica Evidenziazione' + 'Publish Highlight': 'Pubblica Evidenziazione', + 'Show replies': 'Mostra risposte', + 'Hide replies': 'Nascondi risposte' } } diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index 8bd6419..586c36a 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -594,6 +594,8 @@ export default { 'Create Highlight': 'ハイライトを作成', 'Write your thoughts about this highlight...': 'このハイライトについての考えを書いてください...', - 'Publish Highlight': 'ハイライトを公開' + 'Publish Highlight': 'ハイライトを公開', + 'Show replies': '返信を表示', + 'Hide replies': '返信を非表示' } } diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index 9ac6fa9..d4f0ab5 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -592,6 +592,8 @@ export default { 'Relay Feeds': '릴레이 피드', 'Create Highlight': '하이라이트 만들기', 'Write your thoughts about this highlight...': '이 하이라이트에 대한 생각을 작성하세요...', - 'Publish Highlight': '하이라이트 게시' + 'Publish Highlight': '하이라이트 게시', + 'Show replies': '답글 표시', + 'Hide replies': '답글 숨기기' } } diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index 575909d..1056c6a 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -600,6 +600,8 @@ export default { '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' + 'Publish Highlight': 'Opublikuj Podświetlenie', + 'Show replies': 'Pokaż odpowiedzi', + 'Hide replies': 'Ukryj odpowiedzi' } } diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index 7144e12..030d497 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -595,6 +595,8 @@ export default { 'Create Highlight': 'Criar Destaque', 'Write your thoughts about this highlight...': 'Escreva seus pensamentos sobre este destaque...', - 'Publish Highlight': 'Publicar Destaque' + 'Publish Highlight': 'Publicar Destaque', + 'Show replies': 'Mostrar respostas', + 'Hide replies': 'Ocultar respostas' } } diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index aacfb04..902eed4 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -598,6 +598,8 @@ export default { 'Create Highlight': 'Criar Destaque', 'Write your thoughts about this highlight...': 'Escreva os seus pensamentos sobre este destaque...', - 'Publish Highlight': 'Publicar Destaque' + 'Publish Highlight': 'Publicar Destaque', + 'Show replies': 'Mostrar respostas', + 'Hide replies': 'Ocultar respostas' } } diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index b2f8cc7..022d13c 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -599,6 +599,8 @@ export default { 'Relay Feeds': 'Ленты Релеев', 'Create Highlight': 'Создать Выделение', 'Write your thoughts about this highlight...': 'Напишите свои мысли об этом выделении...', - 'Publish Highlight': 'Опубликовать Выделение' + 'Publish Highlight': 'Опубликовать Выделение', + 'Show replies': 'Показать ответы', + 'Hide replies': 'Скрыть ответы' } } diff --git a/src/i18n/locales/th.ts b/src/i18n/locales/th.ts index 2a50c0d..71db2df 100644 --- a/src/i18n/locales/th.ts +++ b/src/i18n/locales/th.ts @@ -586,6 +586,8 @@ export default { 'Relay Feeds': 'ฟีดรีเลย์', 'Create Highlight': 'สร้างไฮไลท์', 'Write your thoughts about this highlight...': 'เขียนความคิดของคุณเกี่ยวกับไฮไลท์นี้...', - 'Publish Highlight': 'เผยแพร่ไฮไลท์' + 'Publish Highlight': 'เผยแพร่ไฮไลท์', + 'Show replies': 'แสดงการตอบกลับ', + 'Hide replies': 'ซ่อนการตอบกลับ' } } diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index 9359112..a62d05e 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -579,6 +579,8 @@ export default { 'Relay Feeds': '中继订阅', 'Create Highlight': '创建高亮', 'Write your thoughts about this highlight...': '写下你对这段高亮的想法...', - 'Publish Highlight': '发布高亮' + 'Publish Highlight': '发布高亮', + 'Show replies': '显示回复', + 'Hide replies': '隐藏回复' } }