feat: note publishing progress
This commit is contained in:
@ -1,14 +1,24 @@
|
||||
button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spinner-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.spinner-button > span {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.light .spinner-button {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--font-secondary);
|
||||
box-shadow: rgba(0, 0, 0, 0.08) 0 1px 1px;
|
||||
}
|
||||
|
||||
.light .spinner-button:hover {
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0 1px 3px;
|
||||
}
|
||||
|
@ -30,10 +30,10 @@ const AsyncButton = React.forwardRef<HTMLButtonElement, AsyncButtonProps>((props
|
||||
return (
|
||||
<button
|
||||
ref={ref as ForwardedRef<HTMLButtonElement>}
|
||||
className="spinner-button"
|
||||
type="button"
|
||||
disabled={loading || props.disabled}
|
||||
{...props}
|
||||
className={`spinner-button${props.className ? ` ${props.className}` : ""}`}
|
||||
onClick={handle}>
|
||||
<span style={{ visibility: loading ? "hidden" : "visible" }}>{props.children}</span>
|
||||
{loading && (
|
||||
|
85
packages/app/src/Element/Event/NoteBroadcaster.tsx
Normal file
85
packages/app/src/Element/Event/NoteBroadcaster.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { removeUndefined } from "@snort/shared";
|
||||
import { NostrEvent, OkResponse } from "@snort/system";
|
||||
|
||||
import AsyncButton from "Element/AsyncButton";
|
||||
import Icon from "Icons/Icon";
|
||||
import { getRelayName } from "SnortUtils";
|
||||
import { System } from "index";
|
||||
|
||||
export function NoteBroadcaster({
|
||||
evs,
|
||||
onClose,
|
||||
customRelays,
|
||||
}: {
|
||||
evs: Array<NostrEvent>;
|
||||
onClose: () => void;
|
||||
customRelays?: Array<string>;
|
||||
}) {
|
||||
const [results, setResults] = useState<Array<OkResponse>>([]);
|
||||
|
||||
async function sendEventToRelays(ev: NostrEvent) {
|
||||
if (customRelays) {
|
||||
return removeUndefined(
|
||||
await Promise.all(
|
||||
customRelays.map(async r => {
|
||||
try {
|
||||
return await System.WriteOnceToRelay(r, ev);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return await System.BroadcastEvent(ev, r => setResults(x => [...x, r]));
|
||||
}
|
||||
}
|
||||
|
||||
async function sendNote() {
|
||||
const results = await Promise.all(evs.map(a => sendEventToRelays(a)).flat());
|
||||
if (results.flat().every(a => a.ok)) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
sendNote().catch(console.error);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex-column g4">
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Sending notes and other stuff" />
|
||||
</h3>
|
||||
{results
|
||||
.filter(a => a.message !== "Duplicate request")
|
||||
.sort(a => (a.ok ? -1 : 1))
|
||||
.map(r => (
|
||||
<div className="p-compact flex-row g16">
|
||||
<Icon name={r.ok ? "check" : "x-close"} className={r.ok ? "success" : "error"} />
|
||||
<div className="flex-column f-grow g4">
|
||||
<b>{getRelayName(r.relay)}</b>
|
||||
{r.message && <small>{r.message}</small>}
|
||||
</div>
|
||||
{!r.ok && false && (
|
||||
<div className="flex g8">
|
||||
<AsyncButton onClick={() => {}} className="p4 br-compact flex f-center secondary">
|
||||
<Icon name="refresh-ccw-01" />
|
||||
</AsyncButton>
|
||||
<AsyncButton onClick={() => {}} className="p4 br-compact flex f-center secondary">
|
||||
<Icon name="trash-01" className="trash-icon" />
|
||||
</AsyncButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="flex-row g8">
|
||||
<button type="button" onClick={() => onClose()}>
|
||||
<FormattedMessage defaultMessage="Close" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,14 +1,6 @@
|
||||
import "./NoteCreator.css";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import {
|
||||
EventKind,
|
||||
NostrPrefix,
|
||||
TaggedNostrEvent,
|
||||
EventBuilder,
|
||||
tryParseNostrLink,
|
||||
NostrLink,
|
||||
NostrEvent,
|
||||
} from "@snort/system";
|
||||
import { EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder, tryParseNostrLink, NostrLink } from "@snort/system";
|
||||
|
||||
import Icon from "Icons/Icon";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
@ -21,12 +13,13 @@ import Note from "Element/Event/Note";
|
||||
|
||||
import { ClipboardEventHandler } from "react";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { System, WasmPowWorker } from "index";
|
||||
import { WasmPowWorker } from "index";
|
||||
import AsyncButton from "Element/AsyncButton";
|
||||
import { AsyncIcon } from "Element/AsyncIcon";
|
||||
import { fetchNip05Pubkey } from "@snort/shared";
|
||||
import { ZapTarget } from "Zapper";
|
||||
import { useNoteCreator } from "State/NoteCreator";
|
||||
import { NoteBroadcaster } from "./NoteBroadcaster";
|
||||
|
||||
export function NoteCreator() {
|
||||
const { formatMessage } = useIntl();
|
||||
@ -123,24 +116,11 @@ export function NoteCreator() {
|
||||
}
|
||||
}
|
||||
|
||||
async function sendEventToRelays(ev: NostrEvent) {
|
||||
if (note.selectedCustomRelays) {
|
||||
await Promise.all(note.selectedCustomRelays.map(r => System.WriteOnceToRelay(r, ev)));
|
||||
} else {
|
||||
System.BroadcastEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendNote() {
|
||||
const ev = await buildNote();
|
||||
if (ev) {
|
||||
await sendEventToRelays(ev);
|
||||
for (const oe of note.otherEvents ?? []) {
|
||||
await sendEventToRelays(oe);
|
||||
}
|
||||
note.update(v => {
|
||||
v.reset();
|
||||
v.show = false;
|
||||
note.update(n => {
|
||||
n.sending = (note.otherEvents ?? []).concat(ev);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -327,60 +307,109 @@ export function NoteCreator() {
|
||||
));
|
||||
}*/
|
||||
|
||||
const handlePaste: ClipboardEventHandler<HTMLDivElement> = evt => {
|
||||
if (evt.clipboardData) {
|
||||
const clipboardItems = evt.clipboardData.items;
|
||||
const items: DataTransferItem[] = Array.from(clipboardItems).filter(function (item: DataTransferItem) {
|
||||
// Filter the image items only
|
||||
return /^image\//.test(item.type);
|
||||
});
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = items[0];
|
||||
const blob = item.getAsFile();
|
||||
if (blob) {
|
||||
uploadFile(blob);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!note.show) return null;
|
||||
return (
|
||||
<Modal id="note-creator" className="note-creator-modal" onClose={() => note.update(v => (v.show = false))}>
|
||||
{note.replyTo && (
|
||||
<Note
|
||||
data={note.replyTo}
|
||||
related={[]}
|
||||
options={{
|
||||
showFooter: false,
|
||||
showContextMenu: false,
|
||||
showTime: false,
|
||||
canClick: false,
|
||||
showMedia: false,
|
||||
longFormPreview: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{note.preview && getPreviewNote()}
|
||||
{!note.preview && (
|
||||
<div onPaste={handlePaste} className={`note-creator${note.pollOptions ? " poll" : ""}`}>
|
||||
<Textarea
|
||||
autoFocus
|
||||
className={`textarea ${note.active ? "textarea--focused" : ""}`}
|
||||
onChange={c => onChange(c)}
|
||||
value={note.note}
|
||||
onFocus={() => note.update(v => (v.active = true))}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter" && e.metaKey) {
|
||||
sendNote().catch(console.warn);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{renderPollOptions()}
|
||||
function noteCreatorAdvanced() {
|
||||
return (
|
||||
<>
|
||||
<button className="secondary" onClick={loadPreview}>
|
||||
<FormattedMessage defaultMessage="Toggle Preview" />
|
||||
</button>
|
||||
<div>
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Custom Relays" />
|
||||
</h4>
|
||||
<p>
|
||||
<FormattedMessage defaultMessage="Send note to a subset of your write relays" />
|
||||
</p>
|
||||
{renderRelayCustomisation()}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-column g8">
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Zap Splits" />
|
||||
</h4>
|
||||
<FormattedMessage defaultMessage="Zaps on this note will be split to the following users." />
|
||||
<div className="flex-column g8">
|
||||
{[...(note.zapSplits ?? [])].map((v, i, arr) => (
|
||||
<div className="flex f-center g8">
|
||||
<div className="flex-column f-4 g4">
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Recipient" />
|
||||
</h4>
|
||||
<input
|
||||
type="text"
|
||||
value={v.value}
|
||||
onChange={e =>
|
||||
note.update(
|
||||
v => (v.zapSplits = arr.map((vv, ii) => (ii === i ? { ...vv, value: e.target.value } : vv))),
|
||||
)
|
||||
}
|
||||
placeholder={formatMessage({ defaultMessage: "npub / nprofile / nostr address" })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-column f-1 g4">
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Weight" />
|
||||
</h4>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
value={v.weight}
|
||||
onChange={e =>
|
||||
note.update(
|
||||
v =>
|
||||
(v.zapSplits = arr.map((vv, ii) =>
|
||||
ii === i ? { ...vv, weight: Number(e.target.value) } : vv,
|
||||
)),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-column f-shrink g4">
|
||||
<div> </div>
|
||||
<Icon
|
||||
name="close"
|
||||
onClick={() => note.update(v => (v.zapSplits = (v.zapSplits ?? []).filter((_v, ii) => ii !== i)))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
note.update(v => (v.zapSplits = [...(v.zapSplits ?? []), { type: "pubkey", value: "", weight: 1 }]))
|
||||
}>
|
||||
<FormattedMessage defaultMessage="Add" />
|
||||
</button>
|
||||
</div>
|
||||
<span className="warning">
|
||||
<FormattedMessage defaultMessage="Not all clients support this, you may still receive some zaps as if zap splits was not configured" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-column g8">
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Sensitive Content" />
|
||||
</h4>
|
||||
<FormattedMessage defaultMessage="Users must accept the content warning to show the content of your note." />
|
||||
<input
|
||||
className="w-max"
|
||||
type="text"
|
||||
value={note.sensitive}
|
||||
onChange={e => note.update(v => (v.sensitive = e.target.value))}
|
||||
maxLength={50}
|
||||
minLength={1}
|
||||
placeholder={formatMessage({
|
||||
defaultMessage: "Reason",
|
||||
})}
|
||||
/>
|
||||
<span className="warning">
|
||||
<FormattedMessage defaultMessage="Not all clients support this yet" />
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function noteCreatorFooter() {
|
||||
return (
|
||||
<div className="flex f-space">
|
||||
<div className="flex g8">
|
||||
<ProfileImage
|
||||
@ -409,105 +438,86 @@ export function NoteCreator() {
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</div>
|
||||
{note.error && <span className="error">{note.error}</span>}
|
||||
{note.advanced && (
|
||||
<>
|
||||
<button className="secondary" onClick={loadPreview}>
|
||||
<FormattedMessage defaultMessage="Toggle Preview" />
|
||||
</button>
|
||||
<div>
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Custom Relays" />
|
||||
</h4>
|
||||
<p>
|
||||
<FormattedMessage defaultMessage="Send note to a subset of your write relays" />
|
||||
</p>
|
||||
{renderRelayCustomisation()}
|
||||
</div>
|
||||
<div className="flex-column g8">
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Zap Splits" />
|
||||
</h4>
|
||||
<FormattedMessage defaultMessage="Zaps on this note will be split to the following users." />
|
||||
<div className="flex-column g8">
|
||||
{[...(note.zapSplits ?? [])].map((v, i, arr) => (
|
||||
<div className="flex f-center g8">
|
||||
<div className="flex-column f-4 g4">
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Recipient" />
|
||||
</h4>
|
||||
<input
|
||||
type="text"
|
||||
value={v.value}
|
||||
onChange={e =>
|
||||
note.update(
|
||||
v => (v.zapSplits = arr.map((vv, ii) => (ii === i ? { ...vv, value: e.target.value } : vv))),
|
||||
)
|
||||
}
|
||||
placeholder={formatMessage({ defaultMessage: "npub / nprofile / nostr address" })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-column f-1 g4">
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Weight" />
|
||||
</h4>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
value={v.weight}
|
||||
onChange={e =>
|
||||
note.update(
|
||||
v =>
|
||||
(v.zapSplits = arr.map((vv, ii) =>
|
||||
ii === i ? { ...vv, weight: Number(e.target.value) } : vv,
|
||||
)),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-column f-shrink g4">
|
||||
<div> </div>
|
||||
<Icon
|
||||
name="close"
|
||||
onClick={() => note.update(v => (v.zapSplits = (v.zapSplits ?? []).filter((_v, ii) => ii !== i)))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
note.update(v => (v.zapSplits = [...(v.zapSplits ?? []), { type: "pubkey", value: "", weight: 1 }]))
|
||||
}>
|
||||
<FormattedMessage defaultMessage="Add" />
|
||||
</button>
|
||||
</div>
|
||||
<span className="warning">
|
||||
<FormattedMessage defaultMessage="Not all clients support this, you may still receive some zaps as if zap splits was not configured" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-column g8">
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Sensitive Content" />
|
||||
</h4>
|
||||
<FormattedMessage defaultMessage="Users must accept the content warning to show the content of your note." />
|
||||
<input
|
||||
className="w-max"
|
||||
type="text"
|
||||
value={note.sensitive}
|
||||
onChange={e => note.update(v => (v.sensitive = e.target.value))}
|
||||
maxLength={50}
|
||||
minLength={1}
|
||||
placeholder={formatMessage({
|
||||
defaultMessage: "Reason",
|
||||
})}
|
||||
);
|
||||
}
|
||||
|
||||
const handlePaste: ClipboardEventHandler<HTMLDivElement> = evt => {
|
||||
if (evt.clipboardData) {
|
||||
const clipboardItems = evt.clipboardData.items;
|
||||
const items: DataTransferItem[] = Array.from(clipboardItems).filter(function (item: DataTransferItem) {
|
||||
// Filter the image items only
|
||||
return /^image\//.test(item.type);
|
||||
});
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = items[0];
|
||||
const blob = item.getAsFile();
|
||||
if (blob) {
|
||||
uploadFile(blob);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function noteCreatorForm() {
|
||||
return (
|
||||
<>
|
||||
{note.replyTo && (
|
||||
<Note
|
||||
data={note.replyTo}
|
||||
related={[]}
|
||||
options={{
|
||||
showFooter: false,
|
||||
showContextMenu: false,
|
||||
showTime: false,
|
||||
canClick: false,
|
||||
showMedia: false,
|
||||
longFormPreview: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{note.preview && getPreviewNote()}
|
||||
{!note.preview && (
|
||||
<div onPaste={handlePaste} className={`note-creator${note.pollOptions ? " poll" : ""}`}>
|
||||
<Textarea
|
||||
autoFocus
|
||||
className={`textarea ${note.active ? "textarea--focused" : ""}`}
|
||||
onChange={c => onChange(c)}
|
||||
value={note.note}
|
||||
onFocus={() => note.update(v => (v.active = true))}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter" && e.metaKey) {
|
||||
sendNote().catch(console.warn);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="warning">
|
||||
<FormattedMessage defaultMessage="Not all clients support this yet" />
|
||||
</span>
|
||||
{renderPollOptions()}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{noteCreatorFooter()}
|
||||
{note.error && <span className="error">{note.error}</span>}
|
||||
{note.advanced && noteCreatorAdvanced()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!note.show) return null;
|
||||
return (
|
||||
<Modal id="note-creator" className="note-creator-modal" onClose={() => note.update(v => (v.show = false))}>
|
||||
{note.sending && (
|
||||
<NoteBroadcaster
|
||||
evs={note.sending}
|
||||
onClose={() => {
|
||||
note.update(n => {
|
||||
n.reset();
|
||||
n.show = false;
|
||||
});
|
||||
}}
|
||||
customRelays={note.selectedCustomRelays}
|
||||
/>
|
||||
)}
|
||||
{!note.sending && noteCreatorForm()}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { TaggedNostrEvent, EventKind, MetadataCache } from "@snort/system";
|
||||
import { getDisplayName } from "Element/User/DisplayName";
|
||||
import { MentionRegex } from "Const";
|
||||
import { defaultAvatar, tagFilterOfTextRepost, unwrap } from "SnortUtils";
|
||||
import { defaultAvatar, tagFilterOfTextRepost } from "SnortUtils";
|
||||
import { UserCache } from "Cache";
|
||||
import { LoginSession } from "Login";
|
||||
import { removeUndefined } from "@snort/shared";
|
||||
|
||||
export interface NotificationRequest {
|
||||
title: string;
|
||||
@ -20,10 +21,7 @@ export async function makeNotification(ev: TaggedNostrEvent): Promise<Notificati
|
||||
}
|
||||
const pubkeys = new Set([ev.pubkey, ...ev.tags.filter(a => a[0] === "p").map(a => a[1])]);
|
||||
await UserCache.buffer([...pubkeys]);
|
||||
const allUsers = [...pubkeys]
|
||||
.map(a => UserCache.getFromCache(a))
|
||||
.filter(a => a)
|
||||
.map(a => unwrap(a));
|
||||
const allUsers = removeUndefined([...pubkeys].map(a => UserCache.getFromCache(a)));
|
||||
const fromUser = UserCache.getFromCache(ev.pubkey);
|
||||
const name = getDisplayName(fromUser, ev.pubkey);
|
||||
const avatarUrl = fromUser?.picture || defaultAvatar(ev.pubkey);
|
||||
|
@ -131,7 +131,7 @@ export default function ProfilePage({ id: propId }: ProfilePageProps) {
|
||||
return (
|
||||
<div className="flex g8">
|
||||
{cover && <ProxyImg src={cover} size={40} />}
|
||||
<small>🎵 {unwrap(status.music).content}</small>
|
||||
🎵 {unwrap(status.music).content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,3 @@
|
||||
.preferences small {
|
||||
color: var(--font-secondary-color);
|
||||
}
|
||||
|
||||
.preferences select {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
@ -57,8 +57,3 @@
|
||||
.settings .actions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.settings small {
|
||||
font-size: 14px;
|
||||
color: var(--font-secondary-color);
|
||||
}
|
||||
|
@ -17,6 +17,8 @@ interface NoteCreatorDataSnapshot {
|
||||
sensitive?: string;
|
||||
pollOptions?: Array<string>;
|
||||
otherEvents?: Array<NostrEvent>;
|
||||
sending?: Array<NostrEvent>;
|
||||
sendStarted: boolean;
|
||||
reset: () => void;
|
||||
update: (fn: (v: NoteCreatorDataSnapshot) => void) => void;
|
||||
}
|
||||
@ -32,6 +34,7 @@ class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
|
||||
error: "",
|
||||
active: false,
|
||||
advanced: false,
|
||||
sendStarted: false,
|
||||
reset: () => {
|
||||
this.#reset(this.#data);
|
||||
this.notifyChange(this.#data);
|
||||
@ -49,6 +52,7 @@ class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
|
||||
d.error = "";
|
||||
d.active = false;
|
||||
d.advanced = false;
|
||||
d.sendStarted = false;
|
||||
d.preview = undefined;
|
||||
d.replyTo = undefined;
|
||||
d.selectedCustomRelays = undefined;
|
||||
@ -56,6 +60,7 @@ class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
|
||||
d.sensitive = undefined;
|
||||
d.pollOptions = undefined;
|
||||
d.otherEvents = undefined;
|
||||
d.sending = undefined;
|
||||
}
|
||||
|
||||
takeSnapshot(): NoteCreatorDataSnapshot {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { ExternalStore, FeedCache, dedupe } from "@snort/shared";
|
||||
import { ExternalStore, FeedCache, dedupe, removeUndefined } from "@snort/shared";
|
||||
import { RequestBuilder, NostrEvent, EventKind, SystemInterface, TaggedNostrEvent } from "@snort/system";
|
||||
import { LoginSession } from "Login";
|
||||
import { unwrap } from "SnortUtils";
|
||||
import { Chat, ChatSystem, ChatType, lastReadInChat } from "chat";
|
||||
|
||||
export class Nip29ChatSystem extends ExternalStore<Array<Chat>> implements ChatSystem {
|
||||
@ -45,11 +44,7 @@ export class Nip29ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
|
||||
listChats(): Chat[] {
|
||||
const allMessages = this.#nip29Chats();
|
||||
const groups = dedupe(
|
||||
allMessages
|
||||
.map(a => a.tags.find(b => b[0] === "g"))
|
||||
.filter(a => a !== undefined)
|
||||
.map(a => unwrap(a))
|
||||
.map(a => `${a[2]}${a[1]}`),
|
||||
removeUndefined(allMessages.map(a => a.tags.find(b => b[0] === "g"))).map(a => `${a[2]}${a[1]}`),
|
||||
);
|
||||
return groups.map(g => {
|
||||
const [relay, channel] = g.split("/", 2);
|
||||
|
@ -167,10 +167,18 @@ a.ext {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.br-compact {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.p {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.p-compact {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.p4 {
|
||||
padding: 4px;
|
||||
}
|
||||
@ -212,7 +220,14 @@ a.ext {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--font-secondary-color);
|
||||
font-size: 14px;
|
||||
line-height: 22px; /* 157.143% */
|
||||
}
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 10px 16px;
|
||||
font-weight: 600;
|
||||
@ -408,6 +423,11 @@ input:disabled {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -646,6 +666,18 @@ div.form-col {
|
||||
background-color: var(--repost);
|
||||
}
|
||||
|
||||
.bg-dark {
|
||||
background-color: var(--gray-dark);
|
||||
}
|
||||
|
||||
.bg-superdark {
|
||||
background-color: var(--gray-superdark);
|
||||
}
|
||||
|
||||
.bg-ultradark {
|
||||
background-color: var(--gray-ultradark);
|
||||
}
|
||||
|
||||
.text-zap {
|
||||
color: var(--zap);
|
||||
}
|
||||
@ -865,17 +897,6 @@ svg.zap-solid {
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.light .spinner-button {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--font-secondary);
|
||||
box-shadow: rgba(0, 0, 0, 0.08) 0 1px 1px;
|
||||
}
|
||||
|
||||
.light .spinner-button:hover {
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0 1px 3px;
|
||||
}
|
||||
|
||||
.main-content.p {
|
||||
border-bottom: 0;
|
||||
border-top: 0;
|
||||
|
@ -47,6 +47,7 @@ import { preload, RelayMetrics, SystemDb, UserCache, UserRelays } from "Cache";
|
||||
import { LoginStore } from "Login";
|
||||
import { SnortDeckLayout } from "Pages/DeckLayout";
|
||||
import FreeNostrAddressPage from "./Pages/FreeNostrAddressPage";
|
||||
import { removeUndefined } from "@snort/shared";
|
||||
|
||||
const WasmQueryOptimizer = {
|
||||
expandFilter: (f: ReqFilter) => {
|
||||
@ -107,7 +108,7 @@ async function fetchProfile(key: string) {
|
||||
*/
|
||||
if (CONFIG.httpCache) {
|
||||
System.ProfileLoader.loaderFn = async (keys: Array<string>) => {
|
||||
return (await Promise.all(keys.map(a => fetchProfile(a)))).filter(a => a !== undefined).map(a => unwrap(a));
|
||||
return removeUndefined(await Promise.all(keys.map(a => fetchProfile(a))));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1335,6 +1335,9 @@
|
||||
"rT14Ow": {
|
||||
"defaultMessage": "Add Relays"
|
||||
},
|
||||
"rbrahO": {
|
||||
"defaultMessage": "Close"
|
||||
},
|
||||
"reJ6SM": {
|
||||
"defaultMessage": "It is recommended to use one of the following browser extensions if you are on a desktop computer to secure your key:"
|
||||
},
|
||||
@ -1389,6 +1392,9 @@
|
||||
"uc0din": {
|
||||
"defaultMessage": "Send sats splits to"
|
||||
},
|
||||
"ugyJnE": {
|
||||
"defaultMessage": "Sending notes and other stuff"
|
||||
},
|
||||
"usAvMr": {
|
||||
"defaultMessage": "Edit Profile"
|
||||
},
|
||||
|
@ -437,6 +437,7 @@
|
||||
"r3C4x/": "Software",
|
||||
"r5srDR": "Enter wallet password",
|
||||
"rT14Ow": "Add Relays",
|
||||
"rbrahO": "Close",
|
||||
"reJ6SM": "It is recommended to use one of the following browser extensions if you are on a desktop computer to secure your key:",
|
||||
"rfuMjE": "(Default)",
|
||||
"rmdsT4": "{n} days",
|
||||
@ -455,6 +456,7 @@
|
||||
"u4bHcR": "Check out the code here: {link}",
|
||||
"uSV4Ti": "Reposts need to be manually confirmed",
|
||||
"uc0din": "Send sats splits to",
|
||||
"ugyJnE": "Sending notes and other stuff",
|
||||
"usAvMr": "Edit Profile",
|
||||
"ut+2Cd": "Get a partner identifier",
|
||||
"v8lolG": "Start chat",
|
||||
|
Reference in New Issue
Block a user