feat: optimistic update for cards
This commit is contained in:
parent
811b1ceaec
commit
efd2f756fe
@ -1,10 +1,9 @@
|
||||
import "./emoji-pack.css";
|
||||
import { type NostrEvent } from "@snort/system";
|
||||
import { unixNow } from "@snort/shared";
|
||||
|
||||
import AsyncButton from "element/async-button";
|
||||
import { useLogin } from "hooks/login";
|
||||
import { toEmojiPack } from "hooks/emoji";
|
||||
import AsyncButton from "element/async-button";
|
||||
import { Mention } from "element/mention";
|
||||
import { findTag } from "utils";
|
||||
import { USER_EMOJIS } from "const";
|
||||
@ -38,7 +37,7 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
|
||||
});
|
||||
console.debug(ev);
|
||||
System.BroadcastEvent(ev);
|
||||
Login.setEmojis(newPacks, unixNow());
|
||||
Login.setEmojis(newPacks, ev.created_at);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { EventKind } from "@snort/system";
|
||||
import { unixNow } from "@snort/shared";
|
||||
|
||||
import { useLogin } from "hooks/login";
|
||||
import AsyncButton from "element/async-button";
|
||||
@ -24,7 +23,7 @@ export function LoggedInFollowButton({ pubkey }: { pubkey: string }) {
|
||||
});
|
||||
console.debug(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);
|
||||
System.BroadcastEvent(ev);
|
||||
Login.setFollows(newFollows, login.follows.content, unixNow());
|
||||
Login.setFollows(newFollows, login.follows.content, ev.created_at);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { unixNow } from "@snort/shared";
|
||||
|
||||
import { useLogin } from "hooks/login";
|
||||
import AsyncButton from "element/async-button";
|
||||
import { Login, System } from "index";
|
||||
@ -24,7 +22,7 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
|
||||
});
|
||||
console.debug(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);
|
||||
System.BroadcastEvent(ev);
|
||||
Login.setMuted(newMuted, login.muted.content, unixNow());
|
||||
Login.setMuted(newMuted, login.muted.content, ev.created_at);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,10 +9,10 @@ import type { NostrEvent } from "@snort/system";
|
||||
|
||||
import { Toggle } from "element/toggle";
|
||||
import { useLogin } from "hooks/login";
|
||||
import { useCards } from "hooks/cards";
|
||||
import { useUserCards } from "hooks/cards";
|
||||
import { CARD, USER_CARDS } from "const";
|
||||
import { toTag } from "utils";
|
||||
import { System } from "index";
|
||||
import { Login, System } from "index";
|
||||
import { findTag } from "utils";
|
||||
import { Icon } from "./icon";
|
||||
import { ExternalLink } from "./external-link";
|
||||
@ -130,6 +130,7 @@ function Card({ canEdit, ev, cards }: CardProps) {
|
||||
});
|
||||
console.debug(userCardsEv);
|
||||
System.BroadcastEvent(userCardsEv);
|
||||
Login.setCards(newTags, userCardsEv.created_at);
|
||||
},
|
||||
}),
|
||||
[canEdit, tags, identifier],
|
||||
@ -278,18 +279,18 @@ function EditCard({ card, cards }: EditCardProps) {
|
||||
async function onCancel() {
|
||||
const pub = login?.publisher();
|
||||
if (pub) {
|
||||
const newTags = tags.filter((t) => !t.at(1).endsWith(`:${identifier}`));
|
||||
const userCardsEv = await pub.generic((eb) => {
|
||||
eb.kind(USER_CARDS).content("");
|
||||
for (const tag of tags) {
|
||||
if (!tag.at(1).endsWith(`:${identifier}`)) {
|
||||
eb.tag(tag);
|
||||
}
|
||||
for (const tag of newTags) {
|
||||
eb.tag(tag);
|
||||
}
|
||||
return eb;
|
||||
});
|
||||
|
||||
console.debug(userCardsEv);
|
||||
System.BroadcastEvent(userCardsEv);
|
||||
Login.setCards(newTags, userCardsEv.created_at);
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
@ -384,7 +385,7 @@ function AddCard({ cards }: AddCardProps) {
|
||||
export function StreamCards({ host }) {
|
||||
const login = useLogin();
|
||||
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 components = (
|
||||
<>
|
||||
|
@ -11,6 +11,67 @@ import { USER_CARDS, CARD } from "const";
|
||||
import { findTag } from "utils";
|
||||
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) {
|
||||
const sub = useMemo(() => {
|
||||
const b = new RequestBuilder(`user-cards:${pubkey.slice(0, 12)}`);
|
||||
|
@ -33,13 +33,10 @@ export function packId(pack: EmojiPack): string {
|
||||
return `${pack.author}:${pack.name}`;
|
||||
}
|
||||
|
||||
export function useUserEmojiPacks(
|
||||
pubkey?: string,
|
||||
userEmoji: { tags: string[][] },
|
||||
) {
|
||||
export function useUserEmojiPacks(pubkey?: string, userEmoji: Array<string[]>) {
|
||||
const related = useMemo(() => {
|
||||
if (userEmoji) {
|
||||
return userEmoji.tags.filter(
|
||||
if (userEmoji?.length > 0) {
|
||||
return userEmoji.filter(
|
||||
(t) => t.at(0) === "a" && t.at(1)?.startsWith(`${EMOJI_PACK}:`),
|
||||
);
|
||||
}
|
||||
@ -101,6 +98,6 @@ export default function useEmoji(pubkey?: string) {
|
||||
sub,
|
||||
);
|
||||
|
||||
const emojis = useUserEmojiPacks(pubkey, userEmoji ?? { tags: [] });
|
||||
const emojis = useUserEmojiPacks(pubkey, userEmoji?.tags ?? []);
|
||||
return emojis;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { EventKind, NoteCollection, RequestBuilder } from "@snort/system";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
|
||||
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 { getPublisher } from "login";
|
||||
|
||||
@ -46,7 +46,13 @@ export function useLoginEvents(pubkey?: string, leaveOpen = false) {
|
||||
})
|
||||
.withFilter()
|
||||
.authors([pubkey])
|
||||
.kinds([EventKind.ContactList, EventKind.Relays, MUTED, USER_EMOJIS]);
|
||||
.kinds([
|
||||
EventKind.ContactList,
|
||||
EventKind.Relays,
|
||||
MUTED,
|
||||
USER_EMOJIS,
|
||||
USER_CARDS,
|
||||
]);
|
||||
return b;
|
||||
}, [pubkey, leaveOpen]);
|
||||
|
||||
@ -64,6 +70,9 @@ export function useLoginEvents(pubkey?: string, leaveOpen = false) {
|
||||
if (ev?.kind === USER_EMOJIS) {
|
||||
setUserEmojis(ev.tags);
|
||||
}
|
||||
if (ev?.kind === USER_CARDS) {
|
||||
Login.setCards(ev.tags, ev.created_at);
|
||||
}
|
||||
if (ev?.kind === MUTED) {
|
||||
Login.setMuted(ev.tags, ev.content, ev.created_at);
|
||||
}
|
||||
@ -76,7 +85,7 @@ export function useLoginEvents(pubkey?: string, leaveOpen = false) {
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const emojis = useUserEmojiPacks(pubkey, { tags: userEmojis });
|
||||
const emojis = useUserEmojiPacks(pubkey, userEmojis);
|
||||
useEffect(() => {
|
||||
Login.setEmojis(emojis);
|
||||
}, [emojis]);
|
||||
|
31
src/login.ts
31
src/login.ts
@ -12,7 +12,7 @@ export enum LoginType {
|
||||
|
||||
interface ReplaceableTags {
|
||||
tags: Array<string[]>;
|
||||
content: "";
|
||||
content?: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
@ -22,10 +22,19 @@ export interface LoginSession {
|
||||
privateKey?: string;
|
||||
follows: ReplaceableTags;
|
||||
muted: ReplaceableTags;
|
||||
cards: ReplaceableTags;
|
||||
relays: Relays;
|
||||
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> {
|
||||
#session?: LoginSession;
|
||||
|
||||
@ -33,7 +42,7 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
|
||||
super();
|
||||
const json = window.localStorage.getItem("session");
|
||||
if (json) {
|
||||
this.#session = JSON.parse(json);
|
||||
this.#session = { ...initialState, ...JSON.parse(json) };
|
||||
if (this.#session) {
|
||||
this.#session.type ??= LoginType.Nip7;
|
||||
}
|
||||
@ -44,10 +53,7 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
|
||||
this.#session = {
|
||||
type,
|
||||
pubkey: pk,
|
||||
muted: { tags: [], timestamp: 0 },
|
||||
follows: { tags: [], timestamp: 0 },
|
||||
relays: defaultRelays,
|
||||
emojis: [],
|
||||
...initialState,
|
||||
};
|
||||
this.#save();
|
||||
}
|
||||
@ -57,9 +63,7 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
|
||||
type: LoginType.PrivateKey,
|
||||
pubkey: bytesToHex(schnorr.getPublicKey(key)),
|
||||
privateKey: key,
|
||||
follows: { tags: [], timestamp: 0 },
|
||||
muted: { tags: [], timestamp: 0 },
|
||||
emojis: [],
|
||||
...initialState,
|
||||
};
|
||||
this.#save();
|
||||
}
|
||||
@ -98,6 +102,15 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
|
||||
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) {
|
||||
if (this.#session.relays.timestamp >= ts) {
|
||||
return;
|
||||
|
Loading…
Reference in New Issue
Block a user