refactor: RequestBuilder
This commit is contained in:
@ -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 ?? [];
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
@ -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))];
|
||||
|
@ -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())];
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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[];
|
||||
|
73
packages/app/src/Feed/RelaysFeedFollows.tsx
Normal file
73
packages/app/src/Feed/RelaysFeedFollows.tsx
Normal 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]);
|
||||
}
|
@ -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,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 };
|
||||
}
|
Reference in New Issue
Block a user