feat: toast notify on publish
Some checks are pending
continuous-integration/drone/push Build is running

This commit is contained in:
Kieran 2023-12-02 22:44:44 +00:00
parent 46ee7385fc
commit cfb446c7c8
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
11 changed files with 104 additions and 144 deletions

View File

@ -10,7 +10,6 @@
"publicDir": "public/snort",
"httpCache": "",
"animalNamePlaceholders": false,
"showNoteBroadcaster": true,
"defaultZapPoolFee": 0.5,
"bypassImgProxyError": false,
"features": {
@ -22,6 +21,7 @@
"signUp": {
"moderation": true
},
"noteCreatorToast": true,
"hideFromNavbar": ["/graph"],
"deckSubKind": 1,
"eventLinkPrefix": "nevent",

View File

@ -10,7 +10,6 @@
"publicDir": "public/iris",
"httpCache": "https://api.iris.to",
"animalNamePlaceholders": true,
"showNoteBroadcaster": false,
"defaultZapPoolFee": 0.5,
"bypassImgProxyError": true,
"features": {

View File

@ -51,7 +51,6 @@ declare const CONFIG: {
navLogo: string | null;
httpCache: string;
animalNamePlaceholders: boolean;
showNoteBroadcaster: boolean;
defaultZapPoolFee: number;
bypassImgProxyError: boolean;
features: {
@ -67,6 +66,8 @@ declare const CONFIG: {
hideFromNavbar: Array<string>;
// Limit deck to certain subscriber tier
deckSubKind?: number;
// Create toast notifications when publishing notes
noteCreatorToast?: boolean;
eventLinkPrefix: NostrPrefix;
profileLinkPrefix: NostrPrefix;
defaultRelays: Record<string, RelaySettings>;

View File

@ -1,102 +0,0 @@
import { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { unwrap } from "@snort/shared";
import { NostrEvent, OkResponse } from "@snort/system";
import AsyncButton from "@/Element/Button/AsyncButton";
import Icon from "@/Icons/Icon";
import { getRelayName, sanitizeRelayUrl } from "@/SnortUtils";
import { removeRelay } from "@/Login";
import useLogin from "@/Hooks/useLogin";
import useEventPublisher from "@/Hooks/useEventPublisher";
import { saveRelays } from "@/Pages/settings/Relays";
import { sendEventToRelays } from "@/Element/Event/Create/util";
export function NoteBroadcaster({
evs,
onClose,
customRelays,
}: {
evs: Array<NostrEvent>;
onClose: () => void;
customRelays?: Array<string>;
}) {
const [results, setResults] = useState<Array<OkResponse>>([]);
const { formatMessage } = useIntl();
const login = useLogin();
const { publisher, system } = useEventPublisher();
useEffect(() => {
Promise.all(evs.map(a => sendEventToRelays(system, a, customRelays, setResults)).flat()).catch(console.error);
}, []);
async function removeRelayFromResult(r: OkResponse) {
if (publisher) {
removeRelay(login, unwrap(sanitizeRelayUrl(r.relay)));
await saveRelays(system, publisher, login.relays.item);
setResults(s => s.filter(a => a.relay !== r.relay));
}
}
async function retryPublish(r: OkResponse) {
const ev = evs.find(a => a.id === r.id);
if (ev) {
const rsp = await system.WriteOnceToRelay(unwrap(sanitizeRelayUrl(r.relay)), ev);
setResults(s =>
s.map(x => {
if (x.relay === r.relay && x.id === r.id) {
return rsp; //replace with new response
}
return x;
}),
);
}
}
return (
<div className="flex flex-col g16">
<h3>
<FormattedMessage defaultMessage="Sending notes and other stuff" id="ugyJnE" />
</h3>
{results
.filter(a => a.message !== "Duplicate request")
.sort(a => (a.ok ? -1 : 1))
.map(r => (
<div className="flex items-center g16">
<Icon name={r.ok ? "check" : "x"} className={r.ok ? "success" : "error"} size={24} />
<div className="flex flex-col grow g4">
<b>{getRelayName(r.relay)}</b>
{r.message && <small>{r.message}</small>}
</div>
{!r.ok && (
<div className="flex g8">
<AsyncButton
onClick={() => retryPublish(r)}
className="p4 br-compact flex items-center secondary"
title={formatMessage({
defaultMessage: "Retry publishing",
id: "9kSari",
})}>
<Icon name="refresh-ccw-01" />
</AsyncButton>
<AsyncButton
onClick={() => removeRelayFromResult(r)}
className="p4 br-compact flex items-center secondary"
title={formatMessage({
defaultMessage: "Remove from my relays",
id: "UJTWqI",
})}>
<Icon name="trash-01" className="trash-icon" />
</AsyncButton>
</div>
)}
</div>
))}
<div className="flex-row g8">
<button type="button" onClick={() => onClose()}>
<FormattedMessage defaultMessage="Close" id="rbrahO" />
</button>
</div>
</div>
);
}

View File

@ -18,14 +18,15 @@ import useLogin from "@/Hooks/useLogin";
import { GetPowWorker } from "@/index";
import AsyncButton from "@/Element/Button/AsyncButton";
import { AsyncIcon } from "@/Element/Button/AsyncIcon";
import { fetchNip05Pubkey } from "@snort/shared";
import { fetchNip05Pubkey, unixNow } from "@snort/shared";
import { ZapTarget } from "@/Zapper";
import { useNoteCreator } from "@/State/NoteCreator";
import { NoteBroadcaster } from "@/Element/Event/Create/NoteBroadcaster";
import FileUploadProgress from "../FileUpload";
import { ToggleSwitch } from "@/Icons/Toggle";
import { sendEventToRelays } from "@/Element/Event/Create/util";
import { TrendingHashTagsLine } from "@/Element/Event/Create/TrendingHashTagsLine";
import { Toastore } from "@/Toaster";
import { OkResponseRow } from "./OkResponseRow";
export function NoteCreator() {
const { formatMessage } = useIntl();
@ -151,15 +152,17 @@ export function NoteCreator() {
const ev = await buildNote();
if (ev) {
const events = (note.otherEvents ?? []).concat(ev);
note.update(n => {
n.sending = events;
});
if (!CONFIG.showNoteBroadcaster) {
Promise.all(events.map(a => sendEventToRelays(system, a, note.selectedCustomRelays)).flat()).catch(
console.error,
);
reset();
}
events.map(a => sendEventToRelays(system, a, note.selectedCustomRelays, r => {
if (CONFIG.noteCreatorToast) {
r.forEach(rr => {
Toastore.push({
element: <OkResponseRow rsp={rr} />,
expire: unixNow() + (rr.ok ? 5 : 55555)
})
});
}
}));
note.update(n => n.reset());
}
}
@ -324,18 +327,18 @@ export function NoteCreator() {
onChange={e => {
note.update(
v =>
(v.selectedCustomRelays =
// set false if all relays selected
e.target.checked &&
(v.selectedCustomRelays =
// set false if all relays selected
e.target.checked &&
note.selectedCustomRelays &&
note.selectedCustomRelays.length == a.length - 1
? undefined
: // otherwise return selectedCustomRelays with target relay added / removed
a.filter(el =>
el === r
? e.target.checked
: !note.selectedCustomRelays || note.selectedCustomRelays.includes(el),
)),
? undefined
: // otherwise return selectedCustomRelays with target relay added / removed
a.filter(el =>
el === r
? e.target.checked
: !note.selectedCustomRelays || note.selectedCustomRelays.includes(el),
)),
);
}}
/>
@ -404,9 +407,9 @@ export function NoteCreator() {
onChange={e =>
note.update(
v =>
(v.zapSplits = arr.map((vv, ii) =>
ii === i ? { ...vv, weight: Number(e.target.value) } : vv,
)),
(v.zapSplits = arr.map((vv, ii) =>
ii === i ? { ...vv, weight: Number(e.target.value) } : vv,
)),
)
}
/>
@ -650,8 +653,7 @@ export function NoteCreator() {
if (!note.show) return null;
return (
<Modal id="note-creator" className="note-creator-modal" onClose={reset}>
{note.sending && <NoteBroadcaster evs={note.sending} onClose={reset} customRelays={note.selectedCustomRelays} />}
{!note.sending && noteCreatorForm()}
{noteCreatorForm()}
</Modal>
);
}
}

View File

@ -0,0 +1,60 @@
import AsyncButton from "@/Element/Button/AsyncButton";
import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin";
import Icon from "@/Icons/Icon";
import { removeRelay } from "@/Login";
import { saveRelays } from "@/Pages/settings/Relays";
import { getRelayName } from "@/SnortUtils";
import { unwrap, sanitizeRelayUrl } from "@snort/shared";
import { OkResponse } from "@snort/system";
import { useState } from "react";
import { useIntl } from "react-intl";
export function OkResponseRow({ rsp }: { rsp: OkResponse }) {
const [r, setResult] = useState(rsp);
const { formatMessage } = useIntl();
const { publisher, system } = useEventPublisher();
const login = useLogin();
async function removeRelayFromResult(r: OkResponse) {
if (publisher) {
removeRelay(login, unwrap(sanitizeRelayUrl(r.relay)));
await saveRelays(system, publisher, login.relays.item);
}
}
async function retryPublish(r: OkResponse) {
const rsp = await system.WriteOnceToRelay(unwrap(sanitizeRelayUrl(r.relay)), r.event);
setResult(rsp);
}
return <div className="flex items-center g16">
<Icon name={r.ok ? "check" : "x"} className={r.ok ? "success" : "error"} size={24} />
<div className="flex flex-col grow g4">
<b>{getRelayName(r.relay)}</b>
{r.message && <small>{r.message}</small>}
</div>
{!r.ok && (
<div className="flex g8">
<AsyncButton
onClick={() => retryPublish(r)}
className="p4 br-compact flex items-center secondary"
title={formatMessage({
defaultMessage: "Retry publishing",
id: "9kSari",
})}>
<Icon name="refresh-ccw-01" />
</AsyncButton>
<AsyncButton
onClick={() => removeRelayFromResult(r)}
className="p4 br-compact flex items-center secondary"
title={formatMessage({
defaultMessage: "Remove from my relays",
id: "UJTWqI",
})}>
<Icon name="trash-01" className="trash-icon" />
</AsyncButton>
</div>
)}
</div>
}

View File

@ -75,7 +75,6 @@ export default function useLoginFeed() {
EventKind.InterestsList,
EventKind.PublicChatsList,
]);
b.withFilter().authors([pubKey]).kinds([]);
if (CONFIG.features.subscriptions && !login.readonly) {
b.withFilter().authors([pubKey]).kinds([EventKind.AppData]).tag("d", ["snort"]);
b.withFilter()

View File

@ -1,12 +1,9 @@
.toaster {
position: fixed;
bottom: 0;
left: 0;
bottom: 2px;
left: 2px;
display: flex;
flex-direction: column-reverse;
z-index: 9999;
}
.toaster > .card {
border: 1px solid var(--gray);
}
gap: 4px;
}

View File

@ -18,7 +18,7 @@ class ToasterSlots extends ExternalStore<Array<ToastNotification>> {
#cleanup = setInterval(() => this.#eatToast(), 1000);
push(n: ToastNotification) {
n.expire ??= unixNow() + 3;
n.expire ??= unixNow() + 10;
n.id ??= uuid();
this.#stack.push(n);
this.notifyChange();
@ -46,8 +46,8 @@ export default function Toaster() {
return createPortal(
<div className="toaster">
{toast.map(a => (
<div className="card flex" key={a.id}>
<Icon name={a.icon ?? "bell"} className="mr5" />
<div className="p br b flex bg-dark g8 fade-in" key={a.id}>
{a.icon && <Icon name={a.icon} />}
{a.element}
</div>
))}

View File

@ -785,7 +785,7 @@ button.tall {
opacity: 1;
animation-name: fadeInOpacity;
animation-timing-function: ease-in;
animation-duration: 1s;
animation-duration: 0.5s;
}
@keyframes fadeInOpacity {

View File

@ -23,6 +23,7 @@ export interface OkResponse {
id: string;
relay: string;
message?: string;
event: NostrEvent;
}
/**
@ -284,6 +285,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
id: e.id,
relay: this.Address,
message: "Duplicate request",
event: e,
});
return;
}
@ -294,6 +296,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
id: e.id,
relay: this.Address,
message: "Timeout waiting for OK response",
event: e,
});
}, timeout);
@ -305,6 +308,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
id: id as string,
relay: this.Address,
message: message as string | undefined,
event: e,
});
});