From c3ed24dfe73f41f03ccd537b4033515f435d6856 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 23 Jan 2023 16:50:41 -0500 Subject: [PATCH 1/2] Move the compose toolbar icon picking logic into the view --- .../xcshareddata/swiftpm/Package.resolved | 24 +-- .../ComposeContentToolbarView+ViewModel.swift | 68 ------- .../Toolbar/ComposeContentToolbarView.swift | 184 +++++++++++------- 3 files changed, 122 insertions(+), 154 deletions(-) diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 232ec9c871..03b618b0e1 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/Alamofire/Alamofire.git", "state": { "branch": null, - "revision": "8dd85aee02e39dd280c75eef88ffdb86eed4b07b", - "version": "5.6.2" + "revision": "354dda32d89fc8cd4f5c46487f64957d355f53d8", + "version": "5.6.1" } }, { @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/Flipboard/FLAnimatedImage.git", "state": { "branch": null, - "revision": "d4f07b6f164d53c1212c3e54d6460738b1981e9f", - "version": "1.0.17" + "revision": "e7f9fd4681ae41bf6f3056db08af4f401d61da52", + "version": "1.0.16" } }, { @@ -105,8 +105,8 @@ "repositoryURL": "https://github.com/kean/Nuke.git", "state": { "branch": null, - "revision": "a002b7fd786f2df2ed4333fe73a9727499fd9d97", - "version": "10.11.2" + "revision": "0ea7545b5c918285aacc044dc75048625c8257cc", + "version": "10.8.0" } }, { @@ -123,8 +123,8 @@ "repositoryURL": "https://github.com/uias/Pageboy", "state": { "branch": null, - "revision": "af8fa81788b893205e1ff42ddd88c5b0b315d7c5", - "version": "3.7.0" + "revision": "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6", + "version": "3.6.2" } }, { @@ -141,8 +141,8 @@ "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", "state": { "branch": null, - "revision": "3312bf5e67b52fbce7c3caf431b0cda721a9f7bb", - "version": "5.14.2" + "revision": "2e63d0061da449ad0ed130768d05dceb1496de44", + "version": "5.12.5" } }, { @@ -186,8 +186,8 @@ "repositoryURL": "https://github.com/scinfu/SwiftSoup.git", "state": { "branch": null, - "revision": "6778575285177365cbad3e5b8a72f2a20583cfec", - "version": "2.4.3" + "revision": "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886", + "version": "2.4.2" } }, { diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView+ViewModel.swift index aaf2d2dca1..4c164009b1 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView+ViewModel.swift @@ -61,40 +61,6 @@ extension ComposeContentToolbarView.ViewModel { case contentWarning case visibility case language - - var activeImage: UIImage { - switch self { - case .attachment: - return Asset.Scene.Compose.media.image.withRenderingMode(.alwaysTemplate) - case .poll: - return Asset.Scene.Compose.pollFill.image.withRenderingMode(.alwaysTemplate) - case .emoji: - return Asset.Scene.Compose.emojiFill.image.withRenderingMode(.alwaysTemplate) - case .contentWarning: - return Asset.Scene.Compose.chatWarningFill.image.withRenderingMode(.alwaysTemplate) - case .visibility: - return Asset.Scene.Compose.earth.image.withRenderingMode(.alwaysTemplate) - case .language: - fatalError("Language’s active image is never accessed") - } - } - - var inactiveImage: UIImage { - switch self { - case .attachment: - return Asset.Scene.Compose.media.image.withRenderingMode(.alwaysTemplate) - case .poll: - return Asset.Scene.Compose.poll.image.withRenderingMode(.alwaysTemplate) - case .emoji: - return Asset.Scene.Compose.emoji.image.withRenderingMode(.alwaysTemplate) - case .contentWarning: - return Asset.Scene.Compose.chatWarning.image.withRenderingMode(.alwaysTemplate) - case .visibility: - return Asset.Scene.Compose.earth.image.withRenderingMode(.alwaysTemplate) - case .language: - fatalError("Language’s inactive image is never accessed") - } - } } enum AttachmentAction: CaseIterable { @@ -119,37 +85,3 @@ extension ComposeContentToolbarView.ViewModel { } } } - -extension ComposeContentToolbarView.ViewModel { - func image(for action: Action) -> UIImage { - switch action { - case .poll: - return isPollActive ? action.activeImage : action.inactiveImage - case .emoji: - return isEmojiActive ? action.activeImage : action.inactiveImage - case .contentWarning: - return isContentWarningActive ? action.activeImage : action.inactiveImage - case .language: - fatalError("Language’s image is never accessed") - default: - return action.inactiveImage - } - } - - func label(for action: Action) -> String { - switch action { - case .attachment: - return L10n.Scene.Compose.Accessibility.appendAttachment - case .poll: - return isPollActive ? L10n.Scene.Compose.Accessibility.removePoll : L10n.Scene.Compose.Accessibility.appendPoll - case .emoji: - return L10n.Scene.Compose.Accessibility.customEmojiPicker - case .contentWarning: - return isContentWarningActive ? L10n.Scene.Compose.Accessibility.disableContentWarning : L10n.Scene.Compose.Accessibility.enableContentWarning - case .visibility: - return L10n.Scene.Compose.Accessibility.postVisibilityMenu - case .language: - return "[[language]]" - } - } -} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift index b480834c24..27692c0ed0 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift @@ -33,6 +33,11 @@ struct ComposeContentToolbarView: View { var body: some View { HStack(spacing: .zero) { ForEach(ComposeContentToolbarView.ViewModel.Action.allCases, id: \.self) { action in + let basicHandler = { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(String(describing: action))") + viewModel.delegate?.composeContentToolbarView(viewModel, toolbarItemDidPressed: action) + } + switch action { case .attachment: Menu { @@ -49,11 +54,12 @@ struct ComposeContentToolbarView: View { } } } label: { - label(for: action) - .opacity(viewModel.isAttachmentButtonEnabled ? 1.0 : 0.5) + ComposeContentToolbarAction( + label: L10n.Scene.Compose.Accessibility.appendAttachment, + image: Asset.Scene.Compose.media + ) } .disabled(!viewModel.isAttachmentButtonEnabled) - .frame(width: 48, height: 48) case .visibility: Menu { Picker(selection: $viewModel.visibility) { @@ -61,29 +67,31 @@ struct ComposeContentToolbarView: View { Label { Text(visibility.title) } icon: { - Image(uiImage: visibility.image) + visibility.image.swiftUIImage } } } label: { Text(viewModel.visibility.title) } } label: { - label(for: viewModel.visibility.image) - .accessibilityLabel(L10n.Scene.Compose.Keyboard.selectVisibilityEntry(viewModel.visibility.title)) - .opacity(viewModel.isVisibilityButtonEnabled ? 1.0 : 0.5) + ComposeContentToolbarAction( + label: L10n.Scene.Compose.Keyboard.selectVisibilityEntry(viewModel.visibility.title), + image: viewModel.visibility.image + ) } .disabled(!viewModel.isVisibilityButtonEnabled) - .frame(width: 48, height: 48) case .poll: - Button { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(String(describing: action))") - viewModel.delegate?.composeContentToolbarView(viewModel, toolbarItemDidPressed: action) - } label: { - label(for: action) - .opacity(viewModel.isPollButtonEnabled ? 1.0 : 0.5) + Button(action: basicHandler) { + ComposeContentToolbarAction( + label: viewModel.isPollActive + ? L10n.Scene.Compose.Accessibility.removePoll + : L10n.Scene.Compose.Accessibility.appendPoll, + image: viewModel.isPollActive + ? Asset.Scene.Compose.pollFill + : Asset.Scene.Compose.poll + ) } .disabled(!viewModel.isPollButtonEnabled) - .frame(width: 48, height: 48) case .language: Menu { Section {} // workaround a bug where the “Suggested” section doesn’t appear @@ -115,40 +123,16 @@ struct ComposeContentToolbarView: View { showingLanguagePicker = true } } label: { - let font: SwiftUI.Font = { - if #available(iOS 16, *) { - return .system(size: 11, weight: .semibold).width(viewModel.language.count == 3 ? .compressed : .standard) - } else { - return .system(size: 11, weight: .semibold) - } - }() - - Text(viewModel.language) - .font(font) - .textCase(.uppercase) - .padding(.horizontal, 4) - .minimumScaleFactor(0.5) - .frame(width: 24, height: 24, alignment: .center) - .overlay { RoundedRectangle(cornerRadius: 7).inset(by: 3).stroke(lineWidth: 1.5) } - .accessibilityLabel(L10n.Scene.Compose.Language.title) - .accessibilityValue(Text(Language(id: viewModel.language)?.label ?? AttributedString("\(viewModel.language)"))) - .foregroundColor(Color(Asset.Scene.Compose.buttonTint.color)) - .overlay(alignment: .topTrailing) { - Group { - if let suggested = viewModel.highConfidenceSuggestedLanguage, - suggested != viewModel.language, - !didChangeLanguage { - Circle().fill(.blue) - .frame(width: 8, height: 8) - } + ComposeContentToolbarAction( + label: L10n.Scene.Compose.Language.title, + icon: LanguagePickerIcon(language: viewModel.language, showBadge: { + if let suggested = viewModel.highConfidenceSuggestedLanguage { + return !didChangeLanguage && suggested != viewModel.language } - .transition(.opacity) - .animation(.default, value: [viewModel.highConfidenceSuggestedLanguage, viewModel.language]) - } - // fixes weird appearance when drawing at low opacity (eg when pressed) - .drawingGroup() + return false + }()) + ).accessibilityValue(Text(Language(id: viewModel.language)?.label ?? AttributedString("\(viewModel.language)"))) } - .frame(width: 48, height: 48) .popover(isPresented: $showingLanguagePicker) { let picker = LanguagePicker { newLanguage in viewModel.language = newLanguage @@ -162,16 +146,27 @@ struct ComposeContentToolbarView: View { picker } } - default: - Button { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(String(describing: action))") - viewModel.delegate?.composeContentToolbarView(viewModel, toolbarItemDidPressed: action) - } label: { - label(for: action) + + case .emoji: + Button(action: basicHandler) { + ComposeContentToolbarAction( + label: L10n.Scene.Compose.Accessibility.customEmojiPicker, + image: viewModel.isEmojiActive ? Asset.Scene.Compose.emojiFill : Asset.Scene.Compose.emoji + ) + } + case .contentWarning: + Button(action: basicHandler) { + ComposeContentToolbarAction( + label: viewModel.isContentWarningActive + ? L10n.Scene.Compose.Accessibility.disableContentWarning + : L10n.Scene.Compose.Accessibility.enableContentWarning, + image: viewModel.isContentWarningActive + ? Asset.Scene.Compose.chatWarningFill + : Asset.Scene.Compose.chatWarning + ) } - .frame(width: 48, height: 48) } - } + }.frame(width: 48, height: 48) Spacer() let count: Int = { if viewModel.isContentWarningActive { @@ -194,23 +189,44 @@ struct ComposeContentToolbarView: View { .accessibilityElement(children: .contain) .accessibilityLabel(L10n.Scene.Compose.Accessibility.postOptions) } - } extension ComposeContentToolbarView { - func label(for action: ComposeContentToolbarView.ViewModel.Action) -> some View { - Image(uiImage: viewModel.image(for: action)) - .foregroundColor(Color(Asset.Scene.Compose.buttonTint.color)) - .frame(width: 24, height: 24, alignment: .center) - .accessibilityLabel(viewModel.label(for: action)) - } - - func label(for image: UIImage) -> some View { - Image(uiImage: image) - .foregroundColor(Color(Asset.Scene.Compose.buttonTint.color)) - .frame(width: 24, height: 24, alignment: .center) + struct LanguagePickerIcon: View { + let language: String + let showBadge: Bool + + var body: some View { + let font: SwiftUI.Font = { + if #available(iOS 16, *) { + return .system(size: 11, weight: .semibold).width(language.count == 3 ? .compressed : .standard) + } else { + return .system(size: 11, weight: .semibold) + } + }() + + Text(language) + .font(font) + .textCase(.uppercase) + .padding(.horizontal, 4) + .minimumScaleFactor(0.5) + .frame(width: 24, height: 24, alignment: .center) + .overlay { RoundedRectangle(cornerRadius: 7).inset(by: 3).stroke(lineWidth: 1.5) } + .overlay(alignment: .topTrailing) { + Group { + if showBadge { + Circle().fill(.blue) + .frame(width: 8, height: 8) + } + } + .transition(.opacity) + .animation(.default, value: showBadge) + } + // fixes weird appearance when drawing at low opacity (eg when pressed) + .drawingGroup() + } } - + private func languageBinding(for code: String) -> Binding { Binding { code == viewModel.language @@ -223,6 +239,26 @@ extension ComposeContentToolbarView { } } +struct ComposeContentToolbarAction: View { + let label: String + let icon: Icon + + @Environment(\.isEnabled) private var isEnabled + + var body: some View { + icon.foregroundColor(Color(Asset.Scene.Compose.buttonTint.color)) + .frame(width: 24, height: 24, alignment: .center) + .opacity(isEnabled ? 1 : 0.5) + .accessibilityLabel(label) + } +} + +extension ComposeContentToolbarAction { + init(label: String, image: ImageAsset) { + self.init(label: label, icon: image.swiftUIImage.renderingMode(.template)) + } +} + extension Mastodon.Entity.Status.Visibility { fileprivate var title: String { switch self { @@ -234,13 +270,13 @@ extension Mastodon.Entity.Status.Visibility { } } - fileprivate var image: UIImage { + fileprivate var image: ImageAsset { switch self { - case .public: return Asset.Scene.Compose.earth.image.withRenderingMode(.alwaysTemplate) - case .unlisted: return Asset.Scene.Compose.people.image.withRenderingMode(.alwaysTemplate) - case .private: return Asset.Scene.Compose.peopleAdd.image.withRenderingMode(.alwaysTemplate) - case .direct: return Asset.Scene.Compose.mention.image.withRenderingMode(.alwaysTemplate) - case ._other: return Asset.Scene.Compose.more.image.withRenderingMode(.alwaysTemplate) + case .public: return Asset.Scene.Compose.earth + case .unlisted: return Asset.Scene.Compose.people + case .private: return Asset.Scene.Compose.peopleAdd + case .direct: return Asset.Scene.Compose.mention + case ._other: return Asset.Scene.Compose.more } } } From 00987435d989fda8d4ca6c031e1fdd7025453e95 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 23 Jan 2023 16:58:42 -0500 Subject: [PATCH 2/2] Remove switch and ComposeContentToolbarView.ViewModel.Action cases that do not correspond to buttons --- .../ComposeContentViewController.swift | 4 - .../ComposeContentToolbarView+ViewModel.swift | 3 - .../Toolbar/ComposeContentToolbarView.swift | 255 +++++++++--------- 3 files changed, 132 insertions(+), 130 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index 1f88a0bab9..c760dd986a 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -531,8 +531,6 @@ extension ComposeContentViewController: ComposeContentToolbarViewDelegate { toolbarItemDidPressed action: ComposeContentToolbarView.ViewModel.Action ) { switch action { - case .attachment: - assertionFailure() case .poll: self.viewModel.isPollActive.toggle() case .emoji: @@ -549,8 +547,6 @@ extension ComposeContentViewController: ComposeContentToolbarViewDelegate { self.viewModel.setContentTextViewFirstResponderIfNeeds() } } - case .visibility, .language: - assertionFailure() } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView+ViewModel.swift index 4c164009b1..f0ccb45fb7 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView+ViewModel.swift @@ -55,12 +55,9 @@ extension ComposeContentToolbarView { extension ComposeContentToolbarView.ViewModel { enum Action: CaseIterable { - case attachment case poll case emoji case contentWarning - case visibility - case language } enum AttachmentAction: CaseIterable { diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift index 27692c0ed0..c519dce60f 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift @@ -32,142 +32,151 @@ struct ComposeContentToolbarView: View { var body: some View { HStack(spacing: .zero) { - ForEach(ComposeContentToolbarView.ViewModel.Action.allCases, id: \.self) { action in - let basicHandler = { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(String(describing: action))") - viewModel.delegate?.composeContentToolbarView(viewModel, toolbarItemDidPressed: action) - } + let makeBasicHandler: (ViewModel.Action) -> () -> Void = { action in { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(String(describing: action))") + viewModel.delegate?.composeContentToolbarView(viewModel, toolbarItemDidPressed: action) + } } - switch action { - case .attachment: - Menu { - ForEach(ComposeContentToolbarView.ViewModel.AttachmentAction.allCases, id: \.self) { attachmentAction in - Button { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public), \(attachmentAction.title)") - viewModel.delegate?.composeContentToolbarView(viewModel, attachmentMenuDidPressed: attachmentAction) - } label: { - Label { - Text(attachmentAction.title) - } icon: { - Image(uiImage: attachmentAction.image) - } - } - } + // MARK: Attachment + Menu { + ForEach(ComposeContentToolbarView.ViewModel.AttachmentAction.allCases, id: \.self) { attachmentAction in + Button { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public), \(attachmentAction.title)") + viewModel.delegate?.composeContentToolbarView(viewModel, attachmentMenuDidPressed: attachmentAction) } label: { - ComposeContentToolbarAction( - label: L10n.Scene.Compose.Accessibility.appendAttachment, - image: Asset.Scene.Compose.media - ) - } - .disabled(!viewModel.isAttachmentButtonEnabled) - case .visibility: - Menu { - Picker(selection: $viewModel.visibility) { - ForEach(viewModel.allVisibilities, id: \.self) { visibility in - Label { - Text(visibility.title) - } icon: { - visibility.image.swiftUIImage - } - } - } label: { - Text(viewModel.visibility.title) + Label { + Text(attachmentAction.title) + } icon: { + Image(uiImage: attachmentAction.image) } - } label: { - ComposeContentToolbarAction( - label: L10n.Scene.Compose.Keyboard.selectVisibilityEntry(viewModel.visibility.title), - image: viewModel.visibility.image - ) - } - .disabled(!viewModel.isVisibilityButtonEnabled) - case .poll: - Button(action: basicHandler) { - ComposeContentToolbarAction( - label: viewModel.isPollActive - ? L10n.Scene.Compose.Accessibility.removePoll - : L10n.Scene.Compose.Accessibility.appendPoll, - image: viewModel.isPollActive - ? Asset.Scene.Compose.pollFill - : Asset.Scene.Compose.poll - ) } - .disabled(!viewModel.isPollButtonEnabled) - case .language: - Menu { - Section {} // workaround a bug where the “Suggested” section doesn’t appear - if !viewModel.suggestedLanguages.isEmpty { - Section(L10n.Scene.Compose.Language.suggested) { - ForEach(viewModel.suggestedLanguages.compactMap(Language.init(id:))) { lang in - Toggle(isOn: languageBinding(for: lang.id)) { - Text(lang.label) - } - } - } - } - let recent = viewModel.recentLanguages.filter { !viewModel.suggestedLanguages.contains($0) } - if !recent.isEmpty { - Section(L10n.Scene.Compose.Language.recent) { - ForEach(recent.compactMap(Language.init(id:))) { lang in - Toggle(isOn: languageBinding(for: lang.id)) { - Text(lang.label) - } - } - } + } + } label: { + ComposeContentToolbarAction( + label: L10n.Scene.Compose.Accessibility.appendAttachment, + image: Asset.Scene.Compose.media + ) + } + .disabled(!viewModel.isAttachmentButtonEnabled) + .frame(width: Self.toolbarHeight, height: Self.toolbarHeight) + + // MARK: Poll + Button(action: makeBasicHandler(.poll)) { + ComposeContentToolbarAction( + label: viewModel.isPollActive + ? L10n.Scene.Compose.Accessibility.removePoll + : L10n.Scene.Compose.Accessibility.appendPoll, + image: viewModel.isPollActive + ? Asset.Scene.Compose.pollFill + : Asset.Scene.Compose.poll + ) + } + .disabled(!viewModel.isPollButtonEnabled) + .frame(width: Self.toolbarHeight, height: Self.toolbarHeight) + + // MARK: Emoji + Button(action: makeBasicHandler(.emoji)) { + ComposeContentToolbarAction( + label: L10n.Scene.Compose.Accessibility.customEmojiPicker, + image: viewModel.isEmojiActive ? Asset.Scene.Compose.emojiFill : Asset.Scene.Compose.emoji + ) + } + .frame(width: Self.toolbarHeight, height: Self.toolbarHeight) + + // MARK: Content Warning + Button(action: makeBasicHandler(.contentWarning)) { + ComposeContentToolbarAction( + label: viewModel.isContentWarningActive + ? L10n.Scene.Compose.Accessibility.disableContentWarning + : L10n.Scene.Compose.Accessibility.enableContentWarning, + image: viewModel.isContentWarningActive + ? Asset.Scene.Compose.chatWarningFill + : Asset.Scene.Compose.chatWarning + ) + } + .frame(width: Self.toolbarHeight, height: Self.toolbarHeight) + + // MARK: Visibility + Menu { + Picker(selection: $viewModel.visibility) { + ForEach(viewModel.allVisibilities, id: \.self) { visibility in + Label { + Text(visibility.title) + } icon: { + visibility.image.swiftUIImage } - if !(recent + viewModel.suggestedLanguages).contains(viewModel.language) { - Toggle(isOn: languageBinding(for: viewModel.language)) { - Text(Language(id: viewModel.language)?.label ?? AttributedString("\(viewModel.language)")) + } + } label: { + Text(viewModel.visibility.title) + } + } label: { + ComposeContentToolbarAction( + label: L10n.Scene.Compose.Keyboard.selectVisibilityEntry(viewModel.visibility.title), + image: viewModel.visibility.image + ) + } + .disabled(!viewModel.isVisibilityButtonEnabled) + .frame(width: Self.toolbarHeight, height: Self.toolbarHeight) + + // MARK: Language + Menu { + Section {} // workaround a bug where the “Suggested” section doesn’t appear + if !viewModel.suggestedLanguages.isEmpty { + Section(L10n.Scene.Compose.Language.suggested) { + ForEach(viewModel.suggestedLanguages.compactMap(Language.init(id:))) { lang in + Toggle(isOn: languageBinding(for: lang.id)) { + Text(lang.label) } } - Button(L10n.Scene.Compose.Language.other) { - showingLanguagePicker = true - } - } label: { - ComposeContentToolbarAction( - label: L10n.Scene.Compose.Language.title, - icon: LanguagePickerIcon(language: viewModel.language, showBadge: { - if let suggested = viewModel.highConfidenceSuggestedLanguage { - return !didChangeLanguage && suggested != viewModel.language - } - return false - }()) - ).accessibilityValue(Text(Language(id: viewModel.language)?.label ?? AttributedString("\(viewModel.language)"))) } - .popover(isPresented: $showingLanguagePicker) { - let picker = LanguagePicker { newLanguage in - viewModel.language = newLanguage - didChangeLanguage = true - showingLanguagePicker = false - } - if verticalSizeClass == .regular && horizontalSizeClass == .regular { - // explicitly size picker when it’s a popover - picker.frame(width: 400, height: 500) - } else { - picker + } + let recent = viewModel.recentLanguages.filter { !viewModel.suggestedLanguages.contains($0) } + if !recent.isEmpty { + Section(L10n.Scene.Compose.Language.recent) { + ForEach(recent.compactMap(Language.init(id:))) { lang in + Toggle(isOn: languageBinding(for: lang.id)) { + Text(lang.label) + } } } - - case .emoji: - Button(action: basicHandler) { - ComposeContentToolbarAction( - label: L10n.Scene.Compose.Accessibility.customEmojiPicker, - image: viewModel.isEmojiActive ? Asset.Scene.Compose.emojiFill : Asset.Scene.Compose.emoji - ) - } - case .contentWarning: - Button(action: basicHandler) { - ComposeContentToolbarAction( - label: viewModel.isContentWarningActive - ? L10n.Scene.Compose.Accessibility.disableContentWarning - : L10n.Scene.Compose.Accessibility.enableContentWarning, - image: viewModel.isContentWarningActive - ? Asset.Scene.Compose.chatWarningFill - : Asset.Scene.Compose.chatWarning - ) + } + if !(recent + viewModel.suggestedLanguages).contains(viewModel.language) { + Toggle(isOn: languageBinding(for: viewModel.language)) { + Text(Language(id: viewModel.language)?.label ?? AttributedString("\(viewModel.language)")) } } - }.frame(width: 48, height: 48) + Button(L10n.Scene.Compose.Language.other) { + showingLanguagePicker = true + } + } label: { + ComposeContentToolbarAction( + label: L10n.Scene.Compose.Language.title, + icon: LanguagePickerIcon(language: viewModel.language, showBadge: { + if let suggested = viewModel.highConfidenceSuggestedLanguage { + return !didChangeLanguage && suggested != viewModel.language + } + return false + }()) + ).accessibilityValue(Text(Language(id: viewModel.language)?.label ?? AttributedString("\(viewModel.language)"))) + } + .frame(width: Self.toolbarHeight, height: Self.toolbarHeight) + .popover(isPresented: $showingLanguagePicker) { + let picker = LanguagePicker { newLanguage in + viewModel.language = newLanguage + didChangeLanguage = true + showingLanguagePicker = false + } + if verticalSizeClass == .regular, horizontalSizeClass == .regular { + // explicitly size picker when it’s a popover + picker.frame(width: 400, height: 500) + } else { + picker + } + } + Spacer() + + // MARK: Character count let count: Int = { if viewModel.isContentWarningActive { return viewModel.contentWeightedLength + viewModel.contentWarningWeightedLength