mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-10-11 05:13:21 +02:00
rebuild event tag reference helpers
DMs: refocus input after sending
This commit is contained in:
@@ -2,7 +2,7 @@ import { Modal, ModalOverlay, ModalContent, ModalBody, ModalCloseButton, Flex }
|
|||||||
import { ModalProps } from "@chakra-ui/react";
|
import { ModalProps } from "@chakra-ui/react";
|
||||||
import { nip19 } from "nostr-tools";
|
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 { NostrEvent } from "../../types/nostr-event";
|
||||||
import RawJson from "./raw-json";
|
import RawJson from "./raw-json";
|
||||||
import RawValue from "./raw-value";
|
import RawValue from "./raw-value";
|
||||||
@@ -22,7 +22,8 @@ export default function NoteDebugModal({ event, ...props }: { event: NostrEvent
|
|||||||
<RawValue heading="NIP-19 Pointer" value={getSharableEventAddress(event)} />
|
<RawValue heading="NIP-19 Pointer" value={getSharableEventAddress(event)} />
|
||||||
<RawPre heading="Content" value={event.content} />
|
<RawPre heading="Content" value={event.content} />
|
||||||
<RawJson heading="JSON" json={event} />
|
<RawJson heading="JSON" json={event} />
|
||||||
<RawJson heading="References" json={getReferences(event)} />
|
<RawJson heading="Thread Tags" json={getReferences(event)} />
|
||||||
|
<RawJson heading="Tags referenced in content" json={getContentTagRefs(event.content, event.tags)} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
@@ -9,7 +9,6 @@ import appSettings from "../../../services/settings/app-settings";
|
|||||||
import EventVerificationIcon from "../../event-verification-icon";
|
import EventVerificationIcon from "../../event-verification-icon";
|
||||||
import { TrustProvider } from "../../../providers/local/trust";
|
import { TrustProvider } from "../../../providers/local/trust";
|
||||||
import Timestamp from "../../timestamp";
|
import Timestamp from "../../timestamp";
|
||||||
import { getNeventForEventId } from "../../../helpers/nip19";
|
|
||||||
import { CompactNoteContent } from "../../compact-note-content";
|
import { CompactNoteContent } from "../../compact-note-content";
|
||||||
import HoverLinkOverlay from "../../hover-link-overlay";
|
import HoverLinkOverlay from "../../hover-link-overlay";
|
||||||
import { getReferences } from "../../../helpers/nostr/events";
|
import { getReferences } from "../../../helpers/nostr/events";
|
||||||
@@ -17,6 +16,7 @@ import useSingleEvent from "../../../hooks/use-single-event";
|
|||||||
import { getTorrentTitle } from "../../../helpers/nostr/torrents";
|
import { getTorrentTitle } from "../../../helpers/nostr/torrents";
|
||||||
import { useNavigateInDrawer } from "../../../providers/drawer-sub-view-provider";
|
import { useNavigateInDrawer } from "../../../providers/drawer-sub-view-provider";
|
||||||
import { MouseEventHandler, useCallback } from "react";
|
import { MouseEventHandler, useCallback } from "react";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
|
||||||
export default function EmbeddedTorrentComment({
|
export default function EmbeddedTorrentComment({
|
||||||
comment,
|
comment,
|
||||||
@@ -25,8 +25,8 @@ export default function EmbeddedTorrentComment({
|
|||||||
const navigate = useNavigateInDrawer();
|
const navigate = useNavigateInDrawer();
|
||||||
const { showSignatureVerification } = useSubject(appSettings);
|
const { showSignatureVerification } = useSubject(appSettings);
|
||||||
const refs = getReferences(comment);
|
const refs = getReferences(comment);
|
||||||
const torrent = useSingleEvent(refs.rootId, refs.rootRelay ? [refs.rootRelay] : []);
|
const torrent = useSingleEvent(refs.root?.e?.id, refs.root?.e?.relays);
|
||||||
const linkToTorrent = refs.rootId && `/torrents/${getNeventForEventId(refs.rootId)}`;
|
const linkToTorrent = refs.root?.e && `/torrents/${nip19.neventEncode(refs.root.e)}`;
|
||||||
|
|
||||||
const handleClick = useCallback<MouseEventHandler>(
|
const handleClick = useCallback<MouseEventHandler>(
|
||||||
(e) => {
|
(e) => {
|
||||||
|
@@ -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 { Image, InputProps, Textarea, TextareaProps, Input } from "@chakra-ui/react";
|
||||||
import ReactTextareaAutocomplete, {
|
import ReactTextareaAutocomplete, {
|
||||||
ItemComponentProps,
|
ItemComponentProps,
|
||||||
@@ -85,34 +85,42 @@ function useAutocompleteTriggers() {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export type RefType = ReactTextareaAutocomplete<Token, TextareaProps>;
|
export type RefType = ReactTextareaAutocomplete<Token, TextareaProps>;
|
||||||
|
|
||||||
export function MagicInput({ instanceRef, ...props }: InputProps & { instanceRef?: LegacyRef<RefType> }) {
|
const MagicInput = forwardRef<HTMLInputElement, InputProps & { instanceRef?: LegacyRef<RefType> }>(
|
||||||
const triggers = useAutocompleteTriggers();
|
({ instanceRef, ...props }, ref) => {
|
||||||
|
const triggers = useAutocompleteTriggers();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
<ReactTextareaAutocomplete<Token, InputProps>
|
<ReactTextareaAutocomplete<Token, InputProps>
|
||||||
{...props}
|
{...props}
|
||||||
textAreaComponent={Input}
|
textAreaComponent={Input}
|
||||||
ref={instanceRef}
|
ref={instanceRef}
|
||||||
loadingComponent={Loading}
|
loadingComponent={Loading}
|
||||||
minChar={0}
|
minChar={0}
|
||||||
trigger={triggers}
|
trigger={triggers}
|
||||||
/>
|
innerRef={ref && (typeof ref === "function" ? ref : (el) => (ref.current = el))}
|
||||||
);
|
/>
|
||||||
}
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export default function MagicTextArea({ instanceRef, ...props }: TextareaProps & { instanceRef?: LegacyRef<RefType> }) {
|
const MagicTextArea = forwardRef<HTMLTextAreaElement, TextareaProps & { instanceRef?: LegacyRef<RefType> }>(
|
||||||
const triggers = useAutocompleteTriggers();
|
({ instanceRef, ...props }, ref) => {
|
||||||
|
const triggers = useAutocompleteTriggers();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
<ReactTextareaAutocomplete<Token, TextareaProps>
|
<ReactTextareaAutocomplete<Token, TextareaProps>
|
||||||
{...props}
|
{...props}
|
||||||
ref={instanceRef}
|
ref={instanceRef}
|
||||||
textAreaComponent={Textarea}
|
textAreaComponent={Textarea}
|
||||||
loadingComponent={Loading}
|
loadingComponent={Loading}
|
||||||
minChar={0}
|
minChar={0}
|
||||||
trigger={triggers}
|
trigger={triggers}
|
||||||
/>
|
innerRef={ref && (typeof ref === "function" ? ref : (el) => (ref.current = el))}
|
||||||
);
|
/>
|
||||||
}
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export { MagicInput, MagicTextArea as default };
|
||||||
|
@@ -3,14 +3,18 @@ import { Link, LinkProps } from "@chakra-ui/react";
|
|||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
import { truncatedId } from "../helpers/nostr/events";
|
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 & {
|
export type NoteLinkProps = LinkProps & {
|
||||||
noteId: string;
|
noteId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NoteLink = ({ children, noteId, color = "blue.500", ...props }: NoteLinkProps) => {
|
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 (
|
return (
|
||||||
<Link as={RouterLink} to={`/n/${nevent}`} color={color} {...props}>
|
<Link as={RouterLink} to={`/n/${nevent}`} color={color} {...props}>
|
||||||
|
@@ -36,7 +36,7 @@ import BookmarkButton from "./components/bookmark-button";
|
|||||||
import useCurrentAccount from "../../hooks/use-current-account";
|
import useCurrentAccount from "../../hooks/use-current-account";
|
||||||
import NoteReactions from "./components/note-reactions";
|
import NoteReactions from "./components/note-reactions";
|
||||||
import ReplyForm from "../../views/thread/components/reply-form";
|
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 Timestamp from "../timestamp";
|
||||||
import OpenInDrawerButton from "../open-in-drawer-button";
|
import OpenInDrawerButton from "../open-in-drawer-button";
|
||||||
import { getSharableEventAddress } from "../../helpers/nip19";
|
import { getSharableEventAddress } from "../../helpers/nip19";
|
||||||
@@ -49,6 +49,57 @@ import NoteProxyLink from "./components/note-proxy-link";
|
|||||||
import { NoteDetailsButton } from "./components/note-details-button";
|
import { NoteDetailsButton } from "./components/note-details-button";
|
||||||
import EventInteractionDetailsModal from "../event-interactions-modal";
|
import EventInteractionDetailsModal from "../event-interactions-modal";
|
||||||
import singleEventService from "../../services/single-event";
|
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 (
|
||||||
|
<Text>
|
||||||
|
Replying to{" "}
|
||||||
|
<Link as={RouterLink} to={`/l/${nevent}`} color="blue.500">
|
||||||
|
{truncatedId(nevent)}
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Text>
|
||||||
|
Replying to <UserLink pubkey={event.pubkey} fontWeight="bold" />
|
||||||
|
</Text>
|
||||||
|
<CompactNoteContent event={event} maxLength={96} isTruncated textOnly />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function ReplyToA({ pointer }: { pointer: AddressPointer }) {
|
||||||
|
const naddr = nip19.naddrEncode(pointer);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
Replying to{" "}
|
||||||
|
<Link as={RouterLink} to={`/l/${naddr}`} color="blue.500">
|
||||||
|
{truncatedId(naddr)}
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReplyLine({ event }: { event: NostrEvent }) {
|
||||||
|
const refs = getReferences(event);
|
||||||
|
if (!refs.reply) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex gap="2" fontStyle="italic" alignItems="center" whiteSpace="nowrap">
|
||||||
|
<ReplyIcon />
|
||||||
|
{refs.reply.type === "nevent" ? <ReplyToE pointer={refs.reply.data} /> : <ReplyToA pointer={refs.reply.data} />}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export type NoteProps = Omit<CardProps, "children"> & {
|
export type NoteProps = Omit<CardProps, "children"> & {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
@@ -79,9 +130,6 @@ export const Note = React.memo(
|
|||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
useRegisterIntersectionEntity(ref, event.id);
|
useRegisterIntersectionEntity(ref, event.id);
|
||||||
|
|
||||||
const refs = getReferences(event);
|
|
||||||
const repliedTo = useSingleEvent(refs.replyId);
|
|
||||||
|
|
||||||
const showReactionsOnNewLine = useBreakpointValue({ base: true, lg: false });
|
const showReactionsOnNewLine = useBreakpointValue({ base: true, lg: false });
|
||||||
|
|
||||||
const reactionButtons = showReactions && <NoteReactions event={event} flexWrap="wrap" variant="ghost" size="sm" />;
|
const reactionButtons = showReactions && <NoteReactions event={event} flexWrap="wrap" variant="ghost" size="sm" />;
|
||||||
@@ -123,15 +171,7 @@ export const Note = React.memo(
|
|||||||
</Link>
|
</Link>
|
||||||
</Flex>
|
</Flex>
|
||||||
<NoteCommunityMetadata event={event} />
|
<NoteCommunityMetadata event={event} />
|
||||||
{showReplyLine && repliedTo && (
|
{showReplyLine && <ReplyLine event={event} />}
|
||||||
<Flex gap="2" fontStyle="italic" alignItems="center" whiteSpace="nowrap">
|
|
||||||
<ReplyIcon />
|
|
||||||
<Text>
|
|
||||||
Replying to <UserLink pubkey={repliedTo.pubkey} fontWeight="bold" />
|
|
||||||
</Text>
|
|
||||||
<CompactNoteContent event={repliedTo} maxLength={96} isTruncated textOnly />
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody p="0">
|
<CardBody p="0">
|
||||||
<NoteContentWithWarning event={event} />
|
<NoteContentWithWarning event={event} />
|
||||||
@@ -160,7 +200,7 @@ export const Note = React.memo(
|
|||||||
</Card>
|
</Card>
|
||||||
</ExpandProvider>
|
</ExpandProvider>
|
||||||
{replyForm.isOpen && (
|
{replyForm.isOpen && (
|
||||||
<ReplyForm item={{ event, replies: [], refs }} onCancel={replyForm.onClose} onSubmitted={replyForm.onClose} />
|
<ReplyForm item={{ event, replies: [], refs: getReferences(event) }} onCancel={replyForm.onClose} onSubmitted={replyForm.onClose} />
|
||||||
)}
|
)}
|
||||||
{detailsModal.isOpen && <EventInteractionDetailsModal isOpen onClose={detailsModal.onClose} event={event} />}
|
{detailsModal.isOpen && <EventInteractionDetailsModal isOpen onClose={detailsModal.onClose} event={event} />}
|
||||||
</TrustProvider>
|
</TrustProvider>
|
||||||
|
@@ -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) {
|
export function encodePointer(pointer: DecodeResult) {
|
||||||
switch (pointer.type) {
|
switch (pointer.type) {
|
||||||
case "naddr":
|
case "naddr":
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
import { Kind, nip19, validateEvent } from "nostr-tools";
|
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 { RelayConfig, RelayMode } from "../../classes/relay";
|
||||||
import { getMatchNostrLink } from "../regexp";
|
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 { safeJson } from "../parse";
|
||||||
import { COMMUNITY_DEFINITION_KIND } from "./communities";
|
import { COMMUNITY_DEFINITION_KIND } from "./communities";
|
||||||
|
import { safeDecode } from "../nip19";
|
||||||
|
|
||||||
export function truncatedId(str: string, keep = 6) {
|
export function truncatedId(str: string, keep = 6) {
|
||||||
if (str.length < keep * 2 + 3) return str;
|
if (str.length < keep * 2 + 3) return str;
|
||||||
@@ -28,7 +29,7 @@ export function getEventUID(event: NostrEvent) {
|
|||||||
export function isReply(event: NostrEvent | DraftNostrEvent) {
|
export function isReply(event: NostrEvent | DraftNostrEvent) {
|
||||||
if (event.kind === Kind.Repost) return false;
|
if (event.kind === Kind.Repost) return false;
|
||||||
// TODO: update this to only look for a "root" or "reply" tag
|
// 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) {
|
export function isMentionedInContent(event: NostrEvent | DraftNostrEvent, pubkey: string) {
|
||||||
return filterTagsByContentRefs(event.content, event.tags).some((t) => t[1] === pubkey);
|
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
|
* either with the legacy #[0] syntax or nostr:xxxxx links
|
||||||
*/
|
*/
|
||||||
export function getContentTagRefs(content: string, tags: Tag[]) {
|
export function getContentTagRefs(content: string, tags: Tag[]) {
|
||||||
const indexes = new Set();
|
const foundTags = new Set<Tag>();
|
||||||
Array.from(content.matchAll(/#\[(\d+)\]/gi)).forEach((m) => indexes.add(parseInt(m[1])));
|
|
||||||
|
|
||||||
const linkMatches = Array.from(content.matchAll(getMatchNostrLink()));
|
const linkMatches = Array.from(content.matchAll(getMatchNostrLink()));
|
||||||
for (const [_, _prefix, link] of linkMatches) {
|
for (const [_, _prefix, link] of linkMatches) {
|
||||||
try {
|
const decoded = safeDecode(link);
|
||||||
const decoded = nip19.decode(link);
|
if (!decoded) continue;
|
||||||
|
|
||||||
let type: string;
|
let type: string;
|
||||||
let id: string;
|
let id: string;
|
||||||
switch (decoded.type) {
|
switch (decoded.type) {
|
||||||
case "npub":
|
case "npub":
|
||||||
id = decoded.data;
|
id = decoded.data;
|
||||||
type = "p";
|
type = "p";
|
||||||
break;
|
break;
|
||||||
case "nprofile":
|
case "nprofile":
|
||||||
id = decoded.data.pubkey;
|
id = decoded.data.pubkey;
|
||||||
type = "p";
|
type = "p";
|
||||||
break;
|
break;
|
||||||
case "note":
|
case "note":
|
||||||
id = decoded.data;
|
id = decoded.data;
|
||||||
type = "e";
|
type = "e";
|
||||||
break;
|
break;
|
||||||
case "nevent":
|
case "nevent":
|
||||||
id = decoded.data.id;
|
id = decoded.data.id;
|
||||||
type = "e";
|
type = "e";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let t = tags.find((t) => t[0] === type && t[1] === id);
|
let matchingTags = tags.filter((t) => t[0] === type && t[1] === id);
|
||||||
if (t) {
|
for (const t of matchingTags) foundTags.add(t);
|
||||||
let index = tags.indexOf(t);
|
|
||||||
indexes.add(index);
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
export function filterTagsByContentRefs(content: string, tags: Tag[], referenced = true) {
|
||||||
const contentTagRefs = getContentTagRefs(content, tags);
|
const contentTagRefs = getContentTagRefs(content, tags);
|
||||||
|
return tags.filter((t) => contentTagRefs.includes(t) === referenced);
|
||||||
const newTags: Tag[] = [];
|
|
||||||
for (let i = 0; i < tags.length; i++) {
|
|
||||||
if (contentTagRefs.includes(i) === referenced) {
|
|
||||||
newTags.push(tags[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newTags;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCommunityRefTag(t: Tag): t is ATag {
|
function eTagToEventPointer(tag: ETag): EventPointer {
|
||||||
return t.length >= 2 && t[0] === "a" && t[1].startsWith(COMMUNITY_DEFINITION_KIND + ":");
|
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<typeof getReferences>;
|
export function interpretTags(event: NostrEvent | DraftNostrEvent) {
|
||||||
export function getReferences(event: NostrEvent | DraftNostrEvent) {
|
const eTags = event.tags.filter(isETag);
|
||||||
const contentTagRefs = getContentTagRefs(event.content, event.tags);
|
const aTags = event.tags.filter(isATag);
|
||||||
|
|
||||||
// find the root and reply tags.
|
// find the root and reply tags.
|
||||||
// NOTE: Ignore community reference tags since there is another client out there that is marking them as "root"
|
let rootETag = eTags.find((t) => t[3] === "root");
|
||||||
// and it dose not make sense to "reply" to a community
|
let replyETag = eTags.find((t) => t[3] === "reply");
|
||||||
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 replyId = replyTag?.[1];
|
let rootATag = aTags.find((t) => t[3] === "root");
|
||||||
let replyRelay = replyTag?.[2];
|
let replyATag = aTags.find((t) => t[3] === "reply");
|
||||||
let rootId = rootTag?.[1];
|
|
||||||
let rootRelay = rootTag?.[2];
|
|
||||||
|
|
||||||
if (!rootId || !replyId) {
|
if (!rootETag || !replyETag) {
|
||||||
// a direct reply dose not need a "reply" reference
|
// a direct reply dose not need a "reply" reference
|
||||||
// https://github.com/nostr-protocol/nips/blob/master/10.md
|
// 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 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
|
// 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
|
if (!rootETag && !replyETag) {
|
||||||
// https://github.com/nostr-protocol/nips/blob/master/10.md#positional-e-tags-deprecated
|
const contentTagRefs = getContentTagRefs(event.content, eTags);
|
||||||
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);
|
|
||||||
|
|
||||||
// first tag is the root
|
// legacy behavior
|
||||||
rootId = legacyTags[0][1];
|
// https://github.com/nostr-protocol/nips/blob/master/10.md#positional-e-tags-deprecated
|
||||||
// last tag is reply
|
const legacyETags = eTags.filter((t) => {
|
||||||
replyId = legacyTags[legacyTags.length - 1][1] ?? rootId;
|
// 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 {
|
return {
|
||||||
replyTag,
|
root: rootETag || rootATag ? { e: rootETag, a: rootATag } : undefined,
|
||||||
rootTag,
|
reply: replyETag || replyATag ? { e: replyETag, a: replyATag } : undefined,
|
||||||
mentionTags,
|
};
|
||||||
|
}
|
||||||
|
|
||||||
rootId,
|
export type EventReferences = ReturnType<typeof getReferences>;
|
||||||
rootRelay,
|
export function getReferences(event: NostrEvent | DraftNostrEvent) {
|
||||||
replyId,
|
const tags = interpretTags(event);
|
||||||
replyRelay,
|
|
||||||
|
|
||||||
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),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -19,8 +19,8 @@ function addTag(tags: Tag[], tag: Tag, overwrite = false) {
|
|||||||
}
|
}
|
||||||
return [...tags, tag];
|
return [...tags, tag];
|
||||||
}
|
}
|
||||||
function AddEtag(tags: Tag[], eventId: string, type?: string, overwrite = false) {
|
function AddEtag(tags: Tag[], eventId: string, relayHint?: string, type?: string, overwrite = false) {
|
||||||
const hint = relayHintService.getEventPointerRelayHint(eventId) ?? "";
|
const hint = relayHint || relayHintService.getEventPointerRelayHint(eventId) || "";
|
||||||
|
|
||||||
const tag = type ? ["e", eventId, hint, type] : ["e", eventId, hint];
|
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 */
|
/** adds the "root" and "reply" E tags */
|
||||||
export function addReplyTags(draft: DraftNostrEvent, replyTo: NostrEvent) {
|
export function addReplyTags(draft: DraftNostrEvent, replyTo: NostrEvent) {
|
||||||
const updated: DraftNostrEvent = { ...draft, tags: Array.from(draft.tags) };
|
const updated: DraftNostrEvent = { ...draft, tags: Array.from(draft.tags) };
|
||||||
|
|
||||||
const refs = getReferences(replyTo);
|
const refs = getReferences(replyTo);
|
||||||
|
const rootId = refs.root?.e?.id ?? replyTo.id;
|
||||||
const rootId = refs.rootId ?? replyTo.id;
|
const rootRelayHint = refs.root?.e?.relays?.[0];
|
||||||
const replyId = replyTo.id;
|
const replyId = replyTo.id;
|
||||||
|
const replyRelayHint = relayHintService.getEventPointerRelayHint(replyId);
|
||||||
|
|
||||||
updated.tags = AddEtag(updated.tags, rootId, "root", true);
|
updated.tags = AddEtag(updated.tags, rootId, rootRelayHint, "root", true);
|
||||||
updated.tags = AddEtag(updated.tags, replyId, "reply", true);
|
updated.tags = AddEtag(updated.tags, replyId, replyRelayHint, "reply", true);
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import SuperMap from "../classes/super-map";
|
import SuperMap from "../classes/super-map";
|
||||||
import { NostrEvent } from "../types/nostr-event";
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
import { getReferences, sortByDate } from "./nostr/events";
|
import { getReferences, sortByDate } from "./nostr/events";
|
||||||
@@ -17,7 +18,7 @@ export function groupByRoot(events: NostrEvent[]) {
|
|||||||
const grouped = new SuperMap<string, NostrEvent[]>(() => []);
|
const grouped = new SuperMap<string, NostrEvent[]>(() => []);
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const refs = getReferences(event);
|
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) {
|
for (const [_, groupedEvents] of grouped) {
|
||||||
groupedEvents.sort(sortByDate);
|
groupedEvents.sort(sortByDate);
|
||||||
|
@@ -11,7 +11,7 @@ export type ThreadItem = {
|
|||||||
/** the thread root, according to this event */
|
/** the thread root, according to this event */
|
||||||
root?: ThreadItem;
|
root?: ThreadItem;
|
||||||
/** the parent event this is replying to */
|
/** the parent event this is replying to */
|
||||||
reply?: ThreadItem;
|
replyingTo?: ThreadItem;
|
||||||
/** refs from nostr event */
|
/** refs from nostr event */
|
||||||
refs: EventReferences;
|
refs: EventReferences;
|
||||||
/** direct child replies */
|
/** direct child replies */
|
||||||
@@ -22,11 +22,11 @@ export type ThreadItem = {
|
|||||||
export function getThreadMembers(item: ThreadItem, omit?: string) {
|
export function getThreadMembers(item: ThreadItem, omit?: string) {
|
||||||
const pubkeys = new Set<string>();
|
const pubkeys = new Set<string>();
|
||||||
|
|
||||||
let i = item;
|
let next = item;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (i.event.pubkey !== omit) pubkeys.add(i.event.pubkey);
|
if (next.event.pubkey !== omit) pubkeys.add(next.event.pubkey);
|
||||||
if (!i.reply) break;
|
if (!next.replyingTo) break;
|
||||||
else i = i.reply;
|
else next = next.replyingTo;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(pubkeys);
|
return Array.from(pubkeys);
|
||||||
@@ -39,9 +39,9 @@ export function buildThread(events: NostrEvent[]) {
|
|||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const refs = getReferences(event);
|
const refs = getReferences(event);
|
||||||
|
|
||||||
if (refs.replyId) {
|
if (refs.reply?.type === "nevent") {
|
||||||
idToChildren[refs.replyId] = idToChildren[refs.replyId] || [];
|
idToChildren[refs.reply.data.id] = idToChildren[refs.reply.data.id] || [];
|
||||||
idToChildren[refs.replyId].push(event);
|
idToChildren[refs.reply.data.id].push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
replies.set(event.id, {
|
replies.set(event.id, {
|
||||||
@@ -52,9 +52,9 @@ export function buildThread(events: NostrEvent[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [id, reply] of replies) {
|
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) ?? [];
|
reply.replies = idToChildren[id]?.map((e) => replies.get(e.id) as ThreadItem) ?? [];
|
||||||
|
|
||||||
|
@@ -7,6 +7,7 @@ import singleEventService from "../services/single-event";
|
|||||||
import useTimelineLoader from "./use-timeline-loader";
|
import useTimelineLoader from "./use-timeline-loader";
|
||||||
import { getReferences } from "../helpers/nostr/events";
|
import { getReferences } from "../helpers/nostr/events";
|
||||||
import { NostrEvent } from "../types/nostr-event";
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
|
import { unique } from "../helpers/array";
|
||||||
|
|
||||||
export default function useThreadTimelineLoader(
|
export default function useThreadTimelineLoader(
|
||||||
focusedEvent: NostrEvent | undefined,
|
focusedEvent: NostrEvent | undefined,
|
||||||
@@ -14,12 +15,14 @@ export default function useThreadTimelineLoader(
|
|||||||
kind: number = Kind.Text,
|
kind: number = Kind.Text,
|
||||||
) {
|
) {
|
||||||
const refs = focusedEvent && getReferences(focusedEvent);
|
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 timelineId = `${rootId}-replies`;
|
||||||
const timeline = useTimelineLoader(
|
const timeline = useTimelineLoader(
|
||||||
timelineId,
|
timelineId,
|
||||||
relays,
|
readRelays,
|
||||||
rootId
|
rootId
|
||||||
? {
|
? {
|
||||||
"#e": [rootId],
|
"#e": [rootId],
|
||||||
@@ -35,10 +38,13 @@ export default function useThreadTimelineLoader(
|
|||||||
for (const e of events) singleEventService.handleEvent(e);
|
for (const e of events) singleEventService.handleEvent(e);
|
||||||
}, [events]);
|
}, [events]);
|
||||||
|
|
||||||
const rootEvent = useSingleEvent(rootId, refs?.rootRelay ? [refs.rootRelay] : []);
|
const rootEvent = useSingleEvent(refs?.root?.e?.id, refs?.root?.e?.relays);
|
||||||
const allEvents = useMemo(() => {
|
const allEvents = useMemo(() => {
|
||||||
return rootEvent ? [...events, rootEvent] : events;
|
const arr = Array.from(events);
|
||||||
}, [events, rootEvent]);
|
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 };
|
return { events: allEvents, rootEvent, rootId, timeline };
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
export type ETag = ["e", string] | ["e", string, string] | ["e", string, string, string];
|
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 PTag = ["p", string] | ["p", string, string] | ["p", string, string, string];
|
||||||
export type RTag = ["r", string] | ["r", string, string];
|
export type RTag = ["r", string] | ["r", string, string];
|
||||||
export type DTag = ["d"] | ["d", string];
|
export type DTag = ["d"] | ["d", string];
|
||||||
|
@@ -31,8 +31,9 @@ export default function ChannelMessageForm({
|
|||||||
});
|
});
|
||||||
watch("content");
|
watch("content");
|
||||||
|
|
||||||
const textAreaRef = useRef<RefType | null>(null);
|
const componentRef = useRef<RefType | null>(null);
|
||||||
const { onPaste } = useTextAreaUploadFileWithForm(textAreaRef, getValues, setValue);
|
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
const { onPaste } = useTextAreaUploadFileWithForm(componentRef, getValues, setValue);
|
||||||
|
|
||||||
const sendMessage = handleSubmit(async (values) => {
|
const sendMessage = handleSubmit(async (values) => {
|
||||||
try {
|
try {
|
||||||
@@ -58,6 +59,9 @@ export default function ChannelMessageForm({
|
|||||||
const writeRelays = clientRelaysService.getWriteUrls();
|
const writeRelays = clientRelaysService.getWriteUrls();
|
||||||
new NostrPublishAction("Send DM", writeRelays, signed);
|
new NostrPublishAction("Send DM", writeRelays, signed);
|
||||||
reset();
|
reset();
|
||||||
|
|
||||||
|
// refocus input
|
||||||
|
setTimeout(() => textAreaRef.current?.focus(), 50);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) toast({ status: "error", description: e.message });
|
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 })}
|
onChange={(e) => setValue("content", e.target.value, { shouldDirty: true })}
|
||||||
rows={2}
|
rows={2}
|
||||||
isRequired
|
isRequired
|
||||||
instanceRef={(inst) => (textAreaRef.current = inst)}
|
instanceRef={(inst) => (componentRef.current = inst)}
|
||||||
|
ref={textAreaRef}
|
||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.ctrlKey && e.key === "Enter" && formRef.current) formRef.current.requestSubmit();
|
if (e.ctrlKey && e.key === "Enter" && formRef.current) formRef.current.requestSubmit();
|
||||||
|
@@ -33,8 +33,9 @@ export default function SendMessageForm({
|
|||||||
});
|
});
|
||||||
watch("content");
|
watch("content");
|
||||||
|
|
||||||
const textAreaRef = useRef<RefType | null>(null);
|
const autocompleteRef = useRef<RefType | null>(null);
|
||||||
const { onPaste } = useTextAreaUploadFileWithForm(textAreaRef, getValues, setValue);
|
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
const { onPaste } = useTextAreaUploadFileWithForm(autocompleteRef, getValues, setValue);
|
||||||
|
|
||||||
const usersInbox = useUserRelays(pubkey)
|
const usersInbox = useUserRelays(pubkey)
|
||||||
.filter((r) => r.mode & RelayMode.READ)
|
.filter((r) => r.mode & RelayMode.READ)
|
||||||
@@ -65,6 +66,9 @@ export default function SendMessageForm({
|
|||||||
|
|
||||||
// add plaintext to decryption context
|
// add plaintext to decryption context
|
||||||
getOrCreateContainer(pubkey, encrypted).plaintext.next(values.content);
|
getOrCreateContainer(pubkey, encrypted).plaintext.next(values.content);
|
||||||
|
|
||||||
|
// refocus input
|
||||||
|
setTimeout(() => textAreaRef.current?.focus(), 50);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) toast({ status: "error", description: e.message });
|
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 })}
|
onChange={(e) => setValue("content", e.target.value, { shouldDirty: true })}
|
||||||
rows={2}
|
rows={2}
|
||||||
isRequired
|
isRequired
|
||||||
instanceRef={(inst) => (textAreaRef.current = inst)}
|
instanceRef={(inst) => (autocompleteRef.current = inst)}
|
||||||
|
ref={textAreaRef}
|
||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.ctrlKey && e.key === "Enter" && formRef.current) formRef.current.requestSubmit();
|
if (e.ctrlKey && e.key === "Enter" && formRef.current) formRef.current.requestSubmit();
|
||||||
|
@@ -54,7 +54,7 @@ function ConversationCard({ conversation }: { conversation: KnownConversation })
|
|||||||
<Timestamp flexShrink={0} timestamp={lastMessage.created_at} ml="auto" />
|
<Timestamp flexShrink={0} timestamp={lastMessage.created_at} ml="auto" />
|
||||||
{hasResponded(conversation) && <CheckIcon boxSize={4} color="green.500" />}
|
{hasResponded(conversation) && <CheckIcon boxSize={4} color="green.500" />}
|
||||||
</Flex>
|
</Flex>
|
||||||
{lastReceived === lastMessage && <MessagePreview message={lastReceived} pubkey={lastReceived.pubkey} />}
|
{lastReceived && <MessagePreview message={lastReceived} pubkey={lastReceived.pubkey} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<LinkOverlay as={RouterLink} to={`/dm/${nip19.npubEncode(conversation.correspondent)}` + location.search} />
|
<LinkOverlay as={RouterLink} to={`/dm/${nip19.npubEncode(conversation.correspondent)}` + location.search} />
|
||||||
|
@@ -34,9 +34,9 @@ export const ExpandableToggleButton = ({
|
|||||||
const NoteNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
|
const NoteNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
|
||||||
const account = useCurrentAccount()!;
|
const account = useCurrentAccount()!;
|
||||||
const refs = getReferences(event);
|
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);
|
const isMentioned = isMentionedInContent(event, account.pubkey);
|
||||||
|
|
||||||
if (isReplyingToMe) return <ReplyNotification event={event} ref={ref} />;
|
if (isReplyingToMe) return <ReplyNotification event={event} ref={ref} />;
|
||||||
|
@@ -1,10 +1,9 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Button, Heading, Spinner } from "@chakra-ui/react";
|
import { Button, Heading, Spinner } from "@chakra-ui/react";
|
||||||
import { nip19 } from "nostr-tools";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { useParams, Link as RouterLink } from "react-router-dom";
|
|
||||||
|
|
||||||
import Note from "../../components/note";
|
import Note from "../../components/note";
|
||||||
import { getSharableEventAddress, isHexKey } from "../../helpers/nip19";
|
import { getSharableEventAddress } from "../../helpers/nip19";
|
||||||
import { ThreadPost } from "./components/thread-post";
|
import { ThreadPost } from "./components/thread-post";
|
||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||||
@@ -27,11 +26,11 @@ function ThreadPage({ thread, rootId, focusId }: { thread: Map<string, ThreadIte
|
|||||||
if (!focusedPost) return null;
|
if (!focusedPost) return null;
|
||||||
|
|
||||||
const parentPosts = [];
|
const parentPosts = [];
|
||||||
if (focusedPost.reply) {
|
if (focusedPost.replyingTo) {
|
||||||
let p = focusedPost;
|
let p = focusedPost;
|
||||||
while (p.reply) {
|
while (p.replyingTo) {
|
||||||
parentPosts.unshift(p.reply);
|
parentPosts.unshift(p.replyingTo);
|
||||||
p = p.reply;
|
p = p.replyingTo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,10 +48,10 @@ function ThreadPage({ thread, rootId, focusId }: { thread: Map<string, ThreadIte
|
|||||||
View full thread ({parentPosts.length - 1})
|
View full thread ({parentPosts.length - 1})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{focusedPost.reply && (
|
{focusedPost.replyingTo && (
|
||||||
<Note
|
<Note
|
||||||
key={focusedPost.reply.event.id + "-rely"}
|
key={focusedPost.replyingTo.event.id + "-rely"}
|
||||||
event={focusedPost.reply.event}
|
event={focusedPost.replyingTo.event}
|
||||||
hideDrawerButton
|
hideDrawerButton
|
||||||
showReplyLine={false}
|
showReplyLine={false}
|
||||||
/>
|
/>
|
||||||
|
@@ -7,7 +7,7 @@ import { NostrEvent } from "../../../types/nostr-event";
|
|||||||
import Timestamp from "../../../components/timestamp";
|
import Timestamp from "../../../components/timestamp";
|
||||||
import UserLink from "../../../components/user-link";
|
import UserLink from "../../../components/user-link";
|
||||||
import Magnet from "../../../components/icons/magnet";
|
import Magnet from "../../../components/icons/magnet";
|
||||||
import { getNeventForEventId } from "../../../helpers/nip19";
|
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||||
import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer";
|
import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer";
|
||||||
import { getEventUID } from "../../../helpers/nostr/events";
|
import { getEventUID } from "../../../helpers/nostr/events";
|
||||||
import { formatBytes } from "../../../helpers/number";
|
import { formatBytes } from "../../../helpers/number";
|
||||||
@@ -58,7 +58,7 @@ function TorrentTableRow({ torrent }: { torrent: NostrEvent }) {
|
|||||||
))}
|
))}
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Link as={RouterLink} to={`/torrents/${getNeventForEventId(torrent.id)}`} isTruncated maxW="lg">
|
<Link as={RouterLink} to={`/torrents/${getSharableEventAddress(torrent)}`} isTruncated maxW="lg">
|
||||||
{getTorrentTitle(torrent)}
|
{getTorrentTitle(torrent)}
|
||||||
</Link>
|
</Link>
|
||||||
</Td>
|
</Td>
|
||||||
|
Reference in New Issue
Block a user