diff --git a/package.json b/package.json index 5a969ad..210edd1 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "@noble/hashes": "^1.4.0", "@scure/base": "^1.1.6", "@snort/shared": "^1.0.15", - "@snort/system": "^1.3.3", - "@snort/system-react": "^1.3.3", + "@snort/system": "^1.3.5", + "@snort/system-react": "^1.3.5", "@snort/system-wasm": "^1.0.4", "@snort/wallet": "^0.1.3", "@snort/worker-relay": "^1.1.0", diff --git a/src/const.ts b/src/const.ts index c99cebd..6aa9452 100644 --- a/src/const.ts +++ b/src/const.ts @@ -4,12 +4,9 @@ export const LIVE_STREAM = 30_311 as EventKind; export const LIVE_STREAM_CHAT = 1_311 as EventKind; export const LIVE_STREAM_RAID = 1_312 as EventKind; export const LIVE_STREAM_CLIP = 1_313 as EventKind; -export const EMOJI_PACK = 30_030 as EventKind; -export const USER_EMOJIS = 10_030 as EventKind; export const GOAL = 9041 as EventKind; export const USER_CARDS = 17_777 as EventKind; export const CARD = 37_777 as EventKind; -export const MUTED = 10_000 as EventKind; export const VIDEO_KIND = 34_235 as EventKind; export const SHORTS_KIND = 34_236 as EventKind; diff --git a/src/element/async-button.tsx b/src/element/async-button.tsx index 8b1dcc7..12a9f84 100644 --- a/src/element/async-button.tsx +++ b/src/element/async-button.tsx @@ -20,6 +20,9 @@ const AsyncButton = forwardRef((props: Asyn if (props.onClick) { await props.onClick(e); } + } catch (e) { + console.error(e); + throw e; } finally { setLoading(false); } diff --git a/src/element/chat/live-chat.tsx b/src/element/chat/live-chat.tsx index d4a22f9..9aa6557 100644 --- a/src/element/chat/live-chat.tsx +++ b/src/element/chat/live-chat.tsx @@ -2,7 +2,7 @@ import "./live-chat.css"; import { FormattedMessage } from "react-intl"; import { EventKind, NostrEvent, NostrLink, NostrPrefix, ParsedZap, TaggedNostrEvent } from "@snort/system"; import { useEventFeed, useEventReactions, useReactions, useUserProfile } from "@snort/system-react"; -import { unixNow, unwrap } from "@snort/shared"; +import { removeUndefined, unixNow, unwrap } from "@snort/shared"; import { useEffect, useMemo } from "react"; import { Icon } from "../icon"; @@ -87,11 +87,9 @@ export function LiveChat({ return starts ? Number(starts) : unixNow() - WEEK; }, [ev]); const { badges, awards } = useBadges(host, started); - const mutedPubkeys = useMemo(() => { - return new Set(getTagValues(login?.muted.tags ?? [], "p")); - }, [login]); + const hostMutedPubkeys = useMutedPubkeys(host, true); - const userEmojiPacks = login?.emojis ?? []; + const userEmojiPacks = useEmoji(login?.pubkey); const channelEmojiPacks = useEmoji(host); const allEmojiPacks = useMemo(() => { return uniqBy(userEmojiPacks.concat(channelEmojiPacks), packId); @@ -110,7 +108,7 @@ export function LiveChat({ if (ends) { extra.push({ kind: -2, created_at: Number(ends) } as TaggedNostrEvent); } - return [...feed, ...awards, ...extra] + return removeUndefined([...feed, ...awards, ...extra]) .filter(a => a.created_at >= started) .sort((a, b) => b.created_at - a.created_at); }, [feed, awards]); @@ -145,8 +143,14 @@ export function LiveChat({ }, [adjustLayout]); const filteredEvents = useMemo(() => { - return events.filter(e => !mutedPubkeys.has(e.pubkey) && !hostMutedPubkeys.has(e.pubkey)); - }, [events, mutedPubkeys, hostMutedPubkeys]); + return events.filter(e => { + if (!e.pubkey) return true; // injected content + const author = NostrLink.publicKey(e.pubkey); + return ( + !(login?.state?.muted.some(a => a.equals(author)) ?? true) && !hostMutedPubkeys.some(a => a.equals(author)) + ); + }); + }, [events, login?.state?.version, hostMutedPubkeys]); return (
diff --git a/src/element/emoji-pack.tsx b/src/element/emoji-pack.tsx index 645e850..0485c36 100644 --- a/src/element/emoji-pack.tsx +++ b/src/element/emoji-pack.tsx @@ -1,43 +1,24 @@ import "./emoji-pack.css"; -import { type NostrEvent } from "@snort/system"; +import { EventKind, NostrLink, type NostrEvent } from "@snort/system"; import { FormattedMessage } from "react-intl"; -import { useContext } from "react"; -import { SnortContext } from "@snort/system-react"; import { useLogin } from "@/hooks/login"; -import { toEmojiPack } from "@/hooks/emoji"; -import { findTag } from "@/utils"; -import { USER_EMOJIS } from "@/const"; -import { Login } from "@/login"; -import type { EmojiPack as EmojiPackType } from "@/types"; +import useEmoji from "@/hooks/emoji"; import { DefaultButton, WarningButton } from "./buttons"; export function EmojiPack({ ev }: { ev: NostrEvent }) { - const system = useContext(SnortContext); const login = useLogin(); - const name = findTag(ev, "d"); - const isUsed = login?.emojis.find(e => e.author === ev.pubkey && e.name === name); + const link = NostrLink.fromEvent(ev); + const name = link.id; + const emojis = useEmoji(login?.pubkey); + const isUsed = emojis.find(e => e.author === link.author && e.name === link.id); const emoji = ev.tags.filter(e => e.at(0) === "emoji"); async function toggleEmojiPack() { - let newPacks = [] as EmojiPackType[]; if (isUsed) { - newPacks = login?.emojis.filter(e => e.author !== ev.pubkey && e.name !== name) ?? []; + await login?.state?.removeFromList(EventKind.EmojisList, link, true); } else { - newPacks = [...(login?.emojis ?? []), toEmojiPack(ev)]; - } - const pub = login?.publisher(); - if (pub) { - const ev = await pub.generic(eb => { - eb.kind(USER_EMOJIS).content(""); - for (const e of newPacks) { - eb.tag(["a", e.address]); - } - return eb; - }); - console.debug(ev); - await system.BroadcastEvent(ev); - Login.setEmojis(newPacks); + await login?.state?.addToList(EventKind.EmojisList, link, true); } } diff --git a/src/element/event-embed.tsx b/src/element/event-embed.tsx index 9ae1061..f0a13bf 100644 --- a/src/element/event-embed.tsx +++ b/src/element/event-embed.tsx @@ -5,7 +5,7 @@ import { Goal } from "./goal"; import { Note } from "./note"; import { EmojiPack } from "./emoji-pack"; import { Badge } from "./badge"; -import { EMOJI_PACK, GOAL, LIVE_STREAM_CLIP, StreamState } from "@/const"; +import { GOAL, LIVE_STREAM_CLIP, StreamState } from "@/const"; import { useEventFeed } from "@snort/system-react"; import LiveStreamClip from "./stream/clip"; import { ExternalLink } from "./external-link"; @@ -21,7 +21,7 @@ export function EventIcon({ kind }: { kind?: EventKind }) { switch (kind) { case GOAL: return ; - case EMOJI_PACK: + case EventKind.EmojiSet: return ; case EventKind.Badge: return ; @@ -35,7 +35,7 @@ export function NostrEvent({ ev }: { ev: TaggedNostrEvent }) { case GOAL: { return ; } - case EMOJI_PACK: { + case EventKind.EmojiSet: { return ; } case EventKind.Badge: { diff --git a/src/element/follow-button.tsx b/src/element/follow-button.tsx index 1497a17..a4ab367 100644 --- a/src/element/follow-button.tsx +++ b/src/element/follow-button.tsx @@ -1,67 +1,28 @@ -import { EventKind } from "@snort/system"; +import { NostrHashtagLink, NostrLink, NostrPrefix } from "@snort/system"; import { FormattedMessage } from "react-intl"; -import { useContext } from "react"; -import { SnortContext } from "@snort/system-react"; import { useLogin } from "@/hooks/login"; -import { Login } from "@/login"; import { DefaultButton } from "./buttons"; import { Icon } from "./icon"; -export function LoggedInFollowButton({ - tag, - value, - hideWhenFollowing, -}: { - tag: "p" | "t"; - value: string; - hideWhenFollowing?: boolean; -}) { - const system = useContext(SnortContext); +export function LoggedInFollowButton({ link, hideWhenFollowing }: { link: NostrLink; hideWhenFollowing?: boolean }) { const login = useLogin(); - if (!login) return; + if (!login?.state) return; - const { tags, content, timestamp } = login.follows; - const follows = tags.filter(t => t.at(0) === tag); - const isFollowing = follows.find(t => t.at(1) === value); + const follows = login.state.follows ?? []; + const isFollowing = follows.includes(link.id); async function unfollow() { - const pub = login?.publisher(); - if (pub) { - const newFollows = tags.filter(t => t.at(1) !== value); - const ev = await pub.generic(eb => { - eb.kind(EventKind.ContactList).content(content ?? ""); - for (const t of newFollows) { - eb.tag(t); - } - return eb; - }); - console.debug(ev); - await system.BroadcastEvent(ev); - Login.setFollows(newFollows, content ?? "", ev.created_at); - } + await login?.state?.unfollow(link, true); } async function follow() { - const pub = login?.publisher(); - if (pub) { - const newFollows = [...tags, [tag, value]]; - const ev = await pub.generic(eb => { - eb.kind(EventKind.ContactList).content(content ?? ""); - for (const tag of newFollows) { - eb.tag(tag); - } - return eb; - }); - console.debug(ev); - await system.BroadcastEvent(ev); - Login.setFollows(newFollows, content ?? "", ev.created_at); - } + await login?.state?.follow(link, true); } if (isFollowing && hideWhenFollowing) return; return ( - + {isFollowing ? ( ) : ( @@ -75,11 +36,14 @@ export function LoggedInFollowButton({ } export function FollowTagButton({ tag, hideWhenFollowing }: { tag: string; hideWhenFollowing?: boolean }) { - const login = useLogin(); - return login?.pubkey ? : null; + //const login = useLogin(); + //const link = new NostrHashtagLink(tag); + return; + //return login?.pubkey ? : null; } export function FollowButton({ pubkey, hideWhenFollowing }: { pubkey: string; hideWhenFollowing?: boolean }) { const login = useLogin(); - return login?.pubkey ? : null; + const link = new NostrLink(NostrPrefix.PublicKey, pubkey); + return login?.pubkey ? : null; } diff --git a/src/element/mute-button.tsx b/src/element/mute-button.tsx index e7ee98d..3c7ef89 100644 --- a/src/element/mute-button.tsx +++ b/src/element/mute-button.tsx @@ -1,54 +1,30 @@ -import { useContext, useMemo } from "react"; import { FormattedMessage } from "react-intl"; -import { SnortContext } from "@snort/system-react"; import { useLogin } from "@/hooks/login"; -import { Login } from "@/login"; -import { MUTED } from "@/const"; import { DefaultButton } from "./buttons"; +import { NostrLink } from "@snort/system"; export function useMute(pubkey: string) { - const system = useContext(SnortContext); const login = useLogin(); - const { tags, content } = login?.muted ?? { tags: [] }; - const muted = useMemo(() => tags.filter(t => t.at(0) === "p"), [tags]); - const isMuted = useMemo(() => muted.find(t => t.at(1) === pubkey), [pubkey, muted]); + const link = NostrLink.publicKey(pubkey); async function unmute() { - const pub = login?.publisher(); - if (pub) { - const newMuted = tags.filter(t => t.at(1) !== pubkey); - const ev = await pub.generic(eb => { - eb.kind(MUTED).content(content ?? ""); - for (const t of newMuted) { - eb.tag(t); - } - return eb; - }); - console.debug(ev); - await system.BroadcastEvent(ev); - Login.setMuted(newMuted, content ?? "", ev.created_at); - } + await login?.state?.unmute(link, true); } async function mute() { - const pub = login?.publisher(); - if (pub) { - const newMuted = [...tags, ["p", pubkey]]; - const ev = await pub.generic(eb => { - eb.kind(MUTED).content(content ?? ""); - for (const tag of newMuted) { - eb.tag(tag); - } - return eb; - }); - console.debug(ev); - await system.BroadcastEvent(ev); - Login.setMuted(newMuted, content ?? "", ev.created_at); + try { + await login?.state?.mute(link, true); + } catch (e) { + console.error(e); } } - return { isMuted, mute, unmute }; + return { + isMuted: login?.state?.muted.some(a => a.equals(link)) ?? false, + mute, + unmute, + }; } export function LoggedInMuteButton({ pubkey }: { pubkey: string }) { diff --git a/src/element/stream-cards/index.tsx b/src/element/stream-cards/index.tsx index fd726f7..62122d6 100644 --- a/src/element/stream-cards/index.tsx +++ b/src/element/stream-cards/index.tsx @@ -6,6 +6,7 @@ import { useCards } from "@/hooks/cards"; import { StreamCardEditor } from "./stream-card-editor"; import { Card } from "./card-item"; import classNames from "classnames"; +import { USER_CARDS } from "@/const"; export interface CardType { identifier: string; @@ -40,13 +41,10 @@ export function ReadOnlyStreamCards({ host, className }: StreamCardsProps) { export function StreamCards({ host }: StreamCardsProps) { const login = useLogin(); const canEdit = login?.pubkey === host; + const cards = login?.state?.getList(USER_CARDS); return ( - {canEdit ? ( - - ) : ( - - )} + {canEdit ? : } ); } diff --git a/src/element/stream-cards/stream-card-editor.tsx b/src/element/stream-cards/stream-card-editor.tsx index 8301a00..194e3a7 100644 --- a/src/element/stream-cards/stream-card-editor.tsx +++ b/src/element/stream-cards/stream-card-editor.tsx @@ -3,16 +3,21 @@ import { FormattedMessage } from "react-intl"; import { Toggle } from "../toggle"; import { useUserCards } from "@/hooks/cards"; import { AddCard } from "./add-card"; -import { Tags } from "@/types"; import { Card } from "./card-item"; +import { ToNostrEventTag } from "@snort/system"; +import { Tag } from "@/types"; interface StreamCardEditorProps { pubkey: string; - tags: Tags; + tags: Array; } export function StreamCardEditor({ pubkey, tags }: StreamCardEditorProps) { - const cards = useUserCards(pubkey, tags, true); + const cards = useUserCards( + pubkey, + tags.map(a => a.toEventTag() as Tag), + true, + ); const [isEditing, setIsEditing] = useState(false); return ( <> diff --git a/src/element/video-grid-sorted.tsx b/src/element/video-grid-sorted.tsx index 567a637..47f0c43 100644 --- a/src/element/video-grid-sorted.tsx +++ b/src/element/video-grid-sorted.tsx @@ -1,8 +1,8 @@ import { useLogin } from "@/hooks/login"; import { useSortedStreams } from "@/hooks/useLiveStreams"; import { getTagValues, getHost, extractStreamInfo } from "@/utils"; -import { NostrEvent, TaggedNostrEvent } from "@snort/system"; -import { ReactNode, useCallback, useMemo } from "react"; +import { NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system"; +import { ReactNode, useMemo } from "react"; import { FormattedMessage } from "react-intl"; import VideoGrid from "./video-grid"; import { StreamTile } from "./stream/stream-tile"; @@ -34,33 +34,27 @@ export default function VideoGridSorted({ showVideos, }: VideoGridSortedProps) { const login = useLogin(); - const mutedHosts = new Set(getTagValues(login?.muted.tags ?? [], "p")); - const tags = login?.follows.tags ?? []; - const followsHost = useCallback( - (ev: NostrEvent) => { - return tags.find(t => t.at(1) === getHost(ev)); - }, - [tags], - ); - const { live, planned, ended } = useSortedStreams(evs, showAll ? 0 : undefined); - const hashtags = getTagValues(tags, "t"); + const mutedHosts = login?.state?.muted ?? []; + const follows = login?.state?.follows ?? []; + const followsHost = (ev: NostrEvent) => follows?.includes(getHost(ev)); + + const filteredStreams = evs.filter(a => !mutedHosts.includes(NostrLink.publicKey(getHost(a)))); + const { live, planned, ended } = useSortedStreams(filteredStreams, showAll ? 0 : undefined); + const hashtags: Array = []; const following = live.filter(followsHost); const liveNow = live.filter(e => !following.includes(e)); const hasFollowingLive = following.length > 0; - const plannedEvents = planned.filter(e => !mutedHosts.has(getHost(e))).filter(followsHost); - const endedEvents = ended.filter(e => !mutedHosts.has(getHost(e))); + const plannedEvents = planned.filter(followsHost); const liveByHashtag = useMemo(() => { return hashtags .map(t => ({ tag: t, - live: live - .filter(e => !mutedHosts.has(getHost(e))) - .filter(e => { - const evTags = getTagValues(e.tags, "t"); - return evTags.includes(t); - }), + live: live.filter(e => { + const evTags = getTagValues(e.tags, "t"); + return evTags.includes(t); + }), })) .filter(t => t.live.length > 0); }, [live, hashtags]); @@ -72,11 +66,9 @@ export default function VideoGridSorted({ )} {!hasFollowingLive && ( - {live - .filter(e => !mutedHosts.has(getHost(e))) - .map(e => ( - - ))} + {live.map(e => ( + + ))} )} {liveByHashtag.map(t => ( @@ -96,8 +88,8 @@ export default function VideoGridSorted({ {plannedEvents.length > 0 && (showPlanned ?? true) && ( } items={plannedEvents} /> )} - {endedEvents.length > 0 && (showEnded ?? true) && ( - } items={endedEvents} /> + {ended.length > 0 && (showEnded ?? true) && ( + } items={ended} /> )}
); diff --git a/src/hooks/emoji.tsx b/src/hooks/emoji.tsx index dceffad..ffdf636 100644 --- a/src/hooks/emoji.tsx +++ b/src/hooks/emoji.tsx @@ -1,9 +1,8 @@ import { useMemo } from "react"; -import { NostrEvent, RequestBuilder } from "@snort/system"; +import { EventKind, NostrEvent, RequestBuilder } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; import { findTag, uniqBy } from "@/utils"; -import { EMOJI_PACK, USER_EMOJIS } from "@/const"; import type { EmojiPack, EmojiTag, Tags } from "@/types"; function cleanShortcode(shortcode?: string) { @@ -29,7 +28,7 @@ export function packId(pack: EmojiPack): string { export function useUserEmojiPacks(pubkey?: string, userEmoji?: Tags) { const related = useMemo(() => { if (userEmoji) { - return userEmoji?.filter(t => t.at(0) === "a" && t.at(1)?.startsWith(`${EMOJI_PACK}:`)); + return userEmoji?.filter(t => t.at(0) === "a" && t.at(1)?.startsWith(`${EventKind.EmojiSet}:`)); } return []; }, [userEmoji]); @@ -48,9 +47,9 @@ export function useUserEmojiPacks(pubkey?: string, userEmoji?: Tags) { const rb = new RequestBuilder(`emoji-related:${pubkey}`); - rb.withFilter().kinds([EMOJI_PACK]).authors(authors).tag("d", identifiers); + rb.withFilter().kinds([EventKind.EmojiSet]).authors(authors).tag("d", identifiers); - rb.withFilter().kinds([EMOJI_PACK]).authors([pubkey]); + rb.withFilter().kinds([EventKind.EmojiSet]).authors([pubkey]); return rb; }, [pubkey, related]); @@ -73,8 +72,7 @@ export default function useEmoji(pubkey?: string) { const sub = useMemo(() => { if (!pubkey) return null; const rb = new RequestBuilder(`emoji:${pubkey}`); - - rb.withFilter().authors([pubkey]).kinds([USER_EMOJIS]); + rb.withFilter().authors([pubkey]).kinds([EventKind.EmojisList]); return rb; }, [pubkey]); diff --git a/src/hooks/lists.ts b/src/hooks/lists.ts index 95baf6f..153e58b 100644 --- a/src/hooks/lists.ts +++ b/src/hooks/lists.ts @@ -1,23 +1,20 @@ import { useMemo } from "react"; -import { RequestBuilder } from "@snort/system"; +import { EventKind, NostrLink, RequestBuilder } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; -import { MUTED } from "@/const"; -import { getTagValues } from "@/utils"; - export function useMutedPubkeys(host?: string, leaveOpen = false) { const mutedSub = useMemo(() => { if (!host) return null; const rb = new RequestBuilder(`muted:${host}`); rb.withOptions({ leaveOpen }); - rb.withFilter().kinds([MUTED]).authors([host]); + rb.withFilter().kinds([EventKind.MuteList]).authors([host]); return rb; }, [host]); const muted = useRequestBuilder(mutedSub); const mutedPubkeys = useMemo(() => { - return new Set(getTagValues(muted?.at(0)?.tags ?? [], "p")); + return muted.flatMap(a => NostrLink.fromAllTags(a.tags)); }, [muted]); return mutedPubkeys; diff --git a/src/hooks/login.ts b/src/hooks/login.ts index c21be03..de5f546 100644 --- a/src/hooks/login.ts +++ b/src/hooks/login.ts @@ -1,11 +1,5 @@ -import { useEffect, useMemo, useState, useSyncExternalStore } from "react"; +import { useSyncExternalStore } from "react"; -import { EventKind, RequestBuilder } from "@snort/system"; -import { useRequestBuilder } from "@snort/system-react"; - -import { useUserEmojiPacks } from "@/hooks/emoji"; -import { MUTED, USER_CARDS, USER_EMOJIS } from "@/const"; -import type { Tags } from "@/types"; import { getPublisher, getSigner, Login, LoginSession } from "@/login"; export function useLogin() { @@ -27,52 +21,3 @@ export function useLogin() { }, }; } - -export function useLoginEvents(pubkey?: string, leaveOpen = false) { - const [userEmojis, setUserEmojis] = useState([]); - const session = useSyncExternalStore( - c => Login.hook(c), - () => Login.snapshot(), - ); - - const sub = useMemo(() => { - if (!pubkey) return null; - const b = new RequestBuilder(`login:${pubkey.slice(0, 12)}`); - b.withOptions({ - leaveOpen, - }) - .withFilter() - .authors([pubkey]) - .kinds([EventKind.ContactList, MUTED, USER_EMOJIS, USER_CARDS]); - return b; - }, [pubkey, leaveOpen]); - - const data = useRequestBuilder(sub); - - useEffect(() => { - if (!data) { - return; - } - for (const ev of data) { - 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); - } - if (ev?.kind === EventKind.ContactList) { - Login.setFollows(ev.tags, ev.content, ev.created_at); - } - } - }, [data]); - - const emojis = useUserEmojiPacks(pubkey, userEmojis); - useEffect(() => { - if (session) { - Login.setEmojis(emojis); - } - }, [emojis]); -} diff --git a/src/hooks/profile.ts b/src/hooks/profile.ts index cb8ec87..17475a2 100644 --- a/src/hooks/profile.ts +++ b/src/hooks/profile.ts @@ -4,8 +4,9 @@ import { useRequestBuilder } from "@snort/system-react"; import { LIVE_STREAM } from "@/const"; import { useZaps } from "./zaps"; -export function useProfile(link: NostrLink, leaveOpen = false) { +export function useProfile(link?: NostrLink, leaveOpen = false) { const sub = useMemo(() => { + if (!link) return; const b = new RequestBuilder(`profile:${link.id.slice(0, 12)}`); b.withOptions({ leaveOpen, diff --git a/src/index.tsx b/src/index.tsx index 2f28655..36a59ab 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -119,7 +119,7 @@ const router = createBrowserRouter([ element: , }, { - path: "/p/:npub", + path: "/p/:id", element: , }, { diff --git a/src/login.ts b/src/login.ts index 0247740..7c634d1 100644 --- a/src/login.ts +++ b/src/login.ts @@ -1,28 +1,18 @@ import { bytesToHex } from "@noble/curves/abstract/utils"; import { schnorr } from "@noble/curves/secp256k1"; import { ExternalStore, unwrap } from "@snort/shared"; -import { EventPublisher, Nip7Signer, PrivateKeySigner } from "@snort/system"; -import type { EmojiPack, Tags } from "@/types"; +import { EventPublisher, Nip7Signer, PrivateKeySigner, UserState, UserStateObject } from "@snort/system"; export enum LoginType { Nip7 = "nip7", PrivateKey = "private-key", } -interface ReplaceableTags { - tags: Tags; - content?: string; - timestamp: number; -} - export interface LoginSession { type: LoginType; pubkey: string; privateKey?: string; - follows: ReplaceableTags; - muted: ReplaceableTags; - cards: ReplaceableTags; - emojis: Array; + state?: UserState; color?: string; wallet?: { type: number; @@ -30,13 +20,6 @@ export interface LoginSession { }; } -const initialState = { - follows: { tags: [], timestamp: 0 }, - muted: { tags: [], timestamp: 0 }, - cards: { tags: [], timestamp: 0 }, - emojis: [], -}; - const SESSION_KEY = "session"; export class LoginStore extends ExternalStore { @@ -46,9 +29,40 @@ export class LoginStore extends ExternalStore { super(); const json = window.localStorage.getItem(SESSION_KEY); if (json) { - this.#session = { ...initialState, ...JSON.parse(json) }; + this.#session = JSON.parse(json); if (this.#session) { + let save = false; + this.#session.state = new UserState( + this.#session?.pubkey, + undefined, + this.#session.state as UserStateObject | undefined, + ); + + this.#session.state.on("change", () => { + this.#save(); + }); + //reset this.#session.type ??= LoginType.Nip7; + if ("cards" in this.#session) { + delete this.#session.cards; + save = true; + } + if ("emojis" in this.#session) { + delete this.#session.emojis; + save = true; + } + if ("follows" in this.#session) { + delete this.#session.follows; + save = true; + } + if ("muted" in this.#session) { + delete this.#session.muted; + save = true; + } + + if (save) { + this.#save(); + } } } } @@ -57,7 +71,6 @@ export class LoginStore extends ExternalStore { this.#session = { type, pubkey: pk, - ...initialState, }; this.#save(); } @@ -67,7 +80,6 @@ export class LoginStore extends ExternalStore { type: LoginType.PrivateKey, pubkey: bytesToHex(schnorr.getPublicKey(key)), privateKey: key, - ...initialState, }; this.#save(); } @@ -81,44 +93,6 @@ export class LoginStore extends ExternalStore { return this.#session ? { ...this.#session } : undefined; } - setFollows(follows: Tags, content: string, ts: number) { - if (!this.#session) return; - if (this.#session.follows.timestamp >= ts) { - return; - } - this.#session.follows.tags = follows; - this.#session.follows.content = content; - this.#session.follows.timestamp = ts; - this.#save(); - } - - setEmojis(emojis: Array) { - if (!this.#session) return; - this.#session.emojis = emojis; - this.#save(); - } - - setMuted(muted: Tags, content: string, ts: number) { - if (!this.#session) return; - if (this.#session.muted.timestamp >= ts) { - return; - } - this.#session.muted.tags = muted; - this.#session.muted.content = content; - this.#session.muted.timestamp = ts; - this.#save(); - } - - setCards(cards: Tags, ts: number) { - if (!this.#session) return; - if (this.#session.cards.timestamp >= ts) { - return; - } - this.#session.cards.tags = cards; - this.#session.cards.timestamp = ts; - this.#save(); - } - setColor(color: string) { if (!this.#session) return; this.#session.color = color; @@ -133,7 +107,11 @@ export class LoginStore extends ExternalStore { #save() { if (this.#session) { - window.localStorage.setItem(SESSION_KEY, JSON.stringify(this.#session)); + const ses = { ...this.#session } as Record; + if (this.#session.state instanceof UserState) { + ses.state = this.#session.state.serialize(); + } + window.localStorage.setItem(SESSION_KEY, JSON.stringify(ses)); } else { window.localStorage.removeItem(SESSION_KEY); } diff --git a/src/pages/dashboard/raid-menu.tsx b/src/pages/dashboard/raid-menu.tsx index 2cf5562..a8dc598 100644 --- a/src/pages/dashboard/raid-menu.tsx +++ b/src/pages/dashboard/raid-menu.tsx @@ -1,5 +1,5 @@ import { useStreamsFeed } from "@/hooks/live-streams"; -import { getHost, getTagValues } from "@/utils"; +import { getHost } from "@/utils"; import { dedupe, unwrap } from "@snort/shared"; import { FormattedMessage } from "react-intl"; import { Profile } from "../../element/profile"; @@ -19,12 +19,14 @@ export function DashboardRaidMenu({ link, onClose }: { link: NostrLink; onClose: const [raiding, setRaiding] = useState(""); const [msg, setMsg] = useState(""); - const mutedHosts = new Set(getTagValues(login?.muted.tags ?? [], "p")); - const livePubkeys = dedupe(live.map(a => getHost(a))).filter(a => !mutedHosts.has(a)); + const livePubkeys = dedupe(live.map(a => getHost(a))).filter( + a => !login?.state?.muted.some(b => b.equals(NostrLink.publicKey(a))), + ); async function raid() { - if (login) { - const ev = await login.publisher().generic(eb => { + const pub = login?.publisher(); + if (pub) { + const ev = await pub.generic(eb => { return eb .kind(LIVE_STREAM_RAID) .tag(unwrap(link.toEventTag("root"))) diff --git a/src/pages/layout/index.tsx b/src/pages/layout/index.tsx index 7588b29..0bd864c 100644 --- a/src/pages/layout/index.tsx +++ b/src/pages/layout/index.tsx @@ -1,19 +1,29 @@ import "./layout.css"; -import { CSSProperties, useEffect } from "react"; +import { CSSProperties, useContext, useEffect } from "react"; import { Outlet, useLocation } from "react-router-dom"; import { Helmet } from "react-helmet"; -import { useLogin, useLoginEvents } from "@/hooks/login"; +import { useLogin } from "@/hooks/login"; import { trackEvent } from "@/utils"; import { HeaderNav } from "./header"; import { LeftNav } from "./left-nav"; +import { SnortContext } from "@snort/system-react"; +import { EventKind } from "@snort/system"; +import { USER_CARDS } from "@/const"; export function LayoutPage() { const location = useLocation(); const login = useLogin(); + const system = useContext(SnortContext); - useLoginEvents(login?.pubkey, true); + useEffect(() => { + if (login?.state) { + login.state.checkIsStandardList(EventKind.EmojisList); + login.state.checkIsStandardList(USER_CARDS); + login.state.init(login.signer(), system); + } + }, []); useEffect(() => { trackEvent("pageview"); diff --git a/src/pages/profile-page.tsx b/src/pages/profile-page.tsx index 46466e9..5611dcf 100644 --- a/src/pages/profile-page.tsx +++ b/src/pages/profile-page.tsx @@ -1,9 +1,8 @@ import "./profile-page.css"; import { useMemo } from "react"; -import { useNavigate, useParams } from "react-router-dom"; -import { CachedMetadata, NostrEvent, NostrLink, TaggedNostrEvent, parseNostrLink } from "@snort/system"; +import { useNavigate } from "react-router-dom"; +import { CachedMetadata, NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system"; import { useUserProfile } from "@snort/system-react"; -import { unwrap } from "@snort/shared"; import { FormattedMessage } from "react-intl"; import { Icon } from "@/element/icon"; @@ -25,20 +24,21 @@ import { useProfileClips } from "@/hooks/clips"; import VideoGrid from "@/element/video-grid"; import { ClipTile } from "@/element/stream/clip-tile"; import useImgProxy from "@/hooks/img-proxy"; +import { useStreamLink } from "@/hooks/stream-link"; const defaultBanner = "https://void.cat/d/Hn1AdN5UKmceuDkgDW847q.webp"; export function ProfilePage() { - const params = useParams(); - const link = parseNostrLink(unwrap(params.npub)); + const link = useStreamLink(); const { streams, zaps } = useProfile(link, true); - const profile = useUserProfile(link.id); + const profile = useUserProfile(link?.id); const { proxy } = useImgProxy(); const pastStreams = useMemo(() => { return streams.filter(ev => findTag(ev, "status") === StreamState.Ended); }, [streams]); + if (!link) return; return (
{ - const pubA = findTag(a, "published_at"); - const pubB = findTag(b, "published_at"); - return Number(pubA) > Number(pubB) ? -1 : 1; - }); + console.debug(login?.state?.muted); + const sorted = videos + .filter(a => { + const host = getHost(a); + const link = NostrLink.publicKey(host); + return (login?.state?.muted.length ?? 0) === 0 || !login?.state?.muted.some(a => a.equals(link)); + }) + .sort((a, b) => { + const pubA = findTag(a, "published_at"); + const pubB = findTag(b, "published_at"); + return Number(pubA) > Number(pubB) ? -1 : 1; + }); return (
diff --git a/yarn.lock b/yarn.lock index 28d841c..d460ab6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2687,14 +2687,14 @@ __metadata: languageName: node linkType: hard -"@snort/system-react@npm:^1.3.3": - version: 1.3.3 - resolution: "@snort/system-react@npm:1.3.3" +"@snort/system-react@npm:^1.3.5": + version: 1.3.5 + resolution: "@snort/system-react@npm:1.3.5" dependencies: "@snort/shared": "npm:^1.0.15" - "@snort/system": "npm:^1.3.3" + "@snort/system": "npm:^1.3.5" react: "npm:^18.2.0" - checksum: 10c0/c99d27e3367a31e278c4b293b11b00b8aefd4600dc081d2c422606572ba170d2f0256bff6e55523f8884906e54a3699149154a7feb7119410a484975c6c815c4 + checksum: 10c0/5e03db4a0034bbf672238f2091e86974ba21ecdb6e8562d869a7141b8b8f447802a2c8c3ef10824b81ad6288ae69d39505fb4572965bebba856990edec37dfb2 languageName: node linkType: hard @@ -2726,9 +2726,9 @@ __metadata: languageName: node linkType: hard -"@snort/system@npm:^1.3.3": - version: 1.3.3 - resolution: "@snort/system@npm:1.3.3" +"@snort/system@npm:^1.3.5": + version: 1.3.5 + resolution: "@snort/system@npm:1.3.5" dependencies: "@noble/curves": "npm:^1.4.0" "@noble/hashes": "npm:^1.4.0" @@ -2743,7 +2743,7 @@ __metadata: lru-cache: "npm:^10.2.0" uuid: "npm:^9.0.0" ws: "npm:^8.14.0" - checksum: 10c0/a153ec74f0c12a6f739d1c1fd4cc78e3ca8d5a8a8c61f876a87e875ed3e330339d6e155fa52f36830aab2da0e8bfc210fe55da1f77e960b14a70e3aebd01ab7f + checksum: 10c0/cc48ad0f9e9d857061fb9f04b1d753bc9d284b9f89ec8a766c3afe3a0af33b4f7219dc9d068365ecdc38fde4cc91b9476dbfa3883eb1c9f317b6fbf5b3e681ad languageName: node linkType: hard @@ -7678,8 +7678,8 @@ __metadata: "@noble/hashes": "npm:^1.4.0" "@scure/base": "npm:^1.1.6" "@snort/shared": "npm:^1.0.15" - "@snort/system": "npm:^1.3.3" - "@snort/system-react": "npm:^1.3.3" + "@snort/system": "npm:^1.3.5" + "@snort/system-react": "npm:^1.3.5" "@snort/system-wasm": "npm:^1.0.4" "@snort/wallet": "npm:^0.1.3" "@snort/worker-relay": "npm:^1.1.0"