From dbd72b3523adbc5e5ed5fec09a3897a3318bb4d7 Mon Sep 17 00:00:00 2001 From: NanoSector Date: Sun, 30 Oct 2022 17:50:15 +0100 Subject: [PATCH 001/224] feat: handle paste event and insert images on the clipboard Signed-off-by: NanoSector --- .../Scene/Compose/ComposeViewController.swift | 29 +++++++++++++++++++ .../View/MetaTextView+PasteExtensions.swift | 29 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 Mastodon/Scene/Compose/View/MetaTextView+PasteExtensions.swift diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index f5dfc8ba3..6ca09eba0 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -1449,3 +1449,32 @@ extension ComposeViewController { } } + +extension ComposeViewController { + public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + + // Enable pasting images + if (action == #selector(UIResponderStandardEditActions.paste(_:))) { + return UIPasteboard.general.hasStrings || UIPasteboard.general.hasImages; + } + + return super.canPerformAction(action, withSender: sender); + } + + override func paste(_ sender: Any?) { + logger.debug("Paste event received") + + // Look for images on the clipboard + if (UIPasteboard.general.hasImages) { + if let images = UIPasteboard.general.images { + viewModel.attachmentServices = viewModel.attachmentServices + images.map({ image in + MastodonAttachmentService( + context: context, + image: image, + initialAuthenticationBox: viewModel.authenticationBox + ) + }) + } + } + } +} diff --git a/Mastodon/Scene/Compose/View/MetaTextView+PasteExtensions.swift b/Mastodon/Scene/Compose/View/MetaTextView+PasteExtensions.swift new file mode 100644 index 000000000..8fe1949af --- /dev/null +++ b/Mastodon/Scene/Compose/View/MetaTextView+PasteExtensions.swift @@ -0,0 +1,29 @@ +// +// MetaTextView+PasteExtensions.swift +// Mastodon +// +// Created by Rick Kerkhof on 30/10/2022. +// + +import Foundation +import MetaTextKit +import UIKit + +extension MetaTextView { + public override func paste(_ sender: Any?) { + super.paste(sender) + + var nextResponder = self.next; + + // Force the event to bubble through ALL responders + // This is a workaround as somewhere down the chain the paste event gets eaten + while (nextResponder != nil) { + if let nextResponder = nextResponder { + if (nextResponder.responds(to: #selector(UIResponderStandardEditActions.paste(_:)))) { + nextResponder.perform(#selector(UIResponderStandardEditActions.paste(_:)), with: sender) + } + } + nextResponder = nextResponder?.next; + } + } +} From 2c2ca419dd1d6bde108bd212a302a49f06ecd017 Mon Sep 17 00:00:00 2001 From: NanoSector Date: Sun, 30 Oct 2022 18:00:45 +0100 Subject: [PATCH 002/224] chore: add project entries Signed-off-by: NanoSector --- Mastodon.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 4106af7de..9f0a8f44c 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -115,6 +115,7 @@ 62FD27D32893707B00B205C5 /* BookmarkViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FD27D22893707B00B205C5 /* BookmarkViewController+DataSourceProvider.swift */; }; 62FD27D52893708A00B205C5 /* BookmarkViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FD27D42893708A00B205C5 /* BookmarkViewModel+Diffable.swift */; }; 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; + CD91FB31290EDA6F00BB9463 /* MetaTextView+PasteExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD91FB30290EDA6F00BB9463 /* MetaTextView+PasteExtensions.swift */; }; DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; settings = {ATTRIBUTES = (codegen, ); }; }; DB0009A726AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; }; DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; @@ -858,6 +859,7 @@ BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = ""; }; BD7598A87F4497045EDEF252 /* Pods-Mastodon.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk - release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk - release.xcconfig"; sourceTree = ""; }; C3789232A52F43529CA67E95 /* Pods-MastodonIntent.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonIntent.asdk - debug.xcconfig"; path = "Target Support Files/Pods-MastodonIntent/Pods-MastodonIntent.asdk - debug.xcconfig"; sourceTree = ""; }; + CD91FB30290EDA6F00BB9463 /* MetaTextView+PasteExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MetaTextView+PasteExtensions.swift"; sourceTree = ""; }; CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.debug.xcconfig"; sourceTree = ""; }; DB0009A826AEE5DC009B9D2D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = ""; }; @@ -2493,6 +2495,7 @@ DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */, DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */, DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */, + CD91FB30290EDA6F00BB9463 /* MetaTextView+PasteExtensions.swift */, ); path = View; sourceTree = ""; @@ -4203,6 +4206,7 @@ DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */, 6213AF5A28939C8400BCADB6 /* BookmarkViewModel.swift in Sources */, 5B24BBDB262DB14800A9381B /* ReportStatusViewModel+Diffable.swift in Sources */, + CD91FB31290EDA6F00BB9463 /* MetaTextView+PasteExtensions.swift in Sources */, DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */, 0FB3D2FE25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift in Sources */, 5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */, From 9d7614a4037f98f26cb1bc0c5bf461c501b9cb2f Mon Sep 17 00:00:00 2001 From: NanoSector Date: Tue, 1 Nov 2022 19:55:51 +0100 Subject: [PATCH 003/224] feat: partially restore image paste handler functionality after SwiftUI rewrite Signed-off-by: NanoSector --- .../Scene/Compose/ComposeViewController.swift | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index bf9145d6c..42945514d 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -1212,3 +1212,33 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate { // } // //} + +extension ComposeViewController { + public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + + // Enable pasting images + if (action == #selector(UIResponderStandardEditActions.paste(_:))) { + return UIPasteboard.general.hasStrings || UIPasteboard.general.hasImages; + } + + return super.canPerformAction(action, withSender: sender); + } + + override func paste(_ sender: Any?) { + logger.debug("Paste event received") + + // Look for images on the clipboard + if (UIPasteboard.general.hasImages) { + if let images = UIPasteboard.general.images { + logger.warning("Got image paste event, however attachments are not yet re-implemented."); +// viewModel.attachmentServices = viewModel.attachmentServices + images.map({ image in +// MastodonAttachmentService( +// context: context, +// image: image, +// initialAuthenticationBox: viewModel.authenticationBox +// ) +// }) + } + } + } +} From d489943b45991025a6e3f4b78613f629294e0493 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sun, 6 Nov 2022 20:22:06 -0500 Subject: [PATCH 004/224] Improve ComposeContentView.avatarView label --- .../Scene/ComposeContent/View/ComposeContentView.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift index 25584848a..56ddbbb7f 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift @@ -147,6 +147,9 @@ extension ComposeContentView { } Spacer() } + .accessibilityElement(children: .ignore) + // TODO: i18n + .accessibilityLabel("Posting as \(viewModel.name.string), \(viewModel.username)") } } From 7a3b9205e5f242a3ed69b11ba0399eea7d9896e5 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sun, 6 Nov 2022 20:49:12 -0500 Subject: [PATCH 005/224] Add missing labels to compose toolbar --- .../ComposeContentToolbarView+ViewModel.swift | 15 +++++++++++++++ .../View/ComposeContentToolbarView.swift | 2 ++ 2 files changed, 17 insertions(+) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift index 4a34c77d4..fefc0821f 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift @@ -120,4 +120,19 @@ extension ComposeContentToolbarView.ViewModel { 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 + } + } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift index 52026c636..aba52ff9a 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift @@ -61,6 +61,7 @@ struct ComposeContentToolbarView: View { } } label: { label(for: viewModel.visibility.image) + .accessibilityLabel(L10n.Scene.Compose.Keyboard.selectVisibilityEntry(viewModel.visibility.title)) } .frame(width: 48, height: 48) default: @@ -100,6 +101,7 @@ extension ComposeContentToolbarView { 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 { From 7ac9e7c564095eb82b69e166e332452e12449a97 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sun, 6 Nov 2022 20:51:13 -0500 Subject: [PATCH 006/224] Add description to compose content toolbar container --- .../Scene/ComposeContent/View/ComposeContentToolbarView.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift index aba52ff9a..53058cc12 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift @@ -92,6 +92,9 @@ struct ComposeContentToolbarView: View { .padding(.trailing, 16) .frame(height: ComposeContentToolbarView.toolbarHeight) .background(Color(viewModel.backgroundColor)) + .accessibilityElement(children: .contain) + // TODO: i18n + .accessibilityLabel("Post Options") } } From 022f8c11151801ea3faf2e77eddf60d4a6ee23dc Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sun, 6 Nov 2022 20:51:43 -0500 Subject: [PATCH 007/224] Clarify meaning of character counter --- .../Scene/ComposeContent/View/ComposeContentToolbarView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift index 53058cc12..8c6d84a28 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift @@ -87,6 +87,8 @@ struct ComposeContentToolbarView: View { Text("\(remains)") .foregroundColor(Color(isOverflow ? UIColor.systemRed : UIColor.secondaryLabel)) .font(.system(size: isOverflow ? 18 : 16, weight: isOverflow ? .medium : .regular)) + // TODO: i18n + .accessibilityLabel("\(remains) characters left") } .padding(.leading, 4) // 4 + 12 = 16 .padding(.trailing, 16) From 549739b6cb50433878ef4586508ae6f52007b7ee Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 7 Nov 2022 06:26:20 -0500 Subject: [PATCH 008/224] Add new strings to Localization folder --- Localization/Localizable.stringsdict | 20 +++++++++++++++++++ Localization/app.json | 6 ++++-- .../View/ComposeContentToolbarView.swift | 4 ++-- .../View/ComposeContentView.swift | 4 ++-- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Localization/Localizable.stringsdict b/Localization/Localizable.stringsdict index 051bb50ef..46b79deb2 100644 --- a/Localization/Localizable.stringsdict +++ b/Localization/Localizable.stringsdict @@ -68,6 +68,26 @@ %ld characters + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + zero + no characters left + one + 1 character left + few + %ld characters left + many + %ld characters left + other + %ld characters left + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/Localization/app.json b/Localization/app.json index a965b23ae..45ff94363 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -405,7 +405,9 @@ "custom_emoji_picker": "Custom Emoji Picker", "enable_content_warning": "Enable Content Warning", "disable_content_warning": "Disable Content Warning", - "post_visibility_menu": "Post Visibility Menu" + "post_visibility_menu": "Post Visibility Menu", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Discard Post", @@ -686,4 +688,4 @@ "accessibility_hint": "Double tap to dismiss this wizard" } } -} \ No newline at end of file +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift index 8c6d84a28..ac1a56b20 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift @@ -87,7 +87,7 @@ struct ComposeContentToolbarView: View { Text("\(remains)") .foregroundColor(Color(isOverflow ? UIColor.systemRed : UIColor.secondaryLabel)) .font(.system(size: isOverflow ? 18 : 16, weight: isOverflow ? .medium : .regular)) - // TODO: i18n + // TODO: i18n (a11y.plural.count.characters_left) .accessibilityLabel("\(remains) characters left") } .padding(.leading, 4) // 4 + 12 = 16 @@ -95,7 +95,7 @@ struct ComposeContentToolbarView: View { .frame(height: ComposeContentToolbarView.toolbarHeight) .background(Color(viewModel.backgroundColor)) .accessibilityElement(children: .contain) - // TODO: i18n + // TODO: i18n (scene.compose.accessibility.post_options) .accessibilityLabel("Post Options") } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift index 56ddbbb7f..b48f4e060 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift @@ -148,8 +148,8 @@ extension ComposeContentView { Spacer() } .accessibilityElement(children: .ignore) - // TODO: i18n - .accessibilityLabel("Posting as \(viewModel.name.string), \(viewModel.username)") + // TODO: i18n (scene.compose.accessibility.posting_as) + .accessibilityLabel("Posting as \([viewModel.name.string, viewModel.username].joined(separator: ", "))") } } From 39e8c286e936c4b1fbd2473ac1aa64769f03b170 Mon Sep 17 00:00:00 2001 From: Jordan Kay Date: Mon, 7 Nov 2022 10:52:32 -0500 Subject: [PATCH 009/224] Fix spelling of directory name Diffiable > Diffable --- Mastodon.xcodeproj/project.pbxproj | 6 +++--- .../Account/SelectedAccountItem.swift | 0 .../Account/SelectedAccountSection.swift | 0 .../{Diffiable => Diffable}/Discovery/DiscoveryItem.swift | 0 .../Discovery/DiscoverySection.swift | 0 .../Notification/NotificationItem.swift | 0 .../Notification/NotificationSection.swift | 0 .../Onboarding/CategoryPickerItem.swift | 0 .../Onboarding/CategoryPickerSection.swift | 0 .../{Diffiable => Diffable}/Onboarding/PickServerItem.swift | 0 .../Onboarding/PickServerSection.swift | 0 .../{Diffiable => Diffable}/Onboarding/RegisterItem.swift | 0 .../Onboarding/RegisterSection.swift | 0 .../{Diffiable => Diffable}/Onboarding/ServerRuleItem.swift | 0 .../Onboarding/ServerRuleSection.swift | 0 .../{Diffiable => Diffable}/Profile/ProfileFieldItem.swift | 0 .../Profile/ProfileFieldSection.swift | 0 .../RecommandAccount/RecommendAccountItem.swift | 0 .../RecommandAccount/RecommendAccountSection.swift | 0 Mastodon/{Diffiable => Diffable}/Report/ReportItem.swift | 0 Mastodon/{Diffiable => Diffable}/Report/ReportSection.swift | 0 .../{Diffiable => Diffable}/Search/SearchHistoryItem.swift | 0 .../Search/SearchHistorySection.swift | 0 Mastodon/{Diffiable => Diffable}/Search/SearchItem.swift | 0 .../{Diffiable => Diffable}/Search/SearchResultItem.swift | 0 .../Search/SearchResultSection.swift | 0 Mastodon/{Diffiable => Diffable}/Search/SearchSection.swift | 0 .../{Diffiable => Diffable}/Settings/SettingsItem.swift | 0 .../{Diffiable => Diffable}/Settings/SettingsSection.swift | 0 Mastodon/{Diffiable => Diffable}/Status/StatusItem.swift | 0 Mastodon/{Diffiable => Diffable}/Status/StatusSection.swift | 0 Mastodon/{Diffiable => Diffable}/User/UserItem.swift | 0 Mastodon/{Diffiable => Diffable}/User/UserSection.swift | 0 33 files changed, 3 insertions(+), 3 deletions(-) rename Mastodon/{Diffiable => Diffable}/Account/SelectedAccountItem.swift (100%) rename Mastodon/{Diffiable => Diffable}/Account/SelectedAccountSection.swift (100%) rename Mastodon/{Diffiable => Diffable}/Discovery/DiscoveryItem.swift (100%) rename Mastodon/{Diffiable => Diffable}/Discovery/DiscoverySection.swift (100%) rename Mastodon/{Diffiable => Diffable}/Notification/NotificationItem.swift (100%) rename Mastodon/{Diffiable => Diffable}/Notification/NotificationSection.swift (100%) rename Mastodon/{Diffiable => Diffable}/Onboarding/CategoryPickerItem.swift (100%) rename Mastodon/{Diffiable => Diffable}/Onboarding/CategoryPickerSection.swift (100%) rename Mastodon/{Diffiable => Diffable}/Onboarding/PickServerItem.swift (100%) rename Mastodon/{Diffiable => Diffable}/Onboarding/PickServerSection.swift (100%) rename Mastodon/{Diffiable => Diffable}/Onboarding/RegisterItem.swift (100%) rename Mastodon/{Diffiable => Diffable}/Onboarding/RegisterSection.swift (100%) rename Mastodon/{Diffiable => Diffable}/Onboarding/ServerRuleItem.swift (100%) rename Mastodon/{Diffiable => Diffable}/Onboarding/ServerRuleSection.swift (100%) rename Mastodon/{Diffiable => Diffable}/Profile/ProfileFieldItem.swift (100%) rename Mastodon/{Diffiable => Diffable}/Profile/ProfileFieldSection.swift (100%) rename Mastodon/{Diffiable => Diffable}/RecommandAccount/RecommendAccountItem.swift (100%) rename Mastodon/{Diffiable => Diffable}/RecommandAccount/RecommendAccountSection.swift (100%) rename Mastodon/{Diffiable => Diffable}/Report/ReportItem.swift (100%) rename Mastodon/{Diffiable => Diffable}/Report/ReportSection.swift (100%) rename Mastodon/{Diffiable => Diffable}/Search/SearchHistoryItem.swift (100%) rename Mastodon/{Diffiable => Diffable}/Search/SearchHistorySection.swift (100%) rename Mastodon/{Diffiable => Diffable}/Search/SearchItem.swift (100%) rename Mastodon/{Diffiable => Diffable}/Search/SearchResultItem.swift (100%) rename Mastodon/{Diffiable => Diffable}/Search/SearchResultSection.swift (100%) rename Mastodon/{Diffiable => Diffable}/Search/SearchSection.swift (100%) rename Mastodon/{Diffiable => Diffable}/Settings/SettingsItem.swift (100%) rename Mastodon/{Diffiable => Diffable}/Settings/SettingsSection.swift (100%) rename Mastodon/{Diffiable => Diffable}/Status/StatusItem.swift (100%) rename Mastodon/{Diffiable => Diffable}/Status/StatusSection.swift (100%) rename Mastodon/{Diffiable => Diffable}/User/UserItem.swift (100%) rename Mastodon/{Diffiable => Diffable}/User/UserSection.swift (100%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 68aae2d4e..d9a0cb96f 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -1342,7 +1342,7 @@ path = Protocol; sourceTree = ""; }; - 2D76319C25C151DE00929FB9 /* Diffiable */ = { + 2D76319C25C151DE00929FB9 /* Diffable */ = { isa = PBXGroup; children = ( DB4F097826A039B400D62E92 /* Onboarding */, @@ -1357,7 +1357,7 @@ DB3E6FE52806A5BA00B035AE /* Discovery */, DB0617FA27855B660030EE79 /* Settings */, ); - path = Diffiable; + path = Diffable; sourceTree = ""; }; 2D7631A425C1532200929FB9 /* Share */ = { @@ -1762,7 +1762,7 @@ children = ( DB89BA1025C10FF5008580ED /* Mastodon.entitlements */, DB427DE325BAA00100D1B89D /* Info.plist */, - 2D76319C25C151DE00929FB9 /* Diffiable */, + 2D76319C25C151DE00929FB9 /* Diffable */, DB8AF55525C1379F002E6C99 /* Scene */, DB8AF54125C13647002E6C99 /* Coordinator */, DB8AF56225C138BC002E6C99 /* Extension */, diff --git a/Mastodon/Diffiable/Account/SelectedAccountItem.swift b/Mastodon/Diffable/Account/SelectedAccountItem.swift similarity index 100% rename from Mastodon/Diffiable/Account/SelectedAccountItem.swift rename to Mastodon/Diffable/Account/SelectedAccountItem.swift diff --git a/Mastodon/Diffiable/Account/SelectedAccountSection.swift b/Mastodon/Diffable/Account/SelectedAccountSection.swift similarity index 100% rename from Mastodon/Diffiable/Account/SelectedAccountSection.swift rename to Mastodon/Diffable/Account/SelectedAccountSection.swift diff --git a/Mastodon/Diffiable/Discovery/DiscoveryItem.swift b/Mastodon/Diffable/Discovery/DiscoveryItem.swift similarity index 100% rename from Mastodon/Diffiable/Discovery/DiscoveryItem.swift rename to Mastodon/Diffable/Discovery/DiscoveryItem.swift diff --git a/Mastodon/Diffiable/Discovery/DiscoverySection.swift b/Mastodon/Diffable/Discovery/DiscoverySection.swift similarity index 100% rename from Mastodon/Diffiable/Discovery/DiscoverySection.swift rename to Mastodon/Diffable/Discovery/DiscoverySection.swift diff --git a/Mastodon/Diffiable/Notification/NotificationItem.swift b/Mastodon/Diffable/Notification/NotificationItem.swift similarity index 100% rename from Mastodon/Diffiable/Notification/NotificationItem.swift rename to Mastodon/Diffable/Notification/NotificationItem.swift diff --git a/Mastodon/Diffiable/Notification/NotificationSection.swift b/Mastodon/Diffable/Notification/NotificationSection.swift similarity index 100% rename from Mastodon/Diffiable/Notification/NotificationSection.swift rename to Mastodon/Diffable/Notification/NotificationSection.swift diff --git a/Mastodon/Diffiable/Onboarding/CategoryPickerItem.swift b/Mastodon/Diffable/Onboarding/CategoryPickerItem.swift similarity index 100% rename from Mastodon/Diffiable/Onboarding/CategoryPickerItem.swift rename to Mastodon/Diffable/Onboarding/CategoryPickerItem.swift diff --git a/Mastodon/Diffiable/Onboarding/CategoryPickerSection.swift b/Mastodon/Diffable/Onboarding/CategoryPickerSection.swift similarity index 100% rename from Mastodon/Diffiable/Onboarding/CategoryPickerSection.swift rename to Mastodon/Diffable/Onboarding/CategoryPickerSection.swift diff --git a/Mastodon/Diffiable/Onboarding/PickServerItem.swift b/Mastodon/Diffable/Onboarding/PickServerItem.swift similarity index 100% rename from Mastodon/Diffiable/Onboarding/PickServerItem.swift rename to Mastodon/Diffable/Onboarding/PickServerItem.swift diff --git a/Mastodon/Diffiable/Onboarding/PickServerSection.swift b/Mastodon/Diffable/Onboarding/PickServerSection.swift similarity index 100% rename from Mastodon/Diffiable/Onboarding/PickServerSection.swift rename to Mastodon/Diffable/Onboarding/PickServerSection.swift diff --git a/Mastodon/Diffiable/Onboarding/RegisterItem.swift b/Mastodon/Diffable/Onboarding/RegisterItem.swift similarity index 100% rename from Mastodon/Diffiable/Onboarding/RegisterItem.swift rename to Mastodon/Diffable/Onboarding/RegisterItem.swift diff --git a/Mastodon/Diffiable/Onboarding/RegisterSection.swift b/Mastodon/Diffable/Onboarding/RegisterSection.swift similarity index 100% rename from Mastodon/Diffiable/Onboarding/RegisterSection.swift rename to Mastodon/Diffable/Onboarding/RegisterSection.swift diff --git a/Mastodon/Diffiable/Onboarding/ServerRuleItem.swift b/Mastodon/Diffable/Onboarding/ServerRuleItem.swift similarity index 100% rename from Mastodon/Diffiable/Onboarding/ServerRuleItem.swift rename to Mastodon/Diffable/Onboarding/ServerRuleItem.swift diff --git a/Mastodon/Diffiable/Onboarding/ServerRuleSection.swift b/Mastodon/Diffable/Onboarding/ServerRuleSection.swift similarity index 100% rename from Mastodon/Diffiable/Onboarding/ServerRuleSection.swift rename to Mastodon/Diffable/Onboarding/ServerRuleSection.swift diff --git a/Mastodon/Diffiable/Profile/ProfileFieldItem.swift b/Mastodon/Diffable/Profile/ProfileFieldItem.swift similarity index 100% rename from Mastodon/Diffiable/Profile/ProfileFieldItem.swift rename to Mastodon/Diffable/Profile/ProfileFieldItem.swift diff --git a/Mastodon/Diffiable/Profile/ProfileFieldSection.swift b/Mastodon/Diffable/Profile/ProfileFieldSection.swift similarity index 100% rename from Mastodon/Diffiable/Profile/ProfileFieldSection.swift rename to Mastodon/Diffable/Profile/ProfileFieldSection.swift diff --git a/Mastodon/Diffiable/RecommandAccount/RecommendAccountItem.swift b/Mastodon/Diffable/RecommandAccount/RecommendAccountItem.swift similarity index 100% rename from Mastodon/Diffiable/RecommandAccount/RecommendAccountItem.swift rename to Mastodon/Diffable/RecommandAccount/RecommendAccountItem.swift diff --git a/Mastodon/Diffiable/RecommandAccount/RecommendAccountSection.swift b/Mastodon/Diffable/RecommandAccount/RecommendAccountSection.swift similarity index 100% rename from Mastodon/Diffiable/RecommandAccount/RecommendAccountSection.swift rename to Mastodon/Diffable/RecommandAccount/RecommendAccountSection.swift diff --git a/Mastodon/Diffiable/Report/ReportItem.swift b/Mastodon/Diffable/Report/ReportItem.swift similarity index 100% rename from Mastodon/Diffiable/Report/ReportItem.swift rename to Mastodon/Diffable/Report/ReportItem.swift diff --git a/Mastodon/Diffiable/Report/ReportSection.swift b/Mastodon/Diffable/Report/ReportSection.swift similarity index 100% rename from Mastodon/Diffiable/Report/ReportSection.swift rename to Mastodon/Diffable/Report/ReportSection.swift diff --git a/Mastodon/Diffiable/Search/SearchHistoryItem.swift b/Mastodon/Diffable/Search/SearchHistoryItem.swift similarity index 100% rename from Mastodon/Diffiable/Search/SearchHistoryItem.swift rename to Mastodon/Diffable/Search/SearchHistoryItem.swift diff --git a/Mastodon/Diffiable/Search/SearchHistorySection.swift b/Mastodon/Diffable/Search/SearchHistorySection.swift similarity index 100% rename from Mastodon/Diffiable/Search/SearchHistorySection.swift rename to Mastodon/Diffable/Search/SearchHistorySection.swift diff --git a/Mastodon/Diffiable/Search/SearchItem.swift b/Mastodon/Diffable/Search/SearchItem.swift similarity index 100% rename from Mastodon/Diffiable/Search/SearchItem.swift rename to Mastodon/Diffable/Search/SearchItem.swift diff --git a/Mastodon/Diffiable/Search/SearchResultItem.swift b/Mastodon/Diffable/Search/SearchResultItem.swift similarity index 100% rename from Mastodon/Diffiable/Search/SearchResultItem.swift rename to Mastodon/Diffable/Search/SearchResultItem.swift diff --git a/Mastodon/Diffiable/Search/SearchResultSection.swift b/Mastodon/Diffable/Search/SearchResultSection.swift similarity index 100% rename from Mastodon/Diffiable/Search/SearchResultSection.swift rename to Mastodon/Diffable/Search/SearchResultSection.swift diff --git a/Mastodon/Diffiable/Search/SearchSection.swift b/Mastodon/Diffable/Search/SearchSection.swift similarity index 100% rename from Mastodon/Diffiable/Search/SearchSection.swift rename to Mastodon/Diffable/Search/SearchSection.swift diff --git a/Mastodon/Diffiable/Settings/SettingsItem.swift b/Mastodon/Diffable/Settings/SettingsItem.swift similarity index 100% rename from Mastodon/Diffiable/Settings/SettingsItem.swift rename to Mastodon/Diffable/Settings/SettingsItem.swift diff --git a/Mastodon/Diffiable/Settings/SettingsSection.swift b/Mastodon/Diffable/Settings/SettingsSection.swift similarity index 100% rename from Mastodon/Diffiable/Settings/SettingsSection.swift rename to Mastodon/Diffable/Settings/SettingsSection.swift diff --git a/Mastodon/Diffiable/Status/StatusItem.swift b/Mastodon/Diffable/Status/StatusItem.swift similarity index 100% rename from Mastodon/Diffiable/Status/StatusItem.swift rename to Mastodon/Diffable/Status/StatusItem.swift diff --git a/Mastodon/Diffiable/Status/StatusSection.swift b/Mastodon/Diffable/Status/StatusSection.swift similarity index 100% rename from Mastodon/Diffiable/Status/StatusSection.swift rename to Mastodon/Diffable/Status/StatusSection.swift diff --git a/Mastodon/Diffiable/User/UserItem.swift b/Mastodon/Diffable/User/UserItem.swift similarity index 100% rename from Mastodon/Diffiable/User/UserItem.swift rename to Mastodon/Diffable/User/UserItem.swift diff --git a/Mastodon/Diffiable/User/UserSection.swift b/Mastodon/Diffable/User/UserSection.swift similarity index 100% rename from Mastodon/Diffiable/User/UserSection.swift rename to Mastodon/Diffable/User/UserSection.swift From fc3750c377d3f1db91fdc01b0526d1f30da02b36 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 8 Nov 2022 16:39:19 +0800 Subject: [PATCH 010/224] feat: add mediaView for compose scene --- .../Sources/MastodonUI/Extension/View.swift | 21 ++++++ .../Attachment/AttachmentView.swift | 13 ++-- .../Attachment/AttachmentViewModel.swift | 72 ++++++++----------- .../ComposeContentViewController.swift | 14 ++-- .../View/ComposeContentView.swift | 27 +++++++ .../Scene/View/StatusAttachmentView.swift | 13 ---- 6 files changed, 89 insertions(+), 71 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonUI/Extension/View.swift diff --git a/MastodonSDK/Sources/MastodonUI/Extension/View.swift b/MastodonSDK/Sources/MastodonUI/Extension/View.swift new file mode 100644 index 000000000..756e51b64 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Extension/View.swift @@ -0,0 +1,21 @@ +// +// View.swift +// +// +// Created by MainasuK on 2022/11/8. +// + +import SwiftUI + +extension View { + public func badgeView(_ content: Content) -> some View where Content: View { + overlay( + ZStack { + content + } + .alignmentGuide(.top) { $0.height / 2 } + .alignmentGuide(.trailing) { $0.width / 2 } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) + ) + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift index f4d1397a9..f67745849 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift @@ -13,18 +13,17 @@ import AVKit public struct AttachmentView: View { - static let size = CGSize(width: 56, height: 56) - static let cornerRadius: CGFloat = 8 - @ObservedObject var viewModel: AttachmentViewModel let action: (Action) -> Void - - @State var isCaptionEditorPresented = false - @State var caption = "" public var body: some View { - Text("Hello") + ZStack { + let image = viewModel.thumbnail ?? .placeholder(color: .secondarySystemFill) + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fill) + } // Menu { // menu // } label: { diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift index 7d0e8c859..f2c7e76e7 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift @@ -22,6 +22,7 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable var observations = Set() // input + public let authContext: AuthContext public let input: Input @Published var caption = "" @Published var sizeLimit = SizeLimit() @@ -33,13 +34,19 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable @Published var error: Error? let progress = Progress() // upload progress - public init(input: Input) { + public init( + authContext: AuthContext, + input: Input + ) { + self.authContext = authContext self.input = input super.init() // end init defer { - load(input: input) + Task { + await load(input: input) + } } $output @@ -53,6 +60,7 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable return nil } } + .receive(on: DispatchQueue.main) .assign(to: &$thumbnail) } @@ -86,13 +94,6 @@ extension AttachmentViewModel { case png case jpg } - - public var twitterMediaCategory: TwitterMediaCategory { - switch self { - case .image: return .image - case .video: return .amplifyVideo - } - } } public struct SizeLimit { @@ -115,18 +116,13 @@ extension AttachmentViewModel { case invalidAttachmentType case attachmentTooLarge } - - public enum TwitterMediaCategory: String { - case image = "TWEET_IMAGE" - case GIF = "TWEET_GIF" - case video = "TWEET_VIDEO" - case amplifyVideo = "AMPLIFY_VIDEO" - } + } extension AttachmentViewModel { - private func load(input: Input) { + @MainActor + private func load(input: Input) async { switch input { case .image(let image): guard let data = image.pngData() else { @@ -135,32 +131,26 @@ extension AttachmentViewModel { } output = .image(data, imageKind: .png) case .url(let url): - Task { @MainActor in - do { - let output = try await AttachmentViewModel.load(url: url) - self.output = output - } catch { - self.error = error - } - } // end Task + do { + let output = try await AttachmentViewModel.load(url: url) + self.output = output + } catch { + self.error = error + } case .pickerResult(let pickerResult): - Task { @MainActor in - do { - let output = try await AttachmentViewModel.load(itemProvider: pickerResult.itemProvider) - self.output = output - } catch { - self.error = error - } - } // end Task + do { + let output = try await AttachmentViewModel.load(itemProvider: pickerResult.itemProvider) + self.output = output + } catch { + self.error = error + } case .itemProvider(let itemProvider): - Task { @MainActor in - do { - let output = try await AttachmentViewModel.load(itemProvider: itemProvider) - self.output = output - } catch { - self.error = error - } - } // end Task + do { + let output = try await AttachmentViewModel.load(itemProvider: itemProvider) + self.output = output + } catch { + self.error = error + } } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index 3417ed935..38efe8feb 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -325,16 +325,10 @@ extension ComposeContentViewController: PHPickerViewControllerDelegate { public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.dismiss(animated: true, completion: nil) - // TODO: -// let attachmentServices: [MastodonAttachmentService] = results.map { result in -// let service = MastodonAttachmentService( -// context: context, -// pickerResult: result, -// initialAuthenticationBox: viewModel.authenticationBox -// ) -// return service -// } -// viewModel.attachmentServices = viewModel.attachmentServices + attachmentServices + let attachmentViewModels: [AttachmentViewModel] = results.map { result in + AttachmentViewModel(authContext: viewModel.authContext, input: .pickerResult(result)) + } + viewModel.attachmentViewModels += attachmentViewModels } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift index 25584848a..ffc92c01e 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift @@ -11,6 +11,7 @@ import MastodonAsset import MastodonCore import MastodonLocalization import Stripes +import Kingfisher public struct ComposeContentView: View { @@ -108,6 +109,9 @@ public struct ComposeContentView: View { // poll pollView .padding(.horizontal, ComposeContentView.margin) + // media + mediaView + .padding(.horizontal, ComposeContentView.margin) } .background( GeometryReader { proxy in @@ -194,6 +198,29 @@ extension ComposeContentView { } } // end VStack } + + // MARK: - media + var mediaView: some View { + VStack(spacing: 16) { + ForEach(viewModel.attachmentViewModels, id: \.self) { attachmentViewModel in + Color.clear.aspectRatio(358.0/232.0, contentMode: .fill) + .overlay( + AttachmentView(viewModel: attachmentViewModel) { action in + + } + ) + .clipShape(Rectangle()) + .badgeView( + Button { + viewModel.attachmentViewModels.removeAll(where: { $0 === attachmentViewModel }) + } label: { + Image(systemName: "minus.circle.fill") + .foregroundColor(.red) + } + ) + } // end ForEach + } // end VStack + } } //private struct ScrollOffsetPreferenceKey: PreferenceKey { diff --git a/ShareActionExtension/Scene/View/StatusAttachmentView.swift b/ShareActionExtension/Scene/View/StatusAttachmentView.swift index 90b8aceeb..8540b95f1 100644 --- a/ShareActionExtension/Scene/View/StatusAttachmentView.swift +++ b/ShareActionExtension/Scene/View/StatusAttachmentView.swift @@ -77,19 +77,6 @@ struct StatusAttachmentView: View { } } -extension View { - func badgeView(_ content: Content) -> some View where Content: View { - overlay( - ZStack { - content - } - .alignmentGuide(.top) { $0.height / 2 } - .alignmentGuide(.trailing) { $0.width / 2 } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) - ) - } -} - /// ref: https://stackoverflow.com/a/57715771/3797903 extension View { func placeholder( From bdedd54318e245b21942610b5aaa593809a07b06 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 8 Nov 2022 19:40:58 +0800 Subject: [PATCH 011/224] feat: bind the thumbnail and trigger media upload task --- Localization/app.json | 6 +- .../Scene/Compose/Attachment/Contents.json | 9 + .../Contents.json | 38 +++ .../retry.imageset/Arrow Clockwise.pdf | 91 +++++ .../Attachment/retry.imageset/Contents.json | 15 + .../Attachment/stop.imageset/Contents.json | 15 + .../Attachment/stop.imageset/Dismiss.pdf | 89 +++++ .../MastodonAsset/Generated/Assets.swift | 5 + .../API/Mastodon+API+V2+Media.swift | 14 + .../MastodonSDK/Query/MediaAttachment.swift | 2 +- .../Attachment/AttachmentView.swift | 283 ++++----------- .../AttachmentViewModel+DragAndDrop.swift | 144 ++++++++ .../Attachment/AttachmentViewModel+Load.swift | 148 ++++++++ .../AttachmentViewModel+Upload.swift | 142 +------- .../Attachment/AttachmentViewModel.swift | 323 +++--------------- .../ComposeContentViewController.swift | 6 +- .../Publisher/MastodonStatusPublisher.swift | 15 +- .../MastodonUI/Vendor/VisualEffectView.swift | 15 + 18 files changed, 735 insertions(+), 625 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/indicator.button.background.colorset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/retry.imageset/Arrow Clockwise.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/retry.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/stop.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/stop.imageset/Dismiss.pdf create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+DragAndDrop.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Load.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Vendor/VisualEffectView.swift diff --git a/Localization/app.json b/Localization/app.json index a965b23ae..650aff30e 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -374,7 +374,11 @@ "video": "video", "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "Describe the photo for the visually-impaired...", - "description_video": "Describe the video for the visually-impaired..." + "description_video": "Describe the video for the visually-impaired...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Duration: %s", diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/Contents.json new file mode 100644 index 000000000..6e965652d --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/indicator.button.background.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/indicator.button.background.colorset/Contents.json new file mode 100644 index 000000000..7a1c8d9e2 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/indicator.button.background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.400", + "green" : "0.275", + "red" : "0.275" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.400", + "green" : "0.275", + "red" : "0.275" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/retry.imageset/Arrow Clockwise.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/retry.imageset/Arrow Clockwise.pdf new file mode 100644 index 000000000..a15c522d8 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/retry.imageset/Arrow Clockwise.pdf @@ -0,0 +1,91 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.750000 2.750000 cm +0.000000 0.000000 0.000000 scn +9.250000 16.500000 m +5.245935 16.500000 2.000000 13.254065 2.000000 9.250000 c +2.000000 5.245935 5.245935 2.000000 9.250000 2.000000 c +13.254065 2.000000 16.500000 5.245935 16.500000 9.250000 c +16.500000 9.535608 16.483484 9.817360 16.451357 10.094351 c +16.383255 10.681498 16.809317 11.250000 17.400400 11.250000 c +17.916018 11.250000 18.369314 10.891933 18.431660 10.380100 c +18.476776 10.009713 18.500000 9.632568 18.500000 9.250000 c +18.500000 4.141366 14.358634 0.000000 9.250000 0.000000 c +4.141366 0.000000 0.000000 4.141366 0.000000 9.250000 c +0.000000 14.358634 4.141366 18.500000 9.250000 18.500000 c +11.423139 18.500000 13.421247 17.750608 15.000000 16.496151 c +15.000000 17.000000 l +15.000000 17.552284 15.447716 18.000000 16.000000 18.000000 c +16.552284 18.000000 17.000000 17.552284 17.000000 17.000000 c +17.000000 14.301708 l +17.011232 14.284512 17.022409 14.267276 17.033529 14.250000 c +17.000000 14.250000 l +17.000000 14.000000 l +17.000000 13.447716 16.552284 13.000000 16.000000 13.000000 c +13.000000 13.000000 l +12.447715 13.000000 12.000000 13.447716 12.000000 14.000000 c +12.000000 14.552284 12.447715 15.000000 13.000000 15.000000 c +13.666476 15.000000 l +12.443584 15.940684 10.912110 16.500000 9.250000 16.500000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 1365 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001455 00000 n +0000001478 00000 n +0000001651 00000 n +0000001725 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1784 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/retry.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/retry.imageset/Contents.json new file mode 100644 index 000000000..92bff3aca --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/retry.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Arrow Clockwise.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/stop.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/stop.imageset/Contents.json new file mode 100644 index 000000000..b2b588d4d --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/stop.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Dismiss.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/stop.imageset/Dismiss.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/stop.imageset/Dismiss.pdf new file mode 100644 index 000000000..0616f6275 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/stop.imageset/Dismiss.pdf @@ -0,0 +1,89 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.000000 3.804749 cm +0.000000 0.000000 0.000000 scn +0.209704 15.808150 m +0.292893 15.902358 l +0.653377 16.262842 1.220608 16.290571 1.612899 15.985547 c +1.707107 15.902358 l +8.000000 9.610251 l +14.292892 15.902358 l +14.683416 16.292883 15.316584 16.292883 15.707108 15.902358 c +16.097631 15.511834 16.097631 14.878669 15.707108 14.488145 c +9.415000 8.195251 l +15.707108 1.902359 l +16.067591 1.541875 16.095320 0.974643 15.790295 0.582352 c +15.707108 0.488144 l +15.346623 0.127661 14.779391 0.099932 14.387100 0.404957 c +14.292892 0.488144 l +8.000000 6.780252 l +1.707107 0.488144 l +1.316582 0.097620 0.683418 0.097620 0.292893 0.488144 c +-0.097631 0.878668 -0.097631 1.511835 0.292893 1.902359 c +6.585000 8.195251 l +0.292893 14.488145 l +-0.067591 14.848629 -0.095320 15.415859 0.209704 15.808150 c +0.292893 15.902358 l +0.209704 15.808150 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 914 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001004 00000 n +0000001026 00000 n +0000001199 00000 n +0000001273 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1332 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift index 5cd0059d8..fc47acdfd 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift @@ -130,6 +130,11 @@ public enum Asset { } public enum Scene { public enum Compose { + public enum Attachment { + public static let indicatorButtonBackground = ColorAsset(name: "Scene/Compose/Attachment/indicator.button.background") + public static let retry = ImageAsset(name: "Scene/Compose/Attachment/retry") + public static let stop = ImageAsset(name: "Scene/Compose/Attachment/stop") + } public static let earth = ImageAsset(name: "Scene/Compose/Earth") public static let mention = ImageAsset(name: "Scene/Compose/Mention") public static let more = ImageAsset(name: "Scene/Compose/More") diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+V2+Media.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+V2+Media.swift index 4f8ac71d5..6c905438c 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+V2+Media.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+V2+Media.swift @@ -43,6 +43,20 @@ extension Mastodon.API.V2.Media { request.timeoutInterval = 180 // should > 200 Kb/s for 40 MiB media attachment let serialStream = query.serialStream request.httpBodyStream = serialStream.boundStreams.input + + // total unit count in bytes count + // will small than actally count due to multipart protocol meta + serialStream.progress.totalUnitCount = { + var size = 0 + size += query.file?.sizeInByte ?? 0 + size += query.thumbnail?.sizeInByte ?? 0 + return Int64(size) + }() + query.progress.addChild( + serialStream.progress, + withPendingUnitCount: query.progress.totalUnitCount + ) + return session.dataTaskPublisher(for: request) .tryMap { data, response in let value = try Mastodon.API.decode(type: Mastodon.Entity.Attachment.self, from: data, response: response) diff --git a/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift b/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift index f1fdac8bb..05639964e 100644 --- a/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift +++ b/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift @@ -54,7 +54,7 @@ extension Mastodon.Query.MediaAttachment { return data.map { "data:" + mimeType + ";base64," + $0.base64EncodedString() } } - var sizeInByte: Int? { + public var sizeInByte: Int? { switch self { case .jpeg(let data), .gif(let data), .png(let data): return data?.count diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift index f67745849..c3ca6fc67 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift @@ -10,12 +10,17 @@ import UIKit import SwiftUI import Introspect import AVKit +import MastodonAsset public struct AttachmentView: View { @ObservedObject var viewModel: AttachmentViewModel let action: (Action) -> Void + + var blurEffect: UIBlurEffect { + UIBlurEffect(style: .systemUltraThinMaterialDark) + } public var body: some View { ZStack { @@ -23,223 +28,81 @@ public struct AttachmentView: View { Image(uiImage: image) .resizable() .aspectRatio(contentMode: .fill) + + // loading… + if viewModel.output == nil, viewModel.error == nil { + ProgressView() + .progressViewStyle(.circular) + } + + // load failed + // cannot re-entry + if viewModel.output == nil, let error = viewModel.error { + VisualEffectView(effect: blurEffect) + VStack { + Text("Load Failed") // TODO: i18n + .font(.system(size: 13, weight: .semibold)) + Text(error.localizedDescription) + .font(.system(size: 12, weight: .regular)) + } + } + + // loaded + // uploading… or upload failed + // could retry upload when error emit + if viewModel.output != nil { + VisualEffectView(effect: blurEffect) + VStack { + let image: UIImage = { + if let _ = viewModel.error { + return Asset.Scene.Compose.Attachment.retry.image.withRenderingMode(.alwaysTemplate) + } else { + return Asset.Scene.Compose.Attachment.stop.image.withRenderingMode(.alwaysTemplate) + } + }() + Image(uiImage: image) + .foregroundColor(.white) + .padding() + .background(Color(Asset.Scene.Compose.Attachment.indicatorButtonBackground.color)) + .clipShape(Circle()) + .padding() + let title: String = { + if let _ = viewModel.error { + return "Upload Failed" // TODO: i18n + } else { + let total = ByteCountFormatter.string(fromByteCount: Int64(viewModel.outputSizeInByte), countStyle: .memory) + return "…/\(total)" + } + }() + let subtitle: String = { + if let error = viewModel.error { + return error.localizedDescription + } else { + return "… remaining" + } + }() + Text(title) + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.white) + .padding(.horizontal) + Text(subtitle) + .font(.system(size: 12, weight: .regular)) + .foregroundColor(.white) + .padding(.horizontal) + } + } + } // end ZStack + .onChange(of: viewModel.progress) { progress in + // not works… + print(progress.completedUnitCount) } -// Menu { -// menu -// } label: { -// let image = viewModel.thumbnail ?? .placeholder(color: .systemGray3) -// Image(uiImage: image) -// .resizable() -// .aspectRatio(contentMode: .fill) -// .frame(width: AttachmentView.size.width, height: AttachmentView.size.height) -// .overlay { -// ZStack { -// // spinner -// if viewModel.output == nil { -// Color.clear -// .background(.ultraThinMaterial) -// ProgressView() -// .progressViewStyle(CircularProgressViewStyle()) -// .foregroundStyle(.regularMaterial) -// } -// // border -// RoundedRectangle(cornerRadius: AttachmentView.cornerRadius) -// .stroke(Color.black.opacity(0.05)) -// } -// .transition(.opacity) -// } -// .overlay(alignment: .bottom) { -// HStack(alignment: .bottom) { -// // alt -// VStack(spacing: 2) { -// switch viewModel.output { -// case .video: -// Image(uiImage: Asset.Media.playerRectangle.image) -// .resizable() -// .frame(width: 16, height: 12) -// default: -// EmptyView() -// } -// if !viewModel.caption.isEmpty { -// Image(uiImage: Asset.Media.altRectangle.image) -// .resizable() -// .frame(width: 16, height: 12) -// } -// } -// Spacer() -// // option -// Image(systemName: "ellipsis") -// .resizable() -// .frame(width: 12, height: 12) -// .symbolVariant(.circle) -// .symbolVariant(.fill) -// .symbolRenderingMode(.palette) -// .foregroundStyle(.white, .black) -// } -// .padding(6) -// } -// .cornerRadius(AttachmentView.cornerRadius) -// } // end Menu -// .sheet(isPresented: $isCaptionEditorPresented) { -// captionSheet -// } // end caption sheet -// .sheet(isPresented: $viewModel.isPreviewPresented) { -// previewSheet -// } // end preview sheet - } // end body -// var menu: some View { -// Group { -// Button( -// action: { -// action(.preview) -// }, -// label: { -// Label(L10n.Scene.Compose.Media.preview, systemImage: "photo") -// } -// ) -// // caption -// let canAddCaption: Bool = { -// switch viewModel.output { -// case .image: return true -// case .video: return false -// case .none: return false -// } -// }() -// if canAddCaption { -// Button( -// action: { -// action(.caption) -// caption = viewModel.caption -// isCaptionEditorPresented.toggle() -// }, -// label: { -// let title = viewModel.caption.isEmpty ? L10n.Scene.Compose.Media.Caption.add : L10n.Scene.Compose.Media.Caption.update -// Label(title, systemImage: "text.bubble") -// // FIXME: https://stackoverflow.com/questions/72318730/how-to-customize-swiftui-menu -// // add caption subtitle -// } -// ) -// } -// Divider() -// // remove -// Button( -// role: .destructive, -// action: { -// action(.remove) -// }, -// label: { -// Label(L10n.Scene.Compose.Media.remove, systemImage: "minus.circle") -// } -// ) -// } -// } - -// var captionSheet: some View { -// NavigationView { -// ScrollView(.vertical) { -// VStack { -// // preview -// switch viewModel.output { -// case .image: -// let image = viewModel.thumbnail ?? .placeholder(color: .systemGray3) -// Image(uiImage: image) -// .resizable() -// .aspectRatio(contentMode: .fill) -// case .video(let url, _): -// let player = AVPlayer(url: url) -// VideoPlayer(player: player) -// .frame(height: 300) -// case .none: -// EmptyView() -// } -// // caption textField -// TextField( -// text: $caption, -// prompt: Text(L10n.Scene.Compose.Media.Caption.addADescriptionForThisImage) -// ) { -// Text(L10n.Scene.Compose.Media.Caption.update) -// } -// .padding() -// .introspectTextField { textField in -// textField.becomeFirstResponder() -// } -// } -// } -// .navigationTitle(L10n.Scene.Compose.Media.Caption.update) -// .navigationBarTitleDisplayMode(.inline) -// .toolbar { -// ToolbarItem(placement: .navigationBarLeading) { -// Button { -// isCaptionEditorPresented.toggle() -// } label: { -// Image(systemName: "xmark.circle.fill") -// .resizable() -// .frame(width: 30, height: 30, alignment: .center) -// .symbolRenderingMode(.hierarchical) -// .foregroundStyle(Color(uiColor: .secondaryLabel), Color(uiColor: .tertiaryLabel)) -// } -// } -// ToolbarItem(placement: .navigationBarTrailing) { -// Button { -// viewModel.caption = caption.trimmingCharacters(in: .whitespacesAndNewlines) -// isCaptionEditorPresented.toggle() -// } label: { -// Text(L10n.Common.Controls.Actions.save) -// } -// } -// } -// } // end NavigationView -// } - - // design for share extension - // preferred UIKit preview in app -// var previewSheet: some View { -// NavigationView { -// ScrollView(.vertical) { -// VStack { -// // preview -// switch viewModel.output { -// case .image: -// let image = viewModel.thumbnail ?? .placeholder(color: .systemGray3) -// Image(uiImage: image) -// .resizable() -// .aspectRatio(contentMode: .fill) -// case .video(let url, _): -// let player = AVPlayer(url: url) -// VideoPlayer(player: player) -// .frame(height: 300) -// case .none: -// EmptyView() -// } -// Spacer() -// } -// } -// .navigationTitle(L10n.Scene.Compose.Media.preview) -// .navigationBarTitleDisplayMode(.inline) -// .toolbar { -// ToolbarItem(placement: .navigationBarLeading) { -// Button { -// viewModel.isPreviewPresented.toggle() -// } label: { -// Image(systemName: "xmark.circle.fill") -// .resizable() -// .frame(width: 30, height: 30, alignment: .center) -// .symbolRenderingMode(.hierarchical) -// .foregroundStyle(Color(uiColor: .secondaryLabel), Color(uiColor: .tertiaryLabel)) -// } -// } -// } -// } // end NavigationView -// } - } extension AttachmentView { public enum Action: Hashable { - case preview - case caption case remove + case retry } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+DragAndDrop.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+DragAndDrop.swift new file mode 100644 index 000000000..269b836bc --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+DragAndDrop.swift @@ -0,0 +1,144 @@ +// +// AttachmentViewModel+DragAndDrop.swift +// +// +// Created by MainasuK on 2022/11/8. +// + +import os.log +import UIKit +import Combine +import UniformTypeIdentifiers + +// MARK: - TypeIdentifiedItemProvider +extension AttachmentViewModel: TypeIdentifiedItemProvider { + public static var typeIdentifier: String { + // must in UTI format + // https://developer.apple.com/library/archive/qa/qa1796/_index.html + return "org.joinmastodon.app.AttachmentViewModel" + } +} + +// MARK: - NSItemProviderWriting +extension AttachmentViewModel: NSItemProviderWriting { + + + /// Attachment uniform type idendifiers + /// + /// The latest one for in-app drag and drop. + /// And use generic `image` and `movie` type to + /// allows transformable media in different formats + public static var writableTypeIdentifiersForItemProvider: [String] { + return [ + UTType.image.identifier, + UTType.movie.identifier, + AttachmentViewModel.typeIdentifier, + ] + } + + public var writableTypeIdentifiersForItemProvider: [String] { + // should append elements in priority order from high to low + var typeIdentifiers: [String] = [] + + // FIXME: check jpg or png + switch input { + case .image: + typeIdentifiers.append(UTType.png.identifier) + case .url(let url): + let _uti = UTType(filenameExtension: url.pathExtension) + if let uti = _uti { + if uti.conforms(to: .image) { + typeIdentifiers.append(UTType.png.identifier) + } else if uti.conforms(to: .movie) { + typeIdentifiers.append(UTType.mpeg4Movie.identifier) + } + } + case .pickerResult(let item): + if item.itemProvider.isImage() { + typeIdentifiers.append(UTType.png.identifier) + } else if item.itemProvider.isMovie() { + typeIdentifiers.append(UTType.mpeg4Movie.identifier) + } + case .itemProvider(let itemProvider): + if itemProvider.isImage() { + typeIdentifiers.append(UTType.png.identifier) + } else if itemProvider.isMovie() { + typeIdentifiers.append(UTType.mpeg4Movie.identifier) + } + } + + typeIdentifiers.append(AttachmentViewModel.typeIdentifier) + + return typeIdentifiers + } + + public func loadData( + withTypeIdentifier typeIdentifier: String, + forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void + ) -> Progress? { + switch typeIdentifier { + case AttachmentViewModel.typeIdentifier: + do { + let archiver = NSKeyedArchiver(requiringSecureCoding: false) + try archiver.encodeEncodable(id, forKey: NSKeyedArchiveRootObjectKey) + archiver.finishEncoding() + let data = archiver.encodedData + completionHandler(data, nil) + } catch { + assertionFailure() + completionHandler(nil, nil) + } + default: + break + } + + let loadingProgress = Progress(totalUnitCount: 100) + + Publishers.CombineLatest( + $output, + $error + ) + .sink { [weak self] output, error in + guard let self = self else { return } + + // continue when load completed + guard output != nil || error != nil else { return } + + switch output { + case .image(let data, _): + switch typeIdentifier { + case UTType.png.identifier: + loadingProgress.completedUnitCount = 100 + completionHandler(data, nil) + default: + completionHandler(nil, nil) + } + case .video(let url, _): + switch typeIdentifier { + case UTType.png.identifier: + let _image = AttachmentViewModel.createThumbnailForVideo(url: url) + let _data = _image?.pngData() + loadingProgress.completedUnitCount = 100 + completionHandler(_data, nil) + case UTType.mpeg4Movie.identifier: + let task = URLSession.shared.dataTask(with: url) { data, response, error in + completionHandler(data, error) + } + task.progress.observe(\.fractionCompleted) { progress, change in + loadingProgress.completedUnitCount = Int64(100 * progress.fractionCompleted) + } + .store(in: &self.observations) + task.resume() + default: + completionHandler(nil, nil) + } + case nil: + completionHandler(nil, error) + } + } + .store(in: &disposeBag) + + return loadingProgress + } + +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Load.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Load.swift new file mode 100644 index 000000000..a259485f1 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Load.swift @@ -0,0 +1,148 @@ +// +// AttachmentViewModel+Load.swift +// +// +// Created by MainasuK on 2022/11/8. +// + +import os.log +import UIKit +import AVKit +import UniformTypeIdentifiers + +extension AttachmentViewModel { + + @MainActor + func load(input: Input) async throws -> Output { + switch input { + case .image(let image): + guard let data = image.pngData() else { + throw AttachmentError.invalidAttachmentType + } + return .image(data, imageKind: .png) + case .url(let url): + do { + let output = try await AttachmentViewModel.load(url: url) + return output + } catch { + throw error + } + case .pickerResult(let pickerResult): + do { + let output = try await AttachmentViewModel.load(itemProvider: pickerResult.itemProvider) + return output + } catch { + throw error + } + case .itemProvider(let itemProvider): + do { + let output = try await AttachmentViewModel.load(itemProvider: itemProvider) + return output + } catch { + throw error + } + } + } + + private static func load(url: URL) async throws -> Output { + guard let uti = UTType(filenameExtension: url.pathExtension) else { + throw AttachmentError.invalidAttachmentType + } + + if uti.conforms(to: .image) { + guard url.startAccessingSecurityScopedResource() else { + throw AttachmentError.invalidAttachmentType + } + defer { url.stopAccessingSecurityScopedResource() } + let imageData = try Data(contentsOf: url) + return .image(imageData, imageKind: imageData.kf.imageFormat == .PNG ? .png : .jpg) + } else if uti.conforms(to: .movie) { + guard url.startAccessingSecurityScopedResource() else { + throw AttachmentError.invalidAttachmentType + } + defer { url.stopAccessingSecurityScopedResource() } + + let fileName = UUID().uuidString + let tempDirectoryURL = FileManager.default.temporaryDirectory + let fileURL = tempDirectoryURL.appendingPathComponent(fileName).appendingPathExtension(url.pathExtension) + try FileManager.default.createDirectory(at: tempDirectoryURL, withIntermediateDirectories: true, attributes: nil) + try FileManager.default.copyItem(at: url, to: fileURL) + return .video(fileURL, mimeType: UTType.movie.preferredMIMEType ?? "video/mp4") + } else { + throw AttachmentError.invalidAttachmentType + } + } + + private static func load(itemProvider: NSItemProvider) async throws -> Output { + if itemProvider.isImage() { + guard let result = try await itemProvider.loadImageData() else { + throw AttachmentError.invalidAttachmentType + } + let imageKind: Output.ImageKind = { + if let type = result.type { + if type == UTType.png { + return .png + } + if type == UTType.jpeg { + return .jpg + } + } + + let imageData = result.data + + if imageData.kf.imageFormat == .PNG { + return .png + } + if imageData.kf.imageFormat == .JPEG { + return .jpg + } + + assertionFailure("unknown image kind") + return .jpg + }() + return .image(result.data, imageKind: imageKind) + } else if itemProvider.isMovie() { + guard let result = try await itemProvider.loadVideoData() else { + throw AttachmentError.invalidAttachmentType + } + return .video(result.url, mimeType: "video/mp4") + } else { + assertionFailure() + throw AttachmentError.invalidAttachmentType + } + } + +} + +extension AttachmentViewModel { + static func createThumbnailForVideo(url: URL) -> UIImage? { + guard FileManager.default.fileExists(atPath: url.path) else { return nil } + let asset = AVURLAsset(url: url) + let assetImageGenerator = AVAssetImageGenerator(asset: asset) + assetImageGenerator.appliesPreferredTrackTransform = true // fix orientation + do { + let cgImage = try assetImageGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil) + let image = UIImage(cgImage: cgImage) + return image + } catch { + AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): thumbnail generate fail: \(error.localizedDescription)") + return nil + } + } +} + +extension NSItemProvider { + func isImage() -> Bool { + return hasRepresentationConforming( + toTypeIdentifier: UTType.image.identifier, + fileOptions: [] + ) + } + + func isMovie() -> Bool { + return hasRepresentationConforming( + toTypeIdentifier: UTType.movie.identifier, + fileOptions: [] + ) + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift index 0a4aadec3..fcb30d954 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift @@ -52,153 +52,25 @@ extension Data { } } -// Twitter Only -//extension AttachmentViewModel { -// class SliceResult { -// -// let fileURL: URL -// let chunks: Chunked -// let chunkCount: Int -// let type: UTType -// let sizeInBytes: UInt64 -// -// public init?( -// url: URL, -// type: UTType -// ) { -// guard let chunks = try? FileHandle(forReadingFrom: url).bytes.chunked else { return nil } -// let _sizeInBytes: UInt64? = { -// let attribute = try? FileManager.default.attributesOfItem(atPath: url.path) -// return attribute?[.size] as? UInt64 -// }() -// guard let sizeInBytes = _sizeInBytes else { return nil } -// -// self.fileURL = url -// self.chunks = chunks -// self.chunkCount = SliceResult.chunkCount(chunkSize: UInt64(chunks.chunkSize), sizeInBytes: sizeInBytes) -// self.type = type -// self.sizeInBytes = sizeInBytes -// } -// -// public init?( -// imageData: Data, -// type: UTType -// ) { -// let _fileURL = try? FileManager.default.createTemporaryFileURL( -// filename: UUID().uuidString, -// pathExtension: imageData.kf.imageFormat == .PNG ? "png" : "jpeg" -// ) -// guard let fileURL = _fileURL else { return nil } -// -// do { -// try imageData.write(to: fileURL) -// } catch { -// return nil -// } -// -// guard let chunks = try? FileHandle(forReadingFrom: fileURL).bytes.chunked else { -// return nil -// } -// let sizeInBytes = UInt64(imageData.count) -// -// self.fileURL = fileURL -// self.chunks = chunks -// self.chunkCount = SliceResult.chunkCount(chunkSize: UInt64(chunks.chunkSize), sizeInBytes: sizeInBytes) -// self.type = type -// self.sizeInBytes = sizeInBytes -// } -// -// static func chunkCount(chunkSize: UInt64, sizeInBytes: UInt64) -> Int { -// guard sizeInBytes > 0 else { return 0 } -// let count = sizeInBytes / chunkSize -// let remains = sizeInBytes % chunkSize -// let result = remains > 0 ? count + 1 : count -// return Int(result) -// } -// -// } -// -// static func slice(output: Output, sizeLimit: SizeLimit) -> SliceResult? { -// // needs execute in background -// assert(!Thread.isMainThread) -// -// // try png then use JPEG compress with Q=0.8 -// // then slice into 1MiB chunks -// switch output { -// case .image(let data, _): -// let maxPayloadSizeInBytes = sizeLimit.image -// -// // use processed imageData to remove EXIF -// guard let image = UIImage(data: data), -// var imageData = image.pngData() -// else { return nil } -// -// var didRemoveEXIF = false -// repeat { -// guard let image = KFCrossPlatformImage(data: imageData) else { return nil } -// if imageData.kf.imageFormat == .PNG { -// // A. png image -// guard let pngData = image.pngData() else { return nil } -// didRemoveEXIF = true -// if pngData.count > maxPayloadSizeInBytes { -// guard let compressedJpegData = image.jpegData(compressionQuality: 0.8) else { return nil } -// os_log("%{public}s[%{public}ld], %{public}s: compress png %.2fMiB -> jpeg %.2fMiB", ((#file as NSString).lastPathComponent), #line, #function, Double(imageData.count) / 1024 / 1024, Double(compressedJpegData.count) / 1024 / 1024) -// imageData = compressedJpegData -// } else { -// os_log("%{public}s[%{public}ld], %{public}s: png %.2fMiB", ((#file as NSString).lastPathComponent), #line, #function, Double(pngData.count) / 1024 / 1024) -// imageData = pngData -// } -// } else { -// // B. other image -// if !didRemoveEXIF { -// guard let jpegData = image.jpegData(compressionQuality: 0.8) else { return nil } -// os_log("%{public}s[%{public}ld], %{public}s: compress jpeg %.2fMiB -> jpeg %.2fMiB", ((#file as NSString).lastPathComponent), #line, #function, Double(imageData.count) / 1024 / 1024, Double(jpegData.count) / 1024 / 1024) -// imageData = jpegData -// didRemoveEXIF = true -// } else { -// let targetSize = CGSize(width: image.size.width * 0.8, height: image.size.height * 0.8) -// let scaledImage = image.af.imageScaled(to: targetSize) -// guard let compressedJpegData = scaledImage.jpegData(compressionQuality: 0.8) else { return nil } -// os_log("%{public}s[%{public}ld], %{public}s: compress jpeg %.2fMiB -> jpeg %.2fMiB", ((#file as NSString).lastPathComponent), #line, #function, Double(imageData.count) / 1024 / 1024, Double(compressedJpegData.count) / 1024 / 1024) -// imageData = compressedJpegData -// } -// } -// } while (imageData.count > maxPayloadSizeInBytes) -// -// return SliceResult( -// imageData: imageData, -// type: imageData.kf.imageFormat == .PNG ? UTType.png : UTType.jpeg -// ) -// -//// case .gif(let url): -//// fatalError() -// case .video(let url, _): -// return SliceResult( -// url: url, -// type: .movie -// ) -// } -// } -//} - extension AttachmentViewModel { struct UploadContext { let apiService: APIService let authContext: AuthContext } - enum UploadResult { - case mastodon(Mastodon.Response.Content) - } + public typealias UploadResult = Mastodon.Entity.Attachment } extension AttachmentViewModel { - func upload(context: UploadContext) async throws -> UploadResult { + func upload(context: UploadContext) async throws -> UploadResult { return try await uploadMastodonMedia( context: context ) } + + // MainActor is required here to trigger stream upload task + @MainActor private func uploadMastodonMedia( context: UploadContext ) async throws -> UploadResult { @@ -283,7 +155,7 @@ extension AttachmentViewModel { // escape here progress.completedUnitCount = progress.totalUnitCount - return .mastodon(attachmentStatusResponse) + return attachmentStatusResponse.value } else { AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): attachment processing. Retry \(waitProcessRetryCount)/\(waitProcessRetryLimit)") @@ -296,7 +168,7 @@ extension AttachmentViewModel { } else { AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment success: \(attachmentUploadResponse.value.url ?? "")") - return .mastodon(attachmentUploadResponse) + return attachmentUploadResponse.value } } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift index f2c7e76e7..20e8186ad 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift @@ -15,6 +15,7 @@ import MastodonCore final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable { static let logger = Logger(subsystem: "AttachmentViewModel", category: "ViewModel") + let logger = Logger(subsystem: "AttachmentViewModel", category: "ViewModel") public let id = UUID() @@ -22,32 +23,52 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable var observations = Set() // input + public let api: APIService public let authContext: AuthContext public let input: Input @Published var caption = "" @Published var sizeLimit = SizeLimit() - @Published public var isPreviewPresented = false // output @Published public private(set) var output: Output? @Published public private(set) var thumbnail: UIImage? // original size image thumbnail + @Published public private(set) var outputSizeInByte: Int = 0 + + @Published public var uploadResult: UploadResult? @Published var error: Error? + let progress = Progress() // upload progress public init( + api: APIService, authContext: AuthContext, input: Input ) { + self.api = api self.authContext = authContext self.input = input super.init() // end init - - defer { - Task { - await load(input: input) + + progress + .observe(\.fractionCompleted, options: [.initial, .new]) { [weak self] progress, _ in + guard let self = self else { return } + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): publish progress \(progress.fractionCompleted)") + DispatchQueue.main.async { + self.objectWillChange.send() + } } - } + .store(in: &observations) + + progress + .observe(\.isFinished, options: [.initial, .new]) { [weak self] progress, _ in + guard let self = self else { return } + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): publish progress \(progress.fractionCompleted)") + DispatchQueue.main.async { + self.objectWillChange.send() + } + } + .store(in: &observations) $output .map { output -> UIImage? in @@ -62,6 +83,23 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable } .receive(on: DispatchQueue.main) .assign(to: &$thumbnail) + + defer { + Task { @MainActor in + do { + let output = try await load(input: input) + self.output = output + self.outputSizeInByte = output.asAttachment.sizeInByte ?? 0 + let uploadResult = try await self.upload(context: .init( + apiService: self.api, + authContext: self.authContext + )) + self.uploadResult = uploadResult + } catch { + self.error = error + } + } // end Task + } } deinit { @@ -112,280 +150,23 @@ extension AttachmentViewModel { } } - public enum AttachmentError: Error { + public enum AttachmentError: Error, LocalizedError { case invalidAttachmentType case attachmentTooLarge - } - -} - -extension AttachmentViewModel { - - @MainActor - private func load(input: Input) async { - switch input { - case .image(let image): - guard let data = image.pngData() else { - error = AttachmentError.invalidAttachmentType - return - } - output = .image(data, imageKind: .png) - case .url(let url): - do { - let output = try await AttachmentViewModel.load(url: url) - self.output = output - } catch { - self.error = error - } - case .pickerResult(let pickerResult): - do { - let output = try await AttachmentViewModel.load(itemProvider: pickerResult.itemProvider) - self.output = output - } catch { - self.error = error - } - case .itemProvider(let itemProvider): - do { - let output = try await AttachmentViewModel.load(itemProvider: itemProvider) - self.output = output - } catch { - self.error = error - } - } - } - - private static func load(url: URL) async throws -> Output { - guard let uti = UTType(filenameExtension: url.pathExtension) else { - throw AttachmentError.invalidAttachmentType - } - if uti.conforms(to: .image) { - guard url.startAccessingSecurityScopedResource() else { - throw AttachmentError.invalidAttachmentType + public var errorDescription: String? { + switch self { + case .invalidAttachmentType: + return "Can not regonize this media attachment" // TODO: i18n + case .attachmentTooLarge: + return "Attachment too large" } - defer { url.stopAccessingSecurityScopedResource() } - let imageData = try Data(contentsOf: url) - return .image(imageData, imageKind: imageData.kf.imageFormat == .PNG ? .png : .jpg) - } else if uti.conforms(to: .movie) { - guard url.startAccessingSecurityScopedResource() else { - throw AttachmentError.invalidAttachmentType - } - defer { url.stopAccessingSecurityScopedResource() } - - let fileName = UUID().uuidString - let tempDirectoryURL = FileManager.default.temporaryDirectory - let fileURL = tempDirectoryURL.appendingPathComponent(fileName).appendingPathExtension(url.pathExtension) - try FileManager.default.createDirectory(at: tempDirectoryURL, withIntermediateDirectories: true, attributes: nil) - try FileManager.default.copyItem(at: url, to: fileURL) - return .video(fileURL, mimeType: UTType.movie.preferredMIMEType ?? "video/mp4") - } else { - throw AttachmentError.invalidAttachmentType - } - } - - private static func load(itemProvider: NSItemProvider) async throws -> Output { - if itemProvider.isImage() { - guard let result = try await itemProvider.loadImageData() else { - throw AttachmentError.invalidAttachmentType - } - let imageKind: Output.ImageKind = { - if let type = result.type { - if type == UTType.png { - return .png - } - if type == UTType.jpeg { - return .jpg - } - } - - let imageData = result.data - - if imageData.kf.imageFormat == .PNG { - return .png - } - if imageData.kf.imageFormat == .JPEG { - return .jpg - } - - assertionFailure("unknown image kind") - return .jpg - }() - return .image(result.data, imageKind: imageKind) - } else if itemProvider.isMovie() { - guard let result = try await itemProvider.loadVideoData() else { - throw AttachmentError.invalidAttachmentType - } - return .video(result.url, mimeType: "video/mp4") - } else { - assertionFailure() - throw AttachmentError.invalidAttachmentType } } } -extension AttachmentViewModel { - static func createThumbnailForVideo(url: URL) -> UIImage? { - guard FileManager.default.fileExists(atPath: url.path) else { return nil } - let asset = AVURLAsset(url: url) - let assetImageGenerator = AVAssetImageGenerator(asset: asset) - assetImageGenerator.appliesPreferredTrackTransform = true // fix orientation - do { - let cgImage = try assetImageGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil) - let image = UIImage(cgImage: cgImage) - return image - } catch { - AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): thumbnail generate fail: \(error.localizedDescription)") - return nil - } - } -} -// MARK: - TypeIdentifiedItemProvider -extension AttachmentViewModel: TypeIdentifiedItemProvider { - public static var typeIdentifier: String { - // must in UTI format - // https://developer.apple.com/library/archive/qa/qa1796/_index.html - return "com.twidere.AttachmentViewModel" - } -} -// MARK: - NSItemProviderWriting -extension AttachmentViewModel: NSItemProviderWriting { - - - /// Attachment uniform type idendifiers - /// - /// The latest one for in-app drag and drop. - /// And use generic `image` and `movie` type to - /// allows transformable media in different formats - public static var writableTypeIdentifiersForItemProvider: [String] { - return [ - UTType.image.identifier, - UTType.movie.identifier, - AttachmentViewModel.typeIdentifier, - ] - } - - public var writableTypeIdentifiersForItemProvider: [String] { - // should append elements in priority order from high to low - var typeIdentifiers: [String] = [] - - // FIXME: check jpg or png - switch input { - case .image: - typeIdentifiers.append(UTType.png.identifier) - case .url(let url): - let _uti = UTType(filenameExtension: url.pathExtension) - if let uti = _uti { - if uti.conforms(to: .image) { - typeIdentifiers.append(UTType.png.identifier) - } else if uti.conforms(to: .movie) { - typeIdentifiers.append(UTType.mpeg4Movie.identifier) - } - } - case .pickerResult(let item): - if item.itemProvider.isImage() { - typeIdentifiers.append(UTType.png.identifier) - } else if item.itemProvider.isMovie() { - typeIdentifiers.append(UTType.mpeg4Movie.identifier) - } - case .itemProvider(let itemProvider): - if itemProvider.isImage() { - typeIdentifiers.append(UTType.png.identifier) - } else if itemProvider.isMovie() { - typeIdentifiers.append(UTType.mpeg4Movie.identifier) - } - } - - typeIdentifiers.append(AttachmentViewModel.typeIdentifier) - - return typeIdentifiers - } - - public func loadData( - withTypeIdentifier typeIdentifier: String, - forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void - ) -> Progress? { - switch typeIdentifier { - case AttachmentViewModel.typeIdentifier: - do { - let archiver = NSKeyedArchiver(requiringSecureCoding: false) - try archiver.encodeEncodable(id, forKey: NSKeyedArchiveRootObjectKey) - archiver.finishEncoding() - let data = archiver.encodedData - completionHandler(data, nil) - } catch { - assertionFailure() - completionHandler(nil, nil) - } - default: - break - } - - let loadingProgress = Progress(totalUnitCount: 100) - - Publishers.CombineLatest( - $output, - $error - ) - .sink { [weak self] output, error in - guard let self = self else { return } - - // continue when load completed - guard output != nil || error != nil else { return } - - switch output { - case .image(let data, _): - switch typeIdentifier { - case UTType.png.identifier: - loadingProgress.completedUnitCount = 100 - completionHandler(data, nil) - default: - completionHandler(nil, nil) - } - case .video(let url, _): - switch typeIdentifier { - case UTType.png.identifier: - let _image = AttachmentViewModel.createThumbnailForVideo(url: url) - let _data = _image?.pngData() - loadingProgress.completedUnitCount = 100 - completionHandler(_data, nil) - case UTType.mpeg4Movie.identifier: - let task = URLSession.shared.dataTask(with: url) { data, response, error in - completionHandler(data, error) - } - task.progress.observe(\.fractionCompleted) { progress, change in - loadingProgress.completedUnitCount = Int64(100 * progress.fractionCompleted) - } - .store(in: &self.observations) - task.resume() - default: - completionHandler(nil, nil) - } - case nil: - completionHandler(nil, error) - } - } - .store(in: &disposeBag) - - return loadingProgress - } - -} -extension NSItemProvider { - fileprivate func isImage() -> Bool { - return hasRepresentationConforming( - toTypeIdentifier: UTType.image.identifier, - fileOptions: [] - ) - } - - fileprivate func isMovie() -> Bool { - return hasRepresentationConforming( - toTypeIdentifier: UTType.movie.identifier, - fileOptions: [] - ) - } -} + diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index 38efe8feb..b9fbab856 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -326,7 +326,11 @@ extension ComposeContentViewController: PHPickerViewControllerDelegate { picker.dismiss(animated: true, completion: nil) let attachmentViewModels: [AttachmentViewModel] = results.map { result in - AttachmentViewModel(authContext: viewModel.authContext, input: .pickerResult(result)) + AttachmentViewModel( + api: viewModel.context.apiService, + authContext: viewModel.authContext, + input: .pickerResult(result) + ) } viewModel.attachmentViewModels += attachmentViewModels } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift index ea3be18a8..31568552c 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift @@ -119,13 +119,16 @@ extension MastodonStatusPublisher: StatusPublisher { progress.addChild(attachmentViewModel.progress, withPendingUnitCount: publishAttachmentTaskWeight) // upload media do { - let result = try await attachmentViewModel.upload(context: uploadContext) - guard case let .mastodon(response) = result else { - assertionFailure() - continue + guard let attachment = attachmentViewModel.uploadResult else { + // precondition: all media uploaded + throw AppError.badRequest } - let attachmentID = response.value.id - attachmentIDs.append(attachmentID) + attachmentIDs.append(attachment.id) + + // TODO: allow background upload + // let attachment = try await attachmentViewModel.upload(context: uploadContext) + // let attachmentID = attachment.id + // attachmentIDs.append(attachmentID) } catch { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment fail: \(error.localizedDescription)") _state = .failure(error) diff --git a/MastodonSDK/Sources/MastodonUI/Vendor/VisualEffectView.swift b/MastodonSDK/Sources/MastodonUI/Vendor/VisualEffectView.swift new file mode 100644 index 000000000..fe89b0457 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Vendor/VisualEffectView.swift @@ -0,0 +1,15 @@ +// +// VisualEffectView.swift +// +// +// Created by MainasuK on 2022/11/8. +// + +import SwiftUI + +// ref: https://stackoverflow.com/a/59111492/3797903 +public struct VisualEffectView: UIViewRepresentable { + public var effect: UIVisualEffect? + public func makeUIView(context: UIViewRepresentableContext) -> UIVisualEffectView { UIVisualEffectView() } + public func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext) { uiView.effect = effect } +} From 60b69ca2e539a86d0e8f6ffee40655f0e74c57e3 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 8 Nov 2022 13:50:23 -0500 Subject: [PATCH 012/224] Add real localization keys --- Localization/Localizable.stringsdict | 14 +++++++----- .../input/en.lproj/Localizable.stringsdict | 22 +++++++++++++++++++ .../StringsConvertor/input/en.lproj/app.json | 4 +++- .../Generated/Strings.swift | 10 +++++++++ .../Resources/en.lproj/Localizable.strings | 2 ++ .../en.lproj/Localizable.stringsdict | 22 +++++++++++++++++++ .../View/ComposeContentToolbarView.swift | 6 ++--- .../View/ComposeContentView.swift | 3 +-- 8 files changed, 70 insertions(+), 13 deletions(-) diff --git a/Localization/Localizable.stringsdict b/Localization/Localizable.stringsdict index 46b79deb2..cd97825f4 100644 --- a/Localization/Localizable.stringsdict +++ b/Localization/Localizable.stringsdict @@ -71,21 +71,23 @@ a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ + %#@character_count@ left character_count NSStringFormatSpecTypeKey NSStringPluralRuleType + NSStringFormatValueTypeKey + ld zero - no characters left + no characters one - 1 character left + 1 character few - %ld characters left + %ld characters many - %ld characters left + %ld characters other - %ld characters left + %ld characters plural.count.followed_by_and_mutual diff --git a/Localization/StringsConvertor/input/en.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/en.lproj/Localizable.stringsdict index bdcae6ac9..297e6675a 100644 --- a/Localization/StringsConvertor/input/en.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/en.lproj/Localizable.stringsdict @@ -50,6 +50,28 @@ %ld characters + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + no characters + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/Localization/StringsConvertor/input/en.lproj/app.json b/Localization/StringsConvertor/input/en.lproj/app.json index a965b23ae..69b71b0e9 100644 --- a/Localization/StringsConvertor/input/en.lproj/app.json +++ b/Localization/StringsConvertor/input/en.lproj/app.json @@ -405,7 +405,9 @@ "custom_emoji_picker": "Custom Emoji Picker", "enable_content_warning": "Enable Content Warning", "disable_content_warning": "Disable Content Warning", - "post_visibility_menu": "Post Visibility Menu" + "post_visibility_menu": "Post Visibility Menu", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Discard Post", diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 52ed59c09..d6a2d2579 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -435,6 +435,12 @@ public enum L10n { public static let disableContentWarning = L10n.tr("Localizable", "Scene.Compose.Accessibility.DisableContentWarning") /// Enable Content Warning public static let enableContentWarning = L10n.tr("Localizable", "Scene.Compose.Accessibility.EnableContentWarning") + /// Posting as %@ + public static func postingAs(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.Compose.Accessibility.PostingAs", String(describing: p1)) + } + /// Post Options + public static let postOptions = L10n.tr("Localizable", "Scene.Compose.Accessibility.PostOptions") /// Post Visibility Menu public static let postVisibilityMenu = L10n.tr("Localizable", "Scene.Compose.Accessibility.PostVisibilityMenu") /// Remove Poll @@ -1262,6 +1268,10 @@ public enum L10n { public enum A11y { public enum Plural { public enum Count { + /// Plural format key: "%#@character_count@ left" + public static func charactersLeft(_ p1: Int) -> String { + return L10n.tr("Localizable", "a11y.plural.count.characters_left", p1) + } /// Plural format key: "Input limit exceeds %#@character_count@" public static func inputLimitExceeds(_ p1: Int) -> String { return L10n.tr("Localizable", "a11y.plural.count.input_limit_exceeds", p1) diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings index 6917eb0c7..494d49fdc 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings @@ -160,7 +160,9 @@ Your profile looks like this to them."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Custom Emoji Picker"; "Scene.Compose.Accessibility.DisableContentWarning" = "Disable Content Warning"; "Scene.Compose.Accessibility.EnableContentWarning" = "Enable Content Warning"; +"Scene.Compose.Accessibility.PostOptions" = "Post Options"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Post Visibility Menu"; +"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; "Scene.Compose.Accessibility.RemovePoll" = "Remove Poll"; "Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can’t be uploaded to Mastodon."; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.stringsdict index bdcae6ac9..297e6675a 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.stringsdict @@ -50,6 +50,28 @@ %ld characters + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + no characters + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift index ac1a56b20..b7f01e64a 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift @@ -87,16 +87,14 @@ struct ComposeContentToolbarView: View { Text("\(remains)") .foregroundColor(Color(isOverflow ? UIColor.systemRed : UIColor.secondaryLabel)) .font(.system(size: isOverflow ? 18 : 16, weight: isOverflow ? .medium : .regular)) - // TODO: i18n (a11y.plural.count.characters_left) - .accessibilityLabel("\(remains) characters left") + .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) - // TODO: i18n (scene.compose.accessibility.post_options) - .accessibilityLabel("Post Options") + .accessibilityLabel(L10n.Scene.Compose.Accessibility.postOptions) } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift index b48f4e060..98b55018f 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift @@ -148,8 +148,7 @@ extension ComposeContentView { Spacer() } .accessibilityElement(children: .ignore) - // TODO: i18n (scene.compose.accessibility.posting_as) - .accessibilityLabel("Posting as \([viewModel.name.string, viewModel.username].joined(separator: ", "))") + .accessibilityLabel(L10n.Scene.Compose.Accessibility.postingAs([viewModel.name.string, viewModel.username].joined(separator: ", "))) } } From 1a4f8b795eb1376033e58296edb3d8ca08621086 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 08:27:54 +0100 Subject: [PATCH 013/224] New translations app.json (Catalan) --- Localization/StringsConvertor/input/ca.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ca.lproj/app.json b/Localization/StringsConvertor/input/ca.lproj/app.json index 4766eb31b..79ec14f07 100644 --- a/Localization/StringsConvertor/input/ca.lproj/app.json +++ b/Localization/StringsConvertor/input/ca.lproj/app.json @@ -696,7 +696,7 @@ "accessibility_hint": "Toca dues vegades per descartar l'assistent" }, "bookmark": { - "title": "Bookmarks" + "title": "Marcadors" } } } From 015698dcd677e3ab2fed0d20aac6675296b49dc8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 09:28:19 +0100 Subject: [PATCH 014/224] New translations app.json (Italian) --- Localization/StringsConvertor/input/it.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json index 269d299ec..be98dfda8 100644 --- a/Localization/StringsConvertor/input/it.lproj/app.json +++ b/Localization/StringsConvertor/input/it.lproj/app.json @@ -696,7 +696,7 @@ "accessibility_hint": "Doppio tocco per eliminare questa procedura guidata" }, "bookmark": { - "title": "Bookmarks" + "title": "Segnalibri" } } } From 953b28bdc63f7ec4ecbe982b78c7c8c85b0ba77a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 10:34:00 +0100 Subject: [PATCH 015/224] New translations app.json (Chinese Traditional) --- Localization/StringsConvertor/input/zh-Hant.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json index ae497109e..b8e124a5e 100644 --- a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json @@ -696,7 +696,7 @@ "accessibility_hint": "點兩下以關閉此設定精靈" }, "bookmark": { - "title": "Bookmarks" + "title": "書籤" } } } From 4a07cc8a5060fd7fdacaa8113b873a54f39241bb Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 12:23:33 +0100 Subject: [PATCH 016/224] New translations app.json (Galician) --- .../StringsConvertor/input/gl.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index 513573f79..8385e6c4a 100644 --- a/Localization/StringsConvertor/input/gl.lproj/app.json +++ b/Localization/StringsConvertor/input/gl.lproj/app.json @@ -181,8 +181,8 @@ "unmute_user": "Deixar de acalar a @%s", "muted": "Acalada", "edit_info": "Editar info", - "show_reblogs": "Show Reblogs", - "hide_reblogs": "Hide Reblogs" + "show_reblogs": "Mostrar Promocións", + "hide_reblogs": "Agochar Promocións" }, "timeline": { "filtered": "Filtrado", @@ -459,12 +459,12 @@ "message": "Confirma o desbloqueo de %s" }, "confirm_show_reblogs": { - "title": "Show Reblogs", - "message": "Confirm to show reblogs" + "title": "Mostrar Promocións", + "message": "Confirma para ver promocións" }, "confirm_hide_reblogs": { - "title": "Hide Reblogs", - "message": "Confirm to hide reblogs" + "title": "Agochar Promocións", + "message": "Confirma para agochar promocións" } }, "accessibility": { @@ -696,7 +696,7 @@ "accessibility_hint": "Dobre toque para desbotar este asistente" }, "bookmark": { - "title": "Bookmarks" + "title": "Marcadores" } } } From 1425f348283de31e5bb8b2ca715944fc7b0d54b7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 15:19:18 +0100 Subject: [PATCH 017/224] New translations app.json (Vietnamese) --- Localization/StringsConvertor/input/vi.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/vi.lproj/app.json b/Localization/StringsConvertor/input/vi.lproj/app.json index b857399b3..082091a4c 100644 --- a/Localization/StringsConvertor/input/vi.lproj/app.json +++ b/Localization/StringsConvertor/input/vi.lproj/app.json @@ -696,7 +696,7 @@ "accessibility_hint": "Nhấn hai lần để bỏ qua" }, "bookmark": { - "title": "Bookmarks" + "title": "Tút đã lưu" } } } From d173ceb8c3f80b3ea7cf55693163bc0c4237ba76 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 15:19:19 +0100 Subject: [PATCH 018/224] New translations app.json (Swedish) --- Localization/StringsConvertor/input/sv.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/sv.lproj/app.json b/Localization/StringsConvertor/input/sv.lproj/app.json index 85f243b03..f1c2bf0ff 100644 --- a/Localization/StringsConvertor/input/sv.lproj/app.json +++ b/Localization/StringsConvertor/input/sv.lproj/app.json @@ -696,7 +696,7 @@ "accessibility_hint": "Dubbeltryck för att avvisa den här guiden" }, "bookmark": { - "title": "Bookmarks" + "title": "Bokmärken" } } } From 02ceccf33be169e6ba231805826aa8c749ad99ae Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Wed, 9 Nov 2022 09:45:55 -0500 Subject: [PATCH 019/224] Add accessibility labels to the profile navigation bar --- Mastodon/Scene/Profile/ProfileViewController.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 3ce1fd33a..b1cc573aa 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -51,6 +51,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi action: #selector(ProfileViewController.settingBarButtonItemPressed(_:)) ) barButtonItem.tintColor = .white + barButtonItem.accessibilityLabel = L10n.Common.Controls.Actions.settings return barButtonItem }() @@ -62,6 +63,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi action: #selector(ProfileViewController.shareBarButtonItemPressed(_:)) ) barButtonItem.tintColor = .white + barButtonItem.accessibilityLabel = L10n.Common.Controls.Actions.share return barButtonItem }() @@ -73,6 +75,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi action: #selector(ProfileViewController.favoriteBarButtonItemPressed(_:)) ) barButtonItem.tintColor = .white + barButtonItem.accessibilityLabel = L10n.Scene.Favorite.title return barButtonItem }() @@ -84,18 +87,21 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi action: #selector(ProfileViewController.bookmarkBarButtonItemPressed(_:)) ) barButtonItem.tintColor = .white + barButtonItem.accessibilityLabel = L10n.Scene.Bookmark.title return barButtonItem }() private(set) lazy var replyBarButtonItem: UIBarButtonItem = { let barButtonItem = UIBarButtonItem(image: UIImage(systemName: "arrowshape.turn.up.left"), style: .plain, target: self, action: #selector(ProfileViewController.replyBarButtonItemPressed(_:))) barButtonItem.tintColor = .white + barButtonItem.accessibilityLabel = L10n.Common.Controls.Actions.reply return barButtonItem }() let moreMenuBarButtonItem: UIBarButtonItem = { let barButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .plain, target: nil, action: nil) barButtonItem.tintColor = .white + barButtonItem.accessibilityLabel = L10n.Common.Controls.Actions.seeMore return barButtonItem }() From 306b611887dff07da804b1274685a73f9d9cfd9d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:35:57 +0100 Subject: [PATCH 020/224] New translations app.json (Slovenian) --- Localization/StringsConvertor/input/sl.lproj/app.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/sl.lproj/app.json b/Localization/StringsConvertor/input/sl.lproj/app.json index 99a823feb..d5b594032 100644 --- a/Localization/StringsConvertor/input/sl.lproj/app.json +++ b/Localization/StringsConvertor/input/sl.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Glasuj", "closed": "Zaprto" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Odgovori", "reblog": "Poobjavi", @@ -696,7 +702,7 @@ "accessibility_hint": "Dvakrat tapnite, da zapustite tega čarovnika" }, "bookmark": { - "title": "Bookmarks" + "title": "Zaznamki" } } } From bb5384ee0a6897be5121ff473b22d4002e5c37f6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:00 +0100 Subject: [PATCH 021/224] New translations app.json (Indonesian) --- Localization/StringsConvertor/input/id.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/id.lproj/app.json b/Localization/StringsConvertor/input/id.lproj/app.json index 607e9a638..b0c8aaa4d 100644 --- a/Localization/StringsConvertor/input/id.lproj/app.json +++ b/Localization/StringsConvertor/input/id.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Vote", "closed": "Ditutup" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Balas", "reblog": "Reblog", From 199191f49b1aab800670b139ce485e5df289b133 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:01 +0100 Subject: [PATCH 022/224] New translations app.json (Portuguese) --- Localization/StringsConvertor/input/pt.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/pt.lproj/app.json b/Localization/StringsConvertor/input/pt.lproj/app.json index 80b0882d9..8867385e2 100644 --- a/Localization/StringsConvertor/input/pt.lproj/app.json +++ b/Localization/StringsConvertor/input/pt.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Vote", "closed": "Closed" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Reply", "reblog": "Reblog", From 51687dd60071423aa0d301637bcda57c586fc705 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:02 +0100 Subject: [PATCH 023/224] New translations app.json (Russian) --- Localization/StringsConvertor/input/ru.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/ru.lproj/app.json b/Localization/StringsConvertor/input/ru.lproj/app.json index 7a4833554..da7ac82b0 100644 --- a/Localization/StringsConvertor/input/ru.lproj/app.json +++ b/Localization/StringsConvertor/input/ru.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Проголосовать", "closed": "Завершён" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Ответить", "reblog": "Продвинуть", From 5e16710cd89043a71fc023165bba4bf5b8680eb0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:03 +0100 Subject: [PATCH 024/224] New translations app.json (Chinese Simplified) --- Localization/StringsConvertor/input/zh-Hans.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json index 7f3703b8a..36e3925b7 100644 --- a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json @@ -136,6 +136,12 @@ "vote": "投票", "closed": "已关闭" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "回复", "reblog": "转发", From c709ae0b8a3c7dd8ad6797d15d5c173582f03182 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:04 +0100 Subject: [PATCH 025/224] New translations app.json (English) --- Localization/StringsConvertor/input/en.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/en.lproj/app.json b/Localization/StringsConvertor/input/en.lproj/app.json index 80b0882d9..8867385e2 100644 --- a/Localization/StringsConvertor/input/en.lproj/app.json +++ b/Localization/StringsConvertor/input/en.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Vote", "closed": "Closed" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Reply", "reblog": "Reblog", From a055cffd5212f338b94503dea49ea4867f8300e4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:06 +0100 Subject: [PATCH 026/224] New translations app.json (Galician) --- Localization/StringsConvertor/input/gl.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index 8385e6c4a..9c5e737d8 100644 --- a/Localization/StringsConvertor/input/gl.lproj/app.json +++ b/Localization/StringsConvertor/input/gl.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Votar", "closed": "Pechada" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Responder", "reblog": "Promover", From d1c384fc4e12c370e7f21a8383168208098492b3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:07 +0100 Subject: [PATCH 027/224] New translations app.json (Portuguese, Brazilian) --- Localization/StringsConvertor/input/pt-BR.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/app.json b/Localization/StringsConvertor/input/pt-BR.lproj/app.json index 063ed346c..56656321c 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/app.json +++ b/Localization/StringsConvertor/input/pt-BR.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Vote", "closed": "Closed" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Responder", "reblog": "Reblog", From 25054e3c318ec684f80a55f56deeb3f682f501a6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:08 +0100 Subject: [PATCH 028/224] New translations app.json (Spanish, Argentina) --- Localization/StringsConvertor/input/es-AR.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/es-AR.lproj/app.json b/Localization/StringsConvertor/input/es-AR.lproj/app.json index 62d439a3c..deedf1933 100644 --- a/Localization/StringsConvertor/input/es-AR.lproj/app.json +++ b/Localization/StringsConvertor/input/es-AR.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Votar", "closed": "Cerrada" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Responder", "reblog": "Adherir", From a39159605fd059a01ff90eb82018367ad1077c2b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:09 +0100 Subject: [PATCH 029/224] New translations app.json (Japanese) --- Localization/StringsConvertor/input/ja.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/ja.lproj/app.json b/Localization/StringsConvertor/input/ja.lproj/app.json index b7615abf3..1be96cb77 100644 --- a/Localization/StringsConvertor/input/ja.lproj/app.json +++ b/Localization/StringsConvertor/input/ja.lproj/app.json @@ -136,6 +136,12 @@ "vote": "投票", "closed": "終了" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "返信", "reblog": "ブースト", From 04bd669fb6b4767e66bd0148e52c3083b57096b8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:10 +0100 Subject: [PATCH 030/224] New translations app.json (Thai) --- Localization/StringsConvertor/input/th.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/th.lproj/app.json b/Localization/StringsConvertor/input/th.lproj/app.json index 763b827cd..ee0e6e8b2 100644 --- a/Localization/StringsConvertor/input/th.lproj/app.json +++ b/Localization/StringsConvertor/input/th.lproj/app.json @@ -136,6 +136,12 @@ "vote": "ลงคะแนน", "closed": "ปิดแล้ว" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "ตอบกลับ", "reblog": "ดัน", From d68266688cd548f2f965311c29178130687701ce Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:12 +0100 Subject: [PATCH 031/224] New translations app.json (Latvian) --- Localization/StringsConvertor/input/lv.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/lv.lproj/app.json b/Localization/StringsConvertor/input/lv.lproj/app.json index 0051383db..5a854d5c4 100644 --- a/Localization/StringsConvertor/input/lv.lproj/app.json +++ b/Localization/StringsConvertor/input/lv.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Balsot", "closed": "Aizvērts" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Atbildēt", "reblog": "Reblogot", From 659ec2fe1d7426ad2001c6ef78bb2cf02298c023 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:14 +0100 Subject: [PATCH 032/224] New translations app.json (Hindi) --- Localization/StringsConvertor/input/hi.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/hi.lproj/app.json b/Localization/StringsConvertor/input/hi.lproj/app.json index d9ef32b3a..514961a44 100644 --- a/Localization/StringsConvertor/input/hi.lproj/app.json +++ b/Localization/StringsConvertor/input/hi.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Vote", "closed": "Closed" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Reply", "reblog": "Reblog", From ca900401bcd9630d0b4c86aa76199eb865377d5e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:15 +0100 Subject: [PATCH 033/224] New translations app.json (English, United States) --- Localization/StringsConvertor/input/en-US.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/en-US.lproj/app.json b/Localization/StringsConvertor/input/en-US.lproj/app.json index 80b0882d9..8867385e2 100644 --- a/Localization/StringsConvertor/input/en-US.lproj/app.json +++ b/Localization/StringsConvertor/input/en-US.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Vote", "closed": "Closed" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Reply", "reblog": "Reblog", From 666b655485f76bb6633abfaa8cef13133fbbb134 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:16 +0100 Subject: [PATCH 034/224] New translations app.json (Welsh) --- Localization/StringsConvertor/input/cy.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/cy.lproj/app.json b/Localization/StringsConvertor/input/cy.lproj/app.json index bc7f75d96..00f09eca9 100644 --- a/Localization/StringsConvertor/input/cy.lproj/app.json +++ b/Localization/StringsConvertor/input/cy.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Vote", "closed": "Closed" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Reply", "reblog": "Hybwch", From b7508792ec851ac7ff22b7aaf0b5f3c8ce8a8688 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:18 +0100 Subject: [PATCH 035/224] New translations app.json (Sinhala) --- Localization/StringsConvertor/input/si.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/si.lproj/app.json b/Localization/StringsConvertor/input/si.lproj/app.json index f42e91ae1..c70c0f722 100644 --- a/Localization/StringsConvertor/input/si.lproj/app.json +++ b/Localization/StringsConvertor/input/si.lproj/app.json @@ -136,6 +136,12 @@ "vote": "ඡන්දය", "closed": "වසා ඇත" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "පිළිතුරු", "reblog": "Reblog", From 85d953249237684c87451c310a94f964658c5063 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:19 +0100 Subject: [PATCH 036/224] New translations app.json (Kurmanji (Kurdish)) --- Localization/StringsConvertor/input/kmr.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/kmr.lproj/app.json b/Localization/StringsConvertor/input/kmr.lproj/app.json index d48edf3ae..33c283fc7 100644 --- a/Localization/StringsConvertor/input/kmr.lproj/app.json +++ b/Localization/StringsConvertor/input/kmr.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Deng bide", "closed": "Girtî" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Bersivê bide", "reblog": "Ji nû ve nivîsandin", From 4caa1b85da8bf93700e7ed03f72cadf2c384d93b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:20 +0100 Subject: [PATCH 037/224] New translations app.json (Dutch) --- Localization/StringsConvertor/input/nl.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/nl.lproj/app.json b/Localization/StringsConvertor/input/nl.lproj/app.json index 649fe5064..a94129fef 100644 --- a/Localization/StringsConvertor/input/nl.lproj/app.json +++ b/Localization/StringsConvertor/input/nl.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Stemmen", "closed": "Gesloten" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Reageren", "reblog": "Delen", From 3a460fe5a1b73275ab092fb1193f56402224c910 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:21 +0100 Subject: [PATCH 038/224] New translations app.json (Italian) --- Localization/StringsConvertor/input/it.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json index be98dfda8..dfbb2e9f9 100644 --- a/Localization/StringsConvertor/input/it.lproj/app.json +++ b/Localization/StringsConvertor/input/it.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Vota", "closed": "Chiuso" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Rispondi", "reblog": "Condivisione", From d8773a4f4111e6a0f67ed99b611a8356f79d52f4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:22 +0100 Subject: [PATCH 039/224] New translations app.json (Chinese Traditional) --- Localization/StringsConvertor/input/zh-Hant.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json index b8e124a5e..041d0677c 100644 --- a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json @@ -136,6 +136,12 @@ "vote": "投票", "closed": "已關閉" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "回覆", "reblog": "轉嘟", From def1c940b361fff06b58c333b8a3b473dd0fb4c2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:24 +0100 Subject: [PATCH 040/224] New translations app.json (Ukrainian) --- Localization/StringsConvertor/input/uk.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/uk.lproj/app.json b/Localization/StringsConvertor/input/uk.lproj/app.json index 80b0882d9..8867385e2 100644 --- a/Localization/StringsConvertor/input/uk.lproj/app.json +++ b/Localization/StringsConvertor/input/uk.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Vote", "closed": "Closed" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Reply", "reblog": "Reblog", From 77a5d8e81df5131b7605430ee7e96bb972a8eb3f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:25 +0100 Subject: [PATCH 041/224] New translations app.json (Vietnamese) --- Localization/StringsConvertor/input/vi.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/vi.lproj/app.json b/Localization/StringsConvertor/input/vi.lproj/app.json index 082091a4c..0ae436884 100644 --- a/Localization/StringsConvertor/input/vi.lproj/app.json +++ b/Localization/StringsConvertor/input/vi.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Bình chọn", "closed": "Kết thúc" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Trả lời", "reblog": "Đăng lại", From a1c6e815ae7564dbf3080e6a062086c0de13def7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:26 +0100 Subject: [PATCH 042/224] New translations app.json (Kabyle) --- Localization/StringsConvertor/input/kab.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/kab.lproj/app.json b/Localization/StringsConvertor/input/kab.lproj/app.json index 2cff3d68d..8fefef724 100644 --- a/Localization/StringsConvertor/input/kab.lproj/app.json +++ b/Localization/StringsConvertor/input/kab.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Dɣeṛ", "closed": "Ifukk" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Err", "reblog": "Aɛiwed n usuffeɣ", From 952004e94938b69514c5146b27bec01fe50bd47e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:28 +0100 Subject: [PATCH 043/224] New translations app.json (Korean) --- Localization/StringsConvertor/input/ko.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/ko.lproj/app.json b/Localization/StringsConvertor/input/ko.lproj/app.json index bbb4d1dea..917487705 100644 --- a/Localization/StringsConvertor/input/ko.lproj/app.json +++ b/Localization/StringsConvertor/input/ko.lproj/app.json @@ -136,6 +136,12 @@ "vote": "투표", "closed": "마감" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "답글", "reblog": "리블로그", From 8afa8bc7a22e2659b21c1ce15b7fd565afcfd2e2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:29 +0100 Subject: [PATCH 044/224] New translations app.json (Swedish) --- Localization/StringsConvertor/input/sv.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/sv.lproj/app.json b/Localization/StringsConvertor/input/sv.lproj/app.json index f1c2bf0ff..7451ab2f6 100644 --- a/Localization/StringsConvertor/input/sv.lproj/app.json +++ b/Localization/StringsConvertor/input/sv.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Rösta", "closed": "Stängd" }, + "meta_entity": { + "url": "Länk: %s", + "hashtag": "Hashtagg %s", + "mention": "Visa profil: %s", + "email": "E-postadress: %s" + }, "actions": { "reply": "Svara", "reblog": "Puffa", From 3c654f0fb1ea04c9b8a38d4b941e79ee19484694 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:31 +0100 Subject: [PATCH 045/224] New translations app.json (French) --- Localization/StringsConvertor/input/fr.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/fr.lproj/app.json b/Localization/StringsConvertor/input/fr.lproj/app.json index ed53d1096..dcd1d97ce 100644 --- a/Localization/StringsConvertor/input/fr.lproj/app.json +++ b/Localization/StringsConvertor/input/fr.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Voter", "closed": "Fermé" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Répondre", "reblog": "Rebloguer", From 518b057feb367323d7829ea9091d9c0dcfeb9d56 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:32 +0100 Subject: [PATCH 046/224] New translations app.json (Turkish) --- Localization/StringsConvertor/input/tr.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/tr.lproj/app.json b/Localization/StringsConvertor/input/tr.lproj/app.json index cef7fd7f4..ffe46c14e 100644 --- a/Localization/StringsConvertor/input/tr.lproj/app.json +++ b/Localization/StringsConvertor/input/tr.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Oy ver", "closed": "Kapandı" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Yanıtla", "reblog": "Yeniden paylaş", From f44b3d5f3d0fe713611539eb2ff3c367478558e3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:33 +0100 Subject: [PATCH 047/224] New translations app.json (Czech) --- Localization/StringsConvertor/input/cs.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/cs.lproj/app.json b/Localization/StringsConvertor/input/cs.lproj/app.json index 550f71808..d4076758d 100644 --- a/Localization/StringsConvertor/input/cs.lproj/app.json +++ b/Localization/StringsConvertor/input/cs.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Hlasovat", "closed": "Uzavřeno" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Odpovědět", "reblog": "Boostnout", From d98cf9a1b23ac673f8bfd221baef152da672a33f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:34 +0100 Subject: [PATCH 048/224] New translations app.json (Scottish Gaelic) --- Localization/StringsConvertor/input/gd.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/gd.lproj/app.json b/Localization/StringsConvertor/input/gd.lproj/app.json index a2062a89b..5d1611d6b 100644 --- a/Localization/StringsConvertor/input/gd.lproj/app.json +++ b/Localization/StringsConvertor/input/gd.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Cuir bhòt", "closed": "Dùinte" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Freagair", "reblog": "Brosnaich", From 7053ff8eaa66b27410ae100884c688061341dadd Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:35 +0100 Subject: [PATCH 049/224] New translations app.json (Finnish) --- Localization/StringsConvertor/input/fi.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/fi.lproj/app.json b/Localization/StringsConvertor/input/fi.lproj/app.json index d6210c4d5..383c5f0fe 100644 --- a/Localization/StringsConvertor/input/fi.lproj/app.json +++ b/Localization/StringsConvertor/input/fi.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Vote", "closed": "Suljettu" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Vastaa", "reblog": "Jaa edelleen", From bbc73ffaabfcf6ca0f611386a50cb2be79cd2a80 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:36 +0100 Subject: [PATCH 050/224] New translations app.json (Romanian) --- Localization/StringsConvertor/input/ro.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/ro.lproj/app.json b/Localization/StringsConvertor/input/ro.lproj/app.json index 8b9da0903..7173bfac9 100644 --- a/Localization/StringsConvertor/input/ro.lproj/app.json +++ b/Localization/StringsConvertor/input/ro.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Vote", "closed": "Closed" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Reply", "reblog": "Reblog", From 5d3f62046a6affb078b24140f7cd44681cc0df7b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:38 +0100 Subject: [PATCH 051/224] New translations app.json (Spanish) --- Localization/StringsConvertor/input/es.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/es.lproj/app.json b/Localization/StringsConvertor/input/es.lproj/app.json index 39e0f37d1..48683a18e 100644 --- a/Localization/StringsConvertor/input/es.lproj/app.json +++ b/Localization/StringsConvertor/input/es.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Vota", "closed": "Cerrado" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Responder", "reblog": "Rebloguear", From e4be7965936a8614d07709c7663515076b101910 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:39 +0100 Subject: [PATCH 052/224] New translations app.json (Arabic) --- Localization/StringsConvertor/input/ar.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/ar.lproj/app.json b/Localization/StringsConvertor/input/ar.lproj/app.json index ce68229ac..aed90c72d 100644 --- a/Localization/StringsConvertor/input/ar.lproj/app.json +++ b/Localization/StringsConvertor/input/ar.lproj/app.json @@ -136,6 +136,12 @@ "vote": "صَوِّت", "closed": "انتهى" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "الرَّد", "reblog": "إعادة النشر", From dd772a9bc8dad5bb9c801d2119e313edf6f510be Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:40 +0100 Subject: [PATCH 053/224] New translations app.json (Catalan) --- Localization/StringsConvertor/input/ca.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/ca.lproj/app.json b/Localization/StringsConvertor/input/ca.lproj/app.json index 79ec14f07..b2915dec5 100644 --- a/Localization/StringsConvertor/input/ca.lproj/app.json +++ b/Localization/StringsConvertor/input/ca.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Vota", "closed": "Finalitzada" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Respon", "reblog": "Impuls", From 248ff57f696be40773d89d9a4283bbf860e0e89e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:41 +0100 Subject: [PATCH 054/224] New translations app.json (Danish) --- Localization/StringsConvertor/input/da.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/da.lproj/app.json b/Localization/StringsConvertor/input/da.lproj/app.json index 80b0882d9..8867385e2 100644 --- a/Localization/StringsConvertor/input/da.lproj/app.json +++ b/Localization/StringsConvertor/input/da.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Vote", "closed": "Closed" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Reply", "reblog": "Reblog", From 94a6fae5662aabb3cffd2c876745332a9521a678 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:43 +0100 Subject: [PATCH 055/224] New translations app.json (German) --- Localization/StringsConvertor/input/de.lproj/app.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/de.lproj/app.json b/Localization/StringsConvertor/input/de.lproj/app.json index 355bfcc1b..3643d89ba 100644 --- a/Localization/StringsConvertor/input/de.lproj/app.json +++ b/Localization/StringsConvertor/input/de.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Abstimmen", "closed": "Beendet" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Antworten", "reblog": "Teilen", @@ -460,7 +466,7 @@ }, "confirm_show_reblogs": { "title": "Reblogs anzeigen", - "message": "Confirm to show reblogs" + "message": "Bestätigen um Reblogs anzuzeigen" }, "confirm_hide_reblogs": { "title": "Reblogs ausblenden", @@ -696,7 +702,7 @@ "accessibility_hint": "Doppeltippen, um diesen Assistenten zu schließen" }, "bookmark": { - "title": "Bookmarks" + "title": "Lesezeichen" } } } From a99c4d9a422c533a184f9163a8d1edd70b34e5ca Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:44 +0100 Subject: [PATCH 056/224] New translations app.json (Basque) --- Localization/StringsConvertor/input/eu.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/eu.lproj/app.json b/Localization/StringsConvertor/input/eu.lproj/app.json index 5c2e16601..118c6a2ed 100644 --- a/Localization/StringsConvertor/input/eu.lproj/app.json +++ b/Localization/StringsConvertor/input/eu.lproj/app.json @@ -136,6 +136,12 @@ "vote": "Bozkatu", "closed": "Itxita" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "Erantzun", "reblog": "Bultzada", From 14e32ce4864360a88d6e8bc1f3c15e6654b0c77a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:36:46 +0100 Subject: [PATCH 057/224] New translations app.json (Sorani (Kurdish)) --- Localization/StringsConvertor/input/ckb.lproj/app.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Localization/StringsConvertor/input/ckb.lproj/app.json b/Localization/StringsConvertor/input/ckb.lproj/app.json index 49f72d7a3..312d4d1d0 100644 --- a/Localization/StringsConvertor/input/ckb.lproj/app.json +++ b/Localization/StringsConvertor/input/ckb.lproj/app.json @@ -136,6 +136,12 @@ "vote": "دەنگ بدە", "closed": "داخراوە" }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, "actions": { "reply": "وەڵامی بدەوە", "reblog": "پۆستی بکەوە", From d1bf623c7636371e02633dcf4bf6dd69c91f7668 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 18:02:06 +0100 Subject: [PATCH 058/224] New translations app.json (Slovenian) --- Localization/StringsConvertor/input/sl.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/sl.lproj/app.json b/Localization/StringsConvertor/input/sl.lproj/app.json index d5b594032..d6c914dc7 100644 --- a/Localization/StringsConvertor/input/sl.lproj/app.json +++ b/Localization/StringsConvertor/input/sl.lproj/app.json @@ -137,10 +137,10 @@ "closed": "Zaprto" }, "meta_entity": { - "url": "Link: %s", - "hashtag": "Hastag %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "url": "Povezava: %s", + "hashtag": "Ključnik %s", + "mention": "Pokaži profil: %s", + "email": "E-naslov: %s" }, "actions": { "reply": "Odgovori", From f98f1a1e7b37ccb10a1393bc82c445792183b902 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 18:02:07 +0100 Subject: [PATCH 059/224] New translations app.json (Chinese Traditional) --- .../StringsConvertor/input/zh-Hant.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json index 041d0677c..6d154d039 100644 --- a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json @@ -137,10 +137,10 @@ "closed": "已關閉" }, "meta_entity": { - "url": "Link: %s", - "hashtag": "Hastag %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "url": "連結:%s", + "hashtag": "主題標籤 %s", + "mention": "顯示個人檔案:%s", + "email": "電子郵件地址:%s" }, "actions": { "reply": "回覆", From 2fe0db17103a045f4e25aaaa7a615b763545b3f4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 18:02:08 +0100 Subject: [PATCH 060/224] New translations app.json (Italian) --- Localization/StringsConvertor/input/it.lproj/app.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json index dfbb2e9f9..601dff8b2 100644 --- a/Localization/StringsConvertor/input/it.lproj/app.json +++ b/Localization/StringsConvertor/input/it.lproj/app.json @@ -137,10 +137,10 @@ "closed": "Chiuso" }, "meta_entity": { - "url": "Link: %s", + "url": "Collegamento: %s", "hashtag": "Hastag %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "mention": "Mostra il profilo: %s", + "email": "Indirizzo email: %s" }, "actions": { "reply": "Rispondi", From 5dc3eb2fb187f60bf8f25c206df905db50d81d35 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 19:16:33 +0100 Subject: [PATCH 061/224] New translations app.json (Catalan) --- Localization/StringsConvertor/input/ca.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/ca.lproj/app.json b/Localization/StringsConvertor/input/ca.lproj/app.json index b2915dec5..45497e67d 100644 --- a/Localization/StringsConvertor/input/ca.lproj/app.json +++ b/Localization/StringsConvertor/input/ca.lproj/app.json @@ -137,10 +137,10 @@ "closed": "Finalitzada" }, "meta_entity": { - "url": "Link: %s", - "hashtag": "Hastag %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "url": "Enllaç: %s", + "hashtag": "Etiqueta %s", + "mention": "Mostra el Perfil: %s", + "email": "Correu electrònic: %s" }, "actions": { "reply": "Respon", From baf62ec2004f30b9c1b0cebdc368af048d5a56be Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 20:14:08 +0100 Subject: [PATCH 062/224] New translations app.json (Thai) --- .../StringsConvertor/input/th.lproj/app.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Localization/StringsConvertor/input/th.lproj/app.json b/Localization/StringsConvertor/input/th.lproj/app.json index ee0e6e8b2..d79534b38 100644 --- a/Localization/StringsConvertor/input/th.lproj/app.json +++ b/Localization/StringsConvertor/input/th.lproj/app.json @@ -137,10 +137,10 @@ "closed": "ปิดแล้ว" }, "meta_entity": { - "url": "Link: %s", + "url": "ลิงก์: %s", "hashtag": "Hastag %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "mention": "โปรไฟล์ที่แสดง: %s", + "email": "ที่อยู่อีเมล: %s" }, "actions": { "reply": "ตอบกลับ", @@ -187,8 +187,8 @@ "unmute_user": "เลิกซ่อน %s", "muted": "ซ่อนอยู่", "edit_info": "แก้ไขข้อมูล", - "show_reblogs": "Show Reblogs", - "hide_reblogs": "Hide Reblogs" + "show_reblogs": "แสดงการดัน", + "hide_reblogs": "ซ่อนการดัน" }, "timeline": { "filtered": "กรองอยู่", @@ -465,12 +465,12 @@ "message": "ยืนยันเพื่อเลิกปิดกั้น %s" }, "confirm_show_reblogs": { - "title": "Show Reblogs", - "message": "Confirm to show reblogs" + "title": "แสดงการดัน", + "message": "ยืนยันเพื่อแสดงการดัน" }, "confirm_hide_reblogs": { - "title": "Hide Reblogs", - "message": "Confirm to hide reblogs" + "title": "ซ่อนการดัน", + "message": "ยืนยันเพื่อซ่อนการดัน" } }, "accessibility": { @@ -702,7 +702,7 @@ "accessibility_hint": "แตะสองครั้งเพื่อปิดตัวช่วยสร้างนี้" }, "bookmark": { - "title": "Bookmarks" + "title": "ที่คั่นหน้า" } } } From b22ea07bea81b6264cc30fd6f41956f4b979dede Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 21:10:16 +0100 Subject: [PATCH 063/224] New translations app.json (Italian) --- Localization/StringsConvertor/input/it.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json index 601dff8b2..096deb444 100644 --- a/Localization/StringsConvertor/input/it.lproj/app.json +++ b/Localization/StringsConvertor/input/it.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Collegamento: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Mostra il profilo: %s", "email": "Indirizzo email: %s" }, From 3af257d25ea15a2c74c339b4af1fa883c07b2ca9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 21:10:17 +0100 Subject: [PATCH 064/224] New translations app.json (Thai) --- Localization/StringsConvertor/input/th.lproj/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/th.lproj/app.json b/Localization/StringsConvertor/input/th.lproj/app.json index d79534b38..aa39586da 100644 --- a/Localization/StringsConvertor/input/th.lproj/app.json +++ b/Localization/StringsConvertor/input/th.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "ลิงก์: %s", - "hashtag": "Hastag %s", + "hashtag": "แฮชแท็ก %s", "mention": "โปรไฟล์ที่แสดง: %s", "email": "ที่อยู่อีเมล: %s" }, @@ -221,7 +221,7 @@ "server_picker": { "title": "Mastodon ประกอบด้วยผู้ใช้ในเซิร์ฟเวอร์ต่าง ๆ", "subtitle": "เลือกเซิร์ฟเวอร์ตามความสนใจ, ภูมิภาค หรือวัตถุประสงค์ทั่วไปของคุณ", - "subtitle_extend": "เลือกเซิร์ฟเวอร์ตามความสนใจ, ภูมิภาค หรือวัตถุประสงค์ทั่วไปของคุณ แต่ละเซิร์ฟเวอร์ดำเนินการโดยองค์กรหรือบุคคลที่เป็นอิสระโดยสิ้นเชิง", + "subtitle_extend": "เลือกเซิร์ฟเวอร์ตามความสนใจ, ภูมิภาค หรือวัตถุประสงค์ทั่วไปของคุณ แต่ละเซิร์ฟเวอร์ได้รับการดำเนินงานโดยองค์กรหรือบุคคลที่เป็นอิสระโดยสิ้นเชิง", "button": { "category": { "all": "ทั้งหมด", From ad3e8b46ea63438f3eaa3b3a64c3ee0469cd78f6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 22:06:23 +0100 Subject: [PATCH 065/224] New translations app.json (French) --- Localization/StringsConvertor/input/fr.lproj/app.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Localization/StringsConvertor/input/fr.lproj/app.json b/Localization/StringsConvertor/input/fr.lproj/app.json index dcd1d97ce..ebc654e11 100644 --- a/Localization/StringsConvertor/input/fr.lproj/app.json +++ b/Localization/StringsConvertor/input/fr.lproj/app.json @@ -137,10 +137,10 @@ "closed": "Fermé" }, "meta_entity": { - "url": "Link: %s", - "hashtag": "Hastag %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "url": "Lien : %s", + "hashtag": "Hastag : %s", + "mention": "Afficher le profile : %s", + "email": "Adresse e-mail : %s" }, "actions": { "reply": "Répondre", @@ -702,7 +702,7 @@ "accessibility_hint": "Tapotez deux fois pour fermer cet assistant" }, "bookmark": { - "title": "Bookmarks" + "title": "Favoris" } } } From b70491a338993b68420803e505f388d0fa44c566 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:41 +0100 Subject: [PATCH 066/224] New translations app.json (Slovenian) --- Localization/StringsConvertor/input/sl.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/sl.lproj/app.json b/Localization/StringsConvertor/input/sl.lproj/app.json index d6c914dc7..c4df31a4a 100644 --- a/Localization/StringsConvertor/input/sl.lproj/app.json +++ b/Localization/StringsConvertor/input/sl.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Povezava: %s", - "hashtag": "Ključnik %s", + "hashtag": "Hashtag: %s", "mention": "Pokaži profil: %s", "email": "E-naslov: %s" }, From f73ae9c723d6e84321f9f9afa359386320ee07eb Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:43 +0100 Subject: [PATCH 067/224] New translations app.json (Indonesian) --- Localization/StringsConvertor/input/id.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/id.lproj/app.json b/Localization/StringsConvertor/input/id.lproj/app.json index b0c8aaa4d..fe678e75c 100644 --- a/Localization/StringsConvertor/input/id.lproj/app.json +++ b/Localization/StringsConvertor/input/id.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 4a431ddf5a5d0bb30d83936be43d5ce545ed48cf Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:44 +0100 Subject: [PATCH 068/224] New translations app.json (Portuguese) --- Localization/StringsConvertor/input/pt.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/pt.lproj/app.json b/Localization/StringsConvertor/input/pt.lproj/app.json index 8867385e2..c5a3dac74 100644 --- a/Localization/StringsConvertor/input/pt.lproj/app.json +++ b/Localization/StringsConvertor/input/pt.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 7df3102569ac372145b58a14a41f39590a542970 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:45 +0100 Subject: [PATCH 069/224] New translations app.json (Russian) --- Localization/StringsConvertor/input/ru.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ru.lproj/app.json b/Localization/StringsConvertor/input/ru.lproj/app.json index da7ac82b0..c7d721aea 100644 --- a/Localization/StringsConvertor/input/ru.lproj/app.json +++ b/Localization/StringsConvertor/input/ru.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From cded21162b5ee0971a6063ed9edfa78360d27d5b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:46 +0100 Subject: [PATCH 070/224] New translations app.json (Chinese Simplified) --- Localization/StringsConvertor/input/zh-Hans.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json index 36e3925b7..32d41e016 100644 --- a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From f3ec978e0e242bc99f2fca2c563683aa94a928bd Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:47 +0100 Subject: [PATCH 071/224] New translations app.json (English) --- Localization/StringsConvertor/input/en.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/en.lproj/app.json b/Localization/StringsConvertor/input/en.lproj/app.json index 8867385e2..c5a3dac74 100644 --- a/Localization/StringsConvertor/input/en.lproj/app.json +++ b/Localization/StringsConvertor/input/en.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 6e626a5dc280386aae521839181db412908c8782 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:48 +0100 Subject: [PATCH 072/224] New translations app.json (Galician) --- Localization/StringsConvertor/input/gl.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index 9c5e737d8..ee3801a08 100644 --- a/Localization/StringsConvertor/input/gl.lproj/app.json +++ b/Localization/StringsConvertor/input/gl.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From e59cd191e4ae459a02ccf03864acff52bd56300d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:49 +0100 Subject: [PATCH 073/224] New translations app.json (Portuguese, Brazilian) --- Localization/StringsConvertor/input/pt-BR.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/app.json b/Localization/StringsConvertor/input/pt-BR.lproj/app.json index 56656321c..d2653102b 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/app.json +++ b/Localization/StringsConvertor/input/pt-BR.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 330c9bd39ad2c981506203954f293f6ab28d78f4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:50 +0100 Subject: [PATCH 074/224] New translations app.json (Spanish, Argentina) --- Localization/StringsConvertor/input/es-AR.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/es-AR.lproj/app.json b/Localization/StringsConvertor/input/es-AR.lproj/app.json index deedf1933..33f36134b 100644 --- a/Localization/StringsConvertor/input/es-AR.lproj/app.json +++ b/Localization/StringsConvertor/input/es-AR.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 29a057804baea3e225ead79253bba51111845c07 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:51 +0100 Subject: [PATCH 075/224] New translations app.json (Japanese) --- Localization/StringsConvertor/input/ja.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ja.lproj/app.json b/Localization/StringsConvertor/input/ja.lproj/app.json index 1be96cb77..098f49087 100644 --- a/Localization/StringsConvertor/input/ja.lproj/app.json +++ b/Localization/StringsConvertor/input/ja.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 762e4b7fbddbea1a417dda8649d02c88f3bfc2c9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:52 +0100 Subject: [PATCH 076/224] New translations app.json (Thai) --- Localization/StringsConvertor/input/th.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/th.lproj/app.json b/Localization/StringsConvertor/input/th.lproj/app.json index aa39586da..c8382bd80 100644 --- a/Localization/StringsConvertor/input/th.lproj/app.json +++ b/Localization/StringsConvertor/input/th.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "ลิงก์: %s", - "hashtag": "แฮชแท็ก %s", + "hashtag": "Hashtag: %s", "mention": "โปรไฟล์ที่แสดง: %s", "email": "ที่อยู่อีเมล: %s" }, From 3c4404e5162c25c0e01348d5a82483f95e705dc8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:53 +0100 Subject: [PATCH 077/224] New translations app.json (Latvian) --- Localization/StringsConvertor/input/lv.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/lv.lproj/app.json b/Localization/StringsConvertor/input/lv.lproj/app.json index 5a854d5c4..ba50897ed 100644 --- a/Localization/StringsConvertor/input/lv.lproj/app.json +++ b/Localization/StringsConvertor/input/lv.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 76c4e6fec223440c1cc17ff755d4aeccd05303c5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:54 +0100 Subject: [PATCH 078/224] New translations app.json (Hindi) --- Localization/StringsConvertor/input/hi.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/hi.lproj/app.json b/Localization/StringsConvertor/input/hi.lproj/app.json index 514961a44..35cab7b5a 100644 --- a/Localization/StringsConvertor/input/hi.lproj/app.json +++ b/Localization/StringsConvertor/input/hi.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 53b520d0899df37e28f6caa608a21c186184fce4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:56 +0100 Subject: [PATCH 079/224] New translations app.json (English, United States) --- Localization/StringsConvertor/input/en-US.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/en-US.lproj/app.json b/Localization/StringsConvertor/input/en-US.lproj/app.json index 8867385e2..c5a3dac74 100644 --- a/Localization/StringsConvertor/input/en-US.lproj/app.json +++ b/Localization/StringsConvertor/input/en-US.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From e008170559c69fa353aedf0327fd5dfb2b57d908 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:57 +0100 Subject: [PATCH 080/224] New translations app.json (Welsh) --- Localization/StringsConvertor/input/cy.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/cy.lproj/app.json b/Localization/StringsConvertor/input/cy.lproj/app.json index 00f09eca9..de782cd98 100644 --- a/Localization/StringsConvertor/input/cy.lproj/app.json +++ b/Localization/StringsConvertor/input/cy.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From d50547c3c71387524b2bf65f2688dd61be502baa Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:58 +0100 Subject: [PATCH 081/224] New translations app.json (Sinhala) --- Localization/StringsConvertor/input/si.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/si.lproj/app.json b/Localization/StringsConvertor/input/si.lproj/app.json index c70c0f722..2428da902 100644 --- a/Localization/StringsConvertor/input/si.lproj/app.json +++ b/Localization/StringsConvertor/input/si.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From f87ef855954173265281360e276ef05906faef62 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:23:59 +0100 Subject: [PATCH 082/224] New translations app.json (Kurmanji (Kurdish)) --- Localization/StringsConvertor/input/kmr.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/kmr.lproj/app.json b/Localization/StringsConvertor/input/kmr.lproj/app.json index 33c283fc7..2b6c4a491 100644 --- a/Localization/StringsConvertor/input/kmr.lproj/app.json +++ b/Localization/StringsConvertor/input/kmr.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 39dd13be35976a5df97266b442a0483578281c75 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:00 +0100 Subject: [PATCH 083/224] New translations app.json (Dutch) --- Localization/StringsConvertor/input/nl.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/nl.lproj/app.json b/Localization/StringsConvertor/input/nl.lproj/app.json index a94129fef..415327eaa 100644 --- a/Localization/StringsConvertor/input/nl.lproj/app.json +++ b/Localization/StringsConvertor/input/nl.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 6a8decde7824fafc50c8968d7aac245788f9db7b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:01 +0100 Subject: [PATCH 084/224] New translations app.json (Chinese Traditional) --- Localization/StringsConvertor/input/zh-Hant.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json index 6d154d039..9a7023220 100644 --- a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "連結:%s", - "hashtag": "主題標籤 %s", + "hashtag": "Hashtag: %s", "mention": "顯示個人檔案:%s", "email": "電子郵件地址:%s" }, From 220b9a9c2c47c4b409cd85520d50ce1437f3cd20 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:03 +0100 Subject: [PATCH 085/224] New translations app.json (Ukrainian) --- Localization/StringsConvertor/input/uk.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/uk.lproj/app.json b/Localization/StringsConvertor/input/uk.lproj/app.json index 8867385e2..c5a3dac74 100644 --- a/Localization/StringsConvertor/input/uk.lproj/app.json +++ b/Localization/StringsConvertor/input/uk.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From ff4253afa32f03918557fe1b17fc80e5fc48a595 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:04 +0100 Subject: [PATCH 086/224] New translations app.json (Vietnamese) --- Localization/StringsConvertor/input/vi.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/vi.lproj/app.json b/Localization/StringsConvertor/input/vi.lproj/app.json index 0ae436884..4ab6a5799 100644 --- a/Localization/StringsConvertor/input/vi.lproj/app.json +++ b/Localization/StringsConvertor/input/vi.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From d5a30c186728c1e95d9c22ff57cdc641adf8a371 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:05 +0100 Subject: [PATCH 087/224] New translations app.json (Kabyle) --- Localization/StringsConvertor/input/kab.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/kab.lproj/app.json b/Localization/StringsConvertor/input/kab.lproj/app.json index 8fefef724..ac436b6e1 100644 --- a/Localization/StringsConvertor/input/kab.lproj/app.json +++ b/Localization/StringsConvertor/input/kab.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 9f057b233205794de50e74c9a5f3fd0448e954ed Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:06 +0100 Subject: [PATCH 088/224] New translations app.json (Korean) --- Localization/StringsConvertor/input/ko.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ko.lproj/app.json b/Localization/StringsConvertor/input/ko.lproj/app.json index 917487705..da1561cad 100644 --- a/Localization/StringsConvertor/input/ko.lproj/app.json +++ b/Localization/StringsConvertor/input/ko.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 1aa8c9640ffa59c663a571f8bb45e357b3ce4fcf Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:07 +0100 Subject: [PATCH 089/224] New translations app.json (Swedish) --- Localization/StringsConvertor/input/sv.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/sv.lproj/app.json b/Localization/StringsConvertor/input/sv.lproj/app.json index 7451ab2f6..ab7ff5f24 100644 --- a/Localization/StringsConvertor/input/sv.lproj/app.json +++ b/Localization/StringsConvertor/input/sv.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Länk: %s", - "hashtag": "Hashtagg %s", + "hashtag": "Hashtag: %s", "mention": "Visa profil: %s", "email": "E-postadress: %s" }, From 0355f66a67f248d43e1f023951173422bba15208 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:08 +0100 Subject: [PATCH 090/224] New translations app.json (French) --- Localization/StringsConvertor/input/fr.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/fr.lproj/app.json b/Localization/StringsConvertor/input/fr.lproj/app.json index ebc654e11..1e733db5f 100644 --- a/Localization/StringsConvertor/input/fr.lproj/app.json +++ b/Localization/StringsConvertor/input/fr.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Lien : %s", - "hashtag": "Hastag : %s", + "hashtag": "Hashtag: %s", "mention": "Afficher le profile : %s", "email": "Adresse e-mail : %s" }, From fc6a71f22607f40fff1936c9f1a9404c20d0423f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:10 +0100 Subject: [PATCH 091/224] New translations app.json (Turkish) --- Localization/StringsConvertor/input/tr.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/tr.lproj/app.json b/Localization/StringsConvertor/input/tr.lproj/app.json index ffe46c14e..4cae430f9 100644 --- a/Localization/StringsConvertor/input/tr.lproj/app.json +++ b/Localization/StringsConvertor/input/tr.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 3eaa97820fdaa651d1ec40395e9021536b9f9954 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:11 +0100 Subject: [PATCH 092/224] New translations app.json (Czech) --- Localization/StringsConvertor/input/cs.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/cs.lproj/app.json b/Localization/StringsConvertor/input/cs.lproj/app.json index d4076758d..2be115e55 100644 --- a/Localization/StringsConvertor/input/cs.lproj/app.json +++ b/Localization/StringsConvertor/input/cs.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 77808d3a61e18bfc62877f7a30f57e4b0b6c4463 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:12 +0100 Subject: [PATCH 093/224] New translations app.json (Scottish Gaelic) --- Localization/StringsConvertor/input/gd.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/gd.lproj/app.json b/Localization/StringsConvertor/input/gd.lproj/app.json index 5d1611d6b..65a666396 100644 --- a/Localization/StringsConvertor/input/gd.lproj/app.json +++ b/Localization/StringsConvertor/input/gd.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From a643a7bf3b0531d6ffc7310cfe2b643e9504fc26 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:14 +0100 Subject: [PATCH 094/224] New translations app.json (Finnish) --- Localization/StringsConvertor/input/fi.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/fi.lproj/app.json b/Localization/StringsConvertor/input/fi.lproj/app.json index 383c5f0fe..887c44a99 100644 --- a/Localization/StringsConvertor/input/fi.lproj/app.json +++ b/Localization/StringsConvertor/input/fi.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From d4756e9feebe5cb617eaf42f31f0b1bd5a9e9215 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:15 +0100 Subject: [PATCH 095/224] New translations app.json (Romanian) --- Localization/StringsConvertor/input/ro.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ro.lproj/app.json b/Localization/StringsConvertor/input/ro.lproj/app.json index 7173bfac9..d0e2d0de0 100644 --- a/Localization/StringsConvertor/input/ro.lproj/app.json +++ b/Localization/StringsConvertor/input/ro.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 91db8e0a8bfc78380d2e4e1c78c60b986849a2fe Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:17 +0100 Subject: [PATCH 096/224] New translations app.json (Spanish) --- Localization/StringsConvertor/input/es.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/es.lproj/app.json b/Localization/StringsConvertor/input/es.lproj/app.json index 48683a18e..2afb0cd9c 100644 --- a/Localization/StringsConvertor/input/es.lproj/app.json +++ b/Localization/StringsConvertor/input/es.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 5fce47cf30a15bf5b29329d396a7bd33c6ce036d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:18 +0100 Subject: [PATCH 097/224] New translations app.json (Arabic) --- Localization/StringsConvertor/input/ar.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ar.lproj/app.json b/Localization/StringsConvertor/input/ar.lproj/app.json index aed90c72d..ea96af214 100644 --- a/Localization/StringsConvertor/input/ar.lproj/app.json +++ b/Localization/StringsConvertor/input/ar.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From a24ee5ba6ec026b5b28d12053002c10cd93f525c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:19 +0100 Subject: [PATCH 098/224] New translations app.json (Catalan) --- Localization/StringsConvertor/input/ca.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ca.lproj/app.json b/Localization/StringsConvertor/input/ca.lproj/app.json index 45497e67d..338d73599 100644 --- a/Localization/StringsConvertor/input/ca.lproj/app.json +++ b/Localization/StringsConvertor/input/ca.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Enllaç: %s", - "hashtag": "Etiqueta %s", + "hashtag": "Hashtag: %s", "mention": "Mostra el Perfil: %s", "email": "Correu electrònic: %s" }, From 611bb12ac840264648e8e33e7fdd1444fc0f3d4c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:20 +0100 Subject: [PATCH 099/224] New translations app.json (Danish) --- Localization/StringsConvertor/input/da.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/da.lproj/app.json b/Localization/StringsConvertor/input/da.lproj/app.json index 8867385e2..c5a3dac74 100644 --- a/Localization/StringsConvertor/input/da.lproj/app.json +++ b/Localization/StringsConvertor/input/da.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From e08637c07980ed1ae5161d290c8c661892f1d145 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:21 +0100 Subject: [PATCH 100/224] New translations app.json (German) --- Localization/StringsConvertor/input/de.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/de.lproj/app.json b/Localization/StringsConvertor/input/de.lproj/app.json index 3643d89ba..43fa4bc55 100644 --- a/Localization/StringsConvertor/input/de.lproj/app.json +++ b/Localization/StringsConvertor/input/de.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 4e13a5c8b39c33a3884a7794d343ed0237cc2d88 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:22 +0100 Subject: [PATCH 101/224] New translations app.json (Basque) --- Localization/StringsConvertor/input/eu.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/eu.lproj/app.json b/Localization/StringsConvertor/input/eu.lproj/app.json index 118c6a2ed..94720218f 100644 --- a/Localization/StringsConvertor/input/eu.lproj/app.json +++ b/Localization/StringsConvertor/input/eu.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 034bb6b3f3ff3e41cc33adace1ad7fc7b8b491b5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 23:24:23 +0100 Subject: [PATCH 102/224] New translations app.json (Sorani (Kurdish)) --- Localization/StringsConvertor/input/ckb.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ckb.lproj/app.json b/Localization/StringsConvertor/input/ckb.lproj/app.json index 312d4d1d0..e3db76643 100644 --- a/Localization/StringsConvertor/input/ckb.lproj/app.json +++ b/Localization/StringsConvertor/input/ckb.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Link: %s", - "hashtag": "Hastag %s", + "hashtag": "Hashtag: %s", "mention": "Show Profile: %s", "email": "Email address: %s" }, From 89d9700ecdd6c5f7df1023cd8ba964d008a33af7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 10 Nov 2022 00:41:26 +0100 Subject: [PATCH 103/224] New translations app.json (Chinese Traditional) --- Localization/StringsConvertor/input/zh-Hant.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json index 9a7023220..3700f0dd0 100644 --- a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "連結:%s", - "hashtag": "Hashtag: %s", + "hashtag": "主題標籤: %s", "mention": "顯示個人檔案:%s", "email": "電子郵件地址:%s" }, From b9efc57dd372e0c8407c2139d5451d1f8cf4f277 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 10 Nov 2022 00:41:27 +0100 Subject: [PATCH 104/224] New translations app.json (Catalan) --- Localization/StringsConvertor/input/ca.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ca.lproj/app.json b/Localization/StringsConvertor/input/ca.lproj/app.json index 338d73599..45497e67d 100644 --- a/Localization/StringsConvertor/input/ca.lproj/app.json +++ b/Localization/StringsConvertor/input/ca.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Enllaç: %s", - "hashtag": "Hashtag: %s", + "hashtag": "Etiqueta %s", "mention": "Mostra el Perfil: %s", "email": "Correu electrònic: %s" }, From 211fce1d8e10b032c66f845f9b1eb9c0282c8133 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 10 Nov 2022 01:37:20 +0100 Subject: [PATCH 105/224] New translations app.json (Arabic) --- .../StringsConvertor/input/ar.lproj/app.json | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Localization/StringsConvertor/input/ar.lproj/app.json b/Localization/StringsConvertor/input/ar.lproj/app.json index ea96af214..02132355c 100644 --- a/Localization/StringsConvertor/input/ar.lproj/app.json +++ b/Localization/StringsConvertor/input/ar.lproj/app.json @@ -137,10 +137,10 @@ "closed": "انتهى" }, "meta_entity": { - "url": "Link: %s", - "hashtag": "Hashtag: %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "url": "رابِط: %s", + "hashtag": "وَسْم: %s", + "mention": "إظهار المِلف التعريفي: %s", + "email": "عُنوان البريد الإلكتُروني: %s" }, "actions": { "reply": "الرَّد", @@ -187,8 +187,8 @@ "unmute_user": "رفع الكتم عن %s", "muted": "مكتوم", "edit_info": "تَحريرُ المَعلُومات", - "show_reblogs": "Show Reblogs", - "hide_reblogs": "Hide Reblogs" + "show_reblogs": "إظهار إعادات التدوين", + "hide_reblogs": "إخفاء إعادات التدوين" }, "timeline": { "filtered": "مُصفَّى", @@ -465,12 +465,12 @@ "message": "تأكيدُ رَفع الحَظرِ عَن %s" }, "confirm_show_reblogs": { - "title": "Show Reblogs", - "message": "Confirm to show reblogs" + "title": "إظهار إعادات التدوين", + "message": "التأكيد لِإظهار إعادات التدوين" }, "confirm_hide_reblogs": { - "title": "Hide Reblogs", - "message": "Confirm to hide reblogs" + "title": "إخفاء إعادات التدوين", + "message": "التأكيد لِإخفاء إعادات التدوين" } }, "accessibility": { @@ -702,7 +702,7 @@ "accessibility_hint": "انقر نقرًا مزدوجًا لتجاهُل النافذة المنبثقة" }, "bookmark": { - "title": "Bookmarks" + "title": "العَلاماتُ المَرجعيَّة" } } } From 7906ab5e6149fa9ce64ab7b103a0d68694ca26b2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 10 Nov 2022 05:32:04 +0100 Subject: [PATCH 106/224] New translations app.json (Vietnamese) --- Localization/StringsConvertor/input/vi.lproj/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/vi.lproj/app.json b/Localization/StringsConvertor/input/vi.lproj/app.json index 4ab6a5799..1c60b214b 100644 --- a/Localization/StringsConvertor/input/vi.lproj/app.json +++ b/Localization/StringsConvertor/input/vi.lproj/app.json @@ -139,8 +139,8 @@ "meta_entity": { "url": "Link: %s", "hashtag": "Hashtag: %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "mention": "Hiện hồ sơ: %s", + "email": "Email: %s" }, "actions": { "reply": "Trả lời", From 1bf1b773173bdeb3f4bc3f5dbae88e27d65621e6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 10 Nov 2022 08:52:43 +0100 Subject: [PATCH 107/224] New translations app.json (Slovenian) --- Localization/StringsConvertor/input/sl.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/sl.lproj/app.json b/Localization/StringsConvertor/input/sl.lproj/app.json index c4df31a4a..3f2ddf1e1 100644 --- a/Localization/StringsConvertor/input/sl.lproj/app.json +++ b/Localization/StringsConvertor/input/sl.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Povezava: %s", - "hashtag": "Hashtag: %s", + "hashtag": "Ključnik: %s", "mention": "Pokaži profil: %s", "email": "E-naslov: %s" }, From 4a969e5136d6a4f3433fcd9e934ca8b940d2f314 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 10 Nov 2022 08:52:44 +0100 Subject: [PATCH 108/224] New translations app.json (Galician) --- Localization/StringsConvertor/input/gl.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index ee3801a08..a4aacbdc5 100644 --- a/Localization/StringsConvertor/input/gl.lproj/app.json +++ b/Localization/StringsConvertor/input/gl.lproj/app.json @@ -137,10 +137,10 @@ "closed": "Pechada" }, "meta_entity": { - "url": "Link: %s", - "hashtag": "Hashtag: %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "url": "Ligazón: %s", + "hashtag": "Cancelo: %s", + "mention": "Mostrar Perfil: %s", + "email": "Enderezo de email: %s" }, "actions": { "reply": "Responder", From 786e06458d4f6dc2f83ec2f06ee85b73520f4be6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 10 Nov 2022 11:17:45 +0100 Subject: [PATCH 109/224] New translations app.json (Romanian) --- Localization/StringsConvertor/input/ro.lproj/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/ro.lproj/app.json b/Localization/StringsConvertor/input/ro.lproj/app.json index d0e2d0de0..11b25f687 100644 --- a/Localization/StringsConvertor/input/ro.lproj/app.json +++ b/Localization/StringsConvertor/input/ro.lproj/app.json @@ -32,9 +32,9 @@ "message": "Nu se poate edita profilul. Vă rugăm să încercaţi din nou." }, "sign_out": { - "title": "Deconectați-vă", + "title": "Deconectare", "message": "Sigur doriți să vă deconectați?", - "confirm": "Deconectați-vă" + "confirm": "Deconectare" }, "block_domain": { "title": "Are you really, really sure you want to block the entire %s? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain and any of your followers from that domain will be removed.", From d6b90f40bddfd27edee57611491053194deab257 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 10 Nov 2022 18:36:36 +0800 Subject: [PATCH 110/224] feat: add simple progress remain time estimate --- .../Attachment/AttachmentView.swift | 64 ++++++++----- .../Attachment/AttachmentViewModel.swift | 92 ++++++++++++++++--- .../ComposeContentViewModel.swift | 5 + .../Vendor/CircleProgressView.swift | 29 ++++++ 4 files changed, 158 insertions(+), 32 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonUI/Vendor/CircleProgressView.swift diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift index c3ca6fc67..854b35f41 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift @@ -53,32 +53,58 @@ public struct AttachmentView: View { if viewModel.output != nil { VisualEffectView(effect: blurEffect) VStack { - let image: UIImage = { + let actionType: AttachmentView.Action = { if let _ = viewModel.error { - return Asset.Scene.Compose.Attachment.retry.image.withRenderingMode(.alwaysTemplate) + return .retry } else { - return Asset.Scene.Compose.Attachment.stop.image.withRenderingMode(.alwaysTemplate) + return .remove } }() - Image(uiImage: image) - .foregroundColor(.white) - .padding() - .background(Color(Asset.Scene.Compose.Attachment.indicatorButtonBackground.color)) - .clipShape(Circle()) - .padding() + Button { + action(actionType) + } label: { + let image: UIImage = { + switch actionType { + case .remove: + return Asset.Scene.Compose.Attachment.stop.image.withRenderingMode(.alwaysTemplate) + case .retry: + return Asset.Scene.Compose.Attachment.retry.image.withRenderingMode(.alwaysTemplate) + } + }() + Image(uiImage: image) + .foregroundColor(.white) + .padding() + .background(Color(Asset.Scene.Compose.Attachment.indicatorButtonBackground.color)) + .overlay( + CircleProgressView(progress: viewModel.fractionCompleted) + .animation(.default, value: viewModel.fractionCompleted) + ) + .clipShape(Circle()) + .padding() + } + let title: String = { - if let _ = viewModel.error { + switch actionType { + case .remove: + let totalSizeInByte = viewModel.outputSizeInByte + let uploadSizeInByte = Double(totalSizeInByte) * viewModel.progress.fractionCompleted + let total = ByteCountFormatter.string(fromByteCount: Int64(totalSizeInByte), countStyle: .memory) + let upload = ByteCountFormatter.string(fromByteCount: Int64(uploadSizeInByte), countStyle: .memory) + return "\(upload)/\(total)" + case .retry: return "Upload Failed" // TODO: i18n - } else { - let total = ByteCountFormatter.string(fromByteCount: Int64(viewModel.outputSizeInByte), countStyle: .memory) - return "…/\(total)" } }() let subtitle: String = { - if let error = viewModel.error { - return error.localizedDescription - } else { - return "… remaining" + switch actionType { + case .remove: + if viewModel.progress.fractionCompleted < 1 { + return viewModel.remainTimeLocalizedString ?? "" + } else { + return "" + } + case .retry: + return viewModel.error?.localizedDescription ?? "" } }() Text(title) @@ -92,10 +118,6 @@ public struct AttachmentView: View { } } } // end ZStack - .onChange(of: viewModel.progress) { progress in - // not works… - print(progress.completedUnitCount) - } } // end body } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift index 20e8186ad..276e19de7 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift @@ -32,12 +32,19 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable // output @Published public private(set) var output: Output? @Published public private(set) var thumbnail: UIImage? // original size image thumbnail - @Published public private(set) var outputSizeInByte: Int = 0 + @Published public private(set) var outputSizeInByte: Int64 = 0 @Published public var uploadResult: UploadResult? @Published var error: Error? let progress = Progress() // upload progress + @Published var fractionCompleted: Double = 0 + + var displayLink: CADisplayLink! + private var lastTimestamp: TimeInterval? + private var lastUploadSizeInByte: Int64 = 0 + private var averageUploadSpeedInByte: Int64 = 0 + @Published var remainTimeLocalizedString: String? public init( api: APIService, @@ -49,26 +56,34 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable self.input = input super.init() // end init + + self.displayLink = CADisplayLink( + target: self, + selector: #selector(AttachmentViewModel.step(displayLink:)) + ) + displayLink.add(to: .current, forMode: .common) progress .observe(\.fractionCompleted, options: [.initial, .new]) { [weak self] progress, _ in guard let self = self else { return } self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): publish progress \(progress.fractionCompleted)") + self.fractionCompleted = progress.fractionCompleted DispatchQueue.main.async { self.objectWillChange.send() } } .store(in: &observations) - progress - .observe(\.isFinished, options: [.initial, .new]) { [weak self] progress, _ in - guard let self = self else { return } - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): publish progress \(progress.fractionCompleted)") - DispatchQueue.main.async { - self.objectWillChange.send() - } - } - .store(in: &observations) + // Note: this observation is redundant if .fractionCompleted listener always emit event when reach 1.0 progress + // progress + // .observe(\.isFinished, options: [.initial, .new]) { [weak self] progress, _ in + // guard let self = self else { return } + // self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): publish progress \(progress.fractionCompleted)") + // DispatchQueue.main.async { + // self.objectWillChange.send() + // } + // } + // .store(in: &observations) $output .map { output -> UIImage? in @@ -89,7 +104,7 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable do { let output = try await load(input: input) self.output = output - self.outputSizeInByte = output.asAttachment.sizeInByte ?? 0 + self.outputSizeInByte = output.asAttachment.sizeInByte.flatMap { Int64($0) } ?? 0 let uploadResult = try await self.upload(context: .init( apiService: self.api, authContext: self.authContext @@ -103,6 +118,9 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable } deinit { + displayLink.invalidate() + displayLink.remove(from: .current, forMode: .common) + switch output { case .image: // FIXME: @@ -115,6 +133,58 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable } } +// calculate the upload speed +// ref: https://stackoverflow.com/a/3841706/3797903 +extension AttachmentViewModel { + + static var SpeedSmoothingFactor = 0.4 + static let remainsTimeFormatter: RelativeDateTimeFormatter = { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .short + return formatter + }() + + @objc private func step(displayLink: CADisplayLink) { + guard let lastTimestamp = self.lastTimestamp else { + self.lastTimestamp = displayLink.timestamp + self.lastUploadSizeInByte = Int64(Double(outputSizeInByte) * progress.fractionCompleted) + return + } + + let duration = displayLink.timestamp - lastTimestamp + guard duration >= 1.0 else { return } // update every 1 sec + + let old = self.lastUploadSizeInByte + self.lastUploadSizeInByte = Int64(Double(outputSizeInByte) * progress.fractionCompleted) + + let newSpeed = self.lastUploadSizeInByte - old + let lastAverageSpeed = self.averageUploadSpeedInByte + let newAverageSpeed = Int64(AttachmentViewModel.SpeedSmoothingFactor * Double(newSpeed) + (1 - AttachmentViewModel.SpeedSmoothingFactor) * Double(lastAverageSpeed)) + + let remainSizeInByte = Double(outputSizeInByte) * (1 - progress.fractionCompleted) + + let speed = Double(newAverageSpeed) + if speed != .zero { + // estimate by speed + let uploadRemainTimeInSecond = remainSizeInByte / speed + // estimate by progress 1s for 10% + let remainPercentage = 1 - progress.fractionCompleted + let estimateRemainTimeByProgress = remainPercentage / 0.1 + // max estimate + let remainTimeInSecond = max(estimateRemainTimeByProgress, uploadRemainTimeInSecond) + + let string = AttachmentViewModel.remainsTimeFormatter.localizedString(fromTimeInterval: remainTimeInSecond) + remainTimeLocalizedString = string + // print("remains: \(remainSizeInByte), speed: \(newAverageSpeed), \(string)") + } else { + remainTimeLocalizedString = nil + } + + self.lastTimestamp = displayLink.timestamp + self.averageUploadSpeedInByte = newAverageSpeed + } +} + extension AttachmentViewModel { public enum Input: Hashable { case image(UIImage) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 73272b419..c758a397b 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -278,6 +278,11 @@ extension ComposeContentViewModel { // MARK: - UITextViewDelegate extension ComposeContentViewModel: UITextViewDelegate { public func textViewDidBeginEditing(_ textView: UITextView) { + // Note: + // Xcode warning: + // Publishing changes from within view updates is not allowed, this will cause undefined behavior. + // + // Just ignore the warning and see what will happen… switch textView { case contentMetaText?.textView: isContentEditing = true diff --git a/MastodonSDK/Sources/MastodonUI/Vendor/CircleProgressView.swift b/MastodonSDK/Sources/MastodonUI/Vendor/CircleProgressView.swift new file mode 100644 index 000000000..f9b09e740 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Vendor/CircleProgressView.swift @@ -0,0 +1,29 @@ +// +// CircleProgressView.swift +// +// +// Created by MainasuK on 2022/11/10. +// + +import Foundation +import SwiftUI + +/// https://stackoverflow.com/a/71467536/3797903 +struct CircleProgressView: View { + + let progress: Double + + var body: some View { + let lineWidth: CGFloat = 4 + let tintColor = Color.white + ZStack { + Circle() + .trim(from: 0.0, to: CGFloat(progress)) + .stroke(style: StrokeStyle(lineWidth: lineWidth, lineCap: .butt, lineJoin: .bevel)) + .foregroundColor(tintColor) + .rotationEffect(Angle(degrees: 270.0)) + } + .padding(ceil(lineWidth / 2)) + } + +} From 0e8faddbe92703c9abd227bd9dcf4f972cafb83b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 10 Nov 2022 13:09:53 +0100 Subject: [PATCH 111/224] New translations app.json (Swedish) --- .../StringsConvertor/input/sv.lproj/app.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Localization/StringsConvertor/input/sv.lproj/app.json b/Localization/StringsConvertor/input/sv.lproj/app.json index ab7ff5f24..5025358fd 100644 --- a/Localization/StringsConvertor/input/sv.lproj/app.json +++ b/Localization/StringsConvertor/input/sv.lproj/app.json @@ -111,9 +111,9 @@ "next_status": "Nästa inlägg", "open_status": "Öppna inlägg", "open_author_profile": "Öppna författarens profil", - "open_reblogger_profile": "Öppna ompostarens profil", + "open_reblogger_profile": "Öppna boostarens profil", "reply_status": "Svara på inlägg", - "toggle_reblog": "Växla puff på inlägg", + "toggle_reblog": "Växla boost på inlägg", "toggle_favorite": "Växla favorit på inlägg", "toggle_content_warning": "Växla innehållsvarning", "preview_image": "Förhandsgranska bild" @@ -124,7 +124,7 @@ } }, "status": { - "user_reblogged": "%s puffade", + "user_reblogged": "%s boostade", "user_replied_to": "Svarade på %s", "show_post": "Visa inlägg", "show_user_profile": "Visa användarprofil", @@ -144,8 +144,8 @@ }, "actions": { "reply": "Svara", - "reblog": "Puffa", - "unreblog": "Ångra puff", + "reblog": "Boosta", + "unreblog": "Ångra boost", "favorite": "Favorit", "unfavorite": "Ta bort favorit", "menu": "Meny", @@ -187,8 +187,8 @@ "unmute_user": "Avtysta %s", "muted": "Tystad", "edit_info": "Redigera info", - "show_reblogs": "Visa knuffar", - "hide_reblogs": "Dölj puffar" + "show_reblogs": "Visa boostar", + "hide_reblogs": "Dölj boostar" }, "timeline": { "filtered": "Filtrerat", @@ -465,8 +465,8 @@ "message": "Bekräfta för att avblockera %s" }, "confirm_show_reblogs": { - "title": "Visa puffar", - "message": "Bekräfta för att visa puffar" + "title": "Visa boostar", + "message": "Bekräfta för att visa boostar" }, "confirm_hide_reblogs": { "title": "Dölj puffar", @@ -592,7 +592,7 @@ "title": "Notiser", "favorites": "Favoriserar mitt inlägg", "follows": "Följer mig", - "boosts": "Ompostar mitt inlägg", + "boosts": "Boostar mitt inlägg", "mentions": "Nämner mig", "trigger": { "anyone": "alla", From 323fcf1cc9961d1e5efab2c122e271946dc58da8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 10 Nov 2022 14:10:52 +0100 Subject: [PATCH 112/224] New translations app.json (Swedish) --- Localization/StringsConvertor/input/sv.lproj/app.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Localization/StringsConvertor/input/sv.lproj/app.json b/Localization/StringsConvertor/input/sv.lproj/app.json index 5025358fd..0b04e01d7 100644 --- a/Localization/StringsConvertor/input/sv.lproj/app.json +++ b/Localization/StringsConvertor/input/sv.lproj/app.json @@ -469,8 +469,8 @@ "message": "Bekräfta för att visa boostar" }, "confirm_hide_reblogs": { - "title": "Dölj puffar", - "message": "Bekräfta för att dölja puffar" + "title": "Dölj boostar", + "message": "Bekräfta för att dölja boostar" } }, "accessibility": { @@ -496,7 +496,7 @@ "title": "Favoriserad av" }, "reblogged_by": { - "title": "Puffat av" + "title": "Boostat av" }, "search": { "title": "Sök", @@ -552,7 +552,7 @@ "notification_description": { "followed_you": "följde dig", "favorited_your_post": "favoriserade ditt inlägg", - "reblogged_your_post": "puffade ditt inlägg", + "reblogged_your_post": "boostade ditt inlägg", "mentioned_you": "nämnde dig", "request_to_follow_you": "begär att följa dig", "poll_has_ended": "omröstningen har avslutats" @@ -678,7 +678,7 @@ "unfollowed": "Slutade följa", "unfollow_user": "Avfölj %s", "mute_user": "Tysta %s", - "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "Du kommer inte att se deras inlägg eller ompostningar i ditt hemflöde. De kommer inte att veta att de har blivit tystade.", + "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "Du kommer inte att se deras inlägg eller boostar i ditt hemflöde. De kommer inte att veta att de har blivit tystade.", "block_user": "Blockera %s", "they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "De kommer inte längre att kunna följa eller se dina inlägg, men de kan se om de har blockerats.", "while_we_review_this_you_can_take_action_against_user": "Medan vi granskar detta kan du vidta åtgärder mot %s" From 415bfedb223d43786c8417588bb3136f28a7a140 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 10 Nov 2022 14:10:53 +0100 Subject: [PATCH 113/224] New translations Localizable.stringsdict (Swedish) --- .../StringsConvertor/input/sv.lproj/Localizable.stringsdict | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/sv.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/sv.lproj/Localizable.stringsdict index 048af4732..c7317903d 100644 --- a/Localization/StringsConvertor/input/sv.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/sv.lproj/Localizable.stringsdict @@ -152,9 +152,9 @@ NSStringFormatValueTypeKey ld one - %ld puff + %ld boost other - %ld puffar + %ld boostar plural.count.reply From 002d2796e49d5057b12a32cc5403cf72633ffb2e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 10 Nov 2022 17:23:29 +0100 Subject: [PATCH 114/224] New translations app.json (Thai) --- Localization/StringsConvertor/input/th.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/th.lproj/app.json b/Localization/StringsConvertor/input/th.lproj/app.json index c8382bd80..fc88065d9 100644 --- a/Localization/StringsConvertor/input/th.lproj/app.json +++ b/Localization/StringsConvertor/input/th.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "ลิงก์: %s", - "hashtag": "Hashtag: %s", + "hashtag": "แฮชแท็ก: %s", "mention": "โปรไฟล์ที่แสดง: %s", "email": "ที่อยู่อีเมล: %s" }, From cdb8d9e27f4c0da5f5009c295dbe10c351df3b4c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 10 Nov 2022 18:55:18 +0100 Subject: [PATCH 115/224] New translations app.json (French) --- Localization/StringsConvertor/input/fr.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/fr.lproj/app.json b/Localization/StringsConvertor/input/fr.lproj/app.json index 1e733db5f..f719f21a4 100644 --- a/Localization/StringsConvertor/input/fr.lproj/app.json +++ b/Localization/StringsConvertor/input/fr.lproj/app.json @@ -138,7 +138,7 @@ }, "meta_entity": { "url": "Lien : %s", - "hashtag": "Hashtag: %s", + "hashtag": "Hashtag : %s", "mention": "Afficher le profile : %s", "email": "Adresse e-mail : %s" }, From 3b19773ebecfc038f1e48cc2bb90b0ba864cc836 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 11 Nov 2022 08:28:14 +0100 Subject: [PATCH 116/224] New translations app.json (Indonesian) --- .../StringsConvertor/input/id.lproj/app.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Localization/StringsConvertor/input/id.lproj/app.json b/Localization/StringsConvertor/input/id.lproj/app.json index fe678e75c..987a1c0f4 100644 --- a/Localization/StringsConvertor/input/id.lproj/app.json +++ b/Localization/StringsConvertor/input/id.lproj/app.json @@ -6,29 +6,29 @@ "please_try_again_later": "Silakan coba lagi nanti." }, "sign_up_failure": { - "title": "Sign Up Failure" + "title": "Gagal Mendaftar" }, "server_error": { "title": "Kesalahan Server" }, "vote_failure": { - "title": "Vote Failure", + "title": "Gagal Voting", "poll_ended": "Japat telah berakhir" }, "discard_post_content": { "title": "Hapus Draf", - "message": "Confirm to discard composed post content." + "message": "Konfirmasi untuk mengabaikan postingan yang dibuat." }, "publish_post_failure": { - "title": "Publish Failure", - "message": "Failed to publish the post.\nPlease check your internet connection.", + "title": "Gagal Mempublikasikan", + "message": "Gagal mempublikasikan postingan.\nMohon periksa koneksi Internet Anda.", "attachments_message": { "video_attach_with_photo": "Tidak dapat melampirkan video di postingan yang sudah mengandung gambar.", "more_than_one_video": "Tidak dapat melampirkan lebih dari satu video." } }, "edit_profile_failure": { - "title": "Edit Profile Error", + "title": "Masalah dalam mengubah profil", "message": "Tidak dapat menyunting profil. Harap coba lagi." }, "sign_out": { From 5fb26a5eba5ee837cda75ddf63c33757e81792f3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 11 Nov 2022 09:33:38 +0100 Subject: [PATCH 117/224] New translations app.json (Indonesian) --- .../StringsConvertor/input/id.lproj/app.json | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Localization/StringsConvertor/input/id.lproj/app.json b/Localization/StringsConvertor/input/id.lproj/app.json index 987a1c0f4..689cd0995 100644 --- a/Localization/StringsConvertor/input/id.lproj/app.json +++ b/Localization/StringsConvertor/input/id.lproj/app.json @@ -37,16 +37,16 @@ "confirm": "Keluar" }, "block_domain": { - "title": "Are you really, really sure you want to block the entire %s? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain and any of your followers from that domain will be removed.", + "title": "Apakah Anda benar, benar yakin ingin memblokir keseluruhan %s? Dalam kebanyakan kasus, beberapa pemblokiran atau pembisuan yang ditargetkan sudah cukup dan lebih disukai. Anda tidak akan melihat konten dari domain tersebut dan semua pengikut Anda dari domain itu akan dihapus.", "block_entire_domain": "Blokir Domain" }, "save_photo_failure": { - "title": "Save Photo Failure", - "message": "Please enable the photo library access permission to save the photo." + "title": "Gagal Menyimpan Foto", + "message": "Mohon aktifkan izin akses pustaka foto untuk menyimpan foto." }, "delete_post": { "title": "Apakah Anda yakin ingin menghapus postingan ini?", - "message": "Are you sure you want to delete this post?" + "message": "Apakah Anda yakin untuk menghapus kiriman ini?" }, "clean_cache": { "title": "Bersihkan Cache", @@ -67,11 +67,11 @@ "done": "Selesai", "confirm": "Konfirmasi", "continue": "Lanjut", - "compose": "Compose", + "compose": "Tulis", "cancel": "Batal", - "discard": "Discard", + "discard": "Buang", "try_again": "Coba Lagi", - "take_photo": "Take Photo", + "take_photo": "Ambil Foto", "save_photo": "Simpan Foto", "copy_photo": "Salin Foto", "sign_in": "Masuk", @@ -82,9 +82,9 @@ "share_user": "Bagikan %s", "share_post": "Bagikan Postingan", "open_in_safari": "Buka di Safari", - "open_in_browser": "Open in Browser", + "open_in_browser": "Buka di Peramban", "find_people": "Cari orang untuk diikuti", - "manually_search": "Manually search instead", + "manually_search": "Cari secara manual saja", "skip": "Lewati", "reply": "Balas", "report_user": "Laporkan %s", @@ -111,16 +111,16 @@ "next_status": "Postingan Selanjutnya", "open_status": "Buka Postingan", "open_author_profile": "Buka Profil Penulis", - "open_reblogger_profile": "Open Reblogger's Profile", + "open_reblogger_profile": "Buka Profil Reblogger", "reply_status": "Balas Postingan", - "toggle_reblog": "Toggle Reblog on Post", - "toggle_favorite": "Toggle Favorite on Post", - "toggle_content_warning": "Toggle Content Warning", - "preview_image": "Preview Image" + "toggle_reblog": "Nyalakan Reblog pada Postingan", + "toggle_favorite": "Nyalakan Favorit pada Postingan", + "toggle_content_warning": "Nyalakan Peringatan Konten", + "preview_image": "Pratinjau Gambar" }, "segmented_control": { - "previous_section": "Previous Section", - "next_section": "Next Section" + "previous_section": "Bagian Sebelumnya", + "next_section": "Bagian Selanjutnya" } }, "status": { From 088e6f05ec14eb9a09a9c305fd9833b21be88a5e Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 11 Nov 2022 18:10:13 +0800 Subject: [PATCH 118/224] feat: upload media in queue --- .../Attachment/AttachmentView.swift | 31 +++------ .../AttachmentViewModel+Upload.swift | 49 +++++++++++-- .../Attachment/AttachmentViewModel.swift | 68 +++++++++++++++---- .../ComposeContentViewController.swift | 3 +- .../ComposeContentViewModel.swift | 62 +++++++++++++++++ .../View/ComposeContentView.swift | 4 +- 6 files changed, 173 insertions(+), 44 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift index 854b35f41..3fbccbbc7 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift @@ -15,9 +15,7 @@ import MastodonAsset public struct AttachmentView: View { @ObservedObject var viewModel: AttachmentViewModel - - let action: (Action) -> Void - + var blurEffect: UIBlurEffect { UIBlurEffect(style: .systemUltraThinMaterialDark) } @@ -53,7 +51,7 @@ public struct AttachmentView: View { if viewModel.output != nil { VisualEffectView(effect: blurEffect) VStack { - let actionType: AttachmentView.Action = { + let action: AttachmentViewModel.Action = { if let _ = viewModel.error { return .retry } else { @@ -61,10 +59,10 @@ public struct AttachmentView: View { } }() Button { - action(actionType) + viewModel.delegate?.attachmentViewModel(viewModel, actionButtonDidPressed: action) } label: { let image: UIImage = { - switch actionType { + switch action { case .remove: return Asset.Scene.Compose.Attachment.stop.image.withRenderingMode(.alwaysTemplate) case .retry: @@ -84,21 +82,21 @@ public struct AttachmentView: View { } let title: String = { - switch actionType { + switch action { case .remove: let totalSizeInByte = viewModel.outputSizeInByte - let uploadSizeInByte = Double(totalSizeInByte) * viewModel.progress.fractionCompleted - let total = ByteCountFormatter.string(fromByteCount: Int64(totalSizeInByte), countStyle: .memory) - let upload = ByteCountFormatter.string(fromByteCount: Int64(uploadSizeInByte), countStyle: .memory) - return "\(upload)/\(total)" + let uploadSizeInByte = Double(totalSizeInByte) * min(1.0, viewModel.fractionCompleted) + let total = viewModel.byteCountFormatter.string(fromByteCount: Int64(totalSizeInByte)) + let upload = viewModel.byteCountFormatter.string(fromByteCount: Int64(uploadSizeInByte)) + return "\(upload) / \(total)" case .retry: return "Upload Failed" // TODO: i18n } }() let subtitle: String = { - switch actionType { + switch action { case .remove: - if viewModel.progress.fractionCompleted < 1 { + if viewModel.progress.fractionCompleted < 1, viewModel.uploadState == .uploading { return viewModel.remainTimeLocalizedString ?? "" } else { return "" @@ -121,10 +119,3 @@ public struct AttachmentView: View { } // end body } - -extension AttachmentView { - public enum Action: Hashable { - case remove - case retry - } -} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift index fcb30d954..67f0e71ed 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift @@ -53,6 +53,14 @@ extension Data { } extension AttachmentViewModel { + public enum UploadState { + case none + case ready + case uploading + case fail + case finish + } + struct UploadContext { let apiService: APIService let authContext: AuthContext @@ -62,12 +70,43 @@ extension AttachmentViewModel { } extension AttachmentViewModel { - func upload(context: UploadContext) async throws -> UploadResult { - return try await uploadMastodonMedia( - context: context - ) + @MainActor + func upload(isRetry: Bool = false) async throws { + do { + let result = try await upload( + context: .init( + apiService: self.api, + authContext: self.authContext + ), + isRetry: isRetry + ) + update(uploadResult: result) + } catch { + self.error = error + } } + @MainActor + private func upload(context: UploadContext, isRetry: Bool) async throws -> UploadResult { + if isRetry { + guard uploadState == .fail else { throw AppError.badRequest } + self.error = nil + self.fractionCompleted = 0 + } else { + guard uploadState == .ready else { throw AppError.badRequest } + } + do { + update(uploadState: .uploading) + let result = try await uploadMastodonMedia( + context: context + ) + update(uploadState: .finish) + return result + } catch { + update(uploadState: .fail) + throw error + } + } // MainActor is required here to trigger stream upload task @MainActor @@ -132,7 +171,7 @@ extension AttachmentViewModel { if attachmentUploadResponse.statusCode == 202 { // note: // the Mastodon server append the attachments in order by upload time - // can not upload concurrency + // can not upload parallels let waitProcessRetryLimit = checkUploadTaskRetryLimit var waitProcessRetryCount: Int64 = 0 diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift index 276e19de7..9409e7380 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift @@ -12,6 +12,11 @@ import PhotosUI import Kingfisher import MastodonCore +public protocol AttachmentViewModelDelegate: AnyObject { + func attachmentViewModel(_ viewModel: AttachmentViewModel, uploadStateValueDidChange state: AttachmentViewModel.UploadState) + func attachmentViewModel(_ viewModel: AttachmentViewModel, actionButtonDidPressed action: AttachmentViewModel.Action) +} + final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable { static let logger = Logger(subsystem: "AttachmentViewModel", category: "ViewModel") @@ -21,6 +26,15 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable var disposeBag = Set() var observations = Set() + + weak var delegate: AttachmentViewModelDelegate? + + let byteCountFormatter: ByteCountFormatter = { + let formatter = ByteCountFormatter() + formatter.allowsNonnumericFormatting = true + formatter.countStyle = .memory + return formatter + }() // input public let api: APIService @@ -34,7 +48,9 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable @Published public private(set) var thumbnail: UIImage? // original size image thumbnail @Published public private(set) var outputSizeInByte: Int64 = 0 - @Published public var uploadResult: UploadResult? + @MainActor + @Published public private(set) var uploadState: UploadState = .none + @Published public private(set) var uploadResult: UploadResult? @Published var error: Error? let progress = Progress() // upload progress @@ -44,16 +60,19 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable private var lastTimestamp: TimeInterval? private var lastUploadSizeInByte: Int64 = 0 private var averageUploadSpeedInByte: Int64 = 0 + private var remainTimeInterval: Double? @Published var remainTimeLocalizedString: String? public init( api: APIService, authContext: AuthContext, - input: Input + input: Input, + delegate: AttachmentViewModelDelegate ) { self.api = api self.authContext = authContext self.input = input + self.delegate = delegate super.init() // end init @@ -67,9 +86,8 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable .observe(\.fractionCompleted, options: [.initial, .new]) { [weak self] progress, _ in guard let self = self else { return } self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): publish progress \(progress.fractionCompleted)") - self.fractionCompleted = progress.fractionCompleted DispatchQueue.main.async { - self.objectWillChange.send() + self.fractionCompleted = progress.fractionCompleted } } .store(in: &observations) @@ -105,11 +123,8 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable let output = try await load(input: input) self.output = output self.outputSizeInByte = output.asAttachment.sizeInByte.flatMap { Int64($0) } ?? 0 - let uploadResult = try await self.upload(context: .init( - apiService: self.api, - authContext: self.authContext - )) - self.uploadResult = uploadResult + self.update(uploadState: .ready) + self.delegate?.attachmentViewModel(self, uploadStateValueDidChange: self.uploadState) } catch { self.error = error } @@ -127,7 +142,7 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable break case .video(let url, _): try? FileManager.default.removeItem(at: url) - case nil : + case nil: break } } @@ -140,7 +155,7 @@ extension AttachmentViewModel { static var SpeedSmoothingFactor = 0.4 static let remainsTimeFormatter: RelativeDateTimeFormatter = { let formatter = RelativeDateTimeFormatter() - formatter.unitsStyle = .short + formatter.unitsStyle = .full return formatter }() @@ -171,7 +186,15 @@ extension AttachmentViewModel { let remainPercentage = 1 - progress.fractionCompleted let estimateRemainTimeByProgress = remainPercentage / 0.1 // max estimate - let remainTimeInSecond = max(estimateRemainTimeByProgress, uploadRemainTimeInSecond) + var remainTimeInSecond = max(estimateRemainTimeByProgress, uploadRemainTimeInSecond) + + // do not increate timer when < 5 sec + if let remainTimeInterval = self.remainTimeInterval, remainTimeInSecond < 5 { + remainTimeInSecond = min(remainTimeInterval, remainTimeInSecond) + self.remainTimeInterval = remainTimeInSecond + } else { + self.remainTimeInterval = remainTimeInSecond + } let string = AttachmentViewModel.remainsTimeFormatter.localizedString(fromTimeInterval: remainTimeInSecond) remainTimeLocalizedString = string @@ -236,7 +259,22 @@ extension AttachmentViewModel { } +extension AttachmentViewModel { + public enum Action: Hashable { + case remove + case retry + } +} - - - +extension AttachmentViewModel { + @MainActor + func update(uploadState: UploadState) { + self.uploadState = uploadState + self.delegate?.attachmentViewModel(self, uploadStateValueDidChange: self.uploadState) + } + + @MainActor + func update(uploadResult: UploadResult) { + self.uploadResult = uploadResult + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index b9fbab856..7dde9c8c0 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -329,7 +329,8 @@ extension ComposeContentViewController: PHPickerViewControllerDelegate { AttachmentViewModel( api: viewModel.context.apiService, authContext: viewModel.authContext, - input: .pickerResult(result) + input: .pickerResult(result), + delegate: viewModel ) } viewModel.attachmentViewModels += attachmentViewModels diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index c758a397b..03db3b010 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -177,6 +177,17 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { ) .map { $0 + $1 <= $2 } .assign(to: &$isContentValid) + + // bind attachment + $attachmentViewModels + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + Task { + try await self.uploadMediaInQueue() + } + } + .store(in: &disposeBag) } deinit { @@ -397,3 +408,54 @@ extension ComposeContentViewModel: DeleteBackwardResponseTextFieldRelayDelegate } } + +// MARK: - AttachmentViewModelDelegate +extension ComposeContentViewModel: AttachmentViewModelDelegate { + + public func attachmentViewModel( + _ viewModel: AttachmentViewModel, + uploadStateValueDidChange state: AttachmentViewModel.UploadState + ) { + Task { + try await uploadMediaInQueue() + } + } + + @MainActor + func uploadMediaInQueue() async throws { + for (i, attachmentViewModel) in attachmentViewModels.enumerated() { + switch attachmentViewModel.uploadState { + case .none: + return + case .ready: + let count = self.attachmentViewModels.count + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload \(i)/\(count) attachment") + try await attachmentViewModel.upload() + return + case .uploading: + return + case .fail: + return + case .finish: + continue + } + } + } + + public func attachmentViewModel( + _ viewModel: AttachmentViewModel, + actionButtonDidPressed action: AttachmentViewModel.Action + ) { + switch action { + case .retry: + Task { + try await viewModel.upload(isRetry: true) + } + case .remove: + attachmentViewModels.removeAll(where: { $0 === viewModel }) + Task { + try await uploadMediaInQueue() + } + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift index ffc92c01e..e1954af04 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift @@ -205,9 +205,7 @@ extension ComposeContentView { ForEach(viewModel.attachmentViewModels, id: \.self) { attachmentViewModel in Color.clear.aspectRatio(358.0/232.0, contentMode: .fill) .overlay( - AttachmentView(viewModel: attachmentViewModel) { action in - - } + AttachmentView(viewModel: attachmentViewModel) ) .clipShape(Rectangle()) .badgeView( From 0100d8cbabfe7b22a0e8ee607e9ce0bd4ff00f6a Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 11 Nov 2022 19:02:44 +0800 Subject: [PATCH 119/224] feat: compress video before upload --- .../AttachmentViewModel+Compress.swift | 33 +++++++++++++++++++ .../Attachment/AttachmentViewModel.swift | 17 ++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift new file mode 100644 index 000000000..e9c1df676 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift @@ -0,0 +1,33 @@ +// +// AttachmentViewModel+Compress.swift +// +// +// Created by MainasuK on 2022/11/11. +// + +import UIKit +import AVKit + +extension AttachmentViewModel { + func comporessVideo(url: URL) async throws -> URL { + let task = Task { () -> URL in + let urlAsset = AVURLAsset(url: url) + guard let exportSession = AVAssetExportSession(asset: urlAsset, presetName: AVAssetExportPresetMediumQuality) else { + throw AttachmentError.invalidAttachmentType + } + let outputURL = try FileManager.default.createTemporaryFileURL( + filename: UUID().uuidString, + pathExtension: url.pathExtension + ) + exportSession.outputURL = outputURL + exportSession.outputFileType = AVFileType.mp4 + exportSession.shouldOptimizeForNetworkUse = true + await exportSession.export() + return outputURL + } + + self.compressVideoTask = task + + return try await task.value + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift index 9409e7380..7bbaaefaf 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift @@ -43,6 +43,8 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable @Published var caption = "" @Published var sizeLimit = SizeLimit() + var compressVideoTask: Task? + // output @Published public private(set) var output: Output? @Published public private(set) var thumbnail: UIImage? // original size image thumbnail @@ -120,9 +122,20 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable defer { Task { @MainActor in do { - let output = try await load(input: input) - self.output = output + var output = try await load(input: input) + + switch output { + case .video(let fileURL, let mimeType): + let compressedFileURL = try await comporessVideo(url: fileURL) + output = .video(compressedFileURL, mimeType: mimeType) + try? FileManager.default.removeItem(at: fileURL) // remove old file + default: + break + } + self.outputSizeInByte = output.asAttachment.sizeInByte.flatMap { Int64($0) } ?? 0 + self.output = output + self.update(uploadState: .ready) self.delegate?.attachmentViewModel(self, uploadStateValueDidChange: self.uploadState) } catch { From f7d0186bf3bba6e5a2ae8620d4e5088bb819afb8 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 11 Nov 2022 21:28:19 +0800 Subject: [PATCH 120/224] feat: add compress progress display. Set video compress config to 720p at 60 fps --- .../xcshareddata/swiftpm/Package.resolved | 9 ++ MastodonSDK/Package.swift | 2 + .../MastodonSDK/Query/SerialStream.swift | 4 + .../Attachment/AttachmentView.swift | 46 ++++++++-- .../AttachmentViewModel+Compress.swift | 86 +++++++++++++++---- .../AttachmentViewModel+Upload.swift | 1 + .../Attachment/AttachmentViewModel.swift | 60 +++++++++---- .../ComposeContentViewModel.swift | 2 + 8 files changed, 166 insertions(+), 44 deletions(-) diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 64dc691bb..409b8820d 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -90,6 +90,15 @@ "version" : "2.2.5" } }, + { + "identity" : "nextlevelsessionexporter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/NextLevel/NextLevelSessionExporter.git", + "state" : { + "revision" : "b6c0cce1aa37fe1547d694f958fac3c3524b74da", + "version" : "0.4.6" + } + }, { "identity" : "nuke", "kind" : "remoteSourceControl", diff --git a/MastodonSDK/Package.swift b/MastodonSDK/Package.swift index ca241038b..b364e6519 100644 --- a/MastodonSDK/Package.swift +++ b/MastodonSDK/Package.swift @@ -49,6 +49,7 @@ let package = Package( .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.12.0"), .package(url: "https://github.com/eneko/Stripes.git", from: "0.2.0"), .package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.4.1"), + .package(url: "https://github.com/NextLevel/NextLevelSessionExporter.git", from: "0.4.6"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -124,6 +125,7 @@ let package = Package( .product(name: "PanModal", package: "PanModal"), .product(name: "Stripes", package: "Stripes"), .product(name: "Kingfisher", package: "Kingfisher"), + .product(name: "NextLevelSessionExporter", package: "NextLevelSessionExporter"), ] ), .testTarget( diff --git a/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift b/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift index 5d806b6ba..5808b9f6d 100644 --- a/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift +++ b/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift @@ -82,6 +82,10 @@ final class SerialStream: NSObject { self.progress.completedUnitCount += Int64(writeResult) self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): estimate progress: \(self.progress.completedUnitCount)/\(self.progress.totalUnitCount)") + + if writeResult == -1 { + break + } } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift index 3fbccbbc7..2dc8bf12f 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift @@ -48,7 +48,7 @@ public struct AttachmentView: View { // loaded // uploading… or upload failed // could retry upload when error emit - if viewModel.output != nil { + if viewModel.output != nil, viewModel.uploadState != .finish { VisualEffectView(effect: blurEffect) VStack { let action: AttachmentViewModel.Action = { @@ -74,8 +74,18 @@ public struct AttachmentView: View { .padding() .background(Color(Asset.Scene.Compose.Attachment.indicatorButtonBackground.color)) .overlay( - CircleProgressView(progress: viewModel.fractionCompleted) - .animation(.default, value: viewModel.fractionCompleted) + Group { + switch viewModel.uploadState { + case .compressing: + CircleProgressView(progress: viewModel.videoCompressProgress) + .animation(.default, value: viewModel.videoCompressProgress) + case .uploading: + CircleProgressView(progress: viewModel.fractionCompleted) + .animation(.default, value: viewModel.fractionCompleted) + default: + EmptyView() + } + } ) .clipShape(Circle()) .padding() @@ -84,11 +94,20 @@ public struct AttachmentView: View { let title: String = { switch action { case .remove: - let totalSizeInByte = viewModel.outputSizeInByte - let uploadSizeInByte = Double(totalSizeInByte) * min(1.0, viewModel.fractionCompleted) - let total = viewModel.byteCountFormatter.string(fromByteCount: Int64(totalSizeInByte)) - let upload = viewModel.byteCountFormatter.string(fromByteCount: Int64(uploadSizeInByte)) - return "\(upload) / \(total)" + switch viewModel.uploadState { + case .compressing: + return "Comporessing..." // TODO: i18n + default: + if viewModel.fractionCompleted < 0.9 { + let totalSizeInByte = viewModel.outputSizeInByte + let uploadSizeInByte = Double(totalSizeInByte) * min(1.0, viewModel.fractionCompleted + 0.1) // 9:1 + let total = viewModel.byteCountFormatter.string(fromByteCount: Int64(totalSizeInByte)) + let upload = viewModel.byteCountFormatter.string(fromByteCount: Int64(uploadSizeInByte)) + return "\(upload) / \(total)" + } else { + return "Server Processing..." // TODO: i18n + } + } case .retry: return "Upload Failed" // TODO: i18n } @@ -97,7 +116,13 @@ public struct AttachmentView: View { switch action { case .remove: if viewModel.progress.fractionCompleted < 1, viewModel.uploadState == .uploading { - return viewModel.remainTimeLocalizedString ?? "" + if viewModel.progress.fractionCompleted < 0.9 { + return viewModel.remainTimeLocalizedString ?? "" + } else { + return "" + } + } else if viewModel.videoCompressProgress < 1, viewModel.uploadState == .compressing { + return viewModel.percentageFormatter.string(from: NSNumber(floatLiteral: viewModel.videoCompressProgress)) ?? "" } else { return "" } @@ -113,6 +138,9 @@ public struct AttachmentView: View { .font(.system(size: 12, weight: .regular)) .foregroundColor(.white) .padding(.horizontal) + .lineLimit(nil) + .multilineTextAlignment(.center) + .frame(maxWidth: 240) } } } // end ZStack diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift index e9c1df676..0fd0ab085 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift @@ -5,29 +5,81 @@ // Created by MainasuK on 2022/11/11. // +import os.log import UIKit import AVKit +import SessionExporter +import MastodonCore extension AttachmentViewModel { func comporessVideo(url: URL) async throws -> URL { - let task = Task { () -> URL in - let urlAsset = AVURLAsset(url: url) - guard let exportSession = AVAssetExportSession(asset: urlAsset, presetName: AVAssetExportPresetMediumQuality) else { - throw AttachmentError.invalidAttachmentType - } - let outputURL = try FileManager.default.createTemporaryFileURL( - filename: UUID().uuidString, - pathExtension: url.pathExtension - ) - exportSession.outputURL = outputURL - exportSession.outputFileType = AVFileType.mp4 - exportSession.shouldOptimizeForNetworkUse = true - await exportSession.export() - return outputURL + let urlAsset = AVURLAsset(url: url) + let exporter = NextLevelSessionExporter(withAsset: urlAsset) + exporter.outputFileType = .mp4 + + let outputURL = try FileManager.default.createTemporaryFileURL( + filename: UUID().uuidString, + pathExtension: url.pathExtension + ) + exporter.outputURL = outputURL + + let compressionDict: [String: Any] = [ + AVVideoAverageBitRateKey: NSNumber(integerLiteral: 3000000), // 3000k + AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel as String, + AVVideoAverageNonDroppableFrameRateKey: NSNumber(floatLiteral: 30), // 30 FPS + ] + exporter.videoOutputConfiguration = [ + AVVideoCodecKey: AVVideoCodecType.h264, + AVVideoWidthKey: NSNumber(integerLiteral: 1280), + AVVideoHeightKey: NSNumber(integerLiteral: 720), + AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill, + AVVideoCompressionPropertiesKey: compressionDict + ] + exporter.audioOutputConfiguration = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVEncoderBitRateKey: NSNumber(integerLiteral: 128000), // 128k + AVNumberOfChannelsKey: NSNumber(integerLiteral: 2), + AVSampleRateKey: NSNumber(value: Float(44100)) + ] + + // needs set to LOW priority to prevent priority inverse issue + let task = Task(priority: .utility) { + _ = try await exportVideo(by: exporter) } + _ = try await task.value - self.compressVideoTask = task - - return try await task.value + return outputURL } + + private func exportVideo(by exporter: NextLevelSessionExporter) async throws -> URL { + guard let outputURL = exporter.outputURL else { + throw AppError.badRequest + } + return try await withCheckedThrowingContinuation { continuation in + exporter.export(progressHandler: { progress in + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.videoCompressProgress = Double(progress) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: export progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress) + } + }, completionHandler: { result in + switch result { + case .success(let status): + switch status { + case .completed: + print("NextLevelSessionExporter, export completed, \(exporter.outputURL?.description ?? "")") + continuation.resume(with: .success(outputURL)) + default: + if Task.isCancelled { + exporter.cancelExport() + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: cancel export", ((#file as NSString).lastPathComponent), #line, #function) + } + print("NextLevelSessionExporter, did not complete") + } + case .failure(let error): + continuation.resume(with: .failure(error)) + } + }) + } + } // end func } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift index 67f0e71ed..e26e97d35 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift @@ -55,6 +55,7 @@ extension Data { extension AttachmentViewModel { public enum UploadState { case none + case compressing case ready case uploading case fail diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift index 7bbaaefaf..57f1d6b95 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift @@ -11,6 +11,7 @@ import Combine import PhotosUI import Kingfisher import MastodonCore +import func QuartzCore.CACurrentMediaTime public protocol AttachmentViewModelDelegate: AnyObject { func attachmentViewModel(_ viewModel: AttachmentViewModel, uploadStateValueDidChange state: AttachmentViewModel.UploadState) @@ -35,6 +36,12 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable formatter.countStyle = .memory return formatter }() + + let percentageFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .percent + return formatter + }() // input public let api: APIService @@ -43,7 +50,7 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable @Published var caption = "" @Published var sizeLimit = SizeLimit() - var compressVideoTask: Task? + // var compressVideoTask: Task? // output @Published public private(set) var output: Output? @@ -54,11 +61,14 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable @Published public private(set) var uploadState: UploadState = .none @Published public private(set) var uploadResult: UploadResult? @Published var error: Error? + + var uploadTask: Task<(), Never>? + @Published var videoCompressProgress: Double = 0 + let progress = Progress() // upload progress @Published var fractionCompleted: Double = 0 - var displayLink: CADisplayLink! private var lastTimestamp: TimeInterval? private var lastUploadSizeInByte: Int64 = 0 private var averageUploadSpeedInByte: Int64 = 0 @@ -78,11 +88,15 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable super.init() // end init - self.displayLink = CADisplayLink( - target: self, - selector: #selector(AttachmentViewModel.step(displayLink:)) - ) - displayLink.add(to: .current, forMode: .common) + Timer.publish(every: 1.0 / 60.0, on: .main, in: .common) // 60 FPS + .autoconnect() + .share() + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + self.step() + } + .store(in: &disposeBag) progress .observe(\.fractionCompleted, options: [.initial, .new]) { [weak self] progress, _ in @@ -120,12 +134,14 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable .assign(to: &$thumbnail) defer { - Task { @MainActor in + let uploadTask = Task { @MainActor in do { var output = try await load(input: input) switch output { case .video(let fileURL, let mimeType): + self.output = output + self.update(uploadState: .compressing) let compressedFileURL = try await comporessVideo(url: fileURL) output = .video(compressedFileURL, mimeType: mimeType) try? FileManager.default.removeItem(at: fileURL) // remove old file @@ -142,12 +158,17 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable self.error = error } } // end Task + self.uploadTask = uploadTask + Task { + await uploadTask.value + } } } deinit { - displayLink.invalidate() - displayLink.remove(from: .current, forMode: .common) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + + uploadTask?.cancel() switch output { case .image: @@ -172,31 +193,34 @@ extension AttachmentViewModel { return formatter }() - @objc private func step(displayLink: CADisplayLink) { + @objc private func step() { + + let uploadProgress = min(progress.fractionCompleted + 0.1, 1) // the progress split into 9:1 blocks (download : waiting) + guard let lastTimestamp = self.lastTimestamp else { - self.lastTimestamp = displayLink.timestamp - self.lastUploadSizeInByte = Int64(Double(outputSizeInByte) * progress.fractionCompleted) + self.lastTimestamp = CACurrentMediaTime() + self.lastUploadSizeInByte = Int64(Double(outputSizeInByte) * uploadProgress) return } - let duration = displayLink.timestamp - lastTimestamp + let duration = CACurrentMediaTime() - lastTimestamp guard duration >= 1.0 else { return } // update every 1 sec let old = self.lastUploadSizeInByte - self.lastUploadSizeInByte = Int64(Double(outputSizeInByte) * progress.fractionCompleted) + self.lastUploadSizeInByte = Int64(Double(outputSizeInByte) * uploadProgress) let newSpeed = self.lastUploadSizeInByte - old let lastAverageSpeed = self.averageUploadSpeedInByte let newAverageSpeed = Int64(AttachmentViewModel.SpeedSmoothingFactor * Double(newSpeed) + (1 - AttachmentViewModel.SpeedSmoothingFactor) * Double(lastAverageSpeed)) - let remainSizeInByte = Double(outputSizeInByte) * (1 - progress.fractionCompleted) + let remainSizeInByte = Double(outputSizeInByte) * (1 - uploadProgress) let speed = Double(newAverageSpeed) if speed != .zero { // estimate by speed let uploadRemainTimeInSecond = remainSizeInByte / speed // estimate by progress 1s for 10% - let remainPercentage = 1 - progress.fractionCompleted + let remainPercentage = 1 - uploadProgress let estimateRemainTimeByProgress = remainPercentage / 0.1 // max estimate var remainTimeInSecond = max(estimateRemainTimeByProgress, uploadRemainTimeInSecond) @@ -216,7 +240,7 @@ extension AttachmentViewModel { remainTimeLocalizedString = nil } - self.lastTimestamp = displayLink.timestamp + self.lastTimestamp = CACurrentMediaTime() self.averageUploadSpeedInByte = newAverageSpeed } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 03db3b010..9a12f5e84 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -427,6 +427,8 @@ extension ComposeContentViewModel: AttachmentViewModelDelegate { switch attachmentViewModel.uploadState { case .none: return + case .compressing: + return case .ready: let count = self.attachmentViewModels.count logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload \(i)/\(count) attachment") From 9322a0abc8d90fb2699a9ff25a23b27b5f5b45a1 Mon Sep 17 00:00:00 2001 From: woxtu Date: Sat, 12 Nov 2022 00:33:18 +0900 Subject: [PATCH 121/224] Replace a deprecated method --- MastodonSDK/Package.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/MastodonSDK/Package.swift b/MastodonSDK/Package.swift index ca241038b..8db2a1ca5 100644 --- a/MastodonSDK/Package.swift +++ b/MastodonSDK/Package.swift @@ -25,9 +25,9 @@ let package = Package( ], dependencies: [ .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), - .package(name: "FaviconFinder", url: "https://github.com/will-lumley/FaviconFinder.git", from: "3.2.2"), - .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.3"), - .package(name: "UITextView+Placeholder", url: "https://github.com/MainasuK/UITextView-Placeholder.git", from: "1.4.1"), + .package(url: "https://github.com/will-lumley/FaviconFinder.git", from: "3.2.2"), + .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.3"), + .package(url: "https://github.com/MainasuK/UITextView-Placeholder.git", from: "1.4.1"), .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.4.0"), .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.3"), @@ -112,8 +112,8 @@ let package = Package( .product(name: "FLAnimatedImage", package: "FLAnimatedImage"), .product(name: "FaviconFinder", package: "FaviconFinder"), .product(name: "Nuke", package: "Nuke"), - .product(name: "Introspect", package: "Introspect"), - .product(name: "UITextView+Placeholder", package: "UITextView+Placeholder"), + .product(name: "Introspect", package: "SwiftUI-Introspect"), + .product(name: "UITextView+Placeholder", package: "UITextView-Placeholder"), .product(name: "UIHostingConfigurationBackport", package: "UIHostingConfigurationBackport"), .product(name: "TabBarPager", package: "TabBarPager"), .product(name: "ThirdPartyMailer", package: "ThirdPartyMailer"), From 34fc11bacef47eb676b2eadce7d2caafcd077952 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 11 Nov 2022 19:16:32 +0100 Subject: [PATCH 122/224] New translations app.json (Czech) --- .../StringsConvertor/input/cs.lproj/app.json | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Localization/StringsConvertor/input/cs.lproj/app.json b/Localization/StringsConvertor/input/cs.lproj/app.json index 2be115e55..077315611 100644 --- a/Localization/StringsConvertor/input/cs.lproj/app.json +++ b/Localization/StringsConvertor/input/cs.lproj/app.json @@ -304,25 +304,25 @@ "reserved": "%s je rezervované klíčové slovo", "accepted": "%s musí být přijato", "blank": "%s je vyžadováno", - "invalid": "%s is invalid", - "too_long": "%s is too long", - "too_short": "%s is too short", - "inclusion": "%s is not a supported value" + "invalid": "%s je neplatné", + "too_long": "%s je příliš dlouhé", + "too_short": "%s je příliš krátké", + "inclusion": "%s není podporovaná hodnota" }, "special": { - "username_invalid": "Username must only contain alphanumeric characters and underscores", - "username_too_long": "Username is too long (can’t be longer than 30 characters)", + "username_invalid": "Uživatelské jméno musí obsahovat pouze alfanumerické znaky a podtržítka", + "username_too_long": "Uživatelské jméno je příliš dlouhé (nemůže být delší než 30 znaků)", "email_invalid": "Toto není platná e-mailová adresa", - "password_too_short": "Password is too short (must be at least 8 characters)" + "password_too_short": "Heslo je příliš krátké (musí mít alespoň 8 znaků)" } } }, "server_rules": { - "title": "Some ground rules.", - "subtitle": "These are set and enforced by the %s moderators.", - "prompt": "By continuing, you’re subject to the terms of service and privacy policy for %s.", - "terms_of_service": "terms of service", - "privacy_policy": "privacy policy", + "title": "Některá základní pravidla.", + "subtitle": "Ty nastavují a prosazují moderátoři %s.", + "prompt": "Pokračováním budete podléhat podmínkám služby a zásad ochrany osobních údajů pro uživatele %s.", + "terms_of_service": "podmínky služby", + "privacy_policy": "zásady ochrany osobních údajů", "button": { "confirm": "I Agree" } @@ -356,7 +356,7 @@ "Publishing": "Publikování příspěvku...", "accessibility": { "logo_label": "Logo Button", - "logo_hint": "Tap to scroll to top and tap again to previous location" + "logo_hint": "Klepnutím přejdete nahoru a znovu klepněte na předchozí místo" } } }, From c430e9855748c8e0e3a88d9ee183bfa4610f1dae Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 11 Nov 2022 20:14:23 +0100 Subject: [PATCH 123/224] New translations app.json (Czech) --- .../StringsConvertor/input/cs.lproj/app.json | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/Localization/StringsConvertor/input/cs.lproj/app.json b/Localization/StringsConvertor/input/cs.lproj/app.json index 077315611..0f4e9c297 100644 --- a/Localization/StringsConvertor/input/cs.lproj/app.json +++ b/Localization/StringsConvertor/input/cs.lproj/app.json @@ -361,31 +361,31 @@ } }, "suggestion_account": { - "title": "Find People to Follow", - "follow_explain": "When you follow someone, you’ll see their posts in your home feed." + "title": "Najít lidi pro sledování", + "follow_explain": "Když někoho sledujete, uvidíte jejich příspěvky ve vašem domovském kanálu." }, "compose": { "title": { "new_post": "Nový příspěvek", - "new_reply": "New Reply" + "new_reply": "Nová odpověď" }, "media_selection": { - "camera": "Take Photo", - "photo_library": "Photo Library", - "browse": "Browse" + "camera": "Vyfotit", + "photo_library": "Knihovna fotografií", + "browse": "Procházet" }, - "content_input_placeholder": "Type or paste what’s on your mind", - "compose_action": "Publish", - "replying_to_user": "replying to %s", + "content_input_placeholder": "Napište nebo vložte, co je na mysli", + "compose_action": "Zveřejnit", + "replying_to_user": "odpovídá na %s", "attachment": { - "photo": "photo", + "photo": "fotka", "video": "video", - "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", - "description_photo": "Describe the photo for the visually-impaired...", - "description_video": "Describe the video for the visually-impaired..." + "attachment_broken": "Tento %s je poškozený a nemůže být\nnahrán do Mastodonu.", + "description_photo": "Popište fotografii pro zrakově postižené osoby...", + "description_video": "Popište video pro zrakově postižené..." }, "poll": { - "duration_time": "Duration: %s", + "duration_time": "Doba trvání: %s", "thirty_minutes": "30 minut", "one_hour": "1 hodina", "six_hours": "6 hodin", @@ -395,7 +395,7 @@ "option_number": "Možnost %ld" }, "content_warning": { - "placeholder": "Write an accurate warning here..." + "placeholder": "Zde napište přesné varování..." }, "visibility": { "public": "Veřejný", @@ -404,7 +404,7 @@ "direct": "Pouze lidé, které zmíním" }, "auto_complete": { - "space_to_add": "Space to add" + "space_to_add": "Mezera k přidání" }, "accessibility": { "append_attachment": "Přidat přílohu", @@ -419,7 +419,7 @@ "discard_post": "Zahodit příspěvek", "publish_post": "Publikovat příspěvek", "toggle_poll": "Přepnout anketu", - "toggle_content_warning": "Toggle Content Warning", + "toggle_content_warning": "Přepnout varování obsahu", "append_attachment_entry": "Přidat přílohu - %s", "select_visibility_entry": "Vyberte viditelnost - %s" } @@ -430,13 +430,13 @@ }, "dashboard": { "posts": "příspěvky", - "following": "following", + "following": "sledování", "followers": "sledující" }, "fields": { "add_row": "Přidat řádek", "placeholder": { - "label": "Label", + "label": "Označení", "content": "Obsah" } }, @@ -445,7 +445,7 @@ "replies": "Odpovědí", "posts_and_replies": "Příspěvky a odpovědi", "media": "Média", - "about": "About" + "about": "O uživateli" }, "relationship_action_alert": { "confirm_mute_user": { @@ -454,7 +454,7 @@ }, "confirm_unmute_user": { "title": "Zrušit skrytí účtu", - "message": "Confirm to unmute %s" + "message": "Potvrďte zrušení ztlumení %s" }, "confirm_block_user": { "title": "Blokovat účet", @@ -462,7 +462,7 @@ }, "confirm_unblock_user": { "title": "Odblokovat účet", - "message": "Confirm to unblock %s" + "message": "Potvrďte odblokování %s" }, "confirm_show_reblogs": { "title": "Show Reblogs", @@ -474,32 +474,32 @@ } }, "accessibility": { - "show_avatar_image": "Show avatar image", - "edit_avatar_image": "Edit avatar image", - "show_banner_image": "Show banner image", - "double_tap_to_open_the_list": "Double tap to open the list" + "show_avatar_image": "Zobrazit obrázek avataru", + "edit_avatar_image": "Upravit obrázek avataru", + "show_banner_image": "Zobrazit obrázek banneru", + "double_tap_to_open_the_list": "Dvojitým poklepáním otevřete seznam" } }, "follower": { - "title": "follower", - "footer": "Followers from other servers are not displayed." + "title": "sledující", + "footer": "Sledující z jiných serverů nejsou zobrazeni." }, "following": { - "title": "following", - "footer": "Follows from other servers are not displayed." + "title": "sledování", + "footer": "Sledování z jiných serverů není zobrazeno." }, "familiarFollowers": { - "title": "Followers you familiar", - "followed_by_names": "Followed by %s" + "title": "Sledující, které znáte", + "followed_by_names": "Sledován od %s" }, "favorited_by": { - "title": "Favorited By" + "title": "Oblíben" }, "reblogged_by": { "title": "Reblogged By" }, "search": { - "title": "Search", + "title": "Hledat", "search_bar": { "placeholder": "Search hashtags and users", "cancel": "Cancel" From c3009d60099e1435f6f885cfe4c0629e5597efe2 Mon Sep 17 00:00:00 2001 From: David Godfrey Date: Fri, 11 Nov 2022 20:34:26 +0000 Subject: [PATCH 124/224] Add visual indication that a url has been validated in a profile's fields --- .../Diffiable/Profile/ProfileFieldItem.swift | 4 ++ .../Profile/ProfileFieldSection.swift | 11 ++++++ .../Cell/ProfileFieldCollectionViewCell.swift | 15 +++++++- .../Profile/About/ProfileAboutViewModel.swift | 7 ++-- .../Scene/Profile/About/Contents.json | 9 +++++ .../Contents.json | 38 +++++++++++++++++++ .../Contents.json | 38 +++++++++++++++++++ .../MastodonAsset/Generated/Assets.swift | 4 ++ 8 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.background.colorset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.checkmark.colorset/Contents.json diff --git a/Mastodon/Diffiable/Profile/ProfileFieldItem.swift b/Mastodon/Diffiable/Profile/ProfileFieldItem.swift index 47848cc01..e33a2f883 100644 --- a/Mastodon/Diffiable/Profile/ProfileFieldItem.swift +++ b/Mastodon/Diffiable/Profile/ProfileFieldItem.swift @@ -23,6 +23,7 @@ extension ProfileFieldItem { var name: CurrentValueSubject var value: CurrentValueSubject + var verifiedAt: CurrentValueSubject let emojiMeta: MastodonContent.Emojis @@ -30,11 +31,13 @@ extension ProfileFieldItem { id: UUID = UUID(), name: String, value: String, + verifiedAt: Date?, emojiMeta: MastodonContent.Emojis ) { self.id = id self.name = CurrentValueSubject(name) self.value = CurrentValueSubject(value) + self.verifiedAt = CurrentValueSubject(verifiedAt) self.emojiMeta = emojiMeta } @@ -45,6 +48,7 @@ extension ProfileFieldItem { return lhs.id == rhs.id && lhs.name.value == rhs.name.value && lhs.value.value == rhs.value.value + && lhs.verifiedAt.value == rhs.verifiedAt.value && lhs.emojiMeta == rhs.emojiMeta } diff --git a/Mastodon/Diffiable/Profile/ProfileFieldSection.swift b/Mastodon/Diffiable/Profile/ProfileFieldSection.swift index 19771b5db..332916a02 100644 --- a/Mastodon/Diffiable/Profile/ProfileFieldSection.swift +++ b/Mastodon/Diffiable/Profile/ProfileFieldSection.swift @@ -8,6 +8,7 @@ import os import UIKit import Combine +import MastodonAsset import MastodonCore import MastodonMeta import MastodonLocalization @@ -57,7 +58,17 @@ extension ProfileFieldSection { // set background var backgroundConfiguration = UIBackgroundConfiguration.listPlainCell() backgroundConfiguration.backgroundColor = UIColor.secondarySystemBackground + if (field.verifiedAt.value != nil) { + backgroundConfiguration.backgroundColor = Asset.Scene.Profile.About.bioAboutFieldValidatedBackground.color + } cell.backgroundConfiguration = backgroundConfiguration + + // set checkmark + cell.checkmark.isHidden = true + if let verifiedAt = field.verifiedAt.value { + cell.checkmark.isHidden = false + cell.checkmark.accessibilityLabel = "Ownership of this link was checked on \(verifiedAt)" // TODO: I18N / L10N + } cell.delegate = configuration.profileFieldCollectionViewCellDelegate } diff --git a/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift b/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift index ed6f68fec..076b8373e 100644 --- a/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift +++ b/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift @@ -26,6 +26,8 @@ final class ProfileFieldCollectionViewCell: UICollectionViewCell { let keyMetaLabel = MetaLabel(style: .profileFieldName) let valueMetaLabel = MetaLabel(style: .profileFieldValue) + let checkmark = UIImageView(image: Asset.Editing.checkmark.image.withRenderingMode(.alwaysTemplate)) + override func prepareForReuse() { super.prepareForReuse() @@ -47,6 +49,8 @@ final class ProfileFieldCollectionViewCell: UICollectionViewCell { extension ProfileFieldCollectionViewCell { private func _init() { + checkmark.tintColor = Asset.Scene.Profile.About.bioAboutFieldValidatedCheckmark.color; + // containerStackView: V - [ metaContainer | plainContainer ] let containerStackView = UIStackView() containerStackView.axis = .vertical @@ -63,14 +67,21 @@ extension ProfileFieldCollectionViewCell { bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor, constant: 11), ]) - // metaContainer: V - [ keyMetaLabel | valueMetaLabel ] + // metaContainer: V - [ keyMetaLabel | valueContainer ] let metaContainer = UIStackView() metaContainer.axis = .vertical metaContainer.spacing = 2 containerStackView.addArrangedSubview(metaContainer) + // valueContainer: H - [ valueMetaLabel | checkmark ] + let valueContainer = UIStackView() + valueContainer.axis = .horizontal + valueContainer.spacing = 2 + metaContainer.addArrangedSubview(keyMetaLabel) - metaContainer.addArrangedSubview(valueMetaLabel) + valueContainer.addArrangedSubview(valueMetaLabel) + valueContainer.addArrangedSubview(checkmark) + metaContainer.addArrangedSubview(valueContainer) keyMetaLabel.linkDelegate = self valueMetaLabel.linkDelegate = self diff --git a/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift b/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift index 68a3d0fea..044894b8a 100644 --- a/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift +++ b/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift @@ -52,7 +52,7 @@ final class ProfileAboutViewModel { $emojiMeta ) .map { fields, emojiMeta in - fields.map { ProfileFieldItem.FieldValue(name: $0.name, value: $0.value, emojiMeta: emojiMeta) } + fields.map { ProfileFieldItem.FieldValue(name: $0.name, value: $0.value, verifiedAt: $0.verifiedAt, emojiMeta: emojiMeta) } } .assign(to: &profileInfo.$fields) @@ -72,6 +72,7 @@ final class ProfileAboutViewModel { ProfileFieldItem.FieldValue( name: field.name, value: field.value, + verifiedAt: field.verifiedAt, emojiMeta: [:] // no use for editing ) } ?? [] @@ -92,7 +93,7 @@ extension ProfileAboutViewModel { func appendFieldItem() { var fields = profileInfoEditing.fields guard fields.count < ProfileHeaderViewModel.maxProfileFieldCount else { return } - fields.append(ProfileFieldItem.FieldValue(name: "", value: "", emojiMeta: [:])) + fields.append(ProfileFieldItem.FieldValue(name: "", value: "", verifiedAt: nil, emojiMeta: [:])) profileInfoEditing.fields = fields } @@ -112,7 +113,7 @@ extension ProfileAboutViewModel: ProfileViewModelEditable { let isFieldsEqual: Bool = { let originalFields = self.accountForEdit?.source?.fields?.compactMap { field in - ProfileFieldItem.FieldValue(name: field.name, value: field.value, emojiMeta: [:]) + ProfileFieldItem.FieldValue(name: field.name, value: field.value, verifiedAt: nil, emojiMeta: [:]) } ?? [] let editFields = profileInfoEditing.fields guard editFields.count == originalFields.count else { return false } diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/Contents.json new file mode 100644 index 000000000..6e965652d --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.background.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.background.colorset/Contents.json new file mode 100644 index 000000000..86944ced3 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.852", + "green" : "0.894", + "red" : "0.835" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.354", + "green" : "0.353", + "red" : "0.268" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.checkmark.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.checkmark.colorset/Contents.json new file mode 100644 index 000000000..f5112f04f --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.checkmark.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.371", + "green" : "0.565", + "red" : "0.290" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.603", + "green" : "0.742", + "red" : "0.476" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift index 5cd0059d8..45861c583 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift @@ -163,6 +163,10 @@ public enum Asset { public static let textFieldBackground = ColorAsset(name: "Scene/Onboarding/textField.background") } public enum Profile { + public enum About { + public static let bioAboutFieldValidatedBackground = ColorAsset(name: "Scene/Profile/About/bio.about.field.validated.background") + public static let bioAboutFieldValidatedCheckmark = ColorAsset(name: "Scene/Profile/About/bio.about.field.validated.checkmark") + } public enum Banner { public static let bioEditBackgroundGray = ColorAsset(name: "Scene/Profile/Banner/bio.edit.background.gray") public static let nameEditBackgroundGray = ColorAsset(name: "Scene/Profile/Banner/name.edit.background.gray") From 25b1d23037ecd0713c25f8a15ad21a1bac78b483 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 11 Nov 2022 21:37:27 +0100 Subject: [PATCH 125/224] New translations app.json (Czech) --- .../StringsConvertor/input/cs.lproj/app.json | 322 +++++++++--------- 1 file changed, 161 insertions(+), 161 deletions(-) diff --git a/Localization/StringsConvertor/input/cs.lproj/app.json b/Localization/StringsConvertor/input/cs.lproj/app.json index 0f4e9c297..9ec6c0d56 100644 --- a/Localization/StringsConvertor/input/cs.lproj/app.json +++ b/Localization/StringsConvertor/input/cs.lproj/app.json @@ -37,12 +37,12 @@ "confirm": "Odhlásit se" }, "block_domain": { - "title": "Are you really, really sure you want to block the entire %s? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain and any of your followers from that domain will be removed.", + "title": "Opravdu chcete blokovat celou doménu %s? Ve většině případů stačí zablokovat nebo skrýt pár konkrétních uživatelů, což také doporučujeme. Z této domény neuvidíte obsah v žádné veřejné časové ose ani v oznámeních. Vaši sledující z této domény budou odstraněni.", "block_entire_domain": "Blokovat doménu" }, "save_photo_failure": { "title": "Uložení fotografie se nezdařilo", - "message": "Please enable the photo library access permission to save the photo." + "message": "Pro uložení fotografie povolte přístup k knihovně fotografií." }, "delete_post": { "title": "Odstranit příspěvek", @@ -79,7 +79,7 @@ "see_more": "Zobrazit více", "preview": "Náhled", "share": "Sdílet", - "share_user": "Share %s", + "share_user": "Sdílet %s", "share_post": "Sdílet příspěvek", "open_in_safari": "Otevřít v Safari", "open_in_browser": "Otevřít v prohlížeči", @@ -137,23 +137,23 @@ "closed": "Uzavřeno" }, "meta_entity": { - "url": "Link: %s", + "url": "Odkaz: %s", "hashtag": "Hashtag: %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "mention": "Zobrazit profil: %s", + "email": "E-mailová adresa: %s" }, "actions": { "reply": "Odpovědět", "reblog": "Boostnout", "unreblog": "Undo reblog", - "favorite": "Favorite", + "favorite": "Oblíbit", "unfavorite": "Odebrat z oblízených", "menu": "Nabídka", "hide": "Skrýt", "show_image": "Zobrazit obrázek", "show_gif": "Zobrazit GIF", "show_video_player": "Zobrazit video přehrávač", - "tap_then_hold_to_show_menu": "Tap then hold to show menu" + "tap_then_hold_to_show_menu": "Klepnutím podržte pro zobrazení nabídky" }, "tag": { "url": "URL", @@ -165,22 +165,22 @@ }, "visibility": { "unlisted": "Každý může vidět tento příspěvek, ale nezobrazovat ve veřejné časové ose.", - "private": "Only their followers can see this post.", - "private_from_me": "Only my followers can see this post.", - "direct": "Only mentioned user can see this post." + "private": "Pouze jejich sledující mohou vidět tento příspěvek.", + "private_from_me": "Pouze moji sledující mohou vidět tento příspěvek.", + "direct": "Pouze zmíněný uživatel může vidět tento příspěvek." } }, "friendship": { "follow": "Sledovat", - "following": "Following", - "request": "Request", + "following": "Sleduji", + "request": "Požadavek", "pending": "Čekající", "block": "Blokovat", "block_user": "Blokovat %s", "block_domain": "Blokovat %s", "unblock": "Odblokovat", "unblock_user": "Odblokovat %s", - "blocked": "Blocked", + "blocked": "Blokovaný", "mute": "Skrýt", "mute_user": "Skrýt %s", "unmute": "Odkrýt", @@ -191,7 +191,7 @@ "hide_reblogs": "Hide Reblogs" }, "timeline": { - "filtered": "Filtered", + "filtered": "Filtrováno", "timestamp": { "now": "Nyní" }, @@ -204,9 +204,9 @@ "no_status_found": "Nebyl nalezen žádný příspěvek", "blocking_warning": "You can’t view this user's profile\nuntil you unblock them.\nYour profile looks like this to them.", "user_blocking_warning": "You can’t view %s’s profile\nuntil you unblock them.\nYour profile looks like this to them.", - "blocked_warning": "You can’t view this user’s profile\nuntil they unblock you.", + "blocked_warning": "Nemůžeš zobrazit profil tohoto uživatele, dokud tě neodblokují.", "user_blocked_warning": "You can’t view %s’s profile\nuntil they unblock you.", - "suspended_warning": "This user has been suspended.", + "suspended_warning": "Tento uživatel byl pozastaven.", "user_suspended_warning": "%s’s account has been suspended." } } @@ -214,14 +214,14 @@ }, "scene": { "welcome": { - "slogan": "Social networking\nback in your hands.", + "slogan": "Sociální sítě opět ve vašich rukou.", "get_started": "Začínáme", "log_in": "Přihlásit se" }, "server_picker": { "title": "Mastodon tvoří uživatelé z různých serverů.", - "subtitle": "Pick a server based on your interests, region, or a general purpose one.", - "subtitle_extend": "Pick a server based on your interests, region, or a general purpose one. Each server is operated by an entirely independent organization or individual.", + "subtitle": "Vyberte server založený na vašich zájmech, regionu nebo obecném účelu.", + "subtitle_extend": "Vyberte server založený na vašich zájmech, regionu nebo obecném účelu. Každý server je provozován zcela nezávislou organizací nebo jednotlivcem.", "button": { "category": { "all": "Vše", @@ -230,7 +230,7 @@ "activism": "aktivismus", "food": "jídlo", "furry": "furry", - "games": "games", + "games": "hry", "general": "obecné", "journalism": "žurnalistika", "lgbt": "lgbt", @@ -258,8 +258,8 @@ } }, "register": { - "title": "Let’s get you set up on %s", - "lets_get_you_set_up_on_domain": "Let’s get you set up on %s", + "title": "Pojďme si nastavit %s", + "lets_get_you_set_up_on_domain": "Pojďme si nastavit %s", "input": { "avatar": { "delete": "Smazat" @@ -298,8 +298,8 @@ "reason": "Důvod" }, "reason": { - "blocked": "%s contains a disallowed email provider", - "unreachable": "%s does not seem to exist", + "blocked": "%s používá zakázanou e-mailovou službu", + "unreachable": "%s pravděpodobně neexistuje", "taken": "%s se již používá", "reserved": "%s je rezervované klíčové slovo", "accepted": "%s musí být přijato", @@ -324,25 +324,25 @@ "terms_of_service": "podmínky služby", "privacy_policy": "zásady ochrany osobních údajů", "button": { - "confirm": "I Agree" + "confirm": "Souhlasím" } }, "confirm_email": { - "title": "One last thing.", - "subtitle": "Tap the link we emailed to you to verify your account.", - "tap_the_link_we_emailed_to_you_to_verify_your_account": "Tap the link we emailed to you to verify your account", + "title": "Ještě jedna věc.", + "subtitle": "Klepněte na odkaz, který jsme vám poslali e-mailem, abyste ověřili Váš účet.", + "tap_the_link_we_emailed_to_you_to_verify_your_account": "Klepněte na odkaz, který jsme vám poslali e-mailem, abyste ověřili Váš účet", "button": { - "open_email_app": "Open Email App", - "resend": "Resend" + "open_email_app": "Otevřít e-mailovou aplikaci", + "resend": "Poslat znovu" }, "dont_receive_email": { - "title": "Check your email", - "description": "Check if your email address is correct as well as your junk folder if you haven’t.", - "resend_email": "Resend Email" + "title": "Zkontrolujte svůj e-mail", + "description": "Zkontrolujte, zda je vaše e-mailová adresa správná, stejně jako složka nevyžádané pošty, pokud ji máte.", + "resend_email": "Znovu odeslat e-mail" }, "open_email_app": { - "title": "Check your inbox.", - "description": "We just sent you an email. Check your junk folder if you haven’t.", + "title": "Zkontrolujte doručenou poštu.", + "description": "Právě jsme vám poslali e-mail. Zkontrolujte složku nevyžádané zprávy, pokud ji máte.", "mail": "Pošta", "open_email_client": "Otevřít e-mailového klienta" } @@ -355,7 +355,7 @@ "published": "Publikováno!", "Publishing": "Publikování příspěvku...", "accessibility": { - "logo_label": "Logo Button", + "logo_label": "Tlačítko s logem", "logo_hint": "Klepnutím přejdete nahoru a znovu klepněte na předchozí místo" } } @@ -501,208 +501,208 @@ "search": { "title": "Hledat", "search_bar": { - "placeholder": "Search hashtags and users", - "cancel": "Cancel" + "placeholder": "Hledat hashtagy a uživatele", + "cancel": "Zrušit" }, "recommend": { - "button_text": "See All", + "button_text": "Zobrazit vše", "hash_tag": { - "title": "Trending on Mastodon", - "description": "Hashtags that are getting quite a bit of attention", - "people_talking": "%s people are talking" + "title": "Populární na Mastodonu", + "description": "Hashtagy, kterým se dostává dosti pozornosti", + "people_talking": "%s lidí mluví" }, "accounts": { - "title": "Accounts you might like", - "description": "You may like to follow these accounts", - "follow": "Follow" + "title": "Účty, které by se vám mohly líbit", + "description": "Možná budete chtít sledovat tyto účty", + "follow": "Sledovat" } }, "searching": { "segment": { - "all": "All", - "people": "People", - "hashtags": "Hashtags", - "posts": "Posts" + "all": "Vše", + "people": "Lidé", + "hashtags": "Hashtagy", + "posts": "Příspěvky" }, "empty_state": { - "no_results": "No results" + "no_results": "Žádné výsledky" }, - "recent_search": "Recent searches", - "clear": "Clear" + "recent_search": "Nedávná hledání", + "clear": "Vymazat" } }, "discovery": { "tabs": { - "posts": "Posts", - "hashtags": "Hashtags", - "news": "News", - "community": "Community", - "for_you": "For You" + "posts": "Příspěvky", + "hashtags": "Hashtagy", + "news": "Zprávy", + "community": "Komunita", + "for_you": "Pro vás" }, - "intro": "These are the posts gaining traction in your corner of Mastodon." + "intro": "Toto jsou příspěvky, které získávají pozornost ve vašem koutu Mastodonu." }, "favorite": { - "title": "Your Favorites" + "title": "Vaše oblíbené" }, "notification": { "title": { - "Everything": "Everything", - "Mentions": "Mentions" + "Everything": "Všechno", + "Mentions": "Zmínky" }, "notification_description": { - "followed_you": "followed you", - "favorited_your_post": "favorited your post", - "reblogged_your_post": "reblogged your post", - "mentioned_you": "mentioned you", - "request_to_follow_you": "request to follow you", - "poll_has_ended": "poll has ended" + "followed_you": "vás sleduje", + "favorited_your_post": "si oblíbil váš příspěvek", + "reblogged_your_post": "boostnul váš příspěvek", + "mentioned_you": "vás zmínil/a", + "request_to_follow_you": "požádat vás o sledování", + "poll_has_ended": "anketa skončila" }, "keyobard": { - "show_everything": "Show Everything", - "show_mentions": "Show Mentions" + "show_everything": "Zobrazit vše", + "show_mentions": "Zobrazit zmínky" }, "follow_request": { - "accept": "Accept", - "accepted": "Accepted", - "reject": "reject", - "rejected": "Rejected" + "accept": "Přijmout", + "accepted": "Přijato", + "reject": "odmítnout", + "rejected": "Zamítnuto" } }, "thread": { - "back_title": "Post", - "title": "Post from %s" + "back_title": "Příspěvek", + "title": "Příspěvek od %s" }, "settings": { - "title": "Settings", + "title": "Nastavení", "section": { "appearance": { - "title": "Appearance", - "automatic": "Automatic", - "light": "Always Light", - "dark": "Always Dark" + "title": "Vzhled", + "automatic": "Automaticky", + "light": "Vždy světlý", + "dark": "Vždy tmavý" }, "look_and_feel": { - "title": "Look and Feel", - "use_system": "Use System", - "really_dark": "Really Dark", + "title": "Vzhled a chování", + "use_system": "Použít systém", + "really_dark": "Skutečně tmavý", "sorta_dark": "Sorta Dark", - "light": "Light" + "light": "Světlý" }, "notifications": { - "title": "Notifications", - "favorites": "Favorites my post", - "follows": "Follows me", - "boosts": "Reblogs my post", - "mentions": "Mentions me", + "title": "Upozornění", + "favorites": "Oblíbil si můj příspěvek", + "follows": "Sleduje mě", + "boosts": "Boostnul můj příspěvek", + "mentions": "Zmiňuje mě", "trigger": { - "anyone": "anyone", - "follower": "a follower", - "follow": "anyone I follow", - "noone": "no one", - "title": "Notify me when" + "anyone": "kdokoliv", + "follower": "sledující", + "follow": "kdokoli, koho sleduji", + "noone": "nikdo", + "title": "Upozornit, když" } }, "preference": { - "title": "Preferences", - "true_black_dark_mode": "True black dark mode", - "disable_avatar_animation": "Disable animated avatars", - "disable_emoji_animation": "Disable animated emojis", - "using_default_browser": "Use default browser to open links", - "open_links_in_mastodon": "Open links in Mastodon" + "title": "Předvolby", + "true_black_dark_mode": "Skutečný černý tmavý režim", + "disable_avatar_animation": "Zakázat animované avatary", + "disable_emoji_animation": "Zakázat animované emoji", + "using_default_browser": "Použít výchozí prohlížeč pro otevírání odkazů", + "open_links_in_mastodon": "Otevřít odkazy v Mastodonu" }, "boring_zone": { - "title": "The Boring Zone", - "account_settings": "Account Settings", - "terms": "Terms of Service", - "privacy": "Privacy Policy" + "title": "Nudná část", + "account_settings": "Nastavení účtu", + "terms": "Podmínky služby", + "privacy": "Zásady ochrany osobních údajů" }, "spicy_zone": { - "title": "The Spicy Zone", - "clear": "Clear Media Cache", - "signout": "Sign Out" + "title": "Ostrá část", + "clear": "Vymazat mezipaměť médií", + "signout": "Odhlásit se" } }, "footer": { - "mastodon_description": "Mastodon is open source software. You can report issues on GitHub at %s (%s)" + "mastodon_description": "Mastodon je open source software. Na GitHub můžete nahlásit problémy na %s (%s)" }, "keyboard": { - "close_settings_window": "Close Settings Window" + "close_settings_window": "Zavřít okno nastavení" } }, "report": { - "title_report": "Report", - "title": "Report %s", - "step1": "Step 1 of 2", - "step2": "Step 2 of 2", - "content1": "Are there any other posts you’d like to add to the report?", - "content2": "Is there anything the moderators should know about this report?", - "report_sent_title": "Thanks for reporting, we’ll look into this.", - "send": "Send Report", - "skip_to_send": "Send without comment", - "text_placeholder": "Type or paste additional comments", + "title_report": "Nahlásit", + "title": "Nahlásit %s", + "step1": "Krok 1 ze 2", + "step2": "Krok 2 ze 2", + "content1": "Existují nějaké další příspěvky, které byste chtěli přidat do zprávy?", + "content2": "Je o tomto hlášení něco, co by měli vědět moderátoři?", + "report_sent_title": "Děkujeme za nahlášení, podíváme se na to.", + "send": "Odeslat hlášení", + "skip_to_send": "Odeslat bez komentáře", + "text_placeholder": "Napište nebo vložte další komentáře", "reported": "REPORTED", "step_one": { - "step_1_of_4": "Step 1 of 4", - "whats_wrong_with_this_post": "What's wrong with this post?", - "whats_wrong_with_this_account": "What's wrong with this account?", - "whats_wrong_with_this_username": "What's wrong with %s?", - "select_the_best_match": "Select the best match", - "i_dont_like_it": "I don’t like it", - "it_is_not_something_you_want_to_see": "It is not something you want to see", - "its_spam": "It’s spam", - "malicious_links_fake_engagement_or_repetetive_replies": "Malicious links, fake engagement, or repetetive replies", - "it_violates_server_rules": "It violates server rules", - "you_are_aware_that_it_breaks_specific_rules": "You are aware that it breaks specific rules", - "its_something_else": "It’s something else", - "the_issue_does_not_fit_into_other_categories": "The issue does not fit into other categories" + "step_1_of_4": "Krok 1 ze 4", + "whats_wrong_with_this_post": "Co je na tomto příspěvku špatně?", + "whats_wrong_with_this_account": "Co je špatně s tímto účtem?", + "whats_wrong_with_this_username": "Co je špatně na %s?", + "select_the_best_match": "Vyberte nejbližší možnost", + "i_dont_like_it": "Nelíbí se mi", + "it_is_not_something_you_want_to_see": "Není to něco, co chcete vidět", + "its_spam": "Je to spam", + "malicious_links_fake_engagement_or_repetetive_replies": "Škodlivé odkazy, falešné zapojení nebo opakující se odpovědi", + "it_violates_server_rules": "Porušuje pravidla serveru", + "you_are_aware_that_it_breaks_specific_rules": "Máte za to, že porušuje konkrétní pravidla", + "its_something_else": "Jde o něco jiného", + "the_issue_does_not_fit_into_other_categories": "Problém neodpovídá ostatním kategoriím" }, "step_two": { - "step_2_of_4": "Step 2 of 4", - "which_rules_are_being_violated": "Which rules are being violated?", - "select_all_that_apply": "Select all that apply", - "i_just_don’t_like_it": "I just don’t like it" + "step_2_of_4": "Krok 2 ze 4", + "which_rules_are_being_violated": "Jaká pravidla jsou porušována?", + "select_all_that_apply": "Vyberte všechna relevantní", + "i_just_don’t_like_it": "Jen se mi to nelíbí" }, "step_three": { - "step_3_of_4": "Step 3 of 4", - "are_there_any_posts_that_back_up_this_report": "Are there any posts that back up this report?", - "select_all_that_apply": "Select all that apply" + "step_3_of_4": "Krok 3 ze 4", + "are_there_any_posts_that_back_up_this_report": "Existují příspěvky dokládající toto hlášení?", + "select_all_that_apply": "Vyberte všechna relevantní" }, "step_four": { - "step_4_of_4": "Step 4 of 4", - "is_there_anything_else_we_should_know": "Is there anything else we should know?" + "step_4_of_4": "Krok 4 ze 4", + "is_there_anything_else_we_should_know": "Je ještě něco jiného, co bychom měli vědět?" }, "step_final": { - "dont_want_to_see_this": "Don’t want to see this?", - "when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "When you see something you don’t like on Mastodon, you can remove the person from your experience.", - "unfollow": "Unfollow", - "unfollowed": "Unfollowed", - "unfollow_user": "Unfollow %s", - "mute_user": "Mute %s", - "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "You won’t see their posts or reblogs in your home feed. They won’t know they’ve been muted.", - "block_user": "Block %s", + "dont_want_to_see_this": "Nechcete to vidět?", + "when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "Když uvidíte něco, co se vám nelíbí na Mastodonu, můžete odstranit tuto osobu ze svého zážitku.", + "unfollow": "Přestat sledovat", + "unfollowed": "Už nesledujete", + "unfollow_user": "Přestat sledovat %s", + "mute_user": "Skrýt %s", + "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "Neuvidíte jejich příspěvky nebo boostnutí v domovském kanálu. Nebudou vědět, že jsou skrytí.", + "block_user": "Blokovat %s", "they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "They will no longer be able to follow or see your posts, but they can see if they’ve been blocked.", - "while_we_review_this_you_can_take_action_against_user": "While we review this, you can take action against %s" + "while_we_review_this_you_can_take_action_against_user": "Zatímco to posuzujeme, můžete podniknout kroky proti %s" } }, "preview": { "keyboard": { - "close_preview": "Close Preview", - "show_next": "Show Next", - "show_previous": "Show Previous" + "close_preview": "Zavřít náhled", + "show_next": "Zobrazit další", + "show_previous": "Zobrazit předchozí" } }, "account_list": { - "tab_bar_hint": "Current selected profile: %s. Double tap then hold to show account switcher", - "dismiss_account_switcher": "Dismiss Account Switcher", - "add_account": "Add Account" + "tab_bar_hint": "Aktuální vybraný profil: %s. Dvojitým poklepáním zobrazíte přepínač účtů", + "dismiss_account_switcher": "Zrušit přepínač účtů", + "add_account": "Přidat účet" }, "wizard": { - "new_in_mastodon": "New in Mastodon", - "multiple_account_switch_intro_description": "Switch between multiple accounts by holding the profile button.", - "accessibility_hint": "Double tap to dismiss this wizard" + "new_in_mastodon": "Nový v Mastodonu", + "multiple_account_switch_intro_description": "Přepínání mezi více účty podržením tlačítka profilu.", + "accessibility_hint": "Dvojitým poklepáním tohoto průvodce odmítnete" }, "bookmark": { - "title": "Bookmarks" + "title": "Záložky" } } } From b19e272dab7edccacc819896ca1a660bb43ea367 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 11 Nov 2022 21:37:28 +0100 Subject: [PATCH 126/224] New translations ios-infoPlist.json (Czech) --- .../StringsConvertor/input/cs.lproj/ios-infoPlist.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/cs.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/cs.lproj/ios-infoPlist.json index c6db73de0..88bbb346a 100644 --- a/Localization/StringsConvertor/input/cs.lproj/ios-infoPlist.json +++ b/Localization/StringsConvertor/input/cs.lproj/ios-infoPlist.json @@ -1,6 +1,6 @@ { - "NSCameraUsageDescription": "Used to take photo for post status", - "NSPhotoLibraryAddUsageDescription": "Used to save photo into the Photo Library", - "NewPostShortcutItemTitle": "New Post", - "SearchShortcutItemTitle": "Search" + "NSCameraUsageDescription": "Slouží k pořízení fotografie pro příspěvek", + "NSPhotoLibraryAddUsageDescription": "Slouží k uložení fotografie do knihovny fotografií", + "NewPostShortcutItemTitle": "Nový příspěvek", + "SearchShortcutItemTitle": "Hledat" } From a9fad73ae255e2840371156e67baa11e898ccc22 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 11 Nov 2022 21:37:29 +0100 Subject: [PATCH 127/224] New translations Intents.strings (Czech) --- .../Intents/input/cs.lproj/Intents.strings | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.strings index 6877490ba..accbdd58d 100644 --- a/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.strings +++ b/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.strings @@ -1,51 +1,51 @@ -"16wxgf" = "Post on Mastodon"; +"16wxgf" = "Příspěvek na Mastodon"; -"751xkl" = "Text Content"; +"751xkl" = "Textový obsah"; -"CsR7G2" = "Post on Mastodon"; +"CsR7G2" = "Příspěvek na Mastodon"; -"HZSGTr" = "What content to post?"; +"HZSGTr" = "Jaký obsah se má přidat?"; -"HdGikU" = "Posting failed"; +"HdGikU" = "Odeslání se nezdařilo"; -"KDNTJ4" = "Failure Reason"; +"KDNTJ4" = "Důvod selhání"; -"RHxKOw" = "Send Post with text content"; +"RHxKOw" = "Odeslat příspěvek s textovým obsahem"; -"RxSqsb" = "Post"; +"RxSqsb" = "Příspěvek"; -"WCIR3D" = "Post ${content} on Mastodon"; +"WCIR3D" = "Zveřejnit ${content} na Mastodon"; -"ZKJSNu" = "Post"; +"ZKJSNu" = "Příspěvek"; "ZS1XaK" = "${content}"; -"ZbSjzC" = "Visibility"; +"ZbSjzC" = "Viditelnost"; -"Zo4jgJ" = "Post Visibility"; +"Zo4jgJ" = "Viditelnost příspěvku"; -"apSxMG-dYQ5NN" = "There are ${count} options matching ‘Public’."; +"apSxMG-dYQ5NN" = "Existuje ${count} možností odpovídajících 'Veřejný'."; -"apSxMG-ehFLjY" = "There are ${count} options matching ‘Followers Only’."; +"apSxMG-ehFLjY" = "Existuje ${count} možností, které odpovídají „jen sledujícím“."; -"ayoYEb-dYQ5NN" = "${content}, Public"; +"ayoYEb-dYQ5NN" = "${content}, veřejné"; -"ayoYEb-ehFLjY" = "${content}, Followers Only"; +"ayoYEb-ehFLjY" = "${content}, pouze sledující"; -"dUyuGg" = "Post on Mastodon"; +"dUyuGg" = "Příspěvek na Mastodon"; -"dYQ5NN" = "Public"; +"dYQ5NN" = "Veřejný"; -"ehFLjY" = "Followers Only"; +"ehFLjY" = "Pouze sledující"; "gfePDu" = "Posting failed. ${failureReason}"; -"k7dbKQ" = "Post was sent successfully."; +"k7dbKQ" = "Příspěvek byl úspěšně odeslán."; -"oGiqmY-dYQ5NN" = "Just to confirm, you wanted ‘Public’?"; +"oGiqmY-dYQ5NN" = "Jen pro kontrolu, chtěli jste „Veřejný“?"; -"oGiqmY-ehFLjY" = "Just to confirm, you wanted ‘Followers Only’?"; +"oGiqmY-ehFLjY" = "Jen pro kontrolu, chtěli jste „Pouze sledující“?"; "rM6dvp" = "URL"; -"ryJLwG" = "Post was sent successfully. "; +"ryJLwG" = "Příspěvek byl úspěšně odeslán. "; From 35775a5b4364d94998f3cc4890228e16b5beb5f8 Mon Sep 17 00:00:00 2001 From: David Godfrey Date: Sat, 12 Nov 2022 01:53:12 +0000 Subject: [PATCH 128/224] Alert validation time on tapping field checkmark, make validated field links green --- .../Profile/ProfileFieldSection.swift | 9 ++++- .../Cell/ProfileFieldCollectionViewCell.swift | 16 ++++++++ .../About/ProfileAboutViewController.swift | 16 ++++++++ .../Scene/Profile/ProfileViewController.swift | 16 ++++++++ .../Contents.json | 38 +++++++++++++++++++ .../MastodonAsset/Generated/Assets.swift | 1 + 6 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.link.colorset/Contents.json diff --git a/Mastodon/Diffiable/Profile/ProfileFieldSection.swift b/Mastodon/Diffiable/Profile/ProfileFieldSection.swift index 332916a02..98acbb73c 100644 --- a/Mastodon/Diffiable/Profile/ProfileFieldSection.swift +++ b/Mastodon/Diffiable/Profile/ProfileFieldSection.swift @@ -49,6 +49,10 @@ extension ProfileFieldSection { do { let mastodonContent = MastodonContent(content: field.value.value, emojis: field.emojiMeta) let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + cell.valueMetaLabel.linkAttributes[.foregroundColor] = Asset.Colors.brand.color + if field.verifiedAt.value != nil { + cell.valueMetaLabel.linkAttributes[.foregroundColor] = Asset.Scene.Profile.About.bioAboutFieldValidatedLink.color + } cell.valueMetaLabel.configure(content: metaContent) } catch { let content = PlaintextMetaContent(string: field.value.value) @@ -67,7 +71,10 @@ extension ProfileFieldSection { cell.checkmark.isHidden = true if let verifiedAt = field.verifiedAt.value { cell.checkmark.isHidden = false - cell.checkmark.accessibilityLabel = "Ownership of this link was checked on \(verifiedAt)" // TODO: I18N / L10N + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .short + cell.checkmark.accessibilityLabel = "Ownership of this link was checked on \(formatter.string(from: verifiedAt))" // TODO: I18N / L10N } cell.delegate = configuration.profileFieldCollectionViewCellDelegate diff --git a/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift b/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift index 076b8373e..2841ba664 100644 --- a/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift +++ b/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift @@ -14,6 +14,11 @@ import MastodonLocalization protocol ProfileFieldCollectionViewCellDelegate: AnyObject { func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, metaLebel: MetaLabel, didSelectMeta meta: Meta) + func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, didTapAction: ProfileFieldCollectionViewCellAction) +} + +enum ProfileFieldCollectionViewCellAction { + case Checkmark } final class ProfileFieldCollectionViewCell: UICollectionViewCell { @@ -27,6 +32,7 @@ final class ProfileFieldCollectionViewCell: UICollectionViewCell { let valueMetaLabel = MetaLabel(style: .profileFieldValue) let checkmark = UIImageView(image: Asset.Editing.checkmark.image.withRenderingMode(.alwaysTemplate)) + let tapGesture = UITapGestureRecognizer(); override func prepareForReuse() { super.prepareForReuse() @@ -49,8 +55,14 @@ final class ProfileFieldCollectionViewCell: UICollectionViewCell { extension ProfileFieldCollectionViewCell { private func _init() { + // Setup colors checkmark.tintColor = Asset.Scene.Profile.About.bioAboutFieldValidatedCheckmark.color; + // Setup gestures + tapGesture.addTarget(self, action: #selector(ProfileFieldCollectionViewCell.didTapCheckmark(_:))) + checkmark.addGestureRecognizer(tapGesture) + checkmark.isUserInteractionEnabled = true + // containerStackView: V - [ metaContainer | plainContainer ] let containerStackView = UIStackView() containerStackView.axis = .vertical @@ -87,6 +99,10 @@ extension ProfileFieldCollectionViewCell { valueMetaLabel.linkDelegate = self } + @objc public func didTapCheckmark(_: UITapGestureRecognizer) { + delegate?.profileFieldCollectionViewCell(self, didTapAction: .Checkmark) + } + } // MARK: - MetaLabelDelegate diff --git a/Mastodon/Scene/Profile/About/ProfileAboutViewController.swift b/Mastodon/Scene/Profile/About/ProfileAboutViewController.swift index eb1e6b39c..bf77c05b9 100644 --- a/Mastodon/Scene/Profile/About/ProfileAboutViewController.swift +++ b/Mastodon/Scene/Profile/About/ProfileAboutViewController.swift @@ -16,6 +16,7 @@ import MastodonCore protocol ProfileAboutViewControllerDelegate: AnyObject { func profileAboutViewController(_ viewController: ProfileAboutViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta) + func profileAboutViewController(_ viewController: ProfileAboutViewController, didTapCheckmarkFor field: ProfileFieldItem.FieldValue) } final class ProfileAboutViewController: UIViewController { @@ -152,6 +153,21 @@ extension ProfileAboutViewController: ProfileFieldCollectionViewCellDelegate { func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, metaLebel: MetaLabel, didSelectMeta meta: Meta) { delegate?.profileAboutViewController(self, profileFieldCollectionViewCell: cell, metaLabel: metaLebel, didSelectMeta: meta) } + + func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, didTapAction action: ProfileFieldCollectionViewCellAction) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let indexPath = collectionView.indexPath(for: cell) else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + + switch item { + case .field(let field): + delegate?.profileAboutViewController(self, didTapCheckmarkFor: field) + case .addEntry: fallthrough + case .editField: fallthrough + case .noResult: + break + } + } } // MARK: - ProfileFieldEditCollectionViewCellDelegate diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 3ce1fd33a..908725e43 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -854,6 +854,22 @@ extension ProfileViewController: ProfileAboutViewControllerDelegate { ) { handleMetaPress(meta) } + + func profileAboutViewController(_ viewController: ProfileAboutViewController, didTapCheckmarkFor field: ProfileFieldItem.FieldValue) { + guard let verifiedAt = field.verifiedAt.value else { + return + } + + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .short + let alert = UIAlertController(title: "Validated", message: "Ownership of this link was checked on \(formatter.string(from: verifiedAt))", preferredStyle: .alert) // TODO: I18N / L10N + alert.addAction(UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) { _ in + alert.dismiss(animated: true) + }) + + self.present(alert, animated: true) + } } // MARK: - MastodonMenuDelegate diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.link.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.link.colorset/Contents.json new file mode 100644 index 000000000..f5112f04f --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.link.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.371", + "green" : "0.565", + "red" : "0.290" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.603", + "green" : "0.742", + "red" : "0.476" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift index 45861c583..986215f5f 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift @@ -166,6 +166,7 @@ public enum Asset { public enum About { public static let bioAboutFieldValidatedBackground = ColorAsset(name: "Scene/Profile/About/bio.about.field.validated.background") public static let bioAboutFieldValidatedCheckmark = ColorAsset(name: "Scene/Profile/About/bio.about.field.validated.checkmark") + public static let bioAboutFieldValidatedLink = ColorAsset(name: "Scene/Profile/About/bio.about.field.validated.link") } public enum Banner { public static let bioEditBackgroundGray = ColorAsset(name: "Scene/Profile/Banner/bio.edit.background.gray") From b0a0aa268f59ae985f05f88e0fc6f4b7434b01a5 Mon Sep 17 00:00:00 2001 From: David Godfrey Date: Sat, 12 Nov 2022 02:10:16 +0000 Subject: [PATCH 129/224] Rename validated to verified in profile field code --- Mastodon/Diffiable/Profile/ProfileFieldSection.swift | 4 ++-- .../Profile/About/Cell/ProfileFieldCollectionViewCell.swift | 2 +- Mastodon/Scene/Profile/ProfileViewController.swift | 2 +- .../Contents.json | 0 .../Contents.json | 0 .../Contents.json | 0 MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift | 6 +++--- 7 files changed, 7 insertions(+), 7 deletions(-) rename MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/{bio.about.field.validated.background.colorset => bio.about.field.verified.background.colorset}/Contents.json (100%) rename MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/{bio.about.field.validated.checkmark.colorset => bio.about.field.verified.checkmark.colorset}/Contents.json (100%) rename MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/{bio.about.field.validated.link.colorset => bio.about.field.verified.link.colorset}/Contents.json (100%) diff --git a/Mastodon/Diffiable/Profile/ProfileFieldSection.swift b/Mastodon/Diffiable/Profile/ProfileFieldSection.swift index 98acbb73c..87f730fa8 100644 --- a/Mastodon/Diffiable/Profile/ProfileFieldSection.swift +++ b/Mastodon/Diffiable/Profile/ProfileFieldSection.swift @@ -51,7 +51,7 @@ extension ProfileFieldSection { let metaContent = try MastodonMetaContent.convert(document: mastodonContent) cell.valueMetaLabel.linkAttributes[.foregroundColor] = Asset.Colors.brand.color if field.verifiedAt.value != nil { - cell.valueMetaLabel.linkAttributes[.foregroundColor] = Asset.Scene.Profile.About.bioAboutFieldValidatedLink.color + cell.valueMetaLabel.linkAttributes[.foregroundColor] = Asset.Scene.Profile.About.bioAboutFieldVerifiedLink.color } cell.valueMetaLabel.configure(content: metaContent) } catch { @@ -63,7 +63,7 @@ extension ProfileFieldSection { var backgroundConfiguration = UIBackgroundConfiguration.listPlainCell() backgroundConfiguration.backgroundColor = UIColor.secondarySystemBackground if (field.verifiedAt.value != nil) { - backgroundConfiguration.backgroundColor = Asset.Scene.Profile.About.bioAboutFieldValidatedBackground.color + backgroundConfiguration.backgroundColor = Asset.Scene.Profile.About.bioAboutFieldVerifiedBackground.color } cell.backgroundConfiguration = backgroundConfiguration diff --git a/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift b/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift index 2841ba664..2d33b2afe 100644 --- a/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift +++ b/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift @@ -56,7 +56,7 @@ extension ProfileFieldCollectionViewCell { private func _init() { // Setup colors - checkmark.tintColor = Asset.Scene.Profile.About.bioAboutFieldValidatedCheckmark.color; + checkmark.tintColor = Asset.Scene.Profile.About.bioAboutFieldVerifiedCheckmark.color; // Setup gestures tapGesture.addTarget(self, action: #selector(ProfileFieldCollectionViewCell.didTapCheckmark(_:))) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 908725e43..70d11a823 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -863,7 +863,7 @@ extension ProfileViewController: ProfileAboutViewControllerDelegate { let formatter = DateFormatter() formatter.dateStyle = .medium formatter.timeStyle = .short - let alert = UIAlertController(title: "Validated", message: "Ownership of this link was checked on \(formatter.string(from: verifiedAt))", preferredStyle: .alert) // TODO: I18N / L10N + let alert = UIAlertController(title: "Verified", message: "Ownership of this link was checked on \(formatter.string(from: verifiedAt))", preferredStyle: .alert) // TODO: I18N / L10N alert.addAction(UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) { _ in alert.dismiss(animated: true) }) diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.background.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.verified.background.colorset/Contents.json similarity index 100% rename from MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.background.colorset/Contents.json rename to MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.verified.background.colorset/Contents.json diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.checkmark.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.verified.checkmark.colorset/Contents.json similarity index 100% rename from MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.checkmark.colorset/Contents.json rename to MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.verified.checkmark.colorset/Contents.json diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.link.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.verified.link.colorset/Contents.json similarity index 100% rename from MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.validated.link.colorset/Contents.json rename to MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.verified.link.colorset/Contents.json diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift index 986215f5f..dec04f142 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift @@ -164,9 +164,9 @@ public enum Asset { } public enum Profile { public enum About { - public static let bioAboutFieldValidatedBackground = ColorAsset(name: "Scene/Profile/About/bio.about.field.validated.background") - public static let bioAboutFieldValidatedCheckmark = ColorAsset(name: "Scene/Profile/About/bio.about.field.validated.checkmark") - public static let bioAboutFieldValidatedLink = ColorAsset(name: "Scene/Profile/About/bio.about.field.validated.link") + public static let bioAboutFieldVerifiedBackground = ColorAsset(name: "Scene/Profile/About/bio.about.field.verified.background") + public static let bioAboutFieldVerifiedCheckmark = ColorAsset(name: "Scene/Profile/About/bio.about.field.verified.checkmark") + public static let bioAboutFieldVerifiedLink = ColorAsset(name: "Scene/Profile/About/bio.about.field.verified.link") } public enum Banner { public static let bioEditBackgroundGray = ColorAsset(name: "Scene/Profile/Banner/bio.edit.background.gray") From 72873fbfc1d22ba86d3b1eb227164058a2c08e43 Mon Sep 17 00:00:00 2001 From: David Godfrey Date: Sat, 12 Nov 2022 02:40:19 +0000 Subject: [PATCH 130/224] Use localisable strings in verified modal --- Localization/app.json | 4 ++++ Mastodon/Diffiable/Profile/ProfileFieldSection.swift | 2 +- Mastodon/Scene/Profile/ProfileViewController.swift | 2 +- .../Sources/MastodonLocalization/Generated/Strings.swift | 8 ++++++++ .../Resources/en.lproj/Localizable.strings | 4 +++- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index c5a3dac74..6ca2cbea9 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -51,6 +51,10 @@ "clean_cache": { "title": "Clean Cache", "message": "Successfully cleaned %s cache." + }, + "verified": { + "title": "Verified", + "message": "Ownership of this link was checked on %s" } }, "controls": { diff --git a/Mastodon/Diffiable/Profile/ProfileFieldSection.swift b/Mastodon/Diffiable/Profile/ProfileFieldSection.swift index 87f730fa8..01e8d8a0a 100644 --- a/Mastodon/Diffiable/Profile/ProfileFieldSection.swift +++ b/Mastodon/Diffiable/Profile/ProfileFieldSection.swift @@ -74,7 +74,7 @@ extension ProfileFieldSection { let formatter = DateFormatter() formatter.dateStyle = .medium formatter.timeStyle = .short - cell.checkmark.accessibilityLabel = "Ownership of this link was checked on \(formatter.string(from: verifiedAt))" // TODO: I18N / L10N + cell.checkmark.accessibilityLabel = L10n.Common.Alerts.Verified.message(formatter.string(from: verifiedAt)) } cell.delegate = configuration.profileFieldCollectionViewCellDelegate diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 70d11a823..1faf291f3 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -863,7 +863,7 @@ extension ProfileViewController: ProfileAboutViewControllerDelegate { let formatter = DateFormatter() formatter.dateStyle = .medium formatter.timeStyle = .short - let alert = UIAlertController(title: "Verified", message: "Ownership of this link was checked on \(formatter.string(from: verifiedAt))", preferredStyle: .alert) // TODO: I18N / L10N + let alert = UIAlertController(title: L10n.Common.Alerts.Verified.title, message: L10n.Common.Alerts.Verified.message(formatter.string(from: verifiedAt)), preferredStyle: .alert) alert.addAction(UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) { _ in alert.dismiss(animated: true) }) diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 44ae29267..fce4d9733 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -87,6 +87,14 @@ public enum L10n { /// Sign Up Failure public static let title = L10n.tr("Localizable", "Common.Alerts.SignUpFailure.Title") } + public enum Verified { + /// Ownership of this link was checked on %s + public static func message(_ p1: UnsafePointer) -> String { + return L10n.tr("Localizable", "Common.Alerts.Verified.Message", p1) + } + /// Verified + public static let title = L10n.tr("Localizable", "Common.Alerts.Verified.Title") + } public enum VoteFailure { /// The poll has ended public static let pollEnded = L10n.tr("Localizable", "Common.Alerts.VoteFailure.PollEnded") diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings index 9114b96e5..fe9566b14 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings @@ -22,6 +22,8 @@ Please check your internet connection."; "Common.Alerts.SignOut.Message" = "Are you sure you want to sign out?"; "Common.Alerts.SignOut.Title" = "Sign Out"; "Common.Alerts.SignUpFailure.Title" = "Sign Up Failure"; +"Common.Alerts.Verified.Title" = "Verified"; +"Common.Alerts.Verified.Message" = "Ownership of this link was checked on %s"; "Common.Alerts.VoteFailure.PollEnded" = "The poll has ended"; "Common.Alerts.VoteFailure.Title" = "Vote Failure"; "Common.Controls.Actions.Add" = "Add"; @@ -448,4 +450,4 @@ uploaded to Mastodon."; back in your hands."; "Scene.Wizard.AccessibilityHint" = "Double tap to dismiss this wizard"; "Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Switch between multiple accounts by holding the profile button."; -"Scene.Wizard.NewInMastodon" = "New in Mastodon"; \ No newline at end of file +"Scene.Wizard.NewInMastodon" = "New in Mastodon"; From f264140e08d8afd94dbce4a35850c61a5ff47027 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 12 Nov 2022 06:29:11 +0100 Subject: [PATCH 131/224] New translations app.json (Korean) --- Localization/StringsConvertor/input/ko.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/ko.lproj/app.json b/Localization/StringsConvertor/input/ko.lproj/app.json index da1561cad..3d63b4368 100644 --- a/Localization/StringsConvertor/input/ko.lproj/app.json +++ b/Localization/StringsConvertor/input/ko.lproj/app.json @@ -137,10 +137,10 @@ "closed": "마감" }, "meta_entity": { - "url": "Link: %s", - "hashtag": "Hashtag: %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "url": "링크: %s", + "hashtag": "해시태그: %s", + "mention": "프로필 보기: %s", + "email": "이메일 주소: %s" }, "actions": { "reply": "답글", From 0307bcd70b9878b2ef56b7fcfe406654c0f2583b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 12 Nov 2022 08:45:45 +0100 Subject: [PATCH 132/224] New translations app.json (Korean) --- Localization/StringsConvertor/input/ko.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ko.lproj/app.json b/Localization/StringsConvertor/input/ko.lproj/app.json index 3d63b4368..f67c7de5b 100644 --- a/Localization/StringsConvertor/input/ko.lproj/app.json +++ b/Localization/StringsConvertor/input/ko.lproj/app.json @@ -129,7 +129,7 @@ "show_post": "게시물 보기", "show_user_profile": "사용자 프로필 보기", "content_warning": "열람 주의", - "sensitive_content": "Sensitive Content", + "sensitive_content": "민감한 콘텐츠", "media_content_warning": "아무 곳이나 눌러서 보기", "tap_to_reveal": "눌러서 확인", "poll": { From 23902a44d60fc5b1a0c7f26dc80874149ddf9dd8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 12 Nov 2022 09:42:48 +0100 Subject: [PATCH 133/224] New translations app.json (Czech) --- .../StringsConvertor/input/cs.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/cs.lproj/app.json b/Localization/StringsConvertor/input/cs.lproj/app.json index 9ec6c0d56..97d210179 100644 --- a/Localization/StringsConvertor/input/cs.lproj/app.json +++ b/Localization/StringsConvertor/input/cs.lproj/app.json @@ -125,7 +125,7 @@ }, "status": { "user_reblogged": "%s reblogged", - "user_replied_to": "Replied to %s", + "user_replied_to": "Odpověděl %s", "show_post": "Zobrazit příspěvek", "show_user_profile": "Zobrazit profil uživatele", "content_warning": "Varování o obsahu", @@ -202,12 +202,12 @@ }, "header": { "no_status_found": "Nebyl nalezen žádný příspěvek", - "blocking_warning": "You can’t view this user's profile\nuntil you unblock them.\nYour profile looks like this to them.", - "user_blocking_warning": "You can’t view %s’s profile\nuntil you unblock them.\nYour profile looks like this to them.", + "blocking_warning": "Nemůžete zobrazit profil tohoto uživatele, dokud ho neodblokujete.\nVáš profil pro něj vypadá takto.", + "user_blocking_warning": "Nemůžete zobrazit profil %s, dokud ho neodblokujete.\nVáš profil pro něj vypadá takto.", "blocked_warning": "Nemůžeš zobrazit profil tohoto uživatele, dokud tě neodblokují.", - "user_blocked_warning": "You can’t view %s’s profile\nuntil they unblock you.", + "user_blocked_warning": "Nemůžete zobrazit profil %s, dokud vás neodblokuje.", "suspended_warning": "Tento uživatel byl pozastaven.", - "user_suspended_warning": "%s’s account has been suspended." + "user_suspended_warning": "Účet %s byl pozastaven." } } } @@ -640,7 +640,7 @@ "send": "Odeslat hlášení", "skip_to_send": "Odeslat bez komentáře", "text_placeholder": "Napište nebo vložte další komentáře", - "reported": "REPORTED", + "reported": "NAHLÁŠEN", "step_one": { "step_1_of_4": "Krok 1 ze 4", "whats_wrong_with_this_post": "Co je na tomto příspěvku špatně?", @@ -680,7 +680,7 @@ "mute_user": "Skrýt %s", "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "Neuvidíte jejich příspěvky nebo boostnutí v domovském kanálu. Nebudou vědět, že jsou skrytí.", "block_user": "Blokovat %s", - "they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "They will no longer be able to follow or see your posts, but they can see if they’ve been blocked.", + "they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "Už nebudou moci sledovat nebo vidět vaše příspěvky, ale mohou vidět, zda byly zablokovány.", "while_we_review_this_you_can_take_action_against_user": "Zatímco to posuzujeme, můžete podniknout kroky proti %s" } }, From e8fe7852cf9e15480c6fdbb93b28232fcde0c018 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 12 Nov 2022 09:42:49 +0100 Subject: [PATCH 134/224] New translations app.json (Kurmanji (Kurdish)) --- .../StringsConvertor/input/kmr.lproj/app.json | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Localization/StringsConvertor/input/kmr.lproj/app.json b/Localization/StringsConvertor/input/kmr.lproj/app.json index 2b6c4a491..bfe22d89a 100644 --- a/Localization/StringsConvertor/input/kmr.lproj/app.json +++ b/Localization/StringsConvertor/input/kmr.lproj/app.json @@ -137,10 +137,10 @@ "closed": "Girtî" }, "meta_entity": { - "url": "Link: %s", - "hashtag": "Hashtag: %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "url": "Girêdan: %s", + "hashtag": "Hashtagê: %s", + "mention": "Profîlê nîşan bide: %s", + "email": "Navnîşanên e-nameyê: %s" }, "actions": { "reply": "Bersivê bide", @@ -187,8 +187,8 @@ "unmute_user": "%s bêdeng neke", "muted": "Bêdengkirî", "edit_info": "Zanyariyan serrast bike", - "show_reblogs": "Show Reblogs", - "hide_reblogs": "Hide Reblogs" + "show_reblogs": "Bilindkirinan nîşan bide", + "hide_reblogs": "Bilindkirinan veşêre" }, "timeline": { "filtered": "Parzûnkirî", @@ -465,12 +465,12 @@ "message": "Ji bo rakirina astengkirinê %s bipejirîne" }, "confirm_show_reblogs": { - "title": "Show Reblogs", - "message": "Confirm to show reblogs" + "title": "Bilindkirinan nîşan bide", + "message": "Bo nîşandana bilindkirinan bipejirîne" }, "confirm_hide_reblogs": { - "title": "Hide Reblogs", - "message": "Confirm to hide reblogs" + "title": "Bilindkirinan veşêre", + "message": "Bo veşartina bilindkirinan bipejirîne" } }, "accessibility": { @@ -702,7 +702,7 @@ "accessibility_hint": "Du caran bitikîne da ku çarçoveyahilpekok ji holê rakî" }, "bookmark": { - "title": "Bookmarks" + "title": "Şûnpel" } } } From 71e5f6269f5686e6309b7e109698dbc9ab207a92 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 12 Nov 2022 09:42:50 +0100 Subject: [PATCH 135/224] New translations Localizable.stringsdict (Czech) --- .../input/cs.lproj/Localizable.stringsdict | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict index cdf35477e..d275025ad 100644 --- a/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict @@ -108,13 +108,13 @@ NSStringFormatValueTypeKey ld one - post + příspěvek few - posts + příspěvky many - posts + příspěvků other - posts + příspěvků plural.count.media From 7254d0e0b0a1e9ebf4b8555c747c77547db17553 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 12 Nov 2022 09:42:51 +0100 Subject: [PATCH 136/224] New translations Intents.strings (Czech) --- .../StringsConvertor/Intents/input/cs.lproj/Intents.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.strings index accbdd58d..6f29830a1 100644 --- a/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.strings +++ b/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.strings @@ -38,7 +38,7 @@ "ehFLjY" = "Pouze sledující"; -"gfePDu" = "Posting failed. ${failureReason}"; +"gfePDu" = "Odeslání se nezdařilo. ${failureReason}"; "k7dbKQ" = "Příspěvek byl úspěšně odeslán."; From 575e1c2fd831e32e52a3d1093a67570eff63aa22 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 12 Nov 2022 09:42:52 +0100 Subject: [PATCH 137/224] New translations Intents.stringsdict (Czech) --- .../StringsConvertor/Intents/input/cs.lproj/Intents.stringsdict | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.stringsdict index a739f778f..deea8db12 100644 --- a/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.stringsdict +++ b/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.stringsdict @@ -5,7 +5,7 @@ There are ${count} options matching ‘${content}’. - 2 NSStringLocalizedFormatKey - There are %#@count_option@ matching ‘${content}’. + Existuje %#@count_option@ odpovídající „${content}“. count_option NSStringFormatSpecTypeKey From 197e180ccdab95e834a9616e6bcb56541f76e085 Mon Sep 17 00:00:00 2001 From: David Godfrey Date: Sat, 12 Nov 2022 14:42:00 +0000 Subject: [PATCH 138/224] Refactor verified alert to use edit menu --- Localization/app.json | 8 +-- .../Profile/ProfileFieldSection.swift | 7 +- .../Cell/ProfileFieldCollectionViewCell.swift | 64 +++++++++++++++++-- .../About/ProfileAboutViewController.swift | 16 ----- .../Scene/Profile/ProfileViewController.swift | 16 ----- .../Generated/Strings.swift | 18 +++--- .../Resources/en.lproj/Localizable.strings | 4 +- 7 files changed, 78 insertions(+), 55 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 6ca2cbea9..dfb204d8c 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -51,10 +51,6 @@ "clean_cache": { "title": "Clean Cache", "message": "Successfully cleaned %s cache." - }, - "verified": { - "title": "Verified", - "message": "Ownership of this link was checked on %s" } }, "controls": { @@ -442,6 +438,10 @@ "placeholder": { "label": "Label", "content": "Content" + }, + "verified": { + "short": "Verified at %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { diff --git a/Mastodon/Diffiable/Profile/ProfileFieldSection.swift b/Mastodon/Diffiable/Profile/ProfileFieldSection.swift index 01e8d8a0a..6e57e6af9 100644 --- a/Mastodon/Diffiable/Profile/ProfileFieldSection.swift +++ b/Mastodon/Diffiable/Profile/ProfileFieldSection.swift @@ -67,14 +67,17 @@ extension ProfileFieldSection { } cell.backgroundConfiguration = backgroundConfiguration - // set checkmark + // set checkmark and edit menu label cell.checkmark.isHidden = true + cell.checkmarkPopoverString = nil if let verifiedAt = field.verifiedAt.value { cell.checkmark.isHidden = false let formatter = DateFormatter() formatter.dateStyle = .medium formatter.timeStyle = .short - cell.checkmark.accessibilityLabel = L10n.Common.Alerts.Verified.message(formatter.string(from: verifiedAt)) + let dateString = formatter.string(from: verifiedAt) + cell.checkmark.accessibilityLabel = L10n.Scene.Profile.Fields.Verified.long(dateString) + cell.checkmarkPopoverString = L10n.Scene.Profile.Fields.Verified.short(dateString) } cell.delegate = configuration.profileFieldCollectionViewCellDelegate diff --git a/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift b/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift index 2d33b2afe..1ed76a485 100644 --- a/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift +++ b/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift @@ -14,11 +14,6 @@ import MastodonLocalization protocol ProfileFieldCollectionViewCellDelegate: AnyObject { func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, metaLebel: MetaLabel, didSelectMeta meta: Meta) - func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, didTapAction: ProfileFieldCollectionViewCellAction) -} - -enum ProfileFieldCollectionViewCellAction { - case Checkmark } final class ProfileFieldCollectionViewCell: UICollectionViewCell { @@ -32,7 +27,14 @@ final class ProfileFieldCollectionViewCell: UICollectionViewCell { let valueMetaLabel = MetaLabel(style: .profileFieldValue) let checkmark = UIImageView(image: Asset.Editing.checkmark.image.withRenderingMode(.alwaysTemplate)) + var checkmarkPopoverString: String? = nil; let tapGesture = UITapGestureRecognizer(); + private var _editMenuInteraction: Any? = nil + @available(iOS 16, *) + fileprivate var editMenuInteraction: UIEditMenuInteraction { + _editMenuInteraction = _editMenuInteraction ?? UIEditMenuInteraction(delegate: self) + return _editMenuInteraction as! UIEditMenuInteraction + } override func prepareForReuse() { super.prepareForReuse() @@ -62,6 +64,9 @@ extension ProfileFieldCollectionViewCell { tapGesture.addTarget(self, action: #selector(ProfileFieldCollectionViewCell.didTapCheckmark(_:))) checkmark.addGestureRecognizer(tapGesture) checkmark.isUserInteractionEnabled = true + if #available(iOS 16, *) { + checkmark.addInteraction(editMenuInteraction) + } // containerStackView: V - [ metaContainer | plainContainer ] let containerStackView = UIStackView() @@ -99,10 +104,42 @@ extension ProfileFieldCollectionViewCell { valueMetaLabel.linkDelegate = self } - @objc public func didTapCheckmark(_: UITapGestureRecognizer) { - delegate?.profileFieldCollectionViewCell(self, didTapAction: .Checkmark) + @objc public func didTapCheckmark(_ recognizer: UITapGestureRecognizer) { + if #available(iOS 16, *) { + editMenuInteraction.presentEditMenu(with: UIEditMenuConfiguration(identifier: nil, sourcePoint: recognizer.location(in: checkmark))) + } else { + guard let editMenuLabel = checkmarkPopoverString else { return } + + self.isUserInteractionEnabled = true + self.becomeFirstResponder() + + UIMenuController.shared.menuItems = [ + UIMenuItem( + title: editMenuLabel, + action: #selector(dismissVerifiedMenu) + ) + ] + UIMenuController.shared.showMenu(from: checkmark, rect: checkmark.bounds) + } + } +} + +// UIMenuController boilerplate +@available(iOS, deprecated: 16, message: "Can be removed when target version is >=16 -- boilerplate to maintain compatibility with UIMenuController") +extension ProfileFieldCollectionViewCell { + override var canBecomeFirstResponder: Bool { true } + + override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + if action == #selector(dismissVerifiedMenu) { + return true + } + + return super.canPerformAction(action, withSender: sender) } + @objc public func dismissVerifiedMenu() { + UIMenuController.shared.hideMenu() + } } // MARK: - MetaLabelDelegate @@ -112,3 +149,16 @@ extension ProfileFieldCollectionViewCell: MetaLabelDelegate { delegate?.profileFieldCollectionViewCell(self, metaLebel: metaLabel, didSelectMeta: meta) } } + +// MARK: UIEditMenuInteractionDelegate +@available(iOS 16.0, *) +extension ProfileFieldCollectionViewCell: UIEditMenuInteractionDelegate { + func editMenuInteraction(_ interaction: UIEditMenuInteraction, menuFor configuration: UIEditMenuConfiguration, suggestedActions: [UIMenuElement]) -> UIMenu? { + guard let editMenuLabel = checkmarkPopoverString else { return UIMenu(children: []) } + return UIMenu(children: [UIAction(title: editMenuLabel) { _ in return }]) + } + + func editMenuInteraction(_ interaction: UIEditMenuInteraction, targetRectFor configuration: UIEditMenuConfiguration) -> CGRect { + return checkmark.frame + } +} diff --git a/Mastodon/Scene/Profile/About/ProfileAboutViewController.swift b/Mastodon/Scene/Profile/About/ProfileAboutViewController.swift index bf77c05b9..eb1e6b39c 100644 --- a/Mastodon/Scene/Profile/About/ProfileAboutViewController.swift +++ b/Mastodon/Scene/Profile/About/ProfileAboutViewController.swift @@ -16,7 +16,6 @@ import MastodonCore protocol ProfileAboutViewControllerDelegate: AnyObject { func profileAboutViewController(_ viewController: ProfileAboutViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta) - func profileAboutViewController(_ viewController: ProfileAboutViewController, didTapCheckmarkFor field: ProfileFieldItem.FieldValue) } final class ProfileAboutViewController: UIViewController { @@ -153,21 +152,6 @@ extension ProfileAboutViewController: ProfileFieldCollectionViewCellDelegate { func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, metaLebel: MetaLabel, didSelectMeta meta: Meta) { delegate?.profileAboutViewController(self, profileFieldCollectionViewCell: cell, metaLabel: metaLebel, didSelectMeta: meta) } - - func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, didTapAction action: ProfileFieldCollectionViewCellAction) { - guard let diffableDataSource = viewModel.diffableDataSource else { return } - guard let indexPath = collectionView.indexPath(for: cell) else { return } - guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } - - switch item { - case .field(let field): - delegate?.profileAboutViewController(self, didTapCheckmarkFor: field) - case .addEntry: fallthrough - case .editField: fallthrough - case .noResult: - break - } - } } // MARK: - ProfileFieldEditCollectionViewCellDelegate diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 1faf291f3..3ce1fd33a 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -854,22 +854,6 @@ extension ProfileViewController: ProfileAboutViewControllerDelegate { ) { handleMetaPress(meta) } - - func profileAboutViewController(_ viewController: ProfileAboutViewController, didTapCheckmarkFor field: ProfileFieldItem.FieldValue) { - guard let verifiedAt = field.verifiedAt.value else { - return - } - - let formatter = DateFormatter() - formatter.dateStyle = .medium - formatter.timeStyle = .short - let alert = UIAlertController(title: L10n.Common.Alerts.Verified.title, message: L10n.Common.Alerts.Verified.message(formatter.string(from: verifiedAt)), preferredStyle: .alert) - alert.addAction(UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) { _ in - alert.dismiss(animated: true) - }) - - self.present(alert, animated: true) - } } // MARK: - MastodonMenuDelegate diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index fce4d9733..794cd182e 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -87,14 +87,6 @@ public enum L10n { /// Sign Up Failure public static let title = L10n.tr("Localizable", "Common.Alerts.SignUpFailure.Title") } - public enum Verified { - /// Ownership of this link was checked on %s - public static func message(_ p1: UnsafePointer) -> String { - return L10n.tr("Localizable", "Common.Alerts.Verified.Message", p1) - } - /// Verified - public static let title = L10n.tr("Localizable", "Common.Alerts.Verified.Title") - } public enum VoteFailure { /// The poll has ended public static let pollEnded = L10n.tr("Localizable", "Common.Alerts.VoteFailure.PollEnded") @@ -721,6 +713,16 @@ public enum L10n { /// Label public static let label = L10n.tr("Localizable", "Scene.Profile.Fields.Placeholder.Label") } + public enum Verified { + /// Ownership of this link was checked on %s + public static func long(_ p1: UnsafePointer) -> String { + return L10n.tr("Localizable", "Scene.Profile.Fields.Verified.Long", p1) + } + /// Verified at %s + public static func short(_ p1: UnsafePointer) -> String { + return L10n.tr("Localizable", "Scene.Profile.Fields.Verified.Short", p1) + } + } } public enum Header { /// Follows You diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings index fe9566b14..2c9b71107 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings @@ -22,8 +22,6 @@ Please check your internet connection."; "Common.Alerts.SignOut.Message" = "Are you sure you want to sign out?"; "Common.Alerts.SignOut.Title" = "Sign Out"; "Common.Alerts.SignUpFailure.Title" = "Sign Up Failure"; -"Common.Alerts.Verified.Title" = "Verified"; -"Common.Alerts.Verified.Message" = "Ownership of this link was checked on %s"; "Common.Alerts.VoteFailure.PollEnded" = "The poll has ended"; "Common.Alerts.VoteFailure.Title" = "Vote Failure"; "Common.Controls.Actions.Add" = "Add"; @@ -259,6 +257,8 @@ uploaded to Mastodon."; "Scene.Profile.Fields.AddRow" = "Add Row"; "Scene.Profile.Fields.Placeholder.Content" = "Content"; "Scene.Profile.Fields.Placeholder.Label" = "Label"; +"Scene.Profile.Fields.Verified.Short" = "Verified at %s"; +"Scene.Profile.Fields.Verified.Long" = "Ownership of this link was checked on %s"; "Scene.Profile.Header.FollowsYou" = "Follows You"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirm to block %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Block Account"; From 5fbba311e5b1a75da5eb03eaa36e7ec289f4f1eb Mon Sep 17 00:00:00 2001 From: woxtu Date: Sun, 13 Nov 2022 00:46:15 +0900 Subject: [PATCH 139/224] Remove an unused dependency --- Podfile | 1 - Podfile.lock | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Podfile b/Podfile index 28757d528..596aec62b 100644 --- a/Podfile +++ b/Podfile @@ -8,7 +8,6 @@ target 'Mastodon' do # Pods for Mastodon # UI - pod 'UITextField+Shake', '~> 1.2' pod 'XLPagerTabStrip', '~> 9.0.0' # misc diff --git a/Podfile.lock b/Podfile.lock index 12680db21..e8785b512 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -6,7 +6,6 @@ PODS: - Sourcery/CLI-Only (= 1.6.1) - Sourcery/CLI-Only (1.6.1) - SwiftGen (6.4.0) - - "UITextField+Shake (1.2.1)" - XLPagerTabStrip (9.0.0) DEPENDENCIES: @@ -15,7 +14,6 @@ DEPENDENCIES: - Kanna (~> 5.2.2) - Sourcery (~> 1.6.1) - SwiftGen (~> 6.4.0) - - "UITextField+Shake (~> 1.2)" - XLPagerTabStrip (~> 9.0.0) SPEC REPOS: @@ -25,7 +23,6 @@ SPEC REPOS: - Kanna - Sourcery - SwiftGen - - "UITextField+Shake" - XLPagerTabStrip SPEC CHECKSUMS: @@ -34,9 +31,8 @@ SPEC CHECKSUMS: Kanna: 01cfbddc127f5ff0963692f285fcbc8a9d62d234 Sourcery: f3759f803bd0739f74fc92a4341eed0473ce61ac SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 - "UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3 XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5 -PODFILE CHECKSUM: 8b15fb6d4e801b7a7e7761a2e2fe40a89b1da4ff +PODFILE CHECKSUM: a60ecee06525582c010e270ac7a17024e441a0da COCOAPODS: 1.11.3 From a1919a19c97cf84d7819fe552289a5662a820a31 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 12 Nov 2022 20:13:06 +0100 Subject: [PATCH 140/224] New translations app.json (German) --- Localization/StringsConvertor/input/de.lproj/app.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/de.lproj/app.json b/Localization/StringsConvertor/input/de.lproj/app.json index 43fa4bc55..013efb393 100644 --- a/Localization/StringsConvertor/input/de.lproj/app.json +++ b/Localization/StringsConvertor/input/de.lproj/app.json @@ -431,7 +431,7 @@ "dashboard": { "posts": "Beiträge", "following": "Gefolgte", - "followers": "Folger" + "followers": "Folgende" }, "fields": { "add_row": "Zeile hinzufügen", @@ -486,7 +486,7 @@ }, "following": { "title": "Folgende", - "footer": "Wem das Konto folgt wird von anderen Servern werden nicht angezeigt." + "footer": "Gefolgte auf anderen Servern werden nicht angezeigt." }, "familiarFollowers": { "title": "Follower, die dir bekannt vorkommen", @@ -596,7 +596,7 @@ "mentions": "Mich erwähnt", "trigger": { "anyone": "jeder", - "follower": "ein Folger", + "follower": "ein Folgender", "follow": "ein von mir Gefolgter", "noone": "niemand", "title": "Benachrichtige mich, wenn" From a0e544bb90286caf0db187905ef7a4fcd939faef Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 12 Nov 2022 20:13:07 +0100 Subject: [PATCH 141/224] New translations ios-infoPlist.json (German) --- .../StringsConvertor/input/de.lproj/ios-infoPlist.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/de.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/de.lproj/ios-infoPlist.json index fe8fe1c1a..a571fba1c 100644 --- a/Localization/StringsConvertor/input/de.lproj/ios-infoPlist.json +++ b/Localization/StringsConvertor/input/de.lproj/ios-infoPlist.json @@ -1,6 +1,6 @@ { - "NSCameraUsageDescription": "Verwendet um Fotos für neue Beiträge aufzunehmen", - "NSPhotoLibraryAddUsageDescription": "Verwendet um Fotos zu speichern", + "NSCameraUsageDescription": "Wird verwendet, um Fotos für neue Beiträge aufzunehmen", + "NSPhotoLibraryAddUsageDescription": "Wird verwendet, um Foto in der Foto-Mediathek zu speichern", "NewPostShortcutItemTitle": "Neuer Beitrag", - "SearchShortcutItemTitle": "Suche" + "SearchShortcutItemTitle": "Suchen" } From ebb0afd8bc47f2ce3ce55427b212320190b9c139 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 12 Nov 2022 21:13:21 +0100 Subject: [PATCH 142/224] New translations Localizable.stringsdict (Czech) --- .../input/cs.lproj/Localizable.stringsdict | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict index d275025ad..21832870a 100644 --- a/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict @@ -13,19 +13,19 @@ NSStringFormatValueTypeKey ld one - 1 unread notification + 1 nepřečtené oznámení few - %ld unread notification + %ld nepřečtené oznámení many - %ld unread notification + %ld nepřečtených oznámení other - %ld unread notification + %ld nepřečtených oznámení a11y.plural.count.input_limit_exceeds NSStringLocalizedFormatKey - Input limit exceeds %#@character_count@ + Vstupní limit přesahuje %#@character_count@ character_count NSStringFormatSpecTypeKey @@ -33,19 +33,19 @@ NSStringFormatValueTypeKey ld one - 1 character + 1 znak few - %ld characters + %ld znaky many - %ld characters + %ld znaků other - %ld characters + %ld znaků a11y.plural.count.input_limit_remains NSStringLocalizedFormatKey - Input limit remains %#@character_count@ + Vstupní limit zůstává %#@character_count@ character_count NSStringFormatSpecTypeKey @@ -53,13 +53,13 @@ NSStringFormatValueTypeKey ld one - 1 character + 1 znak few - %ld characters + %ld znaky many - %ld characters + %ld znaků other - %ld characters + %ld znaků plural.count.followed_by_and_mutual @@ -128,13 +128,13 @@ NSStringFormatValueTypeKey ld one - 1 media + 1 médium few - %ld media + %ld média many - %ld media + %ld médií other - %ld media + %ld médií plural.count.post @@ -148,13 +148,13 @@ NSStringFormatValueTypeKey ld one - 1 post + 1 příspěvek few - %ld posts + %ld příspěvky many - %ld posts + %ld příspěvků other - %ld posts + %ld příspěvků plural.count.favorite @@ -168,7 +168,7 @@ NSStringFormatValueTypeKey ld one - 1 favorite + 1 oblíbený few %ld favorites many From 5d9e2d217e1294b274b7bc4fb32020246b30337f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 12 Nov 2022 22:22:23 +0100 Subject: [PATCH 143/224] New translations app.json (German) --- Localization/StringsConvertor/input/de.lproj/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/de.lproj/app.json b/Localization/StringsConvertor/input/de.lproj/app.json index 013efb393..aa5ea3b1b 100644 --- a/Localization/StringsConvertor/input/de.lproj/app.json +++ b/Localization/StringsConvertor/input/de.lproj/app.json @@ -482,11 +482,11 @@ }, "follower": { "title": "Follower", - "footer": "Follower von anderen Servern werden nicht angezeigt." + "footer": "Folger, die nicht auf deinem Server registriert sind, werden nicht angezeigt." }, "following": { "title": "Folgende", - "footer": "Gefolgte auf anderen Servern werden nicht angezeigt." + "footer": "Gefolgte, die nicht auf deinem Server registriert sind, werden nicht angezeigt." }, "familiarFollowers": { "title": "Follower, die dir bekannt vorkommen", From e7ef0f79c7020fc7cfbe7f99711a3c18e15d3d61 Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 13 Nov 2022 16:04:29 +0800 Subject: [PATCH 144/224] feat: restore auto-complete for compose scene content input --- Mastodon.xcodeproj/project.pbxproj | 4 - .../Scene/Compose/ComposeViewController.swift | 173 +-------------- Mastodon/Scene/Compose/ComposeViewModel.swift | 2 - .../AutoCompleteViewController.swift | 2 +- .../AutoCompleteViewModel+Diffable.swift | 19 +- .../ComposeContentViewController.swift | 60 ++++++ ...eContentViewModel+UITextViewDelegate.swift | 203 ++++++++++++++++++ .../ComposeContentViewModel.swift | 96 ++------- .../Helper/MastodonRegex.swift | 0 .../View/ComposeContentView.swift | 16 ++ 10 files changed, 317 insertions(+), 258 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+UITextViewDelegate.swift rename {Mastodon => MastodonSDK/Sources/MastodonUI/Scene/ComposeContent}/Helper/MastodonRegex.swift (100%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 380f21eac..c6dc27ac1 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -376,7 +376,6 @@ DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */; }; DBB8AB4F26AED13F00F6D281 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDE25BAA00100D1B89D /* Assets.xcassets */; }; DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB9759B262462E1004620BD /* ThreadMetaView.swift */; }; - DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */; }; DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; }; DBC6461526A170AB00B0E31B /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6461426A170AB00B0E31B /* ComposeViewController.swift */; }; DBC6461826A170AB00B0E31B /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DBC6461626A170AB00B0E31B /* MainInterface.storyboard */; }; @@ -962,7 +961,6 @@ DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPostIntentHandler.swift; sourceTree = ""; }; DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = ""; }; DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = ""; }; - DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonRegex.swift; sourceTree = ""; }; DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = ""; }; DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; DBC6461426A170AB00B0E31B /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = ""; }; @@ -2520,7 +2518,6 @@ DBBC24D526A54BCB00398BB9 /* Helper */ = { isa = PBXGroup; children = ( - DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */, DBF3B7402733EB9400E21627 /* MastodonLocalCode.swift */, ); path = Helper; @@ -3246,7 +3243,6 @@ DBFEEC9D279C12C1004F81DD /* ProfileFieldEditCollectionViewCell.swift in Sources */, DB3E6FEC2806D7F100B035AE /* DiscoveryNewsViewController.swift in Sources */, DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */, - DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */, DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */, 2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */, 2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */, diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index bf9145d6c..a2830edff 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -106,13 +106,7 @@ final class ComposeViewController: UIViewController, NeedsDependency { // let composeToolbarBackgroundView = UIView() // // -// private(set) lazy var autoCompleteViewController: AutoCompleteViewController = { -// let viewController = AutoCompleteViewController() -// viewController.viewModel = AutoCompleteViewModel(context: context, authContext: viewModel.authContext) -// viewController.delegate = self -// viewController.viewModel.customEmojiViewModel.value = viewModel.customEmojiViewModel -// return viewController -// }() + deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -243,33 +237,6 @@ extension ComposeViewController { // // update layout when keyboard show/dismiss // view.layoutIfNeeded() // - -// -// // bind auto-complete -// viewModel.$autoCompleteInfo -// .receive(on: DispatchQueue.main) -// .sink { [weak self] info in -// guard let self = self else { return } -// let textEditorView = self.textEditorView -// if self.autoCompleteViewController.view.superview == nil { -// self.autoCompleteViewController.view.frame = self.view.bounds -// // add to container view. seealso: `viewDidLayoutSubviews()` -// self.viewModel.composeStatusContentTableViewCell.textEditorViewContainerView.addSubview(self.autoCompleteViewController.view) -// self.addChild(self.autoCompleteViewController) -// self.autoCompleteViewController.didMove(toParent: self) -// self.autoCompleteViewController.view.isHidden = true -// self.tableView.autoCompleteViewController = self.autoCompleteViewController -// } -// self.updateAutoCompleteViewControllerLayout() -// self.autoCompleteViewController.view.isHidden = info == nil -// guard let info = info else { return } -// let symbolBoundingRectInContainer = textEditorView.textView.convert(info.symbolBoundingRect, to: self.autoCompleteViewController.chevronView) -// self.autoCompleteViewController.view.frame.origin.y = info.textBoundingRect.maxY -// self.autoCompleteViewController.viewModel.symbolBoundingRect.value = symbolBoundingRectInContainer -// self.autoCompleteViewController.viewModel.inputText.value = String(info.inputText) -// } -// .store(in: &disposeBag) -// // // bind publish bar button state // viewModel.$isPublishBarButtonItemEnabled // .receive(on: DispatchQueue.main) @@ -431,23 +398,6 @@ extension ComposeViewController { // viewModel.traitCollectionDidChangePublisher.send() } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - updateAutoCompleteViewControllerLayout() - } - - private func updateAutoCompleteViewControllerLayout() { - // pin autoCompleteViewController frame to current view -// if let containerView = autoCompleteViewController.view.superview { -// let viewFrameInWindow = containerView.convert(autoCompleteViewController.view.frame, to: view) -// if viewFrameInWindow.origin.x != 0 { -// autoCompleteViewController.view.frame.origin.x = -viewFrameInWindow.origin.x -// } -// autoCompleteViewController.view.frame.size.width = view.frame.width -// } - } - } //extension ComposeViewController { @@ -661,126 +611,11 @@ extension ComposeViewController { // return true // } // -// func textViewDidChange(_ textView: UITextView) { -// switch textView { -// case textEditorView.textView: -// // update model -// let metaText = self.textEditorView -// let backedString = metaText.backedString -// viewModel.composeStatusAttribute.composeContent = backedString -// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(backedString)") -// -// // configure auto completion -// setupAutoComplete(for: textView) -// default: -// assertionFailure() -// } -// } + // -// struct AutoCompleteInfo { -// // model -// let inputText: Substring -// // range -// let symbolRange: Range -// let symbolString: Substring -// let toCursorRange: Range -// let toCursorString: Substring -// let toHighlightEndRange: Range -// let toHighlightEndString: Substring -// // geometry -// var textBoundingRect: CGRect = .zero -// var symbolBoundingRect: CGRect = .zero -// } + // -// private func setupAutoComplete(for textView: UITextView) { -// guard var autoCompletion = ComposeViewController.scanAutoCompleteInfo(textView: textView) else { -// viewModel.autoCompleteInfo = nil -// return -// } -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete %s (%s)", ((#file as NSString).lastPathComponent), #line, #function, String(autoCompletion.toHighlightEndString), String(autoCompletion.toCursorString)) -// -// // get layout text bounding rect -// var glyphRange = NSRange() -// textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.toCursorRange, in: textView.text), actualGlyphRange: &glyphRange) -// let textContainer = textView.layoutManager.textContainers[0] -// let textBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) -// -// let retryLayoutTimes = viewModel.autoCompleteRetryLayoutTimes -// guard textBoundingRect.size != .zero else { -// viewModel.autoCompleteRetryLayoutTimes += 1 -// // avoid infinite loop -// guard retryLayoutTimes < 3 else { return } -// // needs retry calculate layout when the rect position changing -// DispatchQueue.main.async { -// self.setupAutoComplete(for: textView) -// } -// return -// } -// viewModel.autoCompleteRetryLayoutTimes = 0 -// -// // get symbol bounding rect -// textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.symbolRange, in: textView.text), actualGlyphRange: &glyphRange) -// let symbolBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) -// -// // set bounding rect and trigger layout -// autoCompletion.textBoundingRect = textBoundingRect -// autoCompletion.symbolBoundingRect = symbolBoundingRect -// viewModel.autoCompleteInfo = autoCompletion -// } -// -// private static func scanAutoCompleteInfo(textView: UITextView) -> AutoCompleteInfo? { -// guard let text = textView.text, -// textView.selectedRange.location > 0, !text.isEmpty, -// let selectedRange = Range(textView.selectedRange, in: text) else { -// return nil -// } -// let cursorIndex = selectedRange.upperBound -// let _highlightStartIndex: String.Index? = { -// var index = text.index(before: cursorIndex) -// while index > text.startIndex { -// let char = text[index] -// if char == "@" || char == "#" || char == ":" { -// return index -// } -// index = text.index(before: index) -// } -// assert(index == text.startIndex) -// let char = text[index] -// if char == "@" || char == "#" || char == ":" { -// return index -// } else { -// return nil -// } -// }() -// -// guard let highlightStartIndex = _highlightStartIndex else { return nil } -// let scanRange = NSRange(highlightStartIndex..= cursorIndex else { return nil } -// let symbolRange = highlightStartIndex.. Bool { // switch textView { diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 45c9f1e93..df9f7b710 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -41,8 +41,6 @@ final class ComposeViewModel: NSObject { // // @Published var selectedStatusVisibility: ComposeToolbarView.VisibilitySelectionType // @Published var repliedToCellFrame: CGRect = .zero -// @Published var autoCompleteRetryLayoutTimes = 0 -// @Published var autoCompleteInfo: ComposeViewController.AutoCompleteInfo? = nil let traitCollectionDidChangePublisher = CurrentValueSubject(Void()) // use CurrentValueSubject to make initial event emit // var isViewAppeared = false diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewController.swift index aa21057d1..9af1ce9bf 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewController.swift @@ -88,7 +88,7 @@ extension AutoCompleteViewController { ]) tableView.delegate = self -// viewModel.setupDiffableDataSource(tableView: tableView) + viewModel.setupDiffableDataSource(tableView: tableView) // bind to layout chevron viewModel.symbolBoundingRect diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+Diffable.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+Diffable.swift index adbf6ac09..2dd815d0a 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+Diffable.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+Diffable.swift @@ -6,17 +6,18 @@ // import UIKit +import MastodonCore extension AutoCompleteViewModel { -// func setupDiffableDataSource( -// tableView: UITableView -// ) { -// diffableDataSource = AutoCompleteSection.tableViewDiffableDataSource(for: tableView) -// -// var snapshot = NSDiffableDataSourceSnapshot() -// snapshot.appendSections([.main]) -// diffableDataSource?.apply(snapshot) -// } + func setupDiffableDataSource( + tableView: UITableView + ) { + diffableDataSource = AutoCompleteSection.tableViewDiffableDataSource(tableView: tableView) + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + diffableDataSource?.apply(snapshot) + } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index 7dde9c8c0..df7246fa7 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -20,6 +20,7 @@ public final class ComposeContentViewController: UIViewController { public var viewModel: ComposeContentViewModel! private(set) lazy var composeContentToolbarViewModel = ComposeContentToolbarView.ViewModel(delegate: self) + // tableView container let tableView: ComposeTableView = { let tableView = ComposeTableView() tableView.estimatedRowHeight = UITableView.automaticDimension @@ -29,6 +30,17 @@ public final class ComposeContentViewController: UIViewController { return tableView }() + // auto complete + private(set) lazy var autoCompleteViewController: AutoCompleteViewController = { + let viewController = AutoCompleteViewController() + viewController.viewModel = AutoCompleteViewModel(context: viewModel.context, authContext: viewModel.authContext) + viewController.delegate = self + // viewController.viewModel.customEmojiViewModel.value = viewModel.customEmojiViewModel + return viewController + }() + + // toolbar + lazy var composeContentToolbarView = ComposeContentToolbarView(viewModel: composeContentToolbarViewModel) var composeContentToolbarViewBottomLayoutConstraint: NSLayoutConstraint! let composeContentToolbarBackgroundView = UIView() @@ -218,6 +230,32 @@ extension ComposeContentViewController { } .store(in: &disposeBag) + // bind auto-complete + viewModel.$autoCompleteInfo + .receive(on: DispatchQueue.main) + .sink { [weak self] info in + guard let self = self else { return } + guard let textView = self.viewModel.contentMetaText?.textView else { return } + if self.autoCompleteViewController.view.superview == nil { + self.autoCompleteViewController.view.frame = self.view.bounds + // add to container view. seealso: `viewDidLayoutSubviews()` + self.viewModel.composeContentTableViewCell.contentView.addSubview(self.autoCompleteViewController.view) + self.addChild(self.autoCompleteViewController) + self.autoCompleteViewController.didMove(toParent: self) + self.autoCompleteViewController.view.isHidden = true + self.tableView.autoCompleteViewController = self.autoCompleteViewController + } + self.updateAutoCompleteViewControllerLayout() + self.autoCompleteViewController.view.isHidden = info == nil + guard let info = info else { return } + let symbolBoundingRectInContainer = textView.convert(info.symbolBoundingRect, to: self.autoCompleteViewController.chevronView) + print(info.symbolBoundingRect) + self.autoCompleteViewController.view.frame.origin.y = info.textBoundingRect.maxY + self.viewModel.contentTextViewFrame.minY + self.autoCompleteViewController.viewModel.symbolBoundingRect.value = symbolBoundingRectInContainer + self.autoCompleteViewController.viewModel.inputText.value = String(info.inputText) + } + .store(in: &disposeBag) + // bind toolbar bindToolbarViewModel() } @@ -226,6 +264,7 @@ extension ComposeContentViewController { super.viewDidLayoutSubviews() viewModel.viewLayoutFrame.update(view: view) + updateAutoCompleteViewControllerLayout() } public override func viewSafeAreaInsetsDidChange() { @@ -264,6 +303,17 @@ extension ComposeContentViewController { viewModel.$contentWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWeightedLength) viewModel.$contentWarningWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWarningWeightedLength) } + + private func updateAutoCompleteViewControllerLayout() { + // pin autoCompleteViewController frame to current view + if let containerView = autoCompleteViewController.view.superview { + let viewFrameInWindow = containerView.convert(autoCompleteViewController.view.frame, to: view) + if viewFrameInWindow.origin.x != 0 { + autoCompleteViewController.view.frame.origin.x = -viewFrameInWindow.origin.x + } + autoCompleteViewController.view.frame.size.width = view.frame.width + } + } } // MARK: - UIScrollViewDelegate @@ -427,3 +477,13 @@ extension ComposeContentViewController: ComposeContentToolbarViewDelegate { } } } + +// MARK: - AutoCompleteViewControllerDelegate +extension ComposeContentViewController: AutoCompleteViewControllerDelegate { + func autoCompleteViewController( + _ viewController: AutoCompleteViewController, + didSelectItem item: AutoCompleteItem + ) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did select item: \(String(describing: item))") + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+UITextViewDelegate.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+UITextViewDelegate.swift new file mode 100644 index 000000000..5b5c018e2 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+UITextViewDelegate.swift @@ -0,0 +1,203 @@ +// +// ComposeContentViewModel+UITextViewDelegate.swift +// +// +// Created by MainasuK on 2022/11/13. +// + +import os.log +import UIKit + +// MARK: - UITextViewDelegate +extension ComposeContentViewModel: UITextViewDelegate { + + public func textViewDidBeginEditing(_ textView: UITextView) { + // Note: + // Xcode warning: + // Publishing changes from within view updates is not allowed, this will cause undefined behavior. + // + // Just ignore the warning and see what will happen… + switch textView { + case contentMetaText?.textView: + isContentEditing = true + case contentWarningMetaText?.textView: + isContentWarningEditing = true + default: + assertionFailure() + break + } + } + + public func textViewDidChange(_ textView: UITextView) { + switch textView { + case contentMetaText?.textView: + // update model + guard let metaText = self.contentMetaText else { + assertionFailure() + return + } + let backedString = metaText.backedString + logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(backedString)") + + // configure auto completion + setupAutoComplete(for: textView) + + case contentWarningMetaText?.textView: + break + default: + assertionFailure() + } + } + + public func textViewDidEndEditing(_ textView: UITextView) { + switch textView { + case contentMetaText?.textView: + isContentEditing = false + case contentWarningMetaText?.textView: + isContentWarningEditing = false + default: + assertionFailure() + break + } + } + + public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + switch textView { + case contentMetaText?.textView: + return true + case contentWarningMetaText?.textView: + let isReturn = text == "\n" + if isReturn { + setContentTextViewFirstResponderIfNeeds() + } + return !isReturn + default: + assertionFailure() + return true + } + } + +} + +extension ComposeContentViewModel { + + func insertContentText(text: String) { + guard let contentMetaText = self.contentMetaText else { return } + // FIXME: smart prefix and suffix + let string = contentMetaText.textStorage.string + let isEmpty = string.isEmpty + let hasPrefix = string.hasPrefix(" ") + if hasPrefix || isEmpty { + contentMetaText.textView.insertText(text) + } else { + contentMetaText.textView.insertText(" " + text) + } + } + + func setContentTextViewFirstResponderIfNeeds() { + guard let contentMetaText = self.contentMetaText else { return } + guard !contentMetaText.textView.isFirstResponder else { return } + contentMetaText.textView.becomeFirstResponder() + } + + func setContentWarningTextViewFirstResponderIfNeeds() { + guard let contentWarningMetaText = self.contentWarningMetaText else { return } + guard !contentWarningMetaText.textView.isFirstResponder else { return } + contentWarningMetaText.textView.becomeFirstResponder() + } + +} + +extension ComposeContentViewModel { + + private func setupAutoComplete(for textView: UITextView) { + guard var autoCompletion = ComposeContentViewModel.scanAutoCompleteInfo(textView: textView) else { + self.autoCompleteInfo = nil + return + } + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete %s (%s)", ((#file as NSString).lastPathComponent), #line, #function, String(autoCompletion.toHighlightEndString), String(autoCompletion.toCursorString)) + + // get layout text bounding rect + var glyphRange = NSRange() + textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.toCursorRange, in: textView.text), actualGlyphRange: &glyphRange) + let textContainer = textView.layoutManager.textContainers[0] + let textBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) + + let retryLayoutTimes = autoCompleteRetryLayoutTimes + guard textBoundingRect.size != .zero else { + autoCompleteRetryLayoutTimes += 1 + // avoid infinite loop + guard retryLayoutTimes < 3 else { return } + // needs retry calculate layout when the rect position changing + DispatchQueue.main.async { + self.setupAutoComplete(for: textView) + } + return + } + autoCompleteRetryLayoutTimes = 0 + + // get symbol bounding rect + textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.symbolRange, in: textView.text), actualGlyphRange: &glyphRange) + let symbolBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) + + // set bounding rect and trigger layout + autoCompletion.textBoundingRect = textBoundingRect + autoCompletion.symbolBoundingRect = symbolBoundingRect + autoCompleteInfo = autoCompletion + } + + private static func scanAutoCompleteInfo(textView: UITextView) -> AutoCompleteInfo? { + guard let text = textView.text, + textView.selectedRange.location > 0, !text.isEmpty, + let selectedRange = Range(textView.selectedRange, in: text) else { + return nil + } + let cursorIndex = selectedRange.upperBound + let _highlightStartIndex: String.Index? = { + var index = text.index(before: cursorIndex) + while index > text.startIndex { + let char = text[index] + if char == "@" || char == "#" || char == ":" { + return index + } + index = text.index(before: index) + } + assert(index == text.startIndex) + let char = text[index] + if char == "@" || char == "#" || char == ":" { + return index + } else { + return nil + } + }() + + guard let highlightStartIndex = _highlightStartIndex else { return nil } + let scanRange = NSRange(highlightStartIndex..= cursorIndex else { return nil } + let symbolRange = highlightStartIndex..) case reply(status: ManagedObjectRecord) } - + public enum ScrollViewState { case fold // snap to input case expand // snap to reply } } +extension ComposeContentViewModel { + struct AutoCompleteInfo { + // model + let inputText: Substring + // range + let symbolRange: Range + let symbolString: Substring + let toCursorRange: Range + let toCursorString: Substring + let toHighlightEndRange: Range + let toHighlightEndString: Substring + // geometry + var textBoundingRect: CGRect = .zero + var symbolBoundingRect: CGRect = .zero + } +} + extension ComposeContentViewModel { func createNewPollOptionIfCould() { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") @@ -286,77 +307,6 @@ extension ComposeContentViewModel { } // end func publisher() } -// MARK: - UITextViewDelegate -extension ComposeContentViewModel: UITextViewDelegate { - public func textViewDidBeginEditing(_ textView: UITextView) { - // Note: - // Xcode warning: - // Publishing changes from within view updates is not allowed, this will cause undefined behavior. - // - // Just ignore the warning and see what will happen… - switch textView { - case contentMetaText?.textView: - isContentEditing = true - case contentWarningMetaText?.textView: - isContentWarningEditing = true - default: - break - } - } - - public func textViewDidEndEditing(_ textView: UITextView) { - switch textView { - case contentMetaText?.textView: - isContentEditing = false - case contentWarningMetaText?.textView: - isContentWarningEditing = false - default: - break - } - } - - public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - switch textView { - case contentMetaText?.textView: - return true - case contentWarningMetaText?.textView: - let isReturn = text == "\n" - if isReturn { - setContentTextViewFirstResponderIfNeeds() - } - return !isReturn - default: - assertionFailure() - return true - } - } - - func insertContentText(text: String) { - guard let contentMetaText = self.contentMetaText else { return } - // FIXME: smart prefix and suffix - let string = contentMetaText.textStorage.string - let isEmpty = string.isEmpty - let hasPrefix = string.hasPrefix(" ") - if hasPrefix || isEmpty { - contentMetaText.textView.insertText(text) - } else { - contentMetaText.textView.insertText(" " + text) - } - } - - func setContentTextViewFirstResponderIfNeeds() { - guard let contentMetaText = self.contentMetaText else { return } - guard !contentMetaText.textView.isFirstResponder else { return } - contentMetaText.textView.becomeFirstResponder() - } - - func setContentWarningTextViewFirstResponderIfNeeds() { - guard let contentWarningMetaText = self.contentWarningMetaText else { return } - guard !contentWarningMetaText.textView.isFirstResponder else { return } - contentWarningMetaText.textView.becomeFirstResponder() - } -} - // MARK: - DeleteBackwardResponseTextFieldRelayDelegate extension ComposeContentViewModel: DeleteBackwardResponseTextFieldRelayDelegate { diff --git a/Mastodon/Helper/MastodonRegex.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Helper/MastodonRegex.swift similarity index 100% rename from Mastodon/Helper/MastodonRegex.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Helper/MastodonRegex.swift diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift index e1954af04..6dad1e19b 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift @@ -18,9 +18,11 @@ public struct ComposeContentView: View { static let logger = Logger(subsystem: "ComposeContentView", category: "View") var logger: Logger { ComposeContentView.logger } + static let contentViewCoordinateSpace = "ComposeContentView.Content" static var margin: CGFloat = 16 @ObservedObject var viewModel: ComposeContentViewModel + public var body: some View { VStack(spacing: .zero) { @@ -106,6 +108,19 @@ public struct ComposeContentView: View { .frame(minHeight: 100) .fixedSize(horizontal: false, vertical: true) .padding(.horizontal, ComposeContentView.margin) + .background( + GeometryReader { proxy in + Color.clear.preference(key: ViewFramePreferenceKey.self, value: proxy.frame(in: .named(ComposeContentView.contentViewCoordinateSpace))) + } + .onPreferenceChange(ViewFramePreferenceKey.self) { frame in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): content textView frame: \(frame.debugDescription)") + let rect = frame.standardized + viewModel.contentTextViewFrame = CGRect( + origin: frame.origin, + size: CGSize(width: floor(rect.width), height: floor(rect.height)) + ) + } + ) // poll pollView .padding(.horizontal, ComposeContentView.margin) @@ -128,6 +143,7 @@ public struct ComposeContentView: View { ) Spacer() } // end VStack + .coordinateSpace(name: ComposeContentView.contentViewCoordinateSpace) } // end body } From 70a59fd54104cccb021b08e8953ed6fa91cf84e7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 10:53:45 +0100 Subject: [PATCH 145/224] New translations Localizable.stringsdict (German) --- .../StringsConvertor/input/de.lproj/Localizable.stringsdict | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/de.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/de.lproj/Localizable.stringsdict index c6a8a4297..f60c6b0d7 100644 --- a/Localization/StringsConvertor/input/de.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/de.lproj/Localizable.stringsdict @@ -248,9 +248,9 @@ NSStringFormatValueTypeKey ld one - 1 Follower + 1 Folgender other - %ld Follower + %ld Folgende date.year.left From 88307057c0dd06c28ea2e5900cc4acebdafa00ad Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 13 Nov 2022 19:42:50 +0800 Subject: [PATCH 146/224] feat: restore emoji picker for post compose --- Mastodon.xcodeproj/project.pbxproj | 16 -- .../Scene/Compose/ComposeViewController.swift | 95 +----------- .../Compose/ComposeViewModel+DataSource.swift | 37 ----- Mastodon/Scene/Compose/ComposeViewModel.swift | 5 - .../Model/Compose/CustomEmojiPickerItem.swift | 14 +- .../CustomEmojiPickerSection+Diffable.swift | 96 ++++++------ .../AutoCompleteViewModel+State.swift | 2 +- .../AutoComplete/AutoCompleteViewModel.swift | 4 +- .../ComposeContentViewController.swift | 141 ++++++++++++++++++ .../ComposeContentViewModel+DataSource.swift | 40 +++++ ...oseContentViewModel+MetaTextDelegate.swift | 4 +- ...eContentViewModel+UITextViewDelegate.swift | 6 + .../ComposeContentViewModel.swift | 28 +++- ...jiPickerHeaderCollectionReusableView.swift | 0 .../CustomEmojiPickerInputView.swift | 0 .../CustomEmojiPickerInputViewModel.swift | 48 +++--- ...tomEmojiPickerItemCollectionViewCell.swift | 0 .../View/ComposeContentView.swift | 2 +- 18 files changed, 304 insertions(+), 234 deletions(-) rename {Mastodon/Scene/Compose/CollectionViewCell => MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker}/CustomEmojiPickerHeaderCollectionReusableView.swift (100%) rename {Mastodon/Scene/Compose/View => MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker}/CustomEmojiPickerInputView.swift (100%) rename {Mastodon/Scene/Compose/View => MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker}/CustomEmojiPickerInputViewModel.swift (52%) rename {Mastodon/Scene/Compose/CollectionViewCell => MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker}/CustomEmojiPickerItemCollectionViewCell.swift (100%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index c6dc27ac1..e3c3c58d3 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -151,7 +151,6 @@ DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */; }; DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */; }; DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */; }; - DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */; }; DB22C92228E700A10082A9E9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB22C92128E700A10082A9E9 /* MastodonSDK */; }; DB22C92428E700A80082A9E9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB22C92328E700A80082A9E9 /* MastodonSDK */; }; DB22C92628E700AF0082A9E9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB22C92528E700AF0082A9E9 /* MastodonSDK */; }; @@ -185,9 +184,6 @@ DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DEC25BAA00100D1B89D /* MastodonTests.swift */; }; DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; }; DB443CD42694627B00159B29 /* AppearanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB443CD32694627B00159B29 /* AppearanceView.swift */; }; - DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */; }; - DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */; }; - DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */; }; DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481B825EE289600BEFB67 /* UITableView.swift */; }; DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */; }; DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */; }; @@ -680,7 +676,6 @@ DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+Diffable.swift"; sourceTree = ""; }; DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerItem.swift; sourceTree = ""; }; DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = ""; }; - DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputViewModel.swift; sourceTree = ""; }; DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = ""; }; DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollExpiresOptionCollectionViewCell.swift; sourceTree = ""; }; @@ -717,9 +712,6 @@ DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = ""; }; DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DB443CD32694627B00159B29 /* AppearanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceView.swift; sourceTree = ""; }; - DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputView.swift; sourceTree = ""; }; - DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerItemCollectionViewCell.swift; sourceTree = ""; }; - DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerHeaderCollectionReusableView.swift; sourceTree = ""; }; DB4481B825EE289600BEFB67 /* UITableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableView.swift; sourceTree = ""; }; DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = ""; }; DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBarButtonItem.swift; sourceTree = ""; }; @@ -1876,8 +1868,6 @@ DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */, DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */, DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */, - DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */, - DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */, DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */, ); path = View; @@ -2157,8 +2147,6 @@ DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */, DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */, DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */, - DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */, - DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */, ); path = CollectionViewCell; sourceTree = ""; @@ -3335,7 +3323,6 @@ DB98EB6227B215EB0082E365 /* ReportResultViewController.swift in Sources */, DB6B74F4272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift in Sources */, DB6B74F2272FB67600C70B6E /* FollowerListViewModel.swift in Sources */, - DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */, 0F20222D261457EE000C64BF /* HashtagTimelineViewModel+State.swift in Sources */, DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */, 5B90C462262599800002E742 /* SettingsSectionHeader.swift in Sources */, @@ -3349,7 +3336,6 @@ DBFEEC9B279BDDD9004F81DD /* ProfileAboutViewModel+Diffable.swift in Sources */, DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */, DB0618012785732C0030EE79 /* ServerRulesTableViewCell.swift in Sources */, - DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */, DB98EB5C27B10A730082E365 /* ReportSupplementaryViewModel.swift in Sources */, DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */, DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */, @@ -3433,7 +3419,6 @@ 2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */, 2D4AD89C263165B500613EFC /* SuggestionAccountCollectionViewCell.swift in Sources */, DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */, - DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */, DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */, DB697DDF278F524F004EF2F7 /* DataSourceFacade+Profile.swift in Sources */, DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */, @@ -3457,7 +3442,6 @@ DB63F74D27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift in Sources */, DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */, DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */, - DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */, 0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */, DB7A9F932818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift in Sources */, 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */, diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index a2830edff..f23e44e5f 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -95,11 +95,7 @@ final class ComposeViewController: UIViewController, NeedsDependency { // } // } // -// // CustomEmojiPickerView -// let customEmojiPickerInputView: CustomEmojiPickerInputView = { -// let view = CustomEmojiPickerInputView(frame: CGRect(x: 0, y: 0, width: 0, height: 300), inputViewStyle: .keyboard) -// return view -// }() + // // let composeToolbarView = ComposeToolbarView() // var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint! @@ -107,7 +103,6 @@ final class ComposeViewController: UIViewController, NeedsDependency { // // - deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) } @@ -225,13 +220,6 @@ extension ComposeViewController { // } // .store(in: &disposeBag) -// customEmojiPickerInputView.collectionView.delegate = self -// viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView -// viewModel.setupCustomEmojiPickerDiffableDataSource( -// for: customEmojiPickerInputView.collectionView, -// dependency: self -// ) - // viewModel.composeStatusContentTableViewCell.delegate = self // // // update layout when keyboard show/dismiss @@ -350,19 +338,6 @@ extension ComposeViewController { // } // .store(in: &disposeBag) // -// // bind custom emoji picker UI -// viewModel.customEmojiViewModel?.emojis -// .receive(on: DispatchQueue.main) -// .sink(receiveValue: { [weak self] emojis in -// guard let self = self else { return } -// if emojis.isEmpty { -// self.customEmojiPickerInputView.activityIndicatorView.startAnimating() -// } else { -// self.customEmojiPickerInputView.activityIndicatorView.stopAnimating() -// } -// }) -// .store(in: &disposeBag) -// // configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar.value) // Publishers.CombineLatest( // keyboardHasShortcutBar, @@ -694,30 +669,6 @@ extension ComposeViewController { //// MARK: - UITableViewDelegate //extension ComposeViewController: UITableViewDelegate { } -// -//// MARK: - UICollectionViewDelegate -//extension ComposeViewController: UICollectionViewDelegate { -// -// func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription) -// -// if collectionView === customEmojiPickerInputView.collectionView { -// guard let diffableDataSource = viewModel.customEmojiPickerDiffableDataSource else { return } -// let item = diffableDataSource.itemIdentifier(for: indexPath) -// guard case let .emoji(attribute) = item else { return } -// let emoji = attribute.emoji -// -// // make click sound -// UIDevice.current.playInputClick() -// -// // retrieve active text input and insert emoji -// // the trailing space is REQUIRED to make regex happy -// _ = viewModel.customEmojiPickerInputViewModel.insertText(":\(emoji.shortcode): ") -// } else { -// // do nothing -// } -// } -//} // MARK: - UIAdaptivePresentationControllerDelegate extension ComposeViewController: UIAdaptivePresentationControllerDelegate { @@ -877,49 +828,7 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate { // return true // } //} -// -//// MARK: - AutoCompleteViewControllerDelegate -//extension ComposeViewController: AutoCompleteViewControllerDelegate { -// func autoCompleteViewController(_ viewController: AutoCompleteViewController, didSelectItem item: AutoCompleteItem) { -// guard let info = viewModel.autoCompleteInfo else { return } -// let _replacedText: String? = { -// var text: String -// switch item { -// case .hashtag(let hashtag): -// text = "#" + hashtag.name -// case .hashtagV1(let hashtagName): -// text = "#" + hashtagName -// case .account(let account): -// text = "@" + account.acct -// case .emoji(let emoji): -// text = ":" + emoji.shortcode + ":" -// case .bottomLoader: -// return nil -// } -// return text -// }() -// guard let replacedText = _replacedText else { return } -// guard let text = textEditorView.textView.text else { return } -// -// let range = NSRange(info.toHighlightEndRange, in: text) -// textEditorView.textStorage.replaceCharacters(in: range, with: replacedText) -// DispatchQueue.main.async { -// self.textEditorView.textView.insertText(" ") // trigger textView delegate update -// } -// viewModel.autoCompleteInfo = nil -// -// switch item { -// case .emoji, .bottomLoader: -// break -// default: -// // set selected range except emoji -// let newRange = NSRange(location: range.location + (replacedText as NSString).length, length: 0) -// guard textEditorView.textStorage.length <= newRange.location else { return } -// textEditorView.textView.selectedRange = newRange -// } -// } -//} -// + //extension ComposeViewController { // override var keyCommands: [UIKeyCommand]? { // composeKeyCommands diff --git a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift b/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift index b3d8f52dc..5ecba3791 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift @@ -51,43 +51,6 @@ extension ComposeViewModel { // // setup data source // tableView.dataSource = self // } -// -// func setupCustomEmojiPickerDiffableDataSource( -// for collectionView: UICollectionView, -// dependency: NeedsDependency -// ) { -// let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource( -// for: collectionView, -// dependency: dependency -// ) -// self.customEmojiPickerDiffableDataSource = diffableDataSource -// -// let _domain = customEmojiViewModel?.domain -// customEmojiViewModel?.emojis -// .receive(on: DispatchQueue.main) -// .sink { [weak self, weak diffableDataSource] emojis in -// guard let _ = self else { return } -// guard let diffableDataSource = diffableDataSource else { return } -// -// var snapshot = NSDiffableDataSourceSnapshot() -// let domain = _domain?.uppercased() ?? " " -// let customEmojiSection = CustomEmojiPickerSection.emoji(name: domain) -// snapshot.appendSections([customEmojiSection]) -// let items: [CustomEmojiPickerItem] = { -// var items = [CustomEmojiPickerItem]() -// for emoji in emojis where emoji.visibleInPicker { -// let attribute = CustomEmojiPickerItem.CustomEmojiAttribute(emoji: emoji) -// let item = CustomEmojiPickerItem.emoji(attribute: attribute) -// items.append(item) -// } -// return items -// }() -// snapshot.appendItems(items, toSection: customEmojiSection) -// -// diffableDataSource.apply(snapshot) -// } -// .store(in: &disposeBag) -// } } diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index df9f7b710..5e5fbb1c3 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -102,11 +102,6 @@ final class ComposeViewModel: NSObject { // // for mention: "@ " // var preInsertedContent: String? // -// // custom emojis -// let customEmojiViewModel: EmojiService.CustomEmojiViewModel? -// let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel() -// @Published var isLoadingCustomEmoji = false -// // // attachment // @Published var attachmentServices: [MastodonAttachmentService] = [] // diff --git a/MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerItem.swift index 52f522703..6174f4687 100644 --- a/MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerItem.swift +++ b/MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerItem.swift @@ -8,28 +8,28 @@ import Foundation import MastodonSDK -enum CustomEmojiPickerItem { +public enum CustomEmojiPickerItem { case emoji(attribute: CustomEmojiAttribute) } extension CustomEmojiPickerItem: Equatable, Hashable { } extension CustomEmojiPickerItem { - final class CustomEmojiAttribute: Equatable, Hashable { - let id = UUID() + public final class CustomEmojiAttribute: Equatable, Hashable { + public let id = UUID() - let emoji: Mastodon.Entity.Emoji + public let emoji: Mastodon.Entity.Emoji - init(emoji: Mastodon.Entity.Emoji) { + public init(emoji: Mastodon.Entity.Emoji) { self.emoji = emoji } - static func == (lhs: CustomEmojiPickerItem.CustomEmojiAttribute, rhs: CustomEmojiPickerItem.CustomEmojiAttribute) -> Bool { + public static func == (lhs: CustomEmojiPickerItem.CustomEmojiAttribute, rhs: CustomEmojiPickerItem.CustomEmojiAttribute) -> Bool { return lhs.id == rhs.id && lhs.emoji.shortcode == rhs.emoji.shortcode } - func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(id) } } diff --git a/MastodonSDK/Sources/MastodonUI/DataSource/CustomEmojiPickerSection+Diffable.swift b/MastodonSDK/Sources/MastodonUI/DataSource/CustomEmojiPickerSection+Diffable.swift index ca3658e95..4c142b532 100644 --- a/MastodonSDK/Sources/MastodonUI/DataSource/CustomEmojiPickerSection+Diffable.swift +++ b/MastodonSDK/Sources/MastodonUI/DataSource/CustomEmojiPickerSection+Diffable.swift @@ -5,55 +5,55 @@ // Created by MainasuK on 22/10/10. // -import Foundation +import UIKit import MastodonCore extension CustomEmojiPickerSection { -// static func collectionViewDiffableDataSource( -// collectionView: UICollectionView, -// dependency: NeedsDependency -// ) -> UICollectionViewDiffableDataSource { -// let dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak dependency] collectionView, indexPath, item -> UICollectionViewCell? in -// guard let _ = dependency else { return nil } -// switch item { -// case .emoji(let attribute): -// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CustomEmojiPickerItemCollectionViewCell.self), for: indexPath) as! CustomEmojiPickerItemCollectionViewCell -// let placeholder = UIImage.placeholder(size: CustomEmojiPickerItemCollectionViewCell.itemSize, color: .systemFill) -// .af.imageRounded(withCornerRadius: 4) -// -// let isAnimated = !UserDefaults.shared.preferredStaticEmoji -// let url = URL(string: isAnimated ? attribute.emoji.url : attribute.emoji.staticURL) -// cell.emojiImageView.sd_setImage( -// with: url, -// placeholderImage: placeholder, -// options: [], -// context: nil -// ) -// cell.accessibilityLabel = attribute.emoji.shortcode -// return cell -// } -// } -// -// dataSource.supplementaryViewProvider = { [weak dataSource] collectionView, kind, indexPath -> UICollectionReusableView? in -// guard let dataSource = dataSource else { return nil } -// let sections = dataSource.snapshot().sectionIdentifiers -// guard indexPath.section < sections.count else { return nil } -// let section = sections[indexPath.section] -// -// switch kind { -// case String(describing: CustomEmojiPickerHeaderCollectionReusableView.self): -// let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self), for: indexPath) as! CustomEmojiPickerHeaderCollectionReusableView -// switch section { -// case .emoji(let name): -// header.titleLabel.text = name -// } -// return header -// default: -// assertionFailure() -// return nil -// } -// } -// -// return dataSource -// } + static func collectionViewDiffableDataSource( + collectionView: UICollectionView, + context: AppContext + ) -> UICollectionViewDiffableDataSource { + let dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak context] collectionView, indexPath, item -> UICollectionViewCell? in + guard let _ = context else { return nil } + switch item { + case .emoji(let attribute): + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CustomEmojiPickerItemCollectionViewCell.self), for: indexPath) as! CustomEmojiPickerItemCollectionViewCell + let placeholder = UIImage.placeholder(size: CustomEmojiPickerItemCollectionViewCell.itemSize, color: .systemFill) + .af.imageRounded(withCornerRadius: 4) + + let isAnimated = !UserDefaults.shared.preferredStaticEmoji + let url = URL(string: isAnimated ? attribute.emoji.url : attribute.emoji.staticURL) + cell.emojiImageView.sd_setImage( + with: url, + placeholderImage: placeholder, + options: [], + context: nil + ) + cell.accessibilityLabel = attribute.emoji.shortcode + return cell + } + } + + dataSource.supplementaryViewProvider = { [weak dataSource] collectionView, kind, indexPath -> UICollectionReusableView? in + guard let dataSource = dataSource else { return nil } + let sections = dataSource.snapshot().sectionIdentifiers + guard indexPath.section < sections.count else { return nil } + let section = sections[indexPath.section] + + switch kind { + case String(describing: CustomEmojiPickerHeaderCollectionReusableView.self): + let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self), for: indexPath) as! CustomEmojiPickerHeaderCollectionReusableView + switch section { + case .emoji(let name): + header.titleLabel.text = name + } + return header + default: + assertionFailure() + return nil + } + } + + return dataSource + } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+State.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+State.swift index b1f5f3187..7f93c4ba7 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+State.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+State.swift @@ -102,7 +102,7 @@ extension AutoCompleteViewModel.State { return } - guard let customEmojiViewModel = viewModel.customEmojiViewModel.value else { + guard let customEmojiViewModel = viewModel.customEmojiViewModel else { await enter(state: Fail.self) return } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift index 61715cd63..7459f68d1 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift @@ -20,7 +20,7 @@ final class AutoCompleteViewModel { let authContext: AuthContext public let inputText = CurrentValueSubject("") // contains "@" or "#" prefix public let symbolBoundingRect = CurrentValueSubject(.zero) - public let customEmojiViewModel = CurrentValueSubject(nil) + public let customEmojiViewModel: EmojiService.CustomEmojiViewModel? // output public var autoCompleteItems = CurrentValueSubject<[AutoCompleteItem], Never>([]) @@ -40,6 +40,8 @@ final class AutoCompleteViewModel { init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext + self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authContext.mastodonAuthenticationBox.domain) + // end init autoCompleteItems .receive(on: DispatchQueue.main) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index df7246fa7..54fc6e67a 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -72,6 +72,15 @@ public final class ComposeContentViewController: UIViewController { documentPickerController.delegate = self return documentPickerController }() + + // emoji picker inputView + let customEmojiPickerInputView: CustomEmojiPickerInputView = { + let view = CustomEmojiPickerInputView( + frame: CGRect(x: 0, y: 0, width: 0, height: 300), + inputViewStyle: .keyboard + ) + return view + }() deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -83,6 +92,8 @@ extension ComposeContentViewController { public override func viewDidLoad() { super.viewDidLoad() + viewModel.delegate = self + // setup view self.setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) ThemeService.shared.currentTheme @@ -106,6 +117,12 @@ extension ComposeContentViewController { tableView.delegate = self viewModel.setupDataSource(tableView: tableView) + // setup emoji picker + customEmojiPickerInputView.collectionView.delegate = self + viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView + viewModel.setupCustomEmojiPickerDiffableDataSource(collectionView: customEmojiPickerInputView.collectionView) + + // setup toolbar let toolbarHostingView = UIHostingController(rootView: composeContentToolbarView) toolbarHostingView.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(toolbarHostingView.view) @@ -128,6 +145,7 @@ extension ComposeContentViewController { view.bottomAnchor.constraint(equalTo: composeContentToolbarBackgroundView.bottomAnchor), ]) + // bind keyboard let keyboardHasShortcutBar = CurrentValueSubject(traitCollection.userInterfaceIdiom == .pad) // update default value later let keyboardEventPublishers = Publishers.CombineLatest3( KeyboardResponderService.shared.isShow, @@ -256,6 +274,19 @@ extension ComposeContentViewController { } .store(in: &disposeBag) + // bind emoji picker + viewModel.customEmojiViewModel?.emojis + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] emojis in + guard let self = self else { return } + if emojis.isEmpty { + self.customEmojiPickerInputView.activityIndicatorView.startAnimating() + } else { + self.customEmojiPickerInputView.activityIndicatorView.stopAnimating() + } + }) + .store(in: &disposeBag) + // bind toolbar bindToolbarViewModel() } @@ -485,5 +516,115 @@ extension ComposeContentViewController: AutoCompleteViewControllerDelegate { didSelectItem item: AutoCompleteItem ) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did select item: \(String(describing: item))") + + guard let info = viewModel.autoCompleteInfo else { return } + guard let metaText = viewModel.contentMetaText else { return } + + let _replacedText: String? = { + var text: String + switch item { + case .hashtag(let hashtag): + text = "#" + hashtag.name + case .hashtagV1(let hashtagName): + text = "#" + hashtagName + case .account(let account): + text = "@" + account.acct + case .emoji(let emoji): + text = ":" + emoji.shortcode + ":" + case .bottomLoader: + return nil + } + return text + }() + guard let replacedText = _replacedText else { return } + guard let text = metaText.textView.text else { return } + + let range = NSRange(info.toHighlightEndRange, in: text) + metaText.textStorage.replaceCharacters(in: range, with: replacedText) + viewModel.autoCompleteInfo = nil + + // set selected range + let newRange = NSRange(location: range.location + (replacedText as NSString).length, length: 0) + guard metaText.textStorage.length <= newRange.location else { return } + metaText.textView.selectedRange = newRange + + // append a space and trigger textView delegate update + DispatchQueue.main.async { + metaText.textView.insertText(" ") + } + } +} + +// MARK: - UICollectionViewDelegate +extension ComposeContentViewController: UICollectionViewDelegate { + + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription) + + switch collectionView { + case customEmojiPickerInputView.collectionView: + guard let diffableDataSource = viewModel.customEmojiPickerDiffableDataSource else { return } + let item = diffableDataSource.itemIdentifier(for: indexPath) + guard case let .emoji(attribute) = item else { return } + let emoji = attribute.emoji + + // make click sound + UIDevice.current.playInputClick() + + // retrieve active text input and insert emoji + // the trailing space is REQUIRED to make regex happy + _ = viewModel.customEmojiPickerInputViewModel.insertText(":\(emoji.shortcode): ") + default: + assertionFailure() + } + } // end func + +} + +// MARK: - ComposeContentViewModelDelegate +extension ComposeContentViewController: ComposeContentViewModelDelegate { + public func composeContentViewModel( + _ viewModel: ComposeContentViewModel, + handleAutoComplete info: ComposeContentViewModel.AutoCompleteInfo + ) -> Bool { + let snapshot = autoCompleteViewController.viewModel.diffableDataSource.snapshot() + guard let item = snapshot.itemIdentifiers.first else { return false } + + // FIXME: redundant code + guard let metaText = viewModel.contentMetaText else { return false } + guard let text = metaText.textView.text else { return false } + let _replacedText: String? = { + var text: String + switch item { + case .hashtag(let hashtag): + text = "#" + hashtag.name + case .hashtagV1(let hashtagName): + text = "#" + hashtagName + case .account(let account): + text = "@" + account.acct + case .emoji(let emoji): + text = ":" + emoji.shortcode + ":" + case .bottomLoader: + return nil + } + return text + }() + guard let replacedText = _replacedText else { return false } + + let range = NSRange(info.toHighlightEndRange, in: text) + metaText.textStorage.replaceCharacters(in: range, with: replacedText) + viewModel.autoCompleteInfo = nil + + // set selected range + let newRange = NSRange(location: range.location + (replacedText as NSString).length, length: 0) + guard metaText.textStorage.length <= newRange.location else { return true } + metaText.textView.selectedRange = newRange + + // append a space and trigger textView delegate update + DispatchQueue.main.async { + metaText.textView.insertText(" ") + } + + return true } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift index 3f6028b56..c8bf3ddc8 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift @@ -74,6 +74,7 @@ extension ComposeContentViewModel { } } +// MARK: - UITableViewDataSource extension ComposeContentViewModel: UITableViewDataSource { public func numberOfSections(in tableView: UITableView) -> Int { return Section.allCases.count @@ -99,3 +100,42 @@ extension ComposeContentViewModel: UITableViewDataSource { } } } + +extension ComposeContentViewModel { + + func setupCustomEmojiPickerDiffableDataSource( + collectionView: UICollectionView + ) { + let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource( + collectionView: collectionView, + context: context + ) + self.customEmojiPickerDiffableDataSource = diffableDataSource + + let domain = authContext.mastodonAuthenticationBox.domain.uppercased() + customEmojiViewModel?.emojis + .receive(on: DispatchQueue.main) + .sink { [weak self, weak diffableDataSource] emojis in + guard let _ = self else { return } + guard let diffableDataSource = diffableDataSource else { return } + + var snapshot = NSDiffableDataSourceSnapshot() + let customEmojiSection = CustomEmojiPickerSection.emoji(name: domain) + snapshot.appendSections([customEmojiSection]) + let items: [CustomEmojiPickerItem] = { + var items = [CustomEmojiPickerItem]() + for emoji in emojis where emoji.visibleInPicker { + let attribute = CustomEmojiPickerItem.CustomEmojiAttribute(emoji: emoji) + let item = CustomEmojiPickerItem.emoji(attribute: attribute) + items.append(item) + } + return items + }() + snapshot.appendItems(items, toSection: customEmojiSection) + + diffableDataSource.apply(snapshot) + } + .store(in: &disposeBag) + } + +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift index 80cc033e8..8a189739d 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift @@ -37,7 +37,7 @@ extension ComposeContentViewModel: MetaTextDelegate { let content = MastodonContent( content: textInput, - emojis: [:] // TODO: emojiViewModel?.emojis.asDictionary ?? [:] + emojis: [:] // customEmojiViewModel?.emojis.value.asDictionary ?? [:] ) let metaContent = MastodonMetaContent.convert(text: content) return metaContent @@ -48,7 +48,7 @@ extension ComposeContentViewModel: MetaTextDelegate { let content = MastodonContent( content: textInput, - emojis: [:] // emojiViewModel?.emojis.asDictionary ?? [:] + emojis: [:] // customEmojiViewModel?.emojis.value.asDictionary ?? [:] ) let metaContent = MastodonMetaContent.convert(text: content) return metaContent diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+UITextViewDelegate.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+UITextViewDelegate.swift index 5b5c018e2..cdf322a38 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+UITextViewDelegate.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+UITextViewDelegate.swift @@ -64,6 +64,12 @@ extension ComposeContentViewModel: UITextViewDelegate { public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { switch textView { case contentMetaText?.textView: + if text == " ", let autoCompleteInfo = self.autoCompleteInfo { + assert(delegate != nil) + let isHandled = delegate?.composeContentViewModel(self, handleAutoComplete: autoCompleteInfo) ?? false + return !isHandled + } + return true case contentWarningMetaText?.textView: let isReturn = text == "\n" diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index bf5143fe2..ad9dfa1d8 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -15,6 +15,10 @@ import MastodonMeta import MastodonCore import MastodonSDK +public protocol ComposeContentViewModelDelegate: AnyObject { + func composeContentViewModel(_ viewModel: ComposeContentViewModel, handleAutoComplete info: ComposeContentViewModel.AutoCompleteInfo) -> Bool +} + public final class ComposeContentViewModel: NSObject, ObservableObject { let logger = Logger(subsystem: "ComposeContentViewModel", category: "ViewModel") @@ -28,6 +32,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // input let context: AppContext let kind: Kind + weak var delegate: ComposeContentViewModelDelegate? @Published var viewLayoutFrame = ViewLayoutFrame() @@ -38,6 +43,9 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { @Published var autoCompleteRetryLayoutTimes = 0 @Published var autoCompleteInfo: AutoCompleteInfo? = nil + // emoji + var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource? + // output // limit @@ -46,8 +54,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // content public weak var contentMetaText: MetaText? { didSet { -// guard let textView = contentMetaText?.textView else { return } -// customEmojiPickerInputViewModel.configure(textInput: textView) + guard let textView = contentMetaText?.textView else { return } + customEmojiPickerInputViewModel.configure(textInput: textView) } } @Published public var initialContent = "" @@ -60,8 +68,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // content warning weak var contentWarningMetaText: MetaText? { didSet { - //guard let textView = contentWarningMetaText?.textView else { return } - //customEmojiPickerInputViewModel.configure(textInput: textView) + guard let textView = contentWarningMetaText?.textView else { return } + customEmojiPickerInputViewModel.configure(textInput: textView) } } @Published public var isContentWarningActive = false @@ -95,6 +103,9 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // emoji @Published var isEmojiActive = false + let customEmojiViewModel: EmojiService.CustomEmojiViewModel? + let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel() + @Published var isLoadingCustomEmoji = false // visibility @Published var visibility: Mastodon.Entity.Status.Visibility @@ -148,6 +159,9 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { } return visibility }() + self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel( + for: authContext.mastodonAuthenticationBox.domain + ) super.init() // end init @@ -192,6 +206,10 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { } } .store(in: &disposeBag) + + // bind emoji inputView + $isEmojiActive.assign(to: &customEmojiPickerInputViewModel.$isCustomEmojiComposing) + } deinit { @@ -215,7 +233,7 @@ extension ComposeContentViewModel { } extension ComposeContentViewModel { - struct AutoCompleteInfo { + public struct AutoCompleteInfo { // model let inputText: Substring // range diff --git a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerHeaderCollectionReusableView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerHeaderCollectionReusableView.swift similarity index 100% rename from Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerHeaderCollectionReusableView.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerHeaderCollectionReusableView.swift diff --git a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerInputView.swift similarity index 100% rename from Mastodon/Scene/Compose/View/CustomEmojiPickerInputView.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerInputView.swift diff --git a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerInputViewModel.swift similarity index 52% rename from Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerInputViewModel.swift index 496c8191b..729524ce5 100644 --- a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerInputViewModel.swift @@ -9,7 +9,6 @@ import UIKit import Combine import MetaTextKit import MastodonCore -import MastodonUI final class CustomEmojiPickerInputViewModel { @@ -20,8 +19,7 @@ final class CustomEmojiPickerInputViewModel { // input weak var customEmojiPickerInputView: CustomEmojiPickerInputView? - // output - let isCustomEmojiComposing = CurrentValueSubject(false) + @Published var isCustomEmojiComposing = false } @@ -51,27 +49,28 @@ extension CustomEmojiPickerInputViewModel { for reference in customEmojiReplaceableTextInputReferences { guard let textInput = reference.value else { continue } guard textInput.isFirstResponder == true else { continue } - guard let selectedTextRange = textInput.selectedTextRange else { continue } + // guard let selectedTextRange = textInput.selectedTextRange else { continue } textInput.insertText(text) + // FIXME: inline emoji // due to insert text render as attachment // the cursor reset logic not works // hack with hard code +2 offset - assert(text.hasSuffix(": ")) - guard text.hasPrefix(":") && text.hasSuffix(": ") else { continue } - - if let _ = textInput as? MetaTextView { - if let newPosition = textInput.position(from: selectedTextRange.start, offset: 2) { - let newSelectedTextRange = textInput.textRange(from: newPosition, to: newPosition) - textInput.selectedTextRange = newSelectedTextRange - } - } else { - if let newPosition = textInput.position(from: selectedTextRange.start, offset: text.length) { - let newSelectedTextRange = textInput.textRange(from: newPosition, to: newPosition) - textInput.selectedTextRange = newSelectedTextRange - } - } + // assert(text.hasSuffix(": ")) + // guard text.hasPrefix(":") && text.hasSuffix(": ") else { continue } + // + // if let _ = textInput as? MetaTextView { + // if let newPosition = textInput.position(from: selectedTextRange.start, offset: 2) { + // let newSelectedTextRange = textInput.textRange(from: newPosition, to: newPosition) + // textInput.selectedTextRange = newSelectedTextRange + // } + // } else { + // if let newPosition = textInput.position(from: selectedTextRange.start, offset: text.length) { + // let newSelectedTextRange = textInput.textRange(from: newPosition, to: newPosition) + // textInput.selectedTextRange = newSelectedTextRange + // } + // } return reference } @@ -81,3 +80,16 @@ extension CustomEmojiPickerInputViewModel { } +extension CustomEmojiPickerInputViewModel { + public func configure(textInput: CustomEmojiReplaceableTextInput) { + $isCustomEmojiComposing + .receive(on: DispatchQueue.main) + .sink { [weak self] isCustomEmojiComposing in + guard let self = self else { return } + textInput.inputView = isCustomEmojiComposing ? self.customEmojiPickerInputView : nil + textInput.reloadInputViews() + self.append(customEmojiReplaceableTextInput: textInput) + } + .store(in: &disposeBag) + } +} diff --git a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerItemCollectionViewCell.swift similarity index 100% rename from Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerItemCollectionViewCell.swift diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift index 6dad1e19b..e5f9b56be 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift @@ -185,7 +185,7 @@ extension ComposeContentView { index: _index, deleteBackwardResponseTextFieldRelayDelegate: viewModel ) { textField in - // viewModel.customEmojiPickerInputViewModel.configure(textInput: textField) + viewModel.customEmojiPickerInputViewModel.configure(textInput: textField) } } if viewModel.maxPollOptionLimit != viewModel.pollOptions.count { From 929a27d572a8e33b913d3bd211628de51bc2499c Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 13 Nov 2022 22:08:26 +0800 Subject: [PATCH 147/224] feat: [WIP] restore publish button and compose pre-insert content --- Mastodon.xcodeproj/project.pbxproj | 8 - .../Scene/Compose/ComposeViewController.swift | 645 ++---------------- .../Compose/ComposeViewModel+DataSource.swift | 453 ------------ .../ComposeViewModel+PublishState.swift | 164 ----- Mastodon/Scene/Compose/ComposeViewModel.swift | 333 +-------- .../Attachment/AttachmentViewModel.swift | 1 - .../ComposeContentViewController.swift | 90 ++- .../ComposeContentViewModel.swift | 254 ++++++- .../Poll/PollOptionTextField.swift | 2 +- .../ComposeContentToolbarView+ViewModel.swift | 3 + .../ComposeContentToolbarView.swift | 12 + 11 files changed, 401 insertions(+), 1564 deletions(-) delete mode 100644 Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift delete mode 100644 Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift rename MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/{View => Toolbar}/ComposeContentToolbarView+ViewModel.swift (97%) rename MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/{View => Toolbar}/ComposeContentToolbarView.swift (87%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index e3c3c58d3..24d394497 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -253,7 +253,6 @@ DB64BA452851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */; }; DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */; }; DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB65C63627A2AF6C008BAC2E /* ReportItem.swift */; }; - DB66728C25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */; }; DB6746EB278ED8B0008A6B94 /* PollOptionView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */; }; DB6746ED278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6746EC278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift */; }; DB6746F0278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6746EF278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift */; }; @@ -333,7 +332,6 @@ DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */; }; DB98EB6B27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6A27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift */; }; DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; }; - DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */; }; DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; }; DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; }; DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; }; @@ -811,7 +809,6 @@ DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonAuthentication+Fetch.swift"; sourceTree = ""; }; DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Fetch.swift"; sourceTree = ""; }; DB65C63627A2AF6C008BAC2E /* ReportItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportItem.swift; sourceTree = ""; }; - DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+DataSource.swift"; sourceTree = ""; }; DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PollOptionView+Configuration.swift"; sourceTree = ""; }; DB6746EC278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoGenerateProtocolRelayDelegate.swift; sourceTree = ""; }; DB6746EF278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoGenerateProtocolDelegate.swift; sourceTree = ""; }; @@ -903,7 +900,6 @@ DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultActionTableViewCell.swift; sourceTree = ""; }; DB98EB6A27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsAppearanceTableViewCell+ViewModel.swift"; sourceTree = ""; }; DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentContainerView+EmptyStateView.swift"; sourceTree = ""; }; - DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+PublishState.swift"; sourceTree = ""; }; DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; @@ -2134,8 +2130,6 @@ DB789A2125F9F76D0071ACA0 /* CollectionViewCell */, DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */, DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */, - DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */, - DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */, ); path = Compose; sourceTree = ""; @@ -3183,7 +3177,6 @@ 0F1E2D0B2615C39400C38565 /* DoubleTitleLabelNavigationBarTitleView.swift in Sources */, DBDFF1902805543100557A48 /* DiscoveryPostsViewController.swift in Sources */, DB697DD9278F4CED004EF2F7 /* HomeTimelineViewController+DataSourceProvider.swift in Sources */, - DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */, DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */, 2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */, DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */, @@ -3285,7 +3278,6 @@ DB63F7492799126300455B82 /* FollowerListViewController+DataSourceProvider.swift in Sources */, 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */, 2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */, - DB66728C25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift in Sources */, DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */, DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */, 5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */, diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index f23e44e5f..33eabd721 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -86,22 +86,6 @@ final class ComposeViewController: UIViewController, NeedsDependency { publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled) publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal) } - -// var systemKeyboardHeight: CGFloat = .zero { -// didSet { -// // note: some system AutoLayout warning here -// let height = max(300, systemKeyboardHeight) -// customEmojiPickerInputView.frame.size.height = height -// } -// } -// - -// -// let composeToolbarView = ComposeToolbarView() -// var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint! -// let composeToolbarBackgroundView = UIView() -// -// deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -155,132 +139,30 @@ extension ComposeViewController { ]) composeContentViewController.didMove(toParent: self) -// configureNavigationBarTitleStyle() -// viewModel.traitCollectionDidChangePublisher -// .receive(on: DispatchQueue.main) -// .sink { [weak self] _ in -// guard let self = self else { return } -// self.configureNavigationBarTitleStyle() -// } -// .store(in: &disposeBag) -// -// viewModel.$title -// .receive(on: DispatchQueue.main) -// .sink { [weak self] title in -// guard let self = self else { return } -// self.title = title -// } -// .store(in: &disposeBag) -// -// composeToolbarView.translatesAutoresizingMaskIntoConstraints = false -// view.addSubview(composeToolbarView) -// composeToolbarViewBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: composeToolbarView.bottomAnchor) -// NSLayoutConstraint.activate([ -// composeToolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), -// composeToolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor), -// composeToolbarViewBottomLayoutConstraint, -// composeToolbarView.heightAnchor.constraint(equalToConstant: ComposeToolbarView.toolbarHeight), -// ]) -// composeToolbarView.preservesSuperviewLayoutMargins = true -// composeToolbarView.delegate = self -// -// composeToolbarBackgroundView.translatesAutoresizingMaskIntoConstraints = false -// view.insertSubview(composeToolbarBackgroundView, belowSubview: composeToolbarView) -// NSLayoutConstraint.activate([ -// composeToolbarBackgroundView.topAnchor.constraint(equalTo: composeToolbarView.topAnchor), -// composeToolbarBackgroundView.leadingAnchor.constraint(equalTo: composeToolbarView.leadingAnchor), -// composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor), -// view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor), -// ]) + // bind navigation bar style + configureNavigationBarTitleStyle() + viewModel.traitCollectionDidChangePublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + self.configureNavigationBarTitleStyle() + } + .store(in: &disposeBag) -// tableView.delegate = self -// viewModel.setupDataSource( -// tableView: tableView, -// metaTextDelegate: self, -// metaTextViewDelegate: self, -// customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel, -// composeStatusAttachmentCollectionViewCellDelegate: self, -// composeStatusPollOptionCollectionViewCellDelegate: self, -// composeStatusPollOptionAppendEntryCollectionViewCellDelegate: self, -// composeStatusPollExpiresOptionCollectionViewCellDelegate: self -// ) + // bind title + viewModel.$title + .receive(on: DispatchQueue.main) + .sink { [weak self] title in + guard let self = self else { return } + self.title = title + } + .store(in: &disposeBag) -// viewModel.composeStatusAttribute.$composeContent -// .removeDuplicates() -// .receive(on: DispatchQueue.main) -// .sink { [weak self] _ in -// guard let self = self else { return } -// guard self.view.window != nil else { return } -// UIView.performWithoutAnimation { -// self.tableView.beginUpdates() -// self.tableView.setNeedsLayout() -// self.tableView.layoutIfNeeded() -// self.tableView.endUpdates() -// } -// } -// .store(in: &disposeBag) - -// viewModel.composeStatusContentTableViewCell.delegate = self -// -// // update layout when keyboard show/dismiss -// view.layoutIfNeeded() -// -// // bind publish bar button state -// viewModel.$isPublishBarButtonItemEnabled -// .receive(on: DispatchQueue.main) -// .assign(to: \.isEnabled, on: publishButton) -// .store(in: &disposeBag) -// -// // bind media button toolbar state -// viewModel.$isMediaToolbarButtonEnabled -// .receive(on: DispatchQueue.main) -// .sink { [weak self] isMediaToolbarButtonEnabled in -// guard let self = self else { return } -// self.composeToolbarView.mediaBarButtonItem.isEnabled = isMediaToolbarButtonEnabled -// self.composeToolbarView.mediaButton.isEnabled = isMediaToolbarButtonEnabled -// } -// .store(in: &disposeBag) -// -// // bind poll button toolbar state -// viewModel.$isPollToolbarButtonEnabled -// .receive(on: DispatchQueue.main) -// .sink { [weak self] isPollToolbarButtonEnabled in -// guard let self = self else { return } -// self.composeToolbarView.pollBarButtonItem.isEnabled = isPollToolbarButtonEnabled -// self.composeToolbarView.pollButton.isEnabled = isPollToolbarButtonEnabled -// } -// .store(in: &disposeBag) -// -// Publishers.CombineLatest( -// viewModel.$isPollComposing, -// viewModel.$isPollToolbarButtonEnabled -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] isPollComposing, isPollToolbarButtonEnabled in -// guard let self = self else { return } -// guard isPollToolbarButtonEnabled else { -// let accessibilityLabel = L10n.Scene.Compose.Accessibility.appendPoll -// self.composeToolbarView.pollBarButtonItem.accessibilityLabel = accessibilityLabel -// self.composeToolbarView.pollButton.accessibilityLabel = accessibilityLabel -// return -// } -// let accessibilityLabel = isPollComposing ? L10n.Scene.Compose.Accessibility.removePoll : L10n.Scene.Compose.Accessibility.appendPoll -// self.composeToolbarView.pollBarButtonItem.accessibilityLabel = accessibilityLabel -// self.composeToolbarView.pollButton.accessibilityLabel = accessibilityLabel -// } -// .store(in: &disposeBag) -// -// // bind image picker toolbar state -// viewModel.$attachmentServices -// .receive(on: DispatchQueue.main) -// .sink { [weak self] attachmentServices in -// guard let self = self else { return } -// let isEnabled = attachmentServices.count < self.viewModel.maxMediaAttachments -// self.composeToolbarView.mediaBarButtonItem.isEnabled = isEnabled -// self.composeToolbarView.mediaButton.isEnabled = isEnabled -// self.resetImagePicker() -// } -// .store(in: &disposeBag) + // bind publish bar button state + composeContentViewModel.$isPublishBarButtonItemEnabled + .receive(on: DispatchQueue.main) + .assign(to: \.isEnabled, on: publishButton) + .store(in: &disposeBag) // // // bind content warning button state // viewModel.$isContentWarningComposing @@ -292,72 +174,7 @@ extension ComposeViewController { // self.composeToolbarView.contentWarningButton.accessibilityLabel = accessibilityLabel // } // .store(in: &disposeBag) -// -// // bind visibility toolbar UI -// Publishers.CombineLatest( -// viewModel.$selectedStatusVisibility, -// viewModel.traitCollectionDidChangePublisher -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] type, _ in -// guard let self = self else { return } -// let image = type.image(interfaceStyle: self.traitCollection.userInterfaceStyle) -// self.composeToolbarView.visibilityBarButtonItem.image = image -// self.composeToolbarView.visibilityButton.setImage(image, for: .normal) -// self.composeToolbarView.activeVisibilityType.value = type -// } -// .store(in: &disposeBag) -// -// viewModel.$characterCount -// .receive(on: DispatchQueue.main) -// .sink { [weak self] characterCount in -// guard let self = self else { return } -// let count = self.viewModel.composeContentLimit - characterCount -// self.composeToolbarView.characterCountLabel.text = "\(count)" -// self.characterCountLabel.text = "\(count)" -// let font: UIFont -// let textColor: UIColor -// let accessibilityLabel: String -// switch count { -// case _ where count < 0: -// font = .monospacedDigitSystemFont(ofSize: 24, weight: .bold) -// textColor = Asset.Colors.danger.color -// accessibilityLabel = L10n.A11y.Plural.Count.inputLimitExceeds(abs(count)) -// default: -// font = .monospacedDigitSystemFont(ofSize: 15, weight: .regular) -// textColor = Asset.Colors.Label.secondary.color -// accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(count) -// } -// self.composeToolbarView.characterCountLabel.font = font -// self.composeToolbarView.characterCountLabel.textColor = textColor -// self.composeToolbarView.characterCountLabel.accessibilityLabel = accessibilityLabel -// self.characterCountLabel.font = font -// self.characterCountLabel.textColor = textColor -// self.characterCountLabel.accessibilityLabel = accessibilityLabel -// self.characterCountLabel.sizeToFit() -// } -// .store(in: &disposeBag) -// -// configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar.value) -// Publishers.CombineLatest( -// keyboardHasShortcutBar, -// viewModel.traitCollectionDidChangePublisher -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] keyboardHasShortcutBar, _ in -// guard let self = self else { return } -// self.configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar) -// } -// .store(in: &disposeBag) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - -// // update MetaText without trigger call underlaying `UITextStorage.processEditing` -// _ = textEditorView.processEditing(textEditorView.textStorage) - -// markTextEditorViewBecomeFirstResponser() + } override func viewDidAppear(_ animated: Bool) { @@ -369,102 +186,27 @@ extension ComposeViewController { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) -// configurePublishButtonApperance() -// viewModel.traitCollectionDidChangePublisher.send() + configurePublishButtonApperance() + viewModel.traitCollectionDidChangePublisher.send() } } -//extension ComposeViewController { -// -// private var textEditorView: MetaText { -// return viewModel.composeStatusContentTableViewCell.metaText -// } -// -// private func markTextEditorViewBecomeFirstResponser() { -// textEditorView.textView.becomeFirstResponder() -// } -// -// private func contentWarningEditorTextView() -> UITextView? { -// viewModel.composeStatusContentTableViewCell.statusContentWarningEditorView.textView -// } -// -// private func pollOptionCollectionViewCell(of item: ComposeStatusPollItem) -> ComposeStatusPollOptionCollectionViewCell? { -// guard case .pollOption = item else { return nil } -// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil } -// guard let indexPath = dataSource.indexPath(for: item), -// let cell = viewModel.composeStatusPollTableViewCell.collectionView.cellForItem(at: indexPath) as? ComposeStatusPollOptionCollectionViewCell else { -// return nil -// } -// -// return cell -// } -// -// private func firstPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? { -// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil } -// let items = dataSource.snapshot().itemIdentifiers(inSection: .main) -// let firstPollItem = items.first { item -> Bool in -// guard case .pollOption = item else { return false } -// return true -// } -// -// guard let item = firstPollItem else { -// return nil -// } -// -// return pollOptionCollectionViewCell(of: item) -// } -// -// private func lastPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? { -// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil } -// let items = dataSource.snapshot().itemIdentifiers(inSection: .main) -// let lastPollItem = items.last { item -> Bool in -// guard case .pollOption = item else { return false } -// return true -// } -// -// guard let item = lastPollItem else { -// return nil -// } -// -// return pollOptionCollectionViewCell(of: item) -// } -// -// private func markFirstPollOptionCollectionViewCellBecomeFirstResponser() { -// guard let cell = firstPollOptionCollectionViewCell() else { return } -// cell.pollOptionView.optionTextField.becomeFirstResponder() -// } -// -// private func markLastPollOptionCollectionViewCellBecomeFirstResponser() { -// guard let cell = lastPollOptionCollectionViewCell() else { return } -// cell.pollOptionView.optionTextField.becomeFirstResponder() -// } -// -// private func showDismissConfirmAlertController() { -// let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) -// let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { [weak self] _ in -// guard let self = self else { return } -// self.dismiss(animated: true, completion: nil) -// } -// alertController.addAction(discardAction) -// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel) -// alertController.addAction(cancelAction) -// alertController.popoverPresentationController?.barButtonItem = cancelBarButtonItem -// present(alertController, animated: true, completion: nil) -// } -// -// private func resetImagePicker() { -// let selectionLimit = max(1, viewModel.maxMediaAttachments - viewModel.attachmentServices.count) -// let configuration = ComposeViewController.createPhotoLibraryPickerConfiguration(selectionLimit: selectionLimit) -// photoLibraryPicker = createImagePicker(configuration: configuration) -// } -// -// private func createImagePicker(configuration: PHPickerConfiguration) -> PHPickerViewController { -// let imagePicker = PHPickerViewController(configuration: configuration) -// imagePicker.delegate = self -// return imagePicker -// } -// +extension ComposeViewController { + + private func showDismissConfirmAlertController() { + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { [weak self] _ in + guard let self = self else { return } + self.dismiss(animated: true, completion: nil) + } + alertController.addAction(discardAction) + let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel) + alertController.addAction(cancelAction) + alertController.popoverPresentationController?.barButtonItem = cancelBarButtonItem + present(alertController, animated: true, completion: nil) + } + // private func setupBackgroundColor(theme: Theme) { // let backgroundColor = UIColor(dynamicProvider: { traitCollection in // switch traitCollection.userInterfaceStyle { @@ -503,46 +245,40 @@ extension ComposeViewController { // } // } // -// private func configureNavigationBarTitleStyle() { -// switch traitCollection.userInterfaceIdiom { -// case .pad: -// navigationController?.navigationBar.prefersLargeTitles = traitCollection.horizontalSizeClass == .regular -// default: -// break -// } -// } -// -//} -// + private func configureNavigationBarTitleStyle() { + switch traitCollection.userInterfaceIdiom { + case .pad: + navigationController?.navigationBar.prefersLargeTitles = traitCollection.horizontalSizeClass == .regular + default: + break + } + } + +} + extension ComposeViewController { @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") -// guard viewModel.shouldDismiss else { -// showDismissConfirmAlertController() -// return -// } + guard composeContentViewModel.shouldDismiss else { + showDismissConfirmAlertController() + return + } dismiss(animated: true, completion: nil) } @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) -// do { -// try viewModel.checkAttachmentPrecondition() -// } catch { -// let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) -// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) -// alertController.addAction(okAction) -// coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil)) -// return -// } -// guard viewModel.publishStateMachine.enter(ComposeViewModel.PublishState.Publishing.self) else { -// // TODO: handle error -// return -// } - - // context.statusPublishService.publish(composeViewModel: viewModel) + do { + try composeContentViewModel.checkAttachmentPrecondition() + } catch { + let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) + alertController.addAction(okAction) + coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil)) + return + } do { let statusPublisher = try composeContentViewModel.statusPublisher() @@ -565,111 +301,6 @@ extension ComposeViewController { } -//// MARK: - MetaTextDelegate -//extension ComposeViewController: MetaTextDelegate { -// func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? { -// let string = metaText.textStorage.string -// let content = MastodonContent( -// content: string, -// emojis: viewModel.customEmojiViewModel?.emojiMapping.value ?? [:] -// ) -// let metaContent = MastodonMetaContent.convert(text: content) -// return metaContent -// } -//} -// -//// MARK: - UITextViewDelegate -//extension ComposeViewController: UITextViewDelegate { -// -// func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { -// setupInputAssistantItem(item: textView.inputAssistantItem) -// return true -// } -// - -// - -// - -// -// func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { -// switch textView { -// case textEditorView.textView: -// return false -// default: -// return true -// } -// } -// -// func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { -// switch textView { -// case textEditorView.textView: -// return false -// default: -// return true -// } -// } -// -//} -// -//// MARK: - ComposeToolbarViewDelegate -//extension ComposeViewController: ComposeToolbarViewDelegate { - -// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, pollButtonDidPressed sender: Any) { -// // toggle poll composing state -// viewModel.isPollComposing.toggle() -// -// // cancel custom picker input -// viewModel.isCustomEmojiComposing = false -// -// // setup initial poll option if needs -// if viewModel.isPollComposing, viewModel.pollOptionAttributes.isEmpty { -// viewModel.pollOptionAttributes = [ComposeStatusPollItem.PollOptionAttribute(), ComposeStatusPollItem.PollOptionAttribute()] -// } -// -// if viewModel.isPollComposing { -// // Magic RunLoop -// DispatchQueue.main.async { -// self.markFirstPollOptionCollectionViewCellBecomeFirstResponser() -// } -// } else { -// markTextEditorViewBecomeFirstResponser() -// } -// } -// -// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: Any) { -// viewModel.isCustomEmojiComposing.toggle() -// } -// -// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: Any) { -// // cancel custom picker input -// viewModel.isCustomEmojiComposing = false -// -// // restore first responder for text editor when content warning dismiss -// if viewModel.isContentWarningComposing { -// if contentWarningEditorTextView()?.isFirstResponder == true { -// markTextEditorViewBecomeFirstResponser() -// } -// } -// -// // toggle composing status -// viewModel.isContentWarningComposing.toggle() -// -// // active content warning after toggled -// if viewModel.isContentWarningComposing { -// contentWarningEditorTextView()?.becomeFirstResponder() -// } -// } -// -// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: Any, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) { -// viewModel.selectedStatusVisibility = type -// } -// -//} - -//// MARK: - UITableViewDelegate -//extension ComposeViewController: UITableViewDelegate { } - // MARK: - UIAdaptivePresentationControllerDelegate extension ComposeViewController: UIAdaptivePresentationControllerDelegate { @@ -681,15 +312,15 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate { return .pageSheet } } - -// func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { -// return viewModel.shouldDismiss -// } -// func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) { -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) -// showDismissConfirmAlertController() -// } + func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { + return composeContentViewModel.shouldDismiss + } + + func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + showDismissConfirmAlertController() + } func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -697,138 +328,6 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate { } -//// MARK: - ComposeStatusAttachmentTableViewCellDelegate -//extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelegate { -// -// func composeStatusAttachmentCollectionViewCell(_ cell: ComposeStatusAttachmentCollectionViewCell, removeButtonDidPressed button: UIButton) { -// guard let diffableDataSource = viewModel.composeStatusAttachmentTableViewCell.dataSource else { return } -// guard let indexPath = viewModel.composeStatusAttachmentTableViewCell.collectionView.indexPath(for: cell) else { return } -// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } -// guard case let .attachment(attachmentService) = item else { return } -// -// var attachmentServices = viewModel.attachmentServices -// guard let index = attachmentServices.firstIndex(of: attachmentService) else { return } -// let removedItem = attachmentServices[index] -// attachmentServices.remove(at: index) -// viewModel.attachmentServices = attachmentServices -// -// // cancel task -// removedItem.disposeBag.removeAll() -// } -// -//} -// -//// MARK: - ComposeStatusPollOptionCollectionViewCellDelegate -//extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelegate { -// -// func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textFieldDidBeginEditing textField: UITextField) { -// -// setupInputAssistantItem(item: textField.inputAssistantItem) -// -// // FIXME: make poll section visible -// // DispatchQueue.main.async { -// // self.collectionView.scroll(to: .bottom, animated: true) -// // } -// } -// -// -// // handle delete backward event for poll option input -// func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?) { -// guard (text ?? "").isEmpty else { return } -// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return } -// guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return } -// guard let item = dataSource.itemIdentifier(for: indexPath) else { return } -// guard case let .pollOption(attribute) = item else { return } -// -// var pollAttributes = viewModel.pollOptionAttributes -// guard let index = pollAttributes.firstIndex(of: attribute) else { return } -// -// // mark previous (fallback to next) item of removed middle poll option become first responder -// let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main) -// if let indexOfItem = pollItems.firstIndex(of: item), index > 0 { -// func cellBeforeRemoved() -> ComposeStatusPollOptionCollectionViewCell? { -// guard index > 0 else { return nil } -// let indexBeforeRemoved = pollItems.index(before: indexOfItem) -// let itemBeforeRemoved = pollItems[indexBeforeRemoved] -// return pollOptionCollectionViewCell(of: itemBeforeRemoved) -// } -// -// func cellAfterRemoved() -> ComposeStatusPollOptionCollectionViewCell? { -// guard index < pollItems.count - 1 else { return nil } -// let indexAfterRemoved = pollItems.index(after: index) -// let itemAfterRemoved = pollItems[indexAfterRemoved] -// return pollOptionCollectionViewCell(of: itemAfterRemoved) -// } -// -// var cell: ComposeStatusPollOptionCollectionViewCell? = cellBeforeRemoved() -// if cell == nil { -// cell = cellAfterRemoved() -// } -// cell?.pollOptionView.optionTextField.becomeFirstResponder() -// } -// -// guard pollAttributes.count > 2 else { -// return -// } -// pollAttributes.remove(at: index) -// -// // update data source -// viewModel.pollOptionAttributes = pollAttributes -// } -// -// // handle keyboard return event for poll option input -// func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField) { -// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return } -// guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return } -// let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main).filter { item in -// guard case .pollOption = item else { return false } -// return true -// } -// guard let item = dataSource.itemIdentifier(for: indexPath) else { return } -// guard let index = pollItems.firstIndex(of: item) else { return } -// -// if index == pollItems.count - 1 { -// // is the last -// viewModel.createNewPollOptionIfPossible() -// DispatchQueue.main.async { -// self.markLastPollOptionCollectionViewCellBecomeFirstResponser() -// } -// } else { -// // not the last -// let indexAfter = pollItems.index(after: index) -// let itemAfter = pollItems[indexAfter] -// let cell = pollOptionCollectionViewCell(of: itemAfter) -// cell?.pollOptionView.optionTextField.becomeFirstResponder() -// } -// } -// -//} -// -//// MARK: - ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate -//extension ComposeViewController: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate { -// func composeStatusPollOptionAppendEntryCollectionViewCellDidPressed(_ cell: ComposeStatusPollOptionAppendEntryCollectionViewCell) { -// viewModel.createNewPollOptionIfPossible() -// DispatchQueue.main.async { -// self.markLastPollOptionCollectionViewCellBecomeFirstResponser() -// } -// } -//} -// -//// MARK: - ComposeStatusPollExpiresOptionCollectionViewCellDelegate -//extension ComposeViewController: ComposeStatusPollExpiresOptionCollectionViewCellDelegate { -// func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption) { -// viewModel.pollExpiresOptionAttribute.expiresOption.value = expiresOption -// } -//} -// -//// MARK: - ComposeStatusContentTableViewCellDelegate -//extension ComposeViewController: ComposeStatusContentTableViewCellDelegate { -// func composeStatusContentTableViewCell(_ cell: ComposeStatusContentTableViewCell, textViewShouldBeginEditing textView: UITextView) -> Bool { -// setupInputAssistantItem(item: textView.inputAssistantItem) -// return true -// } -//} - //extension ComposeViewController { // override var keyCommands: [UIKeyCommand]? { // composeKeyCommands diff --git a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift b/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift deleted file mode 100644 index 5ecba3791..000000000 --- a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift +++ /dev/null @@ -1,453 +0,0 @@ -// -// ComposeViewModel+Diffable.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-3-11. -// - -import os.log -import UIKit -import Combine -import CoreDataStack -import MetaTextKit -import MastodonMeta -import MastodonAsset -import MastodonCore -import MastodonLocalization -import MastodonSDK - -extension ComposeViewModel { - -// func setupDataSource( -// tableView: UITableView, -// metaTextDelegate: MetaTextDelegate, -// metaTextViewDelegate: UITextViewDelegate, -// customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel, -// composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate, -// composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate, -// composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate, -// composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate -// ) { -// // UI -// bind() -// -// // content -// bind(cell: composeStatusContentTableViewCell, tableView: tableView) -// composeStatusContentTableViewCell.metaText.delegate = metaTextDelegate -// composeStatusContentTableViewCell.metaText.textView.delegate = metaTextViewDelegate -// -// // attachment -// bind(cell: composeStatusAttachmentTableViewCell, tableView: tableView) -// composeStatusAttachmentTableViewCell.composeStatusAttachmentCollectionViewCellDelegate = composeStatusAttachmentCollectionViewCellDelegate -// -// // poll -// bind(cell: composeStatusPollTableViewCell, tableView: tableView) -// composeStatusPollTableViewCell.delegate = self -// composeStatusPollTableViewCell.customEmojiPickerInputViewModel = customEmojiPickerInputViewModel -// composeStatusPollTableViewCell.composeStatusPollOptionCollectionViewCellDelegate = composeStatusPollOptionCollectionViewCellDelegate -// composeStatusPollTableViewCell.composeStatusPollOptionAppendEntryCollectionViewCellDelegate = composeStatusPollOptionAppendEntryCollectionViewCellDelegate -// composeStatusPollTableViewCell.composeStatusPollExpiresOptionCollectionViewCellDelegate = composeStatusPollExpiresOptionCollectionViewCellDelegate -// -// // setup data source -// tableView.dataSource = self -// } - -} - -//// MARK: - UITableViewDataSource -//extension ComposeViewModel: UITableViewDataSource { - -// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { -// switch Section.allCases[indexPath.section] { -// case .repliedTo: -// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentTableViewCell -// guard case let .reply(record) = composeKind else { return cell } -// -// // bind frame publisher -// cell.framePublisher -// .receive(on: DispatchQueue.main) -// .assign(to: \.repliedToCellFrame, on: self) -// .store(in: &cell.disposeBag) -// -// // set initial width -// if cell.statusView.frame.width == .zero { -// cell.statusView.frame.size.width = tableView.frame.width -// } -// -// // configure status -// context.managedObjectContext.performAndWait { -// guard let replyTo = record.object(in: context.managedObjectContext) else { return } -// cell.statusView.configure(status: replyTo) -// } -// -// return cell -// case .status: -// return composeStatusContentTableViewCell -// case .attachment: -// return composeStatusAttachmentTableViewCell -// case .poll: -// return composeStatusPollTableViewCell -// } -// } -//} - -//// MARK: - ComposeStatusPollTableViewCellDelegate -//extension ComposeViewModel: ComposeStatusPollTableViewCellDelegate { -// func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute]) { -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) -// -// self.pollOptionAttributes = options -// } -//} -// -//extension ComposeViewModel { -// private func bind() { -// $isCustomEmojiComposing -// .assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing) -// .store(in: &disposeBag) -// -// $isContentWarningComposing -// .assign(to: \.isContentWarningComposing, on: composeStatusAttribute) -// .store(in: &disposeBag) -// -// // bind compose toolbar UI state -// Publishers.CombineLatest( -// $isPollComposing, -// $attachmentServices -// ) -// .receive(on: DispatchQueue.main) -// .sink(receiveValue: { [weak self] isPollComposing, attachmentServices in -// guard let self = self else { return } -// let shouldMediaDisable = isPollComposing || attachmentServices.count >= self.maxMediaAttachments -// let shouldPollDisable = attachmentServices.count > 0 -// -// self.isMediaToolbarButtonEnabled = !shouldMediaDisable -// self.isPollToolbarButtonEnabled = !shouldPollDisable -// }) -// .store(in: &disposeBag) -// -// // calculate `Idempotency-Key` -// let content = Publishers.CombineLatest3( -// composeStatusAttribute.$isContentWarningComposing, -// composeStatusAttribute.$contentWarningContent, -// composeStatusAttribute.$composeContent -// ) -// .map { isContentWarningComposing, contentWarningContent, composeContent -> String in -// if isContentWarningComposing { -// return contentWarningContent + (composeContent ?? "") -// } else { -// return composeContent ?? "" -// } -// } -// let attachmentIDs = $attachmentServices.map { attachments -> String in -// let attachmentIDs = attachments.compactMap { $0.attachment.value?.id } -// return attachmentIDs.joined(separator: ",") -// } -// let pollOptionsAndDuration = Publishers.CombineLatest3( -// $isPollComposing, -// $pollOptionAttributes, -// pollExpiresOptionAttribute.expiresOption -// ) -// .map { isPollComposing, pollOptionAttributes, expiresOption -> String in -// guard isPollComposing else { -// return "" -// } -// -// let pollOptions = pollOptionAttributes.map { $0.option.value }.joined(separator: ",") -// return pollOptions + expiresOption.rawValue -// } -// -// Publishers.CombineLatest4( -// content, -// attachmentIDs, -// pollOptionsAndDuration, -// $selectedStatusVisibility -// ) -// .map { content, attachmentIDs, pollOptionsAndDuration, selectedStatusVisibility -> String in -// var hasher = Hasher() -// hasher.combine(content) -// hasher.combine(attachmentIDs) -// hasher.combine(pollOptionsAndDuration) -// hasher.combine(selectedStatusVisibility.visibility.rawValue) -// let hashValue = hasher.finalize() -// return "\(hashValue)" -// } -// .assign(to: \.value, on: idempotencyKey) -// .store(in: &disposeBag) -// -// // bind modal dismiss state -// composeStatusAttribute.$composeContent -// .receive(on: DispatchQueue.main) -// .map { [weak self] content in -// let content = content ?? "" -// if content.isEmpty { -// return true -// } -// // if preInsertedContent plus a space is equal to the content, simply dismiss the modal -// if let preInsertedContent = self?.preInsertedContent { -// return content == preInsertedContent -// } -// return false -// } -// .assign(to: &$shouldDismiss) -// -// // bind compose bar button item UI state -// let isComposeContentEmpty = composeStatusAttribute.$composeContent -// .map { ($0 ?? "").isEmpty } -// let isComposeContentValid = $characterCount -// .compactMap { [weak self] characterCount -> Bool in -// guard let self = self else { return characterCount <= 500 } -// return characterCount <= self.composeContentLimit -// } -// let isMediaEmpty = $attachmentServices -// .map { $0.isEmpty } -// let isMediaUploadAllSuccess = $attachmentServices -// .map { services in -// services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish } -// } -// let isPollAttributeAllValid = $pollOptionAttributes -// .map { pollAttributes in -// pollAttributes.allSatisfy { attribute -> Bool in -// !attribute.option.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty -// } -// } -// -// let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4( -// isComposeContentEmpty, -// isComposeContentValid, -// isMediaEmpty, -// isMediaUploadAllSuccess -// ) -// .map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in -// if isMediaEmpty { -// return isComposeContentValid && !isComposeContentEmpty -// } else { -// return isComposeContentValid && isMediaUploadAllSuccess -// } -// } -// .eraseToAnyPublisher() -// -// let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4( -// isComposeContentEmpty, -// isComposeContentValid, -// $isPollComposing, -// isPollAttributeAllValid -// ) -// .map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollAttributeAllValid -> Bool in -// if isPollComposing { -// return isComposeContentValid && !isComposeContentEmpty && isPollAttributeAllValid -// } else { -// return isComposeContentValid && !isComposeContentEmpty -// } -// } -// .eraseToAnyPublisher() -// -// Publishers.CombineLatest( -// isPublishBarButtonItemEnabledPrecondition1, -// isPublishBarButtonItemEnabledPrecondition2 -// ) -// .map { $0 && $1 } -// .assign(to: &$isPublishBarButtonItemEnabled) -// } -//} -// -//extension ComposeViewModel { -// private func bind( -// cell: ComposeStatusContentTableViewCell, -// tableView: UITableView -// ) { -// // bind status content character count -// Publishers.CombineLatest3( -// composeStatusAttribute.$composeContent, -// composeStatusAttribute.$isContentWarningComposing, -// composeStatusAttribute.$contentWarningContent -// ) -// .map { composeContent, isContentWarningComposing, contentWarningContent -> Int in -// let composeContent = composeContent ?? "" -// var count = composeContent.count -// if isContentWarningComposing { -// count += contentWarningContent.count -// } -// return count -// } -// .assign(to: &$characterCount) -// -// // bind content warning -// composeStatusAttribute.$isContentWarningComposing -// .receive(on: DispatchQueue.main) -// .sink { [weak cell, weak tableView] isContentWarningComposing in -// guard let cell = cell else { return } -// guard let tableView = tableView else { return } -// -// // self size input cell -// cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing -// cell.statusContentWarningEditorView.alpha = 0 -// UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) { -// cell.statusContentWarningEditorView.alpha = 1 -// tableView.beginUpdates() -// tableView.endUpdates() -// } completion: { _ in -// // do nothing -// } -// } -// .store(in: &disposeBag) -// -// cell.contentWarningContent -// .removeDuplicates() -// .receive(on: DispatchQueue.main) -// .sink { [weak tableView, weak self] text in -// guard let self = self else { return } -// // bind input data -// self.composeStatusAttribute.contentWarningContent = text -// -// // self size input cell -// guard let tableView = tableView else { return } -// UIView.performWithoutAnimation { -// tableView.beginUpdates() -// tableView.endUpdates() -// } -// } -// .store(in: &cell.disposeBag) -// -// // configure custom emoji picker -// ComposeStatusSection.configureCustomEmojiPicker( -// viewModel: customEmojiPickerInputViewModel, -// customEmojiReplaceableTextInput: cell.metaText.textView, -// disposeBag: &disposeBag -// ) -// ComposeStatusSection.configureCustomEmojiPicker( -// viewModel: customEmojiPickerInputViewModel, -// customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView, -// disposeBag: &disposeBag -// ) -// } -//} -// -//extension ComposeViewModel { -// private func bind( -// cell: ComposeStatusPollTableViewCell, -// tableView: UITableView -// ) { -// Publishers.CombineLatest( -// $isPollComposing, -// $pollOptionAttributes -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] isPollComposing, pollOptionAttributes in -// guard let self = self else { return } -// guard self.isViewAppeared else { return } -// -// let cell = self.composeStatusPollTableViewCell -// guard let dataSource = cell.dataSource else { return } -// -// var snapshot = NSDiffableDataSourceSnapshot() -// snapshot.appendSections([.main]) -// var items: [ComposeStatusPollItem] = [] -// if isPollComposing { -// for attribute in pollOptionAttributes { -// items.append(.pollOption(attribute: attribute)) -// } -// if pollOptionAttributes.count < self.maxPollOptions { -// items.append(.pollOptionAppendEntry) -// } -// items.append(.pollExpiresOption(attribute: self.pollExpiresOptionAttribute)) -// } -// snapshot.appendItems(items, toSection: .main) -// -// tableView.performBatchUpdates { -// if #available(iOS 15.0, *) { -// dataSource.apply(snapshot, animatingDifferences: false) -// } else { -// dataSource.apply(snapshot, animatingDifferences: true) -// } -// } -// } -// .store(in: &disposeBag) -// -// // bind delegate -// $pollOptionAttributes -// .sink { [weak self] pollAttributes in -// guard let self = self else { return } -// pollAttributes.forEach { $0.delegate = self } -// } -// .store(in: &disposeBag) -// } -//} -// -//extension ComposeViewModel { -// private func bind( -// cell: ComposeStatusAttachmentTableViewCell, -// tableView: UITableView -// ) { -// cell.collectionViewHeightDidUpdate -// .receive(on: DispatchQueue.main) -// .sink { [weak self] _ in -// guard let _ = self else { return } -// tableView.beginUpdates() -// tableView.endUpdates() -// } -// .store(in: &disposeBag) -// -// $attachmentServices -// .removeDuplicates() -// .receive(on: DispatchQueue.main) -// .sink { [weak self] attachmentServices in -// guard let self = self else { return } -// guard self.isViewAppeared else { return } -// -// let cell = self.composeStatusAttachmentTableViewCell -// guard let dataSource = cell.dataSource else { return } -// -// var snapshot = NSDiffableDataSourceSnapshot() -// snapshot.appendSections([.main]) -// let items = attachmentServices.map { ComposeStatusAttachmentItem.attachment(attachmentService: $0) } -// snapshot.appendItems(items, toSection: .main) -// -// if #available(iOS 15.0, *) { -// dataSource.applySnapshotUsingReloadData(snapshot) -// } else { -// dataSource.apply(snapshot, animatingDifferences: false) -// } -// } -// .store(in: &disposeBag) -// -// // setup attribute updater -// $attachmentServices -// .receive(on: DispatchQueue.main) -// .debounce(for: 0.3, scheduler: DispatchQueue.main) -// .sink { attachmentServices in -// // drive service upload state -// // make image upload in the queue -// for attachmentService in attachmentServices { -// // skip when prefix N task when task finish OR fail OR uploading -// guard let currentState = attachmentService.uploadStateMachine.currentState else { break } -// if currentState is MastodonAttachmentService.UploadState.Fail { -// continue -// } -// if currentState is MastodonAttachmentService.UploadState.Finish { -// continue -// } -// if currentState is MastodonAttachmentService.UploadState.Processing { -// continue -// } -// if currentState is MastodonAttachmentService.UploadState.Uploading { -// break -// } -// // trigger uploading one by one -// if currentState is MastodonAttachmentService.UploadState.Initial { -// attachmentService.uploadStateMachine.enter(MastodonAttachmentService.UploadState.Uploading.self) -// break -// } -// } -// } -// .store(in: &disposeBag) -// -// // bind delegate -// $attachmentServices -// .sink { [weak self] attachmentServices in -// guard let self = self else { return } -// attachmentServices.forEach { $0.delegate = self } -// } -// .store(in: &disposeBag) -// } -//} diff --git a/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift b/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift deleted file mode 100644 index b9ed18c45..000000000 --- a/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift +++ /dev/null @@ -1,164 +0,0 @@ -// -// ComposeViewModel+PublishState.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-3-18. -// - -import os.log -import Foundation -import Combine -import CoreDataStack -import GameplayKit -import MastodonSDK - -//extension ComposeViewModel { -// class PublishState: GKState { -// weak var viewModel: ComposeViewModel? -// -// init(viewModel: ComposeViewModel) { -// self.viewModel = viewModel -// } -// -// override func didEnter(from previousState: GKState?) { -// os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription) -// viewModel?.publishStateMachinePublisher.value = self -// } -// } -//} - -//extension ComposeViewModel.PublishState { -// class Initial: ComposeViewModel.PublishState { -// override func isValidNextState(_ stateClass: AnyClass) -> Bool { -// return stateClass == Publishing.self -// } -// } -// -// class Publishing: ComposeViewModel.PublishState { -// -// var publishingSubscription: AnyCancellable? -// -// override func isValidNextState(_ stateClass: AnyClass) -> Bool { -// return stateClass == Fail.self || stateClass == Finish.self -// } -// -// override func didEnter(from previousState: GKState?) { -// super.didEnter(from: previousState) -// guard let viewModel = viewModel, let stateMachine = stateMachine else { return } -// -// viewModel.updatePublishDate() -// -// let authenticationBox = viewModel.authenticationBox -// let domain = authenticationBox.domain -// let attachmentServices = viewModel.attachmentServices -// let mediaIDs = attachmentServices.compactMap { attachmentService in -// attachmentService.attachment.value?.id -// } -// let pollOptions: [String]? = { -// guard viewModel.isPollComposing else { return nil } -// return viewModel.pollOptionAttributes.map { attribute in attribute.option.value } -// }() -// let pollExpiresIn: Int? = { -// guard viewModel.isPollComposing else { return nil } -// return viewModel.pollExpiresOptionAttribute.expiresOption.value.seconds -// }() -// let inReplyToID: Mastodon.Entity.Status.ID? = { -// guard case let .reply(status) = viewModel.composeKind else { return nil } -// var id: Mastodon.Entity.Status.ID? -// viewModel.context.managedObjectContext.performAndWait { -// guard let replyTo = status.object(in: viewModel.context.managedObjectContext) else { return } -// id = replyTo.id -// } -// return id -// }() -// let sensitive: Bool = viewModel.isContentWarningComposing -// let spoilerText: String? = { -// let text = viewModel.composeStatusAttribute.contentWarningContent.trimmingCharacters(in: .whitespacesAndNewlines) -// guard !text.isEmpty else { -// return nil -// } -// return text -// }() -// let visibility = viewModel.selectedStatusVisibility.visibility -// -// let updateMediaQuerySubscriptions: [AnyPublisher, Error>] = { -// var subscriptions: [AnyPublisher, Error>] = [] -// for attachmentService in attachmentServices { -// guard let attachmentID = attachmentService.attachment.value?.id else { continue } -// let description = attachmentService.description.value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" -// guard !description.isEmpty else { continue } -// let query = Mastodon.API.Media.UpdateMediaQuery( -// file: nil, -// thumbnail: nil, -// description: description, -// focus: nil -// ) -// let subscription = viewModel.context.apiService.updateMedia( -// domain: domain, -// attachmentID: attachmentID, -// query: query, -// mastodonAuthenticationBox: authenticationBox -// ) -// subscriptions.append(subscription) -// } -// return subscriptions -// }() -// -// let idempotencyKey = viewModel.idempotencyKey.value -// -// publishingSubscription = Publishers.MergeMany(updateMediaQuerySubscriptions) -// .collect() -// .asyncMap { attachments -> Mastodon.Response.Content in -// let query = Mastodon.API.Statuses.PublishStatusQuery( -// status: viewModel.composeStatusAttribute.composeContent, -// mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs, -// pollOptions: pollOptions, -// pollExpiresIn: pollExpiresIn, -// inReplyToID: inReplyToID, -// sensitive: sensitive, -// spoilerText: spoilerText, -// visibility: visibility -// ) -// return try await viewModel.context.apiService.publishStatus( -// domain: domain, -// idempotencyKey: idempotencyKey, -// query: query, -// authenticationBox: authenticationBox -// ) -// } -// .receive(on: DispatchQueue.main) -// .sink { completion in -// switch completion { -// case .failure(let error): -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) -// stateMachine.enter(Fail.self) -// case .finished: -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status success", ((#file as NSString).lastPathComponent), #line, #function) -// stateMachine.enter(Finish.self) -// } -// } receiveValue: { response in -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: status %s published: %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.id, response.value.uri) -// } -// } -// } -// -// class Fail: ComposeViewModel.PublishState { -// override func isValidNextState(_ stateClass: AnyClass) -> Bool { -// // allow discard publishing -// return stateClass == Publishing.self || stateClass == Discard.self -// } -// } -// -// class Discard: ComposeViewModel.PublishState { -// override func isValidNextState(_ stateClass: AnyClass) -> Bool { -// return false -// } -// } -// -// class Finish: ComposeViewModel.PublishState { -// override func isValidNextState(_ stateClass: AnyClass) -> Bool { -// return false -// } -// } -// -//} diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 5e5fbb1c3..bf234b095 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -18,7 +18,7 @@ import MastodonLocalization import MastodonMeta import MastodonUI -final class ComposeViewModel: NSObject { +final class ComposeViewModel { let logger = Logger(subsystem: "ComposeViewModel", category: "ViewModel") @@ -30,84 +30,13 @@ final class ComposeViewModel: NSObject { let context: AppContext let authContext: AuthContext let kind: ComposeContentViewModel.Kind - -// var authenticationBox: MastodonAuthenticationBox { -// authContext.mastodonAuthenticationBox -// } -// -// @Published var isPollComposing = false -// @Published var isCustomEmojiComposing = false -// @Published var isContentWarningComposing = false -// -// @Published var selectedStatusVisibility: ComposeToolbarView.VisibilitySelectionType -// @Published var repliedToCellFrame: CGRect = .zero let traitCollectionDidChangePublisher = CurrentValueSubject(Void()) // use CurrentValueSubject to make initial event emit -// var isViewAppeared = false // output -// let instanceConfiguration: Mastodon.Entity.Instance.Configuration? -// var composeContentLimit: Int { -// guard let maxCharacters = instanceConfiguration?.statuses?.maxCharacters else { return 500 } -// return max(1, maxCharacters) -// } -// var maxMediaAttachments: Int { -// guard let maxMediaAttachments = instanceConfiguration?.statuses?.maxMediaAttachments else { -// return 4 -// } -// // FIXME: update timeline media preview UI -// return min(4, max(1, maxMediaAttachments)) -// // return max(1, maxMediaAttachments) -// } -// var maxPollOptions: Int { -// guard let maxOptions = instanceConfiguration?.polls?.maxOptions else { return 4 } -// return max(2, maxOptions) -// } -// -// let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute() -// let composeStatusContentTableViewCell = ComposeStatusContentTableViewCell() -// let composeStatusAttachmentTableViewCell = ComposeStatusAttachmentTableViewCell() -// let composeStatusPollTableViewCell = ComposeStatusPollTableViewCell() -// -// // var dataSource: UITableViewDiffableDataSource? -// var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource? -// private(set) lazy var publishStateMachine: GKStateMachine = { -// // exclude timeline middle fetcher state -// let stateMachine = GKStateMachine(states: [ -// PublishState.Initial(viewModel: self), -// PublishState.Publishing(viewModel: self), -// PublishState.Fail(viewModel: self), -// PublishState.Discard(viewModel: self), -// PublishState.Finish(viewModel: self), -// ]) -// stateMachine.enter(PublishState.Initial.self) -// return stateMachine -// }() -// private(set) lazy var publishStateMachinePublisher = CurrentValueSubject(nil) -// private(set) var publishDate = Date() // update it when enter Publishing state -// -// // TODO: group post material into Hashable class -// var idempotencyKey = CurrentValueSubject(UUID().uuidString) -// -// // UI & UX -// @Published var title: String -// @Published var shouldDismiss = true -// @Published var isPublishBarButtonItemEnabled = false -// @Published var isMediaToolbarButtonEnabled = true -// @Published var isPollToolbarButtonEnabled = true -// @Published var characterCount = 0 -// @Published var collectionViewState: CollectionViewState = .fold -// -// // for hashtag: "# " -// // for mention: "@ " -// var preInsertedContent: String? -// -// // attachment -// @Published var attachmentServices: [MastodonAttachmentService] = [] -// -// // polls -// @Published var pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute] = [] -// let pollExpiresOptionAttribute = ComposeStatusPollItem.PollExpiresOptionAttribute() + + // UI & UX + @Published var title: String init( context: AppContext, @@ -117,63 +46,14 @@ final class ComposeViewModel: NSObject { self.context = context self.authContext = authContext self.kind = kind + // end init -// self.title = { -// switch composeKind { -// case .post, .hashtag, .mention: return L10n.Scene.Compose.Title.newPost -// case .reply: return L10n.Scene.Compose.Title.newReply -// } -// }() -// self.selectedStatusVisibility = { -// // default private when user locked -// var visibility: ComposeToolbarView.VisibilitySelectionType = { -// guard let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user -// else { -// return .public -// } -// return author.locked ? .private : .public -// }() -// // set visibility for reply post -// switch composeKind { -// case .reply(let record): -// context.managedObjectContext.performAndWait { -// guard let status = record.object(in: context.managedObjectContext) else { -// assertionFailure() -// return -// } -// let repliedStatusVisibility = status.visibility -// switch repliedStatusVisibility { -// case .public, .unlisted: -// // keep default -// break -// case .private: -// visibility = .private -// case .direct: -// visibility = .direct -// case ._other: -// assertionFailure() -// break -// } -// } -// default: -// break -// } -// return visibility -// }() -// // set limit -// self.instanceConfiguration = { -// var configuration: Mastodon.Entity.Instance.Configuration? = nil -// context.managedObjectContext.performAndWait { -// guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) else { return } -// configuration = authentication.instance?.configuration -// } -// return configuration -// }() -// self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authContext.mastodonAuthenticationBox.domain) -// super.init() -// // end init -// -// setup(cell: composeStatusContentTableViewCell) + self.title = { + switch kind { + case .post, .hashtag, .mention: return L10n.Scene.Compose.Title.newPost + case .reply: return L10n.Scene.Compose.Title.newReply + } + }() } deinit { @@ -181,194 +61,3 @@ final class ComposeViewModel: NSObject { } } - -extension ComposeViewModel { -// func createNewPollOptionIfPossible() { -// guard pollOptionAttributes.count < maxPollOptions else { return } -// -// let attribute = ComposeStatusPollItem.PollOptionAttribute() -// pollOptionAttributes = pollOptionAttributes + [attribute] -// } -// -// func updatePublishDate() { -// publishDate = Date() -// } -} - -//extension ComposeViewModel { -// -// enum AttachmentPrecondition: Error, LocalizedError { -// case videoAttachWithPhoto -// case moreThanOneVideo -// -// var errorDescription: String? { -// return L10n.Common.Alerts.PublishPostFailure.title -// } -// -// var failureReason: String? { -// switch self { -// case .videoAttachWithPhoto: -// return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.videoAttachWithPhoto -// case .moreThanOneVideo: -// return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.moreThanOneVideo -// } -// } -// } -// -// // check exclusive limit: -// // - up to 1 video -// // - up to N photos -// func checkAttachmentPrecondition() throws { -// let attachmentServices = self.attachmentServices -// guard !attachmentServices.isEmpty else { return } -// var photoAttachmentServices: [MastodonAttachmentService] = [] -// var videoAttachmentServices: [MastodonAttachmentService] = [] -// attachmentServices.forEach { service in -// guard let file = service.file.value else { -// assertionFailure() -// return -// } -// switch file { -// case .jpeg, .png, .gif: -// photoAttachmentServices.append(service) -// case .other: -// videoAttachmentServices.append(service) -// } -// } -// -// if !videoAttachmentServices.isEmpty { -// guard videoAttachmentServices.count == 1 else { -// throw AttachmentPrecondition.moreThanOneVideo -// } -// guard photoAttachmentServices.isEmpty else { -// throw AttachmentPrecondition.videoAttachWithPhoto -// } -// } -// } -// -//} -// -//// MARK: - MastodonAttachmentServiceDelegate -//extension ComposeViewModel: MastodonAttachmentServiceDelegate { -// func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) { -// // trigger new output event -// attachmentServices = attachmentServices -// } -//} -// -//// MARK: - ComposePollAttributeDelegate -//extension ComposeViewModel: ComposePollAttributeDelegate { -// func composePollAttribute(_ attribute: ComposeStatusPollItem.PollOptionAttribute, pollOptionDidChange: String?) { -// // trigger update -// pollOptionAttributes = pollOptionAttributes -// } -//} -// -//extension ComposeViewModel { -// private func setup( -// cell: ComposeStatusContentTableViewCell -// ) { -// setupStatusHeader(cell: cell) -// setupStatusAuthor(cell: cell) -// setupStatusContent(cell: cell) -// } -// -// private func setupStatusHeader( -// cell: ComposeStatusContentTableViewCell -// ) { -// // configure header -// let managedObjectContext = context.managedObjectContext -// managedObjectContext.performAndWait { -// guard case let .reply(record) = self.composeKind, -// let replyTo = record.object(in: managedObjectContext) -// else { -// cell.statusView.viewModel.header = .none -// return -// } -// -// let info: StatusView.ViewModel.Header.ReplyInfo -// do { -// let content = MastodonContent( -// content: replyTo.author.displayNameWithFallback, -// emojis: replyTo.author.emojis.asDictionary -// ) -// let metaContent = try MastodonMetaContent.convert(document: content) -// info = .init(header: metaContent) -// } catch { -// let metaContent = PlaintextMetaContent(string: replyTo.author.displayNameWithFallback) -// info = .init(header: metaContent) -// } -// cell.statusView.viewModel.header = .reply(info: info) -// } -// } -// -// private func setupStatusAuthor( -// cell: ComposeStatusContentTableViewCell -// ) { -// self.context.managedObjectContext.performAndWait { -// guard let author = authenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return } -// cell.statusView.configureAuthor(author: author) -// } -// } -// -// private func setupStatusContent( -// cell: ComposeStatusContentTableViewCell -// ) { -// switch composeKind { -// case .reply(let record): -// context.managedObjectContext.performAndWait { -// guard let status = record.object(in: context.managedObjectContext) else { return } -// let author = self.authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user -// -// var mentionAccts: [String] = [] -// if author?.id != status.author.id { -// mentionAccts.append("@" + status.author.acct) -// } -// let mentions = status.mentions -// .filter { author?.id != $0.id } -// for mention in mentions { -// let acct = "@" + mention.acct -// guard !mentionAccts.contains(acct) else { continue } -// mentionAccts.append(acct) -// } -// for acct in mentionAccts { -// UITextChecker.learnWord(acct) -// } -// if let spoilerText = status.spoilerText, !spoilerText.isEmpty { -// self.isContentWarningComposing = true -// self.composeStatusAttribute.contentWarningContent = spoilerText -// } -// -// let initialComposeContent = mentionAccts.joined(separator: " ") -// let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " " -// self.preInsertedContent = preInsertedContent -// self.composeStatusAttribute.composeContent = preInsertedContent -// } -// case .hashtag(let hashtag): -// let initialComposeContent = "#" + hashtag -// UITextChecker.learnWord(initialComposeContent) -// let preInsertedContent = initialComposeContent + " " -// self.preInsertedContent = preInsertedContent -// self.composeStatusAttribute.composeContent = preInsertedContent -// case .mention(let record): -// context.managedObjectContext.performAndWait { -// guard let user = record.object(in: context.managedObjectContext) else { return } -// let initialComposeContent = "@" + user.acct -// UITextChecker.learnWord(initialComposeContent) -// let preInsertedContent = initialComposeContent + " " -// self.preInsertedContent = preInsertedContent -// self.composeStatusAttribute.composeContent = preInsertedContent -// } -// case .post: -// self.preInsertedContent = nil -// } -// -// // configure content warning -// if let composeContent = composeStatusAttribute.composeContent { -// cell.metaText.textView.text = composeContent -// } -// -// // configure content warning -// cell.statusContentWarningEditorView.textView.text = composeStatusAttribute.contentWarningContent -// } -//} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift index 57f1d6b95..9a0f58f47 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift @@ -57,7 +57,6 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable @Published public private(set) var thumbnail: UIImage? // original size image thumbnail @Published public private(set) var outputSizeInByte: Int64 = 0 - @MainActor @Published public private(set) var uploadState: UploadState = .none @Published public private(set) var uploadResult: UploadResult? @Published var error: Error? diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index 54fc6e67a..bab22b7ba 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -14,6 +14,8 @@ import MastodonCore public final class ComposeContentViewController: UIViewController { + static let minAutoCompleteVisibleHeight: CGFloat = 100 + let logger = Logger(subsystem: "ComposeContentViewController", category: "ViewController") var disposeBag = Set() @@ -40,7 +42,6 @@ public final class ComposeContentViewController: UIViewController { }() // toolbar - lazy var composeContentToolbarView = ComposeContentToolbarView(viewModel: composeContentToolbarViewModel) var composeContentToolbarViewBottomLayoutConstraint: NSLayoutConstraint! let composeContentToolbarBackgroundView = UIView() @@ -146,49 +147,42 @@ extension ComposeContentViewController { ]) // bind keyboard - let keyboardHasShortcutBar = CurrentValueSubject(traitCollection.userInterfaceIdiom == .pad) // update default value later let keyboardEventPublishers = Publishers.CombineLatest3( KeyboardResponderService.shared.isShow, KeyboardResponderService.shared.state, KeyboardResponderService.shared.endFrame ) -// Publishers.CombineLatest3( -// viewModel.$isCustomEmojiComposing, -// ) - keyboardEventPublishers - .sink(receiveValue: { [weak self] keyboardEvents in + Publishers.CombineLatest3( + keyboardEventPublishers, + viewModel.$isEmojiActive, + viewModel.$autoCompleteInfo + ) + .sink(receiveValue: { [weak self] keyboardEvents, isEmojiActive, autoCompleteInfo in guard let self = self else { return } let (isShow, state, endFrame) = keyboardEvents - -// switch self.traitCollection.userInterfaceIdiom { -// case .pad: -// keyboardHasShortcutBar.value = state != .floating -// default: -// keyboardHasShortcutBar.value = false -// } -// + let extraMargin: CGFloat = { var margin = ComposeContentToolbarView.toolbarHeight -// if autoCompleteInfo != nil { -//// margin += ComposeViewController.minAutoCompleteVisibleHeight -// } + if autoCompleteInfo != nil { + margin += ComposeContentViewController.minAutoCompleteVisibleHeight + } return margin }() -// + guard isShow, state == .dock else { self.tableView.contentInset.bottom = extraMargin self.tableView.verticalScrollIndicatorInsets.bottom = extraMargin -// if let superView = self.autoCompleteViewController.tableView.superview { -// let autoCompleteTableViewBottomInset: CGFloat = { -// let tableViewFrameInWindow = superView.convert(self.autoCompleteViewController.tableView.frame, to: nil) -// let padding = tableViewFrameInWindow.maxY + self.composeToolbarView.frame.height + AutoCompleteViewController.chevronViewHeight - self.view.frame.maxY -// return max(0, padding) -// }() -// self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset -// self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset -// } + if let superView = self.autoCompleteViewController.tableView.superview { + let autoCompleteTableViewBottomInset: CGFloat = { + let tableViewFrameInWindow = superView.convert(self.autoCompleteViewController.tableView.frame, to: nil) + let padding = tableViewFrameInWindow.maxY + ComposeContentToolbarView.toolbarHeight + AutoCompleteViewController.chevronViewHeight - self.view.frame.maxY + return max(0, padding) + }() + self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset + self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset + } UIView.animate(withDuration: 0.3) { self.composeContentToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom @@ -199,17 +193,16 @@ extension ComposeContentViewController { return } // isShow AND dock state -// self.systemKeyboardHeight = endFrame.height // adjust inset for auto-complete -// let autoCompleteTableViewBottomInset: CGFloat = { -// guard let superview = self.autoCompleteViewController.tableView.superview else { return .zero } -// let tableViewFrameInWindow = superview.convert(self.autoCompleteViewController.tableView.frame, to: nil) -// let padding = tableViewFrameInWindow.maxY + self.composeToolbarView.frame.height + AutoCompleteViewController.chevronViewHeight - endFrame.minY -// return max(0, padding) -// }() -// self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset -// self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset + let autoCompleteTableViewBottomInset: CGFloat = { + guard let superview = self.autoCompleteViewController.tableView.superview else { return .zero } + let tableViewFrameInWindow = superview.convert(self.autoCompleteViewController.tableView.frame, to: nil) + let padding = tableViewFrameInWindow.maxY + ComposeContentToolbarView.toolbarHeight + AutoCompleteViewController.chevronViewHeight - endFrame.minY + return max(0, padding) + }() + self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset + self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset // adjust inset for tableView let contentFrame = self.view.convert(self.tableView.frame, to: nil) @@ -289,6 +282,15 @@ extension ComposeContentViewController { // bind toolbar bindToolbarViewModel() + + // bind attachment picker + viewModel.$attachmentViewModels + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + self.resetImagePicker() + } + .store(in: &disposeBag) } public override func viewDidLayoutSubviews() { @@ -327,6 +329,8 @@ extension ComposeContentViewController { } private func bindToolbarViewModel() { + viewModel.$isAttachmentButtonEnabled.assign(to: &composeContentToolbarViewModel.$isAttachmentButtonEnabled) + viewModel.$isPollButtonEnabled.assign(to: &composeContentToolbarViewModel.$isPollButtonEnabled) viewModel.$isPollActive.assign(to: &composeContentToolbarViewModel.$isPollActive) viewModel.$isEmojiActive.assign(to: &composeContentToolbarViewModel.$isEmojiActive) viewModel.$isContentWarningActive.assign(to: &composeContentToolbarViewModel.$isContentWarningActive) @@ -345,6 +349,18 @@ extension ComposeContentViewController { autoCompleteViewController.view.frame.size.width = view.frame.width } } + + private func resetImagePicker() { + let selectionLimit = max(1, viewModel.maxMediaAttachmentLimit - viewModel.attachmentViewModels.count) + let configuration = ComposeContentViewController.createPhotoLibraryPickerConfiguration(selectionLimit: selectionLimit) + photoLibraryPicker = createImagePicker(configuration: configuration) + } + + private func createImagePicker(configuration: PHPickerConfiguration) -> PHPickerViewController { + let imagePicker = PHPickerViewController(configuration: configuration) + imagePicker.delegate = self + return imagePicker + } } // MARK: - UIScrollViewDelegate diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index ad9dfa1d8..cdf92ec26 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -14,6 +14,7 @@ import MetaTextKit import MastodonMeta import MastodonCore import MastodonSDK +import MastodonLocalization public protocol ComposeContentViewModelDelegate: AnyObject { func composeContentViewModel(_ viewModel: ComposeContentViewModel, handleAutoComplete info: ComposeContentViewModel.AutoCompleteInfo) -> Bool @@ -58,6 +59,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { customEmojiPickerInputViewModel.configure(textInput: textView) } } + // for hashtag: "# " + // for mention: "@ " @Published public var initialContent = "" @Published public var content = "" @Published public var contentWeightedLength = 0 @@ -115,6 +118,14 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { @Published var contentCellFrame: CGRect = .zero @Published var contentTextViewFrame: CGRect = .zero @Published var scrollViewState: ScrollViewState = .fold + + @Published var characterCount: Int = 0 + + @Published public private(set) var isPublishBarButtonItemEnabled = true + @Published var isAttachmentButtonEnabled = false + @Published var isPollButtonEnabled = false + + @Published public private(set) var shouldDismiss = true public init( context: AppContext, @@ -165,6 +176,70 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { super.init() // end init + // setup initial value + switch kind { + case .reply(let record): + context.managedObjectContext.performAndWait { + guard let status = record.object(in: context.managedObjectContext) else { + assertionFailure() + return + } + let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user + + var mentionAccts: [String] = [] + if author?.id != status.author.id { + mentionAccts.append("@" + status.author.acct) + } + let mentions = status.mentions + .filter { author?.id != $0.id } + for mention in mentions { + let acct = "@" + mention.acct + guard !mentionAccts.contains(acct) else { continue } + mentionAccts.append(acct) + } + for acct in mentionAccts { + UITextChecker.learnWord(acct) + } + if let spoilerText = status.spoilerText, !spoilerText.isEmpty { + self.isContentWarningActive = true + self.contentWarning = spoilerText + } + + let initialComposeContent = mentionAccts.joined(separator: " ") + let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " " + self.initialContent = preInsertedContent ?? "" + self.content = preInsertedContent ?? "" + } + case .hashtag(let hashtag): + let initialComposeContent = "#" + hashtag + UITextChecker.learnWord(initialComposeContent) + let preInsertedContent = initialComposeContent + " " + self.initialContent = preInsertedContent + self.content = preInsertedContent + case .mention(let record): + context.managedObjectContext.performAndWait { + guard let user = record.object(in: context.managedObjectContext) else { return } + let initialComposeContent = "@" + user.acct + UITextChecker.learnWord(initialComposeContent) + let preInsertedContent = initialComposeContent + " " + self.initialContent = preInsertedContent + self.content = preInsertedContent + } + case .post: + break + } + + bind() + } + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension ComposeContentViewModel { + private func bind() { // bind author $authContext .sink { [weak self] authContext in @@ -210,12 +285,129 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // bind emoji inputView $isEmojiActive.assign(to: &customEmojiPickerInputViewModel.$isCustomEmojiComposing) - } - - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } + // bind toolbar + Publishers.CombineLatest3( + $isPollActive, + $attachmentViewModels, + $maxMediaAttachmentLimit + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] isPollActive, attachmentViewModels, maxMediaAttachmentLimit in + guard let self = self else { return } + let shouldMediaDisable = isPollActive || attachmentViewModels.count >= maxMediaAttachmentLimit + let shouldPollDisable = attachmentViewModels.count > 0 + + self.isAttachmentButtonEnabled = !shouldMediaDisable + self.isPollButtonEnabled = !shouldPollDisable + } + .store(in: &disposeBag) + + // bind status content character count + Publishers.CombineLatest3( + $contentWeightedLength, + $contentWarningWeightedLength, + $isContentWarningActive + ) + .map { contentWeightedLength, contentWarningWeightedLength, isContentWarningActive -> Int in + var count = contentWeightedLength + if isContentWarningActive { + count += contentWarningWeightedLength + } + return count + } + .assign(to: &$characterCount) + + // bind compose bar button item UI state + let isComposeContentEmpty = $content + .map { $0.isEmpty } + let isComposeContentValid = Publishers.CombineLatest( + $characterCount, + $maxTextInputLimit + ) + .map { characterCount, maxTextInputLimit in + characterCount <= maxTextInputLimit + } + let isMediaEmpty = $attachmentViewModels + .map { $0.isEmpty } + let isMediaUploadAllSuccess = $attachmentViewModels + .map { attachmentViewModels in + return Publishers.MergeMany(attachmentViewModels.map { $0.$uploadState }) + .delay(for: 0.5, scheduler: DispatchQueue.main) // convert to outputs with delay. Due to @Published emit before changes + .map { _ in attachmentViewModels.map { $0.uploadState } } + } + .switchToLatest() + .map { outputs in + guard outputs.allSatisfy({ $0 == .finish }) else { return false } + return true + } + + isMediaUploadAllSuccess.sink { result in + print(result) + } + .store(in: &disposeBag) + + let isPollOptionsAllValid = $pollOptions + .map { options in + return Publishers.MergeMany(options.map { $0.$text }) + .delay(for: 0.5, scheduler: DispatchQueue.main) // convert to outputs with delay. Due to @Published emit before changes + .map { _ in options.map { $0.text } } + } + .switchToLatest() + .map { outputs in + return outputs.allSatisfy { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } + } + + let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4( + isComposeContentEmpty, + isComposeContentValid, + isMediaEmpty, + isMediaUploadAllSuccess + ) + .map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in + if isMediaEmpty { + return isComposeContentValid && !isComposeContentEmpty + } else { + return isComposeContentValid && isMediaUploadAllSuccess + } + } + .eraseToAnyPublisher() + + let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4( + isComposeContentEmpty, + isComposeContentValid, + $isPollActive, + isPollOptionsAllValid + ) + .map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollOptionsAllValid -> Bool in + if isPollComposing { + return isComposeContentValid && !isComposeContentEmpty && isPollOptionsAllValid + } else { + return isComposeContentValid && !isComposeContentEmpty + } + } + .eraseToAnyPublisher() + + Publishers.CombineLatest( + isPublishBarButtonItemEnabledPrecondition1, + isPublishBarButtonItemEnabledPrecondition2 + ) + .map { $0 && $1 } + .assign(to: &$isPublishBarButtonItemEnabled) + + // bind modal dismiss state + $content + .receive(on: DispatchQueue.main) + .map { [weak self] content in + guard let self = self else { return } + if content.isEmpty { + return true + } + // if the trimmed content equal to initial content + return content.trimmingCharacters(in: .whitespacesAndNewlines) == self.initialContent + } + .assign(to: &$shouldDismiss) + } } extension ComposeContentViewModel { @@ -325,6 +517,58 @@ extension ComposeContentViewModel { } // end func publisher() } +extension ComposeContentViewModel { + public enum AttachmentPrecondition: Error, LocalizedError { + case videoAttachWithPhoto + case moreThanOneVideo + + public var errorDescription: String? { + return L10n.Common.Alerts.PublishPostFailure.title + } + + public var failureReason: String? { + switch self { + case .videoAttachWithPhoto: + return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.videoAttachWithPhoto + case .moreThanOneVideo: + return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.moreThanOneVideo + } + } + } + + // check exclusive limit: + // - up to 1 video + // - up to N photos + public func checkAttachmentPrecondition() throws { + let attachmentViewModels = self.attachmentViewModels + guard !attachmentViewModels.isEmpty else { return } + var photoAttachmentViewModels: [AttachmentViewModel] = [] + var videoAttachmentViewModels: [AttachmentViewModel] = [] + attachmentViewModels.forEach { attachmentViewModel in + guard let output = attachmentViewModel.output else { + assertionFailure() + return + } + switch output { + case .image: + photoAttachmentViewModels.append(attachmentViewModel) + case .video: + videoAttachmentViewModels.append(attachmentViewModel) + } + } + + if !videoAttachmentViewModels.isEmpty { + guard videoAttachmentViewModels.count == 1 else { + throw AttachmentPrecondition.moreThanOneVideo + } + guard photoAttachmentViewModels.isEmpty else { + throw AttachmentPrecondition.videoAttachWithPhoto + } + } + } + +} + // MARK: - DeleteBackwardResponseTextFieldRelayDelegate extension ComposeContentViewModel: DeleteBackwardResponseTextFieldRelayDelegate { diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift index 5143bea35..fa409c114 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift @@ -39,7 +39,7 @@ public struct PollOptionTextField: UIViewRepresentable { textField.text = text textField.placeholder = { if index >= 0 { - return L10n.Scene.Compose.Poll.optionNumber(index) + return L10n.Scene.Compose.Poll.optionNumber(index + 1) } else { assertionFailure() return "" diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView+ViewModel.swift similarity index 97% rename from MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView+ViewModel.swift index 4a34c77d4..ee58bace4 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView+ViewModel.swift @@ -27,6 +27,9 @@ extension ComposeContentToolbarView { @Published var isEmojiActive = false @Published var isContentWarningActive = false + @Published var isAttachmentButtonEnabled = false + @Published var isPollButtonEnabled = false + @Published public var maxTextInputLimit = 500 @Published public var contentWeightedLength = 0 @Published public var contentWarningWeightedLength = 0 diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift similarity index 87% rename from MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift index 52026c636..7efb15340 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift @@ -44,7 +44,9 @@ struct ComposeContentToolbarView: View { } } label: { label(for: action) + .opacity(viewModel.isAttachmentButtonEnabled ? 1.0 : 0.5) } + .disabled(!viewModel.isAttachmentButtonEnabled) .frame(width: 48, height: 48) case .visibility: Menu { @@ -63,6 +65,16 @@ struct ComposeContentToolbarView: View { label(for: viewModel.visibility.image) } .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) default: Button { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(String(describing: action))") From f80b751d938a5f8f58eca7a49a6415ed5855e2d0 Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 13 Nov 2022 22:40:03 +0800 Subject: [PATCH 148/224] feat: camera and file attachment input --- .../ComposeContentViewController.swift | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index bab22b7ba..ea6a0136a 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -441,12 +441,13 @@ extension ComposeContentViewController: UIImagePickerControllerDelegate & UINavi guard let image = info[.originalImage] as? UIImage else { return } -// let attachmentService = MastodonAttachmentService( -// context: context, -// image: image, -// initialAuthenticationBox: viewModel.authenticationBox -// ) -// viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService] + let attachmentViewModel = AttachmentViewModel( + api: viewModel.context.apiService, + authContext: viewModel.authContext, + input: .image(image), + delegate: viewModel + ) + viewModel.attachmentViewModels += [attachmentViewModel] } public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { @@ -460,12 +461,13 @@ extension ComposeContentViewController: UIDocumentPickerDelegate { public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let url = urls.first else { return } -// let attachmentService = MastodonAttachmentService( -// context: context, -// documentURL: url, -// initialAuthenticationBox: viewModel.authenticationBox -// ) -// viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService] + let attachmentViewModel = AttachmentViewModel( + api: viewModel.context.apiService, + authContext: viewModel.authContext, + input: .url(url), + delegate: viewModel + ) + viewModel.attachmentViewModels += [attachmentViewModel] } } From b47f8ead378077f8a50f8069a4ed3a4f82532693 Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 13 Nov 2022 22:40:26 +0800 Subject: [PATCH 149/224] fix: compile issue --- .../Scene/ComposeContent/ComposeContentViewModel.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index cdf92ec26..ab3e25804 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -398,8 +398,7 @@ extension ComposeContentViewModel { // bind modal dismiss state $content .receive(on: DispatchQueue.main) - .map { [weak self] content in - guard let self = self else { return } + .map { content in if content.isEmpty { return true } From 26c6b8f2eee64493c18d56efcde117e2d9ac0e28 Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 13 Nov 2022 22:40:36 +0800 Subject: [PATCH 150/224] chore: code clean up --- .../View/AutoCompleteTopChevronView.swift | 1 - .../ComposeContentViewModel+DataSource.swift | 4 +- .../ComposeContentViewModel.swift | 17 +- .../ComposeContentTableViewCell.swift | 135 ----------- ...ComposeStatusAttachmentTableViewCell.swift | 172 -------------- .../ComposeStatusPollTableViewCell.swift | 209 ------------------ 6 files changed, 9 insertions(+), 529 deletions(-) delete mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusAttachmentTableViewCell.swift delete mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusPollTableViewCell.swift diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/View/AutoCompleteTopChevronView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/View/AutoCompleteTopChevronView.swift index ccc36b1df..6b842c9f1 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/View/AutoCompleteTopChevronView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/View/AutoCompleteTopChevronView.swift @@ -8,7 +8,6 @@ import UIKit import Combine import MastodonCore -import MastodonUI final class AutoCompleteTopChevronView: UIView { diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift index c8bf3ddc8..abbfe0e61 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift @@ -66,9 +66,9 @@ extension ComposeContentViewModel { guard let replyTo = status.object(in: context.managedObjectContext) else { return } cell.statusView.configure(status: replyTo) } - case .hashtag(let hashtag): + case .hashtag: break - case .mention(let user): + case .mention: break } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index ab3e25804..2bf4e26ff 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -185,7 +185,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { return } let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user - + var mentionAccts: [String] = [] if author?.id != status.author.id { mentionAccts.append("@" + status.author.acct) @@ -204,7 +204,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { self.isContentWarningActive = true self.contentWarning = spoilerText } - + let initialComposeContent = mentionAccts.joined(separator: " ") let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " " self.initialContent = preInsertedContent ?? "" @@ -342,11 +342,6 @@ extension ComposeContentViewModel { return true } - isMediaUploadAllSuccess.sink { result in - print(result) - } - .store(in: &disposeBag) - let isPollOptionsAllValid = $pollOptions .map { options in return Publishers.MergeMany(options.map { $0.$text }) @@ -517,14 +512,15 @@ extension ComposeContentViewModel { } extension ComposeContentViewModel { + public enum AttachmentPrecondition: Error, LocalizedError { case videoAttachWithPhoto case moreThanOneVideo - + public var errorDescription: String? { return L10n.Common.Alerts.PublishPostFailure.title } - + public var failureReason: String? { switch self { case .videoAttachWithPhoto: @@ -534,13 +530,14 @@ extension ComposeContentViewModel { } } } - + // check exclusive limit: // - up to 1 video // - up to N photos public func checkAttachmentPrecondition() throws { let attachmentViewModels = self.attachmentViewModels guard !attachmentViewModels.isEmpty else { return } + var photoAttachmentViewModels: [AttachmentViewModel] = [] var videoAttachmentViewModels: [AttachmentViewModel] = [] attachmentViewModels.forEach { attachmentViewModel in diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeContentTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeContentTableViewCell.swift index 3a646f1fc..90d432825 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeContentTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeContentTableViewCell.swift @@ -7,74 +7,12 @@ import os.log import UIKit -import Combine -import MetaTextKit -import UITextView_Placeholder -import MastodonAsset -import MastodonLocalization import UIHostingConfigurationBackport -//protocol ComposeStatusContentTableViewCellDelegate: AnyObject { -// func composeStatusContentTableViewCell(_ cell: ComposeStatusContentTableViewCell, textViewShouldBeginEditing textView: UITextView) -> Bool -//} - final class ComposeContentTableViewCell: UITableViewCell { let logger = Logger(subsystem: "ComposeContentTableViewCell", category: "View") -// var disposeBag = Set() -// weak var delegate: ComposeStatusContentTableViewCellDelegate? -// -// let statusView = StatusView() -// -// let statusContentWarningEditorView = StatusContentWarningEditorView() -// -// let textEditorViewContainerView = UIView() -// -// static let metaTextViewTag: Int = 333 -// let metaText: MetaText = { -// let metaText = MetaText() -// metaText.textView.backgroundColor = .clear -// metaText.textView.isScrollEnabled = false -// metaText.textView.keyboardType = .twitter -// metaText.textView.textDragInteraction?.isEnabled = false // disable drag for link and attachment -// metaText.textView.textContainer.lineFragmentPadding = 10 // leading inset -// metaText.textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)) -// metaText.textView.attributedPlaceholder = { -// var attributes = metaText.textAttributes -// attributes[.foregroundColor] = Asset.Colors.Label.secondary.color -// return NSAttributedString( -// string: L10n.Scene.Compose.contentInputPlaceholder, -// attributes: attributes -// ) -// }() -// metaText.paragraphStyle = { -// let style = NSMutableParagraphStyle() -// style.lineSpacing = 5 -// style.paragraphSpacing = 0 -// return style -// }() -// metaText.textAttributes = [ -// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)), -// .foregroundColor: Asset.Colors.Label.primary.color, -// ] -// metaText.linkAttributes = [ -// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)), -// .foregroundColor: Asset.Colors.brand.color, -// ] -// return metaText -// }() -// -// // output -// let contentWarningContent = PassthroughSubject() -// -// override func prepareForReuse() { -// super.prepareForReuse() -// -// metaText.delegate = nil -// metaText.textView.delegate = nil -// } - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) _init() @@ -93,79 +31,6 @@ extension ComposeContentTableViewCell { selectionStyle = .none layer.zPosition = 999 backgroundColor = .clear - -// let containerStackView = UIStackView() -// containerStackView.axis = .vertical -// containerStackView.translatesAutoresizingMaskIntoConstraints = false -// contentView.addSubview(containerStackView) -// NSLayoutConstraint.activate([ -// containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), -// containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), -// containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), -// containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), -// ]) -// containerStackView.preservesSuperviewLayoutMargins = true -// -// containerStackView.addArrangedSubview(statusContentWarningEditorView) -// statusContentWarningEditorView.setContentHuggingPriority(.required - 1, for: .vertical) -// -// let statusContainerView = UIView() -// statusContainerView.preservesSuperviewLayoutMargins = true -// containerStackView.addArrangedSubview(statusContainerView) -// statusView.translatesAutoresizingMaskIntoConstraints = false -// statusContainerView.addSubview(statusView) -// NSLayoutConstraint.activate([ -// statusView.topAnchor.constraint(equalTo: statusContainerView.topAnchor, constant: 20), -// statusView.leadingAnchor.constraint(equalTo: statusContainerView.leadingAnchor), -// statusView.trailingAnchor.constraint(equalTo: statusContainerView.trailingAnchor), -// statusView.bottomAnchor.constraint(equalTo: statusContainerView.bottomAnchor), -// ]) -// statusView.setup(style: .composeStatusAuthor) -// -// containerStackView.addArrangedSubview(textEditorViewContainerView) -// metaText.textView.translatesAutoresizingMaskIntoConstraints = false -// textEditorViewContainerView.addSubview(metaText.textView) -// NSLayoutConstraint.activate([ -// metaText.textView.topAnchor.constraint(equalTo: textEditorViewContainerView.topAnchor), -// metaText.textView.leadingAnchor.constraint(equalTo: textEditorViewContainerView.layoutMarginsGuide.leadingAnchor), -// metaText.textView.trailingAnchor.constraint(equalTo: textEditorViewContainerView.layoutMarginsGuide.trailingAnchor), -// metaText.textView.bottomAnchor.constraint(equalTo: textEditorViewContainerView.bottomAnchor), -// metaText.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 64).priority(.defaultHigh), -// ]) -// statusContentWarningEditorView.textView.delegate = self } } - -// MARK: - UITextViewDelegate -//extension ComposeStatusContentTableViewCell: UITextViewDelegate { -// -// func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { -// return delegate?.composeStatusContentTableViewCell(self, textViewShouldBeginEditing: textView) ?? true -// } -// -// func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { -// switch textView { -// case statusContentWarningEditorView.textView: -// // disable input line break -// guard text != "\n" else { return false } -// return true -// default: -// assertionFailure() -// return true -// } -// } -// -// func textViewDidChange(_ textView: UITextView) { -// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): text: \(textView.text ?? "")") -// guard textView === statusContentWarningEditorView.textView else { return } -// // replace line break with space -// // needs check input state to prevent break the IME -// if textView.markedTextRange == nil { -// textView.text = textView.text.replacingOccurrences(of: "\n", with: " ") -// } -// contentWarningContent.send(textView.text) -// } -// -//} - diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusAttachmentTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusAttachmentTableViewCell.swift deleted file mode 100644 index 42a851bf1..000000000 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusAttachmentTableViewCell.swift +++ /dev/null @@ -1,172 +0,0 @@ -// -// ComposeStatusAttachmentTableViewCell.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-29. -// - -import UIKit -import SwiftUI -import Combine -import AlamofireImage -import MastodonAsset -import MastodonCore -import MastodonLocalization -import UIHostingConfigurationBackport - -//final class ComposeStatusAttachmentTableViewCell: UITableViewCell { -// -// private(set) var dataSource: UICollectionViewDiffableDataSource! -// weak var composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate? -// var observations = Set() -// -// private static func createLayout() -> UICollectionViewLayout { -// let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) -// let item = NSCollectionLayoutItem(layoutSize: itemSize) -// let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) -// let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) -// let section = NSCollectionLayoutSection(group: group) -// section.contentInsetsReference = .readableContent -// return UICollectionViewCompositionalLayout(section: section) -// } -// -// private(set) var collectionViewHeightLayoutConstraint: NSLayoutConstraint! -// let collectionView: UICollectionView = { -// let collectionViewLayout = ComposeStatusAttachmentTableViewCell.createLayout() -// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) -// collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self)) -// collectionView.backgroundColor = .clear -// collectionView.alwaysBounceVertical = true -// collectionView.isScrollEnabled = false -// return collectionView -// }() -// let collectionViewHeightDidUpdate = PassthroughSubject() -// -// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { -// super.init(style: style, reuseIdentifier: reuseIdentifier) -// _init() -// } -// -// required init?(coder: NSCoder) { -// super.init(coder: coder) -// _init() -// } -// -//} -// -//extension ComposeStatusAttachmentTableViewCell { -// -// private func _init() { -// backgroundColor = .clear -// contentView.backgroundColor = .clear -// -// collectionView.translatesAutoresizingMaskIntoConstraints = false -// contentView.addSubview(collectionView) -// collectionViewHeightLayoutConstraint = collectionView.heightAnchor.constraint(equalToConstant: 200).priority(.defaultHigh) -// NSLayoutConstraint.activate([ -// collectionView.topAnchor.constraint(equalTo: contentView.topAnchor), -// collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), -// collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), -// collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), -// collectionViewHeightLayoutConstraint, -// ]) -// -// collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, _ in -// guard let self = self else { return } -// self.collectionViewHeightLayoutConstraint.constant = collectionView.contentSize.height -// self.collectionViewHeightDidUpdate.send() -// } -// .store(in: &observations) -// -// self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { -// [weak self] collectionView, indexPath, item -> UICollectionViewCell? in -// guard let _ = self else { return UICollectionViewCell() } -// switch item { -// case .attachment: -// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell -// cell.contentConfiguration = UIHostingConfigurationBackport { -// HStack { -// Image(systemName: "star") -// Text("Favorites") -// Spacer() -// } -// } -//// cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value -//// cell.delegate = self.composeStatusAttachmentCollectionViewCellDelegate -//// attachmentService.thumbnailImage -//// .receive(on: DispatchQueue.main) -//// .sink { [weak cell] thumbnailImage in -//// guard let cell = cell else { return } -//// let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1) -//// guard let image = thumbnailImage else { -//// let placeholder = UIImage.placeholder( -//// size: size, -//// color: ThemeService.shared.currentTheme.value.systemGroupedBackgroundColor -//// ) -//// .af.imageRounded( -//// withCornerRadius: AttachmentContainerView.containerViewCornerRadius -//// ) -//// cell.attachmentContainerView.previewImageView.image = placeholder -//// return -//// } -//// // cannot get correct size. set corner radius on layer -//// cell.attachmentContainerView.previewImageView.image = image -//// } -//// .store(in: &cell.disposeBag) -//// Publishers.CombineLatest( -//// attachmentService.uploadStateMachineSubject.eraseToAnyPublisher(), -//// attachmentService.error.eraseToAnyPublisher() -//// ) -//// .receive(on: DispatchQueue.main) -//// .sink { [weak cell, weak attachmentService] uploadState, error in -//// guard let cell = cell else { return } -//// guard let attachmentService = attachmentService else { return } -//// cell.attachmentContainerView.emptyStateView.isHidden = error == nil -//// cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil -//// if let error = error { -//// cell.attachmentContainerView.activityIndicatorView.stopAnimating() -//// cell.attachmentContainerView.emptyStateView.label.text = error.localizedDescription -//// } else { -//// guard let uploadState = uploadState else { return } -//// switch uploadState { -//// case is MastodonAttachmentService.UploadState.Finish: -//// cell.attachmentContainerView.activityIndicatorView.stopAnimating() -//// case is MastodonAttachmentService.UploadState.Fail: -//// cell.attachmentContainerView.activityIndicatorView.stopAnimating() -//// // FIXME: not display -//// cell.attachmentContainerView.emptyStateView.label.text = { -//// if let file = attachmentService.file.value { -//// switch file { -//// case .jpeg, .png, .gif: -//// return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) -//// case .other: -//// return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video) -//// } -//// } else { -//// return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) -//// } -//// }() -//// default: -//// break -//// } -//// } -//// } -//// .store(in: &cell.disposeBag) -//// NotificationCenter.default.publisher( -//// for: UITextView.textDidChangeNotification, -//// object: cell.attachmentContainerView.descriptionTextView -//// ) -//// .receive(on: DispatchQueue.main) -//// .sink { notification in -//// guard let textField = notification.object as? UITextView else { return } -//// let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) -//// attachmentService.description.value = text -//// } -//// .store(in: &cell.disposeBag) -// return cell -// } -// } -// } -// -//} -// diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusPollTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusPollTableViewCell.swift deleted file mode 100644 index 27b835a5a..000000000 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusPollTableViewCell.swift +++ /dev/null @@ -1,209 +0,0 @@ -// -// ComposeStatusPollTableViewCell.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-29. -// - -import os.log -import UIKit -import Combine -import MastodonAsset -import MastodonLocalization - -//protocol ComposeStatusPollTableViewCellDelegate: AnyObject { -// func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute]) -//} -// -//final class ComposeStatusPollTableViewCell: UITableViewCell { -// -// let logger = Logger(subsystem: "ComposeStatusPollTableViewCell", category: "UI") -// -// private(set) var dataSource: UICollectionViewDiffableDataSource! -// var observations = Set() -// -// weak var customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel? -// weak var delegate: ComposeStatusPollTableViewCellDelegate? -// weak var composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate? -// weak var composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate? -// weak var composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate? -// -// private static func createLayout() -> UICollectionViewLayout { -// let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) -// let item = NSCollectionLayoutItem(layoutSize: itemSize) -// let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) -// let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) -// let section = NSCollectionLayoutSection(group: group) -// section.contentInsetsReference = .readableContent -// return UICollectionViewCompositionalLayout(section: section) -// } -// -// private(set) var collectionViewHeightLayoutConstraint: NSLayoutConstraint! -// let collectionView: UICollectionView = { -// let collectionViewLayout = ComposeStatusPollTableViewCell.createLayout() -// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) -// collectionView.register(ComposeStatusPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self)) -// collectionView.register(ComposeStatusPollOptionAppendEntryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self)) -// collectionView.register(ComposeStatusPollExpiresOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self)) -// collectionView.backgroundColor = .clear -// collectionView.alwaysBounceVertical = true -// collectionView.isScrollEnabled = false -// collectionView.dragInteractionEnabled = true -// return collectionView -// }() -// let collectionViewHeightDidUpdate = PassthroughSubject() -// -// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { -// super.init(style: style, reuseIdentifier: reuseIdentifier) -// _init() -// } -// -// required init?(coder: NSCoder) { -// super.init(coder: coder) -// _init() -// } -// -//} -// -//extension ComposeStatusPollTableViewCell { -// -// private func _init() { -// backgroundColor = .clear -// contentView.backgroundColor = .clear -// -// collectionView.translatesAutoresizingMaskIntoConstraints = false -// contentView.addSubview(collectionView) -// collectionViewHeightLayoutConstraint = collectionView.heightAnchor.constraint(equalToConstant: 300).priority(.defaultHigh) -// NSLayoutConstraint.activate([ -// collectionView.topAnchor.constraint(equalTo: contentView.topAnchor), -// collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), -// collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), -// collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), -// collectionViewHeightLayoutConstraint, -// ]) -// -// collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, _ in -// guard let self = self else { return } -// self.collectionViewHeightLayoutConstraint.constant = collectionView.contentSize.height -// self.collectionViewHeightDidUpdate.send() -// } -// .store(in: &observations) -// -// self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [ -// weak self -// ] collectionView, indexPath, item -> UICollectionViewCell? in -// guard let self = self else { return UICollectionViewCell() } -// -// switch item { -// case .pollOption(let attribute): -// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionCollectionViewCell -// cell.pollOptionView.optionTextField.text = attribute.option.value -// cell.pollOptionView.optionTextField.placeholder = L10n.Scene.Compose.Poll.optionNumber(indexPath.item + 1) -// cell.pollOption -// .receive(on: DispatchQueue.main) -// .assign(to: \.value, on: attribute.option) -// .store(in: &cell.disposeBag) -// cell.delegate = self.composeStatusPollOptionCollectionViewCellDelegate -// if let customEmojiPickerInputViewModel = self.customEmojiPickerInputViewModel { -// ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.pollOptionView.optionTextField, disposeBag: &cell.disposeBag) -// } -// return cell -// case .pollOptionAppendEntry: -// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionAppendEntryCollectionViewCell -// cell.delegate = self.composeStatusPollOptionAppendEntryCollectionViewCellDelegate -// return cell -// case .pollExpiresOption(let attribute): -// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollExpiresOptionCollectionViewCell -// cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(attribute.expiresOption.value.title), for: .normal) -// attribute.expiresOption -// .receive(on: DispatchQueue.main) -// .sink { [weak cell] expiresOption in -// guard let cell = cell else { return } -// cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(expiresOption.title), for: .normal) -// } -// .store(in: &cell.disposeBag) -// cell.delegate = self.composeStatusPollExpiresOptionCollectionViewCellDelegate -// return cell -// } -// } -// -// collectionView.dragDelegate = self -// collectionView.dropDelegate = self -// } -// -//} -// -//// MARK: - UICollectionViewDragDelegate -//extension ComposeStatusPollTableViewCell: UICollectionViewDragDelegate { -// -// func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { -// guard let item = dataSource.itemIdentifier(for: indexPath) else { return [] } -// switch item { -// case .pollOption: -// let itemProvider = NSItemProvider(object: String(item.hashValue) as NSString) -// let dragItem = UIDragItem(itemProvider: itemProvider) -// dragItem.localObject = item -// return [dragItem] -// default: -// return [] -// } -// } -// -// func collectionView(_ collectionView: UICollectionView, dragSessionIsRestrictedToDraggingApplication session: UIDragSession) -> Bool { -// // drag to app should be the same app -// return true -// } -//} -// -//// MARK: - UICollectionViewDropDelegate -//extension ComposeStatusPollTableViewCell: UICollectionViewDropDelegate { -// // didUpdate -// func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal { -// guard collectionView.hasActiveDrag, -// let destinationIndexPath = destinationIndexPath, -// let item = dataSource.itemIdentifier(for: destinationIndexPath) -// else { -// return UICollectionViewDropProposal(operation: .forbidden) -// } -// -// switch item { -// case .pollOption: -// return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) -// default: -// return UICollectionViewDropProposal(operation: .cancel) -// } -// } -// -// // performDrop -// func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) { -// guard let dropItem = coordinator.items.first, -// let item = dropItem.dragItem.localObject as? ComposeStatusPollItem, -// case .pollOption = item -// else { return } -// -// guard coordinator.proposal.operation == .move else { return } -// guard let destinationIndexPath = coordinator.destinationIndexPath, -// let _ = collectionView.cellForItem(at: destinationIndexPath) as? ComposeStatusPollOptionCollectionViewCell -// else { return } -// -// var snapshot = dataSource.snapshot() -// guard destinationIndexPath.row < snapshot.itemIdentifiers.count else { return } -// let anchorItem = snapshot.itemIdentifiers[destinationIndexPath.row] -// snapshot.moveItem(item, afterItem: anchorItem) -// dataSource.apply(snapshot) -// -// coordinator.drop(dropItem.dragItem, toItemAt: destinationIndexPath) -// } -//} -// -//extension ComposeStatusPollTableViewCell: UICollectionViewDelegate { -// func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath { -// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(originalIndexPath.debugDescription) -> \(proposedIndexPath.debugDescription)") -// -// guard let _ = collectionView.cellForItem(at: proposedIndexPath) as? ComposeStatusPollOptionCollectionViewCell else { -// return originalIndexPath -// } -// -// return proposedIndexPath -// } -//} From 91bfc8ad5a374b820e74ecb4310cbd3d2b860944 Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 13 Nov 2022 22:57:35 +0800 Subject: [PATCH 151/224] feat: add paste image input for post compose scene --- Mastodon.xcodeproj/project.pbxproj | 4 -- .../Scene/Compose/ComposeViewController.swift | 60 +++++++++---------- .../MetaTextView+PasteExtensions.swift | 0 Podfile.lock | 2 +- 4 files changed, 31 insertions(+), 35 deletions(-) rename {Mastodon/Scene/Compose/View => MastodonSDK/Sources/MastodonUI/Vendor}/MetaTextView+PasteExtensions.swift (100%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index aa944bb84..0b7a5806a 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -87,7 +87,6 @@ 62FD27D32893707B00B205C5 /* BookmarkViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FD27D22893707B00B205C5 /* BookmarkViewController+DataSourceProvider.swift */; }; 62FD27D52893708A00B205C5 /* BookmarkViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FD27D42893708A00B205C5 /* BookmarkViewModel+Diffable.swift */; }; 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; - CD91FB31290EDA6F00BB9463 /* MetaTextView+PasteExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD91FB30290EDA6F00BB9463 /* MetaTextView+PasteExtensions.swift */; }; DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; settings = {ATTRIBUTES = (codegen, ); }; }; DB0009A726AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; }; DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; @@ -605,7 +604,6 @@ BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = ""; }; BD7598A87F4497045EDEF252 /* Pods-Mastodon.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk - release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk - release.xcconfig"; sourceTree = ""; }; C3789232A52F43529CA67E95 /* Pods-MastodonIntent.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonIntent.asdk - debug.xcconfig"; path = "Target Support Files/Pods-MastodonIntent/Pods-MastodonIntent.asdk - debug.xcconfig"; sourceTree = ""; }; - CD91FB30290EDA6F00BB9463 /* MetaTextView+PasteExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MetaTextView+PasteExtensions.swift"; sourceTree = ""; }; CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.debug.xcconfig"; sourceTree = ""; }; DB0009A826AEE5DC009B9D2D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = ""; }; @@ -1867,7 +1865,6 @@ DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */, DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */, DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */, - CD91FB30290EDA6F00BB9463 /* MetaTextView+PasteExtensions.swift */, ); path = View; sourceTree = ""; @@ -3296,7 +3293,6 @@ 2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */, 6213AF5A28939C8400BCADB6 /* BookmarkViewModel.swift in Sources */, 5B24BBDB262DB14800A9381B /* ReportStatusViewModel+Diffable.swift in Sources */, - CD91FB31290EDA6F00BB9463 /* MetaTextView+PasteExtensions.swift in Sources */, DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */, 0FB3D2FE25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift in Sources */, 5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */, diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index fb6ee43f9..d2bc0a2cb 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -301,6 +301,36 @@ extension ComposeViewController { } +extension ComposeViewController { + public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + + // Enable pasting images + if (action == #selector(UIResponderStandardEditActions.paste(_:))) { + return UIPasteboard.general.hasStrings || UIPasteboard.general.hasImages; + } + + return super.canPerformAction(action, withSender: sender); + } + + override func paste(_ sender: Any?) { + logger.debug("Paste event received") + + // Look for images on the clipboard + if UIPasteboard.general.hasImages, let images = UIPasteboard.general.images { + logger.warning("Got image paste event, however attachments are not yet re-implemented."); + let attachmentViewModels = images.map { image in + return AttachmentViewModel( + api: viewModel.context.apiService, + authContext: viewModel.authContext, + input: .image(image), + delegate: composeContentViewModel + ) + } + composeContentViewModel.attachmentViewModels += attachmentViewModels + } + } +} + // MARK: - UIAdaptivePresentationControllerDelegate extension ComposeViewController: UIAdaptivePresentationControllerDelegate { @@ -455,33 +485,3 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate { // } // //} - -extension ComposeViewController { - public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - - // Enable pasting images - if (action == #selector(UIResponderStandardEditActions.paste(_:))) { - return UIPasteboard.general.hasStrings || UIPasteboard.general.hasImages; - } - - return super.canPerformAction(action, withSender: sender); - } - - override func paste(_ sender: Any?) { - logger.debug("Paste event received") - - // Look for images on the clipboard - if (UIPasteboard.general.hasImages) { - if let images = UIPasteboard.general.images { - logger.warning("Got image paste event, however attachments are not yet re-implemented."); -// viewModel.attachmentServices = viewModel.attachmentServices + images.map({ image in -// MastodonAttachmentService( -// context: context, -// image: image, -// initialAuthenticationBox: viewModel.authenticationBox -// ) -// }) - } - } - } -} diff --git a/Mastodon/Scene/Compose/View/MetaTextView+PasteExtensions.swift b/MastodonSDK/Sources/MastodonUI/Vendor/MetaTextView+PasteExtensions.swift similarity index 100% rename from Mastodon/Scene/Compose/View/MetaTextView+PasteExtensions.swift rename to MastodonSDK/Sources/MastodonUI/Vendor/MetaTextView+PasteExtensions.swift diff --git a/Podfile.lock b/Podfile.lock index e8785b512..3b9928a0b 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -33,6 +33,6 @@ SPEC CHECKSUMS: SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5 -PODFILE CHECKSUM: a60ecee06525582c010e270ac7a17024e441a0da +PODFILE CHECKSUM: 8fddf46611e09d2eb1a5d67c464c236884a08e80 COCOAPODS: 1.11.3 From 939429aacc648a0c09b8e6f9a97e0fe2c48e6deb Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 14 Nov 2022 00:05:43 +0800 Subject: [PATCH 152/224] feat: restore share action extension --- Mastodon.xcodeproj/project.pbxproj | 16 +- .../xcschemes/xcschememanagement.plist | 2 +- .../Scene/Compose/ComposeViewController.swift | 26 +- .../Scene/ComposeViewController.swift | 326 -------------- .../Scene/ComposeViewModel.swift | 416 ------------------ .../Scene/ShareViewController.swift | 330 ++++++++++++++ .../Scene/ShareViewModel.swift | 43 ++ 7 files changed, 386 insertions(+), 773 deletions(-) delete mode 100644 ShareActionExtension/Scene/ComposeViewController.swift delete mode 100644 ShareActionExtension/Scene/ComposeViewModel.swift create mode 100644 ShareActionExtension/Scene/ShareViewController.swift create mode 100644 ShareActionExtension/Scene/ShareViewModel.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 0b7a5806a..fc8e6ff25 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -371,10 +371,10 @@ DBB8AB4F26AED13F00F6D281 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDE25BAA00100D1B89D /* Assets.xcassets */; }; DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB9759B262462E1004620BD /* ThreadMetaView.swift */; }; DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; }; - DBC6461526A170AB00B0E31B /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6461426A170AB00B0E31B /* ComposeViewController.swift */; }; + DBC3872429214121001EC0FD /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC3872329214121001EC0FD /* ShareViewController.swift */; }; DBC6461826A170AB00B0E31B /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DBC6461626A170AB00B0E31B /* MainInterface.storyboard */; }; DBC6461C26A170AB00B0E31B /* ShareActionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - DBC6462326A1712000B0E31B /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6462226A1712000B0E31B /* ComposeViewModel.swift */; }; + DBC6462326A1712000B0E31B /* ShareViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6462226A1712000B0E31B /* ShareViewModel.swift */; }; DBC6462826A1736300B0E31B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDE25BAA00100D1B89D /* Assets.xcassets */; }; DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; }; DBCA0EBC282BB38A0029E2B0 /* PageboyNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCA0EBB282BB38A0029E2B0 /* PageboyNavigateable.swift */; }; @@ -950,11 +950,11 @@ DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = ""; }; DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = ""; }; DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = ""; }; + DBC3872329214121001EC0FD /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - DBC6461426A170AB00B0E31B /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = ""; }; DBC6461726A170AB00B0E31B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; DBC6461926A170AB00B0E31B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DBC6462226A1712000B0E31B /* ComposeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; }; + DBC6462226A1712000B0E31B /* ShareViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareViewModel.swift; sourceTree = ""; }; DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = ""; }; DBC9E3A3282E13BB0063A4D9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Intents.strings; sourceTree = ""; }; DBC9E3A4282E13BB0063A4D9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -2686,8 +2686,8 @@ isa = PBXGroup; children = ( DBFEF05426A576EE006D7ED1 /* View */, - DBC6462226A1712000B0E31B /* ComposeViewModel.swift */, - DBC6461426A170AB00B0E31B /* ComposeViewController.swift */, + DBC6462226A1712000B0E31B /* ShareViewModel.swift */, + DBC3872329214121001EC0FD /* ShareViewController.swift */, ); path = Scene; sourceTree = ""; @@ -3539,9 +3539,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DBC6462326A1712000B0E31B /* ComposeViewModel.swift in Sources */, + DBC6462326A1712000B0E31B /* ShareViewModel.swift in Sources */, DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */, - DBC6461526A170AB00B0E31B /* ComposeViewController.swift in Sources */, + DBC3872429214121001EC0FD /* ShareViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 979c8c0e6..78a3a9e70 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -117,7 +117,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 18 + 16 ShareActionExtension.xcscheme_^#shared#^_ diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index d2bc0a2cb..6de17e31f 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -120,9 +120,9 @@ extension ComposeViewController { guard let self = self else { return } guard self.traitCollection.userInterfaceIdiom == .pad else { return } var items = [self.publishBarButtonItem] - if self.traitCollection.horizontalSizeClass == .regular { - items.append(self.characterCountBarButtonItem) - } + // if self.traitCollection.horizontalSizeClass == .regular { + // items.append(self.characterCountBarButtonItem) + // } self.navigationItem.rightBarButtonItems = items } .store(in: &disposeBag) @@ -140,7 +140,7 @@ extension ComposeViewController { composeContentViewController.didMove(toParent: self) // bind navigation bar style - configureNavigationBarTitleStyle() + // configureNavigationBarTitleStyle() viewModel.traitCollectionDidChangePublisher .receive(on: DispatchQueue.main) .sink { [weak self] _ in @@ -163,24 +163,6 @@ extension ComposeViewController { .receive(on: DispatchQueue.main) .assign(to: \.isEnabled, on: publishButton) .store(in: &disposeBag) -// -// // bind content warning button state -// viewModel.$isContentWarningComposing -// .receive(on: DispatchQueue.main) -// .sink { [weak self] isContentWarningComposing in -// guard let self = self else { return } -// let accessibilityLabel = isContentWarningComposing ? L10n.Scene.Compose.Accessibility.disableContentWarning : L10n.Scene.Compose.Accessibility.enableContentWarning -// self.composeToolbarView.contentWarningBarButtonItem.accessibilityLabel = accessibilityLabel -// self.composeToolbarView.contentWarningButton.accessibilityLabel = accessibilityLabel -// } -// .store(in: &disposeBag) - - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - -// viewModel.isViewAppeared = true } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { diff --git a/ShareActionExtension/Scene/ComposeViewController.swift b/ShareActionExtension/Scene/ComposeViewController.swift deleted file mode 100644 index a7605da17..000000000 --- a/ShareActionExtension/Scene/ComposeViewController.swift +++ /dev/null @@ -1,326 +0,0 @@ -// -// ComposeViewController.swift -// MastodonShareAction -// -// Created by MainasuK Cirno on 2021-7-16. -// - -import os.log -import UIKit -import Combine -import SwiftUI -import MastodonAsset -import MastodonLocalization -import MastodonCore -import MastodonUI - -class ComposeViewController: UIViewController { - - let logger = Logger(subsystem: "ComposeViewController", category: "ViewController") - - let context = AppContext() - - var disposeBag = Set() - private(set) lazy var viewModel = ComposeViewModel(context: context) - - let publishButton: UIButton = { - let button = RoundedEdgesButton(type: .custom) - button.setTitle(L10n.Scene.Compose.composeAction, for: .normal) - button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold) - button.setBackgroundImage(.placeholder(color: Asset.Colors.brand.color), for: .normal) - button.setBackgroundImage(.placeholder(color: Asset.Colors.brand.color.withAlphaComponent(0.5)), for: .highlighted) - button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled) - button.setTitleColor(.white, for: .normal) - button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16) // set 28pt height - button.adjustsImageWhenHighlighted = false - return button - }() - - private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:))) - private(set) lazy var publishBarButtonItem: UIBarButtonItem = { - let barButtonItem = UIBarButtonItem(customView: publishButton) - publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) - return barButtonItem - }() - - let activityIndicatorBarButtonItem: UIBarButtonItem = { - let indicatorView = UIActivityIndicatorView(style: .medium) - let barButtonItem = UIBarButtonItem(customView: indicatorView) - indicatorView.startAnimating() - return barButtonItem - }() - - -// let viewSafeAreaDidChange = PassthroughSubject() -// let composeToolbarView = ComposeToolbarView() -// var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint! -// let composeToolbarBackgroundView = UIView() -} - -extension ComposeViewController { - - override func viewDidLoad() { - super.viewDidLoad() - -// navigationController?.presentationController?.delegate = self -// -// setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) -// ThemeService.shared.currentTheme -// .receive(on: DispatchQueue.main) -// .sink { [weak self] theme in -// guard let self = self else { return } -// self.setupBackgroundColor(theme: theme) -// } -// .store(in: &disposeBag) -// -// navigationItem.leftBarButtonItem = cancelBarButtonItem -// viewModel.isBusy -// .receive(on: DispatchQueue.main) -// .sink { [weak self] isBusy in -// guard let self = self else { return } -// self.navigationItem.rightBarButtonItem = isBusy ? self.activityIndicatorBarButtonItem : self.publishBarButtonItem -// } -// .store(in: &disposeBag) -// -// let hostingViewController = UIHostingController( -// rootView: ComposeView().environmentObject(viewModel.composeViewModel) -// ) -// addChild(hostingViewController) -// view.addSubview(hostingViewController.view) -// hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false -// view.addSubview(hostingViewController.view) -// NSLayoutConstraint.activate([ -// hostingViewController.view.topAnchor.constraint(equalTo: view.topAnchor), -// hostingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), -// hostingViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), -// hostingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), -// ]) -// hostingViewController.didMove(toParent: self) -// -// composeToolbarView.translatesAutoresizingMaskIntoConstraints = false -// view.addSubview(composeToolbarView) -// composeToolbarViewBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: composeToolbarView.bottomAnchor) -// NSLayoutConstraint.activate([ -// composeToolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), -// composeToolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor), -// composeToolbarViewBottomLayoutConstraint, -// composeToolbarView.heightAnchor.constraint(equalToConstant: ComposeToolbarView.toolbarHeight), -// ]) -// composeToolbarView.preservesSuperviewLayoutMargins = true -// composeToolbarView.delegate = self -// -// composeToolbarBackgroundView.translatesAutoresizingMaskIntoConstraints = false -// view.insertSubview(composeToolbarBackgroundView, belowSubview: composeToolbarView) -// NSLayoutConstraint.activate([ -// composeToolbarBackgroundView.topAnchor.constraint(equalTo: composeToolbarView.topAnchor), -// composeToolbarBackgroundView.leadingAnchor.constraint(equalTo: composeToolbarView.leadingAnchor), -// composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor), -// view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor), -// ]) -// -// // FIXME: using iOS 15 toolbar for .keyboard placement -// let keyboardEventPublishers = Publishers.CombineLatest3( -// KeyboardResponderService.shared.isShow, -// KeyboardResponderService.shared.state, -// KeyboardResponderService.shared.endFrame -// ) -// -// Publishers.CombineLatest( -// keyboardEventPublishers, -// viewSafeAreaDidChange -// ) -// .sink(receiveValue: { [weak self] keyboardEvents, _ in -// guard let self = self else { return } -// -// let (isShow, state, endFrame) = keyboardEvents -// guard isShow, state == .dock else { -// UIView.animate(withDuration: 0.3) { -// self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom -// self.view.layoutIfNeeded() -// } -// return -// } -// // isShow AND dock state -// -// UIView.animate(withDuration: 0.3) { -// self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height -// self.view.layoutIfNeeded() -// } -// }) -// .store(in: &disposeBag) -// -// // bind visibility toolbar UI -// Publishers.CombineLatest( -// viewModel.selectedStatusVisibility, -// viewModel.traitCollectionDidChangePublisher -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] type, _ in -// guard let self = self else { return } -// let image = type.image(interfaceStyle: self.traitCollection.userInterfaceStyle) -// self.composeToolbarView.visibilityButton.setImage(image, for: .normal) -// self.composeToolbarView.activeVisibilityType.value = type -// } -// .store(in: &disposeBag) -// -// // bind counter -// viewModel.characterCount -// .receive(on: DispatchQueue.main) -// .sink { [weak self] characterCount in -// guard let self = self else { return } -// let count = ShareViewModel.composeContentLimit - characterCount -// self.composeToolbarView.characterCountLabel.text = "\(count)" -// switch count { -// case _ where count < 0: -// self.composeToolbarView.characterCountLabel.font = .monospacedDigitSystemFont(ofSize: 24, weight: .bold) -// self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.danger.color -// self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitExceeds(abs(count)) -// default: -// self.composeToolbarView.characterCountLabel.font = .monospacedDigitSystemFont(ofSize: 15, weight: .regular) -// self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.Label.secondary.color -// self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(count) -// } -// } -// .store(in: &disposeBag) -// -// // bind valid -// viewModel.isValid -// .receive(on: DispatchQueue.main) -// .assign(to: \.isEnabled, on: publishButton) -// .store(in: &disposeBag) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - -// viewModel.viewDidAppear.value = true -// viewModel.inputItems.value = extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? [] -// -// viewModel.composeViewModel.viewDidAppear = true - } - - override func viewSafeAreaInsetsDidChange() { - super.viewSafeAreaInsetsDidChange() - -// viewSafeAreaDidChange.send() - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - -// viewModel.traitCollectionDidChangePublisher.send() - } - -} - -//extension ComposeViewController { -// private func setupBackgroundColor(theme: Theme) { -// view.backgroundColor = theme.systemElevatedBackgroundColor -// viewModel.composeViewModel.backgroundColor = theme.systemElevatedBackgroundColor -// composeToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor -// -// let barAppearance = UINavigationBarAppearance() -// barAppearance.configureWithDefaultBackground() -// barAppearance.backgroundColor = theme.navigationBarBackgroundColor -// navigationItem.standardAppearance = barAppearance -// navigationItem.compactAppearance = barAppearance -// navigationItem.scrollEdgeAppearance = barAppearance -// } -// -// private func showDismissConfirmAlertController() { -// let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) // can not use alert in extension -// let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { _ in -// self.extensionContext?.cancelRequest(withError: ShareViewModel.ShareError.userCancelShare) -// } -// alertController.addAction(discardAction) -// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .cancel, handler: nil) -// alertController.addAction(okAction) -// self.present(alertController, animated: true, completion: nil) -// } -//} -// -extension ComposeViewController { - @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) { - logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - -// showDismissConfirmAlertController() - } - - @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) { - logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - -// viewModel.isPublishing.value = true -// -// viewModel.publish() -// .delay(for: 2, scheduler: DispatchQueue.main) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] completion in -// guard let self = self else { return } -// self.viewModel.isPublishing.value = false -// -// switch completion { -// case .failure: -// let alertController = UIAlertController( -// title: L10n.Common.Alerts.PublishPostFailure.title, -// message: L10n.Common.Alerts.PublishPostFailure.message, -// preferredStyle: .actionSheet // can not use alert in extension -// ) -// let okAction = UIAlertAction( -// title: L10n.Common.Controls.Actions.ok, -// style: .cancel, -// handler: nil -// ) -// alertController.addAction(okAction) -// self.present(alertController, animated: true, completion: nil) -// case .finished: -// self.publishButton.setTitle(L10n.Common.Controls.Actions.done, for: .normal) -// self.publishButton.isUserInteractionEnabled = false -// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in -// guard let self = self else { return } -// self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) -// } -// } -// } receiveValue: { response in -// // do nothing -// } -// .store(in: &disposeBag) - } -} - -//// MARK - ComposeToolbarViewDelegate -//extension ComposeViewController: ComposeToolbarViewDelegate { -// -// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton) { -// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") -// -// withAnimation { -// viewModel.composeViewModel.isContentWarningComposing.toggle() -// } -// } -// -// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) { -// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") -// -// viewModel.selectedStatusVisibility.value = type -// } -// -//} -// -//// MARK: - UIAdaptivePresentationControllerDelegate -//extension ComposeViewController: UIAdaptivePresentationControllerDelegate { -// -// func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { -// return viewModel.shouldDismiss.value -// } -// -// func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) { -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) -// showDismissConfirmAlertController() -// -// } -// -// func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) -// } -// -//} diff --git a/ShareActionExtension/Scene/ComposeViewModel.swift b/ShareActionExtension/Scene/ComposeViewModel.swift deleted file mode 100644 index 93515b7dc..000000000 --- a/ShareActionExtension/Scene/ComposeViewModel.swift +++ /dev/null @@ -1,416 +0,0 @@ -// -// ComposeViewModel.swift -// MastodonShareAction -// -// Created by MainasuK Cirno on 2021-7-16. -// - -import os.log -import Foundation -import Combine -import CoreData -import CoreDataStack -import MastodonSDK -import SwiftUI -import UniformTypeIdentifiers -import MastodonAsset -import MastodonLocalization -import MastodonUI -import MastodonCore - -final class ComposeViewModel { - - let logger = Logger(subsystem: "ComposeViewModel", category: "ViewModel") - - var disposeBag = Set() - - static let composeContentLimit: Int = 500 - - // input - let context: AppContext - -// private var coreDataStack: CoreDataStack? -// var managedObjectContext: NSManagedObjectContext? -// var api: APIService? -// -// var inputItems = CurrentValueSubject<[NSExtensionItem], Never>([]) -// let viewDidAppear = CurrentValueSubject(false) -// let traitCollectionDidChangePublisher = CurrentValueSubject(Void()) // use CurrentValueSubject to make initial event emit -// let selectedStatusVisibility = CurrentValueSubject(.public) -// -// // output -// let authentication = CurrentValueSubject?, Never>(nil) -// let isFetchAuthentication = CurrentValueSubject(true) -// let isPublishing = CurrentValueSubject(false) -// let isBusy = CurrentValueSubject(true) -// let isValid = CurrentValueSubject(false) -// let shouldDismiss = CurrentValueSubject(true) -// let composeViewModel = ComposeViewModel() -// let characterCount = CurrentValueSubject(0) - - init(context: AppContext) { - self.context = context - // end init - -// viewDidAppear.receive(on: DispatchQueue.main) -// .removeDuplicates() -// .sink { [weak self] viewDidAppear in -// guard let self = self else { return } -// guard viewDidAppear else { return } -// self.setupCoreData() -// } -// .store(in: &disposeBag) -// -// Publishers.CombineLatest( -// inputItems.removeDuplicates(), -// viewDidAppear.removeDuplicates() -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] inputItems, _ in -// guard let self = self else { return } -// self.parse(inputItems: inputItems) -// } -// .store(in: &disposeBag) -// -// // bind authentication loading state -// authentication -// .map { result in result == nil } -// .assign(to: \.value, on: isFetchAuthentication) -// .store(in: &disposeBag) -// -// // bind user locked state -// authentication -// .compactMap { result -> Bool? in -// guard let result = result else { return nil } -// switch result { -// case .success(let authentication): -// return authentication.user.locked -// case .failure: -// return nil -// } -// } -// .map { locked -> ComposeToolbarView.VisibilitySelectionType in -// locked ? .private : .public -// } -// .assign(to: \.value, on: selectedStatusVisibility) -// .store(in: &disposeBag) -// -// // bind author -// authentication -// .receive(on: DispatchQueue.main) -// .sink { [weak self] result in -// guard let self = self else { return } -// guard let result = result else { return } -// switch result { -// case .success(let authentication): -// self.composeViewModel.avatarImageURL = authentication.user.avatarImageURL() -// self.composeViewModel.authorName = authentication.user.displayNameWithFallback -// self.composeViewModel.authorUsername = "@" + authentication.user.username -// case .failure: -// self.composeViewModel.avatarImageURL = nil -// self.composeViewModel.authorName = " " -// self.composeViewModel.authorUsername = " " -// } -// } -// .store(in: &disposeBag) -// -// // bind authentication to compose view model -// authentication -// .map { result -> MastodonAuthentication? in -// guard let result = result else { return nil } -// switch result { -// case .success(let authentication): -// return authentication -// case .failure: -// return nil -// } -// } -// .assign(to: &composeViewModel.$authentication) -// -// // bind isBusy -// Publishers.CombineLatest( -// isFetchAuthentication, -// isPublishing -// ) -// .receive(on: DispatchQueue.main) -// .map { $0 || $1 } -// .assign(to: \.value, on: isBusy) -// .store(in: &disposeBag) -// -// // pass initial i18n string -// composeViewModel.statusPlaceholder = L10n.Scene.Compose.contentInputPlaceholder -// composeViewModel.contentWarningPlaceholder = L10n.Scene.Compose.ContentWarning.placeholder -// composeViewModel.toolbarHeight = ComposeToolbarView.toolbarHeight -// -// // bind compose bar button item UI state -// let isComposeContentEmpty = composeViewModel.$statusContent -// .map { $0.isEmpty } -// -// isComposeContentEmpty -// .assign(to: \.value, on: shouldDismiss) -// .store(in: &disposeBag) -// -// let isComposeContentValid = composeViewModel.$characterCount -// .map { characterCount -> Bool in -// return characterCount <= ShareViewModel.composeContentLimit -// } -// let isMediaEmpty = composeViewModel.$attachmentViewModels -// .map { $0.isEmpty } -// let isMediaUploadAllSuccess = composeViewModel.$attachmentViewModels -// .map { viewModels in -// viewModels.allSatisfy { $0.uploadStateMachineSubject.value is StatusAttachmentViewModel.UploadState.Finish } -// } -// -// let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4( -// isComposeContentEmpty, -// isComposeContentValid, -// isMediaEmpty, -// isMediaUploadAllSuccess -// ) -// .map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in -// if isMediaEmpty { -// return isComposeContentValid && !isComposeContentEmpty -// } else { -// return isComposeContentValid && isMediaUploadAllSuccess -// } -// } -// .eraseToAnyPublisher() -// -// let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest( -// isComposeContentEmpty, -// isComposeContentValid -// ) -// .map { isComposeContentEmpty, isComposeContentValid -> Bool in -// return isComposeContentValid && !isComposeContentEmpty -// } -// .eraseToAnyPublisher() -// -// Publishers.CombineLatest( -// isPublishBarButtonItemEnabledPrecondition1, -// isPublishBarButtonItemEnabledPrecondition2 -// ) -// .map { $0 && $1 } -// .assign(to: \.value, on: isValid) -// .store(in: &disposeBag) -// -// // bind counter -// composeViewModel.$characterCount -// .assign(to: \.value, on: characterCount) -// .store(in: &disposeBag) -// -// // setup theme -// setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) -// ThemeService.shared.currentTheme -// .receive(on: DispatchQueue.main) -// .sink { [weak self] theme in -// guard let self = self else { return } -// self.setupBackgroundColor(theme: theme) -// } -// .store(in: &disposeBag) - } - - private func setupBackgroundColor(theme: Theme) { -// composeViewModel.contentWarningBackgroundColor = Color(theme.contentWarningOverlayBackgroundColor) - } - -} - -//extension ShareViewModel { -// enum ShareError: Error { -// case `internal`(error: Error) -// case userCancelShare -// case missingAuthentication -// } -//} - -extension ComposeViewModel { -// private func setupCoreData() { -// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") -// DispatchQueue.global().async { -// let _coreDataStack = CoreDataStack() -// self.coreDataStack = _coreDataStack -// self.managedObjectContext = _coreDataStack.persistentContainer.viewContext -// -// _coreDataStack.didFinishLoad -// .receive(on: RunLoop.main) -// .sink { [weak self] didFinishLoad in -// guard let self = self else { return } -// guard didFinishLoad else { return } -// guard let managedObjectContext = self.managedObjectContext else { return } -// -// -// self.api = APIService(backgroundManagedObjectContext: _coreDataStack.newTaskContext()) -// -// self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch authentication…") -// managedObjectContext.perform { -// do { -// let request = MastodonAuthentication.sortedFetchRequest -// let authentications = try managedObjectContext.fetch(request) -// let authentication = authentications.sorted(by: { $0.activedAt > $1.activedAt }).first -// guard let activeAuthentication = authentication else { -// self.authentication.value = .failure(ShareError.missingAuthentication) -// return -// } -// self.authentication.value = .success(activeAuthentication) -// self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch authentication success \(activeAuthentication.userID)") -// } catch { -// self.authentication.value = .failure(ShareError.internal(error: error)) -// self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch authentication fail \(error.localizedDescription)") -// assertionFailure(error.localizedDescription) -// } -// } -// } -// .store(in: &self.disposeBag) -// } -// } -} - -//extension ShareViewModel { -// func parse(inputItems: [NSExtensionItem]) { -// var itemProviders: [NSItemProvider] = [] -// -// for item in inputItems { -// itemProviders.append(contentsOf: item.attachments ?? []) -// } -// -// let _textProvider = itemProviders.first { provider in -// return provider.hasRepresentationConforming(toTypeIdentifier: UTType.plainText.identifier, fileOptions: []) -// } -// -// let _urlProvider = itemProviders.first { provider in -// return provider.hasRepresentationConforming(toTypeIdentifier: UTType.url.identifier, fileOptions: []) -// } -// -// let _movieProvider = itemProviders.first { provider in -// return provider.hasRepresentationConforming(toTypeIdentifier: UTType.movie.identifier, fileOptions: []) -// } -// -// let imageProviders = itemProviders.filter { provider in -// return provider.hasRepresentationConforming(toTypeIdentifier: UTType.image.identifier, fileOptions: []) -// } -// -// Task { @MainActor in -// async let text = ShareViewModel.loadText(textProvider: _textProvider) -// async let url = ShareViewModel.loadURL(textProvider: _urlProvider) -// -// let content = await [text, url] -// .compactMap { $0 } -// .joined(separator: " ") -// self.composeViewModel.statusContent = content -// } -// -// guard let api = self.api else { return } -// -// if let movieProvider = _movieProvider { -// composeViewModel.setupAttachmentViewModels([ -// StatusAttachmentViewModel(api: api, itemProvider: movieProvider) -// ]) -// } else if !imageProviders.isEmpty { -// let viewModels = imageProviders.map { provider in -// StatusAttachmentViewModel(api: api, itemProvider: provider) -// } -// composeViewModel.setupAttachmentViewModels(viewModels) -// } -// -// } -// -// private static func loadText(textProvider: NSItemProvider?) async -> String? { -// guard let textProvider = textProvider else { return nil } -// do { -// let item = try await textProvider.loadItem(forTypeIdentifier: UTType.plainText.identifier) -// guard let text = item as? String else { return nil } -// return text -// } catch { -// return nil -// } -// } -// -// private static func loadURL(textProvider: NSItemProvider?) async -> String? { -// guard let textProvider = textProvider else { return nil } -// do { -// let item = try await textProvider.loadItem(forTypeIdentifier: UTType.url.identifier) -// guard let url = item as? URL else { return nil } -// return url.absoluteString -// } catch { -// return nil -// } -// } -// -//} -// -//extension ShareViewModel { -// func publish() -> AnyPublisher, Error> { -// guard let authentication = composeViewModel.authentication, -// let api = self.api -// else { -// return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher() -// } -// let authenticationBox = MastodonAuthenticationBox( -// authenticationRecord: .init(objectID: authentication.objectID), -// domain: authentication.domain, -// userID: authentication.userID, -// appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken), -// userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken) -// ) -// -// let domain = authentication.domain -// let attachmentViewModels = composeViewModel.attachmentViewModels -// let mediaIDs = attachmentViewModels.compactMap { viewModel in -// viewModel.attachment.value?.id -// } -// let sensitive: Bool = composeViewModel.isContentWarningComposing -// let spoilerText: String? = { -// let text = composeViewModel.contentWarningContent -// guard !text.isEmpty else { return nil } -// return text -// }() -// let visibility = selectedStatusVisibility.value.visibility -// -// let updateMediaQuerySubscriptions: [AnyPublisher, Error>] = { -// var subscriptions: [AnyPublisher, Error>] = [] -// for attachmentViewModel in attachmentViewModels { -// guard let attachmentID = attachmentViewModel.attachment.value?.id else { continue } -// let description = attachmentViewModel.descriptionContent.trimmingCharacters(in: .whitespacesAndNewlines) -// guard !description.isEmpty else { continue } -// let query = Mastodon.API.Media.UpdateMediaQuery( -// file: nil, -// thumbnail: nil, -// description: description, -// focus: nil -// ) -// let subscription = api.updateMedia( -// domain: domain, -// attachmentID: attachmentID, -// query: query, -// mastodonAuthenticationBox: authenticationBox -// ) -// subscriptions.append(subscription) -// } -// return subscriptions -// }() -// -// let status = composeViewModel.statusContent -// -// return Publishers.MergeMany(updateMediaQuerySubscriptions) -// .collect() -// .asyncMap { attachments in -// let query = Mastodon.API.Statuses.PublishStatusQuery( -// status: status, -// mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs, -// pollOptions: nil, -// pollExpiresIn: nil, -// inReplyToID: nil, -// sensitive: sensitive, -// spoilerText: spoilerText, -// visibility: visibility -// ) -// return try await api.publishStatus( -// domain: domain, -// idempotencyKey: nil, // FIXME: -// query: query, -// authenticationBox: authenticationBox -// ) -// } -// .eraseToAnyPublisher() -// } -//} diff --git a/ShareActionExtension/Scene/ShareViewController.swift b/ShareActionExtension/Scene/ShareViewController.swift new file mode 100644 index 000000000..7757452cd --- /dev/null +++ b/ShareActionExtension/Scene/ShareViewController.swift @@ -0,0 +1,330 @@ +// +// ShareViewController.swift +// ShareActionExtension +// +// Created by MainasuK on 2022/11/13. +// + +import os.log +import UIKit +import Combine +import CoreDataStack +import MastodonCore +import MastodonUI +import MastodonAsset +import MastodonLocalization +import UniformTypeIdentifiers + +final class ShareViewController: UIViewController { + + let logger = Logger(subsystem: "ShareViewController", category: "ViewController") + + var disposeBag = Set() + + let context = AppContext() + private(set) lazy var viewModel = ShareViewModel(context: context) + + let publishButton: UIButton = { + let button = RoundedEdgesButton(type: .custom) + button.cornerRadius = 10 + button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16) // set 28pt height + button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold) + button.setTitle(L10n.Scene.Compose.composeAction, for: .normal) + return button + }() + private func configurePublishButtonApperance() { + publishButton.adjustsImageWhenHighlighted = false + publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color), for: .normal) + publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color.withAlphaComponent(0.5)), for: .highlighted) + publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled) + publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal) + } + + private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ShareViewController.cancelBarButtonItemPressed(_:))) + private(set) lazy var publishBarButtonItem: UIBarButtonItem = { + let barButtonItem = UIBarButtonItem(customView: publishButton) + publishButton.addTarget(self, action: #selector(ShareViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) + return barButtonItem + }() + let activityIndicatorBarButtonItem: UIBarButtonItem = { + let indicatorView = UIActivityIndicatorView(style: .medium) + let barButtonItem = UIBarButtonItem(customView: indicatorView) + indicatorView.startAnimating() + return barButtonItem + }() + + private var composeContentViewModel: ComposeContentViewModel? + private var composeContentViewController: ComposeContentViewController? + + let notSignInLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .subheadline) + label.textColor = .secondaryLabel + label.text = "No Available Account" // TODO: i18n + return label + }() + +} + +extension ShareViewController { + override func viewDidLoad() { + super.viewDidLoad() + + setupTheme(theme: ThemeService.shared.currentTheme.value) + ThemeService.shared.apply(theme: ThemeService.shared.currentTheme.value) + ThemeService.shared.currentTheme + .receive(on: DispatchQueue.main) + .sink { [weak self] theme in + guard let self = self else { return } + self.setupTheme(theme: theme) + } + .store(in: &disposeBag) + + view.backgroundColor = .systemBackground + title = L10n.Scene.Compose.Title.newPost + + navigationItem.leftBarButtonItem = cancelBarButtonItem + navigationItem.rightBarButtonItem = publishBarButtonItem + + do { + guard let authContext = try setupAuthContext() else { + setupHintLabel() + return + } + viewModel.authContext = authContext + let composeContentViewModel = ComposeContentViewModel( + context: context, + authContext: authContext, + kind: .post + ) + let composeContentViewController = ComposeContentViewController() + composeContentViewController.viewModel = composeContentViewModel + addChild(composeContentViewController) + composeContentViewController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(composeContentViewController.view) + NSLayoutConstraint.activate([ + composeContentViewController.view.topAnchor.constraint(equalTo: view.topAnchor), + composeContentViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + composeContentViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + composeContentViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + composeContentViewController.didMove(toParent: self) + + self.composeContentViewModel = composeContentViewModel + self.composeContentViewController = composeContentViewController + + Task { @MainActor in + let inputItems = self.extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? [] + await load(inputItems: inputItems) + } // end Task + } catch { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): error: \(error.localizedDescription)") + } + + viewModel.$isPublishing + .receive(on: DispatchQueue.main) + .sink { [weak self] isBusy in + guard let self = self else { return } + self.navigationItem.rightBarButtonItem = isBusy ? self.activityIndicatorBarButtonItem : self.publishBarButtonItem + } + .store(in: &disposeBag) + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + configurePublishButtonApperance() + } +} + +extension ShareViewController { + @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) { + logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + + extensionContext?.cancelRequest(withError: NSError(domain: "org.joinmastodon.app.ShareActionExtension", code: -1)) + } + + @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) { + logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + + + Task { @MainActor in + viewModel.isPublishing = true + do { + guard let statusPublisher = try composeContentViewModel?.statusPublisher(), + let authContext = viewModel.authContext + else { + throw AppError.badRequest + } + + _ = try await statusPublisher.publish(api: context.apiService, authContext: authContext) + + self.publishButton.setTitle(L10n.Common.Controls.Actions.done, for: .normal) + try await Task.sleep(nanoseconds: 1 * .second) + + self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + + } catch { + let alertController = UIAlertController.standardAlert(of: error) + present(alertController, animated: true) + return + } + viewModel.isPublishing = false + + } + } +} + +extension ShareViewController { + private func setupAuthContext() throws -> AuthContext? { + let request = MastodonAuthentication.activeSortedFetchRequest // use active order + let _authentication = try context.managedObjectContext.fetch(request).first + let _authContext = _authentication.flatMap { AuthContext(authentication: $0) } + return _authContext + } + + private func setupHintLabel() { + notSignInLabel.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(notSignInLabel) + NSLayoutConstraint.activate([ + notSignInLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + notSignInLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + } + + private func setupTheme(theme: Theme) { + view.backgroundColor = theme.systemElevatedBackgroundColor + + let barAppearance = UINavigationBarAppearance() + barAppearance.configureWithDefaultBackground() + barAppearance.backgroundColor = theme.navigationBarBackgroundColor + navigationItem.standardAppearance = barAppearance + navigationItem.compactAppearance = barAppearance + navigationItem.scrollEdgeAppearance = barAppearance + } + + private func showDismissConfirmAlertController() { + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) // can not use alert in extension + let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { _ in + self.extensionContext?.cancelRequest(withError: ShareError.userCancelShare) + } + alertController.addAction(discardAction) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .cancel, handler: nil) + alertController.addAction(okAction) + self.present(alertController, animated: true, completion: nil) + } +} + +// MARK: - UIAdaptivePresentationControllerDelegate +extension ShareViewController: UIAdaptivePresentationControllerDelegate { + + func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { + return composeContentViewModel?.shouldDismiss ?? true + } + + func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + showDismissConfirmAlertController() + } + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension ShareViewController { + + private func load(inputItems: [NSExtensionItem]) async { + guard let composeContentViewModel = self.composeContentViewModel, + let authContext = viewModel.authContext + else { + assertionFailure() + return + } + var itemProviders: [NSItemProvider] = [] + + for item in inputItems { + itemProviders.append(contentsOf: item.attachments ?? []) + } + + let _textProvider = itemProviders.first { provider in + return provider.hasRepresentationConforming(toTypeIdentifier: UTType.plainText.identifier, fileOptions: []) + } + + let _urlProvider = itemProviders.first { provider in + return provider.hasRepresentationConforming(toTypeIdentifier: UTType.url.identifier, fileOptions: []) + } + + let _movieProvider = itemProviders.first { provider in + return provider.hasRepresentationConforming(toTypeIdentifier: UTType.movie.identifier, fileOptions: []) + } + + let imageProviders = itemProviders.filter { provider in + return provider.hasRepresentationConforming(toTypeIdentifier: UTType.image.identifier, fileOptions: []) + } + + async let text = ShareViewController.loadText(textProvider: _textProvider) + async let url = ShareViewController.loadURL(textProvider: _urlProvider) + + let content = await [text, url] + .compactMap { $0 } + .joined(separator: " ") + // passby the viewModel `content` value + if !content.isEmpty { + composeContentViewModel.content = content + " " + composeContentViewModel.contentMetaText?.textView.insertText(content + " ") + } + + if let movieProvider = _movieProvider { + let attachmentViewModel = AttachmentViewModel( + api: context.apiService, + authContext: authContext, + input: .itemProvider(movieProvider), + delegate: composeContentViewModel + ) + composeContentViewModel.attachmentViewModels.append(attachmentViewModel) + } else if !imageProviders.isEmpty { + let attachmentViewModels = imageProviders.map { provider in + AttachmentViewModel( + api: context.apiService, + authContext: authContext, + input: .itemProvider(provider), + delegate: composeContentViewModel + ) + } + composeContentViewModel.attachmentViewModels.append(contentsOf: attachmentViewModels) + } + } + + private static func loadText(textProvider: NSItemProvider?) async -> String? { + guard let textProvider = textProvider else { return nil } + do { + let item = try await textProvider.loadItem(forTypeIdentifier: UTType.plainText.identifier) + guard let text = item as? String else { return nil } + return text + } catch { + return nil + } + } + + private static func loadURL(textProvider: NSItemProvider?) async -> String? { + guard let textProvider = textProvider else { return nil } + do { + let item = try await textProvider.loadItem(forTypeIdentifier: UTType.url.identifier) + guard let url = item as? URL else { return nil } + return url.absoluteString + } catch { + return nil + } + } + +} + +extension ShareViewController { + enum ShareError: Error { + case `internal`(error: Error) + case userCancelShare + case missingAuthentication + } +} diff --git a/ShareActionExtension/Scene/ShareViewModel.swift b/ShareActionExtension/Scene/ShareViewModel.swift new file mode 100644 index 000000000..ef8e200a6 --- /dev/null +++ b/ShareActionExtension/Scene/ShareViewModel.swift @@ -0,0 +1,43 @@ +// +// ShareViewModel.swift +// MastodonShareAction +// +// Created by MainasuK Cirno on 2021-7-16. +// + +import os.log +import Foundation +import Combine +import CoreData +import CoreDataStack +import MastodonSDK +import SwiftUI +import UniformTypeIdentifiers +import MastodonAsset +import MastodonLocalization +import MastodonUI +import MastodonCore + +final class ShareViewModel { + + let logger = Logger(subsystem: "ComposeViewModel", category: "ViewModel") + + var disposeBag = Set() + + // input + let context: AppContext + @Published var authContext: AuthContext? + + @Published var isPublishing = false + + // output + + init( + context: AppContext + ) { + self.context = context + // end init + + } + +} From 82abc68486eaac1977ba2f3fe02139e0b2a141a4 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 14 Nov 2022 00:06:44 +0800 Subject: [PATCH 153/224] chore: code clean --- Mastodon.xcodeproj/project.pbxproj | 26 -- .../Scene/View/ComposeToolbarView.swift | 262 ------------------ .../Scene/View/ComposeView.swift | 151 ---------- .../Scene/View/ComposeViewModel.swift | 130 --------- .../Scene/View/ContentWarningEditorView.swift | 48 ---- .../Scene/View/StatusAttachmentView.swift | 113 -------- ...tatusAttachmentViewModel+UploadState.swift | 131 --------- .../View/StatusAttachmentViewModel.swift | 227 --------------- .../Scene/View/StatusAuthorView.swift | 45 --- .../Scene/View/StatusEditorView.swift | 105 ------- 10 files changed, 1238 deletions(-) delete mode 100644 ShareActionExtension/Scene/View/ComposeToolbarView.swift delete mode 100644 ShareActionExtension/Scene/View/ComposeView.swift delete mode 100644 ShareActionExtension/Scene/View/ComposeViewModel.swift delete mode 100644 ShareActionExtension/Scene/View/ContentWarningEditorView.swift delete mode 100644 ShareActionExtension/Scene/View/StatusAttachmentView.swift delete mode 100644 ShareActionExtension/Scene/View/StatusAttachmentViewModel+UploadState.swift delete mode 100644 ShareActionExtension/Scene/View/StatusAttachmentViewModel.swift delete mode 100644 ShareActionExtension/Scene/View/StatusAuthorView.swift delete mode 100644 ShareActionExtension/Scene/View/StatusEditorView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index fc8e6ff25..434926d94 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -948,7 +948,6 @@ DBB525632612C988002F1F29 /* MeProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeProfileViewModel.swift; sourceTree = ""; }; DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPostIntentHandler.swift; sourceTree = ""; }; DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = ""; }; - DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = ""; }; DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = ""; }; DBC3872329214121001EC0FD /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1028,15 +1027,7 @@ DBFEEC98279BDCDE004F81DD /* ProfileAboutViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileAboutViewModel.swift; sourceTree = ""; }; DBFEEC9A279BDDD9004F81DD /* ProfileAboutViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileAboutViewModel+Diffable.swift"; sourceTree = ""; }; DBFEEC9C279C12C1004F81DD /* ProfileFieldEditCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldEditCollectionViewCell.swift; sourceTree = ""; }; - DBFEF05526A576EE006D7ED1 /* StatusEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditorView.swift; sourceTree = ""; }; - DBFEF05626A576EE006D7ED1 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; }; - DBFEF05726A576EE006D7ED1 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; }; - DBFEF05826A576EE006D7ED1 /* ContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningEditorView.swift; sourceTree = ""; }; - DBFEF05926A576EE006D7ED1 /* StatusAuthorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAuthorView.swift; sourceTree = ""; }; - DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentView.swift; sourceTree = ""; }; - DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentViewModel.swift; sourceTree = ""; }; DBFEF06726A58D07006D7ED1 /* ShareActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareActionExtension.entitlements; sourceTree = ""; }; - DBFEF06C26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusAttachmentViewModel+UploadState.swift"; sourceTree = ""; }; DDB1B139FA8EA26F510D58B6 /* Pods-AppShared.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.asdk - release.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.asdk - release.xcconfig"; sourceTree = ""; }; DF65937EC1FF64462BC002EE /* Pods-MastodonTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.profile.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.profile.xcconfig"; sourceTree = ""; }; E5C7236E58D14A0322FE00F2 /* Pods-Mastodon-MastodonUITests.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.asdk - debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.asdk - debug.xcconfig"; sourceTree = ""; }; @@ -2666,26 +2657,9 @@ path = Cell; sourceTree = ""; }; - DBFEF05426A576EE006D7ED1 /* View */ = { - isa = PBXGroup; - children = ( - DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */, - DBFEF05526A576EE006D7ED1 /* StatusEditorView.swift */, - DBFEF05626A576EE006D7ED1 /* ComposeView.swift */, - DBFEF05726A576EE006D7ED1 /* ComposeViewModel.swift */, - DBFEF05826A576EE006D7ED1 /* ContentWarningEditorView.swift */, - DBFEF05926A576EE006D7ED1 /* StatusAuthorView.swift */, - DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */, - DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */, - DBFEF06C26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift */, - ); - path = View; - sourceTree = ""; - }; DBFEF06126A57721006D7ED1 /* Scene */ = { isa = PBXGroup; children = ( - DBFEF05426A576EE006D7ED1 /* View */, DBC6462226A1712000B0E31B /* ShareViewModel.swift */, DBC3872329214121001EC0FD /* ShareViewController.swift */, ); diff --git a/ShareActionExtension/Scene/View/ComposeToolbarView.swift b/ShareActionExtension/Scene/View/ComposeToolbarView.swift deleted file mode 100644 index 07833ac90..000000000 --- a/ShareActionExtension/Scene/View/ComposeToolbarView.swift +++ /dev/null @@ -1,262 +0,0 @@ -// -// ComposeToolbarView.swift -// ShareActionExtension -// -// Created by MainasuK Cirno on 2021-7-19. -// - -import os.log -import UIKit -import Combine -import MastodonSDK -import MastodonAsset -import MastodonLocalization -import MastodonCore -import MastodonUI - -protocol ComposeToolbarViewDelegate: AnyObject { - func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton) - func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) -} - -final class ComposeToolbarView: UIView { - - var disposeBag = Set() - - static let toolbarButtonSize: CGSize = CGSize(width: 44, height: 44) - static let toolbarHeight: CGFloat = 44 - - weak var delegate: ComposeToolbarViewDelegate? - - let contentWarningButton: UIButton = { - let button = HighlightDimmableButton() - ComposeToolbarView.configureToolbarButtonAppearance(button: button) - button.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal) - button.accessibilityLabel = L10n.Scene.Compose.Accessibility.enableContentWarning - return button - }() - - let visibilityButton: UIButton = { - let button = HighlightDimmableButton() - ComposeToolbarView.configureToolbarButtonAppearance(button: button) - button.setImage(UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)), for: .normal) - button.accessibilityLabel = L10n.Scene.Compose.Accessibility.postVisibilityMenu - return button - }() - - let characterCountLabel: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: 15, weight: .regular) - label.text = "500" - label.textColor = Asset.Colors.Label.secondary.color - label.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(500) - return label - }() - - let activeVisibilityType = CurrentValueSubject(.public) - - override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension ComposeToolbarView { - - private func _init() { - setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) - ThemeService.shared.currentTheme - .receive(on: DispatchQueue.main) - .sink { [weak self] theme in - guard let self = self else { return } - self.setupBackgroundColor(theme: theme) - } - .store(in: &disposeBag) - - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.spacing = 0 - stackView.distribution = .fillEqually - stackView.translatesAutoresizingMaskIntoConstraints = false - addSubview(stackView) - NSLayoutConstraint.activate([ - stackView.centerYAnchor.constraint(equalTo: centerYAnchor), - layoutMarginsGuide.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 8), // tweak button margin offset - ]) - - let buttons = [ - contentWarningButton, - visibilityButton, - ] - buttons.forEach { button in - button.translatesAutoresizingMaskIntoConstraints = false - stackView.addArrangedSubview(button) - NSLayoutConstraint.activate([ - button.widthAnchor.constraint(equalToConstant: 44), - button.heightAnchor.constraint(equalToConstant: 44), - ]) - } - - characterCountLabel.translatesAutoresizingMaskIntoConstraints = false - addSubview(characterCountLabel) - NSLayoutConstraint.activate([ - characterCountLabel.topAnchor.constraint(equalTo: topAnchor), - characterCountLabel.leadingAnchor.constraint(greaterThanOrEqualTo: stackView.trailingAnchor, constant: 8), - characterCountLabel.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), - characterCountLabel.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - characterCountLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) - - contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.contentWarningButtonDidPressed(_:)), for: .touchUpInside) - visibilityButton.menu = createVisibilityContextMenu(interfaceStyle: traitCollection.userInterfaceStyle) - visibilityButton.showsMenuAsPrimaryAction = true - - updateToolbarButtonUserInterfaceStyle() - - // update menu when selected visibility type changed - activeVisibilityType - .receive(on: RunLoop.main) - .sink { [weak self] type in - guard let self = self else { return } - self.visibilityButton.menu = self.createVisibilityContextMenu(interfaceStyle: self.traitCollection.userInterfaceStyle) - } - .store(in: &disposeBag) - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - updateToolbarButtonUserInterfaceStyle() - } - -} - -extension ComposeToolbarView { - private func setupBackgroundColor(theme: Theme) { - backgroundColor = theme.composeToolbarBackgroundColor - } -} - -extension ComposeToolbarView { - enum MediaSelectionType: String { - case camera - case photoLibrary - case browse - } - - enum VisibilitySelectionType: String, CaseIterable { - case `public` - // TODO: remove unlisted option from codebase - // case unlisted - case `private` - case direct - - 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 - } - } - - func image(interfaceStyle: UIUserInterfaceStyle) -> UIImage { - switch self { - case .public: return UIImage(systemName: "globe", withConfiguration: UIImage.SymbolConfiguration(pointSize: 19, weight: .medium))! - // case .unlisted: return UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))! - case .private: - switch interfaceStyle { - case .light: return UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))! - default: return UIImage(systemName: "person.3.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))! - } - case .direct: return UIImage(systemName: "at", withConfiguration: UIImage.SymbolConfiguration(pointSize: 19, weight: .regular))! - } - } - - var visibility: Mastodon.Entity.Status.Visibility { - switch self { - case .public: return .public - // case .unlisted: return .unlisted - case .private: return .private - case .direct: return .direct - } - } - } -} - -extension ComposeToolbarView { - - private static func configureToolbarButtonAppearance(button: UIButton) { - button.tintColor = ThemeService.tintColor - button.setBackgroundImage(.placeholder(size: ComposeToolbarView.toolbarButtonSize, color: .systemFill), for: .highlighted) - button.layer.masksToBounds = true - button.layer.cornerRadius = 5 - button.layer.cornerCurve = .continuous - } - - private func updateToolbarButtonUserInterfaceStyle() { - switch traitCollection.userInterfaceStyle { - case .light: - contentWarningButton.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))!, for: .normal) - - case .dark: - contentWarningButton.setImage(UIImage(systemName: "exclamationmark.shield.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))!, for: .normal) - - default: - assertionFailure() - } - - visibilityButton.menu = createVisibilityContextMenu(interfaceStyle: traitCollection.userInterfaceStyle) - } - - private func createVisibilityContextMenu(interfaceStyle: UIUserInterfaceStyle) -> UIMenu { - let children: [UIMenuElement] = VisibilitySelectionType.allCases.map { type in - let state: UIMenuElement.State = activeVisibilityType.value == type ? .on : .off - return UIAction(title: type.title, image: type.image(interfaceStyle: interfaceStyle), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { [weak self] action in - guard let self = self else { return } - os_log(.info, "%{public}s[%{public}ld], %{public}s: visibilitySelectionType: %s", ((#file as NSString).lastPathComponent), #line, #function, type.rawValue) - self.delegate?.composeToolbarView(self, visibilityButtonDidPressed: self.visibilityButton, visibilitySelectionType: type) - } - } - return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children) - } - -} - -extension ComposeToolbarView { - - @objc private func contentWarningButtonDidPressed(_ sender: UIButton) { - os_log(.info, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - delegate?.composeToolbarView(self, contentWarningButtonDidPressed: sender) - } - -} - -#if canImport(SwiftUI) && DEBUG -import SwiftUI - -struct ComposeToolbarView_Previews: PreviewProvider { - - static var previews: some View { - UIViewPreview(width: 375) { - let toolbarView = ComposeToolbarView() - toolbarView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - toolbarView.widthAnchor.constraint(equalToConstant: 375).priority(.defaultHigh), - toolbarView.heightAnchor.constraint(equalToConstant: 64).priority(.defaultHigh), - ]) - return toolbarView - } - .previewLayout(.fixed(width: 375, height: 100)) - } - -} - -#endif - diff --git a/ShareActionExtension/Scene/View/ComposeView.swift b/ShareActionExtension/Scene/View/ComposeView.swift deleted file mode 100644 index a688d6492..000000000 --- a/ShareActionExtension/Scene/View/ComposeView.swift +++ /dev/null @@ -1,151 +0,0 @@ -// -// ComposeView.swift -// -// -// Created by MainasuK Cirno on 2021-7-16. -// - -import UIKit -import SwiftUI - -public struct ComposeView: View { - - @EnvironmentObject var viewModel: ComposeViewModel - @State var statusEditorViewWidth: CGFloat = .zero - - let horizontalMargin: CGFloat = 20 - - public init() { } - - public var body: some View { - GeometryReader { proxy in - List { - // Content Warning - if viewModel.isContentWarningComposing { - ContentWarningEditorView( - contentWarningContent: $viewModel.contentWarningContent, - placeholder: viewModel.contentWarningPlaceholder - ) - .padding(EdgeInsets(top: 6, leading: horizontalMargin, bottom: 6, trailing: horizontalMargin)) - .background(viewModel.contentWarningBackgroundColor) - .transition(.opacity) - .listRow(backgroundColor: Color(viewModel.backgroundColor)) - } - - // Author - StatusAuthorView( - avatarImageURL: viewModel.avatarImageURL, - name: viewModel.authorName, - username: viewModel.authorUsername - ) - .padding(EdgeInsets(top: 20, leading: horizontalMargin, bottom: 16, trailing: horizontalMargin)) - .listRow(backgroundColor: Color(viewModel.backgroundColor)) - - // Editor - StatusEditorView( - string: $viewModel.statusContent, - placeholder: viewModel.statusPlaceholder, - width: statusEditorViewWidth, - attributedString: viewModel.statusContentAttributedString, - keyboardType: .twitter, - viewDidAppear: $viewModel.viewDidAppear - ) - .frame(width: statusEditorViewWidth) - .frame(minHeight: 100) - .padding(EdgeInsets(top: 0, leading: horizontalMargin, bottom: 0, trailing: horizontalMargin)) - .listRow(backgroundColor: Color(viewModel.backgroundColor)) - - // Attachments - ForEach(viewModel.attachmentViewModels) { attachmentViewModel in - let descriptionBinding = Binding { - return attachmentViewModel.descriptionContent - } set: { newValue in - attachmentViewModel.descriptionContent = newValue - } - - StatusAttachmentView( - image: attachmentViewModel.thumbnailImage, - descriptionPlaceholder: attachmentViewModel.descriptionPlaceholder, - description: descriptionBinding, - errorPrompt: attachmentViewModel.errorPrompt, - errorPromptImage: attachmentViewModel.errorPromptImage, - isUploading: attachmentViewModel.isUploading, - progressViewTintColor: attachmentViewModel.progressViewTintColor, - removeButtonAction: { - self.viewModel.removeAttachmentViewModel(attachmentViewModel) - } - ) - } - .padding(EdgeInsets(top: 16, leading: horizontalMargin, bottom: 0, trailing: horizontalMargin)) - .fixedSize(horizontal: false, vertical: true) - .listRow(backgroundColor: Color(viewModel.backgroundColor)) - - // bottom padding - Color.clear - .frame(height: viewModel.toolbarHeight + 20) - .listRow(backgroundColor: Color(viewModel.backgroundColor)) - } // end List - .listStyle(.plain) - .introspectTableView(customize: { tableView in - // tableView.keyboardDismissMode = .onDrag - tableView.verticalScrollIndicatorInsets.bottom = viewModel.toolbarHeight - }) - .preference( - key: ComposeListViewFramePreferenceKey.self, - value: proxy.frame(in: .local) - ) - .onPreferenceChange(ComposeListViewFramePreferenceKey.self) { frame in - var frame = frame - frame.size.width = frame.width - 2 * horizontalMargin - statusEditorViewWidth = frame.width - } // end List - .introspectTableView(customize: { tableView in - tableView.backgroundColor = .clear - }) - .overrideBackground(color: Color(viewModel.backgroundColor)) - } // end GeometryReader - } // end body -} - -struct ComposeListViewFramePreferenceKey: PreferenceKey { - static var defaultValue: CGRect = .zero - static func reduce(value: inout CGRect, nextValue: () -> CGRect) { } -} - -extension View { - // hack for separator line - @ViewBuilder - func listRow(backgroundColor: Color) -> some View { - // expand list row to edge (set inset) - // then hide the separator - if #available(iOS 15, *) { - frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) - .listRowInsets(EdgeInsets(top: -1, leading: -1, bottom: -1, trailing: -1)) - .background(backgroundColor) - .listRowSeparator(.hidden) // new API - } else { - frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) - .listRowInsets(EdgeInsets(top: -1, leading: -1, bottom: -1, trailing: -1)) // separator line hidden magic - .background(backgroundColor) - } - } - - @ViewBuilder - func overrideBackground(color: Color) -> some View { - background(color.ignoresSafeArea()) - } -} - - -struct ComposeView_Previews: PreviewProvider { - - static let viewModel: ComposeViewModel = { - let viewModel = ComposeViewModel() - return viewModel - }() - - static var previews: some View { - ComposeView().environmentObject(viewModel) - } - -} diff --git a/ShareActionExtension/Scene/View/ComposeViewModel.swift b/ShareActionExtension/Scene/View/ComposeViewModel.swift deleted file mode 100644 index 88c2b896f..000000000 --- a/ShareActionExtension/Scene/View/ComposeViewModel.swift +++ /dev/null @@ -1,130 +0,0 @@ -// -// ComposeViewModel.swift -// ShareActionExtension -// -// Created by MainasuK Cirno on 2021-7-16. -// - -import Foundation -import SwiftUI -import Combine -import CoreDataStack - -class ComposeViewModel: ObservableObject { - - var disposeBag = Set() - - @Published var authentication: MastodonAuthentication? - - @Published var backgroundColor: UIColor = .clear - @Published var toolbarHeight: CGFloat = 0 - @Published var viewDidAppear = false - - @Published var avatarImageURL: URL? - @Published var authorName: String = "" - @Published var authorUsername: String = "" - - @Published var statusContent = "" - @Published var statusPlaceholder = "" - @Published var statusContentAttributedString = NSAttributedString() - - @Published var isContentWarningComposing = false - @Published var contentWarningBackgroundColor = Color.secondary - @Published var contentWarningPlaceholder = "" - @Published var contentWarningContent = "" - - @Published private(set) var attachmentViewModels: [StatusAttachmentViewModel] = [] - - @Published var characterCount = 0 - - public init() { - $statusContent - .map { NSAttributedString(string: $0) } - .assign(to: &$statusContentAttributedString) - - Publishers.CombineLatest3( - $statusContent, - $isContentWarningComposing, - $contentWarningContent - ) - .map { statusContent, isContentWarningComposing, contentWarningContent in - var count = statusContent.count - if isContentWarningComposing { - count += contentWarningContent.count - } - return count - } - .assign(to: &$characterCount) - - // setup attribute updater - $attachmentViewModels - .receive(on: DispatchQueue.main) - .debounce(for: 0.3, scheduler: DispatchQueue.main) - .sink { attachmentViewModels in - // drive upload state - // make image upload in the queue - for attachmentViewModel in attachmentViewModels { - // skip when prefix N task when task finish OR fail OR uploading - guard let currentState = attachmentViewModel.uploadStateMachine.currentState else { break } - if currentState is StatusAttachmentViewModel.UploadState.Fail { - continue - } - if currentState is StatusAttachmentViewModel.UploadState.Finish { - continue - } - if currentState is StatusAttachmentViewModel.UploadState.Uploading { - break - } - // trigger uploading one by one - if currentState is StatusAttachmentViewModel.UploadState.Initial { - attachmentViewModel.uploadStateMachine.enter(StatusAttachmentViewModel.UploadState.Uploading.self) - break - } - } - } - .store(in: &disposeBag) - - #if DEBUG - // avatarImageURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif") - // authorName = "Alice" - // authorUsername = "alice" - #endif - } - -} - -extension ComposeViewModel { - func setupAttachmentViewModels(_ viewModels: [StatusAttachmentViewModel]) { - attachmentViewModels = viewModels - for viewModel in viewModels { - // set delegate - viewModel.delegate = self - // set observed - viewModel.objectWillChange.sink { [weak self] _ in - guard let self = self else { return } - self.objectWillChange.send() - } - .store(in: &viewModel.disposeBag) - // bind authentication - $authentication - .assign(to: \.value, on: viewModel.authentication) - .store(in: &viewModel.disposeBag) - } - } - - func removeAttachmentViewModel(_ viewModel: StatusAttachmentViewModel) { - if let index = attachmentViewModels.firstIndex(where: { $0 === viewModel }) { - attachmentViewModels.remove(at: index) - } - } -} - -// MARK: - StatusAttachmentViewModelDelegate -extension ComposeViewModel: StatusAttachmentViewModelDelegate { - func statusAttachmentViewModel(_ viewModel: StatusAttachmentViewModel, uploadStateDidChange state: StatusAttachmentViewModel.UploadState?) { - // trigger event update - DispatchQueue.main.async { - self.attachmentViewModels = self.attachmentViewModels - } - } -} diff --git a/ShareActionExtension/Scene/View/ContentWarningEditorView.swift b/ShareActionExtension/Scene/View/ContentWarningEditorView.swift deleted file mode 100644 index 833c919fc..000000000 --- a/ShareActionExtension/Scene/View/ContentWarningEditorView.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// ContentWarningEditorView.swift -// -// -// Created by MainasuK Cirno on 2021-7-19. -// - -import SwiftUI -import Introspect - -struct ContentWarningEditorView: View { - - @Binding var contentWarningContent: String - let placeholder: String - let spacing: CGFloat = 11 - - var body: some View { - HStack(alignment: .center, spacing: spacing) { - Image(systemName: "exclamationmark.shield") - .font(.system(size: 30, weight: .regular)) - Text(contentWarningContent.isEmpty ? " " : contentWarningContent) - .opacity(0) - .padding(.all, 8) - .frame(maxWidth: .infinity) - .overlay( - TextEditor(text: $contentWarningContent) - .introspectTextView { textView in - textView.backgroundColor = .clear - textView.placeholder = placeholder - } - ) - } - } -} - -struct ContentWarningEditorView_Previews: PreviewProvider { - - @State static var content = "" - - static var previews: some View { - ContentWarningEditorView( - contentWarningContent: $content, - placeholder: "Write an accurate warning here..." - ) - .previewLayout(.fixed(width: 375, height: 100)) - } -} - diff --git a/ShareActionExtension/Scene/View/StatusAttachmentView.swift b/ShareActionExtension/Scene/View/StatusAttachmentView.swift deleted file mode 100644 index 8540b95f1..000000000 --- a/ShareActionExtension/Scene/View/StatusAttachmentView.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// StatusAttachmentView.swift -// -// -// Created by MainasuK Cirno on 2021-7-19. -// - -import SwiftUI -import Introspect - -struct StatusAttachmentView: View { - - let image: UIImage? - let descriptionPlaceholder: String - @Binding var description: String - let errorPrompt: String? - let errorPromptImage: UIImage - let isUploading: Bool - let progressViewTintColor: UIColor - - let removeButtonAction: () -> Void - - var body: some View { - let image = image ?? UIImage.placeholder(color: .systemFill) - ZStack(alignment: .bottom) { - if let errorPrompt = errorPrompt { - Color.clear - .aspectRatio(CGSize(width: 16, height: 9), contentMode: .fill) - .overlay( - VStack(alignment: .center) { - Image(uiImage: errorPromptImage) - Text(errorPrompt) - .lineLimit(2) - } - ) - .background(Color.gray) - } else { - Color.clear - .aspectRatio(CGSize(width: 16, height: 9), contentMode: .fill) - .overlay( - Image(uiImage: image) - .resizable() - .aspectRatio(contentMode: .fill) - ) - .background(Color.gray) - LinearGradient(gradient: Gradient(colors: [Color(white: 0, opacity: 0.69), Color.clear]), startPoint: .bottom, endPoint: .top) - .frame(maxHeight: 71) - TextField("", text: $description) - .placeholder(when: description.isEmpty) { - Text(descriptionPlaceholder).foregroundColor(Color(white: 1, opacity: 0.6)) - .lineLimit(1) - } - .foregroundColor(.white) - .font(.system(size: 15, weight: .regular, design: .default)) - .padding(EdgeInsets(top: 0, leading: 8, bottom: 7, trailing: 8)) - } - } - .cornerRadius(4) - .badgeView( - Button(action: { - removeButtonAction() - }, label: { - Image(systemName: "minus.circle.fill") - .renderingMode(.original) - .font(.system(size: 22, weight: .bold, design: .default)) - }) - .buttonStyle(BorderlessButtonStyle()) - ) - .overlay( - Group { - if isUploading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: Color(progressViewTintColor))) - } - } - ) - } -} - -/// ref: https://stackoverflow.com/a/57715771/3797903 -extension View { - func placeholder( - when shouldShow: Bool, - alignment: Alignment = .leading, - @ViewBuilder placeholder: () -> Content) -> some View { - - ZStack(alignment: alignment) { - placeholder().opacity(shouldShow ? 1 : 0) - self - } - } -} - - -//struct StatusAttachmentView_Previews: PreviewProvider { -// static var previews: some View { -// ScrollView { -// StatusAttachmentView( -// image: UIImage(systemName: "photo"), -// descriptionPlaceholder: "Describe photo", -// description: .constant(""), -// errorPrompt: nil, -// errorPromptImage: StatusAttachmentViewModel.photoFillSplitImage, -// isUploading: true, -// progressViewTintColor: .systemFill, -// removeButtonAction: { -// // do nothing -// } -// ) -// .padding(20) -// } -// } -//} diff --git a/ShareActionExtension/Scene/View/StatusAttachmentViewModel+UploadState.swift b/ShareActionExtension/Scene/View/StatusAttachmentViewModel+UploadState.swift deleted file mode 100644 index 56942cde0..000000000 --- a/ShareActionExtension/Scene/View/StatusAttachmentViewModel+UploadState.swift +++ /dev/null @@ -1,131 +0,0 @@ -// -// StatusAttachmentViewModel+UploadState.swift -// ShareActionExtension -// -// Created by MainasuK Cirno on 2021-7-20. -// - -import os.log -import Foundation -import Combine -import GameplayKit -import MastodonSDK -import MastodonCore - -extension StatusAttachmentViewModel { - class UploadState: GKState { - weak var viewModel: StatusAttachmentViewModel? - - init(viewModel: StatusAttachmentViewModel) { - self.viewModel = viewModel - } - - override func didEnter(from previousState: GKState?) { - os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription) - viewModel?.uploadStateMachineSubject.send(self) - } - } -} - -extension StatusAttachmentViewModel.UploadState { - - class Initial: StatusAttachmentViewModel.UploadState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - guard viewModel?.authentication.value != nil else { return false } - if stateClass == Initial.self { - return true - } - - if viewModel?.file.value != nil { - return stateClass == Uploading.self - } else { - return stateClass == Fail.self - } - } - } - - class Uploading: StatusAttachmentViewModel.UploadState { - let logger = Logger(subsystem: "StatusAttachmentViewModel.UploadState.Uploading", category: "logic") - var needsFallback = false - - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Fail.self || stateClass == Finish.self || stateClass == Uploading.self - } - - override func didEnter(from previousState: GKState?) { - super.didEnter(from: previousState) - - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authentication = viewModel.authentication.value else { return } - guard let file = viewModel.file.value else { return } - - let description = viewModel.descriptionContent - let query = Mastodon.API.Media.UploadMediaQuery( - file: file, - thumbnail: nil, - description: description, - focus: nil - ) - - let mastodonAuthenticationBox = MastodonAuthenticationBox( - authenticationRecord: .init(objectID: authentication.objectID), - domain: authentication.domain, - userID: authentication.userID, - appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken), - userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken) - ) - - // and needs clone the `query` if needs retry - viewModel.api.uploadMedia( - domain: mastodonAuthenticationBox.domain, - query: query, - mastodonAuthenticationBox: mastodonAuthenticationBox, - needsFallback: needsFallback - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self = self else { return } - switch completion { - case .failure(let error): - if let apiError = error as? Mastodon.API.Error, - apiError.httpResponseStatus == .notFound, - self.needsFallback == false - { - self.needsFallback = true - stateMachine.enter(Uploading.self) - self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fallback to V1") - } else { - self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fail: \(error.localizedDescription)") - viewModel.error = error - stateMachine.enter(Fail.self) - } - case .finished: - self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment success") - break - } - } receiveValue: { [weak self] response in - guard let self = self else { return } - self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment \(response.value.id) success, \(response.value.url ?? "")") - viewModel.attachment.value = response.value - stateMachine.enter(Finish.self) - } - .store(in: &viewModel.disposeBag) - } - - } - - class Fail: StatusAttachmentViewModel.UploadState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - // allow discard publishing - return stateClass == Uploading.self || stateClass == Finish.self - } - } - - class Finish: StatusAttachmentViewModel.UploadState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return false - } - } - -} - diff --git a/ShareActionExtension/Scene/View/StatusAttachmentViewModel.swift b/ShareActionExtension/Scene/View/StatusAttachmentViewModel.swift deleted file mode 100644 index 19251d0be..000000000 --- a/ShareActionExtension/Scene/View/StatusAttachmentViewModel.swift +++ /dev/null @@ -1,227 +0,0 @@ -// -// StatusAttachmentViewModel.swift -// ShareActionExtension -// -// Created by MainasuK Cirno on 2021-7-19. -// - -import os.log -import Foundation -import SwiftUI -import Combine -import CoreDataStack -import MastodonSDK -import MastodonUI -import AVFoundation -import GameplayKit -import MobileCoreServices -import UniformTypeIdentifiers -import MastodonAsset -import MastodonCore -import MastodonLocalization - -protocol StatusAttachmentViewModelDelegate: AnyObject { - func statusAttachmentViewModel(_ viewModel: StatusAttachmentViewModel, uploadStateDidChange state: StatusAttachmentViewModel.UploadState?) -} - -final class StatusAttachmentViewModel: ObservableObject, Identifiable { - - static let photoFillSplitImage = Asset.Connectivity.photoFillSplit.image.withRenderingMode(.alwaysTemplate) - static let videoSplashImage: UIImage = { - let image = UIImage(systemName: "video.slash")!.withConfiguration(UIImage.SymbolConfiguration(pointSize: 64)) - return image - }() - - let logger = Logger(subsystem: "StatusAttachmentViewModel", category: "logic") - - weak var delegate: StatusAttachmentViewModelDelegate? - var disposeBag = Set() - - let id = UUID() - let itemProvider: NSItemProvider - - // input - let api: APIService - let file = CurrentValueSubject(nil) - let authentication = CurrentValueSubject(nil) - @Published var descriptionContent = "" - - // output - let attachment = CurrentValueSubject(nil) - @Published var thumbnailImage: UIImage? - @Published var descriptionPlaceholder = "" - @Published var isUploading = true - @Published var progressViewTintColor = UIColor.systemFill - @Published var error: Error? - @Published var errorPrompt: String? - @Published var errorPromptImage: UIImage = StatusAttachmentViewModel.photoFillSplitImage - - private(set) lazy var uploadStateMachine: GKStateMachine = { - // exclude timeline middle fetcher state - let stateMachine = GKStateMachine(states: [ - UploadState.Initial(viewModel: self), - UploadState.Uploading(viewModel: self), - UploadState.Fail(viewModel: self), - UploadState.Finish(viewModel: self), - ]) - stateMachine.enter(UploadState.Initial.self) - return stateMachine - }() - lazy var uploadStateMachineSubject = CurrentValueSubject(nil) - - init( - api: APIService, - itemProvider: NSItemProvider - ) { - self.api = api - self.itemProvider = itemProvider - - // bind attachment from item provider - Just(itemProvider) - .receive(on: DispatchQueue.main) - .flatMap { result -> AnyPublisher in - if itemProvider.hasRepresentationConforming(toTypeIdentifier: UTType.image.identifier, fileOptions: []) { - return ItemProviderLoader.loadImageData(from: result).eraseToAnyPublisher() - } - if itemProvider.hasRepresentationConforming(toTypeIdentifier: UTType.movie.identifier, fileOptions: []) { - return ItemProviderLoader.loadVideoData(from: result).eraseToAnyPublisher() - } - return Fail(error: AttachmentError.invalidAttachmentType).eraseToAnyPublisher() - } - .sink { [weak self] completion in - guard let self = self else { return } - switch completion { - case .failure(let error): - self.error = error - self.uploadStateMachine.enter(UploadState.Fail.self) - case .finished: - break - } - } receiveValue: { [weak self] file in - guard let self = self else { return } - self.file.value = file - self.uploadStateMachine.enter(UploadState.Initial.self) - } - .store(in: &disposeBag) - - // bind progress view tint color - $thumbnailImage - .receive(on: DispatchQueue.main) - .map { image -> UIColor in - guard let image = image else { return .systemFill } - switch image.domainLumaCoefficientsStyle { - case .light: - return UIColor.black.withAlphaComponent(0.8) - default: - return UIColor.white.withAlphaComponent(0.8) - } - } - .assign(to: &$progressViewTintColor) - - // bind description placeholder and error prompt image - file - .receive(on: DispatchQueue.main) - .sink { [weak self] file in - guard let self = self else { return } - guard let file = file else { return } - switch file { - case .jpeg, .png, .gif: - self.descriptionPlaceholder = L10n.Scene.Compose.Attachment.descriptionPhoto - self.errorPromptImage = StatusAttachmentViewModel.photoFillSplitImage - case .other: - self.descriptionPlaceholder = L10n.Scene.Compose.Attachment.descriptionVideo - self.errorPromptImage = StatusAttachmentViewModel.videoSplashImage - } - } - .store(in: &disposeBag) - - // bind thumbnail image - file - .receive(on: DispatchQueue.main) - .map { file -> UIImage? in - guard let file = file else { - return nil - } - - switch file { - case .jpeg(let data), .png(let data): - return data.flatMap { UIImage(data: $0) } - case .gif: - // TODO: - return nil - case .other(let url, _, _): - guard let url = url, FileManager.default.fileExists(atPath: url.path) else { return nil } - let asset = AVURLAsset(url: url) - let assetImageGenerator = AVAssetImageGenerator(asset: asset) - assetImageGenerator.appliesPreferredTrackTransform = true // fix orientation - do { - let cgImage = try assetImageGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil) - let image = UIImage(cgImage: cgImage) - return image - } catch { - self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): thumbnail generate fail: \(error.localizedDescription)") - return nil - } - } - } - .assign(to: &$thumbnailImage) - - // bind state and error - Publishers.CombineLatest( - uploadStateMachineSubject, - $error - ) - .sink { [weak self] state, error in - guard let self = self else { return } - // trigger delegate - self.delegate?.statusAttachmentViewModel(self, uploadStateDidChange: state) - - // set error prompt - if let error = error { - self.isUploading = false - self.errorPrompt = error.localizedDescription - } else { - guard let state = state else { return } - switch state { - case is UploadState.Finish: - self.isUploading = false - case is UploadState.Fail: - self.isUploading = false - // FIXME: not display - self.errorPrompt = { - guard let file = self.file.value else { - return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) - } - switch file { - case .jpeg, .png, .gif: - return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) - case .other: - return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video) - } - }() - default: - break - } - } - } - .store(in: &disposeBag) - - // trigger delegate when authentication get new value - authentication - .receive(on: DispatchQueue.main) - .sink { [weak self] authentication in - guard let self = self else { return } - guard authentication != nil else { return } - self.delegate?.statusAttachmentViewModel(self, uploadStateDidChange: self.uploadStateMachineSubject.value) - } - .store(in: &disposeBag) - } - -} - -extension StatusAttachmentViewModel { - enum AttachmentError: Error { - case invalidAttachmentType - case attachmentTooLarge - } -} diff --git a/ShareActionExtension/Scene/View/StatusAuthorView.swift b/ShareActionExtension/Scene/View/StatusAuthorView.swift deleted file mode 100644 index 24453abe2..000000000 --- a/ShareActionExtension/Scene/View/StatusAuthorView.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// StatusAuthorView.swift -// -// -// Created by MainasuK Cirno on 2021-7-16. -// - -import SwiftUI -import MastodonUI -import Nuke -import FLAnimatedImage - -struct StatusAuthorView: View { - - let avatarImageURL: URL? - let name: String - let username: String - - var body: some View { - HStack(spacing: 5) { - AnimatedImage(imageURL: avatarImageURL) - .frame(width: 42, height: 42) - .background(Color(UIColor.systemFill)) - .cornerRadius(4) - VStack(alignment: .leading) { - Text(name) - .font(.headline) - Text(username) - .font(.subheadline) - .foregroundColor(.secondary) - } - Spacer() - } - } -} - -struct StatusAuthorView_Previews: PreviewProvider { - static var previews: some View { - StatusAuthorView( - avatarImageURL: URL(string: "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif"), - name: "Alice", - username: "alice" - ) - } -} diff --git a/ShareActionExtension/Scene/View/StatusEditorView.swift b/ShareActionExtension/Scene/View/StatusEditorView.swift deleted file mode 100644 index f670f6601..000000000 --- a/ShareActionExtension/Scene/View/StatusEditorView.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// StatusEditorView.swift -// -// -// Created by MainasuK Cirno on 2021-7-16. -// - -import UIKit -import SwiftUI -import UITextView_Placeholder - -public struct StatusEditorView: UIViewRepresentable { - - @Binding var string: String - let placeholder: String - let width: CGFloat - let attributedString: NSAttributedString - let keyboardType: UIKeyboardType - @Binding var viewDidAppear: Bool - - public init( - string: Binding, - placeholder: String, - width: CGFloat, - attributedString: NSAttributedString, - keyboardType: UIKeyboardType, - viewDidAppear: Binding - ) { - self._string = string - self.placeholder = placeholder - self.width = width - self.attributedString = attributedString - self.keyboardType = keyboardType - self._viewDidAppear = viewDidAppear - } - - public func makeUIView(context: Context) -> UITextView { - let textView = UITextView(frame: .zero) - textView.placeholder = placeholder - - textView.isScrollEnabled = false - textView.font = .preferredFont(forTextStyle: .body) - textView.textColor = .label - textView.keyboardType = keyboardType - textView.delegate = context.coordinator - textView.backgroundColor = .clear - - textView.translatesAutoresizingMaskIntoConstraints = false - let widthLayoutConstraint = textView.widthAnchor.constraint(equalToConstant: 100) - widthLayoutConstraint.priority = .required - 1 - context.coordinator.widthLayoutConstraint = widthLayoutConstraint - - return textView - } - - public func updateUIView(_ textView: UITextView, context: Context) { - // preserve currently selected text range to prevent cursor jump - let currentlySelectedRange = textView.selectedRange - - // update content - // textView.attributedText = attributedString - textView.text = string - - // update layout - context.coordinator.updateLayout(width: width) - - // set becomeFirstResponder - if viewDidAppear { - viewDidAppear = false - textView.becomeFirstResponder() - } - - // restore selected text range - textView.selectedRange = currentlySelectedRange - } - - public func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - public class Coordinator: NSObject, UITextViewDelegate { - var parent: StatusEditorView - var widthLayoutConstraint: NSLayoutConstraint? - - init(_ parent: StatusEditorView) { - self.parent = parent - } - - public func textViewDidChange(_ textView: UITextView) { - // prevent break IME input - if textView.markedTextRange == nil { - parent.string = textView.text - } - } - - func updateLayout(width: CGFloat) { - guard let widthLayoutConstraint = widthLayoutConstraint else { return } - widthLayoutConstraint.constant = width - widthLayoutConstraint.isActive = true - } - } - -} - - From 1e71f0c147a0ba4665e05a4e61ec6f18caf8b48b Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 14 Nov 2022 00:57:44 +0800 Subject: [PATCH 154/224] feat: restore media description text field --- .../Attachment/AttachmentView.swift | 282 +++++++++++------- .../Publisher/MastodonStatusPublisher.swift | 15 + .../View/ComposeContentView.swift | 5 +- 3 files changed, 183 insertions(+), 119 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift index 2dc8bf12f..f55793591 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift @@ -11,6 +11,8 @@ import SwiftUI import Introspect import AVKit import MastodonAsset +import MastodonLocalization +import Introspect public struct AttachmentView: View { @@ -21,129 +23,179 @@ public struct AttachmentView: View { } public var body: some View { - ZStack { - let image = viewModel.thumbnail ?? .placeholder(color: .secondarySystemFill) - Image(uiImage: image) - .resizable() - .aspectRatio(contentMode: .fill) - - // loading… - if viewModel.output == nil, viewModel.error == nil { - ProgressView() - .progressViewStyle(.circular) - } - - // load failed - // cannot re-entry - if viewModel.output == nil, let error = viewModel.error { - VisualEffectView(effect: blurEffect) - VStack { - Text("Load Failed") // TODO: i18n - .font(.system(size: 13, weight: .semibold)) - Text(error.localizedDescription) - .font(.system(size: 12, weight: .regular)) + Color.clear.aspectRatio(358.0/232.0, contentMode: .fill) + .overlay( + ZStack { + let image = viewModel.thumbnail ?? .placeholder(color: .secondarySystemFill) + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fill) } - } - - // loaded - // uploading… or upload failed - // could retry upload when error emit - if viewModel.output != nil, viewModel.uploadState != .finish { - VisualEffectView(effect: blurEffect) - VStack { - let action: AttachmentViewModel.Action = { - if let _ = viewModel.error { - return .retry - } else { - return .remove - } - }() - Button { - viewModel.delegate?.attachmentViewModel(viewModel, actionButtonDidPressed: action) - } label: { - let image: UIImage = { - switch action { - case .remove: - return Asset.Scene.Compose.Attachment.stop.image.withRenderingMode(.alwaysTemplate) - case .retry: - return Asset.Scene.Compose.Attachment.retry.image.withRenderingMode(.alwaysTemplate) + ) + .overlay( + ZStack { + Color.clear + .overlay( + VStack(alignment: .leading) { + let placeholder: String = { + switch viewModel.output { + case .image: return L10n.Scene.Compose.Attachment.descriptionPhoto + case .video: return L10n.Scene.Compose.Attachment.descriptionVideo + case nil: return "" + } + }() + Spacer() + TextField(placeholder, text: $viewModel.caption) + .textFieldStyle(.plain) + .foregroundColor(.white) + .placeholder(placeholder, when: viewModel.caption.isEmpty) + .padding(8) } - }() - Image(uiImage: image) - .foregroundColor(.white) - .padding() - .background(Color(Asset.Scene.Compose.Attachment.indicatorButtonBackground.color)) - .overlay( - Group { + ) + + // loading… + if viewModel.output == nil, viewModel.error == nil { + ProgressView() + .progressViewStyle(.circular) + } + + // load failed + // cannot re-entry + if viewModel.output == nil, let error = viewModel.error { + VisualEffectView(effect: blurEffect) + VStack { + Text("Load Failed") // TODO: i18n + .font(.system(size: 13, weight: .semibold)) + Text(error.localizedDescription) + .font(.system(size: 12, weight: .regular)) + } + } + + // loaded + // uploading… or upload failed + // could retry upload when error emit + if viewModel.output != nil, viewModel.uploadState != .finish { + VisualEffectView(effect: blurEffect) + VStack { + let action: AttachmentViewModel.Action = { + if let _ = viewModel.error { + return .retry + } else { + return .remove + } + }() + Button { + viewModel.delegate?.attachmentViewModel(viewModel, actionButtonDidPressed: action) + } label: { + let image: UIImage = { + switch action { + case .remove: + return Asset.Scene.Compose.Attachment.stop.image.withRenderingMode(.alwaysTemplate) + case .retry: + return Asset.Scene.Compose.Attachment.retry.image.withRenderingMode(.alwaysTemplate) + } + }() + Image(uiImage: image) + .foregroundColor(.white) + .padding() + .background(Color(Asset.Scene.Compose.Attachment.indicatorButtonBackground.color)) + .overlay( + Group { + switch viewModel.uploadState { + case .compressing: + CircleProgressView(progress: viewModel.videoCompressProgress) + .animation(.default, value: viewModel.videoCompressProgress) + case .uploading: + CircleProgressView(progress: viewModel.fractionCompleted) + .animation(.default, value: viewModel.fractionCompleted) + default: + EmptyView() + } + } + ) + .clipShape(Circle()) + .padding() + } + + let title: String = { + switch action { + case .remove: switch viewModel.uploadState { case .compressing: - CircleProgressView(progress: viewModel.videoCompressProgress) - .animation(.default, value: viewModel.videoCompressProgress) - case .uploading: - CircleProgressView(progress: viewModel.fractionCompleted) - .animation(.default, value: viewModel.fractionCompleted) + return "Comporessing..." // TODO: i18n default: - EmptyView() + if viewModel.fractionCompleted < 0.9 { + let totalSizeInByte = viewModel.outputSizeInByte + let uploadSizeInByte = Double(totalSizeInByte) * min(1.0, viewModel.fractionCompleted + 0.1) // 9:1 + let total = viewModel.byteCountFormatter.string(fromByteCount: Int64(totalSizeInByte)) + let upload = viewModel.byteCountFormatter.string(fromByteCount: Int64(uploadSizeInByte)) + return "\(upload) / \(total)" + } else { + return "Server Processing..." // TODO: i18n + } } + case .retry: + return "Upload Failed" // TODO: i18n } - ) - .clipShape(Circle()) - .padding() + }() + let subtitle: String = { + switch action { + case .remove: + if viewModel.progress.fractionCompleted < 1, viewModel.uploadState == .uploading { + if viewModel.progress.fractionCompleted < 0.9 { + return viewModel.remainTimeLocalizedString ?? "" + } else { + return "" + } + } else if viewModel.videoCompressProgress < 1, viewModel.uploadState == .compressing { + return viewModel.percentageFormatter.string(from: NSNumber(floatLiteral: viewModel.videoCompressProgress)) ?? "" + } else { + return "" + } + case .retry: + return viewModel.error?.localizedDescription ?? "" + } + }() + Text(title) + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.white) + .padding(.horizontal) + Text(subtitle) + .font(.system(size: 12, weight: .regular)) + .foregroundColor(.white) + .padding(.horizontal) + .lineLimit(nil) + .multilineTextAlignment(.center) + .frame(maxWidth: 240) + } } - - let title: String = { - switch action { - case .remove: - switch viewModel.uploadState { - case .compressing: - return "Comporessing..." // TODO: i18n - default: - if viewModel.fractionCompleted < 0.9 { - let totalSizeInByte = viewModel.outputSizeInByte - let uploadSizeInByte = Double(totalSizeInByte) * min(1.0, viewModel.fractionCompleted + 0.1) // 9:1 - let total = viewModel.byteCountFormatter.string(fromByteCount: Int64(totalSizeInByte)) - let upload = viewModel.byteCountFormatter.string(fromByteCount: Int64(uploadSizeInByte)) - return "\(upload) / \(total)" - } else { - return "Server Processing..." // TODO: i18n - } - } - case .retry: - return "Upload Failed" // TODO: i18n - } - }() - let subtitle: String = { - switch action { - case .remove: - if viewModel.progress.fractionCompleted < 1, viewModel.uploadState == .uploading { - if viewModel.progress.fractionCompleted < 0.9 { - return viewModel.remainTimeLocalizedString ?? "" - } else { - return "" - } - } else if viewModel.videoCompressProgress < 1, viewModel.uploadState == .compressing { - return viewModel.percentageFormatter.string(from: NSNumber(floatLiteral: viewModel.videoCompressProgress)) ?? "" - } else { - return "" - } - case .retry: - return viewModel.error?.localizedDescription ?? "" - } - }() - Text(title) - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(.white) - .padding(.horizontal) - Text(subtitle) - .font(.system(size: 12, weight: .regular)) - .foregroundColor(.white) - .padding(.horizontal) - .lineLimit(nil) - .multilineTextAlignment(.center) - .frame(maxWidth: 240) - } - } - } // end ZStack + } // end ZStack + ) } // end body } + +// https://stackoverflow.com/a/57715771/3797903 +extension View { + fileprivate func placeholder( + when shouldShow: Bool, + alignment: Alignment = .leading, + @ViewBuilder placeholder: () -> Content) -> some View { + + ZStack(alignment: alignment) { + placeholder().opacity(shouldShow ? 1 : 0) + self + } + } + + fileprivate func placeholder( + _ text: String, + when shouldShow: Bool, + alignment: Alignment = .leading) -> some View { + + placeholder(when: shouldShow, alignment: alignment) { + Text(text) + .foregroundColor(.white.opacity(0.7)) + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift index 31568552c..93f3dd3a1 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift @@ -125,6 +125,21 @@ extension MastodonStatusPublisher: StatusPublisher { } attachmentIDs.append(attachment.id) + let caption = attachmentViewModel.caption + guard !caption.isEmpty else { continue } + + _ = try await api.updateMedia( + domain: authContext.mastodonAuthenticationBox.domain, + attachmentID: attachment.id, + query: .init( + file: nil, + thumbnail: nil, + description: caption, + focus: nil + ), + mastodonAuthenticationBox: authContext.mastodonAuthenticationBox + ).singleOutput() + // TODO: allow background upload // let attachment = try await attachmentViewModel.upload(context: uploadContext) // let attachmentID = attachment.id diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift index e5f9b56be..456816d6d 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift @@ -219,10 +219,7 @@ extension ComposeContentView { var mediaView: some View { VStack(spacing: 16) { ForEach(viewModel.attachmentViewModels, id: \.self) { attachmentViewModel in - Color.clear.aspectRatio(358.0/232.0, contentMode: .fill) - .overlay( - AttachmentView(viewModel: attachmentViewModel) - ) + AttachmentView(viewModel: attachmentViewModel) .clipShape(Rectangle()) .badgeView( Button { From 81bc8eb662c847276c8817c6d07db52268b1898d Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 14 Nov 2022 01:19:39 +0800 Subject: [PATCH 155/224] fix: video may in portrait mode issue --- .../ComposeContent/Attachment/AttachmentView.swift | 2 ++ .../Attachment/AttachmentViewModel+Compress.swift | 13 +++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift index f55793591..a2567d0b6 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift @@ -46,6 +46,7 @@ public struct AttachmentView: View { }() Spacer() TextField(placeholder, text: $viewModel.caption) + .lineLimit(1) .textFieldStyle(.plain) .foregroundColor(.white) .placeholder(placeholder, when: viewModel.caption.isEmpty) @@ -196,6 +197,7 @@ extension View { placeholder(when: shouldShow, alignment: alignment) { Text(text) .foregroundColor(.white.opacity(0.7)) + .lineLimit(1) } } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift index 0fd0ab085..ac1811a06 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift @@ -17,6 +17,15 @@ extension AttachmentViewModel { let exporter = NextLevelSessionExporter(withAsset: urlAsset) exporter.outputFileType = .mp4 + var isLandscape: Bool = { + guard let track = urlAsset.tracks(withMediaType: .video).first else { + return true + } + + let size = track.naturalSize.applying(track.preferredTransform) + return abs(size.width) >= abs(size.height) + }() + let outputURL = try FileManager.default.createTemporaryFileURL( filename: UUID().uuidString, pathExtension: url.pathExtension @@ -30,8 +39,8 @@ extension AttachmentViewModel { ] exporter.videoOutputConfiguration = [ AVVideoCodecKey: AVVideoCodecType.h264, - AVVideoWidthKey: NSNumber(integerLiteral: 1280), - AVVideoHeightKey: NSNumber(integerLiteral: 720), + AVVideoWidthKey: NSNumber(integerLiteral: isLandscape ? 1280 : 720), + AVVideoHeightKey: NSNumber(integerLiteral: isLandscape ? 720 : 1280), AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill, AVVideoCompressionPropertiesKey: compressionDict ] From 4a519f5958a8c09e9d4367342b26c9793f167dc3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:22 +0100 Subject: [PATCH 156/224] New translations app.json (Slovenian) --- Localization/StringsConvertor/input/sl.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/sl.lproj/app.json b/Localization/StringsConvertor/input/sl.lproj/app.json index 3f2ddf1e1..37b62a45d 100644 --- a/Localization/StringsConvertor/input/sl.lproj/app.json +++ b/Localization/StringsConvertor/input/sl.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "To %s je okvarjeno in ga ni\nmožno naložiti v Mastodon.", "description_photo": "Opiši fotografijo za slabovidne in osebe z okvaro vida ...", - "description_video": "Opiši video za slabovidne in osebe z okvaro vida ..." + "description_video": "Opiši video za slabovidne in osebe z okvaro vida ...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Trajanje: %s", From 91c63fb9d24d54431b196b1f0c2da2c86034ba56 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:23 +0100 Subject: [PATCH 157/224] New translations app.json (Indonesian) --- Localization/StringsConvertor/input/id.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/id.lproj/app.json b/Localization/StringsConvertor/input/id.lproj/app.json index 689cd0995..d942a22ad 100644 --- a/Localization/StringsConvertor/input/id.lproj/app.json +++ b/Localization/StringsConvertor/input/id.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "%s ini rusak dan tidak dapat diunggah ke Mastodon.", "description_photo": "Jelaskan fotonya untuk mereka yang tidak dapat melihat dengan jelas...", - "description_video": "Jelaskan videonya untuk mereka yang tidak dapat melihat dengan jelas..." + "description_video": "Jelaskan videonya untuk mereka yang tidak dapat melihat dengan jelas...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Durasi: %s", From 5a0a9830b9b090b3ff13f27cf5896055eb22b2a2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:24 +0100 Subject: [PATCH 158/224] New translations app.json (Portuguese) --- Localization/StringsConvertor/input/pt.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/pt.lproj/app.json b/Localization/StringsConvertor/input/pt.lproj/app.json index c5a3dac74..a6a971860 100644 --- a/Localization/StringsConvertor/input/pt.lproj/app.json +++ b/Localization/StringsConvertor/input/pt.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "Describe the photo for the visually-impaired...", - "description_video": "Describe the video for the visually-impaired..." + "description_video": "Describe the video for the visually-impaired...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Duration: %s", From 8458d5f7341549afd0dd0896429e4cb963b3c2af Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:25 +0100 Subject: [PATCH 159/224] New translations app.json (Russian) --- Localization/StringsConvertor/input/ru.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ru.lproj/app.json b/Localization/StringsConvertor/input/ru.lproj/app.json index c7d721aea..798cdb4c5 100644 --- a/Localization/StringsConvertor/input/ru.lproj/app.json +++ b/Localization/StringsConvertor/input/ru.lproj/app.json @@ -382,7 +382,11 @@ "video": "видео", "attachment_broken": "Это %s повреждено и не может\nбыть отправлено в Mastodon.", "description_photo": "Опишите фото для людей с нарушениями зрения...", - "description_video": "Опишите видео для людей с нарушениями зрения..." + "description_video": "Опишите видео для людей с нарушениями зрения...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Продолжительность: %s", From dff12fa346b6e9f56dce6fc350c470b53ab7ac25 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:26 +0100 Subject: [PATCH 160/224] New translations app.json (Chinese Simplified) --- Localization/StringsConvertor/input/zh-Hans.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json index 32d41e016..ddf89e159 100644 --- a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json @@ -382,7 +382,11 @@ "video": "视频", "attachment_broken": "%s已损坏\n无法上传到 Mastodon", "description_photo": "为视觉障碍人士添加照片的文字说明...", - "description_video": "为视觉障碍人士添加视频的文字说明..." + "description_video": "为视觉障碍人士添加视频的文字说明...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "时长:%s", From 9a232e94351aaccb2cc2194c19b4afe676b18539 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:27 +0100 Subject: [PATCH 161/224] New translations app.json (English) --- Localization/StringsConvertor/input/en.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/en.lproj/app.json b/Localization/StringsConvertor/input/en.lproj/app.json index c5a3dac74..a6a971860 100644 --- a/Localization/StringsConvertor/input/en.lproj/app.json +++ b/Localization/StringsConvertor/input/en.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "Describe the photo for the visually-impaired...", - "description_video": "Describe the video for the visually-impaired..." + "description_video": "Describe the video for the visually-impaired...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Duration: %s", From 0f18d648d5f77f9b27265b487df7f87c034bae8e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:28 +0100 Subject: [PATCH 162/224] New translations app.json (Galician) --- Localization/StringsConvertor/input/gl.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index a4aacbdc5..d183c4cb9 100644 --- a/Localization/StringsConvertor/input/gl.lproj/app.json +++ b/Localization/StringsConvertor/input/gl.lproj/app.json @@ -382,7 +382,11 @@ "video": "vídeo", "attachment_broken": "Este %s está estragado e non pode\nser subido a Mastodon.", "description_photo": "Describe a foto para persoas con problemas visuais...", - "description_video": "Describe o vídeo para persoas con problemas visuais..." + "description_video": "Describe o vídeo para persoas con problemas visuais...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Duración: %s", From b790538da3baf1c496e7dc9b3509189ed34c28cc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:29 +0100 Subject: [PATCH 163/224] New translations app.json (Portuguese, Brazilian) --- Localization/StringsConvertor/input/pt-BR.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/app.json b/Localization/StringsConvertor/input/pt-BR.lproj/app.json index d2653102b..1e0a60511 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/app.json +++ b/Localization/StringsConvertor/input/pt-BR.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "Describe the photo for the visually-impaired...", - "description_video": "Describe the video for the visually-impaired..." + "description_video": "Describe the video for the visually-impaired...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Duration: %s", From 6550ddd45311b7610355003bf07d845317f82f45 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:30 +0100 Subject: [PATCH 164/224] New translations app.json (Spanish, Argentina) --- Localization/StringsConvertor/input/es-AR.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/es-AR.lproj/app.json b/Localization/StringsConvertor/input/es-AR.lproj/app.json index 33f36134b..9b8f9f522 100644 --- a/Localization/StringsConvertor/input/es-AR.lproj/app.json +++ b/Localization/StringsConvertor/input/es-AR.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "Este archivo de %s está roto\ny no se puede subir a Mastodon.", "description_photo": "Describí la imagen para personas con dificultades visuales…", - "description_video": "Describí el video para personas con dificultades visuales…" + "description_video": "Describí el video para personas con dificultades visuales…", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Duración: %s", From 63d624f2988f158e931a0cedbf593b22a6a577f0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:31 +0100 Subject: [PATCH 165/224] New translations app.json (Japanese) --- Localization/StringsConvertor/input/ja.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ja.lproj/app.json b/Localization/StringsConvertor/input/ja.lproj/app.json index 098f49087..9ff2a60a6 100644 --- a/Localization/StringsConvertor/input/ja.lproj/app.json +++ b/Localization/StringsConvertor/input/ja.lproj/app.json @@ -382,7 +382,11 @@ "video": "動画", "attachment_broken": "%sは壊れていてMastodonにアップロードできません。", "description_photo": "閲覧が難しいユーザーへの画像説明", - "description_video": "閲覧が難しいユーザーへの映像説明" + "description_video": "閲覧が難しいユーザーへの映像説明", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "期間: %s", From 9188069b7ebc562126afaf21c21618b92fa16394 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:32 +0100 Subject: [PATCH 166/224] New translations app.json (Thai) --- Localization/StringsConvertor/input/th.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/th.lproj/app.json b/Localization/StringsConvertor/input/th.lproj/app.json index fc88065d9..ffdad5b75 100644 --- a/Localization/StringsConvertor/input/th.lproj/app.json +++ b/Localization/StringsConvertor/input/th.lproj/app.json @@ -382,7 +382,11 @@ "video": "วิดีโอ", "attachment_broken": "%s นี้เสียหายและไม่สามารถ\nอัปโหลดไปยัง Mastodon", "description_photo": "อธิบายรูปภาพสำหรับผู้บกพร่องทางการมองเห็น...", - "description_video": "อธิบายวิดีโอสำหรับผู้บกพร่องทางการมองเห็น..." + "description_video": "อธิบายวิดีโอสำหรับผู้บกพร่องทางการมองเห็น...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "ระยะเวลา: %s", From 701d970bc98c3d2601498cd5023ba024cde1f18e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:33 +0100 Subject: [PATCH 167/224] New translations app.json (Latvian) --- Localization/StringsConvertor/input/lv.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/lv.lproj/app.json b/Localization/StringsConvertor/input/lv.lproj/app.json index ba50897ed..2835f0887 100644 --- a/Localization/StringsConvertor/input/lv.lproj/app.json +++ b/Localization/StringsConvertor/input/lv.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "Describe the photo for the visually-impaired...", - "description_video": "Describe the video for the visually-impaired..." + "description_video": "Describe the video for the visually-impaired...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Duration: %s", From bad26066a4ef8e3659bd06037af78ed81765c005 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:34 +0100 Subject: [PATCH 168/224] New translations app.json (Hindi) --- Localization/StringsConvertor/input/hi.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/hi.lproj/app.json b/Localization/StringsConvertor/input/hi.lproj/app.json index 35cab7b5a..f0fedf75f 100644 --- a/Localization/StringsConvertor/input/hi.lproj/app.json +++ b/Localization/StringsConvertor/input/hi.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "Describe the photo for the visually-impaired...", - "description_video": "Describe the video for the visually-impaired..." + "description_video": "Describe the video for the visually-impaired...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Duration: %s", From ac03ea3991d31540ce28abbc75563a4db246e900 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:35 +0100 Subject: [PATCH 169/224] New translations app.json (English, United States) --- Localization/StringsConvertor/input/en-US.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/en-US.lproj/app.json b/Localization/StringsConvertor/input/en-US.lproj/app.json index c5a3dac74..a6a971860 100644 --- a/Localization/StringsConvertor/input/en-US.lproj/app.json +++ b/Localization/StringsConvertor/input/en-US.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "Describe the photo for the visually-impaired...", - "description_video": "Describe the video for the visually-impaired..." + "description_video": "Describe the video for the visually-impaired...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Duration: %s", From 6c85c9c6312db6a1f52bb103914e7df0eb002781 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:36 +0100 Subject: [PATCH 170/224] New translations app.json (Welsh) --- Localization/StringsConvertor/input/cy.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/cy.lproj/app.json b/Localization/StringsConvertor/input/cy.lproj/app.json index de782cd98..f36fe7d16 100644 --- a/Localization/StringsConvertor/input/cy.lproj/app.json +++ b/Localization/StringsConvertor/input/cy.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "Describe the photo for the visually-impaired...", - "description_video": "Describe the video for the visually-impaired..." + "description_video": "Describe the video for the visually-impaired...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Duration: %s", From 3ba643c6cc58975a62dece46cd8426e09a725bba Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:37 +0100 Subject: [PATCH 171/224] New translations app.json (Sinhala) --- Localization/StringsConvertor/input/si.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/si.lproj/app.json b/Localization/StringsConvertor/input/si.lproj/app.json index 2428da902..816536440 100644 --- a/Localization/StringsConvertor/input/si.lproj/app.json +++ b/Localization/StringsConvertor/input/si.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "Describe the photo for the visually-impaired...", - "description_video": "Describe the video for the visually-impaired..." + "description_video": "Describe the video for the visually-impaired...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Duration: %s", From 9f4e93b2c32f76a7163ba4a31e03a17dd7176c8a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:38 +0100 Subject: [PATCH 172/224] New translations app.json (Kurmanji (Kurdish)) --- Localization/StringsConvertor/input/kmr.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/kmr.lproj/app.json b/Localization/StringsConvertor/input/kmr.lproj/app.json index bfe22d89a..9991169f4 100644 --- a/Localization/StringsConvertor/input/kmr.lproj/app.json +++ b/Localization/StringsConvertor/input/kmr.lproj/app.json @@ -382,7 +382,11 @@ "video": "vîdyo", "attachment_broken": "Ev %s naxebite û nayê barkirin\n li ser Mastodon.", "description_photo": "Wêneyê ji bo kêmbînên dîtbar bide nasîn...", - "description_video": "Vîdyoyê ji bo kêmbînên dîtbar bide nasîn..." + "description_video": "Vîdyoyê ji bo kêmbînên dîtbar bide nasîn...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Dirêjî: %s", From b9eec235f249643fe59d1574fb20ead7b922ecc1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:39 +0100 Subject: [PATCH 173/224] New translations app.json (Dutch) --- Localization/StringsConvertor/input/nl.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/nl.lproj/app.json b/Localization/StringsConvertor/input/nl.lproj/app.json index 415327eaa..e0b2872fb 100644 --- a/Localization/StringsConvertor/input/nl.lproj/app.json +++ b/Localization/StringsConvertor/input/nl.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "Deze %s is corrupt en kan niet geüpload worden naar Mastodon.", "description_photo": "Omschrijf de foto voor mensen met een visuele beperking...", - "description_video": "Omschrijf de video voor mensen met een visuele beperking..." + "description_video": "Omschrijf de video voor mensen met een visuele beperking...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Duur: %s", From 9dc71080a6edf2a1397dab7d05df47bfa7a2ef85 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:40 +0100 Subject: [PATCH 174/224] New translations app.json (Italian) --- Localization/StringsConvertor/input/it.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json index 096deb444..b4c6aab70 100644 --- a/Localization/StringsConvertor/input/it.lproj/app.json +++ b/Localization/StringsConvertor/input/it.lproj/app.json @@ -382,7 +382,11 @@ "video": "filmato", "attachment_broken": "Questo %s è rotto e non può essere\ncaricato su Mastodon.", "description_photo": "Descrivi la foto per gli utenti ipovedenti...", - "description_video": "Descrivi il filmato per gli utenti ipovedenti..." + "description_video": "Descrivi il filmato per gli utenti ipovedenti...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Durata: %s", From 1f6b71e371bba5569146b1cee1ab5f0367f8f73e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:41 +0100 Subject: [PATCH 175/224] New translations app.json (Chinese Traditional) --- Localization/StringsConvertor/input/zh-Hant.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json index 3700f0dd0..ab7343c99 100644 --- a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json @@ -382,7 +382,11 @@ "video": "影片", "attachment_broken": "此 %s 已損毀,並無法被上傳至 Mastodon。", "description_photo": "為視障人士提供圖片說明...", - "description_video": "為視障人士提供影片說明..." + "description_video": "為視障人士提供影片說明...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "持續時間:%s", From 156565507b83bffa881a64c8b91fcdb0266b6c84 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:44 +0100 Subject: [PATCH 176/224] New translations app.json (Ukrainian) --- Localization/StringsConvertor/input/uk.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/uk.lproj/app.json b/Localization/StringsConvertor/input/uk.lproj/app.json index c5a3dac74..a6a971860 100644 --- a/Localization/StringsConvertor/input/uk.lproj/app.json +++ b/Localization/StringsConvertor/input/uk.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "Describe the photo for the visually-impaired...", - "description_video": "Describe the video for the visually-impaired..." + "description_video": "Describe the video for the visually-impaired...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Duration: %s", From c019bb2e277a00aef6184da4f8e44dd496f01a13 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:45 +0100 Subject: [PATCH 177/224] New translations app.json (Vietnamese) --- Localization/StringsConvertor/input/vi.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/vi.lproj/app.json b/Localization/StringsConvertor/input/vi.lproj/app.json index 1c60b214b..5b7696727 100644 --- a/Localization/StringsConvertor/input/vi.lproj/app.json +++ b/Localization/StringsConvertor/input/vi.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "%s này bị lỗi và không thể\ntải lên Mastodon.", "description_photo": "Mô tả hình ảnh cho người khiếm thị...", - "description_video": "Mô tả video cho người khiếm thị..." + "description_video": "Mô tả video cho người khiếm thị...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Thời hạn: %s", From 0204169bca80204796331ad2662d361f536c17c7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:46 +0100 Subject: [PATCH 178/224] New translations app.json (Kabyle) --- Localization/StringsConvertor/input/kab.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/kab.lproj/app.json b/Localization/StringsConvertor/input/kab.lproj/app.json index ac436b6e1..9c5d7659a 100644 --- a/Localization/StringsConvertor/input/kab.lproj/app.json +++ b/Localization/StringsConvertor/input/kab.lproj/app.json @@ -382,7 +382,11 @@ "video": "tavidyutt", "attachment_broken": "%s-a yerreẓ, ur yezmir ara\nAd d-yettwasali ɣef Mastodon.", "description_photo": "Glem-d tawlaft i wid yesɛan ugur deg yiẓri...", - "description_video": "Glem-d tavidyut i wid yesɛan ugur deg yiẓri..." + "description_video": "Glem-d tavidyut i wid yesɛan ugur deg yiẓri...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Tangazt: %s", From a16a5e4f84aaa4b631096e4e1defddd1efeb1eae Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:47 +0100 Subject: [PATCH 179/224] New translations app.json (Korean) --- Localization/StringsConvertor/input/ko.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ko.lproj/app.json b/Localization/StringsConvertor/input/ko.lproj/app.json index f67c7de5b..1d5019474 100644 --- a/Localization/StringsConvertor/input/ko.lproj/app.json +++ b/Localization/StringsConvertor/input/ko.lproj/app.json @@ -382,7 +382,11 @@ "video": "동영상", "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "시각장애인을 위한 사진 설명…", - "description_video": "시각장애인을 위한 영상 설명…" + "description_video": "시각장애인을 위한 영상 설명…", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "기간: %s", From a794358309334a6d550c6e90ffa9fe8617afe026 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:48 +0100 Subject: [PATCH 180/224] New translations app.json (Swedish) --- Localization/StringsConvertor/input/sv.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/sv.lproj/app.json b/Localization/StringsConvertor/input/sv.lproj/app.json index 0b04e01d7..14d670921 100644 --- a/Localization/StringsConvertor/input/sv.lproj/app.json +++ b/Localization/StringsConvertor/input/sv.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "Denna %s är trasig och kan inte\nladdas upp till Mastodon.", "description_photo": "Beskriv fotot för synskadade...", - "description_video": "Beskriv videon för de synskadade..." + "description_video": "Beskriv videon för de synskadade...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Längd: %s", From 8f1b4d335f54f79fafa4b21bd543a386ced33b24 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:49 +0100 Subject: [PATCH 181/224] New translations app.json (French) --- Localization/StringsConvertor/input/fr.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/fr.lproj/app.json b/Localization/StringsConvertor/input/fr.lproj/app.json index f719f21a4..25bb6e511 100644 --- a/Localization/StringsConvertor/input/fr.lproj/app.json +++ b/Localization/StringsConvertor/input/fr.lproj/app.json @@ -382,7 +382,11 @@ "video": "vidéo", "attachment_broken": "Ce %s est brisé et ne peut pas être\ntéléversé sur Mastodon.", "description_photo": "Décrire cette photo pour les personnes malvoyantes...", - "description_video": "Décrire cette vidéo pour les personnes malvoyantes..." + "description_video": "Décrire cette vidéo pour les personnes malvoyantes...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Durée: %s", From 26fc919459a4d9a72748bf0b107cc0c2dbcbe7be Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:50 +0100 Subject: [PATCH 182/224] New translations app.json (Turkish) --- Localization/StringsConvertor/input/tr.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/tr.lproj/app.json b/Localization/StringsConvertor/input/tr.lproj/app.json index 4cae430f9..2abb92845 100644 --- a/Localization/StringsConvertor/input/tr.lproj/app.json +++ b/Localization/StringsConvertor/input/tr.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "Bu %s bozuk ve Mastodon'a\nyüklenemiyor.", "description_photo": "Görme engelliler için fotoğrafı tarif edin...", - "description_video": "Görme engelliler için videoyu tarif edin..." + "description_video": "Görme engelliler için videoyu tarif edin...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Süre: %s", From 3186a54d7b6b7af013c28b1e570b8c89076383fd Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:51 +0100 Subject: [PATCH 183/224] New translations app.json (Czech) --- Localization/StringsConvertor/input/cs.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/cs.lproj/app.json b/Localization/StringsConvertor/input/cs.lproj/app.json index 97d210179..f916343fd 100644 --- a/Localization/StringsConvertor/input/cs.lproj/app.json +++ b/Localization/StringsConvertor/input/cs.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "Tento %s je poškozený a nemůže být\nnahrán do Mastodonu.", "description_photo": "Popište fotografii pro zrakově postižené osoby...", - "description_video": "Popište video pro zrakově postižené..." + "description_video": "Popište video pro zrakově postižené...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Doba trvání: %s", From 923bab23008ec45b3cf5e78aece80052d3715053 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:52 +0100 Subject: [PATCH 184/224] New translations app.json (Scottish Gaelic) --- Localization/StringsConvertor/input/gd.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/gd.lproj/app.json b/Localization/StringsConvertor/input/gd.lproj/app.json index 65a666396..c1d17f813 100644 --- a/Localization/StringsConvertor/input/gd.lproj/app.json +++ b/Localization/StringsConvertor/input/gd.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "Seo %s a tha briste is cha ghabh\na luchdadh suas gu Mastodon.", "description_photo": "Mìnich an dealbh dhan fheadhainn air a bheil cion-lèirsinne…", - "description_video": "Mìnich a’ video dhan fheadhainn air a bheil cion-lèirsinne…" + "description_video": "Mìnich a’ video dhan fheadhainn air a bheil cion-lèirsinne…", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Faide: %s", From 03616dd0824f9905c70c11097cd708d8934fc1c8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:53 +0100 Subject: [PATCH 185/224] New translations app.json (Finnish) --- Localization/StringsConvertor/input/fi.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/fi.lproj/app.json b/Localization/StringsConvertor/input/fi.lproj/app.json index 887c44a99..a42642786 100644 --- a/Localization/StringsConvertor/input/fi.lproj/app.json +++ b/Localization/StringsConvertor/input/fi.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "Kuvaile kuva näkövammaisille...", - "description_video": "Kuvaile video näkövammaisille..." + "description_video": "Kuvaile video näkövammaisille...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Kesto: %s", From 0009735485bad273b5daa8f44cfd4e3bfbe93fc6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:54 +0100 Subject: [PATCH 186/224] New translations app.json (Romanian) --- Localization/StringsConvertor/input/ro.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ro.lproj/app.json b/Localization/StringsConvertor/input/ro.lproj/app.json index 11b25f687..a9d3804fa 100644 --- a/Localization/StringsConvertor/input/ro.lproj/app.json +++ b/Localization/StringsConvertor/input/ro.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "Describe the photo for the visually-impaired...", - "description_video": "Describe the video for the visually-impaired..." + "description_video": "Describe the video for the visually-impaired...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Duration: %s", From 77761b58fbe32d4da7242a9cfb804318fc64a0c1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:55 +0100 Subject: [PATCH 187/224] New translations app.json (Spanish) --- Localization/StringsConvertor/input/es.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/es.lproj/app.json b/Localization/StringsConvertor/input/es.lproj/app.json index 2afb0cd9c..7eaff340d 100644 --- a/Localization/StringsConvertor/input/es.lproj/app.json +++ b/Localization/StringsConvertor/input/es.lproj/app.json @@ -382,7 +382,11 @@ "video": "vídeo", "attachment_broken": "Este %s está roto y no puede\nsubirse a Mastodon.", "description_photo": "Describe la foto para los usuarios con dificultad visual...", - "description_video": "Describe el vídeo para los usuarios con dificultad visual..." + "description_video": "Describe el vídeo para los usuarios con dificultad visual...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Duración: %s", From cf4c05aea1ffd1c16ed4e4d0d7f045810ee121f9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:56 +0100 Subject: [PATCH 188/224] New translations app.json (Arabic) --- Localization/StringsConvertor/input/ar.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ar.lproj/app.json b/Localization/StringsConvertor/input/ar.lproj/app.json index 02132355c..4c5ac4c8b 100644 --- a/Localization/StringsConvertor/input/ar.lproj/app.json +++ b/Localization/StringsConvertor/input/ar.lproj/app.json @@ -382,7 +382,11 @@ "video": "مقطع مرئي", "attachment_broken": "هذا ال%s مُعطَّل\nويتعذَّرُ رفعُه إلى ماستودون.", "description_photo": "صِف الصورة للمَكفوفين...", - "description_video": "صِف المقطع المرئي للمَكفوفين..." + "description_video": "صِف المقطع المرئي للمَكفوفين...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "المُدَّة: %s", From 60cfd3a1d4b8b412471a2b06b532431fc7500bd2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:57 +0100 Subject: [PATCH 189/224] New translations app.json (Catalan) --- Localization/StringsConvertor/input/ca.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ca.lproj/app.json b/Localization/StringsConvertor/input/ca.lproj/app.json index 45497e67d..a87870865 100644 --- a/Localization/StringsConvertor/input/ca.lproj/app.json +++ b/Localization/StringsConvertor/input/ca.lproj/app.json @@ -382,7 +382,11 @@ "video": "vídeo", "attachment_broken": "Aquest %s està trencat i no pot ser\ncarregat a Mastodon.", "description_photo": "Descriu la foto per als disminuïts visuals...", - "description_video": "Descriu el vídeo per als disminuïts visuals..." + "description_video": "Descriu el vídeo per als disminuïts visuals...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Durada: %s", From 3ba0638bece5ab1344302c7f3525edb7274ebf2a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:58 +0100 Subject: [PATCH 190/224] New translations app.json (Danish) --- Localization/StringsConvertor/input/da.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/da.lproj/app.json b/Localization/StringsConvertor/input/da.lproj/app.json index c5a3dac74..a6a971860 100644 --- a/Localization/StringsConvertor/input/da.lproj/app.json +++ b/Localization/StringsConvertor/input/da.lproj/app.json @@ -382,7 +382,11 @@ "video": "video", "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "Describe the photo for the visually-impaired...", - "description_video": "Describe the video for the visually-impaired..." + "description_video": "Describe the video for the visually-impaired...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Duration: %s", From e51a4c7f286b09aa04bc4ce92286713ca1ac6a66 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:36:59 +0100 Subject: [PATCH 191/224] New translations app.json (German) --- Localization/StringsConvertor/input/de.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/de.lproj/app.json b/Localization/StringsConvertor/input/de.lproj/app.json index aa5ea3b1b..9cd262478 100644 --- a/Localization/StringsConvertor/input/de.lproj/app.json +++ b/Localization/StringsConvertor/input/de.lproj/app.json @@ -382,7 +382,11 @@ "video": "Video", "attachment_broken": "Dieses %s scheint defekt zu sein und\nkann nicht auf Mastodon hochgeladen werden.", "description_photo": "Für Menschen mit Sehbehinderung beschreiben...", - "description_video": "Für Menschen mit Sehbehinderung beschreiben..." + "description_video": "Für Menschen mit Sehbehinderung beschreiben...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Dauer: %s", From c1e15aa7f7ac5aa63b627e460edf225ca65b0e80 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:37:00 +0100 Subject: [PATCH 192/224] New translations app.json (Basque) --- Localization/StringsConvertor/input/eu.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/eu.lproj/app.json b/Localization/StringsConvertor/input/eu.lproj/app.json index 94720218f..3f58f522c 100644 --- a/Localization/StringsConvertor/input/eu.lproj/app.json +++ b/Localization/StringsConvertor/input/eu.lproj/app.json @@ -382,7 +382,11 @@ "video": "bideoa", "attachment_broken": "%s hondatuta dago eta ezin da\nMastodonera igo.", "description_photo": "Deskribatu argazkia ikusmen arazoak dituztenentzat...", - "description_video": "Deskribatu bideoa ikusmen arazoak dituztenentzat..." + "description_video": "Deskribatu bideoa ikusmen arazoak dituztenentzat...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "Iraupena: %s", From 83a46304f2e8c8b6fda6f7edaaa61058a2cee574 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 18:37:01 +0100 Subject: [PATCH 193/224] New translations app.json (Sorani (Kurdish)) --- Localization/StringsConvertor/input/ckb.lproj/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ckb.lproj/app.json b/Localization/StringsConvertor/input/ckb.lproj/app.json index e3db76643..25452f38b 100644 --- a/Localization/StringsConvertor/input/ckb.lproj/app.json +++ b/Localization/StringsConvertor/input/ckb.lproj/app.json @@ -382,7 +382,11 @@ "video": "ڤیدیۆ", "attachment_broken": "ئەم %sـە تێک چووە و ناتوانیت بەرزی بکەیتەوە.", "description_photo": "وێنەکەت بۆ نابیناکان باس بکە...", - "description_video": "ڤیدیۆکەت بۆ نابیناکان باس بکە..." + "description_video": "ڤیدیۆکەت بۆ نابیناکان باس بکە...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "attachment_too_large": "Attachment too large" }, "poll": { "duration_time": "کات:‌ %s", From 208870ebafea12b1ef87d8936fb670c1902de88a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 19:45:40 +0100 Subject: [PATCH 194/224] New translations app.json (Catalan) --- Localization/StringsConvertor/input/ca.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/ca.lproj/app.json b/Localization/StringsConvertor/input/ca.lproj/app.json index a87870865..7164d1d12 100644 --- a/Localization/StringsConvertor/input/ca.lproj/app.json +++ b/Localization/StringsConvertor/input/ca.lproj/app.json @@ -383,10 +383,10 @@ "attachment_broken": "Aquest %s està trencat i no pot ser\ncarregat a Mastodon.", "description_photo": "Descriu la foto per als disminuïts visuals...", "description_video": "Descriu el vídeo per als disminuïts visuals...", - "load_failed": "Load Failed", - "upload_failed": "Upload Failed", - "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", - "attachment_too_large": "Attachment too large" + "load_failed": "Ha fallat la càrrega", + "upload_failed": "Pujada fallida", + "can_not_recognize_this_media_attachment": "No es pot reconèixer l'adjunt multimèdia", + "attachment_too_large": "El fitxer adjunt és massa gran" }, "poll": { "duration_time": "Durada: %s", From f8f368023b595ca482cdd4f1bf1261844426cd43 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 19:45:41 +0100 Subject: [PATCH 195/224] New translations app.json (German) --- Localization/StringsConvertor/input/de.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/de.lproj/app.json b/Localization/StringsConvertor/input/de.lproj/app.json index 9cd262478..481f07a4a 100644 --- a/Localization/StringsConvertor/input/de.lproj/app.json +++ b/Localization/StringsConvertor/input/de.lproj/app.json @@ -383,10 +383,10 @@ "attachment_broken": "Dieses %s scheint defekt zu sein und\nkann nicht auf Mastodon hochgeladen werden.", "description_photo": "Für Menschen mit Sehbehinderung beschreiben...", "description_video": "Für Menschen mit Sehbehinderung beschreiben...", - "load_failed": "Load Failed", - "upload_failed": "Upload Failed", - "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", - "attachment_too_large": "Attachment too large" + "load_failed": "Laden fehlgeschlagen", + "upload_failed": "Upload fehlgeschlagen", + "can_not_recognize_this_media_attachment": "Medienanhang wurde nicht erkannt", + "attachment_too_large": "Anhang zu groß" }, "poll": { "duration_time": "Dauer: %s", From 54211a90c1d6b75b8073c5b807c5ef4da92d6eaf Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 19:45:42 +0100 Subject: [PATCH 196/224] New translations app.json (Italian) --- Localization/StringsConvertor/input/it.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json index b4c6aab70..73f42d1eb 100644 --- a/Localization/StringsConvertor/input/it.lproj/app.json +++ b/Localization/StringsConvertor/input/it.lproj/app.json @@ -383,10 +383,10 @@ "attachment_broken": "Questo %s è rotto e non può essere\ncaricato su Mastodon.", "description_photo": "Descrivi la foto per gli utenti ipovedenti...", "description_video": "Descrivi il filmato per gli utenti ipovedenti...", - "load_failed": "Load Failed", - "upload_failed": "Upload Failed", - "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", - "attachment_too_large": "Attachment too large" + "load_failed": "Caricamento fallito", + "upload_failed": "Caricamento fallito", + "can_not_recognize_this_media_attachment": "Impossibile riconoscere questo allegato multimediale", + "attachment_too_large": "Allegato troppo grande" }, "poll": { "duration_time": "Durata: %s", From 6782f228fc0164a01dbeda33cf2dc24c7b3561ca Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 19:45:43 +0100 Subject: [PATCH 197/224] New translations app.json (Spanish, Argentina) --- Localization/StringsConvertor/input/es-AR.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/es-AR.lproj/app.json b/Localization/StringsConvertor/input/es-AR.lproj/app.json index 9b8f9f522..309cf4d34 100644 --- a/Localization/StringsConvertor/input/es-AR.lproj/app.json +++ b/Localization/StringsConvertor/input/es-AR.lproj/app.json @@ -383,10 +383,10 @@ "attachment_broken": "Este archivo de %s está roto\ny no se puede subir a Mastodon.", "description_photo": "Describí la imagen para personas con dificultades visuales…", "description_video": "Describí el video para personas con dificultades visuales…", - "load_failed": "Load Failed", - "upload_failed": "Upload Failed", - "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", - "attachment_too_large": "Attachment too large" + "load_failed": "Falló la descarga", + "upload_failed": "Falló la subida", + "can_not_recognize_this_media_attachment": "No se pudo reconocer este archivo adjunto", + "attachment_too_large": "Adjunto demasiado grande" }, "poll": { "duration_time": "Duración: %s", From d3607ea0f1f02c6a2906cb14a7a68f87e36c3be4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 19:45:45 +0100 Subject: [PATCH 198/224] New translations app.json (Kurmanji (Kurdish)) --- Localization/StringsConvertor/input/kmr.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/kmr.lproj/app.json b/Localization/StringsConvertor/input/kmr.lproj/app.json index 9991169f4..098f514ee 100644 --- a/Localization/StringsConvertor/input/kmr.lproj/app.json +++ b/Localization/StringsConvertor/input/kmr.lproj/app.json @@ -383,10 +383,10 @@ "attachment_broken": "Ev %s naxebite û nayê barkirin\n li ser Mastodon.", "description_photo": "Wêneyê ji bo kêmbînên dîtbar bide nasîn...", "description_video": "Vîdyoyê ji bo kêmbînên dîtbar bide nasîn...", - "load_failed": "Load Failed", - "upload_failed": "Upload Failed", - "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", - "attachment_too_large": "Attachment too large" + "load_failed": "Barkirin têk çû", + "upload_failed": "Barkirin têk çû", + "can_not_recognize_this_media_attachment": "Nikare ev pêveka medyayê nas bike", + "attachment_too_large": "Pêvek pir mezin e" }, "poll": { "duration_time": "Dirêjî: %s", From 0b04a423086bec96e79a5e5e8de885f2bfb1a6d9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 20:56:34 +0100 Subject: [PATCH 199/224] New translations app.json (Czech) --- Localization/StringsConvertor/input/cs.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/cs.lproj/app.json b/Localization/StringsConvertor/input/cs.lproj/app.json index f916343fd..4050352f4 100644 --- a/Localization/StringsConvertor/input/cs.lproj/app.json +++ b/Localization/StringsConvertor/input/cs.lproj/app.json @@ -383,10 +383,10 @@ "attachment_broken": "Tento %s je poškozený a nemůže být\nnahrán do Mastodonu.", "description_photo": "Popište fotografii pro zrakově postižené osoby...", "description_video": "Popište video pro zrakově postižené...", - "load_failed": "Load Failed", - "upload_failed": "Upload Failed", - "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", - "attachment_too_large": "Attachment too large" + "load_failed": "Načtení se nezdařilo", + "upload_failed": "Nahrání selhalo", + "can_not_recognize_this_media_attachment": "Nelze rozpoznat toto medium přílohy", + "attachment_too_large": "Příloha je příliš velká" }, "poll": { "duration_time": "Doba trvání: %s", From 8dcdd92cd69ff701c320878a0a1b12897d402cec Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 20:56:35 +0100 Subject: [PATCH 200/224] New translations app.json (Thai) --- Localization/StringsConvertor/input/th.lproj/app.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/th.lproj/app.json b/Localization/StringsConvertor/input/th.lproj/app.json index ffdad5b75..9b4316025 100644 --- a/Localization/StringsConvertor/input/th.lproj/app.json +++ b/Localization/StringsConvertor/input/th.lproj/app.json @@ -383,10 +383,10 @@ "attachment_broken": "%s นี้เสียหายและไม่สามารถ\nอัปโหลดไปยัง Mastodon", "description_photo": "อธิบายรูปภาพสำหรับผู้บกพร่องทางการมองเห็น...", "description_video": "อธิบายวิดีโอสำหรับผู้บกพร่องทางการมองเห็น...", - "load_failed": "Load Failed", - "upload_failed": "Upload Failed", + "load_failed": "การโหลดล้มเหลว", + "upload_failed": "การอัปโหลดล้มเหลว", "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", - "attachment_too_large": "Attachment too large" + "attachment_too_large": "ไฟล์แนบใหญ่เกินไป" }, "poll": { "duration_time": "ระยะเวลา: %s", From 454e77e495be9526310af00008c105a42860cf91 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Nov 2022 20:56:36 +0100 Subject: [PATCH 201/224] New translations Localizable.stringsdict (Czech) --- .../input/cs.lproj/Localizable.stringsdict | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict index 21832870a..827bd79e6 100644 --- a/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict @@ -208,13 +208,13 @@ NSStringFormatValueTypeKey ld one - 1 reply + 1 odpověď few - %ld replies + %ld odpovědi many - %ld replies + %ld odpovědí other - %ld replies + %ld odpovědí plural.count.vote @@ -228,13 +228,13 @@ NSStringFormatValueTypeKey ld one - 1 vote + 1 hlas few - %ld votes + %ld hlasy many - %ld votes + %ld hlasů other - %ld votes + %ld hlasů plural.count.voter @@ -248,13 +248,13 @@ NSStringFormatValueTypeKey ld one - 1 voter + 1 hlasující few - %ld voters + %ld hlasující many - %ld voters + %ld hlasujících other - %ld voters + %ld hlasujících plural.people_talking From 1e7da6e82c21076c7783fb02a40648617d3b7926 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 01:00:34 +0100 Subject: [PATCH 202/224] New translations app.json (Swedish) --- Localization/StringsConvertor/input/sv.lproj/app.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/sv.lproj/app.json b/Localization/StringsConvertor/input/sv.lproj/app.json index 14d670921..51e8f1c12 100644 --- a/Localization/StringsConvertor/input/sv.lproj/app.json +++ b/Localization/StringsConvertor/input/sv.lproj/app.json @@ -383,9 +383,9 @@ "attachment_broken": "Denna %s är trasig och kan inte\nladdas upp till Mastodon.", "description_photo": "Beskriv fotot för synskadade...", "description_video": "Beskriv videon för de synskadade...", - "load_failed": "Load Failed", - "upload_failed": "Upload Failed", - "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "load_failed": "Det gick inte att läsa in", + "upload_failed": "Uppladdning misslyckades", + "can_not_recognize_this_media_attachment": "Känner inte igen mediebilagan", "attachment_too_large": "Attachment too large" }, "poll": { From 28b3c25c1eee2d9d7efd4829ee045b696d7aafe5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 01:58:05 +0100 Subject: [PATCH 203/224] New translations app.json (Swedish) --- Localization/StringsConvertor/input/sv.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/sv.lproj/app.json b/Localization/StringsConvertor/input/sv.lproj/app.json index 51e8f1c12..c740609c9 100644 --- a/Localization/StringsConvertor/input/sv.lproj/app.json +++ b/Localization/StringsConvertor/input/sv.lproj/app.json @@ -386,7 +386,7 @@ "load_failed": "Det gick inte att läsa in", "upload_failed": "Uppladdning misslyckades", "can_not_recognize_this_media_attachment": "Känner inte igen mediebilagan", - "attachment_too_large": "Attachment too large" + "attachment_too_large": "Bilagan är för stor" }, "poll": { "duration_time": "Längd: %s", From a1ef060132bb9768ef79093b84a1db0776c6c614 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 04:37:02 +0100 Subject: [PATCH 204/224] New translations app.json (Korean) --- Localization/StringsConvertor/input/ko.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/ko.lproj/app.json b/Localization/StringsConvertor/input/ko.lproj/app.json index 1d5019474..826ac389c 100644 --- a/Localization/StringsConvertor/input/ko.lproj/app.json +++ b/Localization/StringsConvertor/input/ko.lproj/app.json @@ -383,10 +383,10 @@ "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "시각장애인을 위한 사진 설명…", "description_video": "시각장애인을 위한 영상 설명…", - "load_failed": "Load Failed", - "upload_failed": "Upload Failed", - "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", - "attachment_too_large": "Attachment too large" + "load_failed": "불러오기 실패", + "upload_failed": "업로드 실패", + "can_not_recognize_this_media_attachment": "이 미디어 첨부파일을 인식할 수 없습니다", + "attachment_too_large": "첨부파일이 너무 큽니다" }, "poll": { "duration_time": "기간: %s", From 208cc3aa4d602430fe479caa17526a4912e8e7d3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 05:49:38 +0100 Subject: [PATCH 205/224] New translations app.json (Portuguese, Brazilian) --- .../input/pt-BR.lproj/app.json | 196 +++++++++--------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/app.json b/Localization/StringsConvertor/input/pt-BR.lproj/app.json index 1e0a60511..dfcad6cf5 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/app.json +++ b/Localization/StringsConvertor/input/pt-BR.lproj/app.json @@ -6,7 +6,7 @@ "please_try_again_later": "Tente novamente mais tarde." }, "sign_up_failure": { - "title": "Sign Up Failure" + "title": "Falha no cadastro" }, "server_error": { "title": "Erro do servidor" @@ -17,7 +17,7 @@ }, "discard_post_content": { "title": "Deletar Rascunho", - "message": "Confirm to discard composed post content." + "message": "Confirme para descartar o conteúdo da publicação composta." }, "publish_post_failure": { "title": "Falha ao publicar", @@ -42,7 +42,7 @@ }, "save_photo_failure": { "title": "Falha ao salvar foto", - "message": "Please enable the photo library access permission to save the photo." + "message": "Por favor, ative a permissão de acesso à galeria para salvar a foto." }, "delete_post": { "title": "Deletar Toot", @@ -71,137 +71,137 @@ "cancel": "Cancelar", "discard": "Descartar", "try_again": "Tente novamente", - "take_photo": "Take Photo", + "take_photo": "Tirar foto", "save_photo": "Salvar foto", "copy_photo": "Copiar foto", - "sign_in": "Sign In", - "sign_up": "Sign Up", - "see_more": "See More", - "preview": "Preview", + "sign_in": "Entrar", + "sign_up": "Criar conta", + "see_more": "Ver mais", + "preview": "Pré-visualização", "share": "Compartilhar", "share_user": "Compartilhar %s", - "share_post": "Share Post", - "open_in_safari": "Open in Safari", - "open_in_browser": "Open in Browser", - "find_people": "Find people to follow", - "manually_search": "Manually search instead", - "skip": "Skip", - "reply": "Reply", - "report_user": "Report %s", - "block_domain": "Block %s", - "unblock_domain": "Unblock %s", - "settings": "Settings", - "delete": "Delete" + "share_post": "Compartilhar postagem", + "open_in_safari": "Abrir no Safari", + "open_in_browser": "Abrir no navegador", + "find_people": "Encontre pessoas para seguir", + "manually_search": "Procure manualmente em vez disso", + "skip": "Pular", + "reply": "Responder", + "report_user": "Denunciar %s", + "block_domain": "Bloquear %s", + "unblock_domain": "Desbloquear %s", + "settings": "Configurações", + "delete": "Excluir" }, "tabs": { - "home": "Home", - "search": "Search", - "notification": "Notification", - "profile": "Profile" + "home": "Início", + "search": "Buscar", + "notification": "Notificação", + "profile": "Perfil" }, "keyboard": { "common": { - "switch_to_tab": "Switch to %s", - "compose_new_post": "Compose New Post", - "show_favorites": "Show Favorites", - "open_settings": "Open Settings" + "switch_to_tab": "Mudar para %s", + "compose_new_post": "Compor novo toot", + "show_favorites": "Mostrar favoritos", + "open_settings": "Abrir configurações" }, "timeline": { - "previous_status": "Previous Post", - "next_status": "Next Post", - "open_status": "Open Post", - "open_author_profile": "Open Author's Profile", - "open_reblogger_profile": "Open Reblogger's Profile", - "reply_status": "Reply to Post", - "toggle_reblog": "Toggle Reblog on Post", - "toggle_favorite": "Toggle Favorite on Post", - "toggle_content_warning": "Toggle Content Warning", - "preview_image": "Preview Image" + "previous_status": "Postagem anterior", + "next_status": "Próxima postagem", + "open_status": "Abrir toot", + "open_author_profile": "Abrir perfil do autor", + "open_reblogger_profile": "Abrir perfil do reblogger", + "reply_status": "Responder toot", + "toggle_reblog": "Ativar/desativar Reblog na postagem", + "toggle_favorite": "Ativar/desativar Favorito na postagem", + "toggle_content_warning": "Ativar/desativar Aviso de Conteúdo", + "preview_image": "Pré-visualizar imagem" }, "segmented_control": { - "previous_section": "Previous Section", - "next_section": "Next Section" + "previous_section": "Seção anterior", + "next_section": "Próxima seção" } }, "status": { - "user_reblogged": "%s reblogged", - "user_replied_to": "Replied to %s", - "show_post": "Show Post", - "show_user_profile": "Show user profile", - "content_warning": "Content Warning", - "sensitive_content": "Sensitive Content", - "media_content_warning": "Tap anywhere to reveal", - "tap_to_reveal": "Tap to reveal", + "user_reblogged": "%s reblogou", + "user_replied_to": "Em resposta a %s", + "show_post": "Mostrar postagem", + "show_user_profile": "Mostrar perfil de usuário", + "content_warning": "Aviso de Conteúdo", + "sensitive_content": "Conteúdo sensível", + "media_content_warning": "Toque em qualquer lugar para revelar", + "tap_to_reveal": "Toque para revelar", "poll": { - "vote": "Vote", - "closed": "Closed" + "vote": "Votar", + "closed": "Fechado" }, "meta_entity": { "url": "Link: %s", "hashtag": "Hashtag: %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "mention": "Mostrar perfil: %s", + "email": "Endereço de e-mail: %s" }, "actions": { "reply": "Responder", - "reblog": "Reblog", - "unreblog": "Undo reblog", - "favorite": "Favorite", - "unfavorite": "Unfavorite", + "reblog": "Reblogar", + "unreblog": "Desfazer reblog", + "favorite": "Favoritar", + "unfavorite": "Remover favorito", "menu": "Menu", - "hide": "Hide", - "show_image": "Show image", - "show_gif": "Show GIF", - "show_video_player": "Show video player", - "tap_then_hold_to_show_menu": "Tap then hold to show menu" + "hide": "Ocultar", + "show_image": "Exibir imagem", + "show_gif": "Exibir GIF", + "show_video_player": "Mostrar reprodutor de vídeo", + "tap_then_hold_to_show_menu": "Toque e em seguida segure para exibir o menu" }, "tag": { "url": "URL", - "mention": "Mention", + "mention": "Mencionar", "link": "Link", "hashtag": "Hashtag", - "email": "Email", + "email": "E-mail", "emoji": "Emoji" }, "visibility": { "unlisted": "Everyone can see this post but not display in the public timeline.", - "private": "Only their followers can see this post.", - "private_from_me": "Only my followers can see this post.", - "direct": "Only mentioned user can see this post." + "private": "Somente seus seguidores podem ver essa postagem.", + "private_from_me": "Somente meus seguidores podem ver essa postagem.", + "direct": "Somente o usuário mencionado pode ver essa postagem." } }, "friendship": { - "follow": "Follow", - "following": "Following", - "request": "Request", - "pending": "Pending", - "block": "Block", - "block_user": "Block %s", - "block_domain": "Block %s", - "unblock": "Unblock", - "unblock_user": "Unblock %s", - "blocked": "Blocked", - "mute": "Mute", - "mute_user": "Mute %s", - "unmute": "Unmute", - "unmute_user": "Unmute %s", - "muted": "Muted", - "edit_info": "Edit Info", - "show_reblogs": "Show Reblogs", - "hide_reblogs": "Hide Reblogs" + "follow": "Seguir", + "following": "Seguindo", + "request": "Solicitação", + "pending": "Pendente", + "block": "Bloquear", + "block_user": "Bloquear %s", + "block_domain": "Bloquear %s", + "unblock": "Desbloquear", + "unblock_user": "Desbloquear %s", + "blocked": "Bloqueado", + "mute": "Silenciar", + "mute_user": "Silenciar %s", + "unmute": "Remover silenciado", + "unmute_user": "Remover silenciado %s", + "muted": "Silenciado", + "edit_info": "Editar informação", + "show_reblogs": "Mostrar Reblogs", + "hide_reblogs": "Ocultar Reblogs" }, "timeline": { - "filtered": "Filtered", + "filtered": "Filtrado", "timestamp": { - "now": "Now" + "now": "Agora" }, "loader": { - "load_missing_posts": "Load missing posts", - "loading_missing_posts": "Loading missing posts...", - "show_more_replies": "Show more replies" + "load_missing_posts": "Carregar postagens em falta", + "loading_missing_posts": "Carregando postagens em falta...", + "show_more_replies": "Exibir mais respostas" }, "header": { - "no_status_found": "No Post Found", + "no_status_found": "Nenhuma postagem encontrada", "blocking_warning": "You can’t view this user's profile\nuntil you unblock them.\nYour profile looks like this to them.", "user_blocking_warning": "You can’t view %s’s profile\nuntil you unblock them.\nYour profile looks like this to them.", "blocked_warning": "You can’t view this user’s profile\nuntil they unblock you.", @@ -290,17 +290,17 @@ }, "error": { "item": { - "username": "Username", - "email": "Email", - "password": "Password", - "agreement": "Agreement", - "locale": "Locale", - "reason": "Reason" + "username": "Nome de usuário", + "email": "E-mail", + "password": "Senha", + "agreement": "Termos de uso", + "locale": "Localidade", + "reason": "Motivo" }, "reason": { "blocked": "%s contains a disallowed email provider", - "unreachable": "%s does not seem to exist", - "taken": "%s is already in use", + "unreachable": "%s parece não existir", + "taken": "%s já está em uso", "reserved": "%s is a reserved keyword", "accepted": "%s must be accepted", "blank": "%s is required", From b730c3784d2c9078cad082df0dec4154ce1bbbe4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 05:49:39 +0100 Subject: [PATCH 206/224] New translations ios-infoPlist.json (Portuguese, Brazilian) --- .../StringsConvertor/input/pt-BR.lproj/ios-infoPlist.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/pt-BR.lproj/ios-infoPlist.json index c6db73de0..04b53a160 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/ios-infoPlist.json +++ b/Localization/StringsConvertor/input/pt-BR.lproj/ios-infoPlist.json @@ -1,6 +1,6 @@ { - "NSCameraUsageDescription": "Used to take photo for post status", - "NSPhotoLibraryAddUsageDescription": "Used to save photo into the Photo Library", - "NewPostShortcutItemTitle": "New Post", - "SearchShortcutItemTitle": "Search" + "NSCameraUsageDescription": "Usado para tirar uma foto para postagem", + "NSPhotoLibraryAddUsageDescription": "Usado para salvar foto na Galeria", + "NewPostShortcutItemTitle": "Novo Toot", + "SearchShortcutItemTitle": "Buscar" } From ba1cc9ae6f748579c0cb5db8e5672eb3f3cc4bb4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 05:49:41 +0100 Subject: [PATCH 207/224] New translations Intents.stringsdict (Portuguese, Brazilian) --- .../Intents/input/pt-BR.lproj/Intents.stringsdict | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.stringsdict index 18422c772..a48559b4a 100644 --- a/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.stringsdict +++ b/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.stringsdict @@ -5,7 +5,7 @@ There are ${count} options matching ‘${content}’. - 2 NSStringLocalizedFormatKey - There are %#@count_option@ matching ‘${content}’. + Existem %#@count_option@ opções correspondentes a ‘${content}’. count_option NSStringFormatSpecTypeKey @@ -13,15 +13,15 @@ NSStringFormatValueTypeKey %ld one - 1 option + 1 opção other - %ld options + %ld opções There are ${count} options matching ‘${visibility}’. NSStringLocalizedFormatKey - There are %#@count_option@ matching ‘${visibility}’. + Existem %#@count_option@ opções correspondentes a ‘${visibility}’. count_option NSStringFormatSpecTypeKey @@ -29,9 +29,9 @@ NSStringFormatValueTypeKey %ld one - 1 option + 1 opção other - %ld options + %ld opções From 906bad32d7c12dfb0b4776fcb6223d72298697dd Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 09:20:56 +0100 Subject: [PATCH 208/224] New translations app.json (Portuguese, Brazilian) --- .../input/pt-BR.lproj/app.json | 138 +++++++++--------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/app.json b/Localization/StringsConvertor/input/pt-BR.lproj/app.json index dfcad6cf5..26e6edb76 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/app.json +++ b/Localization/StringsConvertor/input/pt-BR.lproj/app.json @@ -204,85 +204,85 @@ "no_status_found": "Nenhuma postagem encontrada", "blocking_warning": "You can’t view this user's profile\nuntil you unblock them.\nYour profile looks like this to them.", "user_blocking_warning": "You can’t view %s’s profile\nuntil you unblock them.\nYour profile looks like this to them.", - "blocked_warning": "You can’t view this user’s profile\nuntil they unblock you.", - "user_blocked_warning": "You can’t view %s’s profile\nuntil they unblock you.", - "suspended_warning": "This user has been suspended.", - "user_suspended_warning": "%s’s account has been suspended." + "blocked_warning": "Você não pode ver o perfil desse usuário até que ele o desbloqueie.", + "user_blocked_warning": "Você não pode ver o perfil de %s até que ele o desbloqueie.", + "suspended_warning": "Esse usuário foi suspenso.", + "user_suspended_warning": "A conta de %s foi suspensa." } } } }, "scene": { "welcome": { - "slogan": "Social networking\nback in your hands.", - "get_started": "Get Started", - "log_in": "Log In" + "slogan": "Você no controle de sua rede social.", + "get_started": "Comece já", + "log_in": "Entrar" }, "server_picker": { - "title": "Mastodon is made of users in different servers.", - "subtitle": "Pick a server based on your interests, region, or a general purpose one.", - "subtitle_extend": "Pick a server based on your interests, region, or a general purpose one. Each server is operated by an entirely independent organization or individual.", + "title": "Mastodon é feito de usuários em instâncias diferentes.", + "subtitle": "Escolha uma instância baseada nos seus interesses, região, ou em uma proposta geral.", + "subtitle_extend": "Escolha uma instância baseada nos seus interesses, região, ou em uma proposta geral. Cada instância é operada por um indivíduo ou uma organização totalmente independente.", "button": { "category": { - "all": "All", - "all_accessiblity_description": "Category: All", - "academia": "academia", - "activism": "activism", - "food": "food", + "all": "Todos", + "all_accessiblity_description": "Categoria: Todos", + "academia": "acadêmico", + "activism": "ativismo", + "food": "comida", "furry": "furry", - "games": "games", - "general": "general", - "journalism": "journalism", + "games": "jogos", + "general": "geral", + "journalism": "jornalismo", "lgbt": "lgbt", "regional": "regional", - "art": "art", - "music": "music", - "tech": "tech" + "art": "arte", + "music": "música", + "tech": "tecnologia" }, - "see_less": "See Less", - "see_more": "See More" + "see_less": "Ver menos", + "see_more": "Ver mais" }, "label": { - "language": "LANGUAGE", - "users": "USERS", - "category": "CATEGORY" + "language": "Idioma", + "users": "Usuários", + "category": "Categoria" }, "input": { - "placeholder": "Search servers", - "search_servers_or_enter_url": "Search servers or enter URL" + "placeholder": "Procurar instâncias", + "search_servers_or_enter_url": "Procurar instâncias ou inserir URL" }, "empty_state": { - "finding_servers": "Finding available servers...", - "bad_network": "Something went wrong while loading the data. Check your internet connection.", - "no_results": "No results" + "finding_servers": "Procurando instâncias disponíveis...", + "bad_network": "Algo deu errado ao carregar os dados. Verifique sua conexão com a internet.", + "no_results": "Sem resultados" } }, "register": { - "title": "Let’s get you set up on %s", + "title": "Vamos configurar você em %s", "lets_get_you_set_up_on_domain": "Let’s get you set up on %s", "input": { "avatar": { - "delete": "Delete" + "delete": "Excluir" }, "username": { - "placeholder": "username", - "duplicate_prompt": "This username is taken." + "placeholder": "nome de usuário", + "duplicate_prompt": "Esse nome de usuário já está sendo usado." }, "display_name": { - "placeholder": "display name" + "placeholder": "nome de exibição" }, "email": { - "placeholder": "email" + "placeholder": "e-mail" }, "password": { - "placeholder": "password", - "require": "Your password needs at least:", - "character_limit": "8 characters", + "placeholder": "senha", + "require": "Sua senha deve ter pelo menos:", + "character_limit": "8 carácteres", "accessibility": { "checked": "checked", "unchecked": "unchecked" }, - "hint": "Your password needs at least eight characters" + "hint": "Sua senha precisa ter pelo menos oito carácteres" }, "invite": { "registration_user_invite_request": "Why do you want to join?" @@ -372,14 +372,14 @@ "media_selection": { "camera": "Take Photo", "photo_library": "Photo Library", - "browse": "Browse" + "browse": "Navegar" }, - "content_input_placeholder": "Type or paste what’s on your mind", - "compose_action": "Publish", - "replying_to_user": "replying to %s", + "content_input_placeholder": "Digite ou cole o que está na sua mente", + "compose_action": "Publicar", + "replying_to_user": "em resposta a %s", "attachment": { - "photo": "photo", - "video": "video", + "photo": "foto", + "video": "vídeo", "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "Describe the photo for the visually-impaired...", "description_video": "Describe the video for the visually-impaired...", @@ -389,14 +389,14 @@ "attachment_too_large": "Attachment too large" }, "poll": { - "duration_time": "Duration: %s", - "thirty_minutes": "30 minutes", - "one_hour": "1 Hour", - "six_hours": "6 Hours", - "one_day": "1 Day", - "three_days": "3 Days", - "seven_days": "7 Days", - "option_number": "Option %ld" + "duration_time": "Duração: %s", + "thirty_minutes": "30 minutos", + "one_hour": "1 hora", + "six_hours": "6 horas", + "one_day": "1 dia", + "three_days": "3 dias", + "seven_days": "7 dias", + "option_number": "Opção %ld" }, "content_warning": { "placeholder": "Write an accurate warning here..." @@ -433,9 +433,9 @@ "follows_you": "Follows You" }, "dashboard": { - "posts": "posts", - "following": "following", - "followers": "followers" + "posts": "toots", + "following": "seguindo", + "followers": "seguidores" }, "fields": { "add_row": "Add Row", @@ -518,30 +518,30 @@ "accounts": { "title": "Accounts you might like", "description": "You may like to follow these accounts", - "follow": "Follow" + "follow": "Seguir" } }, "searching": { "segment": { - "all": "All", - "people": "People", + "all": "Todos", + "people": "Pessoas", "hashtags": "Hashtags", - "posts": "Posts" + "posts": "Toots" }, "empty_state": { - "no_results": "No results" + "no_results": "Sem resultados" }, - "recent_search": "Recent searches", - "clear": "Clear" + "recent_search": "Pesquisas recentes", + "clear": "Limpar" } }, "discovery": { "tabs": { - "posts": "Posts", + "posts": "Toots", "hashtags": "Hashtags", - "news": "News", - "community": "Community", - "for_you": "For You" + "news": "Notícias", + "community": "Comunidade", + "for_you": "Para você" }, "intro": "These are the posts gaining traction in your corner of Mastodon." }, From 12aa8ac09acb7396303c58c2d9b3d7411cf443ab Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 09:20:57 +0100 Subject: [PATCH 209/224] New translations Intents.strings (Portuguese, Brazilian) --- .../Intents/input/pt-BR.lproj/Intents.strings | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.strings index 6877490ba..4d4e426c6 100644 --- a/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.strings +++ b/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.strings @@ -1,26 +1,26 @@ -"16wxgf" = "Post on Mastodon"; +"16wxgf" = "Postar no Mastodon"; -"751xkl" = "Text Content"; +"751xkl" = "Conteúdo do texto"; -"CsR7G2" = "Post on Mastodon"; +"CsR7G2" = "Postar no Mastodon"; "HZSGTr" = "What content to post?"; "HdGikU" = "Posting failed"; -"KDNTJ4" = "Failure Reason"; +"KDNTJ4" = "Motivo da falha"; -"RHxKOw" = "Send Post with text content"; +"RHxKOw" = "Enviar postagem com conteúdo de texto"; -"RxSqsb" = "Post"; +"RxSqsb" = "Postagem"; -"WCIR3D" = "Post ${content} on Mastodon"; +"WCIR3D" = "Postar ${content} no Mastodon"; -"ZKJSNu" = "Post"; +"ZKJSNu" = "Postar"; "ZS1XaK" = "${content}"; -"ZbSjzC" = "Visibility"; +"ZbSjzC" = "Visibilidade"; "Zo4jgJ" = "Post Visibility"; From 2833771a8fb3c2c3f26a1ec49caf7bfe2d5a2015 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 10:22:03 +0100 Subject: [PATCH 210/224] New translations app.json (Galician) --- Localization/StringsConvertor/input/gl.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index d183c4cb9..3c394be95 100644 --- a/Localization/StringsConvertor/input/gl.lproj/app.json +++ b/Localization/StringsConvertor/input/gl.lproj/app.json @@ -383,10 +383,10 @@ "attachment_broken": "Este %s está estragado e non pode\nser subido a Mastodon.", "description_photo": "Describe a foto para persoas con problemas visuais...", "description_video": "Describe o vídeo para persoas con problemas visuais...", - "load_failed": "Load Failed", - "upload_failed": "Upload Failed", - "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", - "attachment_too_large": "Attachment too large" + "load_failed": "Fallou a carga", + "upload_failed": "Erro na subida", + "can_not_recognize_this_media_attachment": "Non se recoñece o tipo de multimedia", + "attachment_too_large": "Adxunto demasiado grande" }, "poll": { "duration_time": "Duración: %s", From 7b37d46c9b54f1484d76c35afb6df46eda3c52f0 Mon Sep 17 00:00:00 2001 From: David Godfrey Date: Mon, 14 Nov 2022 10:38:32 +0000 Subject: [PATCH 211/224] Update Localization/app.json Co-authored-by: Jed Fox --- Localization/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/app.json b/Localization/app.json index dfb204d8c..867e64aed 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -440,7 +440,7 @@ "content": "Content" }, "verified": { - "short": "Verified at %s", + "short": "Verified on %s", "long": "Ownership of this link was checked on %s" } }, From 7e7f41112e6b26b1f3f89998ecd835a8802979d7 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 14 Nov 2022 18:41:54 +0800 Subject: [PATCH 212/224] fix: visibility missing bind back to source issue --- .../ComposeContentViewController.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index ea6a0136a..7034aeefb 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -334,9 +334,21 @@ extension ComposeContentViewController { viewModel.$isPollActive.assign(to: &composeContentToolbarViewModel.$isPollActive) viewModel.$isEmojiActive.assign(to: &composeContentToolbarViewModel.$isEmojiActive) viewModel.$isContentWarningActive.assign(to: &composeContentToolbarViewModel.$isContentWarningActive) + viewModel.$visibility.assign(to: &composeContentToolbarViewModel.$visibility) viewModel.$maxTextInputLimit.assign(to: &composeContentToolbarViewModel.$maxTextInputLimit) viewModel.$contentWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWeightedLength) viewModel.$contentWarningWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWarningWeightedLength) + + // bind back to source due to visibility not update via delegate + composeContentToolbarViewModel.$visibility + .dropFirst() + .sink { [weak self] visibility in + guard let self = self else { return } + if self.viewModel.visibility != visibility { + self.viewModel.visibility = visibility + } + } + .store(in: &disposeBag) } private func updateAutoCompleteViewControllerLayout() { From 2b2707c600d9a096b3bff5f3e30929e13d9bd328 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 14 Nov 2022 18:53:09 +0800 Subject: [PATCH 213/224] feat: add throttle for post compose auto-complete query --- .../ComposeContent/AutoComplete/AutoCompleteViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift index 7459f68d1..d8fa06db6 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift @@ -73,7 +73,7 @@ final class AutoCompleteViewModel { inputText .removeDuplicates() - .receive(on: DispatchQueue.main) + .throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true) .sink { [weak self] inputText in guard let self = self else { return } self.stateMachine.enter(State.Loading.self) From 4d03e114cac466616f1688192c62fa683d5fa63d Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 14 Nov 2022 19:14:13 +0800 Subject: [PATCH 214/224] fix: iPad navigation bar still could be large title issue --- .../Scene/Compose/ComposeViewController.swift | 46 ++++++------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 6de17e31f..cc595d5bc 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -44,18 +44,20 @@ final class ComposeViewController: UIViewController, NeedsDependency { }() private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:))) - let characterCountLabel: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: 15, weight: .regular) - label.text = "500" - label.textColor = Asset.Colors.Label.secondary.color - label.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(500) - return label - }() - private(set) lazy var characterCountBarButtonItem: UIBarButtonItem = { - let barButtonItem = UIBarButtonItem(customView: characterCountLabel) - return barButtonItem - }() + + // FIXME: deprecated + // let characterCountLabel: UILabel = { + // let label = UILabel() + // label.font = .systemFont(ofSize: 15, weight: .regular) + // label.text = "500" + // label.textColor = Asset.Colors.Label.secondary.color + // label.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(500) + // return label + // }() + // private(set) lazy var characterCountBarButtonItem: UIBarButtonItem = { + // let barButtonItem = UIBarButtonItem(customView: characterCountLabel) + // return barButtonItem + // }() let publishButton: UIButton = { let button = RoundedEdgesButton(type: .custom) @@ -139,16 +141,6 @@ extension ComposeViewController { ]) composeContentViewController.didMove(toParent: self) - // bind navigation bar style - // configureNavigationBarTitleStyle() - viewModel.traitCollectionDidChangePublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self = self else { return } - self.configureNavigationBarTitleStyle() - } - .store(in: &disposeBag) - // bind title viewModel.$title .receive(on: DispatchQueue.main) @@ -226,15 +218,7 @@ extension ComposeViewController { // break // } // } -// - private func configureNavigationBarTitleStyle() { - switch traitCollection.userInterfaceIdiom { - case .pad: - navigationController?.navigationBar.prefersLargeTitles = traitCollection.horizontalSizeClass == .regular - default: - break - } - } +// } From 25f4a6b082e9f454107b9a69201c41c58b38b496 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 14 Nov 2022 19:14:46 +0800 Subject: [PATCH 215/224] feat: restore post compose limit --- .../ComposeContentViewModel.swift | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 2bf4e26ff..91be96248 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -229,6 +229,32 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { break } + // set limit + let _configuration: Mastodon.Entity.Instance.Configuration? = { + var configuration: Mastodon.Entity.Instance.Configuration? = nil + context.managedObjectContext.performAndWait { + guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) + else { return } + configuration = authentication.instance?.configuration + } + return configuration + }() + if let configuration = _configuration { + // set character limit + if let maxCharacters = configuration.statuses?.maxCharacters { + maxTextInputLimit = maxCharacters + } + // set media limit + if let maxMediaAttachments = configuration.statuses?.maxMediaAttachments { + maxMediaAttachmentLimit = maxMediaAttachments + } + // set poll option limit + if let maxOptions = configuration.polls?.maxOptions { + maxPollOptionLimit = maxOptions + } + // TODO: more limit + } + bind() } From bc428486ae1785664465c29056af6707377f4980 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 14 Nov 2022 19:20:14 +0800 Subject: [PATCH 216/224] chore: update i18n resources --- .../Generated/Strings.swift | 10 ++++- .../Resources/ar.lproj/Localizable.strings | 22 ++++++---- .../Resources/ca.lproj/Localizable.strings | 10 ++++- .../Resources/ckb.lproj/Localizable.strings | 8 ++++ .../Resources/de.lproj/Localizable.strings | 20 +++++++--- .../de.lproj/Localizable.stringsdict | 4 +- .../Resources/en.lproj/Localizable.strings | 6 ++- .../Resources/es.lproj/Localizable.strings | 8 ++++ .../Resources/eu.lproj/Localizable.strings | 8 ++++ .../Resources/fi.lproj/Localizable.strings | 8 ++++ .../Resources/fr.lproj/Localizable.strings | 10 ++++- .../Resources/gd.lproj/Localizable.strings | 8 ++++ .../Resources/gl.lproj/Localizable.strings | 22 ++++++---- .../Resources/it.lproj/Localizable.strings | 10 ++++- .../Resources/ja.lproj/Localizable.strings | 8 ++++ .../Resources/kab.lproj/Localizable.strings | 8 ++++ .../Resources/ku.lproj/Localizable.strings | 22 ++++++---- .../Resources/nl.lproj/Localizable.strings | 8 ++++ .../Resources/ru.lproj/Localizable.strings | 8 ++++ .../Resources/sv.lproj/Localizable.strings | 40 +++++++++++-------- .../sv.lproj/Localizable.stringsdict | 4 +- .../Resources/th.lproj/Localizable.strings | 24 +++++++---- .../Resources/tr.lproj/Localizable.strings | 8 ++++ .../Resources/vi.lproj/Localizable.strings | 10 ++++- .../zh-Hans.lproj/Localizable.strings | 8 ++++ .../zh-Hant.lproj/Localizable.strings | 10 ++++- .../Attachment/AttachmentView.swift | 2 +- 27 files changed, 251 insertions(+), 63 deletions(-) diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 44ae29267..41090ca48 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -319,7 +319,7 @@ public enum L10n { public static func email(_ p1: Any) -> String { return L10n.tr("Localizable", "Common.Controls.Status.MetaEntity.Email", String(describing: p1)) } - /// Hastag %@ + /// Hashtag: %@ public static func hashtag(_ p1: Any) -> String { return L10n.tr("Localizable", "Common.Controls.Status.MetaEntity.Hashtag", String(describing: p1)) } @@ -459,12 +459,20 @@ public enum L10n { public static func attachmentBroken(_ p1: Any) -> String { return L10n.tr("Localizable", "Scene.Compose.Attachment.AttachmentBroken", String(describing: p1)) } + /// Attachment too large + public static let attachmentTooLarge = L10n.tr("Localizable", "Scene.Compose.Attachment.AttachmentTooLarge") + /// Can not regonize this media attachment + public static let canNotRecognizeThisMediaAttachment = L10n.tr("Localizable", "Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment") /// Describe the photo for the visually-impaired... public static let descriptionPhoto = L10n.tr("Localizable", "Scene.Compose.Attachment.DescriptionPhoto") /// Describe the video for the visually-impaired... public static let descriptionVideo = L10n.tr("Localizable", "Scene.Compose.Attachment.DescriptionVideo") + /// Load Failed + public static let loadFailed = L10n.tr("Localizable", "Scene.Compose.Attachment.LoadFailed") /// photo public static let photo = L10n.tr("Localizable", "Scene.Compose.Attachment.Photo") + /// Upload Failed + public static let uploadFailed = L10n.tr("Localizable", "Scene.Compose.Attachment.UploadFailed") /// video public static let video = L10n.tr("Localizable", "Scene.Compose.Attachment.Video") } diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.strings index 9ecfa450e..943d56a9f 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.strings @@ -68,13 +68,13 @@ "Common.Controls.Friendship.EditInfo" = "تَحريرُ المَعلُومات"; "Common.Controls.Friendship.Follow" = "مُتابَعَة"; "Common.Controls.Friendship.Following" = "مُتابَع"; -"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; +"Common.Controls.Friendship.HideReblogs" = "إخفاء إعادات التدوين"; "Common.Controls.Friendship.Mute" = "كَتم"; "Common.Controls.Friendship.MuteUser" = "كَتمُ %@"; "Common.Controls.Friendship.Muted" = "مكتوم"; "Common.Controls.Friendship.Pending" = "قيد المُراجعة"; "Common.Controls.Friendship.Request" = "إرسال طَلَب"; -"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; +"Common.Controls.Friendship.ShowReblogs" = "إظهار إعادات التدوين"; "Common.Controls.Friendship.Unblock" = "رفع الحَظر"; "Common.Controls.Friendship.UnblockUser" = "رفع الحَظر عن %@"; "Common.Controls.Friendship.Unmute" = "رفع الكتم"; @@ -108,6 +108,10 @@ "Common.Controls.Status.Actions.Unreblog" = "التراجُع عن إعادة النشر"; "Common.Controls.Status.ContentWarning" = "تحذير المُحتوى"; "Common.Controls.Status.MediaContentWarning" = "اُنقُر لِلكَشف"; +"Common.Controls.Status.MetaEntity.Email" = "عُنوان البريد الإلكتُروني: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "وَسْم: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "إظهار المِلف التعريفي: %@"; +"Common.Controls.Status.MetaEntity.Url" = "رابِط: %@"; "Common.Controls.Status.Poll.Closed" = "انتهى"; "Common.Controls.Status.Poll.Vote" = "صَوِّت"; "Common.Controls.Status.SensitiveContent" = "مُحتَوى حَسَّاس"; @@ -151,7 +155,7 @@ "Scene.AccountList.AddAccount" = "إضافَةُ حِساب"; "Scene.AccountList.DismissAccountSwitcher" = "تجاهُل مبدِّل الحِساب"; "Scene.AccountList.TabBarHint" = "المِلَفُّ المُحدَّدُ حالِيًّا: %@. اُنقُر نَقرًا مُزدَوَجًا مَعَ الاِستِمرارِ لِإظهارِ مُبدِّلِ الحِساب"; -"Scene.Bookmark.Title" = "Bookmarks"; +"Scene.Bookmark.Title" = "العَلاماتُ المَرجعيَّة"; "Scene.Compose.Accessibility.AppendAttachment" = "إضافة مُرفَق"; "Scene.Compose.Accessibility.AppendPoll" = "اضافة استطلاع رأي"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "منتقي الرموز التعبيرية المُخصَّص"; @@ -161,9 +165,13 @@ "Scene.Compose.Accessibility.RemovePoll" = "إزالة الاستطلاع"; "Scene.Compose.Attachment.AttachmentBroken" = "هذا ال%@ مُعطَّل ويتعذَّرُ رفعُه إلى ماستودون."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "صِف الصورة للمَكفوفين..."; "Scene.Compose.Attachment.DescriptionVideo" = "صِف المقطع المرئي للمَكفوفين..."; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "صورة"; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "مقطع مرئي"; "Scene.Compose.AutoComplete.SpaceToAdd" = "انقر على مساحة لإضافتِها"; "Scene.Compose.ComposeAction" = "نَشر"; @@ -256,12 +264,12 @@ "Scene.Profile.Header.FollowsYou" = "يُتابِعُك"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "تأكيدُ حَظر %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "حَظرُ الحِساب"; -"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; -"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "التأكيد لِإخفاء إعادات التدوين"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "إخفاء إعادات التدوين"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "تأكيدُ كَتم %@"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "كَتمُ الحِساب"; -"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs"; -"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "التأكيد لِإظهار إعادات التدوين"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "إظهار إعادات التدوين"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "تأكيدُ رَفع الحَظرِ عَن %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "رَفعُ الحَظرِ عَنِ الحِساب"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "أكِّد لرفع الكتمْ عن %@"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings index 1e691f8a9..fca658aef 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings @@ -108,6 +108,10 @@ Comprova la teva connexió a Internet."; "Common.Controls.Status.Actions.Unreblog" = "Desfer l'impuls"; "Common.Controls.Status.ContentWarning" = "Advertència de Contingut"; "Common.Controls.Status.MediaContentWarning" = "Toca qualsevol lloc per a mostrar"; +"Common.Controls.Status.MetaEntity.Email" = "Correu electrònic: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Etiqueta %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Mostra el Perfil: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Enllaç: %@"; "Common.Controls.Status.Poll.Closed" = "Finalitzada"; "Common.Controls.Status.Poll.Vote" = "Vota"; "Common.Controls.Status.SensitiveContent" = "Contingut sensible"; @@ -151,7 +155,7 @@ El teu perfil els sembla així."; "Scene.AccountList.AddAccount" = "Afegir compte"; "Scene.AccountList.DismissAccountSwitcher" = "Descartar el commutador de comptes"; "Scene.AccountList.TabBarHint" = "Perfil actual seleccionat: %@. Toca dues vegades i manté el dit per a mostrar el commutador de comptes"; -"Scene.Bookmark.Title" = "Bookmarks"; +"Scene.Bookmark.Title" = "Marcadors"; "Scene.Compose.Accessibility.AppendAttachment" = "Afegeix Adjunt"; "Scene.Compose.Accessibility.AppendPoll" = "Afegir enquesta"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Selector d'Emoji Personalitzat"; @@ -161,9 +165,13 @@ El teu perfil els sembla així."; "Scene.Compose.Accessibility.RemovePoll" = "Eliminar Enquesta"; "Scene.Compose.Attachment.AttachmentBroken" = "Aquest %@ està trencat i no pot ser carregat a Mastodon."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "El fitxer adjunt és massa gran"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "No es pot reconèixer l'adjunt multimèdia"; "Scene.Compose.Attachment.DescriptionPhoto" = "Descriu la foto per als disminuïts visuals..."; "Scene.Compose.Attachment.DescriptionVideo" = "Descriu el vídeo per als disminuïts visuals..."; +"Scene.Compose.Attachment.LoadFailed" = "Ha fallat la càrrega"; "Scene.Compose.Attachment.Photo" = "foto"; +"Scene.Compose.Attachment.UploadFailed" = "Pujada fallida"; "Scene.Compose.Attachment.Video" = "vídeo"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Espai per afegir"; "Scene.Compose.ComposeAction" = "Publica"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.strings index 053211f28..05c575520 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.strings @@ -108,6 +108,10 @@ "Common.Controls.Status.Actions.Unreblog" = "پۆستکردنەکە بگەڕێنەوە"; "Common.Controls.Status.ContentWarning" = "ئاگاداریی ناوەڕۆک"; "Common.Controls.Status.MediaContentWarning" = "دەستی پیا بنێ بۆ نیشاندانی"; +"Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Link: %@"; "Common.Controls.Status.Poll.Closed" = "داخراوە"; "Common.Controls.Status.Poll.Vote" = "دەنگ بدە"; "Common.Controls.Status.SensitiveContent" = "ناوەڕۆکی هەستیار"; @@ -160,9 +164,13 @@ "Scene.Compose.Accessibility.PostVisibilityMenu" = "پێڕستی شێوازی دەرکەوتنی پۆست"; "Scene.Compose.Accessibility.RemovePoll" = "دانگدانەکە لابە"; "Scene.Compose.Attachment.AttachmentBroken" = "ئەم %@ـە تێک چووە و ناتوانیت بەرزی بکەیتەوە."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "وێنەکەت بۆ نابیناکان باس بکە..."; "Scene.Compose.Attachment.DescriptionVideo" = "ڤیدیۆکەت بۆ نابیناکان باس بکە..."; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "وێنە"; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "ڤیدیۆ"; "Scene.Compose.AutoComplete.SpaceToAdd" = "بۆشایی دابنێ بۆ زیادکردن"; "Scene.Compose.ComposeAction" = "بڵاوی بکەوە"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings index 0a78adc48..4f9a15906 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings @@ -108,6 +108,10 @@ Bitte überprüfe deine Internetverbindung."; "Common.Controls.Status.Actions.Unreblog" = "Nicht mehr teilen"; "Common.Controls.Status.ContentWarning" = "Inhaltswarnung"; "Common.Controls.Status.MediaContentWarning" = "Tippe irgendwo zum Anzeigen"; +"Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Link: %@"; "Common.Controls.Status.Poll.Closed" = "Beendet"; "Common.Controls.Status.Poll.Vote" = "Abstimmen"; "Common.Controls.Status.SensitiveContent" = "NSFW-Inhalt"; @@ -151,7 +155,7 @@ Dein Profil sieht für diesen Benutzer auch so aus."; "Scene.AccountList.AddAccount" = "Konto hinzufügen"; "Scene.AccountList.DismissAccountSwitcher" = "Dialog zum Wechseln des Kontos schließen"; "Scene.AccountList.TabBarHint" = "Aktuell ausgewähltes Profil: %@. Doppeltippen dann gedrückt halten, um den Kontoschalter anzuzeigen"; -"Scene.Bookmark.Title" = "Bookmarks"; +"Scene.Bookmark.Title" = "Lesezeichen"; "Scene.Compose.Accessibility.AppendAttachment" = "Anhang hinzufügen"; "Scene.Compose.Accessibility.AppendPoll" = "Umfrage hinzufügen"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Benutzerdefinierter Emojiwähler"; @@ -161,9 +165,13 @@ Dein Profil sieht für diesen Benutzer auch so aus."; "Scene.Compose.Accessibility.RemovePoll" = "Umfrage entfernen"; "Scene.Compose.Attachment.AttachmentBroken" = "Dieses %@ scheint defekt zu sein und kann nicht auf Mastodon hochgeladen werden."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Anhang zu groß"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Medienanhang wurde nicht erkannt"; "Scene.Compose.Attachment.DescriptionPhoto" = "Für Menschen mit Sehbehinderung beschreiben..."; "Scene.Compose.Attachment.DescriptionVideo" = "Für Menschen mit Sehbehinderung beschreiben..."; +"Scene.Compose.Attachment.LoadFailed" = "Laden fehlgeschlagen"; "Scene.Compose.Attachment.Photo" = "Foto"; +"Scene.Compose.Attachment.UploadFailed" = "Upload fehlgeschlagen"; "Scene.Compose.Attachment.Video" = "Video"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Leerzeichen um hinzuzufügen"; "Scene.Compose.ComposeAction" = "Veröffentlichen"; @@ -215,9 +223,9 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.Familiarfollowers.Title" = "Follower, die dir bekannt vorkommen"; "Scene.Favorite.Title" = "Deine Favoriten"; "Scene.FavoritedBy.Title" = "Favorisiert von"; -"Scene.Follower.Footer" = "Follower von anderen Servern werden nicht angezeigt."; +"Scene.Follower.Footer" = "Folger, die nicht auf deinem Server registriert sind, werden nicht angezeigt."; "Scene.Follower.Title" = "Follower"; -"Scene.Following.Footer" = "Wem das Konto folgt wird von anderen Servern werden nicht angezeigt."; +"Scene.Following.Footer" = "Gefolgte, die nicht auf deinem Server registriert sind, werden nicht angezeigt."; "Scene.Following.Title" = "Folgende"; "Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint" = "Zum Scrollen nach oben tippen und zum vorherigen Ort erneut tippen"; "Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel" = "Logo-Button"; @@ -247,7 +255,7 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.Profile.Accessibility.EditAvatarImage" = "Profilbild bearbeiten"; "Scene.Profile.Accessibility.ShowAvatarImage" = "Profilbild anzeigen"; "Scene.Profile.Accessibility.ShowBannerImage" = "Bannerbild anzeigen"; -"Scene.Profile.Dashboard.Followers" = "Folger"; +"Scene.Profile.Dashboard.Followers" = "Folgende"; "Scene.Profile.Dashboard.Following" = "Gefolgte"; "Scene.Profile.Dashboard.Posts" = "Beiträge"; "Scene.Profile.Fields.AddRow" = "Zeile hinzufügen"; @@ -260,7 +268,7 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Reblogs ausblenden"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Bestätige %@ stumm zu schalten"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Konto stummschalten"; -"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Bestätigen um Reblogs anzuzeigen"; "Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Reblogs anzeigen"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Bestätige %@ zu entsperren"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Konto entsperren"; @@ -422,7 +430,7 @@ beliebigen Server."; "Scene.Settings.Section.Notifications.Title" = "Benachrichtigungen"; "Scene.Settings.Section.Notifications.Trigger.Anyone" = "jeder"; "Scene.Settings.Section.Notifications.Trigger.Follow" = "ein von mir Gefolgter"; -"Scene.Settings.Section.Notifications.Trigger.Follower" = "ein Folger"; +"Scene.Settings.Section.Notifications.Trigger.Follower" = "ein Folgender"; "Scene.Settings.Section.Notifications.Trigger.Noone" = "niemand"; "Scene.Settings.Section.Notifications.Trigger.Title" = "Benachrichtige mich, wenn"; "Scene.Settings.Section.Preference.DisableAvatarAnimation" = "Animierte Profilbilder deaktivieren"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.stringsdict index c6a8a4297..f60c6b0d7 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.stringsdict @@ -248,9 +248,9 @@ NSStringFormatValueTypeKey ld one - 1 Follower + 1 Folgender other - %ld Follower + %ld Folgende date.year.left diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings index 9114b96e5..a5acb78d7 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings @@ -109,7 +109,7 @@ Please check your internet connection."; "Common.Controls.Status.ContentWarning" = "Content Warning"; "Common.Controls.Status.MediaContentWarning" = "Tap anywhere to reveal"; "Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; -"Common.Controls.Status.MetaEntity.Hashtag" = "Hastag %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; "Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@"; "Common.Controls.Status.MetaEntity.Url" = "Link: %@"; "Common.Controls.Status.Poll.Closed" = "Closed"; @@ -165,9 +165,13 @@ Your profile looks like this to them."; "Scene.Compose.Accessibility.RemovePoll" = "Remove Poll"; "Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can’t be uploaded to Mastodon."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "Describe the photo for the visually-impaired..."; "Scene.Compose.Attachment.DescriptionVideo" = "Describe the video for the visually-impaired..."; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "photo"; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Space to add"; "Scene.Compose.ComposeAction" = "Publish"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.strings index 47ed11bb9..c16bec6cf 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.strings @@ -108,6 +108,10 @@ Por favor, revise su conexión a internet."; "Common.Controls.Status.Actions.Unreblog" = "Deshacer reblogueo"; "Common.Controls.Status.ContentWarning" = "Advertencia de Contenido"; "Common.Controls.Status.MediaContentWarning" = "Pulsa en cualquier sitio para mostrar"; +"Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Link: %@"; "Common.Controls.Status.Poll.Closed" = "Cerrado"; "Common.Controls.Status.Poll.Vote" = "Vota"; "Common.Controls.Status.SensitiveContent" = "Contenido sensible"; @@ -161,9 +165,13 @@ Tu perfil se ve así para él."; "Scene.Compose.Accessibility.RemovePoll" = "Eliminar Encuesta"; "Scene.Compose.Attachment.AttachmentBroken" = "Este %@ está roto y no puede subirse a Mastodon."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "Describe la foto para los usuarios con dificultad visual..."; "Scene.Compose.Attachment.DescriptionVideo" = "Describe el vídeo para los usuarios con dificultad visual..."; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "foto"; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "vídeo"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Espacio para añadir"; "Scene.Compose.ComposeAction" = "Publicar"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.strings index e2be3068d..aef7a7507 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.strings @@ -108,6 +108,10 @@ Egiaztatu Interneteko konexioa."; "Common.Controls.Status.Actions.Unreblog" = "Desegin bultzada"; "Common.Controls.Status.ContentWarning" = "Edukiaren abisua"; "Common.Controls.Status.MediaContentWarning" = "Ukitu edonon bistaratzeko"; +"Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Link: %@"; "Common.Controls.Status.Poll.Closed" = "Itxita"; "Common.Controls.Status.Poll.Vote" = "Bozkatu"; "Common.Controls.Status.SensitiveContent" = "Sensitive Content"; @@ -161,9 +165,13 @@ Zure profilak itxura hau du berarentzat."; "Scene.Compose.Accessibility.RemovePoll" = "Kendu inkesta"; "Scene.Compose.Attachment.AttachmentBroken" = "%@ hondatuta dago eta ezin da Mastodonera igo."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "Deskribatu argazkia ikusmen arazoak dituztenentzat..."; "Scene.Compose.Attachment.DescriptionVideo" = "Deskribatu bideoa ikusmen arazoak dituztenentzat..."; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "argazkia"; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "bideoa"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Sakatu zuriunea gehitzeko"; "Scene.Compose.ComposeAction" = "Argitaratu"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.strings index fbf48fa83..11259ace5 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.strings @@ -108,6 +108,10 @@ Tarkista internet-yhteytesi."; "Common.Controls.Status.Actions.Unreblog" = "Peru edelleen jako"; "Common.Controls.Status.ContentWarning" = "Sisältövaroitus"; "Common.Controls.Status.MediaContentWarning" = "Napauta mistä tahansa paljastaaksesi"; +"Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Link: %@"; "Common.Controls.Status.Poll.Closed" = "Suljettu"; "Common.Controls.Status.Poll.Vote" = "Vote"; "Common.Controls.Status.SensitiveContent" = "Sensitive Content"; @@ -161,9 +165,13 @@ Profiilisi näyttää tältä hänelle."; "Scene.Compose.Accessibility.RemovePoll" = "Poista kysely"; "Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can’t be uploaded to Mastodon."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "Kuvaile kuva näkövammaisille..."; "Scene.Compose.Attachment.DescriptionVideo" = "Kuvaile video näkövammaisille..."; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "kuva"; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Space to add"; "Scene.Compose.ComposeAction" = "Julkaise"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings index 03efc3549..931219c21 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings @@ -108,6 +108,10 @@ Veuillez vérifier votre accès à Internet."; "Common.Controls.Status.Actions.Unreblog" = "Annuler le reblog"; "Common.Controls.Status.ContentWarning" = "Avertissement de contenu"; "Common.Controls.Status.MediaContentWarning" = "Tapotez n’importe où pour révéler la publication"; +"Common.Controls.Status.MetaEntity.Email" = "Adresse e-mail : %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag : %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Afficher le profile : %@"; +"Common.Controls.Status.MetaEntity.Url" = "Lien : %@"; "Common.Controls.Status.Poll.Closed" = "Fermé"; "Common.Controls.Status.Poll.Vote" = "Voter"; "Common.Controls.Status.SensitiveContent" = "Contenu sensible"; @@ -151,7 +155,7 @@ Votre profil ressemble à ça pour lui."; "Scene.AccountList.AddAccount" = "Ajouter un compte"; "Scene.AccountList.DismissAccountSwitcher" = "Rejeter le commutateur de compte"; "Scene.AccountList.TabBarHint" = "Profil sélectionné actuel: %@. Double appui puis maintenez enfoncé pour afficher le changement de compte"; -"Scene.Bookmark.Title" = "Bookmarks"; +"Scene.Bookmark.Title" = "Favoris"; "Scene.Compose.Accessibility.AppendAttachment" = "Joindre un document"; "Scene.Compose.Accessibility.AppendPoll" = "Ajouter un Sondage"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Sélecteur d’émojis personnalisés"; @@ -161,9 +165,13 @@ Votre profil ressemble à ça pour lui."; "Scene.Compose.Accessibility.RemovePoll" = "Retirer le sondage"; "Scene.Compose.Attachment.AttachmentBroken" = "Ce %@ est brisé et ne peut pas être téléversé sur Mastodon."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "Décrire cette photo pour les personnes malvoyantes..."; "Scene.Compose.Attachment.DescriptionVideo" = "Décrire cette vidéo pour les personnes malvoyantes..."; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "photo"; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "vidéo"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Espace à ajouter"; "Scene.Compose.ComposeAction" = "Publier"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings index 2d1964d81..ce1764eac 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings @@ -108,6 +108,10 @@ Thoir sùil air a’ cheangal agad ris an eadar-lìon."; "Common.Controls.Status.Actions.Unreblog" = "Na brosnaich tuilleadh"; "Common.Controls.Status.ContentWarning" = "Rabhadh susbainte"; "Common.Controls.Status.MediaContentWarning" = "Thoir gnogag àite sam bith gus a nochdadh"; +"Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Link: %@"; "Common.Controls.Status.Poll.Closed" = "Dùinte"; "Common.Controls.Status.Poll.Vote" = "Cuir bhòt"; "Common.Controls.Status.SensitiveContent" = "Susbaint fhrionasach"; @@ -161,9 +165,13 @@ Seo an coltas a th’ air a’ phròifil agad dhaibh-san."; "Scene.Compose.Accessibility.RemovePoll" = "Thoir air falbh an cunntas-bheachd"; "Scene.Compose.Attachment.AttachmentBroken" = "Seo %@ a tha briste is cha ghabh a luchdadh suas gu Mastodon."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "Mìnich an dealbh dhan fheadhainn air a bheil cion-lèirsinne…"; "Scene.Compose.Attachment.DescriptionVideo" = "Mìnich a’ video dhan fheadhainn air a bheil cion-lèirsinne…"; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "dealbh"; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Brùth air Space gus a chur ris"; "Scene.Compose.ComposeAction" = "Foillsich"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings index c76089221..3087f33c5 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings @@ -68,13 +68,13 @@ Comproba a conexión a internet."; "Common.Controls.Friendship.EditInfo" = "Editar info"; "Common.Controls.Friendship.Follow" = "Seguir"; "Common.Controls.Friendship.Following" = "Seguindo"; -"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; +"Common.Controls.Friendship.HideReblogs" = "Agochar Promocións"; "Common.Controls.Friendship.Mute" = "Acalar"; "Common.Controls.Friendship.MuteUser" = "Acalar a %@"; "Common.Controls.Friendship.Muted" = "Acalada"; "Common.Controls.Friendship.Pending" = "Pendente"; "Common.Controls.Friendship.Request" = "Solicitar"; -"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; +"Common.Controls.Friendship.ShowReblogs" = "Mostrar Promocións"; "Common.Controls.Friendship.Unblock" = "Desbloquear"; "Common.Controls.Friendship.UnblockUser" = "Desbloquear a %@"; "Common.Controls.Friendship.Unmute" = "Non Acalar"; @@ -108,6 +108,10 @@ Comproba a conexión a internet."; "Common.Controls.Status.Actions.Unreblog" = "Retirar promoción"; "Common.Controls.Status.ContentWarning" = "Aviso sobre o contido"; "Common.Controls.Status.MediaContentWarning" = "Toca nalgures para mostrar"; +"Common.Controls.Status.MetaEntity.Email" = "Enderezo de email: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Cancelo: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Mostrar Perfil: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Ligazón: %@"; "Common.Controls.Status.Poll.Closed" = "Pechada"; "Common.Controls.Status.Poll.Vote" = "Votar"; "Common.Controls.Status.SensitiveContent" = "Contido sensible"; @@ -151,7 +155,7 @@ Así se ve o teu perfil."; "Scene.AccountList.AddAccount" = "Engadir conta"; "Scene.AccountList.DismissAccountSwitcher" = "Desbotar intercambiador de contas"; "Scene.AccountList.TabBarHint" = "Perfil seleccionado: %@. Dobre toque e manter para mostrar o intercambiador de contas"; -"Scene.Bookmark.Title" = "Bookmarks"; +"Scene.Bookmark.Title" = "Marcadores"; "Scene.Compose.Accessibility.AppendAttachment" = "Engadir anexo"; "Scene.Compose.Accessibility.AppendPoll" = "Engadir enquisa"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Selector emoji personalizado"; @@ -161,9 +165,13 @@ Así se ve o teu perfil."; "Scene.Compose.Accessibility.RemovePoll" = "Eliminar enquisa"; "Scene.Compose.Attachment.AttachmentBroken" = "Este %@ está estragado e non pode ser subido a Mastodon."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Adxunto demasiado grande"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Non se recoñece o tipo de multimedia"; "Scene.Compose.Attachment.DescriptionPhoto" = "Describe a foto para persoas con problemas visuais..."; "Scene.Compose.Attachment.DescriptionVideo" = "Describe o vídeo para persoas con problemas visuais..."; +"Scene.Compose.Attachment.LoadFailed" = "Fallou a carga"; "Scene.Compose.Attachment.Photo" = "foto"; +"Scene.Compose.Attachment.UploadFailed" = "Erro na subida"; "Scene.Compose.Attachment.Video" = "vídeo"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Barra de espazo engade"; "Scene.Compose.ComposeAction" = "Publicar"; @@ -256,12 +264,12 @@ ser subido a Mastodon."; "Scene.Profile.Header.FollowsYou" = "Séguete"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirma o bloqueo de %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Bloquear Conta"; -"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; -"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirma para agochar promocións"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Agochar Promocións"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Confirma Acalar a %@"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Acalar conta"; -"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs"; -"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirma para ver promocións"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Mostrar Promocións"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Confirma o desbloqueo de %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Desbloquear Conta"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirma restablecer a %@"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings index c83cb7458..8f99028ed 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings @@ -108,6 +108,10 @@ Per favore verifica la tua connessione internet."; "Common.Controls.Status.Actions.Unreblog" = "Annulla condivisione"; "Common.Controls.Status.ContentWarning" = "Avviso sul contenuto"; "Common.Controls.Status.MediaContentWarning" = "Tocca ovunque per rivelare"; +"Common.Controls.Status.MetaEntity.Email" = "Indirizzo email: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Mostra il profilo: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Collegamento: %@"; "Common.Controls.Status.Poll.Closed" = "Chiuso"; "Common.Controls.Status.Poll.Vote" = "Vota"; "Common.Controls.Status.SensitiveContent" = "Contenuto sensibile"; @@ -151,7 +155,7 @@ Il tuo profilo sembra questo per loro."; "Scene.AccountList.AddAccount" = "Aggiungi account"; "Scene.AccountList.DismissAccountSwitcher" = "Ignora il cambio account"; "Scene.AccountList.TabBarHint" = "Profilo corrente selezionato: %@. Doppio tocco e tieni premuto per mostrare il cambio account"; -"Scene.Bookmark.Title" = "Bookmarks"; +"Scene.Bookmark.Title" = "Segnalibri"; "Scene.Compose.Accessibility.AppendAttachment" = "Aggiungi allegato"; "Scene.Compose.Accessibility.AppendPoll" = "Aggiungi sondaggio"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Selettore Emoji personalizzato"; @@ -161,9 +165,13 @@ Il tuo profilo sembra questo per loro."; "Scene.Compose.Accessibility.RemovePoll" = "Elimina sondaggio"; "Scene.Compose.Attachment.AttachmentBroken" = "Questo %@ è rotto e non può essere caricato su Mastodon."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Allegato troppo grande"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Impossibile riconoscere questo allegato multimediale"; "Scene.Compose.Attachment.DescriptionPhoto" = "Descrivi la foto per gli utenti ipovedenti..."; "Scene.Compose.Attachment.DescriptionVideo" = "Descrivi il filmato per gli utenti ipovedenti..."; +"Scene.Compose.Attachment.LoadFailed" = "Caricamento fallito"; "Scene.Compose.Attachment.Photo" = "foto"; +"Scene.Compose.Attachment.UploadFailed" = "Caricamento fallito"; "Scene.Compose.Attachment.Video" = "filmato"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Spazio da aggiungere"; "Scene.Compose.ComposeAction" = "Pubblica"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings index 080624f06..cad44f531 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings @@ -108,6 +108,10 @@ "Common.Controls.Status.Actions.Unreblog" = "ブーストを戻す"; "Common.Controls.Status.ContentWarning" = "コンテンツ警告"; "Common.Controls.Status.MediaContentWarning" = "どこかをタップして表示"; +"Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Link: %@"; "Common.Controls.Status.Poll.Closed" = "終了"; "Common.Controls.Status.Poll.Vote" = "投票"; "Common.Controls.Status.SensitiveContent" = "閲覧注意"; @@ -156,9 +160,13 @@ "Scene.Compose.Accessibility.PostVisibilityMenu" = "投稿の表示メニュー"; "Scene.Compose.Accessibility.RemovePoll" = "投票を消去"; "Scene.Compose.Attachment.AttachmentBroken" = "%@は壊れていてMastodonにアップロードできません。"; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "閲覧が難しいユーザーへの画像説明"; "Scene.Compose.Attachment.DescriptionVideo" = "閲覧が難しいユーザーへの映像説明"; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "写真"; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "動画"; "Scene.Compose.AutoComplete.SpaceToAdd" = "スペースを追加"; "Scene.Compose.ComposeAction" = "投稿"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings index 1339af4cf..03108a25a 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings @@ -108,6 +108,10 @@ Ma ulac aɣilif, senqed tuqqna-inek internet."; "Common.Controls.Status.Actions.Unreblog" = "Sefsex allus n usuffeɣ"; "Common.Controls.Status.ContentWarning" = "Alɣu n ugbur"; "Common.Controls.Status.MediaContentWarning" = "Sit anida tebɣiḍ i wakken ad twaliḍ"; +"Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Link: %@"; "Common.Controls.Status.Poll.Closed" = "Ifukk"; "Common.Controls.Status.Poll.Vote" = "Dɣeṛ"; "Common.Controls.Status.SensitiveContent" = "Agbur amḥulfu"; @@ -161,9 +165,13 @@ Akka i as-d-yettban umaɣnu-inek."; "Scene.Compose.Accessibility.RemovePoll" = "Kkes asenqed"; "Scene.Compose.Attachment.AttachmentBroken" = "%@-a yerreẓ, ur yezmir ara Ad d-yettwasali ɣef Mastodon."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "Glem-d tawlaft i wid yesɛan ugur deg yiẓri..."; "Scene.Compose.Attachment.DescriptionVideo" = "Glem-d tavidyut i wid yesɛan ugur deg yiẓri..."; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "tawlaft"; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "tavidyutt"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Tallunt ara yettwarnun"; "Scene.Compose.ComposeAction" = "Sufeɣ"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings index a72543a3e..10d88488a 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings @@ -68,13 +68,13 @@ Jkx girêdana înternetê xwe kontrol bike."; "Common.Controls.Friendship.EditInfo" = "Zanyariyan serrast bike"; "Common.Controls.Friendship.Follow" = "Bişopîne"; "Common.Controls.Friendship.Following" = "Dişopîne"; -"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; +"Common.Controls.Friendship.HideReblogs" = "Bilindkirinan veşêre"; "Common.Controls.Friendship.Mute" = "Bêdeng bike"; "Common.Controls.Friendship.MuteUser" = "%@ bêdeng bike"; "Common.Controls.Friendship.Muted" = "Bêdengkirî"; "Common.Controls.Friendship.Pending" = "Tê nirxandin"; "Common.Controls.Friendship.Request" = "Daxwaz bike"; -"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; +"Common.Controls.Friendship.ShowReblogs" = "Bilindkirinan nîşan bide"; "Common.Controls.Friendship.Unblock" = "Astengiyê rake"; "Common.Controls.Friendship.UnblockUser" = "%@ asteng neke"; "Common.Controls.Friendship.Unmute" = "Bêdeng neke"; @@ -108,6 +108,10 @@ Jkx girêdana înternetê xwe kontrol bike."; "Common.Controls.Status.Actions.Unreblog" = "Ji nû ve nivîsandinê vegere"; "Common.Controls.Status.ContentWarning" = "Hişyariya naverokê"; "Common.Controls.Status.MediaContentWarning" = "Ji bo eşkerekirinê li derekî bitikîne"; +"Common.Controls.Status.MetaEntity.Email" = "Navnîşanên e-nameyê: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtagê: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Profîlê nîşan bide: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Girêdan: %@"; "Common.Controls.Status.Poll.Closed" = "Girtî"; "Common.Controls.Status.Poll.Vote" = "Deng bide"; "Common.Controls.Status.SensitiveContent" = "Naveroka hestiyarî"; @@ -151,7 +155,7 @@ Profîla te ji wan ra wiha xuya dike."; "Scene.AccountList.AddAccount" = "Ajimêr tevlî bike"; "Scene.AccountList.DismissAccountSwitcher" = "Guherkera ajimêrê paş guh bike"; "Scene.AccountList.TabBarHint" = "Profîla hilbijartî ya niha: %@. Du caran bitikîne û paşê dest bide ser da ku guhêrbara ajimêr were nîşandan"; -"Scene.Bookmark.Title" = "Bookmarks"; +"Scene.Bookmark.Title" = "Şûnpel"; "Scene.Compose.Accessibility.AppendAttachment" = "Pêvek tevlî bike"; "Scene.Compose.Accessibility.AppendPoll" = "Rapirsî tevlî bike"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Hilbijêrê emojî yên kesanekirî"; @@ -161,9 +165,13 @@ Profîla te ji wan ra wiha xuya dike."; "Scene.Compose.Accessibility.RemovePoll" = "Rapirsî rake"; "Scene.Compose.Attachment.AttachmentBroken" = "Ev %@ naxebite û nayê barkirin li ser Mastodon."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Pêvek pir mezin e"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Nikare ev pêveka medyayê nas bike"; "Scene.Compose.Attachment.DescriptionPhoto" = "Wêneyê ji bo kêmbînên dîtbar bide nasîn..."; "Scene.Compose.Attachment.DescriptionVideo" = "Vîdyoyê ji bo kêmbînên dîtbar bide nasîn..."; +"Scene.Compose.Attachment.LoadFailed" = "Barkirin têk çû"; "Scene.Compose.Attachment.Photo" = "wêne"; +"Scene.Compose.Attachment.UploadFailed" = "Barkirin têk çû"; "Scene.Compose.Attachment.Video" = "vîdyo"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Bicîhkirinê tevlî bike"; "Scene.Compose.ComposeAction" = "Biweşîne"; @@ -257,12 +265,12 @@ girêdanê bitikne da ku ajimêra xwe bidî piştrastkirin."; "Scene.Profile.Header.FollowsYou" = "Te dişopîne"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Ji bo rakirina astengkirinê %@ bipejirîne"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Ajimêr asteng bike"; -"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; -"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Bo veşartina bilindkirinan bipejirîne"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Bilindkirinan veşêre"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Ji bo bêdengkirina %@ bipejirîne"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Ajimêrê bêdeng bike"; -"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs"; -"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Bo nîşandana bilindkirinan bipejirîne"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Bilindkirinan nîşan bide"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Ji bo rakirina astengkirinê %@ bipejirîne"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Astengiyê li ser ajimêr rake"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Ji bo vekirina bêdengkirinê %@ bipejirîne"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.strings index 3bcc33bf5..e719583aa 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.strings @@ -107,6 +107,10 @@ "Common.Controls.Status.Actions.Unreblog" = "Delen ongedaan maken"; "Common.Controls.Status.ContentWarning" = "Inhoudswaarschuwing"; "Common.Controls.Status.MediaContentWarning" = "Tap hier om te tonen"; +"Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Link: %@"; "Common.Controls.Status.Poll.Closed" = "Gesloten"; "Common.Controls.Status.Poll.Vote" = "Stemmen"; "Common.Controls.Status.SensitiveContent" = "Gevoelige inhoud"; @@ -155,9 +159,13 @@ Uw profiel ziet er zo uit voor hen."; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Berichtzichtbaarheidsmenu"; "Scene.Compose.Accessibility.RemovePoll" = "Peiling verwijderen"; "Scene.Compose.Attachment.AttachmentBroken" = "Deze %@ is corrupt en kan niet geüpload worden naar Mastodon."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "Omschrijf de foto voor mensen met een visuele beperking..."; "Scene.Compose.Attachment.DescriptionVideo" = "Omschrijf de video voor mensen met een visuele beperking..."; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "foto"; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Spaties toe te voegen"; "Scene.Compose.ComposeAction" = "Publiceren"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.strings index 0513a955b..65d4fa65e 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.strings @@ -108,6 +108,10 @@ "Common.Controls.Status.Actions.Unreblog" = "Убрать продвижение"; "Common.Controls.Status.ContentWarning" = "Предупреждение о содержании"; "Common.Controls.Status.MediaContentWarning" = "Нажмите в любом месте, чтобы показать"; +"Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Link: %@"; "Common.Controls.Status.Poll.Closed" = "Завершён"; "Common.Controls.Status.Poll.Vote" = "Проголосовать"; "Common.Controls.Status.SensitiveContent" = "Sensitive Content"; @@ -169,9 +173,13 @@ "Scene.Compose.Accessibility.RemovePoll" = "Убрать опрос"; "Scene.Compose.Attachment.AttachmentBroken" = "Это %@ повреждено и не может быть отправлено в Mastodon."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "Опишите фото для людей с нарушениями зрения..."; "Scene.Compose.Attachment.DescriptionVideo" = "Опишите видео для людей с нарушениями зрения..."; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "изображение"; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "видео"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Пробел, чтобы добавить"; "Scene.Compose.ComposeAction" = "Опубликовать"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.strings index 849d88284..dbba5ddda 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.strings @@ -68,13 +68,13 @@ Kontrollera din internetanslutning."; "Common.Controls.Friendship.EditInfo" = "Redigera info"; "Common.Controls.Friendship.Follow" = "Följ"; "Common.Controls.Friendship.Following" = "Följer"; -"Common.Controls.Friendship.HideReblogs" = "Dölj puffar"; +"Common.Controls.Friendship.HideReblogs" = "Dölj boostar"; "Common.Controls.Friendship.Mute" = "Tysta"; "Common.Controls.Friendship.MuteUser" = "Tysta %@"; "Common.Controls.Friendship.Muted" = "Tystad"; "Common.Controls.Friendship.Pending" = "Väntande"; "Common.Controls.Friendship.Request" = "Följ"; -"Common.Controls.Friendship.ShowReblogs" = "Visa knuffar"; +"Common.Controls.Friendship.ShowReblogs" = "Visa boostar"; "Common.Controls.Friendship.Unblock" = "Avblockera"; "Common.Controls.Friendship.UnblockUser" = "Avblockera %@"; "Common.Controls.Friendship.Unmute" = "Avtysta"; @@ -87,27 +87,31 @@ Kontrollera din internetanslutning."; "Common.Controls.Keyboard.SegmentedControl.PreviousSection" = "Föregående avsnitt"; "Common.Controls.Keyboard.Timeline.NextStatus" = "Nästa inlägg"; "Common.Controls.Keyboard.Timeline.OpenAuthorProfile" = "Öppna författarens profil"; -"Common.Controls.Keyboard.Timeline.OpenRebloggerProfile" = "Öppna ompostarens profil"; +"Common.Controls.Keyboard.Timeline.OpenRebloggerProfile" = "Öppna boostarens profil"; "Common.Controls.Keyboard.Timeline.OpenStatus" = "Öppna inlägg"; "Common.Controls.Keyboard.Timeline.PreviewImage" = "Förhandsgranska bild"; "Common.Controls.Keyboard.Timeline.PreviousStatus" = "Föregående inlägg"; "Common.Controls.Keyboard.Timeline.ReplyStatus" = "Svara på inlägg"; "Common.Controls.Keyboard.Timeline.ToggleContentWarning" = "Växla innehållsvarning"; "Common.Controls.Keyboard.Timeline.ToggleFavorite" = "Växla favorit på inlägg"; -"Common.Controls.Keyboard.Timeline.ToggleReblog" = "Växla puff på inlägg"; +"Common.Controls.Keyboard.Timeline.ToggleReblog" = "Växla boost på inlägg"; "Common.Controls.Status.Actions.Favorite" = "Favorit"; "Common.Controls.Status.Actions.Hide" = "Dölj"; "Common.Controls.Status.Actions.Menu" = "Meny"; -"Common.Controls.Status.Actions.Reblog" = "Puffa"; +"Common.Controls.Status.Actions.Reblog" = "Boosta"; "Common.Controls.Status.Actions.Reply" = "Svara"; "Common.Controls.Status.Actions.ShowGif" = "Visa GIF"; "Common.Controls.Status.Actions.ShowImage" = "Visa bild"; "Common.Controls.Status.Actions.ShowVideoPlayer" = "Visa videospelare"; "Common.Controls.Status.Actions.TapThenHoldToShowMenu" = "Tryck och håll ned för att visa menyn"; "Common.Controls.Status.Actions.Unfavorite" = "Ta bort favorit"; -"Common.Controls.Status.Actions.Unreblog" = "Ångra puff"; +"Common.Controls.Status.Actions.Unreblog" = "Ångra boost"; "Common.Controls.Status.ContentWarning" = "Innehållsvarning"; "Common.Controls.Status.MediaContentWarning" = "Tryck var som helst för att visa"; +"Common.Controls.Status.MetaEntity.Email" = "E-postadress: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Visa profil: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Länk: %@"; "Common.Controls.Status.Poll.Closed" = "Stängd"; "Common.Controls.Status.Poll.Vote" = "Rösta"; "Common.Controls.Status.SensitiveContent" = "Känsligt innehåll"; @@ -120,7 +124,7 @@ Kontrollera din internetanslutning."; "Common.Controls.Status.Tag.Mention" = "Omnämn"; "Common.Controls.Status.Tag.Url" = "URL"; "Common.Controls.Status.TapToReveal" = "Tryck för att visa"; -"Common.Controls.Status.UserReblogged" = "%@ puffade"; +"Common.Controls.Status.UserReblogged" = "%@ boostade"; "Common.Controls.Status.UserRepliedTo" = "Svarade på %@"; "Common.Controls.Status.Visibility.Direct" = "Endast omnämnda användare kan se detta inlägg."; "Common.Controls.Status.Visibility.Private" = "Endast deras följare kan se detta inlägg."; @@ -151,7 +155,7 @@ Din profil ser ut så här för dem."; "Scene.AccountList.AddAccount" = "Lägg till konto"; "Scene.AccountList.DismissAccountSwitcher" = "Stäng kontoväxlare"; "Scene.AccountList.TabBarHint" = "Nuvarande vald profil: %@. Dubbeltryck och håll för att visa kontoväxlare"; -"Scene.Bookmark.Title" = "Bookmarks"; +"Scene.Bookmark.Title" = "Bokmärken"; "Scene.Compose.Accessibility.AppendAttachment" = "Lägg till bilaga"; "Scene.Compose.Accessibility.AppendPoll" = "Lägg till omröstning"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Anpassad emoji-väljare"; @@ -161,9 +165,13 @@ Din profil ser ut så här för dem."; "Scene.Compose.Accessibility.RemovePoll" = "Ta bort omröstning"; "Scene.Compose.Attachment.AttachmentBroken" = "Denna %@ är trasig och kan inte laddas upp till Mastodon."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Bilagan är för stor"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Känner inte igen mediebilagan"; "Scene.Compose.Attachment.DescriptionPhoto" = "Beskriv fotot för synskadade..."; "Scene.Compose.Attachment.DescriptionVideo" = "Beskriv videon för de synskadade..."; +"Scene.Compose.Attachment.LoadFailed" = "Det gick inte att läsa in"; "Scene.Compose.Attachment.Photo" = "foto"; +"Scene.Compose.Attachment.UploadFailed" = "Uppladdning misslyckades"; "Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Mellanslag för att lägga till"; "Scene.Compose.ComposeAction" = "Publicera"; @@ -236,7 +244,7 @@ laddas upp till Mastodon."; "Scene.Notification.NotificationDescription.FollowedYou" = "följde dig"; "Scene.Notification.NotificationDescription.MentionedYou" = "nämnde dig"; "Scene.Notification.NotificationDescription.PollHasEnded" = "omröstningen har avslutats"; -"Scene.Notification.NotificationDescription.RebloggedYourPost" = "puffade ditt inlägg"; +"Scene.Notification.NotificationDescription.RebloggedYourPost" = "boostade ditt inlägg"; "Scene.Notification.NotificationDescription.RequestToFollowYou" = "begär att följa dig"; "Scene.Notification.Title.Everything" = "Allting"; "Scene.Notification.Title.Mentions" = "Omnämningar"; @@ -256,12 +264,12 @@ laddas upp till Mastodon."; "Scene.Profile.Header.FollowsYou" = "Följer dig"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Bekräfta för att blockera %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Blockera konto"; -"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Bekräfta för att dölja puffar"; -"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Dölj puffar"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Bekräfta för att dölja boostar"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Dölj boostar"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Bekräfta för att tysta %@"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Tysta konto"; -"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Bekräfta för att visa puffar"; -"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Visa puffar"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Bekräfta för att visa boostar"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Visa boostar"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Bekräfta för att avblockera %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Avblockera konto"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Bekräfta för att avtysta %@"; @@ -271,7 +279,7 @@ laddas upp till Mastodon."; "Scene.Profile.SegmentedControl.Posts" = "Inlägg"; "Scene.Profile.SegmentedControl.PostsAndReplies" = "Inlägg och svar"; "Scene.Profile.SegmentedControl.Replies" = "Svar"; -"Scene.RebloggedBy.Title" = "Puffat av"; +"Scene.RebloggedBy.Title" = "Boostat av"; "Scene.Register.Error.Item.Agreement" = "Avtal"; "Scene.Register.Error.Item.Email" = "E-post"; "Scene.Register.Error.Item.Locale" = "Språk"; @@ -323,7 +331,7 @@ laddas upp till Mastodon."; "Scene.Report.StepFinal.Unfollowed" = "Slutade följa"; "Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "När du ser något som du inte gillar på Mastodon kan du ta bort personen från din upplevelse."; "Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser" = "Medan vi granskar detta kan du vidta åtgärder mot %@"; -"Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "Du kommer inte att se deras inlägg eller ompostningar i ditt hemflöde. De kommer inte att veta att de har blivit tystade."; +"Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "Du kommer inte att se deras inlägg eller boostar i ditt hemflöde. De kommer inte att veta att de har blivit tystade."; "Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "Finns det något annat vi borde veta?"; "Scene.Report.StepFour.Step4Of4" = "Steg 4 av 4"; "Scene.Report.StepOne.IDontLikeIt" = "Jag tycker inte om det"; @@ -414,7 +422,7 @@ laddas upp till Mastodon."; "Scene.Settings.Section.LookAndFeel.SortaDark" = "Ganska mörk"; "Scene.Settings.Section.LookAndFeel.Title" = "Utseende och känsla"; "Scene.Settings.Section.LookAndFeel.UseSystem" = "Följ systeminställningarna"; -"Scene.Settings.Section.Notifications.Boosts" = "Ompostar mitt inlägg"; +"Scene.Settings.Section.Notifications.Boosts" = "Boostar mitt inlägg"; "Scene.Settings.Section.Notifications.Favorites" = "Favoriserar mitt inlägg"; "Scene.Settings.Section.Notifications.Follows" = "Följer mig"; "Scene.Settings.Section.Notifications.Mentions" = "Nämner mig"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.stringsdict index 048af4732..c7317903d 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.stringsdict @@ -152,9 +152,9 @@ NSStringFormatValueTypeKey ld one - %ld puff + %ld boost other - %ld puffar + %ld boostar plural.count.reply diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings index 15514928c..de982308d 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings @@ -68,13 +68,13 @@ "Common.Controls.Friendship.EditInfo" = "แก้ไขข้อมูล"; "Common.Controls.Friendship.Follow" = "ติดตาม"; "Common.Controls.Friendship.Following" = "กำลังติดตาม"; -"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; +"Common.Controls.Friendship.HideReblogs" = "ซ่อนการดัน"; "Common.Controls.Friendship.Mute" = "ซ่อน"; "Common.Controls.Friendship.MuteUser" = "ซ่อน %@"; "Common.Controls.Friendship.Muted" = "ซ่อนอยู่"; "Common.Controls.Friendship.Pending" = "รอดำเนินการ"; "Common.Controls.Friendship.Request" = "ขอ"; -"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; +"Common.Controls.Friendship.ShowReblogs" = "แสดงการดัน"; "Common.Controls.Friendship.Unblock" = "เลิกปิดกั้น"; "Common.Controls.Friendship.UnblockUser" = "เลิกปิดกั้น %@"; "Common.Controls.Friendship.Unmute" = "เลิกซ่อน"; @@ -108,6 +108,10 @@ "Common.Controls.Status.Actions.Unreblog" = "เลิกทำการดัน"; "Common.Controls.Status.ContentWarning" = "คำเตือนเนื้อหา"; "Common.Controls.Status.MediaContentWarning" = "แตะที่ใดก็ตามเพื่อเปิดเผย"; +"Common.Controls.Status.MetaEntity.Email" = "ที่อยู่อีเมล: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "แฮชแท็ก: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "โปรไฟล์ที่แสดง: %@"; +"Common.Controls.Status.MetaEntity.Url" = "ลิงก์: %@"; "Common.Controls.Status.Poll.Closed" = "ปิดแล้ว"; "Common.Controls.Status.Poll.Vote" = "ลงคะแนน"; "Common.Controls.Status.SensitiveContent" = "เนื้อหาที่ละเอียดอ่อน"; @@ -151,7 +155,7 @@ "Scene.AccountList.AddAccount" = "เพิ่มบัญชี"; "Scene.AccountList.DismissAccountSwitcher" = "ปิดตัวสลับบัญชี"; "Scene.AccountList.TabBarHint" = "โปรไฟล์ที่เลือกในปัจจุบัน: %@ แตะสองครั้งแล้วกดค้างไว้เพื่อแสดงตัวสลับบัญชี"; -"Scene.Bookmark.Title" = "Bookmarks"; +"Scene.Bookmark.Title" = "ที่คั่นหน้า"; "Scene.Compose.Accessibility.AppendAttachment" = "เพิ่มไฟล์แนบ"; "Scene.Compose.Accessibility.AppendPoll" = "เพิ่มการสำรวจความคิดเห็น"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "ตัวเลือกอีโมจิที่กำหนดเอง"; @@ -161,9 +165,13 @@ "Scene.Compose.Accessibility.RemovePoll" = "เอาการสำรวจความคิดเห็นออก"; "Scene.Compose.Attachment.AttachmentBroken" = "%@ นี้เสียหายและไม่สามารถ อัปโหลดไปยัง Mastodon"; +"Scene.Compose.Attachment.AttachmentTooLarge" = "ไฟล์แนบใหญ่เกินไป"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "อธิบายรูปภาพสำหรับผู้บกพร่องทางการมองเห็น..."; "Scene.Compose.Attachment.DescriptionVideo" = "อธิบายวิดีโอสำหรับผู้บกพร่องทางการมองเห็น..."; +"Scene.Compose.Attachment.LoadFailed" = "การโหลดล้มเหลว"; "Scene.Compose.Attachment.Photo" = "รูปภาพ"; +"Scene.Compose.Attachment.UploadFailed" = "การอัปโหลดล้มเหลว"; "Scene.Compose.Attachment.Video" = "วิดีโอ"; "Scene.Compose.AutoComplete.SpaceToAdd" = "เว้นวรรคเพื่อเพิ่ม"; "Scene.Compose.ComposeAction" = "เผยแพร่"; @@ -256,12 +264,12 @@ "Scene.Profile.Header.FollowsYou" = "ติดตามคุณ"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "ยืนยันเพื่อปิดกั้น %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "ปิดกั้นบัญชี"; -"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; -"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "ยืนยันเพื่อซ่อนการดัน"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "ซ่อนการดัน"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "ยืนยันเพื่อซ่อน %@"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "ซ่อนบัญชี"; -"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs"; -"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "ยืนยันเพื่อแสดงการดัน"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "แสดงการดัน"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "ยืนยันเพื่อเลิกปิดกั้น %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "เลิกปิดกั้นบัญชี"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "ยืนยันเพื่อเลิกซ่อน %@"; @@ -391,7 +399,7 @@ "Scene.ServerPicker.Label.Language" = "ภาษา"; "Scene.ServerPicker.Label.Users" = "ผู้ใช้"; "Scene.ServerPicker.Subtitle" = "เลือกเซิร์ฟเวอร์ตามความสนใจ, ภูมิภาค หรือวัตถุประสงค์ทั่วไปของคุณ"; -"Scene.ServerPicker.SubtitleExtend" = "เลือกเซิร์ฟเวอร์ตามความสนใจ, ภูมิภาค หรือวัตถุประสงค์ทั่วไปของคุณ แต่ละเซิร์ฟเวอร์ดำเนินการโดยองค์กรหรือบุคคลที่เป็นอิสระโดยสิ้นเชิง"; +"Scene.ServerPicker.SubtitleExtend" = "เลือกเซิร์ฟเวอร์ตามความสนใจ, ภูมิภาค หรือวัตถุประสงค์ทั่วไปของคุณ แต่ละเซิร์ฟเวอร์ได้รับการดำเนินงานโดยองค์กรหรือบุคคลที่เป็นอิสระโดยสิ้นเชิง"; "Scene.ServerPicker.Title" = "Mastodon ประกอบด้วยผู้ใช้ในเซิร์ฟเวอร์ต่าง ๆ"; "Scene.ServerRules.Button.Confirm" = "ฉันเห็นด้วย"; "Scene.ServerRules.PrivacyPolicy" = "นโยบายความเป็นส่วนตัว"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.strings index eec4a2940..614ea6dc5 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.strings @@ -107,6 +107,10 @@ "Common.Controls.Status.Actions.Unreblog" = "Yeniden paylaşımı geri al"; "Common.Controls.Status.ContentWarning" = "İçerik Uyarısı"; "Common.Controls.Status.MediaContentWarning" = "Göstermek için herhangi bir yere basın"; +"Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Link: %@"; "Common.Controls.Status.Poll.Closed" = "Kapandı"; "Common.Controls.Status.Poll.Vote" = "Oy ver"; "Common.Controls.Status.SensitiveContent" = "Hassas İçerik"; @@ -160,9 +164,13 @@ Bu kişiye göre profiliniz böyle gözüküyor."; "Scene.Compose.Accessibility.RemovePoll" = "Anketi Kaldır"; "Scene.Compose.Attachment.AttachmentBroken" = "Bu %@ bozuk ve Mastodon'a yüklenemiyor."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "Görme engelliler için fotoğrafı tarif edin..."; "Scene.Compose.Attachment.DescriptionVideo" = "Görme engelliler için videoyu tarif edin..."; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "fotoğraf"; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Eklemek için boşluk tuşuna basın"; "Scene.Compose.ComposeAction" = "Yayınla"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings index 14f36c7e7..41ac9aa20 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings @@ -108,6 +108,10 @@ Vui lòng kiểm tra kết nối mạng."; "Common.Controls.Status.Actions.Unreblog" = "Hủy đăng lại"; "Common.Controls.Status.ContentWarning" = "Nội dung ẩn"; "Common.Controls.Status.MediaContentWarning" = "Nhấn để hiển thị"; +"Common.Controls.Status.MetaEntity.Email" = "Email: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Hiện hồ sơ: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Link: %@"; "Common.Controls.Status.Poll.Closed" = "Kết thúc"; "Common.Controls.Status.Poll.Vote" = "Bình chọn"; "Common.Controls.Status.SensitiveContent" = "Nội dung nhạy cảm"; @@ -151,7 +155,7 @@ Họ sẽ thấy trang của bạn như thế này."; "Scene.AccountList.AddAccount" = "Thêm tài khoản"; "Scene.AccountList.DismissAccountSwitcher" = "Bỏ qua chuyển đổi tài khoản"; "Scene.AccountList.TabBarHint" = "Đang dùng tài khoản: %@. Nhấn hai lần và giữ để đổi sang tài khoản khác"; -"Scene.Bookmark.Title" = "Bookmarks"; +"Scene.Bookmark.Title" = "Tút đã lưu"; "Scene.Compose.Accessibility.AppendAttachment" = "Thêm media"; "Scene.Compose.Accessibility.AppendPoll" = "Tạo bình chọn"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Chọn emoji"; @@ -161,9 +165,13 @@ Họ sẽ thấy trang của bạn như thế này."; "Scene.Compose.Accessibility.RemovePoll" = "Xóa bình chọn"; "Scene.Compose.Attachment.AttachmentBroken" = "%@ này bị lỗi và không thể tải lên Mastodon."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "Mô tả hình ảnh cho người khiếm thị..."; "Scene.Compose.Attachment.DescriptionVideo" = "Mô tả video cho người khiếm thị..."; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "ảnh"; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Khoảng cách để thêm"; "Scene.Compose.ComposeAction" = "Đăng"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings index 8e5de6b52..3883e30df 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings @@ -108,6 +108,10 @@ "Common.Controls.Status.Actions.Unreblog" = "取消转发"; "Common.Controls.Status.ContentWarning" = "内容警告"; "Common.Controls.Status.MediaContentWarning" = "点击任意位置显示"; +"Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Link: %@"; "Common.Controls.Status.Poll.Closed" = "已关闭"; "Common.Controls.Status.Poll.Vote" = "投票"; "Common.Controls.Status.SensitiveContent" = "敏感内容"; @@ -161,9 +165,13 @@ "Scene.Compose.Accessibility.RemovePoll" = "移除投票"; "Scene.Compose.Attachment.AttachmentBroken" = "%@已损坏 无法上传到 Mastodon"; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "为视觉障碍人士添加照片的文字说明..."; "Scene.Compose.Attachment.DescriptionVideo" = "为视觉障碍人士添加视频的文字说明..."; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "照片"; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "视频"; "Scene.Compose.AutoComplete.SpaceToAdd" = "输入空格键入"; "Scene.Compose.ComposeAction" = "发送"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings index f97926795..34e59a582 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings @@ -108,6 +108,10 @@ "Common.Controls.Status.Actions.Unreblog" = "取消轉嘟"; "Common.Controls.Status.ContentWarning" = "內容警告"; "Common.Controls.Status.MediaContentWarning" = "輕觸任何地方以顯示"; +"Common.Controls.Status.MetaEntity.Email" = "電子郵件地址:%@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "主題標籤: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "顯示個人檔案:%@"; +"Common.Controls.Status.MetaEntity.Url" = "連結:%@"; "Common.Controls.Status.Poll.Closed" = "已關閉"; "Common.Controls.Status.Poll.Vote" = "投票"; "Common.Controls.Status.SensitiveContent" = "敏感內容"; @@ -147,7 +151,7 @@ "Scene.AccountList.AddAccount" = "新增帳號"; "Scene.AccountList.DismissAccountSwitcher" = "關閉帳號切換器"; "Scene.AccountList.TabBarHint" = "目前已選擇的個人檔案:%@。點兩下然後按住以顯示帳號切換器"; -"Scene.Bookmark.Title" = "Bookmarks"; +"Scene.Bookmark.Title" = "書籤"; "Scene.Compose.Accessibility.AppendAttachment" = "新增附件"; "Scene.Compose.Accessibility.AppendPoll" = "新增投票"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "自訂 emoji 選擇器"; @@ -156,9 +160,13 @@ "Scene.Compose.Accessibility.PostVisibilityMenu" = "嘟文可見性選單"; "Scene.Compose.Accessibility.RemovePoll" = "移除投票"; "Scene.Compose.Attachment.AttachmentBroken" = "此 %@ 已損毀,並無法被上傳至 Mastodon。"; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment"; "Scene.Compose.Attachment.DescriptionPhoto" = "為視障人士提供圖片說明..."; "Scene.Compose.Attachment.DescriptionVideo" = "為視障人士提供影片說明..."; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "照片"; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "影片"; "Scene.Compose.AutoComplete.SpaceToAdd" = "添加的空白"; "Scene.Compose.ComposeAction" = "嘟出去"; diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift index a2567d0b6..073ca7b10 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift @@ -65,7 +65,7 @@ public struct AttachmentView: View { if viewModel.output == nil, let error = viewModel.error { VisualEffectView(effect: blurEffect) VStack { - Text("Load Failed") // TODO: i18n + Text(L10n.Scene.Compose.Attachment) // TODO: i18n .font(.system(size: 13, weight: .semibold)) Text(error.localizedDescription) .font(.system(size: 12, weight: .regular)) From af0dc45d1bea6d7b36df2ffd8b3e18c3f76760b4 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 14 Nov 2022 19:27:09 +0800 Subject: [PATCH 217/224] feat: update i18n string --- Localization/app.json | 10 +++++++--- .../Attachment/AttachmentView.swift | 6 +++--- .../Attachment/AttachmentViewModel.swift | 16 ++++++++-------- .../ComposeContent/ComposeContentViewModel.swift | 2 +- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index a6a971860..af3189e8b 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -386,7 +386,9 @@ "load_failed": "Load Failed", "upload_failed": "Upload Failed", "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", - "attachment_too_large": "Attachment too large" + "attachment_too_large": "Attachment too large", + "compressing_state": "Compressing...", + "server_processing_state": "Server Processing..." }, "poll": { "duration_time": "Duration: %s", @@ -396,7 +398,9 @@ "one_day": "1 Day", "three_days": "3 Days", "seven_days": "7 Days", - "option_number": "Option %ld" + "option_number": "Option %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "Write an accurate warning here..." @@ -709,4 +713,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift index 073ca7b10..d2baeed71 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift @@ -65,7 +65,7 @@ public struct AttachmentView: View { if viewModel.output == nil, let error = viewModel.error { VisualEffectView(effect: blurEffect) VStack { - Text(L10n.Scene.Compose.Attachment) // TODO: i18n + Text(L10n.Scene.Compose.Attachment.loadFailed) .font(.system(size: 13, weight: .semibold)) Text(error.localizedDescription) .font(.system(size: 12, weight: .regular)) @@ -123,7 +123,7 @@ public struct AttachmentView: View { case .remove: switch viewModel.uploadState { case .compressing: - return "Comporessing..." // TODO: i18n + return "Compressing..." // TODO: i18n default: if viewModel.fractionCompleted < 0.9 { let totalSizeInByte = viewModel.outputSizeInByte @@ -136,7 +136,7 @@ public struct AttachmentView: View { } } case .retry: - return "Upload Failed" // TODO: i18n + return L10n.Scene.Compose.Attachment.uploadFailed } }() let subtitle: String = { diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift index 9a0f58f47..18da157c5 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift @@ -11,6 +11,7 @@ import Combine import PhotosUI import Kingfisher import MastodonCore +import MastodonLocalization import func QuartzCore.CACurrentMediaTime public protocol AttachmentViewModelDelegate: AnyObject { @@ -48,9 +49,7 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable public let authContext: AuthContext public let input: Input @Published var caption = "" - @Published var sizeLimit = SizeLimit() - - // var compressVideoTask: Task? + // @Published var sizeLimit = SizeLimit() // output @Published public private(set) var output: Output? @@ -263,15 +262,16 @@ extension AttachmentViewModel { } } + // not in using public struct SizeLimit { public let image: Int public let gif: Int public let video: Int public init( - image: Int = 5 * 1024 * 1024, // 5 MiB, - gif: Int = 15 * 1024 * 1024, // 15 MiB, - video: Int = 512 * 1024 * 1024 // 512 MiB + image: Int = 10 * 1024 * 1024, // 10 MiB + gif: Int = 40 * 1024 * 1024, // 40 MiB + video: Int = 40 * 1024 * 1024 // 40 MiB ) { self.image = image self.gif = gif @@ -286,9 +286,9 @@ extension AttachmentViewModel { public var errorDescription: String? { switch self { case .invalidAttachmentType: - return "Can not regonize this media attachment" // TODO: i18n + return L10n.Scene.Compose.Attachment.canNotRecognizeThisMediaAttachment case .attachmentTooLarge: - return "Attachment too large" + return L10n.Scene.Compose.Attachment.attachmentTooLarge } } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 91be96248..37a6bdebf 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -479,7 +479,7 @@ extension ComposeContentViewModel { public var errorDescription: String? { switch self { case .pollHasEmptyOption: - return "The post poll is invalid" // TODO: i18n + return "The poll is invalid" // TODO: i18n } } From 591acb4c2c72609c6579560d6d1f875af9e84e1a Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 14 Nov 2022 19:43:32 +0800 Subject: [PATCH 218/224] feat: restore keyboard shortcut for compose scene --- .../Scene/Compose/ComposeViewController.swift | 261 +++++++++--------- .../ComposeContentViewController.swift | 7 +- .../ComposeContentViewModel.swift | 4 +- 3 files changed, 140 insertions(+), 132 deletions(-) diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index cc595d5bc..414c53269 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -324,130 +324,137 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate { } -//extension ComposeViewController { -// override var keyCommands: [UIKeyCommand]? { -// composeKeyCommands -// } -//} -// -//extension ComposeViewController { -// -// enum ComposeKeyCommand: String, CaseIterable { -// case discardPost -// case publishPost -// case mediaBrowse -// case mediaPhotoLibrary -// case mediaCamera -// case togglePoll -// case toggleContentWarning -// case selectVisibilityPublic -// // TODO: remove selectVisibilityUnlisted from codebase -// // case selectVisibilityUnlisted -// case selectVisibilityPrivate -// case selectVisibilityDirect -// -// var title: String { -// switch self { -// case .discardPost: return L10n.Scene.Compose.Keyboard.discardPost -// case .publishPost: return L10n.Scene.Compose.Keyboard.publishPost -// case .mediaBrowse: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.browse) -// case .mediaPhotoLibrary: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.photoLibrary) -// case .mediaCamera: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.camera) -// case .togglePoll: return L10n.Scene.Compose.Keyboard.togglePoll -// case .toggleContentWarning: return L10n.Scene.Compose.Keyboard.toggleContentWarning -// case .selectVisibilityPublic: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.public) -// // case .selectVisibilityUnlisted: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.unlisted) -// case .selectVisibilityPrivate: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.private) -// case .selectVisibilityDirect: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.direct) -// } -// } -// -// // UIKeyCommand input -// var input: String { -// switch self { -// case .discardPost: return "w" // + command -// case .publishPost: return "\r" // (enter) + command -// case .mediaBrowse: return "b" // + option + command -// case .mediaPhotoLibrary: return "p" // + option + command -// case .mediaCamera: return "c" // + option + command -// case .togglePoll: return "p" // + shift + command -// case .toggleContentWarning: return "c" // + shift + command -// case .selectVisibilityPublic: return "1" // + command -// // case .selectVisibilityUnlisted: return "2" // + command -// case .selectVisibilityPrivate: return "2" // + command -// case .selectVisibilityDirect: return "3" // + command -// } -// } -// -// var modifierFlags: UIKeyModifierFlags { -// switch self { -// case .discardPost: return [.command] -// case .publishPost: return [.command] -// case .mediaBrowse: return [.alternate, .command] -// case .mediaPhotoLibrary: return [.alternate, .command] -// case .mediaCamera: return [.alternate, .command] -// case .togglePoll: return [.shift, .command] -// case .toggleContentWarning: return [.shift, .command] -// case .selectVisibilityPublic: return [.command] -// // case .selectVisibilityUnlisted: return [.command] -// case .selectVisibilityPrivate: return [.command] -// case .selectVisibilityDirect: return [.command] -// } -// } -// -// var propertyList: Any { -// return rawValue -// } -// } -// -// var composeKeyCommands: [UIKeyCommand]? { -// ComposeKeyCommand.allCases.map { command in -// UIKeyCommand( -// title: command.title, -// image: nil, -// action: #selector(Self.composeKeyCommandHandler(_:)), -// input: command.input, -// modifierFlags: command.modifierFlags, -// propertyList: command.propertyList, -// alternates: [], -// discoverabilityTitle: nil, -// attributes: [], -// state: .off -// ) -// } -// } -// -// @objc private func composeKeyCommandHandler(_ sender: UIKeyCommand) { -// guard let rawValue = sender.propertyList as? String, -// let command = ComposeKeyCommand(rawValue: rawValue) else { return } -// -// switch command { -// case .discardPost: -// cancelBarButtonItemPressed(cancelBarButtonItem) -// case .publishPost: -// publishBarButtonItemPressed(publishBarButtonItem) -// case .mediaBrowse: -// present(documentPickerController, animated: true, completion: nil) -// case .mediaPhotoLibrary: -// present(photoLibraryPicker, animated: true, completion: nil) -// case .mediaCamera: -// guard UIImagePickerController.isSourceTypeAvailable(.camera) else { -// return -// } -// present(imagePickerController, animated: true, completion: nil) -// case .togglePoll: -// composeToolbarView.pollButton.sendActions(for: .touchUpInside) -// case .toggleContentWarning: -// composeToolbarView.contentWarningButton.sendActions(for: .touchUpInside) -// case .selectVisibilityPublic: -// viewModel.selectedStatusVisibility = .public -// // case .selectVisibilityUnlisted: -// // viewModel.selectedStatusVisibility.value = .unlisted -// case .selectVisibilityPrivate: -// viewModel.selectedStatusVisibility = .private -// case .selectVisibilityDirect: -// viewModel.selectedStatusVisibility = .direct -// } -// } -// -//} +extension ComposeViewController { + override var keyCommands: [UIKeyCommand]? { + composeKeyCommands + } +} + +extension ComposeViewController { + + enum ComposeKeyCommand: String, CaseIterable { + case discardPost + case publishPost + case mediaBrowse + case mediaPhotoLibrary + case mediaCamera + case togglePoll + case toggleContentWarning + case selectVisibilityPublic + // TODO: remove selectVisibilityUnlisted from codebase + // case selectVisibilityUnlisted + case selectVisibilityPrivate + case selectVisibilityDirect + + var title: String { + switch self { + case .discardPost: return L10n.Scene.Compose.Keyboard.discardPost + case .publishPost: return L10n.Scene.Compose.Keyboard.publishPost + case .mediaBrowse: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.browse) + case .mediaPhotoLibrary: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.photoLibrary) + case .mediaCamera: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.camera) + case .togglePoll: return L10n.Scene.Compose.Keyboard.togglePoll + case .toggleContentWarning: return L10n.Scene.Compose.Keyboard.toggleContentWarning + case .selectVisibilityPublic: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.public) + // case .selectVisibilityUnlisted: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.unlisted) + case .selectVisibilityPrivate: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.private) + case .selectVisibilityDirect: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.direct) + } + } + + // UIKeyCommand input + var input: String { + switch self { + case .discardPost: return "w" // + command + case .publishPost: return "\r" // (enter) + command + case .mediaBrowse: return "b" // + option + command + case .mediaPhotoLibrary: return "p" // + option + command + case .mediaCamera: return "c" // + option + command + case .togglePoll: return "p" // + shift + command + case .toggleContentWarning: return "c" // + shift + command + case .selectVisibilityPublic: return "1" // + command + // case .selectVisibilityUnlisted: return "2" // + command + case .selectVisibilityPrivate: return "2" // + command + case .selectVisibilityDirect: return "3" // + command + } + } + + var modifierFlags: UIKeyModifierFlags { + switch self { + case .discardPost: return [.command] + case .publishPost: return [.command] + case .mediaBrowse: return [.alternate, .command] + case .mediaPhotoLibrary: return [.alternate, .command] + case .mediaCamera: return [.alternate, .command] + case .togglePoll: return [.shift, .command] + case .toggleContentWarning: return [.shift, .command] + case .selectVisibilityPublic: return [.command] + // case .selectVisibilityUnlisted: return [.command] + case .selectVisibilityPrivate: return [.command] + case .selectVisibilityDirect: return [.command] + } + } + + var propertyList: Any { + return rawValue + } + } + + var composeKeyCommands: [UIKeyCommand]? { + ComposeKeyCommand.allCases.map { command in + UIKeyCommand( + title: command.title, + image: nil, + action: #selector(Self.composeKeyCommandHandler(_:)), + input: command.input, + modifierFlags: command.modifierFlags, + propertyList: command.propertyList, + alternates: [], + discoverabilityTitle: nil, + attributes: [], + state: .off + ) + } + } + + @objc private func composeKeyCommandHandler(_ sender: UIKeyCommand) { + guard let rawValue = sender.propertyList as? String, + let command = ComposeKeyCommand(rawValue: rawValue) else { return } + + switch command { + case .discardPost: + cancelBarButtonItemPressed(cancelBarButtonItem) + case .publishPost: + publishBarButtonItemPressed(publishBarButtonItem) + case .mediaBrowse: + guard !isViewControllerIsAlreadyModal(composeContentViewController.documentPickerController) else { return } + present(composeContentViewController.documentPickerController, animated: true, completion: nil) + case .mediaPhotoLibrary: + guard !isViewControllerIsAlreadyModal(composeContentViewController.photoLibraryPicker) else { return } + present(composeContentViewController.photoLibraryPicker, animated: true, completion: nil) + case .mediaCamera: + guard UIImagePickerController.isSourceTypeAvailable(.camera) else { + return + } + guard !isViewControllerIsAlreadyModal(composeContentViewController.imagePickerController) else { return } + present(composeContentViewController.imagePickerController, animated: true, completion: nil) + case .togglePoll: + composeContentViewModel.isPollActive.toggle() + case .toggleContentWarning: + composeContentViewModel.isContentWarningActive.toggle() + case .selectVisibilityPublic: + composeContentViewModel.visibility = .public + // case .selectVisibilityUnlisted: + // viewModel.selectedStatusVisibility.value = .unlisted + case .selectVisibilityPrivate: + composeContentViewModel.visibility = .private + case .selectVisibilityDirect: + composeContentViewModel.visibility = .direct + } + } + + private func isViewControllerIsAlreadyModal(_ viewController: UIViewController) -> Bool { + return viewController.presentingViewController != nil + } + +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index 7034aeefb..b84b47385 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -55,20 +55,20 @@ public final class ComposeContentViewController: UIViewController { return configuration } - private(set) lazy var photoLibraryPicker: PHPickerViewController = { + public private(set) lazy var photoLibraryPicker: PHPickerViewController = { let imagePicker = PHPickerViewController(configuration: ComposeContentViewController.createPhotoLibraryPickerConfiguration()) imagePicker.delegate = self return imagePicker }() - private(set) lazy var imagePickerController: UIImagePickerController = { + public private(set) lazy var imagePickerController: UIImagePickerController = { let imagePickerController = UIImagePickerController() imagePickerController.sourceType = .camera imagePickerController.delegate = self return imagePickerController }() - private(set) lazy var documentPickerController: UIDocumentPickerViewController = { + public private(set) lazy var documentPickerController: UIDocumentPickerViewController = { let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image, .movie]) documentPickerController.delegate = self return documentPickerController @@ -342,6 +342,7 @@ extension ComposeContentViewController { // bind back to source due to visibility not update via delegate composeContentToolbarViewModel.$visibility .dropFirst() + .receive(on: DispatchQueue.main) .sink { [weak self] visibility in guard let self = self else { return } if self.viewModel.visibility != visibility { diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 37a6bdebf..e840b53fd 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -91,7 +91,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // @Published public internal(set) var isMediaValid = true // poll - @Published var isPollActive = false + @Published public var isPollActive = false @Published public var pollOptions: [PollComposeItem.Option] = { // initial with 2 options var options: [PollComposeItem.Option] = [] @@ -111,7 +111,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { @Published var isLoadingCustomEmoji = false // visibility - @Published var visibility: Mastodon.Entity.Status.Visibility + @Published public var visibility: Mastodon.Entity.Status.Visibility // UI & UX @Published var replyToCellFrame: CGRect = .zero From a2f2fb83cdc433a411741c34964227941355b68f Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 14 Nov 2022 13:12:16 +0100 Subject: [PATCH 219/224] Fix authenticated user account not reloaded --- .../Scene/Profile/ProfileViewController.swift | 3 +++ .../Root/MainTab/MainTabBarController.swift | 27 ++++++++++++++++++- Mastodon/Supporting Files/SceneDelegate.swift | 3 +++ .../CoreDataStack/MastodonUser+Property.swift | 4 +++ .../Service/API/APIService+Account.swift | 9 +++++++ .../Service/InstanceService.swift | 3 ++- 6 files changed, 47 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 3ce1fd33a..212e92d06 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -552,6 +552,9 @@ extension ProfileViewController { userTimelineViewController.viewModel.stateMachine.enter(UserTimelineViewModel.State.Reloading.self) } + // trigger authenticated user account update + viewModel.context.instanceService.updateActiveUserAccountPublisher.send() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { sender.endRefreshing() } diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index c49dcc1a1..64066746a 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -312,7 +312,12 @@ extension MainTabBarController { guard let profileTabItem = _profileTabItem else { return } let currentUserDisplayName = user.displayNameWithFallback ?? "no user" profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName) - + + context.instanceService.updateActiveUserAccountPublisher + .sink { [weak self] in + self?.updateUserAccount() + } + .store(in: &disposeBag) } else { self.avatarURLObserver = nil } @@ -487,6 +492,26 @@ extension MainTabBarController { avatarButton.setNeedsLayout() } + private func updateUserAccount() { + guard let authContext = authContext else { return } + + Task { @MainActor in + let profileResponse = try await context.apiService.authenticatedUserInfo( + authenticationBox: authContext.mastodonAuthenticationBox + ) + + if let user = authContext.mastodonAuthenticationBox.authenticationRecord.object( + in: context.managedObjectContext + )?.user { + user.update( + property: .init( + entity: profileResponse.value, + domain: authContext.mastodonAuthenticationBox.domain + ) + ) + } + } + } } extension MainTabBarController { diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 1e97fb179..fad0f0cd5 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -109,6 +109,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // trigger status filter update AppContext.shared.statusFilterService.filterUpdatePublisher.send() + + // trigger authenticated user account update + AppContext.shared.instanceService.updateActiveUserAccountPublisher.send() if let shortcutItem = savedShortCutItem { Task { diff --git a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser+Property.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser+Property.swift index cebe7f8af..8d2f77ba7 100644 --- a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser+Property.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser+Property.swift @@ -10,6 +10,10 @@ import CoreDataStack import MastodonSDK extension MastodonUser.Property { + public init(entity: Mastodon.Entity.Account, domain: String) { + self.init(entity: entity, domain: domain, networkDate: Date()) + } + init(entity: Mastodon.Entity.Account, domain: String, networkDate: Date) { self.init( identifier: entity.id + "@" + domain, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index 68649d24c..1b6a57a83 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -13,6 +13,15 @@ import MastodonCommon import MastodonSDK extension APIService { + public func authenticatedUserInfo( + authenticationBox: MastodonAuthenticationBox + ) async throws -> Mastodon.Response.Content { + try await accountInfo( + domain: authenticationBox.domain, + userID: authenticationBox.userID, + authorization: authenticationBox.userAuthorization + ) + } public func accountInfo( domain: String, diff --git a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift index c63e965bd..7f6669250 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift @@ -24,7 +24,8 @@ public final class InstanceService { weak var authenticationService: AuthenticationService? // output - + public let updateActiveUserAccountPublisher = PassthroughSubject() + init( apiService: APIService, authenticationService: AuthenticationService From 548543a8c0a0e70fa43a3d306721cf800ad4ddbe Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 14 Nov 2022 14:15:28 +0100 Subject: [PATCH 220/224] chore: Move updateActiveUserAccountPublisher to AuthenticationService --- Mastodon/Scene/Profile/ProfileViewController.swift | 2 +- Mastodon/Scene/Root/MainTab/MainTabBarController.swift | 2 +- Mastodon/Supporting Files/SceneDelegate.swift | 2 +- .../Sources/MastodonCore/Service/AuthenticationService.swift | 1 + MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift | 1 - 5 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 212e92d06..07d72c0b6 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -553,7 +553,7 @@ extension ProfileViewController { } // trigger authenticated user account update - viewModel.context.instanceService.updateActiveUserAccountPublisher.send() + viewModel.context.authenticationService.updateActiveUserAccountPublisher.send() DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { sender.endRefreshing() diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 64066746a..987c1141b 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -313,7 +313,7 @@ extension MainTabBarController { let currentUserDisplayName = user.displayNameWithFallback ?? "no user" profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName) - context.instanceService.updateActiveUserAccountPublisher + context.authenticationService.updateActiveUserAccountPublisher .sink { [weak self] in self?.updateUserAccount() } diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index fad0f0cd5..bf02b8031 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -111,7 +111,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { AppContext.shared.statusFilterService.filterUpdatePublisher.send() // trigger authenticated user account update - AppContext.shared.instanceService.updateActiveUserAccountPublisher.send() + AppContext.shared.authenticationService.updateActiveUserAccountPublisher.send() if let shortcutItem = savedShortCutItem { Task { diff --git a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift index 48da254c6..0e5160679 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift @@ -25,6 +25,7 @@ public final class AuthenticationService: NSObject { // output @Published public var mastodonAuthentications: [ManagedObjectRecord] = [] @Published public var mastodonAuthenticationBoxes: [MastodonAuthenticationBox] = [] + public let updateActiveUserAccountPublisher = PassthroughSubject() init( managedObjectContext: NSManagedObjectContext, diff --git a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift index 7f6669250..4cd804036 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift @@ -24,7 +24,6 @@ public final class InstanceService { weak var authenticationService: AuthenticationService? // output - public let updateActiveUserAccountPublisher = PassthroughSubject() init( apiService: APIService, From abe6292696277c925dd5c467662ee237bbd9f731 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 15 Nov 2022 00:59:04 +0800 Subject: [PATCH 221/224] chore: code clean --- .../Scene/Compose/ComposeViewController.swift | 67 ------------------- 1 file changed, 67 deletions(-) diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 414c53269..fd5ed5817 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -44,20 +44,6 @@ final class ComposeViewController: UIViewController, NeedsDependency { }() private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:))) - - // FIXME: deprecated - // let characterCountLabel: UILabel = { - // let label = UILabel() - // label.font = .systemFont(ofSize: 15, weight: .regular) - // label.text = "500" - // label.textColor = Asset.Colors.Label.secondary.color - // label.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(500) - // return label - // }() - // private(set) lazy var characterCountBarButtonItem: UIBarButtonItem = { - // let barButtonItem = UIBarButtonItem(customView: characterCountLabel) - // return barButtonItem - // }() let publishButton: UIButton = { let button = RoundedEdgesButton(type: .custom) @@ -95,20 +81,6 @@ final class ComposeViewController: UIViewController, NeedsDependency { } -extension ComposeViewController { - private static func createLayout() -> UICollectionViewLayout { - let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) - let item = NSCollectionLayoutItem(layoutSize: itemSize) - let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) - let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) - let section = NSCollectionLayoutSection(group: group) - section.contentInsetsReference = .readableContent - // section.interGroupSpacing = 10 - // section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10) - return UICollectionViewCompositionalLayout(section: section) - } -} - extension ComposeViewController { override func viewDidLoad() { @@ -181,45 +153,6 @@ extension ComposeViewController { present(alertController, animated: true, completion: nil) } -// private func setupBackgroundColor(theme: Theme) { -// let backgroundColor = UIColor(dynamicProvider: { traitCollection in -// switch traitCollection.userInterfaceStyle { -// case .light: -// return .systemBackground -// default: -// return theme.systemElevatedBackgroundColor -// } -// }) -// view.backgroundColor = backgroundColor -//// tableView.backgroundColor = backgroundColor -//// composeToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor -// } -// -// // keyboard shortcutBar -// private func setupInputAssistantItem(item: UITextInputAssistantItem) { -// let barButtonItems = [ -// composeToolbarView.mediaBarButtonItem, -// composeToolbarView.pollBarButtonItem, -// composeToolbarView.contentWarningBarButtonItem, -// composeToolbarView.visibilityBarButtonItem, -// ] -// let group = UIBarButtonItemGroup(barButtonItems: barButtonItems, representativeItem: nil) -// -// item.trailingBarButtonGroups = [group] -// } -// -// private func configureToolbarDisplay(keyboardHasShortcutBar: Bool) { -// switch self.traitCollection.userInterfaceIdiom { -// case .pad: -// let shouldHideToolbar = keyboardHasShortcutBar && self.traitCollection.horizontalSizeClass == .regular -// self.composeToolbarView.alpha = shouldHideToolbar ? 0 : 1 -// self.composeToolbarBackgroundView.alpha = shouldHideToolbar ? 0 : 1 -// default: -// break -// } -// } -// - } extension ComposeViewController { From 33383de85de8279f230009cbb9ef23213773e29a Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 15 Nov 2022 01:42:14 +0800 Subject: [PATCH 222/224] chore: update SwiftGen to the latest version --- Podfile | 2 +- Podfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Podfile b/Podfile index 596aec62b..4df2d4d7c 100644 --- a/Podfile +++ b/Podfile @@ -11,7 +11,7 @@ target 'Mastodon' do pod 'XLPagerTabStrip', '~> 9.0.0' # misc - pod 'SwiftGen', '~> 6.4.0' + pod 'SwiftGen', '~> 6.6.2' pod 'DateToolsSwift', '~> 5.0.0' pod 'Kanna', '~> 5.2.2' pod 'Sourcery', '~> 1.6.1' diff --git a/Podfile.lock b/Podfile.lock index 3b9928a0b..c7220b00a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -5,7 +5,7 @@ PODS: - Sourcery (1.6.1): - Sourcery/CLI-Only (= 1.6.1) - Sourcery/CLI-Only (1.6.1) - - SwiftGen (6.4.0) + - SwiftGen (6.6.2) - XLPagerTabStrip (9.0.0) DEPENDENCIES: @@ -13,7 +13,7 @@ DEPENDENCIES: - FLEX (~> 4.4.0) - Kanna (~> 5.2.2) - Sourcery (~> 1.6.1) - - SwiftGen (~> 6.4.0) + - SwiftGen (~> 6.6.2) - XLPagerTabStrip (~> 9.0.0) SPEC REPOS: @@ -30,9 +30,9 @@ SPEC CHECKSUMS: FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab Kanna: 01cfbddc127f5ff0963692f285fcbc8a9d62d234 Sourcery: f3759f803bd0739f74fc92a4341eed0473ce61ac - SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 + SwiftGen: 1366a7f71aeef49954ca5a63ba4bef6b0f24138c XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5 -PODFILE CHECKSUM: 8fddf46611e09d2eb1a5d67c464c236884a08e80 +PODFILE CHECKSUM: 7499a197793f73c4dcf1d16a315434baaa688873 COCOAPODS: 1.11.3 From 220fd6ae02cfbd240a58bda002ea2d0a0fb91751 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 15 Nov 2022 01:44:28 +0800 Subject: [PATCH 223/224] feat: improve the i18n workflow --- .../Sources/StringsConvertor/main.swift | 1 + .../input/Base.lproj/Localizable.stringsdict | 631 +++++++++++ .../input/Base.lproj/app.json | 718 +++++++++++++ .../input/Base.lproj/ios-infoPlist.json | 6 + Localization/app.json | 2 +- .../MastodonAsset/Generated/Assets.swift | 71 ++ .../MastodonAsset/Generated/Fonts.swift | 77 +- .../Generated/Strings.swift | 996 +++++++++--------- .../Resources/Base.lproj/Localizable.strings | 461 ++++++++ .../Base.lproj/Localizable.stringsdict | 631 +++++++++++ .../Attachment/AttachmentView.swift | 4 +- .../ComposeContentViewModel.swift | 4 +- swiftgen.yml | 4 +- update_localization.sh | 21 +- 14 files changed, 3110 insertions(+), 517 deletions(-) create mode 100644 Localization/StringsConvertor/input/Base.lproj/Localizable.stringsdict create mode 100644 Localization/StringsConvertor/input/Base.lproj/app.json create mode 100644 Localization/StringsConvertor/input/Base.lproj/ios-infoPlist.json create mode 100644 MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings create mode 100644 MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.stringsdict diff --git a/Localization/StringsConvertor/Sources/StringsConvertor/main.swift b/Localization/StringsConvertor/Sources/StringsConvertor/main.swift index 76dc722f6..beba6cb3f 100644 --- a/Localization/StringsConvertor/Sources/StringsConvertor/main.swift +++ b/Localization/StringsConvertor/Sources/StringsConvertor/main.swift @@ -47,6 +47,7 @@ private func convert(from inputDirectoryURL: URL, to outputDirectory: URL) { private func map(language: String) -> String? { switch language { + case "Base.lproj": return "Base" case "ar.lproj": return "ar" // Arabic case "eu.lproj": return "eu" // Basque case "ca.lproj": return "ca" // Catalan diff --git a/Localization/StringsConvertor/input/Base.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/Base.lproj/Localizable.stringsdict new file mode 100644 index 000000000..cd97825f4 --- /dev/null +++ b/Localization/StringsConvertor/input/Base.lproj/Localizable.stringsdict @@ -0,0 +1,631 @@ + + + + + a11y.plural.count.unread.notification + + NSStringLocalizedFormatKey + %#@notification_count_unread_notification@ + notification_count_unread_notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + no unread notification + one + 1 unread notification + few + %ld unread notifications + many + %ld unread notification + other + %ld unread notification + + + a11y.plural.count.input_limit_exceeds + + NSStringLocalizedFormatKey + Input limit exceeds %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 characters + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + + a11y.plural.count.input_limit_remains + + NSStringLocalizedFormatKey + Input limit remains %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 characters + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + no characters + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + + plural.count.followed_by_and_mutual + + NSStringLocalizedFormatKey + %#@names@%#@count_mutual@ + names + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + + + count_mutual + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + Followed by %1$@ + one + Followed by %1$@, and another mutual + few + Followed by %1$@, and %ld mutuals + many + Followed by %1$@, and %ld mutuals + other + Followed by %1$@, and %ld mutuals + + + plural.count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + posts + one + post + few + posts + many + posts + other + posts + + + plural.count.media + + NSStringLocalizedFormatKey + %#@media_count@ + media_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 media + one + 1 media + few + %ld media + many + %ld media + other + %ld media + + + plural.count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 posts + one + 1 post + few + %ld posts + many + %ld posts + other + %ld posts + + + plural.count.favorite + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 favorites + one + 1 favorite + few + %ld favorites + many + %ld favorites + other + %ld favorites + + + plural.count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 reblogs + one + 1 reblog + few + %ld reblogs + many + %ld reblogs + other + %ld reblogs + + + plural.count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 replies + one + 1 reply + few + %ld replies + many + %ld replies + other + %ld replies + + + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 votes + one + 1 vote + few + %ld votes + many + %ld votes + other + %ld votes + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 voters + one + 1 voter + few + %ld voters + many + %ld voters + other + %ld voters + + + plural.people_talking + + NSStringLocalizedFormatKey + %#@count_people_talking@ + count_people_talking + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 people talking + one + 1 people talking + few + %ld people talking + many + %ld people talking + other + %ld people talking + + + plural.count.following + + NSStringLocalizedFormatKey + %#@count_following@ + count_following + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 following + one + 1 following + few + %ld following + many + %ld following + other + %ld following + + + plural.count.follower + + NSStringLocalizedFormatKey + %#@count_follower@ + count_follower + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 followers + one + 1 follower + few + %ld followers + many + %ld followers + other + %ld followers + + + date.year.left + + NSStringLocalizedFormatKey + %#@count_year_left@ + count_year_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 years left + one + 1 year left + few + %ld years left + many + %ld years left + other + %ld years left + + + date.month.left + + NSStringLocalizedFormatKey + %#@count_month_left@ + count_month_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 months left + one + 1 months left + few + %ld months left + many + %ld months left + other + %ld months left + + + date.day.left + + NSStringLocalizedFormatKey + %#@count_day_left@ + count_day_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 days left + one + 1 day left + few + %ld days left + many + %ld days left + other + %ld days left + + + date.hour.left + + NSStringLocalizedFormatKey + %#@count_hour_left@ + count_hour_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 hours left + one + 1 hour left + few + %ld hours left + many + %ld hours left + other + %ld hours left + + + date.minute.left + + NSStringLocalizedFormatKey + %#@count_minute_left@ + count_minute_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 minutes left + one + 1 minute left + few + %ld minutes left + many + %ld minutes left + other + %ld minutes left + + + date.second.left + + NSStringLocalizedFormatKey + %#@count_second_left@ + count_second_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 seconds left + one + 1 second left + few + %ld seconds left + many + %ld seconds left + other + %ld seconds left + + + date.year.ago.abbr + + NSStringLocalizedFormatKey + %#@count_year_ago_abbr@ + count_year_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0y ago + one + 1y ago + few + %ldy ago + many + %ldy ago + other + %ldy ago + + + date.month.ago.abbr + + NSStringLocalizedFormatKey + %#@count_month_ago_abbr@ + count_month_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0M ago + one + 1M ago + few + %ldM ago + many + %ldM ago + other + %ldM ago + + + date.day.ago.abbr + + NSStringLocalizedFormatKey + %#@count_day_ago_abbr@ + count_day_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0d ago + one + 1d ago + few + %ldd ago + many + %ldd ago + other + %ldd ago + + + date.hour.ago.abbr + + NSStringLocalizedFormatKey + %#@count_hour_ago_abbr@ + count_hour_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0h ago + one + 1h ago + few + %ldh ago + many + %ldh ago + other + %ldh ago + + + date.minute.ago.abbr + + NSStringLocalizedFormatKey + %#@count_minute_ago_abbr@ + count_minute_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0m ago + one + 1m ago + few + %ldm ago + many + %ldm ago + other + %ldm ago + + + date.second.ago.abbr + + NSStringLocalizedFormatKey + %#@count_second_ago_abbr@ + count_second_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0s ago + one + 1s ago + few + %lds ago + many + %lds ago + other + %lds ago + + + + diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json new file mode 100644 index 000000000..c40c0a39e --- /dev/null +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -0,0 +1,718 @@ +{ + "common": { + "alerts": { + "common": { + "please_try_again": "Please try again.", + "please_try_again_later": "Please try again later." + }, + "sign_up_failure": { + "title": "Sign Up Failure" + }, + "server_error": { + "title": "Server Error" + }, + "vote_failure": { + "title": "Vote Failure", + "poll_ended": "The poll has ended" + }, + "discard_post_content": { + "title": "Discard Draft", + "message": "Confirm to discard composed post content." + }, + "publish_post_failure": { + "title": "Publish Failure", + "message": "Failed to publish the post.\nPlease check your internet connection.", + "attachments_message": { + "video_attach_with_photo": "Cannot attach a video to a post that already contains images.", + "more_than_one_video": "Cannot attach more than one video." + } + }, + "edit_profile_failure": { + "title": "Edit Profile Error", + "message": "Cannot edit profile. Please try again." + }, + "sign_out": { + "title": "Sign Out", + "message": "Are you sure you want to sign out?", + "confirm": "Sign Out" + }, + "block_domain": { + "title": "Are you really, really sure you want to block the entire %s? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain and any of your followers from that domain will be removed.", + "block_entire_domain": "Block Domain" + }, + "save_photo_failure": { + "title": "Save Photo Failure", + "message": "Please enable the photo library access permission to save the photo." + }, + "delete_post": { + "title": "Delete Post", + "message": "Are you sure you want to delete this post?" + }, + "clean_cache": { + "title": "Clean Cache", + "message": "Successfully cleaned %s cache." + } + }, + "controls": { + "actions": { + "back": "Back", + "next": "Next", + "previous": "Previous", + "open": "Open", + "add": "Add", + "remove": "Remove", + "edit": "Edit", + "save": "Save", + "ok": "OK", + "done": "Done", + "confirm": "Confirm", + "continue": "Continue", + "compose": "Compose", + "cancel": "Cancel", + "discard": "Discard", + "try_again": "Try Again", + "take_photo": "Take Photo", + "save_photo": "Save Photo", + "copy_photo": "Copy Photo", + "sign_in": "Sign In", + "sign_up": "Sign Up", + "see_more": "See More", + "preview": "Preview", + "share": "Share", + "share_user": "Share %s", + "share_post": "Share Post", + "open_in_safari": "Open in Safari", + "open_in_browser": "Open in Browser", + "find_people": "Find people to follow", + "manually_search": "Manually search instead", + "skip": "Skip", + "reply": "Reply", + "report_user": "Report %s", + "block_domain": "Block %s", + "unblock_domain": "Unblock %s", + "settings": "Settings", + "delete": "Delete" + }, + "tabs": { + "home": "Home", + "search": "Search", + "notification": "Notification", + "profile": "Profile" + }, + "keyboard": { + "common": { + "switch_to_tab": "Switch to %s", + "compose_new_post": "Compose New Post", + "show_favorites": "Show Favorites", + "open_settings": "Open Settings" + }, + "timeline": { + "previous_status": "Previous Post", + "next_status": "Next Post", + "open_status": "Open Post", + "open_author_profile": "Open Author's Profile", + "open_reblogger_profile": "Open Reblogger's Profile", + "reply_status": "Reply to Post", + "toggle_reblog": "Toggle Reblog on Post", + "toggle_favorite": "Toggle Favorite on Post", + "toggle_content_warning": "Toggle Content Warning", + "preview_image": "Preview Image" + }, + "segmented_control": { + "previous_section": "Previous Section", + "next_section": "Next Section" + } + }, + "status": { + "user_reblogged": "%s reblogged", + "user_replied_to": "Replied to %s", + "show_post": "Show Post", + "show_user_profile": "Show user profile", + "content_warning": "Content Warning", + "sensitive_content": "Sensitive Content", + "media_content_warning": "Tap anywhere to reveal", + "tap_to_reveal": "Tap to reveal", + "poll": { + "vote": "Vote", + "closed": "Closed" + }, + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hashtag: %s", + "mention": "Show Profile: %s", + "email": "Email address: %s" + }, + "actions": { + "reply": "Reply", + "reblog": "Reblog", + "unreblog": "Undo reblog", + "favorite": "Favorite", + "unfavorite": "Unfavorite", + "menu": "Menu", + "hide": "Hide", + "show_image": "Show image", + "show_gif": "Show GIF", + "show_video_player": "Show video player", + "tap_then_hold_to_show_menu": "Tap then hold to show menu" + }, + "tag": { + "url": "URL", + "mention": "Mention", + "link": "Link", + "hashtag": "Hashtag", + "email": "Email", + "emoji": "Emoji" + }, + "visibility": { + "unlisted": "Everyone can see this post but not display in the public timeline.", + "private": "Only their followers can see this post.", + "private_from_me": "Only my followers can see this post.", + "direct": "Only mentioned user can see this post." + } + }, + "friendship": { + "follow": "Follow", + "following": "Following", + "request": "Request", + "pending": "Pending", + "block": "Block", + "block_user": "Block %s", + "block_domain": "Block %s", + "unblock": "Unblock", + "unblock_user": "Unblock %s", + "blocked": "Blocked", + "mute": "Mute", + "mute_user": "Mute %s", + "unmute": "Unmute", + "unmute_user": "Unmute %s", + "muted": "Muted", + "edit_info": "Edit Info", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" + }, + "timeline": { + "filtered": "Filtered", + "timestamp": { + "now": "Now" + }, + "loader": { + "load_missing_posts": "Load missing posts", + "loading_missing_posts": "Loading missing posts...", + "show_more_replies": "Show more replies" + }, + "header": { + "no_status_found": "No Post Found", + "blocking_warning": "You can’t view this user's profile\nuntil you unblock them.\nYour profile looks like this to them.", + "user_blocking_warning": "You can’t view %s’s profile\nuntil you unblock them.\nYour profile looks like this to them.", + "blocked_warning": "You can’t view this user’s profile\nuntil they unblock you.", + "user_blocked_warning": "You can’t view %s’s profile\nuntil they unblock you.", + "suspended_warning": "This user has been suspended.", + "user_suspended_warning": "%s’s account has been suspended." + } + } + } + }, + "scene": { + "welcome": { + "slogan": "Social networking\nback in your hands.", + "get_started": "Get Started", + "log_in": "Log In" + }, + "server_picker": { + "title": "Mastodon is made of users in different servers.", + "subtitle": "Pick a server based on your interests, region, or a general purpose one.", + "subtitle_extend": "Pick a server based on your interests, region, or a general purpose one. Each server is operated by an entirely independent organization or individual.", + "button": { + "category": { + "all": "All", + "all_accessiblity_description": "Category: All", + "academia": "academia", + "activism": "activism", + "food": "food", + "furry": "furry", + "games": "games", + "general": "general", + "journalism": "journalism", + "lgbt": "lgbt", + "regional": "regional", + "art": "art", + "music": "music", + "tech": "tech" + }, + "see_less": "See Less", + "see_more": "See More" + }, + "label": { + "language": "LANGUAGE", + "users": "USERS", + "category": "CATEGORY" + }, + "input": { + "placeholder": "Search servers", + "search_servers_or_enter_url": "Search servers or enter URL" + }, + "empty_state": { + "finding_servers": "Finding available servers...", + "bad_network": "Something went wrong while loading the data. Check your internet connection.", + "no_results": "No results" + } + }, + "register": { + "title": "Let’s get you set up on %s", + "lets_get_you_set_up_on_domain": "Let’s get you set up on %s", + "input": { + "avatar": { + "delete": "Delete" + }, + "username": { + "placeholder": "username", + "duplicate_prompt": "This username is taken." + }, + "display_name": { + "placeholder": "display name" + }, + "email": { + "placeholder": "email" + }, + "password": { + "placeholder": "password", + "require": "Your password needs at least:", + "character_limit": "8 characters", + "accessibility": { + "checked": "checked", + "unchecked": "unchecked" + }, + "hint": "Your password needs at least eight characters" + }, + "invite": { + "registration_user_invite_request": "Why do you want to join?" + } + }, + "error": { + "item": { + "username": "Username", + "email": "Email", + "password": "Password", + "agreement": "Agreement", + "locale": "Locale", + "reason": "Reason" + }, + "reason": { + "blocked": "%s contains a disallowed email provider", + "unreachable": "%s does not seem to exist", + "taken": "%s is already in use", + "reserved": "%s is a reserved keyword", + "accepted": "%s must be accepted", + "blank": "%s is required", + "invalid": "%s is invalid", + "too_long": "%s is too long", + "too_short": "%s is too short", + "inclusion": "%s is not a supported value" + }, + "special": { + "username_invalid": "Username must only contain alphanumeric characters and underscores", + "username_too_long": "Username is too long (can’t be longer than 30 characters)", + "email_invalid": "This is not a valid email address", + "password_too_short": "Password is too short (must be at least 8 characters)" + } + } + }, + "server_rules": { + "title": "Some ground rules.", + "subtitle": "These are set and enforced by the %s moderators.", + "prompt": "By continuing, you’re subject to the terms of service and privacy policy for %s.", + "terms_of_service": "terms of service", + "privacy_policy": "privacy policy", + "button": { + "confirm": "I Agree" + } + }, + "confirm_email": { + "title": "One last thing.", + "subtitle": "Tap the link we emailed to you to verify your account.", + "tap_the_link_we_emailed_to_you_to_verify_your_account": "Tap the link we emailed to you to verify your account", + "button": { + "open_email_app": "Open Email App", + "resend": "Resend" + }, + "dont_receive_email": { + "title": "Check your email", + "description": "Check if your email address is correct as well as your junk folder if you haven’t.", + "resend_email": "Resend Email" + }, + "open_email_app": { + "title": "Check your inbox.", + "description": "We just sent you an email. Check your junk folder if you haven’t.", + "mail": "Mail", + "open_email_client": "Open Email Client" + } + }, + "home_timeline": { + "title": "Home", + "navigation_bar_state": { + "offline": "Offline", + "new_posts": "See new posts", + "published": "Published!", + "Publishing": "Publishing post...", + "accessibility": { + "logo_label": "Logo Button", + "logo_hint": "Tap to scroll to top and tap again to previous location" + } + } + }, + "suggestion_account": { + "title": "Find People to Follow", + "follow_explain": "When you follow someone, you’ll see their posts in your home feed." + }, + "compose": { + "title": { + "new_post": "New Post", + "new_reply": "New Reply" + }, + "media_selection": { + "camera": "Take Photo", + "photo_library": "Photo Library", + "browse": "Browse" + }, + "content_input_placeholder": "Type or paste what’s on your mind", + "compose_action": "Publish", + "replying_to_user": "replying to %s", + "attachment": { + "photo": "photo", + "video": "video", + "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", + "description_photo": "Describe the photo for the visually-impaired...", + "description_video": "Describe the video for the visually-impaired...", + "load_failed": "Load Failed", + "upload_failed": "Upload Failed", + "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "attachment_too_large": "Attachment too large", + "compressing_state": "Compressing...", + "server_processing_state": "Server Processing..." + }, + "poll": { + "duration_time": "Duration: %s", + "thirty_minutes": "30 minutes", + "one_hour": "1 Hour", + "six_hours": "6 Hours", + "one_day": "1 Day", + "three_days": "3 Days", + "seven_days": "7 Days", + "option_number": "Option %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" + }, + "content_warning": { + "placeholder": "Write an accurate warning here..." + }, + "visibility": { + "public": "Public", + "unlisted": "Unlisted", + "private": "Followers only", + "direct": "Only people I mention" + }, + "auto_complete": { + "space_to_add": "Space to add" + }, + "accessibility": { + "append_attachment": "Add Attachment", + "append_poll": "Add Poll", + "remove_poll": "Remove Poll", + "custom_emoji_picker": "Custom Emoji Picker", + "enable_content_warning": "Enable Content Warning", + "disable_content_warning": "Disable Content Warning", + "post_visibility_menu": "Post Visibility Menu", + "post_options": "Post Options", + "posting_as": "Posting as %s" + }, + "keyboard": { + "discard_post": "Discard Post", + "publish_post": "Publish Post", + "toggle_poll": "Toggle Poll", + "toggle_content_warning": "Toggle Content Warning", + "append_attachment_entry": "Add Attachment - %s", + "select_visibility_entry": "Select Visibility - %s" + } + }, + "profile": { + "header": { + "follows_you": "Follows You" + }, + "dashboard": { + "posts": "posts", + "following": "following", + "followers": "followers" + }, + "fields": { + "add_row": "Add Row", + "placeholder": { + "label": "Label", + "content": "Content" + } + }, + "segmented_control": { + "posts": "Posts", + "replies": "Replies", + "posts_and_replies": "Posts and Replies", + "media": "Media", + "about": "About" + }, + "relationship_action_alert": { + "confirm_mute_user": { + "title": "Mute Account", + "message": "Confirm to mute %s" + }, + "confirm_unmute_user": { + "title": "Unmute Account", + "message": "Confirm to unmute %s" + }, + "confirm_block_user": { + "title": "Block Account", + "message": "Confirm to block %s" + }, + "confirm_unblock_user": { + "title": "Unblock Account", + "message": "Confirm to unblock %s" + }, + "confirm_show_reblogs": { + "title": "Show Reblogs", + "message": "Confirm to show reblogs" + }, + "confirm_hide_reblogs": { + "title": "Hide Reblogs", + "message": "Confirm to hide reblogs" + } + }, + "accessibility": { + "show_avatar_image": "Show avatar image", + "edit_avatar_image": "Edit avatar image", + "show_banner_image": "Show banner image", + "double_tap_to_open_the_list": "Double tap to open the list" + } + }, + "follower": { + "title": "follower", + "footer": "Followers from other servers are not displayed." + }, + "following": { + "title": "following", + "footer": "Follows from other servers are not displayed." + }, + "familiarFollowers": { + "title": "Followers you familiar", + "followed_by_names": "Followed by %s" + }, + "favorited_by": { + "title": "Favorited By" + }, + "reblogged_by": { + "title": "Reblogged By" + }, + "search": { + "title": "Search", + "search_bar": { + "placeholder": "Search hashtags and users", + "cancel": "Cancel" + }, + "recommend": { + "button_text": "See All", + "hash_tag": { + "title": "Trending on Mastodon", + "description": "Hashtags that are getting quite a bit of attention", + "people_talking": "%s people are talking" + }, + "accounts": { + "title": "Accounts you might like", + "description": "You may like to follow these accounts", + "follow": "Follow" + } + }, + "searching": { + "segment": { + "all": "All", + "people": "People", + "hashtags": "Hashtags", + "posts": "Posts" + }, + "empty_state": { + "no_results": "No results" + }, + "recent_search": "Recent searches", + "clear": "Clear" + } + }, + "discovery": { + "tabs": { + "posts": "Posts", + "hashtags": "Hashtags", + "news": "News", + "community": "Community", + "for_you": "For You" + }, + "intro": "These are the posts gaining traction in your corner of Mastodon." + }, + "favorite": { + "title": "Your Favorites" + }, + "notification": { + "title": { + "Everything": "Everything", + "Mentions": "Mentions" + }, + "notification_description": { + "followed_you": "followed you", + "favorited_your_post": "favorited your post", + "reblogged_your_post": "reblogged your post", + "mentioned_you": "mentioned you", + "request_to_follow_you": "request to follow you", + "poll_has_ended": "poll has ended" + }, + "keyobard": { + "show_everything": "Show Everything", + "show_mentions": "Show Mentions" + }, + "follow_request": { + "accept": "Accept", + "accepted": "Accepted", + "reject": "reject", + "rejected": "Rejected" + } + }, + "thread": { + "back_title": "Post", + "title": "Post from %s" + }, + "settings": { + "title": "Settings", + "section": { + "appearance": { + "title": "Appearance", + "automatic": "Automatic", + "light": "Always Light", + "dark": "Always Dark" + }, + "look_and_feel": { + "title": "Look and Feel", + "use_system": "Use System", + "really_dark": "Really Dark", + "sorta_dark": "Sorta Dark", + "light": "Light" + }, + "notifications": { + "title": "Notifications", + "favorites": "Favorites my post", + "follows": "Follows me", + "boosts": "Reblogs my post", + "mentions": "Mentions me", + "trigger": { + "anyone": "anyone", + "follower": "a follower", + "follow": "anyone I follow", + "noone": "no one", + "title": "Notify me when" + } + }, + "preference": { + "title": "Preferences", + "true_black_dark_mode": "True black dark mode", + "disable_avatar_animation": "Disable animated avatars", + "disable_emoji_animation": "Disable animated emojis", + "using_default_browser": "Use default browser to open links", + "open_links_in_mastodon": "Open links in Mastodon" + }, + "boring_zone": { + "title": "The Boring Zone", + "account_settings": "Account Settings", + "terms": "Terms of Service", + "privacy": "Privacy Policy" + }, + "spicy_zone": { + "title": "The Spicy Zone", + "clear": "Clear Media Cache", + "signout": "Sign Out" + } + }, + "footer": { + "mastodon_description": "Mastodon is open source software. You can report issues on GitHub at %s (%s)" + }, + "keyboard": { + "close_settings_window": "Close Settings Window" + } + }, + "report": { + "title_report": "Report", + "title": "Report %s", + "step1": "Step 1 of 2", + "step2": "Step 2 of 2", + "content1": "Are there any other posts you’d like to add to the report?", + "content2": "Is there anything the moderators should know about this report?", + "report_sent_title": "Thanks for reporting, we’ll look into this.", + "send": "Send Report", + "skip_to_send": "Send without comment", + "text_placeholder": "Type or paste additional comments", + "reported": "REPORTED", + "step_one": { + "step_1_of_4": "Step 1 of 4", + "whats_wrong_with_this_post": "What's wrong with this post?", + "whats_wrong_with_this_account": "What's wrong with this account?", + "whats_wrong_with_this_username": "What's wrong with %s?", + "select_the_best_match": "Select the best match", + "i_dont_like_it": "I don’t like it", + "it_is_not_something_you_want_to_see": "It is not something you want to see", + "its_spam": "It’s spam", + "malicious_links_fake_engagement_or_repetetive_replies": "Malicious links, fake engagement, or repetetive replies", + "it_violates_server_rules": "It violates server rules", + "you_are_aware_that_it_breaks_specific_rules": "You are aware that it breaks specific rules", + "its_something_else": "It’s something else", + "the_issue_does_not_fit_into_other_categories": "The issue does not fit into other categories" + }, + "step_two": { + "step_2_of_4": "Step 2 of 4", + "which_rules_are_being_violated": "Which rules are being violated?", + "select_all_that_apply": "Select all that apply", + "i_just_don’t_like_it": "I just don’t like it" + }, + "step_three": { + "step_3_of_4": "Step 3 of 4", + "are_there_any_posts_that_back_up_this_report": "Are there any posts that back up this report?", + "select_all_that_apply": "Select all that apply" + }, + "step_four": { + "step_4_of_4": "Step 4 of 4", + "is_there_anything_else_we_should_know": "Is there anything else we should know?" + }, + "step_final": { + "dont_want_to_see_this": "Don’t want to see this?", + "when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "When you see something you don’t like on Mastodon, you can remove the person from your experience.", + "unfollow": "Unfollow", + "unfollowed": "Unfollowed", + "unfollow_user": "Unfollow %s", + "mute_user": "Mute %s", + "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "You won’t see their posts or reblogs in your home feed. They won’t know they’ve been muted.", + "block_user": "Block %s", + "they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "They will no longer be able to follow or see your posts, but they can see if they’ve been blocked.", + "while_we_review_this_you_can_take_action_against_user": "While we review this, you can take action against %s" + } + }, + "preview": { + "keyboard": { + "close_preview": "Close Preview", + "show_next": "Show Next", + "show_previous": "Show Previous" + } + }, + "account_list": { + "tab_bar_hint": "Current selected profile: %s. Double tap then hold to show account switcher", + "dismiss_account_switcher": "Dismiss Account Switcher", + "add_account": "Add Account" + }, + "wizard": { + "new_in_mastodon": "New in Mastodon", + "multiple_account_switch_intro_description": "Switch between multiple accounts by holding the profile button.", + "accessibility_hint": "Double tap to dismiss this wizard" + }, + "bookmark": { + "title": "Bookmarks" + } + } +} \ No newline at end of file diff --git a/Localization/StringsConvertor/input/Base.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/Base.lproj/ios-infoPlist.json new file mode 100644 index 000000000..c6db73de0 --- /dev/null +++ b/Localization/StringsConvertor/input/Base.lproj/ios-infoPlist.json @@ -0,0 +1,6 @@ +{ + "NSCameraUsageDescription": "Used to take photo for post status", + "NSPhotoLibraryAddUsageDescription": "Used to save photo into the Photo Library", + "NewPostShortcutItemTitle": "New Post", + "SearchShortcutItemTitle": "Search" +} diff --git a/Localization/app.json b/Localization/app.json index e06ca136c..c40c0a39e 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -385,7 +385,7 @@ "description_video": "Describe the video for the visually-impaired...", "load_failed": "Load Failed", "upload_failed": "Upload Failed", - "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", "attachment_too_large": "Attachment too large", "compressing_state": "Compressing...", "server_processing_state": "Server Processing..." diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift index fc47acdfd..53d81edb6 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift @@ -8,6 +8,9 @@ #elseif os(tvOS) || os(watchOS) import UIKit #endif +#if canImport(SwiftUI) + import SwiftUI +#endif // Deprecated typealiases @available(*, deprecated, renamed: "ColorAsset.Color", message: "This typealias will be removed in SwiftGen 7.0") @@ -279,6 +282,24 @@ public final class ColorAsset { return color }() + #if os(iOS) || os(tvOS) + @available(iOS 11.0, tvOS 11.0, *) + public func color(compatibleWith traitCollection: UITraitCollection) -> Color { + let bundle = Bundle.module + guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else { + fatalError("Unable to load color asset named \(name).") + } + return color + } + #endif + + #if canImport(SwiftUI) + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + public private(set) lazy var swiftUIColor: SwiftUI.Color = { + SwiftUI.Color(asset: self) + }() + #endif + fileprivate init(name: String) { self.name = name } @@ -298,6 +319,16 @@ public extension ColorAsset.Color { } } +#if canImport(SwiftUI) +@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) +public extension SwiftUI.Color { + init(asset: ColorAsset) { + let bundle = Bundle.module + self.init(asset.name, bundle: bundle) + } +} +#endif + public struct ImageAsset { public fileprivate(set) var name: String @@ -307,6 +338,7 @@ public struct ImageAsset { public typealias Image = UIImage #endif + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *) public var image: Image { let bundle = Bundle.module #if os(iOS) || os(tvOS) @@ -322,9 +354,28 @@ public struct ImageAsset { } return result } + + #if os(iOS) || os(tvOS) + @available(iOS 8.0, tvOS 9.0, *) + public func image(compatibleWith traitCollection: UITraitCollection) -> Image { + let bundle = Bundle.module + guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else { + fatalError("Unable to load image asset named \(name).") + } + return result + } + #endif + + #if canImport(SwiftUI) + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + public var swiftUIImage: SwiftUI.Image { + SwiftUI.Image(asset: self) + } + #endif } public extension ImageAsset.Image { + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, *) @available(macOS, deprecated, message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") convenience init?(asset: ImageAsset) { @@ -338,3 +389,23 @@ public extension ImageAsset.Image { #endif } } + +#if canImport(SwiftUI) +@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) +public extension SwiftUI.Image { + init(asset: ImageAsset) { + let bundle = Bundle.module + self.init(asset.name, bundle: bundle) + } + + init(asset: ImageAsset, label: Text) { + let bundle = Bundle.module + self.init(asset.name, bundle: bundle, label: label) + } + + init(decorative asset: ImageAsset) { + let bundle = Bundle.module + self.init(decorative: asset.name, bundle: bundle) + } +} +#endif diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Fonts.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Fonts.swift index 22c6c9ed3..740c44bc9 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Fonts.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Fonts.swift @@ -1,18 +1,20 @@ // swiftlint:disable all // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen -#if os(OSX) +#if os(macOS) import AppKit.NSFont #elseif os(iOS) || os(tvOS) || os(watchOS) import UIKit.UIFont #endif +#if canImport(SwiftUI) + import SwiftUI +#endif // Deprecated typealiases @available(*, deprecated, renamed: "FontConvertible.Font", message: "This typealias will be removed in SwiftGen 7.0") public typealias Font = FontConvertible.Font -// swiftlint:disable superfluous_disable_command -// swiftlint:disable file_length +// swiftlint:disable superfluous_disable_command file_length implicit_return // MARK: - Fonts @@ -36,7 +38,7 @@ public struct FontConvertible { public let family: String public let path: String - #if os(OSX) + #if os(macOS) public typealias Font = NSFont #elseif os(iOS) || os(tvOS) || os(watchOS) public typealias Font = UIFont @@ -49,12 +51,41 @@ public struct FontConvertible { return font } + #if canImport(SwiftUI) + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + public func swiftUIFont(size: CGFloat) -> SwiftUI.Font { + return SwiftUI.Font.custom(self, size: size) + } + + @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + public func swiftUIFont(fixedSize: CGFloat) -> SwiftUI.Font { + return SwiftUI.Font.custom(self, fixedSize: fixedSize) + } + + @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) + public func swiftUIFont(size: CGFloat, relativeTo textStyle: SwiftUI.Font.TextStyle) -> SwiftUI.Font { + return SwiftUI.Font.custom(self, size: size, relativeTo: textStyle) + } + #endif + public func register() { // swiftlint:disable:next conditional_returns_on_newline guard let url = url else { return } CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil) } + fileprivate func registerIfNeeded() { + #if os(iOS) || os(tvOS) || os(watchOS) + if !UIFont.fontNames(forFamilyName: family).contains(name) { + register() + } + #elseif os(macOS) + if let url = url, CTFontManagerGetScopeForURL(url as CFURL) == .none { + register() + } + #endif + } + fileprivate var url: URL? { // swiftlint:disable:next implicit_return return Bundle.module.url(forResource: path, withExtension: nil) @@ -63,16 +94,34 @@ public struct FontConvertible { public extension FontConvertible.Font { convenience init?(font: FontConvertible, size: CGFloat) { - #if os(iOS) || os(tvOS) || os(watchOS) - if !UIFont.fontNames(forFamilyName: font.family).contains(font.name) { - font.register() - } - #elseif os(OSX) - if let url = font.url, CTFontManagerGetScopeForURL(url as CFURL) == .none { - font.register() - } - #endif - + font.registerIfNeeded() self.init(name: font.name, size: size) } } + +#if canImport(SwiftUI) +@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) +public extension SwiftUI.Font { + static func custom(_ font: FontConvertible, size: CGFloat) -> SwiftUI.Font { + font.registerIfNeeded() + return custom(font.name, size: size) + } +} + +@available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) +public extension SwiftUI.Font { + static func custom(_ font: FontConvertible, fixedSize: CGFloat) -> SwiftUI.Font { + font.registerIfNeeded() + return custom(font.name, fixedSize: fixedSize) + } + + static func custom( + _ font: FontConvertible, + size: CGFloat, + relativeTo textStyle: SwiftUI.Font.TextStyle + ) -> SwiftUI.Font { + font.registerIfNeeded() + return custom(font.name, size: size, relativeTo: textStyle) + } +} +#endif diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index e9e213fa9..f3d69950e 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -3,1440 +3,1452 @@ import Foundation -// swiftlint:disable superfluous_disable_command file_length implicit_return +// swiftlint:disable superfluous_disable_command file_length implicit_return prefer_self_in_static_references // MARK: - Strings // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces public enum L10n { - public enum Common { public enum Alerts { public enum BlockDomain { /// Block Domain - public static let blockEntireDomain = L10n.tr("Localizable", "Common.Alerts.BlockDomain.BlockEntireDomain") + public static let blockEntireDomain = L10n.tr("Localizable", "Common.Alerts.BlockDomain.BlockEntireDomain", fallback: "Block Domain") /// Are you really, really sure you want to block the entire %@? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain and any of your followers from that domain will be removed. public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.BlockDomain.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.BlockDomain.Title", String(describing: p1), fallback: "Are you really, really sure you want to block the entire %@? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain and any of your followers from that domain will be removed.") } } public enum CleanCache { /// Successfully cleaned %@ cache. public static func message(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.CleanCache.Message", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.CleanCache.Message", String(describing: p1), fallback: "Successfully cleaned %@ cache.") } /// Clean Cache - public static let title = L10n.tr("Localizable", "Common.Alerts.CleanCache.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.CleanCache.Title", fallback: "Clean Cache") } public enum Common { /// Please try again. - public static let pleaseTryAgain = L10n.tr("Localizable", "Common.Alerts.Common.PleaseTryAgain") + public static let pleaseTryAgain = L10n.tr("Localizable", "Common.Alerts.Common.PleaseTryAgain", fallback: "Please try again.") /// Please try again later. - public static let pleaseTryAgainLater = L10n.tr("Localizable", "Common.Alerts.Common.PleaseTryAgainLater") + public static let pleaseTryAgainLater = L10n.tr("Localizable", "Common.Alerts.Common.PleaseTryAgainLater", fallback: "Please try again later.") } public enum DeletePost { /// Are you sure you want to delete this post? - public static let message = L10n.tr("Localizable", "Common.Alerts.DeletePost.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.DeletePost.Message", fallback: "Are you sure you want to delete this post?") /// Delete Post - public static let title = L10n.tr("Localizable", "Common.Alerts.DeletePost.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.DeletePost.Title", fallback: "Delete Post") } public enum DiscardPostContent { /// Confirm to discard composed post content. - public static let message = L10n.tr("Localizable", "Common.Alerts.DiscardPostContent.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.DiscardPostContent.Message", fallback: "Confirm to discard composed post content.") /// Discard Draft - public static let title = L10n.tr("Localizable", "Common.Alerts.DiscardPostContent.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.DiscardPostContent.Title", fallback: "Discard Draft") } public enum EditProfileFailure { /// Cannot edit profile. Please try again. - public static let message = L10n.tr("Localizable", "Common.Alerts.EditProfileFailure.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.EditProfileFailure.Message", fallback: "Cannot edit profile. Please try again.") /// Edit Profile Error - public static let title = L10n.tr("Localizable", "Common.Alerts.EditProfileFailure.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.EditProfileFailure.Title", fallback: "Edit Profile Error") } public enum PublishPostFailure { - /// Failed to publish the post.\nPlease check your internet connection. - public static let message = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Message") + /// Failed to publish the post. + /// Please check your internet connection. + public static let message = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Message", fallback: "Failed to publish the post.\nPlease check your internet connection.") /// Publish Failure - public static let title = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Title", fallback: "Publish Failure") public enum AttachmentsMessage { /// Cannot attach more than one video. - public static let moreThanOneVideo = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.AttachmentsMessage.MoreThanOneVideo") + public static let moreThanOneVideo = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.AttachmentsMessage.MoreThanOneVideo", fallback: "Cannot attach more than one video.") /// Cannot attach a video to a post that already contains images. - public static let videoAttachWithPhoto = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.AttachmentsMessage.VideoAttachWithPhoto") + public static let videoAttachWithPhoto = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.AttachmentsMessage.VideoAttachWithPhoto", fallback: "Cannot attach a video to a post that already contains images.") } } public enum SavePhotoFailure { /// Please enable the photo library access permission to save the photo. - public static let message = L10n.tr("Localizable", "Common.Alerts.SavePhotoFailure.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.SavePhotoFailure.Message", fallback: "Please enable the photo library access permission to save the photo.") /// Save Photo Failure - public static let title = L10n.tr("Localizable", "Common.Alerts.SavePhotoFailure.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.SavePhotoFailure.Title", fallback: "Save Photo Failure") } public enum ServerError { /// Server Error - public static let title = L10n.tr("Localizable", "Common.Alerts.ServerError.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.ServerError.Title", fallback: "Server Error") } public enum SignOut { /// Sign Out - public static let confirm = L10n.tr("Localizable", "Common.Alerts.SignOut.Confirm") + public static let confirm = L10n.tr("Localizable", "Common.Alerts.SignOut.Confirm", fallback: "Sign Out") /// Are you sure you want to sign out? - public static let message = L10n.tr("Localizable", "Common.Alerts.SignOut.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.SignOut.Message", fallback: "Are you sure you want to sign out?") /// Sign Out - public static let title = L10n.tr("Localizable", "Common.Alerts.SignOut.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.SignOut.Title", fallback: "Sign Out") } public enum SignUpFailure { /// Sign Up Failure - public static let title = L10n.tr("Localizable", "Common.Alerts.SignUpFailure.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.SignUpFailure.Title", fallback: "Sign Up Failure") } public enum VoteFailure { /// The poll has ended - public static let pollEnded = L10n.tr("Localizable", "Common.Alerts.VoteFailure.PollEnded") + public static let pollEnded = L10n.tr("Localizable", "Common.Alerts.VoteFailure.PollEnded", fallback: "The poll has ended") /// Vote Failure - public static let title = L10n.tr("Localizable", "Common.Alerts.VoteFailure.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.VoteFailure.Title", fallback: "Vote Failure") } } public enum Controls { public enum Actions { /// Add - public static let add = L10n.tr("Localizable", "Common.Controls.Actions.Add") + public static let add = L10n.tr("Localizable", "Common.Controls.Actions.Add", fallback: "Add") /// Back - public static let back = L10n.tr("Localizable", "Common.Controls.Actions.Back") + public static let back = L10n.tr("Localizable", "Common.Controls.Actions.Back", fallback: "Back") /// Block %@ public static func blockDomain(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Actions.BlockDomain", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Actions.BlockDomain", String(describing: p1), fallback: "Block %@") } /// Cancel - public static let cancel = L10n.tr("Localizable", "Common.Controls.Actions.Cancel") + public static let cancel = L10n.tr("Localizable", "Common.Controls.Actions.Cancel", fallback: "Cancel") /// Compose - public static let compose = L10n.tr("Localizable", "Common.Controls.Actions.Compose") + public static let compose = L10n.tr("Localizable", "Common.Controls.Actions.Compose", fallback: "Compose") /// Confirm - public static let confirm = L10n.tr("Localizable", "Common.Controls.Actions.Confirm") + public static let confirm = L10n.tr("Localizable", "Common.Controls.Actions.Confirm", fallback: "Confirm") /// Continue - public static let `continue` = L10n.tr("Localizable", "Common.Controls.Actions.Continue") + public static let `continue` = L10n.tr("Localizable", "Common.Controls.Actions.Continue", fallback: "Continue") /// Copy Photo - public static let copyPhoto = L10n.tr("Localizable", "Common.Controls.Actions.CopyPhoto") + public static let copyPhoto = L10n.tr("Localizable", "Common.Controls.Actions.CopyPhoto", fallback: "Copy Photo") /// Delete - public static let delete = L10n.tr("Localizable", "Common.Controls.Actions.Delete") + public static let delete = L10n.tr("Localizable", "Common.Controls.Actions.Delete", fallback: "Delete") /// Discard - public static let discard = L10n.tr("Localizable", "Common.Controls.Actions.Discard") + public static let discard = L10n.tr("Localizable", "Common.Controls.Actions.Discard", fallback: "Discard") /// Done - public static let done = L10n.tr("Localizable", "Common.Controls.Actions.Done") + public static let done = L10n.tr("Localizable", "Common.Controls.Actions.Done", fallback: "Done") /// Edit - public static let edit = L10n.tr("Localizable", "Common.Controls.Actions.Edit") + public static let edit = L10n.tr("Localizable", "Common.Controls.Actions.Edit", fallback: "Edit") /// Find people to follow - public static let findPeople = L10n.tr("Localizable", "Common.Controls.Actions.FindPeople") + public static let findPeople = L10n.tr("Localizable", "Common.Controls.Actions.FindPeople", fallback: "Find people to follow") /// Manually search instead - public static let manuallySearch = L10n.tr("Localizable", "Common.Controls.Actions.ManuallySearch") + public static let manuallySearch = L10n.tr("Localizable", "Common.Controls.Actions.ManuallySearch", fallback: "Manually search instead") /// Next - public static let next = L10n.tr("Localizable", "Common.Controls.Actions.Next") + public static let next = L10n.tr("Localizable", "Common.Controls.Actions.Next", fallback: "Next") /// OK - public static let ok = L10n.tr("Localizable", "Common.Controls.Actions.Ok") + public static let ok = L10n.tr("Localizable", "Common.Controls.Actions.Ok", fallback: "OK") /// Open - public static let `open` = L10n.tr("Localizable", "Common.Controls.Actions.Open") + public static let `open` = L10n.tr("Localizable", "Common.Controls.Actions.Open", fallback: "Open") /// Open in Browser - public static let openInBrowser = L10n.tr("Localizable", "Common.Controls.Actions.OpenInBrowser") + public static let openInBrowser = L10n.tr("Localizable", "Common.Controls.Actions.OpenInBrowser", fallback: "Open in Browser") /// Open in Safari - public static let openInSafari = L10n.tr("Localizable", "Common.Controls.Actions.OpenInSafari") + public static let openInSafari = L10n.tr("Localizable", "Common.Controls.Actions.OpenInSafari", fallback: "Open in Safari") /// Preview - public static let preview = L10n.tr("Localizable", "Common.Controls.Actions.Preview") + public static let preview = L10n.tr("Localizable", "Common.Controls.Actions.Preview", fallback: "Preview") /// Previous - public static let previous = L10n.tr("Localizable", "Common.Controls.Actions.Previous") + public static let previous = L10n.tr("Localizable", "Common.Controls.Actions.Previous", fallback: "Previous") /// Remove - public static let remove = L10n.tr("Localizable", "Common.Controls.Actions.Remove") + public static let remove = L10n.tr("Localizable", "Common.Controls.Actions.Remove", fallback: "Remove") /// Reply - public static let reply = L10n.tr("Localizable", "Common.Controls.Actions.Reply") + public static let reply = L10n.tr("Localizable", "Common.Controls.Actions.Reply", fallback: "Reply") /// Report %@ public static func reportUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Actions.ReportUser", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Actions.ReportUser", String(describing: p1), fallback: "Report %@") } /// Save - public static let save = L10n.tr("Localizable", "Common.Controls.Actions.Save") + public static let save = L10n.tr("Localizable", "Common.Controls.Actions.Save", fallback: "Save") /// Save Photo - public static let savePhoto = L10n.tr("Localizable", "Common.Controls.Actions.SavePhoto") + public static let savePhoto = L10n.tr("Localizable", "Common.Controls.Actions.SavePhoto", fallback: "Save Photo") /// See More - public static let seeMore = L10n.tr("Localizable", "Common.Controls.Actions.SeeMore") + public static let seeMore = L10n.tr("Localizable", "Common.Controls.Actions.SeeMore", fallback: "See More") /// Settings - public static let settings = L10n.tr("Localizable", "Common.Controls.Actions.Settings") + public static let settings = L10n.tr("Localizable", "Common.Controls.Actions.Settings", fallback: "Settings") /// Share - public static let share = L10n.tr("Localizable", "Common.Controls.Actions.Share") + public static let share = L10n.tr("Localizable", "Common.Controls.Actions.Share", fallback: "Share") /// Share Post - public static let sharePost = L10n.tr("Localizable", "Common.Controls.Actions.SharePost") + public static let sharePost = L10n.tr("Localizable", "Common.Controls.Actions.SharePost", fallback: "Share Post") /// Share %@ public static func shareUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Actions.ShareUser", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Actions.ShareUser", String(describing: p1), fallback: "Share %@") } /// Sign In - public static let signIn = L10n.tr("Localizable", "Common.Controls.Actions.SignIn") + public static let signIn = L10n.tr("Localizable", "Common.Controls.Actions.SignIn", fallback: "Sign In") /// Sign Up - public static let signUp = L10n.tr("Localizable", "Common.Controls.Actions.SignUp") + public static let signUp = L10n.tr("Localizable", "Common.Controls.Actions.SignUp", fallback: "Sign Up") /// Skip - public static let skip = L10n.tr("Localizable", "Common.Controls.Actions.Skip") + public static let skip = L10n.tr("Localizable", "Common.Controls.Actions.Skip", fallback: "Skip") /// Take Photo - public static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto") + public static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto", fallback: "Take Photo") /// Try Again - public static let tryAgain = L10n.tr("Localizable", "Common.Controls.Actions.TryAgain") + public static let tryAgain = L10n.tr("Localizable", "Common.Controls.Actions.TryAgain", fallback: "Try Again") /// Unblock %@ public static func unblockDomain(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Actions.UnblockDomain", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Actions.UnblockDomain", String(describing: p1), fallback: "Unblock %@") } } public enum Friendship { /// Block - public static let block = L10n.tr("Localizable", "Common.Controls.Friendship.Block") + public static let block = L10n.tr("Localizable", "Common.Controls.Friendship.Block", fallback: "Block") /// Block %@ public static func blockDomain(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Friendship.BlockDomain", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Friendship.BlockDomain", String(describing: p1), fallback: "Block %@") } /// Blocked - public static let blocked = L10n.tr("Localizable", "Common.Controls.Friendship.Blocked") + public static let blocked = L10n.tr("Localizable", "Common.Controls.Friendship.Blocked", fallback: "Blocked") /// Block %@ public static func blockUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Friendship.BlockUser", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Friendship.BlockUser", String(describing: p1), fallback: "Block %@") } /// Edit Info - public static let editInfo = L10n.tr("Localizable", "Common.Controls.Friendship.EditInfo") + public static let editInfo = L10n.tr("Localizable", "Common.Controls.Friendship.EditInfo", fallback: "Edit Info") /// Follow - public static let follow = L10n.tr("Localizable", "Common.Controls.Friendship.Follow") + public static let follow = L10n.tr("Localizable", "Common.Controls.Friendship.Follow", fallback: "Follow") /// Following - public static let following = L10n.tr("Localizable", "Common.Controls.Friendship.Following") + public static let following = L10n.tr("Localizable", "Common.Controls.Friendship.Following", fallback: "Following") /// Hide Reblogs - public static let hideReblogs = L10n.tr("Localizable", "Common.Controls.Friendship.HideReblogs") + public static let hideReblogs = L10n.tr("Localizable", "Common.Controls.Friendship.HideReblogs", fallback: "Hide Reblogs") /// Mute - public static let mute = L10n.tr("Localizable", "Common.Controls.Friendship.Mute") + public static let mute = L10n.tr("Localizable", "Common.Controls.Friendship.Mute", fallback: "Mute") /// Muted - public static let muted = L10n.tr("Localizable", "Common.Controls.Friendship.Muted") + public static let muted = L10n.tr("Localizable", "Common.Controls.Friendship.Muted", fallback: "Muted") /// Mute %@ public static func muteUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Friendship.MuteUser", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Friendship.MuteUser", String(describing: p1), fallback: "Mute %@") } /// Pending - public static let pending = L10n.tr("Localizable", "Common.Controls.Friendship.Pending") + public static let pending = L10n.tr("Localizable", "Common.Controls.Friendship.Pending", fallback: "Pending") /// Request - public static let request = L10n.tr("Localizable", "Common.Controls.Friendship.Request") + public static let request = L10n.tr("Localizable", "Common.Controls.Friendship.Request", fallback: "Request") /// Show Reblogs - public static let showReblogs = L10n.tr("Localizable", "Common.Controls.Friendship.ShowReblogs") + public static let showReblogs = L10n.tr("Localizable", "Common.Controls.Friendship.ShowReblogs", fallback: "Show Reblogs") /// Unblock - public static let unblock = L10n.tr("Localizable", "Common.Controls.Friendship.Unblock") + public static let unblock = L10n.tr("Localizable", "Common.Controls.Friendship.Unblock", fallback: "Unblock") /// Unblock %@ public static func unblockUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Friendship.UnblockUser", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Friendship.UnblockUser", String(describing: p1), fallback: "Unblock %@") } /// Unmute - public static let unmute = L10n.tr("Localizable", "Common.Controls.Friendship.Unmute") + public static let unmute = L10n.tr("Localizable", "Common.Controls.Friendship.Unmute", fallback: "Unmute") /// Unmute %@ public static func unmuteUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Friendship.UnmuteUser", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Friendship.UnmuteUser", String(describing: p1), fallback: "Unmute %@") } } public enum Keyboard { public enum Common { /// Compose New Post - public static let composeNewPost = L10n.tr("Localizable", "Common.Controls.Keyboard.Common.ComposeNewPost") + public static let composeNewPost = L10n.tr("Localizable", "Common.Controls.Keyboard.Common.ComposeNewPost", fallback: "Compose New Post") /// Open Settings - public static let openSettings = L10n.tr("Localizable", "Common.Controls.Keyboard.Common.OpenSettings") + public static let openSettings = L10n.tr("Localizable", "Common.Controls.Keyboard.Common.OpenSettings", fallback: "Open Settings") /// Show Favorites - public static let showFavorites = L10n.tr("Localizable", "Common.Controls.Keyboard.Common.ShowFavorites") + public static let showFavorites = L10n.tr("Localizable", "Common.Controls.Keyboard.Common.ShowFavorites", fallback: "Show Favorites") /// Switch to %@ public static func switchToTab(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Keyboard.Common.SwitchToTab", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Keyboard.Common.SwitchToTab", String(describing: p1), fallback: "Switch to %@") } } public enum SegmentedControl { /// Next Section - public static let nextSection = L10n.tr("Localizable", "Common.Controls.Keyboard.SegmentedControl.NextSection") + public static let nextSection = L10n.tr("Localizable", "Common.Controls.Keyboard.SegmentedControl.NextSection", fallback: "Next Section") /// Previous Section - public static let previousSection = L10n.tr("Localizable", "Common.Controls.Keyboard.SegmentedControl.PreviousSection") + public static let previousSection = L10n.tr("Localizable", "Common.Controls.Keyboard.SegmentedControl.PreviousSection", fallback: "Previous Section") } public enum Timeline { /// Next Post - public static let nextStatus = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.NextStatus") + public static let nextStatus = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.NextStatus", fallback: "Next Post") /// Open Author's Profile - public static let openAuthorProfile = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.OpenAuthorProfile") + public static let openAuthorProfile = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.OpenAuthorProfile", fallback: "Open Author's Profile") /// Open Reblogger's Profile - public static let openRebloggerProfile = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.OpenRebloggerProfile") + public static let openRebloggerProfile = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.OpenRebloggerProfile", fallback: "Open Reblogger's Profile") /// Open Post - public static let openStatus = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.OpenStatus") + public static let openStatus = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.OpenStatus", fallback: "Open Post") /// Preview Image - public static let previewImage = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.PreviewImage") + public static let previewImage = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.PreviewImage", fallback: "Preview Image") /// Previous Post - public static let previousStatus = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.PreviousStatus") + public static let previousStatus = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.PreviousStatus", fallback: "Previous Post") /// Reply to Post - public static let replyStatus = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.ReplyStatus") + public static let replyStatus = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.ReplyStatus", fallback: "Reply to Post") /// Toggle Content Warning - public static let toggleContentWarning = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.ToggleContentWarning") + public static let toggleContentWarning = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.ToggleContentWarning", fallback: "Toggle Content Warning") /// Toggle Favorite on Post - public static let toggleFavorite = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.ToggleFavorite") + public static let toggleFavorite = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.ToggleFavorite", fallback: "Toggle Favorite on Post") /// Toggle Reblog on Post - public static let toggleReblog = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.ToggleReblog") + public static let toggleReblog = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.ToggleReblog", fallback: "Toggle Reblog on Post") } } public enum Status { /// Content Warning - public static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning") + public static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning", fallback: "Content Warning") /// Tap anywhere to reveal - public static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning") + public static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning", fallback: "Tap anywhere to reveal") /// Sensitive Content - public static let sensitiveContent = L10n.tr("Localizable", "Common.Controls.Status.SensitiveContent") + public static let sensitiveContent = L10n.tr("Localizable", "Common.Controls.Status.SensitiveContent", fallback: "Sensitive Content") /// Show Post - public static let showPost = L10n.tr("Localizable", "Common.Controls.Status.ShowPost") + public static let showPost = L10n.tr("Localizable", "Common.Controls.Status.ShowPost", fallback: "Show Post") /// Show user profile - public static let showUserProfile = L10n.tr("Localizable", "Common.Controls.Status.ShowUserProfile") + public static let showUserProfile = L10n.tr("Localizable", "Common.Controls.Status.ShowUserProfile", fallback: "Show user profile") /// Tap to reveal - public static let tapToReveal = L10n.tr("Localizable", "Common.Controls.Status.TapToReveal") + public static let tapToReveal = L10n.tr("Localizable", "Common.Controls.Status.TapToReveal", fallback: "Tap to reveal") /// %@ reblogged public static func userReblogged(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Status.UserReblogged", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Status.UserReblogged", String(describing: p1), fallback: "%@ reblogged") } /// Replied to %@ public static func userRepliedTo(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Status.UserRepliedTo", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Status.UserRepliedTo", String(describing: p1), fallback: "Replied to %@") } public enum Actions { /// Favorite - public static let favorite = L10n.tr("Localizable", "Common.Controls.Status.Actions.Favorite") + public static let favorite = L10n.tr("Localizable", "Common.Controls.Status.Actions.Favorite", fallback: "Favorite") /// Hide - public static let hide = L10n.tr("Localizable", "Common.Controls.Status.Actions.Hide") + public static let hide = L10n.tr("Localizable", "Common.Controls.Status.Actions.Hide", fallback: "Hide") /// Menu - public static let menu = L10n.tr("Localizable", "Common.Controls.Status.Actions.Menu") + public static let menu = L10n.tr("Localizable", "Common.Controls.Status.Actions.Menu", fallback: "Menu") /// Reblog - public static let reblog = L10n.tr("Localizable", "Common.Controls.Status.Actions.Reblog") + public static let reblog = L10n.tr("Localizable", "Common.Controls.Status.Actions.Reblog", fallback: "Reblog") /// Reply - public static let reply = L10n.tr("Localizable", "Common.Controls.Status.Actions.Reply") + public static let reply = L10n.tr("Localizable", "Common.Controls.Status.Actions.Reply", fallback: "Reply") /// Show GIF - public static let showGif = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShowGif") + public static let showGif = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShowGif", fallback: "Show GIF") /// Show image - public static let showImage = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShowImage") + public static let showImage = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShowImage", fallback: "Show image") /// Show video player - public static let showVideoPlayer = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShowVideoPlayer") + public static let showVideoPlayer = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShowVideoPlayer", fallback: "Show video player") /// Tap then hold to show menu - public static let tapThenHoldToShowMenu = L10n.tr("Localizable", "Common.Controls.Status.Actions.TapThenHoldToShowMenu") + public static let tapThenHoldToShowMenu = L10n.tr("Localizable", "Common.Controls.Status.Actions.TapThenHoldToShowMenu", fallback: "Tap then hold to show menu") /// Unfavorite - public static let unfavorite = L10n.tr("Localizable", "Common.Controls.Status.Actions.Unfavorite") + public static let unfavorite = L10n.tr("Localizable", "Common.Controls.Status.Actions.Unfavorite", fallback: "Unfavorite") /// Undo reblog - public static let unreblog = L10n.tr("Localizable", "Common.Controls.Status.Actions.Unreblog") + public static let unreblog = L10n.tr("Localizable", "Common.Controls.Status.Actions.Unreblog", fallback: "Undo reblog") } public enum MetaEntity { /// Email address: %@ public static func email(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Status.MetaEntity.Email", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Status.MetaEntity.Email", String(describing: p1), fallback: "Email address: %@") } /// Hashtag: %@ public static func hashtag(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Status.MetaEntity.Hashtag", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Status.MetaEntity.Hashtag", String(describing: p1), fallback: "Hashtag: %@") } /// Show Profile: %@ public static func mention(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Status.MetaEntity.Mention", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Status.MetaEntity.Mention", String(describing: p1), fallback: "Show Profile: %@") } /// Link: %@ public static func url(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Status.MetaEntity.Url", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Status.MetaEntity.Url", String(describing: p1), fallback: "Link: %@") } } public enum Poll { /// Closed - public static let closed = L10n.tr("Localizable", "Common.Controls.Status.Poll.Closed") + public static let closed = L10n.tr("Localizable", "Common.Controls.Status.Poll.Closed", fallback: "Closed") /// Vote - public static let vote = L10n.tr("Localizable", "Common.Controls.Status.Poll.Vote") + public static let vote = L10n.tr("Localizable", "Common.Controls.Status.Poll.Vote", fallback: "Vote") } public enum Tag { /// Email - public static let email = L10n.tr("Localizable", "Common.Controls.Status.Tag.Email") + public static let email = L10n.tr("Localizable", "Common.Controls.Status.Tag.Email", fallback: "Email") /// Emoji - public static let emoji = L10n.tr("Localizable", "Common.Controls.Status.Tag.Emoji") + public static let emoji = L10n.tr("Localizable", "Common.Controls.Status.Tag.Emoji", fallback: "Emoji") /// Hashtag - public static let hashtag = L10n.tr("Localizable", "Common.Controls.Status.Tag.Hashtag") + public static let hashtag = L10n.tr("Localizable", "Common.Controls.Status.Tag.Hashtag", fallback: "Hashtag") /// Link - public static let link = L10n.tr("Localizable", "Common.Controls.Status.Tag.Link") + public static let link = L10n.tr("Localizable", "Common.Controls.Status.Tag.Link", fallback: "Link") /// Mention - public static let mention = L10n.tr("Localizable", "Common.Controls.Status.Tag.Mention") + public static let mention = L10n.tr("Localizable", "Common.Controls.Status.Tag.Mention", fallback: "Mention") /// URL - public static let url = L10n.tr("Localizable", "Common.Controls.Status.Tag.Url") + public static let url = L10n.tr("Localizable", "Common.Controls.Status.Tag.Url", fallback: "URL") } public enum Visibility { /// Only mentioned user can see this post. - public static let direct = L10n.tr("Localizable", "Common.Controls.Status.Visibility.Direct") + public static let direct = L10n.tr("Localizable", "Common.Controls.Status.Visibility.Direct", fallback: "Only mentioned user can see this post.") /// Only their followers can see this post. - public static let `private` = L10n.tr("Localizable", "Common.Controls.Status.Visibility.Private") + public static let `private` = L10n.tr("Localizable", "Common.Controls.Status.Visibility.Private", fallback: "Only their followers can see this post.") /// Only my followers can see this post. - public static let privateFromMe = L10n.tr("Localizable", "Common.Controls.Status.Visibility.PrivateFromMe") + public static let privateFromMe = L10n.tr("Localizable", "Common.Controls.Status.Visibility.PrivateFromMe", fallback: "Only my followers can see this post.") /// Everyone can see this post but not display in the public timeline. - public static let unlisted = L10n.tr("Localizable", "Common.Controls.Status.Visibility.Unlisted") + public static let unlisted = L10n.tr("Localizable", "Common.Controls.Status.Visibility.Unlisted", fallback: "Everyone can see this post but not display in the public timeline.") } } public enum Tabs { /// Home - public static let home = L10n.tr("Localizable", "Common.Controls.Tabs.Home") + public static let home = L10n.tr("Localizable", "Common.Controls.Tabs.Home", fallback: "Home") /// Notification - public static let notification = L10n.tr("Localizable", "Common.Controls.Tabs.Notification") + public static let notification = L10n.tr("Localizable", "Common.Controls.Tabs.Notification", fallback: "Notification") /// Profile - public static let profile = L10n.tr("Localizable", "Common.Controls.Tabs.Profile") + public static let profile = L10n.tr("Localizable", "Common.Controls.Tabs.Profile", fallback: "Profile") /// Search - public static let search = L10n.tr("Localizable", "Common.Controls.Tabs.Search") + public static let search = L10n.tr("Localizable", "Common.Controls.Tabs.Search", fallback: "Search") } public enum Timeline { /// Filtered - public static let filtered = L10n.tr("Localizable", "Common.Controls.Timeline.Filtered") + public static let filtered = L10n.tr("Localizable", "Common.Controls.Timeline.Filtered", fallback: "Filtered") public enum Header { - /// You can’t view this user’s profile\nuntil they unblock you. - public static let blockedWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.BlockedWarning") - /// You can’t view this user's profile\nuntil you unblock them.\nYour profile looks like this to them. - public static let blockingWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.BlockingWarning") + /// You can’t view this user’s profile + /// until they unblock you. + public static let blockedWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.BlockedWarning", fallback: "You can’t view this user’s profile\nuntil they unblock you.") + /// You can’t view this user's profile + /// until you unblock them. + /// Your profile looks like this to them. + public static let blockingWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.BlockingWarning", fallback: "You can’t view this user's profile\nuntil you unblock them.\nYour profile looks like this to them.") /// No Post Found - public static let noStatusFound = L10n.tr("Localizable", "Common.Controls.Timeline.Header.NoStatusFound") + public static let noStatusFound = L10n.tr("Localizable", "Common.Controls.Timeline.Header.NoStatusFound", fallback: "No Post Found") /// This user has been suspended. - public static let suspendedWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.SuspendedWarning") - /// You can’t view %@’s profile\nuntil they unblock you. + public static let suspendedWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.SuspendedWarning", fallback: "This user has been suspended.") + /// You can’t view %@’s profile + /// until they unblock you. public static func userBlockedWarning(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Timeline.Header.UserBlockedWarning", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Timeline.Header.UserBlockedWarning", String(describing: p1), fallback: "You can’t view %@’s profile\nuntil they unblock you.") } - /// You can’t view %@’s profile\nuntil you unblock them.\nYour profile looks like this to them. + /// You can’t view %@’s profile + /// until you unblock them. + /// Your profile looks like this to them. public static func userBlockingWarning(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Timeline.Header.UserBlockingWarning", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Timeline.Header.UserBlockingWarning", String(describing: p1), fallback: "You can’t view %@’s profile\nuntil you unblock them.\nYour profile looks like this to them.") } /// %@’s account has been suspended. public static func userSuspendedWarning(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Timeline.Header.UserSuspendedWarning", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Timeline.Header.UserSuspendedWarning", String(describing: p1), fallback: "%@’s account has been suspended.") } } public enum Loader { /// Loading missing posts... - public static let loadingMissingPosts = L10n.tr("Localizable", "Common.Controls.Timeline.Loader.LoadingMissingPosts") + public static let loadingMissingPosts = L10n.tr("Localizable", "Common.Controls.Timeline.Loader.LoadingMissingPosts", fallback: "Loading missing posts...") /// Load missing posts - public static let loadMissingPosts = L10n.tr("Localizable", "Common.Controls.Timeline.Loader.LoadMissingPosts") + public static let loadMissingPosts = L10n.tr("Localizable", "Common.Controls.Timeline.Loader.LoadMissingPosts", fallback: "Load missing posts") /// Show more replies - public static let showMoreReplies = L10n.tr("Localizable", "Common.Controls.Timeline.Loader.ShowMoreReplies") + public static let showMoreReplies = L10n.tr("Localizable", "Common.Controls.Timeline.Loader.ShowMoreReplies", fallback: "Show more replies") } public enum Timestamp { /// Now - public static let now = L10n.tr("Localizable", "Common.Controls.Timeline.Timestamp.Now") + public static let now = L10n.tr("Localizable", "Common.Controls.Timeline.Timestamp.Now", fallback: "Now") } } } } - public enum Scene { public enum AccountList { /// Add Account - public static let addAccount = L10n.tr("Localizable", "Scene.AccountList.AddAccount") + public static let addAccount = L10n.tr("Localizable", "Scene.AccountList.AddAccount", fallback: "Add Account") /// Dismiss Account Switcher - public static let dismissAccountSwitcher = L10n.tr("Localizable", "Scene.AccountList.DismissAccountSwitcher") + public static let dismissAccountSwitcher = L10n.tr("Localizable", "Scene.AccountList.DismissAccountSwitcher", fallback: "Dismiss Account Switcher") /// Current selected profile: %@. Double tap then hold to show account switcher public static func tabBarHint(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.AccountList.TabBarHint", String(describing: p1)) + return L10n.tr("Localizable", "Scene.AccountList.TabBarHint", String(describing: p1), fallback: "Current selected profile: %@. Double tap then hold to show account switcher") } } public enum Bookmark { /// Bookmarks - public static let title = L10n.tr("Localizable", "Scene.Bookmark.Title") + public static let title = L10n.tr("Localizable", "Scene.Bookmark.Title", fallback: "Bookmarks") } public enum Compose { /// Publish - public static let composeAction = L10n.tr("Localizable", "Scene.Compose.ComposeAction") + public static let composeAction = L10n.tr("Localizable", "Scene.Compose.ComposeAction", fallback: "Publish") /// Type or paste what’s on your mind - public static let contentInputPlaceholder = L10n.tr("Localizable", "Scene.Compose.ContentInputPlaceholder") + public static let contentInputPlaceholder = L10n.tr("Localizable", "Scene.Compose.ContentInputPlaceholder", fallback: "Type or paste what’s on your mind") /// replying to %@ public static func replyingToUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Compose.ReplyingToUser", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Compose.ReplyingToUser", String(describing: p1), fallback: "replying to %@") } public enum Accessibility { /// Add Attachment - public static let appendAttachment = L10n.tr("Localizable", "Scene.Compose.Accessibility.AppendAttachment") + public static let appendAttachment = L10n.tr("Localizable", "Scene.Compose.Accessibility.AppendAttachment", fallback: "Add Attachment") /// Add Poll - public static let appendPoll = L10n.tr("Localizable", "Scene.Compose.Accessibility.AppendPoll") + public static let appendPoll = L10n.tr("Localizable", "Scene.Compose.Accessibility.AppendPoll", fallback: "Add Poll") /// Custom Emoji Picker - public static let customEmojiPicker = L10n.tr("Localizable", "Scene.Compose.Accessibility.CustomEmojiPicker") + public static let customEmojiPicker = L10n.tr("Localizable", "Scene.Compose.Accessibility.CustomEmojiPicker", fallback: "Custom Emoji Picker") /// Disable Content Warning - public static let disableContentWarning = L10n.tr("Localizable", "Scene.Compose.Accessibility.DisableContentWarning") + public static let disableContentWarning = L10n.tr("Localizable", "Scene.Compose.Accessibility.DisableContentWarning", fallback: "Disable Content Warning") /// Enable Content Warning - public static let enableContentWarning = L10n.tr("Localizable", "Scene.Compose.Accessibility.EnableContentWarning") + public static let enableContentWarning = L10n.tr("Localizable", "Scene.Compose.Accessibility.EnableContentWarning", fallback: "Enable Content Warning") /// Posting as %@ public static func postingAs(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Compose.Accessibility.PostingAs", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Compose.Accessibility.PostingAs", String(describing: p1), fallback: "Posting as %@") } /// Post Options - public static let postOptions = L10n.tr("Localizable", "Scene.Compose.Accessibility.PostOptions") + public static let postOptions = L10n.tr("Localizable", "Scene.Compose.Accessibility.PostOptions", fallback: "Post Options") /// Post Visibility Menu - public static let postVisibilityMenu = L10n.tr("Localizable", "Scene.Compose.Accessibility.PostVisibilityMenu") + public static let postVisibilityMenu = L10n.tr("Localizable", "Scene.Compose.Accessibility.PostVisibilityMenu", fallback: "Post Visibility Menu") /// Remove Poll - public static let removePoll = L10n.tr("Localizable", "Scene.Compose.Accessibility.RemovePoll") + public static let removePoll = L10n.tr("Localizable", "Scene.Compose.Accessibility.RemovePoll", fallback: "Remove Poll") } public enum Attachment { - /// This %@ is broken and can’t be\nuploaded to Mastodon. + /// This %@ is broken and can’t be + /// uploaded to Mastodon. public static func attachmentBroken(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Compose.Attachment.AttachmentBroken", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Compose.Attachment.AttachmentBroken", String(describing: p1), fallback: "This %@ is broken and can’t be\nuploaded to Mastodon.") } /// Attachment too large - public static let attachmentTooLarge = L10n.tr("Localizable", "Scene.Compose.Attachment.AttachmentTooLarge") - /// Can not regonize this media attachment - public static let canNotRecognizeThisMediaAttachment = L10n.tr("Localizable", "Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment") + public static let attachmentTooLarge = L10n.tr("Localizable", "Scene.Compose.Attachment.AttachmentTooLarge", fallback: "Attachment too large") + /// Can not recognize this media attachment + public static let canNotRecognizeThisMediaAttachment = L10n.tr("Localizable", "Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment", fallback: "Can not recognize this media attachment") + /// Compressing... + public static let compressingState = L10n.tr("Localizable", "Scene.Compose.Attachment.CompressingState", fallback: "Compressing...") /// Describe the photo for the visually-impaired... - public static let descriptionPhoto = L10n.tr("Localizable", "Scene.Compose.Attachment.DescriptionPhoto") + public static let descriptionPhoto = L10n.tr("Localizable", "Scene.Compose.Attachment.DescriptionPhoto", fallback: "Describe the photo for the visually-impaired...") /// Describe the video for the visually-impaired... - public static let descriptionVideo = L10n.tr("Localizable", "Scene.Compose.Attachment.DescriptionVideo") + public static let descriptionVideo = L10n.tr("Localizable", "Scene.Compose.Attachment.DescriptionVideo", fallback: "Describe the video for the visually-impaired...") /// Load Failed - public static let loadFailed = L10n.tr("Localizable", "Scene.Compose.Attachment.LoadFailed") + public static let loadFailed = L10n.tr("Localizable", "Scene.Compose.Attachment.LoadFailed", fallback: "Load Failed") /// photo - public static let photo = L10n.tr("Localizable", "Scene.Compose.Attachment.Photo") + public static let photo = L10n.tr("Localizable", "Scene.Compose.Attachment.Photo", fallback: "photo") + /// Server Processing... + public static let serverProcessingState = L10n.tr("Localizable", "Scene.Compose.Attachment.ServerProcessingState", fallback: "Server Processing...") /// Upload Failed - public static let uploadFailed = L10n.tr("Localizable", "Scene.Compose.Attachment.UploadFailed") + public static let uploadFailed = L10n.tr("Localizable", "Scene.Compose.Attachment.UploadFailed", fallback: "Upload Failed") /// video - public static let video = L10n.tr("Localizable", "Scene.Compose.Attachment.Video") + public static let video = L10n.tr("Localizable", "Scene.Compose.Attachment.Video", fallback: "video") } public enum AutoComplete { /// Space to add - public static let spaceToAdd = L10n.tr("Localizable", "Scene.Compose.AutoComplete.SpaceToAdd") + public static let spaceToAdd = L10n.tr("Localizable", "Scene.Compose.AutoComplete.SpaceToAdd", fallback: "Space to add") } public enum ContentWarning { /// Write an accurate warning here... - public static let placeholder = L10n.tr("Localizable", "Scene.Compose.ContentWarning.Placeholder") + public static let placeholder = L10n.tr("Localizable", "Scene.Compose.ContentWarning.Placeholder", fallback: "Write an accurate warning here...") } public enum Keyboard { /// Add Attachment - %@ public static func appendAttachmentEntry(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Compose.Keyboard.AppendAttachmentEntry", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Compose.Keyboard.AppendAttachmentEntry", String(describing: p1), fallback: "Add Attachment - %@") } /// Discard Post - public static let discardPost = L10n.tr("Localizable", "Scene.Compose.Keyboard.DiscardPost") + public static let discardPost = L10n.tr("Localizable", "Scene.Compose.Keyboard.DiscardPost", fallback: "Discard Post") /// Publish Post - public static let publishPost = L10n.tr("Localizable", "Scene.Compose.Keyboard.PublishPost") + public static let publishPost = L10n.tr("Localizable", "Scene.Compose.Keyboard.PublishPost", fallback: "Publish Post") /// Select Visibility - %@ public static func selectVisibilityEntry(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Compose.Keyboard.SelectVisibilityEntry", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Compose.Keyboard.SelectVisibilityEntry", String(describing: p1), fallback: "Select Visibility - %@") } /// Toggle Content Warning - public static let toggleContentWarning = L10n.tr("Localizable", "Scene.Compose.Keyboard.ToggleContentWarning") + public static let toggleContentWarning = L10n.tr("Localizable", "Scene.Compose.Keyboard.ToggleContentWarning", fallback: "Toggle Content Warning") /// Toggle Poll - public static let togglePoll = L10n.tr("Localizable", "Scene.Compose.Keyboard.TogglePoll") + public static let togglePoll = L10n.tr("Localizable", "Scene.Compose.Keyboard.TogglePoll", fallback: "Toggle Poll") } public enum MediaSelection { /// Browse - public static let browse = L10n.tr("Localizable", "Scene.Compose.MediaSelection.Browse") + public static let browse = L10n.tr("Localizable", "Scene.Compose.MediaSelection.Browse", fallback: "Browse") /// Take Photo - public static let camera = L10n.tr("Localizable", "Scene.Compose.MediaSelection.Camera") + public static let camera = L10n.tr("Localizable", "Scene.Compose.MediaSelection.Camera", fallback: "Take Photo") /// Photo Library - public static let photoLibrary = L10n.tr("Localizable", "Scene.Compose.MediaSelection.PhotoLibrary") + public static let photoLibrary = L10n.tr("Localizable", "Scene.Compose.MediaSelection.PhotoLibrary", fallback: "Photo Library") } public enum Poll { /// Duration: %@ public static func durationTime(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Compose.Poll.DurationTime", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Compose.Poll.DurationTime", String(describing: p1), fallback: "Duration: %@") } /// 1 Day - public static let oneDay = L10n.tr("Localizable", "Scene.Compose.Poll.OneDay") + public static let oneDay = L10n.tr("Localizable", "Scene.Compose.Poll.OneDay", fallback: "1 Day") /// 1 Hour - public static let oneHour = L10n.tr("Localizable", "Scene.Compose.Poll.OneHour") + public static let oneHour = L10n.tr("Localizable", "Scene.Compose.Poll.OneHour", fallback: "1 Hour") /// Option %ld public static func optionNumber(_ p1: Int) -> String { - return L10n.tr("Localizable", "Scene.Compose.Poll.OptionNumber", p1) + return L10n.tr("Localizable", "Scene.Compose.Poll.OptionNumber", p1, fallback: "Option %ld") } /// 7 Days - public static let sevenDays = L10n.tr("Localizable", "Scene.Compose.Poll.SevenDays") + public static let sevenDays = L10n.tr("Localizable", "Scene.Compose.Poll.SevenDays", fallback: "7 Days") /// 6 Hours - public static let sixHours = L10n.tr("Localizable", "Scene.Compose.Poll.SixHours") + public static let sixHours = L10n.tr("Localizable", "Scene.Compose.Poll.SixHours", fallback: "6 Hours") + /// The poll has empty option + public static let thePollHasEmptyOption = L10n.tr("Localizable", "Scene.Compose.Poll.ThePollHasEmptyOption", fallback: "The poll has empty option") + /// The poll is invalid + public static let thePollIsInvalid = L10n.tr("Localizable", "Scene.Compose.Poll.ThePollIsInvalid", fallback: "The poll is invalid") /// 30 minutes - public static let thirtyMinutes = L10n.tr("Localizable", "Scene.Compose.Poll.ThirtyMinutes") + public static let thirtyMinutes = L10n.tr("Localizable", "Scene.Compose.Poll.ThirtyMinutes", fallback: "30 minutes") /// 3 Days - public static let threeDays = L10n.tr("Localizable", "Scene.Compose.Poll.ThreeDays") + public static let threeDays = L10n.tr("Localizable", "Scene.Compose.Poll.ThreeDays", fallback: "3 Days") } public enum Title { /// New Post - public static let newPost = L10n.tr("Localizable", "Scene.Compose.Title.NewPost") + public static let newPost = L10n.tr("Localizable", "Scene.Compose.Title.NewPost", fallback: "New Post") /// New Reply - public static let newReply = L10n.tr("Localizable", "Scene.Compose.Title.NewReply") + public static let newReply = L10n.tr("Localizable", "Scene.Compose.Title.NewReply", fallback: "New Reply") } public enum Visibility { /// Only people I mention - public static let direct = L10n.tr("Localizable", "Scene.Compose.Visibility.Direct") + public static let direct = L10n.tr("Localizable", "Scene.Compose.Visibility.Direct", fallback: "Only people I mention") /// Followers only - public static let `private` = L10n.tr("Localizable", "Scene.Compose.Visibility.Private") + public static let `private` = L10n.tr("Localizable", "Scene.Compose.Visibility.Private", fallback: "Followers only") /// Public - public static let `public` = L10n.tr("Localizable", "Scene.Compose.Visibility.Public") + public static let `public` = L10n.tr("Localizable", "Scene.Compose.Visibility.Public", fallback: "Public") /// Unlisted - public static let unlisted = L10n.tr("Localizable", "Scene.Compose.Visibility.Unlisted") + public static let unlisted = L10n.tr("Localizable", "Scene.Compose.Visibility.Unlisted", fallback: "Unlisted") } } public enum ConfirmEmail { /// Tap the link we emailed to you to verify your account. - public static let subtitle = L10n.tr("Localizable", "Scene.ConfirmEmail.Subtitle") + public static let subtitle = L10n.tr("Localizable", "Scene.ConfirmEmail.Subtitle", fallback: "Tap the link we emailed to you to verify your account.") /// Tap the link we emailed to you to verify your account - public static let tapTheLinkWeEmailedToYouToVerifyYourAccount = L10n.tr("Localizable", "Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount") + public static let tapTheLinkWeEmailedToYouToVerifyYourAccount = L10n.tr("Localizable", "Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount", fallback: "Tap the link we emailed to you to verify your account") /// One last thing. - public static let title = L10n.tr("Localizable", "Scene.ConfirmEmail.Title") + public static let title = L10n.tr("Localizable", "Scene.ConfirmEmail.Title", fallback: "One last thing.") public enum Button { /// Open Email App - public static let openEmailApp = L10n.tr("Localizable", "Scene.ConfirmEmail.Button.OpenEmailApp") + public static let openEmailApp = L10n.tr("Localizable", "Scene.ConfirmEmail.Button.OpenEmailApp", fallback: "Open Email App") /// Resend - public static let resend = L10n.tr("Localizable", "Scene.ConfirmEmail.Button.Resend") + public static let resend = L10n.tr("Localizable", "Scene.ConfirmEmail.Button.Resend", fallback: "Resend") } public enum DontReceiveEmail { /// Check if your email address is correct as well as your junk folder if you haven’t. - public static let description = L10n.tr("Localizable", "Scene.ConfirmEmail.DontReceiveEmail.Description") + public static let description = L10n.tr("Localizable", "Scene.ConfirmEmail.DontReceiveEmail.Description", fallback: "Check if your email address is correct as well as your junk folder if you haven’t.") /// Resend Email - public static let resendEmail = L10n.tr("Localizable", "Scene.ConfirmEmail.DontReceiveEmail.ResendEmail") + public static let resendEmail = L10n.tr("Localizable", "Scene.ConfirmEmail.DontReceiveEmail.ResendEmail", fallback: "Resend Email") /// Check your email - public static let title = L10n.tr("Localizable", "Scene.ConfirmEmail.DontReceiveEmail.Title") + public static let title = L10n.tr("Localizable", "Scene.ConfirmEmail.DontReceiveEmail.Title", fallback: "Check your email") } public enum OpenEmailApp { /// We just sent you an email. Check your junk folder if you haven’t. - public static let description = L10n.tr("Localizable", "Scene.ConfirmEmail.OpenEmailApp.Description") + public static let description = L10n.tr("Localizable", "Scene.ConfirmEmail.OpenEmailApp.Description", fallback: "We just sent you an email. Check your junk folder if you haven’t.") /// Mail - public static let mail = L10n.tr("Localizable", "Scene.ConfirmEmail.OpenEmailApp.Mail") + public static let mail = L10n.tr("Localizable", "Scene.ConfirmEmail.OpenEmailApp.Mail", fallback: "Mail") /// Open Email Client - public static let openEmailClient = L10n.tr("Localizable", "Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient") + public static let openEmailClient = L10n.tr("Localizable", "Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient", fallback: "Open Email Client") /// Check your inbox. - public static let title = L10n.tr("Localizable", "Scene.ConfirmEmail.OpenEmailApp.Title") + public static let title = L10n.tr("Localizable", "Scene.ConfirmEmail.OpenEmailApp.Title", fallback: "Check your inbox.") } } public enum Discovery { /// These are the posts gaining traction in your corner of Mastodon. - public static let intro = L10n.tr("Localizable", "Scene.Discovery.Intro") + public static let intro = L10n.tr("Localizable", "Scene.Discovery.Intro", fallback: "These are the posts gaining traction in your corner of Mastodon.") public enum Tabs { /// Community - public static let community = L10n.tr("Localizable", "Scene.Discovery.Tabs.Community") + public static let community = L10n.tr("Localizable", "Scene.Discovery.Tabs.Community", fallback: "Community") /// For You - public static let forYou = L10n.tr("Localizable", "Scene.Discovery.Tabs.ForYou") + public static let forYou = L10n.tr("Localizable", "Scene.Discovery.Tabs.ForYou", fallback: "For You") /// Hashtags - public static let hashtags = L10n.tr("Localizable", "Scene.Discovery.Tabs.Hashtags") + public static let hashtags = L10n.tr("Localizable", "Scene.Discovery.Tabs.Hashtags", fallback: "Hashtags") /// News - public static let news = L10n.tr("Localizable", "Scene.Discovery.Tabs.News") + public static let news = L10n.tr("Localizable", "Scene.Discovery.Tabs.News", fallback: "News") /// Posts - public static let posts = L10n.tr("Localizable", "Scene.Discovery.Tabs.Posts") + public static let posts = L10n.tr("Localizable", "Scene.Discovery.Tabs.Posts", fallback: "Posts") } } public enum Familiarfollowers { /// Followed by %@ public static func followedByNames(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Familiarfollowers.FollowedByNames", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Familiarfollowers.FollowedByNames", String(describing: p1), fallback: "Followed by %@") } /// Followers you familiar - public static let title = L10n.tr("Localizable", "Scene.Familiarfollowers.Title") + public static let title = L10n.tr("Localizable", "Scene.Familiarfollowers.Title", fallback: "Followers you familiar") } public enum Favorite { /// Your Favorites - public static let title = L10n.tr("Localizable", "Scene.Favorite.Title") + public static let title = L10n.tr("Localizable", "Scene.Favorite.Title", fallback: "Your Favorites") } public enum FavoritedBy { /// Favorited By - public static let title = L10n.tr("Localizable", "Scene.FavoritedBy.Title") + public static let title = L10n.tr("Localizable", "Scene.FavoritedBy.Title", fallback: "Favorited By") } public enum Follower { /// Followers from other servers are not displayed. - public static let footer = L10n.tr("Localizable", "Scene.Follower.Footer") + public static let footer = L10n.tr("Localizable", "Scene.Follower.Footer", fallback: "Followers from other servers are not displayed.") /// follower - public static let title = L10n.tr("Localizable", "Scene.Follower.Title") + public static let title = L10n.tr("Localizable", "Scene.Follower.Title", fallback: "follower") } public enum Following { /// Follows from other servers are not displayed. - public static let footer = L10n.tr("Localizable", "Scene.Following.Footer") + public static let footer = L10n.tr("Localizable", "Scene.Following.Footer", fallback: "Follows from other servers are not displayed.") /// following - public static let title = L10n.tr("Localizable", "Scene.Following.Title") + public static let title = L10n.tr("Localizable", "Scene.Following.Title", fallback: "following") } public enum HomeTimeline { /// Home - public static let title = L10n.tr("Localizable", "Scene.HomeTimeline.Title") + public static let title = L10n.tr("Localizable", "Scene.HomeTimeline.Title", fallback: "Home") public enum NavigationBarState { /// See new posts - public static let newPosts = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.NewPosts") + public static let newPosts = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.NewPosts", fallback: "See new posts") /// Offline - public static let offline = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Offline") + public static let offline = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Offline", fallback: "Offline") /// Published! - public static let published = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Published") + public static let published = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Published", fallback: "Published!") /// Publishing post... - public static let publishing = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Publishing") + public static let publishing = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Publishing", fallback: "Publishing post...") public enum Accessibility { /// Tap to scroll to top and tap again to previous location - public static let logoHint = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint") + public static let logoHint = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint", fallback: "Tap to scroll to top and tap again to previous location") /// Logo Button - public static let logoLabel = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel") + public static let logoLabel = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel", fallback: "Logo Button") } } } public enum Notification { public enum FollowRequest { /// Accept - public static let accept = L10n.tr("Localizable", "Scene.Notification.FollowRequest.Accept") + public static let accept = L10n.tr("Localizable", "Scene.Notification.FollowRequest.Accept", fallback: "Accept") /// Accepted - public static let accepted = L10n.tr("Localizable", "Scene.Notification.FollowRequest.Accepted") + public static let accepted = L10n.tr("Localizable", "Scene.Notification.FollowRequest.Accepted", fallback: "Accepted") /// reject - public static let reject = L10n.tr("Localizable", "Scene.Notification.FollowRequest.Reject") + public static let reject = L10n.tr("Localizable", "Scene.Notification.FollowRequest.Reject", fallback: "reject") /// Rejected - public static let rejected = L10n.tr("Localizable", "Scene.Notification.FollowRequest.Rejected") + public static let rejected = L10n.tr("Localizable", "Scene.Notification.FollowRequest.Rejected", fallback: "Rejected") } public enum Keyobard { /// Show Everything - public static let showEverything = L10n.tr("Localizable", "Scene.Notification.Keyobard.ShowEverything") + public static let showEverything = L10n.tr("Localizable", "Scene.Notification.Keyobard.ShowEverything", fallback: "Show Everything") /// Show Mentions - public static let showMentions = L10n.tr("Localizable", "Scene.Notification.Keyobard.ShowMentions") + public static let showMentions = L10n.tr("Localizable", "Scene.Notification.Keyobard.ShowMentions", fallback: "Show Mentions") } public enum NotificationDescription { /// favorited your post - public static let favoritedYourPost = L10n.tr("Localizable", "Scene.Notification.NotificationDescription.FavoritedYourPost") + public static let favoritedYourPost = L10n.tr("Localizable", "Scene.Notification.NotificationDescription.FavoritedYourPost", fallback: "favorited your post") /// followed you - public static let followedYou = L10n.tr("Localizable", "Scene.Notification.NotificationDescription.FollowedYou") + public static let followedYou = L10n.tr("Localizable", "Scene.Notification.NotificationDescription.FollowedYou", fallback: "followed you") /// mentioned you - public static let mentionedYou = L10n.tr("Localizable", "Scene.Notification.NotificationDescription.MentionedYou") + public static let mentionedYou = L10n.tr("Localizable", "Scene.Notification.NotificationDescription.MentionedYou", fallback: "mentioned you") /// poll has ended - public static let pollHasEnded = L10n.tr("Localizable", "Scene.Notification.NotificationDescription.PollHasEnded") + public static let pollHasEnded = L10n.tr("Localizable", "Scene.Notification.NotificationDescription.PollHasEnded", fallback: "poll has ended") /// reblogged your post - public static let rebloggedYourPost = L10n.tr("Localizable", "Scene.Notification.NotificationDescription.RebloggedYourPost") + public static let rebloggedYourPost = L10n.tr("Localizable", "Scene.Notification.NotificationDescription.RebloggedYourPost", fallback: "reblogged your post") /// request to follow you - public static let requestToFollowYou = L10n.tr("Localizable", "Scene.Notification.NotificationDescription.RequestToFollowYou") + public static let requestToFollowYou = L10n.tr("Localizable", "Scene.Notification.NotificationDescription.RequestToFollowYou", fallback: "request to follow you") } public enum Title { /// Everything - public static let everything = L10n.tr("Localizable", "Scene.Notification.Title.Everything") + public static let everything = L10n.tr("Localizable", "Scene.Notification.Title.Everything", fallback: "Everything") /// Mentions - public static let mentions = L10n.tr("Localizable", "Scene.Notification.Title.Mentions") + public static let mentions = L10n.tr("Localizable", "Scene.Notification.Title.Mentions", fallback: "Mentions") } } public enum Preview { public enum Keyboard { /// Close Preview - public static let closePreview = L10n.tr("Localizable", "Scene.Preview.Keyboard.ClosePreview") + public static let closePreview = L10n.tr("Localizable", "Scene.Preview.Keyboard.ClosePreview", fallback: "Close Preview") /// Show Next - public static let showNext = L10n.tr("Localizable", "Scene.Preview.Keyboard.ShowNext") + public static let showNext = L10n.tr("Localizable", "Scene.Preview.Keyboard.ShowNext", fallback: "Show Next") /// Show Previous - public static let showPrevious = L10n.tr("Localizable", "Scene.Preview.Keyboard.ShowPrevious") + public static let showPrevious = L10n.tr("Localizable", "Scene.Preview.Keyboard.ShowPrevious", fallback: "Show Previous") } } public enum Profile { public enum Accessibility { /// Double tap to open the list - public static let doubleTapToOpenTheList = L10n.tr("Localizable", "Scene.Profile.Accessibility.DoubleTapToOpenTheList") + public static let doubleTapToOpenTheList = L10n.tr("Localizable", "Scene.Profile.Accessibility.DoubleTapToOpenTheList", fallback: "Double tap to open the list") /// Edit avatar image - public static let editAvatarImage = L10n.tr("Localizable", "Scene.Profile.Accessibility.EditAvatarImage") + public static let editAvatarImage = L10n.tr("Localizable", "Scene.Profile.Accessibility.EditAvatarImage", fallback: "Edit avatar image") /// Show avatar image - public static let showAvatarImage = L10n.tr("Localizable", "Scene.Profile.Accessibility.ShowAvatarImage") + public static let showAvatarImage = L10n.tr("Localizable", "Scene.Profile.Accessibility.ShowAvatarImage", fallback: "Show avatar image") /// Show banner image - public static let showBannerImage = L10n.tr("Localizable", "Scene.Profile.Accessibility.ShowBannerImage") + public static let showBannerImage = L10n.tr("Localizable", "Scene.Profile.Accessibility.ShowBannerImage", fallback: "Show banner image") } public enum Dashboard { /// followers - public static let followers = L10n.tr("Localizable", "Scene.Profile.Dashboard.Followers") + public static let followers = L10n.tr("Localizable", "Scene.Profile.Dashboard.Followers", fallback: "followers") /// following - public static let following = L10n.tr("Localizable", "Scene.Profile.Dashboard.Following") + public static let following = L10n.tr("Localizable", "Scene.Profile.Dashboard.Following", fallback: "following") /// posts - public static let posts = L10n.tr("Localizable", "Scene.Profile.Dashboard.Posts") + public static let posts = L10n.tr("Localizable", "Scene.Profile.Dashboard.Posts", fallback: "posts") } public enum Fields { /// Add Row - public static let addRow = L10n.tr("Localizable", "Scene.Profile.Fields.AddRow") + public static let addRow = L10n.tr("Localizable", "Scene.Profile.Fields.AddRow", fallback: "Add Row") public enum Placeholder { /// Content - public static let content = L10n.tr("Localizable", "Scene.Profile.Fields.Placeholder.Content") + public static let content = L10n.tr("Localizable", "Scene.Profile.Fields.Placeholder.Content", fallback: "Content") /// Label - public static let label = L10n.tr("Localizable", "Scene.Profile.Fields.Placeholder.Label") + public static let label = L10n.tr("Localizable", "Scene.Profile.Fields.Placeholder.Label", fallback: "Label") } } public enum Header { /// Follows You - public static let followsYou = L10n.tr("Localizable", "Scene.Profile.Header.FollowsYou") + public static let followsYou = L10n.tr("Localizable", "Scene.Profile.Header.FollowsYou", fallback: "Follows You") } public enum RelationshipActionAlert { public enum ConfirmBlockUser { /// Confirm to block %@ public static func message(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message", String(describing: p1), fallback: "Confirm to block %@") } /// Block Account - public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title") + public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title", fallback: "Block Account") } public enum ConfirmHideReblogs { /// Confirm to hide reblogs - public static let message = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message") + public static let message = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message", fallback: "Confirm to hide reblogs") /// Hide Reblogs - public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title") + public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title", fallback: "Hide Reblogs") } public enum ConfirmMuteUser { /// Confirm to mute %@ public static func message(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message", String(describing: p1), fallback: "Confirm to mute %@") } /// Mute Account - public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title") + public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title", fallback: "Mute Account") } public enum ConfirmShowReblogs { /// Confirm to show reblogs - public static let message = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message") + public static let message = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message", fallback: "Confirm to show reblogs") /// Show Reblogs - public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title") + public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title", fallback: "Show Reblogs") } public enum ConfirmUnblockUser { /// Confirm to unblock %@ public static func message(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message", String(describing: p1), fallback: "Confirm to unblock %@") } /// Unblock Account - public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title") + public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title", fallback: "Unblock Account") } public enum ConfirmUnmuteUser { /// Confirm to unmute %@ public static func message(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message", String(describing: p1), fallback: "Confirm to unmute %@") } /// Unmute Account - public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title") + public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title", fallback: "Unmute Account") } } public enum SegmentedControl { /// About - public static let about = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.About") + public static let about = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.About", fallback: "About") /// Media - public static let media = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.Media") + public static let media = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.Media", fallback: "Media") /// Posts - public static let posts = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.Posts") + public static let posts = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.Posts", fallback: "Posts") /// Posts and Replies - public static let postsAndReplies = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.PostsAndReplies") + public static let postsAndReplies = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.PostsAndReplies", fallback: "Posts and Replies") /// Replies - public static let replies = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.Replies") + public static let replies = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.Replies", fallback: "Replies") } } public enum RebloggedBy { /// Reblogged By - public static let title = L10n.tr("Localizable", "Scene.RebloggedBy.Title") + public static let title = L10n.tr("Localizable", "Scene.RebloggedBy.Title", fallback: "Reblogged By") } public enum Register { /// Let’s get you set up on %@ public static func letsGetYouSetUpOnDomain(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Register.LetsGetYouSetUpOnDomain", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Register.LetsGetYouSetUpOnDomain", String(describing: p1), fallback: "Let’s get you set up on %@") } /// Let’s get you set up on %@ public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Register.Title", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Register.Title", String(describing: p1), fallback: "Let’s get you set up on %@") } public enum Error { public enum Item { /// Agreement - public static let agreement = L10n.tr("Localizable", "Scene.Register.Error.Item.Agreement") + public static let agreement = L10n.tr("Localizable", "Scene.Register.Error.Item.Agreement", fallback: "Agreement") /// Email - public static let email = L10n.tr("Localizable", "Scene.Register.Error.Item.Email") + public static let email = L10n.tr("Localizable", "Scene.Register.Error.Item.Email", fallback: "Email") /// Locale - public static let locale = L10n.tr("Localizable", "Scene.Register.Error.Item.Locale") + public static let locale = L10n.tr("Localizable", "Scene.Register.Error.Item.Locale", fallback: "Locale") /// Password - public static let password = L10n.tr("Localizable", "Scene.Register.Error.Item.Password") + public static let password = L10n.tr("Localizable", "Scene.Register.Error.Item.Password", fallback: "Password") /// Reason - public static let reason = L10n.tr("Localizable", "Scene.Register.Error.Item.Reason") + public static let reason = L10n.tr("Localizable", "Scene.Register.Error.Item.Reason", fallback: "Reason") /// Username - public static let username = L10n.tr("Localizable", "Scene.Register.Error.Item.Username") + public static let username = L10n.tr("Localizable", "Scene.Register.Error.Item.Username", fallback: "Username") } public enum Reason { /// %@ must be accepted public static func accepted(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Register.Error.Reason.Accepted", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Register.Error.Reason.Accepted", String(describing: p1), fallback: "%@ must be accepted") } /// %@ is required public static func blank(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Register.Error.Reason.Blank", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Register.Error.Reason.Blank", String(describing: p1), fallback: "%@ is required") } /// %@ contains a disallowed email provider public static func blocked(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Register.Error.Reason.Blocked", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Register.Error.Reason.Blocked", String(describing: p1), fallback: "%@ contains a disallowed email provider") } /// %@ is not a supported value public static func inclusion(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Register.Error.Reason.Inclusion", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Register.Error.Reason.Inclusion", String(describing: p1), fallback: "%@ is not a supported value") } /// %@ is invalid public static func invalid(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Register.Error.Reason.Invalid", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Register.Error.Reason.Invalid", String(describing: p1), fallback: "%@ is invalid") } /// %@ is a reserved keyword public static func reserved(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Register.Error.Reason.Reserved", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Register.Error.Reason.Reserved", String(describing: p1), fallback: "%@ is a reserved keyword") } /// %@ is already in use public static func taken(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Register.Error.Reason.Taken", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Register.Error.Reason.Taken", String(describing: p1), fallback: "%@ is already in use") } /// %@ is too long public static func tooLong(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Register.Error.Reason.TooLong", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Register.Error.Reason.TooLong", String(describing: p1), fallback: "%@ is too long") } /// %@ is too short public static func tooShort(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Register.Error.Reason.TooShort", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Register.Error.Reason.TooShort", String(describing: p1), fallback: "%@ is too short") } /// %@ does not seem to exist public static func unreachable(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Register.Error.Reason.Unreachable", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Register.Error.Reason.Unreachable", String(describing: p1), fallback: "%@ does not seem to exist") } } public enum Special { /// This is not a valid email address - public static let emailInvalid = L10n.tr("Localizable", "Scene.Register.Error.Special.EmailInvalid") + public static let emailInvalid = L10n.tr("Localizable", "Scene.Register.Error.Special.EmailInvalid", fallback: "This is not a valid email address") /// Password is too short (must be at least 8 characters) - public static let passwordTooShort = L10n.tr("Localizable", "Scene.Register.Error.Special.PasswordTooShort") + public static let passwordTooShort = L10n.tr("Localizable", "Scene.Register.Error.Special.PasswordTooShort", fallback: "Password is too short (must be at least 8 characters)") /// Username must only contain alphanumeric characters and underscores - public static let usernameInvalid = L10n.tr("Localizable", "Scene.Register.Error.Special.UsernameInvalid") + public static let usernameInvalid = L10n.tr("Localizable", "Scene.Register.Error.Special.UsernameInvalid", fallback: "Username must only contain alphanumeric characters and underscores") /// Username is too long (can’t be longer than 30 characters) - public static let usernameTooLong = L10n.tr("Localizable", "Scene.Register.Error.Special.UsernameTooLong") + public static let usernameTooLong = L10n.tr("Localizable", "Scene.Register.Error.Special.UsernameTooLong", fallback: "Username is too long (can’t be longer than 30 characters)") } } public enum Input { public enum Avatar { /// Delete - public static let delete = L10n.tr("Localizable", "Scene.Register.Input.Avatar.Delete") + public static let delete = L10n.tr("Localizable", "Scene.Register.Input.Avatar.Delete", fallback: "Delete") } public enum DisplayName { /// display name - public static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.DisplayName.Placeholder") + public static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.DisplayName.Placeholder", fallback: "display name") } public enum Email { /// email - public static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.Email.Placeholder") + public static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.Email.Placeholder", fallback: "email") } public enum Invite { /// Why do you want to join? - public static let registrationUserInviteRequest = L10n.tr("Localizable", "Scene.Register.Input.Invite.RegistrationUserInviteRequest") + public static let registrationUserInviteRequest = L10n.tr("Localizable", "Scene.Register.Input.Invite.RegistrationUserInviteRequest", fallback: "Why do you want to join?") } public enum Password { /// 8 characters - public static let characterLimit = L10n.tr("Localizable", "Scene.Register.Input.Password.CharacterLimit") + public static let characterLimit = L10n.tr("Localizable", "Scene.Register.Input.Password.CharacterLimit", fallback: "8 characters") /// Your password needs at least eight characters - public static let hint = L10n.tr("Localizable", "Scene.Register.Input.Password.Hint") + public static let hint = L10n.tr("Localizable", "Scene.Register.Input.Password.Hint", fallback: "Your password needs at least eight characters") /// password - public static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.Password.Placeholder") + public static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.Password.Placeholder", fallback: "password") /// Your password needs at least: - public static let require = L10n.tr("Localizable", "Scene.Register.Input.Password.Require") + public static let require = L10n.tr("Localizable", "Scene.Register.Input.Password.Require", fallback: "Your password needs at least:") public enum Accessibility { /// checked - public static let checked = L10n.tr("Localizable", "Scene.Register.Input.Password.Accessibility.Checked") + public static let checked = L10n.tr("Localizable", "Scene.Register.Input.Password.Accessibility.Checked", fallback: "checked") /// unchecked - public static let unchecked = L10n.tr("Localizable", "Scene.Register.Input.Password.Accessibility.Unchecked") + public static let unchecked = L10n.tr("Localizable", "Scene.Register.Input.Password.Accessibility.Unchecked", fallback: "unchecked") } } public enum Username { /// This username is taken. - public static let duplicatePrompt = L10n.tr("Localizable", "Scene.Register.Input.Username.DuplicatePrompt") + public static let duplicatePrompt = L10n.tr("Localizable", "Scene.Register.Input.Username.DuplicatePrompt", fallback: "This username is taken.") /// username - public static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.Username.Placeholder") + public static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.Username.Placeholder", fallback: "username") } } } public enum Report { /// Are there any other posts you’d like to add to the report? - public static let content1 = L10n.tr("Localizable", "Scene.Report.Content1") + public static let content1 = L10n.tr("Localizable", "Scene.Report.Content1", fallback: "Are there any other posts you’d like to add to the report?") /// Is there anything the moderators should know about this report? - public static let content2 = L10n.tr("Localizable", "Scene.Report.Content2") + public static let content2 = L10n.tr("Localizable", "Scene.Report.Content2", fallback: "Is there anything the moderators should know about this report?") /// REPORTED - public static let reported = L10n.tr("Localizable", "Scene.Report.Reported") + public static let reported = L10n.tr("Localizable", "Scene.Report.Reported", fallback: "REPORTED") /// Thanks for reporting, we’ll look into this. - public static let reportSentTitle = L10n.tr("Localizable", "Scene.Report.ReportSentTitle") + public static let reportSentTitle = L10n.tr("Localizable", "Scene.Report.ReportSentTitle", fallback: "Thanks for reporting, we’ll look into this.") /// Send Report - public static let send = L10n.tr("Localizable", "Scene.Report.Send") + public static let send = L10n.tr("Localizable", "Scene.Report.Send", fallback: "Send Report") /// Send without comment - public static let skipToSend = L10n.tr("Localizable", "Scene.Report.SkipToSend") + public static let skipToSend = L10n.tr("Localizable", "Scene.Report.SkipToSend", fallback: "Send without comment") /// Step 1 of 2 - public static let step1 = L10n.tr("Localizable", "Scene.Report.Step1") + public static let step1 = L10n.tr("Localizable", "Scene.Report.Step1", fallback: "Step 1 of 2") /// Step 2 of 2 - public static let step2 = L10n.tr("Localizable", "Scene.Report.Step2") + public static let step2 = L10n.tr("Localizable", "Scene.Report.Step2", fallback: "Step 2 of 2") /// Type or paste additional comments - public static let textPlaceholder = L10n.tr("Localizable", "Scene.Report.TextPlaceholder") + public static let textPlaceholder = L10n.tr("Localizable", "Scene.Report.TextPlaceholder", fallback: "Type or paste additional comments") /// Report %@ public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Report.Title", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Report.Title", String(describing: p1), fallback: "Report %@") } /// Report - public static let titleReport = L10n.tr("Localizable", "Scene.Report.TitleReport") + public static let titleReport = L10n.tr("Localizable", "Scene.Report.TitleReport", fallback: "Report") public enum StepFinal { /// Block %@ public static func blockUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Report.StepFinal.BlockUser", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Report.StepFinal.BlockUser", String(describing: p1), fallback: "Block %@") } /// Don’t want to see this? - public static let dontWantToSeeThis = L10n.tr("Localizable", "Scene.Report.StepFinal.DontWantToSeeThis") + public static let dontWantToSeeThis = L10n.tr("Localizable", "Scene.Report.StepFinal.DontWantToSeeThis", fallback: "Don’t want to see this?") /// Mute %@ public static func muteUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Report.StepFinal.MuteUser", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Report.StepFinal.MuteUser", String(describing: p1), fallback: "Mute %@") } /// They will no longer be able to follow or see your posts, but they can see if they’ve been blocked. - public static let theyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked = L10n.tr("Localizable", "Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked") + public static let theyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked = L10n.tr("Localizable", "Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked", fallback: "They will no longer be able to follow or see your posts, but they can see if they’ve been blocked.") /// Unfollow - public static let unfollow = L10n.tr("Localizable", "Scene.Report.StepFinal.Unfollow") + public static let unfollow = L10n.tr("Localizable", "Scene.Report.StepFinal.Unfollow", fallback: "Unfollow") /// Unfollowed - public static let unfollowed = L10n.tr("Localizable", "Scene.Report.StepFinal.Unfollowed") + public static let unfollowed = L10n.tr("Localizable", "Scene.Report.StepFinal.Unfollowed", fallback: "Unfollowed") /// Unfollow %@ public static func unfollowUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Report.StepFinal.UnfollowUser", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Report.StepFinal.UnfollowUser", String(describing: p1), fallback: "Unfollow %@") } /// When you see something you don’t like on Mastodon, you can remove the person from your experience. - public static let whenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience = L10n.tr("Localizable", "Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience.") + public static let whenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience = L10n.tr("Localizable", "Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience.", fallback: "When you see something you don’t like on Mastodon, you can remove the person from your experience.") /// While we review this, you can take action against %@ public static func whileWeReviewThisYouCanTakeActionAgainstUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser", String(describing: p1), fallback: "While we review this, you can take action against %@") } /// You won’t see their posts or reblogs in your home feed. They won’t know they’ve been muted. - public static let youWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted = L10n.tr("Localizable", "Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted") + public static let youWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted = L10n.tr("Localizable", "Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted", fallback: "You won’t see their posts or reblogs in your home feed. They won’t know they’ve been muted.") } public enum StepFour { /// Is there anything else we should know? - public static let isThereAnythingElseWeShouldKnow = L10n.tr("Localizable", "Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow") + public static let isThereAnythingElseWeShouldKnow = L10n.tr("Localizable", "Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow", fallback: "Is there anything else we should know?") /// Step 4 of 4 - public static let step4Of4 = L10n.tr("Localizable", "Scene.Report.StepFour.Step4Of4") + public static let step4Of4 = L10n.tr("Localizable", "Scene.Report.StepFour.Step4Of4", fallback: "Step 4 of 4") } public enum StepOne { /// I don’t like it - public static let iDontLikeIt = L10n.tr("Localizable", "Scene.Report.StepOne.IDontLikeIt") + public static let iDontLikeIt = L10n.tr("Localizable", "Scene.Report.StepOne.IDontLikeIt", fallback: "I don’t like it") /// It is not something you want to see - public static let itIsNotSomethingYouWantToSee = L10n.tr("Localizable", "Scene.Report.StepOne.ItIsNotSomethingYouWantToSee") + public static let itIsNotSomethingYouWantToSee = L10n.tr("Localizable", "Scene.Report.StepOne.ItIsNotSomethingYouWantToSee", fallback: "It is not something you want to see") /// It’s something else - public static let itsSomethingElse = L10n.tr("Localizable", "Scene.Report.StepOne.ItsSomethingElse") + public static let itsSomethingElse = L10n.tr("Localizable", "Scene.Report.StepOne.ItsSomethingElse", fallback: "It’s something else") /// It’s spam - public static let itsSpam = L10n.tr("Localizable", "Scene.Report.StepOne.ItsSpam") + public static let itsSpam = L10n.tr("Localizable", "Scene.Report.StepOne.ItsSpam", fallback: "It’s spam") /// It violates server rules - public static let itViolatesServerRules = L10n.tr("Localizable", "Scene.Report.StepOne.ItViolatesServerRules") + public static let itViolatesServerRules = L10n.tr("Localizable", "Scene.Report.StepOne.ItViolatesServerRules", fallback: "It violates server rules") /// Malicious links, fake engagement, or repetetive replies - public static let maliciousLinksFakeEngagementOrRepetetiveReplies = L10n.tr("Localizable", "Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies") + public static let maliciousLinksFakeEngagementOrRepetetiveReplies = L10n.tr("Localizable", "Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies", fallback: "Malicious links, fake engagement, or repetetive replies") /// Select the best match - public static let selectTheBestMatch = L10n.tr("Localizable", "Scene.Report.StepOne.SelectTheBestMatch") + public static let selectTheBestMatch = L10n.tr("Localizable", "Scene.Report.StepOne.SelectTheBestMatch", fallback: "Select the best match") /// Step 1 of 4 - public static let step1Of4 = L10n.tr("Localizable", "Scene.Report.StepOne.Step1Of4") + public static let step1Of4 = L10n.tr("Localizable", "Scene.Report.StepOne.Step1Of4", fallback: "Step 1 of 4") /// The issue does not fit into other categories - public static let theIssueDoesNotFitIntoOtherCategories = L10n.tr("Localizable", "Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories") + public static let theIssueDoesNotFitIntoOtherCategories = L10n.tr("Localizable", "Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories", fallback: "The issue does not fit into other categories") /// What's wrong with this account? - public static let whatsWrongWithThisAccount = L10n.tr("Localizable", "Scene.Report.StepOne.WhatsWrongWithThisAccount") + public static let whatsWrongWithThisAccount = L10n.tr("Localizable", "Scene.Report.StepOne.WhatsWrongWithThisAccount", fallback: "What's wrong with this account?") /// What's wrong with this post? - public static let whatsWrongWithThisPost = L10n.tr("Localizable", "Scene.Report.StepOne.WhatsWrongWithThisPost") + public static let whatsWrongWithThisPost = L10n.tr("Localizable", "Scene.Report.StepOne.WhatsWrongWithThisPost", fallback: "What's wrong with this post?") /// What's wrong with %@? public static func whatsWrongWithThisUsername(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Report.StepOne.WhatsWrongWithThisUsername", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Report.StepOne.WhatsWrongWithThisUsername", String(describing: p1), fallback: "What's wrong with %@?") } /// You are aware that it breaks specific rules - public static let youAreAwareThatItBreaksSpecificRules = L10n.tr("Localizable", "Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules") + public static let youAreAwareThatItBreaksSpecificRules = L10n.tr("Localizable", "Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules", fallback: "You are aware that it breaks specific rules") } public enum StepThree { /// Are there any posts that back up this report? - public static let areThereAnyPostsThatBackUpThisReport = L10n.tr("Localizable", "Scene.Report.StepThree.AreThereAnyPostsThatBackUpThisReport") + public static let areThereAnyPostsThatBackUpThisReport = L10n.tr("Localizable", "Scene.Report.StepThree.AreThereAnyPostsThatBackUpThisReport", fallback: "Are there any posts that back up this report?") /// Select all that apply - public static let selectAllThatApply = L10n.tr("Localizable", "Scene.Report.StepThree.SelectAllThatApply") + public static let selectAllThatApply = L10n.tr("Localizable", "Scene.Report.StepThree.SelectAllThatApply", fallback: "Select all that apply") /// Step 3 of 4 - public static let step3Of4 = L10n.tr("Localizable", "Scene.Report.StepThree.Step3Of4") + public static let step3Of4 = L10n.tr("Localizable", "Scene.Report.StepThree.Step3Of4", fallback: "Step 3 of 4") } public enum StepTwo { /// I just don’t like it - public static let iJustDonTLikeIt = L10n.tr("Localizable", "Scene.Report.StepTwo.IJustDon’tLikeIt") + public static let iJustDonTLikeIt = L10n.tr("Localizable", "Scene.Report.StepTwo.IJustDon’tLikeIt", fallback: "I just don’t like it") /// Select all that apply - public static let selectAllThatApply = L10n.tr("Localizable", "Scene.Report.StepTwo.SelectAllThatApply") + public static let selectAllThatApply = L10n.tr("Localizable", "Scene.Report.StepTwo.SelectAllThatApply", fallback: "Select all that apply") /// Step 2 of 4 - public static let step2Of4 = L10n.tr("Localizable", "Scene.Report.StepTwo.Step2Of4") + public static let step2Of4 = L10n.tr("Localizable", "Scene.Report.StepTwo.Step2Of4", fallback: "Step 2 of 4") /// Which rules are being violated? - public static let whichRulesAreBeingViolated = L10n.tr("Localizable", "Scene.Report.StepTwo.WhichRulesAreBeingViolated") + public static let whichRulesAreBeingViolated = L10n.tr("Localizable", "Scene.Report.StepTwo.WhichRulesAreBeingViolated", fallback: "Which rules are being violated?") } } public enum Search { /// Search - public static let title = L10n.tr("Localizable", "Scene.Search.Title") + public static let title = L10n.tr("Localizable", "Scene.Search.Title", fallback: "Search") public enum Recommend { /// See All - public static let buttonText = L10n.tr("Localizable", "Scene.Search.Recommend.ButtonText") + public static let buttonText = L10n.tr("Localizable", "Scene.Search.Recommend.ButtonText", fallback: "See All") public enum Accounts { /// You may like to follow these accounts - public static let description = L10n.tr("Localizable", "Scene.Search.Recommend.Accounts.Description") + public static let description = L10n.tr("Localizable", "Scene.Search.Recommend.Accounts.Description", fallback: "You may like to follow these accounts") /// Follow - public static let follow = L10n.tr("Localizable", "Scene.Search.Recommend.Accounts.Follow") + public static let follow = L10n.tr("Localizable", "Scene.Search.Recommend.Accounts.Follow", fallback: "Follow") /// Accounts you might like - public static let title = L10n.tr("Localizable", "Scene.Search.Recommend.Accounts.Title") + public static let title = L10n.tr("Localizable", "Scene.Search.Recommend.Accounts.Title", fallback: "Accounts you might like") } public enum HashTag { /// Hashtags that are getting quite a bit of attention - public static let description = L10n.tr("Localizable", "Scene.Search.Recommend.HashTag.Description") + public static let description = L10n.tr("Localizable", "Scene.Search.Recommend.HashTag.Description", fallback: "Hashtags that are getting quite a bit of attention") /// %@ people are talking public static func peopleTalking(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Search.Recommend.HashTag.PeopleTalking", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Search.Recommend.HashTag.PeopleTalking", String(describing: p1), fallback: "%@ people are talking") } /// Trending on Mastodon - public static let title = L10n.tr("Localizable", "Scene.Search.Recommend.HashTag.Title") + public static let title = L10n.tr("Localizable", "Scene.Search.Recommend.HashTag.Title", fallback: "Trending on Mastodon") } } public enum SearchBar { /// Cancel - public static let cancel = L10n.tr("Localizable", "Scene.Search.SearchBar.Cancel") + public static let cancel = L10n.tr("Localizable", "Scene.Search.SearchBar.Cancel", fallback: "Cancel") /// Search hashtags and users - public static let placeholder = L10n.tr("Localizable", "Scene.Search.SearchBar.Placeholder") + public static let placeholder = L10n.tr("Localizable", "Scene.Search.SearchBar.Placeholder", fallback: "Search hashtags and users") } public enum Searching { /// Clear - public static let clear = L10n.tr("Localizable", "Scene.Search.Searching.Clear") + public static let clear = L10n.tr("Localizable", "Scene.Search.Searching.Clear", fallback: "Clear") /// Recent searches - public static let recentSearch = L10n.tr("Localizable", "Scene.Search.Searching.RecentSearch") + public static let recentSearch = L10n.tr("Localizable", "Scene.Search.Searching.RecentSearch", fallback: "Recent searches") public enum EmptyState { /// No results - public static let noResults = L10n.tr("Localizable", "Scene.Search.Searching.EmptyState.NoResults") + public static let noResults = L10n.tr("Localizable", "Scene.Search.Searching.EmptyState.NoResults", fallback: "No results") } public enum Segment { /// All - public static let all = L10n.tr("Localizable", "Scene.Search.Searching.Segment.All") + public static let all = L10n.tr("Localizable", "Scene.Search.Searching.Segment.All", fallback: "All") /// Hashtags - public static let hashtags = L10n.tr("Localizable", "Scene.Search.Searching.Segment.Hashtags") + public static let hashtags = L10n.tr("Localizable", "Scene.Search.Searching.Segment.Hashtags", fallback: "Hashtags") /// People - public static let people = L10n.tr("Localizable", "Scene.Search.Searching.Segment.People") + public static let people = L10n.tr("Localizable", "Scene.Search.Searching.Segment.People", fallback: "People") /// Posts - public static let posts = L10n.tr("Localizable", "Scene.Search.Searching.Segment.Posts") + public static let posts = L10n.tr("Localizable", "Scene.Search.Searching.Segment.Posts", fallback: "Posts") } } } public enum ServerPicker { /// Pick a server based on your interests, region, or a general purpose one. - public static let subtitle = L10n.tr("Localizable", "Scene.ServerPicker.Subtitle") + public static let subtitle = L10n.tr("Localizable", "Scene.ServerPicker.Subtitle", fallback: "Pick a server based on your interests, region, or a general purpose one.") /// Pick a server based on your interests, region, or a general purpose one. Each server is operated by an entirely independent organization or individual. - public static let subtitleExtend = L10n.tr("Localizable", "Scene.ServerPicker.SubtitleExtend") + public static let subtitleExtend = L10n.tr("Localizable", "Scene.ServerPicker.SubtitleExtend", fallback: "Pick a server based on your interests, region, or a general purpose one. Each server is operated by an entirely independent organization or individual.") /// Mastodon is made of users in different servers. - public static let title = L10n.tr("Localizable", "Scene.ServerPicker.Title") + public static let title = L10n.tr("Localizable", "Scene.ServerPicker.Title", fallback: "Mastodon is made of users in different servers.") public enum Button { /// See Less - public static let seeLess = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeLess") + public static let seeLess = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeLess", fallback: "See Less") /// See More - public static let seeMore = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeMore") + public static let seeMore = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeMore", fallback: "See More") public enum Category { /// academia - public static let academia = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Academia") + public static let academia = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Academia", fallback: "academia") /// activism - public static let activism = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Activism") + public static let activism = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Activism", fallback: "activism") /// All - public static let all = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.All") + public static let all = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.All", fallback: "All") /// Category: All - public static let allAccessiblityDescription = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.AllAccessiblityDescription") + public static let allAccessiblityDescription = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.AllAccessiblityDescription", fallback: "Category: All") /// art - public static let art = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Art") + public static let art = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Art", fallback: "art") /// food - public static let food = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Food") + public static let food = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Food", fallback: "food") /// furry - public static let furry = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Furry") + public static let furry = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Furry", fallback: "furry") /// games - public static let games = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Games") + public static let games = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Games", fallback: "games") /// general - public static let general = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.General") + public static let general = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.General", fallback: "general") /// journalism - public static let journalism = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Journalism") + public static let journalism = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Journalism", fallback: "journalism") /// lgbt - public static let lgbt = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Lgbt") + public static let lgbt = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Lgbt", fallback: "lgbt") /// music - public static let music = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Music") + public static let music = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Music", fallback: "music") /// regional - public static let regional = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Regional") + public static let regional = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Regional", fallback: "regional") /// tech - public static let tech = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Tech") + public static let tech = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Tech", fallback: "tech") } } public enum EmptyState { /// Something went wrong while loading the data. Check your internet connection. - public static let badNetwork = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.BadNetwork") + public static let badNetwork = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.BadNetwork", fallback: "Something went wrong while loading the data. Check your internet connection.") /// Finding available servers... - public static let findingServers = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.FindingServers") + public static let findingServers = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.FindingServers", fallback: "Finding available servers...") /// No results - public static let noResults = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.NoResults") + public static let noResults = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.NoResults", fallback: "No results") } public enum Input { /// Search servers - public static let placeholder = L10n.tr("Localizable", "Scene.ServerPicker.Input.Placeholder") + public static let placeholder = L10n.tr("Localizable", "Scene.ServerPicker.Input.Placeholder", fallback: "Search servers") /// Search servers or enter URL - public static let searchServersOrEnterUrl = L10n.tr("Localizable", "Scene.ServerPicker.Input.SearchServersOrEnterUrl") + public static let searchServersOrEnterUrl = L10n.tr("Localizable", "Scene.ServerPicker.Input.SearchServersOrEnterUrl", fallback: "Search servers or enter URL") } public enum Label { /// CATEGORY - public static let category = L10n.tr("Localizable", "Scene.ServerPicker.Label.Category") + public static let category = L10n.tr("Localizable", "Scene.ServerPicker.Label.Category", fallback: "CATEGORY") /// LANGUAGE - public static let language = L10n.tr("Localizable", "Scene.ServerPicker.Label.Language") + public static let language = L10n.tr("Localizable", "Scene.ServerPicker.Label.Language", fallback: "LANGUAGE") /// USERS - public static let users = L10n.tr("Localizable", "Scene.ServerPicker.Label.Users") + public static let users = L10n.tr("Localizable", "Scene.ServerPicker.Label.Users", fallback: "USERS") } } public enum ServerRules { /// privacy policy - public static let privacyPolicy = L10n.tr("Localizable", "Scene.ServerRules.PrivacyPolicy") + public static let privacyPolicy = L10n.tr("Localizable", "Scene.ServerRules.PrivacyPolicy", fallback: "privacy policy") /// By continuing, you’re subject to the terms of service and privacy policy for %@. public static func prompt(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.ServerRules.Prompt", String(describing: p1)) + return L10n.tr("Localizable", "Scene.ServerRules.Prompt", String(describing: p1), fallback: "By continuing, you’re subject to the terms of service and privacy policy for %@.") } /// These are set and enforced by the %@ moderators. public static func subtitle(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.ServerRules.Subtitle", String(describing: p1)) + return L10n.tr("Localizable", "Scene.ServerRules.Subtitle", String(describing: p1), fallback: "These are set and enforced by the %@ moderators.") } /// terms of service - public static let termsOfService = L10n.tr("Localizable", "Scene.ServerRules.TermsOfService") + public static let termsOfService = L10n.tr("Localizable", "Scene.ServerRules.TermsOfService", fallback: "terms of service") /// Some ground rules. - public static let title = L10n.tr("Localizable", "Scene.ServerRules.Title") + public static let title = L10n.tr("Localizable", "Scene.ServerRules.Title", fallback: "Some ground rules.") public enum Button { /// I Agree - public static let confirm = L10n.tr("Localizable", "Scene.ServerRules.Button.Confirm") + public static let confirm = L10n.tr("Localizable", "Scene.ServerRules.Button.Confirm", fallback: "I Agree") } } public enum Settings { /// Settings - public static let title = L10n.tr("Localizable", "Scene.Settings.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Title", fallback: "Settings") public enum Footer { /// Mastodon is open source software. You can report issues on GitHub at %@ (%@) public static func mastodonDescription(_ p1: Any, _ p2: Any) -> String { - return L10n.tr("Localizable", "Scene.Settings.Footer.MastodonDescription", String(describing: p1), String(describing: p2)) + return L10n.tr("Localizable", "Scene.Settings.Footer.MastodonDescription", String(describing: p1), String(describing: p2), fallback: "Mastodon is open source software. You can report issues on GitHub at %@ (%@)") } } public enum Keyboard { /// Close Settings Window - public static let closeSettingsWindow = L10n.tr("Localizable", "Scene.Settings.Keyboard.CloseSettingsWindow") + public static let closeSettingsWindow = L10n.tr("Localizable", "Scene.Settings.Keyboard.CloseSettingsWindow", fallback: "Close Settings Window") } public enum Section { public enum Appearance { /// Automatic - public static let automatic = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Automatic") + public static let automatic = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Automatic", fallback: "Automatic") /// Always Dark - public static let dark = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Dark") + public static let dark = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Dark", fallback: "Always Dark") /// Always Light - public static let light = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Light") + public static let light = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Light", fallback: "Always Light") /// Appearance - public static let title = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Title", fallback: "Appearance") } public enum BoringZone { /// Account Settings - public static let accountSettings = L10n.tr("Localizable", "Scene.Settings.Section.BoringZone.AccountSettings") + public static let accountSettings = L10n.tr("Localizable", "Scene.Settings.Section.BoringZone.AccountSettings", fallback: "Account Settings") /// Privacy Policy - public static let privacy = L10n.tr("Localizable", "Scene.Settings.Section.BoringZone.Privacy") + public static let privacy = L10n.tr("Localizable", "Scene.Settings.Section.BoringZone.Privacy", fallback: "Privacy Policy") /// Terms of Service - public static let terms = L10n.tr("Localizable", "Scene.Settings.Section.BoringZone.Terms") + public static let terms = L10n.tr("Localizable", "Scene.Settings.Section.BoringZone.Terms", fallback: "Terms of Service") /// The Boring Zone - public static let title = L10n.tr("Localizable", "Scene.Settings.Section.BoringZone.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Section.BoringZone.Title", fallback: "The Boring Zone") } public enum LookAndFeel { /// Light - public static let light = L10n.tr("Localizable", "Scene.Settings.Section.LookAndFeel.Light") + public static let light = L10n.tr("Localizable", "Scene.Settings.Section.LookAndFeel.Light", fallback: "Light") /// Really Dark - public static let reallyDark = L10n.tr("Localizable", "Scene.Settings.Section.LookAndFeel.ReallyDark") + public static let reallyDark = L10n.tr("Localizable", "Scene.Settings.Section.LookAndFeel.ReallyDark", fallback: "Really Dark") /// Sorta Dark - public static let sortaDark = L10n.tr("Localizable", "Scene.Settings.Section.LookAndFeel.SortaDark") + public static let sortaDark = L10n.tr("Localizable", "Scene.Settings.Section.LookAndFeel.SortaDark", fallback: "Sorta Dark") /// Look and Feel - public static let title = L10n.tr("Localizable", "Scene.Settings.Section.LookAndFeel.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Section.LookAndFeel.Title", fallback: "Look and Feel") /// Use System - public static let useSystem = L10n.tr("Localizable", "Scene.Settings.Section.LookAndFeel.UseSystem") + public static let useSystem = L10n.tr("Localizable", "Scene.Settings.Section.LookAndFeel.UseSystem", fallback: "Use System") } public enum Notifications { /// Reblogs my post - public static let boosts = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Boosts") + public static let boosts = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Boosts", fallback: "Reblogs my post") /// Favorites my post - public static let favorites = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Favorites") + public static let favorites = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Favorites", fallback: "Favorites my post") /// Follows me - public static let follows = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Follows") + public static let follows = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Follows", fallback: "Follows me") /// Mentions me - public static let mentions = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Mentions") + public static let mentions = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Mentions", fallback: "Mentions me") /// Notifications - public static let title = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Title", fallback: "Notifications") public enum Trigger { /// anyone - public static let anyone = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Anyone") + public static let anyone = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Anyone", fallback: "anyone") /// anyone I follow - public static let follow = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Follow") + public static let follow = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Follow", fallback: "anyone I follow") /// a follower - public static let follower = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Follower") + public static let follower = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Follower", fallback: "a follower") /// no one - public static let noone = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Noone") + public static let noone = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Noone", fallback: "no one") /// Notify me when - public static let title = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Title", fallback: "Notify me when") } } public enum Preference { /// Disable animated avatars - public static let disableAvatarAnimation = L10n.tr("Localizable", "Scene.Settings.Section.Preference.DisableAvatarAnimation") + public static let disableAvatarAnimation = L10n.tr("Localizable", "Scene.Settings.Section.Preference.DisableAvatarAnimation", fallback: "Disable animated avatars") /// Disable animated emojis - public static let disableEmojiAnimation = L10n.tr("Localizable", "Scene.Settings.Section.Preference.DisableEmojiAnimation") + public static let disableEmojiAnimation = L10n.tr("Localizable", "Scene.Settings.Section.Preference.DisableEmojiAnimation", fallback: "Disable animated emojis") /// Open links in Mastodon - public static let openLinksInMastodon = L10n.tr("Localizable", "Scene.Settings.Section.Preference.OpenLinksInMastodon") + public static let openLinksInMastodon = L10n.tr("Localizable", "Scene.Settings.Section.Preference.OpenLinksInMastodon", fallback: "Open links in Mastodon") /// Preferences - public static let title = L10n.tr("Localizable", "Scene.Settings.Section.Preference.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Section.Preference.Title", fallback: "Preferences") /// True black dark mode - public static let trueBlackDarkMode = L10n.tr("Localizable", "Scene.Settings.Section.Preference.TrueBlackDarkMode") + public static let trueBlackDarkMode = L10n.tr("Localizable", "Scene.Settings.Section.Preference.TrueBlackDarkMode", fallback: "True black dark mode") /// Use default browser to open links - public static let usingDefaultBrowser = L10n.tr("Localizable", "Scene.Settings.Section.Preference.UsingDefaultBrowser") + public static let usingDefaultBrowser = L10n.tr("Localizable", "Scene.Settings.Section.Preference.UsingDefaultBrowser", fallback: "Use default browser to open links") } public enum SpicyZone { /// Clear Media Cache - public static let clear = L10n.tr("Localizable", "Scene.Settings.Section.SpicyZone.Clear") + public static let clear = L10n.tr("Localizable", "Scene.Settings.Section.SpicyZone.Clear", fallback: "Clear Media Cache") /// Sign Out - public static let signout = L10n.tr("Localizable", "Scene.Settings.Section.SpicyZone.Signout") + public static let signout = L10n.tr("Localizable", "Scene.Settings.Section.SpicyZone.Signout", fallback: "Sign Out") /// The Spicy Zone - public static let title = L10n.tr("Localizable", "Scene.Settings.Section.SpicyZone.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Section.SpicyZone.Title", fallback: "The Spicy Zone") } } } public enum SuggestionAccount { /// When you follow someone, you’ll see their posts in your home feed. - public static let followExplain = L10n.tr("Localizable", "Scene.SuggestionAccount.FollowExplain") + public static let followExplain = L10n.tr("Localizable", "Scene.SuggestionAccount.FollowExplain", fallback: "When you follow someone, you’ll see their posts in your home feed.") /// Find People to Follow - public static let title = L10n.tr("Localizable", "Scene.SuggestionAccount.Title") + public static let title = L10n.tr("Localizable", "Scene.SuggestionAccount.Title", fallback: "Find People to Follow") } public enum Thread { /// Post - public static let backTitle = L10n.tr("Localizable", "Scene.Thread.BackTitle") + public static let backTitle = L10n.tr("Localizable", "Scene.Thread.BackTitle", fallback: "Post") /// Post from %@ public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Thread.Title", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Thread.Title", String(describing: p1), fallback: "Post from %@") } } public enum Welcome { /// Get Started - public static let getStarted = L10n.tr("Localizable", "Scene.Welcome.GetStarted") + public static let getStarted = L10n.tr("Localizable", "Scene.Welcome.GetStarted", fallback: "Get Started") /// Log In - public static let logIn = L10n.tr("Localizable", "Scene.Welcome.LogIn") - /// Social networking\nback in your hands. - public static let slogan = L10n.tr("Localizable", "Scene.Welcome.Slogan") + public static let logIn = L10n.tr("Localizable", "Scene.Welcome.LogIn", fallback: "Log In") + /// Social networking + /// back in your hands. + public static let slogan = L10n.tr("Localizable", "Scene.Welcome.Slogan", fallback: "Social networking\nback in your hands.") } public enum Wizard { /// Double tap to dismiss this wizard - public static let accessibilityHint = L10n.tr("Localizable", "Scene.Wizard.AccessibilityHint") + public static let accessibilityHint = L10n.tr("Localizable", "Scene.Wizard.AccessibilityHint", fallback: "Double tap to dismiss this wizard") /// Switch between multiple accounts by holding the profile button. - public static let multipleAccountSwitchIntroDescription = L10n.tr("Localizable", "Scene.Wizard.MultipleAccountSwitchIntroDescription") + public static let multipleAccountSwitchIntroDescription = L10n.tr("Localizable", "Scene.Wizard.MultipleAccountSwitchIntroDescription", fallback: "Switch between multiple accounts by holding the profile button.") /// New in Mastodon - public static let newInMastodon = L10n.tr("Localizable", "Scene.Wizard.NewInMastodon") + public static let newInMastodon = L10n.tr("Localizable", "Scene.Wizard.NewInMastodon", fallback: "New in Mastodon") } } - public enum A11y { public enum Plural { public enum Count { /// Plural format key: "%#@character_count@ left" public static func charactersLeft(_ p1: Int) -> String { - return L10n.tr("Localizable", "a11y.plural.count.characters_left", p1) + return L10n.tr("Localizable", "a11y.plural.count.characters_left", p1, fallback: "Plural format key: \"%#@character_count@ left\"") } /// Plural format key: "Input limit exceeds %#@character_count@" public static func inputLimitExceeds(_ p1: Int) -> String { - return L10n.tr("Localizable", "a11y.plural.count.input_limit_exceeds", p1) + return L10n.tr("Localizable", "a11y.plural.count.input_limit_exceeds", p1, fallback: "Plural format key: \"Input limit exceeds %#@character_count@\"") } /// Plural format key: "Input limit remains %#@character_count@" public static func inputLimitRemains(_ p1: Int) -> String { - return L10n.tr("Localizable", "a11y.plural.count.input_limit_remains", p1) + return L10n.tr("Localizable", "a11y.plural.count.input_limit_remains", p1, fallback: "Plural format key: \"Input limit remains %#@character_count@\"") } public enum Unread { /// Plural format key: "%#@notification_count_unread_notification@" public static func notification(_ p1: Int) -> String { - return L10n.tr("Localizable", "a11y.plural.count.unread.notification", p1) + return L10n.tr("Localizable", "a11y.plural.count.unread.notification", p1, fallback: "Plural format key: \"%#@notification_count_unread_notification@\"") } } } } } - public enum Date { public enum Day { /// Plural format key: "%#@count_day_left@" public static func `left`(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.day.left", p1) + return L10n.tr("Localizable", "date.day.left", p1, fallback: "Plural format key: \"%#@count_day_left@\"") } public enum Ago { /// Plural format key: "%#@count_day_ago_abbr@" public static func abbr(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.day.ago.abbr", p1) + return L10n.tr("Localizable", "date.day.ago.abbr", p1, fallback: "Plural format key: \"%#@count_day_ago_abbr@\"") } } } public enum Hour { /// Plural format key: "%#@count_hour_left@" public static func `left`(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.hour.left", p1) + return L10n.tr("Localizable", "date.hour.left", p1, fallback: "Plural format key: \"%#@count_hour_left@\"") } public enum Ago { /// Plural format key: "%#@count_hour_ago_abbr@" public static func abbr(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.hour.ago.abbr", p1) + return L10n.tr("Localizable", "date.hour.ago.abbr", p1, fallback: "Plural format key: \"%#@count_hour_ago_abbr@\"") } } } public enum Minute { /// Plural format key: "%#@count_minute_left@" public static func `left`(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.minute.left", p1) + return L10n.tr("Localizable", "date.minute.left", p1, fallback: "Plural format key: \"%#@count_minute_left@\"") } public enum Ago { /// Plural format key: "%#@count_minute_ago_abbr@" public static func abbr(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.minute.ago.abbr", p1) + return L10n.tr("Localizable", "date.minute.ago.abbr", p1, fallback: "Plural format key: \"%#@count_minute_ago_abbr@\"") } } } public enum Month { /// Plural format key: "%#@count_month_left@" public static func `left`(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.month.left", p1) + return L10n.tr("Localizable", "date.month.left", p1, fallback: "Plural format key: \"%#@count_month_left@\"") } public enum Ago { /// Plural format key: "%#@count_month_ago_abbr@" public static func abbr(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.month.ago.abbr", p1) + return L10n.tr("Localizable", "date.month.ago.abbr", p1, fallback: "Plural format key: \"%#@count_month_ago_abbr@\"") } } } public enum Second { /// Plural format key: "%#@count_second_left@" public static func `left`(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.second.left", p1) + return L10n.tr("Localizable", "date.second.left", p1, fallback: "Plural format key: \"%#@count_second_left@\"") } public enum Ago { /// Plural format key: "%#@count_second_ago_abbr@" public static func abbr(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.second.ago.abbr", p1) + return L10n.tr("Localizable", "date.second.ago.abbr", p1, fallback: "Plural format key: \"%#@count_second_ago_abbr@\"") } } } public enum Year { /// Plural format key: "%#@count_year_left@" public static func `left`(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.year.left", p1) + return L10n.tr("Localizable", "date.year.left", p1, fallback: "Plural format key: \"%#@count_year_left@\"") } public enum Ago { /// Plural format key: "%#@count_year_ago_abbr@" public static func abbr(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.year.ago.abbr", p1) + return L10n.tr("Localizable", "date.year.ago.abbr", p1, fallback: "Plural format key: \"%#@count_year_ago_abbr@\"") } } } } - public enum Plural { /// Plural format key: "%#@count_people_talking@" public static func peopleTalking(_ p1: Int) -> String { - return L10n.tr("Localizable", "plural.people_talking", p1) + return L10n.tr("Localizable", "plural.people_talking", p1, fallback: "Plural format key: \"%#@count_people_talking@\"") } public enum Count { /// Plural format key: "%#@favorite_count@" public static func favorite(_ p1: Int) -> String { - return L10n.tr("Localizable", "plural.count.favorite", p1) + return L10n.tr("Localizable", "plural.count.favorite", p1, fallback: "Plural format key: \"%#@favorite_count@\"") } /// Plural format key: "%#@names@%#@count_mutual@" public static func followedByAndMutual(_ p1: Int, _ p2: Int) -> String { - return L10n.tr("Localizable", "plural.count.followed_by_and_mutual", p1, p2) + return L10n.tr("Localizable", "plural.count.followed_by_and_mutual", p1, p2, fallback: "Plural format key: \"%#@names@%#@count_mutual@\"") } /// Plural format key: "%#@count_follower@" public static func follower(_ p1: Int) -> String { - return L10n.tr("Localizable", "plural.count.follower", p1) + return L10n.tr("Localizable", "plural.count.follower", p1, fallback: "Plural format key: \"%#@count_follower@\"") } /// Plural format key: "%#@count_following@" public static func following(_ p1: Int) -> String { - return L10n.tr("Localizable", "plural.count.following", p1) + return L10n.tr("Localizable", "plural.count.following", p1, fallback: "Plural format key: \"%#@count_following@\"") } /// Plural format key: "%#@media_count@" public static func media(_ p1: Int) -> String { - return L10n.tr("Localizable", "plural.count.media", p1) + return L10n.tr("Localizable", "plural.count.media", p1, fallback: "Plural format key: \"%#@media_count@\"") } /// Plural format key: "%#@post_count@" public static func post(_ p1: Int) -> String { - return L10n.tr("Localizable", "plural.count.post", p1) + return L10n.tr("Localizable", "plural.count.post", p1, fallback: "Plural format key: \"%#@post_count@\"") } /// Plural format key: "%#@reblog_count@" public static func reblog(_ p1: Int) -> String { - return L10n.tr("Localizable", "plural.count.reblog", p1) + return L10n.tr("Localizable", "plural.count.reblog", p1, fallback: "Plural format key: \"%#@reblog_count@\"") } /// Plural format key: "%#@reply_count@" public static func reply(_ p1: Int) -> String { - return L10n.tr("Localizable", "plural.count.reply", p1) + return L10n.tr("Localizable", "plural.count.reply", p1, fallback: "Plural format key: \"%#@reply_count@\"") } /// Plural format key: "%#@vote_count@" public static func vote(_ p1: Int) -> String { - return L10n.tr("Localizable", "plural.count.vote", p1) + return L10n.tr("Localizable", "plural.count.vote", p1, fallback: "Plural format key: \"%#@vote_count@\"") } /// Plural format key: "%#@voter_count@" public static func voter(_ p1: Int) -> String { - return L10n.tr("Localizable", "plural.count.voter", p1) + return L10n.tr("Localizable", "plural.count.voter", p1, fallback: "Plural format key: \"%#@voter_count@\"") } public enum MetricFormatted { /// Plural format key: "%@ %#@post_count@" public static func post(_ p1: Any, _ p2: Int) -> String { - return L10n.tr("Localizable", "plural.count.metric_formatted.post", String(describing: p1), p2) + return L10n.tr("Localizable", "plural.count.metric_formatted.post", String(describing: p1), p2, fallback: "Plural format key: \"%@ %#@post_count@\"") } } } @@ -1448,8 +1460,8 @@ public enum L10n { // MARK: - Implementation Details extension L10n { - private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { - let format = Bundle.module.localizedString(forKey: key, value: nil, table: table) + private static func tr(_ table: String, _ key: String, _ args: CVarArg..., fallback value: String) -> String { + let format = Bundle.module.localizedString(forKey: key, value: value, table: table) return String(format: format, locale: Locale.current, arguments: args) } } diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings new file mode 100644 index 000000000..a352b0526 --- /dev/null +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -0,0 +1,461 @@ +"Common.Alerts.BlockDomain.BlockEntireDomain" = "Block Domain"; +"Common.Alerts.BlockDomain.Title" = "Are you really, really sure you want to block the entire %@? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain and any of your followers from that domain will be removed."; +"Common.Alerts.CleanCache.Message" = "Successfully cleaned %@ cache."; +"Common.Alerts.CleanCache.Title" = "Clean Cache"; +"Common.Alerts.Common.PleaseTryAgain" = "Please try again."; +"Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later."; +"Common.Alerts.DeletePost.Message" = "Are you sure you want to delete this post?"; +"Common.Alerts.DeletePost.Title" = "Delete Post"; +"Common.Alerts.DiscardPostContent.Message" = "Confirm to discard composed post content."; +"Common.Alerts.DiscardPostContent.Title" = "Discard Draft"; +"Common.Alerts.EditProfileFailure.Message" = "Cannot edit profile. Please try again."; +"Common.Alerts.EditProfileFailure.Title" = "Edit Profile Error"; +"Common.Alerts.PublishPostFailure.AttachmentsMessage.MoreThanOneVideo" = "Cannot attach more than one video."; +"Common.Alerts.PublishPostFailure.AttachmentsMessage.VideoAttachWithPhoto" = "Cannot attach a video to a post that already contains images."; +"Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post. +Please check your internet connection."; +"Common.Alerts.PublishPostFailure.Title" = "Publish Failure"; +"Common.Alerts.SavePhotoFailure.Message" = "Please enable the photo library access permission to save the photo."; +"Common.Alerts.SavePhotoFailure.Title" = "Save Photo Failure"; +"Common.Alerts.ServerError.Title" = "Server Error"; +"Common.Alerts.SignOut.Confirm" = "Sign Out"; +"Common.Alerts.SignOut.Message" = "Are you sure you want to sign out?"; +"Common.Alerts.SignOut.Title" = "Sign Out"; +"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure"; +"Common.Alerts.VoteFailure.PollEnded" = "The poll has ended"; +"Common.Alerts.VoteFailure.Title" = "Vote Failure"; +"Common.Controls.Actions.Add" = "Add"; +"Common.Controls.Actions.Back" = "Back"; +"Common.Controls.Actions.BlockDomain" = "Block %@"; +"Common.Controls.Actions.Cancel" = "Cancel"; +"Common.Controls.Actions.Compose" = "Compose"; +"Common.Controls.Actions.Confirm" = "Confirm"; +"Common.Controls.Actions.Continue" = "Continue"; +"Common.Controls.Actions.CopyPhoto" = "Copy Photo"; +"Common.Controls.Actions.Delete" = "Delete"; +"Common.Controls.Actions.Discard" = "Discard"; +"Common.Controls.Actions.Done" = "Done"; +"Common.Controls.Actions.Edit" = "Edit"; +"Common.Controls.Actions.FindPeople" = "Find people to follow"; +"Common.Controls.Actions.ManuallySearch" = "Manually search instead"; +"Common.Controls.Actions.Next" = "Next"; +"Common.Controls.Actions.Ok" = "OK"; +"Common.Controls.Actions.Open" = "Open"; +"Common.Controls.Actions.OpenInBrowser" = "Open in Browser"; +"Common.Controls.Actions.OpenInSafari" = "Open in Safari"; +"Common.Controls.Actions.Preview" = "Preview"; +"Common.Controls.Actions.Previous" = "Previous"; +"Common.Controls.Actions.Remove" = "Remove"; +"Common.Controls.Actions.Reply" = "Reply"; +"Common.Controls.Actions.ReportUser" = "Report %@"; +"Common.Controls.Actions.Save" = "Save"; +"Common.Controls.Actions.SavePhoto" = "Save Photo"; +"Common.Controls.Actions.SeeMore" = "See More"; +"Common.Controls.Actions.Settings" = "Settings"; +"Common.Controls.Actions.Share" = "Share"; +"Common.Controls.Actions.SharePost" = "Share Post"; +"Common.Controls.Actions.ShareUser" = "Share %@"; +"Common.Controls.Actions.SignIn" = "Sign In"; +"Common.Controls.Actions.SignUp" = "Sign Up"; +"Common.Controls.Actions.Skip" = "Skip"; +"Common.Controls.Actions.TakePhoto" = "Take Photo"; +"Common.Controls.Actions.TryAgain" = "Try Again"; +"Common.Controls.Actions.UnblockDomain" = "Unblock %@"; +"Common.Controls.Friendship.Block" = "Block"; +"Common.Controls.Friendship.BlockDomain" = "Block %@"; +"Common.Controls.Friendship.BlockUser" = "Block %@"; +"Common.Controls.Friendship.Blocked" = "Blocked"; +"Common.Controls.Friendship.EditInfo" = "Edit Info"; +"Common.Controls.Friendship.Follow" = "Follow"; +"Common.Controls.Friendship.Following" = "Following"; +"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; +"Common.Controls.Friendship.Mute" = "Mute"; +"Common.Controls.Friendship.MuteUser" = "Mute %@"; +"Common.Controls.Friendship.Muted" = "Muted"; +"Common.Controls.Friendship.Pending" = "Pending"; +"Common.Controls.Friendship.Request" = "Request"; +"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; +"Common.Controls.Friendship.Unblock" = "Unblock"; +"Common.Controls.Friendship.UnblockUser" = "Unblock %@"; +"Common.Controls.Friendship.Unmute" = "Unmute"; +"Common.Controls.Friendship.UnmuteUser" = "Unmute %@"; +"Common.Controls.Keyboard.Common.ComposeNewPost" = "Compose New Post"; +"Common.Controls.Keyboard.Common.OpenSettings" = "Open Settings"; +"Common.Controls.Keyboard.Common.ShowFavorites" = "Show Favorites"; +"Common.Controls.Keyboard.Common.SwitchToTab" = "Switch to %@"; +"Common.Controls.Keyboard.SegmentedControl.NextSection" = "Next Section"; +"Common.Controls.Keyboard.SegmentedControl.PreviousSection" = "Previous Section"; +"Common.Controls.Keyboard.Timeline.NextStatus" = "Next Post"; +"Common.Controls.Keyboard.Timeline.OpenAuthorProfile" = "Open Author's Profile"; +"Common.Controls.Keyboard.Timeline.OpenRebloggerProfile" = "Open Reblogger's Profile"; +"Common.Controls.Keyboard.Timeline.OpenStatus" = "Open Post"; +"Common.Controls.Keyboard.Timeline.PreviewImage" = "Preview Image"; +"Common.Controls.Keyboard.Timeline.PreviousStatus" = "Previous Post"; +"Common.Controls.Keyboard.Timeline.ReplyStatus" = "Reply to Post"; +"Common.Controls.Keyboard.Timeline.ToggleContentWarning" = "Toggle Content Warning"; +"Common.Controls.Keyboard.Timeline.ToggleFavorite" = "Toggle Favorite on Post"; +"Common.Controls.Keyboard.Timeline.ToggleReblog" = "Toggle Reblog on Post"; +"Common.Controls.Status.Actions.Favorite" = "Favorite"; +"Common.Controls.Status.Actions.Hide" = "Hide"; +"Common.Controls.Status.Actions.Menu" = "Menu"; +"Common.Controls.Status.Actions.Reblog" = "Reblog"; +"Common.Controls.Status.Actions.Reply" = "Reply"; +"Common.Controls.Status.Actions.ShowGif" = "Show GIF"; +"Common.Controls.Status.Actions.ShowImage" = "Show image"; +"Common.Controls.Status.Actions.ShowVideoPlayer" = "Show video player"; +"Common.Controls.Status.Actions.TapThenHoldToShowMenu" = "Tap then hold to show menu"; +"Common.Controls.Status.Actions.Unfavorite" = "Unfavorite"; +"Common.Controls.Status.Actions.Unreblog" = "Undo reblog"; +"Common.Controls.Status.ContentWarning" = "Content Warning"; +"Common.Controls.Status.MediaContentWarning" = "Tap anywhere to reveal"; +"Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Link: %@"; +"Common.Controls.Status.Poll.Closed" = "Closed"; +"Common.Controls.Status.Poll.Vote" = "Vote"; +"Common.Controls.Status.SensitiveContent" = "Sensitive Content"; +"Common.Controls.Status.ShowPost" = "Show Post"; +"Common.Controls.Status.ShowUserProfile" = "Show user profile"; +"Common.Controls.Status.Tag.Email" = "Email"; +"Common.Controls.Status.Tag.Emoji" = "Emoji"; +"Common.Controls.Status.Tag.Hashtag" = "Hashtag"; +"Common.Controls.Status.Tag.Link" = "Link"; +"Common.Controls.Status.Tag.Mention" = "Mention"; +"Common.Controls.Status.Tag.Url" = "URL"; +"Common.Controls.Status.TapToReveal" = "Tap to reveal"; +"Common.Controls.Status.UserReblogged" = "%@ reblogged"; +"Common.Controls.Status.UserRepliedTo" = "Replied to %@"; +"Common.Controls.Status.Visibility.Direct" = "Only mentioned user can see this post."; +"Common.Controls.Status.Visibility.Private" = "Only their followers can see this post."; +"Common.Controls.Status.Visibility.PrivateFromMe" = "Only my followers can see this post."; +"Common.Controls.Status.Visibility.Unlisted" = "Everyone can see this post but not display in the public timeline."; +"Common.Controls.Tabs.Home" = "Home"; +"Common.Controls.Tabs.Notification" = "Notification"; +"Common.Controls.Tabs.Profile" = "Profile"; +"Common.Controls.Tabs.Search" = "Search"; +"Common.Controls.Timeline.Filtered" = "Filtered"; +"Common.Controls.Timeline.Header.BlockedWarning" = "You can’t view this user’s profile +until they unblock you."; +"Common.Controls.Timeline.Header.BlockingWarning" = "You can’t view this user's profile +until you unblock them. +Your profile looks like this to them."; +"Common.Controls.Timeline.Header.NoStatusFound" = "No Post Found"; +"Common.Controls.Timeline.Header.SuspendedWarning" = "This user has been suspended."; +"Common.Controls.Timeline.Header.UserBlockedWarning" = "You can’t view %@’s profile +until they unblock you."; +"Common.Controls.Timeline.Header.UserBlockingWarning" = "You can’t view %@’s profile +until you unblock them. +Your profile looks like this to them."; +"Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@’s account has been suspended."; +"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts"; +"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts..."; +"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies"; +"Common.Controls.Timeline.Timestamp.Now" = "Now"; +"Scene.AccountList.AddAccount" = "Add Account"; +"Scene.AccountList.DismissAccountSwitcher" = "Dismiss Account Switcher"; +"Scene.AccountList.TabBarHint" = "Current selected profile: %@. Double tap then hold to show account switcher"; +"Scene.Bookmark.Title" = "Bookmarks"; +"Scene.Compose.Accessibility.AppendAttachment" = "Add Attachment"; +"Scene.Compose.Accessibility.AppendPoll" = "Add Poll"; +"Scene.Compose.Accessibility.CustomEmojiPicker" = "Custom Emoji Picker"; +"Scene.Compose.Accessibility.DisableContentWarning" = "Disable Content Warning"; +"Scene.Compose.Accessibility.EnableContentWarning" = "Enable Content Warning"; +"Scene.Compose.Accessibility.PostOptions" = "Post Options"; +"Scene.Compose.Accessibility.PostVisibilityMenu" = "Post Visibility Menu"; +"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; +"Scene.Compose.Accessibility.RemovePoll" = "Remove Poll"; +"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can’t be +uploaded to Mastodon."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not recognize this media attachment"; +"Scene.Compose.Attachment.CompressingState" = "Compressing..."; +"Scene.Compose.Attachment.DescriptionPhoto" = "Describe the photo for the visually-impaired..."; +"Scene.Compose.Attachment.DescriptionVideo" = "Describe the video for the visually-impaired..."; +"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; +"Scene.Compose.Attachment.Photo" = "photo"; +"Scene.Compose.Attachment.ServerProcessingState" = "Server Processing..."; +"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; +"Scene.Compose.Attachment.Video" = "video"; +"Scene.Compose.AutoComplete.SpaceToAdd" = "Space to add"; +"Scene.Compose.ComposeAction" = "Publish"; +"Scene.Compose.ContentInputPlaceholder" = "Type or paste what’s on your mind"; +"Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here..."; +"Scene.Compose.Keyboard.AppendAttachmentEntry" = "Add Attachment - %@"; +"Scene.Compose.Keyboard.DiscardPost" = "Discard Post"; +"Scene.Compose.Keyboard.PublishPost" = "Publish Post"; +"Scene.Compose.Keyboard.SelectVisibilityEntry" = "Select Visibility - %@"; +"Scene.Compose.Keyboard.ToggleContentWarning" = "Toggle Content Warning"; +"Scene.Compose.Keyboard.TogglePoll" = "Toggle Poll"; +"Scene.Compose.MediaSelection.Browse" = "Browse"; +"Scene.Compose.MediaSelection.Camera" = "Take Photo"; +"Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library"; +"Scene.Compose.Poll.DurationTime" = "Duration: %@"; +"Scene.Compose.Poll.OneDay" = "1 Day"; +"Scene.Compose.Poll.OneHour" = "1 Hour"; +"Scene.Compose.Poll.OptionNumber" = "Option %ld"; +"Scene.Compose.Poll.SevenDays" = "7 Days"; +"Scene.Compose.Poll.SixHours" = "6 Hours"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; +"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; +"Scene.Compose.Poll.ThirtyMinutes" = "30 minutes"; +"Scene.Compose.Poll.ThreeDays" = "3 Days"; +"Scene.Compose.ReplyingToUser" = "replying to %@"; +"Scene.Compose.Title.NewPost" = "New Post"; +"Scene.Compose.Title.NewReply" = "New Reply"; +"Scene.Compose.Visibility.Direct" = "Only people I mention"; +"Scene.Compose.Visibility.Private" = "Followers only"; +"Scene.Compose.Visibility.Public" = "Public"; +"Scene.Compose.Visibility.Unlisted" = "Unlisted"; +"Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App"; +"Scene.ConfirmEmail.Button.Resend" = "Resend"; +"Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you haven’t."; +"Scene.ConfirmEmail.DontReceiveEmail.ResendEmail" = "Resend Email"; +"Scene.ConfirmEmail.DontReceiveEmail.Title" = "Check your email"; +"Scene.ConfirmEmail.OpenEmailApp.Description" = "We just sent you an email. Check your junk folder if you haven’t."; +"Scene.ConfirmEmail.OpenEmailApp.Mail" = "Mail"; +"Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "Open Email Client"; +"Scene.ConfirmEmail.OpenEmailApp.Title" = "Check your inbox."; +"Scene.ConfirmEmail.Subtitle" = "Tap the link we emailed to you to verify your account."; +"Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount" = "Tap the link we emailed to you to verify your account"; +"Scene.ConfirmEmail.Title" = "One last thing."; +"Scene.Discovery.Intro" = "These are the posts gaining traction in your corner of Mastodon."; +"Scene.Discovery.Tabs.Community" = "Community"; +"Scene.Discovery.Tabs.ForYou" = "For You"; +"Scene.Discovery.Tabs.Hashtags" = "Hashtags"; +"Scene.Discovery.Tabs.News" = "News"; +"Scene.Discovery.Tabs.Posts" = "Posts"; +"Scene.Familiarfollowers.FollowedByNames" = "Followed by %@"; +"Scene.Familiarfollowers.Title" = "Followers you familiar"; +"Scene.Favorite.Title" = "Your Favorites"; +"Scene.FavoritedBy.Title" = "Favorited By"; +"Scene.Follower.Footer" = "Followers from other servers are not displayed."; +"Scene.Follower.Title" = "follower"; +"Scene.Following.Footer" = "Follows from other servers are not displayed."; +"Scene.Following.Title" = "following"; +"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint" = "Tap to scroll to top and tap again to previous location"; +"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel" = "Logo Button"; +"Scene.HomeTimeline.NavigationBarState.NewPosts" = "See new posts"; +"Scene.HomeTimeline.NavigationBarState.Offline" = "Offline"; +"Scene.HomeTimeline.NavigationBarState.Published" = "Published!"; +"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post..."; +"Scene.HomeTimeline.Title" = "Home"; +"Scene.Notification.FollowRequest.Accept" = "Accept"; +"Scene.Notification.FollowRequest.Accepted" = "Accepted"; +"Scene.Notification.FollowRequest.Reject" = "reject"; +"Scene.Notification.FollowRequest.Rejected" = "Rejected"; +"Scene.Notification.Keyobard.ShowEverything" = "Show Everything"; +"Scene.Notification.Keyobard.ShowMentions" = "Show Mentions"; +"Scene.Notification.NotificationDescription.FavoritedYourPost" = "favorited your post"; +"Scene.Notification.NotificationDescription.FollowedYou" = "followed you"; +"Scene.Notification.NotificationDescription.MentionedYou" = "mentioned you"; +"Scene.Notification.NotificationDescription.PollHasEnded" = "poll has ended"; +"Scene.Notification.NotificationDescription.RebloggedYourPost" = "reblogged your post"; +"Scene.Notification.NotificationDescription.RequestToFollowYou" = "request to follow you"; +"Scene.Notification.Title.Everything" = "Everything"; +"Scene.Notification.Title.Mentions" = "Mentions"; +"Scene.Preview.Keyboard.ClosePreview" = "Close Preview"; +"Scene.Preview.Keyboard.ShowNext" = "Show Next"; +"Scene.Preview.Keyboard.ShowPrevious" = "Show Previous"; +"Scene.Profile.Accessibility.DoubleTapToOpenTheList" = "Double tap to open the list"; +"Scene.Profile.Accessibility.EditAvatarImage" = "Edit avatar image"; +"Scene.Profile.Accessibility.ShowAvatarImage" = "Show avatar image"; +"Scene.Profile.Accessibility.ShowBannerImage" = "Show banner image"; +"Scene.Profile.Dashboard.Followers" = "followers"; +"Scene.Profile.Dashboard.Following" = "following"; +"Scene.Profile.Dashboard.Posts" = "posts"; +"Scene.Profile.Fields.AddRow" = "Add Row"; +"Scene.Profile.Fields.Placeholder.Content" = "Content"; +"Scene.Profile.Fields.Placeholder.Label" = "Label"; +"Scene.Profile.Header.FollowsYou" = "Follows You"; +"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirm to block %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Block Account"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Confirm to mute %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Mute Account"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Confirm to unblock %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Unblock Account"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirm to unmute %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title" = "Unmute Account"; +"Scene.Profile.SegmentedControl.About" = "About"; +"Scene.Profile.SegmentedControl.Media" = "Media"; +"Scene.Profile.SegmentedControl.Posts" = "Posts"; +"Scene.Profile.SegmentedControl.PostsAndReplies" = "Posts and Replies"; +"Scene.Profile.SegmentedControl.Replies" = "Replies"; +"Scene.RebloggedBy.Title" = "Reblogged By"; +"Scene.Register.Error.Item.Agreement" = "Agreement"; +"Scene.Register.Error.Item.Email" = "Email"; +"Scene.Register.Error.Item.Locale" = "Locale"; +"Scene.Register.Error.Item.Password" = "Password"; +"Scene.Register.Error.Item.Reason" = "Reason"; +"Scene.Register.Error.Item.Username" = "Username"; +"Scene.Register.Error.Reason.Accepted" = "%@ must be accepted"; +"Scene.Register.Error.Reason.Blank" = "%@ is required"; +"Scene.Register.Error.Reason.Blocked" = "%@ contains a disallowed email provider"; +"Scene.Register.Error.Reason.Inclusion" = "%@ is not a supported value"; +"Scene.Register.Error.Reason.Invalid" = "%@ is invalid"; +"Scene.Register.Error.Reason.Reserved" = "%@ is a reserved keyword"; +"Scene.Register.Error.Reason.Taken" = "%@ is already in use"; +"Scene.Register.Error.Reason.TooLong" = "%@ is too long"; +"Scene.Register.Error.Reason.TooShort" = "%@ is too short"; +"Scene.Register.Error.Reason.Unreachable" = "%@ does not seem to exist"; +"Scene.Register.Error.Special.EmailInvalid" = "This is not a valid email address"; +"Scene.Register.Error.Special.PasswordTooShort" = "Password is too short (must be at least 8 characters)"; +"Scene.Register.Error.Special.UsernameInvalid" = "Username must only contain alphanumeric characters and underscores"; +"Scene.Register.Error.Special.UsernameTooLong" = "Username is too long (can’t be longer than 30 characters)"; +"Scene.Register.Input.Avatar.Delete" = "Delete"; +"Scene.Register.Input.DisplayName.Placeholder" = "display name"; +"Scene.Register.Input.Email.Placeholder" = "email"; +"Scene.Register.Input.Invite.RegistrationUserInviteRequest" = "Why do you want to join?"; +"Scene.Register.Input.Password.Accessibility.Checked" = "checked"; +"Scene.Register.Input.Password.Accessibility.Unchecked" = "unchecked"; +"Scene.Register.Input.Password.CharacterLimit" = "8 characters"; +"Scene.Register.Input.Password.Hint" = "Your password needs at least eight characters"; +"Scene.Register.Input.Password.Placeholder" = "password"; +"Scene.Register.Input.Password.Require" = "Your password needs at least:"; +"Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken."; +"Scene.Register.Input.Username.Placeholder" = "username"; +"Scene.Register.LetsGetYouSetUpOnDomain" = "Let’s get you set up on %@"; +"Scene.Register.Title" = "Let’s get you set up on %@"; +"Scene.Report.Content1" = "Are there any other posts you’d like to add to the report?"; +"Scene.Report.Content2" = "Is there anything the moderators should know about this report?"; +"Scene.Report.ReportSentTitle" = "Thanks for reporting, we’ll look into this."; +"Scene.Report.Reported" = "REPORTED"; +"Scene.Report.Send" = "Send Report"; +"Scene.Report.SkipToSend" = "Send without comment"; +"Scene.Report.Step1" = "Step 1 of 2"; +"Scene.Report.Step2" = "Step 2 of 2"; +"Scene.Report.StepFinal.BlockUser" = "Block %@"; +"Scene.Report.StepFinal.DontWantToSeeThis" = "Don’t want to see this?"; +"Scene.Report.StepFinal.MuteUser" = "Mute %@"; +"Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked" = "They will no longer be able to follow or see your posts, but they can see if they’ve been blocked."; +"Scene.Report.StepFinal.Unfollow" = "Unfollow"; +"Scene.Report.StepFinal.UnfollowUser" = "Unfollow %@"; +"Scene.Report.StepFinal.Unfollowed" = "Unfollowed"; +"Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "When you see something you don’t like on Mastodon, you can remove the person from your experience."; +"Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser" = "While we review this, you can take action against %@"; +"Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "You won’t see their posts or reblogs in your home feed. They won’t know they’ve been muted."; +"Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "Is there anything else we should know?"; +"Scene.Report.StepFour.Step4Of4" = "Step 4 of 4"; +"Scene.Report.StepOne.IDontLikeIt" = "I don’t like it"; +"Scene.Report.StepOne.ItIsNotSomethingYouWantToSee" = "It is not something you want to see"; +"Scene.Report.StepOne.ItViolatesServerRules" = "It violates server rules"; +"Scene.Report.StepOne.ItsSomethingElse" = "It’s something else"; +"Scene.Report.StepOne.ItsSpam" = "It’s spam"; +"Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies" = "Malicious links, fake engagement, or repetetive replies"; +"Scene.Report.StepOne.SelectTheBestMatch" = "Select the best match"; +"Scene.Report.StepOne.Step1Of4" = "Step 1 of 4"; +"Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories" = "The issue does not fit into other categories"; +"Scene.Report.StepOne.WhatsWrongWithThisAccount" = "What's wrong with this account?"; +"Scene.Report.StepOne.WhatsWrongWithThisPost" = "What's wrong with this post?"; +"Scene.Report.StepOne.WhatsWrongWithThisUsername" = "What's wrong with %@?"; +"Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules" = "You are aware that it breaks specific rules"; +"Scene.Report.StepThree.AreThereAnyPostsThatBackUpThisReport" = "Are there any posts that back up this report?"; +"Scene.Report.StepThree.SelectAllThatApply" = "Select all that apply"; +"Scene.Report.StepThree.Step3Of4" = "Step 3 of 4"; +"Scene.Report.StepTwo.IJustDon’tLikeIt" = "I just don’t like it"; +"Scene.Report.StepTwo.SelectAllThatApply" = "Select all that apply"; +"Scene.Report.StepTwo.Step2Of4" = "Step 2 of 4"; +"Scene.Report.StepTwo.WhichRulesAreBeingViolated" = "Which rules are being violated?"; +"Scene.Report.TextPlaceholder" = "Type or paste additional comments"; +"Scene.Report.Title" = "Report %@"; +"Scene.Report.TitleReport" = "Report"; +"Scene.Search.Recommend.Accounts.Description" = "You may like to follow these accounts"; +"Scene.Search.Recommend.Accounts.Follow" = "Follow"; +"Scene.Search.Recommend.Accounts.Title" = "Accounts you might like"; +"Scene.Search.Recommend.ButtonText" = "See All"; +"Scene.Search.Recommend.HashTag.Description" = "Hashtags that are getting quite a bit of attention"; +"Scene.Search.Recommend.HashTag.PeopleTalking" = "%@ people are talking"; +"Scene.Search.Recommend.HashTag.Title" = "Trending on Mastodon"; +"Scene.Search.SearchBar.Cancel" = "Cancel"; +"Scene.Search.SearchBar.Placeholder" = "Search hashtags and users"; +"Scene.Search.Searching.Clear" = "Clear"; +"Scene.Search.Searching.EmptyState.NoResults" = "No results"; +"Scene.Search.Searching.RecentSearch" = "Recent searches"; +"Scene.Search.Searching.Segment.All" = "All"; +"Scene.Search.Searching.Segment.Hashtags" = "Hashtags"; +"Scene.Search.Searching.Segment.People" = "People"; +"Scene.Search.Searching.Segment.Posts" = "Posts"; +"Scene.Search.Title" = "Search"; +"Scene.ServerPicker.Button.Category.Academia" = "academia"; +"Scene.ServerPicker.Button.Category.Activism" = "activism"; +"Scene.ServerPicker.Button.Category.All" = "All"; +"Scene.ServerPicker.Button.Category.AllAccessiblityDescription" = "Category: All"; +"Scene.ServerPicker.Button.Category.Art" = "art"; +"Scene.ServerPicker.Button.Category.Food" = "food"; +"Scene.ServerPicker.Button.Category.Furry" = "furry"; +"Scene.ServerPicker.Button.Category.Games" = "games"; +"Scene.ServerPicker.Button.Category.General" = "general"; +"Scene.ServerPicker.Button.Category.Journalism" = "journalism"; +"Scene.ServerPicker.Button.Category.Lgbt" = "lgbt"; +"Scene.ServerPicker.Button.Category.Music" = "music"; +"Scene.ServerPicker.Button.Category.Regional" = "regional"; +"Scene.ServerPicker.Button.Category.Tech" = "tech"; +"Scene.ServerPicker.Button.SeeLess" = "See Less"; +"Scene.ServerPicker.Button.SeeMore" = "See More"; +"Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading the data. Check your internet connection."; +"Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers..."; +"Scene.ServerPicker.EmptyState.NoResults" = "No results"; +"Scene.ServerPicker.Input.Placeholder" = "Search servers"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search servers or enter URL"; +"Scene.ServerPicker.Label.Category" = "CATEGORY"; +"Scene.ServerPicker.Label.Language" = "LANGUAGE"; +"Scene.ServerPicker.Label.Users" = "USERS"; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your interests, region, or a general purpose one."; +"Scene.ServerPicker.SubtitleExtend" = "Pick a server based on your interests, region, or a general purpose one. Each server is operated by an entirely independent organization or individual."; +"Scene.ServerPicker.Title" = "Mastodon is made of users in different servers."; +"Scene.ServerRules.Button.Confirm" = "I Agree"; +"Scene.ServerRules.PrivacyPolicy" = "privacy policy"; +"Scene.ServerRules.Prompt" = "By continuing, you’re subject to the terms of service and privacy policy for %@."; +"Scene.ServerRules.Subtitle" = "These are set and enforced by the %@ moderators."; +"Scene.ServerRules.TermsOfService" = "terms of service"; +"Scene.ServerRules.Title" = "Some ground rules."; +"Scene.Settings.Footer.MastodonDescription" = "Mastodon is open source software. You can report issues on GitHub at %@ (%@)"; +"Scene.Settings.Keyboard.CloseSettingsWindow" = "Close Settings Window"; +"Scene.Settings.Section.Appearance.Automatic" = "Automatic"; +"Scene.Settings.Section.Appearance.Dark" = "Always Dark"; +"Scene.Settings.Section.Appearance.Light" = "Always Light"; +"Scene.Settings.Section.Appearance.Title" = "Appearance"; +"Scene.Settings.Section.BoringZone.AccountSettings" = "Account Settings"; +"Scene.Settings.Section.BoringZone.Privacy" = "Privacy Policy"; +"Scene.Settings.Section.BoringZone.Terms" = "Terms of Service"; +"Scene.Settings.Section.BoringZone.Title" = "The Boring Zone"; +"Scene.Settings.Section.LookAndFeel.Light" = "Light"; +"Scene.Settings.Section.LookAndFeel.ReallyDark" = "Really Dark"; +"Scene.Settings.Section.LookAndFeel.SortaDark" = "Sorta Dark"; +"Scene.Settings.Section.LookAndFeel.Title" = "Look and Feel"; +"Scene.Settings.Section.LookAndFeel.UseSystem" = "Use System"; +"Scene.Settings.Section.Notifications.Boosts" = "Reblogs my post"; +"Scene.Settings.Section.Notifications.Favorites" = "Favorites my post"; +"Scene.Settings.Section.Notifications.Follows" = "Follows me"; +"Scene.Settings.Section.Notifications.Mentions" = "Mentions me"; +"Scene.Settings.Section.Notifications.Title" = "Notifications"; +"Scene.Settings.Section.Notifications.Trigger.Anyone" = "anyone"; +"Scene.Settings.Section.Notifications.Trigger.Follow" = "anyone I follow"; +"Scene.Settings.Section.Notifications.Trigger.Follower" = "a follower"; +"Scene.Settings.Section.Notifications.Trigger.Noone" = "no one"; +"Scene.Settings.Section.Notifications.Trigger.Title" = "Notify me when"; +"Scene.Settings.Section.Preference.DisableAvatarAnimation" = "Disable animated avatars"; +"Scene.Settings.Section.Preference.DisableEmojiAnimation" = "Disable animated emojis"; +"Scene.Settings.Section.Preference.OpenLinksInMastodon" = "Open links in Mastodon"; +"Scene.Settings.Section.Preference.Title" = "Preferences"; +"Scene.Settings.Section.Preference.TrueBlackDarkMode" = "True black dark mode"; +"Scene.Settings.Section.Preference.UsingDefaultBrowser" = "Use default browser to open links"; +"Scene.Settings.Section.SpicyZone.Clear" = "Clear Media Cache"; +"Scene.Settings.Section.SpicyZone.Signout" = "Sign Out"; +"Scene.Settings.Section.SpicyZone.Title" = "The Spicy Zone"; +"Scene.Settings.Title" = "Settings"; +"Scene.SuggestionAccount.FollowExplain" = "When you follow someone, you’ll see their posts in your home feed."; +"Scene.SuggestionAccount.Title" = "Find People to Follow"; +"Scene.Thread.BackTitle" = "Post"; +"Scene.Thread.Title" = "Post from %@"; +"Scene.Welcome.GetStarted" = "Get Started"; +"Scene.Welcome.LogIn" = "Log In"; +"Scene.Welcome.Slogan" = "Social networking +back in your hands."; +"Scene.Wizard.AccessibilityHint" = "Double tap to dismiss this wizard"; +"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Switch between multiple accounts by holding the profile button."; +"Scene.Wizard.NewInMastodon" = "New in Mastodon"; \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.stringsdict new file mode 100644 index 000000000..cd97825f4 --- /dev/null +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.stringsdict @@ -0,0 +1,631 @@ + + + + + a11y.plural.count.unread.notification + + NSStringLocalizedFormatKey + %#@notification_count_unread_notification@ + notification_count_unread_notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + no unread notification + one + 1 unread notification + few + %ld unread notifications + many + %ld unread notification + other + %ld unread notification + + + a11y.plural.count.input_limit_exceeds + + NSStringLocalizedFormatKey + Input limit exceeds %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 characters + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + + a11y.plural.count.input_limit_remains + + NSStringLocalizedFormatKey + Input limit remains %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 characters + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + no characters + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + + plural.count.followed_by_and_mutual + + NSStringLocalizedFormatKey + %#@names@%#@count_mutual@ + names + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + + + count_mutual + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + Followed by %1$@ + one + Followed by %1$@, and another mutual + few + Followed by %1$@, and %ld mutuals + many + Followed by %1$@, and %ld mutuals + other + Followed by %1$@, and %ld mutuals + + + plural.count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + posts + one + post + few + posts + many + posts + other + posts + + + plural.count.media + + NSStringLocalizedFormatKey + %#@media_count@ + media_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 media + one + 1 media + few + %ld media + many + %ld media + other + %ld media + + + plural.count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 posts + one + 1 post + few + %ld posts + many + %ld posts + other + %ld posts + + + plural.count.favorite + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 favorites + one + 1 favorite + few + %ld favorites + many + %ld favorites + other + %ld favorites + + + plural.count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 reblogs + one + 1 reblog + few + %ld reblogs + many + %ld reblogs + other + %ld reblogs + + + plural.count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 replies + one + 1 reply + few + %ld replies + many + %ld replies + other + %ld replies + + + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 votes + one + 1 vote + few + %ld votes + many + %ld votes + other + %ld votes + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 voters + one + 1 voter + few + %ld voters + many + %ld voters + other + %ld voters + + + plural.people_talking + + NSStringLocalizedFormatKey + %#@count_people_talking@ + count_people_talking + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 people talking + one + 1 people talking + few + %ld people talking + many + %ld people talking + other + %ld people talking + + + plural.count.following + + NSStringLocalizedFormatKey + %#@count_following@ + count_following + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 following + one + 1 following + few + %ld following + many + %ld following + other + %ld following + + + plural.count.follower + + NSStringLocalizedFormatKey + %#@count_follower@ + count_follower + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 followers + one + 1 follower + few + %ld followers + many + %ld followers + other + %ld followers + + + date.year.left + + NSStringLocalizedFormatKey + %#@count_year_left@ + count_year_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 years left + one + 1 year left + few + %ld years left + many + %ld years left + other + %ld years left + + + date.month.left + + NSStringLocalizedFormatKey + %#@count_month_left@ + count_month_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 months left + one + 1 months left + few + %ld months left + many + %ld months left + other + %ld months left + + + date.day.left + + NSStringLocalizedFormatKey + %#@count_day_left@ + count_day_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 days left + one + 1 day left + few + %ld days left + many + %ld days left + other + %ld days left + + + date.hour.left + + NSStringLocalizedFormatKey + %#@count_hour_left@ + count_hour_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 hours left + one + 1 hour left + few + %ld hours left + many + %ld hours left + other + %ld hours left + + + date.minute.left + + NSStringLocalizedFormatKey + %#@count_minute_left@ + count_minute_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 minutes left + one + 1 minute left + few + %ld minutes left + many + %ld minutes left + other + %ld minutes left + + + date.second.left + + NSStringLocalizedFormatKey + %#@count_second_left@ + count_second_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 seconds left + one + 1 second left + few + %ld seconds left + many + %ld seconds left + other + %ld seconds left + + + date.year.ago.abbr + + NSStringLocalizedFormatKey + %#@count_year_ago_abbr@ + count_year_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0y ago + one + 1y ago + few + %ldy ago + many + %ldy ago + other + %ldy ago + + + date.month.ago.abbr + + NSStringLocalizedFormatKey + %#@count_month_ago_abbr@ + count_month_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0M ago + one + 1M ago + few + %ldM ago + many + %ldM ago + other + %ldM ago + + + date.day.ago.abbr + + NSStringLocalizedFormatKey + %#@count_day_ago_abbr@ + count_day_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0d ago + one + 1d ago + few + %ldd ago + many + %ldd ago + other + %ldd ago + + + date.hour.ago.abbr + + NSStringLocalizedFormatKey + %#@count_hour_ago_abbr@ + count_hour_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0h ago + one + 1h ago + few + %ldh ago + many + %ldh ago + other + %ldh ago + + + date.minute.ago.abbr + + NSStringLocalizedFormatKey + %#@count_minute_ago_abbr@ + count_minute_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0m ago + one + 1m ago + few + %ldm ago + many + %ldm ago + other + %ldm ago + + + date.second.ago.abbr + + NSStringLocalizedFormatKey + %#@count_second_ago_abbr@ + count_second_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0s ago + one + 1s ago + few + %lds ago + many + %lds ago + other + %lds ago + + + + diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift index d2baeed71..9346c3bee 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift @@ -123,7 +123,7 @@ public struct AttachmentView: View { case .remove: switch viewModel.uploadState { case .compressing: - return "Compressing..." // TODO: i18n + return L10n.Scene.Compose.Attachment.compressingState default: if viewModel.fractionCompleted < 0.9 { let totalSizeInByte = viewModel.outputSizeInByte @@ -132,7 +132,7 @@ public struct AttachmentView: View { let upload = viewModel.byteCountFormatter.string(fromByteCount: Int64(uploadSizeInByte)) return "\(upload) / \(total)" } else { - return "Server Processing..." // TODO: i18n + return L10n.Scene.Compose.Attachment.serverProcessingState } } case .retry: diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index e840b53fd..06d84566b 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -479,14 +479,14 @@ extension ComposeContentViewModel { public var errorDescription: String? { switch self { case .pollHasEmptyOption: - return "The poll is invalid" // TODO: i18n + return L10n.Scene.Compose.Poll.thePollIsInvalid } } public var failureReason: String? { switch self { case .pollHasEmptyOption: - return "The poll has empty option" // TODO: i18n + return L10n.Scene.Compose.Poll.thePollHasEmptyOption } } } diff --git a/swiftgen.yml b/swiftgen.yml index e9c21260a..967abe370 100644 --- a/swiftgen.yml +++ b/swiftgen.yml @@ -1,7 +1,7 @@ strings: inputs: - - MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings - - MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.stringsdict + - MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings + - MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.stringsdict outputs: - templateName: structured-swift5 output: MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift diff --git a/update_localization.sh b/update_localization.sh index 09cfc21d6..87477bdaa 100755 --- a/update_localization.sh +++ b/update_localization.sh @@ -7,15 +7,28 @@ PODS_ROOT='Pods' echo ${SRCROOT} -# task 1 generate strings file +# Task 1 +# here we use the template source as input to +# generate strings so we could use new strings +# before sync to Crowdin + +# clean Base.lproj +rm -rf ${SRCROOT}/Localization/StringsConvertor/input/Base.lproj +# copy tempate sources +mkdir ${SRCROOT}/Localization/StringsConvertor/input/Base.lproj +cp ${SRCROOT}/Localization/app.json ${SRCROOT}/Localization/StringsConvertor/input/Base.lproj/app.json +cp ${SRCROOT}/Localization/ios-infoPlist.json ${SRCROOT}/Localization/StringsConvertor/input/Base.lproj/ios-infoPlist.json +cp ${SRCROOT}/Localization/Localizable.stringsdict ${SRCROOT}/Localization/StringsConvertor/input/Base.lproj/Localizable.stringsdict + +# Task 2 generate strings file cd ${SRCROOT}/Localization/StringsConvertor sh ./scripts/build.sh -# task 2 copy strings file +# Task 3 copy strings file cp -R ${SRCROOT}/Localization/StringsConvertor/output/module/ ${SRCROOT}/MastodonSDK/Sources/MastodonLocalization/Resources cp -R ${SRCROOT}/Localization/StringsConvertor/Intents/output/ ${SRCROOT}/MastodonIntent -# task 3 swiftgen +# Task 4 swiftgen cd ${SRCROOT} echo "${PODS_ROOT}/SwiftGen/bin/swiftgen" if [[ -f "${PODS_ROOT}/SwiftGen/bin/swiftgen" ]] then @@ -24,6 +37,6 @@ else echo "Run 'bundle exec pod install' or update your CocoaPods installation." fi -#task 4 clean temp file +# Task 5 clean temp file rm -rf ${SRCROOT}/Localization/StringsConvertor/output rm -rf ${SRCROOT}/Localization/StringsConvertor/intents/output From 9e912be7c45a59e477dfa899364e6b99a6614ce8 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 14 Nov 2022 23:19:53 +0100 Subject: [PATCH 224/224] Fix build Happened due to localization, we changed the workflow, but didn't consider another pr. so boom. --- .../StringsConvertor/input/Base.lproj/app.json | 4 ++++ .../MastodonLocalization/Generated/Strings.swift | 12 ++++++------ .../Resources/Base.lproj/Localizable.strings | 2 ++ .../Resources/en.lproj/Localizable.strings | 4 +--- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json index c40c0a39e..30566d8d6 100644 --- a/Localization/StringsConvertor/input/Base.lproj/app.json +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -448,6 +448,10 @@ "placeholder": { "label": "Label", "content": "Content" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 65a97c615..1ad98b0ea 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -742,13 +742,13 @@ public enum L10n { public static let label = L10n.tr("Localizable", "Scene.Profile.Fields.Placeholder.Label", fallback: "Label") } public enum Verified { - /// Ownership of this link was checked on %s - public static func long(_ p1: UnsafePointer) -> String { - return L10n.tr("Localizable", "Scene.Profile.Fields.Verified.Long", p1) + /// Ownership of this link was checked on %@ + public static func long(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.Profile.Fields.Verified.Long", String(describing: p1), fallback: "Ownership of this link was checked on %@") } - /// Verified at %s - public static func short(_ p1: UnsafePointer) -> String { - return L10n.tr("Localizable", "Scene.Profile.Fields.Verified.Short", p1) + /// Verified on %@ + public static func short(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.Profile.Fields.Verified.Short", String(describing: p1), fallback: "Verified on %@") } } } diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index a352b0526..73bc292cf 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -267,6 +267,8 @@ uploaded to Mastodon."; "Scene.Profile.Fields.AddRow" = "Add Row"; "Scene.Profile.Fields.Placeholder.Content" = "Content"; "Scene.Profile.Fields.Placeholder.Label" = "Label"; +"Scene.Profile.Fields.Verified.Long" = "Ownership of this link was checked on %@"; +"Scene.Profile.Fields.Verified.Short" = "Verified on %@"; "Scene.Profile.Header.FollowsYou" = "Follows You"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirm to block %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Block Account"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings index e269a45a7..07ccd2c1b 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings @@ -263,8 +263,6 @@ uploaded to Mastodon."; "Scene.Profile.Fields.AddRow" = "Add Row"; "Scene.Profile.Fields.Placeholder.Content" = "Content"; "Scene.Profile.Fields.Placeholder.Label" = "Label"; -"Scene.Profile.Fields.Verified.Short" = "Verified at %s"; -"Scene.Profile.Fields.Verified.Long" = "Ownership of this link was checked on %s"; "Scene.Profile.Header.FollowsYou" = "Follows You"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirm to block %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Block Account"; @@ -456,4 +454,4 @@ uploaded to Mastodon."; back in your hands."; "Scene.Wizard.AccessibilityHint" = "Double tap to dismiss this wizard"; "Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Switch between multiple accounts by holding the profile button."; -"Scene.Wizard.NewInMastodon" = "New in Mastodon"; +"Scene.Wizard.NewInMastodon" = "New in Mastodon"; \ No newline at end of file