fix reaction bug

This commit is contained in:
hzrd149 2023-08-31 08:54:24 -05:00
parent bb16bddb9f
commit 954ec50afc
15 changed files with 80 additions and 57 deletions

@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Fix bug with reactions showing on wrong notes

@ -27,7 +27,7 @@ import NostrLinkView from "./views/link";
import UserReportsTab from "./views/user/reports";
import ToolsHomeView from "./views/tools";
import UserAboutTab from "./views/user/about";
import UserLikesTab from "./views/user/likes";
import UserReactionsTab from "./views/user/reactions";
import useSetColorMode from "./hooks/use-set-color-mode";
import UserStreamsTab from "./views/user/streams";
import { PageProviders } from "./providers";
@ -97,7 +97,7 @@ const router = createHashRouter([
{ path: "notes", element: <UserNotesTab /> },
{ path: "streams", element: <UserStreamsTab /> },
{ path: "zaps", element: <UserZapsTab /> },
{ path: "likes", element: <UserLikesTab /> },
{ path: "likes", element: <UserReactionsTab /> },
{ path: "lists", element: <UserListsTab /> },
{ path: "followers", element: <UserFollowersTab /> },
{ path: "following", element: <UserFollowingTab /> },

@ -2,6 +2,8 @@ import { getEventUID } from "../helpers/nostr/events";
import { NostrEvent } from "../types/nostr-event";
import Subject from "./subject";
export type EventFilter = (event: NostrEvent) => boolean;
export default class EventStore {
name?: string;
events = new Map<string, NostrEvent>();
@ -25,8 +27,8 @@ export default class EventStore {
this.onEvent.next(event);
}
}
getEvent(id:string){
return this.events.get(id)
getEvent(id: string) {
return this.events.get(id);
}
clear() {
@ -41,18 +43,28 @@ export default class EventStore {
other.onEvent.unsubscribe(this.addEvent, this);
}
getFirstEvent(nth = 0) {
getFirstEvent(nth = 0, filter?: EventFilter) {
const events = this.getSortedEvents();
for (let i = 0; i <= nth; i++) {
const event = events[i];
if (event) return event;
let i = 0;
while (true) {
const event = events.shift();
if (!event) return;
if (filter && !filter(event)) continue;
if (i === nth) return event;
i++;
}
}
getLastEvent(nth = 0) {
getLastEvent(nth = 0, filter?: EventFilter) {
const events = this.getSortedEvents();
for (let i = nth; i >= 0; i--) {
const event = events[events.length - 1 - i];
if (event) return event;
let i = 0;
while (true) {
const event = events.pop();
if (!event) return;
if (filter && !filter(event)) continue;
if (i === nth) return event;
i++;
}
}
}

@ -19,6 +19,8 @@ function addToQuery(filter: NostrRequestFilter, query: NostrQuery) {
const BLOCK_SIZE = 30;
export type EventFilter = (event: NostrEvent) => boolean;
export class RelayTimelineLoader {
relay: string;
query: NostrRequestFilter;
@ -72,11 +74,11 @@ export class RelayTimelineLoader {
return this.events.addEvent(event);
}
getFirstEvent(nth = 0) {
return this.events.getFirstEvent(nth);
getFirstEvent(nth = 0, filter?: EventFilter) {
return this.events.getFirstEvent(nth, filter);
}
getLastEvent(nth = 0) {
return this.events.getLastEvent(nth);
getLastEvent(nth = 0, filter?: EventFilter) {
return this.events.getLastEvent(nth, filter);
}
}
@ -91,7 +93,7 @@ export class TimelineLoader {
complete = new PersistentSubject(false);
loadNextBlockBuffer = 2;
eventFilter?: (event: NostrEvent) => boolean;
eventFilter?: EventFilter;
name: string;
private log: Debugger;
@ -192,7 +194,7 @@ export class TimelineLoader {
let triggeredLoad = false;
for (const [relay, loader] of this.relayTimelineLoaders) {
if (loader.complete || loader.loading) continue;
const event = loader.getLastEvent(this.loadNextBlockBuffer);
const event = loader.getLastEvent(this.loadNextBlockBuffer, this.eventFilter);
if (!event || event.created_at >= this.cursor) {
loader.loadNextBlock();
triggeredLoad = true;

@ -5,7 +5,7 @@ import relayScoreboardService from "../services/relay-scoreboard";
import { NostrEvent, isDTag } from "../types/nostr-event";
import { getEventUID } from "./nostr/events";
export function isHex(key?: string) {
export function isHexKey(key?: string) {
if (key?.toLowerCase()?.match(/^[0-9a-f]{64}$/)) return true;
return false;
}
@ -15,7 +15,7 @@ export function isBech32Key(bech32String: string) {
try {
const { prefix } = bech32.decode(bech32String.toLowerCase());
if (!prefix) return false;
if (!isHex(bech32ToHex(bech32String))) return false;
if (!isHexKey(bech32ToHex(bech32String))) return false;
} catch (error) {
return false;
}
@ -59,7 +59,7 @@ export function getPubkey(result: nip19.DecodeResult) {
}
export function normalizeToHex(hex: string) {
if (isHex(hex)) return hex;
if (isHexKey(hex)) return hex;
if (isBech32Key(hex)) return bech32ToHex(hex);
return null;
}

@ -20,14 +20,14 @@ export function groupReactions(reactions: NostrEvent[]) {
return Array.from(Object.values(groups)).sort((a, b) => b.count - a.count);
}
export function draftEventReaction(event: NostrEvent, emoji = "+", url?: string) {
export function draftEventReaction(reacted: NostrEvent, emoji = "+", url?: string) {
// only keep the e, and p tags on the parent event
const inheritedTags = reacted.tags.filter((tag) => tag.length >= 2 && (tag[0] === "e" || tag[0] === "p"));
const draft: DraftNostrEvent = {
kind: Kind.Reaction,
content: url ? ":" + emoji + ":" : emoji,
tags: [
["e", event.id],
["p", event.pubkey], // TODO: pick a relay for the user
],
tags: [...inheritedTags, ["e", reacted.id], ["p", reacted.pubkey]],
created_at: dayjs().unix(),
};

@ -1,8 +1,10 @@
import { useAsync } from "react-use";
import singleEventService from "../services/single-event";
export default function useSingleEvent(id: string, relays: string[] = []) {
const { loading, value: event } = useAsync(() => singleEventService.requestEvent(id, relays), [id, relays.join("|")]);
export default function useSingleEvent(id?: string, relays: string[] = []) {
const { loading, value: event } = useAsync(async () => {
if (id) return singleEventService.requestEvent(id, relays);
}, [id, relays.join("|")]);
return {
event,

@ -1,8 +1,8 @@
import { Kind } from "nostr-tools";
import { Kind, nip25 } from "nostr-tools";
import { NostrRequest } from "../classes/nostr-request";
import Subject from "../classes/subject";
import { SuperMap } from "../classes/super-map";
import { getReferences } from "../helpers/nostr/events";
import { NostrEvent } from "../types/nostr-event";
type eventId = string;
@ -26,11 +26,10 @@ class EventReactionsService {
handleEvent(event: NostrEvent) {
if (event.kind !== Kind.Reaction) return;
const refs = getReferences(event);
const id = refs.events[0];
if (!id) return;
const pointer = nip25.getReactedEventPointer(event);
if (!pointer?.id) return;
const subject = this.subjects.get(id);
const subject = this.subjects.get(pointer.id);
if (!subject.value) {
subject.next([event]);
} else if (!subject.value.some((e) => e.id === event.id)) {

@ -1,6 +1,6 @@
import db from "./db";
import { fetchWithCorsFallback } from "../helpers/cors";
import { isHex } from "../helpers/nip19";
import { isHexKey } from "../helpers/nip19";
export type RelayInformationDocument = {
name: string;
@ -15,7 +15,7 @@ export type RelayInformationDocument = {
};
function sanitizeInfo(info: RelayInformationDocument) {
if (info.pubkey && !isHex(info.pubkey)) {
if (info.pubkey && !isHexKey(info.pubkey)) {
delete info.pubkey;
}
return info;

@ -2,13 +2,13 @@ import { Flex, Spinner } from "@chakra-ui/react";
import { nip19 } from "nostr-tools";
import { useParams } from "react-router-dom";
import { Note } from "../../components/note";
import { isHex } from "../../helpers/nip19";
import { isHexKey } from "../../helpers/nip19";
import { useThreadLoader } from "../../hooks/use-thread-loader";
import { ThreadPost } from "./components/thread-post";
function useNotePointer() {
const { id } = useParams() as { id: string };
if (isHex(id)) return { id, relays: [] };
if (isHexKey(id)) return { id, relays: [] };
const pointer = nip19.decode(id);
switch (pointer.type) {

@ -197,7 +197,6 @@ export function RelayPaidTag({ url }: { url: string }) {
}
export default function RelayCard({ url, ...props }: { url: string } & Omit<CardProps, "children">) {
const { info } = useRelayInfo(url);
return (
<>
<Card variant="outline" {...props}>

@ -10,12 +10,12 @@ import AddCustomRelayModal from "./components/add-custom-modal";
import RelayCard from "./components/relay-card";
import clientRelaysService from "../../services/client-relays";
import { RelayMode } from "../../classes/relay";
import { ErrorBoundary } from "../../components/error-boundary";
export default function RelaysView() {
const [search, setSearch] = useState("");
const deboundedSearch = useDeferredValue(search);
const isSearching = deboundedSearch.length > 2;
const showAll = useDisclosure();
const addRelayModal = useDisclosure();
const clientRelays = useClientRelays().map((r) => r.url);
@ -33,16 +33,13 @@ export default function RelaysView() {
return onlineRelays.filter((url) => url.includes(deboundedSearch));
}
return showAll.isOpen ? onlineRelays : clientRelays;
}, [isSearching, deboundedSearch, onlineRelays, clientRelays, showAll.isOpen]);
return clientRelays;
}, [isSearching, deboundedSearch, onlineRelays, clientRelays]);
return (
<Flex direction="column" gap="2" p="2">
<Flex alignItems="center" gap="2" wrap="wrap">
<Input type="search" placeholder="search" value={search} onChange={(e) => setSearch(e.target.value)} w="auto" />
<Switch isChecked={showAll.isOpen} onChange={showAll.onToggle}>
Show All
</Switch>
<Spacer />
<Button as={RouterLink} to="/relays/reviews">
Browse Reviews
@ -53,7 +50,9 @@ export default function RelaysView() {
</Flex>
<SimpleGrid columns={[1, 1, 1, 2, 3]} spacing="2">
{filteredRelays.map((url) => (
<RelayCard key={url} url={url} variant="outline" />
<ErrorBoundary>
<RelayCard key={url} url={url} variant="outline" />
</ErrorBoundary>
))}
</SimpleGrid>
@ -63,7 +62,9 @@ export default function RelaysView() {
<Heading size="lg">Discovered Relays</Heading>
<SimpleGrid columns={[1, 1, 1, 2, 3]} spacing="2">
{discoveredRelays.map((url) => (
<RelayCard key={url} url={url} variant="outline" />
<ErrorBoundary>
<RelayCard key={url} url={url} variant="outline" />
</ErrorBoundary>
))}
</SimpleGrid>
</>

@ -27,7 +27,7 @@ import {
import { Outlet, useMatches, useNavigate, useParams } from "react-router-dom";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import { getUserDisplayName } from "../../helpers/user-metadata";
import { isHex } from "../../helpers/nip19";
import { isHexKey } from "../../helpers/nip19";
import { useAppTitle } from "../../hooks/use-app-title";
import { Suspense, useState } from "react";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
@ -56,7 +56,7 @@ const tabs = [
function useUserPointer() {
const { pubkey } = useParams() as { pubkey: string };
if (isHex(pubkey)) return { pubkey, relays: [] };
if (isHexKey(pubkey)) return { pubkey, relays: [] };
const pointer = nip19.decode(pubkey);
switch (pointer.type) {

@ -2,7 +2,8 @@ import { useRef } from "react";
import { useOutletContext } from "react-router-dom";
import { Box, Flex, SkeletonText, Spacer, Text } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { getReferences, truncatedId } from "../../helpers/nostr/events";
import { nip25 } from "nostr-tools";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { NostrEvent } from "../../types/nostr-event";
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
@ -18,16 +19,15 @@ import { UserAvatar } from "../../components/user-avatar";
import { UserLink } from "../../components/user-link";
import { NoteMenu } from "../../components/note/note-menu";
const Like = ({ event }: { event: NostrEvent }) => {
const Reaction = ({ event }: { event: NostrEvent }) => {
const ref = useRef<HTMLDivElement | null>(null);
useRegisterIntersectionEntity(ref, event.id);
const contextRelays = useAdditionalRelayContext();
const readRelays = useReadRelayUrls(contextRelays);
const refs = getReferences(event);
const eventId: string | undefined = refs.events[0];
const { event: note } = useSingleEvent(eventId, readRelays);
const pointer = nip25.getReactedEventPointer(event);
const { event: note } = useSingleEvent(pointer?.id, readRelays);
var content = <></>;
if (!note) return <SkeletonText />;
@ -51,7 +51,7 @@ const Like = ({ event }: { event: NostrEvent }) => {
return <Box ref={ref}>{content}</Box>;
};
export default function UserLikesTab() {
export default function UserReactionsTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
const contextRelays = useAdditionalRelayContext();
const readRelays = useReadRelayUrls(contextRelays);
@ -67,7 +67,7 @@ export default function UserLikesTab() {
<TrustProvider trust>
<Flex direction="column" gap="2" p="2" pb="8">
{likes.map((event) => (
<Like event={event} />
<Reaction event={event} />
))}
<TimelineActionAndStatus timeline={timeline} />

@ -13,6 +13,7 @@ import { RelayDebugButton, RelayJoinAction, RelayMetadata, RelayShareButton } fr
import IntersectionObserverProvider from "../../providers/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import { useRelayInfo } from "../../hooks/use-relay-info";
import { ErrorBoundary } from "../../components/error-boundary";
function Relay({ url, reviews }: { url: string; reviews: NostrEvent[] }) {
const { info } = useRelayInfo(url);
@ -75,7 +76,9 @@ const UserRelaysTab = () => {
<IntersectionObserverProvider<string> callback={callback}>
<VStack divider={<StackDivider />} py="2" align="stretch">
{userRelays.map((relayConfig) => (
<Relay url={relayConfig.url} reviews={getRelayReviews(relayConfig.url, reviews)} />
<ErrorBoundary>
<Relay key={relayConfig.url} url={relayConfig.url} reviews={getRelayReviews(relayConfig.url, reviews)} />
</ErrorBoundary>
))}
</VStack>
{otherReviews.length > 0 && (