From 9fd59817d9cc2293b488aa978aa82f8ccdadbb2c Mon Sep 17 00:00:00 2001 From: verbiricha Date: Sat, 2 Sep 2023 18:33:19 +0200 Subject: [PATCH] feat: allow to configure a stream goal --- src/element/live-chat.tsx | 2 -- src/element/new-goal.tsx | 13 +++-------- src/element/stream-editor.tsx | 43 ++++++++++++++++++++++++++++++++++- src/hooks/goals.ts | 31 ++++++++++++++++--------- src/index.css | 10 ++++++++ src/pages/stream-page.tsx | 2 +- src/providers/zsz.ts | 2 ++ 7 files changed, 78 insertions(+), 25 deletions(-) diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index 1caf24e..61ce96d 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -12,7 +12,6 @@ import { Profile } from "element/profile"; import { ChatMessage } from "element/chat-message"; import { Goal } from "element/goal"; import { Badge } from "element/badge"; -import { NewGoalDialog } from "element/new-goal"; import { WriteMessage } from "element/write-message"; import useEmoji, { packId } from "hooks/emoji"; import { useLiveChatFeed } from "hooks/live-chat"; @@ -117,7 +116,6 @@ export function LiveChat({ {goal && } - {login?.pubkey === host && } )}
diff --git a/src/element/new-goal.tsx b/src/element/new-goal.tsx index cd950e6..48264bb 100644 --- a/src/element/new-goal.tsx +++ b/src/element/new-goal.tsx @@ -2,19 +2,15 @@ import "./new-goal.css"; import * as Dialog from "@radix-ui/react-dialog"; import AsyncButton from "./async-button"; -import { NostrLink } from "@snort/system"; import { Icon } from "element/icon"; import { useState } from "react"; import { System } from "index"; import { GOAL } from "const"; import { useLogin } from "hooks/login"; import { FormattedMessage } from "react-intl"; +import { defaultRelays } from "const"; -interface NewGoalDialogProps { - link: NostrLink; -} - -export function NewGoalDialog({ link }: NewGoalDialogProps) { +export function NewGoalDialog() { const [open, setOpen] = useState(false); const login = useLogin(); @@ -26,12 +22,9 @@ export function NewGoalDialog({ link }: NewGoalDialogProps) { if (pub) { const evNew = await pub.generic(eb => { eb.kind(GOAL) - .tag(["a", `${link.kind}:${link.author}:${link.id}`]) .tag(["amount", String(Number(goalAmount) * 1000)]) + .tag(["relays", ...Object.keys(defaultRelays)]) .content(goalName); - if (link.relays?.length) { - eb.tag(["relays", ...link.relays]); - } return eb; }); console.debug(evNew); diff --git a/src/element/stream-editor.tsx b/src/element/stream-editor.tsx index 9270594..3710300 100644 --- a/src/element/stream-editor.tsx +++ b/src/element/stream-editor.tsx @@ -3,12 +3,14 @@ import { useEffect, useState, useCallback } from "react"; import { NostrEvent } from "@snort/system"; import { unixNow } from "@snort/shared"; import { TagsInput } from "react-tag-input-component"; +import { FormattedMessage, useIntl } from "react-intl"; import AsyncButton from "./async-button"; import { StreamState } from "../index"; import { findTag } from "../utils"; import { useLogin } from "hooks/login"; -import { FormattedMessage, useIntl } from "react-intl"; +import { NewGoalDialog } from "element/new-goal"; +import { useGoals } from "hooks/goals"; export interface StreamEditorProps { ev?: NostrEvent; @@ -24,6 +26,27 @@ export interface StreamEditorProps { }; } +interface GoalSelectorProps { + goal?: string; + pubkey: string; + onGoalSelect: (g: string) => void; +} + +function GoalSelector({ goal, pubkey, onGoalSelect }: GoalSelectorProps) { + const goals = useGoals(pubkey, true); + const { formatMessage } = useIntl(); + return ( + + ); +} + export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) { const [title, setTitle] = useState(""); const [summary, setSummary] = useState(""); @@ -34,6 +57,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) { const [tags, setTags] = useState([]); const [contentWarning, setContentWarning] = useState(false); const [isValid, setIsValid] = useState(false); + const [goal, setGoal] = useState(); const login = useLogin(); const { formatMessage } = useIntl(); @@ -46,6 +70,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) { setStart(findTag(ev, "starts")); setTags(ev?.tags.filter(a => a[0] === "t").map(a => a[1]) ?? []); setContentWarning(findTag(ev, "content-warning") !== undefined); + setGoal(findTag(ev, "goal")); }, [ev?.id]); const validate = useCallback(() => { @@ -90,6 +115,9 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) { if (contentWarning) { eb.tag(["content-warning", "nsfw"]); } + if (goal && goal.length > 0) { + eb.tag(["goal", goal]); + } return eb; }); console.debug(evNew); @@ -201,6 +229,19 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
)} + {login?.pubkey && ( + <> +
+

+ +

+
+ +
+
+ + + )} {(options?.canSetContentWarning ?? true) && (
diff --git a/src/hooks/goals.ts b/src/hooks/goals.ts index 76d067e..d97bcdd 100644 --- a/src/hooks/goals.ts +++ b/src/hooks/goals.ts @@ -1,22 +1,31 @@ import { useMemo } from "react"; -import { RequestBuilder, ReplaceableNoteStore, NostrLink } from "@snort/system"; +import { RequestBuilder, FlatNoteStore, ReplaceableNoteStore } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; -import { unwrap } from "@snort/shared"; import { GOAL } from "const"; -export function useZapGoal(host: string, link?: NostrLink, leaveOpen = false) { +export function useZapGoal(id?: string) { const sub = useMemo(() => { - if (!link) return null; - const b = new RequestBuilder(`goals:${host.slice(0, 12)}`); - b.withOptions({ leaveOpen }); - b.withFilter() - .kinds([GOAL]) - .authors([host]) - .tag("a", [`${link.kind}:${unwrap(link.author)}:${link.id}`]); + if (!id) return null; + const b = new RequestBuilder(`goal:${id.slice(0, 12)}`); + b.withFilter().kinds([GOAL]).ids([id]); return b; - }, [link, leaveOpen]); + }, [id]); const { data } = useRequestBuilder(ReplaceableNoteStore, sub); return data; } + +export function useGoals(pubkey?: string, leaveOpen = false) { + const sub = useMemo(() => { + if (!pubkey) return null; + const b = new RequestBuilder(`goals:${pubkey.slice(0, 12)}`); + b.withOptions({ leaveOpen }); + b.withFilter().kinds([GOAL]).authors([pubkey]); + return b; + }, [pubkey, leaveOpen]); + + const { data } = useRequestBuilder(FlatNoteStore, sub); + + return data; +} diff --git a/src/index.css b/src/index.css index 1411e7e..dcd7ab0 100644 --- a/src/index.css +++ b/src/index.css @@ -190,6 +190,16 @@ input[type="number"] { font-weight: 500; } +select { + font-family: inherit; + border: unset; + background-color: unset; + color: inherit; + width: 100%; + font-size: 16px; + font-weight: 500; +} + input[type="checkbox"] { -webkit-appearance: none; -moz-appearance: none; diff --git a/src/pages/stream-page.tsx b/src/pages/stream-page.tsx index a0016df..3973d46 100644 --- a/src/pages/stream-page.tsx +++ b/src/pages/stream-page.tsx @@ -114,7 +114,7 @@ export function StreamPage({ link, evPreload }: { evPreload?: NostrEvent; link: const ev = useCurrentStreamFeed(link, true, evPreload); const host = getHost(ev); const evLink = ev ? eventToLink(ev) : undefined; - const goal = useZapGoal(host, evLink, true); + const goal = useZapGoal(findTag(ev, "goal")); const title = findTag(ev, "title"); const summary = findTag(ev, "summary"); diff --git a/src/providers/zsz.ts b/src/providers/zsz.ts index 77f06a4..fe11c72 100644 --- a/src/providers/zsz.ts +++ b/src/providers/zsz.ts @@ -59,12 +59,14 @@ export class Nip103StreamProvider implements StreamProvider { const image = findTag(ev, "image"); const tags = ev?.tags.filter(a => a[0] === "t").map(a => a[1]); const contentWarning = findTag(ev, "content-warning"); + const goal = findTag(ev, "goal"); await this.#getJson("PATCH", "event", { title, summary, image, tags, content_warning: contentWarning, + goal, }); }