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/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 aaf2d2dca1..f0ccb45fb7 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView+ViewModel.swift @@ -55,46 +55,9 @@ extension ComposeContentToolbarView { extension ComposeContentToolbarView.ViewModel { enum Action: CaseIterable { - case attachment case poll case emoji 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 +82,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..c519dce60f 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift @@ -32,147 +32,151 @@ struct ComposeContentToolbarView: View { var body: some View { HStack(spacing: .zero) { - ForEach(ComposeContentToolbarView.ViewModel.Action.allCases, id: \.self) { action in - 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) - } - } - } - } label: { - label(for: action) - .opacity(viewModel.isAttachmentButtonEnabled ? 1.0 : 0.5) - } - .disabled(!viewModel.isAttachmentButtonEnabled) - .frame(width: 48, height: 48) - case .visibility: - Menu { - Picker(selection: $viewModel.visibility) { - ForEach(viewModel.allVisibilities, id: \.self) { visibility in - Label { - Text(visibility.title) - } icon: { - Image(uiImage: visibility.image) - } - } - } 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) - } - .disabled(!viewModel.isVisibilityButtonEnabled) - .frame(width: 48, height: 48) - case .poll: + 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) + } } + + // 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): \(String(describing: action))") - viewModel.delegate?.composeContentToolbarView(viewModel, toolbarItemDidPressed: action) + 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(for: action) - .opacity(viewModel.isPollButtonEnabled ? 1.0 : 0.5) - } - .disabled(!viewModel.isPollButtonEnabled) - .frame(width: 48, height: 48) - 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) - } - } - } + Label { + Text(attachmentAction.title) + } icon: { + Image(uiImage: attachmentAction.image) } - 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: { - 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) - } - } - .transition(.opacity) - .animation(.default, value: [viewModel.highConfidenceSuggestedLanguage, viewModel.language]) - } - // fixes weird appearance when drawing at low opacity (eg when pressed) - .drawingGroup() } - .frame(width: 48, height: 48) - .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) + } } } - 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) + } + 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 @@ -194,23 +198,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 +248,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 +279,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 } } }