feat: optimistic update for cards

This commit is contained in:
2023-07-30 23:01:32 +02:00
parent 811b1ceaec
commit efd2f756fe
8 changed files with 113 additions and 36 deletions

View File

@ -1,10 +1,9 @@
import "./emoji-pack.css"; import "./emoji-pack.css";
import { type NostrEvent } from "@snort/system"; import { type NostrEvent } from "@snort/system";
import { unixNow } from "@snort/shared";
import AsyncButton from "element/async-button";
import { useLogin } from "hooks/login"; import { useLogin } from "hooks/login";
import { toEmojiPack } from "hooks/emoji"; import { toEmojiPack } from "hooks/emoji";
import AsyncButton from "element/async-button";
import { Mention } from "element/mention"; import { Mention } from "element/mention";
import { findTag } from "utils"; import { findTag } from "utils";
import { USER_EMOJIS } from "const"; import { USER_EMOJIS } from "const";
@ -38,7 +37,7 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
}); });
console.debug(ev); console.debug(ev);
System.BroadcastEvent(ev); System.BroadcastEvent(ev);
Login.setEmojis(newPacks, unixNow()); Login.setEmojis(newPacks, ev.created_at);
} }
} }

View File

@ -1,5 +1,4 @@
import { EventKind } from "@snort/system"; import { EventKind } from "@snort/system";
import { unixNow } from "@snort/shared";
import { useLogin } from "hooks/login"; import { useLogin } from "hooks/login";
import AsyncButton from "element/async-button"; import AsyncButton from "element/async-button";
@ -24,7 +23,7 @@ export function LoggedInFollowButton({ pubkey }: { pubkey: string }) {
}); });
console.debug(ev); console.debug(ev);
System.BroadcastEvent(ev); System.BroadcastEvent(ev);
Login.setFollows(newFollows, login.follows.content, unixNow()); Login.setFollows(newFollows, login.follows.content, ev.created_at);
} }
} }
@ -41,7 +40,7 @@ export function LoggedInFollowButton({ pubkey }: { pubkey: string }) {
}); });
console.debug(ev); console.debug(ev);
System.BroadcastEvent(ev); System.BroadcastEvent(ev);
Login.setFollows(newFollows, login.follows.content, unixNow()); Login.setFollows(newFollows, login.follows.content, ev.created_at);
} }
} }

View File

@ -1,5 +1,3 @@
import { unixNow } from "@snort/shared";
import { useLogin } from "hooks/login"; import { useLogin } from "hooks/login";
import AsyncButton from "element/async-button"; import AsyncButton from "element/async-button";
import { Login, System } from "index"; import { Login, System } from "index";
@ -24,7 +22,7 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
}); });
console.debug(ev); console.debug(ev);
System.BroadcastEvent(ev); System.BroadcastEvent(ev);
Login.setMuted(newMuted, login.muted.content, unixNow()); Login.setMuted(newMuted, login.muted.content, ev.created_at);
} }
} }
@ -41,7 +39,7 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
}); });
console.debug(ev); console.debug(ev);
System.BroadcastEvent(ev); System.BroadcastEvent(ev);
Login.setMuted(newMuted, login.muted.content, unixNow()); Login.setMuted(newMuted, login.muted.content, ev.created_at);
} }
} }

View File

@ -9,10 +9,10 @@ import type { NostrEvent } from "@snort/system";
import { Toggle } from "element/toggle"; import { Toggle } from "element/toggle";
import { useLogin } from "hooks/login"; import { useLogin } from "hooks/login";
import { useCards } from "hooks/cards"; import { useUserCards } from "hooks/cards";
import { CARD, USER_CARDS } from "const"; import { CARD, USER_CARDS } from "const";
import { toTag } from "utils"; import { toTag } from "utils";
import { System } from "index"; import { Login, System } from "index";
import { findTag } from "utils"; import { findTag } from "utils";
import { Icon } from "./icon"; import { Icon } from "./icon";
import { ExternalLink } from "./external-link"; import { ExternalLink } from "./external-link";
@ -130,6 +130,7 @@ function Card({ canEdit, ev, cards }: CardProps) {
}); });
console.debug(userCardsEv); console.debug(userCardsEv);
System.BroadcastEvent(userCardsEv); System.BroadcastEvent(userCardsEv);
Login.setCards(newTags, userCardsEv.created_at);
}, },
}), }),
[canEdit, tags, identifier], [canEdit, tags, identifier],
@ -278,18 +279,18 @@ function EditCard({ card, cards }: EditCardProps) {
async function onCancel() { async function onCancel() {
const pub = login?.publisher(); const pub = login?.publisher();
if (pub) { if (pub) {
const newTags = tags.filter((t) => !t.at(1).endsWith(`:${identifier}`));
const userCardsEv = await pub.generic((eb) => { const userCardsEv = await pub.generic((eb) => {
eb.kind(USER_CARDS).content(""); eb.kind(USER_CARDS).content("");
for (const tag of tags) { for (const tag of newTags) {
if (!tag.at(1).endsWith(`:${identifier}`)) { eb.tag(tag);
eb.tag(tag);
}
} }
return eb; return eb;
}); });
console.debug(userCardsEv); console.debug(userCardsEv);
System.BroadcastEvent(userCardsEv); System.BroadcastEvent(userCardsEv);
Login.setCards(newTags, userCardsEv.created_at);
setIsOpen(false); setIsOpen(false);
} }
} }
@ -384,7 +385,7 @@ function AddCard({ cards }: AddCardProps) {
export function StreamCards({ host }) { export function StreamCards({ host }) {
const login = useLogin(); const login = useLogin();
const canEdit = login?.pubkey === host; const canEdit = login?.pubkey === host;
const cards = useCards(host, canEdit); const cards = useUserCards(login.pubkey, login.cards.tags, canEdit);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const components = ( const components = (
<> <>

View File

@ -11,6 +11,67 @@ import { USER_CARDS, CARD } from "const";
import { findTag } from "utils"; import { findTag } from "utils";
import { System } from "index"; import { System } from "index";
export function useUserCards(
pubkey: string,
userCards: Array<string[]>,
leaveOpen = false,
) {
const related = useMemo(() => {
// filtering to only show CARD kinds for now, but in the future we could link and render anything
if (userCards?.length > 0) {
return userCards.filter(
(t) => t.at(0) === "a" && t.at(1)?.startsWith(`${CARD}:`),
);
}
return [];
}, [userCards]);
const subRelated = useMemo(() => {
if (!pubkey) return null;
const splitted = related.map((t) => t.at(1)!.split(":"));
const authors = splitted
.map((s) => s.at(1))
.filter((s) => s)
.map((s) => s as string);
const identifiers = splitted
.map((s) => s.at(2))
.filter((s) => s)
.map((s) => s as string);
const rb = new RequestBuilder(`cards:${pubkey}`);
rb.withOptions({ leaveOpen })
.withFilter()
.kinds([CARD])
.authors(authors)
.tag("d", identifiers);
return rb;
}, [pubkey, related]);
const { data } = useRequestBuilder<NoteCollection>(
System,
NoteCollection,
subRelated,
);
const cards = useMemo(() => {
return related
.map((t) => {
const [k, pubkey, identifier] = t.at(1).split(":");
const kind = Number(k);
return (data ?? []).find(
(e) =>
e.kind === kind &&
e.pubkey === pubkey &&
findTag(e, "d") === identifier,
);
})
.filter((e) => e);
}, [related, data]);
return cards;
}
export function useCards(pubkey: string, leaveOpen = false) { export function useCards(pubkey: string, leaveOpen = false) {
const sub = useMemo(() => { const sub = useMemo(() => {
const b = new RequestBuilder(`user-cards:${pubkey.slice(0, 12)}`); const b = new RequestBuilder(`user-cards:${pubkey.slice(0, 12)}`);

View File

@ -33,13 +33,10 @@ export function packId(pack: EmojiPack): string {
return `${pack.author}:${pack.name}`; return `${pack.author}:${pack.name}`;
} }
export function useUserEmojiPacks( export function useUserEmojiPacks(pubkey?: string, userEmoji: Array<string[]>) {
pubkey?: string,
userEmoji: { tags: string[][] },
) {
const related = useMemo(() => { const related = useMemo(() => {
if (userEmoji) { if (userEmoji?.length > 0) {
return userEmoji.tags.filter( return userEmoji.filter(
(t) => t.at(0) === "a" && t.at(1)?.startsWith(`${EMOJI_PACK}:`), (t) => t.at(0) === "a" && t.at(1)?.startsWith(`${EMOJI_PACK}:`),
); );
} }
@ -101,6 +98,6 @@ export default function useEmoji(pubkey?: string) {
sub, sub,
); );
const emojis = useUserEmojiPacks(pubkey, userEmoji ?? { tags: [] }); const emojis = useUserEmojiPacks(pubkey, userEmoji?.tags ?? []);
return emojis; return emojis;
} }

View File

@ -4,7 +4,7 @@ import { EventKind, NoteCollection, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react"; import { useRequestBuilder } from "@snort/system-react";
import { useUserEmojiPacks } from "hooks/emoji"; import { useUserEmojiPacks } from "hooks/emoji";
import { MUTED, USER_EMOJIS } from "const"; import { MUTED, USER_CARDS, USER_EMOJIS } from "const";
import { System, Login } from "index"; import { System, Login } from "index";
import { getPublisher } from "login"; import { getPublisher } from "login";
@ -46,7 +46,13 @@ export function useLoginEvents(pubkey?: string, leaveOpen = false) {
}) })
.withFilter() .withFilter()
.authors([pubkey]) .authors([pubkey])
.kinds([EventKind.ContactList, EventKind.Relays, MUTED, USER_EMOJIS]); .kinds([
EventKind.ContactList,
EventKind.Relays,
MUTED,
USER_EMOJIS,
USER_CARDS,
]);
return b; return b;
}, [pubkey, leaveOpen]); }, [pubkey, leaveOpen]);
@ -64,6 +70,9 @@ export function useLoginEvents(pubkey?: string, leaveOpen = false) {
if (ev?.kind === USER_EMOJIS) { if (ev?.kind === USER_EMOJIS) {
setUserEmojis(ev.tags); setUserEmojis(ev.tags);
} }
if (ev?.kind === USER_CARDS) {
Login.setCards(ev.tags, ev.created_at);
}
if (ev?.kind === MUTED) { if (ev?.kind === MUTED) {
Login.setMuted(ev.tags, ev.content, ev.created_at); Login.setMuted(ev.tags, ev.content, ev.created_at);
} }
@ -76,7 +85,7 @@ export function useLoginEvents(pubkey?: string, leaveOpen = false) {
} }
}, [data]); }, [data]);
const emojis = useUserEmojiPacks(pubkey, { tags: userEmojis }); const emojis = useUserEmojiPacks(pubkey, userEmojis);
useEffect(() => { useEffect(() => {
Login.setEmojis(emojis); Login.setEmojis(emojis);
}, [emojis]); }, [emojis]);

View File

@ -12,7 +12,7 @@ export enum LoginType {
interface ReplaceableTags { interface ReplaceableTags {
tags: Array<string[]>; tags: Array<string[]>;
content: ""; content?: string;
timestamp: number; timestamp: number;
} }
@ -22,10 +22,19 @@ export interface LoginSession {
privateKey?: string; privateKey?: string;
follows: ReplaceableTags; follows: ReplaceableTags;
muted: ReplaceableTags; muted: ReplaceableTags;
cards: ReplaceableTags;
relays: Relays; relays: Relays;
emojis: Array<EmojiPack>; emojis: Array<EmojiPack>;
} }
const initialState = {
follows: { tags: [], timestamp: 0 },
muted: { tags: [], timestamp: 0 },
cards: { tags: [], timestamp: 0 },
relays: defaultRelays,
emojis: [],
};
export class LoginStore extends ExternalStore<LoginSession | undefined> { export class LoginStore extends ExternalStore<LoginSession | undefined> {
#session?: LoginSession; #session?: LoginSession;
@ -33,7 +42,7 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
super(); super();
const json = window.localStorage.getItem("session"); const json = window.localStorage.getItem("session");
if (json) { if (json) {
this.#session = JSON.parse(json); this.#session = { ...initialState, ...JSON.parse(json) };
if (this.#session) { if (this.#session) {
this.#session.type ??= LoginType.Nip7; this.#session.type ??= LoginType.Nip7;
} }
@ -44,10 +53,7 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
this.#session = { this.#session = {
type, type,
pubkey: pk, pubkey: pk,
muted: { tags: [], timestamp: 0 }, ...initialState,
follows: { tags: [], timestamp: 0 },
relays: defaultRelays,
emojis: [],
}; };
this.#save(); this.#save();
} }
@ -57,9 +63,7 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
type: LoginType.PrivateKey, type: LoginType.PrivateKey,
pubkey: bytesToHex(schnorr.getPublicKey(key)), pubkey: bytesToHex(schnorr.getPublicKey(key)),
privateKey: key, privateKey: key,
follows: { tags: [], timestamp: 0 }, ...initialState,
muted: { tags: [], timestamp: 0 },
emojis: [],
}; };
this.#save(); this.#save();
} }
@ -98,6 +102,15 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
this.#save(); this.#save();
} }
setCards(cards: Array<string[]>, ts: number) {
if (this.#session.cards.timestamp >= ts) {
return;
}
this.#session.cards.tags = cards;
this.#session.cards.timestamp = ts;
this.#save();
}
setRelays(relays: Array<string>, ts: number) { setRelays(relays: Array<string>, ts: number) {
if (this.#session.relays.timestamp >= ts) { if (this.#session.relays.timestamp >= ts) {
return; return;