Files
buzz/mobile/ios/Runner/NativePresenceMenu.swift
klopez4212 7e8aac4403 fix(mobile): polish channel management and iOS Settings (#8171)
## Summary

Use native iOS sheets for channel editing and Add Members, with profile
avatars, matching search, and destructive removal dialogs. Canvas
content becomes a tappable preview with Add/Remove actions.

Fix the Settings availability menu and clipped copy confirmation. Align
channel action messages with chat styling and keep agent identities
distinguishable across selection, attribution, and profile surfaces.

### Related issue

No duplicate found in the existing mobile Settings/channel issues and
PRs.

### Testing

- Flutter analysis, formatting, file-size checks, focused collision
regressions, and the full mobile test suite passed.
- Production UIKit picker simulator check passed: distinct
visible/accessibility labels, avatar updates, selected rows, and pubkey
callbacks. Wired into Mobile Swift CI.
- Original UI was built and tested on the physical iPhone with author
confirmation; profile/community data preserved.
- Review fixes await renewed author device confirmation before restoring
`buzz-review-completed` and ready-for-review status.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-08 14:32:55 -04:00

80 lines
3.8 KiB
Swift

import Flutter
import UIKit
final class NativePresenceMenuFactory: NSObject, FlutterPlatformViewFactory {
private let messenger: FlutterBinaryMessenger
init(messenger: FlutterBinaryMessenger) { self.messenger = messenger; super.init() }
func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { FlutterStandardMessageCodec.sharedInstance() }
func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView {
NativePresenceMenu(frame: frame, id: viewId, args: args, messenger: messenger)
}
}
private final class NativePresenceMenu: NSObject, FlutterPlatformView {
private let container: UIView
private let button = UIButton(type: .system)
private let channel: FlutterMethodChannel
init(frame: CGRect, id: Int64, args: Any?, messenger: FlutterBinaryMessenger) {
container = UIView(frame: frame)
channel = FlutterMethodChannel(name: "buzz/presence_menu/\(id)", binaryMessenger: messenger)
super.init()
container.backgroundColor = .clear
container.isOpaque = false
button.translatesAutoresizingMaskIntoConstraints = false
button.showsMenuAsPrimaryAction = true
container.addSubview(button)
NSLayoutConstraint.activate([
button.leadingAnchor.constraint(equalTo: container.leadingAnchor),
button.trailingAnchor.constraint(equalTo: container.trailingAnchor),
button.topAnchor.constraint(equalTo: container.topAnchor),
button.bottomAnchor.constraint(equalTo: container.bottomAnchor),
])
update(args as? [String: Any] ?? [:])
channel.setMethodCallHandler { [weak self] call, result in
guard call.method == "update", let data = call.arguments as? [String: Any] else {
result(FlutterMethodNotImplemented); return
}
self?.update(data)
result(nil)
}
}
func view() -> UIView { container }
private func update(_ data: [String: Any]) {
let presence = data["presence"] as? String ?? "offline"
let label = data["label"] as? String ?? "Offline"
container.overrideUserInterfaceStyle = data["dark"] as? Bool == true ? .dark : .light
var configuration = UIButton.Configuration.plain()
configuration.title = label
configuration.baseForegroundColor = Self.color(data["foreground"])
configuration.background.backgroundColor = Self.color(data["background"])
configuration.background.cornerRadius = 100
configuration.background.backgroundInsets = NSDirectionalEdgeInsets(top: 7, leading: 0, bottom: 7, trailing: 0)
configuration.contentInsets = NSDirectionalEdgeInsets(top: 7, leading: 12, bottom: 7, trailing: 12)
let fontSize = (data["fontSize"] as? NSNumber)?.doubleValue ?? 15
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
var attributes = incoming
attributes.font = UIFont.systemFont(ofSize: fontSize, weight: .medium)
return attributes
}
button.configuration = configuration
button.accessibilityLabel = "Availability"
button.accessibilityValue = label
let options: [(String, String, UIColor)] = [("online", "Online", .systemGreen), ("away", "Away", .systemOrange), ("offline", "Offline", .systemGray)]
button.menu = UIMenu(children: options.map { value, title, color in
UIAction(title: title,
image: UIImage(systemName: "circle.fill")?.withTintColor(color, renderingMode: .alwaysOriginal),
state: presence == value ? .on : .off
) { [weak self] _ in self?.channel.invokeMethod("selected", arguments: value) }
})
}
private static func color(_ value: Any?) -> UIColor {
let argb = (value as? NSNumber)?.uint32Value ?? 0
return UIColor(red: CGFloat((argb >> 16) & 255) / 255, green: CGFloat((argb >> 8) & 255) / 255,
blue: CGFloat(argb & 255) / 255, alpha: CGFloat((argb >> 24) & 255) / 255)
}
}