mirror of
https://github.com/block/buzz.git
synced 2026-11-06 17:31:19 +00:00
Mobile had two New chat screens. [#8884](https://github.com/block/buzz/pull/8884) made the Flutter draft page (first send creates and names the chat) the New chat screen, but kept the older create form (a native iOS sheet, with a Flutter bottom sheet on Android) for one case: resuming a creation that an older build had saved with a chat name and no first message. Nothing creates that state anymore, so the old form only appeared for users with an interrupted creation from before that change. This removes the old form. Every New chat, including that resume case, now opens the draft page: - **New chat**: unchanged. - **Resuming an interrupted creation**: after the existing Resume / Discard setup / Later prompt, the draft page shows the saved chat with its agent and workspace locked and a Retry button, the same choices the old sheet offered when it locked its fields. A creation saved with a first message resumes exactly as before. One saved without a message finishes without one, because a saved creation can't change after its first relay write. The iOS editor's chat-only code (`presentChat`, `updateChatAssistants`, the agent list, and its avatar loader) goes too. The same editor still handles profile fields, channel details and workspace names. The native test and CI step that covered only the removed chat form are removed. ### Screenshots Resuming an interrupted creation saved without a first message: | Before: old native sheet (removed) | After: draft page (kept) | | --- | --- | | <img src="https://raw.githubusercontent.com/block/buzz/5d0a502e92fc3dfeffbe4fa95d63ba8f73d1e612/pr-9079--01-before-resume-native-sheet.png" width="300"> | <img src="https://raw.githubusercontent.com/block/buzz/5d0a502e92fc3dfeffbe4fa95d63ba8f73d1e612/pr-9079--02-after-resume-draft-page.png" width="300"> | A brand new chat already opened the draft page and is unchanged: <img src="https://raw.githubusercontent.com/block/buzz/5d0a502e92fc3dfeffbe4fa95d63ba8f73d1e612/pr-9079--03-new-chat-draft-page.png" width="300"> ### Validation - Changed tests: `me_page_test` checks that Resume and Discard setup both open the draft page and that no native sheet is requested. `solo_creation_tags_test` checks that recovering an interrupted named creation through the draft page's Retry continues with member setup, keeps the saved name and agent, and adds no first message. - Full mobile Flutter suite (4,431 passed) and `flutter analyze` (clean). iOS debug simulator build succeeds. - Automated simulator run (iPhone 17 Pro, iOS 27, signed-in account): New chat opened the draft page, the agent picker selected an agent, and the first send created the chat, which then appeared under Solo. New workspace still opened the native editor, enabled Create once named, and showed the Discard changes prompt on cancel. The recording isn't attached here because it shows internal account content. - Resuming an interrupted pre-[#8884](https://github.com/block/buzz/pull/8884) creation can't be produced by the current build. For the screenshots above, a capture-only, uncommitted patch to a `main` build seeded one; the branch build installed over it resumed it on the draft page. The widget tests cover finishing it. - `solo_creation_tags_test` also covers a conflicting pending Solo organization edit during that Retry: the draft offers the Conflicting Solo changes dialog, keeps the saved creation, and the next Retry finishes it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) buzz-review-completed --------- Signed-off-by: Tom Brow <tomb@squareup.com> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
574 lines
20 KiB
Swift
574 lines
20 KiB
Swift
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 == "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,
|
|
isProjectEditor: call.method == "presentProject",
|
|
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,
|
|
isProjectEditor: Bool,
|
|
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,
|
|
isProjectEditor: isProjectEditor,
|
|
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 isProjectEditor: Bool
|
|
private var hasFocusedEditor = 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
|
|
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,
|
|
isProjectEditor: Bool,
|
|
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.isProjectEditor = isProjectEditor
|
|
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
|
|
NavigationTopEdge.configure(tableView, item: navigationItem)
|
|
navigationItem.leftBarButtonItem = UIBarButtonItem(
|
|
barButtonSystemItem: .cancel,
|
|
target: self,
|
|
action: #selector(cancelTapped)
|
|
)
|
|
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
|
title: isProjectEditor ? "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 isProjectEditor {
|
|
return !currentValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
}
|
|
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)
|
|
&& (!isProjectEditor || (
|
|
!currentValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
&& currentValue.count <= 120
|
|
))
|
|
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 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)
|
|
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 { isChannelEditor ? (hasCanvas && canEditCanvas ? 4 : 3) : 1 }
|
|
|
|
override func tableView(
|
|
_ tableView: UITableView,
|
|
numberOfRowsInSection section: Int
|
|
) -> Int { 1 }
|
|
|
|
override func tableView(
|
|
_ tableView: UITableView,
|
|
heightForRowAt indexPath: IndexPath
|
|
) -> CGFloat {
|
|
if isChannelEditor && indexPath.section >= 2 { return UITableView.automaticDimension }
|
|
return (isChannelEditor ? indexPath.section == 1 : multiline && indexPath.section == 0) ? 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 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
|
|
)
|
|
}
|
|
}
|