refactor: use UserState

This commit is contained in:
2024-05-28 14:51:35 +01:00
parent 154fa551b4
commit c32c230227
22 changed files with 189 additions and 328 deletions

View File

@ -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",

View File

@ -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;

View File

@ -20,6 +20,9 @@ const AsyncButton = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: Asyn
if (props.onClick) {
await props.onClick(e);
}
} catch (e) {
console.error(e);
throw e;
} finally {
setLoading(false);
}

View File

@ -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 (
<div className={classNames("flex flex-col gap-1", className)} style={height ? { height: `${height}px` } : {}}>

View File

@ -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);
}
}

View File

@ -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 <Icon name="piggybank" />;
case EMOJI_PACK:
case EventKind.EmojiSet:
return <Icon name="face-content" />;
case EventKind.Badge:
return <Icon name="badge" />;
@ -35,7 +35,7 @@ export function NostrEvent({ ev }: { ev: TaggedNostrEvent }) {
case GOAL: {
return <Goal ev={ev} />;
}
case EMOJI_PACK: {
case EventKind.EmojiSet: {
return <EmojiPack ev={ev} />;
}
case EventKind.Badge: {

View File

@ -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 (
<DefaultButton disabled={timestamp ? timestamp === 0 : true} onClick={isFollowing ? unfollow : follow}>
<DefaultButton onClick={isFollowing ? unfollow : follow}>
{isFollowing ? (
<FormattedMessage defaultMessage="Unfollow" />
) : (
@ -75,11 +36,14 @@ export function LoggedInFollowButton({
}
export function FollowTagButton({ tag, hideWhenFollowing }: { tag: string; hideWhenFollowing?: boolean }) {
const login = useLogin();
return login?.pubkey ? <LoggedInFollowButton tag={"t"} value={tag} hideWhenFollowing={hideWhenFollowing} /> : null;
//const login = useLogin();
//const link = new NostrHashtagLink(tag);
return;
//return login?.pubkey ? <LoggedInFollowButton link={link} hideWhenFollowing={hideWhenFollowing} /> : null;
}
export function FollowButton({ pubkey, hideWhenFollowing }: { pubkey: string; hideWhenFollowing?: boolean }) {
const login = useLogin();
return login?.pubkey ? <LoggedInFollowButton tag={"p"} value={pubkey} hideWhenFollowing={hideWhenFollowing} /> : null;
const link = new NostrLink(NostrPrefix.PublicKey, pubkey);
return login?.pubkey ? <LoggedInFollowButton link={link} hideWhenFollowing={hideWhenFollowing} /> : null;
}

View File

@ -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 }) {

View File

@ -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 (
<DndProvider backend={HTML5Backend}>
{canEdit ? (
<StreamCardEditor tags={login.cards.tags} pubkey={login.pubkey} />
) : (
<ReadOnlyStreamCards host={host} />
)}
{canEdit ? <StreamCardEditor tags={cards ?? []} pubkey={login.pubkey} /> : <ReadOnlyStreamCards host={host} />}
</DndProvider>
);
}

View File

@ -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<ToNostrEventTag>;
}
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 (
<>

View File

@ -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<string> = [];
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 && (
<VideoGrid>
{live
.filter(e => !mutedHosts.has(getHost(e)))
.map(e => (
<StreamTile ev={e} key={e.id} style="grid" />
))}
{live.map(e => (
<StreamTile ev={e} key={e.id} style="grid" />
))}
</VideoGrid>
)}
{liveByHashtag.map(t => (
@ -96,8 +88,8 @@ export default function VideoGridSorted({
{plannedEvents.length > 0 && (showPlanned ?? true) && (
<GridSection header={<FormattedMessage defaultMessage="Planned" id="kp0NPF" />} items={plannedEvents} />
)}
{endedEvents.length > 0 && (showEnded ?? true) && (
<GridSection header={<FormattedMessage defaultMessage="Ended" id="TP/cMX" />} items={endedEvents} />
{ended.length > 0 && (showEnded ?? true) && (
<GridSection header={<FormattedMessage defaultMessage="Ended" id="TP/cMX" />} items={ended} />
)}
</div>
);

View File

@ -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]);

View File

@ -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;

View File

@ -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<Tags>([]);
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]);
}

View File

@ -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,

View File

@ -119,7 +119,7 @@ const router = createBrowserRouter([
element: <TagPage />,
},
{
path: "/p/:npub",
path: "/p/:id",
element: <ProfilePage />,
},
{

View File

@ -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<EmojiPack>;
state?: UserState<never>;
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<LoginSession | undefined> {
@ -46,9 +29,40 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
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<never> | 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<LoginSession | undefined> {
this.#session = {
type,
pubkey: pk,
...initialState,
};
this.#save();
}
@ -67,7 +80,6 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
type: LoginType.PrivateKey,
pubkey: bytesToHex(schnorr.getPublicKey(key)),
privateKey: key,
...initialState,
};
this.#save();
}
@ -81,44 +93,6 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
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<EmojiPack>) {
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<LoginSession | undefined> {
#save() {
if (this.#session) {
window.localStorage.setItem(SESSION_KEY, JSON.stringify(this.#session));
const ses = { ...this.#session } as Record<string, unknown>;
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);
}

View File

@ -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")))

View File

@ -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");

View File

@ -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 (
<div className="flex flex-col gap-3 xl:px-4 w-full">
<img

View File

@ -1,22 +1,32 @@
import { VIDEO_KIND } from "@/const";
import VideoGrid from "@/element/video-grid";
import { findTag } from "@/utils";
import { RequestBuilder } from "@snort/system";
import { findTag, getHost } from "@/utils";
import { NostrLink, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { FormattedMessage } from "react-intl";
import { VideoTile } from "@/element/video/video-tile";
import { useLogin } from "@/hooks/login";
export function VideosPage() {
const login = useLogin();
const rb = new RequestBuilder("videos");
rb.withFilter().kinds([VIDEO_KIND]);
const videos = useRequestBuilder(rb);
const sorted = videos.sort((a, b) => {
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 (
<div className="p-4">

View File

@ -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"