Add prettier formatting (#214)
* chore: add prettier * chore: format codebase
This commit is contained in:
@ -6,342 +6,371 @@ import EventKind from "Nostr/EventKind";
|
||||
import Tag from "Nostr/Tag";
|
||||
import { RootState } from "State/Store";
|
||||
import { HexKey, RawEvent, u256, UserMetadata, Lists } from "Nostr";
|
||||
import { bech32ToHex } from "Util"
|
||||
import { bech32ToHex } from "Util";
|
||||
import { DefaultRelays, HashtagRegex } from "Const";
|
||||
import { RelaySettings } from "Nostr/Connection";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
nostr: {
|
||||
getPublicKey: () => Promise<HexKey>,
|
||||
signEvent: (event: RawEvent) => Promise<RawEvent>,
|
||||
getRelays: () => Promise<Record<string, { read: boolean, write: boolean }>>,
|
||||
nip04: {
|
||||
encrypt: (pubkey: HexKey, content: string) => Promise<string>,
|
||||
decrypt: (pubkey: HexKey, content: string) => Promise<string>
|
||||
}
|
||||
}
|
||||
}
|
||||
interface Window {
|
||||
nostr: {
|
||||
getPublicKey: () => Promise<HexKey>;
|
||||
signEvent: (event: RawEvent) => Promise<RawEvent>;
|
||||
getRelays: () => Promise<
|
||||
Record<string, { read: boolean; write: boolean }>
|
||||
>;
|
||||
nip04: {
|
||||
encrypt: (pubkey: HexKey, content: string) => Promise<string>;
|
||||
decrypt: (pubkey: HexKey, content: string) => Promise<string>;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function useEventPublisher() {
|
||||
const pubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const privKey = useSelector<RootState, HexKey | undefined>(s => s.login.privateKey);
|
||||
const follows = useSelector<RootState, HexKey[]>(s => s.login.follows);
|
||||
const relays = useSelector((s: RootState) => s.login.relays);
|
||||
const hasNip07 = 'nostr' in window;
|
||||
const pubKey = useSelector<RootState, HexKey | undefined>(
|
||||
(s) => s.login.publicKey
|
||||
);
|
||||
const privKey = useSelector<RootState, HexKey | undefined>(
|
||||
(s) => s.login.privateKey
|
||||
);
|
||||
const follows = useSelector<RootState, HexKey[]>((s) => s.login.follows);
|
||||
const relays = useSelector((s: RootState) => s.login.relays);
|
||||
const hasNip07 = "nostr" in window;
|
||||
|
||||
async function signEvent(ev: NEvent): Promise<NEvent> {
|
||||
if (hasNip07 && !privKey) {
|
||||
ev.Id = await ev.CreateId();
|
||||
let tmpEv = await barierNip07(() => window.nostr.signEvent(ev.ToObject()));
|
||||
return new NEvent(tmpEv);
|
||||
} else if (privKey) {
|
||||
await ev.Sign(privKey);
|
||||
} else {
|
||||
console.warn("Count not sign event, no private keys available");
|
||||
}
|
||||
return ev;
|
||||
async function signEvent(ev: NEvent): Promise<NEvent> {
|
||||
if (hasNip07 && !privKey) {
|
||||
ev.Id = await ev.CreateId();
|
||||
let tmpEv = await barierNip07(() =>
|
||||
window.nostr.signEvent(ev.ToObject())
|
||||
);
|
||||
return new NEvent(tmpEv);
|
||||
} else if (privKey) {
|
||||
await ev.Sign(privKey);
|
||||
} else {
|
||||
console.warn("Count not sign event, no private keys available");
|
||||
}
|
||||
return ev;
|
||||
}
|
||||
|
||||
function processContent(ev: NEvent, 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));
|
||||
return `#[${idx}]`
|
||||
} catch (error) {
|
||||
return match
|
||||
}
|
||||
function processContent(ev: NEvent, 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));
|
||||
return `#[${idx}]`;
|
||||
} catch (error) {
|
||||
return match;
|
||||
}
|
||||
};
|
||||
const replaceNoteId = (match: string) => {
|
||||
try {
|
||||
const hex = bech32ToHex(match);
|
||||
const idx = ev.Tags.length;
|
||||
ev.Tags.push(new Tag(["e", hex, "", "mention"], idx));
|
||||
return `#[${idx}]`;
|
||||
} catch (error) {
|
||||
return match;
|
||||
}
|
||||
};
|
||||
const replaceHashtag = (match: string) => {
|
||||
const tag = match.slice(1);
|
||||
const idx = ev.Tags.length;
|
||||
ev.Tags.push(new Tag(["t", tag.toLowerCase()], idx));
|
||||
return match;
|
||||
};
|
||||
const content = msg
|
||||
.replace(/@npub[a-z0-9]+/g, replaceNpub)
|
||||
.replace(/note[a-z0-9]+/g, replaceNoteId)
|
||||
.replace(HashtagRegex, replaceHashtag);
|
||||
ev.Content = content;
|
||||
}
|
||||
|
||||
return {
|
||||
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));
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
broadcast: (ev: NEvent | undefined) => {
|
||||
if (ev) {
|
||||
console.debug("Sending event: ", ev);
|
||||
System.BroadcastEvent(ev);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Write event to DefaultRelays, this is important for profiles / relay lists to prevent bugs
|
||||
* 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) => {
|
||||
if (ev) {
|
||||
for (let [k, _] of DefaultRelays) {
|
||||
System.WriteOnceToRelay(k, ev);
|
||||
}
|
||||
const replaceNoteId = (match: string) => {
|
||||
try {
|
||||
const hex = bech32ToHex(match);
|
||||
const idx = ev.Tags.length;
|
||||
ev.Tags.push(new Tag(["e", hex, "", "mention"], idx));
|
||||
return `#[${idx}]`
|
||||
} catch (error) {
|
||||
return match
|
||||
}
|
||||
}
|
||||
},
|
||||
muted: async (keys: HexKey[], priv: HexKey[]) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.Lists;
|
||||
ev.Tags.push(new Tag(["d", Lists.Muted], ev.Tags.length));
|
||||
keys.forEach((p) => {
|
||||
ev.Tags.push(new Tag(["p", p], ev.Tags.length));
|
||||
});
|
||||
let content = "";
|
||||
if (priv.length > 0) {
|
||||
const ps = priv.map((p) => ["p", p]);
|
||||
const plaintext = JSON.stringify(ps);
|
||||
if (hasNip07 && !privKey) {
|
||||
content = await barierNip07(() =>
|
||||
window.nostr.nip04.encrypt(pubKey, plaintext)
|
||||
);
|
||||
} else if (privKey) {
|
||||
content = await ev.EncryptData(plaintext, pubKey, privKey);
|
||||
}
|
||||
}
|
||||
const replaceHashtag = (match: string) => {
|
||||
const tag = match.slice(1);
|
||||
const idx = ev.Tags.length;
|
||||
ev.Tags.push(new Tag(["t", tag.toLowerCase()], idx));
|
||||
return match;
|
||||
}
|
||||
const content = msg.replace(/@npub[a-z0-9]+/g, replaceNpub)
|
||||
.replace(/note[a-z0-9]+/g, replaceNoteId)
|
||||
.replace(HashtagRegex, replaceHashtag);
|
||||
ev.Content = content;
|
||||
}
|
||||
|
||||
return {
|
||||
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));
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
broadcast: (ev: NEvent | undefined) => {
|
||||
if (ev) {
|
||||
console.debug("Sending event: ", ev);
|
||||
System.BroadcastEvent(ev);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Write event to DefaultRelays, this is important for profiles / relay lists to prevent bugs
|
||||
* 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) => {
|
||||
if (ev) {
|
||||
for (let [k, _] of DefaultRelays) {
|
||||
System.WriteOnceToRelay(k, ev);
|
||||
}
|
||||
}
|
||||
},
|
||||
muted: async (keys: HexKey[], priv: HexKey[]) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.Lists;
|
||||
ev.Tags.push(new Tag(["d", Lists.Muted], ev.Tags.length))
|
||||
keys.forEach(p => {
|
||||
ev.Tags.push(new Tag(["p", p], ev.Tags.length))
|
||||
})
|
||||
let content = ""
|
||||
if (priv.length > 0) {
|
||||
const ps = priv.map(p => ["p", p])
|
||||
const plaintext = JSON.stringify(ps)
|
||||
if (hasNip07 && !privKey) {
|
||||
content = await barierNip07(() => window.nostr.nip04.encrypt(pubKey, plaintext));
|
||||
} else if (privKey) {
|
||||
content = await ev.EncryptData(plaintext, pubKey, privKey)
|
||||
}
|
||||
}
|
||||
ev.Content = content;
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
metadata: async (obj: UserMetadata) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.SetMetadata;
|
||||
ev.Content = JSON.stringify(obj);
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
note: async (msg: string) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.TextNote;
|
||||
processContent(ev, msg);
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
zap: async (author: HexKey, note?: HexKey, msg?: string) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.ZapRequest;
|
||||
if (note) {
|
||||
// @ts-ignore
|
||||
ev.Tags.push(new Tag(["e", note]))
|
||||
}
|
||||
// @ts-ignore
|
||||
ev.Tags.push(new Tag(["p", author]))
|
||||
// @ts-ignore
|
||||
const relayTag = ['relays', ...Object.keys(relays).slice(0, 10)]
|
||||
// @ts-ignore
|
||||
ev.Tags.push(new Tag(relayTag))
|
||||
processContent(ev, msg || '');
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Reply to a note
|
||||
*/
|
||||
reply: async (replyTo: NEvent, msg: string) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.TextNote;
|
||||
|
||||
let thread = replyTo.Thread;
|
||||
if (thread) {
|
||||
if (thread.Root || thread.ReplyTo) {
|
||||
ev.Tags.push(new Tag(["e", thread.Root?.Event ?? thread.ReplyTo?.Event!, "", "root"], ev.Tags.length));
|
||||
}
|
||||
ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], ev.Tags.length));
|
||||
|
||||
// dont tag self in replies
|
||||
if (replyTo.PubKey !== pubKey) {
|
||||
ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length));
|
||||
}
|
||||
|
||||
for (let pk of thread.PubKeys) {
|
||||
if (pk === pubKey) {
|
||||
continue; // dont tag self in replies
|
||||
}
|
||||
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
|
||||
}
|
||||
} else {
|
||||
ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], 0));
|
||||
// dont tag self in replies
|
||||
if (replyTo.PubKey !== pubKey) {
|
||||
ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length));
|
||||
}
|
||||
}
|
||||
processContent(ev, msg);
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
react: async (evRef: NEvent, content = "+") => {
|
||||
if (pubKey) {
|
||||
let 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));
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
saveRelays: async () => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.ContactList;
|
||||
ev.Content = JSON.stringify(relays);
|
||||
for (let pk of follows) {
|
||||
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
|
||||
}
|
||||
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
addFollow: async (pkAdd: HexKey | HexKey[], newRelays?: Record<string, RelaySettings>) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.ContactList;
|
||||
ev.Content = JSON.stringify(newRelays ?? relays);
|
||||
let temp = new Set(follows);
|
||||
if (Array.isArray(pkAdd)) {
|
||||
pkAdd.forEach(a => temp.add(a));
|
||||
} else {
|
||||
temp.add(pkAdd);
|
||||
}
|
||||
for (let pk of temp) {
|
||||
if (pk.length !== 64) {
|
||||
continue;
|
||||
}
|
||||
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
|
||||
}
|
||||
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
removeFollow: async (pkRemove: HexKey) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.ContactList;
|
||||
ev.Content = JSON.stringify(relays);
|
||||
for (let pk of follows) {
|
||||
if (pk === pkRemove || pk.length !== 64) {
|
||||
continue;
|
||||
}
|
||||
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
|
||||
}
|
||||
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Delete an event (NIP-09)
|
||||
*/
|
||||
delete: async (id: u256) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.Deletion;
|
||||
ev.Content = "";
|
||||
ev.Tags.push(new Tag(["e", id], 0));
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Respot a note (NIP-18)
|
||||
*/
|
||||
repost: async (note: NEvent) => {
|
||||
if (pubKey) {
|
||||
let 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));
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
decryptDm: async (note: NEvent): Promise<string | undefined> => {
|
||||
if (pubKey) {
|
||||
if (note.PubKey !== pubKey && !note.Tags.some(a => a.PubKey === pubKey)) {
|
||||
return "<CANT DECRYPT>";
|
||||
}
|
||||
try {
|
||||
let otherPubKey = note.PubKey === pubKey ? note.Tags.filter(a => a.Key === "p")[0].PubKey! : note.PubKey;
|
||||
if (hasNip07 && !privKey) {
|
||||
return await barierNip07(() => window.nostr.nip04.decrypt(otherPubKey, note.Content));
|
||||
} else if (privKey) {
|
||||
await note.DecryptDm(privKey, otherPubKey);
|
||||
return note.Content;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Decyrption failed", e);
|
||||
return "<DECRYPTION FAILED>";
|
||||
}
|
||||
}
|
||||
},
|
||||
sendDm: async (content: string, to: HexKey) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.DirectMessage;
|
||||
ev.Content = content;
|
||||
ev.Tags.push(new Tag(["p", to], 0));
|
||||
|
||||
try {
|
||||
if (hasNip07 && !privKey) {
|
||||
let cx: string = await barierNip07(() => window.nostr.nip04.encrypt(to, content));
|
||||
ev.Content = cx;
|
||||
return await signEvent(ev);
|
||||
} else if (privKey) {
|
||||
await ev.EncryptDmForPubkey(to, privKey);
|
||||
return await signEvent(ev);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Encryption failed", e);
|
||||
}
|
||||
}
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
metadata: async (obj: UserMetadata) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.SetMetadata;
|
||||
ev.Content = JSON.stringify(obj);
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
note: async (msg: string) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.TextNote;
|
||||
processContent(ev, msg);
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
zap: async (author: HexKey, note?: HexKey, msg?: string) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.ZapRequest;
|
||||
if (note) {
|
||||
// @ts-ignore
|
||||
ev.Tags.push(new Tag(["e", note]));
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
ev.Tags.push(new Tag(["p", author]));
|
||||
// @ts-ignore
|
||||
const relayTag = ["relays", ...Object.keys(relays).slice(0, 10)];
|
||||
// @ts-ignore
|
||||
ev.Tags.push(new Tag(relayTag));
|
||||
processContent(ev, msg || "");
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Reply to a note
|
||||
*/
|
||||
reply: async (replyTo: NEvent, msg: string) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.TextNote;
|
||||
|
||||
let thread = replyTo.Thread;
|
||||
if (thread) {
|
||||
if (thread.Root || thread.ReplyTo) {
|
||||
ev.Tags.push(
|
||||
new Tag(
|
||||
["e", thread.Root?.Event ?? thread.ReplyTo?.Event!, "", "root"],
|
||||
ev.Tags.length
|
||||
)
|
||||
);
|
||||
}
|
||||
ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], ev.Tags.length));
|
||||
|
||||
// dont tag self in replies
|
||||
if (replyTo.PubKey !== pubKey) {
|
||||
ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length));
|
||||
}
|
||||
|
||||
for (let pk of thread.PubKeys) {
|
||||
if (pk === pubKey) {
|
||||
continue; // dont tag self in replies
|
||||
}
|
||||
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
|
||||
}
|
||||
} else {
|
||||
ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], 0));
|
||||
// dont tag self in replies
|
||||
if (replyTo.PubKey !== pubKey) {
|
||||
ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length));
|
||||
}
|
||||
}
|
||||
processContent(ev, msg);
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
react: async (evRef: NEvent, content = "+") => {
|
||||
if (pubKey) {
|
||||
let 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));
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
saveRelays: async () => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.ContactList;
|
||||
ev.Content = JSON.stringify(relays);
|
||||
for (let pk of follows) {
|
||||
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
|
||||
}
|
||||
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
addFollow: async (
|
||||
pkAdd: HexKey | HexKey[],
|
||||
newRelays?: Record<string, RelaySettings>
|
||||
) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.ContactList;
|
||||
ev.Content = JSON.stringify(newRelays ?? relays);
|
||||
let temp = new Set(follows);
|
||||
if (Array.isArray(pkAdd)) {
|
||||
pkAdd.forEach((a) => temp.add(a));
|
||||
} else {
|
||||
temp.add(pkAdd);
|
||||
}
|
||||
for (let pk of temp) {
|
||||
if (pk.length !== 64) {
|
||||
continue;
|
||||
}
|
||||
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
|
||||
}
|
||||
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
removeFollow: async (pkRemove: HexKey) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.ContactList;
|
||||
ev.Content = JSON.stringify(relays);
|
||||
for (let pk of follows) {
|
||||
if (pk === pkRemove || pk.length !== 64) {
|
||||
continue;
|
||||
}
|
||||
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
|
||||
}
|
||||
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Delete an event (NIP-09)
|
||||
*/
|
||||
delete: async (id: u256) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.Deletion;
|
||||
ev.Content = "";
|
||||
ev.Tags.push(new Tag(["e", id], 0));
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Respot a note (NIP-18)
|
||||
*/
|
||||
repost: async (note: NEvent) => {
|
||||
if (pubKey) {
|
||||
let 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));
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
decryptDm: async (note: NEvent): Promise<string | undefined> => {
|
||||
if (pubKey) {
|
||||
if (
|
||||
note.PubKey !== pubKey &&
|
||||
!note.Tags.some((a) => a.PubKey === pubKey)
|
||||
) {
|
||||
return "<CANT DECRYPT>";
|
||||
}
|
||||
try {
|
||||
let otherPubKey =
|
||||
note.PubKey === pubKey
|
||||
? note.Tags.filter((a) => a.Key === "p")[0].PubKey!
|
||||
: note.PubKey;
|
||||
if (hasNip07 && !privKey) {
|
||||
return await barierNip07(() =>
|
||||
window.nostr.nip04.decrypt(otherPubKey, note.Content)
|
||||
);
|
||||
} else if (privKey) {
|
||||
await note.DecryptDm(privKey, otherPubKey);
|
||||
return note.Content;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Decyrption failed", e);
|
||||
return "<DECRYPTION FAILED>";
|
||||
}
|
||||
}
|
||||
},
|
||||
sendDm: async (content: string, to: HexKey) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.DirectMessage;
|
||||
ev.Content = content;
|
||||
ev.Tags.push(new Tag(["p", to], 0));
|
||||
|
||||
try {
|
||||
if (hasNip07 && !privKey) {
|
||||
let cx: string = await barierNip07(() =>
|
||||
window.nostr.nip04.encrypt(to, content)
|
||||
);
|
||||
ev.Content = cx;
|
||||
return await signEvent(ev);
|
||||
} else if (privKey) {
|
||||
await ev.EncryptDmForPubkey(to, privKey);
|
||||
return await signEvent(ev);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Encryption failed", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let isNip07Busy = false;
|
||||
|
||||
const delay = (t: number) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(resolve, t);
|
||||
});
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(resolve, t);
|
||||
});
|
||||
};
|
||||
|
||||
export const barierNip07 = async (then: () => Promise<any>) => {
|
||||
while (isNip07Busy) {
|
||||
await delay(10);
|
||||
}
|
||||
isNip07Busy = true;
|
||||
try {
|
||||
return await then();
|
||||
} finally {
|
||||
isNip07Busy = false;
|
||||
}
|
||||
while (isNip07Busy) {
|
||||
await delay(10);
|
||||
}
|
||||
isNip07Busy = true;
|
||||
try {
|
||||
return await then();
|
||||
} finally {
|
||||
isNip07Busy = false;
|
||||
}
|
||||
};
|
||||
|
@ -5,14 +5,14 @@ import { Subscriptions } from "Nostr/Subscriptions";
|
||||
import useSubscription from "Feed/Subscription";
|
||||
|
||||
export default function useFollowersFeed(pubkey: HexKey) {
|
||||
const sub = useMemo(() => {
|
||||
let x = new Subscriptions();
|
||||
x.Id = `followers:${pubkey.slice(0, 12)}`;
|
||||
x.Kinds = new Set([EventKind.ContactList]);
|
||||
x.PTags = new Set([pubkey]);
|
||||
const sub = useMemo(() => {
|
||||
let x = new Subscriptions();
|
||||
x.Id = `followers:${pubkey.slice(0, 12)}`;
|
||||
x.Kinds = new Set([EventKind.ContactList]);
|
||||
x.PTags = new Set([pubkey]);
|
||||
|
||||
return x;
|
||||
}, [pubkey]);
|
||||
return x;
|
||||
}, [pubkey]);
|
||||
|
||||
return useSubscription(sub);
|
||||
}
|
||||
return useSubscription(sub);
|
||||
}
|
||||
|
@ -1,24 +1,28 @@
|
||||
import { useMemo } from "react";
|
||||
import { HexKey } from "Nostr";
|
||||
import EventKind from "Nostr/EventKind";
|
||||
import { Subscriptions} from "Nostr/Subscriptions";
|
||||
import { Subscriptions } from "Nostr/Subscriptions";
|
||||
import useSubscription, { NoteStore } from "Feed/Subscription";
|
||||
|
||||
export default function useFollowsFeed(pubkey: HexKey) {
|
||||
const sub = useMemo(() => {
|
||||
let x = new Subscriptions();
|
||||
x.Id = `follows:${pubkey.slice(0, 12)}`;
|
||||
x.Kinds = new Set([EventKind.ContactList]);
|
||||
x.Authors = new Set([pubkey]);
|
||||
const sub = useMemo(() => {
|
||||
let x = new Subscriptions();
|
||||
x.Id = `follows:${pubkey.slice(0, 12)}`;
|
||||
x.Kinds = new Set([EventKind.ContactList]);
|
||||
x.Authors = new Set([pubkey]);
|
||||
|
||||
return x;
|
||||
}, [pubkey]);
|
||||
return x;
|
||||
}, [pubkey]);
|
||||
|
||||
return useSubscription(sub);
|
||||
return useSubscription(sub);
|
||||
}
|
||||
|
||||
export function getFollowers(feed: NoteStore, pubkey: HexKey) {
|
||||
let contactLists = feed?.notes.filter(a => a.kind === EventKind.ContactList && a.pubkey === pubkey);
|
||||
let pTags = contactLists?.map(a => a.tags.filter(b => b[0] === "p").map(c => c[1]));
|
||||
return [...new Set(pTags?.flat())];
|
||||
let contactLists = feed?.notes.filter(
|
||||
(a) => a.kind === EventKind.ContactList && a.pubkey === pubkey
|
||||
);
|
||||
let pTags = contactLists?.map((a) =>
|
||||
a.tags.filter((b) => b[0] === "p").map((c) => c[1])
|
||||
);
|
||||
return [...new Set(pTags?.flat())];
|
||||
}
|
||||
|
@ -1,39 +1,44 @@
|
||||
import * as secp from "@noble/secp256k1"
|
||||
import * as base64 from "@protobufjs/base64"
|
||||
import * as secp from "@noble/secp256k1";
|
||||
import * as base64 from "@protobufjs/base64";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "State/Store";
|
||||
|
||||
export interface ImgProxySettings {
|
||||
url: string,
|
||||
key: string,
|
||||
salt: string
|
||||
url: string;
|
||||
key: string;
|
||||
salt: string;
|
||||
}
|
||||
|
||||
export default function useImgProxy() {
|
||||
const settings = useSelector((s: RootState) => s.login.preferences.imgProxyConfig);
|
||||
const te = new TextEncoder();
|
||||
const settings = useSelector(
|
||||
(s: RootState) => s.login.preferences.imgProxyConfig
|
||||
);
|
||||
const te = new TextEncoder();
|
||||
|
||||
function urlSafe(s: string) {
|
||||
return s.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
||||
}
|
||||
function urlSafe(s: string) {
|
||||
return s.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
|
||||
async function signUrl(u: string) {
|
||||
const result = await secp.utils.hmacSha256(
|
||||
secp.utils.hexToBytes(settings!.key),
|
||||
secp.utils.hexToBytes(settings!.salt),
|
||||
te.encode(u));
|
||||
return urlSafe(base64.encode(result, 0, result.byteLength));
|
||||
}
|
||||
async function signUrl(u: string) {
|
||||
const result = await secp.utils.hmacSha256(
|
||||
secp.utils.hexToBytes(settings!.key),
|
||||
secp.utils.hexToBytes(settings!.salt),
|
||||
te.encode(u)
|
||||
);
|
||||
return urlSafe(base64.encode(result, 0, result.byteLength));
|
||||
}
|
||||
|
||||
return {
|
||||
proxy: async (url: string, resize?: number) => {
|
||||
if (!settings) return url;
|
||||
const opt = resize ? `rs:fit:${resize}:${resize}` : "";
|
||||
const urlBytes = te.encode(url);
|
||||
const urlEncoded = urlSafe(base64.encode(urlBytes, 0, urlBytes.byteLength));
|
||||
const path = `/${opt}/${urlEncoded}`;
|
||||
const sig = await signUrl(path);
|
||||
return `${new URL(settings.url).toString()}${sig}${path}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
proxy: async (url: string, resize?: number) => {
|
||||
if (!settings) return url;
|
||||
const opt = resize ? `rs:fit:${resize}:${resize}` : "";
|
||||
const urlBytes = te.encode(url);
|
||||
const urlEncoded = urlSafe(
|
||||
base64.encode(urlBytes, 0, urlBytes.byteLength)
|
||||
);
|
||||
const path = `/${opt}/${urlEncoded}`;
|
||||
const sig = await signUrl(path);
|
||||
return `${new URL(settings.url).toString()}${sig}${path}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -6,7 +6,15 @@ import { TaggedRawEvent, HexKey, Lists } from "Nostr";
|
||||
import EventKind from "Nostr/EventKind";
|
||||
import Event from "Nostr/Event";
|
||||
import { Subscriptions } from "Nostr/Subscriptions";
|
||||
import { addDirectMessage, setFollows, setRelays, setMuted, setBlocked, sendNotification, setLatestNotifications } from "State/Login";
|
||||
import {
|
||||
addDirectMessage,
|
||||
setFollows,
|
||||
setRelays,
|
||||
setMuted,
|
||||
setBlocked,
|
||||
sendNotification,
|
||||
setLatestNotifications,
|
||||
} from "State/Login";
|
||||
import { RootState } from "State/Store";
|
||||
import { mapEventToProfile, MetadataCache } from "State/Users";
|
||||
import { useDb } from "State/Users/Db";
|
||||
@ -20,7 +28,12 @@ import useModeration from "Hooks/useModeration";
|
||||
*/
|
||||
export default function useLoginFeed() {
|
||||
const dispatch = useDispatch();
|
||||
const { publicKey: pubKey, privateKey: privKey, latestMuted, readNotifications } = useSelector((s: RootState) => s.login);
|
||||
const {
|
||||
publicKey: pubKey,
|
||||
privateKey: privKey,
|
||||
latestMuted,
|
||||
readNotifications,
|
||||
} = useSelector((s: RootState) => s.login);
|
||||
const { isMuted } = useModeration();
|
||||
const db = useDb();
|
||||
|
||||
@ -31,7 +44,7 @@ export default function useLoginFeed() {
|
||||
sub.Id = `login:meta`;
|
||||
sub.Authors = new Set([pubKey]);
|
||||
sub.Kinds = new Set([EventKind.ContactList, EventKind.SetMetadata]);
|
||||
sub.Limit = 2
|
||||
sub.Limit = 2;
|
||||
|
||||
return sub;
|
||||
}, [pubKey]);
|
||||
@ -77,35 +90,49 @@ export default function useLoginFeed() {
|
||||
return dms;
|
||||
}, [pubKey]);
|
||||
|
||||
const metadataFeed = useSubscription(subMetadata, { leaveOpen: true, cache: true });
|
||||
const notificationFeed = useSubscription(subNotification, { leaveOpen: true, cache: true });
|
||||
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 });
|
||||
|
||||
useEffect(() => {
|
||||
let contactList = metadataFeed.store.notes.filter(a => a.kind === EventKind.ContactList);
|
||||
let metadata = metadataFeed.store.notes.filter(a => a.kind === EventKind.SetMetadata);
|
||||
let profiles = metadata.map(a => mapEventToProfile(a))
|
||||
.filter(a => a !== undefined)
|
||||
.map(a => a!);
|
||||
let contactList = metadataFeed.store.notes.filter(
|
||||
(a) => a.kind === EventKind.ContactList
|
||||
);
|
||||
let metadata = metadataFeed.store.notes.filter(
|
||||
(a) => a.kind === EventKind.SetMetadata
|
||||
);
|
||||
let profiles = metadata
|
||||
.map((a) => mapEventToProfile(a))
|
||||
.filter((a) => a !== undefined)
|
||||
.map((a) => a!);
|
||||
|
||||
for (let cl of contactList) {
|
||||
if (cl.content !== "" && cl.content !== "{}") {
|
||||
let relays = JSON.parse(cl.content);
|
||||
dispatch(setRelays({ relays, createdAt: cl.created_at }));
|
||||
}
|
||||
let pTags = cl.tags.filter(a => a[0] === "p").map(a => a[1]);
|
||||
let pTags = cl.tags.filter((a) => a[0] === "p").map((a) => a[1]);
|
||||
dispatch(setFollows({ keys: pTags, createdAt: cl.created_at }));
|
||||
}
|
||||
|
||||
(async () => {
|
||||
let maxProfile = profiles.reduce((acc, v) => {
|
||||
if (v.created > acc.created) {
|
||||
acc.profile = v;
|
||||
acc.created = v.created;
|
||||
}
|
||||
return acc;
|
||||
}, { created: 0, profile: null as MetadataCache | null });
|
||||
let maxProfile = profiles.reduce(
|
||||
(acc, v) => {
|
||||
if (v.created > acc.created) {
|
||||
acc.profile = v;
|
||||
acc.created = v.created;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ created: 0, profile: null as MetadataCache | null }
|
||||
);
|
||||
if (maxProfile.profile) {
|
||||
let existing = await db.find(maxProfile.profile.pubkey);
|
||||
if ((existing?.created ?? 0) < maxProfile.created) {
|
||||
@ -116,52 +143,74 @@ export default function useLoginFeed() {
|
||||
}, [dispatch, metadataFeed.store, db]);
|
||||
|
||||
useEffect(() => {
|
||||
const replies = notificationFeed.store.notes.
|
||||
filter(a => a.kind === EventKind.TextNote && !isMuted(a.pubkey) && a.created_at > readNotifications)
|
||||
replies.forEach(nx => {
|
||||
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(db, nx).then(notification => {
|
||||
makeNotification(db, nx).then((notification) => {
|
||||
if (notification) {
|
||||
// @ts-ignore
|
||||
dispatch(sendNotification(notification))
|
||||
dispatch(sendNotification(notification));
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
}, [dispatch, notificationFeed.store, db, readNotifications]);
|
||||
|
||||
useEffect(() => {
|
||||
const muted = getMutedKeys(mutedFeed.store.notes)
|
||||
dispatch(setMuted(muted))
|
||||
const muted = getMutedKeys(mutedFeed.store.notes);
|
||||
dispatch(setMuted(muted));
|
||||
|
||||
const newest = getNewest(mutedFeed.store.notes)
|
||||
if (newest && newest.content.length > 0 && pubKey && newest.created_at > latestMuted) {
|
||||
decryptBlocked(newest, pubKey, privKey).then((plaintext) => {
|
||||
try {
|
||||
const blocked = JSON.parse(plaintext)
|
||||
const keys = blocked.filter((p: any) => p && p.length === 2 && p[0] === "p").map((p: any) => p[1])
|
||||
dispatch(setBlocked({
|
||||
keys,
|
||||
createdAt: newest.created_at,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.debug("Couldn't parse JSON")
|
||||
}
|
||||
}).catch((error) => console.warn(error))
|
||||
const newest = getNewest(mutedFeed.store.notes);
|
||||
if (
|
||||
newest &&
|
||||
newest.content.length > 0 &&
|
||||
pubKey &&
|
||||
newest.created_at > latestMuted
|
||||
) {
|
||||
decryptBlocked(newest, pubKey, privKey)
|
||||
.then((plaintext) => {
|
||||
try {
|
||||
const blocked = JSON.parse(plaintext);
|
||||
const keys = blocked
|
||||
.filter((p: any) => p && p.length === 2 && p[0] === "p")
|
||||
.map((p: any) => p[1]);
|
||||
dispatch(
|
||||
setBlocked({
|
||||
keys,
|
||||
createdAt: newest.created_at,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.debug("Couldn't parse JSON");
|
||||
}
|
||||
})
|
||||
.catch((error) => console.warn(error));
|
||||
}
|
||||
}, [dispatch, mutedFeed.store])
|
||||
}, [dispatch, mutedFeed.store]);
|
||||
|
||||
useEffect(() => {
|
||||
let dms = dmsFeed.store.notes.filter(a => a.kind === EventKind.DirectMessage);
|
||||
let dms = dmsFeed.store.notes.filter(
|
||||
(a) => a.kind === EventKind.DirectMessage
|
||||
);
|
||||
dispatch(addDirectMessage(dms));
|
||||
}, [dispatch, dmsFeed.store]);
|
||||
}
|
||||
|
||||
|
||||
async function decryptBlocked(raw: TaggedRawEvent, pubKey: HexKey, privKey?: HexKey) {
|
||||
const ev = new Event(raw)
|
||||
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 ev.DecryptData(raw.content, privKey, pubKey);
|
||||
} else {
|
||||
return await barierNip07(() => window.nostr.nip04.decrypt(pubKey, raw.content));
|
||||
return await barierNip07(() =>
|
||||
window.nostr.nip04.decrypt(pubKey, raw.content)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -6,41 +6,46 @@ import { Subscriptions } from "Nostr/Subscriptions";
|
||||
import useSubscription, { NoteStore } from "Feed/Subscription";
|
||||
|
||||
export default function useMutedFeed(pubkey: HexKey) {
|
||||
const sub = useMemo(() => {
|
||||
let sub = new Subscriptions();
|
||||
sub.Id = `muted:${pubkey.slice(0, 12)}`;
|
||||
sub.Kinds = new Set([EventKind.Lists]);
|
||||
sub.Authors = new Set([pubkey]);
|
||||
sub.DTags = new Set([Lists.Muted]);
|
||||
sub.Limit = 1;
|
||||
return sub;
|
||||
}, [pubkey]);
|
||||
const sub = useMemo(() => {
|
||||
let sub = new Subscriptions();
|
||||
sub.Id = `muted:${pubkey.slice(0, 12)}`;
|
||||
sub.Kinds = new Set([EventKind.Lists]);
|
||||
sub.Authors = new Set([pubkey]);
|
||||
sub.DTags = new Set([Lists.Muted]);
|
||||
sub.Limit = 1;
|
||||
return sub;
|
||||
}, [pubkey]);
|
||||
|
||||
return useSubscription(sub);
|
||||
return useSubscription(sub);
|
||||
}
|
||||
|
||||
export function getNewest(rawNotes: TaggedRawEvent[]){
|
||||
const notes = [...rawNotes]
|
||||
notes.sort((a, b) => a.created_at - b.created_at)
|
||||
if (notes.length > 0) {
|
||||
return notes[0]
|
||||
}
|
||||
export function getNewest(rawNotes: TaggedRawEvent[]) {
|
||||
const notes = [...rawNotes];
|
||||
notes.sort((a, b) => a.created_at - b.created_at);
|
||||
if (notes.length > 0) {
|
||||
return notes[0];
|
||||
}
|
||||
}
|
||||
|
||||
export function getMutedKeys(rawNotes: TaggedRawEvent[]): { createdAt: number, keys: HexKey[] } {
|
||||
const newest = getNewest(rawNotes)
|
||||
if (newest) {
|
||||
const { created_at, tags } = newest
|
||||
const keys = tags.filter(t => t[0] === "p").map(t => t[1])
|
||||
return {
|
||||
keys,
|
||||
createdAt: created_at,
|
||||
}
|
||||
}
|
||||
return { createdAt: 0, keys: [] }
|
||||
export function getMutedKeys(rawNotes: TaggedRawEvent[]): {
|
||||
createdAt: number;
|
||||
keys: HexKey[];
|
||||
} {
|
||||
const newest = getNewest(rawNotes);
|
||||
if (newest) {
|
||||
const { created_at, tags } = newest;
|
||||
const keys = tags.filter((t) => t[0] === "p").map((t) => t[1]);
|
||||
return {
|
||||
keys,
|
||||
createdAt: created_at,
|
||||
};
|
||||
}
|
||||
return { createdAt: 0, keys: [] };
|
||||
}
|
||||
|
||||
export function getMuted(feed: NoteStore, pubkey: HexKey): HexKey[] {
|
||||
let lists = feed?.notes.filter(a => a.kind === EventKind.Lists && a.pubkey === pubkey);
|
||||
return getMutedKeys(lists).keys;
|
||||
let lists = feed?.notes.filter(
|
||||
(a) => a.kind === EventKind.Lists && a.pubkey === pubkey
|
||||
);
|
||||
return getMutedKeys(lists).keys;
|
||||
}
|
||||
|
@ -5,28 +5,29 @@ import { HexKey } from "Nostr";
|
||||
import { System } from "Nostr/System";
|
||||
|
||||
export function useUserProfile(pubKey: HexKey): MetadataCache | undefined {
|
||||
const users = useKey(pubKey);
|
||||
const users = useKey(pubKey);
|
||||
|
||||
useEffect(() => {
|
||||
if (pubKey) {
|
||||
System.TrackMetadata(pubKey);
|
||||
return () => System.UntrackMetadata(pubKey);
|
||||
}
|
||||
}, [pubKey]);
|
||||
useEffect(() => {
|
||||
if (pubKey) {
|
||||
System.TrackMetadata(pubKey);
|
||||
return () => System.UntrackMetadata(pubKey);
|
||||
}
|
||||
}, [pubKey]);
|
||||
|
||||
return users;
|
||||
return users;
|
||||
}
|
||||
|
||||
export function useUserProfiles(
|
||||
pubKeys: Array<HexKey>
|
||||
): Map<HexKey, MetadataCache> | undefined {
|
||||
const users = useKeys(pubKeys);
|
||||
|
||||
export function useUserProfiles(pubKeys: Array<HexKey>): Map<HexKey, MetadataCache> | undefined {
|
||||
const users = useKeys(pubKeys);
|
||||
useEffect(() => {
|
||||
if (pubKeys) {
|
||||
System.TrackMetadata(pubKeys);
|
||||
return () => System.UntrackMetadata(pubKeys);
|
||||
}
|
||||
}, [pubKeys]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pubKeys) {
|
||||
System.TrackMetadata(pubKeys);
|
||||
return () => System.UntrackMetadata(pubKeys);
|
||||
}
|
||||
}, [pubKeys]);
|
||||
|
||||
return users;
|
||||
return users;
|
||||
}
|
||||
|
@ -2,12 +2,17 @@ import { useSyncExternalStore } from "react";
|
||||
import { System } from "Nostr/System";
|
||||
import { CustomHook, StateSnapshot } from "Nostr/Connection";
|
||||
|
||||
const noop = (f: CustomHook) => { return () => { }; };
|
||||
const noop = (f: CustomHook) => {
|
||||
return () => {};
|
||||
};
|
||||
const noopState = (): StateSnapshot | undefined => {
|
||||
return undefined;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export default function useRelayState(addr: string) {
|
||||
let c = System.Sockets.get(addr);
|
||||
return useSyncExternalStore<StateSnapshot | undefined>(c?.StatusHook.bind(c) ?? noop, c?.GetState.bind(c) ?? noopState);
|
||||
}
|
||||
let c = System.Sockets.get(addr);
|
||||
return useSyncExternalStore<StateSnapshot | undefined>(
|
||||
c?.StatusHook.bind(c) ?? noop,
|
||||
c?.GetState.bind(c) ?? noopState
|
||||
);
|
||||
}
|
||||
|
@ -6,62 +6,59 @@ import { debounce } from "Util";
|
||||
import { db } from "Db";
|
||||
|
||||
export type NoteStore = {
|
||||
notes: Array<TaggedRawEvent>,
|
||||
end: boolean
|
||||
notes: Array<TaggedRawEvent>;
|
||||
end: boolean;
|
||||
};
|
||||
|
||||
export type UseSubscriptionOptions = {
|
||||
leaveOpen: boolean,
|
||||
cache: boolean
|
||||
}
|
||||
leaveOpen: boolean;
|
||||
cache: boolean;
|
||||
};
|
||||
|
||||
interface ReducerArg {
|
||||
type: "END" | "EVENT" | "CLEAR",
|
||||
ev?: TaggedRawEvent | Array<TaggedRawEvent>,
|
||||
end?: boolean
|
||||
type: "END" | "EVENT" | "CLEAR";
|
||||
ev?: TaggedRawEvent | Array<TaggedRawEvent>;
|
||||
end?: boolean;
|
||||
}
|
||||
|
||||
function notesReducer(state: NoteStore, arg: ReducerArg) {
|
||||
if (arg.type === "END") {
|
||||
return {
|
||||
notes: state.notes,
|
||||
end: arg.end!
|
||||
} as NoteStore;
|
||||
}
|
||||
|
||||
if (arg.type === "CLEAR") {
|
||||
return {
|
||||
notes: [],
|
||||
end: state.end,
|
||||
} as NoteStore;
|
||||
}
|
||||
|
||||
let evs = arg.ev!;
|
||||
if (!Array.isArray(evs)) {
|
||||
evs = [evs];
|
||||
}
|
||||
let existingIds = new Set(state.notes.map(a => a.id));
|
||||
evs = evs.filter(a => !existingIds.has(a.id));
|
||||
if (evs.length === 0) {
|
||||
return state;
|
||||
}
|
||||
if (arg.type === "END") {
|
||||
return {
|
||||
notes: [
|
||||
...state.notes,
|
||||
...evs
|
||||
]
|
||||
notes: state.notes,
|
||||
end: arg.end!,
|
||||
} as NoteStore;
|
||||
}
|
||||
|
||||
if (arg.type === "CLEAR") {
|
||||
return {
|
||||
notes: [],
|
||||
end: state.end,
|
||||
} as NoteStore;
|
||||
}
|
||||
|
||||
let evs = arg.ev!;
|
||||
if (!Array.isArray(evs)) {
|
||||
evs = [evs];
|
||||
}
|
||||
let 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
|
||||
notes: [],
|
||||
end: false,
|
||||
};
|
||||
|
||||
export interface UseSubscriptionState {
|
||||
store: NoteStore,
|
||||
clear: () => void,
|
||||
append: (notes: TaggedRawEvent[]) => void
|
||||
store: NoteStore;
|
||||
clear: () => void;
|
||||
append: (notes: TaggedRawEvent[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -70,121 +67,131 @@ export interface UseSubscriptionState {
|
||||
const DebounceMs = 200;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Subscriptions} sub
|
||||
* @param {any} opt
|
||||
* @returns
|
||||
*
|
||||
* @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>();
|
||||
const useCache = useMemo(() => options?.cache === true, [options]);
|
||||
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>();
|
||||
const useCache = useMemo(() => options?.cache === true, [options]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sub) {
|
||||
return debounce(DebounceMs, () => {
|
||||
setSubDebounced(sub);
|
||||
});
|
||||
}
|
||||
}, [sub, options]);
|
||||
|
||||
useEffect(() => {
|
||||
if (subDebounce) {
|
||||
dispatch({
|
||||
type: "END",
|
||||
end: false
|
||||
});
|
||||
|
||||
if (useCache) {
|
||||
// preload notes from db
|
||||
PreloadNotes(subDebounce.Id)
|
||||
.then(ev => {
|
||||
dispatch({
|
||||
type: "EVENT",
|
||||
ev: ev
|
||||
});
|
||||
})
|
||||
.catch(console.warn);
|
||||
}
|
||||
subDebounce.OnEvent = (e) => {
|
||||
dispatch({
|
||||
type: "EVENT",
|
||||
ev: e
|
||||
});
|
||||
if (useCache) {
|
||||
db.events.put(e);
|
||||
}
|
||||
};
|
||||
|
||||
subDebounce.OnEnd = (c) => {
|
||||
if (!(options?.leaveOpen ?? false)) {
|
||||
c.RemoveSubscription(subDebounce.Id);
|
||||
if (subDebounce.IsFinished()) {
|
||||
System.RemoveSubscription(subDebounce.Id);
|
||||
}
|
||||
}
|
||||
dispatch({
|
||||
type: "END",
|
||||
end: true
|
||||
});
|
||||
};
|
||||
|
||||
console.debug("Adding sub: ", subDebounce.ToObject());
|
||||
System.AddSubscription(subDebounce);
|
||||
return () => {
|
||||
console.debug("Removing sub: ", subDebounce.ToObject());
|
||||
System.RemoveSubscription(subDebounce.Id);
|
||||
};
|
||||
}
|
||||
}, [subDebounce, useCache]);
|
||||
|
||||
useEffect(() => {
|
||||
if (subDebounce && useCache) {
|
||||
return debounce(500, () => {
|
||||
TrackNotesInFeed(subDebounce.Id, state.notes)
|
||||
.catch(console.warn);
|
||||
});
|
||||
}
|
||||
}, [state, useCache]);
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
useEffect(() => {
|
||||
if (sub) {
|
||||
return debounce(DebounceMs, () => {
|
||||
setSubDebounced(sub);
|
||||
});
|
||||
}
|
||||
}, [sub, options]);
|
||||
|
||||
useEffect(() => {
|
||||
if (subDebounce) {
|
||||
dispatch({
|
||||
type: "END",
|
||||
end: false,
|
||||
});
|
||||
|
||||
if (useCache) {
|
||||
// preload notes from db
|
||||
PreloadNotes(subDebounce.Id)
|
||||
.then((ev) => {
|
||||
dispatch({
|
||||
type: "EVENT",
|
||||
ev: ev,
|
||||
});
|
||||
})
|
||||
.catch(console.warn);
|
||||
}
|
||||
subDebounce.OnEvent = (e) => {
|
||||
dispatch({
|
||||
type: "EVENT",
|
||||
ev: e,
|
||||
});
|
||||
if (useCache) {
|
||||
db.events.put(e);
|
||||
}
|
||||
};
|
||||
|
||||
subDebounce.OnEnd = (c) => {
|
||||
if (!(options?.leaveOpen ?? false)) {
|
||||
c.RemoveSubscription(subDebounce.Id);
|
||||
if (subDebounce.IsFinished()) {
|
||||
System.RemoveSubscription(subDebounce.Id);
|
||||
}
|
||||
}
|
||||
dispatch({
|
||||
type: "END",
|
||||
end: true,
|
||||
});
|
||||
};
|
||||
|
||||
console.debug("Adding sub: ", subDebounce.ToObject());
|
||||
System.AddSubscription(subDebounce);
|
||||
return () => {
|
||||
console.debug("Removing sub: ", subDebounce.ToObject());
|
||||
System.RemoveSubscription(subDebounce.Id);
|
||||
};
|
||||
}
|
||||
}, [subDebounce, useCache]);
|
||||
|
||||
useEffect(() => {
|
||||
if (subDebounce && useCache) {
|
||||
return debounce(500, () => {
|
||||
TrackNotesInFeed(subDebounce.Id, state.notes).catch(console.warn);
|
||||
});
|
||||
}
|
||||
}, [state, useCache]);
|
||||
|
||||
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,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup cached copy of feed
|
||||
*/
|
||||
const PreloadNotes = async (id: string): Promise<TaggedRawEvent[]> => {
|
||||
const feed = await db.feeds.get(id);
|
||||
if (feed) {
|
||||
const events = await db.events.bulkGet(feed.ids);
|
||||
return events.filter(a => a !== undefined).map(a => a!);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
const feed = await db.feeds.get(id);
|
||||
if (feed) {
|
||||
const events = await db.events.bulkGet(feed.ids);
|
||||
return events.filter((a) => a !== undefined).map((a) => a!);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const TrackNotesInFeed = async (id: string, notes: TaggedRawEvent[]) => {
|
||||
const existing = await db.feeds.get(id);
|
||||
const ids = Array.from(new Set([...(existing?.ids || []), ...notes.map(a => a.id)]));
|
||||
const since = notes.reduce((acc, v) => acc > v.created_at ? v.created_at : acc, +Infinity);
|
||||
const until = notes.reduce((acc, v) => acc < v.created_at ? v.created_at : acc, -Infinity);
|
||||
await db.feeds.put({ id, ids, since, until });
|
||||
}
|
||||
const existing = await db.feeds.get(id);
|
||||
const ids = Array.from(
|
||||
new Set([...(existing?.ids || []), ...notes.map((a) => a.id)])
|
||||
);
|
||||
const since = notes.reduce(
|
||||
(acc, v) => (acc > v.created_at ? v.created_at : acc),
|
||||
+Infinity
|
||||
);
|
||||
const until = notes.reduce(
|
||||
(acc, v) => (acc < v.created_at ? v.created_at : acc),
|
||||
-Infinity
|
||||
);
|
||||
await db.feeds.put({ id, ids, since, until });
|
||||
};
|
||||
|
@ -9,51 +9,66 @@ import { UserPreferences } from "State/Login";
|
||||
import { debounce } from "Util";
|
||||
|
||||
export default function useThreadFeed(id: u256) {
|
||||
const [trackingEvents, setTrackingEvent] = useState<u256[]>([id]);
|
||||
const pref = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||
const [trackingEvents, setTrackingEvent] = useState<u256[]>([id]);
|
||||
const pref = useSelector<RootState, UserPreferences>(
|
||||
(s) => s.login.preferences
|
||||
);
|
||||
|
||||
function addId(id: u256[]) {
|
||||
setTrackingEvent((s) => {
|
||||
let orig = new Set(s);
|
||||
if (id.some(a => !orig.has(a))) {
|
||||
let tmp = new Set([...s, ...id]);
|
||||
return Array.from(tmp);
|
||||
} else {
|
||||
return s;
|
||||
}
|
||||
})
|
||||
function addId(id: u256[]) {
|
||||
setTrackingEvent((s) => {
|
||||
let orig = new Set(s);
|
||||
if (id.some((a) => !orig.has(a))) {
|
||||
let tmp = new Set([...s, ...id]);
|
||||
return Array.from(tmp);
|
||||
} else {
|
||||
return s;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const sub = useMemo(() => {
|
||||
const thisSub = new Subscriptions();
|
||||
thisSub.Id = `thread:${id.substring(0, 8)}`;
|
||||
thisSub.Ids = new Set(trackingEvents);
|
||||
|
||||
// get replies to this event
|
||||
const subRelated = new Subscriptions();
|
||||
subRelated.Kinds = new Set(
|
||||
pref.enableReactions
|
||||
? [
|
||||
EventKind.Reaction,
|
||||
EventKind.TextNote,
|
||||
EventKind.Deletion,
|
||||
EventKind.Repost,
|
||||
EventKind.ZapReceipt,
|
||||
]
|
||||
: [EventKind.TextNote]
|
||||
);
|
||||
subRelated.ETags = thisSub.Ids;
|
||||
thisSub.AddSubscription(subRelated);
|
||||
|
||||
return thisSub;
|
||||
}, [trackingEvents, pref, id]);
|
||||
|
||||
const main = useSubscription(sub, { leaveOpen: true, cache: true });
|
||||
|
||||
useEffect(() => {
|
||||
if (main.store) {
|
||||
return debounce(200, () => {
|
||||
let mainNotes = main.store.notes.filter(
|
||||
(a) => a.kind === EventKind.TextNote
|
||||
);
|
||||
|
||||
let eTags = mainNotes
|
||||
.filter((a) => a.kind === EventKind.TextNote)
|
||||
.map((a) => a.tags.filter((b) => b[0] === "e").map((b) => b[1]))
|
||||
.flat();
|
||||
let ids = mainNotes.map((a) => a.id);
|
||||
let allEvents = new Set([...eTags, ...ids]);
|
||||
addId(Array.from(allEvents));
|
||||
});
|
||||
}
|
||||
}, [main.store]);
|
||||
|
||||
const sub = useMemo(() => {
|
||||
const thisSub = new Subscriptions();
|
||||
thisSub.Id = `thread:${id.substring(0, 8)}`;
|
||||
thisSub.Ids = new Set(trackingEvents);
|
||||
|
||||
// get replies to this event
|
||||
const subRelated = new Subscriptions();
|
||||
subRelated.Kinds = new Set(pref.enableReactions ? [EventKind.Reaction, EventKind.TextNote, EventKind.Deletion, EventKind.Repost, EventKind.ZapReceipt] : [EventKind.TextNote]);
|
||||
subRelated.ETags = thisSub.Ids;
|
||||
thisSub.AddSubscription(subRelated);
|
||||
|
||||
return thisSub;
|
||||
}, [trackingEvents, pref, id]);
|
||||
|
||||
const main = useSubscription(sub, { leaveOpen: true, cache: true });
|
||||
|
||||
useEffect(() => {
|
||||
if (main.store) {
|
||||
return debounce(200, () => {
|
||||
let mainNotes = main.store.notes.filter(a => a.kind === EventKind.TextNote);
|
||||
|
||||
let eTags = mainNotes
|
||||
.filter(a => a.kind === EventKind.TextNote)
|
||||
.map(a => a.tags.filter(b => b[0] === "e").map(b => b[1])).flat();
|
||||
let ids = mainNotes.map(a => a.id);
|
||||
let allEvents = new Set([...eTags, ...ids]);
|
||||
addId(Array.from(allEvents));
|
||||
})
|
||||
}
|
||||
}, [main.store]);
|
||||
|
||||
return main.store;
|
||||
return main.store;
|
||||
}
|
||||
|
@ -9,169 +9,184 @@ import { RootState } from "State/Store";
|
||||
import { UserPreferences } from "State/Login";
|
||||
|
||||
export interface TimelineFeedOptions {
|
||||
method: "TIME_RANGE" | "LIMIT_UNTIL",
|
||||
window?: number
|
||||
method: "TIME_RANGE" | "LIMIT_UNTIL";
|
||||
window?: number;
|
||||
}
|
||||
|
||||
export interface TimelineSubject {
|
||||
type: "pubkey" | "hashtag" | "global" | "ptag" | "keyword",
|
||||
discriminator: string,
|
||||
items: string[]
|
||||
type: "pubkey" | "hashtag" | "global" | "ptag" | "keyword";
|
||||
discriminator: string;
|
||||
items: string[];
|
||||
}
|
||||
|
||||
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 [trackingEvents, setTrackingEvent] = useState<u256[]>([]);
|
||||
const [trackingParentEvents, setTrackingParentEvents] = useState<u256[]>([]);
|
||||
const pref = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||
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 [trackingEvents, setTrackingEvent] = useState<u256[]>([]);
|
||||
const [trackingParentEvents, setTrackingParentEvents] = useState<u256[]>([]);
|
||||
const pref = useSelector<RootState, UserPreferences>(
|
||||
(s) => s.login.preferences
|
||||
);
|
||||
|
||||
const createSub = useCallback(() => {
|
||||
if (subject.type !== "global" && subject.items.length === 0) {
|
||||
return null;
|
||||
const createSub = useCallback(() => {
|
||||
if (subject.type !== "global" && subject.items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let sub = new Subscriptions();
|
||||
sub.Id = `timeline:${subject.type}:${subject.discriminator}`;
|
||||
sub.Kinds = new Set([EventKind.TextNote, EventKind.Repost]);
|
||||
switch (subject.type) {
|
||||
case "pubkey": {
|
||||
sub.Authors = new Set(subject.items);
|
||||
break;
|
||||
}
|
||||
case "hashtag": {
|
||||
sub.HashTags = new Set(subject.items);
|
||||
break;
|
||||
}
|
||||
case "ptag": {
|
||||
sub.PTags = new Set(subject.items);
|
||||
break;
|
||||
}
|
||||
case "keyword": {
|
||||
sub.Kinds.add(EventKind.SetMetadata);
|
||||
sub.Search = subject.items[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sub;
|
||||
}, [subject.type, subject.items, subject.discriminator]);
|
||||
|
||||
const sub = useMemo(() => {
|
||||
let sub = createSub();
|
||||
if (sub) {
|
||||
if (options.method === "LIMIT_UNTIL") {
|
||||
sub.Until = until;
|
||||
sub.Limit = 10;
|
||||
} else {
|
||||
sub.Since = since;
|
||||
sub.Until = until;
|
||||
if (since === undefined) {
|
||||
sub.Limit = 50;
|
||||
}
|
||||
}
|
||||
|
||||
let sub = new Subscriptions();
|
||||
sub.Id = `timeline:${subject.type}:${subject.discriminator}`;
|
||||
sub.Kinds = new Set([EventKind.TextNote, EventKind.Repost]);
|
||||
switch (subject.type) {
|
||||
case "pubkey": {
|
||||
sub.Authors = new Set(subject.items);
|
||||
break;
|
||||
}
|
||||
case "hashtag": {
|
||||
sub.HashTags = new Set(subject.items);
|
||||
break;
|
||||
}
|
||||
case "ptag": {
|
||||
sub.PTags = new Set(subject.items);
|
||||
break;
|
||||
}
|
||||
case "keyword": {
|
||||
sub.Kinds.add(EventKind.SetMetadata);
|
||||
sub.Search = subject.items[0];
|
||||
break;
|
||||
}
|
||||
if (pref.autoShowLatest) {
|
||||
// copy properties of main sub but with limit 0
|
||||
// this will put latest directly into main feed
|
||||
let 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);
|
||||
}
|
||||
}
|
||||
return sub;
|
||||
}, [until, since, options.method, pref, createSub]);
|
||||
|
||||
const main = useSubscription(sub, { leaveOpen: true, cache: true });
|
||||
|
||||
const subRealtime = useMemo(() => {
|
||||
let subLatest = createSub();
|
||||
if (subLatest && !pref.autoShowLatest) {
|
||||
subLatest.Id = `${subLatest.Id}:latest`;
|
||||
subLatest.Limit = 1;
|
||||
subLatest.Since = Math.floor(new Date().getTime() / 1000);
|
||||
}
|
||||
return subLatest;
|
||||
}, [pref, createSub]);
|
||||
|
||||
const latest = useSubscription(subRealtime, {
|
||||
leaveOpen: true,
|
||||
cache: false,
|
||||
});
|
||||
|
||||
const subNext = useMemo(() => {
|
||||
let sub: Subscriptions | undefined;
|
||||
if (trackingEvents.length > 0 && pref.enableReactions) {
|
||||
sub = new Subscriptions();
|
||||
sub.Id = `timeline-related:${subject.type}`;
|
||||
sub.Kinds = new Set([
|
||||
EventKind.Reaction,
|
||||
EventKind.Deletion,
|
||||
EventKind.ZapReceipt,
|
||||
]);
|
||||
sub.ETags = new Set(trackingEvents);
|
||||
}
|
||||
return sub ?? null;
|
||||
}, [trackingEvents, pref, subject.type]);
|
||||
|
||||
const others = useSubscription(subNext, { leaveOpen: true, cache: true });
|
||||
|
||||
const subParents = useMemo(() => {
|
||||
if (trackingParentEvents.length > 0) {
|
||||
let 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);
|
||||
|
||||
useEffect(() => {
|
||||
if (main.store.notes.length > 0) {
|
||||
setTrackingEvent((s) => {
|
||||
let ids = main.store.notes.map((a) => a.id);
|
||||
if (ids.some((a) => !s.includes(a))) {
|
||||
return Array.from(new Set([...s, ...ids]));
|
||||
}
|
||||
return sub;
|
||||
}, [subject.type, subject.items, subject.discriminator]);
|
||||
return s;
|
||||
});
|
||||
let reposts = main.store.notes
|
||||
.filter((a) => a.kind === EventKind.Repost && a.content === "")
|
||||
.map((a) => a.tags.find((b) => b[0] === "e"))
|
||||
.filter((a) => a)
|
||||
.map((a) => a![1]);
|
||||
if (reposts.length > 0) {
|
||||
setTrackingParentEvents((s) => {
|
||||
if (reposts.some((a) => !s.includes(a))) {
|
||||
let temp = new Set([...s, ...reposts]);
|
||||
return Array.from(temp);
|
||||
}
|
||||
return s;
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [main.store]);
|
||||
|
||||
const sub = useMemo(() => {
|
||||
let sub = createSub();
|
||||
if (sub) {
|
||||
if (options.method === "LIMIT_UNTIL") {
|
||||
sub.Until = until;
|
||||
sub.Limit = 10;
|
||||
} else {
|
||||
sub.Since = since;
|
||||
sub.Until = until;
|
||||
if (since === undefined) {
|
||||
sub.Limit = 50;
|
||||
}
|
||||
}
|
||||
|
||||
if (pref.autoShowLatest) {
|
||||
// copy properties of main sub but with limit 0
|
||||
// this will put latest directly into main feed
|
||||
let 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);
|
||||
}
|
||||
}
|
||||
return sub;
|
||||
}, [until, since, options.method, pref, createSub]);
|
||||
|
||||
const main = useSubscription(sub, { leaveOpen: true, cache: true });
|
||||
|
||||
const subRealtime = useMemo(() => {
|
||||
let subLatest = createSub();
|
||||
if (subLatest && !pref.autoShowLatest) {
|
||||
subLatest.Id = `${subLatest.Id}:latest`;
|
||||
subLatest.Limit = 1;
|
||||
subLatest.Since = Math.floor(new Date().getTime() / 1000);
|
||||
}
|
||||
return subLatest;
|
||||
}, [pref, createSub]);
|
||||
|
||||
const latest = useSubscription(subRealtime, { leaveOpen: true, cache: false });
|
||||
|
||||
const subNext = useMemo(() => {
|
||||
let sub: Subscriptions | undefined;
|
||||
if (trackingEvents.length > 0 && pref.enableReactions) {
|
||||
sub = new Subscriptions();
|
||||
sub.Id = `timeline-related:${subject.type}`;
|
||||
sub.Kinds = new Set([EventKind.Reaction, EventKind.Deletion, EventKind.ZapReceipt]);
|
||||
sub.ETags = new Set(trackingEvents);
|
||||
}
|
||||
return sub ?? null;
|
||||
}, [trackingEvents, pref, subject.type]);
|
||||
|
||||
const others = useSubscription(subNext, { leaveOpen: true, cache: true });
|
||||
|
||||
const subParents = useMemo(() => {
|
||||
if (trackingParentEvents.length > 0) {
|
||||
let 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);
|
||||
|
||||
useEffect(() => {
|
||||
if (main.store.notes.length > 0) {
|
||||
setTrackingEvent(s => {
|
||||
let ids = main.store.notes.map(a => a.id);
|
||||
if (ids.some(a => !s.includes(a))) {
|
||||
return Array.from(new Set([...s, ...ids]));
|
||||
}
|
||||
return s;
|
||||
});
|
||||
let reposts = main.store.notes
|
||||
.filter(a => a.kind === EventKind.Repost && a.content === "")
|
||||
.map(a => a.tags.find(b => b[0] === "e"))
|
||||
.filter(a => a)
|
||||
.map(a => a![1]);
|
||||
if (reposts.length > 0) {
|
||||
setTrackingParentEvents(s => {
|
||||
if (reposts.some(a => !s.includes(a))) {
|
||||
let temp = new Set([...s, ...reposts]);
|
||||
return Array.from(temp);
|
||||
}
|
||||
return s;
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [main.store]);
|
||||
|
||||
return {
|
||||
main: main.store,
|
||||
related: others.store,
|
||||
latest: latest.store,
|
||||
parent: parent.store,
|
||||
loadMore: () => {
|
||||
console.debug("Timeline load more!")
|
||||
if (options.method === "LIMIT_UNTIL") {
|
||||
let 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);
|
||||
}
|
||||
},
|
||||
showLatest: () => {
|
||||
main.append(latest.store.notes);
|
||||
latest.clear();
|
||||
}
|
||||
};
|
||||
return {
|
||||
main: main.store,
|
||||
related: others.store,
|
||||
latest: latest.store,
|
||||
parent: parent.store,
|
||||
loadMore: () => {
|
||||
console.debug("Timeline load more!");
|
||||
if (options.method === "LIMIT_UNTIL") {
|
||||
let 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);
|
||||
}
|
||||
},
|
||||
showLatest: () => {
|
||||
main.append(latest.store.notes);
|
||||
latest.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -5,13 +5,13 @@ import { Subscriptions } from "Nostr/Subscriptions";
|
||||
import useSubscription from "./Subscription";
|
||||
|
||||
export default function useZapsFeed(pubkey: HexKey) {
|
||||
const sub = useMemo(() => {
|
||||
let x = new Subscriptions();
|
||||
x.Id = `zaps:${pubkey.slice(0, 12)}`;
|
||||
x.Kinds = new Set([EventKind.ZapReceipt]);
|
||||
x.PTags = new Set([pubkey]);
|
||||
return x;
|
||||
}, [pubkey]);
|
||||
const sub = useMemo(() => {
|
||||
let x = new Subscriptions();
|
||||
x.Id = `zaps:${pubkey.slice(0, 12)}`;
|
||||
x.Kinds = new Set([EventKind.ZapReceipt]);
|
||||
x.PTags = new Set([pubkey]);
|
||||
return x;
|
||||
}, [pubkey]);
|
||||
|
||||
return useSubscription(sub, { leaveOpen: true, cache: true });
|
||||
return useSubscription(sub, { leaveOpen: true, cache: true });
|
||||
}
|
||||
|
Reference in New Issue
Block a user