aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/features/messages.js30
-rw-r--r--src/nostr.js19
-rw-r--r--src/style.css2
3 files changed, 30 insertions, 21 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 20b2b03..6a73ad0 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -394,8 +394,9 @@ export function messagesFeature(ctx) {
const id = await identity();
if (!id) { noIdToast(); return; }
// The rumor id is a plain hash, so the message can be on screen before
- // any signing, encryption or network runs. The pending flag renders as a
- // dimmed bubble; a relay accepting the wrap brings it to full strength.
+ // any signing, encryption or network runs. The pending flag never shows —
+ // it only keeps unconfirmed messages out of the cache and marks what to
+ // withdraw if signing fails.
const { created_at, ms } = msTags(Date.now());
const rumor = rumorWithId({
kind: 9, pubkey: id.pubkey, content: text,
@@ -411,9 +412,8 @@ export function messagesFeature(ctx) {
const wrap = await wrapRumor(rumor, id.signer, room.chStream(chId));
seenWraps.add(wrap.id); // our own echo has nothing to add
const ok = await publishOn(room.relays, wrap);
- if (!ok) { toast(t('msgSendFailed')); return; } // stays dimmed
+ if (!ok) { toast(t('msgSendFailed')); return; }
delete entry.pending;
- render();
ensureJoined(room, id).catch(() => {});
persistCache(room);
} catch (e) {
@@ -1012,9 +1012,8 @@ export function messagesFeature(ctx) {
const id = await identity();
if (!id) { noIdToast(); return; }
if (!(id.signer instanceof Uint8Array) && !id.signer.encryptTo) { toast(t('msgSignerNoDm')); return; }
- // Same optimistic shape as channel sends: the rumor is synchronous, the
- // bubble shows dimmed at once, and undims when a relay takes the
- // recipient's wrap. Wrapping the same rumor keeps the id, so the sent-copy
+ // Same optimistic shape as channel sends: the rumor is synchronous and on
+ // screen at once. Wrapping the same rumor keeps the id, so the sent-copy
// echo folds into this entry instead of duplicating it.
const rumor = makeDMRumor(id.pubkey, peer, text);
const entry = { rumor, mine: true, pending: true };
@@ -1026,12 +1025,17 @@ export function messagesFeature(ctx) {
// Sequential on purpose — a remote signer is happier signing one at a time.
const toPeer = await wrapDM(id.signer, peer, rumor);
const toSelf = await wrapDM(id.signer, id.pubkey, rumor);
- const inbox = (await fetchInboxRelays(peer)).slice(0, 4);
- const ok = await publishOn([...new Set([...inbox, ...DM_RELAYS])], toPeer);
+ // Our own relays take the wrap immediately; the peer's declared inbox
+ // (a lookup that can take seconds) is chased off the critical path.
+ // Publishing the same wrap twice is harmless — relays dedupe on id.
+ const ok = await publishOn(DM_RELAYS, toPeer);
publishOn(DM_RELAYS, toSelf);
- if (!ok) { toast(t('msgSendFailed')); return; } // stays dimmed
+ fetchInboxRelays(peer).then((inbox) => {
+ const extra = inbox.slice(0, 4).filter((r) => !DM_RELAYS.includes(r));
+ if (extra.length) publishOn(extra, toPeer);
+ }).catch(() => {});
+ if (!ok) { toast(t('msgSendFailed')); return; }
delete entry.pending;
- render();
persistDms();
} catch (e) {
threadOf(peer).delete(rumor.id);
@@ -2077,7 +2081,7 @@ export function messagesFeature(ctx) {
const counts = new Map();
if (reacts) for (const emoji of reacts.values()) counts.set(emoji, (counts.get(emoji) || 0) + 1);
return h(
- 'div', { class: 'chat-row' + (mine ? ' mine' : '') + (grouped ? ' grouped' : '') + (m.pending ? ' pending' : '') },
+ 'div', { class: 'chat-row' + (mine ? ' mine' : '') + (grouped ? ' grouped' : '') },
grouped ? h('div', { class: 'chat-avatar spacer' }) : avatar(m.author),
h('div', { class: 'chat-body' },
grouped ? null : h('div', { class: 'chat-meta' },
@@ -2273,7 +2277,7 @@ export function messagesFeature(ctx) {
},
...(msgs.length
? msgs.map((m) =>
- h('div', { class: 'chat-row dm' + (m.mine ? ' mine' : '') + (m.pending ? ' pending' : '') },
+ h('div', { class: 'chat-row dm' + (m.mine ? ' mine' : '') },
h('div', { class: 'chat-body' },
h('div', { class: 'chat-bubble' + (m.mine ? ' me' : '') }, m.rumor.content),
h('div', { class: 'chat-time' }, timeLabel(m.rumor.created_at * 1000)))))
diff --git a/src/nostr.js b/src/nostr.js
index 2166880..753d4a8 100644
--- a/src/nostr.js
+++ b/src/nostr.js
@@ -83,20 +83,27 @@ export async function queryOn(relays, filter, maxWait = 1500) {
export async function publishOn(relays, evt) {
try {
const live = liveRelays(relays);
- const results = await Promise.allSettled(pool.publish(live, evt));
// A rejected publish is a relay refusing the event (policy, rate limit,
// ban). Swallowing that silently once hid a relay-side rejection behind
// what looked like a client bug — always say which relay refused and why.
- results.forEach((r, i) => {
- if (r.status === 'rejected') {
- const msg = r.reason?.message || String(r.reason || '');
+ const pubs = pool.publish(live, evt).map((p, i) =>
+ Promise.resolve(p).then(() => true, (reason) => {
+ const msg = reason?.message || String(reason || '');
// "relay connection errored/closed" means the socket never came up —
// that's a sick relay, not a rejected event.
if (/connect|closed|errored|timeout|network/i.test(msg)) markRelaySick(live[i], msg);
else console.warn(`nostr: publish kind ${evt.kind} refused by ${live[i]}:`, msg);
- }
+ return false;
+ }));
+ // The first acceptance IS delivery — one durable copy is enough, and
+ // waiting on allSettled meant every send moved at the pace of the
+ // slowest relay in the set. The stragglers keep settling in the
+ // background so the sick-list still learns from them.
+ return await new Promise((resolve) => {
+ let left = pubs.length;
+ if (!left) return resolve(false);
+ for (const p of pubs) p.then((ok) => { if (ok) resolve(true); else if (!--left) resolve(false); });
});
- return results.some((r) => r.status === 'fulfilled');
} catch { return false; }
}
diff --git a/src/style.css b/src/style.css
index 1b059f3..2433d82 100644
--- a/src/style.css
+++ b/src/style.css
@@ -386,8 +386,6 @@ hr.divider { margin: 16px 0; }
overflow-wrap: break-word; white-space: pre-wrap;
}
.chat-edited { font-size: 11px; color: var(--faint); }
-/* Optimistic sends: dimmed while in flight, full strength once a relay has it. */
-.chat-row.pending .chat-bubble { opacity: 0.55; }
.chat-del {
position: absolute; top: -7px; right: -7px; width: 18px; height: 18px;
display: none; align-items: center; justify-content: center;