Files
buzz/mobile/lib/shared/me/me_chat_creation.dart
klopez4212 b14884980e Speed up mobile Solo chat creation and agent discovery (#8547)
## 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>
2026-10-27 15:09:22 +00:00

133 lines
4.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}