refactor: use UserState
This commit is contained in:
@ -8,8 +8,8 @@
|
|||||||
"@noble/hashes": "^1.4.0",
|
"@noble/hashes": "^1.4.0",
|
||||||
"@scure/base": "^1.1.6",
|
"@scure/base": "^1.1.6",
|
||||||
"@snort/shared": "^1.0.15",
|
"@snort/shared": "^1.0.15",
|
||||||
"@snort/system": "^1.3.3",
|
"@snort/system": "^1.3.5",
|
||||||
"@snort/system-react": "^1.3.3",
|
"@snort/system-react": "^1.3.5",
|
||||||
"@snort/system-wasm": "^1.0.4",
|
"@snort/system-wasm": "^1.0.4",
|
||||||
"@snort/wallet": "^0.1.3",
|
"@snort/wallet": "^0.1.3",
|
||||||
"@snort/worker-relay": "^1.1.0",
|
"@snort/worker-relay": "^1.1.0",
|
||||||
|
@ -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_CHAT = 1_311 as EventKind;
|
||||||
export const LIVE_STREAM_RAID = 1_312 as EventKind;
|
export const LIVE_STREAM_RAID = 1_312 as EventKind;
|
||||||
export const LIVE_STREAM_CLIP = 1_313 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 GOAL = 9041 as EventKind;
|
||||||
export const USER_CARDS = 17_777 as EventKind;
|
export const USER_CARDS = 17_777 as EventKind;
|
||||||
export const CARD = 37_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 VIDEO_KIND = 34_235 as EventKind;
|
||||||
export const SHORTS_KIND = 34_236 as EventKind;
|
export const SHORTS_KIND = 34_236 as EventKind;
|
||||||
|
@ -20,6 +20,9 @@ const AsyncButton = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: Asyn
|
|||||||
if (props.onClick) {
|
if (props.onClick) {
|
||||||
await props.onClick(e);
|
await props.onClick(e);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import "./live-chat.css";
|
|||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { EventKind, NostrEvent, NostrLink, NostrPrefix, ParsedZap, TaggedNostrEvent } from "@snort/system";
|
import { EventKind, NostrEvent, NostrLink, NostrPrefix, ParsedZap, TaggedNostrEvent } from "@snort/system";
|
||||||
import { useEventFeed, useEventReactions, useReactions, useUserProfile } from "@snort/system-react";
|
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 { useEffect, useMemo } from "react";
|
||||||
|
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
@ -87,11 +87,9 @@ export function LiveChat({
|
|||||||
return starts ? Number(starts) : unixNow() - WEEK;
|
return starts ? Number(starts) : unixNow() - WEEK;
|
||||||
}, [ev]);
|
}, [ev]);
|
||||||
const { badges, awards } = useBadges(host, started);
|
const { badges, awards } = useBadges(host, started);
|
||||||
const mutedPubkeys = useMemo(() => {
|
|
||||||
return new Set(getTagValues(login?.muted.tags ?? [], "p"));
|
|
||||||
}, [login]);
|
|
||||||
const hostMutedPubkeys = useMutedPubkeys(host, true);
|
const hostMutedPubkeys = useMutedPubkeys(host, true);
|
||||||
const userEmojiPacks = login?.emojis ?? [];
|
const userEmojiPacks = useEmoji(login?.pubkey);
|
||||||
const channelEmojiPacks = useEmoji(host);
|
const channelEmojiPacks = useEmoji(host);
|
||||||
const allEmojiPacks = useMemo(() => {
|
const allEmojiPacks = useMemo(() => {
|
||||||
return uniqBy(userEmojiPacks.concat(channelEmojiPacks), packId);
|
return uniqBy(userEmojiPacks.concat(channelEmojiPacks), packId);
|
||||||
@ -110,7 +108,7 @@ export function LiveChat({
|
|||||||
if (ends) {
|
if (ends) {
|
||||||
extra.push({ kind: -2, created_at: Number(ends) } as TaggedNostrEvent);
|
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)
|
.filter(a => a.created_at >= started)
|
||||||
.sort((a, b) => b.created_at - a.created_at);
|
.sort((a, b) => b.created_at - a.created_at);
|
||||||
}, [feed, awards]);
|
}, [feed, awards]);
|
||||||
@ -145,8 +143,14 @@ export function LiveChat({
|
|||||||
}, [adjustLayout]);
|
}, [adjustLayout]);
|
||||||
|
|
||||||
const filteredEvents = useMemo(() => {
|
const filteredEvents = useMemo(() => {
|
||||||
return events.filter(e => !mutedPubkeys.has(e.pubkey) && !hostMutedPubkeys.has(e.pubkey));
|
return events.filter(e => {
|
||||||
}, [events, mutedPubkeys, hostMutedPubkeys]);
|
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 (
|
return (
|
||||||
<div className={classNames("flex flex-col gap-1", className)} style={height ? { height: `${height}px` } : {}}>
|
<div className={classNames("flex flex-col gap-1", className)} style={height ? { height: `${height}px` } : {}}>
|
||||||
|
@ -1,43 +1,24 @@
|
|||||||
import "./emoji-pack.css";
|
import "./emoji-pack.css";
|
||||||
import { type NostrEvent } from "@snort/system";
|
import { EventKind, NostrLink, type NostrEvent } from "@snort/system";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useContext } from "react";
|
|
||||||
import { SnortContext } from "@snort/system-react";
|
|
||||||
|
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { toEmojiPack } from "@/hooks/emoji";
|
import useEmoji from "@/hooks/emoji";
|
||||||
import { findTag } from "@/utils";
|
|
||||||
import { USER_EMOJIS } from "@/const";
|
|
||||||
import { Login } from "@/login";
|
|
||||||
import type { EmojiPack as EmojiPackType } from "@/types";
|
|
||||||
import { DefaultButton, WarningButton } from "./buttons";
|
import { DefaultButton, WarningButton } from "./buttons";
|
||||||
|
|
||||||
export function EmojiPack({ ev }: { ev: NostrEvent }) {
|
export function EmojiPack({ ev }: { ev: NostrEvent }) {
|
||||||
const system = useContext(SnortContext);
|
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const name = findTag(ev, "d");
|
const link = NostrLink.fromEvent(ev);
|
||||||
const isUsed = login?.emojis.find(e => e.author === ev.pubkey && e.name === name);
|
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");
|
const emoji = ev.tags.filter(e => e.at(0) === "emoji");
|
||||||
|
|
||||||
async function toggleEmojiPack() {
|
async function toggleEmojiPack() {
|
||||||
let newPacks = [] as EmojiPackType[];
|
|
||||||
if (isUsed) {
|
if (isUsed) {
|
||||||
newPacks = login?.emojis.filter(e => e.author !== ev.pubkey && e.name !== name) ?? [];
|
await login?.state?.removeFromList(EventKind.EmojisList, link, true);
|
||||||
} else {
|
} else {
|
||||||
newPacks = [...(login?.emojis ?? []), toEmojiPack(ev)];
|
await login?.state?.addToList(EventKind.EmojisList, link, true);
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { Goal } from "./goal";
|
|||||||
import { Note } from "./note";
|
import { Note } from "./note";
|
||||||
import { EmojiPack } from "./emoji-pack";
|
import { EmojiPack } from "./emoji-pack";
|
||||||
import { Badge } from "./badge";
|
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 { useEventFeed } from "@snort/system-react";
|
||||||
import LiveStreamClip from "./stream/clip";
|
import LiveStreamClip from "./stream/clip";
|
||||||
import { ExternalLink } from "./external-link";
|
import { ExternalLink } from "./external-link";
|
||||||
@ -21,7 +21,7 @@ export function EventIcon({ kind }: { kind?: EventKind }) {
|
|||||||
switch (kind) {
|
switch (kind) {
|
||||||
case GOAL:
|
case GOAL:
|
||||||
return <Icon name="piggybank" />;
|
return <Icon name="piggybank" />;
|
||||||
case EMOJI_PACK:
|
case EventKind.EmojiSet:
|
||||||
return <Icon name="face-content" />;
|
return <Icon name="face-content" />;
|
||||||
case EventKind.Badge:
|
case EventKind.Badge:
|
||||||
return <Icon name="badge" />;
|
return <Icon name="badge" />;
|
||||||
@ -35,7 +35,7 @@ export function NostrEvent({ ev }: { ev: TaggedNostrEvent }) {
|
|||||||
case GOAL: {
|
case GOAL: {
|
||||||
return <Goal ev={ev} />;
|
return <Goal ev={ev} />;
|
||||||
}
|
}
|
||||||
case EMOJI_PACK: {
|
case EventKind.EmojiSet: {
|
||||||
return <EmojiPack ev={ev} />;
|
return <EmojiPack ev={ev} />;
|
||||||
}
|
}
|
||||||
case EventKind.Badge: {
|
case EventKind.Badge: {
|
||||||
|
@ -1,67 +1,28 @@
|
|||||||
import { EventKind } from "@snort/system";
|
import { NostrHashtagLink, NostrLink, NostrPrefix } from "@snort/system";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useContext } from "react";
|
|
||||||
import { SnortContext } from "@snort/system-react";
|
|
||||||
|
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { Login } from "@/login";
|
|
||||||
import { DefaultButton } from "./buttons";
|
import { DefaultButton } from "./buttons";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
|
|
||||||
export function LoggedInFollowButton({
|
export function LoggedInFollowButton({ link, hideWhenFollowing }: { link: NostrLink; hideWhenFollowing?: boolean }) {
|
||||||
tag,
|
|
||||||
value,
|
|
||||||
hideWhenFollowing,
|
|
||||||
}: {
|
|
||||||
tag: "p" | "t";
|
|
||||||
value: string;
|
|
||||||
hideWhenFollowing?: boolean;
|
|
||||||
}) {
|
|
||||||
const system = useContext(SnortContext);
|
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
if (!login) return;
|
if (!login?.state) return;
|
||||||
|
|
||||||
const { tags, content, timestamp } = login.follows;
|
const follows = login.state.follows ?? [];
|
||||||
const follows = tags.filter(t => t.at(0) === tag);
|
const isFollowing = follows.includes(link.id);
|
||||||
const isFollowing = follows.find(t => t.at(1) === value);
|
|
||||||
|
|
||||||
async function unfollow() {
|
async function unfollow() {
|
||||||
const pub = login?.publisher();
|
await login?.state?.unfollow(link, true);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function follow() {
|
async function follow() {
|
||||||
const pub = login?.publisher();
|
await login?.state?.follow(link, true);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFollowing && hideWhenFollowing) return;
|
if (isFollowing && hideWhenFollowing) return;
|
||||||
return (
|
return (
|
||||||
<DefaultButton disabled={timestamp ? timestamp === 0 : true} onClick={isFollowing ? unfollow : follow}>
|
<DefaultButton onClick={isFollowing ? unfollow : follow}>
|
||||||
{isFollowing ? (
|
{isFollowing ? (
|
||||||
<FormattedMessage defaultMessage="Unfollow" />
|
<FormattedMessage defaultMessage="Unfollow" />
|
||||||
) : (
|
) : (
|
||||||
@ -75,11 +36,14 @@ export function LoggedInFollowButton({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FollowTagButton({ tag, hideWhenFollowing }: { tag: string; hideWhenFollowing?: boolean }) {
|
export function FollowTagButton({ tag, hideWhenFollowing }: { tag: string; hideWhenFollowing?: boolean }) {
|
||||||
const login = useLogin();
|
//const login = useLogin();
|
||||||
return login?.pubkey ? <LoggedInFollowButton tag={"t"} value={tag} hideWhenFollowing={hideWhenFollowing} /> : null;
|
//const link = new NostrHashtagLink(tag);
|
||||||
|
return;
|
||||||
|
//return login?.pubkey ? <LoggedInFollowButton link={link} hideWhenFollowing={hideWhenFollowing} /> : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FollowButton({ pubkey, hideWhenFollowing }: { pubkey: string; hideWhenFollowing?: boolean }) {
|
export function FollowButton({ pubkey, hideWhenFollowing }: { pubkey: string; hideWhenFollowing?: boolean }) {
|
||||||
const login = useLogin();
|
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;
|
||||||
}
|
}
|
||||||
|
@ -1,54 +1,30 @@
|
|||||||
import { useContext, useMemo } from "react";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { SnortContext } from "@snort/system-react";
|
|
||||||
|
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { Login } from "@/login";
|
|
||||||
import { MUTED } from "@/const";
|
|
||||||
import { DefaultButton } from "./buttons";
|
import { DefaultButton } from "./buttons";
|
||||||
|
import { NostrLink } from "@snort/system";
|
||||||
|
|
||||||
export function useMute(pubkey: string) {
|
export function useMute(pubkey: string) {
|
||||||
const system = useContext(SnortContext);
|
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const { tags, content } = login?.muted ?? { tags: [] };
|
const link = NostrLink.publicKey(pubkey);
|
||||||
const muted = useMemo(() => tags.filter(t => t.at(0) === "p"), [tags]);
|
|
||||||
const isMuted = useMemo(() => muted.find(t => t.at(1) === pubkey), [pubkey, muted]);
|
|
||||||
|
|
||||||
async function unmute() {
|
async function unmute() {
|
||||||
const pub = login?.publisher();
|
await login?.state?.unmute(link, true);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mute() {
|
async function mute() {
|
||||||
const pub = login?.publisher();
|
try {
|
||||||
if (pub) {
|
await login?.state?.mute(link, true);
|
||||||
const newMuted = [...tags, ["p", pubkey]];
|
} catch (e) {
|
||||||
const ev = await pub.generic(eb => {
|
console.error(e);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isMuted, mute, unmute };
|
return {
|
||||||
|
isMuted: login?.state?.muted.some(a => a.equals(link)) ?? false,
|
||||||
|
mute,
|
||||||
|
unmute,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
|
export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
|
||||||
|
@ -6,6 +6,7 @@ import { useCards } from "@/hooks/cards";
|
|||||||
import { StreamCardEditor } from "./stream-card-editor";
|
import { StreamCardEditor } from "./stream-card-editor";
|
||||||
import { Card } from "./card-item";
|
import { Card } from "./card-item";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { USER_CARDS } from "@/const";
|
||||||
|
|
||||||
export interface CardType {
|
export interface CardType {
|
||||||
identifier: string;
|
identifier: string;
|
||||||
@ -40,13 +41,10 @@ export function ReadOnlyStreamCards({ host, className }: StreamCardsProps) {
|
|||||||
export function StreamCards({ host }: StreamCardsProps) {
|
export function StreamCards({ host }: StreamCardsProps) {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const canEdit = login?.pubkey === host;
|
const canEdit = login?.pubkey === host;
|
||||||
|
const cards = login?.state?.getList(USER_CARDS);
|
||||||
return (
|
return (
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
{canEdit ? (
|
{canEdit ? <StreamCardEditor tags={cards ?? []} pubkey={login.pubkey} /> : <ReadOnlyStreamCards host={host} />}
|
||||||
<StreamCardEditor tags={login.cards.tags} pubkey={login.pubkey} />
|
|
||||||
) : (
|
|
||||||
<ReadOnlyStreamCards host={host} />
|
|
||||||
)}
|
|
||||||
</DndProvider>
|
</DndProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,16 +3,21 @@ import { FormattedMessage } from "react-intl";
|
|||||||
import { Toggle } from "../toggle";
|
import { Toggle } from "../toggle";
|
||||||
import { useUserCards } from "@/hooks/cards";
|
import { useUserCards } from "@/hooks/cards";
|
||||||
import { AddCard } from "./add-card";
|
import { AddCard } from "./add-card";
|
||||||
import { Tags } from "@/types";
|
|
||||||
import { Card } from "./card-item";
|
import { Card } from "./card-item";
|
||||||
|
import { ToNostrEventTag } from "@snort/system";
|
||||||
|
import { Tag } from "@/types";
|
||||||
|
|
||||||
interface StreamCardEditorProps {
|
interface StreamCardEditorProps {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
tags: Tags;
|
tags: Array<ToNostrEventTag>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StreamCardEditor({ pubkey, tags }: StreamCardEditorProps) {
|
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);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { useSortedStreams } from "@/hooks/useLiveStreams";
|
import { useSortedStreams } from "@/hooks/useLiveStreams";
|
||||||
import { getTagValues, getHost, extractStreamInfo } from "@/utils";
|
import { getTagValues, getHost, extractStreamInfo } from "@/utils";
|
||||||
import { NostrEvent, TaggedNostrEvent } from "@snort/system";
|
import { NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
import { ReactNode, useCallback, useMemo } from "react";
|
import { ReactNode, useMemo } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import VideoGrid from "./video-grid";
|
import VideoGrid from "./video-grid";
|
||||||
import { StreamTile } from "./stream/stream-tile";
|
import { StreamTile } from "./stream/stream-tile";
|
||||||
@ -34,30 +34,24 @@ export default function VideoGridSorted({
|
|||||||
showVideos,
|
showVideos,
|
||||||
}: VideoGridSortedProps) {
|
}: VideoGridSortedProps) {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const mutedHosts = new Set(getTagValues(login?.muted.tags ?? [], "p"));
|
const mutedHosts = login?.state?.muted ?? [];
|
||||||
const tags = login?.follows.tags ?? [];
|
const follows = login?.state?.follows ?? [];
|
||||||
const followsHost = useCallback(
|
const followsHost = (ev: NostrEvent) => follows?.includes(getHost(ev));
|
||||||
(ev: NostrEvent) => {
|
|
||||||
return tags.find(t => t.at(1) === getHost(ev));
|
const filteredStreams = evs.filter(a => !mutedHosts.includes(NostrLink.publicKey(getHost(a))));
|
||||||
},
|
const { live, planned, ended } = useSortedStreams(filteredStreams, showAll ? 0 : undefined);
|
||||||
[tags],
|
const hashtags: Array<string> = [];
|
||||||
);
|
|
||||||
const { live, planned, ended } = useSortedStreams(evs, showAll ? 0 : undefined);
|
|
||||||
const hashtags = getTagValues(tags, "t");
|
|
||||||
const following = live.filter(followsHost);
|
const following = live.filter(followsHost);
|
||||||
const liveNow = live.filter(e => !following.includes(e));
|
const liveNow = live.filter(e => !following.includes(e));
|
||||||
const hasFollowingLive = following.length > 0;
|
const hasFollowingLive = following.length > 0;
|
||||||
|
|
||||||
const plannedEvents = planned.filter(e => !mutedHosts.has(getHost(e))).filter(followsHost);
|
const plannedEvents = planned.filter(followsHost);
|
||||||
const endedEvents = ended.filter(e => !mutedHosts.has(getHost(e)));
|
|
||||||
|
|
||||||
const liveByHashtag = useMemo(() => {
|
const liveByHashtag = useMemo(() => {
|
||||||
return hashtags
|
return hashtags
|
||||||
.map(t => ({
|
.map(t => ({
|
||||||
tag: t,
|
tag: t,
|
||||||
live: live
|
live: live.filter(e => {
|
||||||
.filter(e => !mutedHosts.has(getHost(e)))
|
|
||||||
.filter(e => {
|
|
||||||
const evTags = getTagValues(e.tags, "t");
|
const evTags = getTagValues(e.tags, "t");
|
||||||
return evTags.includes(t);
|
return evTags.includes(t);
|
||||||
}),
|
}),
|
||||||
@ -72,9 +66,7 @@ export default function VideoGridSorted({
|
|||||||
)}
|
)}
|
||||||
{!hasFollowingLive && (
|
{!hasFollowingLive && (
|
||||||
<VideoGrid>
|
<VideoGrid>
|
||||||
{live
|
{live.map(e => (
|
||||||
.filter(e => !mutedHosts.has(getHost(e)))
|
|
||||||
.map(e => (
|
|
||||||
<StreamTile ev={e} key={e.id} style="grid" />
|
<StreamTile ev={e} key={e.id} style="grid" />
|
||||||
))}
|
))}
|
||||||
</VideoGrid>
|
</VideoGrid>
|
||||||
@ -96,8 +88,8 @@ export default function VideoGridSorted({
|
|||||||
{plannedEvents.length > 0 && (showPlanned ?? true) && (
|
{plannedEvents.length > 0 && (showPlanned ?? true) && (
|
||||||
<GridSection header={<FormattedMessage defaultMessage="Planned" id="kp0NPF" />} items={plannedEvents} />
|
<GridSection header={<FormattedMessage defaultMessage="Planned" id="kp0NPF" />} items={plannedEvents} />
|
||||||
)}
|
)}
|
||||||
{endedEvents.length > 0 && (showEnded ?? true) && (
|
{ended.length > 0 && (showEnded ?? true) && (
|
||||||
<GridSection header={<FormattedMessage defaultMessage="Ended" id="TP/cMX" />} items={endedEvents} />
|
<GridSection header={<FormattedMessage defaultMessage="Ended" id="TP/cMX" />} items={ended} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { NostrEvent, RequestBuilder } from "@snort/system";
|
import { EventKind, NostrEvent, RequestBuilder } from "@snort/system";
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
import { findTag, uniqBy } from "@/utils";
|
import { findTag, uniqBy } from "@/utils";
|
||||||
import { EMOJI_PACK, USER_EMOJIS } from "@/const";
|
|
||||||
import type { EmojiPack, EmojiTag, Tags } from "@/types";
|
import type { EmojiPack, EmojiTag, Tags } from "@/types";
|
||||||
|
|
||||||
function cleanShortcode(shortcode?: string) {
|
function cleanShortcode(shortcode?: string) {
|
||||||
@ -29,7 +28,7 @@ export function packId(pack: EmojiPack): string {
|
|||||||
export function useUserEmojiPacks(pubkey?: string, userEmoji?: Tags) {
|
export function useUserEmojiPacks(pubkey?: string, userEmoji?: Tags) {
|
||||||
const related = useMemo(() => {
|
const related = useMemo(() => {
|
||||||
if (userEmoji) {
|
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 [];
|
return [];
|
||||||
}, [userEmoji]);
|
}, [userEmoji]);
|
||||||
@ -48,9 +47,9 @@ export function useUserEmojiPacks(pubkey?: string, userEmoji?: Tags) {
|
|||||||
|
|
||||||
const rb = new RequestBuilder(`emoji-related:${pubkey}`);
|
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;
|
return rb;
|
||||||
}, [pubkey, related]);
|
}, [pubkey, related]);
|
||||||
@ -73,8 +72,7 @@ export default function useEmoji(pubkey?: string) {
|
|||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
if (!pubkey) return null;
|
if (!pubkey) return null;
|
||||||
const rb = new RequestBuilder(`emoji:${pubkey}`);
|
const rb = new RequestBuilder(`emoji:${pubkey}`);
|
||||||
|
rb.withFilter().authors([pubkey]).kinds([EventKind.EmojisList]);
|
||||||
rb.withFilter().authors([pubkey]).kinds([USER_EMOJIS]);
|
|
||||||
|
|
||||||
return rb;
|
return rb;
|
||||||
}, [pubkey]);
|
}, [pubkey]);
|
||||||
|
@ -1,23 +1,20 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { RequestBuilder } from "@snort/system";
|
import { EventKind, NostrLink, RequestBuilder } from "@snort/system";
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
|
||||||
import { MUTED } from "@/const";
|
|
||||||
import { getTagValues } from "@/utils";
|
|
||||||
|
|
||||||
export function useMutedPubkeys(host?: string, leaveOpen = false) {
|
export function useMutedPubkeys(host?: string, leaveOpen = false) {
|
||||||
const mutedSub = useMemo(() => {
|
const mutedSub = useMemo(() => {
|
||||||
if (!host) return null;
|
if (!host) return null;
|
||||||
const rb = new RequestBuilder(`muted:${host}`);
|
const rb = new RequestBuilder(`muted:${host}`);
|
||||||
rb.withOptions({ leaveOpen });
|
rb.withOptions({ leaveOpen });
|
||||||
rb.withFilter().kinds([MUTED]).authors([host]);
|
rb.withFilter().kinds([EventKind.MuteList]).authors([host]);
|
||||||
return rb;
|
return rb;
|
||||||
}, [host]);
|
}, [host]);
|
||||||
|
|
||||||
const muted = useRequestBuilder(mutedSub);
|
const muted = useRequestBuilder(mutedSub);
|
||||||
const mutedPubkeys = useMemo(() => {
|
const mutedPubkeys = useMemo(() => {
|
||||||
return new Set(getTagValues(muted?.at(0)?.tags ?? [], "p"));
|
return muted.flatMap(a => NostrLink.fromAllTags(a.tags));
|
||||||
}, [muted]);
|
}, [muted]);
|
||||||
|
|
||||||
return mutedPubkeys;
|
return mutedPubkeys;
|
||||||
|
@ -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";
|
import { getPublisher, getSigner, Login, LoginSession } from "@/login";
|
||||||
|
|
||||||
export function useLogin() {
|
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]);
|
|
||||||
}
|
|
||||||
|
@ -4,8 +4,9 @@ import { useRequestBuilder } from "@snort/system-react";
|
|||||||
import { LIVE_STREAM } from "@/const";
|
import { LIVE_STREAM } from "@/const";
|
||||||
import { useZaps } from "./zaps";
|
import { useZaps } from "./zaps";
|
||||||
|
|
||||||
export function useProfile(link: NostrLink, leaveOpen = false) {
|
export function useProfile(link?: NostrLink, leaveOpen = false) {
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
|
if (!link) return;
|
||||||
const b = new RequestBuilder(`profile:${link.id.slice(0, 12)}`);
|
const b = new RequestBuilder(`profile:${link.id.slice(0, 12)}`);
|
||||||
b.withOptions({
|
b.withOptions({
|
||||||
leaveOpen,
|
leaveOpen,
|
||||||
|
@ -119,7 +119,7 @@ const router = createBrowserRouter([
|
|||||||
element: <TagPage />,
|
element: <TagPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/p/:npub",
|
path: "/p/:id",
|
||||||
element: <ProfilePage />,
|
element: <ProfilePage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
100
src/login.ts
100
src/login.ts
@ -1,28 +1,18 @@
|
|||||||
import { bytesToHex } from "@noble/curves/abstract/utils";
|
import { bytesToHex } from "@noble/curves/abstract/utils";
|
||||||
import { schnorr } from "@noble/curves/secp256k1";
|
import { schnorr } from "@noble/curves/secp256k1";
|
||||||
import { ExternalStore, unwrap } from "@snort/shared";
|
import { ExternalStore, unwrap } from "@snort/shared";
|
||||||
import { EventPublisher, Nip7Signer, PrivateKeySigner } from "@snort/system";
|
import { EventPublisher, Nip7Signer, PrivateKeySigner, UserState, UserStateObject } from "@snort/system";
|
||||||
import type { EmojiPack, Tags } from "@/types";
|
|
||||||
|
|
||||||
export enum LoginType {
|
export enum LoginType {
|
||||||
Nip7 = "nip7",
|
Nip7 = "nip7",
|
||||||
PrivateKey = "private-key",
|
PrivateKey = "private-key",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReplaceableTags {
|
|
||||||
tags: Tags;
|
|
||||||
content?: string;
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LoginSession {
|
export interface LoginSession {
|
||||||
type: LoginType;
|
type: LoginType;
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
follows: ReplaceableTags;
|
state?: UserState<never>;
|
||||||
muted: ReplaceableTags;
|
|
||||||
cards: ReplaceableTags;
|
|
||||||
emojis: Array<EmojiPack>;
|
|
||||||
color?: string;
|
color?: string;
|
||||||
wallet?: {
|
wallet?: {
|
||||||
type: number;
|
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";
|
const SESSION_KEY = "session";
|
||||||
|
|
||||||
export class LoginStore extends ExternalStore<LoginSession | undefined> {
|
export class LoginStore extends ExternalStore<LoginSession | undefined> {
|
||||||
@ -46,9 +29,40 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
|
|||||||
super();
|
super();
|
||||||
const json = window.localStorage.getItem(SESSION_KEY);
|
const json = window.localStorage.getItem(SESSION_KEY);
|
||||||
if (json) {
|
if (json) {
|
||||||
this.#session = { ...initialState, ...JSON.parse(json) };
|
this.#session = JSON.parse(json);
|
||||||
if (this.#session) {
|
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;
|
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 = {
|
this.#session = {
|
||||||
type,
|
type,
|
||||||
pubkey: pk,
|
pubkey: pk,
|
||||||
...initialState,
|
|
||||||
};
|
};
|
||||||
this.#save();
|
this.#save();
|
||||||
}
|
}
|
||||||
@ -67,7 +80,6 @@ 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,
|
||||||
...initialState,
|
|
||||||
};
|
};
|
||||||
this.#save();
|
this.#save();
|
||||||
}
|
}
|
||||||
@ -81,44 +93,6 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
|
|||||||
return this.#session ? { ...this.#session } : 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) {
|
setColor(color: string) {
|
||||||
if (!this.#session) return;
|
if (!this.#session) return;
|
||||||
this.#session.color = color;
|
this.#session.color = color;
|
||||||
@ -133,7 +107,11 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
|
|||||||
|
|
||||||
#save() {
|
#save() {
|
||||||
if (this.#session) {
|
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 {
|
} else {
|
||||||
window.localStorage.removeItem(SESSION_KEY);
|
window.localStorage.removeItem(SESSION_KEY);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useStreamsFeed } from "@/hooks/live-streams";
|
import { useStreamsFeed } from "@/hooks/live-streams";
|
||||||
import { getHost, getTagValues } from "@/utils";
|
import { getHost } from "@/utils";
|
||||||
import { dedupe, unwrap } from "@snort/shared";
|
import { dedupe, unwrap } from "@snort/shared";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { Profile } from "../../element/profile";
|
import { Profile } from "../../element/profile";
|
||||||
@ -19,12 +19,14 @@ export function DashboardRaidMenu({ link, onClose }: { link: NostrLink; onClose:
|
|||||||
const [raiding, setRaiding] = useState("");
|
const [raiding, setRaiding] = useState("");
|
||||||
const [msg, setMsg] = useState("");
|
const [msg, setMsg] = useState("");
|
||||||
|
|
||||||
const mutedHosts = new Set(getTagValues(login?.muted.tags ?? [], "p"));
|
const livePubkeys = dedupe(live.map(a => getHost(a))).filter(
|
||||||
const livePubkeys = dedupe(live.map(a => getHost(a))).filter(a => !mutedHosts.has(a));
|
a => !login?.state?.muted.some(b => b.equals(NostrLink.publicKey(a))),
|
||||||
|
);
|
||||||
|
|
||||||
async function raid() {
|
async function raid() {
|
||||||
if (login) {
|
const pub = login?.publisher();
|
||||||
const ev = await login.publisher().generic(eb => {
|
if (pub) {
|
||||||
|
const ev = await pub.generic(eb => {
|
||||||
return eb
|
return eb
|
||||||
.kind(LIVE_STREAM_RAID)
|
.kind(LIVE_STREAM_RAID)
|
||||||
.tag(unwrap(link.toEventTag("root")))
|
.tag(unwrap(link.toEventTag("root")))
|
||||||
|
@ -1,19 +1,29 @@
|
|||||||
import "./layout.css";
|
import "./layout.css";
|
||||||
|
|
||||||
import { CSSProperties, useEffect } from "react";
|
import { CSSProperties, useContext, useEffect } from "react";
|
||||||
import { Outlet, useLocation } from "react-router-dom";
|
import { Outlet, useLocation } from "react-router-dom";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
import { useLogin, useLoginEvents } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { trackEvent } from "@/utils";
|
import { trackEvent } from "@/utils";
|
||||||
import { HeaderNav } from "./header";
|
import { HeaderNav } from "./header";
|
||||||
import { LeftNav } from "./left-nav";
|
import { LeftNav } from "./left-nav";
|
||||||
|
import { SnortContext } from "@snort/system-react";
|
||||||
|
import { EventKind } from "@snort/system";
|
||||||
|
import { USER_CARDS } from "@/const";
|
||||||
|
|
||||||
export function LayoutPage() {
|
export function LayoutPage() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const login = useLogin();
|
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(() => {
|
useEffect(() => {
|
||||||
trackEvent("pageview");
|
trackEvent("pageview");
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import "./profile-page.css";
|
import "./profile-page.css";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CachedMetadata, NostrEvent, NostrLink, TaggedNostrEvent, parseNostrLink } from "@snort/system";
|
import { CachedMetadata, NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import { unwrap } from "@snort/shared";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { Icon } from "@/element/icon";
|
import { Icon } from "@/element/icon";
|
||||||
@ -25,20 +24,21 @@ import { useProfileClips } from "@/hooks/clips";
|
|||||||
import VideoGrid from "@/element/video-grid";
|
import VideoGrid from "@/element/video-grid";
|
||||||
import { ClipTile } from "@/element/stream/clip-tile";
|
import { ClipTile } from "@/element/stream/clip-tile";
|
||||||
import useImgProxy from "@/hooks/img-proxy";
|
import useImgProxy from "@/hooks/img-proxy";
|
||||||
|
import { useStreamLink } from "@/hooks/stream-link";
|
||||||
|
|
||||||
const defaultBanner = "https://void.cat/d/Hn1AdN5UKmceuDkgDW847q.webp";
|
const defaultBanner = "https://void.cat/d/Hn1AdN5UKmceuDkgDW847q.webp";
|
||||||
|
|
||||||
export function ProfilePage() {
|
export function ProfilePage() {
|
||||||
const params = useParams();
|
const link = useStreamLink();
|
||||||
const link = parseNostrLink(unwrap(params.npub));
|
|
||||||
const { streams, zaps } = useProfile(link, true);
|
const { streams, zaps } = useProfile(link, true);
|
||||||
const profile = useUserProfile(link.id);
|
const profile = useUserProfile(link?.id);
|
||||||
const { proxy } = useImgProxy();
|
const { proxy } = useImgProxy();
|
||||||
|
|
||||||
const pastStreams = useMemo(() => {
|
const pastStreams = useMemo(() => {
|
||||||
return streams.filter(ev => findTag(ev, "status") === StreamState.Ended);
|
return streams.filter(ev => findTag(ev, "status") === StreamState.Ended);
|
||||||
}, [streams]);
|
}, [streams]);
|
||||||
|
|
||||||
|
if (!link) return;
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 xl:px-4 w-full">
|
<div className="flex flex-col gap-3 xl:px-4 w-full">
|
||||||
<img
|
<img
|
||||||
|
@ -1,18 +1,28 @@
|
|||||||
import { VIDEO_KIND } from "@/const";
|
import { VIDEO_KIND } from "@/const";
|
||||||
import VideoGrid from "@/element/video-grid";
|
import VideoGrid from "@/element/video-grid";
|
||||||
import { findTag } from "@/utils";
|
import { findTag, getHost } from "@/utils";
|
||||||
import { RequestBuilder } from "@snort/system";
|
import { NostrLink, RequestBuilder } from "@snort/system";
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { VideoTile } from "@/element/video/video-tile";
|
import { VideoTile } from "@/element/video/video-tile";
|
||||||
|
import { useLogin } from "@/hooks/login";
|
||||||
|
|
||||||
export function VideosPage() {
|
export function VideosPage() {
|
||||||
|
const login = useLogin();
|
||||||
|
|
||||||
const rb = new RequestBuilder("videos");
|
const rb = new RequestBuilder("videos");
|
||||||
rb.withFilter().kinds([VIDEO_KIND]);
|
rb.withFilter().kinds([VIDEO_KIND]);
|
||||||
|
|
||||||
const videos = useRequestBuilder(rb);
|
const videos = useRequestBuilder(rb);
|
||||||
|
|
||||||
const sorted = videos.sort((a, b) => {
|
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 pubA = findTag(a, "published_at");
|
||||||
const pubB = findTag(b, "published_at");
|
const pubB = findTag(b, "published_at");
|
||||||
return Number(pubA) > Number(pubB) ? -1 : 1;
|
return Number(pubA) > Number(pubB) ? -1 : 1;
|
||||||
|
22
yarn.lock
22
yarn.lock
@ -2687,14 +2687,14 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@snort/system-react@npm:^1.3.3":
|
"@snort/system-react@npm:^1.3.5":
|
||||||
version: 1.3.3
|
version: 1.3.5
|
||||||
resolution: "@snort/system-react@npm:1.3.3"
|
resolution: "@snort/system-react@npm:1.3.5"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@snort/shared": "npm:^1.0.15"
|
"@snort/shared": "npm:^1.0.15"
|
||||||
"@snort/system": "npm:^1.3.3"
|
"@snort/system": "npm:^1.3.5"
|
||||||
react: "npm:^18.2.0"
|
react: "npm:^18.2.0"
|
||||||
checksum: 10c0/c99d27e3367a31e278c4b293b11b00b8aefd4600dc081d2c422606572ba170d2f0256bff6e55523f8884906e54a3699149154a7feb7119410a484975c6c815c4
|
checksum: 10c0/5e03db4a0034bbf672238f2091e86974ba21ecdb6e8562d869a7141b8b8f447802a2c8c3ef10824b81ad6288ae69d39505fb4572965bebba856990edec37dfb2
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -2726,9 +2726,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@snort/system@npm:^1.3.3":
|
"@snort/system@npm:^1.3.5":
|
||||||
version: 1.3.3
|
version: 1.3.5
|
||||||
resolution: "@snort/system@npm:1.3.3"
|
resolution: "@snort/system@npm:1.3.5"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@noble/curves": "npm:^1.4.0"
|
"@noble/curves": "npm:^1.4.0"
|
||||||
"@noble/hashes": "npm:^1.4.0"
|
"@noble/hashes": "npm:^1.4.0"
|
||||||
@ -2743,7 +2743,7 @@ __metadata:
|
|||||||
lru-cache: "npm:^10.2.0"
|
lru-cache: "npm:^10.2.0"
|
||||||
uuid: "npm:^9.0.0"
|
uuid: "npm:^9.0.0"
|
||||||
ws: "npm:^8.14.0"
|
ws: "npm:^8.14.0"
|
||||||
checksum: 10c0/a153ec74f0c12a6f739d1c1fd4cc78e3ca8d5a8a8c61f876a87e875ed3e330339d6e155fa52f36830aab2da0e8bfc210fe55da1f77e960b14a70e3aebd01ab7f
|
checksum: 10c0/cc48ad0f9e9d857061fb9f04b1d753bc9d284b9f89ec8a766c3afe3a0af33b4f7219dc9d068365ecdc38fde4cc91b9476dbfa3883eb1c9f317b6fbf5b3e681ad
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -7678,8 +7678,8 @@ __metadata:
|
|||||||
"@noble/hashes": "npm:^1.4.0"
|
"@noble/hashes": "npm:^1.4.0"
|
||||||
"@scure/base": "npm:^1.1.6"
|
"@scure/base": "npm:^1.1.6"
|
||||||
"@snort/shared": "npm:^1.0.15"
|
"@snort/shared": "npm:^1.0.15"
|
||||||
"@snort/system": "npm:^1.3.3"
|
"@snort/system": "npm:^1.3.5"
|
||||||
"@snort/system-react": "npm:^1.3.3"
|
"@snort/system-react": "npm:^1.3.5"
|
||||||
"@snort/system-wasm": "npm:^1.0.4"
|
"@snort/system-wasm": "npm:^1.0.4"
|
||||||
"@snort/wallet": "npm:^0.1.3"
|
"@snort/wallet": "npm:^0.1.3"
|
||||||
"@snort/worker-relay": "npm:^1.1.0"
|
"@snort/worker-relay": "npm:^1.1.0"
|
||||||
|
Reference in New Issue
Block a user