Files
buzz/mobile/lib/shared/me/me_project_creation.dart
klopez4212 634f48a8bd Mobile: open Solo chats with Bestie and workspace controls (#8884)
Mobile New chat now opens directly with the synced Bestie selected. The
first send creates and names the chat; leaving an empty draft creates
nothing. New and existing Solo chats can select or clear a workspace
from the header popover, with a checkmark for the active choice and a No
Workspace option. Workspace and agent controls have separate iOS glass
surfaces, Bestie stays at the far right, and Back uses a chevron. The
agent sheet fits its list, with scrolling for long lists. Workspace
creation stays in the Home New menu.

The New menu groups Solo above Team. Android expands across the full
control row; iOS keeps its native menu while Search and Bestie merge
into New through Liquid Glass, with synchronized icon motion and the
residual glass fading away. Empty workspaces use a compact, muted
message.

Validation: 4,055 mobile tests passed (34 skipped), static analysis
passed, and native menu checks passed. Earlier physical-device previews
and the iPhone New-menu animation were visually approved before this
consolidation.

This PR consolidates the controls previously developed in #8891 into the
existing draft/first-send implementation. The New menu animations,
attachment handling, and durable retries remain in this PR. The combined
controls have widget interaction coverage; the combined build has not
yet had a new physical-device walkthrough.

Current combined controls below are Android Flutter widget renders with
fixture data, not device screenshots. Images are GitHub attachments
only.

| New conversation | Workspace popover | Compact agent sheet |
| --- | --- | --- |
| ![New chat
composer](https://github.com/user-attachments/assets/f9ea2a16-342e-4cea-a436-0777ff5e37aa)
| ![Checked workspace
popover](https://github.com/user-attachments/assets/80eb7a71-e424-4188-86d7-eb6292b148bf)
| ![Content-sized agent
sheet](https://github.com/user-attachments/assets/be608383-0188-4339-b56b-924085137452)
|

Empty workspace widget preview (unchanged):

![Compact empty workspace
message](https://github.com/user-attachments/assets/387c0729-dea3-4250-bb54-afc4e61b54a7)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-11-01 21:40:30 +00:00

120 lines
3.8 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_groups.dart';
import 'me_sync.dart';
import 'me_journal.dart';
/// An accepted project name and stable ID, retained until confirmed or discarded.
class MeProjectCreation {
/// Stable UUID reused across restarts and retries.
final String id;
/// Reviewed name retained before the first network dependency.
final String name;
/// Build an intent; [complete] persists it before syncing.
const MeProjectCreation(this.id, this.name, {this.channelId});
/// Existing Solo chat to place in this workspace in the same synced write.
final String? channelId;
/// Isolate private drafts by both account and community.
static String storageKey(MeSync sync) =>
'me-project-creation:${sync.community}:${sync.pubkey}';
/// Restore without requiring connectivity.
static MeProjectCreation? 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 MeProjectCreation(
value['id'] as String,
value['name'] as String,
channelId: value['channelId'] as String?,
);
}
void _checkScope(MeSync sync) {
if (sync.disposed) {
throw StateError('Your community changed. Open Solo again.');
}
}
/// Persist before connectivity checks or relay reads, including while offline.
Future<void> save(MeSync sync) async {
_checkScope(sync);
final previous = pending(sync);
if (previous != null &&
(previous.id != id ||
previous.name != name ||
previous.channelId != channelId)) {
throw StateError(
'Review your unfinished project before starting another.',
);
}
if (sync.prefs == null ||
!await sync.prefs!.setString(
storageKey(sync),
nip44Encrypt(
sync.key,
jsonEncode({'id': id, 'name': name, 'channelId': ?channelId}),
),
)) {
throw StateError('Couldn’t save this project on your phone. Try again.');
}
_checkScope(sync);
}
/// Compare before removing the reviewed intent. Existing projects are kept.
Future<void> discard(MeSync sync) async {
_checkScope(sync);
final current = pending(sync);
if (current?.id != id ||
current?.name != name ||
current?.channelId != channelId) {
throw StateError('The unfinished project changed. Review it again.');
}
await clearMeJournal(
sync.prefs!,
storageKey(sync),
sync.prefs!.getString(storageKey(sync))!,
checkScope: () => _checkScope(sync),
);
}
/// Retry using the same ID, preserving projects already confirmed on desktop.
Future<void> complete(MeSync sync) async {
await save(sync);
final record = sync.prefs!.getString(storageKey(sync))!;
final head = await sync.head();
final confirmed = head == null ? null : sync.decode(head);
final exists =
confirmed != null &&
confirmed.groups.any((g) => g['id'] == id) &&
(channelId == null || confirmed.assignments[channelId] == id);
if (!exists || sync.prefs?.getString(sync.pendingKey) != null) {
await sync.update((current) {
final groups = current.groups.any((g) => g['id'] == id)
? current
: MeGroups(
groups: [
...current.groups,
{'id': id, 'name': name, 'defaultTemplateId': ''},
],
assignments: current.assignments,
channels: current.channels,
);
return channelId == null ? groups : groups.place(channelId!, group: id);
});
}
await clearMeJournal(
sync.prefs!,
storageKey(sync),
record,
checkScope: () => _checkScope(sync),
);
}
}