mirror of
https://github.com/block/buzz.git
synced 2026-11-10 11:21:05 +00:00
## Summary Solo compose now opens before agent discovery finishes, and chat creation avoids full channel-list reloads while overlapping independent reads. Durable retries, ownership checks, and relay confirmation remain intact. The agent picker uses the owner's saved inventory, excludes known archived identities, verifies profiles off the UI thread, and reuses avatar artwork. ### Related issue No matching issue found. Related: #8546 adds project selection to the same compose surface. ### Testing - Full mobile suite: 3,755 passed, 19 skipped; four additional artifact tests passed. - Analysis, formatting, file-size checks, and iOS Release build passed. - Regression tests fail when the old blocking paths are restored. - Installed on a physical iPhone with existing profile/community settings preserved. Owner tested the earlier creation/picker improvements; final compose timing awaits device feedback. No timing recording captured. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
133 lines
4.3 KiB
Dart
133 lines
4.3 KiB
Dart
import 'dart:convert';
|
||
|
||
import '../crypto/nip44.dart';
|
||
import 'me_sync.dart';
|
||
import 'me_journal.dart';
|
||
|
||
/// Durable, encrypted intent reused after a failed or interrupted chat creation.
|
||
class MeChatCreation {
|
||
/// Stable channel UUID, reused for every retry.
|
||
final String id;
|
||
|
||
/// User-supplied name frozen before the first relay mutation.
|
||
final String name;
|
||
|
||
/// Selected agent public key.
|
||
final String agent;
|
||
|
||
/// Optional Solo project, frozen with the rest of the creation intent.
|
||
final String? project;
|
||
|
||
/// Create a new intent; persistence happens before [complete] performs writes.
|
||
const MeChatCreation(this.id, this.name, this.agent, {this.project});
|
||
|
||
/// Account and community boundary for durable creation storage.
|
||
static String storageKey(MeSync sync) =>
|
||
'me-chat-creation:${sync.community}:${sync.pubkey}';
|
||
|
||
/// Restore an unfinished intent without performing relay writes.
|
||
static MeChatCreation? pending(MeSync sync) {
|
||
final saved = sync.prefs?.getString(storageKey(sync));
|
||
if (saved == null) return null;
|
||
final value =
|
||
jsonDecode(nip44Decrypt(sync.key, saved)) as Map<String, dynamic>;
|
||
return MeChatCreation(
|
||
value['id'] as String,
|
||
value['name'] as String,
|
||
value['agent'] as String,
|
||
project: value['project'] as String?,
|
||
);
|
||
}
|
||
|
||
/// Persist before any relay mutation. Do not replace another pending intent.
|
||
Future<void> save(MeSync sync) async {
|
||
if (sync.disposed) throw StateError('Your community changed. Try again.');
|
||
final previous = pending(sync);
|
||
if (previous != null &&
|
||
(previous.id != id ||
|
||
previous.name != name ||
|
||
previous.agent != agent ||
|
||
previous.project != project)) {
|
||
throw StateError('Resume the pending chat before starting another.');
|
||
}
|
||
// A matching cached value does not prove a previous disk write succeeded.
|
||
if (sync.prefs == null ||
|
||
!await sync.prefs!.setString(
|
||
storageKey(sync),
|
||
nip44Encrypt(
|
||
sync.key,
|
||
jsonEncode({
|
||
'id': id,
|
||
'name': name,
|
||
'agent': agent,
|
||
'project': project,
|
||
}),
|
||
),
|
||
)) {
|
||
throw StateError('Couldn’t save this chat on your phone. Try again.');
|
||
}
|
||
if (sync.disposed) throw StateError('Your community changed. Try again.');
|
||
}
|
||
|
||
/// Abandon only this reviewed intent, preserving any relay channel/messages.
|
||
/// Partial channels remain accessible from Home (or Solo if already placed).
|
||
Future<void> discard(MeSync sync) async {
|
||
if (sync.disposed) throw StateError('Your community changed. Try again.');
|
||
final current = pending(sync);
|
||
if (current?.id != id ||
|
||
current?.name != name ||
|
||
current?.agent != agent ||
|
||
current?.project != project) {
|
||
throw StateError('The unfinished chat changed. Review it again.');
|
||
}
|
||
await clearMeJournal(
|
||
sync.prefs!,
|
||
storageKey(sync),
|
||
sync.prefs!.getString(storageKey(sync))!,
|
||
checkScope: () {
|
||
if (sync.disposed) {
|
||
throw StateError('Your community changed. Try again.');
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
/// Keep the journal through every relay step, including Me placement.
|
||
Future<T> complete<T>(
|
||
MeSync sync, {
|
||
required Future<T?> Function() findChannel,
|
||
required Future<T> Function() createChannel,
|
||
required Future<bool> Function() hasAgent,
|
||
required Future<void> Function() addAgent,
|
||
required Future<bool> Function() isPlaced,
|
||
required Future<void> Function() place,
|
||
}) async {
|
||
await save(sync);
|
||
final record = sync.prefs!.getString(storageKey(sync))!;
|
||
sync.check();
|
||
final existing = await findChannel();
|
||
sync.check();
|
||
final result = existing ?? await createChannel();
|
||
sync.check();
|
||
final status = await Future.wait([hasAgent(), isPlaced()]);
|
||
final member = status[0];
|
||
sync.check();
|
||
if (!member) await addAgent();
|
||
sync.check();
|
||
final placed = status[1];
|
||
sync.check();
|
||
// Placement may be confirmed while its inner journal cleanup failed.
|
||
if (!placed || sync.prefs?.getString(sync.pendingKey) != null) {
|
||
await place();
|
||
}
|
||
sync.check();
|
||
await clearMeJournal(
|
||
sync.prefs!,
|
||
storageKey(sync),
|
||
record,
|
||
checkScope: sync.check,
|
||
);
|
||
return result;
|
||
}
|
||
}
|