From d0a510261e87f1b34c04af19f3809ad867a0ff2c Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Mon, 1 Jan 2024 15:35:38 -0600 Subject: [PATCH] rebuild event tag reference helpers DMs: refocus input after sending --- .../debug-modals/note-debug-modal.tsx | 5 +- .../event-types/embedded-torrent-comment.tsx | 6 +- src/components/magic-textarea.tsx | 66 ++++--- src/components/note-link.tsx | 8 +- src/components/note/index.tsx | 68 +++++-- src/helpers/nip19.ts | 6 - src/helpers/nostr/events.ts | 167 +++++++++--------- src/helpers/nostr/post.ts | 14 +- src/helpers/notification.ts | 3 +- src/helpers/thread.ts | 20 +-- src/hooks/use-thread-timeline-loader.ts | 16 +- src/types/nostr-event.ts | 2 +- .../channels/components/send-message-form.tsx | 11 +- .../dms/components/send-message-form.tsx | 11 +- src/views/dms/index.tsx | 2 +- src/views/notifications/notification-item.tsx | 4 +- src/views/thread/index.tsx | 19 +- .../torrents/components/torrent-table-row.tsx | 4 +- 18 files changed, 251 insertions(+), 181 deletions(-) diff --git a/src/components/debug-modals/note-debug-modal.tsx b/src/components/debug-modals/note-debug-modal.tsx index 6a2baff1d..38c13e24b 100644 --- a/src/components/debug-modals/note-debug-modal.tsx +++ b/src/components/debug-modals/note-debug-modal.tsx @@ -2,7 +2,7 @@ import { Modal, ModalOverlay, ModalContent, ModalBody, ModalCloseButton, Flex } import { ModalProps } from "@chakra-ui/react"; import { nip19 } from "nostr-tools"; -import { getReferences } from "../../helpers/nostr/events"; +import { getContentTagRefs, getReferences } from "../../helpers/nostr/events"; import { NostrEvent } from "../../types/nostr-event"; import RawJson from "./raw-json"; import RawValue from "./raw-value"; @@ -22,7 +22,8 @@ export default function NoteDebugModal({ event, ...props }: { event: NostrEvent - + + diff --git a/src/components/embed-event/event-types/embedded-torrent-comment.tsx b/src/components/embed-event/event-types/embedded-torrent-comment.tsx index b0775d9e3..f0d7032cc 100644 --- a/src/components/embed-event/event-types/embedded-torrent-comment.tsx +++ b/src/components/embed-event/event-types/embedded-torrent-comment.tsx @@ -9,7 +9,6 @@ import appSettings from "../../../services/settings/app-settings"; import EventVerificationIcon from "../../event-verification-icon"; import { TrustProvider } from "../../../providers/local/trust"; import Timestamp from "../../timestamp"; -import { getNeventForEventId } from "../../../helpers/nip19"; import { CompactNoteContent } from "../../compact-note-content"; import HoverLinkOverlay from "../../hover-link-overlay"; import { getReferences } from "../../../helpers/nostr/events"; @@ -17,6 +16,7 @@ import useSingleEvent from "../../../hooks/use-single-event"; import { getTorrentTitle } from "../../../helpers/nostr/torrents"; import { useNavigateInDrawer } from "../../../providers/drawer-sub-view-provider"; import { MouseEventHandler, useCallback } from "react"; +import { nip19 } from "nostr-tools"; export default function EmbeddedTorrentComment({ comment, @@ -25,8 +25,8 @@ export default function EmbeddedTorrentComment({ const navigate = useNavigateInDrawer(); const { showSignatureVerification } = useSubject(appSettings); const refs = getReferences(comment); - const torrent = useSingleEvent(refs.rootId, refs.rootRelay ? [refs.rootRelay] : []); - const linkToTorrent = refs.rootId && `/torrents/${getNeventForEventId(refs.rootId)}`; + const torrent = useSingleEvent(refs.root?.e?.id, refs.root?.e?.relays); + const linkToTorrent = refs.root?.e && `/torrents/${nip19.neventEncode(refs.root.e)}`; const handleClick = useCallback( (e) => { diff --git a/src/components/magic-textarea.tsx b/src/components/magic-textarea.tsx index b06ceacfc..21efbd40a 100644 --- a/src/components/magic-textarea.tsx +++ b/src/components/magic-textarea.tsx @@ -1,4 +1,4 @@ -import React, { LegacyRef } from "react"; +import React, { LegacyRef, forwardRef } from "react"; import { Image, InputProps, Textarea, TextareaProps, Input } from "@chakra-ui/react"; import ReactTextareaAutocomplete, { ItemComponentProps, @@ -85,34 +85,42 @@ function useAutocompleteTriggers() { // @ts-ignore export type RefType = ReactTextareaAutocomplete; -export function MagicInput({ instanceRef, ...props }: InputProps & { instanceRef?: LegacyRef }) { - const triggers = useAutocompleteTriggers(); +const MagicInput = forwardRef }>( + ({ instanceRef, ...props }, ref) => { + const triggers = useAutocompleteTriggers(); - return ( - // @ts-ignore - - {...props} - textAreaComponent={Input} - ref={instanceRef} - loadingComponent={Loading} - minChar={0} - trigger={triggers} - /> - ); -} + return ( + // @ts-ignore + + {...props} + textAreaComponent={Input} + ref={instanceRef} + loadingComponent={Loading} + minChar={0} + trigger={triggers} + innerRef={ref && (typeof ref === "function" ? ref : (el) => (ref.current = el))} + /> + ); + }, +); -export default function MagicTextArea({ instanceRef, ...props }: TextareaProps & { instanceRef?: LegacyRef }) { - const triggers = useAutocompleteTriggers(); +const MagicTextArea = forwardRef }>( + ({ instanceRef, ...props }, ref) => { + const triggers = useAutocompleteTriggers(); - return ( - // @ts-ignore - - {...props} - ref={instanceRef} - textAreaComponent={Textarea} - loadingComponent={Loading} - minChar={0} - trigger={triggers} - /> - ); -} + return ( + // @ts-ignore + + {...props} + ref={instanceRef} + textAreaComponent={Textarea} + loadingComponent={Loading} + minChar={0} + trigger={triggers} + innerRef={ref && (typeof ref === "function" ? ref : (el) => (ref.current = el))} + /> + ); + }, +); + +export { MagicInput, MagicTextArea as default }; diff --git a/src/components/note-link.tsx b/src/components/note-link.tsx index 3da4fdf89..dfc1e42d0 100644 --- a/src/components/note-link.tsx +++ b/src/components/note-link.tsx @@ -3,14 +3,18 @@ import { Link, LinkProps } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; import { truncatedId } from "../helpers/nostr/events"; -import { getNeventForEventId } from "../helpers/nip19"; +import relayHintService from "../services/event-relay-hint"; +import { nip19 } from "nostr-tools"; export type NoteLinkProps = LinkProps & { noteId: string; }; export const NoteLink = ({ children, noteId, color = "blue.500", ...props }: NoteLinkProps) => { - const nevent = useMemo(() => getNeventForEventId(noteId), [noteId]); + const nevent = useMemo(() => { + const relays = relayHintService.getEventPointerRelayHints(noteId).slice(0, 2); + return nip19.neventEncode({ id: noteId, relays }); + }, [noteId]); return ( diff --git a/src/components/note/index.tsx b/src/components/note/index.tsx index 708b53e23..21e2dc29b 100644 --- a/src/components/note/index.tsx +++ b/src/components/note/index.tsx @@ -36,7 +36,7 @@ import BookmarkButton from "./components/bookmark-button"; import useCurrentAccount from "../../hooks/use-current-account"; import NoteReactions from "./components/note-reactions"; import ReplyForm from "../../views/thread/components/reply-form"; -import { getReferences } from "../../helpers/nostr/events"; +import { getReferences, truncatedId } from "../../helpers/nostr/events"; import Timestamp from "../timestamp"; import OpenInDrawerButton from "../open-in-drawer-button"; import { getSharableEventAddress } from "../../helpers/nip19"; @@ -49,6 +49,57 @@ import NoteProxyLink from "./components/note-proxy-link"; import { NoteDetailsButton } from "./components/note-details-button"; import EventInteractionDetailsModal from "../event-interactions-modal"; import singleEventService from "../../services/single-event"; +import { AddressPointer, EventPointer } from "nostr-tools/lib/types/nip19"; +import { nip19 } from "nostr-tools"; + +function ReplyToE({ pointer }: { pointer: EventPointer }) { + const event = useSingleEvent(pointer.id, pointer.relays); + + if (!event) { + const nevent = nip19.neventEncode(pointer); + return ( + + Replying to{" "} + + {truncatedId(nevent)} + + + ); + } + + return ( + <> + + Replying to + + + + ); +} +function ReplyToA({ pointer }: { pointer: AddressPointer }) { + const naddr = nip19.naddrEncode(pointer); + + return ( + + Replying to{" "} + + {truncatedId(naddr)} + + + ); +} + +function ReplyLine({ event }: { event: NostrEvent }) { + const refs = getReferences(event); + if (!refs.reply) return null; + + return ( + + + {refs.reply.type === "nevent" ? : } + + ); +} export type NoteProps = Omit & { event: NostrEvent; @@ -79,9 +130,6 @@ export const Note = React.memo( const ref = useRef(null); useRegisterIntersectionEntity(ref, event.id); - const refs = getReferences(event); - const repliedTo = useSingleEvent(refs.replyId); - const showReactionsOnNewLine = useBreakpointValue({ base: true, lg: false }); const reactionButtons = showReactions && ; @@ -123,15 +171,7 @@ export const Note = React.memo( - {showReplyLine && repliedTo && ( - - - - Replying to - - - - )} + {showReplyLine && } @@ -160,7 +200,7 @@ export const Note = React.memo( {replyForm.isOpen && ( - + )} {detailsModal.isOpen && } diff --git a/src/helpers/nip19.ts b/src/helpers/nip19.ts index 272e459db..4a5ca44b7 100644 --- a/src/helpers/nip19.ts +++ b/src/helpers/nip19.ts @@ -52,12 +52,6 @@ export function getSharableEventAddress(event: NostrEvent) { } } -/** @deprecated use getSharableEventAddress unless required */ -export function getNeventForEventId(eventId: string, maxRelays = 2) { - const relays = relayHintService.getEventPointerRelayHints(eventId).slice(0, maxRelays); - return nip19.neventEncode({ id: eventId, relays }); -} - export function encodePointer(pointer: DecodeResult) { switch (pointer.type) { case "naddr": diff --git a/src/helpers/nostr/events.ts b/src/helpers/nostr/events.ts index f034fb710..2e43a2136 100644 --- a/src/helpers/nostr/events.ts +++ b/src/helpers/nostr/events.ts @@ -1,11 +1,12 @@ import { Kind, nip19, validateEvent } from "nostr-tools"; -import { ATag, DraftNostrEvent, isDTag, isETag, NostrEvent, RTag, Tag } from "../../types/nostr-event"; +import { ATag, DraftNostrEvent, ETag, isATag, isDTag, isETag, NostrEvent, RTag, Tag } from "../../types/nostr-event"; import { RelayConfig, RelayMode } from "../../classes/relay"; import { getMatchNostrLink } from "../regexp"; -import { AddressPointer } from "nostr-tools/lib/types/nip19"; +import { AddressPointer, EventPointer } from "nostr-tools/lib/types/nip19"; import { safeJson } from "../parse"; import { COMMUNITY_DEFINITION_KIND } from "./communities"; +import { safeDecode } from "../nip19"; export function truncatedId(str: string, keep = 6) { if (str.length < keep * 2 + 3) return str; @@ -28,7 +29,7 @@ export function getEventUID(event: NostrEvent) { export function isReply(event: NostrEvent | DraftNostrEvent) { if (event.kind === Kind.Repost) return false; // TODO: update this to only look for a "root" or "reply" tag - return !!getReferences(event).replyId; + return !!getReferences(event).reply; } export function isMentionedInContent(event: NostrEvent | DraftNostrEvent, pubkey: string) { return filterTagsByContentRefs(event.content, event.tags).some((t) => t[1] === pubkey); @@ -46,116 +47,120 @@ export function isRepost(event: NostrEvent | DraftNostrEvent) { * either with the legacy #[0] syntax or nostr:xxxxx links */ export function getContentTagRefs(content: string, tags: Tag[]) { - const indexes = new Set(); - Array.from(content.matchAll(/#\[(\d+)\]/gi)).forEach((m) => indexes.add(parseInt(m[1]))); + const foundTags = new Set(); const linkMatches = Array.from(content.matchAll(getMatchNostrLink())); for (const [_, _prefix, link] of linkMatches) { - try { - const decoded = nip19.decode(link); + const decoded = safeDecode(link); + if (!decoded) continue; - let type: string; - let id: string; - switch (decoded.type) { - case "npub": - id = decoded.data; - type = "p"; - break; - case "nprofile": - id = decoded.data.pubkey; - type = "p"; - break; - case "note": - id = decoded.data; - type = "e"; - break; - case "nevent": - id = decoded.data.id; - type = "e"; - break; - } + let type: string; + let id: string; + switch (decoded.type) { + case "npub": + id = decoded.data; + type = "p"; + break; + case "nprofile": + id = decoded.data.pubkey; + type = "p"; + break; + case "note": + id = decoded.data; + type = "e"; + break; + case "nevent": + id = decoded.data.id; + type = "e"; + break; + } - let t = tags.find((t) => t[0] === type && t[1] === id); - if (t) { - let index = tags.indexOf(t); - indexes.add(index); - } - } catch (e) {} + let matchingTags = tags.filter((t) => t[0] === type && t[1] === id); + for (const t of matchingTags) foundTags.add(t); } - return Array.from(indexes); + return Array.from(foundTags); } +/** + * returns all tags that are referenced in the content + */ export function filterTagsByContentRefs(content: string, tags: Tag[], referenced = true) { const contentTagRefs = getContentTagRefs(content, tags); - - const newTags: Tag[] = []; - for (let i = 0; i < tags.length; i++) { - if (contentTagRefs.includes(i) === referenced) { - newTags.push(tags[i]); - } - } - return newTags; + return tags.filter((t) => contentTagRefs.includes(t) === referenced); } -function isCommunityRefTag(t: Tag): t is ATag { - return t.length >= 2 && t[0] === "a" && t[1].startsWith(COMMUNITY_DEFINITION_KIND + ":"); +function eTagToEventPointer(tag: ETag): EventPointer { + return { id: tag[1], relays: tag[2] ? [tag[2]] : [] }; +} +function aTagToAddressPointer(tag: ATag): AddressPointer { + const cord = parseCoordinate(tag[1], true, false); + if (tag[2]) cord.relays = [tag[2]]; + return cord; } -export type EventReferences = ReturnType; -export function getReferences(event: NostrEvent | DraftNostrEvent) { - const contentTagRefs = getContentTagRefs(event.content, event.tags); +export function interpretTags(event: NostrEvent | DraftNostrEvent) { + const eTags = event.tags.filter(isETag); + const aTags = event.tags.filter(isATag); // find the root and reply tags. - // NOTE: Ignore community reference tags since there is another client out there that is marking them as "root" - // and it dose not make sense to "reply" to a community - const replyTag = event.tags.find((t) => !isCommunityRefTag(t) && t[3] === "reply"); - const rootTag = event.tags.find((t) => !isCommunityRefTag(t) && t[3] === "root"); - const mentionTags = event.tags.find((t) => t[3] === "mention"); + let rootETag = eTags.find((t) => t[3] === "root"); + let replyETag = eTags.find((t) => t[3] === "reply"); - let replyId = replyTag?.[1]; - let replyRelay = replyTag?.[2]; - let rootId = rootTag?.[1]; - let rootRelay = rootTag?.[2]; + let rootATag = aTags.find((t) => t[3] === "root"); + let replyATag = aTags.find((t) => t[3] === "reply"); - if (!rootId || !replyId) { + if (!rootETag || !replyETag) { // a direct reply dose not need a "reply" reference // https://github.com/nostr-protocol/nips/blob/master/10.md // this is not necessarily to spec. but if there is only one id (root or reply) then assign it to both // this handles the cases where a client only set a "reply" tag and no root - rootId = replyId = rootId || replyId; + rootETag = replyETag = rootETag || replyETag; + } + if (!rootATag || !replyATag) { + rootATag = replyATag = rootATag || replyATag; } - // legacy behavior - // https://github.com/nostr-protocol/nips/blob/master/10.md#positional-e-tags-deprecated - const legacyTags = event.tags.filter(isETag).filter((t, i) => { - // ignore it if there is a type - if (t[3]) return false; - const tagIndex = event.tags.indexOf(t); - if (contentTagRefs.includes(tagIndex)) return false; - return true; - }); - if (!rootId && !replyId && legacyTags.length >= 1) { - // console.info(`Using legacy threading behavior for ${event.id}`, event); + if (!rootETag && !replyETag) { + const contentTagRefs = getContentTagRefs(event.content, eTags); - // first tag is the root - rootId = legacyTags[0][1]; - // last tag is reply - replyId = legacyTags[legacyTags.length - 1][1] ?? rootId; + // legacy behavior + // https://github.com/nostr-protocol/nips/blob/master/10.md#positional-e-tags-deprecated + const legacyETags = eTags.filter((t) => { + // ignore it if there is a type + if (t[3]) return false; + if (contentTagRefs.includes(t)) return false; + return true; + }); + + if (legacyETags.length >= 1) { + // first tag is the root + rootETag = legacyETags[0]; + // last tag is reply + replyETag = legacyETags[legacyETags.length - 1] ?? rootETag; + } } return { - replyTag, - rootTag, - mentionTags, + root: rootETag || rootATag ? { e: rootETag, a: rootATag } : undefined, + reply: replyETag || replyATag ? { e: replyETag, a: replyATag } : undefined, + }; +} - rootId, - rootRelay, - replyId, - replyRelay, +export type EventReferences = ReturnType; +export function getReferences(event: NostrEvent | DraftNostrEvent) { + const tags = interpretTags(event); - contentTagRefs, + return { + root: tags.root && { + e: tags.root.e && eTagToEventPointer(tags.root.e), + a: tags.root.a && aTagToAddressPointer(tags.root.a), + }, + reply: tags.reply && { + e: tags.reply.e && eTagToEventPointer(tags.reply.e), + a: tags.reply.a && aTagToAddressPointer(tags.reply.a), + }, }; } diff --git a/src/helpers/nostr/post.ts b/src/helpers/nostr/post.ts index 7039fd9df..b99e9d2df 100644 --- a/src/helpers/nostr/post.ts +++ b/src/helpers/nostr/post.ts @@ -19,8 +19,8 @@ function addTag(tags: Tag[], tag: Tag, overwrite = false) { } return [...tags, tag]; } -function AddEtag(tags: Tag[], eventId: string, type?: string, overwrite = false) { - const hint = relayHintService.getEventPointerRelayHint(eventId) ?? ""; +function AddEtag(tags: Tag[], eventId: string, relayHint?: string, type?: string, overwrite = false) { + const hint = relayHint || relayHintService.getEventPointerRelayHint(eventId) || ""; const tag = type ? ["e", eventId, hint, type] : ["e", eventId, hint]; @@ -39,13 +39,15 @@ function AddEtag(tags: Tag[], eventId: string, type?: string, overwrite = false) /** adds the "root" and "reply" E tags */ export function addReplyTags(draft: DraftNostrEvent, replyTo: NostrEvent) { const updated: DraftNostrEvent = { ...draft, tags: Array.from(draft.tags) }; + const refs = getReferences(replyTo); - - const rootId = refs.rootId ?? replyTo.id; + const rootId = refs.root?.e?.id ?? replyTo.id; + const rootRelayHint = refs.root?.e?.relays?.[0]; const replyId = replyTo.id; + const replyRelayHint = relayHintService.getEventPointerRelayHint(replyId); - updated.tags = AddEtag(updated.tags, rootId, "root", true); - updated.tags = AddEtag(updated.tags, replyId, "reply", true); + updated.tags = AddEtag(updated.tags, rootId, rootRelayHint, "root", true); + updated.tags = AddEtag(updated.tags, replyId, replyRelayHint, "reply", true); return updated; } diff --git a/src/helpers/notification.ts b/src/helpers/notification.ts index 3eb3bd678..6472fc269 100644 --- a/src/helpers/notification.ts +++ b/src/helpers/notification.ts @@ -1,4 +1,5 @@ import dayjs from "dayjs"; + import SuperMap from "../classes/super-map"; import { NostrEvent } from "../types/nostr-event"; import { getReferences, sortByDate } from "./nostr/events"; @@ -17,7 +18,7 @@ export function groupByRoot(events: NostrEvent[]) { const grouped = new SuperMap(() => []); for (const event of events) { const refs = getReferences(event); - if (refs.rootId) grouped.get(refs.rootId).push(event); + if (refs.root?.e?.id) grouped.get(refs.root.e.id).push(event); } for (const [_, groupedEvents] of grouped) { groupedEvents.sort(sortByDate); diff --git a/src/helpers/thread.ts b/src/helpers/thread.ts index 888dcab7c..c2ca10212 100644 --- a/src/helpers/thread.ts +++ b/src/helpers/thread.ts @@ -11,7 +11,7 @@ export type ThreadItem = { /** the thread root, according to this event */ root?: ThreadItem; /** the parent event this is replying to */ - reply?: ThreadItem; + replyingTo?: ThreadItem; /** refs from nostr event */ refs: EventReferences; /** direct child replies */ @@ -22,11 +22,11 @@ export type ThreadItem = { export function getThreadMembers(item: ThreadItem, omit?: string) { const pubkeys = new Set(); - let i = item; + let next = item; while (true) { - if (i.event.pubkey !== omit) pubkeys.add(i.event.pubkey); - if (!i.reply) break; - else i = i.reply; + if (next.event.pubkey !== omit) pubkeys.add(next.event.pubkey); + if (!next.replyingTo) break; + else next = next.replyingTo; } return Array.from(pubkeys); @@ -39,9 +39,9 @@ export function buildThread(events: NostrEvent[]) { for (const event of events) { const refs = getReferences(event); - if (refs.replyId) { - idToChildren[refs.replyId] = idToChildren[refs.replyId] || []; - idToChildren[refs.replyId].push(event); + if (refs.reply?.type === "nevent") { + idToChildren[refs.reply.data.id] = idToChildren[refs.reply.data.id] || []; + idToChildren[refs.reply.data.id].push(event); } replies.set(event.id, { @@ -52,9 +52,9 @@ export function buildThread(events: NostrEvent[]) { } for (const [id, reply] of replies) { - reply.root = reply.refs.rootId ? replies.get(reply.refs.rootId) : undefined; + reply.root = reply.refs.root?.type === "nevent" ? replies.get(reply.refs.root.data.id) : undefined; - reply.reply = reply.refs.replyId ? replies.get(reply.refs.replyId) : undefined; + reply.replyingTo = reply.refs.reply?.type === "nevent" ? replies.get(reply.refs.reply.data.id) : undefined; reply.replies = idToChildren[id]?.map((e) => replies.get(e.id) as ThreadItem) ?? []; diff --git a/src/hooks/use-thread-timeline-loader.ts b/src/hooks/use-thread-timeline-loader.ts index 3ad6fd6d8..10c8b445d 100644 --- a/src/hooks/use-thread-timeline-loader.ts +++ b/src/hooks/use-thread-timeline-loader.ts @@ -7,6 +7,7 @@ import singleEventService from "../services/single-event"; import useTimelineLoader from "./use-timeline-loader"; import { getReferences } from "../helpers/nostr/events"; import { NostrEvent } from "../types/nostr-event"; +import { unique } from "../helpers/array"; export default function useThreadTimelineLoader( focusedEvent: NostrEvent | undefined, @@ -14,12 +15,14 @@ export default function useThreadTimelineLoader( kind: number = Kind.Text, ) { const refs = focusedEvent && getReferences(focusedEvent); - const rootId = refs ? refs.rootId || focusedEvent.id : undefined; + const rootId = refs?.root?.e?.id || focusedEvent?.id; + + const readRelays = unique([...relays, ...(refs?.root?.e?.relays ?? [])]); const timelineId = `${rootId}-replies`; const timeline = useTimelineLoader( timelineId, - relays, + readRelays, rootId ? { "#e": [rootId], @@ -35,10 +38,13 @@ export default function useThreadTimelineLoader( for (const e of events) singleEventService.handleEvent(e); }, [events]); - const rootEvent = useSingleEvent(rootId, refs?.rootRelay ? [refs.rootRelay] : []); + const rootEvent = useSingleEvent(refs?.root?.e?.id, refs?.root?.e?.relays); const allEvents = useMemo(() => { - return rootEvent ? [...events, rootEvent] : events; - }, [events, rootEvent]); + const arr = Array.from(events); + if (focusedEvent) arr.push(focusedEvent); + if (rootEvent && focusedEvent && rootEvent.id !== focusedEvent.id) arr.push(rootEvent); + return arr; + }, [events, rootEvent, focusedEvent]); return { events: allEvents, rootEvent, rootId, timeline }; } diff --git a/src/types/nostr-event.ts b/src/types/nostr-event.ts index f380bab19..7f31c53cb 100644 --- a/src/types/nostr-event.ts +++ b/src/types/nostr-event.ts @@ -1,5 +1,5 @@ export type ETag = ["e", string] | ["e", string, string] | ["e", string, string, string]; -export type ATag = ["a", string] | ["a", string, string]; +export type ATag = ["a", string] | ["a", string, string] | ["e", string, string, string]; export type PTag = ["p", string] | ["p", string, string] | ["p", string, string, string]; export type RTag = ["r", string] | ["r", string, string]; export type DTag = ["d"] | ["d", string]; diff --git a/src/views/channels/components/send-message-form.tsx b/src/views/channels/components/send-message-form.tsx index a154e5893..f20d45746 100644 --- a/src/views/channels/components/send-message-form.tsx +++ b/src/views/channels/components/send-message-form.tsx @@ -31,8 +31,9 @@ export default function ChannelMessageForm({ }); watch("content"); - const textAreaRef = useRef(null); - const { onPaste } = useTextAreaUploadFileWithForm(textAreaRef, getValues, setValue); + const componentRef = useRef(null); + const textAreaRef = useRef(null); + const { onPaste } = useTextAreaUploadFileWithForm(componentRef, getValues, setValue); const sendMessage = handleSubmit(async (values) => { try { @@ -58,6 +59,9 @@ export default function ChannelMessageForm({ const writeRelays = clientRelaysService.getWriteUrls(); new NostrPublishAction("Send DM", writeRelays, signed); reset(); + + // refocus input + setTimeout(() => textAreaRef.current?.focus(), 50); } catch (e) { if (e instanceof Error) toast({ status: "error", description: e.message }); } @@ -80,7 +84,8 @@ export default function ChannelMessageForm({ onChange={(e) => setValue("content", e.target.value, { shouldDirty: true })} rows={2} isRequired - instanceRef={(inst) => (textAreaRef.current = inst)} + instanceRef={(inst) => (componentRef.current = inst)} + ref={textAreaRef} onPaste={onPaste} onKeyDown={(e) => { if (e.ctrlKey && e.key === "Enter" && formRef.current) formRef.current.requestSubmit(); diff --git a/src/views/dms/components/send-message-form.tsx b/src/views/dms/components/send-message-form.tsx index 14583d2bb..f9de13fe9 100644 --- a/src/views/dms/components/send-message-form.tsx +++ b/src/views/dms/components/send-message-form.tsx @@ -33,8 +33,9 @@ export default function SendMessageForm({ }); watch("content"); - const textAreaRef = useRef(null); - const { onPaste } = useTextAreaUploadFileWithForm(textAreaRef, getValues, setValue); + const autocompleteRef = useRef(null); + const textAreaRef = useRef(null); + const { onPaste } = useTextAreaUploadFileWithForm(autocompleteRef, getValues, setValue); const usersInbox = useUserRelays(pubkey) .filter((r) => r.mode & RelayMode.READ) @@ -65,6 +66,9 @@ export default function SendMessageForm({ // add plaintext to decryption context getOrCreateContainer(pubkey, encrypted).plaintext.next(values.content); + + // refocus input + setTimeout(() => textAreaRef.current?.focus(), 50); } catch (e) { if (e instanceof Error) toast({ status: "error", description: e.message }); } @@ -87,7 +91,8 @@ export default function SendMessageForm({ onChange={(e) => setValue("content", e.target.value, { shouldDirty: true })} rows={2} isRequired - instanceRef={(inst) => (textAreaRef.current = inst)} + instanceRef={(inst) => (autocompleteRef.current = inst)} + ref={textAreaRef} onPaste={onPaste} onKeyDown={(e) => { if (e.ctrlKey && e.key === "Enter" && formRef.current) formRef.current.requestSubmit(); diff --git a/src/views/dms/index.tsx b/src/views/dms/index.tsx index 299041003..ef0bd9857 100644 --- a/src/views/dms/index.tsx +++ b/src/views/dms/index.tsx @@ -54,7 +54,7 @@ function ConversationCard({ conversation }: { conversation: KnownConversation }) {hasResponded(conversation) && } - {lastReceived === lastMessage && } + {lastReceived && } diff --git a/src/views/notifications/notification-item.tsx b/src/views/notifications/notification-item.tsx index 0067b1df1..cb339217e 100644 --- a/src/views/notifications/notification-item.tsx +++ b/src/views/notifications/notification-item.tsx @@ -34,9 +34,9 @@ export const ExpandableToggleButton = ({ const NoteNotification = forwardRef(({ event }, ref) => { const account = useCurrentAccount()!; const refs = getReferences(event); - const parent = useSingleEvent(refs.replyId); + const parent = useSingleEvent(refs.reply?.e?.id); - const isReplyingToMe = !!refs.replyId && (parent ? parent.pubkey === account.pubkey : true); + const isReplyingToMe = !!refs.reply?.e?.id && (parent ? parent.pubkey === account.pubkey : true); const isMentioned = isMentionedInContent(event, account.pubkey); if (isReplyingToMe) return ; diff --git a/src/views/thread/index.tsx b/src/views/thread/index.tsx index 765692724..894fceee7 100644 --- a/src/views/thread/index.tsx +++ b/src/views/thread/index.tsx @@ -1,10 +1,9 @@ import { useMemo } from "react"; import { Button, Heading, Spinner } from "@chakra-ui/react"; -import { nip19 } from "nostr-tools"; -import { useParams, Link as RouterLink } from "react-router-dom"; +import { Link as RouterLink } from "react-router-dom"; import Note from "../../components/note"; -import { getSharableEventAddress, isHexKey } from "../../helpers/nip19"; +import { getSharableEventAddress } from "../../helpers/nip19"; import { ThreadPost } from "./components/thread-post"; import VerticalPageLayout from "../../components/vertical-page-layout"; import { useReadRelayUrls } from "../../hooks/use-client-relays"; @@ -27,11 +26,11 @@ function ThreadPage({ thread, rootId, focusId }: { thread: Map )} - {focusedPost.reply && ( + {focusedPost.replyingTo && ( diff --git a/src/views/torrents/components/torrent-table-row.tsx b/src/views/torrents/components/torrent-table-row.tsx index 11644058e..908373483 100644 --- a/src/views/torrents/components/torrent-table-row.tsx +++ b/src/views/torrents/components/torrent-table-row.tsx @@ -7,7 +7,7 @@ import { NostrEvent } from "../../../types/nostr-event"; import Timestamp from "../../../components/timestamp"; import UserLink from "../../../components/user-link"; import Magnet from "../../../components/icons/magnet"; -import { getNeventForEventId } from "../../../helpers/nip19"; +import { getSharableEventAddress } from "../../../helpers/nip19"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; import { getEventUID } from "../../../helpers/nostr/events"; import { formatBytes } from "../../../helpers/number"; @@ -58,7 +58,7 @@ function TorrentTableRow({ torrent }: { torrent: NostrEvent }) { ))} - + {getTorrentTitle(torrent)}