feat: toast notify on publish
Some checks are pending
continuous-integration/drone/push Build is running
Some checks are pending
continuous-integration/drone/push Build is running
This commit is contained in:
parent
46ee7385fc
commit
cfb446c7c8
@ -10,7 +10,6 @@
|
|||||||
"publicDir": "public/snort",
|
"publicDir": "public/snort",
|
||||||
"httpCache": "",
|
"httpCache": "",
|
||||||
"animalNamePlaceholders": false,
|
"animalNamePlaceholders": false,
|
||||||
"showNoteBroadcaster": true,
|
|
||||||
"defaultZapPoolFee": 0.5,
|
"defaultZapPoolFee": 0.5,
|
||||||
"bypassImgProxyError": false,
|
"bypassImgProxyError": false,
|
||||||
"features": {
|
"features": {
|
||||||
@ -22,6 +21,7 @@
|
|||||||
"signUp": {
|
"signUp": {
|
||||||
"moderation": true
|
"moderation": true
|
||||||
},
|
},
|
||||||
|
"noteCreatorToast": true,
|
||||||
"hideFromNavbar": ["/graph"],
|
"hideFromNavbar": ["/graph"],
|
||||||
"deckSubKind": 1,
|
"deckSubKind": 1,
|
||||||
"eventLinkPrefix": "nevent",
|
"eventLinkPrefix": "nevent",
|
||||||
|
@ -10,7 +10,6 @@
|
|||||||
"publicDir": "public/iris",
|
"publicDir": "public/iris",
|
||||||
"httpCache": "https://api.iris.to",
|
"httpCache": "https://api.iris.to",
|
||||||
"animalNamePlaceholders": true,
|
"animalNamePlaceholders": true,
|
||||||
"showNoteBroadcaster": false,
|
|
||||||
"defaultZapPoolFee": 0.5,
|
"defaultZapPoolFee": 0.5,
|
||||||
"bypassImgProxyError": true,
|
"bypassImgProxyError": true,
|
||||||
"features": {
|
"features": {
|
||||||
|
3
packages/app/custom.d.ts
vendored
3
packages/app/custom.d.ts
vendored
@ -51,7 +51,6 @@ declare const CONFIG: {
|
|||||||
navLogo: string | null;
|
navLogo: string | null;
|
||||||
httpCache: string;
|
httpCache: string;
|
||||||
animalNamePlaceholders: boolean;
|
animalNamePlaceholders: boolean;
|
||||||
showNoteBroadcaster: boolean;
|
|
||||||
defaultZapPoolFee: number;
|
defaultZapPoolFee: number;
|
||||||
bypassImgProxyError: boolean;
|
bypassImgProxyError: boolean;
|
||||||
features: {
|
features: {
|
||||||
@ -67,6 +66,8 @@ declare const CONFIG: {
|
|||||||
hideFromNavbar: Array<string>;
|
hideFromNavbar: Array<string>;
|
||||||
// Limit deck to certain subscriber tier
|
// Limit deck to certain subscriber tier
|
||||||
deckSubKind?: number;
|
deckSubKind?: number;
|
||||||
|
// Create toast notifications when publishing notes
|
||||||
|
noteCreatorToast?: boolean;
|
||||||
eventLinkPrefix: NostrPrefix;
|
eventLinkPrefix: NostrPrefix;
|
||||||
profileLinkPrefix: NostrPrefix;
|
profileLinkPrefix: NostrPrefix;
|
||||||
defaultRelays: Record<string, RelaySettings>;
|
defaultRelays: Record<string, RelaySettings>;
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -18,14 +18,15 @@ import useLogin from "@/Hooks/useLogin";
|
|||||||
import { GetPowWorker } from "@/index";
|
import { GetPowWorker } from "@/index";
|
||||||
import AsyncButton from "@/Element/Button/AsyncButton";
|
import AsyncButton from "@/Element/Button/AsyncButton";
|
||||||
import { AsyncIcon } from "@/Element/Button/AsyncIcon";
|
import { AsyncIcon } from "@/Element/Button/AsyncIcon";
|
||||||
import { fetchNip05Pubkey } from "@snort/shared";
|
import { fetchNip05Pubkey, unixNow } from "@snort/shared";
|
||||||
import { ZapTarget } from "@/Zapper";
|
import { ZapTarget } from "@/Zapper";
|
||||||
import { useNoteCreator } from "@/State/NoteCreator";
|
import { useNoteCreator } from "@/State/NoteCreator";
|
||||||
import { NoteBroadcaster } from "@/Element/Event/Create/NoteBroadcaster";
|
|
||||||
import FileUploadProgress from "../FileUpload";
|
import FileUploadProgress from "../FileUpload";
|
||||||
import { ToggleSwitch } from "@/Icons/Toggle";
|
import { ToggleSwitch } from "@/Icons/Toggle";
|
||||||
import { sendEventToRelays } from "@/Element/Event/Create/util";
|
import { sendEventToRelays } from "@/Element/Event/Create/util";
|
||||||
import { TrendingHashTagsLine } from "@/Element/Event/Create/TrendingHashTagsLine";
|
import { TrendingHashTagsLine } from "@/Element/Event/Create/TrendingHashTagsLine";
|
||||||
|
import { Toastore } from "@/Toaster";
|
||||||
|
import { OkResponseRow } from "./OkResponseRow";
|
||||||
|
|
||||||
export function NoteCreator() {
|
export function NoteCreator() {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
@ -151,15 +152,17 @@ export function NoteCreator() {
|
|||||||
const ev = await buildNote();
|
const ev = await buildNote();
|
||||||
if (ev) {
|
if (ev) {
|
||||||
const events = (note.otherEvents ?? []).concat(ev);
|
const events = (note.otherEvents ?? []).concat(ev);
|
||||||
note.update(n => {
|
events.map(a => sendEventToRelays(system, a, note.selectedCustomRelays, r => {
|
||||||
n.sending = events;
|
if (CONFIG.noteCreatorToast) {
|
||||||
});
|
r.forEach(rr => {
|
||||||
if (!CONFIG.showNoteBroadcaster) {
|
Toastore.push({
|
||||||
Promise.all(events.map(a => sendEventToRelays(system, a, note.selectedCustomRelays)).flat()).catch(
|
element: <OkResponseRow rsp={rr} />,
|
||||||
console.error,
|
expire: unixNow() + (rr.ok ? 5 : 55555)
|
||||||
);
|
})
|
||||||
reset();
|
});
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
|
note.update(n => n.reset());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,18 +327,18 @@ export function NoteCreator() {
|
|||||||
onChange={e => {
|
onChange={e => {
|
||||||
note.update(
|
note.update(
|
||||||
v =>
|
v =>
|
||||||
(v.selectedCustomRelays =
|
(v.selectedCustomRelays =
|
||||||
// set false if all relays selected
|
// set false if all relays selected
|
||||||
e.target.checked &&
|
e.target.checked &&
|
||||||
note.selectedCustomRelays &&
|
note.selectedCustomRelays &&
|
||||||
note.selectedCustomRelays.length == a.length - 1
|
note.selectedCustomRelays.length == a.length - 1
|
||||||
? undefined
|
? undefined
|
||||||
: // otherwise return selectedCustomRelays with target relay added / removed
|
: // otherwise return selectedCustomRelays with target relay added / removed
|
||||||
a.filter(el =>
|
a.filter(el =>
|
||||||
el === r
|
el === r
|
||||||
? e.target.checked
|
? e.target.checked
|
||||||
: !note.selectedCustomRelays || note.selectedCustomRelays.includes(el),
|
: !note.selectedCustomRelays || note.selectedCustomRelays.includes(el),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -404,9 +407,9 @@ export function NoteCreator() {
|
|||||||
onChange={e =>
|
onChange={e =>
|
||||||
note.update(
|
note.update(
|
||||||
v =>
|
v =>
|
||||||
(v.zapSplits = arr.map((vv, ii) =>
|
(v.zapSplits = arr.map((vv, ii) =>
|
||||||
ii === i ? { ...vv, weight: Number(e.target.value) } : vv,
|
ii === i ? { ...vv, weight: Number(e.target.value) } : vv,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -650,8 +653,7 @@ export function NoteCreator() {
|
|||||||
if (!note.show) return null;
|
if (!note.show) return null;
|
||||||
return (
|
return (
|
||||||
<Modal id="note-creator" className="note-creator-modal" onClose={reset}>
|
<Modal id="note-creator" className="note-creator-modal" onClose={reset}>
|
||||||
{note.sending && <NoteBroadcaster evs={note.sending} onClose={reset} customRelays={note.selectedCustomRelays} />}
|
{noteCreatorForm()}
|
||||||
{!note.sending && noteCreatorForm()}
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
60
packages/app/src/Element/Event/Create/OkResponseRow.tsx
Normal file
60
packages/app/src/Element/Event/Create/OkResponseRow.tsx
Normal 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>
|
||||||
|
}
|
@ -75,7 +75,6 @@ export default function useLoginFeed() {
|
|||||||
EventKind.InterestsList,
|
EventKind.InterestsList,
|
||||||
EventKind.PublicChatsList,
|
EventKind.PublicChatsList,
|
||||||
]);
|
]);
|
||||||
b.withFilter().authors([pubKey]).kinds([]);
|
|
||||||
if (CONFIG.features.subscriptions && !login.readonly) {
|
if (CONFIG.features.subscriptions && !login.readonly) {
|
||||||
b.withFilter().authors([pubKey]).kinds([EventKind.AppData]).tag("d", ["snort"]);
|
b.withFilter().authors([pubKey]).kinds([EventKind.AppData]).tag("d", ["snort"]);
|
||||||
b.withFilter()
|
b.withFilter()
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
.toaster {
|
.toaster {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 2px;
|
||||||
left: 0;
|
left: 2px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
gap: 4px;
|
||||||
|
}
|
||||||
.toaster > .card {
|
|
||||||
border: 1px solid var(--gray);
|
|
||||||
}
|
|
@ -18,7 +18,7 @@ class ToasterSlots extends ExternalStore<Array<ToastNotification>> {
|
|||||||
#cleanup = setInterval(() => this.#eatToast(), 1000);
|
#cleanup = setInterval(() => this.#eatToast(), 1000);
|
||||||
|
|
||||||
push(n: ToastNotification) {
|
push(n: ToastNotification) {
|
||||||
n.expire ??= unixNow() + 3;
|
n.expire ??= unixNow() + 10;
|
||||||
n.id ??= uuid();
|
n.id ??= uuid();
|
||||||
this.#stack.push(n);
|
this.#stack.push(n);
|
||||||
this.notifyChange();
|
this.notifyChange();
|
||||||
@ -46,8 +46,8 @@ export default function Toaster() {
|
|||||||
return createPortal(
|
return createPortal(
|
||||||
<div className="toaster">
|
<div className="toaster">
|
||||||
{toast.map(a => (
|
{toast.map(a => (
|
||||||
<div className="card flex" key={a.id}>
|
<div className="p br b flex bg-dark g8 fade-in" key={a.id}>
|
||||||
<Icon name={a.icon ?? "bell"} className="mr5" />
|
{a.icon && <Icon name={a.icon} />}
|
||||||
{a.element}
|
{a.element}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -785,7 +785,7 @@ button.tall {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
animation-name: fadeInOpacity;
|
animation-name: fadeInOpacity;
|
||||||
animation-timing-function: ease-in;
|
animation-timing-function: ease-in;
|
||||||
animation-duration: 1s;
|
animation-duration: 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeInOpacity {
|
@keyframes fadeInOpacity {
|
||||||
|
@ -23,6 +23,7 @@ export interface OkResponse {
|
|||||||
id: string;
|
id: string;
|
||||||
relay: string;
|
relay: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
event: NostrEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -284,6 +285,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|||||||
id: e.id,
|
id: e.id,
|
||||||
relay: this.Address,
|
relay: this.Address,
|
||||||
message: "Duplicate request",
|
message: "Duplicate request",
|
||||||
|
event: e,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -294,6 +296,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|||||||
id: e.id,
|
id: e.id,
|
||||||
relay: this.Address,
|
relay: this.Address,
|
||||||
message: "Timeout waiting for OK response",
|
message: "Timeout waiting for OK response",
|
||||||
|
event: e,
|
||||||
});
|
});
|
||||||
}, timeout);
|
}, timeout);
|
||||||
|
|
||||||
@ -305,6 +308,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|||||||
id: id as string,
|
id: id as string,
|
||||||
relay: this.Address,
|
relay: this.Address,
|
||||||
message: message as string | undefined,
|
message: message as string | undefined,
|
||||||
|
event: e,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user