fix(mobile): keep New DM avatars steady across picker updates

Every state update from Dart reloads the native member picker, which built
fresh cells showing the initials fallback and fetched each avatar again over
the platform channel, so avatars blinked whenever results refreshed, most
visibly right after foregrounding. Keep loaded avatars by key and draw them
at once when a cell is rebuilt.

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:
Tom Brow
2026-11-07 16:18:38 -08:00
parent d839d3731a
commit e3e169afcc
2 changed files with 20 additions and 8 deletions

View File

@@ -37,6 +37,12 @@ struct AddMembersIdentityTests {
picker.tableView(picker.tableView, didSelectRowAt: path)
}
precondition(selected == keys, "Each distinct label must select its own pubkey")
avatarReplies.removeAll()
picker.update(["users": users, "selected": [], "query": "", "loading": false])
let rebuilt = picker.tableView(picker.tableView, cellForRowAt: IndexPath(row: 0, section: 1))
let rebuiltImage = (rebuilt.contentConfiguration as? UIListContentConfiguration)?.image
precondition(avatarReplies.isEmpty, "A loaded avatar is not fetched again on update")
precondition(rebuiltImage?.pngData() == UIImage(data: avatar!)?.pngData(), "A rebuilt row draws its loaded avatar at once")
picker.update(["users": users, "selected": [users[1]], "query": "", "loading": false])
let chosen = picker.tableView(picker.tableView, cellForRowAt: IndexPath(row: 0, section: 0))
precondition(chosen.accessibilityLabel == "\(labels[1]), Same owner", "Selected identity retains its qualifier")

View File

@@ -97,6 +97,10 @@ final class NativeAddMembersViewController: UITableViewController, UISearchResul
private var state: [String: Any]
private let send: (String, [String: Any]) -> Void
private let loadAvatar: (String, String, @escaping (Data?) -> Void) -> Void
/// Loaded avatars by key. Every state update rebuilds the cells, so a row
/// draws its known avatar at once instead of blinking to its initial while
/// the image comes back over the channel.
private var avatars: [String: UIImage] = [:]
private let search = UISearchController(searchResultsController: nil)
private let pageColor: UIColor
private let rowColor: UIColor
@@ -282,12 +286,12 @@ final class NativeAddMembersViewController: UITableViewController, UISearchResul
.compactMap { $0 }.filter { !$0.isEmpty }.joined(separator: ", ")
content.textProperties.numberOfLines = 0
content.secondaryTextProperties.numberOfLines = 1
content.image = fallbackAvatar(user)
content.imageProperties.maximumSize = CGSize(width: 40, height: 40)
content.imageProperties.reservedLayoutSize = CGSize(width: 40, height: 40)
if let pubkey = user["pubkey"] as? String, let key = user["avatarKey"] as? String {
avatarRequest = (pubkey, key)
}
content.image = avatarRequest.flatMap { avatars[$0.1] } ?? fallbackAvatar(user)
content.imageProperties.maximumSize = CGSize(width: 40, height: 40)
content.imageProperties.reservedLayoutSize = CGSize(width: 40, height: 40)
cell.accessoryType = indexPath.section == 0 ? .checkmark : .none
if unavailable {
content.textProperties.color = .secondaryLabel
@@ -303,13 +307,15 @@ final class NativeAddMembersViewController: UITableViewController, UISearchResul
cell.accessibilityLabel = memberLabel
cell.contentView.accessibilityElementsHidden = true
}
if let (pubkey, key) = avatarRequest {
cell.accessibilityIdentifier = key
loadAvatar(pubkey, key) { [weak cell] data in
if let (_, key) = avatarRequest { cell.accessibilityIdentifier = key }
if let (pubkey, key) = avatarRequest, avatars[key] == nil {
loadAvatar(pubkey, key) { [weak self, weak cell] data in
guard let data, let image = UIImage(data: data) else { return }
let avatar = image.withRenderingMode(.alwaysOriginal)
self?.avatars[key] = avatar
guard let cell, cell.accessibilityIdentifier == key,
let data, let image = UIImage(data: data),
var configuration = cell.contentConfiguration as? UIListContentConfiguration else { return }
configuration.image = image.withRenderingMode(.alwaysOriginal)
configuration.image = avatar
let label = cell.accessibilityLabel
cell.contentConfiguration = configuration
cell.accessibilityLabel = label