mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
Add project selection to new Solo chats on mobile (#8546)
New Solo chats can optionally choose a project in the Flutter form and native iOS sheet. Project appears above the agent picker with matching neutral text, and the choice survives retries and restarts. Validation: full Flutter suite passed; analysis and iOS builds passed. Installed in Pairing Preview on iPhone Air and visually approved, preserving saved preferences. Successful live chat creation remains unverified because no agents were available. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -18,7 +18,15 @@ struct MeAgentAccessibilityTests {
|
|||||||
onCancel: {}, onSet: { _ in }
|
onCancel: {}, onSet: { _ in }
|
||||||
)
|
)
|
||||||
controller.loadViewIfNeeded()
|
controller.loadViewIfNeeded()
|
||||||
let cell = controller.tableView(controller.tableView, cellForRowAt: IndexPath(row: 0, section: 1))
|
precondition(controller.numberOfSections(in: controller.tableView) == 3)
|
||||||
|
let projectCell = controller.tableView(controller.tableView, cellForRowAt: IndexPath(row: 0, section: 1))
|
||||||
|
let project = projectCell.accessoryView as! UIButton
|
||||||
|
precondition(project.accessibilityLabel == "Project")
|
||||||
|
precondition(project.accessibilityValue == "No project")
|
||||||
|
precondition(project.titleColor(for: .normal) == .label)
|
||||||
|
precondition(project.isEnabled == !locked)
|
||||||
|
precondition(project.showsMenuAsPrimaryAction && project.menu != nil)
|
||||||
|
let cell = controller.tableView(controller.tableView, cellForRowAt: IndexPath(row: 0, section: 2))
|
||||||
let disabled = locked || empty
|
let disabled = locked || empty
|
||||||
precondition(cell.accessibilityTraits.contains(.notEnabled) == disabled)
|
precondition(cell.accessibilityTraits.contains(.notEnabled) == disabled)
|
||||||
precondition(cell.accessibilityTraits.contains(.button) == !disabled)
|
precondition(cell.accessibilityTraits.contains(.button) == !disabled)
|
||||||
|
|||||||
@@ -245,6 +245,10 @@ private final class NativeProfileTextEditorViewController:
|
|||||||
private var hasFocusedEditor = false
|
private var hasFocusedEditor = false
|
||||||
private var selectedAssistantAvatar: UIImage?
|
private var selectedAssistantAvatar: UIImage?
|
||||||
private var selectedAssistant: String?
|
private var selectedAssistant: String?
|
||||||
|
private var selectedProject: String?
|
||||||
|
private var projects: [[String: String]] {
|
||||||
|
chatOptions?["projects"] as? [[String: String]] ?? []
|
||||||
|
}
|
||||||
private var isChatEditor: Bool { chatOptions != nil }
|
private var isChatEditor: Bool { chatOptions != nil }
|
||||||
private var isProjectEditor: Bool { chatOptions?["projectOnly"] as? Bool ?? false }
|
private var isProjectEditor: Bool { chatOptions?["projectOnly"] as? Bool ?? false }
|
||||||
private var assistants: [[String: String]] {
|
private var assistants: [[String: String]] {
|
||||||
@@ -333,6 +337,7 @@ private final class NativeProfileTextEditorViewController:
|
|||||||
self.canvasLoaded = canvasLoaded
|
self.canvasLoaded = canvasLoaded
|
||||||
self.chatOptions = chatOptions
|
self.chatOptions = chatOptions
|
||||||
self.selectedAssistant = chatOptions?["selectedAssistant"] as? String
|
self.selectedAssistant = chatOptions?["selectedAssistant"] as? String
|
||||||
|
self.selectedProject = chatOptions?["selectedProject"] as? String
|
||||||
self.onCancel = onCancel
|
self.onCancel = onCancel
|
||||||
self.onSet = onSet
|
self.onSet = onSet
|
||||||
super.init(style: .insetGrouped)
|
super.init(style: .insetGrouped)
|
||||||
@@ -472,7 +477,7 @@ private final class NativeProfileTextEditorViewController:
|
|||||||
assistants.contains(where: { $0["id"] == selectedAssistant }),
|
assistants.contains(where: { $0["id"] == selectedAssistant }),
|
||||||
!currentValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
!currentValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||||
currentValue.count <= 120 else { return }
|
currentValue.count <= 120 else { return }
|
||||||
onSet(["name": currentValue, "assistant": selectedAssistant])
|
onSet(["name": currentValue, "assistant": selectedAssistant, "project": selectedProject ?? ""])
|
||||||
} else if isChannelEditor {
|
} else if isChannelEditor {
|
||||||
guard !canonicalChannelName.isEmpty else { return }
|
guard !canonicalChannelName.isEmpty else { return }
|
||||||
onSet(["action": "save", "name": canonicalChannelName, "description": textView.text ?? ""])
|
onSet(["action": "save", "name": canonicalChannelName, "description": textView.text ?? ""])
|
||||||
@@ -503,13 +508,13 @@ private final class NativeProfileTextEditorViewController:
|
|||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
tableView.deselectRow(at: indexPath, animated: true)
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
if isChatEditor && indexPath.section == 1 {
|
if isChatEditor && indexPath.section == 2 {
|
||||||
guard !fieldsLocked && !assistants.isEmpty else { return }
|
guard !fieldsLocked && !assistants.isEmpty else { return }
|
||||||
view.endEditing(true)
|
view.endEditing(true)
|
||||||
let picker = MeAssistantListController(assistants: assistants, selected: selectedAssistant, pageColor: pageBackgroundColor, rowColor: containerBackgroundColor) { [weak self] id, avatar in
|
let picker = MeAssistantListController(assistants: assistants, selected: selectedAssistant, pageColor: pageBackgroundColor, rowColor: containerBackgroundColor) { [weak self] id, avatar in
|
||||||
self?.selectedAssistantAvatar = avatar
|
self?.selectedAssistantAvatar = avatar
|
||||||
self?.selectedAssistant = id
|
self?.selectedAssistant = id
|
||||||
self?.tableView.reloadSections(IndexSet(integer: 1), with: .none)
|
self?.tableView.reloadSections(IndexSet(integer: 2), with: .none)
|
||||||
self?.updateNavigation()
|
self?.updateNavigation()
|
||||||
}
|
}
|
||||||
navigationController?.pushViewController(picker, animated: true)
|
navigationController?.pushViewController(picker, animated: true)
|
||||||
@@ -529,10 +534,10 @@ private final class NativeProfileTextEditorViewController:
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override func numberOfSections(in tableView: UITableView) -> Int { isProjectEditor ? 1 : isChatEditor ? 2 : isChannelEditor ? (hasCanvas && canEditCanvas ? 4 : 3) : 1 }
|
override func numberOfSections(in tableView: UITableView) -> Int { isProjectEditor ? 1 : isChatEditor ? 3 : isChannelEditor ? (hasCanvas && canEditCanvas ? 4 : 3) : 1 }
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||||
guard isChatEditor && section == 1 else { return nil }
|
guard isChatEditor && section == 2 else { return nil }
|
||||||
if let error = chatOptions?["error"] as? String { return error }
|
if let error = chatOptions?["error"] as? String { return error }
|
||||||
return assistants.isEmpty
|
return assistants.isEmpty
|
||||||
? "No agents are available for this account. Connect an agent on desktop, then try again."
|
? "No agents are available for this account. Connect an agent on desktop, then try again."
|
||||||
@@ -548,7 +553,7 @@ private final class NativeProfileTextEditorViewController:
|
|||||||
_ tableView: UITableView,
|
_ tableView: UITableView,
|
||||||
heightForRowAt indexPath: IndexPath
|
heightForRowAt indexPath: IndexPath
|
||||||
) -> CGFloat {
|
) -> CGFloat {
|
||||||
if isChatEditor && indexPath.section == 1 && selectedAssistant != nil { return 64 }
|
if isChatEditor && indexPath.section == 2 && selectedAssistant != nil { return 64 }
|
||||||
if isChannelEditor && indexPath.section >= 2 { return UITableView.automaticDimension }
|
if isChannelEditor && indexPath.section >= 2 { return UITableView.automaticDimension }
|
||||||
return (isChannelEditor ? indexPath.section == 1 : multiline) ? 132 : 52
|
return (isChannelEditor ? indexPath.section == 1 : multiline) ? 132 : 52
|
||||||
}
|
}
|
||||||
@@ -564,6 +569,36 @@ private final class NativeProfileTextEditorViewController:
|
|||||||
cell.layer.cornerCurve = .continuous
|
cell.layer.cornerCurve = .continuous
|
||||||
cell.clipsToBounds = true
|
cell.clipsToBounds = true
|
||||||
if isChatEditor && indexPath.section == 1 {
|
if isChatEditor && indexPath.section == 1 {
|
||||||
|
var content = cell.defaultContentConfiguration()
|
||||||
|
content.text = "Project"
|
||||||
|
content.textProperties.font = .preferredFont(forTextStyle: .body)
|
||||||
|
cell.contentConfiguration = content
|
||||||
|
let selected = projects.first { $0["id"] == selectedProject }
|
||||||
|
let name = selected?["name"] ?? (selectedProject == nil ? "No project" : "Unavailable project")
|
||||||
|
let button = UIButton(type: .system)
|
||||||
|
button.setTitle(name, for: .normal)
|
||||||
|
button.setTitleColor(.label, for: .normal)
|
||||||
|
button.setTitleColor(.secondaryLabel, for: .disabled)
|
||||||
|
button.titleLabel?.font = .preferredFont(forTextStyle: .body)
|
||||||
|
button.titleLabel?.adjustsFontForContentSizeCategory = true
|
||||||
|
button.accessibilityLabel = "Project"
|
||||||
|
button.accessibilityValue = name
|
||||||
|
button.isEnabled = !fieldsLocked
|
||||||
|
button.showsMenuAsPrimaryAction = true
|
||||||
|
let choices = [["id": "", "name": "No project"]] + projects
|
||||||
|
button.menu = UIMenu(children: choices.map { project in
|
||||||
|
let id = project["id"] ?? ""
|
||||||
|
return UIAction(title: project["name"] ?? "Project", state: id == (selectedProject ?? "") ? .on : .off) { [weak self] _ in
|
||||||
|
self?.view.endEditing(true)
|
||||||
|
self?.selectedProject = id.isEmpty ? nil : id
|
||||||
|
self?.tableView.reloadSections(IndexSet(integer: 1), with: .none)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
button.titleLabel?.lineBreakMode = .byTruncatingTail
|
||||||
|
button.sizeToFit()
|
||||||
|
button.frame.size.width = min(button.frame.width, tableView.bounds.width * 0.55)
|
||||||
|
cell.accessoryView = button
|
||||||
|
} else if isChatEditor && indexPath.section == 2 {
|
||||||
var content = cell.defaultContentConfiguration()
|
var content = cell.defaultContentConfiguration()
|
||||||
let selected = assistants.first { $0["id"] == selectedAssistant }
|
let selected = assistants.first { $0["id"] == selectedAssistant }
|
||||||
content.text = selected?["name"] ?? "Choose an agent"
|
content.text = selected?["name"] ?? "Choose an agent"
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ Future<(Channel, String)?> _showNewMeChat(
|
|||||||
final id = pending?.id ?? const Uuid().v4();
|
final id = pending?.id ?? const Uuid().v4();
|
||||||
String name = pending?.name ?? '';
|
String name = pending?.name ?? '';
|
||||||
String? assistant = pending?.agent;
|
String? assistant = pending?.agent;
|
||||||
|
String? project = pending?.project;
|
||||||
String? error = pending == null ? null : 'Resume your unfinished chat.';
|
String? error = pending == null ? null : 'Resume your unfinished chat.';
|
||||||
var retry = pending != null;
|
var retry = pending != null;
|
||||||
while (context.mounted) {
|
while (context.mounted) {
|
||||||
@@ -68,6 +69,12 @@ Future<(Channel, String)?> _showNewMeChat(
|
|||||||
.toARGB32(),
|
.toARGB32(),
|
||||||
'containerCornerRadius': Radii.container,
|
'containerCornerRadius': Radii.container,
|
||||||
'selectedAssistant': assistant,
|
'selectedAssistant': assistant,
|
||||||
|
'selectedProject': project,
|
||||||
|
'projects': [
|
||||||
|
for (final group
|
||||||
|
in ref.read(meGroupsProvider).requireValue.groups)
|
||||||
|
{'id': group['id'], 'name': group['name']},
|
||||||
|
],
|
||||||
'fieldsLocked': retry,
|
'fieldsLocked': retry,
|
||||||
'error': error,
|
'error': error,
|
||||||
'assistants': [
|
'assistants': [
|
||||||
@@ -99,10 +106,19 @@ Future<(Channel, String)?> _showNewMeChat(
|
|||||||
if (draft == null || !context.mounted) return null;
|
if (draft == null || !context.mounted) return null;
|
||||||
name = (draft['name'] as String? ?? '').trim();
|
name = (draft['name'] as String? ?? '').trim();
|
||||||
assistant = draft['assistant'] as String?;
|
assistant = draft['assistant'] as String?;
|
||||||
|
project = draft['project'] as String?;
|
||||||
|
if (project == '') project = null;
|
||||||
if (name.isEmpty || assistant == null) return null;
|
if (name.isEmpty || assistant == null) return null;
|
||||||
retry = true;
|
retry = true;
|
||||||
try {
|
try {
|
||||||
final created = await _createMeChat(ref, sync, id, name, assistant);
|
final created = await _createMeChat(
|
||||||
|
ref,
|
||||||
|
sync,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
assistant,
|
||||||
|
project,
|
||||||
|
);
|
||||||
return (created, assistant);
|
return (created, assistant);
|
||||||
} catch (failure) {
|
} catch (failure) {
|
||||||
if (failure is MeRecoveryConflict && context.mounted) {
|
if (failure is MeRecoveryConflict && context.mounted) {
|
||||||
@@ -129,9 +145,10 @@ Future<Channel> _createMeChat(
|
|||||||
String channelId,
|
String channelId,
|
||||||
String name,
|
String name,
|
||||||
String assistant,
|
String assistant,
|
||||||
|
String? project,
|
||||||
) async {
|
) async {
|
||||||
sync = _currentMeSync(ref, sync);
|
sync = _currentMeSync(ref, sync);
|
||||||
final intent = MeChatCreation(channelId, name, assistant);
|
final intent = MeChatCreation(channelId, name, assistant, project: project);
|
||||||
await intent.save(sync);
|
await intent.save(sync);
|
||||||
final owned = await isOwnedMeAssistant(sync, assistant);
|
final owned = await isOwnedMeAssistant(sync, assistant);
|
||||||
sync.check();
|
sync.check();
|
||||||
@@ -140,7 +157,7 @@ Future<Channel> _createMeChat(
|
|||||||
}
|
}
|
||||||
final head = await sync.head();
|
final head = await sync.head();
|
||||||
(head == null ? const MeGroups() : sync.decode(head))
|
(head == null ? const MeGroups() : sync.decode(head))
|
||||||
.place(channelId)
|
.place(channelId, group: project)
|
||||||
.encode(sync.community);
|
.encode(sync.community);
|
||||||
final actions = ref.read(channelActionsProvider);
|
final actions = ref.read(channelActionsProvider);
|
||||||
return intent.complete<Channel>(
|
return intent.complete<Channel>(
|
||||||
@@ -166,8 +183,12 @@ Future<Channel> _createMeChat(
|
|||||||
actions.addMembers(channelId: channelId, pubkeys: [assistant]),
|
actions.addMembers(channelId: channelId, pubkeys: [assistant]),
|
||||||
isPlaced: () async {
|
isPlaced: () async {
|
||||||
final latest = await sync.head();
|
final latest = await sync.head();
|
||||||
return latest != null && sync.decode(latest).channels.contains(channelId);
|
if (latest == null) return false;
|
||||||
|
final groups = sync.decode(latest);
|
||||||
|
return groups.channels.contains(channelId) &&
|
||||||
|
(project == null || groups.assignments[channelId] == project);
|
||||||
},
|
},
|
||||||
place: () => sync.update((current) => current.place(channelId)),
|
place: () =>
|
||||||
|
sync.update((current) => current.place(channelId, group: project)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ class _NewMeChat extends HookConsumerWidget {
|
|||||||
final pending = useMemoized(() => MeChatCreation.pending(sync));
|
final pending = useMemoized(() => MeChatCreation.pending(sync));
|
||||||
final name = useTextEditingController(text: pending?.name ?? '');
|
final name = useTextEditingController(text: pending?.name ?? '');
|
||||||
final selected = useState<String?>(pending?.agent);
|
final selected = useState<String?>(pending?.agent);
|
||||||
|
final project = useState<String?>(pending?.project);
|
||||||
|
final groups = ref.watch(meGroupsProvider);
|
||||||
final channelId = useMemoized(() => pending?.id ?? const Uuid().v4());
|
final channelId = useMemoized(() => pending?.id ?? const Uuid().v4());
|
||||||
final frozenName = useState<String?>(pending?.name);
|
final frozenName = useState<String?>(pending?.name);
|
||||||
final frozenAgent = useState<String?>(pending?.agent);
|
final frozenAgent = useState<String?>(pending?.agent);
|
||||||
@@ -52,6 +54,34 @@ class _NewMeChat extends HookConsumerWidget {
|
|||||||
onChanged: (_) => error.value = null,
|
onChanged: (_) => error.value = null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: Grid.xs),
|
const SizedBox(height: Grid.xs),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
key: const ValueKey('new-session-project'),
|
||||||
|
initialValue: project.value ?? '',
|
||||||
|
isExpanded: true,
|
||||||
|
decoration: const InputDecoration(labelText: 'Project'),
|
||||||
|
items: [
|
||||||
|
const DropdownMenuItem(value: '', child: Text('No project')),
|
||||||
|
for (final group
|
||||||
|
in groups.asData?.value.groups ?? <Map<String, dynamic>>[])
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: group['id'] as String,
|
||||||
|
child: Text(group['name'] as String),
|
||||||
|
),
|
||||||
|
if (project.value != null &&
|
||||||
|
!(groups.asData?.value.groups ?? []).any(
|
||||||
|
(group) => group['id'] == project.value,
|
||||||
|
))
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: project.value,
|
||||||
|
child: const Text('Unavailable project'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged:
|
||||||
|
busy.value || frozenName.value != null || !groups.hasValue
|
||||||
|
? null
|
||||||
|
: (value) => project.value = value == '' ? null : value,
|
||||||
|
),
|
||||||
|
const SizedBox(height: Grid.xs),
|
||||||
AppListRowRaw(
|
AppListRowRaw(
|
||||||
key: const ValueKey('new-session-assistant'),
|
key: const ValueKey('new-session-assistant'),
|
||||||
leading: chosen == null
|
leading: chosen == null
|
||||||
@@ -130,6 +160,7 @@ class _NewMeChat extends HookConsumerWidget {
|
|||||||
channelId,
|
channelId,
|
||||||
frozenName.value!,
|
frozenName.value!,
|
||||||
frozenAgent.value!,
|
frozenAgent.value!,
|
||||||
|
project.value,
|
||||||
);
|
);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(
|
Navigator.of(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import '../crypto/nip44.dart';
|
import '../crypto/nip44.dart';
|
||||||
import 'me_sync.dart';
|
import 'me_sync.dart';
|
||||||
import 'me_journal.dart';
|
import 'me_journal.dart';
|
||||||
@@ -14,8 +15,11 @@ class MeChatCreation {
|
|||||||
/// Selected agent public key.
|
/// Selected agent public key.
|
||||||
final String agent;
|
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.
|
/// Create a new intent; persistence happens before [complete] performs writes.
|
||||||
const MeChatCreation(this.id, this.name, this.agent);
|
const MeChatCreation(this.id, this.name, this.agent, {this.project});
|
||||||
|
|
||||||
/// Account and community boundary for durable creation storage.
|
/// Account and community boundary for durable creation storage.
|
||||||
static String storageKey(MeSync sync) =>
|
static String storageKey(MeSync sync) =>
|
||||||
@@ -31,6 +35,7 @@ class MeChatCreation {
|
|||||||
value['id'] as String,
|
value['id'] as String,
|
||||||
value['name'] as String,
|
value['name'] as String,
|
||||||
value['agent'] as String,
|
value['agent'] as String,
|
||||||
|
project: value['project'] as String?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +46,8 @@ class MeChatCreation {
|
|||||||
if (previous != null &&
|
if (previous != null &&
|
||||||
(previous.id != id ||
|
(previous.id != id ||
|
||||||
previous.name != name ||
|
previous.name != name ||
|
||||||
previous.agent != agent)) {
|
previous.agent != agent ||
|
||||||
|
previous.project != project)) {
|
||||||
throw StateError('Resume the pending chat before starting another.');
|
throw StateError('Resume the pending chat before starting another.');
|
||||||
}
|
}
|
||||||
if (sync.prefs == null ||
|
if (sync.prefs == null ||
|
||||||
@@ -49,7 +55,12 @@ class MeChatCreation {
|
|||||||
storageKey(sync),
|
storageKey(sync),
|
||||||
nip44Encrypt(
|
nip44Encrypt(
|
||||||
sync.key,
|
sync.key,
|
||||||
jsonEncode({'id': id, 'name': name, 'agent': agent}),
|
jsonEncode({
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'agent': agent,
|
||||||
|
'project': project,
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
)) {
|
)) {
|
||||||
throw StateError('Couldn’t save this chat on your phone. Try again.');
|
throw StateError('Couldn’t save this chat on your phone. Try again.');
|
||||||
@@ -62,7 +73,10 @@ class MeChatCreation {
|
|||||||
Future<void> discard(MeSync sync) async {
|
Future<void> discard(MeSync sync) async {
|
||||||
if (sync.disposed) throw StateError('Your community changed. Try again.');
|
if (sync.disposed) throw StateError('Your community changed. Try again.');
|
||||||
final current = pending(sync);
|
final current = pending(sync);
|
||||||
if (current?.id != id || current?.name != name || current?.agent != agent) {
|
if (current?.id != id ||
|
||||||
|
current?.name != name ||
|
||||||
|
current?.agent != agent ||
|
||||||
|
current?.project != project) {
|
||||||
throw StateError('The unfinished chat changed. Review it again.');
|
throw StateError('The unfinished chat changed. Review it again.');
|
||||||
}
|
}
|
||||||
await clearMeJournal(
|
await clearMeJournal(
|
||||||
|
|||||||
@@ -65,7 +65,11 @@ void main() {
|
|||||||
sync = MeSync(relay, config, prefs: prefs, connected: false);
|
sync = MeSync(relay, config, prefs: prefs, connected: false);
|
||||||
container.invalidate(meSyncProvider);
|
container.invalidate(meSyncProvider);
|
||||||
}
|
}
|
||||||
return {'name': 'Saved chat', 'assistant': 'agent'};
|
return {
|
||||||
|
'name': 'Saved chat',
|
||||||
|
'assistant': 'agent',
|
||||||
|
'project': 'mobile',
|
||||||
|
};
|
||||||
});
|
});
|
||||||
addTearDown(
|
addTearDown(
|
||||||
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||||
@@ -106,6 +110,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(saved!.name, 'Saved chat');
|
expect(saved!.name, 'Saved chat');
|
||||||
expect(saved.agent, 'agent');
|
expect(saved.agent, 'agent');
|
||||||
|
expect(saved.project, 'mobile');
|
||||||
expect(
|
expect(
|
||||||
prefs.getString(MeChatCreation.storageKey(sync)),
|
prefs.getString(MeChatCreation.storageKey(sync)),
|
||||||
isNot(contains('Saved chat')),
|
isNot(contains('Saved chat')),
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'footer new chat action opens both fields in one dismissible sheet',
|
'footer new chat offers project selection without losing its name',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
final action = ValueNotifier<VoidCallback?>(null);
|
final action = ValueNotifier<VoidCallback?>(null);
|
||||||
addTearDown(action.dispose);
|
addTearDown(action.dispose);
|
||||||
@@ -220,7 +220,17 @@ void main() {
|
|||||||
ProviderScope(
|
ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
meGroupsProvider.overrideWith(
|
meGroupsProvider.overrideWith(
|
||||||
(ref) => Stream.value(const MeGroups()),
|
(ref) => Stream.value(
|
||||||
|
const MeGroups(
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
'id': 'mobile',
|
||||||
|
'name': 'Mobile app',
|
||||||
|
'defaultTemplateId': '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
meSyncProvider.overrideWithValue(sync),
|
meSyncProvider.overrideWithValue(sync),
|
||||||
channelsProvider.overrideWith(FakeChannels.new),
|
channelsProvider.overrideWith(FakeChannels.new),
|
||||||
@@ -250,6 +260,16 @@ void main() {
|
|||||||
find.byKey(const ValueKey('new-session-name')),
|
find.byKey(const ValueKey('new-session-name')),
|
||||||
'Design session',
|
'Design session',
|
||||||
);
|
);
|
||||||
|
await tester.tap(find.byKey(const ValueKey('new-session-project')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Mobile app').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Design session'), findsOneWidget);
|
||||||
|
expect(find.text('Mobile app'), findsWidgets);
|
||||||
|
await tester.tap(find.byKey(const ValueKey('new-session-project')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('No project').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.text('Create chat'));
|
await tester.tap(find.text('Create chat'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('Enter a name and choose an agent.'), findsOneWidget);
|
expect(find.text('Enter a name and choose an agent.'), findsOneWidget);
|
||||||
@@ -257,7 +277,7 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets('iOS new chat uses the native profile form with both fields', (
|
testWidgets('iOS new chat uses the native profile form with project choices', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||||||
@@ -292,7 +312,17 @@ void main() {
|
|||||||
ProviderScope(
|
ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
meGroupsProvider.overrideWith(
|
meGroupsProvider.overrideWith(
|
||||||
(ref) => Stream.value(const MeGroups()),
|
(ref) => Stream.value(
|
||||||
|
const MeGroups(
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
'id': 'mobile',
|
||||||
|
'name': 'Mobile app',
|
||||||
|
'defaultTemplateId': '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
meSyncProvider.overrideWithValue(sync),
|
meSyncProvider.overrideWithValue(sync),
|
||||||
channelsProvider.overrideWith(FakeChannels.new),
|
channelsProvider.overrideWith(FakeChannels.new),
|
||||||
@@ -318,6 +348,10 @@ void main() {
|
|||||||
expect(args['title'], 'New chat');
|
expect(args['title'], 'New chat');
|
||||||
expect(args['placeholder'], 'Chat name');
|
expect(args['placeholder'], 'Chat name');
|
||||||
expect(args['assistants'], isEmpty);
|
expect(args['assistants'], isEmpty);
|
||||||
|
expect(args['projects'], [
|
||||||
|
{'id': 'mobile', 'name': 'Mobile app'},
|
||||||
|
]);
|
||||||
|
expect(args['selectedProject'], isNull);
|
||||||
expect(args['containerCornerRadius'], Radii.container);
|
expect(args['containerCornerRadius'], Radii.container);
|
||||||
expect(find.byType(BottomSheet), findsNothing);
|
expect(find.byType(BottomSheet), findsNothing);
|
||||||
await tester.pumpWidget(const SizedBox.shrink());
|
await tester.pumpWidget(const SizedBox.shrink());
|
||||||
@@ -363,6 +397,7 @@ void main() {
|
|||||||
'same-id',
|
'same-id',
|
||||||
'Unfinished chat',
|
'Unfinished chat',
|
||||||
'agent',
|
'agent',
|
||||||
|
project: 'mobile',
|
||||||
).save(sync);
|
).save(sync);
|
||||||
final restarted = MeSync(
|
final restarted = MeSync(
|
||||||
RelaySessionNotifier(),
|
RelaySessionNotifier(),
|
||||||
@@ -401,6 +436,7 @@ void main() {
|
|||||||
expect(args['title'], 'New chat');
|
expect(args['title'], 'New chat');
|
||||||
expect(args['initialValue'], resume ? 'Unfinished chat' : '');
|
expect(args['initialValue'], resume ? 'Unfinished chat' : '');
|
||||||
expect(args['selectedAssistant'], resume ? 'agent' : isNull);
|
expect(args['selectedAssistant'], resume ? 'agent' : isNull);
|
||||||
|
expect(args['selectedProject'], resume ? 'mobile' : isNull);
|
||||||
expect(args['fieldsLocked'], resume);
|
expect(args['fieldsLocked'], resume);
|
||||||
expect(args['error'], resume ? contains('Resume') : isNull);
|
expect(args['error'], resume ? contains('Resume') : isNull);
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -79,7 +79,12 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
MeSync service() => MeSync(RelaySessionNotifier(), config, prefs: prefs);
|
MeSync service() => MeSync(RelaySessionNotifier(), config, prefs: prefs);
|
||||||
const intent = MeChatCreation('same-id', 'Private name', 'agent');
|
const intent = MeChatCreation(
|
||||||
|
'same-id',
|
||||||
|
'Private name',
|
||||||
|
'agent',
|
||||||
|
project: 'mobile',
|
||||||
|
);
|
||||||
var created = false, member = false, placed = false, crash = true;
|
var created = false, member = false, placed = false, crash = true;
|
||||||
final writes = [0, 0, 0];
|
final writes = [0, 0, 0];
|
||||||
void boundary(int point) {
|
void boundary(int point) {
|
||||||
@@ -118,6 +123,16 @@ void main() {
|
|||||||
final pending = MeChatCreation.pending(restarted)!;
|
final pending = MeChatCreation.pending(restarted)!;
|
||||||
expect(pending.name, intent.name);
|
expect(pending.name, intent.name);
|
||||||
expect(pending.agent, intent.agent);
|
expect(pending.agent, intent.agent);
|
||||||
|
expect(pending.project, intent.project);
|
||||||
|
await expectLater(
|
||||||
|
MeChatCreation(
|
||||||
|
intent.id,
|
||||||
|
intent.name,
|
||||||
|
intent.agent,
|
||||||
|
project: 'other',
|
||||||
|
).save(restarted),
|
||||||
|
throwsStateError,
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
prefs.getString(MeChatCreation.storageKey(restarted)),
|
prefs.getString(MeChatCreation.storageKey(restarted)),
|
||||||
isNot(contains(intent.name)),
|
isNot(contains(intent.name)),
|
||||||
|
|||||||
Reference in New Issue
Block a user