Merge pull request 'feat: allow to configure a stream goal' (#90) from stream-goal into main

Reviewed-on: Kieran/stream#90
Reviewed-by: Kieran <kieran@noreply.localhost>
This commit is contained in:
Kieran 2023-09-06 14:48:39 +00:00
commit a278c530e6
41 changed files with 87 additions and 58 deletions

View File

@ -12,7 +12,6 @@ import { Profile } from "element/profile";
import { ChatMessage } from "element/chat-message"; import { ChatMessage } from "element/chat-message";
import { Goal } from "element/goal"; import { Goal } from "element/goal";
import { Badge } from "element/badge"; import { Badge } from "element/badge";
import { NewGoalDialog } from "element/new-goal";
import { WriteMessage } from "element/write-message"; import { WriteMessage } from "element/write-message";
import useEmoji, { packId } from "hooks/emoji"; import useEmoji, { packId } from "hooks/emoji";
import { useLiveChatFeed } from "hooks/live-chat"; import { useLiveChatFeed } from "hooks/live-chat";
@ -117,7 +116,6 @@ export function LiveChat({
<TopZappers zaps={zaps} /> <TopZappers zaps={zaps} />
</div> </div>
{goal && <Goal ev={goal} />} {goal && <Goal ev={goal} />}
{login?.pubkey === host && <NewGoalDialog link={link} />}
</div> </div>
)} )}
<div className="messages"> <div className="messages">

View File

@ -2,19 +2,15 @@ import "./new-goal.css";
import * as Dialog from "@radix-ui/react-dialog"; import * as Dialog from "@radix-ui/react-dialog";
import AsyncButton from "./async-button"; import AsyncButton from "./async-button";
import { NostrLink } from "@snort/system";
import { Icon } from "element/icon"; import { Icon } from "element/icon";
import { useState } from "react"; import { useState } from "react";
import { System } from "index"; import { System } from "index";
import { GOAL } from "const"; import { GOAL } from "const";
import { useLogin } from "hooks/login"; import { useLogin } from "hooks/login";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { defaultRelays } from "const";
interface NewGoalDialogProps { export function NewGoalDialog() {
link: NostrLink;
}
export function NewGoalDialog({ link }: NewGoalDialogProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const login = useLogin(); const login = useLogin();
@ -26,12 +22,9 @@ export function NewGoalDialog({ link }: NewGoalDialogProps) {
if (pub) { if (pub) {
const evNew = await pub.generic(eb => { const evNew = await pub.generic(eb => {
eb.kind(GOAL) eb.kind(GOAL)
.tag(["a", `${link.kind}:${link.author}:${link.id}`])
.tag(["amount", String(Number(goalAmount) * 1000)]) .tag(["amount", String(Number(goalAmount) * 1000)])
.tag(["relays", ...Object.keys(defaultRelays)])
.content(goalName); .content(goalName);
if (link.relays?.length) {
eb.tag(["relays", ...link.relays]);
}
return eb; return eb;
}); });
console.debug(evNew); console.debug(evNew);

View File

@ -3,12 +3,14 @@ import { useEffect, useState, useCallback } from "react";
import { NostrEvent } from "@snort/system"; import { NostrEvent } from "@snort/system";
import { unixNow } from "@snort/shared"; import { unixNow } from "@snort/shared";
import { TagsInput } from "react-tag-input-component"; import { TagsInput } from "react-tag-input-component";
import { FormattedMessage, useIntl } from "react-intl";
import AsyncButton from "./async-button"; import AsyncButton from "./async-button";
import { StreamState } from "../index"; import { StreamState } from "../index";
import { findTag } from "../utils"; import { findTag } from "../utils";
import { useLogin } from "hooks/login"; 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 { export interface StreamEditorProps {
ev?: NostrEvent; 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 (
<select defaultValue={goal} onChange={ev => onGoalSelect(ev.target.value)}>
<option value="">{formatMessage({ defaultMessage: "Select a goal..." })}</option>
{goals?.map(goal => (
<option key={goal.id} value={goal.id}>
{goal.content}
</option>
))}
</select>
);
}
export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) { export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [summary, setSummary] = useState(""); const [summary, setSummary] = useState("");
@ -34,6 +57,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
const [tags, setTags] = useState<string[]>([]); const [tags, setTags] = useState<string[]>([]);
const [contentWarning, setContentWarning] = useState(false); const [contentWarning, setContentWarning] = useState(false);
const [isValid, setIsValid] = useState(false); const [isValid, setIsValid] = useState(false);
const [goal, setGoal] = useState<string | undefined>();
const login = useLogin(); const login = useLogin();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@ -46,6 +70,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
setStart(findTag(ev, "starts")); setStart(findTag(ev, "starts"));
setTags(ev?.tags.filter(a => a[0] === "t").map(a => a[1]) ?? []); setTags(ev?.tags.filter(a => a[0] === "t").map(a => a[1]) ?? []);
setContentWarning(findTag(ev, "content-warning") !== undefined); setContentWarning(findTag(ev, "content-warning") !== undefined);
setGoal(findTag(ev, "goal"));
}, [ev?.id]); }, [ev?.id]);
const validate = useCallback(() => { const validate = useCallback(() => {
@ -90,6 +115,9 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
if (contentWarning) { if (contentWarning) {
eb.tag(["content-warning", "nsfw"]); eb.tag(["content-warning", "nsfw"]);
} }
if (goal && goal.length > 0) {
eb.tag(["goal", goal]);
}
return eb; return eb;
}); });
console.debug(evNew); console.debug(evNew);
@ -201,6 +229,19 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
</div> </div>
</div> </div>
)} )}
{login?.pubkey && (
<>
<div>
<p>
<FormattedMessage defaultMessage="Goal" />
</p>
<div className="paper">
<GoalSelector goal={goal} pubkey={login?.pubkey} onGoalSelect={setGoal} />
</div>
</div>
<NewGoalDialog />
</>
)}
{(options?.canSetContentWarning ?? true) && ( {(options?.canSetContentWarning ?? true) && (
<div className="flex g12 content-warning"> <div className="flex g12 content-warning">
<div> <div>

View File

@ -1,22 +1,31 @@
import { useMemo } from "react"; 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 { useRequestBuilder } from "@snort/system-react";
import { unwrap } from "@snort/shared";
import { GOAL } from "const"; import { GOAL } from "const";
export function useZapGoal(host: string, link?: NostrLink, leaveOpen = false) { export function useZapGoal(id?: string) {
const sub = useMemo(() => { const sub = useMemo(() => {
if (!link) return null; if (!id) return null;
const b = new RequestBuilder(`goals:${host.slice(0, 12)}`); const b = new RequestBuilder(`goal:${id.slice(0, 12)}`);
b.withOptions({ leaveOpen }); b.withFilter().kinds([GOAL]).ids([id]);
b.withFilter()
.kinds([GOAL])
.authors([host])
.tag("a", [`${link.kind}:${unwrap(link.author)}:${link.id}`]);
return b; return b;
}, [link, leaveOpen]); }, [id]);
const { data } = useRequestBuilder(ReplaceableNoteStore, sub); const { data } = useRequestBuilder(ReplaceableNoteStore, sub);
return data; 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;
}

View File

@ -190,6 +190,16 @@ input[type="number"] {
font-weight: 500; 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"] { input[type="checkbox"] {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;

View File

@ -23,6 +23,9 @@
"0GfNiL": { "0GfNiL": {
"defaultMessage": "Stream Zap Goals" "defaultMessage": "Stream Zap Goals"
}, },
"0VV/sK": {
"defaultMessage": "Goal"
},
"1EYCdR": { "1EYCdR": {
"defaultMessage": "Tags" "defaultMessage": "Tags"
}, },
@ -128,6 +131,9 @@
"HAlOn1": { "HAlOn1": {
"defaultMessage": "Name" "defaultMessage": "Name"
}, },
"I/TubD": {
"defaultMessage": "Select a goal..."
},
"I1kjHI": { "I1kjHI": {
"defaultMessage": "Supports {markdown}" "defaultMessage": "Supports {markdown}"
}, },

View File

@ -114,7 +114,7 @@ export function StreamPage({ link, evPreload }: { evPreload?: NostrEvent; link:
const ev = useCurrentStreamFeed(link, true, evPreload); const ev = useCurrentStreamFeed(link, true, evPreload);
const host = getHost(ev); const host = getHost(ev);
const evLink = ev ? eventToLink(ev) : undefined; const evLink = ev ? eventToLink(ev) : undefined;
const goal = useZapGoal(host, evLink, true); const goal = useZapGoal(findTag(ev, "goal"));
const title = findTag(ev, "title"); const title = findTag(ev, "title");
const summary = findTag(ev, "summary"); const summary = findTag(ev, "summary");

View File

@ -59,12 +59,14 @@ export class Nip103StreamProvider implements StreamProvider {
const image = findTag(ev, "image"); const image = findTag(ev, "image");
const tags = ev?.tags.filter(a => a[0] === "t").map(a => a[1]); const tags = ev?.tags.filter(a => a[0] === "t").map(a => a[1]);
const contentWarning = findTag(ev, "content-warning"); const contentWarning = findTag(ev, "content-warning");
const goal = findTag(ev, "goal");
await this.#getJson("PATCH", "event", { await this.#getJson("PATCH", "event", {
title, title,
summary, summary,
image, image,
tags, tags,
content_warning: contentWarning, content_warning: contentWarning,
goal,
}); });
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -7,6 +7,7 @@
"/GCoTA": "Clear", "/GCoTA": "Clear",
"04lmFi": "Save Key", "04lmFi": "Save Key",
"0GfNiL": "Stream Zap Goals", "0GfNiL": "Stream Zap Goals",
"0VV/sK": "Goal",
"1EYCdR": "Tags", "1EYCdR": "Tags",
"1qsXCO": "eg. name@wallet.com", "1qsXCO": "eg. name@wallet.com",
"2/2yg+": "Add", "2/2yg+": "Add",
@ -42,6 +43,7 @@
"H/bNs9": "Save this and keep it safe! If you lose this key, you won't be able to access your account ever again. Yep, it's that serious!", "H/bNs9": "Save this and keep it safe! If you lose this key, you won't be able to access your account ever again. Yep, it's that serious!",
"H5+NAX": "Balance", "H5+NAX": "Balance",
"HAlOn1": "Name", "HAlOn1": "Name",
"I/TubD": "Select a goal...",
"I1kjHI": "Supports {markdown}", "I1kjHI": "Supports {markdown}",
"IJDKz3": "Zap amount in {currency}", "IJDKz3": "Zap amount in {currency}",
"INlWvJ": "OR", "INlWvJ": "OR",
@ -126,4 +128,4 @@
"x82IOl": "Mute", "x82IOl": "Mute",
"yzKwBQ": "eg. nsec1xyz", "yzKwBQ": "eg. nsec1xyz",
"zVDHAu": "Zap Alert" "zVDHAu": "Zap Alert"
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Alerta de Zap" "defaultMessage": "Alerta de Zap"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "هشدار زپ" "defaultMessage": "هشدار زپ"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "ザップアラート" "defaultMessage": "ザップアラート"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Tahadhari ya Zap" "defaultMessage": "Tahadhari ya Zap"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }

View File

@ -381,4 +381,3 @@
"defaultMessage": "Zap Alert" "defaultMessage": "Zap Alert"
} }
} }