refactor: RequestBuilder

This commit is contained in:
2023-03-28 15:34:01 +01:00
parent 1bf6c7031e
commit 465c59ea20
77 changed files with 3141 additions and 2343 deletions

View File

@ -1,7 +1,10 @@
import { useMemo } from "react";
import { TaggedRawEvent, EventKind, HexKey, Lists, Subscriptions } from "@snort/nostr";
import useSubscription from "Feed/Subscription";
import { EventKind, HexKey, Lists } from "@snort/nostr";
import { unwrap, findTag, chunks } from "Util";
import { RequestBuilder } from "System";
import { FlatNoteStore, ReplaceableNoteStore } from "System/NoteCollection";
import useRequestBuilder from "Hooks/useRequestBuilder";
type BadgeAwards = {
pubkeys: string[];
@ -11,22 +14,17 @@ type BadgeAwards = {
export default function useProfileBadges(pubkey?: HexKey) {
const sub = useMemo(() => {
if (!pubkey) return null;
const s = new Subscriptions();
s.Id = `badges:${pubkey.slice(0, 12)}`;
s.Kinds = new Set([EventKind.ProfileBadges]);
s.DTags = new Set([Lists.Badges]);
s.Authors = new Set([pubkey]);
return s;
const b = new RequestBuilder(`badges:${pubkey.slice(0, 12)}`);
b.withFilter().kinds([EventKind.ProfileBadges]).tag("d", [Lists.Badges]).authors([pubkey]);
return b;
}, [pubkey]);
const profileBadges = useSubscription(sub, { leaveOpen: false, cache: false });
const profileBadges = useRequestBuilder<ReplaceableNoteStore>(ReplaceableNoteStore, sub);
const profile = useMemo(() => {
const sorted = [...profileBadges.store.notes];
sorted.sort((a, b) => b.created_at - a.created_at);
const last = sorted[0];
if (last) {
if (profileBadges.data) {
return chunks(
last.tags.filter(t => t[0] === "a" || t[0] === "e"),
profileBadges.data.tags.filter(t => t[0] === "a" || t[0] === "e"),
2
).reduce((acc, [a, e]) => {
return {
@ -36,7 +34,7 @@ export default function useProfileBadges(pubkey?: HexKey) {
}, {});
}
return {};
}, [pubkey, profileBadges.store]);
}, [profileBadges]);
const { ds, pubkeys } = useMemo(() => {
return Object.values(profile).reduce(
@ -55,48 +53,37 @@ export default function useProfileBadges(pubkey?: HexKey) {
const awardsSub = useMemo(() => {
const ids = Object.keys(profile);
if (!pubkey || ids.length === 0) return null;
const s = new Subscriptions();
s.Id = `profile_awards:${pubkey.slice(0, 12)}`;
s.Kinds = new Set([EventKind.BadgeAward]);
s.Ids = new Set(ids);
return s;
}, [pubkey, profileBadges.store]);
const b = new RequestBuilder(`profile_awards:${pubkey.slice(0, 12)}`);
b.withFilter().kinds([EventKind.BadgeAward]).ids(ids);
b.withFilter().kinds([EventKind.Badge]).tag("d", ds).authors(pubkeys);
return b;
}, [profile, ds]);
const awards = useSubscription(awardsSub).store.notes;
const badgesSub = useMemo(() => {
if (!pubkey || pubkeys.length === 0) return null;
const s = new Subscriptions();
s.Id = `profile_badges:${pubkey.slice(0, 12)}`;
s.Kinds = new Set([EventKind.Badge]);
s.DTags = new Set(ds);
s.Authors = new Set(pubkeys);
return s;
}, [pubkey, profile]);
const badges = useSubscription(badgesSub, { leaveOpen: false, cache: false }).store.notes;
const awards = useRequestBuilder<FlatNoteStore>(FlatNoteStore, awardsSub);
const result = useMemo(() => {
return awards
.map((award: TaggedRawEvent) => {
const [, pubkey, d] =
award.tags
.find(t => t[0] === "a")
?.at(1)
?.split(":") ?? [];
const badge = badges.find(b => b.pubkey === pubkey && findTag(b, "d") === d);
if (awards.data) {
return awards.data
.map((award, _, arr) => {
const [, pubkey, d] =
award.tags
.find(t => t[0] === "a")
?.at(1)
?.split(":") ?? [];
const badge = arr.find(b => b.pubkey === pubkey && findTag(b, "d") === d);
return {
award,
badge,
};
})
.filter(
({ award, badge }) =>
badge && award.pubkey === badge.pubkey && award.tags.find(t => t[0] === "p" && t[1] === pubkey)
)
.map(({ badge }) => unwrap(badge));
}, [pubkey, awards, badges]);
return {
award,
badge,
};
})
.filter(
({ award, badge }) =>
badge && award.pubkey === badge.pubkey && award.tags.find(t => t[0] === "p" && t[1] === pubkey)
)
.map(({ badge }) => unwrap(badge));
}
}, [pubkey, awards]);
return result;
return result ?? [];
}

View File

@ -1,8 +1,8 @@
import { useSelector } from "react-redux";
import { HexKey, Lists } from "@snort/nostr";
import { RootState } from "State/Store";
import { HexKey, Lists } from "@snort/nostr";
import useNotelistSubscription from "Feed/useNotelistSubscription";
import useNotelistSubscription from "Hooks/useNotelistSubscription";
export default function useBookmarkFeed(pubkey?: HexKey) {
const { bookmarked } = useSelector((s: RootState) => s.login);

View File

@ -1,14 +1,13 @@
import { useMemo } from "react";
import { useSelector } from "react-redux";
import * as secp from "@noble/secp256k1";
import { EventKind, RelaySettings, TaggedRawEvent, HexKey, RawEvent, u256, UserMetadata, Lists } from "@snort/nostr";
import { TaggedRawEvent } from "@snort/nostr";
import { EventKind, Tag, Event as NEvent, RelaySettings } from "@snort/nostr";
import { RootState } from "State/Store";
import { HexKey, RawEvent, u256, UserMetadata, Lists } from "@snort/nostr";
import { bech32ToHex, delay, unwrap } from "Util";
import { DefaultRelays, HashtagRegex } from "Const";
import { System } from "System";
import { useMemo } from "react";
import { EventExt } from "System/EventExt";
declare global {
interface Window {
@ -33,26 +32,27 @@ export default function useEventPublisher() {
const relays = useSelector((s: RootState) => s.login.relays);
const hasNip07 = "nostr" in window;
async function signEvent(ev: NEvent): Promise<NEvent> {
async function signEvent(ev: RawEvent): Promise<RawEvent> {
if (hasNip07 && !privKey) {
ev.Id = ev.CreateId();
const tmpEv = (await barrierNip07(() => window.nostr.signEvent(ev.ToObject()))) as RawEvent;
return new NEvent(tmpEv as TaggedRawEvent);
ev.id = await EventExt.createId(ev);
const tmpEv = (await barrierNip07(() => window.nostr.signEvent(ev))) as RawEvent;
ev.sig = tmpEv.sig;
return ev;
} else if (privKey) {
await ev.Sign(privKey);
await EventExt.sign(ev, privKey);
} else {
console.warn("Count not sign event, no private keys available");
}
return ev;
}
function processContent(ev: NEvent, msg: string) {
function processContent(ev: RawEvent, msg: string) {
const replaceNpub = (match: string) => {
const npub = match.slice(1);
try {
const hex = bech32ToHex(npub);
const idx = ev.Tags.length;
ev.Tags.push(new Tag(["p", hex], idx));
const idx = ev.tags.length;
ev.tags.push(["p", hex]);
return `#[${idx}]`;
} catch (error) {
return match;
@ -62,8 +62,8 @@ export default function useEventPublisher() {
const noteId = match.slice(1);
try {
const hex = bech32ToHex(noteId);
const idx = ev.Tags.length;
ev.Tags.push(new Tag(["e", hex, "", "mention"], idx));
const idx = ev.tags.length;
ev.tags.push(["e", hex, "", "mention"]);
return `#[${idx}]`;
} catch (error) {
return match;
@ -71,29 +71,26 @@ export default function useEventPublisher() {
};
const replaceHashtag = (match: string) => {
const tag = match.slice(1);
const idx = ev.Tags.length;
ev.Tags.push(new Tag(["t", tag.toLowerCase()], idx));
ev.tags.push(["t", tag.toLowerCase()]);
return match;
};
const content = msg
.replace(/@npub[a-z0-9]+/g, replaceNpub)
.replace(/@note1[acdefghjklmnpqrstuvwxyz023456789]{58}/g, replaceNoteId)
.replace(HashtagRegex, replaceHashtag);
ev.Content = content;
ev.content = content;
}
const ret = {
nip42Auth: async (challenge: string, relay: string) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.Auth;
ev.Content = "";
ev.Tags.push(new Tag(["relay", relay], 0));
ev.Tags.push(new Tag(["challenge", challenge], 1));
const ev = EventExt.forPubKey(pubKey, EventKind.Auth);
ev.tags.push(["relay", relay]);
ev.tags.push(["challenge", challenge]);
return await signEvent(ev);
}
},
broadcast: (ev: NEvent | undefined) => {
broadcast: (ev: RawEvent | undefined) => {
if (ev) {
console.debug("Sending event: ", ev);
System.BroadcastEvent(ev);
@ -104,7 +101,7 @@ export default function useEventPublisher() {
* If a user removes all the DefaultRelays from their relay list and saves that relay list,
* When they open the site again we wont see that updated relay list and so it will appear to reset back to the previous state
*/
broadcastForBootstrap: (ev: NEvent | undefined) => {
broadcastForBootstrap: (ev: RawEvent | undefined) => {
if (ev) {
for (const [k] of DefaultRelays) {
System.WriteOnceToRelay(k, ev);
@ -114,7 +111,7 @@ export default function useEventPublisher() {
/**
* Write event to all given relays.
*/
broadcastAll: (ev: NEvent | undefined, relays: string[]) => {
broadcastAll: (ev: RawEvent | undefined, relays: string[]) => {
if (ev) {
for (const k of relays) {
System.WriteOnceToRelay(k, ev);
@ -123,11 +120,10 @@ export default function useEventPublisher() {
},
muted: async (keys: HexKey[], priv: HexKey[]) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.PubkeyLists;
ev.Tags.push(new Tag(["d", Lists.Muted], ev.Tags.length));
const ev = EventExt.forPubKey(pubKey, EventKind.PubkeyLists);
ev.tags.push(["d", Lists.Muted]);
keys.forEach(p => {
ev.Tags.push(new Tag(["p", p], ev.Tags.length));
ev.tags.push(["p", p]);
});
let content = "";
if (priv.length > 0) {
@ -136,76 +132,67 @@ export default function useEventPublisher() {
if (hasNip07 && !privKey) {
content = await barrierNip07(() => window.nostr.nip04.encrypt(pubKey, plaintext));
} else if (privKey) {
content = await ev.EncryptData(plaintext, pubKey, privKey);
content = await EventExt.encryptData(plaintext, pubKey, privKey);
}
}
ev.Content = content;
ev.content = content;
return await signEvent(ev);
}
},
pinned: async (notes: HexKey[]) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.NoteLists;
ev.Tags.push(new Tag(["d", Lists.Pinned], ev.Tags.length));
const ev = EventExt.forPubKey(pubKey, EventKind.NoteLists);
ev.tags.push(["d", Lists.Pinned]);
notes.forEach(n => {
ev.Tags.push(new Tag(["e", n], ev.Tags.length));
ev.tags.push(["e", n]);
});
ev.Content = "";
return await signEvent(ev);
}
},
bookmarked: async (notes: HexKey[]) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.NoteLists;
ev.Tags.push(new Tag(["d", Lists.Bookmarked], ev.Tags.length));
const ev = EventExt.forPubKey(pubKey, EventKind.NoteLists);
ev.tags.push(["d", Lists.Bookmarked]);
notes.forEach(n => {
ev.Tags.push(new Tag(["e", n], ev.Tags.length));
ev.tags.push(["e", n]);
});
ev.Content = "";
return await signEvent(ev);
}
},
tags: async (tags: string[]) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.TagLists;
ev.Tags.push(new Tag(["d", Lists.Followed], ev.Tags.length));
const ev = EventExt.forPubKey(pubKey, EventKind.TagLists);
ev.tags.push(["d", Lists.Followed]);
tags.forEach(t => {
ev.Tags.push(new Tag(["t", t], ev.Tags.length));
ev.tags.push(["t", t]);
});
ev.Content = "";
return await signEvent(ev);
}
},
metadata: async (obj: UserMetadata) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.SetMetadata;
ev.Content = JSON.stringify(obj);
const ev = EventExt.forPubKey(pubKey, EventKind.SetMetadata);
ev.content = JSON.stringify(obj);
return await signEvent(ev);
}
},
note: async (msg: string) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.TextNote;
const ev = EventExt.forPubKey(pubKey, EventKind.TextNote);
processContent(ev, msg);
return await signEvent(ev);
}
},
zap: async (amount: number, author: HexKey, note?: HexKey, msg?: string) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.ZapRequest;
const ev = EventExt.forPubKey(pubKey, EventKind.ZapRequest);
if (note) {
ev.Tags.push(new Tag(["e", note], ev.Tags.length));
ev.tags.push(["e", note]);
}
ev.Tags.push(new Tag(["p", author], ev.Tags.length));
ev.tags.push(["p", author]);
const relayTag = ["relays", ...Object.keys(relays).map(a => a.trim())];
ev.Tags.push(new Tag(relayTag, ev.Tags.length));
ev.Tags.push(new Tag(["amount", amount.toString()], ev.Tags.length));
ev.tags.push(relayTag);
ev.tags.push(["amount", amount.toString()]);
processContent(ev, msg || "");
return await signEvent(ev);
}
@ -213,57 +200,54 @@ export default function useEventPublisher() {
/**
* Reply to a note
*/
reply: async (replyTo: NEvent, msg: string) => {
reply: async (replyTo: TaggedRawEvent, msg: string) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.TextNote;
const ev = EventExt.forPubKey(pubKey, EventKind.TextNote);
const thread = replyTo.Thread;
const thread = EventExt.extractThread(ev);
if (thread) {
if (thread.Root || thread.ReplyTo) {
ev.Tags.push(new Tag(["e", thread.Root?.Event ?? thread.ReplyTo?.Event ?? "", "", "root"], ev.Tags.length));
if (thread.root || thread.replyTo) {
ev.tags.push(["e", thread.root?.Event ?? thread.replyTo?.Event ?? "", "", "root"]);
}
ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], ev.Tags.length));
ev.tags.push(["e", replyTo.id, replyTo.relays[0] ?? "", "reply"]);
// dont tag self in replies
if (replyTo.PubKey !== pubKey) {
ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length));
if (replyTo.pubkey !== pubKey) {
ev.tags.push(["p", replyTo.pubkey]);
}
for (const pk of thread.PubKeys) {
for (const pk of thread.pubKeys) {
if (pk === pubKey) {
continue; // dont tag self in replies
}
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
ev.tags.push(["p", pk]);
}
} else {
ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], 0));
ev.tags.push(["e", replyTo.id, "", "reply"]);
// dont tag self in replies
if (replyTo.PubKey !== pubKey) {
ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length));
if (replyTo.pubkey !== pubKey) {
ev.tags.push(["p", replyTo.pubkey]);
}
}
processContent(ev, msg);
return await signEvent(ev);
}
},
react: async (evRef: NEvent, content = "+") => {
react: async (evRef: RawEvent, content = "+") => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.Reaction;
ev.Content = content;
ev.Tags.push(new Tag(["e", evRef.Id], 0));
ev.Tags.push(new Tag(["p", evRef.PubKey], 1));
const ev = EventExt.forPubKey(pubKey, EventKind.Reaction);
ev.content = content;
ev.tags.push(["e", evRef.id]);
ev.tags.push(["p", evRef.pubkey]);
return await signEvent(ev);
}
},
saveRelays: async () => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.ContactList;
ev.Content = JSON.stringify(relays);
const ev = EventExt.forPubKey(pubKey, EventKind.ContactList);
ev.content = JSON.stringify(relays);
for (const pk of follows) {
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
ev.tags.push(["p", pk]);
}
return await signEvent(ev);
@ -271,9 +255,7 @@ export default function useEventPublisher() {
},
saveRelaysSettings: async () => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.Relays;
ev.Content = "";
const ev = EventExt.forPubKey(pubKey, EventKind.Relays);
for (const [url, settings] of Object.entries(relays)) {
const rTag = ["r", url];
if (settings.read && !settings.write) {
@ -282,16 +264,15 @@ export default function useEventPublisher() {
if (settings.write && !settings.read) {
rTag.push("write");
}
ev.Tags.push(new Tag(rTag, ev.Tags.length));
ev.tags.push(rTag);
}
return await signEvent(ev);
}
},
addFollow: async (pkAdd: HexKey | HexKey[], newRelays?: Record<string, RelaySettings>) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.ContactList;
ev.Content = JSON.stringify(newRelays ?? relays);
const ev = EventExt.forPubKey(pubKey, EventKind.ContactList);
ev.content = JSON.stringify(newRelays ?? relays);
const temp = new Set(follows);
if (Array.isArray(pkAdd)) {
pkAdd.forEach(a => temp.add(a));
@ -302,7 +283,7 @@ export default function useEventPublisher() {
if (pk.length !== 64) {
continue;
}
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
ev.tags.push(["p", pk.toLowerCase()]);
}
return await signEvent(ev);
@ -310,14 +291,13 @@ export default function useEventPublisher() {
},
removeFollow: async (pkRemove: HexKey) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.ContactList;
ev.Content = JSON.stringify(relays);
const ev = EventExt.forPubKey(pubKey, EventKind.ContactList);
ev.content = JSON.stringify(relays);
for (const pk of follows) {
if (pk === pkRemove || pk.length !== 64) {
continue;
}
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
ev.tags.push(["p", pk]);
}
return await signEvent(ev);
@ -328,39 +308,33 @@ export default function useEventPublisher() {
*/
delete: async (id: u256) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.Deletion;
ev.Content = "";
ev.Tags.push(new Tag(["e", id], 0));
const ev = EventExt.forPubKey(pubKey, EventKind.Deletion);
ev.tags.push(["e", id]);
return await signEvent(ev);
}
},
/**
* Repost a note (NIP-18)
*/
repost: async (note: NEvent) => {
repost: async (note: TaggedRawEvent) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.Repost;
ev.Content = JSON.stringify(note.Original);
ev.Tags.push(new Tag(["e", note.Id], 0));
ev.Tags.push(new Tag(["p", note.PubKey], 1));
const ev = EventExt.forPubKey(pubKey, EventKind.Repost);
ev.tags.push(["e", note.id, ""]);
ev.tags.push(["p", note.pubkey]);
return await signEvent(ev);
}
},
decryptDm: async (note: NEvent): Promise<string | undefined> => {
decryptDm: async (note: RawEvent): Promise<string | undefined> => {
if (pubKey) {
if (note.PubKey !== pubKey && !note.Tags.some(a => a.PubKey === pubKey)) {
if (note.pubkey !== pubKey && !note.tags.some(a => a[1] === pubKey)) {
return "<CANT DECRYPT>";
}
try {
const otherPubKey =
note.PubKey === pubKey ? unwrap(note.Tags.filter(a => a.Key === "p")[0].PubKey) : note.PubKey;
const otherPubKey = note.pubkey === pubKey ? unwrap(note.tags.filter(a => a[0] === "p")[0][1]) : note.pubkey;
if (hasNip07 && !privKey) {
return await barrierNip07(() => window.nostr.nip04.decrypt(otherPubKey, note.Content));
return await barrierNip07(() => window.nostr.nip04.decrypt(otherPubKey, note.content));
} else if (privKey) {
await note.DecryptDm(privKey, otherPubKey);
return note.Content;
return await EventExt.decryptDm(note.content, privKey, otherPubKey);
}
} catch (e) {
console.error("Decryption failed", e);
@ -370,18 +344,17 @@ export default function useEventPublisher() {
},
sendDm: async (content: string, to: HexKey) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.DirectMessage;
ev.Content = content;
ev.Tags.push(new Tag(["p", to], 0));
const ev = EventExt.forPubKey(pubKey, EventKind.DirectMessage);
ev.content = content;
ev.tags.push(["p", to]);
try {
if (hasNip07 && !privKey) {
const cx: string = await barrierNip07(() => window.nostr.nip04.encrypt(to, content));
ev.Content = cx;
ev.content = cx;
return await signEvent(ev);
} else if (privKey) {
await ev.EncryptDmForPubkey(to, privKey);
ev.content = await EventExt.encryptData(content, to, privKey);
return await signEvent(ev);
}
} catch (e) {
@ -399,9 +372,8 @@ export default function useEventPublisher() {
},
generic: async (content: string, kind: EventKind) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = kind;
ev.Content = content;
const ev = EventExt.forPubKey(pubKey, kind);
ev.content = content;
return await signEvent(ev);
}
},

View File

@ -1,23 +1,21 @@
import { useMemo } from "react";
import { HexKey } from "@snort/nostr";
import { EventKind, Subscriptions } from "@snort/nostr";
import useSubscription from "Feed/Subscription";
import { HexKey, EventKind } from "@snort/nostr";
import { PubkeyReplaceableNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
export default function useFollowersFeed(pubkey?: HexKey) {
const sub = useMemo(() => {
if (!pubkey) return null;
const x = new Subscriptions();
x.Id = `followers:${pubkey.slice(0, 12)}`;
x.Kinds = new Set([EventKind.ContactList]);
x.PTags = new Set([pubkey]);
return x;
const b = new RequestBuilder(`followers:${pubkey.slice(0, 12)}`);
b.withFilter().kinds([EventKind.ContactList]).tag("p", [pubkey]);
return b;
}, [pubkey]);
const followersFeed = useSubscription(sub, { leaveOpen: false, cache: true });
const followersFeed = useRequestBuilder<PubkeyReplaceableNoteStore>(PubkeyReplaceableNoteStore, sub);
const followers = useMemo(() => {
const contactLists = followersFeed?.store.notes.filter(
const contactLists = followersFeed.data?.filter(
a => a.kind === EventKind.ContactList && a.tags.some(b => b[0] === "p" && b[1] === pubkey)
);
return [...new Set(contactLists?.map(a => a.pubkey))];

View File

@ -1,9 +1,10 @@
import { useMemo } from "react";
import { useSelector } from "react-redux";
import { HexKey, TaggedRawEvent, EventKind, Subscriptions } from "@snort/nostr";
import { HexKey, TaggedRawEvent, EventKind } from "@snort/nostr";
import useSubscription from "Feed/Subscription";
import { RootState } from "State/Store";
import { PubkeyReplaceableNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
export default function useFollowsFeed(pubkey?: HexKey) {
const { publicKey, follows } = useSelector((s: RootState) => s.login);
@ -11,24 +12,22 @@ export default function useFollowsFeed(pubkey?: HexKey) {
const sub = useMemo(() => {
if (isMe || !pubkey) return null;
const x = new Subscriptions();
x.Id = `follows:${pubkey.slice(0, 12)}`;
x.Kinds = new Set([EventKind.ContactList]);
x.Authors = new Set([pubkey]);
return x;
const b = new RequestBuilder(`follows:${pubkey.slice(0, 12)}`);
b.withFilter().kinds([EventKind.ContactList]).authors([pubkey]);
return b;
}, [isMe, pubkey]);
const contactFeed = useSubscription(sub, { leaveOpen: false, cache: true });
const contactFeed = useRequestBuilder<PubkeyReplaceableNoteStore>(PubkeyReplaceableNoteStore, sub);
return useMemo(() => {
if (isMe) {
return follows;
}
return getFollowing(contactFeed.store.notes ?? [], pubkey);
}, [contactFeed.store, follows, pubkey]);
return getFollowing(contactFeed.data ?? [], pubkey);
}, [contactFeed, follows, pubkey]);
}
export function getFollowing(notes: TaggedRawEvent[], pubkey?: HexKey) {
export function getFollowing(notes: readonly TaggedRawEvent[], pubkey?: HexKey) {
const contactLists = notes.filter(a => a.kind === EventKind.ContactList && a.pubkey === pubkey);
const pTags = contactLists?.map(a => a.tags.filter(b => b[0] === "p").map(c => c[1]));
return [...new Set(pTags?.flat())];

View File

@ -1,10 +1,10 @@
import { useEffect, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { AnyAction, ThunkDispatch } from "@reduxjs/toolkit";
import { TaggedRawEvent, HexKey, Lists, EventKind } from "@snort/nostr";
import { getNewest } from "Util";
import { getNewest, getNewestEventTagsByKey, unwrap } from "Util";
import { makeNotification } from "Notifications";
import { TaggedRawEvent, HexKey, Lists } from "@snort/nostr";
import { Event, EventKind, Subscriptions } from "@snort/nostr";
import {
addDirectMessage,
setFollows,
@ -18,11 +18,12 @@ import {
setLatestNotifications,
} from "State/Login";
import { RootState } from "State/Store";
import useSubscription from "Feed/Subscription";
import { barrierNip07 } from "Feed/EventPublisher";
import { getMutedKeys } from "Feed/MuteList";
import useModeration from "Hooks/useModeration";
import { AnyAction, ThunkDispatch } from "@reduxjs/toolkit";
import { FlatNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
import { EventExt } from "System/EventExt";
/**
* Managed loading data for the current logged in user
@ -37,143 +38,75 @@ export default function useLoginFeed() {
} = useSelector((s: RootState) => s.login);
const { isMuted } = useModeration();
const subMetadata = useMemo(() => {
const subLogin = useMemo(() => {
if (!pubKey) return null;
const sub = new Subscriptions();
sub.Id = `login:meta`;
sub.Authors = new Set([pubKey]);
sub.Kinds = new Set([EventKind.ContactList]);
sub.Limit = 2;
return sub;
}, [pubKey]);
const subNotification = useMemo(() => {
if (!pubKey) return null;
const sub = new Subscriptions();
sub.Id = "login:notifications";
// todo: add zaps
sub.Kinds = new Set([EventKind.TextNote]);
sub.PTags = new Set([pubKey]);
sub.Limit = 1;
return sub;
}, [pubKey]);
const subMuted = useMemo(() => {
if (!pubKey) return null;
const sub = new Subscriptions();
sub.Id = "login:muted";
sub.Kinds = new Set([EventKind.PubkeyLists]);
sub.Authors = new Set([pubKey]);
sub.DTags = new Set([Lists.Muted]);
sub.Limit = 1;
return sub;
}, [pubKey]);
const subTags = useMemo(() => {
if (!pubKey) return null;
const sub = new Subscriptions();
sub.Id = "login:tags";
sub.Kinds = new Set([EventKind.TagLists]);
sub.Authors = new Set([pubKey]);
sub.DTags = new Set([Lists.Followed]);
sub.Limit = 1;
return sub;
}, [pubKey]);
const subPinned = useMemo(() => {
if (!pubKey) return null;
const sub = new Subscriptions();
sub.Id = "login:pinned";
sub.Kinds = new Set([EventKind.NoteLists]);
sub.Authors = new Set([pubKey]);
sub.DTags = new Set([Lists.Pinned]);
sub.Limit = 1;
return sub;
}, [pubKey]);
const subBookmarks = useMemo(() => {
if (!pubKey) return null;
const sub = new Subscriptions();
sub.Id = "login:bookmarks";
sub.Kinds = new Set([EventKind.NoteLists]);
sub.Authors = new Set([pubKey]);
sub.DTags = new Set([Lists.Bookmarked]);
sub.Limit = 1;
return sub;
}, [pubKey]);
const subDms = useMemo(() => {
if (!pubKey) return null;
const dms = new Subscriptions();
dms.Id = "login:dms";
dms.Kinds = new Set([EventKind.DirectMessage]);
dms.PTags = new Set([pubKey]);
const dmsFromME = new Subscriptions();
dmsFromME.Authors = new Set([pubKey]);
dmsFromME.Kinds = new Set([EventKind.DirectMessage]);
dms.AddSubscription(dmsFromME);
return dms;
}, [pubKey]);
const metadataFeed = useSubscription(subMetadata, {
leaveOpen: true,
cache: true,
});
const notificationFeed = useSubscription(subNotification, {
leaveOpen: true,
cache: true,
});
const dmsFeed = useSubscription(subDms, { leaveOpen: true, cache: true });
const mutedFeed = useSubscription(subMuted, { leaveOpen: true, cache: true });
const pinnedFeed = useSubscription(subPinned, { leaveOpen: true, cache: true });
const tagsFeed = useSubscription(subTags, { leaveOpen: true, cache: true });
const bookmarkFeed = useSubscription(subBookmarks, { leaveOpen: true, cache: true });
useEffect(() => {
const contactList = metadataFeed.store.notes.filter(a => a.kind === EventKind.ContactList);
for (const cl of contactList) {
if (cl.content !== "" && cl.content !== "{}") {
const relays = JSON.parse(cl.content);
dispatch(setRelays({ relays, createdAt: cl.created_at }));
}
const pTags = cl.tags.filter(a => a[0] === "p").map(a => a[1]);
dispatch(setFollows({ keys: pTags, createdAt: cl.created_at }));
}
}, [dispatch, metadataFeed.store]);
useEffect(() => {
const replies = notificationFeed.store.notes.filter(
a => a.kind === EventKind.TextNote && !isMuted(a.pubkey) && a.created_at > readNotifications
);
replies.forEach(nx => {
dispatch(setLatestNotifications(nx.created_at));
makeNotification(nx).then(notification => {
if (notification) {
(dispatch as ThunkDispatch<RootState, undefined, AnyAction>)(sendNotification(notification));
}
});
const b = new RequestBuilder("login");
b.withOptions({
leaveOpen: true,
});
}, [dispatch, notificationFeed.store, readNotifications]);
b.withFilter().authors([pubKey]).kinds([EventKind.ContactList, EventKind.DirectMessage]);
b.withFilter().kinds([EventKind.TextNote]).tag("p", [pubKey]).limit(1);
b.withFilter().kinds([EventKind.DirectMessage]).tag("p", [pubKey]);
return b;
}, [pubKey]);
const subLists = useMemo(() => {
if (!pubKey) return null;
const b = new RequestBuilder("login:lists");
b.withOptions({
leaveOpen: true,
});
b.withFilter()
.authors([pubKey])
.kinds([EventKind.PubkeyLists])
.tag("d", [Lists.Muted, Lists.Followed, Lists.Pinned, Lists.Bookmarked]);
return b;
}, [pubKey]);
const loginFeed = useRequestBuilder<FlatNoteStore>(FlatNoteStore, subLogin);
// update relays and follow lists
useEffect(() => {
const muted = getMutedKeys(mutedFeed.store.notes);
if (loginFeed.data) {
const contactList = getNewest(loginFeed.data.filter(a => a.kind === EventKind.ContactList));
if (contactList) {
if (contactList.content !== "" && contactList.content !== "{}") {
const relays = JSON.parse(contactList.content);
dispatch(setRelays({ relays, createdAt: contactList.created_at }));
}
const pTags = contactList.tags.filter(a => a[0] === "p").map(a => a[1]);
dispatch(setFollows({ keys: pTags, createdAt: contactList.created_at }));
}
const dms = loginFeed.data.filter(a => a.kind === EventKind.DirectMessage);
dispatch(addDirectMessage(dms));
}
}, [dispatch, loginFeed]);
// send out notifications
useEffect(() => {
if (loginFeed.data) {
const replies = loginFeed.data.filter(
a => a.kind === EventKind.TextNote && !isMuted(a.pubkey) && a.created_at > readNotifications
);
replies.forEach(nx => {
dispatch(setLatestNotifications(nx.created_at));
makeNotification(nx).then(notification => {
if (notification) {
(dispatch as ThunkDispatch<RootState, undefined, AnyAction>)(sendNotification(notification));
}
});
});
}
}, [dispatch, loginFeed, readNotifications]);
function handleMutedFeed(mutedFeed: TaggedRawEvent[]) {
const muted = getMutedKeys(mutedFeed);
dispatch(setMuted(muted));
const newest = getNewest(mutedFeed.store.notes);
const newest = getNewest(mutedFeed);
if (newest && newest.content.length > 0 && pubKey && newest.created_at > latestMuted) {
decryptBlocked(newest, pubKey, privKey)
.then(plaintext => {
@ -192,57 +125,64 @@ export default function useLoginFeed() {
})
.catch(error => console.warn(error));
}
}, [dispatch, mutedFeed.store]);
}
useEffect(() => {
const newest = getNewest(pinnedFeed.store.notes);
function handlePinnedFeed(pinnedFeed: TaggedRawEvent[]) {
const newest = getNewestEventTagsByKey(pinnedFeed, "e");
if (newest) {
const keys = newest.tags.filter(p => p && p.length === 2 && p[0] === "e").map(p => p[1]);
dispatch(
setPinned({
keys,
createdAt: newest.created_at,
})
);
dispatch(setPinned(newest));
}
}, [dispatch, pinnedFeed.store]);
}
useEffect(() => {
const newest = getNewest(tagsFeed.store.notes);
function handleTagFeed(tagFeed: TaggedRawEvent[]) {
const newest = getNewestEventTagsByKey(tagFeed, "t");
if (newest) {
const tags = newest.tags.filter(p => p && p.length === 2 && p[0] === "t").map(p => p[1]);
dispatch(
setTags({
tags,
createdAt: newest.created_at,
tags: newest.keys,
createdAt: newest.createdAt,
})
);
}
}, [dispatch, tagsFeed.store]);
}
useEffect(() => {
const newest = getNewest(bookmarkFeed.store.notes);
function handleBookmarkFeed(bookmarkFeed: TaggedRawEvent[]) {
const newest = getNewestEventTagsByKey(bookmarkFeed, "e");
if (newest) {
const keys = newest.tags.filter(p => p && p.length === 2 && p[0] === "e").map(p => p[1]);
dispatch(
setBookmarked({
keys,
createdAt: newest.created_at,
})
);
dispatch(setBookmarked(newest));
}
}, [dispatch, bookmarkFeed.store]);
}
const listsFeed = useRequestBuilder<FlatNoteStore>(FlatNoteStore, subLists);
useEffect(() => {
const dms = dmsFeed.store.notes.filter(a => a.kind === EventKind.DirectMessage);
dispatch(addDirectMessage(dms));
}, [dispatch, dmsFeed.store]);
if (listsFeed.data) {
const getList = (evs: readonly TaggedRawEvent[], list: Lists) =>
evs.filter(a => unwrap(a.tags.find(b => b[0] === "d"))[1] === list);
const mutedFeed = getList(listsFeed.data, Lists.Muted);
handleMutedFeed(mutedFeed);
const pinnedFeed = getList(listsFeed.data, Lists.Pinned);
handlePinnedFeed(pinnedFeed);
const tagsFeed = getList(listsFeed.data, Lists.Followed);
handleTagFeed(tagsFeed);
const bookmarkFeed = getList(listsFeed.data, Lists.Bookmarked);
handleBookmarkFeed(bookmarkFeed);
}
}, [dispatch, listsFeed]);
/*const fRelays = useRelaysFeedFollows(follows);
useEffect(() => {
FollowsRelays.bulkSet(fRelays).catch(console.error);
}, [dispatch, fRelays]);*/
}
async function decryptBlocked(raw: TaggedRawEvent, pubKey: HexKey, privKey?: HexKey) {
const ev = new Event(raw);
if (pubKey && privKey) {
return await ev.DecryptData(raw.content, privKey, pubKey);
return await EventExt.decryptData(raw.content, privKey, pubKey);
} else {
return await barrierNip07(() => window.nostr.nip04.decrypt(pubKey, raw.content));
}

View File

@ -2,10 +2,11 @@ import { useMemo } from "react";
import { useSelector } from "react-redux";
import { getNewest } from "Util";
import { HexKey, TaggedRawEvent, Lists } from "@snort/nostr";
import { EventKind, Subscriptions } from "@snort/nostr";
import useSubscription, { NoteStore } from "Feed/Subscription";
import { HexKey, TaggedRawEvent, Lists, EventKind } from "@snort/nostr";
import { RootState } from "State/Store";
import { ParameterizedReplaceableNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
export default function useMutedFeed(pubkey?: HexKey) {
const { publicKey, muted } = useSelector((s: RootState) => s.login);
@ -13,23 +14,19 @@ export default function useMutedFeed(pubkey?: HexKey) {
const sub = useMemo(() => {
if (isMe || !pubkey) return null;
const sub = new Subscriptions();
sub.Id = `muted:${pubkey.slice(0, 12)}`;
sub.Kinds = new Set([EventKind.PubkeyLists]);
sub.Authors = new Set([pubkey]);
sub.DTags = new Set([Lists.Muted]);
sub.Limit = 1;
return sub;
const b = new RequestBuilder(`muted:${pubkey.slice(0, 12)}`);
b.withFilter().authors([pubkey]).kinds([EventKind.PubkeyLists]).tag("d", [Lists.Muted]);
return b;
}, [pubkey]);
const mutedFeed = useSubscription(sub, { leaveOpen: false, cache: true });
const mutedFeed = useRequestBuilder<ParameterizedReplaceableNoteStore>(ParameterizedReplaceableNoteStore, sub);
const mutedList = useMemo(() => {
if (pubkey) {
return getMuted(mutedFeed.store, pubkey);
if (pubkey && mutedFeed.data) {
return getMuted(mutedFeed.data, pubkey);
}
return [];
}, [mutedFeed.store, pubkey]);
}, [mutedFeed, pubkey]);
return isMe ? muted : mutedList;
}
@ -50,7 +47,7 @@ export function getMutedKeys(rawNotes: TaggedRawEvent[]): {
return { createdAt: 0, keys: [] };
}
export function getMuted(feed: NoteStore, pubkey: HexKey): HexKey[] {
const lists = feed?.notes.filter(a => a.kind === EventKind.PubkeyLists && a.pubkey === pubkey);
export function getMuted(feed: readonly TaggedRawEvent[], pubkey: HexKey): HexKey[] {
const lists = feed.filter(a => a.kind === EventKind.PubkeyLists && a.pubkey === pubkey);
return getMutedKeys(lists).keys;
}

View File

@ -2,7 +2,7 @@ import { useSelector } from "react-redux";
import { RootState } from "State/Store";
import { HexKey, Lists } from "@snort/nostr";
import useNotelistSubscription from "Feed/useNotelistSubscription";
import useNotelistSubscription from "Hooks/useNotelistSubscription";
export default function usePinnedFeed(pubkey?: HexKey) {
const { pinned } = useSelector((s: RootState) => s.login);

View File

@ -1,28 +1,26 @@
import { useMemo } from "react";
import { HexKey, FullRelaySettings } from "@snort/nostr";
import { EventKind, Subscriptions } from "@snort/nostr";
import useSubscription from "./Subscription";
import { HexKey, FullRelaySettings, EventKind } from "@snort/nostr";
import { RequestBuilder } from "System";
import { ReplaceableNoteStore } from "System/NoteCollection";
import useRequestBuilder from "Hooks/useRequestBuilder";
export default function useRelaysFeed(pubkey?: HexKey) {
const sub = useMemo(() => {
if (!pubkey) return null;
const x = new Subscriptions();
x.Id = `relays:${pubkey.slice(0, 12)}`;
x.Kinds = new Set([EventKind.ContactList]);
x.Authors = new Set([pubkey]);
x.Limit = 1;
return x;
const b = new RequestBuilder(`relays:${pubkey.slice(0, 12)}`);
b.withFilter().authors([pubkey]).kinds([EventKind.ContactList]);
return b;
}, [pubkey]);
const relays = useSubscription(sub, { leaveOpen: false, cache: false });
const eventContent = relays.store.notes[0]?.content;
const relays = useRequestBuilder<ReplaceableNoteStore>(ReplaceableNoteStore, sub);
if (!eventContent) {
if (!relays.data?.content) {
return [] as FullRelaySettings[];
}
try {
return Object.entries(JSON.parse(eventContent)).map(([url, settings]) => ({
return Object.entries(JSON.parse(relays.data.content)).map(([url, settings]) => ({
url,
settings,
})) as FullRelaySettings[];

View File

@ -0,0 +1,73 @@
import { useMemo } from "react";
import { HexKey, FullRelaySettings, TaggedRawEvent, RelaySettings, EventKind } from "@snort/nostr";
import { sanitizeRelayUrl } from "Util";
import { PubkeyReplaceableNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
type UserRelayMap = Record<HexKey, Array<FullRelaySettings>>;
export default function useRelaysFeedFollows(pubkeys: HexKey[]): UserRelayMap {
const sub = useMemo(() => {
const b = new RequestBuilder(`relays:follows`);
b.withFilter().authors(pubkeys).kinds([EventKind.Relays, EventKind.ContactList]);
return b;
}, [pubkeys]);
function mapFromRelays(notes: Array<TaggedRawEvent>): UserRelayMap {
return Object.fromEntries(
notes.map(ev => {
return [
ev.pubkey,
ev.tags
.map(a => {
return {
url: sanitizeRelayUrl(a[1]),
settings: {
read: a[2] === "read" || a[2] === undefined,
write: a[2] === "write" || a[2] === undefined,
},
} as FullRelaySettings;
})
.filter(a => a.url !== undefined),
];
})
);
}
function mapFromContactList(notes: Array<TaggedRawEvent>): UserRelayMap {
return Object.fromEntries(
notes.map(ev => {
if (ev.content !== "" && ev.content !== "{}" && ev.content.startsWith("{") && ev.content.endsWith("}")) {
try {
const relays: Record<string, RelaySettings> = JSON.parse(ev.content);
return [
ev.pubkey,
Object.entries(relays)
.map(([k, v]) => {
return {
url: sanitizeRelayUrl(k),
settings: v,
} as FullRelaySettings;
})
.filter(a => a.url !== undefined),
];
} catch {
// ignored
}
}
return [ev.pubkey, []];
})
);
}
const relays = useRequestBuilder<PubkeyReplaceableNoteStore>(PubkeyReplaceableNoteStore, sub);
const notesRelays = relays.data?.filter(a => a.kind === EventKind.Relays) ?? [];
const notesContactLists = relays.data?.filter(a => a.kind === EventKind.ContactList) ?? [];
return useMemo(() => {
return {
...mapFromContactList(notesContactLists),
...mapFromRelays(notesRelays),
} as UserRelayMap;
}, [relays]);
}

View File

@ -1,152 +0,0 @@
import { useEffect, useMemo, useReducer, useState } from "react";
import { TaggedRawEvent } from "@snort/nostr";
import { Subscriptions } from "@snort/nostr";
import { System } from "System";
import { debounce } from "Util";
export type NoteStore = {
notes: Array<TaggedRawEvent>;
end: boolean;
};
export type UseSubscriptionOptions = {
leaveOpen: boolean;
cache: boolean;
relay?: string;
};
interface ReducerArg {
type: "END" | "EVENT" | "CLEAR";
ev?: TaggedRawEvent | TaggedRawEvent[];
end?: boolean;
}
function notesReducer(state: NoteStore, arg: ReducerArg) {
if (arg.type === "END") {
return {
notes: state.notes,
end: arg.end ?? true,
} as NoteStore;
}
if (arg.type === "CLEAR") {
return {
notes: [],
end: state.end,
} as NoteStore;
}
let evs = arg.ev;
if (!(evs instanceof Array)) {
evs = evs === undefined ? [] : [evs];
}
const existingIds = new Set(state.notes.map(a => a.id));
evs = evs.filter(a => !existingIds.has(a.id));
if (evs.length === 0) {
return state;
}
return {
notes: [...state.notes, ...evs],
} as NoteStore;
}
const initStore: NoteStore = {
notes: [],
end: false,
};
export interface UseSubscriptionState {
store: NoteStore;
clear: () => void;
append: (notes: TaggedRawEvent[]) => void;
}
/**
* Wait time before returning changed state
*/
const DebounceMs = 200;
/**
*
* @param {Subscriptions} sub
* @param {any} opt
* @returns
*/
export default function useSubscription(
sub: Subscriptions | null,
options?: UseSubscriptionOptions
): UseSubscriptionState {
const [state, dispatch] = useReducer(notesReducer, initStore);
const [debounceOutput, setDebounceOutput] = useState<number>(0);
const [subDebounce, setSubDebounced] = useState<Subscriptions>();
useEffect(() => {
if (sub) {
return debounce(DebounceMs, () => {
setSubDebounced(sub);
});
}
}, [sub, options]);
useEffect(() => {
if (subDebounce) {
dispatch({
type: "END",
end: false,
});
subDebounce.OnEvent = e => {
dispatch({
type: "EVENT",
ev: e,
});
};
subDebounce.OnEnd = c => {
if (!(options?.leaveOpen ?? false)) {
c.RemoveSubscription(subDebounce.Id);
if (subDebounce.IsFinished()) {
System.RemoveSubscription(subDebounce.Id);
}
}
dispatch({
type: "END",
end: true,
});
};
const subObj = subDebounce.ToObject();
console.debug("Adding sub: ", subObj);
if (options?.relay) {
System.AddSubscriptionToRelay(subDebounce, options.relay);
} else {
System.AddSubscription(subDebounce);
}
return () => {
console.debug("Removing sub: ", subObj);
subDebounce.OnEvent = () => undefined;
System.RemoveSubscription(subDebounce.Id);
};
}
}, [subDebounce]);
useEffect(() => {
return debounce(DebounceMs, () => {
setDebounceOutput(s => (s += 1));
});
}, [state]);
const stateDebounced = useMemo(() => state, [debounceOutput]);
return {
store: stateDebounced,
clear: () => {
dispatch({ type: "CLEAR" });
},
append: (n: TaggedRawEvent[]) => {
dispatch({
type: "EVENT",
ev: n,
});
},
};
}

View File

@ -1,63 +1,51 @@
import { useEffect, useMemo, useState } from "react";
import { u256 } from "@snort/nostr";
import { EventKind, Subscriptions } from "@snort/nostr";
import useSubscription from "Feed/Subscription";
import { useSelector } from "react-redux";
import { u256, EventKind } from "@snort/nostr";
import { RootState } from "State/Store";
import { UserPreferences } from "State/Login";
import { debounce, NostrLink } from "Util";
import { appendDedupe, debounce, NostrLink } from "Util";
import { FlatNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
export default function useThreadFeed(link: NostrLink) {
const [trackingEvents, setTrackingEvent] = useState<u256[]>([link.id]);
const pref = useSelector<RootState, UserPreferences>(s => s.login.preferences);
function addId(id: u256[]) {
setTrackingEvent(s => {
const orig = new Set(s);
if (id.some(a => !orig.has(a))) {
const tmp = new Set([...s, ...id]);
return Array.from(tmp);
} else {
return s;
}
});
}
const sub = useMemo(() => {
const thisSub = new Subscriptions();
thisSub.Id = `thread:${link.id.substring(0, 8)}`;
thisSub.Ids = new Set(trackingEvents);
const sub = new RequestBuilder(`thread:${link.id.substring(0, 8)}`);
sub.withOptions({
leaveOpen: true,
});
sub.withFilter().ids(trackingEvents);
sub
.withFilter()
.kinds(
pref.enableReactions
? [EventKind.Reaction, EventKind.TextNote, EventKind.Repost, EventKind.ZapReceipt]
: [EventKind.TextNote, EventKind.ZapReceipt]
)
.tag("e", trackingEvents);
// get replies to this event
const subRelated = new Subscriptions();
subRelated.Kinds = new Set(
pref.enableReactions
? [EventKind.Reaction, EventKind.TextNote, EventKind.Repost, EventKind.ZapReceipt]
: [EventKind.TextNote, EventKind.ZapReceipt]
);
subRelated.ETags = thisSub.Ids;
thisSub.AddSubscription(subRelated);
return thisSub;
return sub;
}, [trackingEvents, pref, link.id]);
const main = useSubscription(sub, { leaveOpen: true, cache: true });
const store = useRequestBuilder<FlatNoteStore>(FlatNoteStore, sub);
useEffect(() => {
if (main.store) {
return debounce(200, () => {
const mainNotes = main.store.notes.filter(a => a.kind === EventKind.TextNote);
if (store.data) {
return debounce(500, () => {
const mainNotes = store.data?.filter(a => a.kind === EventKind.TextNote) ?? [];
const eTags = mainNotes
.filter(a => a.kind === EventKind.TextNote)
.map(a => a.tags.filter(b => b[0] === "e").map(b => b[1]))
.flat();
const ids = mainNotes.map(a => a.id);
const allEvents = new Set([...eTags, ...ids]);
addId(Array.from(allEvents));
const eTagsMissing = eTags.filter(a => !mainNotes.some(b => b.id === a));
setTrackingEvent(s => appendDedupe(s, eTagsMissing));
});
}
}, [main.store]);
}, [store]);
return main.store;
return store;
}

View File

@ -1,16 +1,19 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { u256 } from "@snort/nostr";
import { EventKind, Subscriptions } from "@snort/nostr";
import { unixNow, unwrap, tagFilterOfTextRepost } from "Util";
import useSubscription from "Feed/Subscription";
import { useSelector } from "react-redux";
import { EventKind, u256 } from "@snort/nostr";
import { unixNow, unwrap, tagFilterOfTextRepost } from "Util";
import { RootState } from "State/Store";
import { UserPreferences } from "State/Login";
import { FlatNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
import useTimelineWindow from "Hooks/useTimelineWindow";
export interface TimelineFeedOptions {
method: "TIME_RANGE" | "LIMIT_UNTIL";
window?: number;
relay?: string;
now?: number;
}
export interface TimelineSubject {
@ -19,142 +22,141 @@ export interface TimelineSubject {
items: string[];
}
export type TimelineFeed = ReturnType<typeof useTimelineFeed>;
export default function useTimelineFeed(subject: TimelineSubject, options: TimelineFeedOptions) {
const now = unixNow();
const [window] = useState<number>(options.window ?? 60 * 60);
const [until, setUntil] = useState<number>(now);
const [since, setSince] = useState<number>(now - window);
const { now, since, until, older, setUntil } = useTimelineWindow({
window: options.window,
now: options.now ?? unixNow(),
});
const [trackingEvents, setTrackingEvent] = useState<u256[]>([]);
const [trackingParentEvents, setTrackingParentEvents] = useState<u256[]>([]);
const pref = useSelector<RootState, UserPreferences>(s => s.login.preferences);
const createSub = useCallback(() => {
const createBuilder = useCallback(() => {
if (subject.type !== "global" && subject.items.length === 0) {
return null;
}
const sub = new Subscriptions();
sub.Id = `timeline:${subject.type}:${subject.discriminator}`;
sub.Kinds = new Set([EventKind.TextNote, EventKind.Repost]);
const b = new RequestBuilder(`timeline:${subject.type}:${subject.discriminator}`);
const f = b.withFilter().kinds([EventKind.TextNote, EventKind.Repost]);
if (options.relay) {
b.withOptions({
leaveOpen: false,
relays: [options.relay],
});
}
switch (subject.type) {
case "pubkey": {
sub.Authors = new Set(subject.items);
f.authors(subject.items);
break;
}
case "hashtag": {
sub.HashTags = new Set(subject.items);
f.tag("t", subject.items);
break;
}
case "ptag": {
sub.PTags = new Set(subject.items);
f.tag("p", subject.items);
break;
}
case "keyword": {
sub.Kinds.add(EventKind.SetMetadata);
sub.Search = subject.items[0];
f.search(subject.items[0]);
break;
}
}
return sub;
}, [subject.type, subject.items, subject.discriminator, options.relay]);
return {
builder: b,
filter: f,
};
}, [subject.type, subject.items, subject.discriminator]);
const sub = useMemo(() => {
const sub = createSub();
if (sub) {
const rb = createBuilder();
if (rb) {
if (options.method === "LIMIT_UNTIL") {
sub.Until = until;
sub.Limit = 10;
rb.filter.until(until).limit(10);
} else {
sub.Since = since;
sub.Until = until;
rb.filter.since(since).until(until);
if (since === undefined) {
sub.Limit = 50;
rb.filter.limit(50);
}
}
if (pref.autoShowLatest) {
// copy properties of main sub but with limit 0
// this will put latest directly into main feed
const latestSub = new Subscriptions();
latestSub.Authors = sub.Authors;
latestSub.HashTags = sub.HashTags;
latestSub.PTags = sub.PTags;
latestSub.Kinds = sub.Kinds;
latestSub.Search = sub.Search;
latestSub.Limit = 1;
latestSub.Since = Math.floor(new Date().getTime() / 1000);
sub.AddSubscription(latestSub);
rb.builder
.withOptions({
leaveOpen: true,
})
.withFilter()
.authors(rb.filter.filter.authors)
.kinds(rb.filter.filter.kinds)
.tag("p", rb.filter.filter["#p"])
.tag("t", rb.filter.filter["#t"])
.search(rb.filter.filter.search)
.limit(1)
.since(now);
}
}
return sub;
}, [until, since, options.method, pref, createSub]);
return rb?.builder ?? null;
}, [until, since, options.method, pref, createBuilder]);
const main = useSubscription(sub, { leaveOpen: true, cache: subject.type !== "global", relay: options.relay });
const main = useRequestBuilder<FlatNoteStore>(FlatNoteStore, sub);
const subRealtime = useMemo(() => {
const subLatest = createSub();
if (subLatest && !pref.autoShowLatest) {
subLatest.Id = `${subLatest.Id}:latest`;
subLatest.Limit = 1;
subLatest.Since = Math.floor(new Date().getTime() / 1000);
const rb = createBuilder();
if (rb && !pref.autoShowLatest) {
rb.builder.withOptions({
leaveOpen: true,
});
rb.builder.id = `${rb.builder.id}:latest`;
rb.filter.limit(1).since(now);
}
return subLatest;
}, [pref, createSub]);
return rb?.builder ?? null;
}, [pref.autoShowLatest, createBuilder]);
const latest = useSubscription(subRealtime, {
leaveOpen: true,
cache: false,
relay: options.relay,
});
const latest = useRequestBuilder<FlatNoteStore>(FlatNoteStore, subRealtime);
useEffect(() => {
// clear store if chaning relays
main.clear();
latest.clear();
// clear store if changing relays
main.store.clear();
latest.store.clear();
}, [options.relay]);
const subNext = useMemo(() => {
let sub: Subscriptions | undefined;
const rb = new RequestBuilder(`timeline-related:${subject.type}`);
if (trackingEvents.length > 0) {
sub = new Subscriptions();
sub.Id = `timeline-related:${subject.type}`;
sub.Kinds = new Set(
pref.enableReactions ? [EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt] : [EventKind.ZapReceipt]
);
sub.ETags = new Set(trackingEvents);
rb.withFilter()
.kinds(
pref.enableReactions ? [EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt] : [EventKind.ZapReceipt]
)
.tag("e", trackingEvents);
}
return sub ?? null;
if (trackingParentEvents.length > 0) {
rb.withFilter().ids(trackingParentEvents);
}
return rb.numFilters > 0 ? rb : null;
}, [trackingEvents, pref, subject.type]);
const others = useSubscription(subNext, { leaveOpen: true, cache: subject.type !== "global", relay: options.relay });
const subParents = useMemo(() => {
if (trackingParentEvents.length > 0) {
const parents = new Subscriptions();
parents.Id = `timeline-parent:${subject.type}`;
parents.Ids = new Set(trackingParentEvents);
return parents;
}
return null;
}, [trackingParentEvents, subject.type]);
const parent = useSubscription(subParents, { leaveOpen: false, cache: false, relay: options.relay });
const related = useRequestBuilder<FlatNoteStore>(FlatNoteStore, subNext);
useEffect(() => {
if (main.store.notes.length > 0) {
if (main.data && main.data.length > 0) {
setTrackingEvent(s => {
const ids = main.store.notes.map(a => a.id);
const ids = (main.data ?? []).map(a => a.id);
if (ids.some(a => !s.includes(a))) {
return Array.from(new Set([...s, ...ids]));
}
return s;
});
const repostsByKind6 = main.store.notes
const repostsByKind6 = main.data
.filter(a => a.kind === EventKind.Repost && a.content === "")
.map(a => a.tags.find(b => b[0] === "e"))
.filter(a => a)
.map(a => unwrap(a)[1]);
const repostsByKind1 = main.store.notes
const repostsByKind1 = main.data
.filter(
a => (a.kind === EventKind.Repost || a.kind === EventKind.TextNote) && a.tags.some(tagFilterOfTextRepost(a))
)
@ -172,26 +174,29 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
});
}
}
}, [main.store]);
}, [main]);
return {
main: main.store,
related: others.store,
latest: latest.store,
parent: parent.store,
main: main.data,
related: related.data,
latest: latest.data,
loading: main.store.loading,
loadMore: () => {
console.debug("Timeline load more!");
if (options.method === "LIMIT_UNTIL") {
const oldest = main.store.notes.reduce((acc, v) => (acc = v.created_at < acc ? v.created_at : acc), unixNow());
setUntil(oldest);
} else {
setUntil(s => s - window);
setSince(s => s - window);
if (main.data) {
console.debug("Timeline load more!");
if (options.method === "LIMIT_UNTIL") {
const oldest = main.data.reduce((acc, v) => (acc = v.created_at < acc ? v.created_at : acc), unixNow());
setUntil(oldest);
} else {
older();
}
}
},
showLatest: () => {
main.append(latest.store.notes);
latest.clear();
if (latest.data) {
main.store.add(latest.data);
latest.store.clear();
}
},
};
}

View File

@ -1,26 +1,29 @@
import { useMemo } from "react";
import { HexKey, EventKind, Subscriptions } from "@snort/nostr";
import { HexKey, EventKind } from "@snort/nostr";
import { parseZap } from "Element/Zap";
import useSubscription from "./Subscription";
import { FlatNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
export default function useZapsFeed(pubkey?: HexKey) {
const sub = useMemo(() => {
if (!pubkey) return null;
const x = new Subscriptions();
x.Id = `zaps:${pubkey.slice(0, 12)}`;
x.Kinds = new Set([EventKind.ZapReceipt]);
x.PTags = new Set([pubkey]);
return x;
const b = new RequestBuilder(`zaps:${pubkey.slice(0, 12)}`);
b.withFilter().tag("p", [pubkey]).kinds([EventKind.ZapReceipt]);
return b;
}, [pubkey]);
const zapsFeed = useSubscription(sub, { leaveOpen: false, cache: true });
const zapsFeed = useRequestBuilder<FlatNoteStore>(FlatNoteStore, sub);
const zaps = useMemo(() => {
const profileZaps = zapsFeed.store.notes
.map(parseZap)
.filter(z => z.valid && z.receiver === pubkey && z.sender !== pubkey && !z.event);
profileZaps.sort((a, b) => b.amount - a.amount);
return profileZaps;
if (zapsFeed.data) {
const profileZaps = zapsFeed.data
.map(parseZap)
.filter(z => z.valid && z.receiver === pubkey && z.sender !== pubkey && !z.event);
profileZaps.sort((a, b) => b.amount - a.amount);
return profileZaps;
}
return [];
}, [zapsFeed]);
return zaps;

View File

@ -1,62 +0,0 @@
import { useMemo } from "react";
import { useSelector } from "react-redux";
import { getNewest } from "Util";
import { HexKey, Lists, EventKind, Subscriptions } from "@snort/nostr";
import useSubscription from "Feed/Subscription";
import { RootState } from "State/Store";
export default function useNotelistSubscription(pubkey: HexKey | undefined, l: Lists, defaultIds: HexKey[]) {
const { preferences, publicKey } = useSelector((s: RootState) => s.login);
const isMe = publicKey === pubkey;
const sub = useMemo(() => {
if (isMe || !pubkey) return null;
const sub = new Subscriptions();
sub.Id = `note-list-${l}:${pubkey.slice(0, 12)}`;
sub.Kinds = new Set([EventKind.NoteLists]);
sub.Authors = new Set([pubkey]);
sub.DTags = new Set([l]);
sub.Limit = 1;
return sub;
}, [pubkey]);
const { store } = useSubscription(sub, { leaveOpen: true, cache: true });
const etags = useMemo(() => {
if (isMe) return defaultIds;
const newest = getNewest(store.notes);
if (newest) {
const { tags } = newest;
return tags.filter(t => t[0] === "e").map(t => t[1]);
}
return [];
}, [store.notes, isMe, defaultIds]);
const esub = useMemo(() => {
if (!pubkey) return null;
const s = new Subscriptions();
s.Id = `${l}-notes:${pubkey.slice(0, 12)}`;
s.Kinds = new Set([EventKind.TextNote]);
s.Ids = new Set(etags);
return s;
}, [etags, pubkey]);
const subRelated = useMemo(() => {
let sub: Subscriptions | undefined;
if (etags.length > 0 && preferences.enableReactions) {
sub = new Subscriptions();
sub.Id = `${l}-related`;
sub.Kinds = new Set([EventKind.Reaction, EventKind.Repost, EventKind.Deletion, EventKind.ZapReceipt]);
sub.ETags = new Set(etags);
}
return sub ?? null;
}, [etags, preferences]);
const mainSub = useSubscription(esub, { leaveOpen: true, cache: true });
const relatedSub = useSubscription(subRelated, { leaveOpen: true, cache: true });
const notes = mainSub.store.notes.filter(e => etags.includes(e.id));
const related = relatedSub.store.notes;
return { notes, related };
}