System.ProfileLoader.UntrackMetadata(pubkeys);
}, [feed.zaps]);
- const userEmojiPacks = useEmoji(login?.pubkey);
+ const mutedPubkeys = useMemo(() => {
+ return new Set(
+ login.muted.tags.filter((t) => t.at(0) === "p").map((t) => t.at(1)),
+ );
+ }, [login.muted.tags]);
+ const userEmojiPacks = login?.emojis ?? [];
const channelEmojiPacks = useEmoji(host);
const allEmojiPacks = useMemo(() => {
- return uniqBy(channelEmojiPacks.concat(userEmojiPacks), packId);
+ return uniqBy(userEmojiPacks.concat(channelEmojiPacks), packId);
}, [userEmojiPacks, channelEmojiPacks]);
const zaps = feed.zaps
@@ -105,6 +110,9 @@ export function LiveChat({
);
}
}, [ev]);
+ const filteredEvents = useMemo(() => {
+ return events.filter((e) => !mutedPubkeys.has(e.pubkey));
+ }, [events, mutedPubkeys]);
return (
@@ -135,7 +143,7 @@ export function LiveChat({
)}
- {events.map((a) => {
+ {filteredEvents.map((a) => {
switch (a.kind) {
case LIVE_STREAM_CHAT: {
return (
diff --git a/src/element/send-zap.tsx b/src/element/send-zap.tsx
index 07db3dc..021355c 100644
--- a/src/element/send-zap.tsx
+++ b/src/element/send-zap.tsx
@@ -9,7 +9,6 @@ import { bytesToHex } from "@noble/curves/abstract/utils";
import { formatSats } from "../number";
import { Icon } from "./icon";
import AsyncButton from "./async-button";
-import { Relays } from "index";
import QrCode from "./qr-code";
import { useLogin } from "hooks/login";
import Copy from "./copy";
@@ -21,7 +20,7 @@ export interface LNURLLike {
getInvoice(
amountInSats: number,
comment?: string,
- zap?: NostrEvent
+ zap?: NostrEvent,
): Promise<{ pr?: string }>;
}
@@ -55,7 +54,7 @@ export function SendZaps({
const [comment, setComment] = useState("");
const [invoice, setInvoice] = useState("");
const login = useLogin();
-
+ const relays = Object.keys(login.relays);
const name = targetName ?? svc?.name;
async function loadService(lnurl: string) {
const s = new LNURL(lnurl);
@@ -78,7 +77,9 @@ export function SendZaps({
let pub = login?.publisher();
let isAnon = false;
if (!pub) {
- pub = EventPublisher.privateKey(bytesToHex(secp256k1.utils.randomPrivateKey()));
+ pub = EventPublisher.privateKey(
+ bytesToHex(secp256k1.utils.randomPrivateKey()),
+ );
isAnon = true;
}
@@ -88,7 +89,7 @@ export function SendZaps({
zap = await pub.zap(
amountInSats * 1000,
pubkey,
- Relays,
+ relays,
undefined,
comment,
(eb) => {
@@ -102,7 +103,7 @@ export function SendZaps({
eb.tag(["anon", ""]);
}
return eb;
- }
+ },
);
}
const invoice = await svc.getInvoice(amountInSats, comment, zap);
diff --git a/src/hooks/emoji.tsx b/src/hooks/emoji.tsx
index 54c28df..3675f42 100644
--- a/src/hooks/emoji.tsx
+++ b/src/hooks/emoji.tsx
@@ -1,3 +1,6 @@
+import { useMemo } from "react";
+import uniqBy from "lodash.uniqby";
+
import {
RequestBuilder,
ReplaceableNoteStore,
@@ -6,23 +9,9 @@ import {
} from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { System } from "index";
-import { useMemo } from "react";
import { findTag } from "utils";
import { EMOJI_PACK, USER_EMOJIS } from "const";
-import type { EmojiTag } from "../element/emoji";
-import uniqBy from "lodash.uniqby";
-
-export interface Emoji {
- native?: string;
- id?: string;
-}
-
-export interface EmojiPack {
- address: string;
- name: string;
- author: string;
- emojis: EmojiTag[];
-}
+import { EmojiPack } from "types";
function cleanShortcode(shortcode?: string) {
return shortcode?.replace(/\s+/g, "_").replace(/_$/, "");
@@ -44,22 +33,10 @@ export function packId(pack: EmojiPack): string {
return `${pack.author}:${pack.name}`;
}
-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]);
-
- return rb;
- }, [pubkey]);
-
- const { data: userEmoji } = useRequestBuilder(
- System,
- ReplaceableNoteStore,
- sub,
- );
-
+export function useUserEmojiPacks(
+ pubkey?: string,
+ userEmoji: { tags: string[][] },
+) {
const related = useMemo(() => {
if (userEmoji) {
return userEmoji.tags.filter(
@@ -106,3 +83,23 @@ export default function useEmoji(pubkey?: string) {
return emojis;
}
+
+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]);
+
+ return rb;
+ }, [pubkey]);
+
+ const { data: userEmoji } = useRequestBuilder(
+ System,
+ ReplaceableNoteStore,
+ sub,
+ );
+
+ const emojis = useUserEmojiPacks(pubkey, userEmoji ?? { tags: [] });
+ return emojis;
+}
diff --git a/src/hooks/follows.ts b/src/hooks/follows.ts
deleted file mode 100644
index a9f3921..0000000
--- a/src/hooks/follows.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { useMemo } from "react";
-import { EventKind, ReplaceableNoteStore, RequestBuilder } from "@snort/system";
-import { useRequestBuilder } from "@snort/system-react";
-import { System } from "index";
-
-export default function useFollows(pubkey: string, leaveOpen = false) {
- const sub = useMemo(() => {
- const b = new RequestBuilder(`follows:${pubkey.slice(0, 12)}`);
- b.withOptions({
- leaveOpen,
- })
- .withFilter()
- .authors([pubkey])
- .kinds([EventKind.ContactList]);
- return b;
- }, [pubkey, leaveOpen]);
-
- const { data } = useRequestBuilder(
- System,
- ReplaceableNoteStore,
- sub,
- );
-
- const relays = JSON.parse(data?.content.length > 0 ? data?.content : "{}");
- return data ? { tags: data.tags, relays } : null;
-}
diff --git a/src/hooks/login.ts b/src/hooks/login.ts
index d7662e6..84ad78c 100644
--- a/src/hooks/login.ts
+++ b/src/hooks/login.ts
@@ -1,17 +1,100 @@
-import { Login } from "index";
-import { getPublisher } from "login";
-import { useSyncExternalStore } from "react";
+import { useSyncExternalStore, useMemo, useState, useEffect } from "react";
+
+import { EventKind, NoteCollection, RequestBuilder } from "@snort/system";
+import { useRequestBuilder } from "@snort/system-react";
+
+import { useUserEmojiPacks } from "hooks/emoji";
+import { USER_EMOJIS } from "const";
+import { System, Login } from "index";
+import {
+ getPublisher,
+ setMuted,
+ setEmojis,
+ setFollows,
+ setRelays,
+} from "login";
export function useLogin() {
const session = useSyncExternalStore(
(c) => Login.hook(c),
- () => Login.snapshot()
+ () => Login.snapshot(),
);
if (!session) return;
return {
...session,
publisher: () => {
return getPublisher(session);
- }
- }
+ },
+ };
+}
+
+export function useLoginEvents(pubkey?: string, leaveOpen = false) {
+ const [userEmojis, setUserEmojis] = useState([]);
+ const session = useSyncExternalStore(
+ (c) => Login.hook(c),
+ () => Login.snapshot(),
+ );
+
+ useEffect(() => {
+ if (session) {
+ Object.entries(session.relays).forEach((params) => {
+ const [relay, settings] = params;
+ System.ConnectToRelay(relay, settings);
+ });
+ }
+ }, [session]);
+
+ 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,
+ EventKind.Relays,
+ 10_000 as EventKind,
+ USER_EMOJIS,
+ ]);
+ return b;
+ }, [pubkey, leaveOpen]);
+
+ const { data } = useRequestBuilder(
+ System,
+ NoteCollection,
+ sub,
+ );
+
+ useEffect(() => {
+ if (!data) {
+ return;
+ }
+ if (!session) {
+ return;
+ }
+ for (const ev of data) {
+ if (ev?.kind === USER_EMOJIS) {
+ setUserEmojis(ev.tags);
+ }
+ if (ev?.kind === 10_000) {
+ // todo: decrypt ev.content tags
+ setMuted(session, ev.tags, ev.created_at);
+ }
+ if (ev?.kind === EventKind.ContactList) {
+ setFollows(session, ev.tags, ev.created_at);
+ }
+ if (ev?.kind === EventKind.Relays) {
+ setRelays(session, ev.tags, ev.created_at);
+ }
+ }
+ }, [session, data]);
+
+ const emojis = useUserEmojiPacks(pubkey, { tags: userEmojis });
+ useEffect(() => {
+ if (session) {
+ setEmojis(session, emojis);
+ }
+ }, [session, emojis]);
}
diff --git a/src/index.tsx b/src/index.tsx
index 378861c..09161db 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -6,13 +6,14 @@ import ReactDOM from "react-dom/client";
import { NostrSystem } from "@snort/system";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
-import { RootPage } from "./pages/root";
+import { RootPage } from "pages/root";
import { LayoutPage } from "pages/layout";
import { ProfilePage } from "pages/profile-page";
import { StreamPage } from "pages/stream-page";
import { ChatPopout } from "pages/chat-popout";
import { LoginStore } from "login";
import { StreamProvidersPage } from "pages/providers";
+import { defaultRelays } from "const";
export enum StreamState {
Live = "live",
@@ -23,14 +24,10 @@ export enum StreamState {
export const System = new NostrSystem({});
export const Login = new LoginStore();
-export const Relays = [
- "wss://relay.snort.social",
- "wss://nos.lol",
- "wss://relay.damus.io",
- "wss://nostr.wine",
-];
-
-Relays.forEach((r) => System.ConnectToRelay(r, { read: true, write: true }));
+Object.entries(defaultRelays).forEach((params) => {
+ const [relay, settings] = params;
+ System.ConnectToRelay(relay, settings);
+});
const router = createBrowserRouter([
{
@@ -64,10 +61,10 @@ const router = createBrowserRouter([
},
]);
const root = ReactDOM.createRoot(
- document.getElementById("root") as HTMLDivElement
+ document.getElementById("root") as HTMLDivElement,
);
root.render(
-
+ ,
);
diff --git a/src/login.ts b/src/login.ts
index 1a46871..3089f5a 100644
--- a/src/login.ts
+++ b/src/login.ts
@@ -2,17 +2,27 @@ import { bytesToHex } from "@noble/curves/abstract/utils";
import { schnorr } from "@noble/curves/secp256k1";
import { ExternalStore } from "@snort/shared";
import { EventPublisher, Nip7Signer, PrivateKeySigner } from "@snort/system";
+import type { EmojiPack, Relays } from "types";
+import { defaultRelays } from "const";
export enum LoginType {
Nip7 = "nip7",
PrivateKey = "private-key",
}
+interface ReplaceableTags {
+ tags: Array;
+ timestamp: number;
+}
+
export interface LoginSession {
type: LoginType;
pubkey: string;
privateKey?: string;
- follows: string[];
+ follows: ReplaceableTags;
+ muted: ReplaceableTags;
+ relays: Relays;
+ emojis: Array;
}
export class LoginStore extends ExternalStore {
@@ -33,7 +43,10 @@ export class LoginStore extends ExternalStore {
this.#session = {
type,
pubkey: pk,
- follows: [],
+ muted: { tags: [], timestamp: 0 },
+ follows: { tags: [], timestamp: 0 },
+ relays: defaultRelays,
+ emojis: [],
};
this.#save();
}
@@ -43,7 +56,9 @@ export class LoginStore extends ExternalStore {
type: LoginType.PrivateKey,
pubkey: bytesToHex(schnorr.getPublicKey(key)),
privateKey: key,
- follows: [],
+ follows: { tags: [], timestamp: 0 },
+ muted: { tags: [], timestamp: 0 },
+ emojis: [],
};
this.#save();
}
@@ -53,6 +68,11 @@ export class LoginStore extends ExternalStore {
this.#save();
}
+ updateSession(s: LoginSession) {
+ this.#session = s;
+ this.#save();
+ }
+
takeSnapshot() {
return this.#session ? { ...this.#session } : undefined;
}
@@ -75,8 +95,52 @@ export function getPublisher(session: LoginSession) {
case LoginType.PrivateKey: {
return new EventPublisher(
new PrivateKeySigner(session.privateKey!),
- session.pubkey
+ session.pubkey,
);
}
}
}
+
+export function setFollows(
+ state: LoginSession,
+ follows: Array,
+ ts: number,
+) {
+ if (state.follows.timestamp >= ts) {
+ return;
+ }
+ state.follows.tags = follows;
+ state.follows.timestamp = ts;
+}
+
+export function setEmojis(state: LoginSession, emojis: Array) {
+ state.emojis = emojis;
+}
+
+export function setMuted(
+ state: LoginSession,
+ muted: Array,
+ ts: number,
+) {
+ if (state.muted.timestamp >= ts) {
+ return;
+ }
+ state.muted.tags = muted;
+ state.muted.timestamp = ts;
+}
+
+export function setRelays(
+ state: LoginSession,
+ relays: Array,
+ ts: number,
+) {
+ if (state.relays.timestamp >= ts) {
+ return;
+ }
+ state.relays = relays.reduce((acc, r) => {
+ const [, relay] = r;
+ const write = r.length === 2 || r.includes("write");
+ const read = r.length === 2 || r.includes("read");
+ return { ...acc, [relay]: { read, write } };
+ }, {});
+}
diff --git a/src/pages/layout.tsx b/src/pages/layout.tsx
index fee8b89..02e9531 100644
--- a/src/pages/layout.tsx
+++ b/src/pages/layout.tsx
@@ -5,7 +5,7 @@ import { Outlet, useNavigate } from "react-router-dom";
import { Helmet } from "react-helmet";
import { Icon } from "element/icon";
-import { useLogin } from "hooks/login";
+import { useLogin, useLoginEvents } from "hooks/login";
import { Profile } from "element/profile";
import { NewStreamDialog } from "element/new-stream";
import { LoginSignup } from "element/login-signup";
@@ -17,6 +17,7 @@ export function LayoutPage() {
const navigate = useNavigate();
const login = useLogin();
const [showLogin, setShowLogin] = useState(false);
+ useLoginEvents(login?.pubkey, true);
function loggedIn() {
if (!login) return;
@@ -105,4 +106,4 @@ export function LayoutPage() {
);
-}
\ No newline at end of file
+}
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..d60050d
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,22 @@
+export interface RelaySettings {
+ read: boolean;
+ write: boolean;
+}
+
+export interface Relays {
+ [key: string]: RelaySettings;
+}
+
+export type EmojiTag = ["emoji", string, string];
+
+export interface Emoji {
+ native?: string;
+ id?: string;
+}
+
+export interface EmojiPack {
+ address: string;
+ name: string;
+ author: string;
+ emojis: EmojiTag[];
+}