mirror of
https://github.com/block/buzz.git
synced 2026-11-11 11:51:13 +00:00
fix(mobile): check the current picker selection on a toggle
A second tap could reach Flutter before the rebuild that reflected the first, and both toggles read the selection captured at build time, so the same person was added twice. The diffed native table traps on a duplicate row. Read the current selection instead, and fail loudly if a shown member row is missing from the state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Signed-off-by: Tom Brow <tomb@squareup.com>
This commit is contained in:
@@ -296,7 +296,8 @@ final class NativeAddMembersViewController: UITableViewController, UISearchResul
|
||||
let changed = snapshot.itemIdentifiers.filter { item in
|
||||
drawn[item].map { $0 != next[item] } ?? false
|
||||
}
|
||||
snapshot.reconfigureItems(changed.filter(previous.itemIdentifiers.contains))
|
||||
let shown = Set(previous.itemIdentifiers)
|
||||
snapshot.reconfigureItems(changed.filter(shown.contains))
|
||||
drawn = next
|
||||
let animate = view.window != nil && wasChosen != chosen.map(Item.member)
|
||||
&& !UIAccessibility.isReduceMotionEnabled
|
||||
@@ -327,7 +328,7 @@ final class NativeAddMembersViewController: UITableViewController, UISearchResul
|
||||
cell.contentView.accessibilityElementsHidden = false
|
||||
cell.isUserInteractionEnabled = !busy
|
||||
var content = UIListContentConfiguration.subtitleCell()
|
||||
guard case .member(let pubkey) = item, let user = member(pubkey) else {
|
||||
guard case .member(let pubkey) = item else {
|
||||
content.text = status
|
||||
content.textProperties.color = loadError && !atLimit ? Self.errorColor : .secondaryLabel
|
||||
content.textProperties.numberOfLines = 0
|
||||
@@ -336,6 +337,7 @@ final class NativeAddMembersViewController: UITableViewController, UISearchResul
|
||||
cell.contentConfiguration = content
|
||||
return
|
||||
}
|
||||
guard let user = member(pubkey) else { preconditionFailure("A shown member row must be in the state") }
|
||||
let chosen = selected.contains { $0["pubkey"] as? String == pubkey }
|
||||
let key = user["avatarKey"] as? String
|
||||
content.text = user["name"] as? String
|
||||
@@ -378,10 +380,11 @@ final class NativeAddMembersViewController: UITableViewController, UISearchResul
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard !busy, let item = rows.itemIdentifier(for: indexPath) else { return }
|
||||
guard case .member(let pubkey) = item, let user = member(pubkey) else {
|
||||
guard case .member(let pubkey) = item else {
|
||||
if loadError && !atLimit { send("retry", [:]) }
|
||||
return
|
||||
}
|
||||
guard let user = member(pubkey) else { preconditionFailure("A shown member row must be in the state") }
|
||||
if user["unavailable"] as? Bool == true { return }
|
||||
send("toggle", ["pubkey": pubkey])
|
||||
// Where asked (New DM), a pick ends its query, as an inserted mention
|
||||
|
||||
@@ -161,7 +161,11 @@ class AddChannelMembersSheet extends HookConsumerWidget {
|
||||
void toggleUser(MentionCandidate user) {
|
||||
if (isSubmitting.value) return;
|
||||
final normalized = user.pubkey.toLowerCase();
|
||||
if (selectedPubkeys.contains(normalized)) {
|
||||
// The current selection, not this build's: a second tap can land
|
||||
// before the rebuild that reflects the first.
|
||||
if (selectedUsers.value.any(
|
||||
(selected) => selected.pubkey.toLowerCase() == normalized,
|
||||
)) {
|
||||
selectedUsers.value = [
|
||||
for (final selected in selectedUsers.value)
|
||||
if (selected.pubkey.toLowerCase() != normalized) selected,
|
||||
|
||||
@@ -461,7 +461,9 @@ class _NewDirectMessageSheet extends HookConsumerWidget {
|
||||
},
|
||||
onToggle: (pubkey) {
|
||||
if (isSubmitting.value) return;
|
||||
if (selectedPubkeys.contains(pubkey)) {
|
||||
// The current selection, not this build's: a second tap can land
|
||||
// before the rebuild that reflects the first.
|
||||
if (selectedUsers.value.any((user) => user.pubkey == pubkey)) {
|
||||
selectedUsers.value = selectedUsers.value
|
||||
.where((user) => user.pubkey != pubkey)
|
||||
.toList();
|
||||
|
||||
@@ -64,6 +64,22 @@ void registerNativeCreationTests(Widget Function(List<Override>) buildPage) {
|
||||
expect(found['detail'], isNull);
|
||||
await event('toggle', {'pubkey': _dmKey(2)});
|
||||
await event('query', {'value': 'Person'});
|
||||
// Two taps before Flutter rebuilds still never list a person twice.
|
||||
for (var tap = 0; tap < 2; tap++) {
|
||||
await messenger.handlePlatformMessage(
|
||||
bridge.name,
|
||||
const StandardMethodCodec().encodeMethodCall(
|
||||
MethodCall('toggle', {'session': session, 'pubkey': _dmKey(3)}),
|
||||
),
|
||||
(_) {},
|
||||
);
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
final picked = [
|
||||
for (final user in states.last['selected'] as List)
|
||||
(user as Map)['pubkey'],
|
||||
];
|
||||
expect(picked.toSet(), hasLength(picked.length));
|
||||
for (final key in people.keys) {
|
||||
await event('toggle', {'pubkey': key});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user