hide notes below timeline

This commit is contained in:
hzrd149 2024-01-15 14:45:22 +00:00
parent ea4a9c1e61
commit a3a7cfd26d
12 changed files with 64 additions and 57 deletions

View File

@ -3,7 +3,7 @@ import { Modal, ModalOverlay, ModalContent, ModalBody, ModalCloseButton, Flex, B
import { ModalProps } from "@chakra-ui/react";
import { nip19 } from "nostr-tools";
import { getContentTagRefs, getReferences } from "../../helpers/nostr/events";
import { getContentTagRefs, getThreadReferences } from "../../helpers/nostr/events";
import { NostrEvent } from "../../types/nostr-event";
import RawJson from "./raw-json";
import RawValue from "./raw-value";
@ -33,7 +33,7 @@ export default function NoteDebugModal({ event, ...props }: { event: NostrEvent
<RawValue heading="NIP-19 Pointer" value={getSharableEventAddress(event)} />
<RawPre heading="Content" value={event.content} />
<RawJson heading="JSON" json={event} />
<RawJson heading="Thread Tags" json={getReferences(event)} />
<RawJson heading="Thread Tags" json={getThreadReferences(event)} />
<RawJson heading="Tags referenced in content" json={getContentTagRefs(event.content, event.tags)} />
{/* TODO: extract this out */}
<Button onClick={broadcast} ml="auto" colorScheme="primary" isLoading={loading}>

View File

@ -11,7 +11,7 @@ import { TrustProvider } from "../../../providers/local/trust";
import Timestamp from "../../timestamp";
import { CompactNoteContent } from "../../compact-note-content";
import HoverLinkOverlay from "../../hover-link-overlay";
import { getReferences } from "../../../helpers/nostr/events";
import { getThreadReferences } from "../../../helpers/nostr/events";
import useSingleEvent from "../../../hooks/use-single-event";
import { getTorrentTitle } from "../../../helpers/nostr/torrents";
import { useNavigateInDrawer } from "../../../providers/drawer-sub-view-provider";
@ -24,7 +24,7 @@ export default function EmbeddedTorrentComment({
}: Omit<CardProps, "children"> & { comment: NostrEvent }) {
const navigate = useNavigateInDrawer();
const { showSignatureVerification } = useSubject(appSettings);
const refs = getReferences(comment);
const refs = getThreadReferences(comment);
const torrent = useSingleEvent(refs.root?.e?.id, refs.root?.e?.relays);
const linkToTorrent = refs.root?.e && `/torrents/${nip19.neventEncode(refs.root.e)}`;

View File

@ -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, truncatedId } from "../../helpers/nostr/events";
import { getThreadReferences, truncatedId } from "../../helpers/nostr/events";
import Timestamp from "../timestamp";
import OpenInDrawerButton from "../open-in-drawer-button";
import { getSharableEventAddress } from "../../helpers/nip19";
@ -91,7 +91,7 @@ function ReplyToA({ pointer }: { pointer: AddressPointer }) {
}
function ReplyLine({ event }: { event: NostrEvent }) {
const refs = getReferences(event);
const refs = getThreadReferences(event);
if (!refs.reply) return null;
return (
@ -203,7 +203,7 @@ export const Note = React.memo(
</ExpandProvider>
{replyForm.isOpen && (
<ReplyForm
item={{ event, replies: [], refs: getReferences(event) }}
item={{ event, replies: [], refs: getThreadReferences(event) }}
onCancel={replyForm.onClose}
onSubmitted={replyForm.onClose}
/>

View File

@ -45,15 +45,19 @@ function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
},
[cachedLocationKey, timeline],
);
const [pinDate, setPinDate] = useState(getCachedNumber("pin") ?? events[NOTE_BUFFER]?.created_at ?? 0);
const [maxDate, setMaxDate] = useState(getCachedNumber("max") ?? Infinity);
const [minDate, setMinDate] = useState(getCachedNumber("min") ?? events[NOTE_BUFFER]?.created_at ?? -Infinity);
const [minDate, setMinDate] = useState(getCachedNumber("min") ?? events[NOTE_BUFFER]?.created_at ?? 0);
// reset the latest and minDate when timeline changes
useEffect(() => {
setLatest(dayjs().unix());
setMaxDate(getCachedNumber("max") ?? Infinity);
setMinDate(getCachedNumber("min") ?? timeline.timeline.value[NOTE_BUFFER]?.created_at ?? 0);
}, [timeline, setMinDate, setLatest, getCachedNumber]);
setPinDate(getCachedNumber("min") ?? timeline.timeline.value[NOTE_BUFFER]?.created_at ?? 0);
setPinDate(getCachedNumber("pin") ?? timeline.timeline.value[NOTE_BUFFER]?.created_at ?? 0);
}, [timeline, setPinDate, setLatest, getCachedNumber]);
const updateNoteMinHeight = useCallback(
(id: string, element: Element) => {
@ -78,7 +82,7 @@ function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
let max: number = -Infinity;
let min: number = Infinity;
let preload = NOTE_BUFFER;
let minBuffer = NOTE_BUFFER;
let foundVisible = false;
for (const event of timeline.timeline.value) {
if (event.created_at > latest) continue;
@ -87,7 +91,7 @@ function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
if (!isIntersecting) {
if (foundVisible) {
// found an event below the view
if (preload-- < 0) break;
if (minBuffer-- < 0) break;
if (event.created_at < min) min = event.created_at;
} else {
// found an event above the view
@ -97,15 +101,20 @@ function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
// found visible event
foundVisible = true;
// find the event that is x indexes back
const bufferEvent = timeline.timeline.value[timeline.timeline.value.indexOf(event) - NOTE_BUFFER];
if (bufferEvent && bufferEvent.created_at > max) max = bufferEvent.created_at;
}
}
if (min !== Infinity) {
setMinDate((v) => {
setCachedNumber("min", min);
setMinDate(min);
// only set the pin date if its less than before (the timeline only get longer)
setPinDate((v) => {
const value = Math.min(v, min);
setCachedNumber("min", value);
setCachedNumber("pin", value);
return value;
});
}
@ -123,8 +132,9 @@ function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
intersectionSubject.unsubscribe(listener);
};
}, [
setMinDate,
setPinDate,
setMaxDate,
setMinDate,
intersectionSubject,
intersectionEntryCache,
updateNoteMinHeight,
@ -137,7 +147,7 @@ function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
const notes: NostrEvent[] = [];
for (const note of events) {
if (note.created_at > latest) newNotes.push(note);
else if (note.created_at >= minDate) notes.push(note);
else if (note.created_at >= pinDate) notes.push(note);
}
return (
@ -159,7 +169,7 @@ function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
<TimelineItem
key={note.id}
event={note}
visible={note.created_at <= maxDate}
visible={note.created_at <= maxDate && note.created_at >= minDate}
minHeight={getCachedNumber(getEventUID(note))}
/>
))}

View File

@ -3,11 +3,11 @@ import { memo, useRef } from "react";
import { NostrEvent } from "../../../types/nostr-event";
import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer";
import Note from "../../note";
import { getEventUID } from "nostr-idb";
function ReplyNote({ event }: { event: NostrEvent }) {
// if there is a parent intersection observer, register this card
const ref = useRef<HTMLDivElement | null>(null);
useRegisterIntersectionEntity(ref, event.id);
useRegisterIntersectionEntity(ref, getEventUID(event));
return (
<div ref={ref}>

View File

@ -6,6 +6,7 @@ import { getMatchNostrLink } from "../regexp";
import { AddressPointer, EventPointer } from "nostr-tools/lib/types/nip19";
import { safeJson } from "../parse";
import { safeDecode } from "../nip19";
import { getEventUID } from "nostr-idb";
export function truncatedId(str: string, keep = 6) {
if (str.length < keep * 2 + 3) return str;
@ -32,18 +33,10 @@ export function pointerMatchEvent(event: NostrEvent, pointer: AddressPointer | E
return false;
}
// used to get a unique Id for each event, should take into account replaceable events
export function getEventUID(event: NostrEvent) {
if (isReplaceable(event.kind)) {
return getEventCoordinate(event);
}
return event.id;
}
export function isReply(event: NostrEvent | DraftNostrEvent) {
if (event.kind === kinds.Repost || event.kind === kinds.GenericRepost) return false;
// TODO: update this to only look for a "root" or "reply" tag
return !!getReferences(event).reply;
return !!getThreadReferences(event).reply;
}
export function isMentionedInContent(event: NostrEvent | DraftNostrEvent, pubkey: string) {
return filterTagsByContentRefs(event.content, event.tags).some((t) => t[1] === pubkey);
@ -96,24 +89,13 @@ export function getContentTagRefs(content: string, tags: Tag[]) {
return Array.from(foundTags);
}
/**
* returns all tags that are referenced in the content
*/
/** returns all tags that are referenced in the content */
export function filterTagsByContentRefs(content: string, tags: Tag[], referenced = true) {
const contentTagRefs = getContentTagRefs(content, tags);
return tags.filter((t) => contentTagRefs.includes(t) === referenced);
}
export function eTagToEventPointer(tag: ETag): EventPointer {
return { id: tag[1], relays: tag[2] ? [tag[2]] : [] };
}
export function aTagToAddressPointer(tag: ATag): AddressPointer {
const cord = parseCoordinate(tag[1], true, false);
if (tag[2]) cord.relays = [tag[2]];
return cord;
}
export function interpretTags(event: NostrEvent | DraftNostrEvent) {
export function interpretThreadTags(event: NostrEvent | DraftNostrEvent) {
const eTags = event.tags.filter(isETag);
const aTags = event.tags.filter(isATag);
@ -165,9 +147,9 @@ export function interpretTags(event: NostrEvent | DraftNostrEvent) {
};
}
export type EventReferences = ReturnType<typeof getReferences>;
export function getReferences(event: NostrEvent | DraftNostrEvent) {
const tags = interpretTags(event);
export type EventReferences = ReturnType<typeof getThreadReferences>;
export function getThreadReferences(event: NostrEvent | DraftNostrEvent) {
const tags = interpretThreadTags(event);
return {
root: tags.root && {
@ -205,6 +187,7 @@ export function getEventCoordinate(event: NostrEvent) {
const d = event.tags.find(isDTag)?.[1];
return d ? `${event.kind}:${event.pubkey}:${d}` : `${event.kind}:${event.pubkey}`;
}
export function getEventAddressPointer(event: NostrEvent): AddressPointer {
const { kind, pubkey } = event;
if (!isReplaceable(kind)) throw new Error("Event is not replaceable");
@ -212,11 +195,23 @@ export function getEventAddressPointer(event: NostrEvent): AddressPointer {
if (!identifier) throw new Error("Missing identifier");
return { kind, pubkey, identifier };
}
export function pointerToATag(pointer: AddressPointer): ATag {
export function eTagToEventPointer(tag: ETag): EventPointer {
return { id: tag[1], relays: tag[2] ? [tag[2]] : [] };
}
export function aTagToAddressPointer(tag: ATag): AddressPointer {
const cord = parseCoordinate(tag[1], true, false);
if (tag[2]) cord.relays = [tag[2]];
return cord;
}
export function addressPointerToATag(pointer: AddressPointer): ATag {
const relay = pointer.relays?.[0];
const coordinate = `${pointer.kind}:${pointer.pubkey}:${pointer.identifier}`;
return relay ? ["a", coordinate, relay] : ["a", coordinate];
}
export function eventPointerToETag(pointer: EventPointer): ETag {
return pointer.relays?.length ? ["e", pointer.id, pointer.relays[0]] : ["e", pointer.id];
}
export type CustomAddressPointer = Omit<AddressPointer, "identifier"> & {
identifier?: string;
@ -270,3 +265,5 @@ export function parseHardcodedNoteContent(event: NostrEvent) {
export function sortByDate(a: NostrEvent, b: NostrEvent) {
return b.created_at - a.created_at;
}
export { getEventUID };

View File

@ -1,6 +1,6 @@
import { DraftNostrEvent, NostrEvent, Tag } from "../../types/nostr-event";
import { getMatchEmoji, getMatchHashtag, getMatchNostrLink } from "../regexp";
import { getReferences } from "./events";
import { getThreadReferences } from "./events";
import { getPubkeyFromDecodeResult, safeDecode } from "../nip19";
import { Emoji } from "../../providers/global/emoji-provider";
import { EventSplit } from "./zaps";
@ -40,7 +40,7 @@ function AddEtag(tags: Tag[], eventId: string, relayHint?: string, type?: string
export function addReplyTags(draft: DraftNostrEvent, replyTo: NostrEvent) {
const updated: DraftNostrEvent = { ...draft, tags: Array.from(draft.tags) };
const refs = getReferences(replyTo);
const refs = getThreadReferences(replyTo);
const rootId = refs.root?.e?.id ?? replyTo.id;
const rootRelayHint = refs.root?.e?.relays?.[0];
const replyId = replyTo.id;

View File

@ -1,6 +1,6 @@
import SuperMap from "../classes/super-map";
import { NostrEvent } from "../types/nostr-event";
import { getReferences, sortByDate } from "./nostr/events";
import { getThreadReferences, sortByDate } from "./nostr/events";
const DAY_IN_SECONDS = 60 * 60 * 24;
@ -17,7 +17,7 @@ export function groupByTime(events: NostrEvent[], time = DAY_IN_SECONDS) {
export function groupByRoot(events: NostrEvent[]) {
const grouped = new SuperMap<string, NostrEvent[]>(() => []);
for (const event of events) {
const refs = getReferences(event);
const refs = getThreadReferences(event);
if (refs.root?.e?.id) grouped.get(refs.root.e.id).push(event);
}
for (const [_, groupedEvents] of grouped) {

View File

@ -1,5 +1,5 @@
import { NostrEvent } from "../types/nostr-event";
import { EventReferences, getReferences } from "./nostr/events";
import { EventReferences, getThreadReferences } from "./nostr/events";
export function countReplies(replies: ThreadItem[]): number {
return replies.reduce((c, item) => c + countReplies(item.replies), 0) + replies.length;
@ -37,7 +37,7 @@ export function buildThread(events: NostrEvent[]) {
const replies = new Map<string, ThreadItem>();
for (const event of events) {
const refs = getReferences(event);
const refs = getThreadReferences(event);
if (refs.reply?.e) {
idToChildren[refs.reply.e.id] = idToChildren[refs.reply.e.id] || [];

View File

@ -5,7 +5,7 @@ import useSubject from "./use-subject";
import useSingleEvent from "./use-single-event";
import singleEventService from "../services/single-event";
import useTimelineLoader from "./use-timeline-loader";
import { getReferences } from "../helpers/nostr/events";
import { getThreadReferences } from "../helpers/nostr/events";
import { NostrEvent } from "../types/nostr-event";
import { unique } from "../helpers/array";
@ -14,7 +14,7 @@ export default function useThreadTimelineLoader(
relays: string[],
kind: number = kinds.ShortTextNote,
) {
const refs = focusedEvent && getReferences(focusedEvent);
const refs = focusedEvent && getThreadReferences(focusedEvent);
const rootPointer = refs?.root?.e || (focusedEvent && { id: focusedEvent?.id });
const readRelays = unique([...relays, ...(rootPointer?.relays ?? [])]);

View File

@ -7,7 +7,7 @@ import { NostrEvent, isATag, isETag } from "../../types/nostr-event";
import { useRegisterIntersectionEntity } from "../../providers/local/intersection-observer";
import { parseZapEvent } from "../../helpers/nostr/zaps";
import { readablizeSats } from "../../helpers/bolt11";
import { getEventUID, getReferences, isMentionedInContent, parseCoordinate } from "../../helpers/nostr/events";
import { getEventUID, getThreadReferences, isMentionedInContent, parseCoordinate } from "../../helpers/nostr/events";
import { EmbedEvent, EmbedEventPointer } from "../../components/embed-event";
import EmbeddedUnknown from "../../components/embed-event/event-types/embedded-unknown";
import { ErrorBoundary } from "../../components/error-boundary";
@ -34,7 +34,7 @@ export const ExpandableToggleButton = ({
const NoteNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
const account = useCurrentAccount()!;
const refs = getReferences(event);
const refs = getThreadReferences(event);
const parent = useSingleEvent(refs.reply?.e?.id);
const isReplyingToMe = !!refs.reply?.e?.id && (parent ? parent.pubkey === account.pubkey : true);

View File

@ -41,7 +41,7 @@ import TorrentMenu from "./components/torrent-menu";
import QuoteRepostButton from "../../components/note/components/quote-repost-button";
import TorrentComments from "./components/torrents-comments";
import ReplyForm from "../thread/components/reply-form";
import { getReferences } from "../../helpers/nostr/events";
import { getThreadReferences } from "../../helpers/nostr/events";
import MessageTextCircle01 from "../../components/icons/message-text-circle-01";
import useParamsEventPointer from "../../hooks/use-params-event-pointer";
@ -127,7 +127,7 @@ function TorrentDetailsPage({ torrent }: { torrent: NostrEvent }) {
</Flex>
{replyForm.isOpen && (
<ReplyForm
item={{ event: torrent, refs: getReferences(torrent), replies: [] }}
item={{ event: torrent, refs: getThreadReferences(torrent), replies: [] }}
onCancel={replyForm.onClose}
onSubmitted={replyForm.onClose}
replyKind={TORRENT_COMMENT_KIND}