import Flutter import UIKit final class NativeProfileTextEditorCoordinator: NSObject, UIAdaptivePresentationControllerDelegate { private let channel: FlutterMethodChannel private weak var parentViewController: UIViewController? private weak var presentedController: UIViewController? private var pendingResult: FlutterResult? init( messenger: FlutterBinaryMessenger, parentViewController: UIViewController? ) { channel = FlutterMethodChannel( name: "buzz/profile_text_editor", binaryMessenger: messenger ) self.parentViewController = parentViewController super.init() channel.setMethodCallHandler { [weak self] call, result in self?.handle(call, result: result) } } private func handle( _ call: FlutterMethodCall, result: @escaping FlutterResult ) { guard call.method == "present" || call.method == "presentChannel" || call.method == "presentChat" || call.method == "presentProject" else { result(FlutterMethodNotImplemented) return } guard let arguments = call.arguments as? [String: Any], let title = arguments["title"] as? String, let initialValue = arguments["initialValue"] as? String, let placeholder = arguments["placeholder"] as? String, let multiline = arguments["multiline"] as? Bool, let brightness = arguments["brightness"] as? String, let pageBackgroundArgb = arguments["pageBackgroundArgb"] as? NSNumber, let containerBackgroundArgb = arguments["containerBackgroundArgb"] as? NSNumber, let containerCornerRadius = arguments["containerCornerRadius"] as? NSNumber else { result( FlutterError( code: "invalid_arguments", message: "Expected profile text editor configuration.", details: nil ) ) return } let allowUnchangedSubmission = arguments["allowUnchangedSubmission"] as? Bool ?? false DispatchQueue.main.async { [weak self] in self?.present( title: title, initialValue: initialValue, placeholder: placeholder, multiline: multiline, brightness: brightness, pageBackgroundColor: UIColor(argb: pageBackgroundArgb.uint32Value), containerBackgroundColor: UIColor(argb: containerBackgroundArgb.uint32Value), containerCornerRadius: CGFloat(containerCornerRadius.doubleValue), allowUnchangedSubmission: allowUnchangedSubmission, channelDescription: call.method == "presentChannel" ? arguments["description"] as? String : nil, originalName: arguments["originalName"] as? String, originalDescription: arguments["originalDescription"] as? String, canEditDetails: arguments["canEditDetails"] as? Bool ?? true, canEditCanvas: arguments["canEditCanvas"] as? Bool ?? false, canvasContent: arguments["canvasContent"] as? String ?? "", canvasLoaded: arguments["canvasLoaded"] as? Bool ?? false, chatOptions: (call.method == "presentChat" || call.method == "presentProject") ? arguments.merging(["projectOnly": call.method == "presentProject"]) { _, new in new } : nil, result: result ) } } @MainActor private func present( title: String, initialValue: String, placeholder: String, multiline: Bool, brightness: String, pageBackgroundColor: UIColor, containerBackgroundColor: UIColor, containerCornerRadius: CGFloat, allowUnchangedSubmission: Bool, channelDescription: String?, originalName: String?, originalDescription: String?, canEditDetails: Bool, canEditCanvas: Bool, canvasContent: String, canvasLoaded: Bool, chatOptions: [String: Any]?, result: @escaping FlutterResult ) { guard presentedController == nil else { result( FlutterError( code: "already_presented", message: "A profile editor is already open.", details: nil ) ) return } guard let presenter = topViewController( from: parentViewController ?? activeWindowRootViewController() ) else { result( FlutterError( code: "presentation_failed", message: "Unable to find a view controller for the profile editor.", details: nil ) ) return } let editor = NativeProfileTextEditorViewController( title: title, initialValue: initialValue, placeholder: placeholder, multiline: multiline, pageBackgroundColor: pageBackgroundColor, containerBackgroundColor: containerBackgroundColor, containerCornerRadius: containerCornerRadius, allowUnchangedSubmission: allowUnchangedSubmission, channelDescription: channelDescription, originalName: originalName, originalDescription: originalDescription, canEditDetails: canEditDetails, canEditCanvas: canEditCanvas, canvasContent: canvasContent, canvasLoaded: canvasLoaded, chatOptions: chatOptions, onCancel: { [weak self] in self?.finish(value: nil) }, onSet: { [weak self] value in self?.finish(value: value) } ) let navigationController = UINavigationController(rootViewController: editor) navigationController.overrideUserInterfaceStyle = brightness == "dark" ? .dark : .light if UIDevice.current.userInterfaceIdiom == .pad { navigationController.modalPresentationStyle = .formSheet } if channelDescription != nil { navigationController.sheetPresentationController?.detents = [.large()] } pendingResult = result presentedController = navigationController presenter.present(navigationController, animated: true) { [weak self] in navigationController.presentationController?.delegate = self } } @MainActor private func finish(value: Any?) { guard let controller = presentedController else { resolve(value: value) return } controller.dismiss(animated: true) { [weak self] in self?.resolve(value: value) } } func presentationControllerDidDismiss( _ presentationController: UIPresentationController ) { resolve(value: nil) } @MainActor private func resolve(value: Any?) { let result = pendingResult pendingResult = nil presentedController = nil result?(value) } @MainActor private func activeWindowRootViewController() -> UIViewController? { UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } .filter { $0.activationState == .foregroundActive } .flatMap(\.windows) .first(where: \.isKeyWindow)? .rootViewController } @MainActor private func topViewController( from viewController: UIViewController? ) -> UIViewController? { if let navigationController = viewController as? UINavigationController { return topViewController(from: navigationController.visibleViewController) } if let tabBarController = viewController as? UITabBarController { return topViewController(from: tabBarController.selectedViewController) } if let presentedViewController = viewController?.presentedViewController { return topViewController(from: presentedViewController) } return viewController } } private final class NativeProfileTextEditorViewController: UITableViewController, UITextFieldDelegate, UITextViewDelegate { private let initialValue: String private let placeholder: String private let multiline: Bool private let pageBackgroundColor: UIColor private let containerBackgroundColor: UIColor private let containerCornerRadius: CGFloat private let allowUnchangedSubmission: Bool private let onCancel: () -> Void private let onSet: (Any) -> Void private let channelDescription: String? private let originalName: String? private let originalDescription: String? private let canEditDetails: Bool private let canEditCanvas: Bool private let canvasContent: String private let canvasLoaded: Bool private var hasCanvas: Bool { canvasLoaded && !canvasContent.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } private let chatOptions: [String: Any]? private var hasFocusedEditor = false private var selectedAssistantAvatar: UIImage? private var selectedAssistant: String? private var isChatEditor: Bool { chatOptions != nil } private var isProjectEditor: Bool { chatOptions?["projectOnly"] as? Bool ?? false } private var assistants: [[String: String]] { chatOptions?["assistants"] as? [[String: String]] ?? [] } private var fieldsLocked: Bool { chatOptions?["fieldsLocked"] as? Bool ?? false } private var isChannelEditor: Bool { channelDescription != nil } private lazy var textField: UITextField = { let field = UITextField() field.translatesAutoresizingMaskIntoConstraints = false field.placeholder = placeholder field.text = initialValue field.font = .preferredFont(forTextStyle: .body) field.adjustsFontForContentSizeCategory = true field.clearButtonMode = .whileEditing field.returnKeyType = isChannelEditor ? .next : .done field.isEnabled = canEditDetails && !fieldsLocked field.accessibilityLabel = placeholder field.autocapitalizationType = .sentences field.delegate = self field.addTarget(self, action: #selector(textDidChange), for: .editingChanged) return field }() private lazy var textView: UITextView = { let view = UITextView() view.translatesAutoresizingMaskIntoConstraints = false view.text = channelDescription ?? initialValue view.isEditable = canEditDetails view.accessibilityLabel = isChannelEditor ? "Description" : placeholder view.font = .preferredFont(forTextStyle: .body) view.adjustsFontForContentSizeCategory = true view.backgroundColor = .clear view.textContainerInset = .zero view.textContainer.lineFragmentPadding = 0 view.autocapitalizationType = .sentences view.delegate = self return view }() private lazy var placeholderLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.text = isChannelEditor ? "Description" : placeholder label.font = .preferredFont(forTextStyle: .body) label.adjustsFontForContentSizeCategory = true label.textColor = .placeholderText label.numberOfLines = 0 return label }() init( title: String, initialValue: String, placeholder: String, multiline: Bool, pageBackgroundColor: UIColor, containerBackgroundColor: UIColor, containerCornerRadius: CGFloat, allowUnchangedSubmission: Bool, channelDescription: String?, originalName: String?, originalDescription: String?, canEditDetails: Bool, canEditCanvas: Bool, canvasContent: String, canvasLoaded: Bool, chatOptions: [String: Any]?, onCancel: @escaping () -> Void, onSet: @escaping (Any) -> Void ) { self.initialValue = initialValue self.placeholder = placeholder self.multiline = multiline self.pageBackgroundColor = pageBackgroundColor self.containerBackgroundColor = containerBackgroundColor self.containerCornerRadius = containerCornerRadius self.allowUnchangedSubmission = allowUnchangedSubmission self.channelDescription = channelDescription self.originalName = originalName self.originalDescription = originalDescription self.canEditDetails = canEditDetails self.canEditCanvas = canEditCanvas self.canvasContent = canvasContent self.canvasLoaded = canvasLoaded self.chatOptions = chatOptions self.selectedAssistant = chatOptions?["selectedAssistant"] as? String self.onCancel = onCancel self.onSet = onSet super.init(style: .insetGrouped) self.title = title } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = pageBackgroundColor tableView.backgroundColor = pageBackgroundColor tableView.keyboardDismissMode = .interactive tableView.alwaysBounceVertical = false let navigationAppearance = UINavigationBarAppearance() navigationAppearance.configureWithOpaqueBackground() navigationAppearance.backgroundColor = pageBackgroundColor navigationAppearance.shadowColor = UIColor.separator.withAlphaComponent(0.2) let scrollEdgeAppearance = UINavigationBarAppearance() scrollEdgeAppearance.configureWithOpaqueBackground() scrollEdgeAppearance.backgroundColor = pageBackgroundColor scrollEdgeAppearance.shadowColor = .clear navigationItem.standardAppearance = navigationAppearance navigationItem.scrollEdgeAppearance = scrollEdgeAppearance navigationItem.compactAppearance = navigationAppearance navigationItem.compactScrollEdgeAppearance = scrollEdgeAppearance navigationItem.leftBarButtonItem = UIBarButtonItem( barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped) ) navigationItem.rightBarButtonItem = UIBarButtonItem( title: isChatEditor ? (fieldsLocked ? "Retry" : "Create") : (isChannelEditor ? "Save" : "Set"), style: .done, target: self, action: #selector(setTapped) ) updateNavigation() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) guard canEditDetails && !hasFocusedEditor else { return } hasFocusedEditor = true if multiline { textView.becomeFirstResponder() } else { textField.becomeFirstResponder() } } private var currentValue: String { multiline ? textView.text : (textField.text ?? "") } private var hasUnsavedChanges: Bool { if isChatEditor { return !currentValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || selectedAssistant != nil } if isChannelEditor { return allowUnchangedSubmission || currentValue.trimmingCharacters(in: .whitespacesAndNewlines) != (originalName ?? initialValue).trimmingCharacters(in: .whitespacesAndNewlines) || textView.text.trimmingCharacters(in: .whitespacesAndNewlines) != (originalDescription ?? channelDescription ?? "").trimmingCharacters(in: .whitespacesAndNewlines) } return allowUnchangedSubmission || currentValue.trimmingCharacters(in: .whitespacesAndNewlines) != initialValue.trimmingCharacters(in: .whitespacesAndNewlines) } override var isModalInPresentation: Bool { get { hasUnsavedChanges } set {} } @objc private func textDidChange() { updateNavigation() } func textViewDidChange(_ textView: UITextView) { updateNavigation() } private func updateNavigation() { navigationItem.rightBarButtonItem?.isEnabled = canEditDetails && hasUnsavedChanges && (!isChannelEditor || !canonicalChannelName.isEmpty) && (!isChatEditor || ( !currentValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && currentValue.count <= 120 && (isProjectEditor || assistants.contains { $0["id"] == selectedAssistant }) )) navigationController?.isModalInPresentation = hasUnsavedChanges placeholderLabel.isHidden = !textView.text.isEmpty } @objc private func cancelTapped() { guard hasUnsavedChanges else { onCancel() return } let alert = UIAlertController( title: "Discard changes?", message: nil, preferredStyle: .actionSheet ) alert.addAction(UIAlertAction(title: "Keep Editing", style: .cancel)) alert.addAction( UIAlertAction(title: "Discard", style: .destructive) { [weak self] _ in self?.onCancel() } ) if let popover = alert.popoverPresentationController { popover.barButtonItem = navigationItem.leftBarButtonItem } present(alert, animated: true) } private var canonicalChannelName: String { currentValue.trimmingCharacters(in: .whitespacesAndNewlines) .replacingOccurrences(of: "^#+", with: "", options: .regularExpression) .trimmingCharacters(in: .whitespacesAndNewlines) } @objc private func setTapped() { guard canEditDetails && hasUnsavedChanges else { return } if isProjectEditor { let name = currentValue.trimmingCharacters(in: .whitespacesAndNewlines) guard !name.isEmpty, currentValue.count <= 120 else { return } onSet(name) } else if isChatEditor { guard let selectedAssistant, assistants.contains(where: { $0["id"] == selectedAssistant }), !currentValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, currentValue.count <= 120 else { return } onSet(["name": currentValue, "assistant": selectedAssistant]) } else if isChannelEditor { guard !canonicalChannelName.isEmpty else { return } onSet(["action": "save", "name": canonicalChannelName, "description": textView.text ?? ""]) } else { onSet(currentValue) } } @objc private func editCanvasTapped() { guard canEditCanvas else { return } onSet(["action": "canvas", "name": currentValue, "description": textView.text ?? ""]) } private func removeCanvasTapped() { guard canEditCanvas && hasCanvas else { return } let alert = UIAlertController( title: "Remove canvas?", message: "This removes the canvas content for everyone in this channel.", preferredStyle: .alert ) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) alert.addAction(UIAlertAction(title: "Remove", style: .destructive) { [weak self] _ in guard let self else { return } self.onSet(["action": "removeCanvas", "name": self.currentValue, "description": self.textView.text ?? ""]) }) present(alert, animated: true) } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) if isChatEditor && indexPath.section == 1 { guard !fieldsLocked && !assistants.isEmpty else { return } view.endEditing(true) let picker = MeAssistantListController(assistants: assistants, selected: selectedAssistant, pageColor: pageBackgroundColor, rowColor: containerBackgroundColor) { [weak self] id, avatar in self?.selectedAssistantAvatar = avatar self?.selectedAssistant = id self?.tableView.reloadSections(IndexSet(integer: 1), with: .none) self?.updateNavigation() } navigationController?.pushViewController(picker, animated: true) return } guard isChannelEditor && canEditCanvas else { return } if indexPath.section == 2 { editCanvasTapped() } if indexPath.section == 3 { removeCanvasTapped() } } func textFieldShouldReturn(_ textField: UITextField) -> Bool { if isChannelEditor { textView.becomeFirstResponder() } else { setTapped() } return false } override func numberOfSections(in tableView: UITableView) -> Int { isProjectEditor ? 1 : isChatEditor ? 2 : isChannelEditor ? (hasCanvas && canEditCanvas ? 4 : 3) : 1 } override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { guard isChatEditor && section == 1 else { return nil } if let error = chatOptions?["error"] as? String { return error } return assistants.isEmpty ? "No agents are available for this account. Connect an agent on desktop, then try again." : nil } override func tableView( _ tableView: UITableView, numberOfRowsInSection section: Int ) -> Int { 1 } override func tableView( _ tableView: UITableView, heightForRowAt indexPath: IndexPath ) -> CGFloat { if isChatEditor && indexPath.section == 1 && selectedAssistant != nil { return 64 } if isChannelEditor && indexPath.section >= 2 { return UITableView.automaticDimension } return (isChannelEditor ? indexPath.section == 1 : multiline) ? 132 : 52 } override func tableView( _ tableView: UITableView, cellForRowAt indexPath: IndexPath ) -> UITableViewCell { let cell = UITableViewCell(style: .default, reuseIdentifier: nil) cell.selectionStyle = .none cell.backgroundColor = containerBackgroundColor cell.layer.cornerRadius = containerCornerRadius cell.layer.cornerCurve = .continuous cell.clipsToBounds = true if isChatEditor && indexPath.section == 1 { var content = cell.defaultContentConfiguration() let selected = assistants.first { $0["id"] == selectedAssistant } content.text = selected?["name"] ?? "Choose an agent" if let selected { content.image = selectedAssistantAvatar ?? MeAssistantListController.avatarImage(selected, photo: nil) } content.textProperties.font = .preferredFont(forTextStyle: .body) cell.contentConfiguration = content cell.accessoryType = .disclosureIndicator cell.selectionStyle = fieldsLocked || assistants.isEmpty ? .none : .default cell.accessibilityTraits = fieldsLocked || assistants.isEmpty ? .notEnabled : .button } else if isChannelEditor && indexPath.section >= 2 { var content = cell.defaultContentConfiguration() content.textProperties.font = .preferredFont(forTextStyle: .body) if indexPath.section == 3 { content.text = "Remove Canvas" content.textProperties.color = .systemRed } else if hasCanvas { content.text = "Canvas" content.secondaryText = String(canvasContent.prefix(600)) content.secondaryTextProperties.font = .preferredFont(forTextStyle: .body) content.secondaryTextProperties.numberOfLines = 4 cell.accessoryType = canEditCanvas ? .disclosureIndicator : .none cell.accessibilityHint = canEditCanvas ? "Opens the canvas editor" : nil } else { content.text = canvasLoaded ? "Add Canvas" : "Retry loading canvas" content.textProperties.color = canEditCanvas ? .label : .secondaryLabel cell.accessoryType = canEditCanvas ? .disclosureIndicator : .none } cell.contentConfiguration = content cell.selectionStyle = canEditCanvas ? .default : .none cell.isUserInteractionEnabled = canEditCanvas cell.accessibilityTraits = canEditCanvas ? .button : .staticText } else if isChannelEditor ? indexPath.section == 1 : multiline { cell.contentView.addSubview(textView) textView.addSubview(placeholderLabel) NSLayoutConstraint.activate([ textView.leadingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.leadingAnchor), textView.trailingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.trailingAnchor), textView.topAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.topAnchor), textView.bottomAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.bottomAnchor), placeholderLabel.leadingAnchor.constraint(equalTo: textView.leadingAnchor), placeholderLabel.trailingAnchor.constraint(equalTo: textView.trailingAnchor), placeholderLabel.topAnchor.constraint(equalTo: textView.topAnchor), ]) placeholderLabel.isHidden = !textView.text.isEmpty } else { cell.contentView.addSubview(textField) NSLayoutConstraint.activate([ textField.leadingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.leadingAnchor), textField.trailingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.trailingAnchor), textField.topAnchor.constraint(equalTo: cell.contentView.topAnchor), textField.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor), ]) } return cell } } private extension UIColor { convenience init(argb: UInt32) { self.init( red: CGFloat((argb >> 16) & 0xff) / 255, green: CGFloat((argb >> 8) & 0xff) / 255, blue: CGFloat(argb & 0xff) / 255, alpha: CGFloat((argb >> 24) & 0xff) / 255 ) } } /// A member-style list inside the New chat sheet, rather than an action menu. private final class MeAssistantListController: UITableViewController { private let assistants: [[String: String]] private let selected: String? private let onSelect: (String, UIImage) -> Void private let pageColor: UIColor private let rowColor: UIColor private let avatars: MeAvatarLoader private var requests: [IndexPath: UUID] = [:] init(assistants: [[String: String]], selected: String?, pageColor: UIColor, rowColor: UIColor, avatars: MeAvatarLoader = MeAvatarLoader(), onSelect: @escaping (String, UIImage) -> Void) { self.assistants = assistants self.selected = selected self.onSelect = onSelect self.pageColor = pageColor self.rowColor = rowColor self.avatars = avatars super.init(style: .insetGrouped) title = "Agent" } required init?(coder: NSCoder) { nil } deinit { avatars.cancelAll() } override func viewDidLoad() { super.viewDidLoad() tableView.backgroundColor = pageColor tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 64 } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tableView.reloadData() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) avatars.cancelAll() requests.removeAll() } private func avatarURL(_ assistant: [String: String]) -> URL? { assistant["avatarUrl"].flatMap(URL.init(string:)) } override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { let assistant = assistants[indexPath.row] guard let url = avatarURL(assistant) else { return } requests[indexPath] = avatars.load(url) { [weak self, weak cell] image in guard self != nil, let cell, tableView.indexPath(for: cell) == indexPath else { return } var content = cell.defaultContentConfiguration() content.text = assistant["name"] ?? "Agent" content.image = Self.avatarImage(assistant, photo: image) cell.contentConfiguration = content } } override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { if let request = requests.removeValue(forKey: indexPath) { avatars.cancel(request) } } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { assistants.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let assistant = assistants[indexPath.row] let cell = UITableViewCell(style: .default, reuseIdentifier: nil) var content = cell.defaultContentConfiguration() let name = assistant["name"] ?? "Agent" content.text = name content.image = Self.avatarImage(assistant, photo: avatars.image(for: avatarURL(assistant))) cell.backgroundColor = rowColor cell.contentConfiguration = content cell.accessoryType = assistant["id"] == selected ? .checkmark : .none cell.accessibilityTraits = assistant["id"] == selected ? [.button, .selected] : .button return cell } static func avatarImage(_ assistant: [String: String], photo: UIImage?) -> UIImage { let name = assistant["name"] ?? "Agent" return UIGraphicsImageRenderer(size: CGSize(width: 40, height: 40)).image { _ in let bounds = CGRect(x: 0, y: 0, width: 40, height: 40) UIBezierPath(roundedRect: bounds, cornerRadius: 12).addClip() UIColor.secondarySystemFill.setFill() UIRectFill(bounds) if let photo { let artworkScale = CGFloat(Double(assistant["artworkScale"] ?? "1") ?? 1) let scale = max(40 / photo.size.width, 40 / photo.size.height) * artworkScale let size = CGSize(width: photo.size.width * scale, height: photo.size.height * scale) photo.draw(in: CGRect(x: (40 - size.width) / 2, y: (40 - size.height) / 2, width: size.width, height: size.height)) } else { let initial = String(name.prefix(1)).uppercased() as NSString let attributes: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 18, weight: .medium), .foregroundColor: UIColor.label] let size = initial.size(withAttributes: attributes) initial.draw(at: CGPoint(x: (40 - size.width) / 2, y: (40 - size.height) / 2), withAttributes: attributes) } }.withRenderingMode(.alwaysOriginal) } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let id = assistants[indexPath.row]["id"] else { return } onSelect(id, Self.avatarImage(assistants[indexPath.row], photo: avatars.image(for: avatarURL(assistants[indexPath.row])))) navigationController?.popViewController(animated: true) } }