mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
### What changed? Adds optimistic rendering for newly locally posted messages in channels and threads. - Messages appear immediately - Relay echoes and history are deduplicated by event ID - Rejected or timed-out publishes erase the optimistic rendering. The implementation covers reconnect and hydration races, channel-window and legacy WebSocket paths, thread-local overlays, and rapid concurrent sends. ### Why? This is a valuable partial mitigation for [BOT-1449](https://linear.app/squareup/issue/BOT-1449/buzz-mobile-posted-messages-dont-appear-until-leavingre-entering-the): senders no longer depend on receiving a relay echo before seeing their own post. It does not address the likely primary cause of stale channels. Mobile currently does not recover live subscriptions after a rate-limited relay `CLOSED`; that recovery is being handled separately. ### How is it tested? Full mobile suite: 676 passed, 1 skipped. Added regression coverage for optimistic insertion, authoritative deduplication, rollback, reconnect and hydration, thread replies, rapid and equal-time sends, never-echoed successful sends, and legacy WebSocket retirement. --------- Signed-off-by: npub102wg7q285p64ch2fjvstmf2ntn2sz3c4u5hmwatalc76mhsuauysftjtfj <7a9c8f0147a0755c5d499320bda5535cd5014715e52fb7757dfe3dadde1cef09@buzz.block.builderlab.xyz> Co-authored-by: npub102wg7q285p64ch2fjvstmf2ntn2sz3c4u5hmwatalc76mhsuauysftjtfj <7a9c8f0147a0755c5d499320bda5535cd5014715e52fb7757dfe3dadde1cef09@buzz.block.builderlab.xyz>
60 lines
1.7 KiB
Dart
60 lines
1.7 KiB
Dart
import 'package:nostr/nostr.dart' as nostr;
|
|
|
|
import 'nostr_models.dart';
|
|
import 'relay_session.dart';
|
|
|
|
/// Signs and submits Nostr events through the relay WebSocket connection.
|
|
class SignedEventRelay {
|
|
final RelaySessionNotifier _session;
|
|
final String? _nsec;
|
|
|
|
SignedEventRelay({
|
|
required RelaySessionNotifier session,
|
|
required String? nsec,
|
|
}) : _session = session,
|
|
_nsec = nsec;
|
|
|
|
/// The hex pubkey derived from the signing key, or null if no key.
|
|
String? get pubkey {
|
|
final nsec = _nsec;
|
|
if (nsec == null || nsec.isEmpty) return null;
|
|
final privkeyHex = nostr.Nip19.decode(payload: nsec).data;
|
|
if (privkeyHex.isEmpty) return null;
|
|
return nostr.Keys(privkeyHex).public;
|
|
}
|
|
|
|
/// Sign and submit an event. Returns the relay's OK response as a [NostrEvent]
|
|
/// whose `content` field contains the OK message (e.g. `"response:{...}"`
|
|
/// for command kinds).
|
|
Future<NostrEvent> submit({
|
|
required int kind,
|
|
required String content,
|
|
required List<List<String>> tags,
|
|
int? createdAt,
|
|
void Function(NostrEvent event)? onSigned,
|
|
}) async {
|
|
final nsec = _nsec;
|
|
if (nsec == null || nsec.isEmpty) {
|
|
throw Exception('Cannot submit event: no signing key available');
|
|
}
|
|
|
|
final privkeyHex = nostr.Nip19.decode(payload: nsec).data;
|
|
if (privkeyHex.isEmpty) {
|
|
throw Exception('Invalid nsec');
|
|
}
|
|
|
|
final event = nostr.Event.from(
|
|
kind: kind,
|
|
content: content,
|
|
tags: tags,
|
|
secretKey: privkeyHex,
|
|
createdAt: createdAt,
|
|
verify: false,
|
|
);
|
|
|
|
final nostrEvent = NostrEvent.fromJson(event.toMap());
|
|
onSigned?.call(nostrEvent);
|
|
return _session.publish(nostrEvent);
|
|
}
|
|
}
|