mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(mobile): preserve channel state across partial actions
Signed-off-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz> Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -47,10 +47,12 @@ class AddChannelMembersSheet extends HookConsumerWidget {
|
||||
final selectedPubkeys = selectedUsers.value
|
||||
.map((user) => user.pubkey.toLowerCase())
|
||||
.toSet();
|
||||
final successfulPubkeys = useState<Set<String>>(<String>{});
|
||||
final excludedPubkeys = {...normalizedExisting, ...successfulPubkeys.value};
|
||||
final availableUsers =
|
||||
directoryAsync.asData?.value
|
||||
.where(
|
||||
(user) => !normalizedExisting.contains(user.pubkey.toLowerCase()),
|
||||
(user) => !excludedPubkeys.contains(user.pubkey.toLowerCase()),
|
||||
)
|
||||
.toList() ??
|
||||
const <DirectoryUser>[];
|
||||
@@ -84,6 +86,12 @@ class AddChannelMembersSheet extends HookConsumerWidget {
|
||||
final failedPubkeys = error.failures.keys
|
||||
.map((pubkey) => pubkey.toLowerCase())
|
||||
.toSet();
|
||||
successfulPubkeys.value = {
|
||||
...successfulPubkeys.value,
|
||||
for (final user in submittedUsers)
|
||||
if (!failedPubkeys.contains(user.pubkey.toLowerCase()))
|
||||
user.pubkey.toLowerCase(),
|
||||
};
|
||||
selectedUsers.value = [
|
||||
for (final user in submittedUsers)
|
||||
if (failedPubkeys.contains(user.pubkey.toLowerCase())) user,
|
||||
|
||||
@@ -59,7 +59,7 @@ Future<bool?> showChannelActionsSheet({
|
||||
|
||||
/// Mobile action sheet for channel-level read, organization, and lifecycle
|
||||
/// operations.
|
||||
class ChannelActionsSheet extends ConsumerWidget {
|
||||
class ChannelActionsSheet extends HookConsumerWidget {
|
||||
const ChannelActionsSheet({
|
||||
super.key,
|
||||
required this.channel,
|
||||
@@ -75,6 +75,8 @@ class ChannelActionsSheet extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final displayedChannel = useState(channel);
|
||||
final currentChannel = displayedChannel.value;
|
||||
final isMuted =
|
||||
ref.watch(channelMutesProvider).store.channels[channel.id]?.muted ==
|
||||
true;
|
||||
@@ -220,9 +222,11 @@ class ChannelActionsSheet extends ConsumerWidget {
|
||||
maxHeight: MediaQuery.sizeOf(context).height * 0.9,
|
||||
),
|
||||
builder: (_) => ManageChannelSheet(
|
||||
channel: channel,
|
||||
channel: currentChannel,
|
||||
canEditDetails:
|
||||
canManageLifecycle && !channel.isArchived,
|
||||
canManageLifecycle && !currentChannel.isArchived,
|
||||
onChannelUpdated: (updated) =>
|
||||
displayedChannel.value = updated,
|
||||
),
|
||||
);
|
||||
if (shouldClose == true && context.mounted) {
|
||||
@@ -238,7 +242,7 @@ class ChannelActionsSheet extends ConsumerWidget {
|
||||
close();
|
||||
copyToClipboard(
|
||||
context,
|
||||
channel.name,
|
||||
currentChannel.name,
|
||||
message: 'Channel name copied to clipboard',
|
||||
);
|
||||
},
|
||||
@@ -266,7 +270,7 @@ class ChannelActionsSheet extends ConsumerWidget {
|
||||
onTap: () => _confirmAndRun(
|
||||
context,
|
||||
ref,
|
||||
title: 'Leave #${channel.name}?',
|
||||
title: 'Leave #${currentChannel.name}?',
|
||||
body: 'You’ll stop receiving messages from this channel.',
|
||||
confirmLabel: 'Leave',
|
||||
action: () => ref
|
||||
@@ -297,7 +301,7 @@ class ChannelActionsSheet extends ConsumerWidget {
|
||||
onTap: () => _confirmAndRun(
|
||||
context,
|
||||
ref,
|
||||
title: 'Archive #${channel.name}?',
|
||||
title: 'Archive #${currentChannel.name}?',
|
||||
body: 'The channel will become read-only.',
|
||||
confirmLabel: 'Archive',
|
||||
action: () => ref
|
||||
@@ -312,7 +316,7 @@ class ChannelActionsSheet extends ConsumerWidget {
|
||||
onTap: () => _confirmAndRun(
|
||||
context,
|
||||
ref,
|
||||
title: 'Unarchive #${channel.name}?',
|
||||
title: 'Unarchive #${currentChannel.name}?',
|
||||
body: 'The channel will become active again.',
|
||||
confirmLabel: 'Unarchive',
|
||||
action: () => ref
|
||||
@@ -328,7 +332,7 @@ class ChannelActionsSheet extends ConsumerWidget {
|
||||
onTap: () => _confirmAndRun(
|
||||
context,
|
||||
ref,
|
||||
title: 'Delete #${channel.name}?',
|
||||
title: 'Delete #${currentChannel.name}?',
|
||||
body:
|
||||
'This permanently deletes the channel and cannot be undone.',
|
||||
confirmLabel: 'Delete',
|
||||
|
||||
@@ -371,6 +371,47 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Manage refreshes metadata after saving from actions', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
_modalApp(
|
||||
channel: _channel(),
|
||||
loadMembers: () async => [
|
||||
ChannelMember(
|
||||
pubkey: _currentPubkey,
|
||||
role: 'owner',
|
||||
joinedAt: DateTime(2025),
|
||||
),
|
||||
],
|
||||
createChannelActions: (ref) => _FakeChannelActions(
|
||||
ref,
|
||||
onUpdateChannel: (channelId, name, description) async {},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Open actions'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Manage channel'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('manage-channel-name')),
|
||||
'renamed',
|
||||
);
|
||||
await tester.tap(find.byKey(const ValueKey('manage-channel-save-details')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.widgetWithText(ListTile, 'Manage channel'), findsOneWidget);
|
||||
await tester.tap(find.widgetWithText(ListTile, 'Manage channel'));
|
||||
await tester.pumpAndSettle();
|
||||
final nameField = tester.widget<TextField>(
|
||||
find.byKey(const ValueKey('manage-channel-name')),
|
||||
);
|
||||
expect(nameField.controller?.text, 'renamed');
|
||||
});
|
||||
|
||||
testWidgets('non-member cannot edit canvas from Manage channel', (
|
||||
tester,
|
||||
) async {
|
||||
@@ -491,7 +532,14 @@ class _FakeChannelSectionsNotifier extends ChannelSectionsNotifier {
|
||||
}
|
||||
|
||||
class _FakeChannelActions extends ChannelActions {
|
||||
_FakeChannelActions(Ref ref)
|
||||
final Future<void> Function(
|
||||
String channelId,
|
||||
String? name,
|
||||
String? description,
|
||||
)?
|
||||
onUpdateChannel;
|
||||
|
||||
_FakeChannelActions(Ref ref, {this.onUpdateChannel})
|
||||
: super(
|
||||
ref: ref,
|
||||
session: ref.read(relaySessionProvider.notifier),
|
||||
@@ -502,6 +550,15 @@ class _FakeChannelActions extends ChannelActions {
|
||||
currentPubkey: _currentPubkey,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> updateChannel({
|
||||
required String channelId,
|
||||
String? name,
|
||||
String? description,
|
||||
}) async {
|
||||
await onUpdateChannel?.call(channelId, name, description);
|
||||
}
|
||||
|
||||
String? unarchivedChannelId;
|
||||
|
||||
@override
|
||||
|
||||
@@ -1210,6 +1210,10 @@ void main() {
|
||||
find.byKey(const ValueKey('add-channel-member-selected-bob')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey('add-channel-member-alice')),
|
||||
findsNothing,
|
||||
);
|
||||
// The successful add must not remain selected for a retry.
|
||||
expect(
|
||||
tester
|
||||
|
||||
Reference in New Issue
Block a user