1
0
mirror of https://github.com/mastodon/mastodon-ios.git synced 2025-01-31 17:45:17 +01:00
Jed Fox 0a9689c67f
Add support for selecting the post language (#907)
* Basic fake language picker support

* Recognize languages from post text

* Exclude suggested languages from recents

* Load recent languages from Settings object

* Send the language to the API

* Persist the used language to settings

* Always show the currently selected language in the list

* Fix crash

* Add support for picking arbitrary lanuages

* Fix display of 3 letter language codes

* Improve label to include endonym too

* Limit to 3 recent languages

* Reduce lower bound for displaying language suggestions

* Fix saving recent language when publishing

* Fix tint color of language picker button

* Add a badge to prompt users to change language

* Dismiss the badge even if you pick the same language

* Read language names in the language if possible

* Use a compressed font for 3-letter codes

Also use `minimumScaleFactor` to shrink troublesome codes to fit

Co-Authored-By: samhenrigold <49251320+samhenrigold@users.noreply.github.com>

* Remove .vscode/launch.json

* Add message to fatalError()

Co-authored-by: samhenrigold <49251320+samhenrigold@users.noreply.github.com>
2023-01-24 01:50:10 +01:00

245 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// ComposeContentToolbarView.swift
//
//
// Created by MainasuK on 22/10/18.
//
import os.log
import SwiftUI
import MastodonAsset
import MastodonLocalization
import MastodonSDK
protocol ComposeContentToolbarViewDelegate: AnyObject {
func composeContentToolbarView(_ viewModel: ComposeContentToolbarView.ViewModel, toolbarItemDidPressed action: ComposeContentToolbarView.ViewModel.Action)
func composeContentToolbarView(_ viewModel: ComposeContentToolbarView.ViewModel, attachmentMenuDidPressed action: ComposeContentToolbarView.ViewModel.AttachmentAction)
}
struct ComposeContentToolbarView: View {
let logger = Logger(subsystem: "ComposeContentToolbarView", category: "View")
static var toolbarHeight: CGFloat { 48 }
@ObservedObject var viewModel: ViewModel
@State private var showingLanguagePicker = false
@State private var didChangeLanguage = false
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass
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))
}
.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)
}
.disabled(!viewModel.isPollButtonEnabled)
.frame(width: 48, height: 48)
case .language:
Menu {
Section {} // workaround a bug where the Suggested section doesnt 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)
}
}
}
}
if !(recent + viewModel.suggestedLanguages).contains(viewModel.language) {
Toggle(isOn: languageBinding(for: viewModel.language)) {
Text(Language(id: viewModel.language)?.label ?? AttributedString("\(viewModel.language)"))
}
}
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 its a popover
picker.frame(width: 400, height: 500)
} else {
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)
}
.frame(width: 48, height: 48)
}
}
Spacer()
let count: Int = {
if viewModel.isContentWarningActive {
return viewModel.contentWeightedLength + viewModel.contentWarningWeightedLength
} else {
return viewModel.contentWeightedLength
}
}()
let remains = viewModel.maxTextInputLimit - count
let isOverflow = remains < 0
Text("\(remains)")
.foregroundColor(Color(isOverflow ? UIColor.systemRed : UIColor.secondaryLabel))
.font(.system(size: isOverflow ? 18 : 16, weight: isOverflow ? .medium : .regular))
.accessibilityLabel(L10n.A11y.Plural.Count.charactersLeft(remains))
}
.padding(.leading, 4) // 4 + 12 = 16
.padding(.trailing, 16)
.frame(height: ComposeContentToolbarView.toolbarHeight)
.background(Color(viewModel.backgroundColor))
.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)
}
private func languageBinding(for code: String) -> Binding<Bool> {
Binding {
code == viewModel.language
} set: { newValue in
if newValue {
viewModel.language = code
}
didChangeLanguage = true
}
}
}
extension Mastodon.Entity.Status.Visibility {
fileprivate var title: String {
switch self {
case .public: return L10n.Scene.Compose.Visibility.public
case .unlisted: return L10n.Scene.Compose.Visibility.unlisted
case .private: return L10n.Scene.Compose.Visibility.private
case .direct: return L10n.Scene.Compose.Visibility.direct
case ._other(let value): return value
}
}
fileprivate var image: UIImage {
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)
}
}
}