From bc01a04c387847379d5a530416c31b75f56f8b19 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 4 Jul 2022 04:44:31 +0200 Subject: [PATCH 001/658] 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 418c747f2..df87daffe 100644 --- a/Localization/StringsConvertor/input/kmr.lproj/app.json +++ b/Localization/StringsConvertor/input/kmr.lproj/app.json @@ -546,10 +546,10 @@ "show_mentions": "Qalkirinan nîşan bike" }, "follow_request": { - "accept": "Accept", - "accepted": "Accepted", - "reject": "reject", - "rejected": "Rejected" + "accept": "Bipejirîne", + "accepted": "Pejirandî", + "reject": "nepejirîne", + "rejected": "Nepejirandî" } }, "thread": { From 26f4be11069e36e0b35b35c48ad445e48f6937c4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 9 Jul 2022 22:49:37 +0200 Subject: [PATCH 002/658] New translations app.json (Thai) --- Localization/StringsConvertor/input/th.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/th.lproj/app.json b/Localization/StringsConvertor/input/th.lproj/app.json index 9c2a9f4f7..85d2d52ae 100644 --- a/Localization/StringsConvertor/input/th.lproj/app.json +++ b/Localization/StringsConvertor/input/th.lproj/app.json @@ -546,10 +546,10 @@ "show_mentions": "แสดงการกล่าวถึง" }, "follow_request": { - "accept": "Accept", - "accepted": "Accepted", - "reject": "reject", - "rejected": "Rejected" + "accept": "ยอมรับ", + "accepted": "ยอมรับแล้ว", + "reject": "ปฏิเสธ", + "rejected": "ปฏิเสธแล้ว" } }, "thread": { From d4b38fad5de598a657f7d793bbf072012c6b3633 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 11 Jul 2022 10:05:51 +0200 Subject: [PATCH 003/658] 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 dcb8750c4..6ae2ebb2b 100644 --- a/Localization/StringsConvertor/input/fr.lproj/app.json +++ b/Localization/StringsConvertor/input/fr.lproj/app.json @@ -348,7 +348,7 @@ "Publishing": "Publication en cours ...", "accessibility": { "logo_label": "Bouton logo", - "logo_hint": "Tap to scroll to top and tap again to previous location" + "logo_hint": "Appuyez pour faire défiler vers le haut et appuyez à nouveau vers l'emplacement précédent" } } }, @@ -546,10 +546,10 @@ "show_mentions": "Afficher les mentions" }, "follow_request": { - "accept": "Accept", - "accepted": "Accepted", - "reject": "reject", - "rejected": "Rejected" + "accept": "Accepter", + "accepted": "Accepté", + "reject": "rejeter", + "rejected": "Rejeté" } }, "thread": { From 790ea6109f2d6177aee1286d08f5aeadc977f780 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 12 Jul 2022 22:30:14 +0200 Subject: [PATCH 004/658] New translations Localizable.stringsdict (Arabic) --- .../StringsConvertor/input/ar.lproj/Localizable.stringsdict | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict index 197897e8e..139785714 100644 --- a/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict @@ -224,7 +224,7 @@ NSStringFormatValueTypeKey ld zero - لا إعاد تدوين + لَا إعادَةُ تَدوين one إعادةُ تدوينٍ واحِدة two From c2fdbcfb02bfaebc4a98310e2533f12cab9ae96e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 12 Jul 2022 23:41:36 +0200 Subject: [PATCH 005/658] New translations Localizable.stringsdict (Arabic) --- .../input/ar.lproj/Localizable.stringsdict | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict index 139785714..862d98184 100644 --- a/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict @@ -226,15 +226,15 @@ zero لَا إعادَةُ تَدوين one - إعادةُ تدوينٍ واحِدة + إعادَةُ تَدوينٍ واحِدَة two - إعادتا تدوين + إعادَتَا تَدوين few - %ld إعاداتِ تدوين + %ld إعادَاتِ تَدوين many - %ld إعادةٍ للتدوين + %ld إعادَةٍ لِلتَّدوين other - %ld إعادة تدوين + %ld إعادَة تَدوين plural.count.reply From 0be721be3bfc06a827cb192e1150c54094b8baa0 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 13 Jul 2022 17:44:47 +0800 Subject: [PATCH 006/658] feat: update follow request UI --- Mastodon.xcodeproj/project.pbxproj | 16 ++-- .../xcschemes/xcschememanagement.plist | 18 ++-- .../Provider/DataSourceFacade+Follow.swift | 10 ++- .../forbidden.20.imageset/Contents.json | 15 ++++ .../forbidden.20.imageset/forbidden.20.pdf | 83 +++++++++++++++++++ .../checkmark.20.imageset/Contents.json | 15 ++++ .../checkmark.20.imageset/checkmark.20.pdf | 76 +++++++++++++++++ .../Scene/Notification/Contents.json | 9 ++ .../Contents.json | 38 +++++++++ .../Contents.json | 38 +++++++++ .../MastodonAsset/Generated/Assets.swift | 6 ++ .../Content/NotificationView+ViewModel.swift | 6 +- .../View/Content/NotificationView.swift | 37 +++++---- ...n.swift => MastodonPushNotification.swift} | 0 14 files changed, 333 insertions(+), 34 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Circles/forbidden.20.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Circles/forbidden.20.imageset/forbidden.20.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/checkmark.20.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/checkmark.20.imageset/checkmark.20.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Notification/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Notification/confirm.follow.request.button.background.colorset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Notification/delete.follow.request.button.background.colorset/Contents.json rename NotificationService/{MastodonNotification.swift => MastodonPushNotification.swift} (100%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index c1fb3c70b..04c6b5c8f 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -361,9 +361,9 @@ DB67D08427312970006A36CF /* APIService+Following.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB67D08327312970006A36CF /* APIService+Following.swift */; }; DB67D08627312E67006A36CF /* WizardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB67D08527312E67006A36CF /* WizardViewController.swift */; }; DB67D089273256D7006A36CF /* StoreReviewPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB67D088273256D7006A36CF /* StoreReviewPreference.swift */; }; - DB68045B2636DC6A00430867 /* MastodonNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68045A2636DC6A00430867 /* MastodonNotification.swift */; }; + DB68045B2636DC6A00430867 /* MastodonPushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68045A2636DC6A00430867 /* MastodonPushNotification.swift */; }; DB6804662636DC9000430867 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; }; - DB68046C2636DC9E00430867 /* MastodonNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68045A2636DC6A00430867 /* MastodonNotification.swift */; }; + DB68046C2636DC9E00430867 /* MastodonPushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68045A2636DC6A00430867 /* MastodonPushNotification.swift */; }; DB6804832637CD4C00430867 /* AppShared.h in Headers */ = {isa = PBXBuildFile; fileRef = DB6804812637CD4C00430867 /* AppShared.h */; settings = {ATTRIBUTES = (Public, ); }; }; DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -1119,7 +1119,7 @@ DB67D08327312970006A36CF /* APIService+Following.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Following.swift"; sourceTree = ""; }; DB67D08527312E67006A36CF /* WizardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardViewController.swift; sourceTree = ""; }; DB67D088273256D7006A36CF /* StoreReviewPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreReviewPreference.swift; sourceTree = ""; }; - DB68045A2636DC6A00430867 /* MastodonNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonNotification.swift; sourceTree = ""; }; + DB68045A2636DC6A00430867 /* MastodonPushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPushNotification.swift; sourceTree = ""; }; DB68047F2637CD4C00430867 /* AppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DB6804812637CD4C00430867 /* AppShared.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppShared.h; sourceTree = ""; }; DB6804822637CD4C00430867 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -3347,7 +3347,7 @@ DB68053E2638011000430867 /* NotificationService.entitlements */, DBF8AE15263293E400C9C23C /* NotificationService.swift */, DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */, - DB68045A2636DC6A00430867 /* MastodonNotification.swift */, + DB68045A2636DC6A00430867 /* MastodonPushNotification.swift */, DBCBCBF3267CB070000F5B51 /* Decode85.swift */, DBF8AE17263293E400C9C23C /* Info.plist */, ); @@ -4332,7 +4332,7 @@ DBD5B1FA27BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift in Sources */, DB5B549A2833A60400DEF8B2 /* FamiliarFollowersViewController.swift in Sources */, DB3E6FE22806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift in Sources */, - DB68046C2636DC9E00430867 /* MastodonNotification.swift in Sources */, + DB68046C2636DC9E00430867 /* MastodonPushNotification.swift in Sources */, DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */, DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */, DB6D9F57263577D2008423CD /* APIService+CoreData+Setting.swift in Sources */, @@ -4540,7 +4540,7 @@ buildActionMask = 2147483647; files = ( DBE54ACC2636C8FD004E7C0B /* NotificationPreference.swift in Sources */, - DB68045B2636DC6A00430867 /* MastodonNotification.swift in Sources */, + DB68045B2636DC6A00430867 /* MastodonPushNotification.swift in Sources */, DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */, DB6804662636DC9000430867 /* String.swift in Sources */, DBCBCBF4267CB070000F5B51 /* Decode85.swift in Sources */, @@ -5209,6 +5209,7 @@ DB848E2F282B5E6300A302CC /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 144; @@ -5534,6 +5535,7 @@ DBEB19E627E4658E00B0E80E /* Release Snapshot */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 144; @@ -5605,6 +5607,7 @@ DBF8AE1C263293E400C9C23C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 144; @@ -5628,6 +5631,7 @@ DBF8AE1D263293E400C9C23C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 144; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 49c20c414..0a6b68822 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -9,7 +9,7 @@ isShown orderHint - 9 + 6 CoreDataStack.xcscheme_^#shared#^_ @@ -19,27 +19,27 @@ Mastodon - Profile.xcscheme_^#shared#^_ orderHint - 3 + 2 Mastodon - RTL.xcscheme_^#shared#^_ orderHint - 12 + 7 Mastodon - Release.xcscheme_^#shared#^_ orderHint - 5 + 3 Mastodon - Snapshot.xcscheme_^#shared#^_ orderHint - 7 + 4 Mastodon - ar.xcscheme orderHint - 8 + 5 Mastodon - ar.xcscheme_^#shared#^_ @@ -114,7 +114,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 29 + 22 MastodonIntents.xcscheme_^#shared#^_ @@ -129,12 +129,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 31 + 23 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 30 + 24 SuppressBuildableAutocreation diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index 535189368..f0bc379ae 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -74,6 +74,7 @@ extension DataSourceFacade { authenticationBox: authenticationBox ) } catch { + // reset state when failure try? await managedObjectContext.performChanges { guard let notification = notification.object(in: managedObjectContext) else { return } notification.transientFollowRequestState = .init(state: .none) @@ -111,7 +112,8 @@ extension DataSourceFacade { case .accept: notification.transientFollowRequestState = .init(state: .isAccept) case .reject: - notification.transientFollowRequestState = .init(state: .isReject) + // do nothing due to will delete notification + break } } @@ -122,7 +124,11 @@ extension DataSourceFacade { case .accept: notification.followRequestState = .init(state: .isAccept) case .reject: - notification.followRequestState = .init(state: .isReject) + // delete notification + for feed in notification.feeds { + backgroundManagedObjectContext.delete(feed) + } + backgroundManagedObjectContext.delete(notification) } } } // end func diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Circles/forbidden.20.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Circles/forbidden.20.imageset/Contents.json new file mode 100644 index 000000000..dd7ee9d01 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Circles/forbidden.20.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "forbidden.20.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Circles/forbidden.20.imageset/forbidden.20.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Circles/forbidden.20.imageset/forbidden.20.pdf new file mode 100644 index 000000000..c63ba00f7 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Circles/forbidden.20.imageset/forbidden.20.pdf @@ -0,0 +1,83 @@ +%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.000000 2.000000 cm +0.000000 0.000000 0.000000 scn +16.000000 8.000000 m +16.000000 3.581722 12.418278 0.000000 8.000000 0.000000 c +3.581722 0.000000 0.000000 3.581722 0.000000 8.000000 c +0.000000 12.418278 3.581722 16.000000 8.000000 16.000000 c +12.418278 16.000000 16.000000 12.418278 16.000000 8.000000 c +h +14.500000 8.000000 m +14.500000 9.524690 13.975041 10.926769 13.096009 12.035351 c +3.964649 2.903991 l +5.073231 2.024959 6.475310 1.500000 8.000000 1.500000 c +11.589850 1.500000 14.500000 4.410150 14.500000 8.000000 c +h +2.903990 3.964651 m +12.035349 13.096010 l +10.926767 13.975041 9.524689 14.500000 8.000000 14.500000 c +4.410149 14.500000 1.500000 11.589851 1.500000 8.000000 c +1.500000 6.475311 2.024959 5.073233 2.903990 3.964651 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 819 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 20.000000 20.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 +0000000909 00000 n +0000000931 00000 n +0000001104 00000 n +0000001178 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1237 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/checkmark.20.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/checkmark.20.imageset/Contents.json new file mode 100644 index 000000000..09209f11c --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/checkmark.20.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "checkmark.20.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/checkmark.20.imageset/checkmark.20.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/checkmark.20.imageset/checkmark.20.pdf new file mode 100644 index 000000000..9e0954e89 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/checkmark.20.imageset/checkmark.20.pdf @@ -0,0 +1,76 @@ +%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.250000 4.091309 cm +0.000000 0.000000 0.000000 scn +4.782121 2.001464 m +1.310565 5.906964 l +1.035376 6.216551 0.561322 6.244437 0.251735 5.969249 c +-0.057852 5.694060 -0.085737 5.220006 0.189451 4.910419 c +4.189451 0.410419 l +4.476144 0.087890 4.975201 0.073224 5.280338 0.378362 c +15.780338 10.878361 l +16.073231 11.171254 16.073231 11.646129 15.780338 11.939021 c +15.487445 12.231915 15.012570 12.231915 14.719677 11.939021 c +4.782121 2.001464 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 523 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 20.000000 20.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 +0000000613 00000 n +0000000635 00000 n +0000000808 00000 n +0000000882 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +941 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Notification/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Notification/Contents.json new file mode 100644 index 000000000..6e965652d --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Notification/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Notification/confirm.follow.request.button.background.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Notification/confirm.follow.request.button.background.colorset/Contents.json new file mode 100644 index 000000000..61024cbb4 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Notification/confirm.follow.request.button.background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFC", + "green" : "0x2C", + "red" : "0x56" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.988", + "green" : "0.173", + "red" : "0.337" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Notification/delete.follow.request.button.background.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Notification/delete.follow.request.button.background.colorset/Contents.json new file mode 100644 index 000000000..8d3cb488e --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Notification/delete.follow.request.button.background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.969", + "green" : "0.949", + "red" : "0.949" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.969", + "green" : "0.949", + "red" : "0.949" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift index 1536e1b0e..adcee2944 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift @@ -32,6 +32,7 @@ public enum Asset { public static let mastodonTextLogo = ImageAsset(name: "Asset/mastodon.text.logo") } public enum Circles { + public static let forbidden20 = ImageAsset(name: "Circles/forbidden.20") public static let plusCircleFill = ImageAsset(name: "Circles/plus.circle.fill") public static let plusCircle = ImageAsset(name: "Circles/plus.circle") } @@ -102,6 +103,7 @@ public enum Asset { public static let photoFillSplit = ImageAsset(name: "Connectivity/photo.fill.split") } public enum Editing { + public static let checkmark20 = ImageAsset(name: "Editing/checkmark.20") public static let checkmark = ImageAsset(name: "Editing/checkmark") public static let xmark = ImageAsset(name: "Editing/xmark") } @@ -128,6 +130,10 @@ public enum Asset { public enum Discovery { public static let profileCardBackground = ColorAsset(name: "Scene/Discovery/profile.card.background") } + public enum Notification { + public static let confirmFollowRequestButtonBackground = ColorAsset(name: "Scene/Notification/confirm.follow.request.button.background") + public static let deleteFollowRequestButtonBackground = ColorAsset(name: "Scene/Notification/delete.follow.request.button.background") + } public enum Onboarding { public static let avatarPlaceholder = ImageAsset(name: "Scene/Onboarding/avatar.placeholder") public static let background = ColorAsset(name: "Scene/Onboarding/background") diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift index 0db2c969f..0e8c394b7 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift @@ -178,16 +178,20 @@ extension NotificationView.ViewModel { if state == .isAccepting { notificationView.acceptFollowRequestActivityIndicatorView.startAnimating() notificationView.acceptFollowRequestButton.tintColor = .clear + notificationView.acceptFollowRequestButton.setTitleColor(.clear, for: .normal) } else { notificationView.acceptFollowRequestActivityIndicatorView.stopAnimating() notificationView.acceptFollowRequestButton.tintColor = .white + notificationView.acceptFollowRequestButton.setTitleColor(.white, for: .normal) } if state == .isRejecting { notificationView.rejectFollowRequestActivityIndicatorView.startAnimating() notificationView.rejectFollowRequestButton.tintColor = .clear + notificationView.rejectFollowRequestButton.setTitleColor(.clear, for: .normal) } else { notificationView.rejectFollowRequestActivityIndicatorView.stopAnimating() - notificationView.rejectFollowRequestButton.tintColor = .white + notificationView.rejectFollowRequestButton.tintColor = .black + notificationView.rejectFollowRequestButton.setTitleColor(.black, for: .normal) } UIView.animate(withDuration: 0.3) { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift index daf14b96e..5848844a2 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift @@ -110,17 +110,20 @@ public final class NotificationView: UIView { let acceptFollowRequestButtonShadowBackgroundContainer = ShadowBackgroundContainer() private(set) lazy var acceptFollowRequestButton: UIButton = { - let button = UIButton() - button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .semibold) - button.setImage(Asset.Editing.checkmark.image.withRenderingMode(.alwaysTemplate), for: .normal) + let button = HighlightDimmableButton() + button.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .semibold) + button.setTitleColor(.white, for: .normal) + button.setTitle(L10n.Common.Controls.Actions.confirm, for: .normal) + button.setImage(Asset.Editing.checkmark20.image.withRenderingMode(.alwaysTemplate), for: .normal) button.imageView?.contentMode = .scaleAspectFit - button.setBackgroundImage(.placeholder(color: .systemGreen), for: .normal) + button.setBackgroundImage(.placeholder(color: Asset.Scene.Notification.confirmFollowRequestButtonBackground.color), for: .normal) + button.setInsets(forContentPadding: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), imageTitlePadding: 8) button.tintColor = .white button.layer.masksToBounds = true button.layer.cornerCurve = .continuous - button.layer.cornerRadius = 4 + button.layer.cornerRadius = 10 button.accessibilityLabel = L10n.Scene.Notification.FollowRequest.accept - acceptFollowRequestButtonShadowBackgroundContainer.cornerRadius = 4 + acceptFollowRequestButtonShadowBackgroundContainer.cornerRadius = 10 acceptFollowRequestButtonShadowBackgroundContainer.shadowAlpha = 0.1 button.addTarget(self, action: #selector(NotificationView.acceptFollowRequestButtonDidPressed(_:)), for: .touchUpInside) return button @@ -129,18 +132,20 @@ public final class NotificationView: UIView { let rejectFollowRequestButtonShadowBackgroundContainer = ShadowBackgroundContainer() private(set) lazy var rejectFollowRequestButton: UIButton = { - let button = UIButton() - button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .semibold) - button.setImage(Asset.Editing.xmark.image.withRenderingMode(.alwaysTemplate), for: .normal) + let button = HighlightDimmableButton() + button.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .semibold) + button.setTitleColor(.black, for: .normal) + button.setTitle(L10n.Common.Controls.Actions.delete, for: .normal) + button.setImage(Asset.Circles.forbidden20.image.withRenderingMode(.alwaysTemplate), for: .normal) button.imageView?.contentMode = .scaleAspectFit - button.imageEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) // tweak xmark size - button.setBackgroundImage(.placeholder(color: .systemRed), for: .normal) - button.tintColor = .white + button.setInsets(forContentPadding: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), imageTitlePadding: 8) + button.setBackgroundImage(.placeholder(color: Asset.Scene.Notification.deleteFollowRequestButtonBackground.color), for: .normal) + button.tintColor = .black button.layer.masksToBounds = true button.layer.cornerCurve = .continuous - button.layer.cornerRadius = 4 + button.layer.cornerRadius = 10 button.accessibilityLabel = L10n.Scene.Notification.FollowRequest.reject - rejectFollowRequestButtonShadowBackgroundContainer.cornerRadius = 4 + rejectFollowRequestButtonShadowBackgroundContainer.cornerRadius = 10 rejectFollowRequestButtonShadowBackgroundContainer.shadowAlpha = 0.1 button.addTarget(self, action: #selector(NotificationView.rejectFollowRequestButtonDidPressed(_:)), for: .touchUpInside) return button @@ -303,7 +308,7 @@ extension NotificationView { followRequestContainerView.axis = .horizontal followRequestContainerView.distribution = .fillEqually - followRequestContainerView.spacing = 8 + followRequestContainerView.spacing = 16 followRequestContainerView.isLayoutMarginsRelativeArrangement = true followRequestContainerView.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 16, right: 0) // set bottom padding followRequestContainerView.addArrangedSubview(acceptFollowRequestButtonShadowBackgroundContainer) @@ -326,7 +331,7 @@ extension NotificationView { rejectFollowRequestActivityIndicatorView.centerXAnchor.constraint(equalTo: rejectFollowRequestButton.centerXAnchor), rejectFollowRequestActivityIndicatorView.centerYAnchor.constraint(equalTo: rejectFollowRequestButton.centerYAnchor), ]) - rejectFollowRequestActivityIndicatorView.color = .white + rejectFollowRequestActivityIndicatorView.color = .black acceptFollowRequestActivityIndicatorView.hidesWhenStopped = true rejectFollowRequestActivityIndicatorView.stopAnimating() diff --git a/NotificationService/MastodonNotification.swift b/NotificationService/MastodonPushNotification.swift similarity index 100% rename from NotificationService/MastodonNotification.swift rename to NotificationService/MastodonPushNotification.swift From fec7b92d3898b12766dc321deeb6df932ec1c80f Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 13 Jul 2022 19:23:32 +0800 Subject: [PATCH 007/658] feat: support flick up to pop the media. resolve #464 --- ...wViewControllerAnimatedTransitioning.swift | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift index 1b2d62211..97a488f37 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift @@ -329,13 +329,10 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { return case .began, .changed: let translation = sender.translation(in: transitionContext.containerView) - let percent = popInteractiveTransitionAnimator.fractionComplete + progressStep(for: translation) + let percent = progressStep(for: translation) popInteractiveTransitionAnimator.fractionComplete = percent transitionContext.updateInteractiveTransition(percent) updateTransitionItemPosition(of: translation) - - // Reset translation to zero - sender.setTranslation(CGPoint.zero, in: transitionContext.containerView) case .ended, .cancelled: let targetPosition = completionPosition() os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: target position: %s", ((#file as NSString).lastPathComponent), #line, #function, targetPosition == .end ? "end" : "start") @@ -379,9 +376,9 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { let isFlickDown = isFlick && (velocity.dy > 0.0) let isFlickUp = isFlick && (velocity.dy < 0.0) - if (operation == .push && isFlickUp) || (operation == .pop && isFlickDown) { + if (operation == .push && isFlickUp) || (operation == .pop && (isFlickDown || isFlickUp)) { return .end - } else if (operation == .push && isFlickDown) || (operation == .pop && isFlickUp) { + } else if (operation == .push && isFlickDown) { return .start } else if popInteractiveTransitionAnimator.fractionComplete > completionThreshold { return .end @@ -490,7 +487,8 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { } private func progressStep(for translation: CGPoint) -> CGFloat { - return (operation == .push ? -1.0 : 1.0) * translation.y / transitionContext.containerView.bounds.midY + let progress = abs(translation.y) / (transitionContext.containerView.bounds.height / 2) + return progress } private func updateTransitionItemPosition(of translation: CGPoint) { @@ -500,26 +498,32 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { guard initialSize != .zero else { return } // assert(initialSize != .zero) - guard let snapshot = transitionItem.snapshotTransitioning, - let finalSize = transitionItem.targetFrame?.size else { + guard let transitionView = transitionItem.transitionView, + let snapshot = transitionItem.snapshotTransitioning, + let finalSize = transitionItem.targetFrame?.size + else { return } if snapshot.frame.size == .zero { + assertionFailure("divide 0 error") snapshot.frame.size = initialSize } - let currentSize = snapshot.frame.size + let size = transitionView.frame.size + if size.width == .zero || size.height == .zero { + assertionFailure("divide 0 error") + transitionView.frame.size = initialSize + } - let itemPercentComplete = clip(-0.05, 1.05, (currentSize.width - initialSize.width) / (finalSize.width - initialSize.width) + progress) + let itemPercentComplete = clip(-0.05, 1.05, (size.width - initialSize.width) / (finalSize.width - initialSize.width) + progress) let itemWidth = lerp(initialSize.width, finalSize.width, itemPercentComplete) let itemHeight = lerp(initialSize.height, finalSize.height, itemPercentComplete) - assert(currentSize.width != 0.0) - assert(currentSize.height != 0.0) - let scaleTransform = CGAffineTransform(scaleX: (itemWidth / currentSize.width), y: (itemHeight / currentSize.height)) + let scaleTransform = CGAffineTransform(scaleX: (itemWidth / size.width), y: (itemHeight / size.height)) let scaledOffset = transitionItem.touchOffset.apply(transform: scaleTransform) - snapshot.center = (snapshot.center + (translation + (transitionItem.touchOffset - scaledOffset))).point + let center = transitionView.convert(transitionView.center, to: nil) + snapshot.center = (center + (translation + (transitionItem.touchOffset - scaledOffset))).point snapshot.bounds = CGRect(origin: CGPoint.zero, size: CGSize(width: itemWidth, height: itemHeight)) transitionItem.touchOffset = scaledOffset } From 9561b58a70d8f819f694ff5fb33c2c7016d1c210 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 02:46:48 +0800 Subject: [PATCH 008/658] fix: table reload in the background cannot keep scroll position issue --- .../HomeTimelineViewModel+Diffable.swift | 10 +- .../Thread/ThreadViewModel+Diffable.swift | 162 +----------------- 2 files changed, 15 insertions(+), 157 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index 35f44683a..92c28242d 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -155,8 +155,14 @@ extension HomeTimelineViewModel { ) -> Difference? { guard let sourceIndexPath = (tableView.indexPathsForVisibleRows ?? []).sorted().first else { return nil } let rectForSourceItemCell = tableView.rectForRow(at: sourceIndexPath) - let sourceDistanceToTableViewTopEdge = tableView.convert(rectForSourceItemCell, to: nil).origin.y - tableView.safeAreaInsets.top - + let sourceDistanceToTableViewTopEdge: CGFloat = { + if tableView.window != nil { + return tableView.convert(rectForSourceItemCell, to: nil).origin.y - tableView.safeAreaInsets.top + } else { + return rectForSourceItemCell.origin.y - tableView.contentOffset.y - tableView.safeAreaInsets.top + } + }() + guard sourceIndexPath.section < oldSnapshot.numberOfSections, sourceIndexPath.row < oldSnapshot.numberOfItems(inSection: oldSnapshot.sectionIdentifiers[sourceIndexPath.section]) else { return nil } diff --git a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift index a6b4848c1..ededcb044 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift @@ -139,130 +139,6 @@ extension ThreadViewModel { } // end Task } .store(in: &disposeBag) - - -// Publishers.CombineLatest3( -// rootItem.removeDuplicates(), -// ancestorItems.removeDuplicates(), -// descendantItems.removeDuplicates() -// ) -// .receive(on: RunLoop.main) -// .sink { [weak self] rootItem, ancestorItems, descendantItems in -// guard let self = self else { return } -// var items: [Item] = [] -// rootItem.flatMap { items.append($0) } -// items.append(contentsOf: ancestorItems) -// items.append(contentsOf: descendantItems) -// self.updateDeletedStatus(for: items) -// } -// .store(in: &disposeBag) -// -// Publishers.CombineLatest4( -// rootItem, -// ancestorItems, -// descendantItems, -// existStatusFetchedResultsController.objectIDs -// ) -// .debounce(for: .milliseconds(100), scheduler: RunLoop.main) // some magic to avoid jitter -// .sink { [weak self] rootItem, ancestorItems, descendantItems, existObjectIDs in -// guard let self = self else { return } -// guard let tableView = self.tableView, -// let navigationBar = self.contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar() -// else { return } -// -// guard let diffableDataSource = self.diffableDataSource else { return } -// let oldSnapshot = diffableDataSource.snapshot() -// -// var newSnapshot = NSDiffableDataSourceSnapshot() -// newSnapshot.appendSections([.main]) -// -// let currentState = self.loadThreadStateMachine.currentState -// -// // reply to -// if self.rootNode.value?.replyToID != nil, !(currentState is LoadThreadState.NoMore) { -// newSnapshot.appendItems([.topLoader], toSection: .main) -// } -// -// let ancestorItems = ancestorItems.filter { item in -// guard case let .reply(statusObjectID, _) = item else { return false } -// return existObjectIDs.contains(statusObjectID) -// } -// newSnapshot.appendItems(ancestorItems, toSection: .main) -// -// // root -// if let rootItem = rootItem, -// case let .root(objectID, _) = rootItem, -// existObjectIDs.contains(objectID) { -// newSnapshot.appendItems([rootItem], toSection: .main) -// } -// -// // leaf -// if !(currentState is LoadThreadState.NoMore) { -// newSnapshot.appendItems([.bottomLoader], toSection: .main) -// } -// -// let descendantItems = descendantItems.filter { item in -// switch item { -// case .leaf(let statusObjectID, _): -// return existObjectIDs.contains(statusObjectID) -// default: -// return true -// } -// } -// newSnapshot.appendItems(descendantItems, toSection: .main) -// -// // difference for first visible item exclude .topLoader -// guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else { -// diffableDataSource.apply(newSnapshot) -// return -// } -// -// // additional margin for .topLoader -// let oldTopMargin: CGFloat = { -// let marginHeight = TimelineTopLoaderTableViewCell.cellHeight -// if oldSnapshot.itemIdentifiers.contains(.topLoader) { -// return marginHeight -// } -// if !ancestorItems.isEmpty { -// return marginHeight -// } -// -// return .zero -// }() -// -// let oldRootCell: UITableViewCell? = { -// guard let rootItem = rootItem else { return nil } -// guard let index = oldSnapshot.indexOfItem(rootItem) else { return nil } -// guard let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) else { return nil } -// return cell -// }() -// // save height before cell reuse -// let oldRootCellHeight = oldRootCell?.frame.height -// -// diffableDataSource.reloadData(snapshot: newSnapshot) { -// guard let _ = rootItem else { -// return -// } -// if let oldRootCellHeight = oldRootCellHeight { -// // set bottom inset. Make root item pin to top (with margin). -// let bottomSpacing = tableView.safeAreaLayoutGuide.layoutFrame.height - oldRootCellHeight - oldTopMargin -// tableView.contentInset.bottom = max(0, bottomSpacing) -// } -// -// // set scroll position -// tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false) -// let contentOffsetY: CGFloat = { -// var offset: CGFloat = tableView.contentOffset.y - difference.offset -// if tableView.contentInset.bottom != 0.0 && descendantItems.isEmpty { -// // needs restore top margin if bottom inset adjusted AND no descendantItems -// offset += oldTopMargin -// } -// return offset -// }() -// tableView.setContentOffset(CGPoint(x: 0, y: contentOffsetY), animated: false) -// } -// } -// .store(in: &disposeBag) } } @@ -379,7 +255,13 @@ extension ThreadViewModel { let sourceIndexPath = IndexPath(row: index, section: 0) let rectForSourceItemCell = tableView.rectForRow(at: sourceIndexPath) - let sourceDistanceToTableViewTopEdge = tableView.convert(rectForSourceItemCell, to: nil).origin.y - tableView.safeAreaInsets.top + let sourceDistanceToTableViewTopEdge: CGFloat = { + if tableView.window != nil { + return tableView.convert(rectForSourceItemCell, to: nil).origin.y - tableView.safeAreaInsets.top + } else { + return rectForSourceItemCell.origin.y - tableView.contentOffset.y - tableView.safeAreaInsets.top + } + }() guard sourceIndexPath.section < oldSnapshot.numberOfSections, sourceIndexPath.row < oldSnapshot.numberOfItems(inSection: oldSnapshot.sectionIdentifiers[sourceIndexPath.section]) @@ -403,33 +285,3 @@ extension ThreadViewModel { ) } } - -//extension ThreadViewModel { -// private func updateDeletedStatus(for items: [Item]) { -// let parentManagedObjectContext = context.managedObjectContext -// let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) -// managedObjectContext.parent = parentManagedObjectContext -// managedObjectContext.perform { -// var statusIDs: [Status.ID] = [] -// for item in items { -// switch item { -// case .root(let objectID, _): -// guard let status = managedObjectContext.object(with: objectID) as? Status else { continue } -// statusIDs.append(status.id) -// case .reply(let objectID, _): -// guard let status = managedObjectContext.object(with: objectID) as? Status else { continue } -// statusIDs.append(status.id) -// case .leaf(let objectID, _): -// guard let status = managedObjectContext.object(with: objectID) as? Status else { continue } -// statusIDs.append(status.id) -// default: -// continue -// } -// } -// DispatchQueue.main.async { [weak self] in -// guard let self = self else { return } -// self.existStatusFetchedResultsController.statusIDs.value = statusIDs -// } -// } -// } -//} From c4b8f129d70e3256f16b359c7b7b189080fc6400 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 03:29:04 +0800 Subject: [PATCH 009/658] fix: empty banner could be preview issue --- .../Protocol/Provider/DataSourceFacade+Media.swift | 10 +++++++++- .../Scene/MediaPreview/MediaPreviewViewModel.swift | 13 +++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift index 329a7f39c..3e767f2d3 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift @@ -5,6 +5,7 @@ // Created by MainasuK on 2022-1-26. // +import os.log import UIKit import CoreDataStack import MastodonUI @@ -153,6 +154,8 @@ extension DataSourceFacade { user: ManagedObjectRecord, previewContext: ImagePreviewContext ) async throws { + let logger = Logger(subsystem: "DataSourceFacade", category: "Media") + let managedObjectContext = dependency.context.managedObjectContext var _avatarAssetURL: String? @@ -216,13 +219,18 @@ extension DataSourceFacade { thumbnail: thumbnail )) case .profileBanner: - return .profileAvatar(.init( + return .profileBanner(.init( assetURL: _headerAssetURL, thumbnail: thumbnail )) } }() + guard mediaPreviewItem.isAssetURLValid else { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): discard preview due to assetURL invalid") + return + } + coordinateToMediaPreviewScene( dependency: dependency, mediaPreviewItem: mediaPreviewItem, diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift index 5912e559a..2fbc5f0ac 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift @@ -116,6 +116,19 @@ extension MediaPreviewViewModel { case profileAvatar(ProfileAvatarPreviewContext) case profileBanner(ProfileBannerPreviewContext) // case local(LocalImagePreviewMeta) + + var isAssetURLValid: Bool { + switch self { + case .attachment: + return true // default valid + case .profileAvatar: + return true // default valid + case .profileBanner(let item): + guard let assertURL = item.assetURL else { return false } + guard !assertURL.hasSuffix("missing.png") else { return false } + return true + } + } } struct AttachmentPreviewContext { From 742c02ce6a468e2f116ef631c1a3cc9ec524321f Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 03:30:33 +0800 Subject: [PATCH 010/658] fix: profile header background cannot dynamic adapt UI appearance issue --- Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index ac71c0a5f..e68fd9c7b 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -37,7 +37,8 @@ final class ProfileHeaderView: UIView { weak var delegate: ProfileHeaderViewDelegate? var disposeBag = Set() - + private var _disposeBag = Set() + func prepareForReuse() { disposeBag.removeAll() } @@ -237,7 +238,7 @@ extension ProfileHeaderView { guard let self = self else { return } self.backgroundColor = theme.systemBackgroundColor } - .store(in: &disposeBag) + .store(in: &_disposeBag) // banner bannerContainerView.translatesAutoresizingMaskIntoConstraints = false From 5365fabe01ac8daf0dd8453714af66c3ee8fbb0e Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 03:31:19 +0800 Subject: [PATCH 011/658] fix: profile segmented control cannot adapt UI appearance issue --- .../Scene/Profile/Paging/ProfilePagingViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift b/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift index bfbe45471..335f0116c 100644 --- a/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift +++ b/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift @@ -66,7 +66,7 @@ extension ProfilePagingViewController { override func viewDidLoad() { // configure style before viewDidLoad - settings.style.buttonBarBackgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor + settings.style.buttonBarBackgroundColor = .red // ThemeService.shared.currentTheme.value.systemBackgroundColor settings.style.buttonBarItemBackgroundColor = .clear settings.style.buttonBarItemsShouldFillAvailableWidth = false // alignment from leading to trailing settings.style.selectedBarHeight = 3 @@ -87,6 +87,7 @@ extension ProfilePagingViewController { .sink { [weak self] theme in guard let self = self else { return } self.settings.style.buttonBarBackgroundColor = theme.systemBackgroundColor + self.buttonBarView.backgroundColor = self.settings.style.buttonBarBackgroundColor self.barButtonLayout?.invalidateLayout() } .store(in: &disposeBag) From 65dcf7d6c231fad414acaaaf7154c17665996140 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 03:32:23 +0800 Subject: [PATCH 012/658] fix: discovery For You profile card has the same color in the true dark mode issue --- .../View/Content/ProfileCardView+ViewModel.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+ViewModel.swift index 0003e82b6..90fedf034 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+ViewModel.swift @@ -58,7 +58,12 @@ extension ProfileCardView { guard let userInterfaceStyle = userInterfaceStyle else { return } switch userInterfaceStyle { case .dark: - self.backgroundColor = theme.systemBackgroundColor + switch theme.themeName { + case .mastodon: + self.backgroundColor = theme.systemBackgroundColor + case .system: + self.backgroundColor = theme.secondarySystemBackgroundColor + } case .light, .unspecified: self.backgroundColor = Asset.Scene.Discovery.profileCardBackground.color @unknown default: @@ -99,7 +104,10 @@ extension ProfileCardView.ViewModel { private func bindHeader(view: ProfileCardView) { $authorBannerImageURL .sink { url in - guard let url = url else { return } + guard let url = url, !url.absoluteString.hasSuffix("missing.png") else { + view.bannerImageView.image = .placeholder(color: .systemGray3) + return + } view.bannerImageView.af.setImage( withURL: url, placeholderImage: .placeholder(color: .systemGray3), From 3a7697f714929e8fa7d44639ce5f72e88552b5e3 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 03:42:41 +0800 Subject: [PATCH 013/658] fix: make the profile banner preview move during pan and dimming when release --- .../MediaPreview/MediaPreviewableViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift b/Mastodon/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift index 696b72abd..de7eab216 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift @@ -23,8 +23,8 @@ extension MediaPreviewableViewController { return mediaView.superview?.convert(mediaView.frame, to: nil) case .profileAvatar(let profileHeaderView): return profileHeaderView.avatarButton.superview?.convert(profileHeaderView.avatarButton.frame, to: nil) - case .profileBanner: - return nil // fallback to snapshot.frame + case .profileBanner(let profileHeaderView): + return profileHeaderView.bannerImageView.superview?.convert(profileHeaderView.bannerImageView.frame, to: nil) } } } From 5b4fcdf0c0c80c84324f4caee77f328bcb151e9f Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 03:58:50 +0800 Subject: [PATCH 014/658] fix: dimming profile banner directly when dismiss via tap space area --- ...oMediaPreviewViewControllerAnimatedTransitioning.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift index 97a488f37..5381feb0d 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift @@ -217,7 +217,13 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { if !isInteractive { animator.addAnimations { if let targetFrame = targetFrame { - self.transitionItem.snapshotTransitioning?.frame = targetFrame + switch self.transitionItem.source { + case .profileBanner: + fromView.alpha = 0 + self.transitionItem.snapshotTransitioning?.alpha = 0 + default: + self.transitionItem.snapshotTransitioning?.frame = targetFrame + } } else { fromView.alpha = 0 } From c093e0a80d3aeb10daeb7da0be0651901a58b403 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 04:03:56 +0800 Subject: [PATCH 015/658] chore: restore the debug modify --- .../Scene/Profile/Paging/ProfilePagingViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift b/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift index 335f0116c..cc798b6cf 100644 --- a/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift +++ b/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift @@ -11,6 +11,7 @@ import Combine import XLPagerTabStrip import TabBarPager import MastodonAsset +import MastodonUI protocol ProfilePagingViewControllerDelegate: AnyObject { func profilePagingViewController(_ viewController: ProfilePagingViewController, didScrollToPostCustomScrollViewContainerController customScrollViewContainerController: ScrollViewContainer, atIndex index: Int) @@ -66,7 +67,7 @@ extension ProfilePagingViewController { override func viewDidLoad() { // configure style before viewDidLoad - settings.style.buttonBarBackgroundColor = .red // ThemeService.shared.currentTheme.value.systemBackgroundColor + settings.style.buttonBarBackgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor settings.style.buttonBarItemBackgroundColor = .clear settings.style.buttonBarItemsShouldFillAvailableWidth = false // alignment from leading to trailing settings.style.selectedBarHeight = 3 From d4ed7105da8c23a17268a5cc75e24b75384afc04 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 04:19:44 +0800 Subject: [PATCH 016/658] chore: update i18n resources --- .../Resources/ar.lproj/Localizable.stringsdict | 12 ++++++------ .../Resources/fr.lproj/Localizable.strings | 10 +++++----- .../Resources/ku.lproj/Localizable.strings | 8 ++++---- .../Resources/th.lproj/Localizable.strings | 8 ++++---- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.stringsdict index 197897e8e..862d98184 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.stringsdict @@ -224,17 +224,17 @@ NSStringFormatValueTypeKey ld zero - لا إعاد تدوين + لَا إعادَةُ تَدوين one - إعادةُ تدوينٍ واحِدة + إعادَةُ تَدوينٍ واحِدَة two - إعادتا تدوين + إعادَتَا تَدوين few - %ld إعاداتِ تدوين + %ld إعادَاتِ تَدوين many - %ld إعادةٍ للتدوين + %ld إعادَةٍ لِلتَّدوين other - %ld إعادة تدوين + %ld إعادَة تَدوين plural.count.reply diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings index d0a964b54..5e0b9be61 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings @@ -216,17 +216,17 @@ téléversé sur Mastodon."; "Scene.Follower.Title" = "abonné·e"; "Scene.Following.Footer" = "Les abonnés issus des autres serveurs ne sont pas affichés."; "Scene.Following.Title" = "following"; -"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint" = "Tap to scroll to top and tap again to previous location"; +"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint" = "Appuyez pour faire défiler vers le haut et appuyez à nouveau vers l'emplacement précédent"; "Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel" = "Bouton logo"; "Scene.HomeTimeline.NavigationBarState.NewPosts" = "Voir les nouvelles publications"; "Scene.HomeTimeline.NavigationBarState.Offline" = "Hors ligne"; "Scene.HomeTimeline.NavigationBarState.Published" = "Publié!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Publication en cours ..."; "Scene.HomeTimeline.Title" = "Accueil"; -"Scene.Notification.FollowRequest.Accept" = "Accept"; -"Scene.Notification.FollowRequest.Accepted" = "Accepted"; -"Scene.Notification.FollowRequest.Reject" = "reject"; -"Scene.Notification.FollowRequest.Rejected" = "Rejected"; +"Scene.Notification.FollowRequest.Accept" = "Accepter"; +"Scene.Notification.FollowRequest.Accepted" = "Accepté"; +"Scene.Notification.FollowRequest.Reject" = "rejeter"; +"Scene.Notification.FollowRequest.Rejected" = "Rejeté"; "Scene.Notification.Keyobard.ShowEverything" = "Tout Afficher"; "Scene.Notification.Keyobard.ShowMentions" = "Afficher les mentions"; "Scene.Notification.NotificationDescription.FavoritedYourPost" = "a ajouté votre message à ses favoris"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings index a77923674..6c323adfe 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings @@ -224,10 +224,10 @@ girêdanê bitikne da ku ajimêra xwe bidî piştrastkirin."; "Scene.HomeTimeline.NavigationBarState.Published" = "Hate weşandin!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Şandî tê weşandin..."; "Scene.HomeTimeline.Title" = "Serrûpel"; -"Scene.Notification.FollowRequest.Accept" = "Accept"; -"Scene.Notification.FollowRequest.Accepted" = "Accepted"; -"Scene.Notification.FollowRequest.Reject" = "reject"; -"Scene.Notification.FollowRequest.Rejected" = "Rejected"; +"Scene.Notification.FollowRequest.Accept" = "Bipejirîne"; +"Scene.Notification.FollowRequest.Accepted" = "Pejirandî"; +"Scene.Notification.FollowRequest.Reject" = "nepejirîne"; +"Scene.Notification.FollowRequest.Rejected" = "Nepejirandî"; "Scene.Notification.Keyobard.ShowEverything" = "Her tiştî nîşan bide"; "Scene.Notification.Keyobard.ShowMentions" = "Qalkirinan nîşan bike"; "Scene.Notification.NotificationDescription.FavoritedYourPost" = "şandiya te hez kir"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings index 2b67fa50d..f29e08d82 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings @@ -223,10 +223,10 @@ "Scene.HomeTimeline.NavigationBarState.Published" = "เผยแพร่แล้ว!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "กำลังเผยแพร่โพสต์..."; "Scene.HomeTimeline.Title" = "หน้าแรก"; -"Scene.Notification.FollowRequest.Accept" = "Accept"; -"Scene.Notification.FollowRequest.Accepted" = "Accepted"; -"Scene.Notification.FollowRequest.Reject" = "reject"; -"Scene.Notification.FollowRequest.Rejected" = "Rejected"; +"Scene.Notification.FollowRequest.Accept" = "ยอมรับ"; +"Scene.Notification.FollowRequest.Accepted" = "ยอมรับแล้ว"; +"Scene.Notification.FollowRequest.Reject" = "ปฏิเสธ"; +"Scene.Notification.FollowRequest.Rejected" = "ปฏิเสธแล้ว"; "Scene.Notification.Keyobard.ShowEverything" = "แสดงทุกอย่าง"; "Scene.Notification.Keyobard.ShowMentions" = "แสดงการกล่าวถึง"; "Scene.Notification.NotificationDescription.FavoritedYourPost" = "ได้ชื่นชอบโพสต์ของคุณ"; From 8835531a0e663f333e7e32e30b3be2d086405bde Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 04:20:57 +0800 Subject: [PATCH 017/658] feat: add i18n words for profile "Follows You" indicator --- Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index e68fd9c7b..d105a49fe 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -78,7 +78,7 @@ final class ProfileHeaderView: UIView { let followsYouLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 15, weight: .regular) - label.text = "Follows You" // TODO: i18n + label.text = L10n.Scene.Profile.Header.followsYou return label }() let followsYouMaskView = UIView() From fc1ec7ec627c790c91f488d93f994194dea31432 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 04:23:19 +0800 Subject: [PATCH 018/658] chore: update version to 1.4.6 (145) --- AppShared/Info.plist | 4 +- Mastodon.xcodeproj/project.pbxproj | 48 +++++++++---------- .../xcschemes/xcschememanagement.plist | 16 +++---- Mastodon/Info.plist | 4 +- MastodonIntent/Info.plist | 4 +- MastodonTests/Info.plist | 4 +- MastodonUITests/Info.plist | 4 +- NotificationService/Info.plist | 4 +- ShareActionExtension/Info.plist | 4 +- 9 files changed, 46 insertions(+), 46 deletions(-) diff --git a/AppShared/Info.plist b/AppShared/Info.plist index 21baf4a3e..0b3ad41f7 100644 --- a/AppShared/Info.plist +++ b/AppShared/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleVersion - 144 + 145 diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 04c6b5c8f..e6a2e1356 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -4850,7 +4850,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4880,7 +4880,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4988,11 +4988,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 144; + DYLIB_CURRENT_VERSION = 145; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5019,11 +5019,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 144; + DYLIB_CURRENT_VERSION = 145; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5114,7 +5114,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -5182,11 +5182,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 144; + DYLIB_CURRENT_VERSION = 145; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5212,7 +5212,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5235,7 +5235,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5259,7 +5259,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5283,7 +5283,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5307,7 +5307,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5331,7 +5331,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5355,7 +5355,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5442,7 +5442,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -5509,11 +5509,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 144; + DYLIB_CURRENT_VERSION = 145; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5538,7 +5538,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5561,7 +5561,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5585,7 +5585,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5610,7 +5610,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5634,7 +5634,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 145; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 0a6b68822..5b34be6c3 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -9,7 +9,7 @@ isShown orderHint - 6 + 7 CoreDataStack.xcscheme_^#shared#^_ @@ -19,12 +19,12 @@ Mastodon - Profile.xcscheme_^#shared#^_ orderHint - 2 + 1 Mastodon - RTL.xcscheme_^#shared#^_ orderHint - 7 + 8 Mastodon - Release.xcscheme_^#shared#^_ @@ -34,12 +34,12 @@ Mastodon - Snapshot.xcscheme_^#shared#^_ orderHint - 4 + 5 Mastodon - ar.xcscheme orderHint - 5 + 6 Mastodon - ar.xcscheme_^#shared#^_ @@ -114,7 +114,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 22 + 40 MastodonIntents.xcscheme_^#shared#^_ @@ -129,12 +129,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 23 + 39 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 24 + 38 SuppressBuildableAutocreation diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index 31b425322..fe0fbe2a5 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -30,7 +30,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleURLTypes @@ -43,7 +43,7 @@ CFBundleVersion - 144 + 145 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/MastodonIntent/Info.plist b/MastodonIntent/Info.plist index 05a3cc313..8ca413350 100644 --- a/MastodonIntent/Info.plist +++ b/MastodonIntent/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleVersion - 144 + 145 NSExtension NSExtensionAttributes diff --git a/MastodonTests/Info.plist b/MastodonTests/Info.plist index 21baf4a3e..0b3ad41f7 100644 --- a/MastodonTests/Info.plist +++ b/MastodonTests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleVersion - 144 + 145 diff --git a/MastodonUITests/Info.plist b/MastodonUITests/Info.plist index 21baf4a3e..0b3ad41f7 100644 --- a/MastodonUITests/Info.plist +++ b/MastodonUITests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleVersion - 144 + 145 diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 1361dc875..a17aea1c1 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleVersion - 144 + 145 NSExtension NSExtensionPointIdentifier diff --git a/ShareActionExtension/Info.plist b/ShareActionExtension/Info.plist index 18b7be8a4..401c70467 100644 --- a/ShareActionExtension/Info.plist +++ b/ShareActionExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleVersion - 144 + 145 NSExtension NSExtensionAttributes From fbbfd5cda64382fef5d7b9d5debaaa13714dfabd Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 04:27:41 +0800 Subject: [PATCH 019/658] chore: bump package version and update version to 1.4.6 (146) --- AppShared/Info.plist | 2 +- Mastodon.xcodeproj/project.pbxproj | 48 +++++++++---------- .../xcschemes/xcschememanagement.plist | 6 +-- .../xcshareddata/swiftpm/Package.resolved | 12 ++--- Mastodon/Info.plist | 2 +- MastodonIntent/Info.plist | 2 +- MastodonTests/Info.plist | 2 +- MastodonUITests/Info.plist | 2 +- NotificationService/Info.plist | 2 +- ShareActionExtension/Info.plist | 2 +- 10 files changed, 40 insertions(+), 40 deletions(-) diff --git a/AppShared/Info.plist b/AppShared/Info.plist index 0b3ad41f7..529741c4e 100644 --- a/AppShared/Info.plist +++ b/AppShared/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.6 CFBundleVersion - 145 + 146 diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index e6a2e1356..c3f10e505 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -4850,7 +4850,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4880,7 +4880,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4988,11 +4988,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 145; + DYLIB_CURRENT_VERSION = 146; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5019,11 +5019,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 145; + DYLIB_CURRENT_VERSION = 146; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5114,7 +5114,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -5182,11 +5182,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 145; + DYLIB_CURRENT_VERSION = 146; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5212,7 +5212,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5235,7 +5235,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5259,7 +5259,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5283,7 +5283,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5307,7 +5307,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5331,7 +5331,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5355,7 +5355,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5442,7 +5442,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -5509,11 +5509,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 145; + DYLIB_CURRENT_VERSION = 146; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5538,7 +5538,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5561,7 +5561,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5585,7 +5585,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5610,7 +5610,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5634,7 +5634,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 145; + CURRENT_PROJECT_VERSION = 146; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 5b34be6c3..21c3ac33d 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -114,7 +114,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 40 + 42 MastodonIntents.xcscheme_^#shared#^_ @@ -129,12 +129,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 39 + 43 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 38 + 41 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 29c81554a..b51726639 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -114,8 +114,8 @@ "repositoryURL": "https://github.com/kean/Nuke.git", "state": { "branch": null, - "revision": "0ea7545b5c918285aacc044dc75048625c8257cc", - "version": "10.8.0" + "revision": "a002b7fd786f2df2ed4333fe73a9727499fd9d97", + "version": "10.11.2" } }, { @@ -150,8 +150,8 @@ "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", "state": { "branch": null, - "revision": "2e63d0061da449ad0ed130768d05dceb1496de44", - "version": "5.12.5" + "revision": "c4b8660bb3ef543fe4bdcaac0db956b32dc5583f", + "version": "5.13.0" } }, { @@ -186,8 +186,8 @@ "repositoryURL": "https://github.com/scinfu/SwiftSoup.git", "state": { "branch": null, - "revision": "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886", - "version": "2.4.2" + "revision": "6778575285177365cbad3e5b8a72f2a20583cfec", + "version": "2.4.3" } }, { diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index fe0fbe2a5..e1b5816f3 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -43,7 +43,7 @@ CFBundleVersion - 145 + 146 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/MastodonIntent/Info.plist b/MastodonIntent/Info.plist index 8ca413350..5da7221e2 100644 --- a/MastodonIntent/Info.plist +++ b/MastodonIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.6 CFBundleVersion - 145 + 146 NSExtension NSExtensionAttributes diff --git a/MastodonTests/Info.plist b/MastodonTests/Info.plist index 0b3ad41f7..529741c4e 100644 --- a/MastodonTests/Info.plist +++ b/MastodonTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.6 CFBundleVersion - 145 + 146 diff --git a/MastodonUITests/Info.plist b/MastodonUITests/Info.plist index 0b3ad41f7..529741c4e 100644 --- a/MastodonUITests/Info.plist +++ b/MastodonUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.6 CFBundleVersion - 145 + 146 diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index a17aea1c1..1379a2eaf 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.6 CFBundleVersion - 145 + 146 NSExtension NSExtensionPointIdentifier diff --git a/ShareActionExtension/Info.plist b/ShareActionExtension/Info.plist index 401c70467..ef2a91fac 100644 --- a/ShareActionExtension/Info.plist +++ b/ShareActionExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.6 CFBundleVersion - 145 + 146 NSExtension NSExtensionAttributes From 78b2259b8fdb69d046d4e247fdd352ab582c4f06 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 11:34:16 +0800 Subject: [PATCH 020/658] chore: replace CocoaPods-Keys with Arkana --- .arkana.yml | 16 ++ .gitignore | 5 +- AppShared/AppSecret.swift | 10 +- AppShared/Info.plist | 2 +- Documentation/Setup.md | 13 +- Gemfile | 3 +- Gemfile.lock | 22 +- Mastodon.xcodeproj/project.pbxproj | 197 +++--------------- .../xcschemes/xcschememanagement.plist | 14 +- Mastodon/Info.plist | 2 +- MastodonIntent/Info.plist | 2 +- MastodonSDK/Package.swift | 8 +- MastodonTests/Info.plist | 2 +- MastodonUITests/Info.plist | 2 +- NotificationService/Info.plist | 2 +- Podfile | 8 - Podfile.lock | 9 +- ShareActionExtension/Info.plist | 2 +- env/.env | 3 + 19 files changed, 96 insertions(+), 226 deletions(-) create mode 100644 .arkana.yml create mode 100644 env/.env diff --git a/.arkana.yml b/.arkana.yml new file mode 100644 index 000000000..d18507aba --- /dev/null +++ b/.arkana.yml @@ -0,0 +1,16 @@ +import_name: 'ArkanaKeys' +namespace: 'Keys' +result_path: 'Dependencies' +flavors: + - AppStore +swift_declaration_strategy: let +should_generate_unit_tests: true +package_manager: spm +environments: + - Debug + - Release +global_secrets: + # nothing +environment_secrets: + # Mastodon Push Notification Endpoint + - NotificationEndpoint diff --git a/.gitignore b/.gitignore index 6f4802cab..2d787576b 100644 --- a/.gitignore +++ b/.gitignore @@ -122,4 +122,7 @@ xcuserdata # Localization/StringsConvertor/input Localization/StringsConvertor/output -.DS_Store \ No newline at end of file +.DS_Store + +env/**/** +!env/.env \ No newline at end of file diff --git a/AppShared/AppSecret.swift b/AppShared/AppSecret.swift index 9110f2490..45c2e3546 100644 --- a/AppShared/AppSecret.swift +++ b/AppShared/AppSecret.swift @@ -9,7 +9,7 @@ import Foundation import CryptoKit import KeychainAccess -import Keys +import ArkanaKeys import MastodonCommon public final class AppSecret { @@ -36,12 +36,12 @@ public final class AppSecret { }() init() { - let keys = MastodonKeys() - #if DEBUG - self.notificationEndpoint = keys.notification_endpoint_debug + let keys = Keys.Debug() + self.notificationEndpoint = keys.notificationEndpoint #else - self.notificationEndpoint = keys.notification_endpoint + let keys = Keys.Release() + self.notificationEndpoint = keys.notificationEndpoint #endif } diff --git a/AppShared/Info.plist b/AppShared/Info.plist index 529741c4e..9f09d489c 100644 --- a/AppShared/Info.plist +++ b/AppShared/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.6 CFBundleVersion - 146 + 147 diff --git a/Documentation/Setup.md b/Documentation/Setup.md index 1c2f0a6c5..60722e5fb 100644 --- a/Documentation/Setup.md +++ b/Documentation/Setup.md @@ -12,7 +12,7 @@ Intell the latest version of Xcode from the App Store or Apple Developer Downloa This guide may not suit your machine and actually setup procedure may change in the future. Please file the issue or Pull Request if there are any problems. ## CocoaPods -The app use [CocoaPods]() and [CocoaPods-Keys](https://github.com/orta/cocoapods-keys). Ruby Gems are managed through Bundler. The M1 Mac needs virtual ruby env to workaround compatibility issues. +The app use [CocoaPods]() and [Arkana](https://github.com/rogerluan/arkana). Ruby Gems are managed through Bundler. The M1 Mac needs virtual ruby env to workaround compatibility issues. #### Intel Mac @@ -52,6 +52,9 @@ bundle install bundle install bundle exec pod clean +# setup arkana +bundle exec arkana -e ./env/.env + # make install bundle exec pod install --repo-update @@ -59,14 +62,14 @@ bundle exec pod install --repo-update open Mastodon.xcworkspace ``` -The CocoaPods-Key plugin will request the push notification endpoint. You can fufill the empty string and set it later. To setup the push notification. Please check section `Push Notification` below. +The Arkana plugin will setup the push notification endpoint. You can use the empty template from `./env/.env` or use your own `.env` file. To setup the push notification. Please check section `Push Notification` below. The app requires the `App Group` capability. To make sure it works for your developer membership. Please check [AppSecret.swift](../AppShared/AppSecret.swift) file and set another unique `groupID` and update `App Group` settings. #### Push Notification (Optional) -The app is compatible with [toot-relay](https://github.com/DagAgren/toot-relay) APNs. You can set your push notification endpoint via Cocoapod-Keys. There are two endpoints: -- notification_endpoint: for `RELEASE` usage -- notification_endpoint_debug: for `DEBUG` usage +The app is compatible with [toot-relay](https://github.com/DagAgren/toot-relay) APNs. You can set your push notification endpoint via Arkana. There are two endpoints: +- NotificationEndpointDebug: for `DEBUG` usage. e.g. `https:///relay-to/development` +- NotificationEndpointRelease: for `RELEASE` usage. e.g. `https:///relay-to/production` Please check the [Establishing a Certificate-Based Connection to APNs ](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/establishing_a_certificate-based_connection_to_apns) document to generate the certificate and exports the p12 file. diff --git a/Gemfile b/Gemfile index 48aae3d82..7ecafafc1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,7 @@ source "https://rubygems.org" +gem 'arkana' gem "cocoapods" gem "cocoapods-clean" -gem "cocoapods-keys" +gem "xcpretty" diff --git a/Gemfile.lock b/Gemfile.lock index b27a44a97..8ea5aa35f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,9 +3,6 @@ GEM specs: CFPropertyList (3.0.5) rexml - RubyInline (3.12.5) - ZenTest (~> 4.3) - ZenTest (4.12.1) activesupport (6.1.5.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) @@ -17,6 +14,10 @@ GEM algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) + arkana (1.1.1) + colorize (~> 0.8) + dotenv (~> 2.7) + yaml (~> 0.2) atomos (0.1.3) claide (1.1.0) cocoapods (1.11.3) @@ -50,9 +51,6 @@ GEM typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) cocoapods-downloader (1.6.3) - cocoapods-keys (2.2.1) - dotenv - osx_keychain cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -61,6 +59,7 @@ GEM netrc (~> 0.11) cocoapods-try (1.2.0) colored2 (3.1.2) + colorize (0.8.1) concurrent-ruby (1.1.10) dotenv (2.7.6) escape (0.0.4) @@ -79,10 +78,9 @@ GEM nanaimo (0.3.0) nap (1.1.0) netrc (0.11.0) - osx_keychain (1.0.2) - RubyInline (~> 3) public_suffix (4.0.7) rexml (3.2.5) + rouge (2.0.7) ruby-macho (2.5.1) typhoeus (1.4.0) ethon (>= 0.9.0) @@ -95,15 +93,19 @@ GEM colored2 (~> 3.1) nanaimo (~> 0.3.0) rexml (~> 3.2.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) + yaml (0.2.0) zeitwerk (2.5.4) PLATFORMS ruby DEPENDENCIES + arkana cocoapods cocoapods-clean - cocoapods-keys + xcpretty BUNDLED WITH - 2.3.11 + 2.3.17 diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index c3f10e505..0a81bbdfe 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -56,7 +56,6 @@ 2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D607AD726242FC500B70763 /* NotificationViewModel.swift */; }; 2D61254D262547C200299647 /* APIService+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61254C262547C200299647 /* APIService+Notification.swift */; }; 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; - 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; }; 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */; }; 2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */; }; @@ -122,7 +121,6 @@ DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB029E94266A20430062874E /* MastodonAuthenticationController.swift */; }; DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; }; DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; - DB02EA0B280D180D00E751C5 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = DB02EA0A280D180D00E751C5 /* KeychainAccess */; }; DB03A793272A7E5700EE37C5 /* SidebarListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03A792272A7E5700EE37C5 /* SidebarListHeaderView.swift */; }; DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */; }; DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */; }; @@ -232,18 +230,7 @@ DB3EA8EF281B837000598866 /* DiscoveryCommunityViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EA8EE281B837000598866 /* DiscoveryCommunityViewController+DataSourceProvider.swift */; }; DB3EA8F1281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EA8F0281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift */; }; DB3EA8F5281BB65200598866 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA8F4281BB65200598866 /* MastodonSDK */; }; - DB3EA8FC281BBAE100598866 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA8FB281BBAE100598866 /* AlamofireImage */; }; - DB3EA8FE281BBAF200598866 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA8FD281BBAF200598866 /* Alamofire */; }; DB3EA902281BBD5D00598866 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA901281BBD5D00598866 /* CommonOSLog */; }; - DB3EA904281BBD9400598866 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA903281BBD9400598866 /* Introspect */; }; - DB3EA906281BBE8200598866 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA905281BBE8200598866 /* AlamofireImage */; }; - DB3EA908281BBE8200598866 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA907281BBE8200598866 /* AlamofireNetworkActivityIndicator */; }; - DB3EA90A281BBE8200598866 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA909281BBE8200598866 /* Alamofire */; }; - DB3EA90C281BBE9600598866 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA90B281BBE9600598866 /* AlamofireImage */; }; - DB3EA90E281BBE9600598866 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA90D281BBE9600598866 /* AlamofireNetworkActivityIndicator */; }; - DB3EA910281BBE9600598866 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA90F281BBE9600598866 /* Alamofire */; }; - DB3EA912281BBEA800598866 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA911281BBEA800598866 /* AlamofireImage */; }; - DB3EA914281BBEA800598866 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA913281BBEA800598866 /* Alamofire */; }; DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; }; DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; }; DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDB25BAA00100D1B89D /* Main.storyboard */; }; @@ -1424,7 +1411,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB3EA914281BBEA800598866 /* Alamofire in Frameworks */, 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */, DBB525082611EAC0002F1F29 /* Tabman in Frameworks */, DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */, @@ -1432,14 +1418,12 @@ DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */, DB486C0F282E41F200F69423 /* TabBarPager in Frameworks */, DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */, - 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */, DBAC649E267DFE43007FE9FD /* DiffableDataSources in Frameworks */, 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */, 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */, DBF7A0FC26830C33004176A2 /* FPSIndicator in Frameworks */, DBA5A52F26F07ED800CACBAA /* PanModal in Frameworks */, - DB3EA912281BBEA800598866 /* AlamofireImage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1464,12 +1448,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB3EA8FE281BBAF200598866 /* Alamofire in Frameworks */, DB3EA8F5281BB65200598866 /* MastodonSDK in Frameworks */, - DB3EA8FC281BBAE100598866 /* AlamofireImage in Frameworks */, - DB02EA0B280D180D00E751C5 /* KeychainAccess in Frameworks */, EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */, - DB3EA904281BBD9400598866 /* Introspect in Frameworks */, DB3EA902281BBD5D00598866 /* CommonOSLog in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1487,10 +1467,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB3EA90E281BBE9600598866 /* AlamofireNetworkActivityIndicator in Frameworks */, - DB3EA910281BBE9600598866 /* Alamofire in Frameworks */, DBE3CA6B27A39CAF00AFE27B /* AppShared.framework in Frameworks */, - DB3EA90C281BBE9600598866 /* AlamofireImage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1498,10 +1475,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB3EA908281BBE8200598866 /* AlamofireNetworkActivityIndicator in Frameworks */, - DB3EA90A281BBE8200598866 /* Alamofire in Frameworks */, DBE3CA6827A39CAB00AFE27B /* AppShared.framework in Frameworks */, - DB3EA906281BBE8200598866 /* AlamofireImage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3448,7 +3422,6 @@ ); name = Mastodon; packageProductDependencies = ( - 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */, 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */, 2D939AC725EE14620076FA61 /* CropViewController */, DBB525072611EAC0002F1F29 /* Tabman */, @@ -3458,8 +3431,6 @@ DBF7A0FB26830C33004176A2 /* FPSIndicator */, DB552D4E26BBD10C00E481F6 /* OrderedCollections */, DBA5A52E26F07ED800CACBAA /* PanModal */, - DB3EA911281BBEA800598866 /* AlamofireImage */, - DB3EA913281BBEA800598866 /* Alamofire */, DB486C0E282E41F200F69423 /* TabBarPager */, ); productName = Mastodon; @@ -3521,12 +3492,8 @@ ); name = AppShared; packageProductDependencies = ( - DB02EA0A280D180D00E751C5 /* KeychainAccess */, DB3EA8F4281BB65200598866 /* MastodonSDK */, - DB3EA8FB281BBAE100598866 /* AlamofireImage */, - DB3EA8FD281BBAF200598866 /* Alamofire */, DB3EA901281BBD5D00598866 /* CommonOSLog */, - DB3EA903281BBD9400598866 /* Introspect */, ); productName = AppShared; productReference = DB68047F2637CD4C00430867 /* AppShared.framework */; @@ -3565,9 +3532,6 @@ ); name = ShareActionExtension; packageProductDependencies = ( - DB3EA90B281BBE9600598866 /* AlamofireImage */, - DB3EA90D281BBE9600598866 /* AlamofireNetworkActivityIndicator */, - DB3EA90F281BBE9600598866 /* Alamofire */, ); productName = ShareActionExtension; productReference = DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */; @@ -3588,9 +3552,6 @@ ); name = NotificationService; packageProductDependencies = ( - DB3EA905281BBE8200598866 /* AlamofireImage */, - DB3EA907281BBE8200598866 /* AlamofireNetworkActivityIndicator */, - DB3EA909281BBE8200598866 /* Alamofire */, ); productName = NotificationService; productReference = DBF8AE13263293E400C9C23C /* NotificationService.appex */; @@ -3665,13 +3626,10 @@ ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( - DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */, - 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */, DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */, 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */, DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */, - DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */, DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */, DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */, DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */, @@ -3679,8 +3637,6 @@ DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */, DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */, DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */, - DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, - DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */, DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; @@ -4850,7 +4806,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4880,7 +4836,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4988,11 +4944,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 146; + DYLIB_CURRENT_VERSION = 147; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5019,11 +4975,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 146; + DYLIB_CURRENT_VERSION = 147; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5114,7 +5070,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -5182,11 +5138,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 146; + DYLIB_CURRENT_VERSION = 147; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5212,7 +5168,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5235,7 +5191,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5259,7 +5215,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5283,7 +5239,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5307,7 +5263,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5331,7 +5287,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5355,7 +5311,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5442,7 +5398,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -5509,11 +5465,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 146; + DYLIB_CURRENT_VERSION = 147; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5538,7 +5494,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5561,7 +5517,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5585,7 +5541,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5610,7 +5566,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5634,7 +5590,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 146; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5754,14 +5710,6 @@ minimumVersion = 1.7.1; }; }; - 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Alamofire/AlamofireNetworkActivityIndicator"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 3.1.0; - }; - }; 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TimOliver/TOCropViewController.git"; @@ -5786,22 +5734,6 @@ minimumVersion = 8.0.0; }; }; - DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Alamofire/AlamofireImage.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 4.1.0; - }; - }; - DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Alamofire/Alamofire.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 5.4.0; - }; - }; DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TwidereProject/TabBarPager.git"; @@ -5818,22 +5750,6 @@ minimumVersion = 0.0.5; }; }; - DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 4.2.2; - }; - }; - DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/siteline/SwiftUI-Introspect.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.1.4; - }; - }; DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/slackhq/PanModal.git"; @@ -5890,85 +5806,20 @@ package = 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */; productName = ThirdPartyMailer; }; - 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */ = { - isa = XCSwiftPackageProductDependency; - package = 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */; - productName = AlamofireNetworkActivityIndicator; - }; 2D939AC725EE14620076FA61 /* CropViewController */ = { isa = XCSwiftPackageProductDependency; package = 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */; productName = CropViewController; }; - DB02EA0A280D180D00E751C5 /* KeychainAccess */ = { - isa = XCSwiftPackageProductDependency; - package = DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */; - productName = KeychainAccess; - }; DB3EA8F4281BB65200598866 /* MastodonSDK */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDK; }; - DB3EA8FB281BBAE100598866 /* AlamofireImage */ = { - isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; - productName = AlamofireImage; - }; - DB3EA8FD281BBAF200598866 /* Alamofire */ = { - isa = XCSwiftPackageProductDependency; - package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */; - productName = Alamofire; - }; DB3EA901281BBD5D00598866 /* CommonOSLog */ = { isa = XCSwiftPackageProductDependency; package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; productName = CommonOSLog; }; - DB3EA903281BBD9400598866 /* Introspect */ = { - isa = XCSwiftPackageProductDependency; - package = DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; - productName = Introspect; - }; - DB3EA905281BBE8200598866 /* AlamofireImage */ = { - isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; - productName = AlamofireImage; - }; - DB3EA907281BBE8200598866 /* AlamofireNetworkActivityIndicator */ = { - isa = XCSwiftPackageProductDependency; - package = 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */; - productName = AlamofireNetworkActivityIndicator; - }; - DB3EA909281BBE8200598866 /* Alamofire */ = { - isa = XCSwiftPackageProductDependency; - package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */; - productName = Alamofire; - }; - DB3EA90B281BBE9600598866 /* AlamofireImage */ = { - isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; - productName = AlamofireImage; - }; - DB3EA90D281BBE9600598866 /* AlamofireNetworkActivityIndicator */ = { - isa = XCSwiftPackageProductDependency; - package = 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */; - productName = AlamofireNetworkActivityIndicator; - }; - DB3EA90F281BBE9600598866 /* Alamofire */ = { - isa = XCSwiftPackageProductDependency; - package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */; - productName = Alamofire; - }; - DB3EA911281BBEA800598866 /* AlamofireImage */ = { - isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; - productName = AlamofireImage; - }; - DB3EA913281BBEA800598866 /* Alamofire */ = { - isa = XCSwiftPackageProductDependency; - package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */; - productName = Alamofire; - }; DB486C0E282E41F200F69423 /* TabBarPager */ = { isa = XCSwiftPackageProductDependency; package = DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 21c3ac33d..103cb88d9 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -9,7 +9,7 @@ isShown orderHint - 7 + 6 CoreDataStack.xcscheme_^#shared#^_ @@ -24,7 +24,7 @@ Mastodon - RTL.xcscheme_^#shared#^_ orderHint - 8 + 7 Mastodon - Release.xcscheme_^#shared#^_ @@ -34,12 +34,12 @@ Mastodon - Snapshot.xcscheme_^#shared#^_ orderHint - 5 + 4 Mastodon - ar.xcscheme orderHint - 6 + 5 Mastodon - ar.xcscheme_^#shared#^_ @@ -114,7 +114,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 42 + 31 MastodonIntents.xcscheme_^#shared#^_ @@ -129,12 +129,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 43 + 30 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 41 + 32 SuppressBuildableAutocreation diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index e1b5816f3..ce2875109 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -43,7 +43,7 @@ CFBundleVersion - 146 + 147 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/MastodonIntent/Info.plist b/MastodonIntent/Info.plist index 5da7221e2..3816b660b 100644 --- a/MastodonIntent/Info.plist +++ b/MastodonIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.6 CFBundleVersion - 146 + 147 NSExtension NSExtensionAttributes diff --git a/MastodonSDK/Package.swift b/MastodonSDK/Package.swift index aa349f8e0..ea3f2b01d 100644 --- a/MastodonSDK/Package.swift +++ b/MastodonSDK/Package.swift @@ -30,10 +30,13 @@ let package = Package( .package(url: "https://github.com/TwidereProject/MetaTextKit.git", .exact("2.2.5")), .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/Alamofire/AlamofireNetworkActivityIndicator", from: "3.1.0"), + .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"), .package(name: "NukeFLAnimatedImagePlugin", url: "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", from: "8.0.0"), .package(name: "UITextView+Placeholder", url: "https://github.com/MainasuK/UITextView-Placeholder.git", from: "1.4.1"), .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.3"), .package(name: "FaviconFinder", url: "https://github.com/will-lumley/FaviconFinder.git", from: "3.2.2"), + .package(name: "ArkanaKeys", path: "../Dependencies/ArkanaKeys"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -57,7 +60,9 @@ let package = Package( .target( name: "MastodonCommon", dependencies: [ - "MastodonExtension" + "ArkanaKeys", + "MastodonExtension", + .product(name: "KeychainAccess", package: "KeychainAccess"), ] ), .target( @@ -85,6 +90,7 @@ let package = Package( "MastodonLocalization", .product(name: "Alamofire", package: "Alamofire"), .product(name: "AlamofireImage", package: "AlamofireImage"), + .product(name: "AlamofireNetworkActivityIndicator", package: "AlamofireNetworkActivityIndicator"), .product(name: "FLAnimatedImage", package: "FLAnimatedImage"), .product(name: "FaviconFinder", package: "FaviconFinder"), .product(name: "MetaTextKit", package: "MetaTextKit"), diff --git a/MastodonTests/Info.plist b/MastodonTests/Info.plist index 529741c4e..9f09d489c 100644 --- a/MastodonTests/Info.plist +++ b/MastodonTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.6 CFBundleVersion - 146 + 147 diff --git a/MastodonUITests/Info.plist b/MastodonUITests/Info.plist index 529741c4e..9f09d489c 100644 --- a/MastodonUITests/Info.plist +++ b/MastodonUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.6 CFBundleVersion - 146 + 147 diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 1379a2eaf..854a8a529 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.6 CFBundleVersion - 146 + 147 NSExtension NSExtensionPointIdentifier diff --git a/Podfile b/Podfile index a64cd0e55..774148fa0 100644 --- a/Podfile +++ b/Podfile @@ -35,14 +35,6 @@ target 'AppShared' do use_frameworks! end -plugin 'cocoapods-keys', { - :project => "Mastodon", - :keys => [ - "notification_endpoint", - "notification_endpoint_debug" - ] -} - post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| diff --git a/Podfile.lock b/Podfile.lock index 629a48a87..453610694 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -2,7 +2,6 @@ PODS: - DateToolsSwift (5.0.0) - FLEX (4.4.1) - Kanna (5.2.7) - - Keys (1.0.1) - Sourcery (1.6.1): - Sourcery/CLI-Only (= 1.6.1) - Sourcery/CLI-Only (1.6.1) @@ -14,7 +13,6 @@ DEPENDENCIES: - DateToolsSwift (~> 5.0.0) - FLEX (~> 4.4.0) - Kanna (~> 5.2.2) - - Keys (from `Pods/CocoaPodsKeys`) - Sourcery (~> 1.6.1) - SwiftGen (~> 6.4.0) - "UITextField+Shake (~> 1.2)" @@ -30,20 +28,15 @@ SPEC REPOS: - "UITextField+Shake" - XLPagerTabStrip -EXTERNAL SOURCES: - Keys: - :path: Pods/CocoaPodsKeys - SPEC CHECKSUMS: DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6 FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab Kanna: 01cfbddc127f5ff0963692f285fcbc8a9d62d234 - Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 Sourcery: f3759f803bd0739f74fc92a4341eed0473ce61ac SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 "UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3 XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5 -PODFILE CHECKSUM: 1ac960a2c981ef98f7c24a3bba57bdabc1f66103 +PODFILE CHECKSUM: d95968ab70ea5121c21dfd801aa36b12bcd59c9d COCOAPODS: 1.11.3 diff --git a/ShareActionExtension/Info.plist b/ShareActionExtension/Info.plist index ef2a91fac..4372c5048 100644 --- a/ShareActionExtension/Info.plist +++ b/ShareActionExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.6 CFBundleVersion - 146 + 147 NSExtension NSExtensionAttributes diff --git a/env/.env b/env/.env new file mode 100644 index 000000000..2458052ab --- /dev/null +++ b/env/.env @@ -0,0 +1,3 @@ +# Required +NotificationEndpointDebug="" +NotificationEndpointRelease="" \ No newline at end of file From 5a815cb03b714474cc02d803e5bc89366bbb294e Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 11:35:36 +0800 Subject: [PATCH 021/658] chore: add Xcode Cloud scripts --- AppShared/AppSecret.swift | 1 - ci_scripts/ci_post_clone.sh | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100755 ci_scripts/ci_post_clone.sh diff --git a/AppShared/AppSecret.swift b/AppShared/AppSecret.swift index 45c2e3546..8af5e14f4 100644 --- a/AppShared/AppSecret.swift +++ b/AppShared/AppSecret.swift @@ -5,7 +5,6 @@ // Created by MainasuK Cirno on 2021-4-27. // - import Foundation import CryptoKit import KeychainAccess diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh new file mode 100755 index 000000000..89cd36138 --- /dev/null +++ b/ci_scripts/ci_post_clone.sh @@ -0,0 +1,33 @@ +#!/bin/zsh + +# Xcode Cloud scripts + +set -xeu +set -o pipefail + +# list hardware +system_profiler SPSoftwareDataType SPHardwareDataType + +echo $PWD +cd $CI_WORKSPACE +echo $PWD + +# install rbenv +brew install rbenv +which ruby +echo 'eval "$(rbenv init -)"' >> ~/.zprofile +source ~/.zprofile +which ruby + +rbenv install 3.0.3 +rbenv global 3.0.3 +ruby --version + +# install bundle gem +gem install bundler + +# setup gems +bundle install + +bundle exec arkana +bundle exec pod install From 04fc2e9efec228979b321b61003a750783b4f977 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 12:22:10 +0800 Subject: [PATCH 022/658] chore: workaround openssl 3.0 break CI issue. https://github.com/rbenv/ruby-build/discussions/1853#discussioncomment-2146106 --- ci_scripts/ci_post_clone.sh | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh index 89cd36138..81ff1ee07 100755 --- a/ci_scripts/ci_post_clone.sh +++ b/ci_scripts/ci_post_clone.sh @@ -19,7 +19,25 @@ echo 'eval "$(rbenv init -)"' >> ~/.zprofile source ~/.zprofile which ruby -rbenv install 3.0.3 +# by pass the openssl cannot build issue +# https://github.com/rbenv/ruby-build/discussions/1853#discussioncomment-2146106 +brew cleanup openssl@3.0 +brew uninstall openssl@3.0 +rm -rf /opt/homebrew/etc/openssl@3 + +brew install openssl@1.1 + +export PATH="/opt/homebrew/opt/openssl@1.1/bin:$PATH" +export LDFLAGS="-L/opt/homebrew/opt/openssl@1.1/lib" +export CPPFLAGS="-I/opt/homebrew/opt/openssl@1.1/include" +export PKG_CONFIG_PATH="/opt/homebrew/opt/openssl@1.1/lib/pkgconfig" +export RUBY_CONFIGURE_OPTS="--with-openssl-dir=/opt/homebrew/opt/openssl@1.1" +source ~/.zshrc + +CONFIGURE_OPTS=--with-openssl-dir=`brew --prefix openssl@1.1` CFLAGS="-Wno-error=implicit-function-declaration" rbenv install 3.0.3 + +# install ruby 3.0.3 +# rbenv install 3.0.3 rbenv global 3.0.3 ruby --version From 507b5b412e96c618c12dcb4d7c718cd67257258b Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 12:30:11 +0800 Subject: [PATCH 023/658] chore: ignore openssl cleanup in dry env --- ci_scripts/ci_post_clone.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh index 81ff1ee07..fbe77d99c 100755 --- a/ci_scripts/ci_post_clone.sh +++ b/ci_scripts/ci_post_clone.sh @@ -21,9 +21,10 @@ which ruby # by pass the openssl cannot build issue # https://github.com/rbenv/ruby-build/discussions/1853#discussioncomment-2146106 -brew cleanup openssl@3.0 -brew uninstall openssl@3.0 -rm -rf /opt/homebrew/etc/openssl@3 + +# brew cleanup openssl@3.0 +# brew uninstall openssl@3.0 +# rm -rf /opt/homebrew/etc/openssl@3 brew install openssl@1.1 From af083cd0138151ae70ea83b1d9acb71589572584 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 13:02:54 +0800 Subject: [PATCH 024/658] chore: do not use rbenv due to cannot grant sudo permission --- ci_scripts/ci_post_clone.sh | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh index fbe77d99c..0264103d9 100755 --- a/ci_scripts/ci_post_clone.sh +++ b/ci_scripts/ci_post_clone.sh @@ -12,34 +12,8 @@ echo $PWD cd $CI_WORKSPACE echo $PWD -# install rbenv -brew install rbenv -which ruby -echo 'eval "$(rbenv init -)"' >> ~/.zprofile -source ~/.zprofile -which ruby - -# by pass the openssl cannot build issue -# https://github.com/rbenv/ruby-build/discussions/1853#discussioncomment-2146106 - -# brew cleanup openssl@3.0 -# brew uninstall openssl@3.0 -# rm -rf /opt/homebrew/etc/openssl@3 - -brew install openssl@1.1 - -export PATH="/opt/homebrew/opt/openssl@1.1/bin:$PATH" -export LDFLAGS="-L/opt/homebrew/opt/openssl@1.1/lib" -export CPPFLAGS="-I/opt/homebrew/opt/openssl@1.1/include" -export PKG_CONFIG_PATH="/opt/homebrew/opt/openssl@1.1/lib/pkgconfig" -export RUBY_CONFIGURE_OPTS="--with-openssl-dir=/opt/homebrew/opt/openssl@1.1" -source ~/.zshrc - -CONFIGURE_OPTS=--with-openssl-dir=`brew --prefix openssl@1.1` CFLAGS="-Wno-error=implicit-function-declaration" rbenv install 3.0.3 - -# install ruby 3.0.3 -# rbenv install 3.0.3 -rbenv global 3.0.3 +# install ruby from homebrew +brew install ruby ruby --version # install bundle gem From 100d189e8efa9c131c2a281e23351fbf39f46bdb Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 13:32:02 +0800 Subject: [PATCH 025/658] chore: set custom GEM_HOME due to default one cannot access without sudo --- ci_scripts/ci_post_clone.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh index 0264103d9..ee67ff0b5 100755 --- a/ci_scripts/ci_post_clone.sh +++ b/ci_scripts/ci_post_clone.sh @@ -16,8 +16,14 @@ echo $PWD brew install ruby ruby --version +# workaround default installation location cannot access without sudo problem +echo 'export GEM_HOME=$HOME/gems' >>~/.bash_profile +echo 'export PATH=$HOME/gems/bin:$PATH' >>~/.bash_profile +export GEM_HOME=$HOME/gems +export PATH="$GEM_HOME/bin:$PATH" + # install bundle gem -gem install bundler +gem install bundler --install-dir $GEM_HOME # setup gems bundle install From b61e7518b515e922baace8d506a1ef43b5bac8a3 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 22 Jul 2022 18:39:36 +0800 Subject: [PATCH 026/658] feat: add unread notification application shortcut --- Mastodon/Service/NotificationService.swift | 38 +++++++++++++++++++ Mastodon/Supporting Files/SceneDelegate.swift | 2 + 2 files changed, 40 insertions(+) diff --git a/Mastodon/Service/NotificationService.swift b/Mastodon/Service/NotificationService.swift index e4e7508a3..0d12d54ab 100644 --- a/Mastodon/Service/NotificationService.swift +++ b/Mastodon/Service/NotificationService.swift @@ -12,9 +12,12 @@ import CoreData import CoreDataStack import MastodonSDK import AppShared +import MastodonLocalization final class NotificationService { + public static let unreadShortcutItemIdentifier = "org.joinmastodon.app.NotificationService.unread-shortcut" + var disposeBag = Set() let workingQueue = DispatchQueue(label: "org.joinmastodon.app.NotificationService.working-queue") @@ -74,6 +77,9 @@ final class NotificationService { UserDefaults.shared.notificationBadgeCount = count UIApplication.shared.applicationIconBadgeNumber = count + Task { @MainActor in + UIApplication.shared.shortcutItems = try? await self.unreadApplicationShortcutItems() + } self.unreadNotificationCountDidUpdate.send() } @@ -100,6 +106,38 @@ extension NotificationService { } } +extension NotificationService { + public func unreadApplicationShortcutItems() async throws -> [UIApplicationShortcutItem] { + guard let authenticationService = self.authenticationService else { return [] } + let managedObjectContext = authenticationService.managedObjectContext + return try await managedObjectContext.perform { + var items: [UIApplicationShortcutItem] = [] + for object in authenticationService.mastodonAuthentications.value { + guard let authentication = managedObjectContext.object(with: object.objectID) as? MastodonAuthentication else { continue } + + let accessToken = authentication.userAccessToken + let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) + guard count > 0 else { continue } + + let title = "@\(authentication.user.acctWithDomain)" + let subtitle = L10n.A11y.Plural.Count.Unread.notification(count) + + let item = UIApplicationShortcutItem( + type: NotificationService.unreadShortcutItemIdentifier, + localizedTitle: title, + localizedSubtitle: subtitle, + icon: nil, + userInfo: [ + "accessToken": accessToken as NSSecureCoding + ] + ) + items.append(item) + } + return items + } + } +} + extension NotificationService { func dequeueNotificationViewModel( diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 54b1fd57e..9801b025c 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -142,6 +142,8 @@ extension SceneDelegate { logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(shortcutItem.type)") switch shortcutItem.type { + case NotificationService.unreadShortcutItemIdentifier: + coordinator?.switchToTabBar(tab: .notification) case "org.joinmastodon.app.new-post": if coordinator?.tabBarController.topMost is ComposeViewController { logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): composing…") From 112fa56ee6344d9e3c64445deb1370183cc0b177 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 27 Jul 2022 17:39:27 +0800 Subject: [PATCH 027/658] feat: add unread notification shortcut handler and updater --- .../xcschemes/xcschememanagement.plist | 6 +-- Mastodon/Coordinator/SceneCoordinator.swift | 2 +- Mastodon/Info.plist | 30 +++++++------ Mastodon/Supporting Files/AppDelegate.swift | 8 ++++ Mastodon/Supporting Files/SceneDelegate.swift | 43 ++++++++++++++++--- 5 files changed, 67 insertions(+), 22 deletions(-) diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 103cb88d9..d0b79ce67 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -114,7 +114,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 31 + 20 MastodonIntents.xcscheme_^#shared#^_ @@ -129,12 +129,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 30 + 21 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 32 + 22 SuppressBuildableAutocreation diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 4491d383a..c6d81c9c2 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -19,7 +19,7 @@ final public class SceneCoordinator { private weak var scene: UIScene! private weak var sceneDelegate: SceneDelegate! - private weak var appContext: AppContext! + private(set) weak var appContext: AppContext! let id = UUID().uuidString diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index ce2875109..cfdbb923d 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -2,19 +2,6 @@ - NSAppTransportSecurity - - NSExceptionDomains - - onion - - NSExceptionAllowsInsecureHTTPLoads - - NSIncludesSubdomains - - - - CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion @@ -59,6 +46,19 @@ LSRequiresIPhoneOS + NSAppTransportSecurity + + NSExceptionDomains + + onion + + NSExceptionAllowsInsecureHTTPLoads + + NSIncludesSubdomains + + + + NSUserActivityTypes SendPostIntent @@ -103,6 +103,10 @@ UIApplicationSupportsIndirectInputEvents + UIBackgroundModes + + remote-notification + UILaunchStoryboardName Main UIMainStoryboardFile diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 7b1185f84..7b750a481 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -106,6 +106,14 @@ extension AppDelegate: UNUserNotificationCenterDelegate { completionHandler([.sound]) } + + // notification present in the background (or resume from background) + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult { + let shortcutItems = try? await appContext.notificationService.unreadApplicationShortcutItems() + UIApplication.shared.shortcutItems = shortcutItems + return .noData + } + // response to user action for notification (e.g. redirect to post) func userNotificationCenter( _ center: UNUserNotificationCenter, diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 9801b025c..ee60c14e9 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -110,7 +110,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { AppContext.shared.statusFilterService.filterUpdatePublisher.send() if let shortcutItem = savedShortCutItem { - _ = handler(shortcutItem: shortcutItem) + Task { + _ = await handler(shortcutItem: shortcutItem) + } savedShortCutItem = nil } } @@ -134,16 +136,45 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } extension SceneDelegate { - func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { - completionHandler(handler(shortcutItem: shortcutItem)) + + func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem) async -> Bool { + return await handler(shortcutItem: shortcutItem) } - private func handler(shortcutItem: UIApplicationShortcutItem) -> Bool { + @MainActor + private func handler(shortcutItem: UIApplicationShortcutItem) async -> Bool { logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(shortcutItem.type)") switch shortcutItem.type { case NotificationService.unreadShortcutItemIdentifier: - coordinator?.switchToTabBar(tab: .notification) + guard let coordinator = self.coordinator else { return false } + + guard let accessToken = shortcutItem.userInfo?["accessToken"] as? String else { + assertionFailure() + return false + } + let request = MastodonAuthentication.sortedFetchRequest + request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken) + request.fetchLimit = 1 + + guard let authentication = try? coordinator.appContext.managedObjectContext.fetch(request).first else { + assertionFailure() + return false + } + + let _isActive = try? await coordinator.appContext.authenticationService.activeMastodonUser( + domain: authentication.domain, + userID: authentication.userID + ) + .singleOutput() + .get() + + guard _isActive == true else { + return false + } + + coordinator.switchToTabBar(tab: .notification) + case "org.joinmastodon.app.new-post": if coordinator?.tabBarController.topMost is ComposeViewController { logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): composing…") @@ -160,6 +191,7 @@ extension SceneDelegate { logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): not authenticated") } } + case "org.joinmastodon.app.search": coordinator?.switchToTabBar(tab: .search) logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): select search tab") @@ -168,6 +200,7 @@ extension SceneDelegate { searchViewController.searchBarTapPublisher.send() logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): trigger search") } + default: assertionFailure() break From 5c7b582b7583e2fc3eb903d540827c0ad46728ab Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 27 Jul 2022 18:41:39 +0800 Subject: [PATCH 028/658] chore: export brew installed ruby --- ci_scripts/ci_post_clone.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh index ee67ff0b5..75605f7b7 100755 --- a/ci_scripts/ci_post_clone.sh +++ b/ci_scripts/ci_post_clone.sh @@ -14,7 +14,11 @@ echo $PWD # install ruby from homebrew brew install ruby +echo 'export PATH="/Users/local/Homebrew/opt/ruby/bin:$PATH"' >> ~/.zshrc +source ~/.zshrc + ruby --version +which gem # workaround default installation location cannot access without sudo problem echo 'export GEM_HOME=$HOME/gems' >>~/.bash_profile From c58f3a2be84e3c869506d6180a45a1197cd8ec90 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 27 Jul 2022 19:46:42 +0800 Subject: [PATCH 029/658] chore: rearrange project package dependency to resolve packet signature issue --- Documentation/Acknowledgments.md | 2 - Mastodon.xcodeproj/project.pbxproj | 128 ----- .../xcschemes/xcschememanagement.plist | 4 +- .../xcshareddata/swiftpm/Package.resolved | 492 +++++++++--------- MastodonSDK/Package.swift | 14 +- 5 files changed, 250 insertions(+), 390 deletions(-) diff --git a/Documentation/Acknowledgments.md b/Documentation/Acknowledgments.md index ff6dbc081..6d932102c 100644 --- a/Documentation/Acknowledgments.md +++ b/Documentation/Acknowledgments.md @@ -6,8 +6,6 @@ - [CommonOSLog](https://github.com/mainasuk/CommonOSLog) - [CryptoSwift](https://github.com/krzyzanowskim/CryptoSwift) - [DateToolSwift](https://github.com/MatthewYork/DateTools) -- [DiffableDataSources](https://github.com/ra1028/DiffableDataSources) -- [DifferenceKit](https://github.com/ra1028/DifferenceKit) - [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) - [FLEX](https://github.com/FLEXTool/FLEX) - [FPSIndicator](https://github.com/MainasuK/FPSIndicator) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 0a81bbdfe..45c70af5b 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -230,7 +230,6 @@ DB3EA8EF281B837000598866 /* DiscoveryCommunityViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EA8EE281B837000598866 /* DiscoveryCommunityViewController+DataSourceProvider.swift */; }; DB3EA8F1281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EA8F0281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift */; }; DB3EA8F5281BB65200598866 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA8F4281BB65200598866 /* MastodonSDK */; }; - DB3EA902281BBD5D00598866 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA901281BBD5D00598866 /* CommonOSLog */; }; DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; }; DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; }; DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDB25BAA00100D1B89D /* Main.storyboard */; }; @@ -255,7 +254,6 @@ DB47AB6227CF752B00CD73C7 /* MastodonUISnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB6127CF752B00CD73C7 /* MastodonUISnapshotTests.swift */; }; DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */; }; DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */; }; - DB486C0F282E41F200F69423 /* TabBarPager in Frameworks */ = {isa = PBXBuildFile; productRef = DB486C0E282E41F200F69423 /* TabBarPager */; }; DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4924E126312AB200E9DB22 /* NotificationService.swift */; }; DB4932B126F1FB5300EF46D4 /* WizardCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4932B026F1FB5300EF46D4 /* WizardCardView.swift */; }; DB4932B726F30F0700EF46D4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array.swift */; }; @@ -275,7 +273,6 @@ DB4F097F26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */; }; DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */; }; DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */; }; - DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = DB552D4E26BBD10C00E481F6 /* OrderedCollections */; }; DB564BD3269F3B35001E39A7 /* StatusFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */; }; DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */; }; DB5B549A2833A60400DEF8B2 /* FamiliarFollowersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B54992833A60400DEF8B2 /* FamiliarFollowersViewController.swift */; }; @@ -484,9 +481,6 @@ DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */; }; DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */; }; DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; }; - DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC6482267D0B21007FE9FD /* DifferenceKit */; }; - DBAC649E267DFE43007FE9FD /* DiffableDataSources in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC649D267DFE43007FE9FD /* DiffableDataSources */; }; - DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC64A0267E6D02007FE9FD /* Fuzi */; }; DBAE3F8E2616E0B1004B8251 /* APIService+Block.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F8D2616E0B1004B8251 /* APIService+Block.swift */; }; DBAE3F942616E28B004B8251 /* APIService+Follow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F932616E28B004B8251 /* APIService+Follow.swift */; }; DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; }; @@ -498,7 +492,6 @@ DBB45B5B27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5A27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift */; }; DBB45B6027B50A4F002DC5A7 /* RecommendAccountItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */; }; DBB45B6227B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B6127B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift */; }; - DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; }; DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; }; DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */; }; DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */; }; @@ -1412,14 +1405,8 @@ buildActionMask = 2147483647; files = ( 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */, - DBB525082611EAC0002F1F29 /* Tabman in Frameworks */, DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, - DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */, - DB486C0F282E41F200F69423 /* TabBarPager in Frameworks */, - DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */, - DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */, - DBAC649E267DFE43007FE9FD /* DiffableDataSources in Frameworks */, 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */, 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */, DBF7A0FC26830C33004176A2 /* FPSIndicator in Frameworks */, @@ -1450,7 +1437,6 @@ files = ( DB3EA8F5281BB65200598866 /* MastodonSDK in Frameworks */, EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */, - DB3EA902281BBD5D00598866 /* CommonOSLog in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3424,14 +3410,8 @@ packageProductDependencies = ( 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */, 2D939AC725EE14620076FA61 /* CropViewController */, - DBB525072611EAC0002F1F29 /* Tabman */, - DBAC6482267D0B21007FE9FD /* DifferenceKit */, - DBAC649D267DFE43007FE9FD /* DiffableDataSources */, - DBAC64A0267E6D02007FE9FD /* Fuzi */, DBF7A0FB26830C33004176A2 /* FPSIndicator */, - DB552D4E26BBD10C00E481F6 /* OrderedCollections */, DBA5A52E26F07ED800CACBAA /* PanModal */, - DB486C0E282E41F200F69423 /* TabBarPager */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -3493,7 +3473,6 @@ name = AppShared; packageProductDependencies = ( DB3EA8F4281BB65200598866 /* MastodonSDK */, - DB3EA901281BBD5D00598866 /* CommonOSLog */, ); productName = AppShared; productReference = DB68047F2637CD4C00430867 /* AppShared.framework */; @@ -3626,18 +3605,10 @@ ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( - DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */, 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */, - DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */, - DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */, - DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */, - DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */, DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */, - DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */, - DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */, DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */, - DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -5718,38 +5689,6 @@ minimumVersion = 2.6.0; }; }; - DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/MainasuK/CommonOSLog"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.1.1; - }; - }; - DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 8.0.0; - }; - }; - DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/TwidereProject/TabBarPager.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.1.0; - }; - }; - DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/apple/swift-collections.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.0.5; - }; - }; DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/slackhq/PanModal.git"; @@ -5758,38 +5697,6 @@ minimumVersion = 1.2.7; }; }; - DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/ra1028/DifferenceKit.git"; - requirement = { - kind = exactVersion; - version = 1.2.0; - }; - }; - DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/MainasuK/DiffableDataSources.git"; - requirement = { - branch = "feature/async-display-table"; - kind = branch; - }; - }; - DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/cezheng/Fuzi.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 3.1.3; - }; - }; - DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/uias/Tabman"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.11.0; - }; - }; DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/FPSIndicator.git"; @@ -5815,46 +5722,11 @@ isa = XCSwiftPackageProductDependency; productName = MastodonSDK; }; - DB3EA901281BBD5D00598866 /* CommonOSLog */ = { - isa = XCSwiftPackageProductDependency; - package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; - productName = CommonOSLog; - }; - DB486C0E282E41F200F69423 /* TabBarPager */ = { - isa = XCSwiftPackageProductDependency; - package = DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */; - productName = TabBarPager; - }; - DB552D4E26BBD10C00E481F6 /* OrderedCollections */ = { - isa = XCSwiftPackageProductDependency; - package = DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */; - productName = OrderedCollections; - }; DBA5A52E26F07ED800CACBAA /* PanModal */ = { isa = XCSwiftPackageProductDependency; package = DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */; productName = PanModal; }; - DBAC6482267D0B21007FE9FD /* DifferenceKit */ = { - isa = XCSwiftPackageProductDependency; - package = DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */; - productName = DifferenceKit; - }; - DBAC649D267DFE43007FE9FD /* DiffableDataSources */ = { - isa = XCSwiftPackageProductDependency; - package = DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */; - productName = DiffableDataSources; - }; - DBAC64A0267E6D02007FE9FD /* Fuzi */ = { - isa = XCSwiftPackageProductDependency; - package = DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */; - productName = Fuzi; - }; - DBB525072611EAC0002F1F29 /* Tabman */ = { - isa = XCSwiftPackageProductDependency; - package = DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */; - productName = Tabman; - }; DBF7A0FB26830C33004176A2 /* FPSIndicator */ = { isa = XCSwiftPackageProductDependency; package = DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index d0b79ce67..effcc5bd7 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -129,12 +129,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 21 + 22 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 22 + 21 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index b51726639..4241eeed7 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,259 +1,239 @@ { - "object": { - "pins": [ - { - "package": "Alamofire", - "repositoryURL": "https://github.com/Alamofire/Alamofire.git", - "state": { - "branch": null, - "revision": "354dda32d89fc8cd4f5c46487f64957d355f53d8", - "version": "5.6.1" - } - }, - { - "package": "AlamofireImage", - "repositoryURL": "https://github.com/Alamofire/AlamofireImage.git", - "state": { - "branch": null, - "revision": "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10", - "version": "4.2.0" - } - }, - { - "package": "AlamofireNetworkActivityIndicator", - "repositoryURL": "https://github.com/Alamofire/AlamofireNetworkActivityIndicator", - "state": { - "branch": null, - "revision": "392bed083e8d193aca16bfa684ee24e4bcff0510", - "version": "3.1.0" - } - }, - { - "package": "CommonOSLog", - "repositoryURL": "https://github.com/MainasuK/CommonOSLog", - "state": { - "branch": null, - "revision": "c121624a30698e9886efe38aebb36ff51c01b6c2", - "version": "0.1.1" - } - }, - { - "package": "DiffableDataSources", - "repositoryURL": "https://github.com/MainasuK/DiffableDataSources.git", - "state": { - "branch": "feature/async-display-table", - "revision": "73393a97690959d24387c95594c045c62d9c47cf", - "version": null - } - }, - { - "package": "DifferenceKit", - "repositoryURL": "https://github.com/ra1028/DifferenceKit.git", - "state": { - "branch": null, - "revision": "62745d7780deef4a023a792a1f8f763ec7bf9705", - "version": "1.2.0" - } - }, - { - "package": "FaviconFinder", - "repositoryURL": "https://github.com/will-lumley/FaviconFinder.git", - "state": { - "branch": null, - "revision": "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a", - "version": "3.3.0" - } - }, - { - "package": "FLAnimatedImage", - "repositoryURL": "https://github.com/Flipboard/FLAnimatedImage.git", - "state": { - "branch": null, - "revision": "e7f9fd4681ae41bf6f3056db08af4f401d61da52", - "version": "1.0.16" - } - }, - { - "package": "FPSIndicator", - "repositoryURL": "https://github.com/MainasuK/FPSIndicator.git", - "state": { - "branch": null, - "revision": "e4a5067ccd5293b024c767f09e51056afd4a4796", - "version": "1.1.0" - } - }, - { - "package": "Fuzi", - "repositoryURL": "https://github.com/cezheng/Fuzi.git", - "state": { - "branch": null, - "revision": "f08c8323da21e985f3772610753bcfc652c2103f", - "version": "3.1.3" - } - }, - { - "package": "KeychainAccess", - "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", - "state": { - "branch": null, - "revision": "84e546727d66f1adc5439debad16270d0fdd04e7", - "version": "4.2.2" - } - }, - { - "package": "MetaTextKit", - "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", - "state": { - "branch": null, - "revision": "dcd5255d6930c2fab408dc8562c577547e477624", - "version": "2.2.5" - } - }, - { - "package": "Nuke", - "repositoryURL": "https://github.com/kean/Nuke.git", - "state": { - "branch": null, - "revision": "a002b7fd786f2df2ed4333fe73a9727499fd9d97", - "version": "10.11.2" - } - }, - { - "package": "NukeFLAnimatedImagePlugin", - "repositoryURL": "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", - "state": { - "branch": null, - "revision": "b59c346a7d536336db3b0f12c72c6e53ee709e16", - "version": "8.0.0" - } - }, - { - "package": "Pageboy", - "repositoryURL": "https://github.com/uias/Pageboy", - "state": { - "branch": null, - "revision": "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6", - "version": "3.6.2" - } - }, - { - "package": "PanModal", - "repositoryURL": "https://github.com/slackhq/PanModal.git", - "state": { - "branch": null, - "revision": "b012aecb6b67a8e46369227f893c12544846613f", - "version": "1.2.7" - } - }, - { - "package": "SDWebImage", - "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", - "state": { - "branch": null, - "revision": "c4b8660bb3ef543fe4bdcaac0db956b32dc5583f", - "version": "5.13.0" - } - }, - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections.git", - "state": { - "branch": null, - "revision": "9d8719c8bebdc79740b6969c912ac706eb721d7a", - "version": "0.0.7" - } - }, - { - "package": "swift-nio", - "repositoryURL": "https://github.com/apple/swift-nio.git", - "state": { - "branch": null, - "revision": "546610d52b19be3e19935e0880bb06b9c03f5cef", - "version": "1.14.4" - } - }, - { - "package": "swift-nio-zlib-support", - "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git", - "state": { - "branch": null, - "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd", - "version": "1.0.0" - } - }, - { - "package": "SwiftSoup", - "repositoryURL": "https://github.com/scinfu/SwiftSoup.git", - "state": { - "branch": null, - "revision": "6778575285177365cbad3e5b8a72f2a20583cfec", - "version": "2.4.3" - } - }, - { - "package": "Introspect", - "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git", - "state": { - "branch": null, - "revision": "f2616860a41f9d9932da412a8978fec79c06fe24", - "version": "0.1.4" - } - }, - { - "package": "SwiftyJSON", - "repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git", - "state": { - "branch": null, - "revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07", - "version": "5.0.1" - } - }, - { - "package": "TabBarPager", - "repositoryURL": "https://github.com/TwidereProject/TabBarPager.git", - "state": { - "branch": null, - "revision": "488aa66d157a648901b61721212c0dec23d27ee5", - "version": "0.1.0" - } - }, - { - "package": "Tabman", - "repositoryURL": "https://github.com/uias/Tabman", - "state": { - "branch": null, - "revision": "a9f10cb862a32e6a22549836af013abd6b0692d3", - "version": "2.12.0" - } - }, - { - "package": "ThirdPartyMailer", - "repositoryURL": "https://github.com/vtourraine/ThirdPartyMailer.git", - "state": { - "branch": null, - "revision": "779da6ce0793b461ccbbac2804755c1e29b6fa63", - "version": "1.8.0" - } - }, - { - "package": "TOCropViewController", - "repositoryURL": "https://github.com/TimOliver/TOCropViewController.git", - "state": { - "branch": null, - "revision": "d0470491f56e734731bbf77991944c0dfdee3e0e", - "version": "2.6.1" - } - }, - { - "package": "UITextView+Placeholder", - "repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git", - "state": { - "branch": null, - "revision": "20f513ded04a040cdf5467f0891849b1763ede3b", - "version": "1.4.1" - } + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire.git", + "state" : { + "revision" : "354dda32d89fc8cd4f5c46487f64957d355f53d8", + "version" : "5.6.1" } - ] - }, - "version": 1 + }, + { + "identity" : "alamofireimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/AlamofireImage.git", + "state" : { + "revision" : "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10", + "version" : "4.2.0" + } + }, + { + "identity" : "alamofirenetworkactivityindicator", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/AlamofireNetworkActivityIndicator", + "state" : { + "revision" : "392bed083e8d193aca16bfa684ee24e4bcff0510", + "version" : "3.1.0" + } + }, + { + "identity" : "commonoslog", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MainasuK/CommonOSLog", + "state" : { + "revision" : "c121624a30698e9886efe38aebb36ff51c01b6c2", + "version" : "0.1.1" + } + }, + { + "identity" : "faviconfinder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/will-lumley/FaviconFinder.git", + "state" : { + "revision" : "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a", + "version" : "3.3.0" + } + }, + { + "identity" : "flanimatedimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Flipboard/FLAnimatedImage.git", + "state" : { + "revision" : "e7f9fd4681ae41bf6f3056db08af4f401d61da52", + "version" : "1.0.16" + } + }, + { + "identity" : "fpsindicator", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MainasuK/FPSIndicator.git", + "state" : { + "revision" : "e4a5067ccd5293b024c767f09e51056afd4a4796", + "version" : "1.1.0" + } + }, + { + "identity" : "fuzi", + "kind" : "remoteSourceControl", + "location" : "https://github.com/cezheng/Fuzi.git", + "state" : { + "revision" : "f08c8323da21e985f3772610753bcfc652c2103f", + "version" : "3.1.3" + } + }, + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", + "state" : { + "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", + "version" : "4.2.2" + } + }, + { + "identity" : "metatextkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TwidereProject/MetaTextKit.git", + "state" : { + "revision" : "dcd5255d6930c2fab408dc8562c577547e477624", + "version" : "2.2.5" + } + }, + { + "identity" : "nuke", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kean/Nuke.git", + "state" : { + "revision" : "a002b7fd786f2df2ed4333fe73a9727499fd9d97", + "version" : "10.11.2" + } + }, + { + "identity" : "nuke-flanimatedimage-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", + "state" : { + "revision" : "b59c346a7d536336db3b0f12c72c6e53ee709e16", + "version" : "8.0.0" + } + }, + { + "identity" : "pageboy", + "kind" : "remoteSourceControl", + "location" : "https://github.com/uias/Pageboy", + "state" : { + "revision" : "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6", + "version" : "3.6.2" + } + }, + { + "identity" : "panmodal", + "kind" : "remoteSourceControl", + "location" : "https://github.com/slackhq/PanModal.git", + "state" : { + "revision" : "b012aecb6b67a8e46369227f893c12544846613f", + "version" : "1.2.7" + } + }, + { + "identity" : "sdwebimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SDWebImage/SDWebImage.git", + "state" : { + "revision" : "c4b8660bb3ef543fe4bdcaac0db956b32dc5583f", + "version" : "5.13.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "48254824bb4248676bf7ce56014ff57b142b77eb", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "546610d52b19be3e19935e0880bb06b9c03f5cef", + "version" : "1.14.4" + } + }, + { + "identity" : "swift-nio-zlib-support", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-zlib-support.git", + "state" : { + "revision" : "37760e9a52030bb9011972c5213c3350fa9d41fd", + "version" : "1.0.0" + } + }, + { + "identity" : "swiftsoup", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scinfu/SwiftSoup.git", + "state" : { + "revision" : "6778575285177365cbad3e5b8a72f2a20583cfec", + "version" : "2.4.3" + } + }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", + "state" : { + "revision" : "f2616860a41f9d9932da412a8978fec79c06fe24", + "version" : "0.1.4" + } + }, + { + "identity" : "swiftyjson", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftyJSON/SwiftyJSON.git", + "state" : { + "revision" : "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07", + "version" : "5.0.1" + } + }, + { + "identity" : "tabbarpager", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TwidereProject/TabBarPager.git", + "state" : { + "revision" : "488aa66d157a648901b61721212c0dec23d27ee5", + "version" : "0.1.0" + } + }, + { + "identity" : "tabman", + "kind" : "remoteSourceControl", + "location" : "https://github.com/uias/Tabman", + "state" : { + "revision" : "a9f10cb862a32e6a22549836af013abd6b0692d3", + "version" : "2.12.0" + } + }, + { + "identity" : "thirdpartymailer", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vtourraine/ThirdPartyMailer.git", + "state" : { + "revision" : "779da6ce0793b461ccbbac2804755c1e29b6fa63", + "version" : "1.8.0" + } + }, + { + "identity" : "tocropviewcontroller", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TimOliver/TOCropViewController.git", + "state" : { + "revision" : "d0470491f56e734731bbf77991944c0dfdee3e0e", + "version" : "2.6.1" + } + }, + { + "identity" : "uitextview-placeholder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MainasuK/UITextView-Placeholder.git", + "state" : { + "revision" : "20f513ded04a040cdf5467f0891849b1763ede3b", + "version" : "1.4.1" + } + } + ], + "version" : 2 } diff --git a/MastodonSDK/Package.swift b/MastodonSDK/Package.swift index ea3f2b01d..aa8e03095 100644 --- a/MastodonSDK/Package.swift +++ b/MastodonSDK/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.6 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -32,9 +32,14 @@ let package = Package( .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"), .package(url: "https://github.com/Alamofire/AlamofireNetworkActivityIndicator", from: "3.1.0"), .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"), + .package(url: "https://github.com/cezheng/Fuzi.git", from: "3.1.3"), + .package(url: "https://github.com/MainasuK/CommonOSLog.git", from: "0.1.1"), + .package(url: "https://github.com/uias/Tabman.git", from: "2.12.0"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.2"), + .package(url: "https://github.com/TwidereProject/TabBarPager.git", from: "0.1.0"), .package(name: "NukeFLAnimatedImagePlugin", url: "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", from: "8.0.0"), .package(name: "UITextView+Placeholder", url: "https://github.com/MainasuK/UITextView-Placeholder.git", from: "1.4.1"), - .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.3"), + .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"), .package(name: "FaviconFinder", url: "https://github.com/will-lumley/FaviconFinder.git", from: "3.2.2"), .package(name: "ArkanaKeys", path: "../Dependencies/ArkanaKeys"), ], @@ -62,6 +67,9 @@ let package = Package( dependencies: [ "ArkanaKeys", "MastodonExtension", + .product(name: "Collections", package: "swift-collections"), + .product(name: "CommonOSLog", package: "CommonOSLog"), + .product(name: "Fuzi", package: "Fuzi"), .product(name: "KeychainAccess", package: "KeychainAccess"), ] ), @@ -98,6 +106,8 @@ let package = Package( .product(name: "NukeFLAnimatedImagePlugin", package: "NukeFLAnimatedImagePlugin"), .product(name: "Introspect", package: "Introspect"), .product(name: "UITextView+Placeholder", package: "UITextView+Placeholder"), + .product(name: "Tabman", package: "Tabman"), + .product(name: "TabBarPager", package: "TabBarPager"), ] ), .testTarget( From 213ef94ec553e3c8d43ca1aac041c9fe600b33d9 Mon Sep 17 00:00:00 2001 From: nyaxix <97578718+protolimit@users.noreply.github.com> Date: Fri, 29 Jul 2022 15:31:38 -0500 Subject: [PATCH 030/658] Add bookmarking and bookmarks view Based heavily on the work for favorites. Adds bookmarking functionality to the application. The status view has been updated to include a bookmark button that can bookmark/unbookmark a status. The profile page has been updated to include a button in the header to navigate to a page that lists your bookmarks. --- Mastodon.xcodeproj/project.pbxproj | 36 ++++ Mastodon/Coordinator/SceneCoordinator.swift | 5 + .../Provider/DataSourceFacade+Bookmark.swift | 26 +++ .../Provider/DataSourceFacade+Status.swift | 6 + ...arkViewController+DataSourceProvider.swift | 34 ++++ .../Bookmark/BookmarkViewController.swift | 151 ++++++++++++++ .../Bookmark/BookmarkViewModel+Diffable.swift | 65 ++++++ .../Bookmark/BookmarkViewModel+State.swift | 191 ++++++++++++++++++ .../Profile/Bookmark/BookmarkViewModel.swift | 58 ++++++ .../Scene/Profile/ProfileViewController.swift | 18 ++ .../Content/StatusView+Configuration.swift | 13 ++ .../APIService/APIService+Bookmark.swift | 142 +++++++++++++ .../bookmark.fill.imageset/Contents.json | 15 ++ .../bookmark.fill.imageset/bookmark-solid.pdf | Bin 0 -> 1200 bytes .../bookmark.imageset/Contents.json | 15 ++ .../bookmark.imageset/bookmark-regular.pdf | Bin 0 -> 1226 bytes .../MastodonAsset/Generated/Assets.swift | 2 + .../Generated/Strings.swift | 8 + .../Resources/en.lproj/Localizable.strings | 5 +- .../API/Mastodon+API+Bookmarks.swift | 136 +++++++++++++ .../MastodonSDK/API/Mastodon+API.swift | 1 + .../View/Content/StatusView+ViewModel.swift | 8 + .../View/Control/ActionToolbarContainer.swift | 37 +++- 23 files changed, 969 insertions(+), 3 deletions(-) create mode 100644 Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift create mode 100644 Mastodon/Scene/Profile/Bookmark/BookmarkViewController+DataSourceProvider.swift create mode 100644 Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift create mode 100644 Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+Diffable.swift create mode 100644 Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift create mode 100644 Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift create mode 100644 Mastodon/Service/APIService/APIService+Bookmark.swift create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/ObjectsAndTools/bookmark.fill.imageset/Contents.json create mode 100755 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/ObjectsAndTools/bookmark.fill.imageset/bookmark-solid.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/ObjectsAndTools/bookmark.imageset/Contents.json create mode 100755 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/ObjectsAndTools/bookmark.imageset/bookmark-regular.pdf create mode 100644 MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Bookmarks.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 04c6b5c8f..4106af7de 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -107,6 +107,13 @@ 5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */; }; 5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; }; 5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; }; + 6213AF5828939C4800BCADB6 /* APIService+Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213AF5728939C4700BCADB6 /* APIService+Bookmark.swift */; }; + 6213AF5A28939C8400BCADB6 /* BookmarkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213AF5928939C8400BCADB6 /* BookmarkViewModel.swift */; }; + 6213AF5C28939C8A00BCADB6 /* BookmarkViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213AF5B28939C8A00BCADB6 /* BookmarkViewModel+State.swift */; }; + 6213AF5E2893A8B200BCADB6 /* DataSourceFacade+Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213AF5D2893A8B200BCADB6 /* DataSourceFacade+Bookmark.swift */; }; + 62FD27D12893707600B205C5 /* BookmarkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FD27D02893707600B205C5 /* BookmarkViewController.swift */; }; + 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 */; }; 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 */; }; @@ -824,6 +831,13 @@ 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+History.swift"; sourceTree = ""; }; 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = ""; }; 6130CBE4B26E3C976ACC1688 /* Pods-ShareActionExtension.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareActionExtension.asdk - debug.xcconfig"; path = "Target Support Files/Pods-ShareActionExtension/Pods-ShareActionExtension.asdk - debug.xcconfig"; sourceTree = ""; }; + 6213AF5728939C4700BCADB6 /* APIService+Bookmark.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Bookmark.swift"; sourceTree = ""; }; + 6213AF5928939C8400BCADB6 /* BookmarkViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkViewModel.swift; sourceTree = ""; }; + 6213AF5B28939C8A00BCADB6 /* BookmarkViewModel+State.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BookmarkViewModel+State.swift"; sourceTree = ""; }; + 6213AF5D2893A8B200BCADB6 /* DataSourceFacade+Bookmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Bookmark.swift"; sourceTree = ""; }; + 62FD27D02893707600B205C5 /* BookmarkViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkViewController.swift; sourceTree = ""; }; + 62FD27D22893707B00B205C5 /* BookmarkViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BookmarkViewController+DataSourceProvider.swift"; sourceTree = ""; }; + 62FD27D42893708A00B205C5 /* BookmarkViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BookmarkViewModel+Diffable.swift"; sourceTree = ""; }; 63EF9E6E5B575CD2A8B0475D /* Pods-AppShared.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.profile.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.profile.xcconfig"; sourceTree = ""; }; 728DE51ADA27C395C6E1BAB5 /* Pods-Mastodon-MastodonUITests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.profile.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.profile.xcconfig"; sourceTree = ""; }; 75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = ""; }; @@ -1934,6 +1948,18 @@ path = Webview; sourceTree = ""; }; + 62047EBE28874C8F00A3BA5D /* Bookmark */ = { + isa = PBXGroup; + children = ( + 62FD27D02893707600B205C5 /* BookmarkViewController.swift */, + 62FD27D22893707B00B205C5 /* BookmarkViewController+DataSourceProvider.swift */, + 6213AF5928939C8400BCADB6 /* BookmarkViewModel.swift */, + 62FD27D42893708A00B205C5 /* BookmarkViewModel+Diffable.swift */, + 6213AF5B28939C8A00BCADB6 /* BookmarkViewModel+State.swift */, + ); + path = Bookmark; + sourceTree = ""; + }; DB01409B25C40BB600F9F3CF /* Onboarding */ = { isa = PBXGroup; children = ( @@ -2332,6 +2358,7 @@ DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */, 5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */, DB9D7C20269824B80054B3DF /* APIService+Filter.swift */, + 6213AF5728939C4700BCADB6 /* APIService+Bookmark.swift */, ); path = APIService; sourceTree = ""; @@ -2678,6 +2705,7 @@ isa = PBXGroup; children = ( DB697DDC278F521D004EF2F7 /* DataSourceFacade.swift */, + 6213AF5D2893A8B200BCADB6 /* DataSourceFacade+Bookmark.swift */, DB697DE0278F5296004EF2F7 /* DataSourceFacade+Model.swift */, DB697DDE278F524F004EF2F7 /* DataSourceFacade+Profile.swift */, DB8F7075279E954700E1225B /* DataSourceFacade+Follow.swift */, @@ -3024,6 +3052,7 @@ DB9D6C0825E4F5A60051B173 /* Profile */ = { isa = PBXGroup; children = ( + 62047EBE28874C8F00A3BA5D /* Bookmark */, DBB525462611ED57002F1F29 /* Header */, DBB525262611EBDA002F1F29 /* Paging */, DBB5253B2611ECF5002F1F29 /* Timeline */, @@ -3953,6 +3982,7 @@ DBE3CDCF261C42ED00430CC6 /* TimelineHeaderView.swift in Sources */, DB6746E7278ED633008A6B94 /* MastodonAuthenticationBox.swift in Sources */, DBAE3F8E2616E0B1004B8251 /* APIService+Block.swift in Sources */, + 62FD27D32893707B00B205C5 /* BookmarkViewController+DataSourceProvider.swift in Sources */, DB1D843426579931000346B3 /* TableViewControllerNavigateable.swift in Sources */, 0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */, DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */, @@ -3997,6 +4027,7 @@ 0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */, DB443CD42694627B00159B29 /* AppearanceView.swift in Sources */, DBF1D24E269DAF5D00C1C08A /* SearchDetailViewController.swift in Sources */, + 62FD27D52893708A00B205C5 /* BookmarkViewModel+Diffable.swift in Sources */, DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */, DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */, DB336F36278D77A40031E64B /* PollOption+Property.swift in Sources */, @@ -4020,6 +4051,7 @@ 2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */, DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */, DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */, + 62FD27D12893707600B205C5 /* BookmarkViewController.swift in Sources */, DBA5E7A3263AD0A3004598BB /* PhotoLibraryService.swift in Sources */, DBD5B1F627BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */, 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */, @@ -4169,6 +4201,7 @@ DB63F75C279956D000455B82 /* Persistence+Tag.swift in Sources */, 2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */, DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */, + 6213AF5A28939C8400BCADB6 /* BookmarkViewModel.swift in Sources */, 5B24BBDB262DB14800A9381B /* ReportStatusViewModel+Diffable.swift in Sources */, DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */, 0FB3D2FE25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift in Sources */, @@ -4288,6 +4321,7 @@ DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */, DB336F38278D7AAF0031E64B /* Poll+Property.swift in Sources */, 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */, + 6213AF5C28939C8A00BCADB6 /* BookmarkViewModel+State.swift in Sources */, DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */, 2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */, DBA465932696B495002B41DB /* APIService+WebFinger.swift in Sources */, @@ -4297,6 +4331,7 @@ DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */, 2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */, DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */, + 6213AF5E2893A8B200BCADB6 /* DataSourceFacade+Bookmark.swift in Sources */, DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */, DBF156E22702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m in Sources */, 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */, @@ -4445,6 +4480,7 @@ 5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */, DBEFCD82282A2AB100C0ABEA /* ReportServerRulesView.swift in Sources */, DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */, + 6213AF5828939C4800BCADB6 /* APIService+Bookmark.swift in Sources */, DB023D2A27A0FE5C005AC798 /* DataSourceProvider+NotificationTableViewCellDelegate.swift in Sources */, DB98EB6027B10E150082E365 /* ReportCommentTableViewCell.swift in Sources */, DB0FCB962797E6C2006C02E2 /* SearchResultViewController+DataSourceProvider.swift in Sources */, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 4491d383a..82c58e1f6 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -181,6 +181,7 @@ extension SceneCoordinator { case familiarFollowers(viewModel: FamiliarFollowersViewModel) case rebloggedBy(viewModel: UserListViewModel) case favoritedBy(viewModel: UserListViewModel) + case bookmark(viewModel: BookmarkViewModel) // setting case settings(viewModel: SettingsViewModel) @@ -437,6 +438,10 @@ private extension SceneCoordinator { let _viewController = ProfileViewController() _viewController.viewModel = viewModel viewController = _viewController + case .bookmark(let viewModel): + let _viewController = BookmarkViewController() + _viewController.viewModel = viewModel + viewController = _viewController case .favorite(let viewModel): let _viewController = FavoriteViewController() _viewController.viewModel = viewModel diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift new file mode 100644 index 000000000..0c467778d --- /dev/null +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift @@ -0,0 +1,26 @@ +// +// DataSourceFacade+Bookmark.swift +// Mastodon +// +// Created by ProtoLimit on 2022/07/29. +// + +import UIKit +import CoreData +import CoreDataStack + +extension DataSourceFacade { + static func responseToStatusBookmarkAction( + provider: DataSourceProvider, + status: ManagedObjectRecord, + authenticationBox: MastodonAuthenticationBox + ) async throws { + let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() + await selectionFeedbackGenerator.selectionChanged() + + _ = try await provider.context.apiService.bookmark( + record: status, + authenticationBox: authenticationBox + ) + } +} diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 36ceb6dd6..4c948c716 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -125,6 +125,12 @@ extension DataSourceFacade { status: status, authenticationBox: authenticationBox ) + case .bookmark: + try await DataSourceFacade.responseToStatusBookmarkAction( + provider: provider, + status: status, + authenticationBox: authenticationBox + ) case .share: try await DataSourceFacade.responseToStatusShareAction( provider: provider, diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController+DataSourceProvider.swift new file mode 100644 index 000000000..a22fb4309 --- /dev/null +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController+DataSourceProvider.swift @@ -0,0 +1,34 @@ +// +// BookmarkViewController+DataSourceProvider.swift +// Mastodon +// +// Created by ProtoLimit on 2022-07-19. +// + +import UIKit + +extension BookmarkViewController: DataSourceProvider { + func item(from source: DataSourceItem.Source) async -> DataSourceItem? { + var _indexPath = source.indexPath + if _indexPath == nil, let cell = source.tableViewCell { + _indexPath = await self.indexPath(for: cell) + } + guard let indexPath = _indexPath else { return nil } + + guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { + return nil + } + + switch item { + case .status(let record): + return .status(record: record) + default: + return nil + } + } + + @MainActor + private func indexPath(for cell: UITableViewCell) async -> IndexPath? { + return tableView.indexPath(for: cell) + } +} diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift new file mode 100644 index 000000000..18e3c34fd --- /dev/null +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift @@ -0,0 +1,151 @@ +// +// BookmarkViewController.swift +// Mastodon +// +// Created by ProtoLimit on 2022-07-19. +// + +import os.log +import UIKit +import AVKit +import Combine +import GameplayKit +import MastodonAsset +import MastodonLocalization + +final class BookmarkViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { + + let logger = Logger(subsystem: "BookmarkViewController", category: "ViewController") + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + var disposeBag = Set() + var viewModel: BookmarkViewModel! + + let mediaPreviewTransitionController = MediaPreviewTransitionController() + + let titleView = DoubleTitleLabelNavigationBarTitleView() + + lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self)) + tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) + tableView.rowHeight = UITableView.automaticDimension + tableView.separatorStyle = .none + tableView.backgroundColor = .clear + return tableView + }() + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension BookmarkViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor + ThemeService.shared.currentTheme + .receive(on: DispatchQueue.main) + .sink { [weak self] theme in + guard let self = self else { return } + self.view.backgroundColor = theme.secondarySystemBackgroundColor + } + .store(in: &disposeBag) + + navigationItem.titleView = titleView + titleView.update(title: L10n.Scene.Bookmark.title, subtitle: nil) + + tableView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(tableView) + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + tableView.delegate = self + viewModel.setupDiffableDataSource( + tableView: tableView, + statusTableViewCellDelegate: self + ) + + // setup batch fetch + viewModel.listBatchFetchViewModel.setup(scrollView: tableView) + viewModel.listBatchFetchViewModel.shouldFetch + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + self.viewModel.stateMachine.enter(BookmarkViewModel.State.Loading.self) + } + .store(in: &disposeBag) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + tableView.deselectRow(with: transitionCoordinator, animated: animated) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + +// aspectViewDidDisappear(animated) + } + +} + +// MARK: - UITableViewDelegate +extension BookmarkViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { + // sourcery:inline:BookmarkViewController.AutoGenerateTableViewDelegate + + // Generated using Sourcery + // DO NOT EDIT + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + aspectTableView(tableView, didSelectRowAt: indexPath) + } + + func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + return aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point) + } + + func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + return aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration) + } + + func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + return aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration) + } + + func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { + aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator) + } + + + // sourcery:end +} + +// MARK: - StatusTableViewCellDelegate +extension BookmarkViewController: StatusTableViewCellDelegate { } + +extension BookmarkViewController { + override var keyCommands: [UIKeyCommand]? { + return navigationKeyCommands + statusNavigationKeyCommands + } +} + +// MARK: - StatusTableViewControllerNavigateable +extension BookmarkViewController: StatusTableViewControllerNavigateable { + @objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) { + navigateKeyCommandHandler(sender) + } + + @objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) { + statusKeyCommandHandler(sender) + } +} diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+Diffable.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+Diffable.swift new file mode 100644 index 000000000..06483012c --- /dev/null +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+Diffable.swift @@ -0,0 +1,65 @@ +// +// BookmarkViewModel+Diffable.swift +// Mastodon +// +// Created by ProtoLimit on 2022-07-19. +// + +import UIKit + +extension BookmarkViewModel { + + func setupDiffableDataSource( + tableView: UITableView, + statusTableViewCellDelegate: StatusTableViewCellDelegate + ) { + diffableDataSource = StatusSection.diffableDataSource( + tableView: tableView, + context: context, + configuration: StatusSection.Configuration( + statusTableViewCellDelegate: statusTableViewCellDelegate, + timelineMiddleLoaderTableViewCellDelegate: nil, + filterContext: .none, + activeFilters: nil + ) + ) + // set empty section to make update animation top-to-bottom style + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + diffableDataSource?.apply(snapshot) + + stateMachine.enter(State.Reloading.self) + + statusFetchedResultsController.$records + .receive(on: DispatchQueue.main) + .sink { [weak self] records in + guard let self = self else { return } + guard let diffableDataSource = self.diffableDataSource else { return } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + + let items = records.map { StatusItem.status(record: $0) } + snapshot.appendItems(items, toSection: .main) + + if let currentState = self.stateMachine.currentState { + switch currentState { + case is State.Reloading, + is State.Loading, + is State.Idle, + is State.Fail: + snapshot.appendItems([.bottomLoader], toSection: .main) + case is State.NoMore: + break + default: + assertionFailure() + break + } + } + + diffableDataSource.applySnapshot(snapshot, animated: false) + } + .store(in: &disposeBag) + } + +} diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift new file mode 100644 index 000000000..eda823b50 --- /dev/null +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift @@ -0,0 +1,191 @@ +// +// BookmarkViewModel+State.swift +// Mastodon +// +// Created by ProtoLimit on 2022-07-19. +// + +import os.log +import Foundation +import GameplayKit +import MastodonSDK + +extension BookmarkViewModel { + class State: GKState, NamingState { + + let logger = Logger(subsystem: "BookmarkViewModel.State", category: "StateMachine") + + let id = UUID() + + var name: String { + String(describing: Self.self) + } + + weak var viewModel: BookmarkViewModel? + + init(viewModel: BookmarkViewModel) { + self.viewModel = viewModel + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + let previousState = previousState as? BookmarkViewModel.State + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "")") + } + + @MainActor + func enter(state: State.Type) { + stateMachine?.enter(state) + } + + deinit { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + } + } +} + +extension BookmarkViewModel.State { + class Initial: BookmarkViewModel.State { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + guard let viewModel = viewModel else { return false } + switch stateClass { + case is Reloading.Type: + return viewModel.activeMastodonAuthenticationBox.value != nil + default: + return false + } + } + } + + class Reloading: BookmarkViewModel.State { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + switch stateClass { + case is Loading.Type: + return true + default: + return false + } + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + + // reset + viewModel.statusFetchedResultsController.statusIDs.value = [] + + stateMachine.enter(Loading.self) + } + } + + class Fail: BookmarkViewModel.State { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + switch stateClass { + case is Loading.Type: + return true + default: + return false + } + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + guard let _ = viewModel, let stateMachine = stateMachine else { return } + + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading 3s later…", ((#file as NSString).lastPathComponent), #line, #function) + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading", ((#file as NSString).lastPathComponent), #line, #function) + stateMachine.enter(Loading.self) + } + } + } + + class Idle: BookmarkViewModel.State { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + switch stateClass { + case is Reloading.Type, is Loading.Type: + return true + default: + return false + } + } + } + + class Loading: BookmarkViewModel.State { + + // prefer use `maxID` token in response header + var maxID: String? + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + switch stateClass { + case is Fail.Type: + return true + case is Idle.Type: + return true + case is NoMore.Type: + return true + default: + return false + } + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + + guard let authenticationBox = viewModel.activeMastodonAuthenticationBox.value else { + stateMachine.enter(Fail.self) + return + } + if previousState is Reloading { + maxID = nil + } + + + Task { + do { + let response = try await viewModel.context.apiService.bookmarkedStatuses( + maxID: maxID, + authenticationBox: authenticationBox + ) + + var hasNewStatusesAppend = false + var statusIDs = viewModel.statusFetchedResultsController.statusIDs.value + for status in response.value { + guard !statusIDs.contains(status.id) else { continue } + statusIDs.append(status.id) + hasNewStatusesAppend = true + } + + self.maxID = response.link?.maxID + + let hasNextPage: Bool = { + guard let link = response.link else { return true } // assert has more when link invalid + return link.maxID != nil + }() + + if hasNewStatusesAppend && hasNextPage { + await enter(state: Idle.self) + } else { + await enter(state: NoMore.self) + } + viewModel.statusFetchedResultsController.statusIDs.value = statusIDs + } catch { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch user bookmarks fail: \(error.localizedDescription)") + await enter(state: Fail.self) + } + } // end Task + } // end func + } + + class NoMore: BookmarkViewModel.State { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + switch stateClass { + case is Reloading.Type: + return true + default: + return false + } + } + } +} diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift new file mode 100644 index 000000000..8dc8d734a --- /dev/null +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift @@ -0,0 +1,58 @@ +// +// BookmarkViewModel.swift +// Mastodon +// +// Created by ProtoLimit on 2022-07-19. +// + +import UIKit +import Combine +import CoreData +import CoreDataStack +import GameplayKit + +final class BookmarkViewModel { + + var disposeBag = Set() + + // input + let context: AppContext + let activeMastodonAuthenticationBox: CurrentValueSubject + let statusFetchedResultsController: StatusFetchedResultsController + let listBatchFetchViewModel = ListBatchFetchViewModel() + + // output + var diffableDataSource: UITableViewDiffableDataSource? + private(set) lazy var stateMachine: GKStateMachine = { + let stateMachine = GKStateMachine(states: [ + State.Initial(viewModel: self), + State.Reloading(viewModel: self), + State.Fail(viewModel: self), + State.Idle(viewModel: self), + State.Loading(viewModel: self), + State.NoMore(viewModel: self), + ]) + stateMachine.enter(State.Initial.self) + return stateMachine + }() + + init(context: AppContext) { + self.context = context + self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value) + self.statusFetchedResultsController = StatusFetchedResultsController( + managedObjectContext: context.managedObjectContext, + domain: nil, + additionalTweetPredicate: nil + ) + + context.authenticationService.activeMastodonAuthenticationBox + .assign(to: \.value, on: activeMastodonAuthenticationBox) + .store(in: &disposeBag) + + activeMastodonAuthenticationBox + .map { $0?.domain } + .assign(to: \.value, on: statusFetchedResultsController.domain) + .store(in: &disposeBag) + } + +} diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 96af86026..3e437bb7c 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -74,6 +74,17 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi barButtonItem.tintColor = .white return barButtonItem }() + + private(set) lazy var bookmarkBarButtonItem: UIBarButtonItem = { + let barButtonItem = UIBarButtonItem( + image: Asset.ObjectsAndTools.bookmark.image.withRenderingMode(.alwaysTemplate), + style: .plain, + target: self, + action: #selector(ProfileViewController.bookmarkBarButtonItemPressed(_:)) + ) + barButtonItem.tintColor = .white + 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(_:))) @@ -224,6 +235,7 @@ extension ProfileViewController { items.append(self.settingBarButtonItem) items.append(self.shareBarButtonItem) items.append(self.favoriteBarButtonItem) + items.append(self.bookmarkBarButtonItem) return } @@ -503,6 +515,12 @@ extension ProfileViewController { let favoriteViewModel = FavoriteViewModel(context: context) coordinator.present(scene: .favorite(viewModel: favoriteViewModel), from: self, transition: .show) } + + @objc private func bookmarkBarButtonItemPressed(_ sender: UIBarButtonItem) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + let bookmarkViewModel = BookmarkViewModel(context: context) + coordinator.present(scene: .bookmark(viewModel: bookmarkViewModel), from: self, transition: .show) + } @objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) diff --git a/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift b/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift index ab4fea1ab..1bdff4d80 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift @@ -395,6 +395,19 @@ extension StatusView { } .assign(to: \.isFavorite, on: viewModel) .store(in: &disposeBag) + + Publishers.CombineLatest( + viewModel.$userIdentifier, + status.publisher(for: \.bookmarkedBy) + ) + .map { userIdentifier, bookmarkedBy in + guard let userIdentifier = userIdentifier else { return false } + return bookmarkedBy.contains(where: { + $0.id == userIdentifier.userID && $0.domain == userIdentifier.domain + }) + } + .assign(to: \.isBookmark, on: viewModel) + .store(in: &disposeBag) } private func configureFilter(status: Status) { diff --git a/Mastodon/Service/APIService/APIService+Bookmark.swift b/Mastodon/Service/APIService/APIService+Bookmark.swift new file mode 100644 index 000000000..8100b3b58 --- /dev/null +++ b/Mastodon/Service/APIService/APIService+Bookmark.swift @@ -0,0 +1,142 @@ +// +// APIService+Bookmark.swift +// Mastodon +// +// Created by ProtoLimit on 2022/07/28. +// + +import Foundation +import Combine +import MastodonSDK +import CoreData +import CoreDataStack +import CommonOSLog + +extension APIService { + + private struct MastodonBookmarkContext { + let statusID: Status.ID + let isBookmarked: Bool + } + + func bookmark( + record: ManagedObjectRecord, + authenticationBox: MastodonAuthenticationBox + ) async throws -> Mastodon.Response.Content { + let logger = Logger(subsystem: "APIService", category: "Bookmark") + + let managedObjectContext = backgroundManagedObjectContext + + // update bookmark state and retrieve bookmark context + let bookmarkContext: MastodonBookmarkContext = try await managedObjectContext.performChanges { + guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext), + let _status = record.object(in: managedObjectContext) + else { + throw APIError.implicit(.badRequest) + } + let me = authentication.user + let status = _status.reblog ?? _status + let isBookmarked = status.bookmarkedBy.contains(me) + status.update(bookmarked: !isBookmarked, by: me) + let context = MastodonBookmarkContext( + statusID: status.id, + isBookmarked: isBookmarked + ) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update status bookmark: \(!isBookmarked)") + return context + } + + // request bookmark or undo bookmark + let result: Result, Error> + do { + let response = try await Mastodon.API.Bookmarks.bookmarks( + domain: authenticationBox.domain, + statusID: bookmarkContext.statusID, + session: session, + authorization: authenticationBox.userAuthorization, + bookmarkKind: bookmarkContext.isBookmarked ? .destroy : .create + ).singleOutput() + result = .success(response) + } catch { + result = .failure(error) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update bookmark failure: \(error.localizedDescription)") + } + + // update bookmark state + try await managedObjectContext.performChanges { + guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext), + let _status = record.object(in: managedObjectContext) + else { return } + let me = authentication.user + let status = _status.reblog ?? _status + + switch result { + case .success(let response): + _ = Persistence.Status.createOrMerge( + in: managedObjectContext, + context: Persistence.Status.PersistContext( + domain: authenticationBox.domain, + entity: response.value, + me: me, + statusCache: nil, + userCache: nil, + networkDate: response.networkDate + ) + ) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update status bookmark: \(response.value.bookmarked.debugDescription)") + case .failure: + // rollback + status.update(bookmarked: bookmarkContext.isBookmarked, by: me) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): rollback status bookmark") + } + } + + let response = try result.get() + return response + } + +} + +extension APIService { + func bookmarkedStatuses( + limit: Int = onceRequestStatusMaxCount, + maxID: String? = nil, + authenticationBox: MastodonAuthenticationBox + ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Status]> { + let query = Mastodon.API.Bookmarks.BookmarkStatusesQuery(limit: limit, minID: nil, maxID: maxID) + + let response = try await Mastodon.API.Bookmarks.bookmarkedStatus( + domain: authenticationBox.domain, + session: session, + authorization: authenticationBox.userAuthorization, + query: query + ).singleOutput() + + let managedObjectContext = self.backgroundManagedObjectContext + try await managedObjectContext.performChanges { + guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { + assertionFailure() + return + } + + for entity in response.value { + let result = Persistence.Status.createOrMerge( + in: managedObjectContext, + context: Persistence.Status.PersistContext( + domain: authenticationBox.domain, + entity: entity, + me: me, + statusCache: nil, + userCache: nil, + networkDate: response.networkDate + ) + ) + + result.status.update(bookmarked: true, by: me) + result.status.reblog?.update(bookmarked: true, by: me) + } // end for … in + } + + return response + } // end func +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/ObjectsAndTools/bookmark.fill.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/ObjectsAndTools/bookmark.fill.imageset/Contents.json new file mode 100644 index 000000000..33bc2f0de --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/ObjectsAndTools/bookmark.fill.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "bookmark-solid.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/ObjectsAndTools/bookmark.fill.imageset/bookmark-solid.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/ObjectsAndTools/bookmark.fill.imageset/bookmark-solid.pdf new file mode 100755 index 0000000000000000000000000000000000000000..89d499e8405eb1e9f4b44e5bc61ad6917dcbf40e GIT binary patch literal 1200 zcmY!laB<2%8dkI9gu?faZX;sOgD8P!q?9rn0 ze0TdC-3WD-U$J2ox3eoWnE{)(lV+xx!6nVhWDvEcby?|D#dy=FqVxp6(C z<}{;j%m4;?4;H{Crm%pqAR=H44Us~|7%5~7&4>yb-+sgVLk0q^<^Q|3n@VQQ=U*DA zbcD(KXp?~S%LG>KjZsG@=C=K-?#L8;z5DmQyUQIHIl4Lt&(PW!=RCJAox9YaxtFQo z)^6`3l01h5<1KIfdOc;q(X`2(&L5^{=QDTR+woxG(sfVSy9}3FZu5Ft!jUEyE5DTA zInpkx$@Jv?jptule|uIJ<~>d7?&OmCX4Y46n$MTd^%Oh0E;VV{Ea|Ai!&j|C^Ie|) z&*q=G*7MnMe#!Gm7rM`GDq&_QbiIxlkl^5eg`}k+EF=w$FoH57C^ZcnLa_X7fit{7 zDLFW^DpkP{62RdK(S`~}3P!PD9YOi|B?^WH@M!SNOUqZV1apx~f#igQAOGifHa0gl zb~g4lHckv=^)UF*98sg7#O`76w7DZ}f)JkqvxkA9(R+q2!};k63=F#$v8H1BvA86$ UsHCC@=zL3aU;(A->hHz{0DQ5&5C8xG literal 0 HcmV?d00001 diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/ObjectsAndTools/bookmark.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/ObjectsAndTools/bookmark.imageset/Contents.json new file mode 100644 index 000000000..81aaf7f9b --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/ObjectsAndTools/bookmark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "bookmark-regular.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/ObjectsAndTools/bookmark.imageset/bookmark-regular.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/ObjectsAndTools/bookmark.imageset/bookmark-regular.pdf new file mode 100755 index 0000000000000000000000000000000000000000..c9eebdfb982515c3cf967cc7233fa9f0d921e6b5 GIT binary patch literal 1226 zcmY!laBlN;d8$E3(}x0p*WA@}FB}RjuK4a> zV~|>s=;0xsd!PMdPq9$UqJP_VE`R*-OVppAr|K%LJ&&k9%KSVrDkMZ(>sgCdz=?S) zpKY1$xY%E{Yox}iwAdakx} zv60k`t9inF>Z=#tRkDcEwq2cCBvF5+Wk!g_?Oojllh#eUVa3k!D>KMdAi$+?*({GG za`lQ6O=|uotafV-t_+Z2c=Nya?%kuc3>RZUJh@WyQlS0s}7&4}2O2CwD$b zm>_!YOU#uF`yJ^?586M;eKm;`F1AegpMUMrwF zEY;lJ_a)BcWTlP;&)0g-gKFzF6T;1n>lrnt8Fga@Fvxq*05&v%2e1VZ0b^*06f(w0 zA!BGpRM7bL@8mmVAmCD6f3!q}_1i5CnLrn2-bIcbd>U(xaEWR3PT2R(Ai5 z9cL1^$PpFCz=Nx}9({Phd&Fp5+DY9wg=epYp^&8Dd6 z_X<1beAL|J-BYX4>XG@nw0>jhgLB&}|E>Gz=KlSV*Z3j;i|EU#eqcF8Q^5 z&FgDcQYo*4CI*=VbI)4wv*lPwPsgYC+4(c4_WgQxt3}=L#KE+PDyIKAu9q-_5gZt> zaI~<5B``xHjDU;?N=*ZY4=kfw;0!HLIu6dPN>wm~1aG)Pw4s8Lf>A73M^Ju#iGra4 zJQ6(f(()B7!Cd6xAvq!8$N%}Ajm?dXosGSXjT0}*c^G_Xj+mveNY=xkw6TL#K~=_q tZ9+m)%03Q}TSTmYqP#Cre$ literal 0 HcmV?d00001 diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift index adcee2944..edbf78a0b 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift @@ -117,6 +117,8 @@ public enum Asset { public static let bellBadge = ImageAsset(name: "ObjectsAndTools/bell.badge") public static let bellFill = ImageAsset(name: "ObjectsAndTools/bell.fill") public static let bell = ImageAsset(name: "ObjectsAndTools/bell") + public static let bookmarkFill = ImageAsset(name: "ObjectsAndTools/bookmark.fill") + public static let bookmark = ImageAsset(name: "ObjectsAndTools/bookmark") public static let gear = ImageAsset(name: "ObjectsAndTools/gear") public static let houseFill = ImageAsset(name: "ObjectsAndTools/house.fill") public static let house = ImageAsset(name: "ObjectsAndTools/house") diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 70a807fcb..c64a50fa2 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -287,6 +287,8 @@ public enum L10n { return L10n.tr("Localizable", "Common.Controls.Status.UserRepliedTo", String(describing: p1)) } public enum Actions { + /// Bookmark + public static let bookmark = L10n.tr("Localizable", "Common.Controls.Status.Actions.Bookmark") /// Favorite public static let favorite = L10n.tr("Localizable", "Common.Controls.Status.Actions.Favorite") /// Hide @@ -305,6 +307,8 @@ public enum L10n { public static let showVideoPlayer = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShowVideoPlayer") /// Tap then hold to show menu public static let tapThenHoldToShowMenu = L10n.tr("Localizable", "Common.Controls.Status.Actions.TapThenHoldToShowMenu") + /// Unbookmark + public static let unbookmark = L10n.tr("Localizable", "Common.Controls.Status.Actions.Unbookmark") /// Unfavorite public static let unfavorite = L10n.tr("Localizable", "Common.Controls.Status.Actions.Unfavorite") /// Undo reblog @@ -403,6 +407,10 @@ public enum L10n { return L10n.tr("Localizable", "Scene.AccountList.TabBarHint", String(describing: p1)) } } + public enum Bookmark { + /// Your Bookmarks + public static let title = L10n.tr("Localizable", "Scene.Bookmark.Title") + } public enum Compose { /// Publish public static let composeAction = L10n.tr("Localizable", "Scene.Compose.ComposeAction") diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings index 12df948fc..1c40bf855 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings @@ -104,6 +104,8 @@ Please check your internet connection."; "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.Actions.Bookmark" = "Bookmark"; +"Common.Controls.Status.Actions.Unbookmark" = "Unbookmark"; "Common.Controls.Status.ContentWarning" = "Content Warning"; "Common.Controls.Status.MediaContentWarning" = "Tap anywhere to reveal"; "Common.Controls.Status.Poll.Closed" = "Closed"; @@ -149,6 +151,7 @@ Your profile looks like this to them."; "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" = "Your Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "Add Attachment"; "Scene.Compose.Accessibility.AppendPoll" = "Add Poll"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Custom Emoji Picker"; @@ -437,4 +440,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"; diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Bookmarks.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Bookmarks.swift new file mode 100644 index 000000000..a50a8e337 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Bookmarks.swift @@ -0,0 +1,136 @@ +// +// Mastodon+API+Bookmarks.swift +// +// +// Created by ProtoLimit on 2022/07/28. +// + +import Combine +import Foundation + +extension Mastodon.API.Bookmarks { + + static func bookmarksStatusesEndpointURL(domain: String) -> URL { + return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("bookmarks") + } + + /// Bookmarked statuses + /// + /// Using this endpoint to view the bookmarked list for user + /// + /// - Since: 3.1.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2022/7/28 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/accounts/bookmarks/) + /// - Parameters: + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - session: `URLSession` + /// - authorization: User token + /// - Returns: `AnyPublisher` contains `Server` nested in the response + public static func bookmarkedStatus( + domain: String, + session: URLSession, + authorization: Mastodon.API.OAuth.Authorization, + query: Mastodon.API.Bookmarks.BookmarkStatusesQuery + ) -> AnyPublisher, Error> { + let url = bookmarksStatusesEndpointURL(domain: domain) + let request = Mastodon.API.get(url: url, query: query, authorization: authorization) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: [Mastodon.Entity.Status].self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } + + public struct BookmarkStatusesQuery: GetQuery, PagedQueryType { + + public var limit: Int? + public var minID: String? + public var maxID: String? + public var sinceID: Mastodon.Entity.Status.ID? + + public init(limit: Int? = nil, minID: String? = nil, maxID: String? = nil, sinceID: String? = nil) { + self.limit = limit + self.minID = minID + self.maxID = maxID + self.sinceID = sinceID + } + + var queryItems: [URLQueryItem]? { + var items: [URLQueryItem] = [] + if let limit = self.limit { + items.append(URLQueryItem(name: "limit", value: String(limit))) + } + if let minID = self.minID { + items.append(URLQueryItem(name: "min_id", value: minID)) + } + if let maxID = self.maxID { + items.append(URLQueryItem(name: "max_id", value: maxID)) + } + if let sinceID = self.sinceID { + items.append(URLQueryItem(name: "since_id", value: sinceID)) + } + guard !items.isEmpty else { return nil } + return items + } + } + +} + +extension Mastodon.API.Bookmarks { + + static func bookmarkActionEndpointURL(domain: String, statusID: String, bookmarkKind: BookmarkKind) -> URL { + var actionString: String + switch bookmarkKind { + case .create: + actionString = "/bookmark" + case .destroy: + actionString = "/unbookmark" + } + let pathComponent = "statuses/" + statusID + actionString + return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent) + } + + /// Bookmark / Undo Bookmark + /// + /// Add a status to your bookmarks list / Remove a status from your bookmarks list + /// + /// - Since: 3.1.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2022/7/28 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/statuses/) + /// - Parameters: + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - statusID: Mastodon status id + /// - session: `URLSession` + /// - authorization: User token + /// - Returns: `AnyPublisher` contains `Server` nested in the response + public static func bookmarks( + domain: String, + statusID: String, + session: URLSession, + authorization: Mastodon.API.OAuth.Authorization, + bookmarkKind: BookmarkKind + ) -> AnyPublisher, Error> { + let url: URL = bookmarkActionEndpointURL(domain: domain, statusID: statusID, bookmarkKind: bookmarkKind) + var request = Mastodon.API.post(url: url, query: nil, authorization: authorization) + request.httpMethod = "POST" + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } + + public enum BookmarkKind { + case create + case destroy + } + +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index 66c822b32..43d5873d0 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -102,6 +102,7 @@ extension Mastodon.API { public enum V2 { } public enum Account { } public enum App { } + public enum Bookmarks { } public enum CustomEmojis { } public enum Favorites { } public enum Instance { } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index f3d9f6f80..bd80afe86 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -84,6 +84,7 @@ extension StatusView { @Published public var isReblog: Bool = false @Published public var isReblogEnabled: Bool = true @Published public var isFavorite: Bool = false + @Published public var isBookmark: Bool = false @Published public var replyCount: Int = 0 @Published public var reblogCount: Int = 0 @@ -510,6 +511,13 @@ extension StatusView.ViewModel { ) } .store(in: &disposeBag) + $isBookmark + .sink { isHighlighted in + statusView.actionToolbarContainer.configureBookmark( + isHighlighted: isHighlighted + ) + } + .store(in: &disposeBag) } private func bindMetric(statusView: StatusView) { diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift b/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift index 4a5c44850..ccfc8020e 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift @@ -22,11 +22,14 @@ public final class ActionToolbarContainer: UIView { static let reblogImage = Asset.Arrow.repeat.image.withRenderingMode(.alwaysTemplate) static let starImage = Asset.ObjectsAndTools.star.image.withRenderingMode(.alwaysTemplate) static let starFillImage = Asset.ObjectsAndTools.starFill.image.withRenderingMode(.alwaysTemplate) + static let bookmarkImage = Asset.ObjectsAndTools.bookmark.image.withRenderingMode(.alwaysTemplate) + static let bookmarkFillImage = Asset.ObjectsAndTools.bookmarkFill.image.withRenderingMode(.alwaysTemplate) static let shareImage = Asset.Communication.share.image.withRenderingMode(.alwaysTemplate) public let replyButton = HighlightDimmableButton() public let reblogButton = HighlightDimmableButton() public let favoriteButton = HighlightDimmableButton() + public let bookmarkButton = HighlightDimmableButton() public let shareButton = HighlightDimmableButton() public weak var delegate: ActionToolbarContainerDelegate? @@ -61,6 +64,7 @@ extension ActionToolbarContainer { replyButton.addTarget(self, action: #selector(ActionToolbarContainer.buttonDidPressed(_:)), for: .touchUpInside) reblogButton.addTarget(self, action: #selector(ActionToolbarContainer.buttonDidPressed(_:)), for: .touchUpInside) favoriteButton.addTarget(self, action: #selector(ActionToolbarContainer.buttonDidPressed(_:)), for: .touchUpInside) + bookmarkButton.addTarget(self, action: #selector(ActionToolbarContainer.buttonDidPressed(_:)), for: .touchUpInside) shareButton.addTarget(self, action: #selector(ActionToolbarContainer.buttonDidPressed(_:)), for: .touchUpInside) } @@ -75,7 +79,7 @@ extension ActionToolbarContainer { subview.removeFromSuperview() } - let buttons = [replyButton, reblogButton, favoriteButton, shareButton] + let buttons = [replyButton, reblogButton, favoriteButton, bookmarkButton, shareButton] buttons.forEach { button in button.tintColor = Asset.Colors.Button.actionToolbar.color button.titleLabel?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular) @@ -90,6 +94,7 @@ extension ActionToolbarContainer { replyButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.reply reblogButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.reblog // needs update to follow state favoriteButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.favorite // needs update to follow state + bookmarkButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.bookmark // needs update to follow state shareButton.accessibilityLabel = L10n.Common.Controls.Actions.share switch style { @@ -100,6 +105,7 @@ extension ActionToolbarContainer { replyButton.setImage(ActionToolbarContainer.replyImage, for: .normal) reblogButton.setImage(ActionToolbarContainer.reblogImage, for: .normal) favoriteButton.setImage(ActionToolbarContainer.starImage, for: .normal) + bookmarkButton.setImage(ActionToolbarContainer.bookmarkImage, for: .normal) shareButton.setImage(ActionToolbarContainer.shareImage, for: .normal) container.axis = .horizontal @@ -108,18 +114,22 @@ extension ActionToolbarContainer { replyButton.translatesAutoresizingMaskIntoConstraints = false reblogButton.translatesAutoresizingMaskIntoConstraints = false favoriteButton.translatesAutoresizingMaskIntoConstraints = false + bookmarkButton.translatesAutoresizingMaskIntoConstraints = false shareButton.translatesAutoresizingMaskIntoConstraints = false container.addArrangedSubview(replyButton) container.addArrangedSubview(reblogButton) container.addArrangedSubview(favoriteButton) + container.addArrangedSubview(bookmarkButton) container.addArrangedSubview(shareButton) NSLayoutConstraint.activate([ replyButton.heightAnchor.constraint(equalToConstant: 36).priority(.defaultHigh), replyButton.heightAnchor.constraint(equalTo: reblogButton.heightAnchor).priority(.defaultHigh), replyButton.heightAnchor.constraint(equalTo: favoriteButton.heightAnchor).priority(.defaultHigh), + replyButton.heightAnchor.constraint(equalTo: bookmarkButton.heightAnchor).priority(.defaultHigh), replyButton.heightAnchor.constraint(equalTo: shareButton.heightAnchor).priority(.defaultHigh), replyButton.widthAnchor.constraint(equalTo: reblogButton.widthAnchor).priority(.defaultHigh), replyButton.widthAnchor.constraint(equalTo: favoriteButton.widthAnchor).priority(.defaultHigh), + replyButton.widthAnchor.constraint(equalTo: bookmarkButton.widthAnchor).priority(.defaultHigh), ]) shareButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) shareButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) @@ -131,6 +141,7 @@ extension ActionToolbarContainer { replyButton.setImage(ActionToolbarContainer.replyImage, for: .normal) reblogButton.setImage(ActionToolbarContainer.reblogImage, for: .normal) favoriteButton.setImage(ActionToolbarContainer.starImage, for: .normal) + bookmarkButton.setImage(ActionToolbarContainer.bookmarkImage, for: .normal) container.axis = .horizontal container.spacing = 8 @@ -139,6 +150,7 @@ extension ActionToolbarContainer { container.addArrangedSubview(replyButton) container.addArrangedSubview(reblogButton) container.addArrangedSubview(favoriteButton) + container.addArrangedSubview(bookmarkButton) } } @@ -155,6 +167,7 @@ extension ActionToolbarContainer { case reply case reblog case like + case bookmark case share } @@ -184,6 +197,11 @@ extension ActionToolbarContainer { favoriteButton.setTitleColor(tintColor, for: .highlighted) } + private func isBookmarkButtonHighlightStateDidChange(to isHighlight: Bool) { + let tintColor = isHighlight ? Asset.Colors.brand.color : Asset.Colors.Button.actionToolbar.color + bookmarkButton.tintColor = tintColor + } + } extension ActionToolbarContainer { @@ -196,6 +214,7 @@ extension ActionToolbarContainer { case replyButton: _action = .reply case reblogButton: _action = .reblog case favoriteButton: _action = .like + case bookmarkButton: _action = .bookmark case shareButton: _action = .share default: _action = nil } @@ -256,6 +275,20 @@ extension ActionToolbarContainer { favoriteButton.accessibilityLabel = L10n.Plural.Count.favorite(count) } + public func configureBookmark(isHighlighted: Bool) { + let image = isHighlighted ? ActionToolbarContainer.bookmarkFillImage : ActionToolbarContainer.bookmarkImage + bookmarkButton.setImage(image, for: .normal) + let tintColor = isHighlighted ? Asset.Colors.brand.color : Asset.Colors.Button.actionToolbar.color + bookmarkButton.tintColor = tintColor + + if isHighlighted { + bookmarkButton.accessibilityTraits.insert(.selected) + } else { + bookmarkButton.accessibilityTraits.remove(.selected) + } + bookmarkButton.accessibilityLabel = isHighlighted ? L10n.Common.Controls.Status.Actions.unbookmark : L10n.Common.Controls.Status.Actions.bookmark + } + } extension ActionToolbarContainer { @@ -267,7 +300,7 @@ extension ActionToolbarContainer { extension ActionToolbarContainer { public override var accessibilityElements: [Any]? { - get { [replyButton, reblogButton, favoriteButton, shareButton] } + get { [replyButton, reblogButton, favoriteButton, bookmarkButton, shareButton] } set { } } } From 64f3d2fe3a291c20e82be8dc67f24228959a91dd Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 30 Sep 2022 19:28:09 +0800 Subject: [PATCH 031/658] chore: [WIP] move core logic into package --- .arkana.yml | 16 + AppShared/AppShared.h | 18 - AppShared/Info.plist | 22 - Gemfile | 3 +- Gemfile.lock | 20 +- Mastodon.xcodeproj/project.pbxproj | 1207 +---------------- .../xcschemes/xcschememanagement.plist | 36 +- .../xcshareddata/swiftpm/Package.resolved | 48 +- Mastodon/Coordinator/SceneCoordinator.swift | 3 +- .../Compose/AutoCompleteSection.swift | 1 + .../CoreDataStack/MastodonUser.swift | 28 - .../Helper/MastodonAuthenticationBox.swift | 19 - .../Persistence/Extension/MastodonEmoji.swift | 24 - .../Preference/HomeTimelinePreference.swift | 20 - .../Preference/NotificationPreference.swift | 21 - Mastodon/Preference/ThemePreference.swift | 7 - .../Scene/Account/AccountListViewModel.swift | 2 + .../Scene/Account/AccountViewController.swift | 1 + .../Cell/AddAccountTableViewCell.swift | 2 + .../AutoCompleteViewModel+State.swift | 1 + .../AutoComplete/AutoCompleteViewModel.swift | 11 +- ...seStatusAttachmentCollectionViewCell.swift | 53 +- .../Scene/Compose/ComposeViewController.swift | 742 +++++----- ...ComposeStatusAttachmentTableViewCell.swift | 154 ++- .../View/AttachmentContainerView.swift | 256 ++-- .../DiscoveryCommunityViewModel.swift | 1 + .../NotificationTimelineViewModel.swift | 27 +- .../MastodonAuthenticationController.swift | 5 +- .../Scene/Root/RootSplitViewController.swift | 1 + Mastodon/Service/StatusPublishService.swift | 79 -- Mastodon/State/DocumentStore.swift | 15 - Mastodon/State/ViewStateStore.swift | 14 - Mastodon/Supporting Files/AppDelegate.swift | 4 +- .../Handler/SendPostIntentHandler.swift | 9 +- MastodonIntent/Model/Account+Fetch.swift | 1 + MastodonIntent/Service/APIService.swift | 33 - MastodonSDK/Package.resolved | 241 ++++ MastodonSDK/Package.swift | 66 +- .../Sources/CoreDataStack/CoreDataStack.swift | 9 + .../Preference/Preference+App.swift | 2 +- .../Preference/Preference+Notification.swift | 13 + .../Preference/Preference+StoreReview.swift | 4 +- .../Preference/Preference+Wizard.swift | 2 +- .../Sources/MastodonCore}/AppContext.swift | 46 +- .../Sources/MastodonCore}/AppSecret.swift | 8 +- .../MastodonAuthenticationBox.swift | 32 + .../Sources/MastodonCore/DocumentStore.swift | 15 + .../Extension/CoreDataStack/Instance.swift | 0 .../CoreDataStack/MastodonEmoji.swift | 12 + .../CoreDataStack}/MastodonField.swift | 0 .../CoreDataStack}/MastodonMention.swift | 0 .../CoreDataStack/MastodonStatus.swift | 0 .../MastodonUser+Property.swift | 0 .../CoreDataStack/MastodonUser.swift | 24 +- .../Notification+Property.swift | 0 .../CoreDataStack}/Poll+Property.swift | 0 .../CoreDataStack}/PollOption+Property.swift | 0 .../Extension/CoreDataStack/Setting.swift | 0 .../CoreDataStack}/Status+Property.swift | 0 .../Extension/CoreDataStack/Status.swift | 0 .../CoreDataStack/Subscription.swift | 0 .../CoreDataStack/SubscriptionAlerts.swift | 0 .../CoreDataStack}/Tag+Property.swift | 0 .../MastodonSDK/Mastodon+Entity+Account.swift | 0 .../MastodonSDK/Mastodon+Entity+Link.swift | 0 .../MastodonSDK/Mastodon+Entity+Tag.swift | 0 .../FeedFetchedResultsController.swift | 0 ...SearchHistoryFetchedResultController.swift | 0 .../SettingFetchedResultController.swift | 8 +- .../StatusFetchedResultsController.swift | 1 - .../UserFetchedResultsController.swift | 1 - .../Model/UserIdentifier.swift | 0 .../Persistence+MastodonUser.swift | 8 +- .../Persistence+Notification.swift | 4 +- .../Persistence/Persistence+Poll.swift | 3 +- .../Persistence/Persistence+PollOption.swift | 2 +- .../Persistence+SearchHistory.swift | 3 +- .../Persistence/Persistence+Status.swift | 4 +- .../Persistence/Persistence+Tag.swift | 2 +- .../Persistence/Persistence.swift | 0 .../Protocol/MastodonEmojiContainer.swift | 0 .../Protocol/MastodonFieldContainer.swift | 0 .../Protocol/MastodonMentionContainer.swift | 0 .../Service/API}/APIService+APIError.swift | 8 +- .../Service/API}/APIService+Account.swift | 7 +- .../Service/API}/APIService+App.swift | 2 +- .../API}/APIService+Authentication.swift | 0 .../Service/API}/APIService+Block.swift | 0 .../Service/API}/APIService+Bookmark.swift | 0 .../Service/API}/APIService+CustomEmoji.swift | 1 - .../Service/API}/APIService+DomainBlock.swift | 1 - .../Service/API}/APIService+Favorite.swift | 0 .../Service/API}/APIService+Filter.swift | 0 .../Service/API}/APIService+Follow.swift | 0 .../API}/APIService+FollowRequest.swift | 0 .../Service/API}/APIService+Follower.swift | 0 .../Service/API}/APIService+Following.swift | 0 .../API}/APIService+HashtagTimeline.swift | 1 - .../API}/APIService+HomeTimeline.swift | 1 - .../Service/API}/APIService+Instance.swift | 1 - .../Service/API}/APIService+Media.swift | 6 +- .../Service/API}/APIService+Mute.swift | 0 .../API}/APIService+Notification.swift | 28 +- .../Service/API}/APIService+Onboarding.swift | 0 .../Service/API}/APIService+Poll.swift | 1 - .../API}/APIService+PublicTimeline.swift | 0 .../Service/API}/APIService+Reblog.swift | 0 .../Service/API}/APIService+Recommend.swift | 0 .../API}/APIService+Relationship.swift | 0 .../Service/API}/APIService+Report.swift | 0 .../Service/API}/APIService+Search.swift | 3 +- .../API}/APIService+Status+Publish.swift | 2 +- .../Service/API}/APIService+Status.swift | 1 - .../API}/APIService+Subscriptions.swift | 0 .../Service/API}/APIService+Thread.swift | 0 .../Service/API}/APIService+Trend.swift | 0 .../API}/APIService+UserTimeline.swift | 0 .../Service/API}/APIService+WebFinger.swift | 1 - .../Service/API}/APIService.swift | 15 +- .../APIService+CoreData+Instance.swift | 2 +- ...vice+CoreData+MastodonAuthentication.swift | 0 .../APIService+CoreData+Setting.swift | 0 .../APIService+CoreData+Subscriptions.swift | 0 .../Service/AuthenticationService.swift | 16 +- .../Service/BlockDomainService.swift | 2 +- .../Service/BlurhashImageCacheService.swift | 0 ...rvice+CustomEmojiViewModel+LoadState.swift | 0 .../EmojiService+CustomEmojiViewModel.swift | 11 +- .../Service/Emoji}/EmojiService.swift | 2 +- .../MastodonCore/Service/Emoji}/Trie.swift | 20 +- .../Service/InstanceService.swift | 4 +- ...astodonAttachmentService+UploadState.swift | 0 .../MastodonAttachmentService.swift | 1 - .../MastodonPushNotification.swift | 19 +- .../Notification}/NotificationService.swift | 23 +- .../Service/PhotoLibraryService.swift | 11 +- .../PlaceholderImageCacheService.swift | 2 +- .../MastodonCore}/Service/PlaybackState.swift | 0 .../Service/SettingService.swift | 12 +- .../Service/StatusFilterService.swift | 2 +- .../Service/StatusPublishService.swift | 79 ++ .../Service/Theme}/MastodonTheme.swift | 0 .../Service/Theme}/SystemTheme.swift | 0 .../Service/Theme}/Theme.swift | 0 .../Service/Theme}/ThemeService.swift | 39 +- .../Vendor/BlurHashDecode.swift | 0 .../Vendor/BlurHashEncode.swift | 0 .../Vendor/ItemProviderLoader.swift | 2 +- .../Sources/MastodonExtension}/Array.swift | 8 +- .../NSManagedObjectContext.swift | 2 +- .../Sources/MastodonUI/Extension/UIView.swift | 1 + .../ComposeContent/ComposeContentView.swift | 18 + .../ComposeContentViewController.swift | 24 + .../ComposeContentViewModel.swift | 20 + .../Service/ThemeService/ThemeService.swift | 36 - .../View/Container/AttachmentView.swift | 29 + ...FollowersDashboardView+Configuration.swift | 1 + .../Content/MediaView+Configuration.swift | 1 + .../Content/NotificationView+ViewModel.swift | 1 + .../Content/PollOptionView+ViewModel.swift | 1 + .../Content/ProfileCardView+ViewModel.swift | 1 + .../View/Content/StatusView+ViewModel.swift | 1 + .../API/MastodonSDK+API+TimelineTests.swift | 31 +- NotificationService/NotificationService.swift | 2 +- Podfile | 8 - Podfile.lock | 9 +- .../Scene/ShareViewController.swift | 1 + .../Scene/ShareViewModel.swift | 20 +- .../Scene/View/ComposeToolbarView.swift | 1 + ...tatusAttachmentViewModel+UploadState.swift | 3 +- .../View/StatusAttachmentViewModel.swift | 8 +- .../Scene/View/StatusAuthorView.swift | 1 - ShareActionExtension/Service/APIService.swift | 32 - 173 files changed, 1529 insertions(+), 2471 deletions(-) create mode 100644 .arkana.yml delete mode 100644 AppShared/AppShared.h delete mode 100644 AppShared/Info.plist delete mode 100644 Mastodon/Extension/CoreDataStack/MastodonUser.swift delete mode 100644 Mastodon/Helper/MastodonAuthenticationBox.swift delete mode 100644 Mastodon/Persistence/Extension/MastodonEmoji.swift delete mode 100644 Mastodon/Preference/HomeTimelinePreference.swift delete mode 100644 Mastodon/Preference/NotificationPreference.swift delete mode 100644 Mastodon/Preference/ThemePreference.swift delete mode 100644 Mastodon/Service/StatusPublishService.swift delete mode 100644 Mastodon/State/DocumentStore.swift delete mode 100644 Mastodon/State/ViewStateStore.swift delete mode 100644 MastodonIntent/Service/APIService.swift create mode 100644 MastodonSDK/Package.resolved rename Mastodon/Preference/AppPreference.swift => MastodonSDK/Sources/MastodonCommon/Preference/Preference+App.swift (81%) rename AppShared/UserDefaults+Notification.swift => MastodonSDK/Sources/MastodonCommon/Preference/Preference+Notification.swift (82%) rename Mastodon/Preference/StoreReviewPreference.swift => MastodonSDK/Sources/MastodonCommon/Preference/Preference+StoreReview.swift (75%) rename Mastodon/Preference/WizardPreference.swift => MastodonSDK/Sources/MastodonCommon/Preference/Preference+Wizard.swift (76%) rename {Mastodon/State => MastodonSDK/Sources/MastodonCore}/AppContext.swift (86%) rename {AppShared => MastodonSDK/Sources/MastodonCore}/AppSecret.swift (93%) create mode 100644 MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift create mode 100644 MastodonSDK/Sources/MastodonCore/DocumentStore.swift rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Extension/CoreDataStack/Instance.swift (100%) rename MastodonSDK/Sources/{MastodonUI => MastodonCore}/Extension/CoreDataStack/MastodonEmoji.swift (69%) rename {Mastodon/Persistence/Extension => MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack}/MastodonField.swift (100%) rename {Mastodon/Persistence/Extension => MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack}/MastodonMention.swift (100%) rename MastodonSDK/Sources/{MastodonUI => MastodonCore}/Extension/CoreDataStack/MastodonStatus.swift (100%) rename {Mastodon/Persistence/Extension => MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack}/MastodonUser+Property.swift (100%) rename MastodonSDK/Sources/{MastodonUI => MastodonCore}/Extension/CoreDataStack/MastodonUser.swift (73%) rename {Mastodon/Persistence/Extension => MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack}/Notification+Property.swift (100%) rename {Mastodon/Persistence/Extension => MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack}/Poll+Property.swift (100%) rename {Mastodon/Persistence/Extension => MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack}/PollOption+Property.swift (100%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Extension/CoreDataStack/Setting.swift (100%) rename {Mastodon/Persistence/Extension => MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack}/Status+Property.swift (100%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Extension/CoreDataStack/Status.swift (100%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Extension/CoreDataStack/Subscription.swift (100%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Extension/CoreDataStack/SubscriptionAlerts.swift (100%) rename {Mastodon/Persistence/Extension => MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack}/Tag+Property.swift (100%) rename MastodonSDK/Sources/{MastodonUI => MastodonCore}/Extension/MastodonSDK/Mastodon+Entity+Account.swift (100%) rename MastodonSDK/Sources/{MastodonUI => MastodonCore}/Extension/MastodonSDK/Mastodon+Entity+Link.swift (100%) rename MastodonSDK/Sources/{MastodonUI => MastodonCore}/Extension/MastodonSDK/Mastodon+Entity+Tag.swift (100%) rename {Mastodon/Diffiable => MastodonSDK/Sources/MastodonCore}/FetchedResultsController/FeedFetchedResultsController.swift (100%) rename {Mastodon/Diffiable => MastodonSDK/Sources/MastodonCore}/FetchedResultsController/SearchHistoryFetchedResultController.swift (100%) rename {Mastodon/Diffiable => MastodonSDK/Sources/MastodonCore}/FetchedResultsController/SettingFetchedResultController.swift (80%) rename {Mastodon/Diffiable => MastodonSDK/Sources/MastodonCore}/FetchedResultsController/StatusFetchedResultsController.swift (99%) rename {Mastodon/Diffiable => MastodonSDK/Sources/MastodonCore}/FetchedResultsController/UserFetchedResultsController.swift (99%) rename MastodonSDK/Sources/{MastodonUI => MastodonCore}/Model/UserIdentifier.swift (100%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Persistence/Persistence+MastodonUser.swift (96%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Persistence/Persistence+Notification.swift (98%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Persistence/Persistence+Poll.swift (98%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Persistence/Persistence+PollOption.swift (96%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Persistence/Persistence+SearchHistory.swift (97%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Persistence/Persistence+Status.swift (98%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Persistence/Persistence+Tag.swift (97%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Persistence/Persistence.swift (100%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Persistence/Protocol/MastodonEmojiContainer.swift (100%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Persistence/Protocol/MastodonFieldContainer.swift (100%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Persistence/Protocol/MastodonMentionContainer.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+APIError.swift (95%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Account.swift (93%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+App.swift (92%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Authentication.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Block.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Bookmark.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+CustomEmoji.swift (95%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+DomainBlock.swift (99%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Favorite.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Filter.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Follow.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+FollowRequest.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Follower.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Following.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+HashtagTimeline.swift (98%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+HomeTimeline.swift (99%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Instance.swift (95%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Media.swift (97%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Mute.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Notification.swift (86%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Onboarding.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Poll.swift (99%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+PublicTimeline.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Reblog.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Recommend.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Relationship.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Report.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Search.swift (98%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Status+Publish.swift (98%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Status.swift (99%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Subscriptions.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Thread.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+Trend.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+UserTimeline.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService+WebFinger.swift (97%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/APIService.swift (71%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/CoreData/APIService+CoreData+Instance.swift (99%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/CoreData/APIService+CoreData+MastodonAuthentication.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/CoreData/APIService+CoreData+Setting.swift (100%) rename {Mastodon/Service/APIService => MastodonSDK/Sources/MastodonCore/Service/API}/CoreData/APIService+CoreData+Subscriptions.swift (100%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Service/AuthenticationService.swift (89%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Service/BlockDomainService.swift (99%) rename MastodonSDK/Sources/{MastodonUI => MastodonCore}/Service/BlurhashImageCacheService.swift (100%) rename {Mastodon/Service/EmojiService => MastodonSDK/Sources/MastodonCore/Service/Emoji}/EmojiService+CustomEmojiViewModel+LoadState.swift (100%) rename {Mastodon/Service/EmojiService => MastodonSDK/Sources/MastodonCore/Service/Emoji}/EmojiService+CustomEmojiViewModel.swift (86%) rename {Mastodon/Service/EmojiService => MastodonSDK/Sources/MastodonCore/Service/Emoji}/EmojiService.swift (97%) rename {Mastodon/Service/EmojiService => MastodonSDK/Sources/MastodonCore/Service/Emoji}/Trie.swift (88%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Service/InstanceService.swift (97%) rename {Mastodon/Service/MastodonAttachmentService => MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment}/MastodonAttachmentService+UploadState.swift (100%) rename {Mastodon/Service/MastodonAttachmentService => MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment}/MastodonAttachmentService.swift (99%) rename {NotificationService => MastodonSDK/Sources/MastodonCore/Service/Notification}/MastodonPushNotification.swift (73%) rename {Mastodon/Service => MastodonSDK/Sources/MastodonCore/Service/Notification}/NotificationService.swift (91%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Service/PhotoLibraryService.swift (94%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Service/PlaceholderImageCacheService.swift (97%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Service/PlaybackState.swift (100%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Service/SettingService.swift (91%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Service/StatusFilterService.swift (98%) create mode 100644 MastodonSDK/Sources/MastodonCore/Service/StatusPublishService.swift rename MastodonSDK/Sources/{MastodonUI/Service/ThemeService => MastodonCore/Service/Theme}/MastodonTheme.swift (100%) rename MastodonSDK/Sources/{MastodonUI/Service/ThemeService => MastodonCore/Service/Theme}/SystemTheme.swift (100%) rename MastodonSDK/Sources/{MastodonUI/Service/ThemeService => MastodonCore/Service/Theme}/Theme.swift (100%) rename {Mastodon/Extension/MastodonUI => MastodonSDK/Sources/MastodonCore/Service/Theme}/ThemeService.swift (73%) rename MastodonSDK/Sources/{MastodonUI => MastodonCore}/Vendor/BlurHashDecode.swift (100%) rename MastodonSDK/Sources/{MastodonUI => MastodonCore}/Vendor/BlurHashEncode.swift (100%) rename MastodonSDK/Sources/{MastodonUI => MastodonCore}/Vendor/ItemProviderLoader.swift (99%) rename {Mastodon/Extension => MastodonSDK/Sources/MastodonExtension}/Array.swift (93%) rename {Mastodon/Extension => MastodonSDK/Sources/MastodonExtension}/NSManagedObjectContext.swift (77%) create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentView.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift delete mode 100644 MastodonSDK/Sources/MastodonUI/Service/ThemeService/ThemeService.swift create mode 100644 MastodonSDK/Sources/MastodonUI/View/Container/AttachmentView.swift delete mode 100644 ShareActionExtension/Service/APIService.swift diff --git a/.arkana.yml b/.arkana.yml new file mode 100644 index 000000000..2fd8f1ab4 --- /dev/null +++ b/.arkana.yml @@ -0,0 +1,16 @@ +import_name: 'ArkanaKeys' +namespace: 'Keys' +result_path: 'Dependencies' +flavors: + - AppStore +swift_declaration_strategy: let +should_generate_unit_tests: true +package_manager: spm +environments: + - Debug + - Release +global_secrets: + # nothing +environment_secrets: + # Will lookup for Debug and Release env vars (assuming no flavor was declared) + - NotificationEndpoint diff --git a/AppShared/AppShared.h b/AppShared/AppShared.h deleted file mode 100644 index 3258d4fcb..000000000 --- a/AppShared/AppShared.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// AppShared.h -// AppShared -// -// Created by MainasuK Cirno on 2021-4-27. -// - -#import - -//! Project version number for AppShared. -FOUNDATION_EXPORT double AppSharedVersionNumber; - -//! Project version string for AppShared. -FOUNDATION_EXPORT const unsigned char AppSharedVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/AppShared/Info.plist b/AppShared/Info.plist deleted file mode 100644 index 21baf4a3e..000000000 --- a/AppShared/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.4.5 - CFBundleVersion - 144 - - diff --git a/Gemfile b/Gemfile index 48aae3d82..510d3ccff 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,5 @@ source "https://rubygems.org" +gem 'arkana' gem "cocoapods" gem "cocoapods-clean" -gem "cocoapods-keys" - diff --git a/Gemfile.lock b/Gemfile.lock index b27a44a97..4f4903ed5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,9 +3,6 @@ GEM specs: CFPropertyList (3.0.5) rexml - RubyInline (3.12.5) - ZenTest (~> 4.3) - ZenTest (4.12.1) activesupport (6.1.5.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) @@ -17,6 +14,10 @@ GEM algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) + arkana (1.2.0) + colorize (~> 0.8) + dotenv (~> 2.7) + yaml (~> 0.2) atomos (0.1.3) claide (1.1.0) cocoapods (1.11.3) @@ -50,9 +51,6 @@ GEM typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) cocoapods-downloader (1.6.3) - cocoapods-keys (2.2.1) - dotenv - osx_keychain cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -61,8 +59,9 @@ GEM netrc (~> 0.11) cocoapods-try (1.2.0) colored2 (3.1.2) + colorize (0.8.1) concurrent-ruby (1.1.10) - dotenv (2.7.6) + dotenv (2.8.1) escape (0.0.4) ethon (0.15.0) ffi (>= 1.15.0) @@ -79,8 +78,6 @@ GEM nanaimo (0.3.0) nap (1.1.0) netrc (0.11.0) - osx_keychain (1.0.2) - RubyInline (~> 3) public_suffix (4.0.7) rexml (3.2.5) ruby-macho (2.5.1) @@ -95,15 +92,16 @@ GEM colored2 (~> 3.1) nanaimo (~> 0.3.0) rexml (~> 3.2.4) + yaml (0.2.0) zeitwerk (2.5.4) PLATFORMS ruby DEPENDENCIES + arkana cocoapods cocoapods-clean - cocoapods-keys BUNDLED WITH - 2.3.11 + 2.3.17 diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 4106af7de..d8a01b818 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -11,9 +11,7 @@ 0F2021FB2613262F000C64BF /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F2021FA2613262F000C64BF /* HashtagTimelineViewController.swift */; }; 0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */; }; 0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */; }; - 0F202213261351F5000C64BF /* APIService+HashtagTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */; }; 0F20222D261457EE000C64BF /* HashtagTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20222C261457EE000C64BF /* HashtagTimelineViewModel+State.swift */; }; - 0F20223926146553000C64BF /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array.swift */; }; 0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA0FDE25E0B57E0017CCDE /* WelcomeViewController.swift */; }; 0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA101125E105390017CCDE /* PrimaryActionButton.swift */; }; 0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA101B25E10E760017CCDE /* UIFont.swift */; }; @@ -31,8 +29,6 @@ 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; }; 2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */; }; 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; }; - 2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */; }; - 2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9DA261494120081BFC0 /* APIService+Search.swift */; }; 2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D35237926256D920031AF25 /* NotificationSection.swift */; }; 2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */; }; 2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */; }; @@ -48,15 +44,11 @@ 2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */; }; 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */; }; 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */; }; - 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */; }; 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */; }; 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */; }; 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */; }; 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */; }; 2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D607AD726242FC500B70763 /* NotificationViewModel.swift */; }; - 2D61254D262547C200299647 /* APIService+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61254C262547C200299647 /* APIService+Notification.swift */; }; - 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; - 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; }; 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */; }; 2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */; }; @@ -68,14 +60,9 @@ 2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */; }; 2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift */; }; 2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D84350425FF858100EECE90 /* UIScrollView.swift */; }; - 2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8FCA072637EABB00137F46 /* APIService+FollowRequest.swift */; }; 2D939AB525EDD8A90076FA61 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; }; - 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */ = {isa = PBXBuildFile; productRef = 2D939AC725EE14620076FA61 /* CropViewController */; }; 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */; }; - 2D9DB967263A76FB007C1D71 /* BlockDomainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB966263A76FB007C1D71 /* BlockDomainService.swift */; }; - 2D9DB96B263A91D1007C1D71 /* APIService+DomainBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB96A263A91D1007C1D71 /* APIService+DomainBlock.swift */; }; 2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA504682601ADE7008F4E6C /* SawToothView.swift */; }; - 2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA6054625F716A2006356F9 /* PlaybackState.swift */; }; 2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */; }; 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; }; 2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */; }; @@ -85,17 +72,13 @@ 2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */; }; 2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */; }; 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; }; - 2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */; }; 5B24BBDA262DB14800A9381B /* ReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBD7262DB14800A9381B /* ReportViewModel.swift */; }; 5B24BBDB262DB14800A9381B /* ReportStatusViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBD8262DB14800A9381B /* ReportStatusViewModel+Diffable.swift */; }; - 5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBE1262DB19100A9381B /* APIService+Report.swift */; }; 5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C456262599800002E742 /* SettingsViewModel.swift */; }; 5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */; }; 5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */; }; 5B90C461262599800002E742 /* SettingsLinkTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C45B262599800002E742 /* SettingsLinkTableViewCell.swift */; }; 5B90C462262599800002E742 /* SettingsSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C45C262599800002E742 /* SettingsSectionHeader.swift */; }; - 5B90C48526259BF10002E742 /* APIService+Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */; }; - 5B90C48B26259C120002E742 /* APIService+CoreData+Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */; }; 5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */; }; 5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */; }; 5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; }; @@ -107,7 +90,6 @@ 5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */; }; 5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; }; 5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; }; - 6213AF5828939C4800BCADB6 /* APIService+Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213AF5728939C4700BCADB6 /* APIService+Bookmark.swift */; }; 6213AF5A28939C8400BCADB6 /* BookmarkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213AF5928939C8400BCADB6 /* BookmarkViewModel.swift */; }; 6213AF5C28939C8A00BCADB6 /* BookmarkViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213AF5B28939C8A00BCADB6 /* BookmarkViewModel+State.swift */; }; 6213AF5E2893A8B200BCADB6 /* DataSourceFacade+Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213AF5D2893A8B200BCADB6 /* DataSourceFacade+Bookmark.swift */; }; @@ -123,18 +105,13 @@ DB023D2A27A0FE5C005AC798 /* DataSourceProvider+NotificationTableViewCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB023D2927A0FE5C005AC798 /* DataSourceProvider+NotificationTableViewCellDelegate.swift */; }; DB023D2C27A10464005AC798 /* NotificationTimelineViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB023D2B27A10464005AC798 /* NotificationTimelineViewController+DataSourceProvider.swift */; }; DB025B78278D606A002F581E /* StatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB025B77278D606A002F581E /* StatusItem.swift */; }; - DB025B93278D6501002F581E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB025B92278D6501002F581E /* Persistence.swift */; }; - DB025B95278D6530002F581E /* Persistence+MastodonUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB025B94278D6530002F581E /* Persistence+MastodonUser.swift */; }; - DB025B97278D66D5002F581E /* MastodonUser+Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB025B96278D66D5002F581E /* MastodonUser+Property.swift */; }; DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB029E94266A20430062874E /* MastodonAuthenticationController.swift */; }; DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; }; DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; - DB02EA0B280D180D00E751C5 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = DB02EA0A280D180D00E751C5 /* KeychainAccess */; }; DB03A793272A7E5700EE37C5 /* SidebarListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03A792272A7E5700EE37C5 /* SidebarListHeaderView.swift */; }; DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */; }; DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */; }; DB03F7F52689B782007B274C /* ComposeTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F42689B782007B274C /* ComposeTableView.swift */; }; - DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ED026538E3C00BEE9D8 /* Trie.swift */; }; DB0617EB277EF3820030EE79 /* GradientBorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617EA277EF3820030EE79 /* GradientBorderView.swift */; }; DB0617ED277F02C50030EE79 /* OnboardingNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617EC277F02C50030EE79 /* OnboardingNavigationController.swift */; }; DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617EE277F12720030EE79 /* NavigationActionView.swift */; }; @@ -145,9 +122,7 @@ DB0618012785732C0030EE79 /* ServerRulesTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618002785732C0030EE79 /* ServerRulesTableViewCell.swift */; }; DB0618032785A7100030EE79 /* RegisterSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618022785A7100030EE79 /* RegisterSection.swift */; }; DB0618052785A73D0030EE79 /* RegisterItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618042785A73D0030EE79 /* RegisterItem.swift */; }; - DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; }; DB0A322E280EE9FD001729D2 /* DiscoveryIntroBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0A322D280EE9FD001729D2 /* DiscoveryIntroBannerView.swift */; }; - DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; }; DB0C947726A7FE840088FB11 /* NotificationAvatarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0C947626A7FE840088FB11 /* NotificationAvatarButton.swift */; }; DB0EF72B26FDB1D200347686 /* SidebarListCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0EF72A26FDB1D200347686 /* SidebarListCollectionViewCell.swift */; }; DB0EF72E26FDB24F00347686 /* SidebarListContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0EF72D26FDB24F00347686 /* SidebarListContentView.swift */; }; @@ -155,8 +130,6 @@ DB0F9D54283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F9D53283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift */; }; DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F9D55283EB46200379AF8 /* ProfileHeaderView+Configuration.swift */; }; DB0FCB68279507EF006C02E2 /* DataSourceFacade+Meta.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */; }; - DB0FCB6C27950E29006C02E2 /* MastodonMentionContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB6B27950E29006C02E2 /* MastodonMentionContainer.swift */; }; - DB0FCB6E27950E6B006C02E2 /* MastodonMention.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB6D27950E6B006C02E2 /* MastodonMention.swift */; }; DB0FCB7027951368006C02E2 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB6F27951368006C02E2 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift */; }; DB0FCB7227952986006C02E2 /* NamingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB7127952986006C02E2 /* NamingState.swift */; }; DB0FCB7427956939006C02E2 /* DataSourceFacade+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB7327956939006C02E2 /* DataSourceFacade+Status.swift */; }; @@ -172,7 +145,6 @@ DB0FCB882796BDA9006C02E2 /* SearchItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB872796BDA9006C02E2 /* SearchItem.swift */; }; DB0FCB8C2796BF8D006C02E2 /* SearchViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB8B2796BF8D006C02E2 /* SearchViewModel+Diffable.swift */; }; DB0FCB8E2796C0B7006C02E2 /* TrendCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB8D2796C0B7006C02E2 /* TrendCollectionViewCell.swift */; }; - DB0FCB902796C5EB006C02E2 /* APIService+Trend.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB8F2796C5EB006C02E2 /* APIService+Trend.swift */; }; DB0FCB922796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB912796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift */; }; DB0FCB942797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB932797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift */; }; DB0FCB962797E6C2006C02E2 /* SearchResultViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB952797E6C2006C02E2 /* SearchResultViewController+DataSourceProvider.swift */; }; @@ -194,22 +166,13 @@ 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 */; }; - DB297B1B2679FAE200704C90 /* PlaceholderImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.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 */; }; + DB22C92828E700B70082A9E9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB22C92728E700B70082A9E9 /* MastodonSDK */; }; DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */; }; DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; }; DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */; }; - DB336F1C278D697E0031E64B /* MastodonUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */; }; - DB336F21278D6D960031E64B /* MastodonEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F20278D6D960031E64B /* MastodonEmoji.swift */; }; - DB336F23278D6DED0031E64B /* MastodonEmojiContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F22278D6DED0031E64B /* MastodonEmojiContainer.swift */; }; - DB336F28278D6EC70031E64B /* MastodonFieldContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F27278D6EC70031E64B /* MastodonFieldContainer.swift */; }; - DB336F2A278D6F2B0031E64B /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F29278D6F2B0031E64B /* MastodonField.swift */; }; - DB336F2C278D6FC30031E64B /* Persistence+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F2B278D6FC30031E64B /* Persistence+Status.swift */; }; - DB336F2E278D71AF0031E64B /* Status+Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F2D278D71AF0031E64B /* Status+Property.swift */; }; - DB336F32278D77330031E64B /* Persistence+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F31278D77330031E64B /* Persistence+Poll.swift */; }; - DB336F34278D77730031E64B /* Persistence+PollOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F33278D77730031E64B /* Persistence+PollOption.swift */; }; - DB336F36278D77A40031E64B /* PollOption+Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F35278D77A40031E64B /* PollOption+Property.swift */; }; - DB336F38278D7AAF0031E64B /* Poll+Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F37278D7AAF0031E64B /* Poll+Property.swift */; }; - DB336F3D278D80040031E64B /* FeedFetchedResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F3C278D80040031E64B /* FeedFetchedResultsController.swift */; }; DB336F3F278E668C0031E64B /* StatusTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F3E278E668C0031E64B /* StatusTableViewCell+ViewModel.swift */; }; DB336F41278E68480031E64B /* StatusView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F40278E68480031E64B /* StatusView+Configuration.swift */; }; DB336F43278EB1690031E64B /* MediaView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F42278EB1680031E64B /* MediaView+Configuration.swift */; }; @@ -224,7 +187,6 @@ DB3E6FE22806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FE12806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift */; }; DB3E6FE42806A5B800B035AE /* DiscoverySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FE32806A5B800B035AE /* DiscoverySection.swift */; }; DB3E6FE72806A7A200B035AE /* DiscoveryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FE62806A7A200B035AE /* DiscoveryItem.swift */; }; - DB3E6FE92806BD2200B035AE /* ThemeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FE82806BD2200B035AE /* ThemeService.swift */; }; DB3E6FEC2806D7F100B035AE /* DiscoveryNewsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FEB2806D7F100B035AE /* DiscoveryNewsViewController.swift */; }; DB3E6FEF2806D82600B035AE /* DiscoveryNewsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FEE2806D82600B035AE /* DiscoveryNewsViewModel.swift */; }; DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FF02806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift */; }; @@ -235,22 +197,8 @@ DB3EA8E6281B79E200598866 /* DiscoveryCommunityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EA8E5281B79E200598866 /* DiscoveryCommunityViewController.swift */; }; DB3EA8E9281B7A3700598866 /* DiscoveryCommunityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EA8E8281B7A3700598866 /* DiscoveryCommunityViewModel.swift */; }; DB3EA8EB281B7E0700598866 /* DiscoveryCommunityViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EA8EA281B7E0700598866 /* DiscoveryCommunityViewModel+State.swift */; }; - DB3EA8ED281B810100598866 /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EA8EC281B810100598866 /* APIService+PublicTimeline.swift */; }; DB3EA8EF281B837000598866 /* DiscoveryCommunityViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EA8EE281B837000598866 /* DiscoveryCommunityViewController+DataSourceProvider.swift */; }; DB3EA8F1281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EA8F0281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift */; }; - DB3EA8F5281BB65200598866 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA8F4281BB65200598866 /* MastodonSDK */; }; - DB3EA8FC281BBAE100598866 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA8FB281BBAE100598866 /* AlamofireImage */; }; - DB3EA8FE281BBAF200598866 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA8FD281BBAF200598866 /* Alamofire */; }; - DB3EA902281BBD5D00598866 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA901281BBD5D00598866 /* CommonOSLog */; }; - DB3EA904281BBD9400598866 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA903281BBD9400598866 /* Introspect */; }; - DB3EA906281BBE8200598866 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA905281BBE8200598866 /* AlamofireImage */; }; - DB3EA908281BBE8200598866 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA907281BBE8200598866 /* AlamofireNetworkActivityIndicator */; }; - DB3EA90A281BBE8200598866 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA909281BBE8200598866 /* Alamofire */; }; - DB3EA90C281BBE9600598866 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA90B281BBE9600598866 /* AlamofireImage */; }; - DB3EA90E281BBE9600598866 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA90D281BBE9600598866 /* AlamofireNetworkActivityIndicator */; }; - DB3EA910281BBE9600598866 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA90F281BBE9600598866 /* Alamofire */; }; - DB3EA912281BBEA800598866 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA911281BBEA800598866 /* AlamofireImage */; }; - DB3EA914281BBEA800598866 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA913281BBEA800598866 /* Alamofire */; }; DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; }; DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; }; DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDB25BAA00100D1B89D /* Main.storyboard */; }; @@ -267,23 +215,10 @@ 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 */; }; - DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */; }; - DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */; }; - DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */; }; - DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; }; - DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */; }; DB47AB6227CF752B00CD73C7 /* MastodonUISnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB6127CF752B00CD73C7 /* MastodonUISnapshotTests.swift */; }; DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */; }; - DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */; }; - DB486C0F282E41F200F69423 /* TabBarPager in Frameworks */ = {isa = PBXBuildFile; productRef = DB486C0E282E41F200F69423 /* TabBarPager */; }; - DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4924E126312AB200E9DB22 /* NotificationService.swift */; }; DB4932B126F1FB5300EF46D4 /* WizardCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4932B026F1FB5300EF46D4 /* WizardCardView.swift */; }; - DB4932B726F30F0700EF46D4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array.swift */; }; DB4932B926F31AD300EF46D4 /* BadgeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4932B826F31AD300EF46D4 /* BadgeButton.swift */; }; - DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61325FF2C5600B98345 /* EmojiService.swift */; }; - DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */; }; - DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */; }; - DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */; }; DB4AA6B327BA34B6009EC082 /* CellFrameCacheContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4AA6B227BA34B6009EC082 /* CellFrameCacheContainer.swift */; }; DB4F0963269ED06300D62E92 /* SearchResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0962269ED06300D62E92 /* SearchResultViewController.swift */; }; DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0965269ED52200D62E92 /* SearchResultViewModel.swift */; }; @@ -292,12 +227,8 @@ DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */; }; DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */; }; DB4F097D26A03A5B00D62E92 /* SearchHistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */; }; - DB4F097F26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */; }; DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */; }; DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */; }; - DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = DB552D4E26BBD10C00E481F6 /* OrderedCollections */; }; - DB564BD3269F3B35001E39A7 /* StatusFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */; }; - DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */; }; DB5B549A2833A60400DEF8B2 /* FamiliarFollowersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B54992833A60400DEF8B2 /* FamiliarFollowersViewController.swift */; }; DB5B549D2833A67400DEF8B2 /* FamiliarFollowersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B549C2833A67400DEF8B2 /* FamiliarFollowersViewModel.swift */; }; DB5B549F2833A72500DEF8B2 /* FamiliarFollowersViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B549E2833A72500DEF8B2 /* FamiliarFollowersViewModel+Diffable.swift */; }; @@ -336,45 +267,28 @@ DB63F74F2799405600455B82 /* SearchHistoryViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */; }; DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */; }; DB63F7542799491600455B82 /* DataSourceFacade+SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7532799491600455B82 /* DataSourceFacade+SearchHistory.swift */; }; - DB63F756279949BD00455B82 /* Persistence+SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F755279949BD00455B82 /* Persistence+SearchHistory.swift */; }; DB63F75A279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F759279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift */; }; - DB63F75C279956D000455B82 /* Persistence+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F75B279956D000455B82 /* Persistence+Tag.swift */; }; - DB63F75E27995B3B00455B82 /* Tag+Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F75D27995B3B00455B82 /* Tag+Property.swift */; }; DB63F76227996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F76127996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift */; }; DB63F764279A5E3C00455B82 /* NotificationTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F763279A5E3C00455B82 /* NotificationTimelineViewController.swift */; }; DB63F767279A5EB300455B82 /* NotificationTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F766279A5EB300455B82 /* NotificationTimelineViewModel.swift */; }; DB63F769279A5EBB00455B82 /* NotificationTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F768279A5EBB00455B82 /* NotificationTimelineViewModel+Diffable.swift */; }; DB63F76B279A5ED300455B82 /* NotificationTimelineViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F76A279A5ED300455B82 /* NotificationTimelineViewModel+LoadOldestState.swift */; }; DB63F76F279A7D1100455B82 /* NotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F76E279A7D1100455B82 /* NotificationTableViewCell.swift */; }; - DB63F771279A858500455B82 /* Persistence+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F770279A858500455B82 /* Persistence+Notification.swift */; }; - DB63F773279A87DC00455B82 /* Notification+Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F772279A87DC00455B82 /* Notification+Property.swift */; }; DB63F775279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F774279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift */; }; DB63F777279A9A2A00455B82 /* NotificationView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */; }; DB63F779279ABF9C00455B82 /* DataSourceFacade+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F778279ABF9C00455B82 /* DataSourceFacade+Reblog.swift */; }; DB63F77B279ACAE500455B82 /* DataSourceFacade+Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F77A279ACAE500455B82 /* DataSourceFacade+Favorite.swift */; }; - DB647C5926F1EA2700F7F82C /* WizardPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB647C5826F1EA2700F7F82C /* WizardPreference.swift */; }; 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 */; }; DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729525F9F91600D60309 /* ComposeStatusSection.swift */; }; DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */; }; - DB6746E7278ED633008A6B94 /* MastodonAuthenticationBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC50C0278ED49200AF0CC6 /* MastodonAuthenticationBox.swift */; }; - DB6746E8278ED639008A6B94 /* MastodonAuthenticationBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC50C0278ED49200AF0CC6 /* MastodonAuthenticationBox.swift */; }; - DB6746E9278ED63F008A6B94 /* MastodonAuthenticationBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC50C0278ED49200AF0CC6 /* MastodonAuthenticationBox.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 */; }; - DB67D08427312970006A36CF /* APIService+Following.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB67D08327312970006A36CF /* APIService+Following.swift */; }; DB67D08627312E67006A36CF /* WizardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB67D08527312E67006A36CF /* WizardViewController.swift */; }; - DB67D089273256D7006A36CF /* StoreReviewPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB67D088273256D7006A36CF /* StoreReviewPreference.swift */; }; - DB68045B2636DC6A00430867 /* MastodonPushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68045A2636DC6A00430867 /* MastodonPushNotification.swift */; }; DB6804662636DC9000430867 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; }; - DB68046C2636DC9E00430867 /* MastodonPushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68045A2636DC6A00430867 /* MastodonPushNotification.swift */; }; - DB6804832637CD4C00430867 /* AppShared.h in Headers */ = {isa = PBXBuildFile; fileRef = DB6804812637CD4C00430867 /* AppShared.h */; settings = {ATTRIBUTES = (Public, ); }; }; - DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; - DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804FC2637CFEC00430867 /* AppSecret.swift */; }; DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; }; DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */; }; DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; }; @@ -388,13 +302,11 @@ DB697DDF278F524F004EF2F7 /* DataSourceFacade+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB697DDE278F524F004EF2F7 /* DataSourceFacade+Profile.swift */; }; DB697DE1278F5296004EF2F7 /* DataSourceFacade+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB697DE0278F5296004EF2F7 /* DataSourceFacade+Model.swift */; }; DB6988DE2848D11C002398EF /* PagerTabStripNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6988DD2848D11C002398EF /* PagerTabStripNavigateable.swift */; }; - DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */; }; DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */; }; DB6B74EF272FB55000C70B6E /* FollowerListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */; }; DB6B74F2272FB67600C70B6E /* FollowerListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74F1272FB67600C70B6E /* FollowerListViewModel.swift */; }; DB6B74F4272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74F3272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift */; }; DB6B74F6272FBCDB00C70B6E /* FollowerListViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74F5272FBCDB00C70B6E /* FollowerListViewModel+State.swift */; }; - DB6B74FA272FC2B500C70B6E /* APIService+Follower.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74F9272FC2B500C70B6E /* APIService+Follower.swift */; }; DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74FB272FF55800C70B6E /* UserSection.swift */; }; DB6B74FE272FF59000C70B6E /* UserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74FD272FF59000C70B6E /* UserItem.swift */; }; DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */; }; @@ -402,26 +314,15 @@ DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; }; DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */; }; DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */; }; - DB6D9F4926353FD7008423CD /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F4826353FD6008423CD /* Subscription.swift */; }; - DB6D9F502635761F008423CD /* SubscriptionAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */; }; - DB6D9F57263577D2008423CD /* APIService+CoreData+Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F56263577D2008423CD /* APIService+CoreData+Setting.swift */; }; - DB6D9F6326357848008423CD /* SettingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F6226357848008423CD /* SettingService.swift */; }; - DB6D9F6F2635807F008423CD /* Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F6E2635807F008423CD /* Setting.swift */; }; - DB6D9F76263587C7008423CD /* SettingFetchedResultController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F75263587C7008423CD /* SettingFetchedResultController.swift */; }; DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F7C26358ED4008423CD /* SettingsSection.swift */; }; DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F8326358EEC008423CD /* SettingsItem.swift */; }; DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; }; DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */; }; DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */; }; - DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */; }; DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; }; DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; }; DB7274F4273BB9B200577D95 /* ListBatchFetchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */; }; DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73B48F261F030A002E9E9F /* SafariActivity.swift */; }; - DB73BF3B2711885500781945 /* UserDefaults+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF3A2711885500781945 /* UserDefaults+Notification.swift */; }; - DB73BF43271192BB00781945 /* InstanceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF42271192BB00781945 /* InstanceService.swift */; }; - DB73BF45271195AC00781945 /* APIService+CoreData+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF44271195AC00781945 /* APIService+CoreData+Instance.swift */; }; - DB73BF47271199CA00781945 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF46271199CA00781945 /* Instance.swift */; }; DB73BF4927140BA300781945 /* UICollectionViewDiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF4827140BA300781945 /* UICollectionViewDiffableDataSource.swift */; }; DB73BF4B27140C0800781945 /* UITableViewDiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF4A27140C0800781945 /* UITableViewDiffableDataSource.swift */; }; DB75BF1E263C1C1B00EDBF1F /* CustomScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */; }; @@ -437,9 +338,6 @@ DB852D1F26FB037800FC9D81 /* SidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1E26FB037800FC9D81 /* SidebarViewModel.swift */; }; DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */; }; DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */; }; - DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF52B25C13561002E6C99 /* ViewStateStore.swift */; }; - DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF52C25C13561002E6C99 /* DocumentStore.swift */; }; - DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF52D25C13561002E6C99 /* AppContext.swift */; }; DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54225C13647002E6C99 /* SceneCoordinator.swift */; }; DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54325C13647002E6C99 /* NeedsDependency.swift */; }; DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */; }; @@ -454,13 +352,8 @@ DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0226240EA300E5B6C1 /* CachedThreadViewModel.swift */; }; DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */; }; DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */; }; - DB938F1526241FDF00E5B6C1 /* APIService+Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */; }; DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */; }; DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */; }; - DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98336A25C9420100AD9700 /* APIService+App.swift */; }; - DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337025C9443200AD9700 /* APIService+Authentication.swift */; }; - DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; }; - DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; }; DB98EB4727B0DFAA0082E365 /* ReportStatusViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB4627B0DFAA0082E365 /* ReportStatusViewModel+State.swift */; }; DB98EB4927B0F0CD0082E365 /* ReportStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB4827B0F0CD0082E365 /* ReportStatusTableViewCell.swift */; }; DB98EB4C27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB4B27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift */; }; @@ -476,26 +369,18 @@ 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 */; }; - DB9A489026035963008B817C /* APIService+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488F26035963008B817C /* APIService+Media.swift */; }; - DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A48952603685D008B817C /* MastodonAttachmentService+UploadState.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 */; }; - DB9D7C21269824B80054B3DF /* APIService+Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D7C20269824B80054B3DF /* APIService+Filter.swift */; }; DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */; }; DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9F58EB26EF435000E7BBE9 /* AccountViewController.swift */; }; DB9F58EF26EF491E00E7BBE9 /* AccountListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9F58EE26EF491E00E7BBE9 /* AccountListViewModel.swift */; }; DB9F58F126EF512300E7BBE9 /* AccountListTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9F58F026EF512300E7BBE9 /* AccountListTableViewCell.swift */; }; - DBA088DF26958164003EB4B2 /* UserFetchedResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA088DE26958164003EB4B2 /* UserFetchedResultsController.swift */; }; DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; }; - DBA465932696B495002B41DB /* APIService+WebFinger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA465922696B495002B41DB /* APIService+WebFinger.swift */; }; - DBA465952696E387002B41DB /* AppPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA465942696E387002B41DB /* AppPreference.swift */; }; DBA4B0F626C269880077136E /* Intents.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = DBA4B0F926C269880077136E /* Intents.stringsdict */; }; DBA4B0F726C269880077136E /* Intents.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = DBA4B0F926C269880077136E /* Intents.stringsdict */; }; - DBA5A52F26F07ED800CACBAA /* PanModal in Frameworks */ = {isa = PBXBuildFile; productRef = DBA5A52E26F07ED800CACBAA /* PanModal */; }; DBA5A53126F08EF000CACBAA /* DragIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5A53026F08EF000CACBAA /* DragIndicatorView.swift */; }; DBA5A53526F0A36A00CACBAA /* AddAccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5A53426F0A36A00CACBAA /* AddAccountTableViewCell.swift */; }; - DBA5E7A3263AD0A3004598BB /* PhotoLibraryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */; }; DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7A4263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift */; }; DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7A8263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift */; }; DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7AA263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift */; }; @@ -504,12 +389,6 @@ DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */; }; DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */; }; DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; }; - DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC6482267D0B21007FE9FD /* DifferenceKit */; }; - DBAC649E267DFE43007FE9FD /* DiffableDataSources in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC649D267DFE43007FE9FD /* DiffableDataSources */; }; - DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC64A0267E6D02007FE9FD /* Fuzi */; }; - DBAE3F8E2616E0B1004B8251 /* APIService+Block.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F8D2616E0B1004B8251 /* APIService+Block.swift */; }; - DBAE3F942616E28B004B8251 /* APIService+Follow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F932616E28B004B8251 /* APIService+Follow.swift */; }; - DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; }; DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; }; DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; }; @@ -518,7 +397,6 @@ DBB45B5B27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5A27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift */; }; DBB45B6027B50A4F002DC5A7 /* RecommendAccountItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */; }; DBB45B6227B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B6127B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift */; }; - DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; }; DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; }; DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */; }; DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */; }; @@ -528,14 +406,10 @@ DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */; }; DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525632612C988002F1F29 /* MeProfileViewModel.swift */; }; DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */; }; - DBB8AB4A26AED0B500F6D281 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB8AB4926AED0B500F6D281 /* APIService.swift */; }; - DBB8AB4C26AED11300F6D281 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; }; DBB8AB4F26AED13F00F6D281 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDE25BAA00100D1B89D /* Assets.xcassets */; }; - DBB8AB5226AED1B300F6D281 /* APIService+Status+Publish.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF07A26A6BCE8006D7ED1 /* APIService+Status+Publish.swift */; }; DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB9759B262462E1004620BD /* ThreadMetaView.swift */; }; DBBC24A826A52F9000398BB9 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */; }; DBBC24AC26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */; }; - DBBC24CB26A546C000398BB9 /* ThemePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD376AB2692ECDB007FEC24 /* ThemePreference.swift */; }; DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */; }; DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; }; DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */; }; @@ -550,22 +424,15 @@ 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 */; }; - DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */; }; DBCA0EBC282BB38A0029E2B0 /* PageboyNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCA0EBB282BB38A0029E2B0 /* PageboyNavigateable.swift */; }; DBCBCBF4267CB070000F5B51 /* Decode85.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCBF3267CB070000F5B51 /* Decode85.swift */; }; - DBCBCC0D2680B908000F5B51 /* HomeTimelinePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCC0C2680B908000F5B51 /* HomeTimelinePreference.swift */; }; DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */; }; - DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */; }; DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B2F261440A50045B23D /* UITabBarController.swift */; }; DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */; }; - DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */; }; - DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */; }; - DBD376AC2692ECDB007FEC24 /* ThemePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD376AB2692ECDB007FEC24 /* ThemePreference.swift */; }; DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD376B1269302A4007FEC24 /* UITableViewCell.swift */; }; DBD5B1F627BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD5B1F527BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift */; }; DBD5B1F827BCFD9D00BD6B38 /* DataSourceProvider+TableViewControllerNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD5B1F727BCFD9D00BD6B38 /* DataSourceProvider+TableViewControllerNavigateable.swift */; }; DBD5B1FA27BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD5B1F927BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift */; }; - DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; }; DBDFF1902805543100557A48 /* DiscoveryPostsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBDFF18F2805543100557A48 /* DiscoveryPostsViewController.swift */; }; DBDFF1932805554900557A48 /* DiscoveryPostsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBDFF1922805554900557A48 /* DiscoveryPostsViewModel.swift */; }; DBDFF1952805561700557A48 /* DiscoveryPostsViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBDFF1942805561700557A48 /* DiscoveryPostsViewModel+Diffable.swift */; }; @@ -575,17 +442,12 @@ DBDFF19E2805703700557A48 /* DiscoveryPostsViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBDFF19D2805703700557A48 /* DiscoveryPostsViewController+DataSourceProvider.swift */; }; DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; }; DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; }; - DBE3CA6827A39CAB00AFE27B /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; - DBE3CA6B27A39CAF00AFE27B /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; - DBE3CA6E27A39CB300AFE27B /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CDBA261C427900430CC6 /* TimelineHeaderTableViewCell.swift */; }; DBE3CDCF261C42ED00430CC6 /* TimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CDCE261C42ED00430CC6 /* TimelineHeaderView.swift */; }; DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CDEB261C6B2900430CC6 /* FavoriteViewController.swift */; }; DBE3CDFB261C6CA500430CC6 /* FavoriteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CDFA261C6CA500430CC6 /* FavoriteViewModel.swift */; }; DBE3CE01261D623D00430CC6 /* FavoriteViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CE00261D623D00430CC6 /* FavoriteViewModel+State.swift */; }; DBE3CE07261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CE06261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift */; }; - DBE54AC62636C89F004E7C0B /* NotificationPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */; }; - DBE54ACC2636C8FD004E7C0B /* NotificationPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */; }; DBEFCD71282A12B200C0ABEA /* ReportReasonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEFCD70282A12B200C0ABEA /* ReportReasonViewController.swift */; }; DBEFCD74282A130400C0ABEA /* ReportReasonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEFCD73282A130400C0ABEA /* ReportReasonViewModel.swift */; }; DBEFCD76282A143F00C0ABEA /* ReportStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEFCD75282A143F00C0ABEA /* ReportStatusViewController.swift */; }; @@ -603,7 +465,6 @@ DBF1D257269DBAC600C1C08A /* SearchDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF1D256269DBAC600C1C08A /* SearchDetailViewModel.swift */; }; DBF3B73F2733EAED00E21627 /* local-codes.json in Resources */ = {isa = PBXBuildFile; fileRef = DBF3B73E2733EAED00E21627 /* local-codes.json */; }; DBF3B7412733EB9400E21627 /* MastodonLocalCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF3B7402733EB9400E21627 /* MastodonLocalCode.swift */; }; - DBF7A0FC26830C33004176A2 /* FPSIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = DBF7A0FB26830C33004176A2 /* FPSIndicator */; }; DBF8AE16263293E400C9C23C /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF8AE15263293E400C9C23C /* NotificationService.swift */; }; DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBF8AE13263293E400C9C23C /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBF96325262EC0A6001D8D25 /* AuthenticationServices.framework */; }; @@ -621,12 +482,6 @@ DBFEF06026A57715006D7ED1 /* StatusAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */; }; DBFEF06326A577F2006D7ED1 /* StatusAttachmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */; }; DBFEF06D26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF06C26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift */; }; - DBFEF06F26A690C4006D7ED1 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; }; - DBFEF07326A6913D006D7ED1 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF07226A6913D006D7ED1 /* APIService.swift */; }; - DBFEF07526A69192006D7ED1 /* APIService+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488F26035963008B817C /* APIService+Media.swift */; }; - DBFEF07B26A6BCE8006D7ED1 /* APIService+Status+Publish.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF07A26A6BCE8006D7ED1 /* APIService+Status+Publish.swift */; }; - DBFEF07C26A6BD0A006D7ED1 /* APIService+Status+Publish.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF07A26A6BCE8006D7ED1 /* APIService+Status+Publish.swift */; }; - EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4A2A2D7000E477CA459ADA9 /* Pods_AppShared.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -644,20 +499,6 @@ remoteGlobalIDString = DB427DD125BAA00100D1B89D; remoteInfo = Mastodon; }; - DB6804842637CD4C00430867 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = DB427DCA25BAA00100D1B89D /* Project object */; - proxyType = 1; - remoteGlobalIDString = DB68047E2637CD4C00430867; - remoteInfo = AppShared; - }; - DB6804A72637CDCC00430867 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = DB427DCA25BAA00100D1B89D /* Project object */; - proxyType = 1; - remoteGlobalIDString = DB68047E2637CD4C00430867; - remoteInfo = AppShared; - }; DB8FABCC26AEC7B2008E5AF4 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = DB427DCA25BAA00100D1B89D /* Project object */; @@ -665,13 +506,6 @@ remoteGlobalIDString = DB8FABC526AEC7B2008E5AF4; remoteInfo = MastodonIntent; }; - DB8FABD926AEC873008E5AF4 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = DB427DCA25BAA00100D1B89D /* Project object */; - proxyType = 1; - remoteGlobalIDString = DB68047E2637CD4C00430867; - remoteInfo = AppShared; - }; DBC6461A26A170AB00B0E31B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = DB427DCA25BAA00100D1B89D /* Project object */; @@ -679,13 +513,6 @@ remoteGlobalIDString = DBC6461126A170AB00B0E31B; remoteInfo = ShareActionExtension; }; - DBC6463526A195DB00B0E31B /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = DB427DCA25BAA00100D1B89D /* Project object */; - proxyType = 1; - remoteGlobalIDString = DB68047E2637CD4C00430867; - remoteInfo = AppShared; - }; DBF8AE18263293E400C9C23C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = DB427DCA25BAA00100D1B89D /* Project object */; @@ -702,7 +529,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -729,9 +555,7 @@ 0F2021FA2613262F000C64BF /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = ""; }; 0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewModel.swift; sourceTree = ""; }; 0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+Diffable.swift"; sourceTree = ""; }; - 0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HashtagTimeline.swift"; sourceTree = ""; }; 0F20222C261457EE000C64BF /* HashtagTimelineViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+State.swift"; sourceTree = ""; }; - 0F20223826146553000C64BF /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; 0FAA0FDE25E0B57E0017CCDE /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = ""; }; 0FAA101125E105390017CCDE /* PrimaryActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryActionButton.swift; sourceTree = ""; }; 0FAA101B25E10E760017CCDE /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = ""; }; @@ -749,8 +573,6 @@ 2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = ""; }; 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Gesture.swift"; sourceTree = ""; }; 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = ""; }; - 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Recommend.swift"; sourceTree = ""; }; - 2D34D9DA261494120081BFC0 /* APIService+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Search.swift"; sourceTree = ""; }; 2D35237926256D920031AF25 /* NotificationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSection.swift; sourceTree = ""; }; 2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewController.swift; sourceTree = ""; }; 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModel.swift; sourceTree = ""; }; @@ -771,8 +593,6 @@ 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewContainer.swift; sourceTree = ""; }; 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+DebugAction.swift"; sourceTree = ""; }; 2D607AD726242FC500B70763 /* NotificationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewModel.swift; sourceTree = ""; }; - 2D61254C262547C200299647 /* APIService+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Notification.swift"; sourceTree = ""; }; - 2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = ""; }; 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningOverlayView.swift; sourceTree = ""; }; 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; @@ -784,13 +604,9 @@ 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarTitleViewModel.swift; sourceTree = ""; }; 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarTitleView.swift; sourceTree = ""; }; 2D84350425FF858100EECE90 /* UIScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollView.swift; sourceTree = ""; }; - 2D8FCA072637EABB00137F46 /* APIService+FollowRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+FollowRequest.swift"; sourceTree = ""; }; 2D939AB425EDD8A90076FA61 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonRegisterViewController+Avatar.swift"; sourceTree = ""; }; - 2D9DB966263A76FB007C1D71 /* BlockDomainService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockDomainService.swift; sourceTree = ""; }; - 2D9DB96A263A91D1007C1D71 /* APIService+DomainBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+DomainBlock.swift"; sourceTree = ""; }; 2DA504682601ADE7008F4E6C /* SawToothView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SawToothView.swift; sourceTree = ""; }; - 2DA6054625F716A2006356F9 /* PlaybackState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackState.swift; sourceTree = ""; }; 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLoaderTableViewCell.swift; sourceTree = ""; }; 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = ""; }; 2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; @@ -801,7 +617,6 @@ 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendCollectionHeader.swift; sourceTree = ""; }; 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendAccountSection.swift; sourceTree = ""; }; 2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = ""; }; - 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Favorite.swift"; sourceTree = ""; }; 2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = ""; }; 3B7FD8F28DDA8FBCE5562B78 /* Pods-NotificationService.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.asdk - debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.asdk - debug.xcconfig"; sourceTree = ""; }; 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -811,14 +626,11 @@ 46DAB0EBDDFB678347CD96FF /* Pods-MastodonTests.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.asdk - release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.asdk - release.xcconfig"; sourceTree = ""; }; 5B24BBD7262DB14800A9381B /* ReportViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportViewModel.swift; sourceTree = ""; }; 5B24BBD8262DB14800A9381B /* ReportStatusViewModel+Diffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReportStatusViewModel+Diffable.swift"; sourceTree = ""; }; - 5B24BBE1262DB19100A9381B /* APIService+Report.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Report.swift"; sourceTree = ""; }; 5B90C456262599800002E742 /* SettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsToggleTableViewCell.swift; sourceTree = ""; }; 5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAppearanceTableViewCell.swift; sourceTree = ""; }; 5B90C45B262599800002E742 /* SettingsLinkTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsLinkTableViewCell.swift; sourceTree = ""; }; 5B90C45C262599800002E742 /* SettingsSectionHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsSectionHeader.swift; sourceTree = ""; }; - 5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Subscriptions.swift"; sourceTree = ""; }; - 5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Subscriptions.swift"; sourceTree = ""; }; 5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportViewController.swift; sourceTree = ""; }; 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportSection.swift; sourceTree = ""; }; 5CE45680252519F42FEA2D13 /* Pods-ShareActionExtension.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareActionExtension.asdk - release.xcconfig"; path = "Target Support Files/Pods-ShareActionExtension/Pods-ShareActionExtension.asdk - release.xcconfig"; sourceTree = ""; }; @@ -831,7 +643,6 @@ 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+History.swift"; sourceTree = ""; }; 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = ""; }; 6130CBE4B26E3C976ACC1688 /* Pods-ShareActionExtension.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareActionExtension.asdk - debug.xcconfig"; path = "Target Support Files/Pods-ShareActionExtension/Pods-ShareActionExtension.asdk - debug.xcconfig"; sourceTree = ""; }; - 6213AF5728939C4700BCADB6 /* APIService+Bookmark.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Bookmark.swift"; sourceTree = ""; }; 6213AF5928939C8400BCADB6 /* BookmarkViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkViewModel.swift; sourceTree = ""; }; 6213AF5B28939C8A00BCADB6 /* BookmarkViewModel+State.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BookmarkViewModel+State.swift"; sourceTree = ""; }; 6213AF5D2893A8B200BCADB6 /* DataSourceFacade+Bookmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Bookmark.swift"; sourceTree = ""; }; @@ -868,9 +679,6 @@ DB023D2927A0FE5C005AC798 /* DataSourceProvider+NotificationTableViewCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+NotificationTableViewCellDelegate.swift"; sourceTree = ""; }; DB023D2B27A10464005AC798 /* NotificationTimelineViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationTimelineViewController+DataSourceProvider.swift"; sourceTree = ""; }; DB025B77278D606A002F581E /* StatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItem.swift; sourceTree = ""; }; - DB025B92278D6501002F581E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; - DB025B94278D6530002F581E /* Persistence+MastodonUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Persistence+MastodonUser.swift"; sourceTree = ""; }; - DB025B96278D66D5002F581E /* MastodonUser+Property.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonUser+Property.swift"; sourceTree = ""; }; DB029E94266A20430062874E /* MastodonAuthenticationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationController.swift; sourceTree = ""; }; DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = ""; }; DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = ""; }; @@ -878,7 +686,6 @@ DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSplitViewController.swift; sourceTree = ""; }; DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToStatusContentTableViewCell.swift; sourceTree = ""; }; DB03F7F42689B782007B274C /* ComposeTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTableView.swift; sourceTree = ""; }; - DB040ED026538E3C00BEE9D8 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = ""; }; DB0617EA277EF3820030EE79 /* GradientBorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientBorderView.swift; sourceTree = ""; }; DB0617EC277F02C50030EE79 /* OnboardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationController.swift; sourceTree = ""; }; DB0617EE277F12720030EE79 /* NavigationActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationActionView.swift; sourceTree = ""; }; @@ -891,9 +698,7 @@ DB0618042785A73D0030EE79 /* RegisterItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterItem.swift; sourceTree = ""; }; DB0618062785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonRegisterViewModel+Diffable.swift"; sourceTree = ""; }; DB0618092785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterAvatarTableViewCell.swift; sourceTree = ""; }; - DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; DB0A322D280EE9FD001729D2 /* DiscoveryIntroBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryIntroBannerView.swift; sourceTree = ""; }; - DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = ""; }; DB0C947626A7FE840088FB11 /* NotificationAvatarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAvatarButton.swift; sourceTree = ""; }; DB0EF72A26FDB1D200347686 /* SidebarListCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListCollectionViewCell.swift; sourceTree = ""; }; DB0EF72D26FDB24F00347686 /* SidebarListContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListContentView.swift; sourceTree = ""; }; @@ -902,8 +707,6 @@ DB0F9D53283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileHeaderView+ViewModel.swift"; sourceTree = ""; }; DB0F9D55283EB46200379AF8 /* ProfileHeaderView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileHeaderView+Configuration.swift"; sourceTree = ""; }; DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Meta.swift"; sourceTree = ""; }; - DB0FCB6B27950E29006C02E2 /* MastodonMentionContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMentionContainer.swift; sourceTree = ""; }; - DB0FCB6D27950E6B006C02E2 /* MastodonMention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMention.swift; sourceTree = ""; }; DB0FCB6F27951368006C02E2 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimelineMiddleLoaderTableViewCell+ViewModel.swift"; sourceTree = ""; }; DB0FCB7127952986006C02E2 /* NamingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NamingState.swift; sourceTree = ""; }; DB0FCB7327956939006C02E2 /* DataSourceFacade+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status.swift"; sourceTree = ""; }; @@ -919,7 +722,6 @@ DB0FCB872796BDA9006C02E2 /* SearchItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchItem.swift; sourceTree = ""; }; DB0FCB8B2796BF8D006C02E2 /* SearchViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewModel+Diffable.swift"; sourceTree = ""; }; DB0FCB8D2796C0B7006C02E2 /* TrendCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendCollectionViewCell.swift; sourceTree = ""; }; - DB0FCB8F2796C5EB006C02E2 /* APIService+Trend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Trend.swift"; sourceTree = ""; }; DB0FCB912796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendSectionHeaderCollectionReusableView.swift; sourceTree = ""; }; DB0FCB932797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewModel+Diffable.swift"; sourceTree = ""; }; DB0FCB952797E6C2006C02E2 /* SearchResultViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewController+DataSourceProvider.swift"; sourceTree = ""; }; @@ -942,21 +744,9 @@ 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 = ""; }; - DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderImageCacheService.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 = ""; }; - DB336F20278D6D960031E64B /* MastodonEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonEmoji.swift; sourceTree = ""; }; - DB336F22278D6DED0031E64B /* MastodonEmojiContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonEmojiContainer.swift; sourceTree = ""; }; - DB336F27278D6EC70031E64B /* MastodonFieldContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonFieldContainer.swift; sourceTree = ""; }; - DB336F29278D6F2B0031E64B /* MastodonField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonField.swift; sourceTree = ""; }; - DB336F2B278D6FC30031E64B /* Persistence+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Persistence+Status.swift"; sourceTree = ""; }; - DB336F2D278D71AF0031E64B /* Status+Property.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Status+Property.swift"; sourceTree = ""; }; - DB336F31278D77330031E64B /* Persistence+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Persistence+Poll.swift"; sourceTree = ""; }; - DB336F33278D77730031E64B /* Persistence+PollOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Persistence+PollOption.swift"; sourceTree = ""; }; - DB336F35278D77A40031E64B /* PollOption+Property.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PollOption+Property.swift"; sourceTree = ""; }; - DB336F37278D7AAF0031E64B /* Poll+Property.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Poll+Property.swift"; sourceTree = ""; }; - DB336F3C278D80040031E64B /* FeedFetchedResultsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedFetchedResultsController.swift; sourceTree = ""; }; DB336F3E278E668C0031E64B /* StatusTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusTableViewCell+ViewModel.swift"; sourceTree = ""; }; DB336F40278E68480031E64B /* StatusView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusView+Configuration.swift"; sourceTree = ""; }; DB336F42278EB1680031E64B /* MediaView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaView+Configuration.swift"; sourceTree = ""; }; @@ -972,7 +762,6 @@ DB3E6FE12806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryHashtagsViewModel+Diffable.swift"; sourceTree = ""; }; DB3E6FE32806A5B800B035AE /* DiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoverySection.swift; sourceTree = ""; }; DB3E6FE62806A7A200B035AE /* DiscoveryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryItem.swift; sourceTree = ""; }; - DB3E6FE82806BD2200B035AE /* ThemeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeService.swift; sourceTree = ""; }; DB3E6FEB2806D7F100B035AE /* DiscoveryNewsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryNewsViewController.swift; sourceTree = ""; }; DB3E6FEE2806D82600B035AE /* DiscoveryNewsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryNewsViewModel.swift; sourceTree = ""; }; DB3E6FF02806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryNewsViewModel+Diffable.swift"; sourceTree = ""; }; @@ -983,7 +772,6 @@ DB3EA8E5281B79E200598866 /* DiscoveryCommunityViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryCommunityViewController.swift; sourceTree = ""; }; DB3EA8E8281B7A3700598866 /* DiscoveryCommunityViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryCommunityViewModel.swift; sourceTree = ""; }; DB3EA8EA281B7E0700598866 /* DiscoveryCommunityViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryCommunityViewModel+State.swift"; sourceTree = ""; }; - DB3EA8EC281B810100598866 /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = ""; }; DB3EA8EE281B837000598866 /* DiscoveryCommunityViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryCommunityViewController+DataSourceProvider.swift"; sourceTree = ""; }; DB3EA8F0281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryCommunityViewModel+Diffable.swift"; sourceTree = ""; }; DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1008,22 +796,11 @@ 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 = ""; }; - DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUser.swift; sourceTree = ""; }; - DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+MastodonAuthentication.swift"; sourceTree = ""; }; - DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = ""; }; - DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = ""; }; - DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = ""; }; DB47AB6127CF752B00CD73C7 /* MastodonUISnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUISnapshotTests.swift; sourceTree = ""; }; DB47AB6327CF858400CD73C7 /* AppStoreSnapshotTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AppStoreSnapshotTestPlan.xctestplan; sourceTree = ""; }; DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+State.swift"; sourceTree = ""; }; - DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+UserTimeline.swift"; sourceTree = ""; }; - DB4924E126312AB200E9DB22 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; DB4932B026F1FB5300EF46D4 /* WizardCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardCardView.swift; sourceTree = ""; }; DB4932B826F31AD300EF46D4 /* BadgeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeButton.swift; sourceTree = ""; }; - DB49A61325FF2C5600B98345 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = ""; }; - DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel.swift"; sourceTree = ""; }; - DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel+LoadState.swift"; sourceTree = ""; }; - DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CustomEmoji.swift"; sourceTree = ""; }; DB4AA6B227BA34B6009EC082 /* CellFrameCacheContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellFrameCacheContainer.swift; sourceTree = ""; }; DB4B777F26CA4EFA00B087B3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Intents.strings; sourceTree = ""; }; DB4B778226CA4EFA00B087B3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -1046,7 +823,6 @@ DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryViewModel.swift; sourceTree = ""; }; DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySection.swift; sourceTree = ""; }; DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryItem.swift; sourceTree = ""; }; - DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryFetchedResultController.swift; sourceTree = ""; }; DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchToSearchDetailViewControllerAnimatedTransitioning.swift; sourceTree = ""; }; DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchTransitionController.swift; sourceTree = ""; }; DB519B09281BCA2E00F0C99D /* ckb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ckb; path = ckb.lproj/Intents.strings; sourceTree = ""; }; @@ -1064,8 +840,6 @@ DB519B15281BCC2F00F0C99D /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Intents.strings; sourceTree = ""; }; DB519B16281BCC2F00F0C99D /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; DB519B17281BCC2F00F0C99D /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = tr; path = tr.lproj/Intents.stringsdict; sourceTree = ""; }; - DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFilterService.swift; sourceTree = ""; }; - DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = ""; }; DB5B54992833A60400DEF8B2 /* FamiliarFollowersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FamiliarFollowersViewController.swift; sourceTree = ""; }; DB5B549C2833A67400DEF8B2 /* FamiliarFollowersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FamiliarFollowersViewModel.swift; sourceTree = ""; }; DB5B549E2833A72500DEF8B2 /* FamiliarFollowersViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FamiliarFollowersViewModel+Diffable.swift"; sourceTree = ""; }; @@ -1104,23 +878,17 @@ DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewModel+Diffable.swift"; sourceTree = ""; }; DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySectionHeaderCollectionReusableView.swift; sourceTree = ""; }; DB63F7532799491600455B82 /* DataSourceFacade+SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+SearchHistory.swift"; sourceTree = ""; }; - DB63F755279949BD00455B82 /* Persistence+SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Persistence+SearchHistory.swift"; sourceTree = ""; }; DB63F759279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryUserCollectionViewCell+ViewModel.swift"; sourceTree = ""; }; - DB63F75B279956D000455B82 /* Persistence+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Persistence+Tag.swift"; sourceTree = ""; }; - DB63F75D27995B3B00455B82 /* Tag+Property.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tag+Property.swift"; sourceTree = ""; }; DB63F76127996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewController+DataSourceProvider.swift"; sourceTree = ""; }; DB63F763279A5E3C00455B82 /* NotificationTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTimelineViewController.swift; sourceTree = ""; }; DB63F766279A5EB300455B82 /* NotificationTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTimelineViewModel.swift; sourceTree = ""; }; DB63F768279A5EBB00455B82 /* NotificationTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationTimelineViewModel+Diffable.swift"; sourceTree = ""; }; DB63F76A279A5ED300455B82 /* NotificationTimelineViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationTimelineViewModel+LoadOldestState.swift"; sourceTree = ""; }; DB63F76E279A7D1100455B82 /* NotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTableViewCell.swift; sourceTree = ""; }; - DB63F770279A858500455B82 /* Persistence+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Persistence+Notification.swift"; sourceTree = ""; }; - DB63F772279A87DC00455B82 /* Notification+Property.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Property.swift"; sourceTree = ""; }; DB63F774279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationTableViewCell+ViewModel.swift"; sourceTree = ""; }; DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationView+Configuration.swift"; sourceTree = ""; }; DB63F778279ABF9C00455B82 /* DataSourceFacade+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Reblog.swift"; sourceTree = ""; }; DB63F77A279ACAE500455B82 /* DataSourceFacade+Favorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Favorite.swift"; sourceTree = ""; }; - DB647C5826F1EA2700F7F82C /* WizardPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardPreference.swift; sourceTree = ""; }; 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 = ""; }; @@ -1130,14 +898,7 @@ 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 = ""; }; - DB67D08327312970006A36CF /* APIService+Following.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Following.swift"; sourceTree = ""; }; DB67D08527312E67006A36CF /* WizardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardViewController.swift; sourceTree = ""; }; - DB67D088273256D7006A36CF /* StoreReviewPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreReviewPreference.swift; sourceTree = ""; }; - DB68045A2636DC6A00430867 /* MastodonPushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPushNotification.swift; sourceTree = ""; }; - DB68047F2637CD4C00430867 /* AppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - DB6804812637CD4C00430867 /* AppShared.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppShared.h; sourceTree = ""; }; - DB6804822637CD4C00430867 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DB6804FC2637CFEC00430867 /* AppSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSecret.swift; sourceTree = ""; }; DB68053E2638011000430867 /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = ""; }; DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSKeyValueObservation.swift; sourceTree = ""; }; DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptiveStatusBarStyleNavigationController.swift; sourceTree = ""; }; @@ -1152,13 +913,11 @@ DB697DDE278F524F004EF2F7 /* DataSourceFacade+Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Profile.swift"; sourceTree = ""; }; DB697DE0278F5296004EF2F7 /* DataSourceFacade+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Model.swift"; sourceTree = ""; }; DB6988DD2848D11C002398EF /* PagerTabStripNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagerTabStripNavigateable.swift; sourceTree = ""; }; - DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAttachmentService.swift; sourceTree = ""; }; DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentCollectionViewCell.swift; sourceTree = ""; }; DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerListViewController.swift; sourceTree = ""; }; DB6B74F1272FB67600C70B6E /* FollowerListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerListViewModel.swift; sourceTree = ""; }; DB6B74F3272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowerListViewModel+Diffable.swift"; sourceTree = ""; }; DB6B74F5272FBCDB00C70B6E /* FollowerListViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowerListViewModel+State.swift"; sourceTree = ""; }; - DB6B74F9272FC2B500C70B6E /* APIService+Follower.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Follower.swift"; sourceTree = ""; }; DB6B74FB272FF55800C70B6E /* UserSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSection.swift; sourceTree = ""; }; DB6B74FD272FF59000C70B6E /* UserItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserItem.swift; sourceTree = ""; }; DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTableViewCell.swift; sourceTree = ""; }; @@ -1166,26 +925,15 @@ DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = ""; }; DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+API+Subscriptions+Policy.swift"; sourceTree = ""; }; DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationService+Decrypt.swift"; sourceTree = ""; }; - DB6D9F4826353FD6008423CD /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = ""; }; - DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAlerts.swift; sourceTree = ""; }; - DB6D9F56263577D2008423CD /* APIService+CoreData+Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Setting.swift"; sourceTree = ""; }; - DB6D9F6226357848008423CD /* SettingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingService.swift; sourceTree = ""; }; - DB6D9F6E2635807F008423CD /* Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setting.swift; sourceTree = ""; }; - DB6D9F75263587C7008423CD /* SettingFetchedResultController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingFetchedResultController.swift; sourceTree = ""; }; DB6D9F7C26358ED4008423CD /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = ""; }; DB6D9F8326358EEC008423CD /* SettingsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItem.swift; sourceTree = ""; }; DB6D9F9626367249008423CD /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewController.swift; sourceTree = ""; }; DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTopChevronView.swift; sourceTree = ""; }; - DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Status.swift"; sourceTree = ""; }; DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = ""; }; DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = ""; }; DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListBatchFetchViewModel.swift; sourceTree = ""; }; DB73B48F261F030A002E9E9F /* SafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariActivity.swift; sourceTree = ""; }; - DB73BF3A2711885500781945 /* UserDefaults+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Notification.swift"; sourceTree = ""; }; - DB73BF42271192BB00781945 /* InstanceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceService.swift; sourceTree = ""; }; - DB73BF44271195AC00781945 /* APIService+CoreData+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Instance.swift"; sourceTree = ""; }; - DB73BF46271199CA00781945 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = ""; }; DB73BF4827140BA300781945 /* UICollectionViewDiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICollectionViewDiffableDataSource.swift; sourceTree = ""; }; DB73BF4A27140C0800781945 /* UITableViewDiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableViewDiffableDataSource.swift; sourceTree = ""; }; DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomScheduler.swift; sourceTree = ""; }; @@ -1204,9 +952,6 @@ DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionCollectionViewCell.swift; sourceTree = ""; }; DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionAppendEntryCollectionViewCell.swift; sourceTree = ""; }; DB89BA1025C10FF5008580ED /* Mastodon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Mastodon.entitlements; sourceTree = ""; }; - DB8AF52B25C13561002E6C99 /* ViewStateStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewStateStore.swift; sourceTree = ""; }; - DB8AF52C25C13561002E6C99 /* DocumentStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentStore.swift; sourceTree = ""; }; - DB8AF52D25C13561002E6C99 /* AppContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; DB8AF54225C13647002E6C99 /* SceneCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneCoordinator.swift; sourceTree = ""; }; DB8AF54325C13647002E6C99 /* NeedsDependency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeedsDependency.swift; sourceTree = ""; }; DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainTabBarController.swift; sourceTree = ""; }; @@ -1230,13 +975,8 @@ DB938F0226240EA300E5B6C1 /* CachedThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedThreadViewModel.swift; sourceTree = ""; }; DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteThreadViewModel.swift; sourceTree = ""; }; DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+LoadThreadState.swift"; sourceTree = ""; }; - DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Thread.swift"; sourceTree = ""; }; DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+Diffable.swift"; sourceTree = ""; }; DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTopLoaderTableViewCell.swift; sourceTree = ""; }; - DB98336A25C9420100AD9700 /* APIService+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+App.swift"; sourceTree = ""; }; - DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = ""; }; - DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = ""; }; - DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.swift"; sourceTree = ""; }; DB98EB4627B0DFAA0082E365 /* ReportStatusViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportStatusViewModel+State.swift"; sourceTree = ""; }; DB98EB4827B0F0CD0082E365 /* ReportStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusTableViewCell.swift; sourceTree = ""; }; DB98EB4B27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportStatusTableViewCell+ViewModel.swift"; sourceTree = ""; }; @@ -1252,20 +992,14 @@ 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 = ""; }; - DB9A488F26035963008B817C /* APIService+Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Media.swift"; sourceTree = ""; }; - DB9A48952603685D008B817C /* MastodonAttachmentService+UploadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonAttachmentService+UploadState.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 = ""; }; - DB9D7C20269824B80054B3DF /* APIService+Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Filter.swift"; sourceTree = ""; }; DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIInterpolatingMotionEffect.swift; sourceTree = ""; }; DB9F58EB26EF435000E7BBE9 /* AccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountViewController.swift; sourceTree = ""; }; DB9F58EE26EF491E00E7BBE9 /* AccountListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListViewModel.swift; sourceTree = ""; }; DB9F58F026EF512300E7BBE9 /* AccountListTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListTableViewCell.swift; sourceTree = ""; }; - DBA088DE26958164003EB4B2 /* UserFetchedResultsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFetchedResultsController.swift; sourceTree = ""; }; DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = ""; }; - DBA465922696B495002B41DB /* APIService+WebFinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+WebFinger.swift"; sourceTree = ""; }; - DBA465942696E387002B41DB /* AppPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreference.swift; sourceTree = ""; }; DBA4B0D326BD10AC0077136E /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Intents.strings"; sourceTree = ""; }; DBA4B0D626BD10AD0077136E /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; DBA4B0D726BD10F40077136E /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Intents.strings; sourceTree = ""; }; @@ -1282,7 +1016,6 @@ DBA4B0F826C269880077136E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Intents.stringsdict; sourceTree = ""; }; DBA5A53026F08EF000CACBAA /* DragIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DragIndicatorView.swift; sourceTree = ""; }; DBA5A53426F0A36A00CACBAA /* AddAccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountTableViewCell.swift; sourceTree = ""; }; - DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryService.swift; sourceTree = ""; }; DBA5E7A4263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuImagePreviewViewModel.swift; sourceTree = ""; }; DBA5E7A8263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuImagePreviewViewController.swift; sourceTree = ""; }; DBA5E7AA263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewCellContextMenuConfiguration.swift; sourceTree = ""; }; @@ -1291,9 +1024,6 @@ DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewCell.swift; sourceTree = ""; }; DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Field.swift"; sourceTree = ""; }; DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = ""; }; - DBAE3F8D2616E0B1004B8251 /* APIService+Block.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Block.swift"; sourceTree = ""; }; - DBAE3F932616E28B004B8251 /* APIService+Follow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Follow.swift"; sourceTree = ""; }; - DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = ""; }; DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = ""; }; DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLAnimatedImageView.swift; sourceTree = ""; }; DBB45B5527B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewVideoViewController.swift; sourceTree = ""; }; @@ -1310,12 +1040,10 @@ DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; DBB525632612C988002F1F29 /* MeProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeProfileViewModel.swift; sourceTree = ""; }; DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPostIntentHandler.swift; sourceTree = ""; }; - DBB8AB4926AED0B500F6D281 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.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 = ""; }; DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentTableViewCell.swift; sourceTree = ""; }; DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonRegex.swift; sourceTree = ""; }; - DBBC50C0278ED49200AF0CC6 /* MastodonAuthenticationBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationBox.swift; sourceTree = ""; }; DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = ""; }; DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewModel.swift; sourceTree = ""; }; DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTableViewCell.swift; sourceTree = ""; }; @@ -1329,7 +1057,6 @@ DBC6461926A170AB00B0E31B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; 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 = ""; }; - DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPublishService.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 = ""; }; DBC9E3A5282E13BB0063A4D9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = eu; path = eu.lproj/Intents.stringsdict; sourceTree = ""; }; @@ -1341,19 +1068,13 @@ DBC9E3AB282E17DF0063A4D9 /* es-AR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-AR"; path = "es-AR.lproj/Intents.stringsdict"; sourceTree = ""; }; DBCA0EBB282BB38A0029E2B0 /* PageboyNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageboyNavigateable.swift; sourceTree = ""; }; DBCBCBF3267CB070000F5B51 /* Decode85.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Decode85.swift; sourceTree = ""; }; - DBCBCC0C2680B908000F5B51 /* HomeTimelinePreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelinePreference.swift; sourceTree = ""; }; DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+Diffable.swift"; sourceTree = ""; }; - DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFetchedResultsController.swift; sourceTree = ""; }; DBCC3B2F261440A50045B23D /* UITabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITabBarController.swift; sourceTree = ""; }; DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedProfileViewModel.swift; sourceTree = ""; }; - DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Relationship.swift"; sourceTree = ""; }; - DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Reblog.swift"; sourceTree = ""; }; - DBD376AB2692ECDB007FEC24 /* ThemePreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemePreference.swift; sourceTree = ""; }; DBD376B1269302A4007FEC24 /* UITableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableViewCell.swift; sourceTree = ""; }; DBD5B1F527BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountTableViewCell+ViewModel.swift"; sourceTree = ""; }; DBD5B1F727BCFD9D00BD6B38 /* DataSourceProvider+TableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+TableViewControllerNavigateable.swift"; sourceTree = ""; }; DBD5B1F927BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+StatusTableViewControllerNavigateable.swift"; sourceTree = ""; }; - DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = ""; }; DBDFF18F2805543100557A48 /* DiscoveryPostsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryPostsViewController.swift; sourceTree = ""; }; DBDFF1922805554900557A48 /* DiscoveryPostsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryPostsViewModel.swift; sourceTree = ""; }; DBDFF1942805561700557A48 /* DiscoveryPostsViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryPostsViewModel+Diffable.swift"; sourceTree = ""; }; @@ -1369,7 +1090,6 @@ DBE3CDFA261C6CA500430CC6 /* FavoriteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteViewModel.swift; sourceTree = ""; }; DBE3CE00261D623D00430CC6 /* FavoriteViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteViewModel+State.swift"; sourceTree = ""; }; DBE3CE06261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteViewModel+Diffable.swift"; sourceTree = ""; }; - DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPreference.swift; sourceTree = ""; }; DBEB19E927E4F37B00B0E80E /* ku */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ku; path = ku.lproj/Intents.strings; sourceTree = ""; }; DBEB19EA27E4F37B00B0E80E /* ku */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ku; path = ku.lproj/InfoPlist.strings; sourceTree = ""; }; DBEB19EB27E4F37B00B0E80E /* ku */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ku; path = ku.lproj/Intents.stringsdict; sourceTree = ""; }; @@ -1418,8 +1138,6 @@ 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 = ""; }; - DBFEF07226A6913D006D7ED1 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; - DBFEF07A26A6BCE8006D7ED1 /* APIService+Status+Publish.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Status+Publish.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 = ""; }; @@ -1438,22 +1156,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB3EA914281BBEA800598866 /* Alamofire in Frameworks */, - 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */, - DBB525082611EAC0002F1F29 /* Tabman in Frameworks */, - DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */, + DB22C92428E700A80082A9E9 /* MastodonSDK in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, - DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */, - DB486C0F282E41F200F69423 /* TabBarPager in Frameworks */, - DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */, - 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, - DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */, - DBAC649E267DFE43007FE9FD /* DiffableDataSources in Frameworks */, - 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */, 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */, - DBF7A0FC26830C33004176A2 /* FPSIndicator in Frameworks */, - DBA5A52F26F07ED800CACBAA /* PanModal in Frameworks */, - DB3EA912281BBEA800598866 /* AlamofireImage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1474,26 +1179,12 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DB68047C2637CD4C00430867 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - DB3EA8FE281BBAF200598866 /* Alamofire in Frameworks */, - DB3EA8F5281BB65200598866 /* MastodonSDK in Frameworks */, - DB3EA8FC281BBAE100598866 /* AlamofireImage in Frameworks */, - DB02EA0B280D180D00E751C5 /* KeychainAccess in Frameworks */, - EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */, - DB3EA904281BBD9400598866 /* Introspect in Frameworks */, - DB3EA902281BBD5D00598866 /* CommonOSLog in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; DB8FABC326AEC7B2008E5AF4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DB22C92828E700B70082A9E9 /* MastodonSDK in Frameworks */, DB8FABC726AEC7B2008E5AF4 /* Intents.framework in Frameworks */, - DBE3CA6E27A39CB300AFE27B /* AppShared.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1501,10 +1192,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB3EA90E281BBE9600598866 /* AlamofireNetworkActivityIndicator in Frameworks */, - DB3EA910281BBE9600598866 /* Alamofire in Frameworks */, - DBE3CA6B27A39CAF00AFE27B /* AppShared.framework in Frameworks */, - DB3EA90C281BBE9600598866 /* AlamofireImage in Frameworks */, + DB22C92628E700AF0082A9E9 /* MastodonSDK in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1512,10 +1200,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB3EA908281BBE8200598866 /* AlamofireNetworkActivityIndicator in Frameworks */, - DB3EA90A281BBE8200598866 /* Alamofire in Frameworks */, - DBE3CA6827A39CAB00AFE27B /* AppShared.framework in Frameworks */, - DB3EA906281BBE8200598866 /* AlamofireImage in Frameworks */, + DB22C92228E700A10082A9E9 /* MastodonSDK in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1731,26 +1416,6 @@ path = Vender; sourceTree = ""; }; - 2D61335525C1886800CAE157 /* Service */ = { - isa = PBXGroup; - children = ( - DB45FB0425CA87B4005A8AC7 /* APIService */, - DB49A61925FF327D00B98345 /* EmojiService */, - DB9A489B26036E19008B817C /* MastodonAttachmentService */, - DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */, - 2DA6054625F716A2006356F9 /* PlaybackState.swift */, - DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */, - 2D9DB966263A76FB007C1D71 /* BlockDomainService.swift */, - DB4924E126312AB200E9DB22 /* NotificationService.swift */, - DB6D9F6226357848008423CD /* SettingService.swift */, - DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */, - DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */, - DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */, - DB73BF42271192BB00781945 /* InstanceService.swift */, - ); - path = Service; - sourceTree = ""; - }; 2D69CFF225CA9E2200C3A1B2 /* Protocol */ = { isa = PBXGroup; children = ( @@ -1783,7 +1448,6 @@ DB4F097726A039A200D62E92 /* Search */, DB3E6FE52806A5BA00B035AE /* Discovery */, DB0617FA27855B660030EE79 /* Settings */, - DBCBED2226132E1D00B49291 /* FetchedResultsController */, ); path = Diffiable; sourceTree = ""; @@ -1974,39 +1638,6 @@ path = Onboarding; sourceTree = ""; }; - DB025B91278D64F0002F581E /* Persistence */ = { - isa = PBXGroup; - children = ( - DB025B98278D66D8002F581E /* Extension */, - DB336F24278D6DF40031E64B /* Protocol */, - DB025B92278D6501002F581E /* Persistence.swift */, - DB025B94278D6530002F581E /* Persistence+MastodonUser.swift */, - DB336F2B278D6FC30031E64B /* Persistence+Status.swift */, - DB336F31278D77330031E64B /* Persistence+Poll.swift */, - DB336F33278D77730031E64B /* Persistence+PollOption.swift */, - DB63F75B279956D000455B82 /* Persistence+Tag.swift */, - DB63F755279949BD00455B82 /* Persistence+SearchHistory.swift */, - DB63F770279A858500455B82 /* Persistence+Notification.swift */, - ); - path = Persistence; - sourceTree = ""; - }; - DB025B98278D66D8002F581E /* Extension */ = { - isa = PBXGroup; - children = ( - DB025B96278D66D5002F581E /* MastodonUser+Property.swift */, - DB336F2D278D71AF0031E64B /* Status+Property.swift */, - DB336F37278D7AAF0031E64B /* Poll+Property.swift */, - DB336F35278D77A40031E64B /* PollOption+Property.swift */, - DB63F75D27995B3B00455B82 /* Tag+Property.swift */, - DB63F772279A87DC00455B82 /* Notification+Property.swift */, - DB336F20278D6D960031E64B /* MastodonEmoji.swift */, - DB336F29278D6F2B0031E64B /* MastodonField.swift */, - DB0FCB6D27950E6B006C02E2 /* MastodonMention.swift */, - ); - path = Extension; - sourceTree = ""; - }; DB03F7F1268990A2007B274C /* TableViewCell */ = { isa = PBXGroup; children = ( @@ -2081,19 +1712,6 @@ path = Cell; sourceTree = ""; }; - DB084B5125CBC56300F898ED /* CoreDataStack */ = { - isa = PBXGroup; - children = ( - DB084B5625CBC56C00F898ED /* Status.swift */, - DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */, - DB6D9F6E2635807F008423CD /* Setting.swift */, - DB6D9F4826353FD6008423CD /* Subscription.swift */, - DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */, - DB73BF46271199CA00781945 /* Instance.swift */, - ); - path = CoreDataStack; - sourceTree = ""; - }; DB0A322F280EEA00001729D2 /* View */ = { isa = PBXGroup; children = ( @@ -2147,16 +1765,6 @@ path = View; sourceTree = ""; }; - DB336F24278D6DF40031E64B /* Protocol */ = { - isa = PBXGroup; - children = ( - DB336F22278D6DED0031E64B /* MastodonEmojiContainer.swift */, - DB336F27278D6EC70031E64B /* MastodonFieldContainer.swift */, - DB0FCB6B27950E29006C02E2 /* MastodonMentionContainer.swift */, - ); - path = Protocol; - sourceTree = ""; - }; DB3D0FF725BAA68500EAA174 /* Supporting Files */ = { isa = PBXGroup; children = ( @@ -2203,7 +1811,6 @@ DB3E6FEA2806BD2500B035AE /* MastodonUI */ = { isa = PBXGroup; children = ( - DB3E6FE82806BD2200B035AE /* ThemeService.swift */, ); path = MastodonUI; sourceTree = ""; @@ -2251,7 +1858,6 @@ DB427DD425BAA00100D1B89D /* Mastodon */, DB427DEB25BAA00100D1B89D /* MastodonTests */, DB427DF625BAA00100D1B89D /* MastodonUITests */, - DB6804802637CD4C00430867 /* AppShared */, DBF8AE14263293E400C9C23C /* NotificationService */, DBC6461326A170AB00B0E31B /* ShareActionExtension */, DB8FABC826AEC7B2008E5AF4 /* MastodonIntent */, @@ -2269,7 +1875,6 @@ DB427DE825BAA00100D1B89D /* MastodonTests.xctest */, DB427DF325BAA00100D1B89D /* MastodonUITests.xctest */, DBF8AE13263293E400C9C23C /* NotificationService.appex */, - DB68047F2637CD4C00430867 /* AppShared.framework */, DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */, DB8FABC626AEC7B2008E5AF4 /* MastodonIntent.appex */, ); @@ -2282,15 +1887,12 @@ DB89BA1025C10FF5008580ED /* Mastodon.entitlements */, DB427DE325BAA00100D1B89D /* Info.plist */, 2D76319C25C151DE00929FB9 /* Diffiable */, - DB8AF52A25C13561002E6C99 /* State */, - 2D61335525C1886800CAE157 /* Service */, DB8AF55525C1379F002E6C99 /* Scene */, DB8AF54125C13647002E6C99 /* Coordinator */, DB8AF56225C138BC002E6C99 /* Extension */, 2D5A3D0125CF8640002347D6 /* Vender */, DB73B495261F030D002E9E9F /* Activity */, DBBC24D526A54BCB00398BB9 /* Helper */, - DB025B91278D64F0002F581E /* Persistence */, DB5086CB25CC0DB400C2C187 /* Preference */, 2D69CFF225CA9E2200C3A1B2 /* Protocol */, DB6746EE278F45F3008A6B94 /* Template */, @@ -2319,72 +1921,6 @@ path = MastodonUITests; sourceTree = ""; }; - DB45FB0425CA87B4005A8AC7 /* APIService */ = { - isa = PBXGroup; - children = ( - DB45FB0925CA87BC005A8AC7 /* CoreData */, - 2D61335D25C1894B00CAE157 /* APIService.swift */, - DB98337E25C9452D00AD9700 /* APIService+APIError.swift */, - DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */, - 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */, - DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */, - DB98336A25C9420100AD9700 /* APIService+App.swift */, - DB98337025C9443200AD9700 /* APIService+Authentication.swift */, - DB98339B25C96DE600AD9700 /* APIService+Account.swift */, - 2D9DB96A263A91D1007C1D71 /* APIService+DomainBlock.swift */, - DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */, - DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */, - DB3EA8EC281B810100598866 /* APIService+PublicTimeline.swift */, - DBA465922696B495002B41DB /* APIService+WebFinger.swift */, - DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */, - DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */, - DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */, - DBFEF07A26A6BCE8006D7ED1 /* APIService+Status+Publish.swift */, - DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */, - DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */, - 2D61254C262547C200299647 /* APIService+Notification.swift */, - DB9A488F26035963008B817C /* APIService+Media.swift */, - 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */, - DB0FCB8F2796C5EB006C02E2 /* APIService+Trend.swift */, - 2D34D9DA261494120081BFC0 /* APIService+Search.swift */, - 0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */, - DB6B74F9272FC2B500C70B6E /* APIService+Follower.swift */, - DB67D08327312970006A36CF /* APIService+Following.swift */, - DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */, - 5B24BBE1262DB19100A9381B /* APIService+Report.swift */, - DBAE3F932616E28B004B8251 /* APIService+Follow.swift */, - 2D8FCA072637EABB00137F46 /* APIService+FollowRequest.swift */, - DBAE3F8D2616E0B1004B8251 /* APIService+Block.swift */, - DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */, - 5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */, - DB9D7C20269824B80054B3DF /* APIService+Filter.swift */, - 6213AF5728939C4700BCADB6 /* APIService+Bookmark.swift */, - ); - path = APIService; - sourceTree = ""; - }; - DB45FB0925CA87BC005A8AC7 /* CoreData */ = { - isa = PBXGroup; - children = ( - DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */, - DB6D9F56263577D2008423CD /* APIService+CoreData+Setting.swift */, - 5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */, - DB73BF44271195AC00781945 /* APIService+CoreData+Instance.swift */, - ); - path = CoreData; - sourceTree = ""; - }; - DB49A61925FF327D00B98345 /* EmojiService */ = { - isa = PBXGroup; - children = ( - DB49A61325FF2C5600B98345 /* EmojiService.swift */, - DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */, - DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */, - DB040ED026538E3C00BEE9D8 /* Trie.swift */, - ); - path = EmojiService; - sourceTree = ""; - }; DB4F0964269ED06700D62E92 /* SearchResult */ = { isa = PBXGroup; children = ( @@ -2472,13 +2008,7 @@ DB5086CB25CC0DB400C2C187 /* Preference */ = { isa = PBXGroup; children = ( - DBA465942696E387002B41DB /* AppPreference.swift */, - DB647C5826F1EA2700F7F82C /* WizardPreference.swift */, - DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */, DB1D842F26566512000346B3 /* KeyboardPreference.swift */, - DBCBCC0C2680B908000F5B51 /* HomeTimelinePreference.swift */, - DBD376AB2692ECDB007FEC24 /* ThemePreference.swift */, - DB67D088273256D7006A36CF /* StoreReviewPreference.swift */, ); path = Preference; sourceTree = ""; @@ -2669,17 +2199,6 @@ path = Wizard; sourceTree = ""; }; - DB6804802637CD4C00430867 /* AppShared */ = { - isa = PBXGroup; - children = ( - DB6804812637CD4C00430867 /* AppShared.h */, - DB6804822637CD4C00430867 /* Info.plist */, - DB6804FC2637CFEC00430867 /* AppSecret.swift */, - DB73BF3A2711885500781945 /* UserDefaults+Notification.swift */, - ); - path = AppShared; - sourceTree = ""; - }; DB68A03825E900CC00CFDF14 /* Share */ = { isa = PBXGroup; children = ( @@ -2840,16 +2359,6 @@ path = Root; sourceTree = ""; }; - DB8AF52A25C13561002E6C99 /* State */ = { - isa = PBXGroup; - children = ( - DB8AF52D25C13561002E6C99 /* AppContext.swift */, - DB8AF52B25C13561002E6C99 /* ViewStateStore.swift */, - DB8AF52C25C13561002E6C99 /* DocumentStore.swift */, - ); - path = State; - sourceTree = ""; - }; DB8AF54125C13647002E6C99 /* Coordinator */ = { isa = PBXGroup; children = ( @@ -2895,16 +2404,13 @@ DB8AF56225C138BC002E6C99 /* Extension */ = { isa = PBXGroup; children = ( - DB084B5125CBC56300F898ED /* CoreDataStack */, DB3E6FEA2806BD2500B035AE /* MastodonUI */, DB6C8C0525F0921200AAA452 /* MastodonSDK */, 2DF123A625C3B0210020F248 /* ActiveLabel.swift */, 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */, - 0F20223826146553000C64BF /* Array.swift */, 2D206B8525F5FB0900143C56 /* Double.swift */, DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */, DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */, - DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */, DB0140CE25C42AEE00F9F3CF /* OSLog.swift */, 2D939AB425EDD8A90076FA61 /* String.swift */, DB68A06225E905E000CFDF14 /* UIApplication.swift */, @@ -2936,7 +2442,6 @@ DB8FABC926AEC7B2008E5AF4 /* IntentHandler.swift */, DB64BA462851F23300ADF1B7 /* Model */, DB64BA492851F65F00ADF1B7 /* Handler */, - DBB8AB4B26AED0B800F6D281 /* Service */, DB8FABCB26AEC7B2008E5AF4 /* Info.plist */, ); path = MastodonIntent; @@ -3019,15 +2524,6 @@ path = ReportResult; sourceTree = ""; }; - DB9A489B26036E19008B817C /* MastodonAttachmentService */ = { - isa = PBXGroup; - children = ( - DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */, - DB9A48952603685D008B817C /* MastodonAttachmentService+UploadState.swift */, - ); - path = MastodonAttachmentService; - sourceTree = ""; - }; DB9D6BEE25E4F5370051B173 /* Search */ = { isa = PBXGroup; children = ( @@ -3195,19 +2691,10 @@ path = View; sourceTree = ""; }; - DBB8AB4B26AED0B800F6D281 /* Service */ = { - isa = PBXGroup; - children = ( - DBB8AB4926AED0B500F6D281 /* APIService.swift */, - ); - path = Service; - sourceTree = ""; - }; DBBC24D526A54BCB00398BB9 /* Helper */ = { isa = PBXGroup; children = ( DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */, - DBBC50C0278ED49200AF0CC6 /* MastodonAuthenticationBox.swift */, DBF3B7402733EB9400E21627 /* MastodonLocalCode.swift */, ); path = Helper; @@ -3236,23 +2723,10 @@ DBC6461926A170AB00B0E31B /* Info.plist */, DBC6461626A170AB00B0E31B /* MainInterface.storyboard */, DBFEF06126A57721006D7ED1 /* Scene */, - DBFEF07426A69140006D7ED1 /* Service */, ); path = ShareActionExtension; sourceTree = ""; }; - DBCBED2226132E1D00B49291 /* FetchedResultsController */ = { - isa = PBXGroup; - children = ( - DB336F3C278D80040031E64B /* FeedFetchedResultsController.swift */, - DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */, - DBA088DE26958164003EB4B2 /* UserFetchedResultsController.swift */, - DB6D9F75263587C7008423CD /* SettingFetchedResultController.swift */, - DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */, - ); - path = FetchedResultsController; - sourceTree = ""; - }; DBDFF1912805544800557A48 /* Discovery */ = { isa = PBXGroup; children = ( @@ -3376,7 +2850,6 @@ DB68053E2638011000430867 /* NotificationService.entitlements */, DBF8AE15263293E400C9C23C /* NotificationService.swift */, DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */, - DB68045A2636DC6A00430867 /* MastodonPushNotification.swift */, DBCBCBF3267CB070000F5B51 /* Decode85.swift */, DBF8AE17263293E400C9C23C /* Info.plist */, ); @@ -3430,27 +2903,8 @@ path = Scene; sourceTree = ""; }; - DBFEF07426A69140006D7ED1 /* Service */ = { - isa = PBXGroup; - children = ( - DBFEF07226A6913D006D7ED1 /* APIService.swift */, - ); - path = Service; - sourceTree = ""; - }; /* End PBXGroup section */ -/* Begin PBXHeadersBuildPhase section */ - DB68047A2637CD4C00430867 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - DB6804832637CD4C00430867 /* AppShared.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - /* Begin PBXNativeTarget section */ DB427DD125BAA00100D1B89D /* Mastodon */ = { isa = PBXNativeTarget; @@ -3471,25 +2925,12 @@ ); dependencies = ( DBF8AE19263293E400C9C23C /* PBXTargetDependency */, - DB6804852637CD4C00430867 /* PBXTargetDependency */, DBC6461B26A170AB00B0E31B /* PBXTargetDependency */, DB8FABCD26AEC7B2008E5AF4 /* PBXTargetDependency */, ); name = Mastodon; packageProductDependencies = ( - 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */, - 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */, - 2D939AC725EE14620076FA61 /* CropViewController */, - DBB525072611EAC0002F1F29 /* Tabman */, - DBAC6482267D0B21007FE9FD /* DifferenceKit */, - DBAC649D267DFE43007FE9FD /* DiffableDataSources */, - DBAC64A0267E6D02007FE9FD /* Fuzi */, - DBF7A0FB26830C33004176A2 /* FPSIndicator */, - DB552D4E26BBD10C00E481F6 /* OrderedCollections */, - DBA5A52E26F07ED800CACBAA /* PanModal */, - DB3EA911281BBEA800598866 /* AlamofireImage */, - DB3EA913281BBEA800598866 /* Alamofire */, - DB486C0E282E41F200F69423 /* TabBarPager */, + DB22C92328E700A80082A9E9 /* MastodonSDK */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -3534,33 +2975,6 @@ productReference = DB427DF325BAA00100D1B89D /* MastodonUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; - DB68047E2637CD4C00430867 /* AppShared */ = { - isa = PBXNativeTarget; - buildConfigurationList = DB6804882637CD4C00430867 /* Build configuration list for PBXNativeTarget "AppShared" */; - buildPhases = ( - C6B7D3A8ACD77F6620D0E0AD /* [CP] Check Pods Manifest.lock */, - DB68047A2637CD4C00430867 /* Headers */, - DB68047B2637CD4C00430867 /* Sources */, - DB68047C2637CD4C00430867 /* Frameworks */, - DB68047D2637CD4C00430867 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = AppShared; - packageProductDependencies = ( - DB02EA0A280D180D00E751C5 /* KeychainAccess */, - DB3EA8F4281BB65200598866 /* MastodonSDK */, - DB3EA8FB281BBAE100598866 /* AlamofireImage */, - DB3EA8FD281BBAF200598866 /* Alamofire */, - DB3EA901281BBD5D00598866 /* CommonOSLog */, - DB3EA903281BBD9400598866 /* Introspect */, - ); - productName = AppShared; - productReference = DB68047F2637CD4C00430867 /* AppShared.framework */; - productType = "com.apple.product-type.framework"; - }; DB8FABC526AEC7B2008E5AF4 /* MastodonIntent */ = { isa = PBXNativeTarget; buildConfigurationList = DB8FABCF26AEC7B2008E5AF4 /* Build configuration list for PBXNativeTarget "MastodonIntent" */; @@ -3572,9 +2986,11 @@ buildRules = ( ); dependencies = ( - DB8FABDA26AEC873008E5AF4 /* PBXTargetDependency */, ); name = MastodonIntent; + packageProductDependencies = ( + DB22C92728E700B70082A9E9 /* MastodonSDK */, + ); productName = MastodonIntent; productReference = DB8FABC626AEC7B2008E5AF4 /* MastodonIntent.appex */; productType = "com.apple.product-type.app-extension"; @@ -3590,13 +3006,10 @@ buildRules = ( ); dependencies = ( - DBC6463626A195DB00B0E31B /* PBXTargetDependency */, ); name = ShareActionExtension; packageProductDependencies = ( - DB3EA90B281BBE9600598866 /* AlamofireImage */, - DB3EA90D281BBE9600598866 /* AlamofireNetworkActivityIndicator */, - DB3EA90F281BBE9600598866 /* Alamofire */, + DB22C92528E700AF0082A9E9 /* MastodonSDK */, ); productName = ShareActionExtension; productReference = DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */; @@ -3613,13 +3026,10 @@ buildRules = ( ); dependencies = ( - DB6804A82637CDCC00430867 /* PBXTargetDependency */, ); name = NotificationService; packageProductDependencies = ( - DB3EA905281BBE8200598866 /* AlamofireImage */, - DB3EA907281BBE8200598866 /* AlamofireNetworkActivityIndicator */, - DB3EA909281BBE8200598866 /* Alamofire */, + DB22C92128E700A10082A9E9 /* MastodonSDK */, ); productName = NotificationService; productReference = DBF8AE13263293E400C9C23C /* NotificationService.appex */; @@ -3646,10 +3056,6 @@ CreatedOnToolsVersion = 12.4; TestTargetID = DB427DD125BAA00100D1B89D; }; - DB68047E2637CD4C00430867 = { - CreatedOnToolsVersion = 12.4; - LastSwiftMigration = 1240; - }; DB8FABC526AEC7B2008E5AF4 = { CreatedOnToolsVersion = 12.5.1; }; @@ -3694,23 +3100,6 @@ ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( - DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */, - 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */, - DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, - 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */, - 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */, - DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */, - DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */, - DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */, - DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */, - DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */, - DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */, - DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */, - DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */, - DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */, - DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, - DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */, - DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -3719,7 +3108,6 @@ DB427DD125BAA00100D1B89D /* Mastodon */, DB427DE725BAA00100D1B89D /* MastodonTests */, DB427DF225BAA00100D1B89D /* MastodonUITests */, - DB68047E2637CD4C00430867 /* AppShared */, DBF8AE12263293E400C9C23C /* NotificationService */, DBC6461126A170AB00B0E31B /* ShareActionExtension */, DB8FABC526AEC7B2008E5AF4 /* MastodonIntent */, @@ -3758,13 +3146,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DB68047D2637CD4C00430867 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; DB8FABC426AEC7B2008E5AF4 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -3854,28 +3235,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - C6B7D3A8ACD77F6620D0E0AD /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-AppShared-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; DB025B8E278D6448002F581E /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -3978,29 +3337,19 @@ DBDFF19E2805703700557A48 /* DiscoveryPostsViewController+DataSourceProvider.swift in Sources */, DB6180EB26391C140018D199 /* MediaPreviewTransitionItem.swift in Sources */, DB63F74727990B0600455B82 /* DataSourceFacade+Hashtag.swift in Sources */, - DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */, DBE3CDCF261C42ED00430CC6 /* TimelineHeaderView.swift in Sources */, - DB6746E7278ED633008A6B94 /* MastodonAuthenticationBox.swift in Sources */, - DBAE3F8E2616E0B1004B8251 /* APIService+Block.swift in Sources */, 62FD27D32893707B00B205C5 /* BookmarkViewController+DataSourceProvider.swift in Sources */, DB1D843426579931000346B3 /* TableViewControllerNavigateable.swift in Sources */, 0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */, DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */, DB5B54B22833C24B00DEF8B2 /* RebloggedByViewController+DataSourceProvider.swift in Sources */, 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */, - DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */, DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */, - DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */, DBBF1DC7265251D400E5B703 /* AutoCompleteViewModel+State.swift in Sources */, DB03A793272A7E5700EE37C5 /* SidebarListHeaderView.swift in Sources */, - DB336F2E278D71AF0031E64B /* Status+Property.swift in Sources */, DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */, - DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */, DB5B7298273112C800081888 /* FollowingListViewModel.swift in Sources */, - DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */, 0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */, - 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */, - 2D9DB967263A76FB007C1D71 /* BlockDomainService.swift in Sources */, DB5B54AE2833C15F00DEF8B2 /* UserListViewModel+Diffable.swift in Sources */, DB336F43278EB1690031E64B /* MediaView+Configuration.swift in Sources */, DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */, @@ -4012,7 +3361,6 @@ DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */, DB63F75A279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift in Sources */, DB023D26279FFB0A005AC798 /* ShareActivityProvider.swift in Sources */, - DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */, 5D0393962612D266007FE196 /* WebViewModel.swift in Sources */, 5B24BBDA262DB14800A9381B /* ReportViewModel.swift in Sources */, 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */, @@ -4028,9 +3376,7 @@ DB443CD42694627B00159B29 /* AppearanceView.swift in Sources */, DBF1D24E269DAF5D00C1C08A /* SearchDetailViewController.swift in Sources */, 62FD27D52893708A00B205C5 /* BookmarkViewModel+Diffable.swift in Sources */, - DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */, DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */, - DB336F36278D77A40031E64B /* PollOption+Property.swift in Sources */, 2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */, DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */, DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */, @@ -4047,12 +3393,10 @@ DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */, DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */, 2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */, - DBA465952696E387002B41DB /* AppPreference.swift in Sources */, 2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */, DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */, DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */, 62FD27D12893707600B205C5 /* BookmarkViewController.swift in Sources */, - DBA5E7A3263AD0A3004598BB /* PhotoLibraryService.swift in Sources */, DBD5B1F627BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */, 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */, DB63F767279A5EB300455B82 /* NotificationTimelineViewModel.swift in Sources */, @@ -4060,13 +3404,10 @@ DB5B54AB2833C12A00DEF8B2 /* RebloggedByViewController.swift in Sources */, DB848E33282B62A800A302CC /* ReportResultView.swift in Sources */, DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */, - DB564BD3269F3B35001E39A7 /* StatusFilterService.swift in Sources */, DB0FCB9C27980AB6006C02E2 /* HashtagTimelineViewController+DataSourceProvider.swift in Sources */, DB63F76F279A7D1100455B82 /* NotificationTableViewCell.swift in Sources */, - DB297B1B2679FAE200704C90 /* PlaceholderImageCacheService.swift in Sources */, DB0FCB8C2796BF8D006C02E2 /* SearchViewModel+Diffable.swift in Sources */, DBEFCD76282A143F00C0ABEA /* ReportStatusViewController.swift in Sources */, - 2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */, DBDFF1952805561700557A48 /* DiscoveryPostsViewModel+Diffable.swift in Sources */, DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */, DBDFF19C28055BD600557A48 /* DiscoveryViewModel.swift in Sources */, @@ -4094,31 +3435,20 @@ DB6B74FE272FF59000C70B6E /* UserItem.swift in Sources */, DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */, DBE3CE07261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift in Sources */, - 2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */, - 5B90C48B26259C120002E742 /* APIService+CoreData+Subscriptions.swift in Sources */, DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */, - DB025B93278D6501002F581E /* Persistence.swift in Sources */, 2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */, DBFEEC9D279C12C1004F81DD /* ProfileFieldEditCollectionViewCell.swift in Sources */, - DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, DB3E6FEC2806D7F100B035AE /* DiscoveryNewsViewController.swift in Sources */, - DBA088DF26958164003EB4B2 /* UserFetchedResultsController.swift in Sources */, DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */, - 0F202213261351F5000C64BF /* APIService+HashtagTimeline.swift in Sources */, - DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */, DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */, DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */, 2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */, 2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */, DB3667A8268AE2900027D07F /* ComposeStatusPollItem.swift in Sources */, - DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */, DB603113279EBEBA00A935FE /* DataSourceFacade+Block.swift in Sources */, - DB336F32278D77330031E64B /* Persistence+Poll.swift in Sources */, DB63F777279A9A2A00455B82 /* NotificationView+Configuration.swift in Sources */, - DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */, DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */, DB0C947726A7FE840088FB11 /* NotificationAvatarButton.swift in Sources */, - DB336F34278D77730031E64B /* Persistence+PollOption.swift in Sources */, 5B90C461262599800002E742 /* SettingsLinkTableViewCell.swift in Sources */, DB6180DD263918E30018D199 /* MediaPreviewViewController.swift in Sources */, DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */, @@ -4145,23 +3475,17 @@ DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */, 2D4AD8A826316D3500613EFC /* SelectedAccountItem.swift in Sources */, DBE3CDFB261C6CA500430CC6 /* FavoriteViewModel.swift in Sources */, - DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */, DBE3CE01261D623D00430CC6 /* FavoriteViewModel+State.swift in Sources */, 2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */, 2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */, - DB67D089273256D7006A36CF /* StoreReviewPreference.swift in Sources */, DB5B7295273112B100081888 /* FollowingListViewController.swift in Sources */, 0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */, DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */, - DB4F097F26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift in Sources */, DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */, - DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */, DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */, DB3EA8EB281B7E0700598866 /* DiscoveryCommunityViewModel+State.swift in Sources */, DB0FCB7227952986006C02E2 /* NamingState.swift in Sources */, - DB73BF47271199CA00781945 /* Instance.swift in Sources */, DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */, - DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */, DB98EB5327B0F9890082E365 /* ReportHeadlineTableViewCell.swift in Sources */, DB5B729C273113C200081888 /* FollowingListViewModel+Diffable.swift in Sources */, DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */, @@ -4174,11 +3498,9 @@ DB63F76227996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift in Sources */, DB73BF4927140BA300781945 /* UICollectionViewDiffableDataSource.swift in Sources */, DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */, - DB3E6FE92806BD2200B035AE /* ThemeService.swift in Sources */, DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */, DB63F7492799126300455B82 /* FollowerListViewController+DataSourceProvider.swift in Sources */, DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */, - DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */, 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */, 2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */, DB66728C25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift in Sources */, @@ -4187,20 +3509,14 @@ 5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */, DB5B54A12833A89600DEF8B2 /* FamiliarFollowersViewController+DataSourceProvider.swift in Sources */, 2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */, - DB73BF43271192BB00781945 /* InstanceService.swift in Sources */, - DB67D08427312970006A36CF /* APIService+Following.swift in Sources */, DB025B78278D606A002F581E /* StatusItem.swift in Sources */, DB697DD4278F4927004EF2F7 /* StatusTableViewCellDelegate.swift in Sources */, - DB0FCB902796C5EB006C02E2 /* APIService+Trend.swift in Sources */, DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */, DB3E6FF52807C40300B035AE /* DiscoveryForYouViewController.swift in Sources */, - DB9D7C21269824B80054B3DF /* APIService+Filter.swift in Sources */, 2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */, DB0FCB842796B2A2006C02E2 /* FavoriteViewController+DataSourceProvider.swift in Sources */, DB0FCB68279507EF006C02E2 /* DataSourceFacade+Meta.swift in Sources */, - DB63F75C279956D000455B82 /* Persistence+Tag.swift in Sources */, 2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */, - DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */, 6213AF5A28939C8400BCADB6 /* BookmarkViewModel.swift in Sources */, 5B24BBDB262DB14800A9381B /* ReportStatusViewModel+Diffable.swift in Sources */, DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */, @@ -4210,13 +3526,11 @@ DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */, 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */, DBDFF1932805554900557A48 /* DiscoveryPostsViewModel.swift in Sources */, - 2D9DB96B263A91D1007C1D71 /* APIService+DomainBlock.swift in Sources */, DBBF1DC92652538500E5B703 /* AutoCompleteSection.swift in Sources */, DB3E6FE72806A7A200B035AE /* DiscoveryItem.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, DBEFCD79282A147000C0ABEA /* ReportStatusViewModel.swift in Sources */, DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */, - DB647C5926F1EA2700F7F82C /* WizardPreference.swift in Sources */, DB0A322E280EE9FD001729D2 /* DiscoveryIntroBannerView.swift in Sources */, 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */, 5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */, @@ -4224,11 +3538,9 @@ 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */, DB0FCB7827957678006C02E2 /* DataSourceProvider+UITableViewDelegate.swift in Sources */, DB4932B126F1FB5300EF46D4 /* WizardCardView.swift in Sources */, - DB6D9F76263587C7008423CD /* SettingFetchedResultController.swift in Sources */, DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */, 5D0393902612D259007FE196 /* WebViewController.swift in Sources */, DB98EB6227B215EB0082E365 /* ReportResultViewController.swift in Sources */, - DB6B74FA272FC2B500C70B6E /* APIService+Follower.swift in Sources */, DB6B74F4272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift in Sources */, DB6B74F2272FB67600C70B6E /* FollowerListViewModel.swift in Sources */, DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */, @@ -4239,19 +3551,13 @@ 5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */, DB3E6FEF2806D82600B035AE /* DiscoveryNewsViewModel.swift in Sources */, DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */, - 2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */, DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */, - DB025B95278D6530002F581E /* Persistence+MastodonUser.swift in Sources */, DB3667A6268AE2620027D07F /* ComposeStatusPollSection.swift in Sources */, DB6B750427300B4000C70B6E /* TimelineFooterTableViewCell.swift in Sources */, DB98EB4C27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift in Sources */, - DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */, DB852D1F26FB037800FC9D81 /* SidebarViewModel.swift in Sources */, DB63F769279A5EBB00455B82 /* NotificationTimelineViewModel+Diffable.swift in Sources */, - DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */, - DB63F75E27995B3B00455B82 /* Tag+Property.swift in Sources */, DBFEEC9B279BDDD9004F81DD /* ProfileAboutViewModel+Diffable.swift in Sources */, - 2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */, DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */, DB0618012785732C0030EE79 /* ServerRulesTableViewCell.swift in Sources */, DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */, @@ -4262,7 +3568,6 @@ DB3667A1268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift in Sources */, DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */, DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */, - DB336F28278D6EC70031E64B /* MastodonFieldContainer.swift in Sources */, DBCA0EBC282BB38A0029E2B0 /* PageboyNavigateable.swift in Sources */, DBF156E42702DB3F00EC00B7 /* HandleTapAction.swift in Sources */, DB98EB4727B0DFAA0082E365 /* ReportStatusViewModel+State.swift in Sources */, @@ -4270,8 +3575,6 @@ DB6B74F6272FBCDB00C70B6E /* FollowerListViewModel+State.swift in Sources */, DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */, DBDFF197280556D900557A48 /* DiscoveryPostsViewModel+State.swift in Sources */, - DB336F2C278D6FC30031E64B /* Persistence+Status.swift in Sources */, - DB336F2A278D6F2B0031E64B /* MastodonField.swift in Sources */, DB0FCB7A279576A2006C02E2 /* DataSourceFacade+Thread.swift in Sources */, DB9F58EF26EF491E00E7BBE9 /* AccountListViewModel.swift in Sources */, DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */, @@ -4289,8 +3592,6 @@ DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */, DB7274F4273BB9B200577D95 /* ListBatchFetchViewModel.swift in Sources */, DB0618052785A73D0030EE79 /* RegisterItem.swift in Sources */, - 2D61254D262547C200299647 /* APIService+Notification.swift in Sources */, - DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */, DB73BF4B27140C0800781945 /* UITableViewDiffableDataSource.swift in Sources */, DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */, DB3EA8EF281B837000598866 /* DiscoveryCommunityViewController+DataSourceProvider.swift in Sources */, @@ -4302,7 +3603,6 @@ DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */, DB6988DE2848D11C002398EF /* PagerTabStripNavigateable.swift in Sources */, DBBC24AC26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */, - DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */, 2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */, DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */, DB697DD1278F4871004EF2F7 /* AutoGenerateTableViewDelegate.swift in Sources */, @@ -4319,12 +3619,10 @@ DBBF1DC5265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift in Sources */, DB603111279EB38500A935FE /* DataSourceFacade+Mute.swift in Sources */, DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */, - DB336F38278D7AAF0031E64B /* Poll+Property.swift in Sources */, 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */, 6213AF5C28939C8A00BCADB6 /* BookmarkViewModel+State.swift in Sources */, DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */, 2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */, - DBA465932696B495002B41DB /* APIService+WebFinger.swift in Sources */, DBEFCD7B282A162400C0ABEA /* ReportReasonView.swift in Sources */, DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */, DB63F77B279ACAE500455B82 /* DataSourceFacade+Favorite.swift in Sources */, @@ -4344,36 +3642,27 @@ DBEFCD71282A12B200C0ABEA /* ReportReasonViewController.swift in Sources */, DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, DB98EB5627B0FF1B0082E365 /* ReportViewControllerAppearance.swift in Sources */, - DB938F1526241FDF00E5B6C1 /* APIService+Thread.swift in Sources */, DB3EA8E6281B79E200598866 /* DiscoveryCommunityViewController.swift in Sources */, 2D206B8625F5FB0900143C56 /* Double.swift in Sources */, DB9F58F126EF512300E7BBE9 /* AccountListTableViewCell.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */, - DB6D9F6326357848008423CD /* SettingService.swift in Sources */, 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */, DBA5A53526F0A36A00CACBAA /* AddAccountTableViewCell.swift in Sources */, 2DB72C8C262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift in Sources */, 2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */, - DB084B5725CBC56C00F898ED /* Status.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 */, - DBD376AC2692ECDB007FEC24 /* ThemePreference.swift in Sources */, DB4F097D26A03A5B00D62E92 /* SearchHistoryItem.swift in Sources */, DBD5B1FA27BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift in Sources */, DB5B549A2833A60400DEF8B2 /* FamiliarFollowersViewController.swift in Sources */, DB3E6FE22806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift in Sources */, - DB68046C2636DC9E00430867 /* MastodonPushNotification.swift in Sources */, - DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */, DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */, - DB6D9F57263577D2008423CD /* APIService+CoreData+Setting.swift in Sources */, DB0FCB822796AC78006C02E2 /* UserTimelineViewController+DataSourceProvider.swift in Sources */, - DB63F773279A87DC00455B82 /* Notification+Property.swift in Sources */, - DBCBCC0D2680B908000F5B51 /* HomeTimelinePreference.swift in Sources */, DB0EF72E26FDB24F00347686 /* SidebarListContentView.swift in Sources */, DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */, 2D7867192625B77500211898 /* NotificationItem.swift in Sources */, @@ -4382,36 +3671,20 @@ DBEFCD74282A130400C0ABEA /* ReportReasonViewModel.swift in Sources */, 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */, DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */, - DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */, - DBFEF07B26A6BCE8006D7ED1 /* APIService+Status+Publish.swift in Sources */, DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */, - 5B90C48526259BF10002E742 /* APIService+Subscriptions.swift in Sources */, DBFEEC96279BDC67004F81DD /* ProfileAboutViewController.swift in Sources */, DB63F74F2799405600455B82 /* SearchHistoryViewModel+Diffable.swift in Sources */, - DB336F23278D6DED0031E64B /* MastodonEmojiContainer.swift in Sources */, - 0F20223926146553000C64BF /* Array.swift in Sources */, DB0EF72B26FDB1D200347686 /* SidebarListCollectionViewCell.swift in Sources */, 5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */, DB63F74D27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift in Sources */, DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */, - DB73BF45271195AC00781945 /* APIService+CoreData+Instance.swift in Sources */, - DB336F21278D6D960031E64B /* MastodonEmoji.swift in Sources */, DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */, - DB3EA8ED281B810100598866 /* APIService+PublicTimeline.swift in Sources */, DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */, - DB025B97278D66D5002F581E /* MastodonUser+Property.swift in Sources */, - DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */, - DB0FCB6C27950E29006C02E2 /* MastodonMentionContainer.swift in Sources */, - DB6D9F502635761F008423CD /* SubscriptionAlerts.swift in Sources */, 0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */, DB7A9F932818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift in Sources */, - DBE54AC62636C89F004E7C0B /* NotificationPreference.swift in Sources */, 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */, - DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */, DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */, - 2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */, DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */, - DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */, DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */, 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */, DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */, @@ -4424,7 +3697,6 @@ DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */, DB98EB6B27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift in Sources */, DBB45B5B27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift in Sources */, - DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */, DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */, DB4932B926F31AD300EF46D4 /* BadgeButton.swift in Sources */, 0F2021FB2613262F000C64BF /* HashtagTimelineViewController.swift in Sources */, @@ -4437,10 +3709,8 @@ DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */, DBB45B5627B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift in Sources */, DB0FCB8027968F70006C02E2 /* MastodonStatusThreadViewModel.swift in Sources */, - DB0FCB6E27950E6B006C02E2 /* MastodonMention.swift in Sources */, DB67D08627312E67006A36CF /* WizardViewController.swift in Sources */, DB6746EB278ED8B0008A6B94 /* PollOptionView+Configuration.swift in Sources */, - DB9A489026035963008B817C /* APIService+Media.swift in Sources */, DB0F9D54283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift in Sources */, DBFEEC99279BDCDE004F81DD /* ProfileAboutViewModel.swift in Sources */, 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */, @@ -4451,15 +3721,11 @@ DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */, DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */, DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */, - DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */, - DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */, - DB6D9F6F2635807F008423CD /* Setting.swift in Sources */, DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */, DB6746F0278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift in Sources */, DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */, DBDFF19A28055A1400557A48 /* DiscoveryViewController.swift in Sources */, DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */, - DB63F756279949BD00455B82 /* Persistence+SearchHistory.swift in Sources */, 2D4AD8A226316CD200613EFC /* SelectedAccountSection.swift in Sources */, DB3EA8F1281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift in Sources */, DB63F775279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift in Sources */, @@ -4473,14 +3739,12 @@ DB98EB4927B0F0CD0082E365 /* ReportStatusTableViewCell.swift in Sources */, DB3667A4268AE2370027D07F /* ComposeStatusPollTableViewCell.swift in Sources */, DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */, - 5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */, DBF3B7412733EB9400E21627 /* MastodonLocalCode.swift in Sources */, DB98EB6527B216500082E365 /* ReportResultViewModel.swift in Sources */, DB4F096A269EDAD200D62E92 /* SearchResultViewModel+State.swift in Sources */, 5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */, DBEFCD82282A2AB100C0ABEA /* ReportServerRulesView.swift in Sources */, DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */, - 6213AF5828939C4800BCADB6 /* APIService+Bookmark.swift in Sources */, DB023D2A27A0FE5C005AC798 /* DataSourceProvider+NotificationTableViewCellDelegate.swift in Sources */, DB98EB6027B10E150082E365 /* ReportCommentTableViewCell.swift in Sources */, DB0FCB962797E6C2006C02E2 /* SearchResultViewController+DataSourceProvider.swift in Sources */, @@ -4489,16 +3753,10 @@ DBD5B1F827BCFD9D00BD6B38 /* DataSourceProvider+TableViewControllerNavigateable.swift in Sources */, 0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */, DB0FCB882796BDA9006C02E2 /* SearchItem.swift in Sources */, - DB336F3D278D80040031E64B /* FeedFetchedResultsController.swift in Sources */, - DB6D9F4926353FD7008423CD /* Subscription.swift in Sources */, - DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */, DB6180ED26391C6C0018D199 /* TransitioningMath.swift in Sources */, - DB63F771279A858500455B82 /* Persistence+Notification.swift in Sources */, 2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */, - DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */, DB0617FD27855BFE0030EE79 /* ServerRuleItem.swift in Sources */, 5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */, - DBAE3F942616E28B004B8251 /* APIService+Follow.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4519,28 +3777,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DB68047B2637CD4C00430867 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - DB73BF3B2711885500781945 /* UserDefaults+Notification.swift in Sources */, - DB4932B726F30F0700EF46D4 /* Array.swift in Sources */, - DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; DB8FABC226AEC7B2008E5AF4 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( DB0009A726AEE5DC009B9D2D /* Intents.intentdefinition in Sources */, DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */, - DBB8AB4A26AED0B500F6D281 /* APIService.swift in Sources */, - DBB8AB4C26AED11300F6D281 /* APIService+APIError.swift in Sources */, DB64BA452851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift in Sources */, DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */, - DB6746E9278ED63F008A6B94 /* MastodonAuthenticationBox.swift in Sources */, - DBB8AB5226AED1B300F6D281 /* APIService+Status+Publish.swift in Sources */, DB8FABCA26AEC7B2008E5AF4 /* IntentHandler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4550,20 +3794,13 @@ buildActionMask = 2147483647; files = ( DBFEF05E26A57715006D7ED1 /* ComposeView.swift in Sources */, - DBFEF07326A6913D006D7ED1 /* APIService.swift in Sources */, DBFEF06026A57715006D7ED1 /* StatusAttachmentView.swift in Sources */, DBC6462326A1712000B0E31B /* ShareViewModel.swift in Sources */, DBFEF06D26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift in Sources */, - DBBC24CB26A546C000398BB9 /* ThemePreference.swift in Sources */, DBFEF05F26A57715006D7ED1 /* StatusAuthorView.swift in Sources */, - DB336F1C278D697E0031E64B /* MastodonUser.swift in Sources */, DBFEF05D26A57715006D7ED1 /* ContentWarningEditorView.swift in Sources */, - DBFEF07526A69192006D7ED1 /* APIService+Media.swift in Sources */, - DBFEF06F26A690C4006D7ED1 /* APIService+APIError.swift in Sources */, DBFEF05C26A57715006D7ED1 /* StatusEditorView.swift in Sources */, - DBFEF07C26A6BD0A006D7ED1 /* APIService+Status+Publish.swift in Sources */, DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */, - DB6746E8278ED639008A6B94 /* MastodonAuthenticationBox.swift in Sources */, DBBC24A826A52F9000398BB9 /* ComposeToolbarView.swift in Sources */, DBFEF05B26A57715006D7ED1 /* ComposeViewModel.swift in Sources */, DBFEF06326A577F2006D7ED1 /* StatusAttachmentViewModel.swift in Sources */, @@ -4575,8 +3812,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DBE54ACC2636C8FD004E7C0B /* NotificationPreference.swift in Sources */, - DB68045B2636DC6A00430867 /* MastodonPushNotification.swift in Sources */, DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */, DB6804662636DC9000430867 /* String.swift in Sources */, DBCBCBF4267CB070000F5B51 /* Decode85.swift in Sources */, @@ -4597,36 +3832,16 @@ target = DB427DD125BAA00100D1B89D /* Mastodon */; targetProxy = DB427DF425BAA00100D1B89D /* PBXContainerItemProxy */; }; - DB6804852637CD4C00430867 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DB68047E2637CD4C00430867 /* AppShared */; - targetProxy = DB6804842637CD4C00430867 /* PBXContainerItemProxy */; - }; - DB6804A82637CDCC00430867 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DB68047E2637CD4C00430867 /* AppShared */; - targetProxy = DB6804A72637CDCC00430867 /* PBXContainerItemProxy */; - }; DB8FABCD26AEC7B2008E5AF4 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DB8FABC526AEC7B2008E5AF4 /* MastodonIntent */; targetProxy = DB8FABCC26AEC7B2008E5AF4 /* PBXContainerItemProxy */; }; - DB8FABDA26AEC873008E5AF4 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DB68047E2637CD4C00430867 /* AppShared */; - targetProxy = DB8FABD926AEC873008E5AF4 /* PBXContainerItemProxy */; - }; DBC6461B26A170AB00B0E31B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DBC6461126A170AB00B0E31B /* ShareActionExtension */; targetProxy = DBC6461A26A170AB00B0E31B /* PBXContainerItemProxy */; }; - DBC6463626A195DB00B0E31B /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DB68047E2637CD4C00430867 /* AppShared */; - targetProxy = DBC6463526A195DB00B0E31B /* PBXContainerItemProxy */; - }; DBF8AE19263293E400C9C23C /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DBF8AE12263293E400C9C23C /* NotificationService */; @@ -5017,67 +4232,6 @@ }; name = Release; }; - DB6804892637CD4C00430867 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9A0982D8F349244EB558CDFD /* Pods-AppShared.debug.xcconfig */; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 144; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = AppShared/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.AppShared; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - DB68048A2637CD4C00430867 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = ECA373ABA86BE3C2D7ED878E /* Pods-AppShared.release.xcconfig */; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 144; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = AppShared/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.AppShared; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; DB848E2A282B5E6300A302CC /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { @@ -5211,37 +4365,6 @@ }; name = Profile; }; - DB848E2E282B5E6300A302CC /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 63EF9E6E5B575CD2A8B0475D /* Pods-AppShared.profile.xcconfig */; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 144; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = AppShared/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.AppShared; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Profile; - }; DB848E2F282B5E6300A302CC /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { @@ -5538,36 +4661,6 @@ }; name = "Release Snapshot"; }; - DBEB19E527E4658E00B0E80E /* Release Snapshot */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3E08A432F40BA7B9CAA9DB68 /* Pods-AppShared.release snapshot.xcconfig */; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 144; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = AppShared/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.AppShared; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = "Release Snapshot"; - }; DBEB19E627E4658E00B0E80E /* Release Snapshot */ = { isa = XCBuildConfiguration; buildSettings = { @@ -5735,17 +4828,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - DB6804882637CD4C00430867 /* Build configuration list for PBXNativeTarget "AppShared" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - DB6804892637CD4C00430867 /* Debug */, - DB848E2E282B5E6300A302CC /* Profile */, - DB68048A2637CD4C00430867 /* Release */, - DBEB19E527E4658E00B0E80E /* Release Snapshot */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; DB8FABCF26AEC7B2008E5AF4 /* Build configuration list for PBXNativeTarget "MastodonIntent" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -5781,269 +4863,22 @@ }; /* End XCConfigurationList section */ -/* Begin XCRemoteSwiftPackageReference section */ - 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/vtourraine/ThirdPartyMailer.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.7.1; - }; - }; - 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Alamofire/AlamofireNetworkActivityIndicator"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 3.1.0; - }; - }; - 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/TimOliver/TOCropViewController.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.6.0; - }; - }; - DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/MainasuK/CommonOSLog"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.1.1; - }; - }; - DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 8.0.0; - }; - }; - DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Alamofire/AlamofireImage.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 4.1.0; - }; - }; - DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Alamofire/Alamofire.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 5.4.0; - }; - }; - DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/TwidereProject/TabBarPager.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.1.0; - }; - }; - DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/apple/swift-collections.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.0.5; - }; - }; - DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 4.2.2; - }; - }; - DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/siteline/SwiftUI-Introspect.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.1.4; - }; - }; - DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/slackhq/PanModal.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.2.7; - }; - }; - DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/ra1028/DifferenceKit.git"; - requirement = { - kind = exactVersion; - version = 1.2.0; - }; - }; - DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/MainasuK/DiffableDataSources.git"; - requirement = { - branch = "feature/async-display-table"; - kind = branch; - }; - }; - DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/cezheng/Fuzi.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 3.1.3; - }; - }; - DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/uias/Tabman"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.11.0; - }; - }; - DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/MainasuK/FPSIndicator.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - /* Begin XCSwiftPackageProductDependency section */ - 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */ = { - isa = XCSwiftPackageProductDependency; - package = 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */; - productName = ThirdPartyMailer; - }; - 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */ = { - isa = XCSwiftPackageProductDependency; - package = 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */; - productName = AlamofireNetworkActivityIndicator; - }; - 2D939AC725EE14620076FA61 /* CropViewController */ = { - isa = XCSwiftPackageProductDependency; - package = 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */; - productName = CropViewController; - }; - DB02EA0A280D180D00E751C5 /* KeychainAccess */ = { - isa = XCSwiftPackageProductDependency; - package = DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */; - productName = KeychainAccess; - }; - DB3EA8F4281BB65200598866 /* MastodonSDK */ = { + DB22C92128E700A10082A9E9 /* MastodonSDK */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDK; }; - DB3EA8FB281BBAE100598866 /* AlamofireImage */ = { + DB22C92328E700A80082A9E9 /* MastodonSDK */ = { isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; - productName = AlamofireImage; + productName = MastodonSDK; }; - DB3EA8FD281BBAF200598866 /* Alamofire */ = { + DB22C92528E700AF0082A9E9 /* MastodonSDK */ = { isa = XCSwiftPackageProductDependency; - package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */; - productName = Alamofire; + productName = MastodonSDK; }; - DB3EA901281BBD5D00598866 /* CommonOSLog */ = { + DB22C92728E700B70082A9E9 /* MastodonSDK */ = { isa = XCSwiftPackageProductDependency; - package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; - productName = CommonOSLog; - }; - DB3EA903281BBD9400598866 /* Introspect */ = { - isa = XCSwiftPackageProductDependency; - package = DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; - productName = Introspect; - }; - DB3EA905281BBE8200598866 /* AlamofireImage */ = { - isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; - productName = AlamofireImage; - }; - DB3EA907281BBE8200598866 /* AlamofireNetworkActivityIndicator */ = { - isa = XCSwiftPackageProductDependency; - package = 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */; - productName = AlamofireNetworkActivityIndicator; - }; - DB3EA909281BBE8200598866 /* Alamofire */ = { - isa = XCSwiftPackageProductDependency; - package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */; - productName = Alamofire; - }; - DB3EA90B281BBE9600598866 /* AlamofireImage */ = { - isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; - productName = AlamofireImage; - }; - DB3EA90D281BBE9600598866 /* AlamofireNetworkActivityIndicator */ = { - isa = XCSwiftPackageProductDependency; - package = 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */; - productName = AlamofireNetworkActivityIndicator; - }; - DB3EA90F281BBE9600598866 /* Alamofire */ = { - isa = XCSwiftPackageProductDependency; - package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */; - productName = Alamofire; - }; - DB3EA911281BBEA800598866 /* AlamofireImage */ = { - isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; - productName = AlamofireImage; - }; - DB3EA913281BBEA800598866 /* Alamofire */ = { - isa = XCSwiftPackageProductDependency; - package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */; - productName = Alamofire; - }; - DB486C0E282E41F200F69423 /* TabBarPager */ = { - isa = XCSwiftPackageProductDependency; - package = DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */; - productName = TabBarPager; - }; - DB552D4E26BBD10C00E481F6 /* OrderedCollections */ = { - isa = XCSwiftPackageProductDependency; - package = DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */; - productName = OrderedCollections; - }; - DBA5A52E26F07ED800CACBAA /* PanModal */ = { - isa = XCSwiftPackageProductDependency; - package = DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */; - productName = PanModal; - }; - DBAC6482267D0B21007FE9FD /* DifferenceKit */ = { - isa = XCSwiftPackageProductDependency; - package = DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */; - productName = DifferenceKit; - }; - DBAC649D267DFE43007FE9FD /* DiffableDataSources */ = { - isa = XCSwiftPackageProductDependency; - package = DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */; - productName = DiffableDataSources; - }; - DBAC64A0267E6D02007FE9FD /* Fuzi */ = { - isa = XCSwiftPackageProductDependency; - package = DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */; - productName = Fuzi; - }; - DBB525072611EAC0002F1F29 /* Tabman */ = { - isa = XCSwiftPackageProductDependency; - package = DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */; - productName = Tabman; - }; - DBF7A0FB26830C33004176A2 /* FPSIndicator */ = { - isa = XCSwiftPackageProductDependency; - package = DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */; - productName = FPSIndicator; + productName = MastodonSDK; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 0a6b68822..498c362d6 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -4,13 +4,6 @@ SchemeUserState - AppShared.xcscheme_^#shared#^_ - - isShown - - orderHint - 6 - CoreDataStack.xcscheme_^#shared#^_ orderHint @@ -19,32 +12,27 @@ Mastodon - Profile.xcscheme_^#shared#^_ orderHint - 2 + 1 Mastodon - RTL.xcscheme_^#shared#^_ orderHint - 7 + 5 Mastodon - Release.xcscheme_^#shared#^_ orderHint - 3 + 2 Mastodon - Snapshot.xcscheme_^#shared#^_ orderHint - 4 - - Mastodon - ar.xcscheme - - orderHint - 5 + 3 Mastodon - ar.xcscheme_^#shared#^_ orderHint - 11 + 4 Mastodon - ca.xcscheme_^#shared#^_ @@ -111,11 +99,6 @@ orderHint 0 - MastodonIntent.xcscheme_^#shared#^_ - - orderHint - 22 - MastodonIntents.xcscheme_^#shared#^_ orderHint @@ -129,12 +112,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 23 + 7 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 24 + 6 SuppressBuildableAutocreation @@ -164,6 +147,11 @@ primary + DB8FABC526AEC7B2008E5AF4 + + primary + + diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 29c81554a..ebcacb501 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -19,15 +19,6 @@ "version": "4.2.0" } }, - { - "package": "AlamofireNetworkActivityIndicator", - "repositoryURL": "https://github.com/Alamofire/AlamofireNetworkActivityIndicator", - "state": { - "branch": null, - "revision": "392bed083e8d193aca16bfa684ee24e4bcff0510", - "version": "3.1.0" - } - }, { "package": "CommonOSLog", "repositoryURL": "https://github.com/MainasuK/CommonOSLog", @@ -37,24 +28,6 @@ "version": "0.1.1" } }, - { - "package": "DiffableDataSources", - "repositoryURL": "https://github.com/MainasuK/DiffableDataSources.git", - "state": { - "branch": "feature/async-display-table", - "revision": "73393a97690959d24387c95594c045c62d9c47cf", - "version": null - } - }, - { - "package": "DifferenceKit", - "repositoryURL": "https://github.com/ra1028/DifferenceKit.git", - "state": { - "branch": null, - "revision": "62745d7780deef4a023a792a1f8f763ec7bf9705", - "version": "1.2.0" - } - }, { "package": "FaviconFinder", "repositoryURL": "https://github.com/will-lumley/FaviconFinder.git", @@ -159,8 +132,8 @@ "repositoryURL": "https://github.com/apple/swift-collections.git", "state": { "branch": null, - "revision": "9d8719c8bebdc79740b6969c912ac706eb721d7a", - "version": "0.0.7" + "revision": "f504716c27d2e5d4144fa4794b12129301d17729", + "version": "1.0.3" } }, { @@ -222,8 +195,8 @@ "repositoryURL": "https://github.com/uias/Tabman", "state": { "branch": null, - "revision": "a9f10cb862a32e6a22549836af013abd6b0692d3", - "version": "2.12.0" + "revision": "4a4f7c755b875ffd4f9ef10d67a67883669d2465", + "version": "2.13.0" } }, { @@ -231,8 +204,8 @@ "repositoryURL": "https://github.com/vtourraine/ThirdPartyMailer.git", "state": { "branch": null, - "revision": "779da6ce0793b461ccbbac2804755c1e29b6fa63", - "version": "1.8.0" + "revision": "44c1cfaa6969963f22691aa67f88a69e3b6d651f", + "version": "2.1.0" } }, { @@ -244,6 +217,15 @@ "version": "2.6.1" } }, + { + "package": "UIHostingConfigurationBackport", + "repositoryURL": "https://github.com/woxtu/UIHostingConfigurationBackport.git", + "state": { + "branch": null, + "revision": "6091f2d38faa4b24fc2ca0389c651e2f666624a3", + "version": "0.1.0" + } + }, { "package": "UITextView+Placeholder", "repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git", diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 82c58e1f6..bdfa727a0 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -8,8 +8,9 @@ import UIKit import Combine import SafariServices import CoreDataStack -import MastodonSDK import PanModal +import MastodonSDK +import MastodonCore import MastodonAsset import MastodonLocalization diff --git a/Mastodon/Diffiable/Compose/AutoCompleteSection.swift b/Mastodon/Diffiable/Compose/AutoCompleteSection.swift index 1a2bf45f0..1260f398b 100644 --- a/Mastodon/Diffiable/Compose/AutoCompleteSection.swift +++ b/Mastodon/Diffiable/Compose/AutoCompleteSection.swift @@ -10,6 +10,7 @@ import MastodonSDK import MastodonMeta import MastodonAsset import MastodonLocalization +import MastodonCore enum AutoCompleteSection: Equatable, Hashable { case main diff --git a/Mastodon/Extension/CoreDataStack/MastodonUser.swift b/Mastodon/Extension/CoreDataStack/MastodonUser.swift deleted file mode 100644 index bc5f159d9..000000000 --- a/Mastodon/Extension/CoreDataStack/MastodonUser.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// MastodonUser.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021/2/3. -// - -import Foundation -import CoreDataStack -import MastodonSDK - -extension MastodonUser { - - public var profileURL: URL { - if let urlString = self.url, - let url = URL(string: urlString) { - return url - } else { - return URL(string: "https://\(self.domain)/@\(username)")! - } - } - - public var activityItems: [Any] { - var items: [Any] = [] - items.append(profileURL) - return items - } -} diff --git a/Mastodon/Helper/MastodonAuthenticationBox.swift b/Mastodon/Helper/MastodonAuthenticationBox.swift deleted file mode 100644 index 31c9649c6..000000000 --- a/Mastodon/Helper/MastodonAuthenticationBox.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// MastodonAuthenticationBox.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-7-20. -// - -import Foundation -import CoreDataStack -import MastodonSDK -import MastodonUI - -struct MastodonAuthenticationBox: UserIdentifier { - let authenticationRecord: ManagedObjectRecord - let domain: String - let userID: MastodonUser.ID - let appAuthorization: Mastodon.API.OAuth.Authorization - let userAuthorization: Mastodon.API.OAuth.Authorization -} diff --git a/Mastodon/Persistence/Extension/MastodonEmoji.swift b/Mastodon/Persistence/Extension/MastodonEmoji.swift deleted file mode 100644 index 2ea23c67c..000000000 --- a/Mastodon/Persistence/Extension/MastodonEmoji.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// MastodonEmojis.swift -// MastodonEmojis -// -// Created by Cirno MainasuK on 2021-9-2. -// Copyright © 2021 Twidere. All rights reserved. -// - -import Foundation -import CoreDataStack -import MastodonSDK -import MastodonMeta - -extension MastodonEmoji { - public convenience init(emoji: Mastodon.Entity.Emoji) { - self.init( - code: emoji.shortcode, - url: emoji.url, - staticURL: emoji.staticURL, - visibleInPicker: emoji.visibleInPicker, - category: emoji.category - ) - } -} diff --git a/Mastodon/Preference/HomeTimelinePreference.swift b/Mastodon/Preference/HomeTimelinePreference.swift deleted file mode 100644 index 123692db5..000000000 --- a/Mastodon/Preference/HomeTimelinePreference.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// HomeTimelinePreference.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-21. -// - -import UIKit - -extension UserDefaults { - - @objc dynamic var preferAsyncHomeTimeline: Bool { - get { - register(defaults: [#function: false]) - return bool(forKey: #function) - } - set { self[#function] = newValue } - } - -} diff --git a/Mastodon/Preference/NotificationPreference.swift b/Mastodon/Preference/NotificationPreference.swift deleted file mode 100644 index 63092d56d..000000000 --- a/Mastodon/Preference/NotificationPreference.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// NotificationPreference.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-4-26. -// - -import UIKit -import MastodonExtension - -extension UserDefaults { - - @objc dynamic var notificationBadgeCount: Int { - get { - register(defaults: [#function: 0]) - return integer(forKey: #function) - } - set { self[#function] = newValue } - } - -} diff --git a/Mastodon/Preference/ThemePreference.swift b/Mastodon/Preference/ThemePreference.swift deleted file mode 100644 index 5465cb22f..000000000 --- a/Mastodon/Preference/ThemePreference.swift +++ /dev/null @@ -1,7 +0,0 @@ -// -// ThemePreference.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-7-5. -// - diff --git a/Mastodon/Scene/Account/AccountListViewModel.swift b/Mastodon/Scene/Account/AccountListViewModel.swift index 83d0240f8..3149b201d 100644 --- a/Mastodon/Scene/Account/AccountListViewModel.swift +++ b/Mastodon/Scene/Account/AccountListViewModel.swift @@ -11,6 +11,8 @@ import CoreData import CoreDataStack import MastodonSDK import MastodonMeta +import MastodonCore +import MastodonUI final class AccountListViewModel { diff --git a/Mastodon/Scene/Account/AccountViewController.swift b/Mastodon/Scene/Account/AccountViewController.swift index 20d7b26a1..6a97c6427 100644 --- a/Mastodon/Scene/Account/AccountViewController.swift +++ b/Mastodon/Scene/Account/AccountViewController.swift @@ -12,6 +12,7 @@ import CoreDataStack import PanModal import MastodonAsset import MastodonLocalization +import MastodonCore final class AccountListViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift b/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift index c641434e6..3ff3066a2 100644 --- a/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift +++ b/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift @@ -10,6 +10,8 @@ import Combine import MetaTextKit import MastodonAsset import MastodonLocalization +import MastodonCore +import MastodonUI final class AddAccountTableViewCell: UITableViewCell { diff --git a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift b/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift index 632b57b66..29016fafb 100644 --- a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift +++ b/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift @@ -9,6 +9,7 @@ import os.log import Foundation import GameplayKit import MastodonSDK +import MastodonCore extension AutoCompleteViewModel { class State: GKState, NamingState { diff --git a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel.swift b/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel.swift index 3110f93e3..ecc234612 100644 --- a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel.swift +++ b/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel.swift @@ -9,6 +9,7 @@ import UIKit import Combine import GameplayKit import MastodonSDK +import MastodonCore final class AutoCompleteViewModel { @@ -16,13 +17,13 @@ final class AutoCompleteViewModel { // input let context: AppContext - let inputText = CurrentValueSubject("") // contains "@" or "#" prefix - let symbolBoundingRect = CurrentValueSubject(.zero) - let customEmojiViewModel = CurrentValueSubject(nil) + public let inputText = CurrentValueSubject("") // contains "@" or "#" prefix + public let symbolBoundingRect = CurrentValueSubject(.zero) + public let customEmojiViewModel = CurrentValueSubject(nil) // output - var autoCompleteItems = CurrentValueSubject<[AutoCompleteItem], Never>([]) - var diffableDataSource: UITableViewDiffableDataSource! + public var autoCompleteItems = CurrentValueSubject<[AutoCompleteItem], Never>([]) + public var diffableDataSource: UITableViewDiffableDataSource! private(set) lazy var stateMachine: GKStateMachine = { // exclude timeline middle fetcher state let stateMachine = GKStateMachine(states: [ diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentCollectionViewCell.swift index 76f011121..046247507 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentCollectionViewCell.swift @@ -27,7 +27,7 @@ final class ComposeStatusAttachmentCollectionViewCell: UICollectionViewCell { weak var delegate: ComposeStatusAttachmentCollectionViewCellDelegate? - let attachmentContainerView = AttachmentContainerView() +// let attachmentContainerView = AttachmentContainerView() let removeButton: UIButton = { let button = HighlightDimmableButton() button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10) @@ -45,11 +45,11 @@ final class ComposeStatusAttachmentCollectionViewCell: UICollectionViewCell { override func prepareForReuse() { super.prepareForReuse() - attachmentContainerView.activityIndicatorView.startAnimating() - attachmentContainerView.previewImageView.af.cancelImageRequest() - attachmentContainerView.previewImageView.image = .placeholder(color: .systemFill) - delegate = nil - disposeBag.removeAll() +// attachmentContainerView.activityIndicatorView.startAnimating() +// attachmentContainerView.previewImageView.af.cancelImageRequest() +// attachmentContainerView.previewImageView.image = .placeholder(color: .systemFill) +// delegate = nil +// disposeBag.removeAll() } override init(frame: CGRect) { @@ -73,31 +73,30 @@ extension ComposeStatusAttachmentCollectionViewCell { private func _init() { // selectionStyle = .none - attachmentContainerView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(attachmentContainerView) - NSLayoutConstraint.activate([ - attachmentContainerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: ComposeStatusAttachmentCollectionViewCell.verticalMarginHeight), - attachmentContainerView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - attachmentContainerView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: attachmentContainerView.bottomAnchor, constant: ComposeStatusAttachmentCollectionViewCell.verticalMarginHeight), - attachmentContainerView.heightAnchor.constraint(equalToConstant: 205).priority(.defaultHigh), - ]) - - removeButton.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(removeButton) - NSLayoutConstraint.activate([ - removeButton.centerXAnchor.constraint(equalTo: attachmentContainerView.trailingAnchor), - removeButton.centerYAnchor.constraint(equalTo: attachmentContainerView.topAnchor), - removeButton.widthAnchor.constraint(equalToConstant: ComposeStatusAttachmentCollectionViewCell.removeButtonSize.width).priority(.defaultHigh), - removeButton.heightAnchor.constraint(equalToConstant: ComposeStatusAttachmentCollectionViewCell.removeButtonSize.height).priority(.defaultHigh), - ]) - - removeButton.addTarget(self, action: #selector(ComposeStatusAttachmentCollectionViewCell.removeButtonDidPressed(_:)), for: .touchUpInside) +// attachmentContainerView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(attachmentContainerView) +// NSLayoutConstraint.activate([ +// attachmentContainerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: ComposeStatusAttachmentCollectionViewCell.verticalMarginHeight), +// attachmentContainerView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), +// attachmentContainerView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), +// contentView.bottomAnchor.constraint(equalTo: attachmentContainerView.bottomAnchor, constant: ComposeStatusAttachmentCollectionViewCell.verticalMarginHeight), +// attachmentContainerView.heightAnchor.constraint(equalToConstant: 205).priority(.defaultHigh), +// ]) +// +// removeButton.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(removeButton) +// NSLayoutConstraint.activate([ +// removeButton.centerXAnchor.constraint(equalTo: attachmentContainerView.trailingAnchor), +// removeButton.centerYAnchor.constraint(equalTo: attachmentContainerView.topAnchor), +// removeButton.widthAnchor.constraint(equalToConstant: ComposeStatusAttachmentCollectionViewCell.removeButtonSize.width).priority(.defaultHigh), +// removeButton.heightAnchor.constraint(equalToConstant: ComposeStatusAttachmentCollectionViewCell.removeButtonSize.height).priority(.defaultHigh), +// ]) +// +// removeButton.addTarget(self, action: #selector(ComposeStatusAttachmentCollectionViewCell.removeButtonDidPressed(_:)), for: .touchUpInside) } } - extension ComposeStatusAttachmentCollectionViewCell { @objc private func removeButtonDidPressed(_ sender: UIButton) { diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index f5dfc8ba3..8783c8c0e 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -74,17 +74,23 @@ final class ComposeViewController: UIViewController, NeedsDependency { publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal) } - let tableView: ComposeTableView = { - let tableView = ComposeTableView() - tableView.register(ComposeRepliedToStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self)) - tableView.register(ComposeStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusContentTableViewCell.self)) - tableView.register(ComposeStatusAttachmentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusAttachmentTableViewCell.self)) - tableView.alwaysBounceVertical = true - tableView.separatorStyle = .none - tableView.tableFooterView = UIView() - return tableView + let scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.alwaysBounceVertical = true + return scrollView }() +// let tableView: ComposeTableView = { +// let tableView = ComposeTableView() +// tableView.register(ComposeRepliedToStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self)) +// tableView.register(ComposeStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusContentTableViewCell.self)) +// tableView.register(ComposeStatusAttachmentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusAttachmentTableViewCell.self)) +// tableView.alwaysBounceVertical = true +// tableView.separatorStyle = .none +// tableView.tableFooterView = UIView() +// return tableView +// }() + var systemKeyboardHeight: CGFloat = .zero { didSet { // note: some system AutoLayout warning here @@ -202,13 +208,13 @@ extension ComposeViewController { publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) - tableView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(tableView) + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor), - tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + scrollView.topAnchor.constraint(equalTo: view.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) composeToolbarView.translatesAutoresizingMaskIntoConstraints = false @@ -232,318 +238,320 @@ extension ComposeViewController { view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor), ]) - tableView.delegate = self - viewModel.setupDataSource( - tableView: tableView, - metaTextDelegate: self, - metaTextViewDelegate: self, - customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel, - composeStatusAttachmentCollectionViewCellDelegate: self, - composeStatusPollOptionCollectionViewCellDelegate: self, - composeStatusPollOptionAppendEntryCollectionViewCellDelegate: self, - composeStatusPollExpiresOptionCollectionViewCellDelegate: self - ) +// tableView.delegate = self +// viewModel.setupDataSource( +// tableView: tableView, +// metaTextDelegate: self, +// metaTextViewDelegate: self, +// customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel, +// composeStatusAttachmentCollectionViewCellDelegate: self, +// composeStatusPollOptionCollectionViewCellDelegate: self, +// composeStatusPollOptionAppendEntryCollectionViewCellDelegate: self, +// composeStatusPollExpiresOptionCollectionViewCellDelegate: self +// ) - 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.endUpdates() - } - } - .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) - customEmojiPickerInputView.collectionView.delegate = self - viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView - viewModel.setupCustomEmojiPickerDiffableDataSource( - for: customEmojiPickerInputView.collectionView, - dependency: self - ) +// 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 - view.layoutIfNeeded() - - 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( - keyboardEventPublishers, - viewModel.$isCustomEmojiComposing, - viewModel.$autoCompleteInfo - ) - .sink(receiveValue: { [weak self] keyboardEvents, isCustomEmojiComposing, 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 = self.composeToolbarView.frame.height - if autoCompleteInfo != nil { - margin += ComposeViewController.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 - } - - UIView.animate(withDuration: 0.3) { - self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom - if self.view.window != nil { - self.view.layoutIfNeeded() - } - } - 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 - - // adjust inset for tableView - let contentFrame = self.view.convert(self.tableView.frame, to: nil) - let padding = contentFrame.maxY + extraMargin - endFrame.minY - guard padding > 0 else { - self.tableView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin - self.tableView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin - return - } - - self.tableView.contentInset.bottom = padding - self.view.safeAreaInsets.bottom - self.tableView.verticalScrollIndicatorInsets.bottom = padding - self.view.safeAreaInsets.bottom - UIView.animate(withDuration: 0.3) { - self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height - self.view.layoutIfNeeded() - } - }) - .store(in: &disposeBag) - - // 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) - .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 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) - - // 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) - - // 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) - - // setup snap behavior - Publishers.CombineLatest( - viewModel.$repliedToCellFrame, - viewModel.$collectionViewState - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] repliedToCellFrame, collectionViewState in - guard let self = self else { return } - guard repliedToCellFrame != .zero else { return } - switch collectionViewState { - case .fold: - self.tableView.contentInset.top = -repliedToCellFrame.height - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: set contentInset.top: -%s", ((#file as NSString).lastPathComponent), #line, #function, repliedToCellFrame.height.description) - - case .expand: - self.tableView.contentInset.top = 0 - } - } - .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) +// viewModel.composeStatusContentTableViewCell.delegate = self +// +// // update layout when keyboard show/dismiss +// view.layoutIfNeeded() +// +// 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( +// keyboardEventPublishers, +// viewModel.$isCustomEmojiComposing, +// viewModel.$autoCompleteInfo +// ) +// .sink(receiveValue: { [weak self] keyboardEvents, isCustomEmojiComposing, 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 = self.composeToolbarView.frame.height +// if autoCompleteInfo != nil { +// margin += ComposeViewController.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 +// } +// +// UIView.animate(withDuration: 0.3) { +// self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom +// if self.view.window != nil { +// self.view.layoutIfNeeded() +// } +// } +// 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 +// +// // adjust inset for tableView +// let contentFrame = self.view.convert(self.tableView.frame, to: nil) +// let padding = contentFrame.maxY + extraMargin - endFrame.minY +// guard padding > 0 else { +// self.tableView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin +// self.tableView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin +// return +// } +// +// self.tableView.contentInset.bottom = padding - self.view.safeAreaInsets.bottom +// self.tableView.verticalScrollIndicatorInsets.bottom = padding - self.view.safeAreaInsets.bottom +// UIView.animate(withDuration: 0.3) { +// self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height +// self.view.layoutIfNeeded() +// } +// }) +// .store(in: &disposeBag) +// +// // 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) +// .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 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) +// +// // 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) +// +// // 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) +// +// // setup snap behavior +// Publishers.CombineLatest( +// viewModel.$repliedToCellFrame, +// viewModel.$collectionViewState +// ) +// .receive(on: DispatchQueue.main) +// .sink { [weak self] repliedToCellFrame, collectionViewState in +// guard let self = self else { return } +// guard repliedToCellFrame != .zero else { return } +// switch collectionViewState { +// case .fold: +// self.tableView.contentInset.top = -repliedToCellFrame.height +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: set contentInset.top: -%s", ((#file as NSString).lastPathComponent), #line, #function, repliedToCellFrame.height.description) +// +// case .expand: +// self.tableView.contentInset.top = 0 +// } +// } +// .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) +// // update MetaText without trigger call underlaying `UITextStorage.processEditing` +// _ = textEditorView.processEditing(textEditorView.textStorage) - markTextEditorViewBecomeFirstResponser() +// markTextEditorViewBecomeFirstResponser() } override func viewDidAppear(_ animated: Bool) { @@ -678,8 +686,8 @@ extension ComposeViewController { } }) view.backgroundColor = backgroundColor - tableView.backgroundColor = backgroundColor - composeToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor +// tableView.backgroundColor = backgroundColor +// composeToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor } // keyboard shortcutBar @@ -991,53 +999,53 @@ extension ComposeViewController: ComposeToolbarViewDelegate { // MARK: - UIScrollViewDelegate extension ComposeViewController { - func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { - guard scrollView === tableView else { return } - - let repliedToCellFrame = viewModel.repliedToCellFrame - guard repliedToCellFrame != .zero else { return } - - // try to find some patterns: - // print(""" - // repliedToCellFrame: \(viewModel.repliedToCellFrame.value.height) - // scrollView.contentOffset.y: \(scrollView.contentOffset.y) - // scrollView.contentSize.height: \(scrollView.contentSize.height) - // scrollView.frame: \(scrollView.frame) - // scrollView.adjustedContentInset.top: \(scrollView.adjustedContentInset.top) - // scrollView.adjustedContentInset.bottom: \(scrollView.adjustedContentInset.bottom) - // """) - - switch viewModel.collectionViewState { - case .fold: - os_log("%{public}s[%{public}ld], %{public}s: fold", ((#file as NSString).lastPathComponent), #line, #function) - guard velocity.y < 0 else { return } - let offsetY = scrollView.contentOffset.y + scrollView.adjustedContentInset.top - if offsetY < -44 { - tableView.contentInset.top = 0 - targetContentOffset.pointee = CGPoint(x: 0, y: -scrollView.adjustedContentInset.top) - viewModel.collectionViewState = .expand - } - - case .expand: - os_log("%{public}s[%{public}ld], %{public}s: expand", ((#file as NSString).lastPathComponent), #line, #function) - guard velocity.y > 0 else { return } - // check if top across - let topOffset = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) - repliedToCellFrame.height - - // check if bottom bounce - let bottomOffsetY = scrollView.contentOffset.y + (scrollView.frame.height - scrollView.adjustedContentInset.bottom) - let bottomOffset = bottomOffsetY - scrollView.contentSize.height - - if topOffset > 44 { - // do not interrupt user scrolling - viewModel.collectionViewState = .fold - } else if bottomOffset > 44 { - tableView.contentInset.top = -repliedToCellFrame.height - targetContentOffset.pointee = CGPoint(x: 0, y: -repliedToCellFrame.height) - viewModel.collectionViewState = .fold - } - } - } +// func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { +// guard scrollView === tableView else { return } +// +// let repliedToCellFrame = viewModel.repliedToCellFrame +// guard repliedToCellFrame != .zero else { return } +// +// // try to find some patterns: +// // print(""" +// // repliedToCellFrame: \(viewModel.repliedToCellFrame.value.height) +// // scrollView.contentOffset.y: \(scrollView.contentOffset.y) +// // scrollView.contentSize.height: \(scrollView.contentSize.height) +// // scrollView.frame: \(scrollView.frame) +// // scrollView.adjustedContentInset.top: \(scrollView.adjustedContentInset.top) +// // scrollView.adjustedContentInset.bottom: \(scrollView.adjustedContentInset.bottom) +// // """) +// +// switch viewModel.collectionViewState { +// case .fold: +// os_log("%{public}s[%{public}ld], %{public}s: fold", ((#file as NSString).lastPathComponent), #line, #function) +// guard velocity.y < 0 else { return } +// let offsetY = scrollView.contentOffset.y + scrollView.adjustedContentInset.top +// if offsetY < -44 { +// tableView.contentInset.top = 0 +// targetContentOffset.pointee = CGPoint(x: 0, y: -scrollView.adjustedContentInset.top) +// viewModel.collectionViewState = .expand +// } +// +// case .expand: +// os_log("%{public}s[%{public}ld], %{public}s: expand", ((#file as NSString).lastPathComponent), #line, #function) +// guard velocity.y > 0 else { return } +// // check if top across +// let topOffset = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) - repliedToCellFrame.height +// +// // check if bottom bounce +// let bottomOffsetY = scrollView.contentOffset.y + (scrollView.frame.height - scrollView.adjustedContentInset.bottom) +// let bottomOffset = bottomOffsetY - scrollView.contentSize.height +// +// if topOffset > 44 { +// // do not interrupt user scrolling +// viewModel.collectionViewState = .fold +// } else if bottomOffset > 44 { +// tableView.contentInset.top = -repliedToCellFrame.height +// targetContentOffset.pointee = CGPoint(x: 0, y: -repliedToCellFrame.height) +// viewModel.collectionViewState = .fold +// } +// } +// } } // MARK: - UITableViewDelegate diff --git a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift index 85c36fae0..5591e7dcc 100644 --- a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift +++ b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift @@ -6,10 +6,12 @@ // import UIKit +import SwiftUI import Combine import AlamofireImage import MastodonAsset import MastodonLocalization +import UIHostingConfigurationBackport final class ComposeStatusAttachmentTableViewCell: UITableViewCell { @@ -75,85 +77,91 @@ extension ComposeStatusAttachmentTableViewCell { } .store(in: &observations) - self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [ - weak self - ] collectionView, indexPath, item -> UICollectionViewCell? in + self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { + [weak self] collectionView, indexPath, item -> UICollectionViewCell? in guard let self = self else { return UICollectionViewCell() } switch item { case .attachment(let attachmentService): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell - 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 - } + cell.contentConfiguration = UIHostingConfigurationBackport { + HStack { + Image(systemName: "star") + Text("Favorites") + Spacer() } } - .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) +// 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/Mastodon/Scene/Compose/View/AttachmentContainerView.swift b/Mastodon/Scene/Compose/View/AttachmentContainerView.swift index 4743c9527..dd6a2b0a9 100644 --- a/Mastodon/Scene/Compose/View/AttachmentContainerView.swift +++ b/Mastodon/Scene/Compose/View/AttachmentContainerView.swift @@ -6,61 +6,63 @@ // import UIKit -import UITextView_Placeholder -import MastodonAsset -import MastodonLocalization +import SwiftUI +import MastodonUI final class AttachmentContainerView: UIView { static let containerViewCornerRadius: CGFloat = 4 - var descriptionBackgroundViewFrameObservation: NSKeyValueObservation? +// var descriptionBackgroundViewFrameObservation: NSKeyValueObservation? +// +// let activityIndicatorView: UIActivityIndicatorView = { +// let activityIndicatorView = UIActivityIndicatorView(style: .large) +// activityIndicatorView.color = UIColor.white.withAlphaComponent(0.8) +// return activityIndicatorView +// }() +// +// let previewImageView: UIImageView = { +// let imageView = UIImageView() +// imageView.contentMode = .scaleAspectFill +// imageView.layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius +// imageView.layer.cornerCurve = .continuous +// imageView.layer.masksToBounds = true +// return imageView +// }() +// +// let emptyStateView = AttachmentContainerView.EmptyStateView() +// let descriptionBackgroundView: UIView = { +// let view = UIView() +// view.layer.masksToBounds = true +// view.layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius +// view.layer.cornerCurve = .continuous +// view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] +// view.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 5, right: 8) +// return view +// }() +// let descriptionBackgroundGradientLayer: CAGradientLayer = { +// let gradientLayer = CAGradientLayer() +// gradientLayer.colors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.69).cgColor] +// gradientLayer.locations = [0.0, 1.0] +// gradientLayer.startPoint = CGPoint(x: 0.5, y: 0) +// gradientLayer.endPoint = CGPoint(x: 0.5, y: 1) +// gradientLayer.frame = CGRect(x: 0, y: 0, width: 100, height: 100) +// return gradientLayer +// }() +// let descriptionTextView: UITextView = { +// let textView = UITextView() +// textView.showsVerticalScrollIndicator = false +// textView.backgroundColor = .clear +// textView.textColor = .white +// textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15), maximumPointSize: 20) +// textView.placeholder = L10n.Scene.Compose.Attachment.descriptionPhoto +// textView.placeholderColor = UIColor.white.withAlphaComponent(0.6) // force white with alpha for Light/Dark mode +// textView.returnKeyType = .done +// return textView +// }() - let activityIndicatorView: UIActivityIndicatorView = { - let activityIndicatorView = UIActivityIndicatorView(style: .large) - activityIndicatorView.color = UIColor.white.withAlphaComponent(0.8) - return activityIndicatorView - }() - - let previewImageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFill - imageView.layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius - imageView.layer.cornerCurve = .continuous - imageView.layer.masksToBounds = true - return imageView - }() - - let emptyStateView = AttachmentContainerView.EmptyStateView() - let descriptionBackgroundView: UIView = { - let view = UIView() - view.layer.masksToBounds = true - view.layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius - view.layer.cornerCurve = .continuous - view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] - view.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 5, right: 8) - return view - }() - let descriptionBackgroundGradientLayer: CAGradientLayer = { - let gradientLayer = CAGradientLayer() - gradientLayer.colors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.69).cgColor] - gradientLayer.locations = [0.0, 1.0] - gradientLayer.startPoint = CGPoint(x: 0.5, y: 0) - gradientLayer.endPoint = CGPoint(x: 0.5, y: 1) - gradientLayer.frame = CGRect(x: 0, y: 0, width: 100, height: 100) - return gradientLayer - }() - let descriptionTextView: UITextView = { - let textView = UITextView() - textView.showsVerticalScrollIndicator = false - textView.backgroundColor = .clear - textView.textColor = .white - textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15), maximumPointSize: 20) - textView.placeholder = L10n.Scene.Compose.Attachment.descriptionPhoto - textView.placeholderColor = UIColor.white.withAlphaComponent(0.6) // force white with alpha for Light/Dark mode - textView.returnKeyType = .done - return textView - }() + private(set) lazy var contentView = AttachmentView(viewModel: viewModel) + public var viewModel: AttachmentView.ViewModel! override init(frame: CGRect) { super.init(frame: frame) @@ -77,89 +79,99 @@ final class AttachmentContainerView: UIView { extension AttachmentContainerView { private func _init() { - previewImageView.translatesAutoresizingMaskIntoConstraints = false - addSubview(previewImageView) + let hostingViewController = UIHostingController(rootView: contentView) + hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false + addSubview(hostingViewController.view) NSLayoutConstraint.activate([ - previewImageView.topAnchor.constraint(equalTo: topAnchor), - previewImageView.leadingAnchor.constraint(equalTo: leadingAnchor), - previewImageView.trailingAnchor.constraint(equalTo: trailingAnchor), - previewImageView.bottomAnchor.constraint(equalTo: bottomAnchor), + hostingViewController.view.topAnchor.constraint(equalTo: topAnchor), + hostingViewController.view.leadingAnchor.constraint(equalTo: leadingAnchor), + hostingViewController.view.trailingAnchor.constraint(equalTo: trailingAnchor), + hostingViewController.view.bottomAnchor.constraint(equalTo: bottomAnchor), ]) - descriptionBackgroundView.translatesAutoresizingMaskIntoConstraints = false - addSubview(descriptionBackgroundView) - NSLayoutConstraint.activate([ - descriptionBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor), - descriptionBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor), - descriptionBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor), - descriptionBackgroundView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.3), - ]) - descriptionBackgroundView.layer.addSublayer(descriptionBackgroundGradientLayer) - descriptionBackgroundViewFrameObservation = descriptionBackgroundView.observe(\.bounds, options: [.initial, .new]) { [weak self] _, _ in - guard let self = self else { return } - self.descriptionBackgroundGradientLayer.frame = self.descriptionBackgroundView.bounds - } - - descriptionTextView.translatesAutoresizingMaskIntoConstraints = false - descriptionBackgroundView.addSubview(descriptionTextView) - NSLayoutConstraint.activate([ - descriptionTextView.leadingAnchor.constraint(equalTo: descriptionBackgroundView.layoutMarginsGuide.leadingAnchor), - descriptionTextView.trailingAnchor.constraint(equalTo: descriptionBackgroundView.layoutMarginsGuide.trailingAnchor), - descriptionBackgroundView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: descriptionTextView.bottomAnchor), - descriptionTextView.heightAnchor.constraint(lessThanOrEqualToConstant: 36), - ]) - - emptyStateView.translatesAutoresizingMaskIntoConstraints = false - addSubview(emptyStateView) - NSLayoutConstraint.activate([ - emptyStateView.topAnchor.constraint(equalTo: topAnchor), - emptyStateView.leadingAnchor.constraint(equalTo: leadingAnchor), - emptyStateView.trailingAnchor.constraint(equalTo: trailingAnchor), - emptyStateView.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - - activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false - addSubview(activityIndicatorView) - NSLayoutConstraint.activate([ - activityIndicatorView.centerXAnchor.constraint(equalTo: previewImageView.centerXAnchor), - activityIndicatorView.centerYAnchor.constraint(equalTo: previewImageView.centerYAnchor), - ]) - - setupBroader() - - emptyStateView.isHidden = true - activityIndicatorView.hidesWhenStopped = true - activityIndicatorView.startAnimating() - - descriptionTextView.delegate = self +// previewImageView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(previewImageView) +// NSLayoutConstraint.activate([ +// previewImageView.topAnchor.constraint(equalTo: topAnchor), +// previewImageView.leadingAnchor.constraint(equalTo: leadingAnchor), +// previewImageView.trailingAnchor.constraint(equalTo: trailingAnchor), +// previewImageView.bottomAnchor.constraint(equalTo: bottomAnchor), +// ]) +// +// descriptionBackgroundView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(descriptionBackgroundView) +// NSLayoutConstraint.activate([ +// descriptionBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor), +// descriptionBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor), +// descriptionBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor), +// descriptionBackgroundView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.3), +// ]) +// descriptionBackgroundView.layer.addSublayer(descriptionBackgroundGradientLayer) +// descriptionBackgroundViewFrameObservation = descriptionBackgroundView.observe(\.bounds, options: [.initial, .new]) { [weak self] _, _ in +// guard let self = self else { return } +// self.descriptionBackgroundGradientLayer.frame = self.descriptionBackgroundView.bounds +// } +// +// descriptionTextView.translatesAutoresizingMaskIntoConstraints = false +// descriptionBackgroundView.addSubview(descriptionTextView) +// NSLayoutConstraint.activate([ +// descriptionTextView.leadingAnchor.constraint(equalTo: descriptionBackgroundView.layoutMarginsGuide.leadingAnchor), +// descriptionTextView.trailingAnchor.constraint(equalTo: descriptionBackgroundView.layoutMarginsGuide.trailingAnchor), +// descriptionBackgroundView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: descriptionTextView.bottomAnchor), +// descriptionTextView.heightAnchor.constraint(lessThanOrEqualToConstant: 36), +// ]) +// +// emptyStateView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(emptyStateView) +// NSLayoutConstraint.activate([ +// emptyStateView.topAnchor.constraint(equalTo: topAnchor), +// emptyStateView.leadingAnchor.constraint(equalTo: leadingAnchor), +// emptyStateView.trailingAnchor.constraint(equalTo: trailingAnchor), +// emptyStateView.bottomAnchor.constraint(equalTo: bottomAnchor), +// ]) +// +// activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(activityIndicatorView) +// NSLayoutConstraint.activate([ +// activityIndicatorView.centerXAnchor.constraint(equalTo: previewImageView.centerXAnchor), +// activityIndicatorView.centerYAnchor.constraint(equalTo: previewImageView.centerYAnchor), +// ]) +// +// setupBroader() +// +// emptyStateView.isHidden = true +// activityIndicatorView.hidesWhenStopped = true +// activityIndicatorView.startAnimating() +// +// descriptionTextView.delegate = self } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - setupBroader() - } +// override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { +// super.traitCollectionDidChange(previousTraitCollection) +// +// setupBroader() +// } } extension AttachmentContainerView { - private func setupBroader() { - emptyStateView.layer.borderWidth = 1 - emptyStateView.layer.borderColor = traitCollection.userInterfaceStyle == .dark ? ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor.cgColor : nil - } +// private func setupBroader() { +// emptyStateView.layer.borderWidth = 1 +// emptyStateView.layer.borderColor = traitCollection.userInterfaceStyle == .dark ? ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor.cgColor : nil +// } } -// MARK: - UITextViewDelegate -extension AttachmentContainerView: UITextViewDelegate { - func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - // let keyboard dismiss when input description with "done" type return key - if textView === descriptionTextView, text == "\n" { - textView.resignFirstResponder() - return false - } - - return true - } -} +//// MARK: - UITextViewDelegate +//extension AttachmentContainerView: UITextViewDelegate { +// func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { +// // let keyboard dismiss when input description with "done" type return key +// if textView === descriptionTextView, text == "\n" { +// textView.resignFirstResponder() +// return false +// } +// +// return true +// } +//} diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift index 8911a506e..86d94a3aa 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift @@ -12,6 +12,7 @@ import GameplayKit import CoreData import CoreDataStack import MastodonSDK +import MastodonCore final class DiscoveryCommunityViewModel { diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index c48ed1199..5c86b9f38 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -77,31 +77,8 @@ final class NotificationTimelineViewModel { } extension NotificationTimelineViewModel { - enum Scope: Hashable, CaseIterable { - case everything - case mentions - - var includeTypes: [MastodonNotificationType]? { - switch self { - case .everything: return nil - case .mentions: return [.mention, .status] - } - } - - var excludeTypes: [MastodonNotificationType]? { - switch self { - case .everything: return nil - case .mentions: return [.follow, .followRequest, .reblog, .favourite, .poll] - } - } - - var _excludeTypes: [Mastodon.Entity.Notification.NotificationType]? { - switch self { - case .everything: return nil - case .mentions: return [.follow, .followRequest, .reblog, .favourite, .poll] - } - } - } + + typealias Scope = APIService.NotificationScope static func feedPredicate( authenticationBox: MastodonAuthenticationBox, diff --git a/Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift b/Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift index c97fc1489..470cc79a5 100644 --- a/Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift +++ b/Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift @@ -12,9 +12,6 @@ import AuthenticationServices final class MastodonAuthenticationController { - static let callbackURLScheme = "mastodon" - static let callbackURL = "mastodon://joinmastodon.org/oauth" - var disposeBag = Set() // input @@ -43,7 +40,7 @@ extension MastodonAuthenticationController { private func authentication() { authenticationSession = ASWebAuthenticationSession( url: authenticateURL, - callbackURLScheme: MastodonAuthenticationController.callbackURLScheme + callbackURLScheme: APIService.callbackURLScheme ) { [weak self] callback, error in guard let self = self else { return } os_log("%{public}s[%{public}ld], %{public}s: callback: %s, error: %s", ((#file as NSString).lastPathComponent), #line, #function, callback?.debugDescription ?? "", error.debugDescription) diff --git a/Mastodon/Scene/Root/RootSplitViewController.swift b/Mastodon/Scene/Root/RootSplitViewController.swift index f19282936..1ab2ee0f6 100644 --- a/Mastodon/Scene/Root/RootSplitViewController.swift +++ b/Mastodon/Scene/Root/RootSplitViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine import CoreDataStack +import MastodonCore final class RootSplitViewController: UISplitViewController, NeedsDependency { diff --git a/Mastodon/Service/StatusPublishService.swift b/Mastodon/Service/StatusPublishService.swift deleted file mode 100644 index f5c4cb2dd..000000000 --- a/Mastodon/Service/StatusPublishService.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// StatusPublishService.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-3-26. -// - -import os.log -import Foundation -import Intents -import Combine -import CoreData -import CoreDataStack -import MastodonSDK -import UIKit - -final class StatusPublishService { - - var disposeBag = Set() - - let workingQueue = DispatchQueue(label: "org.joinmastodon.app.StatusPublishService.working-queue") - - // input - var viewModels = CurrentValueSubject<[ComposeViewModel], Never>([]) // use strong reference to retain the view models - - // output - let composeViewModelDidUpdatePublisher = PassthroughSubject() - let latestPublishingComposeViewModel = CurrentValueSubject(nil) - - init() { - Publishers.CombineLatest( - viewModels.eraseToAnyPublisher(), - composeViewModelDidUpdatePublisher.eraseToAnyPublisher() - ) - .map { viewModels, _ in viewModels.last } - .assign(to: \.value, on: latestPublishingComposeViewModel) - .store(in: &disposeBag) - } - -} - -extension StatusPublishService { - - func publish(composeViewModel: ComposeViewModel) { - workingQueue.sync { - guard !self.viewModels.value.contains(where: { $0 === composeViewModel }) else { return } - self.viewModels.value = self.viewModels.value + [composeViewModel] - - composeViewModel.publishStateMachinePublisher - .receive(on: DispatchQueue.main) - .sink { [weak self, weak composeViewModel] state in - guard let self = self else { return } - guard let composeViewModel = composeViewModel else { return } - - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: composeViewModelDidUpdate", ((#file as NSString).lastPathComponent), #line, #function) - self.composeViewModelDidUpdatePublisher.send() - - switch state { - case is ComposeViewModel.PublishState.Finish: - self.remove(composeViewModel: composeViewModel) - default: - break - } - } - .store(in: &composeViewModel.disposeBag) // cancel subscription when viewModel dealloc - } - } - - func remove(composeViewModel: ComposeViewModel) { - workingQueue.async { - var viewModels = self.viewModels.value - viewModels.removeAll(where: { $0 === composeViewModel }) - self.viewModels.value = viewModels - - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: composeViewModel removed", ((#file as NSString).lastPathComponent), #line, #function) - } - } - -} diff --git a/Mastodon/State/DocumentStore.swift b/Mastodon/State/DocumentStore.swift deleted file mode 100644 index cda038176..000000000 --- a/Mastodon/State/DocumentStore.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// DocumentStore.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021-1-27. -// - -import UIKit -import Combine -import MastodonSDK - -class DocumentStore: ObservableObject { - let appStartUpTimestamp = Date() - var defaultRevealStatusDict: [Mastodon.Entity.Status.ID: Bool] = [:] -} diff --git a/Mastodon/State/ViewStateStore.swift b/Mastodon/State/ViewStateStore.swift deleted file mode 100644 index 07d8b844f..000000000 --- a/Mastodon/State/ViewStateStore.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// ViewStateStore.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021-1-27. -// - -import Combine - -struct ViewStateStore { - -} - -enum ViewState { } diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 7b1185f84..7336125b8 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -8,9 +8,9 @@ import os.log import UIKit import UserNotifications -import AppShared import AVFoundation -@_exported import MastodonUI +import MastodonCore +import MastodonUI @main class AppDelegate: UIResponder, UIApplicationDelegate { diff --git a/MastodonIntent/Handler/SendPostIntentHandler.swift b/MastodonIntent/Handler/SendPostIntentHandler.swift index 0da4e113b..afee7d581 100644 --- a/MastodonIntent/Handler/SendPostIntentHandler.swift +++ b/MastodonIntent/Handler/SendPostIntentHandler.swift @@ -11,6 +11,7 @@ import Combine import CoreData import CoreDataStack import MastodonSDK +import MastodonCore final class SendPostIntentHandler: NSObject { @@ -18,8 +19,12 @@ final class SendPostIntentHandler: NSObject { let coreDataStack = CoreDataStack() lazy var managedObjectContext = coreDataStack.persistentContainer.viewContext - lazy var api = APIService.shared - + lazy var api: APIService = { + let backgroundManagedObjectContext = coreDataStack.newTaskContext() + return APIService( + backgroundManagedObjectContext: backgroundManagedObjectContext + ) + }() } // MARK: - SendPostIntentHandling diff --git a/MastodonIntent/Model/Account+Fetch.swift b/MastodonIntent/Model/Account+Fetch.swift index 065ccac12..fd8c81769 100644 --- a/MastodonIntent/Model/Account+Fetch.swift +++ b/MastodonIntent/Model/Account+Fetch.swift @@ -9,6 +9,7 @@ import Foundation import CoreData import CoreDataStack import Intents +import MastodonCore extension Account { diff --git a/MastodonIntent/Service/APIService.swift b/MastodonIntent/Service/APIService.swift deleted file mode 100644 index 0733c08d4..000000000 --- a/MastodonIntent/Service/APIService.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// APIService.swift -// MastodonIntent -// -// Created by Cirno MainasuK on 2021-7-26. -// - -import os.log -import Foundation -import Combine -import CoreData -import CoreDataStack -import MastodonSDK - -// Replica APIService for share extension -final class APIService { - - var disposeBag = Set() - - static let shared = APIService() - - // internal - let session: URLSession - - // output - let error = PassthroughSubject() - - private init() { - self.session = URLSession(configuration: .default) - } - -} - diff --git a/MastodonSDK/Package.resolved b/MastodonSDK/Package.resolved new file mode 100644 index 000000000..06843faa3 --- /dev/null +++ b/MastodonSDK/Package.resolved @@ -0,0 +1,241 @@ +{ + "object": { + "pins": [ + { + "package": "Alamofire", + "repositoryURL": "https://github.com/Alamofire/Alamofire.git", + "state": { + "branch": null, + "revision": "8dd85aee02e39dd280c75eef88ffdb86eed4b07b", + "version": "5.6.2" + } + }, + { + "package": "AlamofireImage", + "repositoryURL": "https://github.com/Alamofire/AlamofireImage.git", + "state": { + "branch": null, + "revision": "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10", + "version": "4.2.0" + } + }, + { + "package": "CommonOSLog", + "repositoryURL": "https://github.com/MainasuK/CommonOSLog", + "state": { + "branch": null, + "revision": "c121624a30698e9886efe38aebb36ff51c01b6c2", + "version": "0.1.1" + } + }, + { + "package": "FaviconFinder", + "repositoryURL": "https://github.com/will-lumley/FaviconFinder.git", + "state": { + "branch": null, + "revision": "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a", + "version": "3.3.0" + } + }, + { + "package": "FLAnimatedImage", + "repositoryURL": "https://github.com/Flipboard/FLAnimatedImage.git", + "state": { + "branch": null, + "revision": "d4f07b6f164d53c1212c3e54d6460738b1981e9f", + "version": "1.0.17" + } + }, + { + "package": "FPSIndicator", + "repositoryURL": "https://github.com/MainasuK/FPSIndicator.git", + "state": { + "branch": null, + "revision": "e4a5067ccd5293b024c767f09e51056afd4a4796", + "version": "1.1.0" + } + }, + { + "package": "Fuzi", + "repositoryURL": "https://github.com/cezheng/Fuzi.git", + "state": { + "branch": null, + "revision": "f08c8323da21e985f3772610753bcfc652c2103f", + "version": "3.1.3" + } + }, + { + "package": "KeychainAccess", + "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", + "state": { + "branch": null, + "revision": "84e546727d66f1adc5439debad16270d0fdd04e7", + "version": "4.2.2" + } + }, + { + "package": "MetaTextKit", + "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", + "state": { + "branch": null, + "revision": "dcd5255d6930c2fab408dc8562c577547e477624", + "version": "2.2.5" + } + }, + { + "package": "Nuke", + "repositoryURL": "https://github.com/kean/Nuke.git", + "state": { + "branch": null, + "revision": "a002b7fd786f2df2ed4333fe73a9727499fd9d97", + "version": "10.11.2" + } + }, + { + "package": "NukeFLAnimatedImagePlugin", + "repositoryURL": "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", + "state": { + "branch": null, + "revision": "b59c346a7d536336db3b0f12c72c6e53ee709e16", + "version": "8.0.0" + } + }, + { + "package": "Pageboy", + "repositoryURL": "https://github.com/uias/Pageboy", + "state": { + "branch": null, + "revision": "af8fa81788b893205e1ff42ddd88c5b0b315d7c5", + "version": "3.7.0" + } + }, + { + "package": "PanModal", + "repositoryURL": "https://github.com/slackhq/PanModal.git", + "state": { + "branch": null, + "revision": "b012aecb6b67a8e46369227f893c12544846613f", + "version": "1.2.7" + } + }, + { + "package": "SDWebImage", + "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", + "state": { + "branch": null, + "revision": "9248fe561a2a153916fb9597e3af4434784c6d32", + "version": "5.13.4" + } + }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections.git", + "state": { + "branch": null, + "revision": "f504716c27d2e5d4144fa4794b12129301d17729", + "version": "1.0.3" + } + }, + { + "package": "swift-nio", + "repositoryURL": "https://github.com/apple/swift-nio.git", + "state": { + "branch": null, + "revision": "546610d52b19be3e19935e0880bb06b9c03f5cef", + "version": "1.14.4" + } + }, + { + "package": "swift-nio-zlib-support", + "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git", + "state": { + "branch": null, + "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd", + "version": "1.0.0" + } + }, + { + "package": "SwiftSoup", + "repositoryURL": "https://github.com/scinfu/SwiftSoup.git", + "state": { + "branch": null, + "revision": "6778575285177365cbad3e5b8a72f2a20583cfec", + "version": "2.4.3" + } + }, + { + "package": "Introspect", + "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git", + "state": { + "branch": null, + "revision": "f2616860a41f9d9932da412a8978fec79c06fe24", + "version": "0.1.4" + } + }, + { + "package": "SwiftyJSON", + "repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git", + "state": { + "branch": null, + "revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07", + "version": "5.0.1" + } + }, + { + "package": "TabBarPager", + "repositoryURL": "https://github.com/TwidereProject/TabBarPager.git", + "state": { + "branch": null, + "revision": "488aa66d157a648901b61721212c0dec23d27ee5", + "version": "0.1.0" + } + }, + { + "package": "Tabman", + "repositoryURL": "https://github.com/uias/Tabman", + "state": { + "branch": null, + "revision": "4a4f7c755b875ffd4f9ef10d67a67883669d2465", + "version": "2.13.0" + } + }, + { + "package": "ThirdPartyMailer", + "repositoryURL": "https://github.com/vtourraine/ThirdPartyMailer.git", + "state": { + "branch": null, + "revision": "44c1cfaa6969963f22691aa67f88a69e3b6d651f", + "version": "2.1.0" + } + }, + { + "package": "TOCropViewController", + "repositoryURL": "https://github.com/TimOliver/TOCropViewController.git", + "state": { + "branch": null, + "revision": "d0470491f56e734731bbf77991944c0dfdee3e0e", + "version": "2.6.1" + } + }, + { + "package": "UIHostingConfigurationBackport", + "repositoryURL": "https://github.com/woxtu/UIHostingConfigurationBackport.git", + "state": { + "branch": null, + "revision": "6091f2d38faa4b24fc2ca0389c651e2f666624a3", + "version": "0.1.0" + } + }, + { + "package": "UITextView+Placeholder", + "repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git", + "state": { + "branch": null, + "revision": "20f513ded04a040cdf5467f0891849b1763ede3b", + "version": "1.4.1" + } + } + ] + }, + "version": 1 +} diff --git a/MastodonSDK/Package.swift b/MastodonSDK/Package.swift index aa349f8e0..1cc1d9ff0 100644 --- a/MastodonSDK/Package.swift +++ b/MastodonSDK/Package.swift @@ -16,6 +16,7 @@ let package = Package( "CoreDataStack", "MastodonAsset", "MastodonCommon", + "MastodonCore", "MastodonExtension", "MastodonLocalization", "MastodonSDK", @@ -23,17 +24,30 @@ let package = Package( ]) ], dependencies: [ - .package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"), - .package(url: "https://github.com/apple/swift-nio.git", from: "1.0.0"), - .package(url: "https://github.com/kean/Nuke.git", from: "10.3.1"), - .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"), - .package(url: "https://github.com/TwidereProject/MetaTextKit.git", .exact("2.2.5")), + .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/Alamofire/Alamofire.git", from: "5.4.0"), .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"), - .package(name: "NukeFLAnimatedImagePlugin", url: "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", from: "8.0.0"), - .package(name: "UITextView+Placeholder", url: "https://github.com/MainasuK/UITextView-Placeholder.git", from: "1.4.1"), - .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.3"), - .package(name: "FaviconFinder", url: "https://github.com/will-lumley/FaviconFinder.git", from: "3.2.2"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.3"), + .package(url: "https://github.com/apple/swift-nio.git", from: "1.0.0"), + .package(url: "https://github.com/cezheng/Fuzi.git", from: "3.1.3"), + .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"), + .package(url: "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", from: "8.0.0"), + .package(url: "https://github.com/kean/Nuke.git", from: "10.3.1"), + .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"), + .package(url: "https://github.com/MainasuK/CommonOSLog", from: "0.1.1"), + .package(url: "https://github.com/MainasuK/FPSIndicator.git", from: "1.0.0"), + .package(url: "https://github.com/slackhq/PanModal.git", from: "1.2.7"), + .package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"), + .package(url: "https://github.com/TimOliver/TOCropViewController.git", from: "2.6.1"), + .package(url: "https://github.com/TwidereProject/MetaTextKit.git", .exact("2.2.5")), + .package(url: "https://github.com/TwidereProject/TabBarPager.git", from: "0.1.0"), + .package(url: "https://github.com/uias/Tabman", from: "2.13.0"), + .package(url: "https://github.com/vtourraine/ThirdPartyMailer.git", from: "2.1.0"), + .package(url: "https://github.com/woxtu/UIHostingConfigurationBackport.git", from: "0.1.0"), + .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.12.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -60,6 +74,22 @@ let package = Package( "MastodonExtension" ] ), + .target( + name: "MastodonCore", + dependencies: [ + "CoreDataStack", + "MastodonAsset", + "MastodonCommon", + "MastodonLocalization", + "MastodonSDK", + .product(name: "Alamofire", package: "Alamofire"), + .product(name: "AlamofireImage", package: "AlamofireImage"), + .product(name: "CommonOSLog", package: "CommonOSLog"), + .product(name: "ArkanaKeys", package: "ArkanaKeys"), + .product(name: "KeychainAccess", package: "KeychainAccess"), + .product(name: "MetaTextKit", package: "MetaTextKit") + ] + ), .target( name: "MastodonExtension", dependencies: [] @@ -78,20 +108,20 @@ let package = Package( .target( name: "MastodonUI", dependencies: [ - "CoreDataStack", - "MastodonSDK", - "MastodonExtension", - "MastodonAsset", - "MastodonLocalization", - .product(name: "Alamofire", package: "Alamofire"), - .product(name: "AlamofireImage", package: "AlamofireImage"), + "MastodonCore", .product(name: "FLAnimatedImage", package: "FLAnimatedImage"), .product(name: "FaviconFinder", package: "FaviconFinder"), - .product(name: "MetaTextKit", package: "MetaTextKit"), .product(name: "Nuke", package: "Nuke"), - .product(name: "NukeFLAnimatedImagePlugin", package: "NukeFLAnimatedImagePlugin"), .product(name: "Introspect", package: "Introspect"), .product(name: "UITextView+Placeholder", package: "UITextView+Placeholder"), + .product(name: "UIHostingConfigurationBackport", package: "UIHostingConfigurationBackport"), + .product(name: "TabBarPager", package: "TabBarPager"), + .product(name: "ThirdPartyMailer", package: "ThirdPartyMailer"), + .product(name: "OrderedCollections", package: "swift-collections"), + .product(name: "Tabman", package: "Tabman"), + .product(name: "MetaTextKit", package: "MetaTextKit"), + .product(name: "CropViewController", package: "TOCropViewController"), + .product(name: "PanModal", package: "PanModal"), ] ), .testTarget( diff --git a/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift b/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift index c5f415758..a1207f116 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift +++ b/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift @@ -113,6 +113,15 @@ public final class CoreDataStack { } +extension CoreDataStack { + public func newTaskContext() -> NSManagedObjectContext { + let taskContext = persistentContainer.newBackgroundContext() + taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + taskContext.undoManager = nil + return taskContext + } +} + extension CoreDataStack { public func rebuild() { diff --git a/Mastodon/Preference/AppPreference.swift b/MastodonSDK/Sources/MastodonCommon/Preference/Preference+App.swift similarity index 81% rename from Mastodon/Preference/AppPreference.swift rename to MastodonSDK/Sources/MastodonCommon/Preference/Preference+App.swift index 4ede61cf8..4453d94b1 100644 --- a/Mastodon/Preference/AppPreference.swift +++ b/MastodonSDK/Sources/MastodonCommon/Preference/Preference+App.swift @@ -9,7 +9,7 @@ import UIKit extension UserDefaults { - @objc dynamic var preferredUsingDefaultBrowser: Bool { + @objc public dynamic var preferredUsingDefaultBrowser: Bool { get { register(defaults: [#function: false]) return bool(forKey: #function) diff --git a/AppShared/UserDefaults+Notification.swift b/MastodonSDK/Sources/MastodonCommon/Preference/Preference+Notification.swift similarity index 82% rename from AppShared/UserDefaults+Notification.swift rename to MastodonSDK/Sources/MastodonCommon/Preference/Preference+Notification.swift index e743e70a0..7b2ac57a1 100644 --- a/AppShared/UserDefaults+Notification.swift +++ b/MastodonSDK/Sources/MastodonCommon/Preference/Preference+Notification.swift @@ -7,6 +7,7 @@ import UIKit import CryptoKit +import MastodonExtension extension UserDefaults { // always use hash value (SHA256) from accessToken as key @@ -38,3 +39,15 @@ extension UserDefaults { } } + +extension UserDefaults { + + @objc public dynamic var notificationBadgeCount: Int { + get { + register(defaults: [#function: 0]) + return integer(forKey: #function) + } + set { self[#function] = newValue } + } + +} diff --git a/Mastodon/Preference/StoreReviewPreference.swift b/MastodonSDK/Sources/MastodonCommon/Preference/Preference+StoreReview.swift similarity index 75% rename from Mastodon/Preference/StoreReviewPreference.swift rename to MastodonSDK/Sources/MastodonCommon/Preference/Preference+StoreReview.swift index e3a403f6d..cb4701a3c 100644 --- a/Mastodon/Preference/StoreReviewPreference.swift +++ b/MastodonSDK/Sources/MastodonCommon/Preference/Preference+StoreReview.swift @@ -9,14 +9,14 @@ import Foundation extension UserDefaults { - @objc dynamic var processCompletedCount: Int { + @objc public dynamic var processCompletedCount: Int { get { return integer(forKey: #function) } set { self[#function] = newValue } } - @objc dynamic var lastVersionPromptedForReview: String? { + @objc public dynamic var lastVersionPromptedForReview: String? { get { return string(forKey: #function) } diff --git a/Mastodon/Preference/WizardPreference.swift b/MastodonSDK/Sources/MastodonCommon/Preference/Preference+Wizard.swift similarity index 76% rename from Mastodon/Preference/WizardPreference.swift rename to MastodonSDK/Sources/MastodonCommon/Preference/Preference+Wizard.swift index c34e34a8a..f8fc245f8 100644 --- a/Mastodon/Preference/WizardPreference.swift +++ b/MastodonSDK/Sources/MastodonCommon/Preference/Preference+Wizard.swift @@ -8,7 +8,7 @@ import UIKit extension UserDefaults { - @objc dynamic var didShowMultipleAccountSwitchWizard: Bool { + @objc public dynamic var didShowMultipleAccountSwitchWizard: Bool { get { return bool(forKey: #function) } set { self[#function] = newValue } } diff --git a/Mastodon/State/AppContext.swift b/MastodonSDK/Sources/MastodonCore/AppContext.swift similarity index 86% rename from Mastodon/State/AppContext.swift rename to MastodonSDK/Sources/MastodonCore/AppContext.swift index 683c81f33..7c61cd75b 100644 --- a/Mastodon/State/AppContext.swift +++ b/MastodonSDK/Sources/MastodonCore/AppContext.swift @@ -1,44 +1,42 @@ // // AppContext.swift -// Mastodon +// // -// Created by Cirno MainasuK on 2021-1-27. +// Created by MainasuK on 22/9/30. // import os.log import UIKit +import SwiftUI import Combine import CoreData import CoreDataStack import AlamofireImage -import MastodonUI -class AppContext: ObservableObject { +public class AppContext: ObservableObject { var disposeBag = Set() - @Published var viewStateStore = ViewStateStore() - - let coreDataStack: CoreDataStack - let managedObjectContext: NSManagedObjectContext - let backgroundManagedObjectContext: NSManagedObjectContext + public let coreDataStack: CoreDataStack + public let managedObjectContext: NSManagedObjectContext + public let backgroundManagedObjectContext: NSManagedObjectContext - let apiService: APIService - let authenticationService: AuthenticationService - let emojiService: EmojiService - let statusPublishService = StatusPublishService() - let notificationService: NotificationService - let settingService: SettingService - let instanceService: InstanceService + public let apiService: APIService + public let authenticationService: AuthenticationService + public let emojiService: EmojiService + public let statusPublishService = StatusPublishService() + public let notificationService: NotificationService + public let settingService: SettingService + public let instanceService: InstanceService - let blockDomainService: BlockDomainService - let statusFilterService: StatusFilterService - let photoLibraryService = PhotoLibraryService() + public let blockDomainService: BlockDomainService + public let statusFilterService: StatusFilterService + public let photoLibraryService = PhotoLibraryService() - let placeholderImageCacheService = PlaceholderImageCacheService() - let blurhashImageCacheService = BlurhashImageCacheService.shared + public let placeholderImageCacheService = PlaceholderImageCacheService() + public let blurhashImageCacheService = BlurhashImageCacheService.shared - let documentStore: DocumentStore + public let documentStore: DocumentStore private var documentStoreSubscription: AnyCancellable! let overrideTraitCollection = CurrentValueSubject(nil) @@ -46,8 +44,8 @@ class AppContext: ObservableObject { .autoconnect() .share() .eraseToAnyPublisher() - - init() { + + public init() { let _coreDataStack = CoreDataStack() let _managedObjectContext = _coreDataStack.persistentContainer.viewContext let _backgroundManagedObjectContext = _coreDataStack.persistentContainer.newBackgroundContext() diff --git a/AppShared/AppSecret.swift b/MastodonSDK/Sources/MastodonCore/AppSecret.swift similarity index 93% rename from AppShared/AppSecret.swift rename to MastodonSDK/Sources/MastodonCore/AppSecret.swift index 9110f2490..3d595c143 100644 --- a/AppShared/AppSecret.swift +++ b/MastodonSDK/Sources/MastodonCore/AppSecret.swift @@ -9,8 +9,8 @@ import Foundation import CryptoKit import KeychainAccess -import Keys import MastodonCommon +import ArkanaKeys public final class AppSecret { @@ -36,12 +36,10 @@ public final class AppSecret { }() init() { - let keys = MastodonKeys() - #if DEBUG - self.notificationEndpoint = keys.notification_endpoint_debug + self.notificationEndpoint = Keys.Debug().notificationEndpoint #else - self.notificationEndpoint = keys.notification_endpoint + self.notificationEndpoint = Keys.Release().notificationEndpoint #endif } diff --git a/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift b/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift new file mode 100644 index 000000000..14cb03ad7 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift @@ -0,0 +1,32 @@ +// +// MastodonAuthenticationBox.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-7-20. +// + +import Foundation +import CoreDataStack +import MastodonSDK + +public struct MastodonAuthenticationBox: UserIdentifier { + public let authenticationRecord: ManagedObjectRecord + public let domain: String + public let userID: MastodonUser.ID + public let appAuthorization: Mastodon.API.OAuth.Authorization + public let userAuthorization: Mastodon.API.OAuth.Authorization + + public init( + authenticationRecord: ManagedObjectRecord, + domain: String, + userID: MastodonUser.ID, + appAuthorization: Mastodon.API.OAuth.Authorization, + userAuthorization: Mastodon.API.OAuth.Authorization + ) { + self.authenticationRecord = authenticationRecord + self.domain = domain + self.userID = userID + self.appAuthorization = appAuthorization + self.userAuthorization = userAuthorization + } +} diff --git a/MastodonSDK/Sources/MastodonCore/DocumentStore.swift b/MastodonSDK/Sources/MastodonCore/DocumentStore.swift new file mode 100644 index 000000000..29a3d4f94 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/DocumentStore.swift @@ -0,0 +1,15 @@ +// +// DocumentStore.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-1-27. +// + +import UIKit +import Combine +import MastodonSDK + +public class DocumentStore: ObservableObject { + public let appStartUpTimestamp = Date() + public var defaultRevealStatusDict: [Mastodon.Entity.Status.ID: Bool] = [:] +} diff --git a/Mastodon/Extension/CoreDataStack/Instance.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Instance.swift similarity index 100% rename from Mastodon/Extension/CoreDataStack/Instance.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Instance.swift diff --git a/MastodonSDK/Sources/MastodonUI/Extension/CoreDataStack/MastodonEmoji.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonEmoji.swift similarity index 69% rename from MastodonSDK/Sources/MastodonUI/Extension/CoreDataStack/MastodonEmoji.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonEmoji.swift index 8e2558bb7..e30085840 100644 --- a/MastodonSDK/Sources/MastodonUI/Extension/CoreDataStack/MastodonEmoji.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonEmoji.swift @@ -29,3 +29,15 @@ extension Collection where Element == Mastodon.Entity.Emoji { return dictionary } } + +extension MastodonEmoji { + public convenience init(emoji: Mastodon.Entity.Emoji) { + self.init( + code: emoji.shortcode, + url: emoji.url, + staticURL: emoji.staticURL, + visibleInPicker: emoji.visibleInPicker, + category: emoji.category + ) + } +} diff --git a/Mastodon/Persistence/Extension/MastodonField.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonField.swift similarity index 100% rename from Mastodon/Persistence/Extension/MastodonField.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonField.swift diff --git a/Mastodon/Persistence/Extension/MastodonMention.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonMention.swift similarity index 100% rename from Mastodon/Persistence/Extension/MastodonMention.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonMention.swift diff --git a/MastodonSDK/Sources/MastodonUI/Extension/CoreDataStack/MastodonStatus.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonStatus.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/Extension/CoreDataStack/MastodonStatus.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonStatus.swift diff --git a/Mastodon/Persistence/Extension/MastodonUser+Property.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser+Property.swift similarity index 100% rename from Mastodon/Persistence/Extension/MastodonUser+Property.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser+Property.swift diff --git a/MastodonSDK/Sources/MastodonUI/Extension/CoreDataStack/MastodonUser.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser.swift similarity index 73% rename from MastodonSDK/Sources/MastodonUI/Extension/CoreDataStack/MastodonUser.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser.swift index 9b61ab5f6..02a983680 100644 --- a/MastodonSDK/Sources/MastodonUI/Extension/CoreDataStack/MastodonUser.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser.swift @@ -1,13 +1,13 @@ // // MastodonUser.swift -// +// Mastodon // -// Created by MainasuK on 2022-4-14. +// Created by MainasuK Cirno on 2021/2/3. // import Foundation import CoreDataStack -import MastodonCommon +import MastodonSDK extension MastodonUser { @@ -55,3 +55,21 @@ extension MastodonUser { } } + +extension MastodonUser { + + public var profileURL: URL { + if let urlString = self.url, + let url = URL(string: urlString) { + return url + } else { + return URL(string: "https://\(self.domain)/@\(username)")! + } + } + + public var activityItems: [Any] { + var items: [Any] = [] + items.append(profileURL) + return items + } +} diff --git a/Mastodon/Persistence/Extension/Notification+Property.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Notification+Property.swift similarity index 100% rename from Mastodon/Persistence/Extension/Notification+Property.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Notification+Property.swift diff --git a/Mastodon/Persistence/Extension/Poll+Property.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Poll+Property.swift similarity index 100% rename from Mastodon/Persistence/Extension/Poll+Property.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Poll+Property.swift diff --git a/Mastodon/Persistence/Extension/PollOption+Property.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/PollOption+Property.swift similarity index 100% rename from Mastodon/Persistence/Extension/PollOption+Property.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/PollOption+Property.swift diff --git a/Mastodon/Extension/CoreDataStack/Setting.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Setting.swift similarity index 100% rename from Mastodon/Extension/CoreDataStack/Setting.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Setting.swift diff --git a/Mastodon/Persistence/Extension/Status+Property.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status+Property.swift similarity index 100% rename from Mastodon/Persistence/Extension/Status+Property.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status+Property.swift diff --git a/Mastodon/Extension/CoreDataStack/Status.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status.swift similarity index 100% rename from Mastodon/Extension/CoreDataStack/Status.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status.swift diff --git a/Mastodon/Extension/CoreDataStack/Subscription.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Subscription.swift similarity index 100% rename from Mastodon/Extension/CoreDataStack/Subscription.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Subscription.swift diff --git a/Mastodon/Extension/CoreDataStack/SubscriptionAlerts.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/SubscriptionAlerts.swift similarity index 100% rename from Mastodon/Extension/CoreDataStack/SubscriptionAlerts.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/SubscriptionAlerts.swift diff --git a/Mastodon/Persistence/Extension/Tag+Property.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Tag+Property.swift similarity index 100% rename from Mastodon/Persistence/Extension/Tag+Property.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Tag+Property.swift diff --git a/MastodonSDK/Sources/MastodonUI/Extension/MastodonSDK/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/Extension/MastodonSDK/Mastodon+Entity+Account.swift rename to MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift diff --git a/MastodonSDK/Sources/MastodonUI/Extension/MastodonSDK/Mastodon+Entity+Link.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Link.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/Extension/MastodonSDK/Mastodon+Entity+Link.swift rename to MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Link.swift diff --git a/MastodonSDK/Sources/MastodonUI/Extension/MastodonSDK/Mastodon+Entity+Tag.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Tag.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/Extension/MastodonSDK/Mastodon+Entity+Tag.swift rename to MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Tag.swift diff --git a/Mastodon/Diffiable/FetchedResultsController/FeedFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift similarity index 100% rename from Mastodon/Diffiable/FetchedResultsController/FeedFetchedResultsController.swift rename to MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift diff --git a/Mastodon/Diffiable/FetchedResultsController/SearchHistoryFetchedResultController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/SearchHistoryFetchedResultController.swift similarity index 100% rename from Mastodon/Diffiable/FetchedResultsController/SearchHistoryFetchedResultController.swift rename to MastodonSDK/Sources/MastodonCore/FetchedResultsController/SearchHistoryFetchedResultController.swift diff --git a/Mastodon/Diffiable/FetchedResultsController/SettingFetchedResultController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/SettingFetchedResultController.swift similarity index 80% rename from Mastodon/Diffiable/FetchedResultsController/SettingFetchedResultController.swift rename to MastodonSDK/Sources/MastodonCore/FetchedResultsController/SettingFetchedResultController.swift index 52eafc6b6..cd8845386 100644 --- a/Mastodon/Diffiable/FetchedResultsController/SettingFetchedResultController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/SettingFetchedResultController.swift @@ -12,7 +12,7 @@ import CoreData import CoreDataStack import MastodonSDK -final class SettingFetchedResultController: NSObject { +public final class SettingFetchedResultController: NSObject { var disposeBag = Set() @@ -21,9 +21,9 @@ final class SettingFetchedResultController: NSObject { // input // output - let settings = CurrentValueSubject<[Setting], Never>([]) + public let settings = CurrentValueSubject<[Setting], Never>([]) - init(managedObjectContext: NSManagedObjectContext, additionalPredicate: NSPredicate?) { + public init(managedObjectContext: NSManagedObjectContext, additionalPredicate: NSPredicate?) { self.fetchedResultsController = { let fetchRequest = Setting.sortedFetchRequest fetchRequest.returnsObjectsAsFaults = false @@ -55,7 +55,7 @@ final class SettingFetchedResultController: NSObject { // MARK: - NSFetchedResultsControllerDelegate extension SettingFetchedResultController: NSFetchedResultsControllerDelegate { - func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { + public func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) let objects = fetchedResultsController.fetchedObjects ?? [] diff --git a/Mastodon/Diffiable/FetchedResultsController/StatusFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift similarity index 99% rename from Mastodon/Diffiable/FetchedResultsController/StatusFetchedResultsController.swift rename to MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift index 24d8a6790..10da3f3fa 100644 --- a/Mastodon/Diffiable/FetchedResultsController/StatusFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift @@ -11,7 +11,6 @@ import Combine import CoreData import CoreDataStack import MastodonSDK -import MastodonUI final class StatusFetchedResultsController: NSObject { diff --git a/Mastodon/Diffiable/FetchedResultsController/UserFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift similarity index 99% rename from Mastodon/Diffiable/FetchedResultsController/UserFetchedResultsController.swift rename to MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift index c0922afcb..bd57314bf 100644 --- a/Mastodon/Diffiable/FetchedResultsController/UserFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift @@ -11,7 +11,6 @@ import Combine import CoreData import CoreDataStack import MastodonSDK -import MastodonUI final class UserFetchedResultsController: NSObject { diff --git a/MastodonSDK/Sources/MastodonUI/Model/UserIdentifier.swift b/MastodonSDK/Sources/MastodonCore/Model/UserIdentifier.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/Model/UserIdentifier.swift rename to MastodonSDK/Sources/MastodonCore/Model/UserIdentifier.swift diff --git a/Mastodon/Persistence/Persistence+MastodonUser.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift similarity index 96% rename from Mastodon/Persistence/Persistence+MastodonUser.swift rename to MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift index 1406f75aa..eb69c36d1 100644 --- a/Mastodon/Persistence/Persistence+MastodonUser.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift @@ -19,8 +19,8 @@ extension Persistence.MastodonUser { public let entity: Mastodon.Entity.Account public let cache: Persistence.PersistCache? public let networkDate: Date - public let log = OSLog.api - + public let log = Logger(subsystem: "MastodonUser", category: "Persistence") + public init( domain: String, entity: Mastodon.Entity.Account, @@ -127,8 +127,8 @@ extension Persistence.MastodonUser { public let entity: Mastodon.Entity.Relationship public let me: MastodonUser public let networkDate: Date - public let log = OSLog.api - + public let log = Logger(subsystem: "MastodonUser", category: "Persistence") + public init( entity: Mastodon.Entity.Relationship, me: MastodonUser, diff --git a/Mastodon/Persistence/Persistence+Notification.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Notification.swift similarity index 98% rename from Mastodon/Persistence/Persistence+Notification.swift rename to MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Notification.swift index b8c2f27fd..5273d2bbf 100644 --- a/Mastodon/Persistence/Persistence+Notification.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Notification.swift @@ -19,8 +19,8 @@ extension Persistence.Notification { public let entity: Mastodon.Entity.Notification public let me: MastodonUser public let networkDate: Date - public let log = OSLog.api - + public let log = Logger(subsystem: "Notification", category: "Persistence") + public init( domain: String, entity: Mastodon.Entity.Notification, diff --git a/Mastodon/Persistence/Persistence+Poll.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Poll.swift similarity index 98% rename from Mastodon/Persistence/Persistence+Poll.swift rename to MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Poll.swift index 1d6802aab..6f7eb60c2 100644 --- a/Mastodon/Persistence/Persistence+Poll.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Poll.swift @@ -18,8 +18,7 @@ extension Persistence.Poll { public let entity: Mastodon.Entity.Poll public let me: MastodonUser? public let networkDate: Date - public let log = OSLog.api - + public let log = Logger(subsystem: "Poll", category: "Persistence") public init( domain: String, entity: Mastodon.Entity.Poll, diff --git a/Mastodon/Persistence/Persistence+PollOption.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+PollOption.swift similarity index 96% rename from Mastodon/Persistence/Persistence+PollOption.swift rename to MastodonSDK/Sources/MastodonCore/Persistence/Persistence+PollOption.swift index 1e284ac72..a872d7edb 100644 --- a/Mastodon/Persistence/Persistence+PollOption.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+PollOption.swift @@ -18,7 +18,7 @@ extension Persistence.PollOption { public let entity: Mastodon.Entity.Poll.Option public let me: MastodonUser? public let networkDate: Date - public let log = OSLog.api + public let log = Logger(subsystem: "PollOption", category: "Persistence") public init( index: Int, diff --git a/Mastodon/Persistence/Persistence+SearchHistory.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+SearchHistory.swift similarity index 97% rename from Mastodon/Persistence/Persistence+SearchHistory.swift rename to MastodonSDK/Sources/MastodonCore/Persistence/Persistence+SearchHistory.swift index 58d4c8fb1..84a7bd15f 100644 --- a/Mastodon/Persistence/Persistence+SearchHistory.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+SearchHistory.swift @@ -17,8 +17,7 @@ extension Persistence.SearchHistory { public let entity: Entity public let me: MastodonUser public let now: Date - public let log = OSLog.api - + public let log = Logger(subsystem: "SearchHistory", category: "Persistence") public init( entity: Entity, me: MastodonUser, diff --git a/Mastodon/Persistence/Persistence+Status.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift similarity index 98% rename from Mastodon/Persistence/Persistence+Status.swift rename to MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift index b20df1496..a15e974e4 100644 --- a/Mastodon/Persistence/Persistence+Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift @@ -21,8 +21,8 @@ extension Persistence.Status { public let statusCache: Persistence.PersistCache? public let userCache: Persistence.PersistCache? public let networkDate: Date - public let log = OSLog.api - + public let log = Logger(subsystem: "Status", category: "Persistence") + public init( domain: String, entity: Mastodon.Entity.Status, diff --git a/Mastodon/Persistence/Persistence+Tag.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Tag.swift similarity index 97% rename from Mastodon/Persistence/Persistence+Tag.swift rename to MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Tag.swift index 7092a52cd..5c7130618 100644 --- a/Mastodon/Persistence/Persistence+Tag.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Tag.swift @@ -18,7 +18,7 @@ extension Persistence.Tag { public let entity: Mastodon.Entity.Tag public let me: MastodonUser? public let networkDate: Date - public let log = OSLog.api + public let log = Logger(subsystem: "Tag", category: "Persistence") public init( domain: String, diff --git a/Mastodon/Persistence/Persistence.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift similarity index 100% rename from Mastodon/Persistence/Persistence.swift rename to MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift diff --git a/Mastodon/Persistence/Protocol/MastodonEmojiContainer.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Protocol/MastodonEmojiContainer.swift similarity index 100% rename from Mastodon/Persistence/Protocol/MastodonEmojiContainer.swift rename to MastodonSDK/Sources/MastodonCore/Persistence/Protocol/MastodonEmojiContainer.swift diff --git a/Mastodon/Persistence/Protocol/MastodonFieldContainer.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Protocol/MastodonFieldContainer.swift similarity index 100% rename from Mastodon/Persistence/Protocol/MastodonFieldContainer.swift rename to MastodonSDK/Sources/MastodonCore/Persistence/Protocol/MastodonFieldContainer.swift diff --git a/Mastodon/Persistence/Protocol/MastodonMentionContainer.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Protocol/MastodonMentionContainer.swift similarity index 100% rename from Mastodon/Persistence/Protocol/MastodonMentionContainer.swift rename to MastodonSDK/Sources/MastodonCore/Persistence/Protocol/MastodonMentionContainer.swift diff --git a/Mastodon/Service/APIService/APIService+APIError.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+APIError.swift similarity index 95% rename from Mastodon/Service/APIService/APIService+APIError.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+APIError.swift index 5670f8053..6fdb973da 100644 --- a/Mastodon/Service/APIService/APIService+APIError.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+APIError.swift @@ -10,12 +10,12 @@ import MastodonSDK import MastodonLocalization extension APIService { - enum APIError: Error { + public enum APIError: Error { case implicit(ErrorReason) case explicit(ErrorReason) - enum ErrorReason { + public enum ErrorReason { // application internal error case authenticationMissing case badRequest @@ -60,7 +60,7 @@ extension APIService.APIError: LocalizedError { } } - var failureReason: String? { + public var failureReason: String? { switch errorReason { case .authenticationMissing: return "Account credential not found." case .badRequest: return "Request invalid." @@ -75,7 +75,7 @@ extension APIService.APIError: LocalizedError { } } - var helpAnchor: String? { + public var helpAnchor: String? { switch errorReason { case .authenticationMissing: return "Please request after authenticated." case .badRequest: return L10n.Common.Alerts.Common.pleaseTryAgain diff --git a/Mastodon/Service/APIService/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift similarity index 93% rename from Mastodon/Service/APIService/APIService+Account.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index 11da2f4ee..520c566c1 100644 --- a/Mastodon/Service/APIService/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -9,6 +9,7 @@ import os.log import Foundation import Combine import CommonOSLog +import MastodonCommon import MastodonSDK extension APIService { @@ -59,7 +60,7 @@ extension APIService { authorization: authorization ) .flatMap { response -> AnyPublisher, Error> in - let log = OSLog.api + let logger = Logger(subsystem: "Account", category: "API") let account = response.value let managedObjectContext = self.backgroundManagedObjectContext @@ -74,7 +75,7 @@ extension APIService { ) ) let flag = result.isNewInsertion ? "+" : "-" - os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: mastodon user [%s](%s)%s verifed", ((#file as NSString).lastPathComponent), #line, #function, flag, result.user.id, result.user.username) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): mastodon user [\(flag)](\(result.user.id))\(result.user.username) verifed") } .setFailureType(to: Error.self) .tryMap { result -> Mastodon.Response.Content in @@ -95,7 +96,7 @@ extension APIService { query: Mastodon.API.Account.UpdateCredentialQuery, authorization: Mastodon.API.OAuth.Authorization ) async throws -> Mastodon.Response.Content { - let logger = Logger(subsystem: "APIService", category: "Account") + let logger = Logger(subsystem: "Account", category: "API") let response = try await Mastodon.API.Account.updateCredentials( session: session, diff --git a/Mastodon/Service/APIService/APIService+App.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+App.swift similarity index 92% rename from Mastodon/Service/APIService/APIService+App.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+App.swift index 7153bc2af..cb46c7e76 100644 --- a/Mastodon/Service/APIService/APIService+App.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+App.swift @@ -24,7 +24,7 @@ extension APIService { func createApplication(domain: String) -> AnyPublisher, Error> { let query = Mastodon.API.App.CreateQuery( clientName: APIService.clientName, - redirectURIs: MastodonAuthenticationController.callbackURL, + redirectURIs: APIService.oauthCallbackURL, website: APIService.appWebsite ) return Mastodon.API.App.create( diff --git a/Mastodon/Service/APIService/APIService+Authentication.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Authentication.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Authentication.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Authentication.swift diff --git a/Mastodon/Service/APIService/APIService+Block.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Block.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift diff --git a/Mastodon/Service/APIService/APIService+Bookmark.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Bookmark.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift diff --git a/Mastodon/Service/APIService/APIService+CustomEmoji.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+CustomEmoji.swift similarity index 95% rename from Mastodon/Service/APIService/APIService+CustomEmoji.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+CustomEmoji.swift index 2a80eca4c..647bb4956 100644 --- a/Mastodon/Service/APIService/APIService+CustomEmoji.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+CustomEmoji.swift @@ -10,7 +10,6 @@ import Combine import CoreData import CoreDataStack import CommonOSLog -import DateToolsSwift import MastodonSDK extension APIService { diff --git a/Mastodon/Service/APIService/APIService+DomainBlock.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+DomainBlock.swift similarity index 99% rename from Mastodon/Service/APIService/APIService+DomainBlock.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+DomainBlock.swift index 222e12999..138997500 100644 --- a/Mastodon/Service/APIService/APIService+DomainBlock.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+DomainBlock.swift @@ -9,7 +9,6 @@ import Combine import CommonOSLog import CoreData import CoreDataStack -import DateToolsSwift import Foundation import MastodonSDK diff --git a/Mastodon/Service/APIService/APIService+Favorite.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Favorite.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift diff --git a/Mastodon/Service/APIService/APIService+Filter.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Filter.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Filter.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Filter.swift diff --git a/Mastodon/Service/APIService/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Follow.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift diff --git a/Mastodon/Service/APIService/APIService+FollowRequest.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+FollowRequest.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift diff --git a/Mastodon/Service/APIService/APIService+Follower.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Follower.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift diff --git a/Mastodon/Service/APIService/APIService+Following.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Following.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift diff --git a/Mastodon/Service/APIService/APIService+HashtagTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift similarity index 98% rename from Mastodon/Service/APIService/APIService+HashtagTimeline.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift index ce8783895..319cec4f3 100644 --- a/Mastodon/Service/APIService/APIService+HashtagTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift @@ -10,7 +10,6 @@ import Combine import CoreData import CoreDataStack import CommonOSLog -import DateToolsSwift import MastodonSDK extension APIService { diff --git a/Mastodon/Service/APIService/APIService+HomeTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift similarity index 99% rename from Mastodon/Service/APIService/APIService+HomeTimeline.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift index 863510af4..612b60d55 100644 --- a/Mastodon/Service/APIService/APIService+HomeTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift @@ -10,7 +10,6 @@ import Combine import CoreData import CoreDataStack import CommonOSLog -import DateToolsSwift import MastodonSDK extension APIService { diff --git a/Mastodon/Service/APIService/APIService+Instance.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift similarity index 95% rename from Mastodon/Service/APIService/APIService+Instance.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift index 4bc613b55..477b1a5b5 100644 --- a/Mastodon/Service/APIService/APIService+Instance.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift @@ -10,7 +10,6 @@ import Combine import CoreData import CoreDataStack import CommonOSLog -import DateToolsSwift import MastodonSDK extension APIService { diff --git a/Mastodon/Service/APIService/APIService+Media.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Media.swift similarity index 97% rename from Mastodon/Service/APIService/APIService+Media.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Media.swift index 0c7822c9d..58932ece8 100644 --- a/Mastodon/Service/APIService/APIService+Media.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Media.swift @@ -11,7 +11,7 @@ import MastodonSDK extension APIService { - func uploadMedia( + public func uploadMedia( domain: String, query: Mastodon.API.Media.UploadMediaQuery, mastodonAuthenticationBox: MastodonAuthenticationBox, @@ -59,7 +59,7 @@ extension APIService { extension APIService { - func getMedia( + public func getMedia( attachmentID: Mastodon.Entity.Attachment.ID, mastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { @@ -78,7 +78,7 @@ extension APIService { extension APIService { - func updateMedia( + public func updateMedia( domain: String, attachmentID: Mastodon.Entity.Attachment.ID, query: Mastodon.API.Media.UpdateMediaQuery, diff --git a/Mastodon/Service/APIService/APIService+Mute.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Mute.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift diff --git a/Mastodon/Service/APIService/APIService+Notification.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift similarity index 86% rename from Mastodon/Service/APIService/APIService+Notification.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift index 88c69847b..871c8349c 100644 --- a/Mastodon/Service/APIService/APIService+Notification.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift @@ -14,9 +14,35 @@ import OSLog import class CoreDataStack.Notification extension APIService { + + enum MastodonNotificationScope: Hashable, CaseIterable { + case everything + case mentions + + var includeTypes: [MastodonNotificationType]? { + switch self { + case .everything: return nil + case .mentions: return [.mention, .status] + } + } + + var excludeTypes: [MastodonNotificationType]? { + switch self { + case .everything: return nil + case .mentions: return [.follow, .followRequest, .reblog, .favourite, .poll] + } + } + + var _excludeTypes: [Mastodon.Entity.Notification.NotificationType]? { + switch self { + case .everything: return nil + case .mentions: return [.follow, .followRequest, .reblog, .favourite, .poll] + } + } + } func notifications( maxID: Mastodon.Entity.Status.ID?, - scope: NotificationTimelineViewModel.Scope, + scope: MastodonNotificationScope, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Notification]> { let authorization = authenticationBox.userAuthorization diff --git a/Mastodon/Service/APIService/APIService+Onboarding.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Onboarding.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Onboarding.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Onboarding.swift diff --git a/Mastodon/Service/APIService/APIService+Poll.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift similarity index 99% rename from Mastodon/Service/APIService/APIService+Poll.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift index 15a6847c7..9d2895857 100644 --- a/Mastodon/Service/APIService/APIService+Poll.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift @@ -10,7 +10,6 @@ import Combine import CoreData import CoreDataStack import CommonOSLog -import DateToolsSwift import MastodonSDK extension APIService { diff --git a/Mastodon/Service/APIService/APIService+PublicTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+PublicTimeline.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift diff --git a/Mastodon/Service/APIService/APIService+Reblog.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Reblog.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift diff --git a/Mastodon/Service/APIService/APIService+Recommend.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Recommend.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Recommend.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Recommend.swift diff --git a/Mastodon/Service/APIService/APIService+Relationship.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Relationship.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift diff --git a/Mastodon/Service/APIService/APIService+Report.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Report.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Report.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Report.swift diff --git a/Mastodon/Service/APIService/APIService+Search.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift similarity index 98% rename from Mastodon/Service/APIService/APIService+Search.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift index 724d7f611..5e6217bcf 100644 --- a/Mastodon/Service/APIService/APIService+Search.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift @@ -12,7 +12,7 @@ import CommonOSLog extension APIService { - func search( + public func search( query: Mastodon.API.V2.Search.Query, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { @@ -61,4 +61,5 @@ extension APIService { return response } + } diff --git a/Mastodon/Service/APIService/APIService+Status+Publish.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift similarity index 98% rename from Mastodon/Service/APIService/APIService+Status+Publish.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift index 2b49584f1..d2cbd3f5c 100644 --- a/Mastodon/Service/APIService/APIService+Status+Publish.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift @@ -14,7 +14,7 @@ import MastodonSDK extension APIService { - func publishStatus( + public func publishStatus( domain: String, idempotencyKey: String?, query: Mastodon.API.Statuses.PublishStatusQuery, diff --git a/Mastodon/Service/APIService/APIService+Status.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift similarity index 99% rename from Mastodon/Service/APIService/APIService+Status.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift index cf6974fbd..b7d6484dd 100644 --- a/Mastodon/Service/APIService/APIService+Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift @@ -10,7 +10,6 @@ import Combine import CoreData import CoreDataStack import CommonOSLog -import DateToolsSwift import MastodonSDK extension APIService { diff --git a/Mastodon/Service/APIService/APIService+Subscriptions.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Subscriptions.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Subscriptions.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Subscriptions.swift diff --git a/Mastodon/Service/APIService/APIService+Thread.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Thread.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift diff --git a/Mastodon/Service/APIService/APIService+Trend.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Trend.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+Trend.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Trend.swift diff --git a/Mastodon/Service/APIService/APIService+UserTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift similarity index 100% rename from Mastodon/Service/APIService/APIService+UserTimeline.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift diff --git a/Mastodon/Service/APIService/APIService+WebFinger.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+WebFinger.swift similarity index 97% rename from Mastodon/Service/APIService/APIService+WebFinger.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+WebFinger.swift index b49ad9e31..c506d43dc 100644 --- a/Mastodon/Service/APIService/APIService+WebFinger.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+WebFinger.swift @@ -10,7 +10,6 @@ import Combine import CoreData import CoreDataStack import CommonOSLog -import DateToolsSwift import MastodonSDK extension APIService { diff --git a/Mastodon/Service/APIService/APIService.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService.swift similarity index 71% rename from Mastodon/Service/APIService/APIService.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService.swift index dabdadfea..b151ee540 100644 --- a/Mastodon/Service/APIService/APIService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService.swift @@ -12,9 +12,12 @@ import CoreData import CoreDataStack import MastodonSDK import AlamofireImage -import AlamofireNetworkActivityIndicator +// import AlamofireNetworkActivityIndicator -final class APIService { +public final class APIService { + + public static let callbackURLScheme = "mastodon" + public static let oauthCallbackURL = "mastodon://joinmastodon.org/oauth" var disposeBag = Set() @@ -27,7 +30,7 @@ final class APIService { // output let error = PassthroughSubject() - init(backgroundManagedObjectContext: NSManagedObjectContext) { + public init(backgroundManagedObjectContext: NSManagedObjectContext) { self.backgroundManagedObjectContext = backgroundManagedObjectContext self.session = URLSession(configuration: .default) @@ -35,9 +38,9 @@ final class APIService { URLCache.shared = URLCache(memoryCapacity: 10 * 1024 * 1024, diskCapacity: 50 * 1024 * 1024, diskPath: nil) // enable network activity manager for AlamofireImage - NetworkActivityIndicatorManager.shared.isEnabled = true - NetworkActivityIndicatorManager.shared.startDelay = 0.2 - NetworkActivityIndicatorManager.shared.completionDelay = 0.5 + // NetworkActivityIndicatorManager.shared.isEnabled = true + // NetworkActivityIndicatorManager.shared.startDelay = 0.2 + // NetworkActivityIndicatorManager.shared.completionDelay = 0.5 UIImageView.af.sharedImageDownloader = ImageDownloader(downloadPrioritization: .lifo) } diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Instance.swift b/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+Instance.swift similarity index 99% rename from Mastodon/Service/APIService/CoreData/APIService+CoreData+Instance.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+Instance.swift index 614d098aa..2127a2981 100644 --- a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Instance.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+Instance.swift @@ -18,7 +18,7 @@ extension APIService.CoreData { domain: String, entity: Mastodon.Entity.Instance, networkDate: Date, - log: OSLog + log: Logger ) -> (instance: Instance, isCreated: Bool) { // fetch old mastodon user let old: Instance? = { diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonAuthentication.swift b/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+MastodonAuthentication.swift similarity index 100% rename from Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonAuthentication.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+MastodonAuthentication.swift diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Setting.swift b/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+Setting.swift similarity index 100% rename from Mastodon/Service/APIService/CoreData/APIService+CoreData+Setting.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+Setting.swift diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Subscriptions.swift b/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+Subscriptions.swift similarity index 100% rename from Mastodon/Service/APIService/CoreData/APIService+CoreData+Subscriptions.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+Subscriptions.swift diff --git a/Mastodon/Service/AuthenticationService.swift b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift similarity index 89% rename from Mastodon/Service/AuthenticationService.swift rename to MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift index 97afde932..105543d52 100644 --- a/Mastodon/Service/AuthenticationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift @@ -12,7 +12,7 @@ import CoreData import CoreDataStack import MastodonSDK -final class AuthenticationService: NSObject { +public final class AuthenticationService: NSObject { var disposeBag = Set() @@ -23,10 +23,10 @@ final class AuthenticationService: NSObject { let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController // output - let mastodonAuthentications = CurrentValueSubject<[MastodonAuthentication], Never>([]) - let mastodonAuthenticationBoxes = CurrentValueSubject<[MastodonAuthenticationBox], Never>([]) - let activeMastodonAuthentication = CurrentValueSubject(nil) - let activeMastodonAuthenticationBox = CurrentValueSubject(nil) + public let mastodonAuthentications = CurrentValueSubject<[MastodonAuthentication], Never>([]) + public let mastodonAuthenticationBoxes = CurrentValueSubject<[MastodonAuthenticationBox], Never>([]) + public let activeMastodonAuthentication = CurrentValueSubject(nil) + public let activeMastodonAuthenticationBox = CurrentValueSubject(nil) init( managedObjectContext: NSManagedObjectContext, @@ -94,7 +94,7 @@ final class AuthenticationService: NSObject { extension AuthenticationService { - func activeMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher, Never> { + public func activeMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher, Never> { var isActive = false var _mastodonAuthentication: MastodonAuthentication? @@ -180,11 +180,11 @@ extension AuthenticationService { // MARK: - NSFetchedResultsControllerDelegate extension AuthenticationService: NSFetchedResultsControllerDelegate { - func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + public func controllerWillChangeContent(_ controller: NSFetchedResultsController) { os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) } - func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { if controller === mastodonAuthenticationFetchedResultsController { mastodonAuthentications.value = mastodonAuthenticationFetchedResultsController.fetchedObjects ?? [] } diff --git a/Mastodon/Service/BlockDomainService.swift b/MastodonSDK/Sources/MastodonCore/Service/BlockDomainService.swift similarity index 99% rename from Mastodon/Service/BlockDomainService.swift rename to MastodonSDK/Sources/MastodonCore/Service/BlockDomainService.swift index 90d860143..c7b2c2333 100644 --- a/Mastodon/Service/BlockDomainService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/BlockDomainService.swift @@ -13,7 +13,7 @@ import MastodonSDK import OSLog import UIKit -final class BlockDomainService { +public final class BlockDomainService { // input weak var backgroundManagedObjectContext: NSManagedObjectContext? weak var authenticationService: AuthenticationService? diff --git a/MastodonSDK/Sources/MastodonUI/Service/BlurhashImageCacheService.swift b/MastodonSDK/Sources/MastodonCore/Service/BlurhashImageCacheService.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/Service/BlurhashImageCacheService.swift rename to MastodonSDK/Sources/MastodonCore/Service/BlurhashImageCacheService.swift diff --git a/Mastodon/Service/EmojiService/EmojiService+CustomEmojiViewModel+LoadState.swift b/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService+CustomEmojiViewModel+LoadState.swift similarity index 100% rename from Mastodon/Service/EmojiService/EmojiService+CustomEmojiViewModel+LoadState.swift rename to MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService+CustomEmojiViewModel+LoadState.swift diff --git a/Mastodon/Service/EmojiService/EmojiService+CustomEmojiViewModel.swift b/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService+CustomEmojiViewModel.swift similarity index 86% rename from Mastodon/Service/EmojiService/EmojiService+CustomEmojiViewModel.swift rename to MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService+CustomEmojiViewModel.swift index b0ee6cb80..f28dfa805 100644 --- a/Mastodon/Service/EmojiService/EmojiService+CustomEmojiViewModel.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService+CustomEmojiViewModel.swift @@ -9,9 +9,10 @@ import Foundation import Combine import GameplayKit import MastodonSDK +import MastodonCommon extension EmojiService { - final class CustomEmojiViewModel { + public final class CustomEmojiViewModel { var disposeBag = Set() @@ -31,10 +32,10 @@ extension EmojiService { stateMachine.enter(LoadState.Initial.self) return stateMachine }() - let emojis = CurrentValueSubject<[Mastodon.Entity.Emoji], Never>([]) - let emojiDict = CurrentValueSubject<[String: [Mastodon.Entity.Emoji]], Never>([:]) - let emojiMapping = CurrentValueSubject<[String: String], Never>([:]) - let emojiTrie = CurrentValueSubject?, Never>(nil) + public let emojis = CurrentValueSubject<[Mastodon.Entity.Emoji], Never>([]) + public let emojiDict = CurrentValueSubject<[String: [Mastodon.Entity.Emoji]], Never>([:]) + public let emojiMapping = CurrentValueSubject<[String: String], Never>([:]) + public let emojiTrie = CurrentValueSubject?, Never>(nil) private var learnedEmoji: Set = Set() diff --git a/Mastodon/Service/EmojiService/EmojiService.swift b/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService.swift similarity index 97% rename from Mastodon/Service/EmojiService/EmojiService.swift rename to MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService.swift index a11ebb8bc..6f44178da 100644 --- a/Mastodon/Service/EmojiService/EmojiService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService.swift @@ -10,7 +10,7 @@ import Foundation import Combine import MastodonSDK -final class EmojiService { +public final class EmojiService { weak var apiService: APIService? diff --git a/Mastodon/Service/EmojiService/Trie.swift b/MastodonSDK/Sources/MastodonCore/Service/Emoji/Trie.swift similarity index 88% rename from Mastodon/Service/EmojiService/Trie.swift rename to MastodonSDK/Sources/MastodonCore/Service/Emoji/Trie.swift index 3bf9eeaf0..cfb9c4362 100644 --- a/Mastodon/Service/EmojiService/Trie.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Emoji/Trie.swift @@ -7,20 +7,20 @@ import Foundation -struct Trie { - let isElement: Bool - let valueSet: NSMutableSet - var children: [Element: Trie] +public struct Trie { + public let isElement: Bool + public let valueSet: NSMutableSet + public var children: [Element: Trie] } extension Trie { - init() { + public init() { isElement = false valueSet = NSMutableSet() children = [:] } - init(_ key: ArraySlice, value: Any) { + public init(_ key: ArraySlice, value: Any) { if let (head, tail) = key.decomposed { let children = [head: Trie(tail, value: value)] self = Trie(isElement: false, valueSet: NSMutableSet(), children: children) @@ -31,7 +31,7 @@ extension Trie { } extension Trie { - var elements: [[Element]] { + public var elements: [[Element]] { var result: [[Element]] = isElement ? [[]] : [] for (key, value) in children { result += value.elements.map { [key] + $0 } @@ -40,12 +40,6 @@ extension Trie { } } -//extension Array { -// var slice: ArraySlice { -// return ArraySlice(self) -// } -//} - extension ArraySlice { var decomposed: (Element, ArraySlice)? { return isEmpty ? nil : (self[startIndex], self.dropFirst()) diff --git a/Mastodon/Service/InstanceService.swift b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift similarity index 97% rename from Mastodon/Service/InstanceService.swift rename to MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift index 03b8dfd4e..7632704b0 100644 --- a/Mastodon/Service/InstanceService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift @@ -12,7 +12,7 @@ import CoreData import CoreDataStack import MastodonSDK -final class InstanceService { +public final class InstanceService { var disposeBag = Set() @@ -59,7 +59,7 @@ extension InstanceService { domain: domain, entity: response.value, networkDate: response.networkDate, - log: OSLog.api + log: Logger(subsystem: "Update", category: "InstanceService") ) // update relationship diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift b/MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService+UploadState.swift similarity index 100% rename from Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift rename to MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService+UploadState.swift diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift b/MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService.swift similarity index 99% rename from Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift rename to MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService.swift index e42c9bf2e..edb8e7ae6 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService.swift @@ -12,7 +12,6 @@ import PhotosUI import GameplayKit import MobileCoreServices import MastodonSDK -import MastodonUI protocol MastodonAttachmentServiceDelegate: AnyObject { func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) diff --git a/NotificationService/MastodonPushNotification.swift b/MastodonSDK/Sources/MastodonCore/Service/Notification/MastodonPushNotification.swift similarity index 73% rename from NotificationService/MastodonPushNotification.swift rename to MastodonSDK/Sources/MastodonCore/Service/Notification/MastodonPushNotification.swift index 7d961f31d..047ba7757 100644 --- a/NotificationService/MastodonPushNotification.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Notification/MastodonPushNotification.swift @@ -7,20 +7,17 @@ import Foundation -struct MastodonPushNotification: Codable { +public struct MastodonPushNotification: Codable { - let accessToken: String -// var accessToken: String { -// return String.normalize(base64String: _accessToken) -// } + public let accessToken: String - let notificationID: Int - let notificationType: String + public let notificationID: Int + public let notificationType: String - let preferredLocale: String? - let icon: String? - let title: String - let body: String + public let preferredLocale: String? + public let icon: String? + public let title: String + public let body: String enum CodingKeys: String, CodingKey { case accessToken = "access_token" diff --git a/Mastodon/Service/NotificationService.swift b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift similarity index 91% rename from Mastodon/Service/NotificationService.swift rename to MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift index e4e7508a3..4e08cd659 100644 --- a/Mastodon/Service/NotificationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift @@ -11,9 +11,9 @@ import Combine import CoreData import CoreDataStack import MastodonSDK -import AppShared +import MastodonCommon -final class NotificationService { +public final class NotificationService { var disposeBag = Set() @@ -22,15 +22,15 @@ final class NotificationService { // input weak var apiService: APIService? weak var authenticationService: AuthenticationService? - let isNotificationPermissionGranted = CurrentValueSubject(false) - let deviceToken = CurrentValueSubject(nil) - let applicationIconBadgeNeedsUpdate = CurrentValueSubject(Void()) + public let isNotificationPermissionGranted = CurrentValueSubject(false) + public let deviceToken = CurrentValueSubject(nil) + public let applicationIconBadgeNeedsUpdate = CurrentValueSubject(Void()) // output /// [Token: NotificationViewModel] - let notificationSubscriptionDict: [String: NotificationViewModel] = [:] - let unreadNotificationCountDidUpdate = CurrentValueSubject(Void()) - let requestRevealNotificationPublisher = PassthroughSubject() + public let notificationSubscriptionDict: [String: NotificationViewModel] = [:] + public let unreadNotificationCountDidUpdate = CurrentValueSubject(Void()) + public let requestRevealNotificationPublisher = PassthroughSubject() init( apiService: APIService, @@ -55,7 +55,8 @@ final class NotificationService { guard let _ = self else { return } guard let deviceToken = deviceToken else { return } let token = [UInt8](deviceToken).toHexString() - os_log(.info, log: .api, "%{public}s[%{public}ld], %{public}s: deviceToken: %s", ((#file as NSString).lastPathComponent), #line, #function, token) + let logger = Logger(subsystem: "DeviceToken", category: "NotificationService") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): deviceToken: \(token)") } .store(in: &disposeBag) @@ -121,7 +122,7 @@ extension NotificationService { return _notificationSubscription } - func handle( + public func handle( pushNotification: MastodonPushNotification ) { defer { @@ -239,7 +240,7 @@ extension NotificationService { // MARK: - NotificationViewModel extension NotificationService { - final class NotificationViewModel { + public final class NotificationViewModel { var disposeBag = Set() diff --git a/Mastodon/Service/PhotoLibraryService.swift b/MastodonSDK/Sources/MastodonCore/Service/PhotoLibraryService.swift similarity index 94% rename from Mastodon/Service/PhotoLibraryService.swift rename to MastodonSDK/Sources/MastodonCore/Service/PhotoLibraryService.swift index 99dcb61ff..053ab3586 100644 --- a/Mastodon/Service/PhotoLibraryService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/PhotoLibraryService.swift @@ -11,20 +11,19 @@ import Combine import Photos import Alamofire import AlamofireImage -import FLAnimatedImage -final class PhotoLibraryService: NSObject { +public final class PhotoLibraryService: NSObject { } extension PhotoLibraryService { - enum PhotoLibraryError: Error { + public enum PhotoLibraryError: Error { case noPermission case badPayload } - enum ImageSource { + public enum ImageSource { case url(URL) case image(UIImage) } @@ -33,7 +32,7 @@ extension PhotoLibraryService { extension PhotoLibraryService { - func save(imageSource source: ImageSource) -> AnyPublisher { + public func save(imageSource source: ImageSource) -> AnyPublisher { let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) let notificationFeedbackGenerator = UINotificationFeedbackGenerator() @@ -68,7 +67,7 @@ extension PhotoLibraryService { extension PhotoLibraryService { - func copy(imageSource source: ImageSource) -> AnyPublisher { + public func copy(imageSource source: ImageSource) -> AnyPublisher { let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) let notificationFeedbackGenerator = UINotificationFeedbackGenerator() diff --git a/Mastodon/Service/PlaceholderImageCacheService.swift b/MastodonSDK/Sources/MastodonCore/Service/PlaceholderImageCacheService.swift similarity index 97% rename from Mastodon/Service/PlaceholderImageCacheService.swift rename to MastodonSDK/Sources/MastodonCore/Service/PlaceholderImageCacheService.swift index ecfde6d49..0f43db9f8 100644 --- a/Mastodon/Service/PlaceholderImageCacheService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/PlaceholderImageCacheService.swift @@ -8,7 +8,7 @@ import UIKit import AlamofireImage -final class PlaceholderImageCacheService { +public final class PlaceholderImageCacheService { let cache = NSCache() diff --git a/Mastodon/Service/PlaybackState.swift b/MastodonSDK/Sources/MastodonCore/Service/PlaybackState.swift similarity index 100% rename from Mastodon/Service/PlaybackState.swift rename to MastodonSDK/Sources/MastodonCore/Service/PlaybackState.swift diff --git a/Mastodon/Service/SettingService.swift b/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift similarity index 91% rename from Mastodon/Service/SettingService.swift rename to MastodonSDK/Sources/MastodonCore/Service/SettingService.swift index bd571d8f4..fc7151605 100644 --- a/Mastodon/Service/SettingService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift @@ -14,7 +14,7 @@ import MastodonAsset import MastodonLocalization import MastodonCommon -final class SettingService { +public final class SettingService { var disposeBag = Set() @@ -108,7 +108,9 @@ final class SettingService { }) } .store(in: &disposeBag) - + + let logger = Logger(subsystem: "Notification", category: "SettingService") + Publishers.CombineLatest3( notificationService.deviceToken, currentSetting.eraseToAnyPublisher(), @@ -158,12 +160,12 @@ final class SettingService { .sink { completion in switch completion { case .failure(let error): - os_log(.info, log: .api, "%{public}s[%{public}ld], %{public}s: [Push Notification] subscribe failure: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Push Notification] subscribe failure: \(error.localizedDescription)") case .finished: - os_log(.info, log: .default, "%{public}s[%{public}ld], %{public}s: [Push Notification] subscribe success", ((#file as NSString).lastPathComponent), #line, #function) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Push Notification] subscribe success") } } receiveValue: { response in - os_log(.info, log: .default, "%{public}s[%{public}ld], %{public}s: [Push Notification] subscribe response: %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.endpoint) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): subscribe response: \(response.value.endpoint)") } .store(in: &self.disposeBag) }) diff --git a/Mastodon/Service/StatusFilterService.swift b/MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift similarity index 98% rename from Mastodon/Service/StatusFilterService.swift rename to MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift index b5afd08ab..692a43706 100644 --- a/Mastodon/Service/StatusFilterService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift @@ -13,7 +13,7 @@ import CoreDataStack import MastodonSDK import MastodonMeta -final class StatusFilterService { +public final class StatusFilterService { var disposeBag = Set() diff --git a/MastodonSDK/Sources/MastodonCore/Service/StatusPublishService.swift b/MastodonSDK/Sources/MastodonCore/Service/StatusPublishService.swift new file mode 100644 index 000000000..a411f30c2 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Service/StatusPublishService.swift @@ -0,0 +1,79 @@ +// +// StatusPublishService.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-26. +// + +import os.log +import Foundation +import Intents +import Combine +import CoreData +import CoreDataStack +import MastodonSDK +import UIKit + +public final class StatusPublishService { + + var disposeBag = Set() + + let workingQueue = DispatchQueue(label: "org.joinmastodon.app.StatusPublishService.working-queue") + + // input + // var viewModels = CurrentValueSubject<[ComposeViewModel], Never>([]) // use strong reference to retain the view models + + // output + let composeViewModelDidUpdatePublisher = PassthroughSubject() + // let latestPublishingComposeViewModel = CurrentValueSubject(nil) + + init() { +// Publishers.CombineLatest( +// viewModels.eraseToAnyPublisher(), +// composeViewModelDidUpdatePublisher.eraseToAnyPublisher() +// ) +// .map { viewModels, _ in viewModels.last } +// .assign(to: \.value, on: latestPublishingComposeViewModel) +// .store(in: &disposeBag) + } + +} + +extension StatusPublishService { + +// func publish(composeViewModel: ComposeViewModel) { +// workingQueue.sync { +// guard !self.viewModels.value.contains(where: { $0 === composeViewModel }) else { return } +// self.viewModels.value = self.viewModels.value + [composeViewModel] +// +// composeViewModel.publishStateMachinePublisher +// .receive(on: DispatchQueue.main) +// .sink { [weak self, weak composeViewModel] state in +// guard let self = self else { return } +// guard let composeViewModel = composeViewModel else { return } +// +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: composeViewModelDidUpdate", ((#file as NSString).lastPathComponent), #line, #function) +// self.composeViewModelDidUpdatePublisher.send() +// +// switch state { +// case is ComposeViewModel.PublishState.Finish: +// self.remove(composeViewModel: composeViewModel) +// default: +// break +// } +// } +// .store(in: &composeViewModel.disposeBag) // cancel subscription when viewModel dealloc +// } +// } +// +// func remove(composeViewModel: ComposeViewModel) { +// workingQueue.async { +// var viewModels = self.viewModels.value +// viewModels.removeAll(where: { $0 === composeViewModel }) +// self.viewModels.value = viewModels +// +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: composeViewModel removed", ((#file as NSString).lastPathComponent), #line, #function) +// } +// } + +} diff --git a/MastodonSDK/Sources/MastodonUI/Service/ThemeService/MastodonTheme.swift b/MastodonSDK/Sources/MastodonCore/Service/Theme/MastodonTheme.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/Service/ThemeService/MastodonTheme.swift rename to MastodonSDK/Sources/MastodonCore/Service/Theme/MastodonTheme.swift diff --git a/MastodonSDK/Sources/MastodonUI/Service/ThemeService/SystemTheme.swift b/MastodonSDK/Sources/MastodonCore/Service/Theme/SystemTheme.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/Service/ThemeService/SystemTheme.swift rename to MastodonSDK/Sources/MastodonCore/Service/Theme/SystemTheme.swift diff --git a/MastodonSDK/Sources/MastodonUI/Service/ThemeService/Theme.swift b/MastodonSDK/Sources/MastodonCore/Service/Theme/Theme.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/Service/ThemeService/Theme.swift rename to MastodonSDK/Sources/MastodonCore/Service/Theme/Theme.swift diff --git a/Mastodon/Extension/MastodonUI/ThemeService.swift b/MastodonSDK/Sources/MastodonCore/Service/Theme/ThemeService.swift similarity index 73% rename from Mastodon/Extension/MastodonUI/ThemeService.swift rename to MastodonSDK/Sources/MastodonCore/Service/Theme/ThemeService.swift index 5fe213d06..869e6875f 100644 --- a/Mastodon/Extension/MastodonUI/ThemeService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Theme/ThemeService.swift @@ -2,15 +2,41 @@ // ThemeService.swift // Mastodon // -// Created by MainasuK on 2022-4-13. +// Created by MainasuK Cirno on 2021-7-5. // import UIKit +import Combine import MastodonCommon -import MastodonUI + +// ref: https://zamzam.io/protocol-oriented-themes-for-ios-apps/ +public final class ThemeService { + + public static let tintColor: UIColor = .label + + // MARK: - Singleton + public static let shared = ThemeService() + + public let currentTheme: CurrentValueSubject + + private init() { + let theme = ThemeName(rawValue: UserDefaults.shared.currentThemeNameRawValue)?.theme ?? ThemeName.mastodon.theme + currentTheme = CurrentValueSubject(theme) + } + +} + +extension ThemeName { + public var theme: Theme { + switch self { + case .system: return SystemTheme() + case .mastodon: return MastodonTheme() + } + } +} extension ThemeService { - func set(themeName: ThemeName) { + public func set(themeName: ThemeName) { UserDefaults.shared.currentThemeNameRawValue = themeName.rawValue let theme = themeName.theme @@ -18,7 +44,7 @@ extension ThemeService { currentTheme.value = theme } - func apply(theme: Theme) { + public func apply(theme: Theme) { // set navigation bar appearance let appearance = UINavigationBarAppearance() appearance.configureWithDefaultBackground() @@ -59,8 +85,9 @@ extension ThemeService { // set table view cell appearance UITableViewCell.appearance().backgroundColor = theme.tableViewCellBackgroundColor - UITableViewCell.appearance(whenContainedInInstancesOf: [SettingsViewController.self]).backgroundColor = theme.secondarySystemGroupedBackgroundColor - UITableViewCell.appearance().selectionColor = theme.tableViewCellSelectionBackgroundColor + // FIXME: refactor + // UITableViewCell.appearance(whenContainedInInstancesOf: [SettingsViewController.self]).backgroundColor = theme.secondarySystemGroupedBackgroundColor + // UITableViewCell.appearance().selectionColor = theme.tableViewCellSelectionBackgroundColor // set search bar appearance UISearchBar.appearance().tintColor = ThemeService.tintColor diff --git a/MastodonSDK/Sources/MastodonUI/Vendor/BlurHashDecode.swift b/MastodonSDK/Sources/MastodonCore/Vendor/BlurHashDecode.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/Vendor/BlurHashDecode.swift rename to MastodonSDK/Sources/MastodonCore/Vendor/BlurHashDecode.swift diff --git a/MastodonSDK/Sources/MastodonUI/Vendor/BlurHashEncode.swift b/MastodonSDK/Sources/MastodonCore/Vendor/BlurHashEncode.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/Vendor/BlurHashEncode.swift rename to MastodonSDK/Sources/MastodonCore/Vendor/BlurHashEncode.swift diff --git a/MastodonSDK/Sources/MastodonUI/Vendor/ItemProviderLoader.swift b/MastodonSDK/Sources/MastodonCore/Vendor/ItemProviderLoader.swift similarity index 99% rename from MastodonSDK/Sources/MastodonUI/Vendor/ItemProviderLoader.swift rename to MastodonSDK/Sources/MastodonCore/Vendor/ItemProviderLoader.swift index ef0c36f1b..9899620fe 100644 --- a/MastodonSDK/Sources/MastodonUI/Vendor/ItemProviderLoader.swift +++ b/MastodonSDK/Sources/MastodonCore/Vendor/ItemProviderLoader.swift @@ -1,6 +1,6 @@ // // ItemProviderLoader.swift -// MastodonUI +// MastodonCore // // Created by MainasuK Cirno on 2021-3-18. // diff --git a/Mastodon/Extension/Array.swift b/MastodonSDK/Sources/MastodonExtension/Array.swift similarity index 93% rename from Mastodon/Extension/Array.swift rename to MastodonSDK/Sources/MastodonExtension/Array.swift index 42f8594d1..3ad5b13ee 100644 --- a/Mastodon/Extension/Array.swift +++ b/MastodonSDK/Sources/MastodonExtension/Array.swift @@ -9,7 +9,7 @@ import Foundation /// https://www.hackingwithswift.com/example-code/language/how-to-remove-duplicate-items-from-an-array extension Array where Element: Hashable { - func removingDuplicates() -> [Element] { + public func removingDuplicates() -> [Element] { var addedDict = [Element: Bool]() return filter { @@ -17,7 +17,7 @@ extension Array where Element: Hashable { } } - mutating func removeDuplicates() { + public mutating func removeDuplicates() { self = self.removingDuplicates() } } @@ -38,12 +38,12 @@ extension Array where Element: Hashable { // extension Array { - init(reserveCapacity: Int) { + public init(reserveCapacity: Int) { self = Array() self.reserveCapacity(reserveCapacity) } - var slice: ArraySlice { + public var slice: ArraySlice { self[self.startIndex ..< self.endIndex] } } diff --git a/Mastodon/Extension/NSManagedObjectContext.swift b/MastodonSDK/Sources/MastodonExtension/NSManagedObjectContext.swift similarity index 77% rename from Mastodon/Extension/NSManagedObjectContext.swift rename to MastodonSDK/Sources/MastodonExtension/NSManagedObjectContext.swift index 9c569a8f4..ecf43d7d9 100644 --- a/Mastodon/Extension/NSManagedObjectContext.swift +++ b/MastodonSDK/Sources/MastodonExtension/NSManagedObjectContext.swift @@ -9,7 +9,7 @@ import Foundation import CoreData extension NSManagedObjectContext { - func safeFetch(_ request: NSFetchRequest) -> [T] where T : NSFetchRequestResult { + public func safeFetch(_ request: NSFetchRequest) -> [T] where T : NSFetchRequestResult { do { return try fetch(request) } catch { diff --git a/MastodonSDK/Sources/MastodonUI/Extension/UIView.swift b/MastodonSDK/Sources/MastodonUI/Extension/UIView.swift index 0489965b5..c66f111a0 100644 --- a/MastodonSDK/Sources/MastodonUI/Extension/UIView.swift +++ b/MastodonSDK/Sources/MastodonUI/Extension/UIView.swift @@ -6,6 +6,7 @@ // import UIKit +import MastodonCore extension UIView { diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentView.swift new file mode 100644 index 000000000..21161a0a6 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentView.swift @@ -0,0 +1,18 @@ +// +// ComposeContentView.swift +// +// +// Created by MainasuK on 22/9/30. +// + +import SwiftUI + +public struct ComposeContentView: View { + public var body: some View { + ScrollView { + VStack { + Text("Hello") + } + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift new file mode 100644 index 000000000..18604c4b3 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -0,0 +1,24 @@ +// +// ComposeContentViewController.swift +// +// +// Created by MainasuK on 22/9/30. +// + +import os.log +import UIKit + +public final class ComposeContentViewController: UIViewController { + + let logger = Logger(subsystem: "ComposeContentViewController", category: "ViewController") + + + +} + +extension ComposeContentViewController { + public override func viewDidLoad() { + super.viewDidLoad() + + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift new file mode 100644 index 000000000..9fa94c5e0 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -0,0 +1,20 @@ +// +// ComposeContentViewModel.swift +// +// +// Created by MainasuK on 22/9/30. +// + +import Foundation +import MastodonCore + +final class ComposeContentViewModel: ObservableObject { + + // input + let context: AppContext + + init(context: AppContext) { + self.context = context + } + +} diff --git a/MastodonSDK/Sources/MastodonUI/Service/ThemeService/ThemeService.swift b/MastodonSDK/Sources/MastodonUI/Service/ThemeService/ThemeService.swift deleted file mode 100644 index 394a5f896..000000000 --- a/MastodonSDK/Sources/MastodonUI/Service/ThemeService/ThemeService.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// ThemeService.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-7-5. -// - -import UIKit -import Combine -import MastodonCommon - -// ref: https://zamzam.io/protocol-oriented-themes-for-ios-apps/ -public final class ThemeService { - - public static let tintColor: UIColor = .label - - // MARK: - Singleton - public static let shared = ThemeService() - - public let currentTheme: CurrentValueSubject - - private init() { - let theme = ThemeName(rawValue: UserDefaults.shared.currentThemeNameRawValue)?.theme ?? ThemeName.mastodon.theme - currentTheme = CurrentValueSubject(theme) - } - -} - -extension ThemeName { - public var theme: Theme { - switch self { - case .system: return SystemTheme() - case .mastodon: return MastodonTheme() - } - } -} diff --git a/MastodonSDK/Sources/MastodonUI/View/Container/AttachmentView.swift b/MastodonSDK/Sources/MastodonUI/View/Container/AttachmentView.swift new file mode 100644 index 000000000..150ff5f51 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/View/Container/AttachmentView.swift @@ -0,0 +1,29 @@ +// +// AttachmentView.swift +// +// +// Created by MainasuK on 22/9/30. +// + +import SwiftUI + +public struct AttachmentView: View { + + @ObservedObject public var viewModel: ViewModel + + public init(viewModel: ViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + Text("Hi") + } + +} + +extension AttachmentView { + public class ViewModel: ObservableObject { + + public init() { } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+Configuration.swift index 0b63f848e..d8fbbf6ca 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+Configuration.swift @@ -7,6 +7,7 @@ import UIKit import MastodonSDK +import MastodonCore extension FamiliarFollowersDashboardView { public func configure(familiarFollowers: Mastodon.Entity.FamiliarFollowers?) { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift index cfe9e73ce..76897a46f 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift @@ -11,6 +11,7 @@ import Combine import CoreData import Photos import AlamofireImage +import MastodonCore extension MediaView { public class Configuration: Hashable { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift index 0e8c394b7..974069801 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift @@ -13,6 +13,7 @@ import MastodonSDK import MastodonAsset import MastodonLocalization import MastodonExtension +import MastodonCore import CoreData import CoreDataStack diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/PollOptionView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/PollOptionView+ViewModel.swift index 7a48ddc3a..b6ea11d49 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/PollOptionView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/PollOptionView+ViewModel.swift @@ -10,6 +10,7 @@ import Combine import CoreData import MetaTextKit import MastodonAsset +import MastodonCore extension PollOptionView { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+ViewModel.swift index 90fedf034..55d270f74 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+ViewModel.swift @@ -14,6 +14,7 @@ import CoreDataStack import MastodonLocalization import MastodonAsset import MastodonSDK +import MastodonCore extension ProfileCardView { public class ViewModel: ObservableObject { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index bd80afe86..648f256b6 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -11,6 +11,7 @@ import Combine import CoreData import Meta import MastodonSDK +import MastodonCore import MastodonAsset import MastodonLocalization import MastodonExtension diff --git a/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+TimelineTests.swift b/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+TimelineTests.swift index 371b7b034..68e5bb669 100644 --- a/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+TimelineTests.swift +++ b/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+TimelineTests.swift @@ -20,20 +20,25 @@ extension MastodonSDKTests { let theExpectation = expectation(description: "Fetch Public Timeline") let query = Mastodon.API.Timeline.PublicTimelineQuery() - Mastodon.API.Timeline.public(session: session, domain: domain, query: query) - .receive(on: DispatchQueue.main) - .sink { completion in - switch completion { - case .failure(let error): - XCTFail(error.localizedDescription) - case .finished: - break - } - } receiveValue: { response in - XCTAssert(!response.value.isEmpty) - theExpectation.fulfill() + Mastodon.API.Timeline.public( + session: session, + domain: domain, + query: query, + authorization: nil + ) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .failure(let error): + XCTFail(error.localizedDescription) + case .finished: + break } - .store(in: &disposeBag) + } receiveValue: { response in + XCTAssert(!response.value.isEmpty) + theExpectation.fulfill() + } + .store(in: &disposeBag) wait(for: [theExpectation], timeout: 10.0) } diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift index c3d02933b..d38884aff 100644 --- a/NotificationService/NotificationService.swift +++ b/NotificationService/NotificationService.swift @@ -9,7 +9,7 @@ import UserNotifications import CommonOSLog import CryptoKit import AlamofireImage -import AppShared +import MastodonCore class NotificationService: UNNotificationServiceExtension { diff --git a/Podfile b/Podfile index a64cd0e55..774148fa0 100644 --- a/Podfile +++ b/Podfile @@ -35,14 +35,6 @@ target 'AppShared' do use_frameworks! end -plugin 'cocoapods-keys', { - :project => "Mastodon", - :keys => [ - "notification_endpoint", - "notification_endpoint_debug" - ] -} - post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| diff --git a/Podfile.lock b/Podfile.lock index 629a48a87..453610694 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -2,7 +2,6 @@ PODS: - DateToolsSwift (5.0.0) - FLEX (4.4.1) - Kanna (5.2.7) - - Keys (1.0.1) - Sourcery (1.6.1): - Sourcery/CLI-Only (= 1.6.1) - Sourcery/CLI-Only (1.6.1) @@ -14,7 +13,6 @@ DEPENDENCIES: - DateToolsSwift (~> 5.0.0) - FLEX (~> 4.4.0) - Kanna (~> 5.2.2) - - Keys (from `Pods/CocoaPodsKeys`) - Sourcery (~> 1.6.1) - SwiftGen (~> 6.4.0) - "UITextField+Shake (~> 1.2)" @@ -30,20 +28,15 @@ SPEC REPOS: - "UITextField+Shake" - XLPagerTabStrip -EXTERNAL SOURCES: - Keys: - :path: Pods/CocoaPodsKeys - SPEC CHECKSUMS: DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6 FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab Kanna: 01cfbddc127f5ff0963692f285fcbc8a9d62d234 - Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 Sourcery: f3759f803bd0739f74fc92a4341eed0473ce61ac SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 "UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3 XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5 -PODFILE CHECKSUM: 1ac960a2c981ef98f7c24a3bba57bdabc1f66103 +PODFILE CHECKSUM: d95968ab70ea5121c21dfd801aa36b12bcd59c9d COCOAPODS: 1.11.3 diff --git a/ShareActionExtension/Scene/ShareViewController.swift b/ShareActionExtension/Scene/ShareViewController.swift index 542fce6d5..a142aa69b 100644 --- a/ShareActionExtension/Scene/ShareViewController.swift +++ b/ShareActionExtension/Scene/ShareViewController.swift @@ -12,6 +12,7 @@ import MastodonUI import SwiftUI import MastodonAsset import MastodonLocalization +import MastodonCore import MastodonUI class ShareViewController: UIViewController { diff --git a/ShareActionExtension/Scene/ShareViewModel.swift b/ShareActionExtension/Scene/ShareViewModel.swift index c56f8ecfd..63a7132f6 100644 --- a/ShareActionExtension/Scene/ShareViewModel.swift +++ b/ShareActionExtension/Scene/ShareViewModel.swift @@ -17,6 +17,7 @@ import UniformTypeIdentifiers import MastodonAsset import MastodonLocalization import MastodonUI +import MastodonCore final class ShareViewModel { @@ -29,6 +30,8 @@ final class ShareViewModel { // input 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 @@ -230,6 +233,9 @@ extension ShareViewModel { 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 { @@ -289,13 +295,15 @@ extension ShareViewModel { self.composeViewModel.statusContent = content } + guard let api = self.api else { return } + if let movieProvider = _movieProvider { composeViewModel.setupAttachmentViewModels([ - StatusAttachmentViewModel(itemProvider: movieProvider) + StatusAttachmentViewModel(api: api, itemProvider: movieProvider) ]) } else if !imageProviders.isEmpty { let viewModels = imageProviders.map { provider in - StatusAttachmentViewModel(itemProvider: provider) + StatusAttachmentViewModel(api: api, itemProvider: provider) } composeViewModel.setupAttachmentViewModels(viewModels) } @@ -328,7 +336,9 @@ extension ShareViewModel { extension ShareViewModel { func publish() -> AnyPublisher, Error> { - guard let authentication = composeViewModel.authentication else { + guard let authentication = composeViewModel.authentication, + let api = self.api + else { return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher() } let authenticationBox = MastodonAuthenticationBox( @@ -364,7 +374,7 @@ extension ShareViewModel { description: description, focus: nil ) - let subscription = APIService.shared.updateMedia( + let subscription = api.updateMedia( domain: domain, attachmentID: attachmentID, query: query, @@ -390,7 +400,7 @@ extension ShareViewModel { spoilerText: spoilerText, visibility: visibility ) - return try await APIService.shared.publishStatus( + return try await api.publishStatus( domain: domain, idempotencyKey: nil, // FIXME: query: query, diff --git a/ShareActionExtension/Scene/View/ComposeToolbarView.swift b/ShareActionExtension/Scene/View/ComposeToolbarView.swift index a903d3ebd..557706fd8 100644 --- a/ShareActionExtension/Scene/View/ComposeToolbarView.swift +++ b/ShareActionExtension/Scene/View/ComposeToolbarView.swift @@ -12,6 +12,7 @@ import MastodonSDK import MastodonUI import MastodonAsset import MastodonLocalization +import MastodonCore import MastodonUI protocol ComposeToolbarViewDelegate: AnyObject { diff --git a/ShareActionExtension/Scene/View/StatusAttachmentViewModel+UploadState.swift b/ShareActionExtension/Scene/View/StatusAttachmentViewModel+UploadState.swift index ce0544aa1..56942cde0 100644 --- a/ShareActionExtension/Scene/View/StatusAttachmentViewModel+UploadState.swift +++ b/ShareActionExtension/Scene/View/StatusAttachmentViewModel+UploadState.swift @@ -10,6 +10,7 @@ import Foundation import Combine import GameplayKit import MastodonSDK +import MastodonCore extension StatusAttachmentViewModel { class UploadState: GKState { @@ -75,7 +76,7 @@ extension StatusAttachmentViewModel.UploadState { ) // and needs clone the `query` if needs retry - APIService.shared.uploadMedia( + viewModel.api.uploadMedia( domain: mastodonAuthenticationBox.domain, query: query, mastodonAuthenticationBox: mastodonAuthenticationBox, diff --git a/ShareActionExtension/Scene/View/StatusAttachmentViewModel.swift b/ShareActionExtension/Scene/View/StatusAttachmentViewModel.swift index 37d4f82e8..19251d0be 100644 --- a/ShareActionExtension/Scene/View/StatusAttachmentViewModel.swift +++ b/ShareActionExtension/Scene/View/StatusAttachmentViewModel.swift @@ -17,6 +17,7 @@ import GameplayKit import MobileCoreServices import UniformTypeIdentifiers import MastodonAsset +import MastodonCore import MastodonLocalization protocol StatusAttachmentViewModelDelegate: AnyObject { @@ -40,6 +41,7 @@ final class StatusAttachmentViewModel: ObservableObject, Identifiable { let itemProvider: NSItemProvider // input + let api: APIService let file = CurrentValueSubject(nil) let authentication = CurrentValueSubject(nil) @Published var descriptionContent = "" @@ -67,7 +69,11 @@ final class StatusAttachmentViewModel: ObservableObject, Identifiable { }() lazy var uploadStateMachineSubject = CurrentValueSubject(nil) - init(itemProvider: NSItemProvider) { + init( + api: APIService, + itemProvider: NSItemProvider + ) { + self.api = api self.itemProvider = itemProvider // bind attachment from item provider diff --git a/ShareActionExtension/Scene/View/StatusAuthorView.swift b/ShareActionExtension/Scene/View/StatusAuthorView.swift index 189a7adc5..24453abe2 100644 --- a/ShareActionExtension/Scene/View/StatusAuthorView.swift +++ b/ShareActionExtension/Scene/View/StatusAuthorView.swift @@ -8,7 +8,6 @@ import SwiftUI import MastodonUI import Nuke -import NukeFLAnimatedImagePlugin import FLAnimatedImage struct StatusAuthorView: View { diff --git a/ShareActionExtension/Service/APIService.swift b/ShareActionExtension/Service/APIService.swift deleted file mode 100644 index a8112167f..000000000 --- a/ShareActionExtension/Service/APIService.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// APIService.swift -// ShareActionExtension -// -// Created by MainasuK Cirno on 2021-7-20. -// - -import os.log -import Foundation -import Combine -import CoreData -import CoreDataStack -import MastodonSDK - -// Replica APIService for share extension -final class APIService { - - var disposeBag = Set() - - static let shared = APIService() - - // internal - let session: URLSession - - // output - let error = PassthroughSubject() - - private init() { - self.session = URLSession(configuration: .default) - } - -} From fa27a28a6092c77d2dbce48eb1f09b3be3e44873 Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 8 Oct 2022 13:43:06 +0800 Subject: [PATCH 032/658] chore: move core logic into package --- Mastodon/Coordinator/NeedsDependency.swift | 1 + .../Account/SelectedAccountSection.swift | 4 +- .../Compose/ComposeStatusAttachmentItem.swift | 1 + .../Diffiable/Compose/ComposeStatusItem.swift | 1 + .../Compose/ComposeStatusSection.swift | 2 +- .../Discovery/DiscoverySection.swift | 1 + .../Notification/NotificationSection.swift | 1 + .../Profile/ProfileFieldSection.swift | 1 + .../RecommendAccountSection.swift | 1 + Mastodon/Diffiable/Report/ReportSection.swift | 1 + .../Search/SearchHistorySection.swift | 1 + .../Search/SearchResultSection.swift | 1 + Mastodon/Diffiable/Search/SearchSection.swift | 1 + .../Diffiable/Settings/SettingsSection.swift | 5 +- Mastodon/Diffiable/Status/StatusSection.swift | 1 + Mastodon/Diffiable/User/UserSection.swift | 1 + .../Provider/DataSourceFacade+Block.swift | 1 + .../Provider/DataSourceFacade+Bookmark.swift | 3 +- .../Provider/DataSourceFacade+Favorite.swift | 3 +- .../Provider/DataSourceFacade+Follow.swift | 1 + .../Provider/DataSourceFacade+Mute.swift | 1 + .../Provider/DataSourceFacade+Reblog.swift | 1 + .../DataSourceFacade+SearchHistory.swift | 1 + .../Provider/DataSourceFacade+Status.swift | 1 + ...taSourceProvider+UITableViewDelegate.swift | 2 + .../Cell/AccountListTableViewCell.swift | 1 + .../AutoCompleteViewController.swift | 1 + .../View/AutoCompleteTopChevronView.swift | 2 + ...sPollExpiresOptionCollectionViewCell.swift | 2 + ...lOptionAppendEntryCollectionViewCell.swift | 2 + ...seStatusPollOptionCollectionViewCell.swift | 1 + .../Scene/Compose/ComposeViewController.swift | 10 ++-- .../Compose/ComposeViewModel+DataSource.swift | 5 +- Mastodon/Scene/Compose/ComposeViewModel.swift | 1 + ...eRepliedToStatusContentTableViewCell.swift | 1 + ...tachmentContainerView+EmptyStateView.swift | 3 +- .../Compose/View/ComposeToolbarView.swift | 2 + .../View/StatusContentWarningEditorView.swift | 1 + .../DiscoveryCommunityViewController.swift | 1 + .../DiscoveryCommunityViewModel+State.swift | 4 +- .../DiscoveryCommunityViewModel.swift | 2 +- .../Discovery/DiscoveryViewController.swift | 1 + .../Scene/Discovery/DiscoveryViewModel.swift | 1 + .../DiscoveryForYouViewController.swift | 1 + .../ForYou/DiscoveryForYouViewModel.swift | 1 + .../DiscoveryHashtagsViewController.swift | 1 + .../Hashtags/DiscoveryHashtagsViewModel.swift | 1 + .../News/DiscoveryNewsViewController.swift | 1 + .../News/DiscoveryNewsViewModel.swift | 1 + .../Posts/DiscoveryPostsViewController.swift | 1 + .../Posts/DiscoveryPostsViewModel+State.swift | 5 +- .../Posts/DiscoveryPostsViewModel.swift | 3 +- .../View/DiscoveryIntroBannerView.swift | 2 + .../HashtagTimelineViewController.swift | 1 + .../HashtagTimelineViewModel+State.swift | 2 +- .../HashtagTimelineViewModel.swift | 5 +- ...meTimelineViewController+DebugAction.swift | 1 + .../HomeTimelineViewController.swift | 3 +- ...omeTimelineViewModel+LoadLatestState.swift | 1 + .../HomeTimeline/HomeTimelineViewModel.swift | 1 + ...eTimelineNavigationBarTitleViewModel.swift | 31 +++++------ .../Image/MediaPreviewImageViewModel.swift | 7 +-- .../MediaPreviewViewController.swift | 2 + .../MediaPreview/MediaPreviewViewModel.swift | 1 + .../Video/MediaPreviewVideoViewModel.swift | 1 + .../Cell/NotificationTableViewCell.swift | 1 + .../NotificationTimelineViewController.swift | 1 + .../NotificationTimelineViewModel.swift | 3 +- .../NotificationViewController.swift | 1 + .../Notification/NotificationViewModel.swift | 5 +- .../MastodonConfirmEmailViewController.swift | 8 +-- .../MastodonConfirmEmailViewModel.swift | 1 + .../MastodonPickServerViewController.swift | 3 +- .../MastodonPickServerViewModel.swift | 2 + .../PickServerLoaderTableViewCell.swift | 2 + .../View/PickServerCategoryView.swift | 1 + .../View/PickServerEmptyStateView.swift | 2 + .../MastodonRegisterViewController.swift | 1 + .../Register/MastodonRegisterViewModel.swift | 1 + .../MastodonResendEmailViewController.swift | 1 + ...todonServerRulesViewController+Debug.swift | 1 + .../MastodonServerRulesViewController.swift | 1 + .../Share/AuthenticationViewModel.swift | 3 +- .../MastodonAuthenticationController.swift | 3 +- .../View/WelcomeIllustrationView.swift | 2 + .../Welcome/WelcomeViewController.swift | 1 + .../Onboarding/Welcome/WelcomeViewModel.swift | 1 + ...ofileFieldAddEntryCollectionViewCell.swift | 1 + .../ProfileFieldEditCollectionViewCell.swift | 2 + .../About/ProfileAboutViewController.swift | 1 + .../Profile/About/ProfileAboutViewModel.swift | 1 + .../Bookmark/BookmarkViewController.swift | 1 + .../Bookmark/BookmarkViewModel+State.swift | 7 +-- .../Profile/Bookmark/BookmarkViewModel.swift | 3 +- .../Profile/CachedProfileViewModel.swift | 1 + .../FamiliarFollowersViewController.swift | 1 + .../FamiliarFollowersViewModel.swift | 1 + .../Favorite/FavoriteViewController.swift | 1 + .../Favorite/FavoriteViewModel+State.swift | 7 +-- .../Profile/Favorite/FavoriteViewModel.swift | 3 +- .../Follower/FollowerListViewController.swift | 1 + .../FollowerListViewModel+State.swift | 1 + .../Follower/FollowerListViewModel.swift | 1 + .../FollowingListViewController.swift | 1 + .../Following/FollowingListViewModel.swift | 2 +- .../Header/ProfileHeaderViewController.swift | 2 + .../Header/ProfileHeaderViewModel.swift | 1 + .../View/ProfileHeaderView+ViewModel.swift | 1 + .../Header/View/ProfileHeaderView.swift | 1 + .../Scene/Profile/MeProfileViewModel.swift | 1 + .../Paging/ProfilePagingViewController.swift | 1 + .../Scene/Profile/ProfileViewController.swift | 3 +- Mastodon/Scene/Profile/ProfileViewModel.swift | 1 + .../Profile/RemoteProfileViewModel.swift | 1 + .../Timeline/UserTimelineViewController.swift | 1 + .../UserTimelineViewModel+State.swift | 9 ++-- .../Timeline/UserTimelineViewModel.swift | 3 +- .../FavoritedByViewController.swift | 1 + .../RebloggedByViewController.swift | 1 + .../UserLIst/UserListViewModel+State.swift | 2 +- .../Profile/UserLIst/UserListViewModel.swift | 3 +- .../Report/Report/ReportViewController.swift | 1 + .../Scene/Report/Report/ReportViewModel.swift | 1 + .../ReportReason/ReportReasonView.swift | 1 + .../ReportReasonViewController.swift | 1 + .../ReportReason/ReportReasonViewModel.swift | 1 + .../ReportResult/ReportResultView.swift | 2 + .../ReportResultViewController.swift | 1 + .../ReportResult/ReportResultViewModel.swift | 1 + .../ReportServerRulesViewController.swift | 3 +- .../ReportServerRulesViewModel.swift | 1 + .../ReportStatusViewController.swift | 1 + .../ReportStatusViewModel+State.swift | 6 +-- .../ReportStatus/ReportStatusViewModel.swift | 3 +- .../ReportSupplementaryViewController.swift | 1 + .../ReportSupplementaryViewModel.swift | 1 + .../Root/ContentSplitViewController.swift | 1 + .../Root/MainTab/MainTabBarController.swift | 52 ++++++++++--------- .../SecondaryPlaceholderViewController.swift | 1 + .../Root/Sidebar/SidebarViewController.swift | 1 + .../Scene/Root/Sidebar/SidebarViewModel.swift | 1 + .../Sidebar/View/SidebarListContentView.swift | 2 + .../Search/Cell/TrendCollectionViewCell.swift | 1 + .../Search/Search/SearchViewController.swift | 1 + .../Scene/Search/Search/SearchViewModel.swift | 1 + .../SearchRecommendCollectionHeader.swift | 2 + .../SearchDetailViewController.swift | 3 +- .../SearchHistoryUserCollectionViewCell.swift | 1 + .../SearchHistoryViewController.swift | 1 + .../SearchHistoryViewModel.swift | 1 + .../View/SearchHistoryTableHeaderView.swift | 1 + .../SearchResultViewController.swift | 2 + .../SearchResultViewModel+State.swift | 3 +- .../SearchResult/SearchResultViewModel.swift | 3 +- .../Settings/SettingsViewController.swift | 8 +-- .../Scene/Settings/SettingsViewModel.swift | 1 + .../Content/ContentWarningOverlayView.swift | 1 + ...ubleTitleLabelNavigationBarTitleView.swift | 2 + .../NotificationView+Configuration.swift | 1 + .../PollOptionView+Configuration.swift | 1 + .../Content/StatusView+Configuration.swift | 2 + .../Share/View/Content/ThreadMetaView.swift | 1 + .../View/Content/UserView+Configuration.swift | 1 + .../Share/View/Decoration/SawToothView.swift | 1 + .../ThreadReplyLoaderTableViewCell.swift | 2 + .../TimelineBottomLoaderTableViewCell.swift | 2 + .../TimelineLoaderTableViewCell.swift | 2 + .../TimelineMiddleLoaderTableViewCell.swift | 1 + .../TimelineTopLoaderTableViewCell.swift | 3 ++ .../Share/Webview/WebViewController.swift | 1 + .../SuggestionAccountViewController.swift | 1 + .../SuggestionAccountViewModel.swift | 1 + ...estionAccountTableViewCell+ViewModel.swift | 2 + .../Scene/Thread/CachedThreadViewModel.swift | 1 + .../MastodonStatusThreadViewModel.swift | 1 + .../Scene/Thread/RemoteThreadViewModel.swift | 1 + .../Scene/Thread/ThreadViewController.swift | 1 + .../Thread/ThreadViewModel+Diffable.swift | 1 + Mastodon/Scene/Thread/ThreadViewModel.swift | 1 + Mastodon/Supporting Files/SceneDelegate.swift | 1 + .../Sources/MastodonCore/AppContext.swift | 8 +-- .../Extension/CoreDataStack/Instance.swift | 2 +- .../Extension/CoreDataStack/Setting.swift | 2 +- .../Extension/CoreDataStack/Status.swift | 12 ++--- .../CoreDataStack/Subscription.swift | 4 +- ...SearchHistoryFetchedResultController.swift | 14 ++--- .../StatusFetchedResultsController.swift | 24 ++++----- .../UserFetchedResultsController.swift | 14 ++--- .../Model/PlaintextMetaContent.swift | 0 .../Service/API/APIService+Account.swift | 10 ++-- .../Service/API/APIService+App.swift | 2 +- .../API/APIService+Authentication.swift | 4 +- .../Service/API/APIService+Block.swift | 2 +- .../Service/API/APIService+Bookmark.swift | 4 +- .../Service/API/APIService+Favorite.swift | 7 +-- .../Service/API/APIService+Follow.swift | 2 +- .../API/APIService+FollowRequest.swift | 4 +- .../Service/API/APIService+Follower.swift | 2 +- .../Service/API/APIService+Following.swift | 2 +- .../API/APIService+HashtagTimeline.swift | 2 +- .../Service/API/APIService+HomeTimeline.swift | 2 +- .../Service/API/APIService+Instance.swift | 2 +- .../Service/API/APIService+Mute.swift | 2 +- .../Service/API/APIService+Notification.swift | 14 ++--- .../Service/API/APIService+Onboarding.swift | 6 +-- .../Service/API/APIService+Poll.swift | 4 +- .../API/APIService+PublicTimeline.swift | 2 +- .../Service/API/APIService+Reblog.swift | 4 +- .../Service/API/APIService+Recommend.swift | 6 +-- .../Service/API/APIService+Relationship.swift | 2 +- .../Service/API/APIService+Status.swift | 4 +- .../Service/API/APIService+Thread.swift | 2 +- .../Service/API/APIService+Trend.swift | 6 +-- .../Service/API/APIService+UserTimeline.swift | 2 +- .../Service/API/APIService+WebFinger.swift | 2 +- .../MastodonCore/Service/API/APIService.swift | 2 +- ...vice+CoreData+MastodonAuthentication.swift | 2 +- .../APIService+CoreData+Subscriptions.swift | 2 +- .../Service/AuthenticationService.swift | 2 +- .../EmojiService+CustomEmojiViewModel.swift | 4 +- .../Service/Emoji/EmojiService.swift | 2 +- .../MastodonCore/Service/Emoji/Trie.swift | 14 ++--- ...astodonAttachmentService+UploadState.swift | 28 +++++----- .../MastodonAttachmentService.swift | 40 +++++++------- .../Notification/NotificationService.swift | 2 +- .../MastodonCore/Service/SettingService.swift | 4 +- .../Service/StatusFilterService.swift | 4 +- ...liarFollowersDashboardView+ViewModel.swift | 1 + .../View/Content/NotificationView.swift | 1 + .../ProfileCardView+Configuration.swift | 1 + .../MastodonUI/View/Content/StatusView.swift | 1 + .../View/Content/UserView+ViewModel.swift | 1 + 232 files changed, 455 insertions(+), 246 deletions(-) rename MastodonSDK/Sources/{MastodonUI => MastodonCore}/Model/PlaintextMetaContent.swift (100%) diff --git a/Mastodon/Coordinator/NeedsDependency.swift b/Mastodon/Coordinator/NeedsDependency.swift index d6a24cce3..c035437ac 100644 --- a/Mastodon/Coordinator/NeedsDependency.swift +++ b/Mastodon/Coordinator/NeedsDependency.swift @@ -6,6 +6,7 @@ // import UIKit +import MastodonCore protocol NeedsDependency: AnyObject { var context: AppContext! { get set } diff --git a/Mastodon/Diffiable/Account/SelectedAccountSection.swift b/Mastodon/Diffiable/Account/SelectedAccountSection.swift index 6c02d7059..d71cbf326 100644 --- a/Mastodon/Diffiable/Account/SelectedAccountSection.swift +++ b/Mastodon/Diffiable/Account/SelectedAccountSection.swift @@ -5,11 +5,11 @@ // Created by sxiaojian on 2021/4/22. // +import UIKit import CoreData import CoreDataStack -import Foundation +import MastodonCore import MastodonSDK -import UIKit enum SelectedAccountSection: Equatable, Hashable { case main diff --git a/Mastodon/Diffiable/Compose/ComposeStatusAttachmentItem.swift b/Mastodon/Diffiable/Compose/ComposeStatusAttachmentItem.swift index 834e1da49..07ae4e5df 100644 --- a/Mastodon/Diffiable/Compose/ComposeStatusAttachmentItem.swift +++ b/Mastodon/Diffiable/Compose/ComposeStatusAttachmentItem.swift @@ -6,6 +6,7 @@ // import Foundation +import MastodonCore enum ComposeStatusAttachmentItem { case attachment(attachmentService: MastodonAttachmentService) diff --git a/Mastodon/Diffiable/Compose/ComposeStatusItem.swift b/Mastodon/Diffiable/Compose/ComposeStatusItem.swift index 65650dcdc..aad93c2d2 100644 --- a/Mastodon/Diffiable/Compose/ComposeStatusItem.swift +++ b/Mastodon/Diffiable/Compose/ComposeStatusItem.swift @@ -8,6 +8,7 @@ import Foundation import Combine import CoreData +import MastodonCore import MastodonMeta import CoreDataStack diff --git a/Mastodon/Diffiable/Compose/ComposeStatusSection.swift b/Mastodon/Diffiable/Compose/ComposeStatusSection.swift index 45ed86783..7b4596267 100644 --- a/Mastodon/Diffiable/Compose/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Compose/ComposeStatusSection.swift @@ -21,7 +21,7 @@ enum ComposeStatusSection: Equatable, Hashable { } extension ComposeStatusSection { - enum ComposeKind { + public enum ComposeKind { case post case hashtag(hashtag: String) case mention(user: ManagedObjectRecord) diff --git a/Mastodon/Diffiable/Discovery/DiscoverySection.swift b/Mastodon/Diffiable/Discovery/DiscoverySection.swift index 94e07c71b..2910171d1 100644 --- a/Mastodon/Diffiable/Discovery/DiscoverySection.swift +++ b/Mastodon/Diffiable/Discovery/DiscoverySection.swift @@ -7,6 +7,7 @@ import os.log import UIKit +import MastodonCore import MastodonUI import MastodonSDK diff --git a/Mastodon/Diffiable/Notification/NotificationSection.swift b/Mastodon/Diffiable/Notification/NotificationSection.swift index 97cf8ada0..6f08a0252 100644 --- a/Mastodon/Diffiable/Notification/NotificationSection.swift +++ b/Mastodon/Diffiable/Notification/NotificationSection.swift @@ -14,6 +14,7 @@ import UIKit import MetaTextKit import MastodonMeta import MastodonAsset +import MastodonCore import MastodonLocalization enum NotificationSection: Equatable, Hashable { diff --git a/Mastodon/Diffiable/Profile/ProfileFieldSection.swift b/Mastodon/Diffiable/Profile/ProfileFieldSection.swift index e1b0d649f..19771b5db 100644 --- a/Mastodon/Diffiable/Profile/ProfileFieldSection.swift +++ b/Mastodon/Diffiable/Profile/ProfileFieldSection.swift @@ -8,6 +8,7 @@ import os import UIKit import Combine +import MastodonCore import MastodonMeta import MastodonLocalization diff --git a/Mastodon/Diffiable/RecommandAccount/RecommendAccountSection.swift b/Mastodon/Diffiable/RecommandAccount/RecommendAccountSection.swift index f59164f35..fc2d68044 100644 --- a/Mastodon/Diffiable/RecommandAccount/RecommendAccountSection.swift +++ b/Mastodon/Diffiable/RecommandAccount/RecommendAccountSection.swift @@ -13,6 +13,7 @@ import UIKit import MetaTextKit import MastodonMeta import Combine +import MastodonCore enum RecommendAccountSection: Equatable, Hashable { case main diff --git a/Mastodon/Diffiable/Report/ReportSection.swift b/Mastodon/Diffiable/Report/ReportSection.swift index 69b9da234..6513c1249 100644 --- a/Mastodon/Diffiable/Report/ReportSection.swift +++ b/Mastodon/Diffiable/Report/ReportSection.swift @@ -13,6 +13,7 @@ import MastodonSDK import UIKit import os.log import MastodonAsset +import MastodonCore import MastodonLocalization enum ReportSection: Equatable, Hashable { diff --git a/Mastodon/Diffiable/Search/SearchHistorySection.swift b/Mastodon/Diffiable/Search/SearchHistorySection.swift index 557b49f2b..03de14c1c 100644 --- a/Mastodon/Diffiable/Search/SearchHistorySection.swift +++ b/Mastodon/Diffiable/Search/SearchHistorySection.swift @@ -7,6 +7,7 @@ import UIKit import CoreDataStack +import MastodonCore enum SearchHistorySection: Hashable { case main diff --git a/Mastodon/Diffiable/Search/SearchResultSection.swift b/Mastodon/Diffiable/Search/SearchResultSection.swift index 1b1ac3ec9..b7fb09df7 100644 --- a/Mastodon/Diffiable/Search/SearchResultSection.swift +++ b/Mastodon/Diffiable/Search/SearchResultSection.swift @@ -12,6 +12,7 @@ import UIKit import CoreData import CoreDataStack import MastodonAsset +import MastodonCore import MastodonLocalization import MastodonUI diff --git a/Mastodon/Diffiable/Search/SearchSection.swift b/Mastodon/Diffiable/Search/SearchSection.swift index 4f550abf7..c7f3922fb 100644 --- a/Mastodon/Diffiable/Search/SearchSection.swift +++ b/Mastodon/Diffiable/Search/SearchSection.swift @@ -7,6 +7,7 @@ import UIKit import MastodonSDK +import MastodonCore import MastodonLocalization enum SearchSection: Hashable { diff --git a/Mastodon/Diffiable/Settings/SettingsSection.swift b/Mastodon/Diffiable/Settings/SettingsSection.swift index 6925303d8..6086a0151 100644 --- a/Mastodon/Diffiable/Settings/SettingsSection.swift +++ b/Mastodon/Diffiable/Settings/SettingsSection.swift @@ -9,6 +9,7 @@ import UIKit import CoreData import CoreDataStack import MastodonAsset +import MastodonCore import MastodonLocalization enum SettingsSection: Hashable { @@ -124,7 +125,7 @@ extension SettingsSection { extension SettingsSection { - static func configureSettingToggle( + public static func configureSettingToggle( cell: SettingsToggleTableViewCell, item: SettingsItem, setting: Setting @@ -155,7 +156,7 @@ extension SettingsSection { } } - static func configureSettingToggle( + public static func configureSettingToggle( cell: SettingsToggleTableViewCell, switchMode: SettingsItem.NotificationSwitchMode, subscription: NotificationSubscription diff --git a/Mastodon/Diffiable/Status/StatusSection.swift b/Mastodon/Diffiable/Status/StatusSection.swift index 40b7e5351..08b55bc69 100644 --- a/Mastodon/Diffiable/Status/StatusSection.swift +++ b/Mastodon/Diffiable/Status/StatusSection.swift @@ -15,6 +15,7 @@ import AlamofireImage import MastodonMeta import MastodonSDK import NaturalLanguage +import MastodonCore import MastodonUI enum StatusSection: Equatable, Hashable { diff --git a/Mastodon/Diffiable/User/UserSection.swift b/Mastodon/Diffiable/User/UserSection.swift index cb806c4e9..6bb402b3a 100644 --- a/Mastodon/Diffiable/User/UserSection.swift +++ b/Mastodon/Diffiable/User/UserSection.swift @@ -9,6 +9,7 @@ import os.log import UIKit import CoreData import CoreDataStack +import MastodonCore import MetaTextKit import MastodonMeta diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift index e9a0b02c0..7166cb39b 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift @@ -7,6 +7,7 @@ import UIKit import CoreDataStack +import MastodonCore extension DataSourceFacade { static func responseToUserBlockAction( diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift index 0c467778d..93da15271 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift @@ -8,9 +8,10 @@ import UIKit import CoreData import CoreDataStack +import MastodonCore extension DataSourceFacade { - static func responseToStatusBookmarkAction( + public static func responseToStatusBookmarkAction( provider: DataSourceProvider, status: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift index fba4f697b..71c02828f 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift @@ -8,9 +8,10 @@ import UIKit import CoreData import CoreDataStack +import MastodonCore extension DataSourceFacade { - static func responseToStatusFavoriteAction( + public static func responseToStatusFavoriteAction( provider: DataSourceProvider, status: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index f0bc379ae..cd29d2eca 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -8,6 +8,7 @@ import UIKit import CoreDataStack import class CoreDataStack.Notification +import MastodonCore import MastodonSDK import MastodonLocalization diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift index b5b2dec97..b48fbf462 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift @@ -7,6 +7,7 @@ import UIKit import CoreDataStack +import MastodonCore extension DataSourceFacade { static func responseToUserMuteAction( diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift index 7eda84599..283a21dc8 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift @@ -7,6 +7,7 @@ import UIKit import CoreDataStack +import MastodonCore import MastodonUI extension DataSourceFacade { diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift index 8beaabbae..25ffd4b8e 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift @@ -7,6 +7,7 @@ import Foundation import CoreDataStack +import MastodonCore extension DataSourceFacade { diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 4c948c716..25ab53103 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -7,6 +7,7 @@ import UIKit import CoreDataStack +import MastodonCore import MastodonUI import MastodonLocalization diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index c00c36971..b62064a6f 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -8,6 +8,8 @@ import os.log import UIKit import CoreDataStack +import MastodonCore +import MastodonUI import MastodonLocalization extension UITableViewDelegate where Self: DataSourceProvider { diff --git a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift index 96111d9f8..66f49efe8 100644 --- a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift +++ b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift @@ -9,6 +9,7 @@ import UIKit import Combine import FLAnimatedImage import MetaTextKit +import MastodonCore import MastodonUI final class AccountListTableViewCell: UITableViewCell { diff --git a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewController.swift b/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewController.swift index 6b71c5dd2..16f65f702 100644 --- a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewController.swift +++ b/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewController.swift @@ -8,6 +8,7 @@ import os.log import UIKit import Combine +import MastodonCore protocol AutoCompleteViewControllerDelegate: AnyObject { func autoCompleteViewController(_ viewController: AutoCompleteViewController, didSelectItem item: AutoCompleteItem) diff --git a/Mastodon/Scene/Compose/AutoComplete/View/AutoCompleteTopChevronView.swift b/Mastodon/Scene/Compose/AutoComplete/View/AutoCompleteTopChevronView.swift index 9c3c81c3a..ccc36b1df 100644 --- a/Mastodon/Scene/Compose/AutoComplete/View/AutoCompleteTopChevronView.swift +++ b/Mastodon/Scene/Compose/AutoComplete/View/AutoCompleteTopChevronView.swift @@ -7,6 +7,8 @@ import UIKit import Combine +import MastodonCore +import MastodonUI final class AutoCompleteTopChevronView: UIView { diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift index 8a00fccde..ac32129cc 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift @@ -9,6 +9,8 @@ import os.log import UIKit import Combine import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization protocol ComposeStatusPollExpiresOptionCollectionViewCellDelegate: AnyObject { diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionAppendEntryCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionAppendEntryCollectionViewCell.swift index 336d109c9..29a9c0c56 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionAppendEntryCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionAppendEntryCollectionViewCell.swift @@ -8,6 +8,8 @@ import os.log import UIKit import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization protocol ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate: AnyObject { diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionCollectionViewCell.swift index c1869669c..96ba4ce59 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionCollectionViewCell.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine import MastodonAsset +import MastodonCore import MastodonLocalization import MastodonUI diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 8783c8c0e..619f6efb1 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -9,11 +9,12 @@ import os.log import UIKit import Combine import PhotosUI +import Meta import MetaTextKit import MastodonMeta -import Meta -import MastodonUI import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization import MastodonSDK @@ -752,7 +753,10 @@ extension ComposeViewController { // TODO: handle error return } - context.statusPublishService.publish(composeViewModel: viewModel) + + // context.statusPublishService.publish(composeViewModel: viewModel) + assertionFailure() + dismiss(animated: true, completion: nil) } diff --git a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift b/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift index c638eb769..f8694376a 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift @@ -9,11 +9,12 @@ import os.log import UIKit import Combine import CoreDataStack -import MastodonSDK -import MastodonMeta import MetaTextKit +import MastodonMeta import MastodonAsset +import MastodonCore import MastodonLocalization +import MastodonSDK extension ComposeViewModel { diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 162043064..35cc965ed 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -13,6 +13,7 @@ import CoreDataStack import GameplayKit import MastodonSDK import MastodonAsset +import MastodonCore import MastodonLocalization import MastodonMeta import MastodonUI diff --git a/Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift b/Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift index f15675b24..b0fbb0194 100644 --- a/Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift +++ b/Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift @@ -7,6 +7,7 @@ import UIKit import Combine +import MastodonUI final class ComposeRepliedToStatusContentTableViewCell: UITableViewCell { diff --git a/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift b/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift index 1d32931af..0dabe7790 100644 --- a/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift +++ b/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift @@ -6,8 +6,9 @@ // import UIKit -import MastodonUI import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization extension AttachmentContainerView { diff --git a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift index 4ed84be7c..a993da228 100644 --- a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift +++ b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift @@ -10,6 +10,8 @@ import UIKit import Combine import MastodonSDK import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization protocol ComposeToolbarViewDelegate: AnyObject { diff --git a/Mastodon/Scene/Compose/View/StatusContentWarningEditorView.swift b/Mastodon/Scene/Compose/View/StatusContentWarningEditorView.swift index 83900c762..80dd04d37 100644 --- a/Mastodon/Scene/Compose/View/StatusContentWarningEditorView.swift +++ b/Mastodon/Scene/Compose/View/StatusContentWarningEditorView.swift @@ -8,6 +8,7 @@ import UIKit import MastodonUI import MastodonAsset +import MastodonCore import MastodonLocalization final class StatusContentWarningEditorView: UIView { diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController.swift index 524805ad7..b8c86974b 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController.swift @@ -8,6 +8,7 @@ import os.log import UIKit import Combine +import MastodonCore import MastodonUI // Local Timeline diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift index a3947e6ab..b7b078c7a 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift @@ -164,7 +164,7 @@ extension DiscoveryCommunityViewModel.State { self.maxID = newMaxID var hasNewStatusesAppend = false - var statusIDs = isReloading ? [] : viewModel.statusFetchedResultsController.statusIDs.value + var statusIDs = isReloading ? [] : viewModel.statusFetchedResultsController.statusIDs for status in response.value { guard !statusIDs.contains(status.id) else { continue } statusIDs.append(status.id) @@ -177,7 +177,7 @@ extension DiscoveryCommunityViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs.value = statusIDs + viewModel.statusFetchedResultsController.statusIDs = statusIDs viewModel.didLoadLatest.send() } catch { diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift index 86d94a3aa..4c01cb0bc 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift @@ -54,7 +54,7 @@ final class DiscoveryCommunityViewModel { context.authenticationService.activeMastodonAuthentication .map { $0?.domain } - .assign(to: \.value, on: statusFetchedResultsController.domain) + .assign(to: \.domain, on: statusFetchedResultsController) .store(in: &disposeBag) } diff --git a/Mastodon/Scene/Discovery/DiscoveryViewController.swift b/Mastodon/Scene/Discovery/DiscoveryViewController.swift index d94e6e592..3fc2e8944 100644 --- a/Mastodon/Scene/Discovery/DiscoveryViewController.swift +++ b/Mastodon/Scene/Discovery/DiscoveryViewController.swift @@ -11,6 +11,7 @@ import Combine import Tabman import Pageboy import MastodonAsset +import MastodonCore import MastodonUI public class DiscoveryViewController: TabmanViewController, NeedsDependency { diff --git a/Mastodon/Scene/Discovery/DiscoveryViewModel.swift b/Mastodon/Scene/Discovery/DiscoveryViewModel.swift index dfeb16e2b..d91d9eee1 100644 --- a/Mastodon/Scene/Discovery/DiscoveryViewModel.swift +++ b/Mastodon/Scene/Discovery/DiscoveryViewModel.swift @@ -9,6 +9,7 @@ import UIKit import Combine import Tabman import Pageboy +import MastodonCore import MastodonLocalization final class DiscoveryViewModel { diff --git a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift index 9f6368e63..aa4d45f6d 100644 --- a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift +++ b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine import MastodonUI +import MastodonCore final class DiscoveryForYouViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel.swift b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel.swift index a31022a7c..5073ae200 100644 --- a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel.swift +++ b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel.swift @@ -12,6 +12,7 @@ import GameplayKit import CoreData import CoreDataStack import MastodonSDK +import MastodonCore final class DiscoveryForYouViewModel { diff --git a/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewController.swift b/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewController.swift index 20ad408a2..8985af2de 100644 --- a/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewController.swift +++ b/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewController.swift @@ -8,6 +8,7 @@ import os.log import UIKit import Combine +import MastodonCore import MastodonUI final class DiscoveryHashtagsViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewModel.swift b/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewModel.swift index 1b119f3d7..5e931063a 100644 --- a/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewModel.swift +++ b/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewModel.swift @@ -11,6 +11,7 @@ import Combine import GameplayKit import CoreData import CoreDataStack +import MastodonCore import MastodonSDK final class DiscoveryHashtagsViewModel { diff --git a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewController.swift b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewController.swift index d2415145c..7f9efb0d9 100644 --- a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewController.swift +++ b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewController.swift @@ -8,6 +8,7 @@ import os.log import UIKit import Combine +import MastodonCore import MastodonUI final class DiscoveryNewsViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel.swift b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel.swift index 2c4d89dc8..35af8f962 100644 --- a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel.swift +++ b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel.swift @@ -12,6 +12,7 @@ import GameplayKit import CoreData import CoreDataStack import MastodonSDK +import MastodonCore final class DiscoveryNewsViewModel { diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift index 537ca1c58..96f20dbcc 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift @@ -8,6 +8,7 @@ import os.log import UIKit import Combine +import MastodonCore import MastodonUI final class DiscoveryPostsViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift index 0ff6cbb14..2e5ae9847 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift @@ -9,6 +9,7 @@ import os.log import Foundation import GameplayKit import MastodonSDK +import MastodonCore extension DiscoveryPostsViewModel { class State: GKState, NamingState { @@ -166,7 +167,7 @@ extension DiscoveryPostsViewModel.State { self.offset = newOffset var hasNewStatusesAppend = false - var statusIDs = isReloading ? [] : viewModel.statusFetchedResultsController.statusIDs.value + var statusIDs = isReloading ? [] : viewModel.statusFetchedResultsController.statusIDs for status in response.value { guard !statusIDs.contains(status.id) else { continue } statusIDs.append(status.id) @@ -178,7 +179,7 @@ extension DiscoveryPostsViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs.value = statusIDs + viewModel.statusFetchedResultsController.statusIDs = statusIDs viewModel.didLoadLatest.send() } catch { diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift index c001bb7b3..ad6639b16 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift @@ -12,6 +12,7 @@ import GameplayKit import CoreData import CoreDataStack import MastodonSDK +import MastodonCore final class DiscoveryPostsViewModel { @@ -51,7 +52,7 @@ final class DiscoveryPostsViewModel { context.authenticationService.activeMastodonAuthentication .map { $0?.domain } - .assign(to: \.value, on: statusFetchedResultsController.domain) + .assign(to: \.domain, on: statusFetchedResultsController) .store(in: &disposeBag) Task { diff --git a/Mastodon/Scene/Discovery/View/DiscoveryIntroBannerView.swift b/Mastodon/Scene/Discovery/View/DiscoveryIntroBannerView.swift index afc2cb7db..492541062 100644 --- a/Mastodon/Scene/Discovery/View/DiscoveryIntroBannerView.swift +++ b/Mastodon/Scene/Discovery/View/DiscoveryIntroBannerView.swift @@ -9,6 +9,8 @@ import os.log import UIKit import Combine import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization public protocol DiscoveryIntroBannerViewDelegate: AnyObject { diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift index 02738747c..b422481f2 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift @@ -12,6 +12,7 @@ import Combine import GameplayKit import CoreData import MastodonAsset +import MastodonCore import MastodonLocalization final class HashtagTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift index 6b75d3875..8e3b6f5d1 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift @@ -167,7 +167,7 @@ extension HashtagTimelineViewModel.State { self.maxID = newMaxID var hasNewStatusesAppend = false - var statusIDs = isReloading ? [] : viewModel.fetchedResultsController.statusIDs.value + var statusIDs = isReloading ? [] : viewModel.fetchedResultsController.statusIDs for status in response.value { guard !statusIDs.contains(status.id) else { continue } statusIDs.append(status.id) diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift index f22987273..88eb8fcdd 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift @@ -12,7 +12,8 @@ import CoreData import CoreDataStack import GameplayKit import MastodonSDK - +import MastodonCore + final class HashtagTimelineViewModel { let logger = Logger(subsystem: "HashtagTimelineViewModel", category: "ViewModel") @@ -63,7 +64,7 @@ final class HashtagTimelineViewModel { context.authenticationService.activeMastodonAuthenticationBox .map { $0?.domain } - .assign(to: \.value, on: fetchedResultsController.domain) + .assign(to: \.domain, on: fetchedResultsController) .store(in: &disposeBag) } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 4fae66d33..2a26c6cf8 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -13,6 +13,7 @@ import CoreData import CoreDataStack import FLEX import SwiftUI +import MastodonCore import MastodonUI import MastodonSDK import StoreKit diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 24b96f265..00433be44 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -16,8 +16,9 @@ import MastodonSDK import AlamofireImage import StoreKit import MastodonAsset -import MastodonLocalization +import MastodonCore import MastodonUI +import MastodonLocalization final class HomeTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index 3e46c2af4..234504e38 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -11,6 +11,7 @@ import Foundation import CoreData import CoreDataStack import GameplayKit +import MastodonCore extension HomeTimelineViewModel { class LoadLatestState: GKState { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index be7de3a5a..72131f366 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -15,6 +15,7 @@ import CoreDataStack import GameplayKit import AlamofireImage import DateToolsSwift +import MastodonCore final class HomeTimelineViewModel: NSObject { diff --git a/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift b/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift index 71b4dda8b..09e750ed1 100644 --- a/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift @@ -8,6 +8,7 @@ import Combine import Foundation import UIKit +import MastodonCore final class HomeTimelineNavigationBarTitleViewModel { @@ -48,21 +49,21 @@ final class HomeTimelineNavigationBarTitleViewModel { .assign(to: \.value, on: isOffline) .store(in: &disposeBag) - context.statusPublishService.latestPublishingComposeViewModel - .receive(on: DispatchQueue.main) - .sink { [weak self] composeViewModel in - guard let self = self else { return } - guard let composeViewModel = composeViewModel, - let state = composeViewModel.publishStateMachine.currentState else { - self.isPublishingPost.value = false - self.isPublished.value = false - return - } - - self.isPublishingPost.value = state is ComposeViewModel.PublishState.Publishing || state is ComposeViewModel.PublishState.Fail - self.isPublished.value = state is ComposeViewModel.PublishState.Finish - } - .store(in: &disposeBag) +// context.statusPublishService.latestPublishingComposeViewModel +// .receive(on: DispatchQueue.main) +// .sink { [weak self] composeViewModel in +// guard let self = self else { return } +// guard let composeViewModel = composeViewModel, +// let state = composeViewModel.publishStateMachine.currentState else { +// self.isPublishingPost.value = false +// self.isPublished.value = false +// return +// } +// +// self.isPublishingPost.value = state is ComposeViewModel.PublishState.Publishing || state is ComposeViewModel.PublishState.Fail +// self.isPublished.value = state is ComposeViewModel.PublishState.Finish +// } +// .store(in: &disposeBag) Publishers.CombineLatest4( hasNewPosts.eraseToAnyPublisher(), diff --git a/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewModel.swift b/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewModel.swift index 1a141c723..e82118f78 100644 --- a/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewModel.swift +++ b/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewModel.swift @@ -11,6 +11,7 @@ import Combine import Alamofire import AlamofireImage import FLAnimatedImage +import MastodonCore class MediaPreviewImageViewModel { @@ -29,18 +30,18 @@ class MediaPreviewImageViewModel { extension MediaPreviewImageViewModel { - enum ImagePreviewItem { + public enum ImagePreviewItem { case remote(RemoteImageContext) case local(LocalImageContext) } - struct RemoteImageContext { + public struct RemoteImageContext { let assetURL: URL? let thumbnail: UIImage? let altText: String? } - struct LocalImageContext { + public struct LocalImageContext { let image: UIImage } diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift index e1e367e37..c6552bcba 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift @@ -10,6 +10,8 @@ import UIKit import Combine import Pageboy import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization final class MediaPreviewViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift index 2fbc5f0ac..c43d24945 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift @@ -10,6 +10,7 @@ import Combine import CoreData import CoreDataStack import Pageboy +import MastodonCore final class MediaPreviewViewModel: NSObject { diff --git a/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewModel.swift b/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewModel.swift index 7485bdb44..97e5f955b 100644 --- a/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewModel.swift +++ b/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewModel.swift @@ -10,6 +10,7 @@ import UIKit import AVKit import Combine import AlamofireImage +import MastodonCore final class MediaPreviewVideoViewModel { diff --git a/Mastodon/Scene/Notification/Cell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/Cell/NotificationTableViewCell.swift index bbdb2afaa..d8949e391 100644 --- a/Mastodon/Scene/Notification/Cell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/Cell/NotificationTableViewCell.swift @@ -8,6 +8,7 @@ import os.log import UIKit import Combine +import MastodonCore import MastodonUI final class NotificationTableViewCell: UITableViewCell { diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index 300b9165d..c549d8f99 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine import CoreDataStack +import MastodonCore import MastodonLocalization final class NotificationTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index 5c86b9f38..5e4f7d7ca 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -11,6 +11,7 @@ import Combine import CoreDataStack import GameplayKit import MastodonSDK +import MastodonCore final class NotificationTimelineViewModel { @@ -78,7 +79,7 @@ final class NotificationTimelineViewModel { extension NotificationTimelineViewModel { - typealias Scope = APIService.NotificationScope + typealias Scope = APIService.MastodonNotificationScope static func feedPredicate( authenticationBox: MastodonAuthenticationBox, diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 0935c9967..474a778d8 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -12,6 +12,7 @@ import MastodonAsset import MastodonLocalization import Tabman import Pageboy +import MastodonCore final class NotificationViewController: TabmanViewController, NeedsDependency { diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index 60967c436..313b206c8 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -8,9 +8,10 @@ import os.log import UIKit import Combine -import MastodonAsset -import MastodonLocalization import Pageboy +import MastodonAsset +import MastodonCore +import MastodonLocalization final class NotificationViewModel { diff --git a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift index 550dae7cc..a90da1e81 100644 --- a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift +++ b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift @@ -11,6 +11,8 @@ import os.log import ThirdPartyMailer import UIKit import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization final class MastodonConfirmEmailViewController: UIViewController, NeedsDependency { @@ -205,10 +207,10 @@ extension MastodonConfirmEmailViewController { } func showEmailAppAlert() { - let clients = ThirdPartyMailClient.clients() + let clients = ThirdPartyMailClient.clients let application = UIApplication.shared let availableClients = clients.filter { client -> Bool in - ThirdPartyMailer.application(application, isMailClientAvailable: client) + ThirdPartyMailer.isMailClientAvailable(client) } let alertController = UIAlertController(title: L10n.Scene.ConfirmEmail.OpenEmailApp.openEmailClient, message: nil, preferredStyle: .alert) @@ -218,7 +220,7 @@ extension MastodonConfirmEmailViewController { alertController.addAction(alertAction) _ = availableClients.compactMap { client -> UIAlertAction in let alertAction = UIAlertAction(title: client.name, style: .default) { _ in - _ = ThirdPartyMailer.application(application, openMailClient: client) + _ = ThirdPartyMailer.open(client, completionHandler: nil) } alertController.addAction(alertAction) return alertAction diff --git a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewModel.swift b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewModel.swift index 35480ba98..bbfbf706b 100644 --- a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewModel.swift +++ b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewModel.swift @@ -7,6 +7,7 @@ import Combine import Foundation +import MastodonCore import MastodonSDK final class MastodonConfirmEmailViewModel { diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index d05b446ae..7845722e5 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -11,6 +11,7 @@ import Combine import GameController import AuthenticationServices import MastodonAsset +import MastodonCore import MastodonLocalization import MastodonUI @@ -281,7 +282,7 @@ extension MastodonPickServerViewController { guard let info = AuthenticationViewModel.AuthenticateInfo( domain: server.domain, application: application, - redirectURI: response.value.redirectURI ?? MastodonAuthenticationController.callbackURL + redirectURI: response.value.redirectURI ?? APIService.oauthCallbackURL ) else { throw APIService.APIError.explicit(.badResponse) } diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift index b077cbbe1..50c1d7aac 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift @@ -13,6 +13,8 @@ import MastodonSDK import CoreDataStack import OrderedCollections import Tabman +import MastodonCore +import MastodonUI class MastodonPickServerViewModel: NSObject { diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift index 5649fe579..3b0ebca8c 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift @@ -8,6 +8,8 @@ import UIKit import Combine import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell { diff --git a/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift b/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift index 784559480..9d6cfc85f 100644 --- a/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift +++ b/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift @@ -8,6 +8,7 @@ import UIKit import MastodonSDK import MastodonAsset +import MastodonUI import MastodonLocalization class PickServerCategoryView: UIView { diff --git a/Mastodon/Scene/Onboarding/PickServer/View/PickServerEmptyStateView.swift b/Mastodon/Scene/Onboarding/PickServer/View/PickServerEmptyStateView.swift index a75570087..5f9b45c10 100644 --- a/Mastodon/Scene/Onboarding/PickServer/View/PickServerEmptyStateView.swift +++ b/Mastodon/Scene/Onboarding/PickServer/View/PickServerEmptyStateView.swift @@ -7,6 +7,8 @@ import UIKit import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization final class PickServerEmptyStateView: UIView { diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index 89c98759f..9b10bd48e 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -14,6 +14,7 @@ import UIKit import SwiftUI import MastodonUI import MastodonAsset +import MastodonCore import MastodonLocalization final class MastodonRegisterViewController: UIViewController, NeedsDependency, OnboardingViewControllerAppearance { diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift index e7fbd307d..a23d4b975 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift @@ -10,6 +10,7 @@ import Foundation import MastodonSDK import UIKit import MastodonAsset +import MastodonCore import MastodonLocalization final class MastodonRegisterViewModel: ObservableObject { diff --git a/Mastodon/Scene/Onboarding/ResendEmail/MastodonResendEmailViewController.swift b/Mastodon/Scene/Onboarding/ResendEmail/MastodonResendEmailViewController.swift index 1d3a29cb5..178d489be 100644 --- a/Mastodon/Scene/Onboarding/ResendEmail/MastodonResendEmailViewController.swift +++ b/Mastodon/Scene/Onboarding/ResendEmail/MastodonResendEmailViewController.swift @@ -9,6 +9,7 @@ import Combine import os.log import UIKit import WebKit +import MastodonCore final class MastodonResendEmailViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController+Debug.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController+Debug.swift index f6aaf5fba..7477d3bc5 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController+Debug.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController+Debug.swift @@ -6,6 +6,7 @@ // import UIKit +import MastodonCore #if DEBUG diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift index 2f13ad193..0d4e27d98 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift @@ -12,6 +12,7 @@ import MastodonSDK import SafariServices import MetaTextKit import MastodonAsset +import MastodonCore import MastodonLocalization final class MastodonServerRulesViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift b/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift index eb3cf5721..920164bce 100644 --- a/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift +++ b/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift @@ -11,6 +11,7 @@ import CoreData import CoreDataStack import Combine import MastodonSDK +import MastodonCore final class AuthenticationViewModel { @@ -121,7 +122,7 @@ extension AuthenticationViewModel { init?( domain: String, application: Mastodon.Entity.Application, - redirectURI: String = MastodonAuthenticationController.callbackURL + redirectURI: String = APIService.oauthCallbackURL ) { self.domain = domain guard let clientID = application.clientID, diff --git a/Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift b/Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift index 470cc79a5..e56c7f126 100644 --- a/Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift +++ b/Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift @@ -9,13 +9,14 @@ import os.log import UIKit import Combine import AuthenticationServices +import MastodonCore final class MastodonAuthenticationController { var disposeBag = Set() // input - var context: AppContext! + var context: AppContext let authenticateURL: URL var authenticationSession: ASWebAuthenticationSession? diff --git a/Mastodon/Scene/Onboarding/Welcome/View/WelcomeIllustrationView.swift b/Mastodon/Scene/Onboarding/Welcome/View/WelcomeIllustrationView.swift index 9a5d6c13e..0530539a2 100644 --- a/Mastodon/Scene/Onboarding/Welcome/View/WelcomeIllustrationView.swift +++ b/Mastodon/Scene/Onboarding/Welcome/View/WelcomeIllustrationView.swift @@ -7,6 +7,8 @@ import UIKit import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization final class WelcomeIllustrationView: UIView { diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift index c64dd469f..41e77987c 100644 --- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift +++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine import MastodonAsset +import MastodonCore import MastodonLocalization final class WelcomeViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewModel.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewModel.swift index 74b13b1a8..d57a6ee5f 100644 --- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewModel.swift +++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewModel.swift @@ -7,6 +7,7 @@ import Foundation import Combine +import MastodonCore final class WelcomeViewModel { diff --git a/Mastodon/Scene/Profile/About/Cell/ProfileFieldAddEntryCollectionViewCell.swift b/Mastodon/Scene/Profile/About/Cell/ProfileFieldAddEntryCollectionViewCell.swift index 9f22886e6..f630ec696 100644 --- a/Mastodon/Scene/Profile/About/Cell/ProfileFieldAddEntryCollectionViewCell.swift +++ b/Mastodon/Scene/Profile/About/Cell/ProfileFieldAddEntryCollectionViewCell.swift @@ -11,6 +11,7 @@ import Combine import MastodonAsset import MastodonLocalization import MetaTextKit +import MastodonCore import MastodonUI final class ProfileFieldAddEntryCollectionViewCell: UICollectionViewCell { diff --git a/Mastodon/Scene/Profile/About/Cell/ProfileFieldEditCollectionViewCell.swift b/Mastodon/Scene/Profile/About/Cell/ProfileFieldEditCollectionViewCell.swift index 43c47f1e1..4286bca03 100644 --- a/Mastodon/Scene/Profile/About/Cell/ProfileFieldEditCollectionViewCell.swift +++ b/Mastodon/Scene/Profile/About/Cell/ProfileFieldEditCollectionViewCell.swift @@ -10,6 +10,8 @@ import UIKit import Combine import MetaTextKit import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization protocol ProfileFieldEditCollectionViewCellDelegate: AnyObject { diff --git a/Mastodon/Scene/Profile/About/ProfileAboutViewController.swift b/Mastodon/Scene/Profile/About/ProfileAboutViewController.swift index 47385813d..eb1e6b39c 100644 --- a/Mastodon/Scene/Profile/About/ProfileAboutViewController.swift +++ b/Mastodon/Scene/Profile/About/ProfileAboutViewController.swift @@ -12,6 +12,7 @@ import MetaTextKit import MastodonLocalization import TabBarPager import XLPagerTabStrip +import MastodonCore protocol ProfileAboutViewControllerDelegate: AnyObject { func profileAboutViewController(_ viewController: ProfileAboutViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta) diff --git a/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift b/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift index ff1e261a2..68a3d0fea 100644 --- a/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift +++ b/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift @@ -11,6 +11,7 @@ import Combine import CoreDataStack import MastodonSDK import MastodonMeta +import MastodonCore import Kanna final class ProfileAboutViewModel { diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift index 18e3c34fd..6d3d80737 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift @@ -11,6 +11,7 @@ import AVKit import Combine import GameplayKit import MastodonAsset +import MastodonCore import MastodonLocalization final class BookmarkViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift index eda823b50..085967ec7 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift @@ -9,6 +9,7 @@ import os.log import Foundation import GameplayKit import MastodonSDK +import MastodonCore extension BookmarkViewModel { class State: GKState, NamingState { @@ -72,7 +73,7 @@ extension BookmarkViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } // reset - viewModel.statusFetchedResultsController.statusIDs.value = [] + viewModel.statusFetchedResultsController.statusIDs = [] stateMachine.enter(Loading.self) } @@ -150,7 +151,7 @@ extension BookmarkViewModel.State { ) var hasNewStatusesAppend = false - var statusIDs = viewModel.statusFetchedResultsController.statusIDs.value + var statusIDs = viewModel.statusFetchedResultsController.statusIDs for status in response.value { guard !statusIDs.contains(status.id) else { continue } statusIDs.append(status.id) @@ -169,7 +170,7 @@ extension BookmarkViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs.value = statusIDs + viewModel.statusFetchedResultsController.statusIDs = statusIDs } catch { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch user bookmarks fail: \(error.localizedDescription)") await enter(state: Fail.self) diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift index 8dc8d734a..df48b68e7 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift @@ -10,6 +10,7 @@ import Combine import CoreData import CoreDataStack import GameplayKit +import MastodonCore final class BookmarkViewModel { @@ -51,7 +52,7 @@ final class BookmarkViewModel { activeMastodonAuthenticationBox .map { $0?.domain } - .assign(to: \.value, on: statusFetchedResultsController.domain) + .assign(to: \.domain, on: statusFetchedResultsController) .store(in: &disposeBag) } diff --git a/Mastodon/Scene/Profile/CachedProfileViewModel.swift b/Mastodon/Scene/Profile/CachedProfileViewModel.swift index c33a905a7..f30fac4ea 100644 --- a/Mastodon/Scene/Profile/CachedProfileViewModel.swift +++ b/Mastodon/Scene/Profile/CachedProfileViewModel.swift @@ -7,6 +7,7 @@ import Foundation import CoreDataStack +import MastodonCore final class CachedProfileViewModel: ProfileViewModel { diff --git a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewController.swift b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewController.swift index 1e60f7a90..c82b8b3ad 100644 --- a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewController.swift +++ b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewController.swift @@ -8,6 +8,7 @@ import os.log import UIKit import Combine +import MastodonCore import MastodonLocalization final class FamiliarFollowersViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel.swift b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel.swift index 544f8a062..065ede47c 100644 --- a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel.swift +++ b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel.swift @@ -7,6 +7,7 @@ import UIKit import Combine +import MastodonCore import MastodonSDK import CoreDataStack diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift index 2ac1e2065..d3cdb2b8e 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift @@ -14,6 +14,7 @@ import AVKit import Combine import GameplayKit import MastodonAsset +import MastodonCore import MastodonLocalization final class FavoriteViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift index 6c539450c..84f2da1db 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift @@ -8,6 +8,7 @@ import os.log import Foundation import GameplayKit +import MastodonCore import MastodonSDK extension FavoriteViewModel { @@ -72,7 +73,7 @@ extension FavoriteViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } // reset - viewModel.statusFetchedResultsController.statusIDs.value = [] + viewModel.statusFetchedResultsController.statusIDs = [] stateMachine.enter(Loading.self) } @@ -150,7 +151,7 @@ extension FavoriteViewModel.State { ) var hasNewStatusesAppend = false - var statusIDs = viewModel.statusFetchedResultsController.statusIDs.value + var statusIDs = viewModel.statusFetchedResultsController.statusIDs for status in response.value { guard !statusIDs.contains(status.id) else { continue } statusIDs.append(status.id) @@ -169,7 +170,7 @@ extension FavoriteViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs.value = statusIDs + viewModel.statusFetchedResultsController.statusIDs = statusIDs } catch { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch user favorites fail: \(error.localizedDescription)") await enter(state: Fail.self) diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift index 150c8f815..0570df2e9 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift @@ -10,6 +10,7 @@ import Combine import CoreData import CoreDataStack import GameplayKit +import MastodonCore final class FavoriteViewModel { @@ -51,7 +52,7 @@ final class FavoriteViewModel { activeMastodonAuthenticationBox .map { $0?.domain } - .assign(to: \.value, on: statusFetchedResultsController.domain) + .assign(to: \.domain, on: statusFetchedResultsController) .store(in: &disposeBag) } diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift index decc1ee97..ff2977d45 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import GameplayKit import Combine +import MastodonCore import MastodonLocalization final class FollowerListViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift index a2958de3c..ffedeae46 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift @@ -9,6 +9,7 @@ import os.log import Foundation import GameplayKit import MastodonSDK +import MastodonCore extension FollowerListViewModel { class State: GKState, NamingState { diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift index 80f26e608..af0f6b139 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift @@ -12,6 +12,7 @@ import CoreData import CoreDataStack import GameplayKit import MastodonSDK +import MastodonCore final class FollowerListViewModel { diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift index c125b0214..18994d26b 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift @@ -10,6 +10,7 @@ import UIKit import GameplayKit import Combine import MastodonLocalization +import MastodonCore final class FollowingListViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift index f1e07f9d8..a809e14d0 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift @@ -7,10 +7,10 @@ import Foundation import Combine -import Combine import CoreData import CoreDataStack import GameplayKit +import MastodonCore import MastodonSDK final class FollowingListViewModel { diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index f35ac6aa4..5c7ce808a 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -15,6 +15,8 @@ import CropViewController import MastodonMeta import MetaTextKit import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization import TabBarPager diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift index e28b250cf..15a13de84 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift @@ -12,6 +12,7 @@ import CoreDataStack import Kanna import MastodonSDK import MastodonMeta +import MastodonCore import MastodonUI final class ProfileHeaderViewModel { diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index b57bf95a5..c51ccfab3 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -11,6 +11,7 @@ import Combine import CoreDataStack import MetaTextKit import MastodonMeta +import MastodonCore import MastodonUI import MastodonAsset import MastodonLocalization diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index e68fd9c7b..9e6e163aa 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -11,6 +11,7 @@ import Combine import FLAnimatedImage import MetaTextKit import MastodonAsset +import MastodonCore import MastodonLocalization import MastodonUI diff --git a/Mastodon/Scene/Profile/MeProfileViewModel.swift b/Mastodon/Scene/Profile/MeProfileViewModel.swift index cee6d5e47..d6a30fd04 100644 --- a/Mastodon/Scene/Profile/MeProfileViewModel.swift +++ b/Mastodon/Scene/Profile/MeProfileViewModel.swift @@ -10,6 +10,7 @@ import UIKit import Combine import CoreData import CoreDataStack +import MastodonCore import MastodonSDK final class MeProfileViewModel: ProfileViewModel { diff --git a/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift b/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift index cc798b6cf..db92617b7 100644 --- a/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift +++ b/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift @@ -11,6 +11,7 @@ import Combine import XLPagerTabStrip import TabBarPager import MastodonAsset +import MastodonCore import MastodonUI protocol ProfilePagingViewControllerDelegate: AnyObject { diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 3e437bb7c..3393a2edf 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -11,8 +11,9 @@ import Combine import MastodonMeta import MetaTextKit import MastodonAsset -import MastodonLocalization +import MastodonCore import MastodonUI +import MastodonLocalization import CoreDataStack import TabBarPager import XLPagerTabStrip diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 9d64df6ee..3893c80a1 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -12,6 +12,7 @@ import CoreDataStack import MastodonSDK import MastodonMeta import MastodonAsset +import MastodonCore import MastodonLocalization import MastodonUI diff --git a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift index bb565c3e0..cb1f0e4c3 100644 --- a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift +++ b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift @@ -10,6 +10,7 @@ import Foundation import Combine import CoreDataStack import MastodonSDK +import MastodonCore final class RemoteProfileViewModel: ProfileViewModel { diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift index fb42b81b8..beed25086 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift @@ -13,6 +13,7 @@ import CoreDataStack import GameplayKit import TabBarPager import XLPagerTabStrip +import MastodonCore final class UserTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift index ca798fa0b..17409a2bc 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift @@ -8,6 +8,7 @@ import os.log import Foundation import GameplayKit +import MastodonCore import MastodonSDK extension UserTimelineViewModel { @@ -72,7 +73,7 @@ extension UserTimelineViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } // reset - viewModel.statusFetchedResultsController.statusIDs.value = [] + viewModel.statusFetchedResultsController.statusIDs = [] stateMachine.enter(Loading.self) } @@ -130,7 +131,7 @@ extension UserTimelineViewModel.State { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - let maxID = viewModel.statusFetchedResultsController.statusIDs.value.last + let maxID = viewModel.statusFetchedResultsController.statusIDs.last guard let userID = viewModel.userIdentifier?.userID, !userID.isEmpty else { stateMachine.enter(Fail.self) @@ -157,7 +158,7 @@ extension UserTimelineViewModel.State { ) var hasNewStatusesAppend = false - var statusIDs = viewModel.statusFetchedResultsController.statusIDs.value + var statusIDs = viewModel.statusFetchedResultsController.statusIDs for status in response.value { guard !statusIDs.contains(status.id) else { continue } statusIDs.append(status.id) @@ -169,7 +170,7 @@ extension UserTimelineViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs.value = statusIDs + viewModel.statusFetchedResultsController.statusIDs = statusIDs } catch { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch user timeline fail: \(error.localizedDescription)") diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift index 2d350fb0b..bd28d2c79 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift @@ -12,6 +12,7 @@ import Combine import CoreData import CoreDataStack import MastodonSDK +import MastodonCore final class UserTimelineViewModel { @@ -64,7 +65,7 @@ final class UserTimelineViewModel { context.authenticationService.activeMastodonAuthenticationBox .map { $0?.domain } - .assign(to: \.value, on: statusFetchedResultsController.domain) + .assign(to: \.domain, on: statusFetchedResultsController) .store(in: &disposeBag) } diff --git a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController.swift b/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController.swift index 0e2bc64ed..5d9a20610 100644 --- a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController.swift +++ b/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import GameplayKit import Combine +import MastodonCore import MastodonLocalization final class FavoritedByViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController.swift b/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController.swift index 78988bb41..3f3e239a1 100644 --- a/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController.swift +++ b/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import GameplayKit import Combine +import MastodonCore import MastodonLocalization final class RebloggedByViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift index 90b928235..9098c7f81 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift @@ -205,7 +205,7 @@ extension UserListViewModel.State { guard let viewModel = viewModel else { return } // trigger reload - viewModel.userFetchedResultsController.records = viewModel.userFetchedResultsController.records + viewModel.userFetchedResultsController.userIDs = viewModel.userFetchedResultsController.userIDs } } } diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift index 472c497f6..f8d9a3bd6 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift @@ -10,6 +10,7 @@ import UIKit import Combine import CoreDataStack import GameplayKit +import MastodonCore final class UserListViewModel { @@ -36,7 +37,7 @@ final class UserListViewModel { return stateMachine }() - init( + public init( context: AppContext, kind: Kind ) { diff --git a/Mastodon/Scene/Report/Report/ReportViewController.swift b/Mastodon/Scene/Report/Report/ReportViewController.swift index 854ea96b6..57107be47 100644 --- a/Mastodon/Scene/Report/Report/ReportViewController.swift +++ b/Mastodon/Scene/Report/Report/ReportViewController.swift @@ -10,6 +10,7 @@ import UIKit import Combine import CoreDataStack import MastodonAsset +import MastodonCore import MastodonLocalization class ReportViewController: UIViewController, NeedsDependency, ReportViewControllerAppearance { diff --git a/Mastodon/Scene/Report/Report/ReportViewModel.swift b/Mastodon/Scene/Report/Report/ReportViewModel.swift index 81ba20125..4e59cb440 100644 --- a/Mastodon/Scene/Report/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/Report/ReportViewModel.swift @@ -14,6 +14,7 @@ import MastodonSDK import OrderedCollections import os.log import UIKit +import MastodonCore import MastodonLocalization class ReportViewModel { diff --git a/Mastodon/Scene/Report/ReportReason/ReportReasonView.swift b/Mastodon/Scene/Report/ReportReason/ReportReasonView.swift index 8ee04311e..763024fa8 100644 --- a/Mastodon/Scene/Report/ReportReason/ReportReasonView.swift +++ b/Mastodon/Scene/Report/ReportReason/ReportReasonView.swift @@ -10,6 +10,7 @@ import SwiftUI import MastodonLocalization import MastodonSDK import MastodonAsset +import MastodonCore struct ReportReasonView: View { diff --git a/Mastodon/Scene/Report/ReportReason/ReportReasonViewController.swift b/Mastodon/Scene/Report/ReportReason/ReportReasonViewController.swift index ac5d9e797..2e8e53d18 100644 --- a/Mastodon/Scene/Report/ReportReason/ReportReasonViewController.swift +++ b/Mastodon/Scene/Report/ReportReason/ReportReasonViewController.swift @@ -11,6 +11,7 @@ import SwiftUI import Combine import MastodonUI import MastodonAsset +import MastodonCore import MastodonLocalization protocol ReportReasonViewControllerDelegate: AnyObject { diff --git a/Mastodon/Scene/Report/ReportReason/ReportReasonViewModel.swift b/Mastodon/Scene/Report/ReportReason/ReportReasonViewModel.swift index 91715cba8..0407307b7 100644 --- a/Mastodon/Scene/Report/ReportReason/ReportReasonViewModel.swift +++ b/Mastodon/Scene/Report/ReportReason/ReportReasonViewModel.swift @@ -8,6 +8,7 @@ import UIKit import SwiftUI import MastodonAsset +import MastodonCore import MastodonSDK import MastodonLocalization diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultView.swift b/Mastodon/Scene/Report/ReportResult/ReportResultView.swift index b3bbe7938..b1ad76415 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultView.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultView.swift @@ -10,6 +10,8 @@ import SwiftUI import MastodonSDK import MastodonUI import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization import CoreDataStack diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift b/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift index 957760f38..1a5aabb67 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift @@ -10,6 +10,7 @@ import UIKit import SwiftUI import Combine import MastodonAsset +import MastodonCore import MastodonLocalization final class ReportResultViewController: UIViewController, NeedsDependency, ReportViewControllerAppearance { diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift b/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift index 67d7475dd..8508d1596 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift @@ -13,6 +13,7 @@ import MastodonSDK import os.log import UIKit import MastodonAsset +import MastodonCore import MastodonUI import MastodonLocalization diff --git a/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesViewController.swift b/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesViewController.swift index 73a47496c..3f1cdf331 100644 --- a/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesViewController.swift +++ b/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesViewController.swift @@ -9,8 +9,9 @@ import os.log import UIKit import SwiftUI import Combine -import MastodonUI import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization protocol ReportServerRulesViewControllerDelegate: AnyObject { diff --git a/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesViewModel.swift b/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesViewModel.swift index 50d2bf2d3..4ea190a74 100644 --- a/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesViewModel.swift +++ b/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesViewModel.swift @@ -8,6 +8,7 @@ import UIKit import SwiftUI import MastodonAsset +import MastodonCore import MastodonSDK import MastodonLocalization diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift index d3844a3be..7ed704434 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift @@ -10,6 +10,7 @@ import UIKit import Combine import CoreDataStack import MastodonAsset +import MastodonCore import MastodonLocalization protocol ReportStatusViewControllerDelegate: AnyObject { diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift index c653fc4ad..6e9d48af0 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift @@ -81,7 +81,7 @@ extension ReportStatusViewModel.State { return } - let maxID = viewModel.statusFetchedResultsController.statusIDs.value.last + let maxID = viewModel.statusFetchedResultsController.statusIDs.last Task { let managedObjectContext = viewModel.context.managedObjectContext @@ -106,7 +106,7 @@ extension ReportStatusViewModel.State { ) var hasNewStatusesAppend = false - var statusIDs = viewModel.statusFetchedResultsController.statusIDs.value + var statusIDs = viewModel.statusFetchedResultsController.statusIDs for status in response.value { guard !statusIDs.contains(status.id) else { continue } statusIDs.append(status.id) @@ -118,7 +118,7 @@ extension ReportStatusViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs.value = statusIDs + viewModel.statusFetchedResultsController.statusIDs = statusIDs } catch { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch user timeline fail: \(error.localizedDescription)") diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift index 239960637..b539909da 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift @@ -14,6 +14,7 @@ import MastodonSDK import OrderedCollections import os.log import UIKit +import MastodonCore class ReportStatusViewModel { @@ -68,7 +69,7 @@ class ReportStatusViewModel { context.authenticationService.activeMastodonAuthenticationBox .map { $0?.domain } - .assign(to: \.value, on: statusFetchedResultsController.domain) + .assign(to: \.domain, on: statusFetchedResultsController) .store(in: &disposeBag) $selectStatuses diff --git a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift index fd7783170..8e44479d3 100644 --- a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift +++ b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine import MastodonAsset +import MastodonCore import MastodonLocalization protocol ReportSupplementaryViewControllerDelegate: AnyObject { diff --git a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift index c07ee1f54..8ddc2d91a 100644 --- a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift +++ b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift @@ -8,6 +8,7 @@ import UIKit import Combine import CoreDataStack +import MastodonCore import MastodonSDK class ReportSupplementaryViewModel { diff --git a/Mastodon/Scene/Root/ContentSplitViewController.swift b/Mastodon/Scene/Root/ContentSplitViewController.swift index 0058f5f6e..1222e4c58 100644 --- a/Mastodon/Scene/Root/ContentSplitViewController.swift +++ b/Mastodon/Scene/Root/ContentSplitViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine import CoreDataStack +import MastodonCore protocol ContentSplitViewControllerDelegate: AnyObject { func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 8970e2f29..39cc23b4b 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -10,6 +10,7 @@ import UIKit import Combine import SafariServices import MastodonAsset +import MastodonCore import MastodonLocalization import MastodonUI @@ -17,7 +18,7 @@ class MainTabBarController: UITabBarController { let logger = Logger(subsystem: "MainTabBarController", category: "UI") - var disposeBag = Set() + public var disposeBag = Set() weak var context: AppContext! weak var coordinator: SceneCoordinator! @@ -220,30 +221,31 @@ extension MainTabBarController { .store(in: &disposeBag) // handle post failure - context.statusPublishService - .latestPublishingComposeViewModel - .receive(on: DispatchQueue.main) - .sink { [weak self] composeViewModel in - guard let self = self else { return } - guard let composeViewModel = composeViewModel else { return } - guard let currentState = composeViewModel.publishStateMachine.currentState else { return } - guard currentState is ComposeViewModel.PublishState.Fail else { return } - - let alertController = UIAlertController(title: L10n.Common.Alerts.PublishPostFailure.title, message: L10n.Common.Alerts.PublishPostFailure.message, preferredStyle: .alert) - let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { [weak self, weak composeViewModel] _ in - guard let self = self else { return } - guard let composeViewModel = composeViewModel else { return } - self.context.statusPublishService.remove(composeViewModel: composeViewModel) - } - alertController.addAction(discardAction) - let retryAction = UIAlertAction(title: L10n.Common.Controls.Actions.tryAgain, style: .default) { [weak composeViewModel] _ in - guard let composeViewModel = composeViewModel else { return } - composeViewModel.publishStateMachine.enter(ComposeViewModel.PublishState.Publishing.self) - } - alertController.addAction(retryAction) - self.present(alertController, animated: true, completion: nil) - } - .store(in: &disposeBag) + // FIXME: refacotr +// context.statusPublishService +// .latestPublishingComposeViewModel +// .receive(on: DispatchQueue.main) +// .sink { [weak self] composeViewModel in +// guard let self = self else { return } +// guard let composeViewModel = composeViewModel else { return } +// guard let currentState = composeViewModel.publishStateMachine.currentState else { return } +// guard currentState is ComposeViewModel.PublishState.Fail else { return } +// +// let alertController = UIAlertController(title: L10n.Common.Alerts.PublishPostFailure.title, message: L10n.Common.Alerts.PublishPostFailure.message, preferredStyle: .alert) +// let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { [weak self, weak composeViewModel] _ in +// guard let self = self else { return } +// guard let composeViewModel = composeViewModel else { return } +// self.context.statusPublishService.remove(composeViewModel: composeViewModel) +// } +// alertController.addAction(discardAction) +// let retryAction = UIAlertAction(title: L10n.Common.Controls.Actions.tryAgain, style: .default) { [weak composeViewModel] _ in +// guard let composeViewModel = composeViewModel else { return } +// composeViewModel.publishStateMachine.enter(ComposeViewModel.PublishState.Publishing.self) +// } +// alertController.addAction(retryAction) +// self.present(alertController, animated: true, completion: nil) +// } +// .store(in: &disposeBag) // handle push notification. // toggle entry when finish fetch latest notification diff --git a/Mastodon/Scene/Root/Sidebar/SecondaryPlaceholderViewController.swift b/Mastodon/Scene/Root/Sidebar/SecondaryPlaceholderViewController.swift index a381844df..6937f1a3f 100644 --- a/Mastodon/Scene/Root/Sidebar/SecondaryPlaceholderViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SecondaryPlaceholderViewController.swift @@ -7,6 +7,7 @@ import UIKit import Combine +import MastodonCore final class SecondaryPlaceholderViewController: UIViewController { var disposeBag = Set() diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift index c7cf3d49d..86d549373 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine import CoreDataStack +import MastodonCore import MastodonUI protocol SidebarViewControllerDelegate: AnyObject { diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index a6698d6c2..967d10b09 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -12,6 +12,7 @@ import CoreDataStack import Meta import MastodonMeta import MastodonAsset +import MastodonCore import MastodonLocalization final class SidebarViewModel { diff --git a/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift b/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift index 794563eaf..515988405 100644 --- a/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift +++ b/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift @@ -9,6 +9,8 @@ import os.log import UIKit import MetaTextKit import FLAnimatedImage +import MastodonCore +import MastodonUI final class SidebarListContentView: UIView, UIContentView { diff --git a/Mastodon/Scene/Search/Search/Cell/TrendCollectionViewCell.swift b/Mastodon/Scene/Search/Search/Cell/TrendCollectionViewCell.swift index 379cba70d..30f618625 100644 --- a/Mastodon/Scene/Search/Search/Cell/TrendCollectionViewCell.swift +++ b/Mastodon/Scene/Search/Search/Cell/TrendCollectionViewCell.swift @@ -9,6 +9,7 @@ import UIKit import Combine import MetaTextKit import MastodonAsset +import MastodonCore import MastodonUI final class TrendCollectionViewCell: UICollectionViewCell { diff --git a/Mastodon/Scene/Search/Search/SearchViewController.swift b/Mastodon/Scene/Search/Search/SearchViewController.swift index 982844f51..947c1593d 100644 --- a/Mastodon/Scene/Search/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/Search/SearchViewController.swift @@ -11,6 +11,7 @@ import GameplayKit import MastodonSDK import UIKit import MastodonAsset +import MastodonCore import MastodonLocalization final class HeightFixedSearchBar: UISearchBar { diff --git a/Mastodon/Scene/Search/Search/SearchViewModel.swift b/Mastodon/Scene/Search/Search/SearchViewModel.swift index b47bc2e88..b0eccd49b 100644 --- a/Mastodon/Scene/Search/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/Search/SearchViewModel.swift @@ -10,6 +10,7 @@ import CoreData import CoreDataStack import Foundation import GameplayKit +import MastodonCore import MastodonSDK import OSLog import UIKit diff --git a/Mastodon/Scene/Search/Search/View/SearchRecommendCollectionHeader.swift b/Mastodon/Scene/Search/Search/View/SearchRecommendCollectionHeader.swift index 19d2e9d4b..1d4788ac8 100644 --- a/Mastodon/Scene/Search/Search/View/SearchRecommendCollectionHeader.swift +++ b/Mastodon/Scene/Search/Search/View/SearchRecommendCollectionHeader.swift @@ -8,6 +8,8 @@ import Foundation import UIKit import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization class SearchRecommendCollectionHeader: UIView { diff --git a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift index 701dc4fa6..0b0a0d003 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift @@ -10,6 +10,7 @@ import UIKit import Combine import Pageboy import MastodonAsset +import MastodonCore import MastodonLocalization final class CustomSearchController: UISearchController { @@ -165,7 +166,7 @@ extension SearchDetailViewController { case .hashtags: viewController.viewModel.hashtags = allSearchScopeViewController.viewModel.hashtags case .posts: - viewController.viewModel.statusFetchedResultsController.statusIDs.value = allSearchScopeViewController.viewModel.statusFetchedResultsController.statusIDs.value + viewController.viewModel.statusFetchedResultsController.statusIDs = allSearchScopeViewController.viewModel.statusFetchedResultsController.statusIDs } } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift index 71663dd66..49bbfa3af 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift @@ -7,6 +7,7 @@ import UIKit import Combine +import MastodonCore import MastodonUI final class SearchHistoryUserCollectionViewCell: UICollectionViewCell { diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift index 0dbb89cf4..7d5f6c60e 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine import CoreDataStack +import MastodonCore final class SearchHistoryViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift index c7a135964..b7987413f 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift @@ -9,6 +9,7 @@ import UIKit import Combine import CoreDataStack import CommonOSLog +import MastodonCore final class SearchHistoryViewModel { diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/View/SearchHistoryTableHeaderView.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/View/SearchHistoryTableHeaderView.swift index 5de09f802..13bf9993f 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/View/SearchHistoryTableHeaderView.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/View/SearchHistoryTableHeaderView.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine import MastodonAsset +import MastodonCore import MastodonLocalization import MastodonUI diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift index f3d989b41..87c981071 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift @@ -8,6 +8,8 @@ import os.log import UIKit import Combine +import MastodonCore +import MastodonUI final class SearchResultViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift index b763547bf..cd1579747 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift @@ -9,6 +9,7 @@ import os.log import Foundation import GameplayKit import MastodonSDK +import MastodonCore extension SearchResultViewModel { class State: GKState, NamingState { @@ -156,7 +157,7 @@ extension SearchResultViewModel.State { // reset data source when the search is refresh if offset == nil { viewModel.userFetchedResultsController.userIDs = [] - viewModel.statusFetchedResultsController.statusIDs.value = [] + viewModel.statusFetchedResultsController.statusIDs = [] viewModel.hashtags = [] } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift index ad012518d..a7b97de6b 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift @@ -12,6 +12,7 @@ import CoreDataStack import GameplayKit import CommonOSLog import MastodonSDK +import MastodonCore final class SearchResultViewModel { @@ -68,7 +69,7 @@ final class SearchResultViewModel { context.authenticationService.activeMastodonAuthenticationBox .map { $0?.domain } - .assign(to: \.value, on: statusFetchedResultsController.domain) + .assign(to: \.domain, on: statusFetchedResultsController) .store(in: &disposeBag) // Publishers.CombineLatest( diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index 8455ac7d9..e17db3e11 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -10,11 +10,13 @@ import UIKit import Combine import CoreData import CoreDataStack -import MastodonSDK -import MetaTextKit -import MastodonMeta import AuthenticationServices +import MetaTextKit +import MastodonSDK +import MastodonMeta import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization class SettingsViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Settings/SettingsViewModel.swift b/Mastodon/Scene/Settings/SettingsViewModel.swift index 1eb9a4094..ea2275429 100644 --- a/Mastodon/Scene/Settings/SettingsViewModel.swift +++ b/Mastodon/Scene/Settings/SettingsViewModel.swift @@ -13,6 +13,7 @@ import MastodonSDK import UIKit import os.log import AuthenticationServices +import MastodonCore class SettingsViewModel { diff --git a/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift b/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift index 8300f865a..ca46193b6 100644 --- a/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift +++ b/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift @@ -10,6 +10,7 @@ import Foundation import Combine import UIKit import MastodonAsset +import MastodonCore import MastodonLocalization import MastodonUI diff --git a/Mastodon/Scene/Share/View/Content/DoubleTitleLabelNavigationBarTitleView.swift b/Mastodon/Scene/Share/View/Content/DoubleTitleLabelNavigationBarTitleView.swift index b6a36f0e0..6734b7b77 100644 --- a/Mastodon/Scene/Share/View/Content/DoubleTitleLabelNavigationBarTitleView.swift +++ b/Mastodon/Scene/Share/View/Content/DoubleTitleLabelNavigationBarTitleView.swift @@ -9,6 +9,8 @@ import UIKit import Meta import MetaTextKit import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization final class DoubleTitleLabelNavigationBarTitleView: UIView { diff --git a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift index 45652321f..ed5e68dc7 100644 --- a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift @@ -13,6 +13,7 @@ import MetaTextKit import MastodonMeta import Meta import MastodonAsset +import MastodonCore import MastodonLocalization import class CoreDataStack.Notification diff --git a/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift b/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift index 99b0f3b6b..7d1baa188 100644 --- a/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift @@ -9,6 +9,7 @@ import UIKit import Combine import CoreDataStack import MetaTextKit +import MastodonCore import MastodonUI extension PollOptionView { diff --git a/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift b/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift index 1bdff4d80..78ca7b88d 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift @@ -10,6 +10,8 @@ import Combine import MastodonUI import CoreDataStack import MastodonSDK +import MastodonCore +import MastodonUI import MastodonLocalization import MastodonMeta import Meta diff --git a/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift b/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift index 0953feecd..8d72d953e 100644 --- a/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift +++ b/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift @@ -7,6 +7,7 @@ import UIKit import MastodonUI +import MastodonCore final class ThreadMetaView: UIView { diff --git a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift index 3d22eedae..2a2130406 100644 --- a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift @@ -11,6 +11,7 @@ import MastodonUI import CoreDataStack import MastodonLocalization import MastodonMeta +import MastodonCore import Meta extension UserView { diff --git a/Mastodon/Scene/Share/View/Decoration/SawToothView.swift b/Mastodon/Scene/Share/View/Decoration/SawToothView.swift index e344b62ef..f154a48e6 100644 --- a/Mastodon/Scene/Share/View/Decoration/SawToothView.swift +++ b/Mastodon/Scene/Share/View/Decoration/SawToothView.swift @@ -8,6 +8,7 @@ import Foundation import UIKit import Combine +import MastodonCore final class SawToothView: UIView { static let widthUint = 8 diff --git a/Mastodon/Scene/Share/View/TableviewCell/ThreadReplyLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/ThreadReplyLoaderTableViewCell.swift index 065a41281..d3abb9e79 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/ThreadReplyLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/ThreadReplyLoaderTableViewCell.swift @@ -9,6 +9,8 @@ import os.log import UIKit import Combine import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization protocol ThreadReplyLoaderTableViewCellDelegate: AnyObject { diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift index 70f366bce..f6c4596ab 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift @@ -7,6 +7,8 @@ import UIKit import Combine +import MastodonCore +import MastodonUI final class TimelineBottomLoaderTableViewCell: TimelineLoaderTableViewCell { diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift index 29344eb28..f9d58da6a 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift @@ -8,6 +8,8 @@ import UIKit import Combine import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization class TimelineLoaderTableViewCell: UITableViewCell { diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift index a12920c59..462a22d48 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift @@ -9,6 +9,7 @@ import Combine import CoreData import os.log import UIKit +import MastodonUI protocol TimelineMiddleLoaderTableViewCellDelegate: AnyObject { func timelineMiddleLoaderTableViewCell(_ cell: TimelineMiddleLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton) diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineTopLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineTopLoaderTableViewCell.swift index 4accee1de..20c117eab 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineTopLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineTopLoaderTableViewCell.swift @@ -7,6 +7,9 @@ import UIKit import Combine +import MastodonCore +import MastodonUI + final class TimelineTopLoaderTableViewCell: TimelineLoaderTableViewCell { override func _init() { diff --git a/Mastodon/Scene/Share/Webview/WebViewController.swift b/Mastodon/Scene/Share/Webview/WebViewController.swift index bde6e8936..cb690de93 100644 --- a/Mastodon/Scene/Share/Webview/WebViewController.swift +++ b/Mastodon/Scene/Share/Webview/WebViewController.swift @@ -10,6 +10,7 @@ import Combine import os.log import UIKit import WebKit +import MastodonCore final class WebViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift index 07c27a721..3dfffa540 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift @@ -12,6 +12,7 @@ import Foundation import OSLog import UIKit import MastodonAsset +import MastodonCore import MastodonLocalization class SuggestionAccountViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift index 0263b61ec..70676bf0f 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift @@ -10,6 +10,7 @@ import CoreData import CoreDataStack import GameplayKit import MastodonSDK +import MastodonCore import os.log import UIKit diff --git a/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell+ViewModel.swift b/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell+ViewModel.swift index 722f76180..6d93be1a9 100644 --- a/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell+ViewModel.swift @@ -9,6 +9,8 @@ import UIKit import Combine import CoreDataStack import MastodonAsset +import MastodonCore +import MastodonUI import MastodonMeta import Meta diff --git a/Mastodon/Scene/Thread/CachedThreadViewModel.swift b/Mastodon/Scene/Thread/CachedThreadViewModel.swift index c4ff3b985..0a39db590 100644 --- a/Mastodon/Scene/Thread/CachedThreadViewModel.swift +++ b/Mastodon/Scene/Thread/CachedThreadViewModel.swift @@ -7,6 +7,7 @@ import Foundation import CoreDataStack +import MastodonCore final class CachedThreadViewModel: ThreadViewModel { init(context: AppContext, status: Status) { diff --git a/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift b/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift index c158270cb..97998fd73 100644 --- a/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift +++ b/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift @@ -12,6 +12,7 @@ import Combine import CoreData import CoreDataStack import MastodonSDK +import MastodonCore import MastodonMeta final class MastodonStatusThreadViewModel { diff --git a/Mastodon/Scene/Thread/RemoteThreadViewModel.swift b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift index 6d2e3d975..da5a9bba0 100644 --- a/Mastodon/Scene/Thread/RemoteThreadViewModel.swift +++ b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift @@ -8,6 +8,7 @@ import os.log import UIKit import CoreDataStack +import MastodonCore import MastodonSDK final class RemoteThreadViewModel: ThreadViewModel { diff --git a/Mastodon/Scene/Thread/ThreadViewController.swift b/Mastodon/Scene/Thread/ThreadViewController.swift index bd90fb370..7915df6e5 100644 --- a/Mastodon/Scene/Thread/ThreadViewController.swift +++ b/Mastodon/Scene/Thread/ThreadViewController.swift @@ -12,6 +12,7 @@ import CoreData import AVKit import MastodonMeta import MastodonAsset +import MastodonCore import MastodonLocalization final class ThreadViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift index ededcb044..a865dd8f0 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift @@ -9,6 +9,7 @@ import UIKit import Combine import CoreData import CoreDataStack +import MastodonCore import MastodonSDK extension ThreadViewModel { diff --git a/Mastodon/Scene/Thread/ThreadViewModel.swift b/Mastodon/Scene/Thread/ThreadViewModel.swift index 5a3127e66..54c9d1599 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel.swift @@ -14,6 +14,7 @@ import GameplayKit import MastodonSDK import MastodonMeta import MastodonAsset +import MastodonCore import MastodonLocalization class ThreadViewModel { diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 54b1fd57e..04f7ea784 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine import CoreDataStack +import MastodonCore #if PROFILE import FPSIndicator diff --git a/MastodonSDK/Sources/MastodonCore/AppContext.swift b/MastodonSDK/Sources/MastodonCore/AppContext.swift index 7c61cd75b..1965a91e2 100644 --- a/MastodonSDK/Sources/MastodonCore/AppContext.swift +++ b/MastodonSDK/Sources/MastodonCore/AppContext.swift @@ -15,7 +15,7 @@ import AlamofireImage public class AppContext: ObservableObject { - var disposeBag = Set() + public var disposeBag = Set() public let coreDataStack: CoreDataStack public let managedObjectContext: NSManagedObjectContext @@ -116,16 +116,16 @@ public class AppContext: ObservableObject { extension AppContext { - typealias ByteCount = Int + public typealias ByteCount = Int - static let byteCountFormatter: ByteCountFormatter = { + public static let byteCountFormatter: ByteCountFormatter = { let formatter = ByteCountFormatter() return formatter }() private static let purgeCacheWorkingQueue = DispatchQueue(label: "org.joinmastodon.app.AppContext.purgeCacheWorkingQueue") - func purgeCache() -> AnyPublisher { + public func purgeCache() -> AnyPublisher { Publishers.MergeMany([ AppContext.purgeAlamofireImageCache(), AppContext.purgeTemporaryDirectory(), diff --git a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Instance.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Instance.swift index 6cacd9db9..4192b68a2 100644 --- a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Instance.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Instance.swift @@ -10,7 +10,7 @@ import CoreDataStack import MastodonSDK extension Instance { - var configuration: Mastodon.Entity.Instance.Configuration? { + public var configuration: Mastodon.Entity.Instance.Configuration? { guard let configurationRaw = configurationRaw else { return nil } guard let configuration = try? JSONDecoder().decode(Mastodon.Entity.Instance.Configuration.self, from: configurationRaw) else { return nil diff --git a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Setting.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Setting.swift index 4d1fc0ca5..bb76b69fb 100644 --- a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Setting.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Setting.swift @@ -15,7 +15,7 @@ extension Setting { // return SettingsItem.AppearanceMode(rawValue: appearanceRaw) ?? .automatic // } - var activeSubscription: Subscription? { + public var activeSubscription: Subscription? { return (subscriptions ?? Set()) .sorted(by: { $0.activedAt > $1.activedAt }) .first diff --git a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status.swift index 2e0cf516a..797abac7f 100644 --- a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status.swift @@ -10,13 +10,13 @@ import Foundation import MastodonSDK extension Status { - enum SensitiveType { + public enum SensitiveType { case none case all case media(isSensitive: Bool) } - var sensitiveType: SensitiveType { + public var sensitiveType: SensitiveType { let spoilerText = self.spoilerText ?? "" // cast .all sensitive when has spoiter text @@ -44,9 +44,9 @@ extension Status { // return author // } //} -// + extension Status { - var statusURL: URL { + public var statusURL: URL { if let urlString = self.url, let url = URL(string: urlString) { @@ -56,7 +56,7 @@ extension Status { } } - var activityItems: [Any] { + public var activityItems: [Any] { var items: [Any] = [] items.append(self.statusURL) return items @@ -71,7 +71,7 @@ extension Status { //} extension Status { - var asRecord: ManagedObjectRecord { + public var asRecord: ManagedObjectRecord { return .init(objectID: self.objectID) } } diff --git a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Subscription.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Subscription.swift index 8253264a0..67f3709d1 100644 --- a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Subscription.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Subscription.swift @@ -9,11 +9,11 @@ import Foundation import CoreDataStack import MastodonSDK -typealias NotificationSubscription = Subscription +public typealias NotificationSubscription = Subscription extension Subscription { - var policy: Mastodon.API.Subscriptions.Policy { + public var policy: Mastodon.API.Subscriptions.Policy { return Mastodon.API.Subscriptions.Policy(rawValue: policyRaw) ?? .all } diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/SearchHistoryFetchedResultController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/SearchHistoryFetchedResultController.swift index c3521c6fe..196c9b8f5 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/SearchHistoryFetchedResultController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/SearchHistoryFetchedResultController.swift @@ -12,19 +12,19 @@ import CoreData import CoreDataStack import MastodonSDK -final class SearchHistoryFetchedResultController: NSObject { +public final class SearchHistoryFetchedResultController: NSObject { var disposeBag = Set() - let fetchedResultsController: NSFetchedResultsController - let domain = CurrentValueSubject(nil) - let userID = CurrentValueSubject(nil) + public let fetchedResultsController: NSFetchedResultsController + public let domain = CurrentValueSubject(nil) + public let userID = CurrentValueSubject(nil) // output let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([]) - @Published var records: [ManagedObjectRecord] = [] + @Published public private(set) var records: [ManagedObjectRecord] = [] - init(managedObjectContext: NSManagedObjectContext) { + public init(managedObjectContext: NSManagedObjectContext) { self.fetchedResultsController = { let fetchRequest = SearchHistory.sortedFetchRequest fetchRequest.returnsObjectsAsFaults = false @@ -70,7 +70,7 @@ final class SearchHistoryFetchedResultController: NSObject { // MARK: - NSFetchedResultsControllerDelegate extension SearchHistoryFetchedResultController: NSFetchedResultsControllerDelegate { - func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { + public func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) let objects = fetchedResultsController.fetchedObjects ?? [] diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift index 10da3f3fa..c08673acb 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift @@ -12,22 +12,22 @@ import CoreData import CoreDataStack import MastodonSDK -final class StatusFetchedResultsController: NSObject { +public final class StatusFetchedResultsController: NSObject { var disposeBag = Set() let fetchedResultsController: NSFetchedResultsController // input - let domain = CurrentValueSubject(nil) - let statusIDs = CurrentValueSubject<[Mastodon.Entity.Status.ID], Never>([]) + @Published public var domain: String? = nil + @Published public var statusIDs: [Mastodon.Entity.Status.ID] = [] // output let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([]) - @Published var records: [ManagedObjectRecord] = [] + @Published public private(set) var records: [ManagedObjectRecord] = [] - init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) { - self.domain.value = domain ?? "" + public init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) { + self.domain = domain ?? "" self.fetchedResultsController = { let fetchRequest = Status.sortedFetchRequest fetchRequest.predicate = Status.predicate(domain: domain ?? "", ids: []) @@ -53,8 +53,8 @@ final class StatusFetchedResultsController: NSObject { fetchedResultsController.delegate = self Publishers.CombineLatest( - self.domain.removeDuplicates(), - self.statusIDs.removeDuplicates() + self.$domain.removeDuplicates(), + self.$statusIDs.removeDuplicates() ) .receive(on: DispatchQueue.main) .sink { [weak self] domain, ids in @@ -78,21 +78,21 @@ final class StatusFetchedResultsController: NSObject { extension StatusFetchedResultsController { public func append(statusIDs: [Mastodon.Entity.Status.ID]) { - var result = self.statusIDs.value + var result = self.statusIDs for statusID in statusIDs where !result.contains(statusID) { result.append(statusID) } - self.statusIDs.value = result + self.statusIDs = result } } // MARK: - NSFetchedResultsControllerDelegate extension StatusFetchedResultsController: NSFetchedResultsControllerDelegate { - func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { + public func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - let indexes = statusIDs.value + let indexes = statusIDs let objects = fetchedResultsController.fetchedObjects ?? [] let items: [NSManagedObjectID] = objects diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift index bd57314bf..d95a62bbb 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift @@ -12,22 +12,22 @@ import CoreData import CoreDataStack import MastodonSDK -final class UserFetchedResultsController: NSObject { +public final class UserFetchedResultsController: NSObject { var disposeBag = Set() let fetchedResultsController: NSFetchedResultsController // input - @Published var domain: String? = nil - @Published var userIDs: [Mastodon.Entity.Account.ID] = [] - @Published var additionalPredicate: NSPredicate? + @Published public var domain: String? = nil + @Published public var userIDs: [Mastodon.Entity.Account.ID] = [] + @Published public var additionalPredicate: NSPredicate? // output let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([]) - @Published var records: [ManagedObjectRecord] = [] + @Published public private(set) var records: [ManagedObjectRecord] = [] - init( + public init( managedObjectContext: NSManagedObjectContext, domain: String?, additionalPredicate: NSPredicate? @@ -96,7 +96,7 @@ extension UserFetchedResultsController { // MARK: - NSFetchedResultsControllerDelegate extension UserFetchedResultsController: NSFetchedResultsControllerDelegate { - func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { + public func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) let indexes = userIDs diff --git a/MastodonSDK/Sources/MastodonUI/Model/PlaintextMetaContent.swift b/MastodonSDK/Sources/MastodonCore/Model/PlaintextMetaContent.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/Model/PlaintextMetaContent.swift rename to MastodonSDK/Sources/MastodonCore/Model/PlaintextMetaContent.swift diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index 520c566c1..68649d24c 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -14,7 +14,7 @@ import MastodonSDK extension APIService { - func accountInfo( + public func accountInfo( domain: String, userID: Mastodon.Entity.Account.ID, authorization: Mastodon.API.OAuth.Authorization @@ -50,7 +50,7 @@ extension APIService { extension APIService { - func accountVerifyCredentials( + public func accountVerifyCredentials( domain: String, authorization: Mastodon.API.OAuth.Authorization ) -> AnyPublisher, Error> { @@ -91,7 +91,7 @@ extension APIService { .eraseToAnyPublisher() } - func accountUpdateCredentials( + public func accountUpdateCredentials( domain: String, query: Mastodon.API.Account.UpdateCredentialQuery, authorization: Mastodon.API.OAuth.Authorization @@ -125,7 +125,7 @@ extension APIService { return response } - func accountRegister( + public func accountRegister( domain: String, query: Mastodon.API.Account.RegisterQuery, authorization: Mastodon.API.OAuth.Authorization @@ -138,7 +138,7 @@ extension APIService { ) } - func accountLookup( + public func accountLookup( domain: String, query: Mastodon.API.Account.AccountLookupQuery, authorization: Mastodon.API.OAuth.Authorization diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+App.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+App.swift index cb46c7e76..82d814294 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+App.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+App.swift @@ -21,7 +21,7 @@ extension APIService { private static let appWebsite = "https://app.joinmastodon.org/ios" - func createApplication(domain: String) -> AnyPublisher, Error> { + public func createApplication(domain: String) -> AnyPublisher, Error> { let query = Mastodon.API.App.CreateQuery( clientName: APIService.clientName, redirectURIs: APIService.oauthCallbackURL, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Authentication.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Authentication.swift index ffd9afd77..d4d096f49 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Authentication.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Authentication.swift @@ -13,7 +13,7 @@ import MastodonSDK extension APIService { - func userAccessToken( + public func userAccessToken( domain: String, clientID: String, clientSecret: String, @@ -34,7 +34,7 @@ extension APIService { ) } - func applicationAccessToken( + public func applicationAccessToken( domain: String, clientID: String, clientSecret: String, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift index 428401703..7c78a65f7 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift @@ -22,7 +22,7 @@ extension APIService { let isFollowing: Bool } - func toggleBlock( + public func toggleBlock( user: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift index 8100b3b58..0d8c243f8 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift @@ -19,7 +19,7 @@ extension APIService { let isBookmarked: Bool } - func bookmark( + public func bookmark( record: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { @@ -98,7 +98,7 @@ extension APIService { } extension APIService { - func bookmarkedStatuses( + public func bookmarkedStatuses( limit: Int = onceRequestStatusMaxCount, maxID: String? = nil, authenticationBox: MastodonAuthenticationBox diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift index 6ae2254e3..f7e595b81 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift @@ -12,6 +12,7 @@ import MastodonSDK import CoreData import CoreDataStack import CommonOSLog +import MastodonCore extension APIService { @@ -21,7 +22,7 @@ extension APIService { let favoritedCount: Int64 } - func favorite( + public func favorite( record: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { @@ -108,7 +109,7 @@ extension APIService { } extension APIService { - func favoritedStatuses( + public func favoritedStatuses( limit: Int = onceRequestStatusMaxCount, maxID: String? = nil, authenticationBox: MastodonAuthenticationBox @@ -152,7 +153,7 @@ extension APIService { } extension APIService { - func favoritedBy( + public func favoritedBy( status: ManagedObjectRecord, query: Mastodon.API.Statuses.FavoriteByQuery, authenticationBox: MastodonAuthenticationBox diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index 1e908a2e4..cfb5b8ee2 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -30,7 +30,7 @@ extension APIService { /// - mastodonUser: target MastodonUser /// - activeMastodonAuthenticationBox: `AuthenticationService.MastodonAuthenticationBox` /// - Returns: publisher for `Relationship` - func toggleFollow( + public func toggleFollow( user: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift index 5c91d282c..f0f0bfb7b 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift @@ -5,8 +5,6 @@ // Created by sxiaojian on 2021/4/27. // -import Foundation - import UIKit import Combine import CoreData @@ -16,7 +14,7 @@ import MastodonSDK extension APIService { - func followRequest( + public func followRequest( userID: Mastodon.Entity.Account.ID, query: Mastodon.API.Account.FollowReqeustQuery, authenticationBox: MastodonAuthenticationBox diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift index f0350013f..0f0ea1a59 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift @@ -14,7 +14,7 @@ import MastodonSDK extension APIService { - func followers( + public func followers( userID: Mastodon.Entity.Account.ID, maxID: String?, authenticationBox: MastodonAuthenticationBox diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift index d0cdc233f..313d715ed 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift @@ -14,7 +14,7 @@ import MastodonSDK extension APIService { - func following( + public func following( userID: Mastodon.Entity.Account.ID, maxID: String?, authenticationBox: MastodonAuthenticationBox diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift index 319cec4f3..d2fbe844a 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift @@ -14,7 +14,7 @@ import MastodonSDK extension APIService { - func hashtagTimeline( + public func hashtagTimeline( domain: String, sinceID: Mastodon.Entity.Status.ID? = nil, maxID: Mastodon.Entity.Status.ID? = nil, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift index 612b60d55..4e65aeadc 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift @@ -14,7 +14,7 @@ import MastodonSDK extension APIService { - func homeTimeline( + public func homeTimeline( sinceID: Mastodon.Entity.Status.ID? = nil, maxID: Mastodon.Entity.Status.ID? = nil, limit: Int = onceRequestStatusMaxCount, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift index 477b1a5b5..93bfcf09a 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift @@ -14,7 +14,7 @@ import MastodonSDK extension APIService { - func instance( + public func instance( domain: String ) -> AnyPublisher, Error> { return Mastodon.API.Instance.instance(session: session, domain: domain) diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift index c93dbcf6f..ee43ddce8 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift @@ -21,7 +21,7 @@ extension APIService { let isMuting: Bool } - func toggleMute( + public func toggleMute( user: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift index 871c8349c..31b6509de 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift @@ -15,32 +15,33 @@ import class CoreDataStack.Notification extension APIService { - enum MastodonNotificationScope: Hashable, CaseIterable { + public enum MastodonNotificationScope: Hashable, CaseIterable { case everything case mentions - var includeTypes: [MastodonNotificationType]? { + public var includeTypes: [MastodonNotificationType]? { switch self { case .everything: return nil case .mentions: return [.mention, .status] } } - var excludeTypes: [MastodonNotificationType]? { + public var excludeTypes: [MastodonNotificationType]? { switch self { case .everything: return nil case .mentions: return [.follow, .followRequest, .reblog, .favourite, .poll] } } - var _excludeTypes: [Mastodon.Entity.Notification.NotificationType]? { + public var _excludeTypes: [Mastodon.Entity.Notification.NotificationType]? { switch self { case .everything: return nil case .mentions: return [.follow, .followRequest, .reblog, .favourite, .poll] } } } - func notifications( + + public func notifications( maxID: Mastodon.Entity.Status.ID?, scope: MastodonNotificationScope, authenticationBox: MastodonAuthenticationBox @@ -158,7 +159,8 @@ extension APIService { } extension APIService { - func notification( + + public func notification( notificationID: Mastodon.Entity.Notification.ID, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Onboarding.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Onboarding.swift index 5cbf455a0..383763359 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Onboarding.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Onboarding.swift @@ -11,7 +11,7 @@ import MastodonSDK extension APIService { - func servers( + public func servers( language: String?, category: String? ) -> AnyPublisher, Error> { @@ -19,11 +19,11 @@ extension APIService { return Mastodon.API.Onboarding.servers(session: session, query: query) } - func categories() -> AnyPublisher, Error> { + public func categories() -> AnyPublisher, Error> { return Mastodon.API.Onboarding.categories(session: session) } - static func stubCategories() -> [Mastodon.Entity.Category] { + public static func stubCategories() -> [Mastodon.Entity.Category] { return Mastodon.Entity.Category.Kind.allCases.map { kind in return Mastodon.Entity.Category(category: kind.rawValue, serversCount: 0) } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift index 9d2895857..5a4b38b29 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift @@ -14,7 +14,7 @@ import MastodonSDK extension APIService { - func poll( + public func poll( poll: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { @@ -55,7 +55,7 @@ extension APIService { extension APIService { - func vote( + public func vote( poll: ManagedObjectRecord, choices: [Int], authenticationBox: MastodonAuthenticationBox diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift index fd0c2f0cd..21f198299 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift @@ -14,7 +14,7 @@ import MastodonSDK extension APIService { - func publicTimeline( + public func publicTimeline( query: Mastodon.API.Timeline.PublicTimelineQuery, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Status]> { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift index 2542636c4..0dc1a40e5 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift @@ -20,7 +20,7 @@ extension APIService { let rebloggedCount: Int64 } - func reblog( + public func reblog( record: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { @@ -108,7 +108,7 @@ extension APIService { } extension APIService { - func rebloggedBy( + public func rebloggedBy( status: ManagedObjectRecord, query: Mastodon.API.Statuses.RebloggedByQuery, authenticationBox: MastodonAuthenticationBox diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Recommend.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Recommend.swift index f88d82305..14255fc82 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Recommend.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Recommend.swift @@ -14,7 +14,7 @@ import OSLog extension APIService { - func suggestionAccount( + public func suggestionAccount( query: Mastodon.API.Suggestions.Query?, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> { @@ -44,7 +44,7 @@ extension APIService { return response } - func suggestionAccountV2( + public func suggestionAccountV2( query: Mastodon.API.Suggestions.Query?, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.V2.SuggestionAccount]> { @@ -77,7 +77,7 @@ extension APIService { extension APIService { - func familiarFollowers( + public func familiarFollowers( query: Mastodon.API.Account.FamiliarFollowersQuery, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.FamiliarFollowers]> { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift index a852eaf67..f5c108725 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift @@ -14,7 +14,7 @@ import MastodonSDK extension APIService { - func relationship( + public func relationship( records: [ManagedObjectRecord], authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift index b7d6484dd..52b169ee7 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift @@ -14,7 +14,7 @@ import MastodonSDK extension APIService { - func status( + public func status( statusID: Mastodon.Entity.Status.ID, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { @@ -47,7 +47,7 @@ extension APIService { return response } - func deleteStatus( + public func deleteStatus( status: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift index f6c36e5b6..ddd782856 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift @@ -14,7 +14,7 @@ import MastodonSDK extension APIService { - func statusContext( + public func statusContext( statusID: Mastodon.Entity.Status.ID, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Trend.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Trend.swift index 47dda6bd2..5432b02a0 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Trend.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Trend.swift @@ -10,7 +10,7 @@ import MastodonSDK extension APIService { - func trendHashtags( + public func trendHashtags( domain: String, query: Mastodon.API.Trends.HashtagQuery? ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Tag]> { @@ -23,7 +23,7 @@ extension APIService { return response } - func trendStatuses( + public func trendStatuses( domain: String, query: Mastodon.API.Trends.StatusQuery ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Status]> { @@ -53,7 +53,7 @@ extension APIService { return response } - func trendLinks( + public func trendLinks( domain: String, query: Mastodon.API.Trends.LinkQuery ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Link]> { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift index 85b8d6153..fdf90a2aa 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift @@ -14,7 +14,7 @@ import MastodonSDK extension APIService { - func userTimeline( + public func userTimeline( accountID: String, maxID: Mastodon.Entity.Status.ID? = nil, sinceID: Mastodon.Entity.Status.ID? = nil, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+WebFinger.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+WebFinger.swift index c506d43dc..b542cbe8d 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+WebFinger.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+WebFinger.swift @@ -21,7 +21,7 @@ extension APIService { .appendingPathComponent("webfinger") } - func webFinger( + public func webFinger( domain: String ) -> AnyPublisher { let url = APIService.webFingerEndpointURL(domain: domain) diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService.swift index b151ee540..d89760576 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService.swift @@ -28,7 +28,7 @@ public final class APIService { let backgroundManagedObjectContext: NSManagedObjectContext // output - let error = PassthroughSubject() + public let error = PassthroughSubject() public init(backgroundManagedObjectContext: NSManagedObjectContext) { self.backgroundManagedObjectContext = backgroundManagedObjectContext diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+MastodonAuthentication.swift b/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+MastodonAuthentication.swift index 15624f71d..1acb52a77 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+MastodonAuthentication.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+MastodonAuthentication.swift @@ -13,7 +13,7 @@ import MastodonSDK extension APIService.CoreData { - static func createOrMergeMastodonAuthentication( + public static func createOrMergeMastodonAuthentication( into managedObjectContext: NSManagedObjectContext, for authenticateMastodonUser: MastodonUser, in domain: String, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+Subscriptions.swift b/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+Subscriptions.swift index d5958cf8f..ec8eec8b2 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+Subscriptions.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+Subscriptions.swift @@ -13,7 +13,7 @@ import MastodonSDK extension APIService.CoreData { - static func createOrFetchSubscription( + public static func createOrFetchSubscription( into managedObjectContext: NSManagedObjectContext, setting: Setting, policy: Mastodon.API.Subscriptions.Policy diff --git a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift index 105543d52..8d69c3558 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift @@ -137,7 +137,7 @@ extension AuthenticationService { .eraseToAnyPublisher() } - func signOutMastodonUser( + public func signOutMastodonUser( authenticationBox: MastodonAuthenticationBox ) async throws { let managedObjectContext = backgroundManagedObjectContext diff --git a/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService+CustomEmojiViewModel.swift b/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService+CustomEmojiViewModel.swift index f28dfa805..38e1e1f7a 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService+CustomEmojiViewModel.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService+CustomEmojiViewModel.swift @@ -17,8 +17,8 @@ extension EmojiService { var disposeBag = Set() // input - let domain: String - weak var service: EmojiService? + public let domain: String + public weak var service: EmojiService? // output private(set) lazy var stateMachine: GKStateMachine = { diff --git a/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService.swift b/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService.swift index 6f44178da..f912217d4 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService.swift @@ -26,7 +26,7 @@ public final class EmojiService { extension EmojiService { - func dequeueCustomEmojiViewModel(for domain: String) -> CustomEmojiViewModel? { + public func dequeueCustomEmojiViewModel(for domain: String) -> CustomEmojiViewModel? { var _customEmojiViewModel: CustomEmojiViewModel? workingQueue.sync { if let viewModel = customEmojiViewModelDict[domain] { diff --git a/MastodonSDK/Sources/MastodonCore/Service/Emoji/Trie.swift b/MastodonSDK/Sources/MastodonCore/Service/Emoji/Trie.swift index cfb9c4362..85f948599 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Emoji/Trie.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Emoji/Trie.swift @@ -41,19 +41,19 @@ extension Trie { } extension ArraySlice { - var decomposed: (Element, ArraySlice)? { + public var decomposed: (Element, ArraySlice)? { return isEmpty ? nil : (self[startIndex], self.dropFirst()) } } extension Trie { - func lookup(key: ArraySlice) -> Bool { + public func lookup(key: ArraySlice) -> Bool { guard let (head, tail) = key.decomposed else { return isElement } guard let subtrie = children[head] else { return false } return subtrie.lookup(key: tail) } - func lookup(key: ArraySlice) -> Trie? { + public func lookup(key: ArraySlice) -> Trie? { guard let (head, tail) = key.decomposed else { return self } guard let remainder = children[head] else { return nil } return remainder.lookup(key: tail) @@ -61,13 +61,13 @@ extension Trie { } extension Trie { - func complete(key: ArraySlice) -> [[Element]] { + public func complete(key: ArraySlice) -> [[Element]] { return lookup(key: key)?.elements ?? [] } } extension Trie { - mutating func inserted(_ key: ArraySlice, value: Any) { + public mutating func inserted(_ key: ArraySlice, value: Any) { guard let (head, tail) = key.decomposed else { self.valueSet.add(value) return @@ -83,7 +83,7 @@ extension Trie { } extension Trie { - func passthrough(_ key: ArraySlice) -> [Trie] { + public func passthrough(_ key: ArraySlice) -> [Trie] { guard let (head, tail) = key.decomposed else { return [self] } @@ -96,7 +96,7 @@ extension Trie { } } - var values: NSSet { + public var values: NSSet { let valueSet = NSMutableSet(set: self.valueSet) for (_, value) in children { valueSet.addObjects(from: Array(value.values)) diff --git a/MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService+UploadState.swift b/MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService+UploadState.swift index 8ff076dc1..c4a403fc9 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService+UploadState.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService+UploadState.swift @@ -12,14 +12,14 @@ import GameplayKit import MastodonSDK extension MastodonAttachmentService { - class UploadState: GKState { + public class UploadState: GKState { weak var service: MastodonAttachmentService? init(service: MastodonAttachmentService) { self.service = service } - override func didEnter(from previousState: GKState?) { + public 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) service?.uploadStateMachineSubject.send(self) } @@ -28,8 +28,8 @@ extension MastodonAttachmentService { extension MastodonAttachmentService.UploadState { - class Initial: MastodonAttachmentService.UploadState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { + public class Initial: MastodonAttachmentService.UploadState { + public override func isValidNextState(_ stateClass: AnyClass) -> Bool { guard service?.authenticationBox != nil else { return false } if stateClass == Initial.self { return true @@ -43,17 +43,17 @@ extension MastodonAttachmentService.UploadState { } } - class Uploading: MastodonAttachmentService.UploadState { + public class Uploading: MastodonAttachmentService.UploadState { var needsFallback = false - override func isValidNextState(_ stateClass: AnyClass) -> Bool { + public override func isValidNextState(_ stateClass: AnyClass) -> Bool { return stateClass == Fail.self || stateClass == Finish.self || stateClass == Uploading.self || stateClass == Processing.self } - override func didEnter(from previousState: GKState?) { + public override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) guard let service = service, let stateMachine = stateMachine else { return } @@ -110,16 +110,16 @@ extension MastodonAttachmentService.UploadState { } } - class Processing: MastodonAttachmentService.UploadState { + public class Processing: MastodonAttachmentService.UploadState { static let retryLimit = 10 var retryCount = 0 - override func isValidNextState(_ stateClass: AnyClass) -> Bool { + public override func isValidNextState(_ stateClass: AnyClass) -> Bool { return stateClass == Fail.self || stateClass == Finish.self || stateClass == Processing.self } - override func didEnter(from previousState: GKState?) { + public override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) guard let service = service, let stateMachine = stateMachine else { return } @@ -165,15 +165,15 @@ extension MastodonAttachmentService.UploadState { } } - class Fail: MastodonAttachmentService.UploadState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { + public class Fail: MastodonAttachmentService.UploadState { + public override func isValidNextState(_ stateClass: AnyClass) -> Bool { // allow discard publishing return stateClass == Uploading.self || stateClass == Finish.self } } - class Finish: MastodonAttachmentService.UploadState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { + public class Finish: MastodonAttachmentService.UploadState { + public override func isValidNextState(_ stateClass: AnyClass) -> Bool { return false } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService.swift b/MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService.swift index edb8e7ae6..1af18efbe 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService.swift @@ -13,29 +13,29 @@ import GameplayKit import MobileCoreServices import MastodonSDK -protocol MastodonAttachmentServiceDelegate: AnyObject { +public protocol MastodonAttachmentServiceDelegate: AnyObject { func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) } -final class MastodonAttachmentService { +public final class MastodonAttachmentService { - var disposeBag = Set() - weak var delegate: MastodonAttachmentServiceDelegate? + public var disposeBag = Set() + public weak var delegate: MastodonAttachmentServiceDelegate? - let identifier = UUID() + public let identifier = UUID() // input - let context: AppContext - var authenticationBox: MastodonAuthenticationBox? - let file = CurrentValueSubject(nil) - let description = CurrentValueSubject(nil) + public let context: AppContext + public var authenticationBox: MastodonAuthenticationBox? + public let file = CurrentValueSubject(nil) + public let description = CurrentValueSubject(nil) // output - let thumbnailImage = CurrentValueSubject(nil) - let attachment = CurrentValueSubject(nil) - let error = CurrentValueSubject(nil) + public let thumbnailImage = CurrentValueSubject(nil) + public let attachment = CurrentValueSubject(nil) + public let error = CurrentValueSubject(nil) - private(set) lazy var uploadStateMachine: GKStateMachine = { + public private(set) lazy var uploadStateMachine: GKStateMachine = { // exclude timeline middle fetcher state let stateMachine = GKStateMachine(states: [ UploadState.Initial(service: self), @@ -47,9 +47,9 @@ final class MastodonAttachmentService { stateMachine.enter(UploadState.Initial.self) return stateMachine }() - lazy var uploadStateMachineSubject = CurrentValueSubject(nil) + public lazy var uploadStateMachineSubject = CurrentValueSubject(nil) - init( + public init( context: AppContext, pickerResult: PHPickerResult, initialAuthenticationBox: MastodonAuthenticationBox? @@ -87,7 +87,7 @@ final class MastodonAttachmentService { .store(in: &disposeBag) } - init( + public init( context: AppContext, image: UIImage, initialAuthenticationBox: MastodonAuthenticationBox? @@ -102,7 +102,7 @@ final class MastodonAttachmentService { uploadStateMachine.enter(UploadState.Initial.self) } - init( + public init( context: AppContext, documentURL: URL, initialAuthenticationBox: MastodonAuthenticationBox? @@ -183,7 +183,7 @@ final class MastodonAttachmentService { } extension MastodonAttachmentService { - enum AttachmentError: Error { + public enum AttachmentError: Error { case invalidAttachmentType case attachmentTooLarge } @@ -199,11 +199,11 @@ extension MastodonAttachmentService { extension MastodonAttachmentService: Equatable, Hashable { - static func == (lhs: MastodonAttachmentService, rhs: MastodonAttachmentService) -> Bool { + public static func == (lhs: MastodonAttachmentService, rhs: MastodonAttachmentService) -> Bool { return lhs.identifier == rhs.identifier } - func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(identifier) } diff --git a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift index 4e08cd659..fff84cd4e 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift @@ -141,7 +141,7 @@ extension NotificationService { } extension NotificationService { - func clearNotificationCountForActiveUser() { + public func clearNotificationCountForActiveUser() { guard let authenticationService = self.authenticationService else { return } if let accessToken = authenticationService.activeMastodonAuthentication.value?.userAccessToken { UserDefaults.shared.setNotificationCountWithAccessToken(accessToken: accessToken, value: 0) diff --git a/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift b/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift index fc7151605..b0932450b 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift @@ -27,7 +27,7 @@ public final class SettingService { // output let settingFetchedResultController: SettingFetchedResultController - let currentSetting = CurrentValueSubject(nil) + public let currentSetting = CurrentValueSubject(nil) init( apiService: APIService, @@ -176,7 +176,7 @@ public final class SettingService { extension SettingService { - static func openSettingsAlertController(title: String, message: String) -> UIAlertController { + public static func openSettingsAlertController(title: String, message: String) -> UIAlertController { let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) let settingAction = UIAlertAction(title: L10n.Common.Controls.Actions.settings, style: .default) { _ in guard let url = URL(string: UIApplication.openSettingsURLString) else { return } diff --git a/MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift b/MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift index 692a43706..92bb10def 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift @@ -20,10 +20,10 @@ public final class StatusFilterService { // input weak var apiService: APIService? weak var authenticationService: AuthenticationService? - let filterUpdatePublisher = PassthroughSubject() + public let filterUpdatePublisher = PassthroughSubject() // output - @Published var activeFilters: [Mastodon.Entity.Filter] = [] + @Published public var activeFilters: [Mastodon.Entity.Filter] = [] init( apiService: APIService, diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+ViewModel.swift index a9bb2c5f0..8de1eead2 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+ViewModel.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine import CoreDataStack +import MastodonCore import MastodonMeta import MastodonLocalization diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift index 5848844a2..cacb56a8a 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift @@ -10,6 +10,7 @@ import UIKit import Combine import MetaTextKit import Meta +import MastodonCore import MastodonAsset import MastodonLocalization diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+Configuration.swift index 350c43736..fcf83e75b 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+Configuration.swift @@ -9,6 +9,7 @@ import Foundation import Combine import CoreDataStack import Meta +import MastodonCore import MastodonMeta import MastodonSDK diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index 4c983df34..084dca3d1 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -11,6 +11,7 @@ import Combine import MetaTextKit import Meta import MastodonAsset +import MastodonCore import MastodonLocalization public protocol StatusViewDelegate: AnyObject { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift index 0a970e884..0cbad4f5b 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine import MetaTextKit +import MastodonCore extension UserView { public final class ViewModel: ObservableObject { From e7509dcd7a764409c632153f38e96a90634fadc9 Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 8 Oct 2022 14:01:21 +0800 Subject: [PATCH 033/658] chore: fix GitHub CI --- .env.example | 7 +++++++ .github/scripts/setup.sh | 5 ++--- .github/workflows/main.yml | 3 +++ Documentation/Acknowledgments.md | 9 +++++---- Documentation/Setup.md | 13 +++++++++---- 5 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..d76d4eaae --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Required + +# https:///relay-to/development +NotificationEndpointDebug="" + +# https:///relay-to/production +NotificationEndpointRelease="" diff --git a/.github/scripts/setup.sh b/.github/scripts/setup.sh index 845730f1f..a630e28cb 100755 --- a/.github/scripts/setup.sh +++ b/.github/scripts/setup.sh @@ -9,8 +9,7 @@ gem install bundler:2.3.11 # Install Ruby Gems bundle install -# stub keys. DO NOT use in production -bundle exec pod keys set notification_endpoint "" -bundle exec pod keys set notification_endpoint_debug "" +# Setup notification endpoint +bundle exec arkana bundle exec pod install diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a2f99d23e..e4a1876a8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,6 +22,9 @@ jobs: - name: force Xcode 13.2.1 run: sudo xcode-select -switch /Applications/Xcode_13.2.1.app - name: setup + env: + NotificationEndpointDebug: ${{ secrets.NotificationEndpointDebug }} + NotificationEndpointRelease: ${{ secrets.NotificationEndpointRelease }} run: exec ./.github/scripts/setup.sh - name: build run: exec ./.github/scripts/build.sh diff --git a/Documentation/Acknowledgments.md b/Documentation/Acknowledgments.md index ff6dbc081..154514f5f 100644 --- a/Documentation/Acknowledgments.md +++ b/Documentation/Acknowledgments.md @@ -1,8 +1,9 @@ # Acknowledgments +- [Alamofire](https://github.com/Alamofire/Alamofire) - [AlamofireImage](https://github.com/Alamofire/AlamofireImage) - [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator) -- [Alamofire](https://github.com/Alamofire/Alamofire) +- [Arkana](https://github.com/rogerluan/arkana) - [CommonOSLog](https://github.com/mainasuk/CommonOSLog) - [CryptoSwift](https://github.com/krzyzanowskim/CryptoSwift) - [DateToolSwift](https://github.com/MatthewYork/DateTools) @@ -26,10 +27,10 @@ - [SwiftGen](https://github.com/SwiftGen/SwiftGen) - [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect) - [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON) -- [Tabman](https://github.com/uias/Tabman) - [TabBarPager](https://github.com/TwidereProject/TabBarPager) -- [TwidereX-iOS](https://github.com/TwidereProject/TwidereX-iOS) +- [Tabman](https://github.com/uias/Tabman) - [ThirdPartyMailer](https://github.com/vtourraine/ThirdPartyMailer) - [TOCropViewController](https://github.com/TimOliver/TOCropViewController) +- [TwidereX-iOS](https://github.com/TwidereProject/TwidereX-iOS) - [TwitterProfile](https://github.com/OfTheWolf/TwitterProfile) -- [UITextView-Placeholder](https://github.com/devxoul/UITextView-Placeholder) \ No newline at end of file +- [UITextView-Placeholder](https://github.com/devxoul/UITextView-Placeholder) diff --git a/Documentation/Setup.md b/Documentation/Setup.md index 1c2f0a6c5..545b7ac5a 100644 --- a/Documentation/Setup.md +++ b/Documentation/Setup.md @@ -12,7 +12,7 @@ Intell the latest version of Xcode from the App Store or Apple Developer Downloa This guide may not suit your machine and actually setup procedure may change in the future. Please file the issue or Pull Request if there are any problems. ## CocoaPods -The app use [CocoaPods]() and [CocoaPods-Keys](https://github.com/orta/cocoapods-keys). Ruby Gems are managed through Bundler. The M1 Mac needs virtual ruby env to workaround compatibility issues. +The app use [CocoaPods]() and [Arkana](https://github.com/rogerluan/arkana). Ruby Gems are managed through Bundler. The M1 Mac needs virtual ruby env to workaround compatibility issues. #### Intel Mac @@ -50,6 +50,12 @@ bundle install ```zsh # make a clean build bundle install + +# setup notification endpoint +# please check the `.env.example` to create your's or use the empty example directly +bundle exec arkana -e `` + +# clean pods bundle exec pod clean # make install @@ -64,9 +70,8 @@ The CocoaPods-Key plugin will request the push notification endpoint. You can fu The app requires the `App Group` capability. To make sure it works for your developer membership. Please check [AppSecret.swift](../AppShared/AppSecret.swift) file and set another unique `groupID` and update `App Group` settings. #### Push Notification (Optional) -The app is compatible with [toot-relay](https://github.com/DagAgren/toot-relay) APNs. You can set your push notification endpoint via Cocoapod-Keys. There are two endpoints: -- notification_endpoint: for `RELEASE` usage -- notification_endpoint_debug: for `DEBUG` usage +The app is compatible with [toot-relay](https://github.com/DagAgren/toot-relay) APNs. You can set your push notification endpoint via Arkana. There are one endpoint: +- NotificationEndpoint: (e.g. https:///relay-to/production) Please check the [Establishing a Certificate-Based Connection to APNs ](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/establishing_a_certificate-based_connection_to_apns) document to generate the certificate and exports the p12 file. From db86bce8cfcf15ab8808dd60ef935b155ddbbfe3 Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 8 Oct 2022 14:04:49 +0800 Subject: [PATCH 034/658] fix: Podfile target issue --- Documentation/Setup.md | 2 +- MastodonSDK/Sources/MastodonCore/AppSecret.swift | 2 +- Podfile | 5 ----- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Documentation/Setup.md b/Documentation/Setup.md index 545b7ac5a..e0e2eb3f2 100644 --- a/Documentation/Setup.md +++ b/Documentation/Setup.md @@ -67,7 +67,7 @@ open Mastodon.xcworkspace The CocoaPods-Key plugin will request the push notification endpoint. You can fufill the empty string and set it later. To setup the push notification. Please check section `Push Notification` below. -The app requires the `App Group` capability. To make sure it works for your developer membership. Please check [AppSecret.swift](../AppShared/AppSecret.swift) file and set another unique `groupID` and update `App Group` settings. +The app requires the `App Group` capability. To make sure it works for your developer membership. Please check [AppSecret.swift](../MastodonSDK/Sources/MastodonCore/AppSecret.swift) file and set another unique `groupID` and update `App Group` settings. #### Push Notification (Optional) The app is compatible with [toot-relay](https://github.com/DagAgren/toot-relay) APNs. You can set your push notification endpoint via Arkana. There are one endpoint: diff --git a/MastodonSDK/Sources/MastodonCore/AppSecret.swift b/MastodonSDK/Sources/MastodonCore/AppSecret.swift index 3d595c143..59c7a7cb5 100644 --- a/MastodonSDK/Sources/MastodonCore/AppSecret.swift +++ b/MastodonSDK/Sources/MastodonCore/AppSecret.swift @@ -1,6 +1,6 @@ // // AppSecret.swift -// AppShared +// MastodonCommon // // Created by MainasuK Cirno on 2021-4-27. // diff --git a/Podfile b/Podfile index 774148fa0..3c482446a 100644 --- a/Podfile +++ b/Podfile @@ -30,11 +30,6 @@ target 'Mastodon' do end -target 'AppShared' do - # Comment the next line if you don't want to use dynamic frameworks - use_frameworks! -end - post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| From f73241caeea81d743a96c4844af5ec7ef176a3fe Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 8 Oct 2022 15:16:10 +0800 Subject: [PATCH 035/658] chore: inject AuthContext --- .github/workflows/main.yml | 4 +- Mastodon/Coordinator/SceneCoordinator.swift | 71 +++++++++---------- .../HomeTimelineViewController.swift | 1 - .../Root/ContentSplitViewController.swift | 4 +- .../Root/MainTab/MainTabBarController.swift | 9 ++- .../Scene/Root/RootSplitViewController.swift | 8 ++- .../Settings/SettingsViewController.swift | 1 - Mastodon/Supporting Files/SceneDelegate.swift | 1 - MastodonSDK/Package.swift | 2 +- .../Sources/MastodonCore/AuthContext.swift | 64 +++++++++++++++++ .../MastodonAuthenticationBox.swift | 14 ++++ .../MastodonSDK/API/Mastodon+API+OAuth.swift | 6 +- 12 files changed, 137 insertions(+), 48 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonCore/AuthContext.swift diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e4a1876a8..827276208 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,12 +15,10 @@ on: jobs: build: name: CI build - runs-on: macos-11 + runs-on: macos-12 steps: - name: checkout uses: actions/checkout@v2 - - name: force Xcode 13.2.1 - run: sudo xcode-select -switch /Applications/Xcode_13.2.1.app - name: setup env: NotificationEndpointDebug: ${{ secrets.NotificationEndpointDebug }} diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index bdfa727a0..3aeafaabd 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -22,6 +22,8 @@ final public class SceneCoordinator { private weak var sceneDelegate: SceneDelegate! private weak var appContext: AppContext! + private var authContext: AuthContext? + let id = UUID().uuidString private(set) weak var tabBarController: MainTabBarController! @@ -30,7 +32,11 @@ final public class SceneCoordinator { private(set) var secondaryStackHashValues = Set() - init(scene: UIScene, sceneDelegate: SceneDelegate, appContext: AppContext) { + init( + scene: UIScene, + sceneDelegate: SceneDelegate, + appContext: AppContext + ) { self.scene = scene self.sceneDelegate = sceneDelegate self.appContext = appContext @@ -225,55 +231,48 @@ extension SceneCoordinator { func setup() { let rootViewController: UIViewController - switch UIDevice.current.userInterfaceIdiom { - case .phone: - let viewController = MainTabBarController(context: appContext, coordinator: self) - self.splitViewController = nil - self.tabBarController = viewController - rootViewController = viewController - default: - let splitViewController = RootSplitViewController(context: appContext, coordinator: self) - self.splitViewController = splitViewController - self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController - rootViewController = splitViewController - } - let wizardViewController = WizardViewController() - if !wizardViewController.items.isEmpty, - let delegate = rootViewController as? WizardViewControllerDelegate - { - // do not add as child view controller. - // otherwise, the tab bar controller will add as a new tab - wizardViewController.delegate = delegate - wizardViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] - wizardViewController.view.frame = rootViewController.view.bounds - rootViewController.view.addSubview(wizardViewController.view) - self.wizardViewController = wizardViewController - } - - sceneDelegate.window?.rootViewController = rootViewController - } - - func setupOnboardingIfNeeds(animated: Bool) { - // Check user authentication status and show onboarding if needs do { let request = MastodonAuthentication.sortedFetchRequest - if try appContext.managedObjectContext.count(for: request) == 0 { + let _authentication = try appContext.managedObjectContext.fetch(request).first + let _authContext = _authentication.flatMap { AuthContext(authentication: $0) } + self.authContext = _authContext + + switch UIDevice.current.userInterfaceIdiom { + case .phone: + let viewController = MainTabBarController(context: appContext, coordinator: self, authContext: _authContext) + self.splitViewController = nil + self.tabBarController = viewController + rootViewController = viewController + default: + let splitViewController = RootSplitViewController(context: appContext, coordinator: self, authContext: _authContext) + self.splitViewController = splitViewController + self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController + rootViewController = splitViewController + } + sceneDelegate.window?.rootViewController = rootViewController // base: main + + if _authContext == nil { // entry #1: welcome DispatchQueue.main.async { - self.present( + _ = self.present( scene: .welcome, from: self.sceneDelegate.window?.rootViewController, - transition: .modal(animated: animated, completion: nil) + transition: .modal(animated: true, completion: nil) ) } } + } catch { assertionFailure(error.localizedDescription) + Task { + try? await Task.sleep(nanoseconds: .second * 2) + setup() // entry #2: retry + } // end Task } } - - @discardableResult + @MainActor + @discardableResult func present(scene: Scene, from sender: UIViewController?, transition: Transition) -> UIViewController? { guard let viewController = get(scene: scene) else { return nil diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 00433be44..7be12e8bf 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -410,7 +410,6 @@ extension HomeTimelineViewController { Task { @MainActor in try await context.authenticationService.signOutMastodonUser(authenticationBox: authenticationBox) self.coordinator.setup() - self.coordinator.setupOnboardingIfNeeds(animated: true) } } diff --git a/Mastodon/Scene/Root/ContentSplitViewController.swift b/Mastodon/Scene/Root/ContentSplitViewController.swift index 1222e4c58..503c56de5 100644 --- a/Mastodon/Scene/Root/ContentSplitViewController.swift +++ b/Mastodon/Scene/Root/ContentSplitViewController.swift @@ -24,6 +24,8 @@ final class ContentSplitViewController: UIViewController, NeedsDependency { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + var authContext: AuthContext? + weak var delegate: ContentSplitViewControllerDelegate? private(set) lazy var sidebarViewController: SidebarViewController = { @@ -37,7 +39,7 @@ final class ContentSplitViewController: UIViewController, NeedsDependency { @Published var currentSupplementaryTab: MainTabBarController.Tab = .home private(set) lazy var mainTabBarController: MainTabBarController = { - let mainTabBarController = MainTabBarController(context: context, coordinator: coordinator) + let mainTabBarController = MainTabBarController(context: context, coordinator: coordinator, authContext: authContext) if let homeTimelineViewController = mainTabBarController.viewController(of: HomeTimelineViewController.self) { homeTimelineViewController.viewModel.displaySettingBarButtonItem = false } diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 39cc23b4b..5c910390c 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -23,6 +23,8 @@ class MainTabBarController: UITabBarController { weak var context: AppContext! weak var coordinator: SceneCoordinator! + var authContext: AuthContext? + let composeButttonShadowBackgroundContainer = ShadowBackgroundContainer() let composeButton: UIButton = { let button = UIButton() @@ -143,9 +145,14 @@ class MainTabBarController: UITabBarController { var avatarURLObserver: AnyCancellable? @Published var avatarURL: URL? - init(context: AppContext, coordinator: SceneCoordinator) { + init( + context: AppContext, + coordinator: SceneCoordinator, + authContext: AuthContext? + ) { self.context = context self.coordinator = coordinator + self.authContext = authContext super.init(nibName: nil, bundle: nil) } diff --git a/Mastodon/Scene/Root/RootSplitViewController.swift b/Mastodon/Scene/Root/RootSplitViewController.swift index 1ab2ee0f6..3a68d3342 100644 --- a/Mastodon/Scene/Root/RootSplitViewController.swift +++ b/Mastodon/Scene/Root/RootSplitViewController.swift @@ -20,12 +20,15 @@ final class RootSplitViewController: UISplitViewController, NeedsDependency { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + var authContext: AuthContext? + private var isPrimaryDisplay = false private(set) lazy var contentSplitViewController: ContentSplitViewController = { let contentSplitViewController = ContentSplitViewController() contentSplitViewController.context = context contentSplitViewController.coordinator = coordinator + contentSplitViewController.authContext = authContext contentSplitViewController.delegate = self return contentSplitViewController }() @@ -37,13 +40,14 @@ final class RootSplitViewController: UISplitViewController, NeedsDependency { return searchViewController }() - lazy var compactMainTabBarViewController = MainTabBarController(context: context, coordinator: coordinator) + lazy var compactMainTabBarViewController = MainTabBarController(context: context, coordinator: coordinator, authContext: authContext) let separatorLine = UIView.separatorLine - init(context: AppContext, coordinator: SceneCoordinator) { + init(context: AppContext, coordinator: SceneCoordinator, authContext: AuthContext?) { self.context = context self.coordinator = coordinator + self.authContext = authContext super.init(style: .doubleColumn) primaryEdge = .trailing diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index e17db3e11..edafbe1a3 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -299,7 +299,6 @@ extension SettingsViewController { Task { @MainActor in try await context.authenticationService.signOutMastodonUser(authenticationBox: authenticationBox) self.coordinator.setup() - self.coordinator.setupOnboardingIfNeeds(animated: true) } } diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 04f7ea784..c1e6d7abe 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -58,7 +58,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.coordinator = sceneCoordinator sceneCoordinator.setup() - sceneCoordinator.setupOnboardingIfNeeds(animated: false) window.makeKeyAndVisible() #if SNAPSHOT diff --git a/MastodonSDK/Package.swift b/MastodonSDK/Package.swift index 1cc1d9ff0..7b0fe6af6 100644 --- a/MastodonSDK/Package.swift +++ b/MastodonSDK/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.7 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/MastodonSDK/Sources/MastodonCore/AuthContext.swift b/MastodonSDK/Sources/MastodonCore/AuthContext.swift new file mode 100644 index 000000000..b93a2e03a --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/AuthContext.swift @@ -0,0 +1,64 @@ +// +// AuthContext.swift +// +// +// Created by MainasuK on 22/10/8. +// + +import os.log +import Foundation +import Combine +import CoreDataStack +import MastodonSDK + +public protocol AuthContextProvider { + var authContext: AuthContext { get } +} + +public class AuthContext { + + var disposeBag = Set() + + let logger = Logger(subsystem: "AuthContext", category: "AuthContext") + + // Mastodon + public private(set) var mastodonAuthenticationBox: MastodonAuthenticationBox + + private init(mastodonAuthenticationBox: MastodonAuthenticationBox) { + self.mastodonAuthenticationBox = mastodonAuthenticationBox + } + +} + +extension AuthContext { + + public convenience init?(authentication: MastodonAuthentication) { + self.init(mastodonAuthenticationBox: MastodonAuthenticationBox(authentication: authentication)) + + ManagedObjectObserver.observe(object: authentication) + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + guard let self = self else { return } + switch completion { + case .failure(let error): + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(error.localizedDescription)") + case .finished: + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): observer finished") + } + } receiveValue: { [weak self] change in + guard let self = self else { return } + switch change.changeType { + case .update(let object): + guard let authentication = object as? MastodonAuthentication else { + assertionFailure() + return + } + self.mastodonAuthenticationBox = .init(authentication: authentication) + default: + break + } + } + .store(in: &disposeBag) + } + +} diff --git a/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift b/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift index 14cb03ad7..ec6cb0bfb 100644 --- a/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift +++ b/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift @@ -30,3 +30,17 @@ public struct MastodonAuthenticationBox: UserIdentifier { self.userAuthorization = userAuthorization } } + +extension MastodonAuthenticationBox { + + init(authentication: MastodonAuthentication) { + self = 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) + ) + } + +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift index b5451e8fa..18f00f6b6 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift @@ -13,11 +13,15 @@ extension Mastodon.API.OAuth { public static let authorizationField = "Authorization" public struct Authorization { - public let accessToken: String + public private(set) var accessToken: String public init(accessToken: String) { self.accessToken = accessToken } + + public mutating func update(accessToken: String) { + self.accessToken = accessToken + } } } From bb5c999bea56b987b6e843eba3b9e8288ad9bd2b Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 9 Oct 2022 20:07:57 +0800 Subject: [PATCH 036/658] chore: [WIP] inject AuthContext into ViewModel --- .github/scripts/build.sh | 1 - Mastodon.xcodeproj/project.pbxproj | 8 +- .../xcschemes/xcschememanagement.plist | 4 +- .../xcshareddata/swiftpm/Package.resolved | 474 +++++++++--------- Mastodon/Coordinator/SceneCoordinator.swift | 183 ++++--- .../ComposeStatusAttachmentSection.swift | 1 - .../Discovery/DiscoverySection.swift | 9 +- .../Notification/NotificationSection.swift | 10 +- .../RecommendAccountSection.swift | 6 +- Mastodon/Diffiable/Report/ReportSection.swift | 7 +- .../Search/SearchResultSection.swift | 9 +- Mastodon/Diffiable/Status/StatusSection.swift | 24 +- .../Provider/DataSourceFacade+Block.swift | 7 +- .../Provider/DataSourceFacade+Bookmark.swift | 7 +- .../Provider/DataSourceFacade+Favorite.swift | 7 +- .../Provider/DataSourceFacade+Follow.swift | 14 +- .../Provider/DataSourceFacade+Hashtag.swift | 9 +- .../Provider/DataSourceFacade+Meta.swift | 12 +- .../Provider/DataSourceFacade+Mute.swift | 7 +- .../Provider/DataSourceFacade+Profile.swift | 19 +- .../Provider/DataSourceFacade+Reblog.swift | 7 +- .../DataSourceFacade+SearchHistory.swift | 16 +- .../Provider/DataSourceFacade+Status.swift | 43 +- .../Provider/DataSourceFacade+Thread.swift | 8 +- ...er+NotificationTableViewCellDelegate.swift | 39 +- ...Provider+StatusTableViewCellDelegate.swift | 31 +- ...tatusTableViewControllerNavigateable.swift | 20 +- ...ider+TableViewControllerNavigateable.swift | 3 +- ...taSourceProvider+UITableViewDelegate.swift | 2 +- .../Scene/Account/AccountListViewModel.swift | 116 +++-- .../Scene/Account/AccountViewController.swift | 23 +- .../AutoCompleteViewModel+State.swift | 7 +- .../AutoComplete/AutoCompleteViewModel.swift | 4 +- .../Scene/Compose/ComposeViewController.swift | 2 +- Mastodon/Scene/Compose/ComposeViewModel.swift | 22 +- .../DiscoveryCommunityViewController.swift | 5 + ...DiscoveryCommunityViewModel+Diffable.swift | 1 + .../DiscoveryCommunityViewModel+State.swift | 7 +- .../DiscoveryCommunityViewModel.swift | 12 +- .../Discovery/DiscoveryViewController.swift | 7 +- .../Scene/Discovery/DiscoveryViewModel.swift | 17 +- .../DiscoveryForYouViewController.swift | 16 +- .../DiscoveryForYouViewModel+Diffable.swift | 1 + .../ForYou/DiscoveryForYouViewModel.swift | 25 +- .../DiscoveryHashtagsViewController.swift | 4 +- .../DiscoveryHashtagsViewModel+Diffable.swift | 2 +- .../Hashtags/DiscoveryHashtagsViewModel.swift | 49 +- .../DiscoveryNewsViewModel+Diffable.swift | 2 +- .../News/DiscoveryNewsViewModel+State.swift | 7 +- .../News/DiscoveryNewsViewModel.swift | 8 +- .../Posts/DiscoveryPostsViewController.swift | 5 + .../DiscoveryPostsViewModel+Diffable.swift | 1 + .../Posts/DiscoveryPostsViewModel+State.swift | 7 +- .../Posts/DiscoveryPostsViewModel.swift | 15 +- .../HashtagTimelineViewController.swift | 8 +- .../HashtagTimelineViewModel+Diffable.swift | 1 + .../HashtagTimelineViewModel+State.swift | 10 +- .../HashtagTimelineViewModel.swift | 11 +- ...meTimelineViewController+DebugAction.swift | 34 +- .../HomeTimelineViewController.swift | 19 +- .../HomeTimelineViewModel+Diffable.swift | 1 + ...omeTimelineViewModel+LoadLatestState.swift | 7 +- ...omeTimelineViewModel+LoadOldestState.swift | 7 +- .../HomeTimeline/HomeTimelineViewModel.swift | 24 +- .../NotificationTimelineViewController.swift | 11 +- ...tificationTimelineViewModel+Diffable.swift | 1 + ...ionTimelineViewModel+LoadOldestState.swift | 7 +- .../NotificationTimelineViewModel.swift | 29 +- .../NotificationViewController.swift | 3 +- .../Notification/NotificationViewModel.swift | 4 +- .../MastodonPickServerViewController.swift | 10 +- .../Welcome/WelcomeViewController.swift | 2 +- .../Onboarding/Welcome/WelcomeViewModel.swift | 7 +- .../Bookmark/BookmarkViewController.swift | 5 + .../Bookmark/BookmarkViewModel+Diffable.swift | 1 + .../Bookmark/BookmarkViewModel+State.swift | 9 +- .../Profile/Bookmark/BookmarkViewModel.swift | 18 +- .../Profile/CachedProfileViewModel.swift | 4 +- .../FamiliarFollowersViewController.swift | 7 + .../FamiliarFollowersViewModel.swift | 11 +- .../Favorite/FavoriteViewController.swift | 5 + .../Favorite/FavoriteViewModel+Diffable.swift | 1 + .../Favorite/FavoriteViewModel+State.swift | 8 +- .../Profile/Favorite/FavoriteViewModel.swift | 17 +- .../Follower/FollowerListViewController.swift | 22 +- .../FollowerListViewModel+Diffable.swift | 6 +- .../FollowerListViewModel+State.swift | 11 +- .../Follower/FollowerListViewModel.swift | 21 +- .../FollowingListViewController.swift | 9 +- .../FollowingListViewModel+Diffable.swift | 11 +- .../FollowingListViewModel+State.swift | 11 +- .../Following/FollowingListViewModel.swift | 18 +- .../Header/ProfileHeaderViewController.swift | 6 +- .../Header/ProfileHeaderViewModel.swift | 5 +- .../Scene/Profile/MeProfileViewModel.swift | 6 +- .../Scene/Profile/ProfileViewController.swift | 40 +- Mastodon/Scene/Profile/ProfileViewModel.swift | 31 +- .../Profile/RemoteProfileViewModel.swift | 29 +- .../Timeline/UserTimelineViewController.swift | 5 + .../UserTimelineViewModel+Diffable.swift | 1 + .../UserTimelineViewModel+State.swift | 6 +- .../Timeline/UserTimelineViewModel.swift | 11 +- .../FavoritedByViewController.swift | 5 + .../RebloggedByViewController.swift | 5 + .../UserLIst/UserListViewModel+State.swift | 8 +- .../Profile/UserLIst/UserListViewModel.swift | 10 +- .../Report/Report/ReportViewController.swift | 14 +- .../Scene/Report/Report/ReportViewModel.swift | 20 +- .../ReportResult/ReportResultView.swift | 128 ++--- .../ReportResultViewController.swift | 23 +- .../ReportResult/ReportResultViewModel.swift | 5 +- .../ReportStatusViewModel+Diffable.swift | 2 +- .../ReportStatusViewModel+State.swift | 6 +- .../ReportStatus/ReportStatusViewModel.swift | 12 +- ...eportSupplementaryViewModel+Diffable.swift | 2 +- .../ReportSupplementaryViewModel.swift | 5 +- .../Root/ContentSplitViewController.swift | 10 +- .../Root/MainTab/MainTabBarController.swift | 97 ++-- .../Scene/Root/RootSplitViewController.swift | 4 + .../Root/Sidebar/SidebarViewController.swift | 11 +- .../Scene/Root/Sidebar/SidebarViewModel.swift | 38 +- .../Search/Search/SearchViewController.swift | 15 +- .../Scene/Search/Search/SearchViewModel.swift | 4 +- .../SearchDetailViewController.swift | 4 +- .../SearchDetail/SearchDetailViewModel.swift | 5 +- .../SearchHistoryViewController.swift | 5 + .../SearchHistoryViewModel.swift | 14 +- .../SearchResultViewController.swift | 7 +- .../SearchResultViewModel+Diffable.swift | 1 + .../SearchResultViewModel+State.swift | 7 +- .../SearchResult/SearchResultViewModel.swift | 18 +- .../Settings/SettingsViewController.swift | 18 +- .../Scene/Settings/SettingsViewModel.swift | 18 +- .../NotificationView+Configuration.swift | 57 +-- .../PollOptionView+Configuration.swift | 8 +- .../Content/StatusView+Configuration.swift | 2 +- .../SuggestionAccountViewController.swift | 12 +- .../SuggestionAccountViewModel+Diffable.swift | 1 + .../SuggestionAccountViewModel.swift | 25 +- .../Scene/Thread/CachedThreadViewModel.swift | 3 +- .../Scene/Thread/RemoteThreadViewModel.swift | 20 +- .../Scene/Thread/ThreadViewController.swift | 12 +- .../Thread/ThreadViewModel+Diffable.swift | 1 + .../ThreadViewModel+LoadThreadState.swift | 8 +- Mastodon/Scene/Thread/ThreadViewModel.swift | 3 + Mastodon/Supporting Files/SceneDelegate.swift | 4 +- .../Utility/ManagedObjectRecord.swift | 6 + .../Preference/Preference+Notification.swift | 2 +- .../Sources/MastodonCore/AppSecret.swift | 2 +- .../Service/AuthenticationService.swift | 88 +--- .../Service/BlockDomainService.swift | 31 +- .../Service/InstanceService.swift | 4 +- .../Notification/NotificationService.swift | 12 +- .../MastodonCore/Service/SettingService.swift | 18 +- .../Service/StatusFilterService.swift | 7 +- .../Content/NotificationView+ViewModel.swift | 10 +- .../View/Content/NotificationView.swift | 2 + .../Content/PollOptionView+ViewModel.swift | 2 +- .../View/Content/StatusView+ViewModel.swift | 18 +- Podfile.lock | 2 +- 160 files changed, 1329 insertions(+), 1435 deletions(-) diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh index f5894901a..3c570b1af 100755 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -7,6 +7,5 @@ set -eo pipefail xcodebuild -workspace Mastodon.xcworkspace \ -scheme Mastodon \ - -destination "platform=iOS Simulator,name=iPhone SE (2nd generation)" \ clean \ build | xcpretty diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index d8a01b818..30f9c0c2d 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -4109,7 +4109,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.7; + MARKETING_VERSION = 1.4.5; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4139,7 +4139,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.7; + MARKETING_VERSION = 1.4.5; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4312,7 +4312,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.7; + MARKETING_VERSION = 1.4.5; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4609,7 +4609,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.7; + MARKETING_VERSION = 1.4.5; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 498c362d6..21906eb03 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -112,12 +112,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 7 + 25 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 6 + 24 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index ebcacb501..4ee0ebeb7 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,241 +1,239 @@ { - "object": { - "pins": [ - { - "package": "Alamofire", - "repositoryURL": "https://github.com/Alamofire/Alamofire.git", - "state": { - "branch": null, - "revision": "354dda32d89fc8cd4f5c46487f64957d355f53d8", - "version": "5.6.1" - } - }, - { - "package": "AlamofireImage", - "repositoryURL": "https://github.com/Alamofire/AlamofireImage.git", - "state": { - "branch": null, - "revision": "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10", - "version": "4.2.0" - } - }, - { - "package": "CommonOSLog", - "repositoryURL": "https://github.com/MainasuK/CommonOSLog", - "state": { - "branch": null, - "revision": "c121624a30698e9886efe38aebb36ff51c01b6c2", - "version": "0.1.1" - } - }, - { - "package": "FaviconFinder", - "repositoryURL": "https://github.com/will-lumley/FaviconFinder.git", - "state": { - "branch": null, - "revision": "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a", - "version": "3.3.0" - } - }, - { - "package": "FLAnimatedImage", - "repositoryURL": "https://github.com/Flipboard/FLAnimatedImage.git", - "state": { - "branch": null, - "revision": "e7f9fd4681ae41bf6f3056db08af4f401d61da52", - "version": "1.0.16" - } - }, - { - "package": "FPSIndicator", - "repositoryURL": "https://github.com/MainasuK/FPSIndicator.git", - "state": { - "branch": null, - "revision": "e4a5067ccd5293b024c767f09e51056afd4a4796", - "version": "1.1.0" - } - }, - { - "package": "Fuzi", - "repositoryURL": "https://github.com/cezheng/Fuzi.git", - "state": { - "branch": null, - "revision": "f08c8323da21e985f3772610753bcfc652c2103f", - "version": "3.1.3" - } - }, - { - "package": "KeychainAccess", - "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", - "state": { - "branch": null, - "revision": "84e546727d66f1adc5439debad16270d0fdd04e7", - "version": "4.2.2" - } - }, - { - "package": "MetaTextKit", - "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", - "state": { - "branch": null, - "revision": "dcd5255d6930c2fab408dc8562c577547e477624", - "version": "2.2.5" - } - }, - { - "package": "Nuke", - "repositoryURL": "https://github.com/kean/Nuke.git", - "state": { - "branch": null, - "revision": "0ea7545b5c918285aacc044dc75048625c8257cc", - "version": "10.8.0" - } - }, - { - "package": "NukeFLAnimatedImagePlugin", - "repositoryURL": "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", - "state": { - "branch": null, - "revision": "b59c346a7d536336db3b0f12c72c6e53ee709e16", - "version": "8.0.0" - } - }, - { - "package": "Pageboy", - "repositoryURL": "https://github.com/uias/Pageboy", - "state": { - "branch": null, - "revision": "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6", - "version": "3.6.2" - } - }, - { - "package": "PanModal", - "repositoryURL": "https://github.com/slackhq/PanModal.git", - "state": { - "branch": null, - "revision": "b012aecb6b67a8e46369227f893c12544846613f", - "version": "1.2.7" - } - }, - { - "package": "SDWebImage", - "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", - "state": { - "branch": null, - "revision": "2e63d0061da449ad0ed130768d05dceb1496de44", - "version": "5.12.5" - } - }, - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections.git", - "state": { - "branch": null, - "revision": "f504716c27d2e5d4144fa4794b12129301d17729", - "version": "1.0.3" - } - }, - { - "package": "swift-nio", - "repositoryURL": "https://github.com/apple/swift-nio.git", - "state": { - "branch": null, - "revision": "546610d52b19be3e19935e0880bb06b9c03f5cef", - "version": "1.14.4" - } - }, - { - "package": "swift-nio-zlib-support", - "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git", - "state": { - "branch": null, - "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd", - "version": "1.0.0" - } - }, - { - "package": "SwiftSoup", - "repositoryURL": "https://github.com/scinfu/SwiftSoup.git", - "state": { - "branch": null, - "revision": "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886", - "version": "2.4.2" - } - }, - { - "package": "Introspect", - "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git", - "state": { - "branch": null, - "revision": "f2616860a41f9d9932da412a8978fec79c06fe24", - "version": "0.1.4" - } - }, - { - "package": "SwiftyJSON", - "repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git", - "state": { - "branch": null, - "revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07", - "version": "5.0.1" - } - }, - { - "package": "TabBarPager", - "repositoryURL": "https://github.com/TwidereProject/TabBarPager.git", - "state": { - "branch": null, - "revision": "488aa66d157a648901b61721212c0dec23d27ee5", - "version": "0.1.0" - } - }, - { - "package": "Tabman", - "repositoryURL": "https://github.com/uias/Tabman", - "state": { - "branch": null, - "revision": "4a4f7c755b875ffd4f9ef10d67a67883669d2465", - "version": "2.13.0" - } - }, - { - "package": "ThirdPartyMailer", - "repositoryURL": "https://github.com/vtourraine/ThirdPartyMailer.git", - "state": { - "branch": null, - "revision": "44c1cfaa6969963f22691aa67f88a69e3b6d651f", - "version": "2.1.0" - } - }, - { - "package": "TOCropViewController", - "repositoryURL": "https://github.com/TimOliver/TOCropViewController.git", - "state": { - "branch": null, - "revision": "d0470491f56e734731bbf77991944c0dfdee3e0e", - "version": "2.6.1" - } - }, - { - "package": "UIHostingConfigurationBackport", - "repositoryURL": "https://github.com/woxtu/UIHostingConfigurationBackport.git", - "state": { - "branch": null, - "revision": "6091f2d38faa4b24fc2ca0389c651e2f666624a3", - "version": "0.1.0" - } - }, - { - "package": "UITextView+Placeholder", - "repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git", - "state": { - "branch": null, - "revision": "20f513ded04a040cdf5467f0891849b1763ede3b", - "version": "1.4.1" - } + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire.git", + "state" : { + "revision" : "354dda32d89fc8cd4f5c46487f64957d355f53d8", + "version" : "5.6.1" } - ] - }, - "version": 1 + }, + { + "identity" : "alamofireimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/AlamofireImage.git", + "state" : { + "revision" : "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10", + "version" : "4.2.0" + } + }, + { + "identity" : "commonoslog", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MainasuK/CommonOSLog", + "state" : { + "revision" : "c121624a30698e9886efe38aebb36ff51c01b6c2", + "version" : "0.1.1" + } + }, + { + "identity" : "faviconfinder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/will-lumley/FaviconFinder.git", + "state" : { + "revision" : "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a", + "version" : "3.3.0" + } + }, + { + "identity" : "flanimatedimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Flipboard/FLAnimatedImage.git", + "state" : { + "revision" : "e7f9fd4681ae41bf6f3056db08af4f401d61da52", + "version" : "1.0.16" + } + }, + { + "identity" : "fpsindicator", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MainasuK/FPSIndicator.git", + "state" : { + "revision" : "e4a5067ccd5293b024c767f09e51056afd4a4796", + "version" : "1.1.0" + } + }, + { + "identity" : "fuzi", + "kind" : "remoteSourceControl", + "location" : "https://github.com/cezheng/Fuzi.git", + "state" : { + "revision" : "f08c8323da21e985f3772610753bcfc652c2103f", + "version" : "3.1.3" + } + }, + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", + "state" : { + "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", + "version" : "4.2.2" + } + }, + { + "identity" : "metatextkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TwidereProject/MetaTextKit.git", + "state" : { + "revision" : "dcd5255d6930c2fab408dc8562c577547e477624", + "version" : "2.2.5" + } + }, + { + "identity" : "nuke", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kean/Nuke.git", + "state" : { + "revision" : "0ea7545b5c918285aacc044dc75048625c8257cc", + "version" : "10.8.0" + } + }, + { + "identity" : "nuke-flanimatedimage-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", + "state" : { + "revision" : "b59c346a7d536336db3b0f12c72c6e53ee709e16", + "version" : "8.0.0" + } + }, + { + "identity" : "pageboy", + "kind" : "remoteSourceControl", + "location" : "https://github.com/uias/Pageboy", + "state" : { + "revision" : "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6", + "version" : "3.6.2" + } + }, + { + "identity" : "panmodal", + "kind" : "remoteSourceControl", + "location" : "https://github.com/slackhq/PanModal.git", + "state" : { + "revision" : "b012aecb6b67a8e46369227f893c12544846613f", + "version" : "1.2.7" + } + }, + { + "identity" : "sdwebimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SDWebImage/SDWebImage.git", + "state" : { + "revision" : "2e63d0061da449ad0ed130768d05dceb1496de44", + "version" : "5.12.5" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "f504716c27d2e5d4144fa4794b12129301d17729", + "version" : "1.0.3" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "546610d52b19be3e19935e0880bb06b9c03f5cef", + "version" : "1.14.4" + } + }, + { + "identity" : "swift-nio-zlib-support", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-zlib-support.git", + "state" : { + "revision" : "37760e9a52030bb9011972c5213c3350fa9d41fd", + "version" : "1.0.0" + } + }, + { + "identity" : "swiftsoup", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scinfu/SwiftSoup.git", + "state" : { + "revision" : "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886", + "version" : "2.4.2" + } + }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", + "state" : { + "revision" : "f2616860a41f9d9932da412a8978fec79c06fe24", + "version" : "0.1.4" + } + }, + { + "identity" : "swiftyjson", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftyJSON/SwiftyJSON.git", + "state" : { + "revision" : "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07", + "version" : "5.0.1" + } + }, + { + "identity" : "tabbarpager", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TwidereProject/TabBarPager.git", + "state" : { + "revision" : "488aa66d157a648901b61721212c0dec23d27ee5", + "version" : "0.1.0" + } + }, + { + "identity" : "tabman", + "kind" : "remoteSourceControl", + "location" : "https://github.com/uias/Tabman", + "state" : { + "revision" : "4a4f7c755b875ffd4f9ef10d67a67883669d2465", + "version" : "2.13.0" + } + }, + { + "identity" : "thirdpartymailer", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vtourraine/ThirdPartyMailer.git", + "state" : { + "revision" : "44c1cfaa6969963f22691aa67f88a69e3b6d651f", + "version" : "2.1.0" + } + }, + { + "identity" : "tocropviewcontroller", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TimOliver/TOCropViewController.git", + "state" : { + "revision" : "d0470491f56e734731bbf77991944c0dfdee3e0e", + "version" : "2.6.1" + } + }, + { + "identity" : "uihostingconfigurationbackport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/woxtu/UIHostingConfigurationBackport.git", + "state" : { + "revision" : "6091f2d38faa4b24fc2ca0389c651e2f666624a3", + "version" : "0.1.0" + } + }, + { + "identity" : "uitextview-placeholder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MainasuK/UITextView-Placeholder.git", + "state" : { + "revision" : "20f513ded04a040cdf5467f0891849b1763ede3b", + "version" : "1.4.1" + } + } + ], + "version" : 2 } diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 3aeafaabd..ae8d81d25 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -22,7 +22,7 @@ final public class SceneCoordinator { private weak var sceneDelegate: SceneDelegate! private weak var appContext: AppContext! - private var authContext: AuthContext? + private(set) var authContext: AuthContext? let id = UUID().uuidString @@ -45,100 +45,83 @@ final public class SceneCoordinator { appContext.notificationService.requestRevealNotificationPublisher .receive(on: DispatchQueue.main) - .compactMap { [weak self] pushNotification -> AnyPublisher in - guard let self = self else { return Just(nil).eraseToAnyPublisher() } - // skip if no available account - guard let currentActiveAuthenticationBox = appContext.authenticationService.activeMastodonAuthenticationBox.value else { - return Just(nil).eraseToAnyPublisher() - } - - let accessToken = pushNotification.accessToken // use raw accessToken value without normalize - if currentActiveAuthenticationBox.userAuthorization.accessToken == accessToken { - // do nothing if notification for current account - return Just(pushNotification).eraseToAnyPublisher() - } else { - // switch to notification's account - let request = MastodonAuthentication.sortedFetchRequest - request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken) - request.returnsObjectsAsFaults = false - request.fetchLimit = 1 - do { - guard let authentication = try appContext.managedObjectContext.fetch(request).first else { - return Just(nil).eraseToAnyPublisher() - } - let domain = authentication.domain - let userID = authentication.userID - return appContext.authenticationService.activeMastodonUser(domain: domain, userID: userID) - .receive(on: DispatchQueue.main) - .map { [weak self] result -> MastodonPushNotification? in - guard let self = self else { return nil } - switch result { - case .success: - // reset view hierarchy - self.setup() - return pushNotification - case .failure: - return nil - } - } - .delay(for: 1, scheduler: DispatchQueue.main) // set delay to slow transition (not must) - .eraseToAnyPublisher() - } catch { - assertionFailure(error.localizedDescription) - return Just(nil).eraseToAnyPublisher() - } - } - } - .switchToLatest() - .receive(on: DispatchQueue.main) - .sink { [weak self] pushNotification in + .sink(receiveValue: { [weak self] pushNotification in guard let self = self else { return } - guard let pushNotification = pushNotification else { return } - - // redirect to notification tab - self.switchToTabBar(tab: .notification) - - - // Delay in next run loop - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - // Note: - // show (push) on phone and pad - let from: UIViewController? = { - if let splitViewController = self.splitViewController { - if splitViewController.compactMainTabBarViewController.topMost?.view.window != nil { - // compact - return splitViewController.compactMainTabBarViewController.topMost - } else { - // expand - return splitViewController.contentSplitViewController.mainTabBarController.topMost + Task { + guard let currentActiveAuthenticationBox = self.authContext?.mastodonAuthenticationBox else { return } + let accessToken = pushNotification.accessToken // use raw accessToken value without normalize + if currentActiveAuthenticationBox.userAuthorization.accessToken == accessToken { + // do nothing if notification for current account + return + } else { + // switch to notification's account + let request = MastodonAuthentication.sortedFetchRequest + request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken) + request.returnsObjectsAsFaults = false + request.fetchLimit = 1 + do { + guard let authentication = try appContext.managedObjectContext.fetch(request).first else { + return } - } else { - return self.tabBarController.topMost + let domain = authentication.domain + let userID = authentication.userID + let isSuccess = try await appContext.authenticationService.activeMastodonUser(domain: domain, userID: userID) + guard isSuccess else { return } + + self.setup() + try await Task.sleep(nanoseconds: .second * 1) + + // redirect to notification tab + self.switchToTabBar(tab: .notification) + + // Delay in next run loop + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + // Note: + // show (push) on phone and pad + let from: UIViewController? = { + if let splitViewController = self.splitViewController { + if splitViewController.compactMainTabBarViewController.topMost?.view.window != nil { + // compact + return splitViewController.compactMainTabBarViewController.topMost + } else { + // expand + return splitViewController.contentSplitViewController.mainTabBarController.topMost + } + } else { + return self.tabBarController.topMost + } + }() + + // show notification related content + guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return } + guard let authContext = self.authContext else { return } + let notificationID = String(pushNotification.notificationID) + + switch type { + case .follow: + let profileViewModel = RemoteProfileViewModel(context: appContext, authContext: authContext, notificationID: notificationID) + _ = self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show) + case .followRequest: + // do nothing + break + case .mention, .reblog, .favourite, .poll, .status: + let threadViewModel = RemoteThreadViewModel(context: appContext, authContext: authContext, notificationID: notificationID) + _ = self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show) + case ._other: + assertionFailure() + break + } + } // end DispatchQueue.main.async + + } catch { + assertionFailure(error.localizedDescription) + return } - }() - - // show notification related content - guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return } - let notificationID = String(pushNotification.notificationID) - - switch type { - case .follow: - let profileViewModel = RemoteProfileViewModel(context: appContext, notificationID: notificationID) - self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show) - case .followRequest: - // do nothing - break - case .mention, .reblog, .favourite, .poll, .status: - let threadViewModel = RemoteThreadViewModel(context: appContext, notificationID: notificationID) - self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show) - case ._other: - assertionFailure() - break } - } // end DispatchQueue.main.async - } + } // end Task + }) .store(in: &disposeBag) } } @@ -180,7 +163,7 @@ extension SceneCoordinator { case hashtagTimeline(viewModel: HashtagTimelineViewModel) // profile - case accountList + case accountList(viewModel: AccountListViewModel) case profile(viewModel: ProfileViewModel) case favorite(viewModel: FavoriteViewModel) case follower(viewModel: FollowerListViewModel) @@ -260,6 +243,19 @@ extension SceneCoordinator { transition: .modal(animated: true, completion: nil) ) } + } else { + let wizardViewController = WizardViewController() + if !wizardViewController.items.isEmpty, + let delegate = rootViewController as? WizardViewControllerDelegate + { + // do not add as child view controller. + // otherwise, the tab bar controller will add as a new tab + wizardViewController.delegate = delegate + wizardViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + wizardViewController.view.frame = rootViewController.view.bounds + rootViewController.view.addSubview(wizardViewController.view) + self.wizardViewController = wizardViewController + } } } catch { @@ -431,8 +427,9 @@ private extension SceneCoordinator { let _viewController = HashtagTimelineViewController() _viewController.viewModel = viewModel viewController = _viewController - case .accountList: + case .accountList(let viewModel): let _viewController = AccountListViewController() + _viewController.viewModel = viewModel viewController = _viewController case .profile(let viewModel): let _viewController = ProfileViewController() diff --git a/Mastodon/Diffiable/Compose/ComposeStatusAttachmentSection.swift b/Mastodon/Diffiable/Compose/ComposeStatusAttachmentSection.swift index 4de7653a5..2e2a94206 100644 --- a/Mastodon/Diffiable/Compose/ComposeStatusAttachmentSection.swift +++ b/Mastodon/Diffiable/Compose/ComposeStatusAttachmentSection.swift @@ -10,4 +10,3 @@ import Foundation enum ComposeStatusAttachmentSection: Hashable { case main } - diff --git a/Mastodon/Diffiable/Discovery/DiscoverySection.swift b/Mastodon/Diffiable/Discovery/DiscoverySection.swift index 2910171d1..225b6f46a 100644 --- a/Mastodon/Diffiable/Discovery/DiscoverySection.swift +++ b/Mastodon/Diffiable/Discovery/DiscoverySection.swift @@ -23,13 +23,16 @@ extension DiscoverySection { static let logger = Logger(subsystem: "DiscoverySection", category: "logic") class Configuration { + let authContext: AuthContext weak var profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? let familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher? public init( + authContext: AuthContext, profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? = nil, familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher? = nil ) { + self.authContext = authContext self.profileCardTableViewCellDelegate = profileCardTableViewCellDelegate self.familiarFollowers = familiarFollowers } @@ -73,11 +76,9 @@ extension DiscoverySection { } else { cell.profileCardView.viewModel.familiarFollowers = nil } + // bind me + cell.profileCardView.viewModel.relationshipViewModel.me = configuration.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user } - context.authenticationService.activeMastodonAuthentication - .map { $0?.user } - .assign(to: \.me, on: cell.profileCardView.viewModel.relationshipViewModel) - .store(in: &cell.disposeBag) return cell case .bottomLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell diff --git a/Mastodon/Diffiable/Notification/NotificationSection.swift b/Mastodon/Diffiable/Notification/NotificationSection.swift index 6f08a0252..a67d407ee 100644 --- a/Mastodon/Diffiable/Notification/NotificationSection.swift +++ b/Mastodon/Diffiable/Notification/NotificationSection.swift @@ -24,6 +24,7 @@ enum NotificationSection: Equatable, Hashable { extension NotificationSection { struct Configuration { + let authContext: AuthContext weak var notificationTableViewCellDelegate: NotificationTableViewCellDelegate? let filterContext: Mastodon.Entity.Filter.Context? let activeFilters: Published<[Mastodon.Entity.Filter]>.Publisher? @@ -74,21 +75,20 @@ extension NotificationSection { viewModel: NotificationTableViewCell.ViewModel, configuration: Configuration ) { + cell.notificationView.viewModel.authContext = configuration.authContext + StatusSection.setupStatusPollDataSource( context: context, + authContext: configuration.authContext, statusView: cell.notificationView.statusView ) StatusSection.setupStatusPollDataSource( context: context, + authContext: configuration.authContext, statusView: cell.notificationView.quoteStatusView ) - context.authenticationService.activeMastodonAuthenticationBox - .map { $0 as UserIdentifier? } - .assign(to: \.userIdentifier, on: cell.notificationView.viewModel) - .store(in: &cell.disposeBag) - cell.configure( tableView: tableView, viewModel: viewModel, diff --git a/Mastodon/Diffiable/RecommandAccount/RecommendAccountSection.swift b/Mastodon/Diffiable/RecommandAccount/RecommendAccountSection.swift index fc2d68044..e5aa0a605 100644 --- a/Mastodon/Diffiable/RecommandAccount/RecommendAccountSection.swift +++ b/Mastodon/Diffiable/RecommandAccount/RecommendAccountSection.swift @@ -133,6 +133,7 @@ enum RecommendAccountSection: Equatable, Hashable { extension RecommendAccountSection { struct Configuration { + let authContext: AuthContext weak var suggestionAccountTableViewCellDelegate: SuggestionAccountTableViewCellDelegate? } @@ -150,10 +151,7 @@ extension RecommendAccountSection { cell.configure(user: user) } - context.authenticationService.activeMastodonAuthenticationBox - .map { $0 as UserIdentifier? } - .assign(to: \.userIdentifier, on: cell.viewModel) - .store(in: &cell.disposeBag) + cell.viewModel.userIdentifier = configuration.authContext.mastodonAuthenticationBox cell.delegate = configuration.suggestionAccountTableViewCellDelegate } return cell diff --git a/Mastodon/Diffiable/Report/ReportSection.swift b/Mastodon/Diffiable/Report/ReportSection.swift index 6513c1249..b815975b0 100644 --- a/Mastodon/Diffiable/Report/ReportSection.swift +++ b/Mastodon/Diffiable/Report/ReportSection.swift @@ -23,6 +23,7 @@ enum ReportSection: Equatable, Hashable { extension ReportSection { struct Configuration { + let authContext: AuthContext } static func diffableDataSource( @@ -101,13 +102,11 @@ extension ReportSection { ) { StatusSection.setupStatusPollDataSource( context: context, + authContext: configuration.authContext, statusView: cell.statusView ) - context.authenticationService.activeMastodonAuthenticationBox - .map { $0 as UserIdentifier? } - .assign(to: \.userIdentifier, on: cell.statusView.viewModel) - .store(in: &cell.disposeBag) + cell.statusView.viewModel.authContext = configuration.authContext cell.configure( tableView: tableView, diff --git a/Mastodon/Diffiable/Search/SearchResultSection.swift b/Mastodon/Diffiable/Search/SearchResultSection.swift index b7fb09df7..8a5d7e75f 100644 --- a/Mastodon/Diffiable/Search/SearchResultSection.swift +++ b/Mastodon/Diffiable/Search/SearchResultSection.swift @@ -25,6 +25,7 @@ extension SearchResultSection { static let logger = Logger(subsystem: "SearchResultSection", category: "logic") struct Configuration { + let authContext: AuthContext weak var statusViewTableViewCellDelegate: StatusTableViewCellDelegate? weak var userTableViewCellDelegate: UserTableViewCellDelegate? } @@ -99,13 +100,11 @@ extension SearchResultSection { ) { StatusSection.setupStatusPollDataSource( context: context, + authContext: configuration.authContext, statusView: cell.statusView ) - context.authenticationService.activeMastodonAuthenticationBox - .map { $0 as UserIdentifier? } - .assign(to: \.userIdentifier, on: cell.statusView.viewModel) - .store(in: &cell.disposeBag) + cell.statusView.viewModel.authContext = configuration.authContext cell.configure( tableView: tableView, @@ -120,7 +119,7 @@ extension SearchResultSection { cell: UserTableViewCell, viewModel: UserTableViewCell.ViewModel, configuration: Configuration - ) { + ) { cell.configure( tableView: tableView, viewModel: viewModel, diff --git a/Mastodon/Diffiable/Status/StatusSection.swift b/Mastodon/Diffiable/Status/StatusSection.swift index 08b55bc69..38b8e641f 100644 --- a/Mastodon/Diffiable/Status/StatusSection.swift +++ b/Mastodon/Diffiable/Status/StatusSection.swift @@ -27,6 +27,7 @@ extension StatusSection { static let logger = Logger(subsystem: "StatusSection", category: "logic") struct Configuration { + let authContext: AuthContext weak var statusTableViewCellDelegate: StatusTableViewCellDelegate? weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate? let filterContext: Mastodon.Entity.Filter.Context? @@ -159,6 +160,7 @@ extension StatusSection { public static func setupStatusPollDataSource( context: AppContext, + authContext: AuthContext, statusView: StatusView ) { let managedObjectContext = context.managedObjectContext @@ -172,10 +174,7 @@ extension StatusSection { return _cell ?? PollOptionTableViewCell() }() - context.authenticationService.activeMastodonAuthenticationBox - .map { $0 as UserIdentifier? } - .assign(to: \.userIdentifier, on: cell.pollOptionView.viewModel) - .store(in: &cell.disposeBag) + cell.pollOptionView.viewModel.authContext = authContext managedObjectContext.performAndWait { guard let option = record.object(in: managedObjectContext) else { @@ -212,14 +211,13 @@ extension StatusSection { return true }() - if needsUpdatePoll, let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value - { + if needsUpdatePoll { let pollRecord: ManagedObjectRecord = .init(objectID: option.poll.objectID) Task { [weak context] in guard let context = context else { return } _ = try await context.apiService.poll( poll: pollRecord, - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) } } @@ -248,13 +246,11 @@ extension StatusSection { ) { setupStatusPollDataSource( context: context, + authContext: configuration.authContext, statusView: cell.statusView ) - context.authenticationService.activeMastodonAuthenticationBox - .map { $0 as UserIdentifier? } - .assign(to: \.userIdentifier, on: cell.statusView.viewModel) - .store(in: &cell.disposeBag) + cell.statusView.viewModel.authContext = configuration.authContext cell.configure( tableView: tableView, @@ -277,13 +273,11 @@ extension StatusSection { ) { setupStatusPollDataSource( context: context, + authContext: configuration.authContext, statusView: cell.statusView ) - context.authenticationService.activeMastodonAuthenticationBox - .map { $0 as UserIdentifier? } - .assign(to: \.userIdentifier, on: cell.statusView.viewModel) - .store(in: &cell.disposeBag) + cell.statusView.viewModel.authContext = configuration.authContext cell.configure( tableView: tableView, diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift index 7166cb39b..2747f4735 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift @@ -11,16 +11,15 @@ import MastodonCore extension DataSourceFacade { static func responseToUserBlockAction( - dependency: NeedsDependency, - user: ManagedObjectRecord, - authenticationBox: MastodonAuthenticationBox + dependency: NeedsDependency & AuthContextProvider, + user: ManagedObjectRecord ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() _ = try await dependency.context.apiService.toggleBlock( user: user, - authenticationBox: authenticationBox + authenticationBox: dependency.authContext.mastodonAuthenticationBox ) } // end func } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift index 93da15271..b7a793551 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift @@ -12,16 +12,15 @@ import MastodonCore extension DataSourceFacade { public static func responseToStatusBookmarkAction( - provider: DataSourceProvider, - status: ManagedObjectRecord, - authenticationBox: MastodonAuthenticationBox + provider: DataSourceProvider & AuthContextProvider, + status: ManagedObjectRecord ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() _ = try await provider.context.apiService.bookmark( record: status, - authenticationBox: authenticationBox + authenticationBox: provider.authContext.mastodonAuthenticationBox ) } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift index 71c02828f..92945b9ee 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift @@ -12,16 +12,15 @@ import MastodonCore extension DataSourceFacade { public static func responseToStatusFavoriteAction( - provider: DataSourceProvider, - status: ManagedObjectRecord, - authenticationBox: MastodonAuthenticationBox + provider: DataSourceProvider & AuthContextProvider, + status: ManagedObjectRecord ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() _ = try await provider.context.apiService.favorite( record: status, - authenticationBox: authenticationBox + authenticationBox: provider.authContext.mastodonAuthenticationBox ) } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index cd29d2eca..c6e40e7d9 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -14,26 +14,24 @@ import MastodonLocalization extension DataSourceFacade { static func responseToUserFollowAction( - dependency: NeedsDependency, - user: ManagedObjectRecord, - authenticationBox: MastodonAuthenticationBox + dependency: NeedsDependency & AuthContextProvider, + user: ManagedObjectRecord ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() _ = try await dependency.context.apiService.toggleFollow( user: user, - authenticationBox: authenticationBox + authenticationBox: dependency.authContext.mastodonAuthenticationBox ) } // end func } extension DataSourceFacade { static func responseToUserFollowRequestAction( - dependency: NeedsDependency, + dependency: NeedsDependency & AuthContextProvider, notification: ManagedObjectRecord, - query: Mastodon.API.Account.FollowReqeustQuery, - authenticationBox: MastodonAuthenticationBox + query: Mastodon.API.Account.FollowReqeustQuery ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() @@ -72,7 +70,7 @@ extension DataSourceFacade { _ = try await dependency.context.apiService.followRequest( userID: userID, query: query, - authenticationBox: authenticationBox + authenticationBox: dependency.authContext.mastodonAuthenticationBox ) } catch { // reset state when failure diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift index 7abde62fe..43d6b954b 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift @@ -7,12 +7,13 @@ import UIKit import CoreDataStack +import MastodonCore import MastodonSDK extension DataSourceFacade { @MainActor static func coordinateToHashtagScene( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, tag: DataSourceItem.TagKind ) async { switch tag { @@ -25,11 +26,12 @@ extension DataSourceFacade { @MainActor static func coordinateToHashtagScene( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, tag: Mastodon.Entity.Tag ) async { let hashtagTimelineViewModel = HashtagTimelineViewModel( context: provider.context, + authContext: provider.authContext, hashtag: tag.name ) @@ -42,7 +44,7 @@ extension DataSourceFacade { @MainActor static func coordinateToHashtagScene( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, tag: ManagedObjectRecord ) async { let managedObjectContext = provider.context.managedObjectContext @@ -55,6 +57,7 @@ extension DataSourceFacade { let hashtagTimelineViewModel = HashtagTimelineViewModel( context: provider.context, + authContext: provider.authContext, hashtag: name ) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift index 7e376ed0f..7e0ed37fc 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift @@ -8,11 +8,12 @@ import Foundation import CoreDataStack import MetaTextKit +import MastodonCore extension DataSourceFacade { static func responseToMetaTextAction( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, target: StatusTarget, status: ManagedObjectRecord, meta: Meta @@ -33,7 +34,7 @@ extension DataSourceFacade { } static func responseToMetaTextAction( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, status: ManagedObjectRecord, meta: Meta ) async { @@ -47,19 +48,20 @@ extension DataSourceFacade { assertionFailure() return } - if let domain = provider.context.authenticationService.activeMastodonAuthenticationBox.value?.domain, url.host == domain, + let domain = provider.authContext.mastodonAuthenticationBox.domain + if url.host == domain, url.pathComponents.count >= 4, url.pathComponents[0] == "/", url.pathComponents[1] == "web", url.pathComponents[2] == "statuses" { let statusID = url.pathComponents[3] - let threadViewModel = RemoteThreadViewModel(context: provider.context, statusID: statusID) + let threadViewModel = RemoteThreadViewModel(context: provider.context, authContext: provider.authContext, statusID: statusID) await provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) } else { await provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) } case .hashtag(_, let hashtag, _): - let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: hashtag) + let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, authContext: provider.authContext, hashtag: hashtag) await provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: provider, transition: .show) case .mention(_, let mention, let userInfo): await coordinateToProfileScene( diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift index b48fbf462..1db94bd4f 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift @@ -11,16 +11,15 @@ import MastodonCore extension DataSourceFacade { static func responseToUserMuteAction( - dependency: NeedsDependency, - user: ManagedObjectRecord, - authenticationBox: MastodonAuthenticationBox + dependency: NeedsDependency & AuthContextProvider, + user: ManagedObjectRecord ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() _ = try await dependency.context.apiService.toggleMute( user: user, - authenticationBox: authenticationBox + authenticationBox: dependency.authContext.mastodonAuthenticationBox ) } // end func } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index 66259a099..ef01b8394 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -7,11 +7,12 @@ import UIKit import CoreDataStack +import MastodonCore extension DataSourceFacade { static func coordinateToProfileScene( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, target: StatusTarget, status: ManagedObjectRecord ) async { @@ -32,7 +33,7 @@ extension DataSourceFacade { @MainActor static func coordinateToProfileScene( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, user: ManagedObjectRecord ) async { guard let user = user.object(in: provider.context.managedObjectContext) else { @@ -42,6 +43,7 @@ extension DataSourceFacade { let profileViewModel = CachedProfileViewModel( context: provider.context, + authContext: provider.authContext, mastodonUser: user ) @@ -57,13 +59,12 @@ extension DataSourceFacade { extension DataSourceFacade { static func coordinateToProfileScene( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, status: ManagedObjectRecord, mention: String, // username, userInfo: [AnyHashable: Any]? ) async { - guard let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return } - let domain = authenticationBox.domain + let domain = provider.authContext.mastodonAuthenticationBox.domain let href = userInfo?["href"] as? String guard let url = href.flatMap({ URL(string: $0) }) else { return } @@ -85,8 +86,8 @@ extension DataSourceFacade { let userID = mention.id let profileViewModel: ProfileViewModel = { // check if self - guard userID != authenticationBox.userID else { - return MeProfileViewModel(context: provider.context) + guard userID != provider.authContext.mastodonAuthenticationBox.userID else { + return MeProfileViewModel(context: provider.context, authContext: provider.authContext) } let request = MastodonUser.sortedFetchRequest @@ -95,9 +96,9 @@ extension DataSourceFacade { let _user = provider.context.managedObjectContext.safeFetch(request).first if let user = _user { - return CachedProfileViewModel(context: provider.context, mastodonUser: user) + return CachedProfileViewModel(context: provider.context, authContext: provider.authContext, mastodonUser: user) } else { - return RemoteProfileViewModel(context: provider.context, userID: userID) + return RemoteProfileViewModel(context: provider.context, authContext: provider.authContext, userID: userID) } }() diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift index 283a21dc8..ff3e95820 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift @@ -12,16 +12,15 @@ import MastodonUI extension DataSourceFacade { static func responseToStatusReblogAction( - provider: DataSourceProvider, - status: ManagedObjectRecord, - authenticationBox: MastodonAuthenticationBox + provider: DataSourceProvider & AuthContextProvider, + status: ManagedObjectRecord ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() _ = try await provider.context.apiService.reblog( record: status, - authenticationBox: authenticationBox + authenticationBox: provider.authContext.mastodonAuthenticationBox ) } // end func } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift index 25ffd4b8e..18d238c02 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift @@ -12,18 +12,18 @@ import MastodonCore extension DataSourceFacade { static func responseToCreateSearchHistory( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, item: DataSourceItem ) async { switch item { case .status: break // not create search history for status case .user(let record): - let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value + let authenticationBox = provider.authContext.mastodonAuthenticationBox let managedObjectContext = provider.context.backgroundManagedObjectContext try? await managedObjectContext.performChanges { - guard let me = authenticationBox?.authenticationRecord.object(in: managedObjectContext)?.user else { return } + guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } guard let user = record.object(in: managedObjectContext) else { return } _ = Persistence.SearchHistory.createOrMerge( in: managedObjectContext, @@ -35,13 +35,12 @@ extension DataSourceFacade { ) } // end try? await managedObjectContext.performChanges { … } case .hashtag(let tag): - let _authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value + let authenticationBox = provider.authContext.mastodonAuthenticationBox let managedObjectContext = provider.context.backgroundManagedObjectContext switch tag { case .entity(let entity): try? await managedObjectContext.performChanges { - guard let authenticationBox = _authenticationBox else { return } guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } let now = Date() @@ -67,7 +66,7 @@ extension DataSourceFacade { } // end try? await managedObjectContext.performChanges { … } case .record(let record): try? await managedObjectContext.performChanges { - guard let authenticationBox = _authenticationBox else { return } + let authenticationBox = provider.authContext.mastodonAuthenticationBox guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } guard let tag = record.object(in: managedObjectContext) else { return } @@ -93,13 +92,12 @@ extension DataSourceFacade { extension DataSourceFacade { static func responseToDeleteSearchHistory( - provider: DataSourceProvider + provider: DataSourceProvider & AuthContextProvider ) async throws { - let _authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value + let authenticationBox = provider.authContext.mastodonAuthenticationBox let managedObjectContext = provider.context.backgroundManagedObjectContext try await managedObjectContext.performChanges { - guard let authenticationBox = _authenticationBox else { return } guard let _ = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } let request = SearchHistory.sortedFetchRequest request.predicate = SearchHistory.predicate( diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 25ab53103..aecfe6a8d 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -15,13 +15,12 @@ import MastodonLocalization extension DataSourceFacade { static func responseToDeleteStatus( - dependency: NeedsDependency, - status: ManagedObjectRecord, - authenticationBox: MastodonAuthenticationBox + dependency: NeedsDependency & AuthContextProvider, + status: ManagedObjectRecord ) async throws { _ = try await dependency.context.apiService.deleteStatus( status: status, - authenticationBox: authenticationBox + authenticationBox: dependency.authContext.mastodonAuthenticationBox ) } @@ -81,10 +80,9 @@ extension DataSourceFacade { extension DataSourceFacade { @MainActor static func responseToActionToolbar( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, status: ManagedObjectRecord, action: ActionToolbarContainer.Action, - authenticationBox: MastodonAuthenticationBox, sender: UIButton ) async throws { let managedObjectContext = provider.context.managedObjectContext @@ -100,16 +98,15 @@ extension DataSourceFacade { switch action { case .reply: - guard let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return } let selectionFeedbackGenerator = UISelectionFeedbackGenerator() selectionFeedbackGenerator.selectionChanged() let composeViewModel = ComposeViewModel( context: provider.context, composeKind: .reply(status: status), - authenticationBox: authenticationBox + authContext: provider.authContext ) - provider.coordinator.present( + _ = provider.coordinator.present( scene: .compose(viewModel: composeViewModel), from: provider, transition: .modal(animated: true, completion: nil) @@ -117,20 +114,17 @@ extension DataSourceFacade { case .reblog: try await DataSourceFacade.responseToStatusReblogAction( provider: provider, - status: status, - authenticationBox: authenticationBox + status: status ) case .like: try await DataSourceFacade.responseToStatusFavoriteAction( provider: provider, - status: status, - authenticationBox: authenticationBox + status: status ) case .bookmark: try await DataSourceFacade.responseToStatusBookmarkAction( provider: provider, - status: status, - authenticationBox: authenticationBox + status: status ) case .share: try await DataSourceFacade.responseToStatusShareAction( @@ -155,10 +149,9 @@ extension DataSourceFacade { @MainActor static func responseToMenuAction( - dependency: NeedsDependency & UIViewController, + dependency: UIViewController & NeedsDependency & AuthContextProvider, action: MastodonMenu.Action, - menuContext: MenuContext, - authenticationBox: MastodonAuthenticationBox + menuContext: MenuContext ) async throws { switch action { case .muteUser(let actionContext): @@ -181,8 +174,7 @@ extension DataSourceFacade { guard let user = _user else { return } try await DataSourceFacade.responseToUserMuteAction( dependency: dependency, - user: user, - authenticationBox: authenticationBox + user: user ) } // end Task } @@ -210,8 +202,7 @@ extension DataSourceFacade { guard let user = _user else { return } try await DataSourceFacade.responseToUserBlockAction( dependency: dependency, - user: user, - authenticationBox: authenticationBox + user: user ) } // end Task } @@ -225,11 +216,12 @@ extension DataSourceFacade { let reportViewModel = ReportViewModel( context: dependency.context, + authContext: dependency.authContext, user: user, status: menuContext.status ) - dependency.coordinator.present( + _ = dependency.coordinator.present( scene: .report(viewModel: reportViewModel), from: dependency, transition: .modal(animated: true, completion: nil) @@ -246,7 +238,7 @@ extension DataSourceFacade { user: user ) guard let activityViewController = _activityViewController else { return } - dependency.coordinator.present( + _ = dependency.coordinator.present( scene: .activityViewController( activityViewController: activityViewController, sourceView: menuContext.button, @@ -270,8 +262,7 @@ extension DataSourceFacade { Task { try await DataSourceFacade.responseToDeleteStatus( dependency: dependency, - status: status, - authenticationBox: authenticationBox + status: status ) } // end Task } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift index 269504215..41f5d58de 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift @@ -8,10 +8,11 @@ import Foundation import CoreData import CoreDataStack +import MastodonCore extension DataSourceFacade { static func coordinateToStatusThreadScene( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, target: StatusTarget, status: ManagedObjectRecord ) async { @@ -39,14 +40,15 @@ extension DataSourceFacade { @MainActor static func coordinateToStatusThreadScene( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, root: StatusItem.Thread ) async { let threadViewModel = ThreadViewModel( context: provider.context, + authContext: provider.authContext, optionalRoot: root ) - provider.coordinator.present( + _ = provider.coordinator.present( scene: .thread(viewModel: threadViewModel), from: provider, transition: .show diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index dab46bba8..e868f418f 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -7,18 +7,18 @@ import UIKit import MetaTextKit -import MastodonUI import CoreDataStack +import MastodonCore +import MastodonUI // MARK: - Notification AuthorMenuAction -extension NotificationTableViewCellDelegate where Self: DataSourceProvider { +extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, notificationView: NotificationView, menuButton button: UIButton, didSelectAction action: MastodonMenu.Action ) { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } Task { let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) guard let item = await item(from: source) else { @@ -47,15 +47,14 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider { status: nil, button: button, barButtonItem: nil - ), - authenticationBox: authenticationBox + ) ) } // end Task } } // MARK: - Notification Author Avatar -extension NotificationTableViewCellDelegate where Self: DataSourceProvider { +extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, notificationView: NotificationView, @@ -88,7 +87,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider { } // MARK: - Follow Request -extension NotificationTableViewCellDelegate where Self: DataSourceProvider { +extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, @@ -106,15 +105,10 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider { return } - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } - try await DataSourceFacade.responseToUserFollowRequestAction( dependency: self, notification: notification, - query: .accept, - authenticationBox: authenticationBox + query: .accept ) } // end Task } @@ -135,15 +129,10 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider { return } - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } - try await DataSourceFacade.responseToUserFollowRequestAction( dependency: self, notification: notification, - query: .reject, - authenticationBox: authenticationBox + query: .reject ) } // end Task } @@ -151,7 +140,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider { } // MARK: - Status Content -extension NotificationTableViewCellDelegate where Self: DataSourceProvider { +extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, notificationView: NotificationView, @@ -279,7 +268,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Med } // MARK: - Status Toolbar -extension NotificationTableViewCellDelegate where Self: DataSourceProvider { +extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, notificationView: NotificationView, @@ -287,7 +276,6 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider { buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action ) { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } Task { let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) guard let item = await item(from: source) else { @@ -311,7 +299,6 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider { provider: self, status: status, action: action, - authenticationBox: authenticationBox, sender: button ) } // end Task @@ -319,7 +306,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider { } // MARK: - Status Author Avatar -extension NotificationTableViewCellDelegate where Self: DataSourceProvider { +extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, notificationView: NotificationView, @@ -354,7 +341,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider { } // MARK: - Status Content -extension NotificationTableViewCellDelegate where Self: DataSourceProvider { +extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, @@ -530,7 +517,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider { } // MARK: a11y -extension NotificationTableViewCellDelegate where Self: DataSourceProvider { +extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, accessibilityActivate: Void) { Task { let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index d02edcc42..82c25d040 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -8,10 +8,11 @@ import UIKit import CoreDataStack import MetaTextKit +import MastodonCore import MastodonUI // MARK: - header -extension StatusTableViewCellDelegate where Self: DataSourceProvider { +extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, @@ -64,7 +65,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { } // MARK: - avatar button -extension StatusTableViewCellDelegate where Self: DataSourceProvider { +extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, @@ -92,7 +93,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { } // MARK: - content -extension StatusTableViewCellDelegate where Self: DataSourceProvider { +extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, @@ -169,7 +170,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & MediaPrev // MARK: - poll -extension StatusTableViewCellDelegate where Self: DataSourceProvider { +extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, @@ -177,7 +178,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath ) { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } guard let pollTableViewDiffableDataSource = statusView.pollTableViewDiffableDataSource else { return } guard let pollItem = pollTableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return } @@ -226,7 +226,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { _ = try await context.apiService.vote( poll: poll, choices: [choice], - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): vote poll for \(choice) success") } catch { @@ -248,7 +248,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { statusView: StatusView, pollVoteButtonPressed button: UIButton ) { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } guard let pollTableViewDiffableDataSource = statusView.pollTableViewDiffableDataSource else { return } guard let firstPollItem = pollTableViewDiffableDataSource.snapshot().itemIdentifiers.first else { return } guard case let .option(firstPollOption) = firstPollItem else { return } @@ -284,7 +283,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { _ = try await context.apiService.vote( poll: poll, choices: choices, - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): vote poll for \(choices) success") } catch { @@ -303,7 +302,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { } // MARK: - toolbar -extension StatusTableViewCellDelegate where Self: DataSourceProvider { +extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, statusView: StatusView, @@ -311,7 +310,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action ) { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } Task { let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) guard let item = await item(from: source) else { @@ -327,7 +325,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { provider: self, status: status, action: action, - authenticationBox: authenticationBox, sender: button ) } // end Task @@ -336,14 +333,13 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { } // MARK: - menu button -extension StatusTableViewCellDelegate where Self: DataSourceProvider { +extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, statusView: StatusView, menuButton button: UIButton, didSelectAction action: MastodonMenu.Action ) { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } Task { let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) guard let item = await item(from: source) else { @@ -372,8 +368,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { status: status, button: button, barButtonItem: nil - ), - authenticationBox: authenticationBox + ) ) } // end Task } @@ -475,7 +470,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { } // MARK: - StatusMetricView -extension StatusTableViewCellDelegate where Self: DataSourceProvider { +extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton) { Task { let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) @@ -489,6 +484,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { } let userListViewModel = UserListViewModel( context: context, + authContext: authContext, kind: .rebloggedBy(status: status) ) await coordinator.present( @@ -512,6 +508,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { } let userListViewModel = UserListViewModel( context: context, + authContext: authContext, kind: .favoritedBy(status: status) ) await coordinator.present( @@ -524,7 +521,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider { } // MARK: a11y -extension StatusTableViewCellDelegate where Self: DataSourceProvider { +extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void) { Task { let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift index c80121e98..390f246d4 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift @@ -8,6 +8,7 @@ import os.log import UIKit import CoreDataStack +import MastodonCore extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & StatusTableViewControllerNavigateableRelay { @@ -30,7 +31,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid } -extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider { +extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider { func statusKeyCommandHandler(_ sender: UIKeyCommand) { guard let rawValue = sender.propertyList as? String, @@ -53,7 +54,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid } // status coordinate -extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider { +extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider { @MainActor private func statusRecord() async -> ManagedObjectRecord? { @@ -93,14 +94,13 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid private func replyStatus() async { guard let status = await statusRecord() else { return } - guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } let selectionFeedbackGenerator = UISelectionFeedbackGenerator() selectionFeedbackGenerator.selectionChanged() let composeViewModel = ComposeViewModel( context: self.context, composeKind: .reply(status: status), - authenticationBox: authenticationBox + authContext: authContext ) self.coordinator.present( scene: .compose(viewModel: composeViewModel), @@ -144,19 +144,16 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid } // toggle -extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider { +extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider { @MainActor private func toggleReblog() async { guard let status = await statusRecord() else { return } - - guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } do { try await DataSourceFacade.responseToStatusReblogAction( provider: self, - status: status, - authenticationBox: authenticationBox + status: status ) } catch { assertionFailure() @@ -167,13 +164,10 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid private func toggleFavorite() async { guard let status = await statusRecord() else { return } - guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } - do { try await DataSourceFacade.responseToStatusFavoriteAction( provider: self, - status: status, - authenticationBox: authenticationBox + status: status ) } catch { assertionFailure() diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+TableViewControllerNavigateable.swift b/Mastodon/Protocol/Provider/DataSourceProvider+TableViewControllerNavigateable.swift index 50fa17866..35ef7761e 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+TableViewControllerNavigateable.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+TableViewControllerNavigateable.swift @@ -7,6 +7,7 @@ import os.log import UIKit +import MastodonCore extension TableViewControllerNavigateableCore where Self: TableViewControllerNavigateableRelay { var navigationKeyCommands: [UIKeyCommand] { @@ -124,7 +125,7 @@ extension TableViewControllerNavigateableCore { } -extension TableViewControllerNavigateableCore where Self: DataSourceProvider { +extension TableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider { func open() { guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return } let source = DataSourceItem.Source(indexPath: indexPathForSelectedRow) diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index b62064a6f..3a71e5346 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -12,7 +12,7 @@ import MastodonCore import MastodonUI import MastodonLocalization -extension UITableViewDelegate where Self: DataSourceProvider { +extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider { func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): indexPath: \(indexPath.debugDescription)") diff --git a/Mastodon/Scene/Account/AccountListViewModel.swift b/Mastodon/Scene/Account/AccountListViewModel.swift index 3149b201d..17eab07ea 100644 --- a/Mastodon/Scene/Account/AccountListViewModel.swift +++ b/Mastodon/Scene/Account/AccountListViewModel.swift @@ -5,6 +5,7 @@ // Created by Cirno MainasuK on 2021-9-13. // +import os.log import UIKit import Combine import CoreData @@ -14,43 +15,43 @@ import MastodonMeta import MastodonCore import MastodonUI -final class AccountListViewModel { +final class AccountListViewModel: NSObject { var disposeBag = Set() // input let context: AppContext + let authContext: AuthContext + let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController // output - let authentications = CurrentValueSubject<[Item], Never>([]) - let activeMastodonUserObjectID = CurrentValueSubject(nil) + @Published var authentications: [ManagedObjectRecord] = [] + @Published var items: [Item] = [] + let dataSourceDidUpdate = PassthroughSubject() var diffableDataSource: UITableViewDiffableDataSource! - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext) { self.context = context + self.authContext = authContext + self.mastodonAuthenticationFetchedResultsController = { + let fetchRequest = MastodonAuthentication.sortedFetchRequest + fetchRequest.returnsObjectsAsFaults = false + fetchRequest.fetchBatchSize = 20 + let controller = NSFetchedResultsController( + fetchRequest: fetchRequest, + managedObjectContext: context.managedObjectContext, + sectionNameKeyPath: nil, + cacheName: nil + ) + return controller + }() + super.init() + // end init + + mastodonAuthenticationFetchedResultsController.delegate = self - Publishers.CombineLatest( - context.authenticationService.mastodonAuthentications, - context.authenticationService.activeMastodonAuthentication - ) - .sink { [weak self] authentications, activeAuthentication in - guard let self = self else { return } - var items: [Item] = [] - var activeMastodonUserObjectID: NSManagedObjectID? - for authentication in authentications { - let item = Item.authentication(objectID: authentication.objectID) - items.append(item) - if authentication === activeAuthentication { - activeMastodonUserObjectID = authentication.user.objectID - } - } - self.authentications.value = items - self.activeMastodonUserObjectID.value = activeMastodonUserObjectID - } - .store(in: &disposeBag) - - authentications + $authentications .receive(on: DispatchQueue.main) .sink { [weak self] authentications in guard let self = self else { return } @@ -58,7 +59,10 @@ final class AccountListViewModel { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) - snapshot.appendItems(authentications, toSection: .main) + let authenticationItems: [Item] = authentications.map { + Item.authentication(record: $0) + } + snapshot.appendItems(authenticationItems, toSection: .main) snapshot.appendItems([.addAccount], toSection: .main) diffableDataSource.apply(snapshot) { @@ -76,7 +80,7 @@ extension AccountListViewModel { } enum Item: Hashable { - case authentication(objectID: NSManagedObjectID) + case authentication(record: ManagedObjectRecord) case addAccount } @@ -86,14 +90,17 @@ extension AccountListViewModel { ) { diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in switch item { - case .authentication(let objectID): - let authentication = managedObjectContext.object(with: objectID) as! MastodonAuthentication + case .authentication(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell - AccountListViewModel.configure( - cell: cell, - authentication: authentication, - activeMastodonUserObjectID: self.activeMastodonUserObjectID.eraseToAnyPublisher() - ) + if let authentication = record.object(in: managedObjectContext), + let activeAuthentication = self.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext) + { + AccountListViewModel.configure( + cell: cell, + authentication: authentication, + activeAuthentication: activeAuthentication + ) + } return cell case .addAccount: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AddAccountTableViewCell.self), for: indexPath) as! AddAccountTableViewCell @@ -109,7 +116,7 @@ extension AccountListViewModel { static func configure( cell: AccountListTableViewCell, authentication: MastodonAuthentication, - activeMastodonUserObjectID: AnyPublisher + activeAuthentication: MastodonAuthentication ) { let user = authentication.user @@ -138,19 +145,14 @@ extension AccountListViewModel { cell.badgeButton.setBadge(number: count) // checkmark - activeMastodonUserObjectID - .receive(on: DispatchQueue.main) - .sink { objectID in - let isCurrentUser = user.objectID == objectID - cell.tintColor = .label - cell.checkmarkImageView.isHidden = !isCurrentUser - if isCurrentUser { - cell.accessibilityTraits.insert(.selected) - } else { - cell.accessibilityTraits.remove(.selected) - } - } - .store(in: &cell.disposeBag) + let isActive = activeAuthentication.userID == authentication.userID + cell.tintColor = .label + cell.checkmarkImageView.isHidden = !isActive + if isActive { + cell.accessibilityTraits.insert(.selected) + } else { + cell.accessibilityTraits.remove(.selected) + } cell.accessibilityLabel = [ cell.nameLabel.text, @@ -161,3 +163,21 @@ extension AccountListViewModel { .joined(separator: " ") } } + +// MARK: - NSFetchedResultsControllerDelegate +extension AccountListViewModel: NSFetchedResultsControllerDelegate { + + public func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + + public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + guard controller === mastodonAuthenticationFetchedResultsController else { + assertionFailure() + return + } + + authentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?.compactMap { $0.asRecrod } ?? [] + } + +} diff --git a/Mastodon/Scene/Account/AccountViewController.swift b/Mastodon/Scene/Account/AccountViewController.swift index 6a97c6427..ed9f883b4 100644 --- a/Mastodon/Scene/Account/AccountViewController.swift +++ b/Mastodon/Scene/Account/AccountViewController.swift @@ -22,7 +22,7 @@ final class AccountListViewController: UIViewController, NeedsDependency { weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() - private(set) lazy var viewModel = AccountListViewModel(context: context) + var viewModel: AccountListViewModel! private(set) lazy var addBarButtonItem: UIBarButtonItem = { let barButtonItem = UIBarButtonItem( @@ -64,7 +64,10 @@ extension AccountListViewController: PanModalPresentable { return .contentHeight(CGFloat(height)) } - let count = viewModel.context.authenticationService.mastodonAuthentications.value.count + 1 + let request = MastodonAuthentication.sortedFetchRequest + let authenticationCount = (try? context.managedObjectContext.count(for: request)) ?? 0 + + let count = authenticationCount + 1 let height = calculateHeight(of: count) return .contentHeight(height) } @@ -174,16 +177,14 @@ extension AccountListViewController: UITableViewDelegate { guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } switch item { - case .authentication(let objectID): + case .authentication(let record): assert(Thread.isMainThread) - let authentication = context.managedObjectContext.object(with: objectID) as! MastodonAuthentication - context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID) - .receive(on: DispatchQueue.main) - .sink { [weak self] result in - guard let self = self else { return } - self.coordinator.setup() - } - .store(in: &disposeBag) + guard let authentication = record.object(in: context.managedObjectContext) else { return } + Task { @MainActor in + let isActive = try await context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID) + guard isActive else { return } + self.coordinator.setup() + } // end Task case .addAccount: // TODO: add dismiss entry for welcome scene coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil)) diff --git a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift b/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift index 29016fafb..1d46fb214 100644 --- a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift +++ b/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift @@ -133,11 +133,6 @@ extension AutoCompleteViewModel.State { await enter(state: Fail.self) return } - - guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - await enter(state: Fail.self) - return - } let searchText = viewModel.inputText.value let searchType = AutoCompleteViewModel.SearchType(inputText: searchText) ?? .default @@ -154,7 +149,7 @@ extension AutoCompleteViewModel.State { do { let response = try await viewModel.context.apiService.search( query: query, - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) await enter(state: Idle.self) diff --git a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel.swift b/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel.swift index ecc234612..61715cd63 100644 --- a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel.swift +++ b/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel.swift @@ -17,6 +17,7 @@ final class AutoCompleteViewModel { // input let context: AppContext + let authContext: AuthContext public let inputText = CurrentValueSubject("") // contains "@" or "#" prefix public let symbolBoundingRect = CurrentValueSubject(.zero) public let customEmojiViewModel = CurrentValueSubject(nil) @@ -36,8 +37,9 @@ final class AutoCompleteViewModel { return stateMachine }() - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext) { self.context = context + self.authContext = authContext autoCompleteItems .receive(on: DispatchQueue.main) diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 619f6efb1..b9605bb78 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -137,7 +137,7 @@ final class ComposeViewController: UIViewController, NeedsDependency { private(set) lazy var autoCompleteViewController: AutoCompleteViewController = { let viewController = AutoCompleteViewController() - viewController.viewModel = AutoCompleteViewModel(context: context) + viewController.viewModel = AutoCompleteViewModel(context: context, authContext: viewModel.authContext) viewController.delegate = self viewController.viewModel.customEmojiViewModel.value = viewModel.customEmojiViewModel return viewController diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 35cc965ed..de088c68b 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -29,8 +29,11 @@ final class ComposeViewModel: NSObject { // input let context: AppContext let composeKind: ComposeStatusSection.ComposeKind - let authenticationBox: MastodonAuthenticationBox - + let authContext: AuthContext + + var authenticationBox: MastodonAuthenticationBox { + authContext.mastodonAuthenticationBox + } @Published var isPollComposing = false @Published var isCustomEmojiComposing = false @@ -116,11 +119,12 @@ final class ComposeViewModel: NSObject { init( context: AppContext, composeKind: ComposeStatusSection.ComposeKind, - authenticationBox: MastodonAuthenticationBox + authContext: AuthContext ) { self.context = context self.composeKind = composeKind - self.authenticationBox = authenticationBox + self.authContext = authContext + self.title = { switch composeKind { case .post, .hashtag, .mention: return L10n.Scene.Compose.Title.newPost @@ -130,8 +134,7 @@ final class ComposeViewModel: NSObject { self.selectedStatusVisibility = { // default private when user locked var visibility: ComposeToolbarView.VisibilitySelectionType = { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value, - let author = authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user + guard let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user else { return .public } @@ -168,15 +171,12 @@ final class ComposeViewModel: NSObject { self.instanceConfiguration = { var configuration: Mastodon.Entity.Instance.Configuration? = nil context.managedObjectContext.performAndWait { - guard let authentication = authenticationBox.authenticationRecord.object(in: context.managedObjectContext) - else { - return - } + guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) else { return } configuration = authentication.instance?.configuration } return configuration }() - self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authenticationBox.domain) + self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authContext.mastodonAuthenticationBox.domain) super.init() // end init diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController.swift index b8c86974b..d592c3033 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController.swift @@ -116,6 +116,11 @@ extension DiscoveryCommunityViewController { } +// MARK: - AuthContextProvider +extension DiscoveryCommunityViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension DiscoveryCommunityViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:CommunityViewController.AutoGenerateTableViewDelegate diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+Diffable.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+Diffable.swift index 26335ec3d..64b4d3b6a 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+Diffable.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+Diffable.swift @@ -18,6 +18,7 @@ extension DiscoveryCommunityViewModel { tableView: tableView, context: context, configuration: StatusSection.Configuration( + authContext: authContext, statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, filterContext: .none, diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift index b7b078c7a..cbf292e3b 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift @@ -136,11 +136,6 @@ extension DiscoveryCommunityViewModel.State { break } - guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - stateMachine.enter(Fail.self) - return - } - let maxID = self.maxID let isReloading = maxID == nil @@ -156,7 +151,7 @@ extension DiscoveryCommunityViewModel.State { minID: nil, limit: 20 ), - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) let newMaxID = response.link?.maxID diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift index 4c01cb0bc..eaf8646c4 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift @@ -22,6 +22,7 @@ final class DiscoveryCommunityViewModel { // input let context: AppContext + let authContext: AuthContext let viewDidAppeared = PassthroughSubject() let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -43,20 +44,15 @@ final class DiscoveryCommunityViewModel { let didLoadLatest = PassthroughSubject() - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext) { self.context = context + self.authContext = authContext self.statusFetchedResultsController = StatusFetchedResultsController( managedObjectContext: context.managedObjectContext, - domain: nil, + domain: authContext.mastodonAuthenticationBox.domain, additionalTweetPredicate: nil ) // end init - - context.authenticationService.activeMastodonAuthentication - .map { $0?.domain } - .assign(to: \.domain, on: statusFetchedResultsController) - .store(in: &disposeBag) - } deinit { diff --git a/Mastodon/Scene/Discovery/DiscoveryViewController.swift b/Mastodon/Scene/Discovery/DiscoveryViewController.swift index 3fc2e8944..33bbefaef 100644 --- a/Mastodon/Scene/Discovery/DiscoveryViewController.swift +++ b/Mastodon/Scene/Discovery/DiscoveryViewController.swift @@ -25,11 +25,8 @@ public class DiscoveryViewController: TabmanViewController, NeedsDependency { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - - private(set) lazy var viewModel = DiscoveryViewModel( - context: context, - coordinator: coordinator - ) + + var viewModel: DiscoveryViewModel! private(set) lazy var buttonBar: TMBar.ButtonBar = { let buttonBar = TMBar.ButtonBar() diff --git a/Mastodon/Scene/Discovery/DiscoveryViewModel.swift b/Mastodon/Scene/Discovery/DiscoveryViewModel.swift index d91d9eee1..244a2e8d4 100644 --- a/Mastodon/Scene/Discovery/DiscoveryViewModel.swift +++ b/Mastodon/Scene/Discovery/DiscoveryViewModel.swift @@ -18,6 +18,7 @@ final class DiscoveryViewModel { // input let context: AppContext + let authContext: AuthContext let discoveryPostsViewController: DiscoveryPostsViewController let discoveryHashtagsViewController: DiscoveryHashtagsViewController let discoveryNewsViewController: DiscoveryNewsViewController @@ -26,41 +27,43 @@ final class DiscoveryViewModel { @Published var viewControllers: [ScrollViewContainer & PageViewController] - init(context: AppContext, coordinator: SceneCoordinator) { + init(context: AppContext, coordinator: SceneCoordinator, authContext: AuthContext) { + self.context = context + self.authContext = authContext + func setupDependency(_ needsDependency: NeedsDependency) { needsDependency.context = context needsDependency.coordinator = coordinator } - self.context = context discoveryPostsViewController = { let viewController = DiscoveryPostsViewController() setupDependency(viewController) - viewController.viewModel = DiscoveryPostsViewModel(context: context) + viewController.viewModel = DiscoveryPostsViewModel(context: context, authContext: authContext) return viewController }() discoveryHashtagsViewController = { let viewController = DiscoveryHashtagsViewController() setupDependency(viewController) - viewController.viewModel = DiscoveryHashtagsViewModel(context: context) + viewController.viewModel = DiscoveryHashtagsViewModel(context: context, authContext: authContext) return viewController }() discoveryNewsViewController = { let viewController = DiscoveryNewsViewController() setupDependency(viewController) - viewController.viewModel = DiscoveryNewsViewModel(context: context) + viewController.viewModel = DiscoveryNewsViewModel(context: context, authContext: authContext) return viewController }() discoveryCommunityViewController = { let viewController = DiscoveryCommunityViewController() setupDependency(viewController) - viewController.viewModel = DiscoveryCommunityViewModel(context: context) + viewController.viewModel = DiscoveryCommunityViewModel(context: context, authContext: authContext) return viewController }() discoveryForYouViewController = { let viewController = DiscoveryForYouViewController() setupDependency(viewController) - viewController.viewModel = DiscoveryForYouViewModel(context: context) + viewController.viewModel = DiscoveryForYouViewModel(context: context, authContext: authContext) return viewController }() self.viewControllers = [ diff --git a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift index aa4d45f6d..ce1aadbb4 100644 --- a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift +++ b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift @@ -101,6 +101,11 @@ extension DiscoveryForYouViewController { } +// MARK: - AuthContextProvider +extension DiscoveryForYouViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension DiscoveryForYouViewController: UITableViewDelegate { @@ -110,9 +115,10 @@ extension DiscoveryForYouViewController: UITableViewDelegate { guard let user = record.object(in: context.managedObjectContext) else { return } let profileViewModel = CachedProfileViewModel( context: context, + authContext: viewModel.authContext, mastodonUser: user ) - coordinator.present( + _ = coordinator.present( scene: .profile(viewModel: profileViewModel), from: self, transition: .show @@ -128,15 +134,13 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate { profileCardView: ProfileCardView, relationshipButtonDidPressed button: ProfileRelationshipActionButton ) { - guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { return } guard let indexPath = tableView.indexPath(for: cell) else { return } guard case let .user(record) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return } Task { try await DataSourceFacade.responseToUserFollowAction( dependency: self, - user: record, - authenticationBox: authenticationBox + user: record ) } // end Task } @@ -157,9 +161,9 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate { return } - let familiarFollowersViewModel = FamiliarFollowersViewModel(context: context) + let familiarFollowersViewModel = FamiliarFollowersViewModel(context: context, authContext: authContext) familiarFollowersViewModel.familiarFollowers = familiarFollowers - coordinator.present( + _ = coordinator.present( scene: .familiarFollowers(viewModel: familiarFollowersViewModel), from: self, transition: .show diff --git a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel+Diffable.swift b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel+Diffable.swift index f93b4c0bd..af8d6ff47 100644 --- a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel+Diffable.swift +++ b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel+Diffable.swift @@ -19,6 +19,7 @@ extension DiscoveryForYouViewModel { tableView: tableView, context: context, configuration: DiscoverySection.Configuration( + authContext: authContext, profileCardTableViewCellDelegate: profileCardTableViewCellDelegate, familiarFollowers: $familiarFollowers ) diff --git a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel.swift b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel.swift index 5073ae200..89122c06e 100644 --- a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel.swift +++ b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel.swift @@ -20,6 +20,7 @@ final class DiscoveryForYouViewModel { // input let context: AppContext + let authContext: AuthContext let userFetchedResultsController: UserFetchedResultsController @MainActor @@ -30,19 +31,15 @@ final class DiscoveryForYouViewModel { var diffableDataSource: UITableViewDiffableDataSource? let didLoadLatest = PassthroughSubject() - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext) { self.context = context + self.authContext = authContext self.userFetchedResultsController = UserFetchedResultsController( managedObjectContext: context.managedObjectContext, - domain: nil, + domain: authContext.mastodonAuthenticationBox.domain, additionalPredicate: nil ) // end init - - context.authenticationService.activeMastodonAuthenticationBox - .map { $0?.domain } - .assign(to: \.domain, on: userFetchedResultsController) - .store(in: &disposeBag) } deinit { @@ -59,16 +56,12 @@ extension DiscoveryForYouViewModel { isFetching = true defer { isFetching = false } - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - throw APIService.APIError.implicit(.badRequest) - } - do { let userIDs = try await fetchSuggestionAccounts() let _familiarFollowersResponse = try? await context.apiService.familiarFollowers( query: .init(ids: userIDs), - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) familiarFollowers = _familiarFollowersResponse?.value ?? [] userFetchedResultsController.userIDs = userIDs @@ -78,14 +71,10 @@ extension DiscoveryForYouViewModel { } private func fetchSuggestionAccounts() async throws -> [Mastodon.Entity.Account.ID] { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - throw APIService.APIError.implicit(.badRequest) - } - do { let response = try await context.apiService.suggestionAccountV2( query: nil, - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) let userIDs = response.value.map { $0.account.id } return userIDs @@ -93,7 +82,7 @@ extension DiscoveryForYouViewModel { // fallback V1 let response = try await context.apiService.suggestionAccount( query: nil, - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) let userIDs = response.value.map { $0.id } return userIDs diff --git a/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewController.swift b/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewController.swift index 8985af2de..e315f04ee 100644 --- a/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewController.swift +++ b/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewController.swift @@ -107,7 +107,7 @@ extension DiscoveryHashtagsViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(indexPath)") guard case let .hashtag(tag) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return } - let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: tag.name) + let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, authContext: viewModel.authContext, hashtag: tag.name) coordinator.present( scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: self, @@ -217,7 +217,7 @@ extension DiscoveryHashtagsViewController: TableViewControllerNavigateable { guard let item = diffableDataSource.itemIdentifier(for: indexPathForSelectedRow) else { return } guard case let .hashtag(tag) = item else { return } - let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: tag.name) + let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, authContext: viewModel.authContext, hashtag: tag.name) coordinator.present( scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: self, diff --git a/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewModel+Diffable.swift b/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewModel+Diffable.swift index 19241d2e6..67362d19e 100644 --- a/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewModel+Diffable.swift +++ b/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewModel+Diffable.swift @@ -15,7 +15,7 @@ extension DiscoveryHashtagsViewModel { diffableDataSource = DiscoverySection.diffableDataSource( tableView: tableView, context: context, - configuration: DiscoverySection.Configuration() + configuration: DiscoverySection.Configuration(authContext: authContext) ) var snapshot = NSDiffableDataSourceSnapshot() diff --git a/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewModel.swift b/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewModel.swift index 5e931063a..7727a2358 100644 --- a/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewModel.swift +++ b/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewModel.swift @@ -22,41 +22,37 @@ final class DiscoveryHashtagsViewModel { // input let context: AppContext + let authContext: AuthContext let viewDidAppeared = PassthroughSubject() // output var diffableDataSource: UITableViewDiffableDataSource? @Published var hashtags: [Mastodon.Entity.Tag] = [] - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext) { self.context = context + self.authContext = authContext // end init - Publishers.CombineLatest( - context.authenticationService.activeMastodonAuthenticationBox, - viewDidAppeared - ) - .compactMap { authenticationBox, _ -> MastodonAuthenticationBox? in - return authenticationBox - } - .throttle(for: 3, scheduler: DispatchQueue.main, latest: true) - .asyncMap { authenticationBox in - try await context.apiService.trendHashtags(domain: authenticationBox.domain, query: nil) - } - .retry(3) - .map { response in Result, Error> { response } } - .catch { error in Just(Result, Error> { throw error }) } - .receive(on: DispatchQueue.main) - .sink { [weak self] result in - guard let self = self else { return } - switch result { - case .success(let response): - self.hashtags = response.value.filter { !$0.name.isEmpty } - case .failure: - break + viewDidAppeared + .throttle(for: 3, scheduler: DispatchQueue.main, latest: true) + .asyncMap { authenticationBox in + try await context.apiService.trendHashtags(domain: authContext.mastodonAuthenticationBox.domain, query: nil) } - } - .store(in: &disposeBag) + .retry(3) + .map { response in Result, Error> { response } } + .catch { error in Just(Result, Error> { throw error }) } + .receive(on: DispatchQueue.main) + .sink { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let response): + self.hashtags = response.value.filter { !$0.name.isEmpty } + case .failure: + break + } + } + .store(in: &disposeBag) } deinit { @@ -69,8 +65,7 @@ extension DiscoveryHashtagsViewModel { @MainActor func fetch() async throws { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } - let response = try await context.apiService.trendHashtags(domain: authenticationBox.domain, query: nil) + let response = try await context.apiService.trendHashtags(domain: authContext.mastodonAuthenticationBox.domain, query: nil) hashtags = response.value.filter { !$0.name.isEmpty } logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch tags: \(response.value.count)") } diff --git a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel+Diffable.swift b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel+Diffable.swift index ab3634a3f..11334dee8 100644 --- a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel+Diffable.swift +++ b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel+Diffable.swift @@ -16,7 +16,7 @@ extension DiscoveryNewsViewModel { diffableDataSource = DiscoverySection.diffableDataSource( tableView: tableView, context: context, - configuration: DiscoverySection.Configuration() + configuration: DiscoverySection.Configuration(authContext: authContext) ) stateMachine.enter(State.Reloading.self) diff --git a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel+State.swift b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel+State.swift index 92b84d176..09c3c57bb 100644 --- a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel+State.swift +++ b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel+State.swift @@ -136,11 +136,6 @@ extension DiscoveryNewsViewModel.State { default: break } - - guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - stateMachine.enter(Fail.self) - return - } let offset = self.offset let isReloading = offset == nil @@ -148,7 +143,7 @@ extension DiscoveryNewsViewModel.State { Task { do { let response = try await viewModel.context.apiService.trendLinks( - domain: authenticationBox.domain, + domain: viewModel.authContext.mastodonAuthenticationBox.domain, query: Mastodon.API.Trends.StatusQuery( offset: offset, limit: nil diff --git a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel.swift b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel.swift index 35af8f962..d440e9528 100644 --- a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel.swift +++ b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel.swift @@ -20,6 +20,7 @@ final class DiscoveryNewsViewModel { // input let context: AppContext + let authContext: AuthContext let listBatchFetchViewModel = ListBatchFetchViewModel() // output @@ -41,8 +42,9 @@ final class DiscoveryNewsViewModel { let didLoadLatest = PassthroughSubject() @Published var isServerSupportEndpoint = true - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext) { self.context = context + self.authContext = authContext // end init Task { @@ -59,11 +61,9 @@ final class DiscoveryNewsViewModel { extension DiscoveryNewsViewModel { func checkServerEndpoint() async { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } - do { _ = try await context.apiService.trendLinks( - domain: authenticationBox.domain, + domain: authContext.mastodonAuthenticationBox.domain, query: .init(offset: nil, limit: nil) ) } catch let error as Mastodon.API.Error where error.httpResponseStatus.code == 404 { diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift index 96f20dbcc..ee3538885 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift @@ -128,6 +128,11 @@ extension DiscoveryPostsViewController { } +// MARK: - AuthContextProvider +extension DiscoveryPostsViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension DiscoveryPostsViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:DiscoveryPostsViewController.AutoGenerateTableViewDelegate diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+Diffable.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+Diffable.swift index 5c82384c7..afa0594d5 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+Diffable.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+Diffable.swift @@ -18,6 +18,7 @@ extension DiscoveryPostsViewModel { tableView: tableView, context: context, configuration: StatusSection.Configuration( + authContext: authContext, statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, filterContext: .none, diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift index 2e5ae9847..9dc1e1f29 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift @@ -136,11 +136,6 @@ extension DiscoveryPostsViewModel.State { default: break } - - guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - stateMachine.enter(Fail.self) - return - } let offset = self.offset let isReloading = offset == nil @@ -148,7 +143,7 @@ extension DiscoveryPostsViewModel.State { Task { do { let response = try await viewModel.context.apiService.trendStatuses( - domain: authenticationBox.domain, + domain: viewModel.authContext.mastodonAuthenticationBox.domain, query: Mastodon.API.Trends.StatusQuery( offset: offset, limit: nil diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift index ad6639b16..7a1b044fd 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift @@ -20,6 +20,7 @@ final class DiscoveryPostsViewModel { // input let context: AppContext + let authContext: AuthContext let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -41,20 +42,16 @@ final class DiscoveryPostsViewModel { let didLoadLatest = PassthroughSubject() @Published var isServerSupportEndpoint = true - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext) { self.context = context + self.authContext = authContext self.statusFetchedResultsController = StatusFetchedResultsController( managedObjectContext: context.managedObjectContext, - domain: nil, + domain: authContext.mastodonAuthenticationBox.domain, additionalTweetPredicate: nil ) // end init - context.authenticationService.activeMastodonAuthentication - .map { $0?.domain } - .assign(to: \.domain, on: statusFetchedResultsController) - .store(in: &disposeBag) - Task { await checkServerEndpoint() } // end Task @@ -68,11 +65,9 @@ final class DiscoveryPostsViewModel { extension DiscoveryPostsViewModel { func checkServerEndpoint() async { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } - do { _ = try await context.apiService.trendStatuses( - domain: authenticationBox.domain, + domain: authContext.mastodonAuthenticationBox.domain, query: .init(offset: nil, limit: nil) ) } catch let error as Mastodon.API.Error where error.httpResponseStatus.code == 404 { diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift index b422481f2..a4818aa71 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift @@ -166,17 +166,21 @@ extension HashtagTimelineViewController { @objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } let composeViewModel = ComposeViewModel( context: context, composeKind: .hashtag(hashtag: viewModel.hashtag), - authenticationBox: authenticationBox + authContext: viewModel.authContext ) coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) } } +// MARK: - AuthContextProvider +extension HashtagTimelineViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension HashtagTimelineViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:HashtagTimelineViewController.AutoGenerateTableViewDelegate diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift index fc80f4846..8d8b0126a 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift @@ -20,6 +20,7 @@ extension HashtagTimelineViewModel { tableView: tableView, context: context, configuration: StatusSection.Configuration( + authContext: authContext, statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, filterContext: .none, diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift index 8e3b6f5d1..57d8c99dd 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift @@ -135,12 +135,6 @@ extension HashtagTimelineViewModel.State { break } - guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - assertionFailure() - stateMachine.enter(Fail.self) - return - } - // TODO: only set large count when using Wi-Fi let maxID = self.maxID let isReloading = maxID == nil @@ -148,10 +142,10 @@ extension HashtagTimelineViewModel.State { Task { do { let response = try await viewModel.context.apiService.hashtagTimeline( - domain: authenticationBox.domain, + domain: viewModel.authContext.mastodonAuthenticationBox.domain, maxID: maxID, hashtag: viewModel.hashtag, - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) let newMaxID: String? = { diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift index 88eb8fcdd..aa57a3930 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift @@ -26,6 +26,7 @@ final class HashtagTimelineViewModel { // input let context: AppContext + let authContext: AuthContext let fetchedResultsController: StatusFetchedResultsController let isFetchingLatestTimeline = CurrentValueSubject(false) let timelinePredicate = CurrentValueSubject(nil) @@ -52,20 +53,16 @@ final class HashtagTimelineViewModel { }() lazy var loadOldestStateMachinePublisher = CurrentValueSubject(nil) - init(context: AppContext, hashtag: String) { + init(context: AppContext, authContext: AuthContext, hashtag: String) { self.context = context + self.authContext = authContext self.hashtag = hashtag self.fetchedResultsController = StatusFetchedResultsController( managedObjectContext: context.managedObjectContext, - domain: nil, + domain: authContext.mastodonAuthenticationBox.domain, additionalTweetPredicate: nil ) // end init - - context.authenticationService.activeMastodonAuthenticationBox - .map { $0?.domain } - .assign(to: \.domain, on: fetchedResultsController) - .store(in: &disposeBag) } deinit { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 2a26c6cf8..d057c0376 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -81,8 +81,11 @@ extension HomeTimelineViewController { }, UIAction(title: "Account Recommend", image: UIImage(systemName: "human"), attributes: []) { [weak self] action in guard let self = self else { return } - let suggestionAccountViewModel = SuggestionAccountViewModel(context: self.context) - self.coordinator.present( + let suggestionAccountViewModel = SuggestionAccountViewModel( + context: self.context, + authContext: self.viewModel.authContext + ) + _ = self.coordinator.present( scene: .suggestionAccount(viewModel: suggestionAccountViewModel), from: self, transition: .modal(animated: true, completion: nil) @@ -150,7 +153,7 @@ extension HomeTimelineViewController { children: [ UIAction(title: "Badge +1", image: UIImage(systemName: "app.badge.fill"), attributes: []) { [weak self] action in guard let self = self else { return } - guard let accessToken = self.context.authenticationService.activeMastodonAuthentication.value?.userAccessToken else { return } + let accessToken = self.viewModel.authContext.mastodonAuthenticationBox.userAuthorization.accessToken UserDefaults.shared.increaseNotificationCount(accessToken: accessToken) self.context.notificationService.applicationIconBadgeNeedsUpdate.send() }, @@ -333,7 +336,8 @@ extension HomeTimelineViewController { } @objc private func showAccountList(_ sender: UIAction) { - coordinator.present(scene: .accountList, from: self, transition: .modal(animated: true, completion: nil)) + let accountListViewModel = AccountListViewModel(context: context, authContext: viewModel.authContext) + coordinator.present(scene: .accountList(viewModel: accountListViewModel), from: self, transition: .modal(animated: true, completion: nil)) } @objc private func showProfileAction(_ sender: UIAction) { @@ -342,7 +346,7 @@ extension HomeTimelineViewController { let showAction = UIAlertAction(title: "Show", style: .default) { [weak self, weak alertController] _ in guard let self = self else { return } guard let textField = alertController?.textFields?.first else { return } - let profileViewModel = RemoteProfileViewModel(context: self.context, userID: textField.text ?? "") + let profileViewModel = RemoteProfileViewModel(context: self.context, authContext: self.viewModel.authContext, userID: textField.text ?? "") self.coordinator.present(scene: .profile(viewModel: profileViewModel), from: self, transition: .show) } alertController.addAction(showAction) @@ -357,7 +361,7 @@ extension HomeTimelineViewController { let showAction = UIAlertAction(title: "Show", style: .default) { [weak self, weak alertController] _ in guard let self = self else { return } guard let textField = alertController?.textFields?.first else { return } - let threadViewModel = RemoteThreadViewModel(context: self.context, statusID: textField.text ?? "") + let threadViewModel = RemoteThreadViewModel(context: self.context, authContext: self.viewModel.authContext, statusID: textField.text ?? "") self.coordinator.present(scene: .thread(viewModel: threadViewModel), from: self, transition: .show) } alertController.addAction(showAction) @@ -367,8 +371,6 @@ extension HomeTimelineViewController { } private func showNotification(_ sender: UIAction, notificationType: Mastodon.Entity.Notification.NotificationType) { - guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } - let alertController = UIAlertController(title: "Enter notification ID", message: nil, preferredStyle: .alert) alertController.addTextField() @@ -380,7 +382,7 @@ extension HomeTimelineViewController { else { return } let pushNotification = MastodonPushNotification( - accessToken: authenticationBox.userAuthorization.accessToken, + accessToken: self.viewModel.authContext.mastodonAuthenticationBox.userAuthorization.accessToken, notificationID: notificationID, notificationType: notificationType.rawValue, preferredLocale: nil, @@ -393,7 +395,7 @@ extension HomeTimelineViewController { alertController.addAction(showAction) // for multiple accounts debug - let boxes = self.context.authenticationService.mastodonAuthenticationBoxes.value // already sorted + let boxes = self.context.authenticationService.mastodonAuthenticationBoxes // already sorted if boxes.count >= 2 { let accessToken = boxes[1].userAuthorization.accessToken let showForSecondaryAction = UIAlertAction(title: "Show for Secondary", style: .default) { [weak self, weak alertController] _ in @@ -420,12 +422,20 @@ extension HomeTimelineViewController { let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) alertController.addAction(cancelAction) - self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) + _ = self.coordinator.present( + scene: .alertController(alertController: alertController), + from: self, + transition: .alertController(animated: true, completion: nil) + ) } @objc private func showSettings(_ sender: UIAction) { guard let currentSetting = context.settingService.currentSetting.value else { return } - let settingsViewModel = SettingsViewModel(context: context, setting: currentSetting) + let settingsViewModel = SettingsViewModel( + context: context, + authContext: viewModel.authContext, + setting: currentSetting + ) coordinator.present( scene: .settings(viewModel: settingsViewModel), from: self, diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 7be12e8bf..095a362bf 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -28,7 +28,7 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() - private(set) lazy var viewModel = HomeTimelineViewModel(context: context) + var viewModel: HomeTimelineViewModel! let mediaPreviewTransitionController = MediaPreviewTransitionController() @@ -373,9 +373,9 @@ extension HomeTimelineViewController { extension HomeTimelineViewController { @objc private func findPeopleButtonPressed(_ sender: PrimaryActionButton) { - let suggestionAccountViewModel = SuggestionAccountViewModel(context: context) + let suggestionAccountViewModel = SuggestionAccountViewModel(context: context, authContext: viewModel.authContext) suggestionAccountViewModel.delegate = viewModel - coordinator.present( + _ = coordinator.present( scene: .suggestionAccount(viewModel: suggestionAccountViewModel), from: self, transition: .modal(animated: true, completion: nil) @@ -391,7 +391,7 @@ extension HomeTimelineViewController { @objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) guard let setting = context.settingService.currentSetting.value else { return } - let settingsViewModel = SettingsViewModel(context: context, setting: setting) + let settingsViewModel = SettingsViewModel(context: context, authContext: viewModel.authContext, setting: setting) coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil)) } @@ -403,12 +403,8 @@ extension HomeTimelineViewController { } @objc func signOutAction(_ sender: UIAction) { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } - Task { @MainActor in - try await context.authenticationService.signOutMastodonUser(authenticationBox: authenticationBox) + try await context.authenticationService.signOutMastodonUser(authenticationBox: viewModel.authContext.mastodonAuthenticationBox) self.coordinator.setup() } } @@ -492,6 +488,11 @@ extension HomeTimelineViewController { } } +// MARK: - AuthContextProvider +extension HomeTimelineViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension HomeTimelineViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:HomeTimelineViewController.AutoGenerateTableViewDelegate diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index 92c28242d..9412abad1 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -21,6 +21,7 @@ extension HomeTimelineViewModel { tableView: tableView, context: context, configuration: StatusSection.Configuration( + authContext: authContext, statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate, filterContext: .home, diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index 234504e38..41cea6b58 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -63,11 +63,6 @@ extension HomeTimelineViewModel.LoadLatestState { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - // sign out when loading will enter here - stateMachine.enter(Fail.self) - return - } let latestFeedRecords = viewModel.fetchedResultsController.records.prefix(APIService.onceRequestStatusMaxCount) let parentManagedObjectContext = viewModel.fetchedResultsController.fetchedResultsController.managedObjectContext @@ -85,7 +80,7 @@ extension HomeTimelineViewModel.LoadLatestState { do { let response = try await viewModel.context.apiService.homeTimeline( - authenticationBox: activeMastodonAuthenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) await enter(state: Idle.self) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift index 1986ac36a..2a0396f20 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift @@ -64,11 +64,6 @@ extension HomeTimelineViewModel.LoadOldestState { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - assertionFailure() - stateMachine.enter(Fail.self) - return - } guard let lastFeedRecord = viewModel.fetchedResultsController.records.last else { stateMachine.enter(Idle.self) @@ -92,7 +87,7 @@ extension HomeTimelineViewModel.LoadOldestState { do { let response = try await viewModel.context.apiService.homeTimeline( maxID: maxID, - authenticationBox: activeMastodonAuthenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) let statuses = response.value diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 72131f366..4cdd1da55 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -26,6 +26,7 @@ final class HomeTimelineViewModel: NSObject { // input let context: AppContext + let authContext: AuthContext let fetchedResultsController: FeedFetchedResultsController let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -76,25 +77,17 @@ final class HomeTimelineViewModel: NSObject { var cellFrameCache = NSCache() - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext) { self.context = context + self.authContext = authContext self.fetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext) self.homeTimelineNavigationBarTitleViewModel = HomeTimelineNavigationBarTitleViewModel(context: context) super.init() - context.authenticationService.activeMastodonAuthenticationBox - .sink { [weak self] authenticationBox in - guard let self = self else { return } - guard let authenticationBox = authenticationBox else { - self.fetchedResultsController.predicate = Feed.predicate(kind: .none, acct: .none) - return - } - self.fetchedResultsController.predicate = Feed.predicate( - kind: .home, - acct: .mastodon(domain: authenticationBox.domain, userID: authenticationBox.userID) - ) - } - .store(in: &disposeBag) + fetchedResultsController.predicate = Feed.predicate( + kind: .home, + acct: .mastodon(domain: authContext.mastodonAuthenticationBox.domain, userID: authContext.mastodonAuthenticationBox.userID) + ) homeTimelineNeedRefresh .sink { [weak self] _ in @@ -131,7 +124,6 @@ extension HomeTimelineViewModel { // load timeline gap func loadMore(item: StatusItem) async { guard case let .feedLoader(record) = item else { return } - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } guard let diffableDataSource = diffableDataSource else { return } var snapshot = diffableDataSource.snapshot() @@ -169,7 +161,7 @@ extension HomeTimelineViewModel { let maxID = status.id _ = try await context.apiService.homeTimeline( maxID: maxID, - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) } catch { do { diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index c549d8f99..00e2a9fc8 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -147,6 +147,11 @@ extension NotificationTimelineViewController { } +// MARK: - AuthContextProvider +extension NotificationTimelineViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension NotificationTimelineViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:NotificationTimelineViewController.AutoGenerateTableViewDelegate @@ -297,9 +302,10 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable { if let stauts = notification.status { let threadViewModel = ThreadViewModel( context: self.context, + authContext: self.viewModel.authContext, optionalRoot: .root(context: .init(status: .init(objectID: stauts.objectID))) ) - self.coordinator.present( + _ = self.coordinator.present( scene: .thread(viewModel: threadViewModel), from: self, transition: .show @@ -307,9 +313,10 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable { } else { let profileViewModel = ProfileViewModel( context: self.context, + authContext: self.viewModel.authContext, optionalMastodonUser: notification.account ) - self.coordinator.present( + _ = self.coordinator.present( scene: .profile(viewModel: profileViewModel), from: self, transition: .show diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index b32eae76b..cb623ff15 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -20,6 +20,7 @@ extension NotificationTimelineViewModel { tableView: tableView, context: context, configuration: NotificationSection.Configuration( + authContext: authContext, notificationTableViewCellDelegate: notificationTableViewCellDelegate, filterContext: .notifications, activeFilters: context.statusFilterService.$activeFilters diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift index 5461223cb..346692adc 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift @@ -63,11 +63,6 @@ extension NotificationTimelineViewModel.LoadOldestState { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - assertionFailure() - stateMachine.enter(Fail.self) - return - } guard let lastFeedRecord = viewModel.feedFetchedResultsController.records.last else { stateMachine.enter(Fail.self) @@ -93,7 +88,7 @@ extension NotificationTimelineViewModel.LoadOldestState { let response = try await viewModel.context.apiService.notifications( maxID: maxID, scope: scope, - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) let notifications = response.value diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index 5e4f7d7ca..f4ce7c2d7 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -21,6 +21,7 @@ final class NotificationTimelineViewModel { // input let context: AppContext + let authContext: AuthContext let scope: Scope let feedFetchedResultsController: FeedFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -47,28 +48,19 @@ final class NotificationTimelineViewModel { init( context: AppContext, + authContext: AuthContext, scope: Scope ) { self.context = context + self.authContext = authContext self.scope = scope self.feedFetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext) // end init - context.authenticationService.activeMastodonAuthenticationBox - .sink { [weak self] authenticationBox in - guard let self = self else { return } - guard let authenticationBox = authenticationBox else { - self.feedFetchedResultsController.predicate = Feed.nonePredicate() - return - } - - let predicate = NotificationTimelineViewModel.feedPredicate( - authenticationBox: authenticationBox, - scope: scope - ) - self.feedFetchedResultsController.predicate = predicate - } - .store(in: &disposeBag) + feedFetchedResultsController.predicate = NotificationTimelineViewModel.feedPredicate( + authenticationBox: authContext.mastodonAuthenticationBox, + scope: scope + ) } deinit { @@ -122,8 +114,6 @@ extension NotificationTimelineViewModel { // load lastest func loadLatest() async { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } - isLoadingLatest = true defer { isLoadingLatest = false } @@ -131,7 +121,7 @@ extension NotificationTimelineViewModel { _ = try await context.apiService.notifications( maxID: nil, scope: scope, - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) } catch { didLoadLatest.send() @@ -142,7 +132,6 @@ extension NotificationTimelineViewModel { // load timeline gap func loadMore(item: NotificationItem) async { guard case let .feedLoader(record) = item else { return } - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } let managedObjectContext = context.managedObjectContext let key = "LoadMore@\(record.objectID)" @@ -163,7 +152,7 @@ extension NotificationTimelineViewModel { _ = try await context.apiService.notifications( maxID: maxID, scope: scope, - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) } catch { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch more failure: \(error.localizedDescription)") diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 474a778d8..665dffefa 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -24,7 +24,7 @@ final class NotificationViewController: TabmanViewController, NeedsDependency { var disposeBag = Set() var observations = Set() - private(set) lazy var viewModel = NotificationViewModel(context: context) + var viewModel: NotificationViewModel! let pageSegmentedControl = UISegmentedControl() @@ -154,6 +154,7 @@ extension NotificationViewController { viewController.coordinator = coordinator viewController.viewModel = NotificationTimelineViewModel( context: context, + authContext: viewModel.authContext, scope: scope ) return viewController diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index 313b206c8..2f2be1805 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -19,6 +19,7 @@ final class NotificationViewModel { // input let context: AppContext + let authContext: AuthContext let viewDidLoad = PassthroughSubject() // output @@ -27,8 +28,9 @@ final class NotificationViewModel { @Published var currentPageIndex = 0 - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext) { self.context = context + self.authContext = authContext // end init } } diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 7845722e5..eb26f75be 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -182,9 +182,13 @@ extension MastodonPickServerViewController { authenticationViewModel .authenticated - .flatMap { [weak self] (domain, user) -> AnyPublisher, Never> in - guard let self = self else { return Just(.success(false)).eraseToAnyPublisher() } - return self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id) + .asyncMap { domain, user -> Result in + do { + let result = try await self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id) + return .success(result) + } catch { + return .failure(error) + } } .receive(on: DispatchQueue.main) .sink { [weak self] result in diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift index 41e77987c..a2b8df83a 100644 --- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift +++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift @@ -144,7 +144,7 @@ extension WelcomeViewController { signUpButton.addTarget(self, action: #selector(signUpButtonDidClicked(_:)), for: .touchUpInside) signInButton.addTarget(self, action: #selector(signInButtonDidClicked(_:)), for: .touchUpInside) - viewModel.needsShowDismissEntry + viewModel.$needsShowDismissEntry .receive(on: DispatchQueue.main) .sink { [weak self] needsShowDismissEntry in guard let self = self else { return } diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewModel.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewModel.swift index d57a6ee5f..e835027f3 100644 --- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewModel.swift +++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewModel.swift @@ -17,15 +17,14 @@ final class WelcomeViewModel { let context: AppContext // output - let needsShowDismissEntry = CurrentValueSubject(false) + @Published var needsShowDismissEntry = false init(context: AppContext) { self.context = context - context.authenticationService.mastodonAuthentications + context.authenticationService.$mastodonAuthenticationBoxes .map { !$0.isEmpty } - .assign(to: \.value, on: needsShowDismissEntry) - .store(in: &disposeBag) + .assign(to: &$needsShowDismissEntry) } } diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift index 6d3d80737..b91b90dad 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift @@ -134,6 +134,11 @@ extension BookmarkViewController: UITableViewDelegate, AutoGenerateTableViewDele // MARK: - StatusTableViewCellDelegate extension BookmarkViewController: StatusTableViewCellDelegate { } +// MARK: - AuthContextProvider +extension BookmarkViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + extension BookmarkViewController { override var keyCommands: [UIKeyCommand]? { return navigationKeyCommands + statusNavigationKeyCommands diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+Diffable.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+Diffable.swift index 06483012c..69075a8ce 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+Diffable.swift @@ -17,6 +17,7 @@ extension BookmarkViewModel { tableView: tableView, context: context, configuration: StatusSection.Configuration( + authContext: authContext, statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, filterContext: .none, diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift index 085967ec7..21cb8e021 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift @@ -51,7 +51,7 @@ extension BookmarkViewModel.State { guard let viewModel = viewModel else { return false } switch stateClass { case is Reloading.Type: - return viewModel.activeMastodonAuthenticationBox.value != nil + return true default: return false } @@ -134,20 +134,15 @@ extension BookmarkViewModel.State { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationBox = viewModel.activeMastodonAuthenticationBox.value else { - stateMachine.enter(Fail.self) - return - } if previousState is Reloading { maxID = nil } - Task { do { let response = try await viewModel.context.apiService.bookmarkedStatuses( maxID: maxID, - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) var hasNewStatusesAppend = false diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift index df48b68e7..f56e65526 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift @@ -18,7 +18,8 @@ final class BookmarkViewModel { // input let context: AppContext - let activeMastodonAuthenticationBox: CurrentValueSubject + let authContext: AuthContext + let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -37,23 +38,14 @@ final class BookmarkViewModel { return stateMachine }() - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext) { self.context = context - self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value) + self.authContext = authContext self.statusFetchedResultsController = StatusFetchedResultsController( managedObjectContext: context.managedObjectContext, - domain: nil, + domain: authContext.mastodonAuthenticationBox.domain, additionalTweetPredicate: nil ) - - context.authenticationService.activeMastodonAuthenticationBox - .assign(to: \.value, on: activeMastodonAuthenticationBox) - .store(in: &disposeBag) - - activeMastodonAuthenticationBox - .map { $0?.domain } - .assign(to: \.domain, on: statusFetchedResultsController) - .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Profile/CachedProfileViewModel.swift b/Mastodon/Scene/Profile/CachedProfileViewModel.swift index f30fac4ea..cdd572fc4 100644 --- a/Mastodon/Scene/Profile/CachedProfileViewModel.swift +++ b/Mastodon/Scene/Profile/CachedProfileViewModel.swift @@ -11,8 +11,8 @@ import MastodonCore final class CachedProfileViewModel: ProfileViewModel { - init(context: AppContext, mastodonUser: MastodonUser) { - super.init(context: context, optionalMastodonUser: mastodonUser) + init(context: AppContext, authContext: AuthContext, mastodonUser: MastodonUser) { + super.init(context: context, authContext: authContext, optionalMastodonUser: mastodonUser) logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Profile] user[\(mastodonUser.id)] profile: \(mastodonUser.acctWithDomain)") } diff --git a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewController.swift b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewController.swift index c82b8b3ad..b6d6f8313 100644 --- a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewController.swift +++ b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewController.swift @@ -75,6 +75,13 @@ extension FamiliarFollowersViewController { } +// MARK: - AuthContextProvider +extension FamiliarFollowersViewController: AuthContextProvider { + var authContext: AuthContext { + viewModel.authContext + } +} + // MARK: - UITableViewDelegate extension FamiliarFollowersViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:FamiliarFollowersViewController.AutoGenerateTableViewDelegate diff --git a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel.swift b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel.swift index 065ede47c..40d4eb14f 100644 --- a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel.swift +++ b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel.swift @@ -17,6 +17,7 @@ final class FamiliarFollowersViewModel { // input let context: AppContext + let authContext: AuthContext let userFetchedResultsController: UserFetchedResultsController @Published var familiarFollowers: Mastodon.Entity.FamiliarFollowers? @@ -24,20 +25,16 @@ final class FamiliarFollowersViewModel { // output var diffableDataSource: UITableViewDiffableDataSource? - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext) { self.context = context + self.authContext = authContext self.userFetchedResultsController = UserFetchedResultsController( managedObjectContext: context.managedObjectContext, - domain: nil, + domain: authContext.mastodonAuthenticationBox.domain, additionalPredicate: nil ) // end init - context.authenticationService.activeMastodonAuthenticationBox - .map { $0?.domain } - .assign(to: \.domain, on: userFetchedResultsController) - .store(in: &disposeBag) - $familiarFollowers .map { familiarFollowers -> [MastodonUser.ID] in guard let familiarFollowers = familiarFollowers else { return [] } diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift index d3cdb2b8e..0b113345e 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift @@ -104,6 +104,11 @@ extension FavoriteViewController { } +// MARK: - AuthContextProvider +extension FavoriteViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension FavoriteViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:FavoriteViewController.AutoGenerateTableViewDelegate diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+Diffable.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+Diffable.swift index 58109247e..3723dae5d 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+Diffable.swift @@ -17,6 +17,7 @@ extension FavoriteViewModel { tableView: tableView, context: context, configuration: StatusSection.Configuration( + authContext: authContext, statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, filterContext: .none, diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift index 84f2da1db..b583ec139 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift @@ -51,7 +51,7 @@ extension FavoriteViewModel.State { guard let viewModel = viewModel else { return false } switch stateClass { case is Reloading.Type: - return viewModel.activeMastodonAuthenticationBox.value != nil + return true default: return false } @@ -134,10 +134,6 @@ extension FavoriteViewModel.State { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationBox = viewModel.activeMastodonAuthenticationBox.value else { - stateMachine.enter(Fail.self) - return - } if previousState is Reloading { maxID = nil } @@ -147,7 +143,7 @@ extension FavoriteViewModel.State { do { let response = try await viewModel.context.apiService.favoritedStatuses( maxID: maxID, - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) var hasNewStatusesAppend = false diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift index 0570df2e9..0dd3c7203 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift @@ -18,7 +18,7 @@ final class FavoriteViewModel { // input let context: AppContext - let activeMastodonAuthenticationBox: CurrentValueSubject + let authContext: AuthContext let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -37,23 +37,14 @@ final class FavoriteViewModel { return stateMachine }() - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext) { self.context = context - self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value) + self.authContext = authContext self.statusFetchedResultsController = StatusFetchedResultsController( managedObjectContext: context.managedObjectContext, - domain: nil, + domain: authContext.mastodonAuthenticationBox.domain, additionalTweetPredicate: nil ) - - context.authenticationService.activeMastodonAuthenticationBox - .assign(to: \.value, on: activeMastodonAuthenticationBox) - .store(in: &disposeBag) - - activeMastodonAuthenticationBox - .map { $0?.domain } - .assign(to: \.domain, on: statusFetchedResultsController) - .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift index ff2977d45..57e068c72 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift @@ -82,15 +82,15 @@ extension FollowerListViewController { // trigger user timeline loading Publishers.CombineLatest( - viewModel.domain.removeDuplicates().eraseToAnyPublisher(), - viewModel.userID.removeDuplicates().eraseToAnyPublisher() + viewModel.$domain.removeDuplicates(), + viewModel.$userID.removeDuplicates() ) - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self = self else { return } - self.viewModel.stateMachine.enter(FollowerListViewModel.State.Reloading.self) - } - .store(in: &disposeBag) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + self.viewModel.stateMachine.enter(FollowerListViewModel.State.Reloading.self) + } + .store(in: &disposeBag) } override func viewWillAppear(_ animated: Bool) { @@ -101,6 +101,12 @@ extension FollowerListViewController { } +// MARK: - AuthContextProvider +extension FollowerListViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + + // MARK: - UITableViewDelegate extension FollowerListViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:FollowerListViewController.AutoGenerateTableViewDelegate diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift index 15cc1be13..7a30c3234 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift @@ -50,10 +50,10 @@ extension FollowerListViewModel { case is State.Idle, is State.Loading, is State.Fail: snapshot.appendItems([.bottomLoader], toSection: .main) case is State.NoMore: - guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value, - let userID = self.userID.value, - userID != activeMastodonAuthenticationBox.userID + guard let userID = self.userID, + userID != self.authContext.mastodonAuthenticationBox.userID else { break } + // display hint footer exclude self let text = L10n.Scene.Follower.footer snapshot.appendItems([.bottomHeader(text: text)], toSection: .main) default: diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift index ffedeae46..fe336f39f 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift @@ -51,7 +51,7 @@ extension FollowerListViewModel.State { guard let viewModel = viewModel else { return false } switch stateClass { case is Reloading.Type: - return viewModel.userID.value != nil + return viewModel.userID != nil default: return false } @@ -139,12 +139,7 @@ extension FollowerListViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let userID = viewModel.userID.value, !userID.isEmpty else { - stateMachine.enter(Fail.self) - return - } - - guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { + guard let userID = viewModel.userID, !userID.isEmpty else { stateMachine.enter(Fail.self) return } @@ -154,7 +149,7 @@ extension FollowerListViewModel.State { let response = try await viewModel.context.apiService.followers( userID: userID, maxID: maxID, - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch \(response.value.count) followers") diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift index af0f6b139..1b0d505b5 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift @@ -20,11 +20,13 @@ final class FollowerListViewModel { // input let context: AppContext - let domain: CurrentValueSubject - let userID: CurrentValueSubject + let authContext: AuthContext let userFetchedResultsController: UserFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() + @Published var domain: String? + @Published var userID: String? + // output var diffableDataSource: UITableViewDiffableDataSource? private(set) lazy var stateMachine: GKStateMachine = { @@ -40,16 +42,21 @@ final class FollowerListViewModel { return stateMachine }() - init(context: AppContext, domain: String?, userID: String?) { + init( + context: AppContext, + authContext: AuthContext, + domain: String?, + userID: String? + ) { self.context = context + self.authContext = authContext self.userFetchedResultsController = UserFetchedResultsController( managedObjectContext: context.managedObjectContext, domain: domain, additionalPredicate: nil ) - self.domain = CurrentValueSubject(domain) - self.userID = CurrentValueSubject(userID) - // super.init() - + self.domain = domain + self.userID = userID + // end init } } diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift index 18994d26b..ccf0f1819 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift @@ -82,8 +82,8 @@ extension FollowingListViewController { // trigger user timeline loading Publishers.CombineLatest( - viewModel.domain.removeDuplicates().eraseToAnyPublisher(), - viewModel.userID.removeDuplicates().eraseToAnyPublisher() + viewModel.$domain.removeDuplicates(), + viewModel.$userID.removeDuplicates() ) .receive(on: DispatchQueue.main) .sink { [weak self] _ in @@ -101,6 +101,11 @@ extension FollowingListViewController { } +// MARK: - AuthContextProvider +extension FollowingListViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension FollowingListViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:FollowingListViewController.AutoGenerateTableViewDelegate diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel+Diffable.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel+Diffable.swift index 116e7567c..e022c5736 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel+Diffable.swift @@ -7,6 +7,7 @@ import UIKit import MastodonAsset +import MastodonCore import MastodonLocalization extension FollowingListViewModel { @@ -44,23 +45,23 @@ extension FollowingListViewModel { snapshot.appendSections([.main]) let items = records.map { UserItem.user(record: $0) } snapshot.appendItems(items, toSection: .main) - + if let currentState = self.stateMachine.currentState { switch currentState { case is State.Idle, is State.Loading, is State.Fail: snapshot.appendItems([.bottomLoader], toSection: .main) case is State.NoMore: - guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value, - let userID = self.userID.value, - userID != activeMastodonAuthenticationBox.userID + guard let userID = self.userID, + userID != self.authContext.mastodonAuthenticationBox.userID else { break } + // display footer exclude self let text = L10n.Scene.Following.footer snapshot.appendItems([.bottomHeader(text: text)], toSection: .main) default: break } } - + diffableDataSource.apply(snapshot, animatingDifferences: false) } .store(in: &disposeBag) diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift index c01a9c8c6..d8c1f765a 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift @@ -50,7 +50,7 @@ extension FollowingListViewModel.State { guard let viewModel = viewModel else { return false } switch stateClass { case is Reloading.Type: - return viewModel.userID.value != nil + return viewModel.userID != nil default: return false } @@ -138,12 +138,7 @@ extension FollowingListViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let userID = viewModel.userID.value, !userID.isEmpty else { - stateMachine.enter(Fail.self) - return - } - - guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { + guard let userID = viewModel.userID, !userID.isEmpty else { stateMachine.enter(Fail.self) return } @@ -153,7 +148,7 @@ extension FollowingListViewModel.State { let response = try await viewModel.context.apiService.following( userID: userID, maxID: maxID, - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch \(response.value.count)") diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift index a809e14d0..12b294b8b 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift @@ -19,11 +19,13 @@ final class FollowingListViewModel { // input let context: AppContext - let domain: CurrentValueSubject - let userID: CurrentValueSubject + let authContext: AuthContext let userFetchedResultsController: UserFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() + @Published var domain: String? + @Published var userID: String? + // output var diffableDataSource: UITableViewDiffableDataSource? private(set) lazy var stateMachine: GKStateMachine = { @@ -39,15 +41,21 @@ final class FollowingListViewModel { return stateMachine }() - init(context: AppContext, domain: String?, userID: String?) { + init( + context: AppContext, + authContext: AuthContext, + domain: String?, + userID: String? + ) { self.context = context + self.authContext = authContext self.userFetchedResultsController = UserFetchedResultsController( managedObjectContext: context.managedObjectContext, domain: domain, additionalPredicate: nil ) - self.domain = CurrentValueSubject(domain) - self.userID = CurrentValueSubject(userID) + self.domain = domain + self.userID = userID // super.init() } diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 5c7ce808a..c5dbbecd4 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -332,10 +332,11 @@ extension ProfileHeaderViewController: ProfileHeaderViewDelegate { else { return } let followerListViewModel = FollowerListViewModel( context: context, + authContext: viewModel.authContext, domain: domain, userID: userID ) - coordinator.present( + _ = coordinator.present( scene: .follower(viewModel: followerListViewModel), from: self, transition: .show @@ -346,10 +347,11 @@ extension ProfileHeaderViewController: ProfileHeaderViewDelegate { else { return } let followingListViewModel = FollowingListViewModel( context: context, + authContext: viewModel.authContext, domain: domain, userID: userID ) - coordinator.present( + _ = coordinator.present( scene: .following(viewModel: followingListViewModel), from: self, transition: .show diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift index 15a13de84..65f15efa7 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift @@ -24,6 +24,8 @@ final class ProfileHeaderViewModel { // input let context: AppContext + let authContext: AuthContext + @Published var user: MastodonUser? @Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none @@ -41,8 +43,9 @@ final class ProfileHeaderViewModel { @Published var isTitleViewDisplaying = false @Published var isTitleViewContentOffsetSet = false - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext) { self.context = context + self.authContext = authContext $accountForEdit .receive(on: DispatchQueue.main) diff --git a/Mastodon/Scene/Profile/MeProfileViewModel.swift b/Mastodon/Scene/Profile/MeProfileViewModel.swift index d6a30fd04..995e32002 100644 --- a/Mastodon/Scene/Profile/MeProfileViewModel.swift +++ b/Mastodon/Scene/Profile/MeProfileViewModel.swift @@ -15,10 +15,12 @@ import MastodonSDK final class MeProfileViewModel: ProfileViewModel { - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext) { + let user = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user super.init( context: context, - optionalMastodonUser: context.authenticationService.activeMastodonAuthentication.value?.user + authContext: authContext, + optionalMastodonUser: user ) $me diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 3393a2edf..30c37473e 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -111,7 +111,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi let viewController = ProfileHeaderViewController() viewController.context = context viewController.coordinator = coordinator - viewController.viewModel = ProfileHeaderViewModel(context: context) + viewController.viewModel = ProfileHeaderViewModel(context: context, authContext: viewModel.authContext) return viewController }() @@ -460,14 +460,14 @@ extension ProfileViewController { switch meta { case .url(_, _, let url, _): guard let url = URL(string: url) else { return } - coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) + _ = coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) case .mention(_, _, let userInfo): guard let href = userInfo?["href"] as? String, let url = URL(string: href) else { return } - coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) + _ = coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) case .hashtag(_, let hashtag, _): - let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag) - coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show) + let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, authContext: viewModel.authContext, hashtag: hashtag) + _ = coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show) case .email, .emoji: break } @@ -485,7 +485,7 @@ extension ProfileViewController { @objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) guard let setting = context.settingService.currentSetting.value else { return } - let settingsViewModel = SettingsViewModel(context: context, setting: setting) + let settingsViewModel = SettingsViewModel(context: context, authContext: viewModel.authContext, setting: setting) coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil)) } @@ -513,24 +513,23 @@ extension ProfileViewController { @objc private func favoriteBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - let favoriteViewModel = FavoriteViewModel(context: context) + let favoriteViewModel = FavoriteViewModel(context: context, authContext: viewModel.authContext) coordinator.present(scene: .favorite(viewModel: favoriteViewModel), from: self, transition: .show) } @objc private func bookmarkBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - let bookmarkViewModel = BookmarkViewModel(context: context) + let bookmarkViewModel = BookmarkViewModel(context: context, authContext: viewModel.authContext) coordinator.present(scene: .bookmark(viewModel: bookmarkViewModel), from: self, transition: .show) } @objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } guard let mastodonUser = viewModel.user else { return } let composeViewModel = ComposeViewModel( context: context, composeKind: .mention(user: .init(objectID: mastodonUser.objectID)), - authenticationBox: authenticationBox + authContext: viewModel.authContext ) coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) } @@ -671,6 +670,11 @@ extension ProfileViewController: TabBarPagerDataSource { // //} +// MARK: - AuthContextProvider +extension ProfileViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - ProfileHeaderViewControllerDelegate extension ProfileViewController: ProfileHeaderViewControllerDelegate { func profileHeaderViewController( @@ -760,16 +764,13 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { case .follow, .request, .pending, .following: guard let user = viewModel.user else { return } let reocrd = ManagedObjectRecord(objectID: user.objectID) - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } Task { try await DataSourceFacade.responseToUserFollowAction( dependency: self, - user: reocrd, - authenticationBox: authenticationBox + user: reocrd ) } case .muting: - guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } guard let user = viewModel.user else { return } let name = user.displayNameWithFallback @@ -784,8 +785,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { Task { try await DataSourceFacade.responseToUserMuteAction( dependency: self, - user: record, - authenticationBox: authenticationBox + user: record ) } } @@ -794,7 +794,6 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { alertController.addAction(cancelAction) present(alertController, animated: true, completion: nil) case .blocking: - guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } guard let user = viewModel.user else { return } let name = user.displayNameWithFallback @@ -809,8 +808,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { Task { try await DataSourceFacade.responseToUserBlockAction( dependency: self, - user: record, - authenticationBox: authenticationBox + user: record ) } } @@ -852,7 +850,6 @@ extension ProfileViewController: ProfileAboutViewControllerDelegate { // MARK: - MastodonMenuDelegate extension ProfileViewController: MastodonMenuDelegate { func menuAction(_ action: MastodonMenu.Action) { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } guard let user = viewModel.user else { return } let userRecord: ManagedObjectRecord = .init(objectID: user.objectID) @@ -866,8 +863,7 @@ extension ProfileViewController: MastodonMenuDelegate { status: nil, button: nil, barButtonItem: self.moreMenuBarButtonItem - ), - authenticationBox: authenticationBox + ) ) } // end Task } diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 3893c80a1..e23b465d4 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -35,6 +35,7 @@ class ProfileViewModel: NSObject { // input let context: AppContext + let authContext: AuthContext @Published var me: MastodonUser? @Published var user: MastodonUser? @@ -58,21 +59,25 @@ class ProfileViewModel: NSObject { // @Published var protected: Bool? = nil // let needsPagePinToTop = CurrentValueSubject(false) - init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) { + init(context: AppContext, authContext: AuthContext, optionalMastodonUser mastodonUser: MastodonUser?) { self.context = context + self.authContext = authContext self.user = mastodonUser self.postsUserTimelineViewModel = UserTimelineViewModel( context: context, + authContext: authContext, title: L10n.Scene.Profile.SegmentedControl.posts, queryFilter: .init(excludeReplies: true) ) self.repliesUserTimelineViewModel = UserTimelineViewModel( context: context, + authContext: authContext, title: L10n.Scene.Profile.SegmentedControl.postsAndReplies, queryFilter: .init(excludeReplies: false) ) self.mediaUserTimelineViewModel = UserTimelineViewModel( context: context, + authContext: authContext, title: L10n.Scene.Profile.SegmentedControl.media, queryFilter: .init(onlyMedia: true) ) @@ -80,13 +85,7 @@ class ProfileViewModel: NSObject { super.init() // bind me - context.authenticationService.activeMastodonAuthenticationBox - .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationBox in - guard let self = self else { return } - self.me = authenticationBox?.authenticationRecord.object(in: context.managedObjectContext)?.user - } - .store(in: &disposeBag) + self.me = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user $me .assign(to: \.me, on: relationshipViewModel) .store(in: &disposeBag) @@ -132,21 +131,18 @@ class ProfileViewModel: NSObject { let pendingRetryPublisher = CurrentValueSubject(1) // observe friendship - Publishers.CombineLatest3( + Publishers.CombineLatest( userRecord, - context.authenticationService.activeMastodonAuthenticationBox, pendingRetryPublisher ) - .sink { [weak self] userRecord, authenticationBox, _ in + .sink { [weak self] userRecord, _ in guard let self = self else { return } - guard let userRecord = userRecord, - let authenticationBox = authenticationBox - else { return } + guard let userRecord = userRecord else { return } Task { do { let response = try await self.updateRelationship( record: userRecord, - authenticationBox: authenticationBox + authenticationBox: self.authContext.mastodonAuthenticationBox ) // there are seconds delay after request follow before requested -> following. Query again when needs guard let relationship = response.value.first else { return } @@ -216,10 +212,7 @@ extension ProfileViewModel { headerProfileInfo: ProfileHeaderViewModel.ProfileInfo, aboutProfileInfo: ProfileAboutViewModel.ProfileInfo ) async throws -> Mastodon.Response.Content { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - throw APIService.APIError.implicit(.badRequest) - } - + let authenticationBox = authContext.mastodonAuthenticationBox let domain = authenticationBox.domain let authorization = authenticationBox.userAuthorization diff --git a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift index cb1f0e4c3..1e1388c95 100644 --- a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift +++ b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift @@ -14,14 +14,11 @@ import MastodonCore final class RemoteProfileViewModel: ProfileViewModel { - init(context: AppContext, userID: Mastodon.Entity.Account.ID) { - super.init(context: context, optionalMastodonUser: nil) + init(context: AppContext, authContext: AuthContext, userID: Mastodon.Entity.Account.ID) { + super.init(context: context, authContext: authContext, optionalMastodonUser: nil) - guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } - let domain = activeMastodonAuthenticationBox.domain - let authorization = activeMastodonAuthenticationBox.userAuthorization + let domain = authContext.mastodonAuthenticationBox.domain + let authorization = authContext.mastodonAuthenticationBox.userAuthorization Just(userID) .asyncMap { userID in try await context.apiService.accountInfo( @@ -54,23 +51,19 @@ final class RemoteProfileViewModel: ProfileViewModel { .store(in: &disposeBag) } - init(context: AppContext, notificationID: Mastodon.Entity.Notification.ID) { - super.init(context: context, optionalMastodonUser: nil) - - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } + init(context: AppContext, authContext: AuthContext, notificationID: Mastodon.Entity.Notification.ID) { + super.init(context: context, authContext: authContext, optionalMastodonUser: nil) Task { @MainActor in let response = try await context.apiService.notification( notificationID: notificationID, - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) let userID = response.value.account.id let _user: MastodonUser? = try await context.managedObjectContext.perform { let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: authenticationBox.domain, id: userID) + request.predicate = MastodonUser.predicate(domain: authContext.mastodonAuthenticationBox.domain, id: userID) request.fetchLimit = 1 return context.managedObjectContext.safeFetch(request).first } @@ -79,14 +72,14 @@ final class RemoteProfileViewModel: ProfileViewModel { self.user = user } else { _ = try await context.apiService.accountInfo( - domain: authenticationBox.domain, + domain: authContext.mastodonAuthenticationBox.domain, userID: userID, - authorization: authenticationBox.userAuthorization + authorization: authContext.mastodonAuthenticationBox.userAuthorization ) let _user: MastodonUser? = try await context.managedObjectContext.perform { let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: authenticationBox.domain, id: userID) + request.predicate = MastodonUser.predicate(domain: authContext.mastodonAuthenticationBox.domain, id: userID) request.fetchLimit = 1 return context.managedObjectContext.safeFetch(request).first } diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift index beed25086..8a983da33 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift @@ -103,6 +103,11 @@ extension UserTimelineViewController: CellFrameCacheContainer { } } +// MARK: - AuthContextProvider +extension UserTimelineViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension UserTimelineViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:UserTimelineViewController.AutoGenerateTableViewDelegate diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift index 7f7341aa6..863d7b44e 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift @@ -18,6 +18,7 @@ extension UserTimelineViewModel { tableView: tableView, context: context, configuration: StatusSection.Configuration( + authContext: authContext, statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, filterContext: .none, diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift index 17409a2bc..b87d4305e 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift @@ -138,10 +138,6 @@ extension UserTimelineViewModel.State { return } - guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - stateMachine.enter(Fail.self) - return - } let queryFilter = viewModel.queryFilter Task { @@ -154,7 +150,7 @@ extension UserTimelineViewModel.State { excludeReplies: queryFilter.excludeReplies, excludeReblogs: queryFilter.excludeReblogs, onlyMedia: queryFilter.onlyMedia, - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) var hasNewStatusesAppend = false diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift index bd28d2c79..0d85b6807 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift @@ -20,6 +20,7 @@ final class UserTimelineViewModel { // input let context: AppContext + let authContext: AuthContext let title: String let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -50,23 +51,19 @@ final class UserTimelineViewModel { init( context: AppContext, + authContext: AuthContext, title: String, queryFilter: QueryFilter ) { self.context = context + self.authContext = authContext self.title = title self.statusFetchedResultsController = StatusFetchedResultsController( managedObjectContext: context.managedObjectContext, - domain: nil, + domain: authContext.mastodonAuthenticationBox.domain, additionalTweetPredicate: nil ) self.queryFilter = queryFilter - // super.init() - - context.authenticationService.activeMastodonAuthenticationBox - .map { $0?.domain } - .assign(to: \.domain, on: statusFetchedResultsController) - .store(in: &disposeBag) } deinit { diff --git a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController.swift b/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController.swift index 5d9a20610..ebce374e7 100644 --- a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController.swift +++ b/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController.swift @@ -93,6 +93,11 @@ extension FavoritedByViewController { } +// MARK: - AuthContextProvider +extension FavoritedByViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension FavoritedByViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:FavoritedByViewController.AutoGenerateTableViewDelegate diff --git a/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController.swift b/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController.swift index 3f3e239a1..0688bcccb 100644 --- a/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController.swift +++ b/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController.swift @@ -93,6 +93,11 @@ extension RebloggedByViewController { } +// MARK: - AuthContextProvider +extension RebloggedByViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension RebloggedByViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:RebloggedByViewController.AutoGenerateTableViewDelegate diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift index 9098c7f81..18c37c403 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift @@ -137,10 +137,6 @@ extension UserListViewModel.State { } guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - stateMachine.enter(Fail.self) - return - } let maxID = self.maxID @@ -152,13 +148,13 @@ extension UserListViewModel.State { response = try await viewModel.context.apiService.favoritedBy( status: status, query: .init(maxID: maxID, limit: nil), - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) case .rebloggedBy(let status): response = try await viewModel.context.apiService.rebloggedBy( status: status, query: .init(maxID: maxID, limit: nil), - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) } diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift index f8d9a3bd6..2a0ed0271 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift @@ -19,6 +19,7 @@ final class UserListViewModel { // input let context: AppContext + let authContext: AuthContext let kind: Kind let userFetchedResultsController: UserFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -39,21 +40,18 @@ final class UserListViewModel { public init( context: AppContext, + authContext: AuthContext, kind: Kind ) { self.context = context + self.authContext = authContext self.kind = kind self.userFetchedResultsController = UserFetchedResultsController( managedObjectContext: context.managedObjectContext, - domain: nil, + domain: authContext.mastodonAuthenticationBox.domain, additionalPredicate: nil ) // end init - - context.authenticationService.activeMastodonAuthenticationBox - .map { $0?.domain } - .assign(to: \.domain, on: userFetchedResultsController) - .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Report/Report/ReportViewController.swift b/Mastodon/Scene/Report/Report/ReportViewController.swift index 57107be47..f1418c5a1 100644 --- a/Mastodon/Scene/Report/Report/ReportViewController.swift +++ b/Mastodon/Scene/Report/Report/ReportViewController.swift @@ -94,22 +94,23 @@ extension ReportViewController: ReportReasonViewControllerDelegate { case .dislike: let reportResultViewModel = ReportResultViewModel( context: context, + authContext: viewModel.authContext, user: viewModel.user, isReported: false ) - coordinator.present( + _ = coordinator.present( scene: .reportResult(viewModel: reportResultViewModel), from: self, transition: .show ) case .violateRule: - coordinator.present( + _ = coordinator.present( scene: .reportServerRules(viewModel: viewModel.reportServerRulesViewModel), from: self, transition: .show ) case .spam, .other: - coordinator.present( + _ = coordinator.present( scene: .reportStatus(viewModel: viewModel.reportStatusViewModel), from: self, transition: .show @@ -144,7 +145,7 @@ extension ReportViewController: ReportStatusViewControllerDelegate { } private func coordinateToReportSupplementary() { - coordinator.present( + _ = coordinator.present( scene: .reportSupplementary(viewModel: viewModel.reportSupplementaryViewModel), from: self, transition: .show @@ -170,11 +171,12 @@ extension ReportViewController: ReportSupplementaryViewControllerDelegate { let reportResultViewModel = ReportResultViewModel( context: context, + authContext: viewModel.authContext, user: viewModel.user, isReported: true ) - coordinator.present( + _ = coordinator.present( scene: .reportResult(viewModel: reportResultViewModel), from: self, transition: .show @@ -184,7 +186,7 @@ extension ReportViewController: ReportSupplementaryViewControllerDelegate { 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) - self.coordinator.present( + _ = self.coordinator.present( scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil) diff --git a/Mastodon/Scene/Report/Report/ReportViewModel.swift b/Mastodon/Scene/Report/Report/ReportViewModel.swift index 4e59cb440..c368ce42c 100644 --- a/Mastodon/Scene/Report/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/Report/ReportViewModel.swift @@ -28,6 +28,7 @@ class ReportViewModel { // input let context: AppContext + let authContext: AuthContext let user: ManagedObjectRecord let status: ManagedObjectRecord? @@ -37,22 +38,20 @@ class ReportViewModel { init( context: AppContext, + authContext: AuthContext, user: ManagedObjectRecord, status: ManagedObjectRecord? ) { self.context = context + self.authContext = authContext self.user = user self.status = status self.reportReasonViewModel = ReportReasonViewModel(context: context) self.reportServerRulesViewModel = ReportServerRulesViewModel(context: context) - self.reportStatusViewModel = ReportStatusViewModel(context: context, user: user, status: status) - self.reportSupplementaryViewModel = ReportSupplementaryViewModel(context: context, user: user) + self.reportStatusViewModel = ReportStatusViewModel(context: context, authContext: authContext, user: user, status: status) + self.reportSupplementaryViewModel = ReportSupplementaryViewModel(context: context, authContext: authContext, user: user) // end init - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } - // setup reason viewModel if status != nil { reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisPost @@ -74,7 +73,7 @@ class ReportViewModel { // bind server rules Task { @MainActor in do { - let response = try await context.apiService.instance(domain: authenticationBox.domain) + let response = try await context.apiService.instance(domain: authContext.mastodonAuthenticationBox.domain) .timeout(3, scheduler: DispatchQueue.main) .singleOutput() let rules = response.value.rules ?? [] @@ -95,12 +94,7 @@ class ReportViewModel { extension ReportViewModel { @MainActor func report() async throws { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value, - !isReporting - else { - assertionFailure() - return - } + guard !isReporting else { return } let managedObjectContext = context.managedObjectContext let _query: Mastodon.API.Reports.FileReportQuery? = try await managedObjectContext.perform { diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultView.swift b/Mastodon/Scene/Report/ReportResult/ReportResultView.swift index b1ad76415..75021934b 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultView.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultView.swift @@ -156,65 +156,69 @@ struct ReportActionButton: View { } -#if DEBUG -struct ReportResultView_Previews: PreviewProvider { - - static func viewModel(isReported: Bool) -> ReportResultViewModel { - let context = AppContext.shared - let request = MastodonUser.sortedFetchRequest - request.fetchLimit = 1 - - let property = MastodonUser.Property( - identifier: "1", - domain: "domain.com", - id: "1", - acct: "@user@domain.com", - username: "user", - displayName: "User", - avatar: "", - avatarStatic: "", - header: "", - headerStatic: "", - note: "", - url: "", - statusesCount: Int64(100), - followingCount: Int64(100), - followersCount: Int64(100), - locked: false, - bot: false, - suspended: false, - createdAt: Date(), - updatedAt: Date(), - emojis: [], - fields: [] - ) - let user = try! context.managedObjectContext.fetch(request).first ?? MastodonUser.insert(into: context.managedObjectContext, property: property) - - return ReportResultViewModel( - context: context, - user: .init(objectID: user.objectID), - isReported: isReported - ) - } - static var previews: some View { - Group { - NavigationView { - ReportResultView(viewModel: viewModel(isReported: true)) - .navigationBarTitle(Text("")) - .navigationBarTitleDisplayMode(.inline) - } - NavigationView { - ReportResultView(viewModel: viewModel(isReported: false)) - .navigationBarTitle(Text("")) - .navigationBarTitleDisplayMode(.inline) - } - NavigationView { - ReportResultView(viewModel: viewModel(isReported: true)) - .navigationBarTitle(Text("")) - .navigationBarTitleDisplayMode(.inline) - } - .preferredColorScheme(.dark) - } - } -} -#endif +//#if DEBUG +// +//struct ReportResultView_Previews: PreviewProvider { +// +// static func viewModel(isReported: Bool) -> ReportResultViewModel { +// let context = AppContext.shared +// let request = MastodonUser.sortedFetchRequest +// request.fetchLimit = 1 +// +// let property = MastodonUser.Property( +// identifier: "1", +// domain: "domain.com", +// id: "1", +// acct: "@user@domain.com", +// username: "user", +// displayName: "User", +// avatar: "", +// avatarStatic: "", +// header: "", +// headerStatic: "", +// note: "", +// url: "", +// statusesCount: Int64(100), +// followingCount: Int64(100), +// followersCount: Int64(100), +// locked: false, +// bot: false, +// suspended: false, +// createdAt: Date(), +// updatedAt: Date(), +// emojis: [], +// fields: [] +// ) +// let user = try! context.managedObjectContext.fetch(request).first ?? MastodonUser.insert(into: context.managedObjectContext, property: property) +// +// return ReportResultViewModel( +// context: context, +// authContext: nil, +// user: .init(objectID: user.objectID), +// isReported: isReported +// ) +// } +// static var previews: some View { +// Group { +// NavigationView { +// ReportResultView(viewModel: viewModel(isReported: true)) +// .navigationBarTitle(Text("")) +// .navigationBarTitleDisplayMode(.inline) +// } +// NavigationView { +// ReportResultView(viewModel: viewModel(isReported: false)) +// .navigationBarTitle(Text("")) +// .navigationBarTitleDisplayMode(.inline) +// } +// NavigationView { +// ReportResultView(viewModel: viewModel(isReported: true)) +// .navigationBarTitle(Text("")) +// .navigationBarTitleDisplayMode(.inline) +// } +// .preferredColorScheme(.dark) +// } +// } +// +//} +// +//#endif diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift b/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift index 1a5aabb67..10dcdf373 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift @@ -93,17 +93,13 @@ extension ReportResultViewController { .throttle(for: 0.3, scheduler: DispatchQueue.main, latest: false) .sink { [weak self] in guard let self = self else { return } - guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } Task { @MainActor in guard !self.viewModel.isRequestFollow else { return } self.viewModel.isRequestFollow = true do { try await DataSourceFacade.responseToUserFollowAction( dependency: self, - user: self.viewModel.user, - authenticationBox: authenticationBox + user: self.viewModel.user ) } catch { // handle error @@ -117,17 +113,13 @@ extension ReportResultViewController { .throttle(for: 0.3, scheduler: DispatchQueue.main, latest: false) .sink { [weak self] in guard let self = self else { return } - guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } Task { @MainActor in guard !self.viewModel.isRequestMute else { return } self.viewModel.isRequestMute = true do { try await DataSourceFacade.responseToUserMuteAction( dependency: self, - user: self.viewModel.user, - authenticationBox: authenticationBox + user: self.viewModel.user ) } catch { // handle error @@ -141,17 +133,13 @@ extension ReportResultViewController { .throttle(for: 0.3, scheduler: DispatchQueue.main, latest: false) .sink { [weak self] in guard let self = self else { return } - guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } Task { @MainActor in guard !self.viewModel.isRequestBlock else { return } self.viewModel.isRequestBlock = true do { try await DataSourceFacade.responseToUserBlockAction( dependency: self, - user: self.viewModel.user, - authenticationBox: authenticationBox + user: self.viewModel.user ) } catch { // handle error @@ -176,6 +164,11 @@ extension ReportResultViewController { } +// MARK: - AuthContextProvider +extension ReportResultViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - PanPopableViewController extension ReportResultViewController: PanPopableViewController { var isPanPopable: Bool { false } diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift b/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift index 8508d1596..8123a8773 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift @@ -23,6 +23,7 @@ class ReportResultViewModel: ObservableObject { // input let context: AppContext + let authContext: AuthContext let user: ManagedObjectRecord let isReported: Bool @@ -47,17 +48,19 @@ class ReportResultViewModel: ObservableObject { init( context: AppContext, + authContext: AuthContext, user: ManagedObjectRecord, isReported: Bool ) { self.context = context + self.authContext = authContext self.user = user self.isReported = isReported // end init Task { @MainActor in guard let user = user.object(in: context.managedObjectContext) else { return } - guard let me = context.authenticationService.activeMastodonAuthenticationBox.value?.authenticationRecord.object(in: context.managedObjectContext)?.user else { return } + guard let me = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user else { return } self.relationshipViewModel.user = user self.relationshipViewModel.me = me diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+Diffable.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+Diffable.swift index 4610a38d3..9879863d6 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+Diffable.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+Diffable.swift @@ -25,7 +25,7 @@ extension ReportStatusViewModel { diffableDataSource = ReportSection.diffableDataSource( tableView: tableView, context: context, - configuration: ReportSection.Configuration() + configuration: ReportSection.Configuration(authContext: authContext) ) var snapshot = NSDiffableDataSourceSnapshot() diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift index 6e9d48af0..01e8715d1 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift @@ -76,10 +76,6 @@ extension ReportStatusViewModel.State { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - stateMachine.enter(Fail.self) - return - } let maxID = viewModel.statusFetchedResultsController.statusIDs.last @@ -102,7 +98,7 @@ extension ReportStatusViewModel.State { excludeReplies: true, excludeReblogs: true, onlyMedia: false, - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) var hasNewStatusesAppend = false diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift index b539909da..5b80a9f3a 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift @@ -24,6 +24,7 @@ class ReportStatusViewModel { // input let context: AppContext + let authContext: AuthContext let user: ManagedObjectRecord let status: ManagedObjectRecord? let statusFetchedResultsController: StatusFetchedResultsController @@ -50,15 +51,17 @@ class ReportStatusViewModel { init( context: AppContext, + authContext: AuthContext, user: ManagedObjectRecord, status: ManagedObjectRecord? ) { self.context = context + self.authContext = authContext self.user = user self.status = status self.statusFetchedResultsController = StatusFetchedResultsController( managedObjectContext: context.managedObjectContext, - domain: nil, + domain: authContext.mastodonAuthenticationBox.domain, additionalTweetPredicate: nil ) // end init @@ -66,12 +69,7 @@ class ReportStatusViewModel { if let status = status { selectStatuses.append(status) } - - context.authenticationService.activeMastodonAuthenticationBox - .map { $0?.domain } - .assign(to: \.domain, on: statusFetchedResultsController) - .store(in: &disposeBag) - + $selectStatuses .map { statuses -> Bool in return status == nil ? !statuses.isEmpty : statuses.count > 1 diff --git a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel+Diffable.swift b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel+Diffable.swift index 8cbc16242..099f542b7 100644 --- a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel+Diffable.swift +++ b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel+Diffable.swift @@ -25,7 +25,7 @@ extension ReportSupplementaryViewModel { diffableDataSource = ReportSection.diffableDataSource( tableView: tableView, context: context, - configuration: ReportSection.Configuration() + configuration: ReportSection.Configuration(authContext: authContext) ) var snapshot = NSDiffableDataSourceSnapshot() diff --git a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift index 8ddc2d91a..a4239bbc4 100644 --- a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift +++ b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift @@ -16,7 +16,8 @@ class ReportSupplementaryViewModel { weak var delegate: ReportSupplementaryViewControllerDelegate? // Input - var context: AppContext + let context: AppContext + let authContext: AuthContext let user: ManagedObjectRecord let commentContext = ReportItem.CommentContext() @@ -29,9 +30,11 @@ class ReportSupplementaryViewModel { init( context: AppContext, + authContext: AuthContext, user: ManagedObjectRecord ) { self.context = context + self.authContext = authContext self.user = user // end init diff --git a/Mastodon/Scene/Root/ContentSplitViewController.swift b/Mastodon/Scene/Root/ContentSplitViewController.swift index 503c56de5..3f4758e8e 100644 --- a/Mastodon/Scene/Root/ContentSplitViewController.swift +++ b/Mastodon/Scene/Root/ContentSplitViewController.swift @@ -32,7 +32,7 @@ final class ContentSplitViewController: UIViewController, NeedsDependency { let sidebarViewController = SidebarViewController() sidebarViewController.context = context sidebarViewController.coordinator = coordinator - sidebarViewController.viewModel = SidebarViewModel(context: context) + sidebarViewController.viewModel = SidebarViewModel(context: context, authContext: authContext) sidebarViewController.delegate = self return sidebarViewController }() @@ -111,8 +111,14 @@ extension ContentSplitViewController: SidebarViewControllerDelegate { func sidebarViewController(_ sidebarViewController: SidebarViewController, didLongPressItem item: SidebarViewModel.Item, sourceView: UIView) { guard case let .tab(tab) = item, tab == .me else { return } + guard let authContext = authContext else { return } - let accountListViewController = coordinator.present(scene: .accountList, from: nil, transition: .popover(sourceView: sourceView)) as! AccountListViewController + let accountListViewModel = AccountListViewModel(context: context, authContext: authContext) + let accountListViewController = coordinator.present( + scene: .accountList(viewModel: accountListViewModel), + from: nil, + transition: .popover(sourceView: sourceView) + ) as! AccountListViewController accountListViewController.dragIndicatorView.barView.isHidden = true // content width needs > 300 to make checkmark display accountListViewController.preferredContentSize = CGSize(width: 375, height: 400) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 5c910390c..31d7d9fde 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -105,18 +105,24 @@ class MainTabBarController: UITabBarController { } } - func viewController(context: AppContext, coordinator: SceneCoordinator) -> UIViewController { + func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) -> UIViewController { + guard let authContext = authContext else { + return UITableViewController() + } + let viewController: UIViewController switch self { case .home: let _viewController = HomeTimelineViewController() _viewController.context = context _viewController.coordinator = coordinator + _viewController.viewModel = .init(context: context, authContext: authContext) viewController = _viewController case .search: let _viewController = SearchViewController() _viewController.context = context _viewController.coordinator = coordinator + _viewController.viewModel = .init(context: context, authContext: authContext) viewController = _viewController case .compose: viewController = UIViewController() @@ -124,12 +130,13 @@ class MainTabBarController: UITabBarController { let _viewController = NotificationViewController() _viewController.context = context _viewController.coordinator = coordinator + _viewController.viewModel = .init(context: context, authContext: authContext) viewController = _viewController case .me: let _viewController = ProfileViewController() _viewController.context = context _viewController.coordinator = coordinator - _viewController.viewModel = MeProfileViewModel(context: context) + _viewController.viewModel = MeProfileViewModel(context: context, authContext: authContext) viewController = _viewController } viewController.title = self.title @@ -185,7 +192,7 @@ extension MainTabBarController { // seealso: `ThemeService.apply(theme:)` let tabs = Tab.allCases let viewControllers: [UIViewController] = tabs.map { tab in - let viewController = tab.viewController(context: context, coordinator: coordinator) + let viewController = tab.viewController(context: context, authContext: authContext, coordinator: coordinator) viewController.tabBarItem.tag = tab.tag viewController.tabBarItem.title = tab.title // needs for acessiblity large content label viewController.tabBarItem.image = tab.image.imageWithoutBaseline() @@ -256,18 +263,18 @@ extension MainTabBarController { // handle push notification. // toggle entry when finish fetch latest notification - Publishers.CombineLatest3( - context.authenticationService.activeMastodonAuthentication, + Publishers.CombineLatest( context.notificationService.unreadNotificationCountDidUpdate, $currentTab ) .receive(on: DispatchQueue.main) - .sink { [weak self] authentication, _, currentTab in + .sink { [weak self] authentication, currentTab in guard let self = self else { return } guard let notificationViewController = self.notificationViewController else { return } + let authentication = self.authContext?.mastodonAuthenticationBox.userAuthorization let hasUnreadPushNotification: Bool = authentication.flatMap { authentication in - let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: authentication.userAccessToken) + let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: authentication.accessToken) return count > 0 } ?? false @@ -297,43 +304,31 @@ extension MainTabBarController { ) } .store(in: &disposeBag) - context.authenticationService.activeMastodonAuthentication - .receive(on: DispatchQueue.main) - .sink { [weak self] activeMastodonAuthentication in - guard let self = self else { return } - - if let user = activeMastodonAuthentication?.user { - self.avatarURLObserver = user.publisher(for: \.avatar) - .sink { [weak self, weak user] _ in - guard let self = self else { return } - guard let user = user else { return } - guard user.managedObjectContext != nil else { return } - self.avatarURL = user.avatarImageURL() - } - } else { - self.avatarURLObserver = nil + + if let user = authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user { + self.avatarURLObserver = user.publisher(for: \.avatar) + .sink { [weak self, weak user] _ in + guard let self = self else { return } + guard let user = user else { return } + guard user.managedObjectContext != nil else { return } + self.avatarURL = user.avatarImageURL() } - - // a11y - let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag } - guard let profileTabItem = _profileTabItem else { return } - - let currentUserDisplayName = activeMastodonAuthentication?.user.displayNameWithFallback ?? "no user" - profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName) - } - .store(in: &disposeBag) + + // a11y + let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag } + guard let profileTabItem = _profileTabItem else { return } + let currentUserDisplayName = user.displayNameWithFallback ?? "no user" + profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName) + + } else { + self.avatarURLObserver = nil + } let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer() tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:))) tabBar.addGestureRecognizer(tabBarLongPressGestureRecognizer) - context.authenticationService.activeMastodonAuthenticationBox - .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationBox in - guard let self = self else { return } - self.isReadyForWizardAvatarButton = authenticationBox != nil - } - .store(in: &disposeBag) + self.isReadyForWizardAvatarButton = authContext != nil $currentTab .receive(on: DispatchQueue.main) @@ -374,13 +369,13 @@ extension MainTabBarController { @objc private func composeButtonDidPressed(_ sender: UIButton) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } + guard let authContext = self.authContext else { return } let composeViewModel = ComposeViewModel( context: context, composeKind: .post, - authenticationBox: authenticationBox + authContext: authContext ) - coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) + _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) } @objc private func tabBarLongPressGestureRecognizerHandler(_ sender: UILongPressGestureRecognizer) { @@ -402,7 +397,9 @@ extension MainTabBarController { switch tab { case .me: - coordinator.present(scene: .accountList, from: self, transition: .panModal) + guard let authContext = self.authContext else { return } + let accountListViewModel = AccountListViewModel(context: context, authContext: authContext) + _ = coordinator.present(scene: .accountList(viewModel: accountListViewModel), from: self, transition: .panModal) default: break } @@ -726,26 +723,28 @@ extension MainTabBarController { @objc private func showFavoritesKeyCommandHandler(_ sender: UIKeyCommand) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - let favoriteViewModel = FavoriteViewModel(context: context) - coordinator.present(scene: .favorite(viewModel: favoriteViewModel), from: nil, transition: .show) + guard let authContext = self.authContext else { return } + let favoriteViewModel = FavoriteViewModel(context: context, authContext: authContext) + _ = coordinator.present(scene: .favorite(viewModel: favoriteViewModel), from: nil, transition: .show) } @objc private func openSettingsKeyCommandHandler(_ sender: UIKeyCommand) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + guard let authContext = self.authContext else { return } guard let setting = context.settingService.currentSetting.value else { return } - let settingsViewModel = SettingsViewModel(context: context, setting: setting) - coordinator.present(scene: .settings(viewModel: settingsViewModel), from: nil, transition: .modal(animated: true, completion: nil)) + let settingsViewModel = SettingsViewModel(context: context, authContext: authContext, setting: setting) + _ = coordinator.present(scene: .settings(viewModel: settingsViewModel), from: nil, transition: .modal(animated: true, completion: nil)) } @objc private func composeNewPostKeyCommandHandler(_ sender: UIKeyCommand) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } + guard let authContext = self.authContext else { return } let composeViewModel = ComposeViewModel( context: context, composeKind: .post, - authenticationBox: authenticationBox + authContext: authContext ) - coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) + _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) } } diff --git a/Mastodon/Scene/Root/RootSplitViewController.swift b/Mastodon/Scene/Root/RootSplitViewController.swift index 3a68d3342..d138f6006 100644 --- a/Mastodon/Scene/Root/RootSplitViewController.swift +++ b/Mastodon/Scene/Root/RootSplitViewController.swift @@ -37,6 +37,10 @@ final class RootSplitViewController: UISplitViewController, NeedsDependency { let searchViewController = SearchViewController() searchViewController.context = context searchViewController.coordinator = coordinator + searchViewController.viewModel = .init( + context: context, + authContext: authContext + ) return searchViewController }() diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift index 86d549373..98006d4c0 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift @@ -191,9 +191,10 @@ extension SidebarViewController: UICollectionViewDelegate { case .tab(let tab): delegate?.sidebarViewController(self, didSelectTab: tab) case .setting: + guard let authContext = viewModel.authContext else { return } guard let setting = context.settingService.currentSetting.value else { return } - let settingsViewModel = SettingsViewModel(context: context, setting: setting) - coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil)) + let settingsViewModel = SettingsViewModel(context: context, authContext: authContext, setting: setting) + _ = coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil)) case .compose: assertionFailure() } @@ -201,15 +202,15 @@ extension SidebarViewController: UICollectionViewDelegate { guard let diffableDataSource = viewModel.secondaryDiffableDataSource else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } + guard let authContext = viewModel.authContext else { return } switch item { case .compose: let composeViewModel = ComposeViewModel( context: context, composeKind: .post, - authenticationBox: authenticationBox + authContext: authContext ) - coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) + _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) default: assertionFailure() } diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index 967d10b09..9f0eb1899 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -21,6 +21,7 @@ final class SidebarViewModel { // input let context: AppContext + let authContext: AuthContext? @Published private var isSidebarDataSourceReady = false @Published private var isAvatarButtonDataReady = false @Published var currentTab: MainTabBarController.Tab = .home @@ -30,10 +31,9 @@ final class SidebarViewModel { var secondaryDiffableDataSource: UICollectionViewDiffableDataSource? @Published private(set) var isReadyForWizardAvatarButton = false - let activeMastodonAuthenticationObjectID = CurrentValueSubject(nil) - - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext?) { self.context = context + self.authContext = authContext Publishers.CombineLatest( $isSidebarDataSourceReady, @@ -42,16 +42,7 @@ final class SidebarViewModel { .map { $0 && $1 } .assign(to: &$isReadyForWizardAvatarButton) - context.authenticationService.activeMastodonAuthentication - .sink { [weak self] authentication in - guard let self = self else { return } - - // bind objectID - self.activeMastodonAuthenticationObjectID.value = authentication?.objectID - - self.isAvatarButtonDataReady = authentication != nil - } - .store(in: &disposeBag) + self.isAvatarButtonDataReady = authContext != nil } } @@ -81,8 +72,8 @@ extension SidebarViewModel { let imageURL: URL? = { switch item { case .me: - let authentication = self.context.authenticationService.activeMastodonAuthentication.value - return authentication?.user.avatarImageURL() + let user = self.authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user + return user?.avatarImageURL() default: return nil } @@ -109,18 +100,19 @@ extension SidebarViewModel { switch item { case .notification: - Publishers.CombineLatest3( - self.context.authenticationService.activeMastodonAuthentication, + Publishers.CombineLatest( self.context.notificationService.unreadNotificationCountDidUpdate, self.$currentTab ) .receive(on: DispatchQueue.main) - .sink { [weak cell] authentication, _, currentTab in + .sink { [weak cell] authentication, currentTab in guard let cell = cell else { return } - let hasUnreadPushNotification: Bool = authentication.flatMap { authentication in - let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: authentication.userAccessToken) + + let hasUnreadPushNotification: Bool = { + guard let accessToken = self.authContext?.mastodonAuthenticationBox.userAuthorization.accessToken else { return false } + let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) return count > 0 - } ?? false + }() let image: UIImage = { if currentTab == .notification { @@ -135,8 +127,8 @@ extension SidebarViewModel { } .store(in: &cell.disposeBag) case .me: - guard let authentication = self.context.authenticationService.activeMastodonAuthentication.value else { break } - let currentUserDisplayName = authentication.user.displayNameWithFallback + guard let user = self.authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return } + let currentUserDisplayName = user.displayNameWithFallback cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName) default: break diff --git a/Mastodon/Scene/Search/Search/SearchViewController.swift b/Mastodon/Scene/Search/Search/SearchViewController.swift index 947c1593d..32f3dbe3d 100644 --- a/Mastodon/Scene/Search/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/Search/SearchViewController.swift @@ -30,7 +30,7 @@ final class SearchViewController: UIViewController, NeedsDependency { var searchTransitionController = SearchTransitionController() var disposeBag = Set() - private(set) lazy var viewModel = SearchViewModel(context: context) + var viewModel: SearchViewModel! // use AutoLayout could set search bar margin automatically to // layout alongside with split mode button (on iPad) @@ -49,10 +49,16 @@ final class SearchViewController: UIViewController, NeedsDependency { let searchBarTapPublisher = PassthroughSubject() - private(set) lazy var discoveryViewController: DiscoveryViewController = { + private(set) lazy var discoveryViewController: DiscoveryViewController? = { + guard let authContext = viewModel.authContext else { return nil } let viewController = DiscoveryViewController() viewController.context = context viewController.coordinator = coordinator + viewController.viewModel = .init( + context: context, + coordinator: coordinator, + authContext: authContext + ) return viewController }() @@ -93,6 +99,8 @@ extension SearchViewController { // collectionView: collectionView // ) + guard let discoveryViewController = self.discoveryViewController else { return } + addChild(discoveryViewController) discoveryViewController.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(discoveryViewController.view) @@ -143,7 +151,8 @@ extension SearchViewController { .sink { [weak self] in guard let self = self else { return } // push to search detail - let searchDetailViewModel = SearchDetailViewModel() + guard let authContext = self.viewModel.authContext else { return } + let searchDetailViewModel = SearchDetailViewModel(authContext: authContext) searchDetailViewModel.needsBecomeFirstResponder = true self.navigationController?.delegate = self.searchTransitionController // FIXME: diff --git a/Mastodon/Scene/Search/Search/SearchViewModel.swift b/Mastodon/Scene/Search/Search/SearchViewModel.swift index b0eccd49b..51d614280 100644 --- a/Mastodon/Scene/Search/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/Search/SearchViewModel.swift @@ -20,14 +20,16 @@ final class SearchViewModel: NSObject { // input let context: AppContext + let authContext: AuthContext? let viewDidAppeared = PassthroughSubject() // output var diffableDataSource: UICollectionViewDiffableDataSource? @Published var hashtags: [Mastodon.Entity.Tag] = [] - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext?) { self.context = context + self.authContext = authContext super.init() // Publishers.CombineLatest( diff --git a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift index 0b0a0d003..6ffc90182 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift @@ -83,7 +83,7 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency { let searchHistoryViewController = SearchHistoryViewController() searchHistoryViewController.context = context searchHistoryViewController.coordinator = coordinator - searchHistoryViewController.viewModel = SearchHistoryViewModel(context: context) + searchHistoryViewController.viewModel = SearchHistoryViewModel(context: context, authContext: viewModel.authContext) return searchHistoryViewController }() } @@ -131,7 +131,7 @@ extension SearchDetailViewController { let searchResultViewController = SearchResultViewController() searchResultViewController.context = context searchResultViewController.coordinator = coordinator - searchResultViewController.viewModel = SearchResultViewModel(context: context, searchScope: scope) + searchResultViewController.viewModel = SearchResultViewModel(context: context, authContext: viewModel.authContext, searchScope: scope) // bind searchText viewModel.searchText diff --git a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewModel.swift index 140fe14e8..779aaa2dc 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewModel.swift @@ -10,12 +10,14 @@ import Foundation import CoreGraphics import Combine import MastodonSDK +import MastodonCore import MastodonAsset import MastodonLocalization final class SearchDetailViewModel { // input + let authContext: AuthContext var needsBecomeFirstResponder = false let viewDidAppear = PassthroughSubject() let navigationBarFrame = CurrentValueSubject(.zero) @@ -26,7 +28,8 @@ final class SearchDetailViewModel { let searchText: CurrentValueSubject let searchActionPublisher = PassthroughSubject() - init(initialSearchText: String = "") { + init(authContext: AuthContext, initialSearchText: String = "") { + self.authContext = authContext self.searchText = CurrentValueSubject(initialSearchText) } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift index 7d5f6c60e..52d0ffb9c 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift @@ -109,6 +109,11 @@ extension SearchHistoryViewController: UICollectionViewDelegate { } +// MARK: - AuthContextProvider +extension SearchHistoryViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - SearchHistorySectionHeaderCollectionReusableViewDelegate extension SearchHistoryViewController: SearchHistorySectionHeaderCollectionReusableViewDelegate { func searchHistorySectionHeaderCollectionReusableView( diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift index b7987413f..1ec06ebe7 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift @@ -17,23 +17,19 @@ final class SearchHistoryViewModel { // input let context: AppContext + let authContext: AuthContext let searchHistoryFetchedResultController: SearchHistoryFetchedResultController // output var diffableDataSource: UICollectionViewDiffableDataSource? - init(context: AppContext) { + init(context: AppContext, authContext: AuthContext) { self.context = context + self.authContext = authContext self.searchHistoryFetchedResultController = SearchHistoryFetchedResultController(managedObjectContext: context.managedObjectContext) - context.authenticationService.activeMastodonAuthenticationBox - .receive(on: DispatchQueue.main) - .sink { [weak self] box in - guard let self = self else { return } - self.searchHistoryFetchedResultController.domain.value = box?.domain - self.searchHistoryFetchedResultController.userID.value = box?.userID - } - .store(in: &disposeBag) + searchHistoryFetchedResultController.domain.value = authContext.mastodonAuthenticationBox.domain + searchHistoryFetchedResultController.userID.value = authContext.mastodonAuthenticationBox.userID } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift index 87c981071..67de62bf3 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift @@ -154,10 +154,9 @@ extension SearchResultViewController { } // MARK: - StatusTableViewCellDelegate -//extension SearchResultViewController: StatusTableViewCellDelegate { -// weak var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { return self } -// func parent() -> UIViewController { return self } -//} +extension SearchResultViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} // MARK: - UITableViewDelegate extension SearchResultViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+Diffable.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+Diffable.swift index ff64b80f0..7d243b1fa 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+Diffable.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+Diffable.swift @@ -18,6 +18,7 @@ extension SearchResultViewModel { tableView: tableView, context: context, configuration: .init( + authContext: authContext, statusViewTableViewCellDelegate: statusTableViewCellDelegate ) ) diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift index cd1579747..b5deb777c 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift @@ -73,11 +73,6 @@ extension SearchResultViewModel.State { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - assertionFailure() - stateMachine.enter(Fail.self) - return - } let searchText = viewModel.searchText.value let searchType = viewModel.searchScope.searchType @@ -133,7 +128,7 @@ extension SearchResultViewModel.State { do { let response = try await viewModel.context.apiService.search( query: query, - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) // discard result when search text is outdated diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift index a7b97de6b..546920749 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift @@ -20,6 +20,7 @@ final class SearchResultViewModel { // input let context: AppContext + let authContext: AuthContext let searchScope: SearchDetailViewModel.SearchScope let searchText = CurrentValueSubject("") @Published var hashtags: [Mastodon.Entity.Tag] = [] @@ -48,30 +49,21 @@ final class SearchResultViewModel { }() let didDataSourceUpdate = PassthroughSubject() - init(context: AppContext, searchScope: SearchDetailViewModel.SearchScope) { + init(context: AppContext, authContext: AuthContext, searchScope: SearchDetailViewModel.SearchScope) { self.context = context + self.authContext = authContext self.searchScope = searchScope self.userFetchedResultsController = UserFetchedResultsController( managedObjectContext: context.managedObjectContext, - domain: nil, + domain: authContext.mastodonAuthenticationBox.domain, additionalPredicate: nil ) self.statusFetchedResultsController = StatusFetchedResultsController( managedObjectContext: context.managedObjectContext, - domain: nil, + domain: authContext.mastodonAuthenticationBox.domain, additionalTweetPredicate: nil ) - context.authenticationService.activeMastodonAuthenticationBox - .map { $0?.domain } - .assign(to: \.domain, on: userFetchedResultsController) - .store(in: &disposeBag) - - context.authenticationService.activeMastodonAuthenticationBox - .map { $0?.domain } - .assign(to: \.domain, on: statusFetchedResultsController) - .store(in: &disposeBag) - // Publishers.CombineLatest( // items, // statusFetchedResultsController.objectIDs.removeDuplicates() diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index edafbe1a3..53a856fd0 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -281,7 +281,7 @@ extension SettingsViewController { } alertController.addAction(cancelAction) alertController.addAction(signOutAction) - self.coordinator.present( + _ = self.coordinator.present( scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil) @@ -289,15 +289,13 @@ extension SettingsViewController { } func signOut() { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } - // clear badge before sign-out context.notificationService.clearNotificationCountForActiveUser() Task { @MainActor in - try await context.authenticationService.signOutMastodonUser(authenticationBox: authenticationBox) + try await context.authenticationService.signOutMastodonUser( + authenticationBox: viewModel.authContext.mastodonAuthenticationBox + ) self.coordinator.setup() } } @@ -372,12 +370,12 @@ extension SettingsViewController: UITableViewDelegate { feedbackGenerator.impactOccurred() switch link { case .accountSettings: - guard let box = context.authenticationService.activeMastodonAuthenticationBox.value, - let url = URL(string: "https://\(box.domain)/auth/edit") else { return } + let domain = viewModel.authContext.mastodonAuthenticationBox.domain + guard let url = URL(string: "https://\(domain)/auth/edit") else { return } viewModel.openAuthenticationPage(authenticateURL: url, presentationContextProvider: self) case .github: guard let url = URL(string: "https://github.com/mastodon/mastodon-ios") else { break } - coordinator.present( + _ = coordinator.present( scene: .safari(url: url), from: self, transition: .safariPresent(animated: true, completion: nil) @@ -385,7 +383,7 @@ extension SettingsViewController: UITableViewDelegate { case .termsOfService, .privacyPolicy: // same URL guard let url = viewModel.privacyURL else { break } - coordinator.present( + _ = coordinator.present( scene: .safari(url: url), from: self, transition: .safariPresent(animated: true, completion: nil) diff --git a/Mastodon/Scene/Settings/SettingsViewModel.swift b/Mastodon/Scene/Settings/SettingsViewModel.swift index ea2275429..8d737b93b 100644 --- a/Mastodon/Scene/Settings/SettingsViewModel.swift +++ b/Mastodon/Scene/Settings/SettingsViewModel.swift @@ -19,10 +19,11 @@ class SettingsViewModel { var disposeBag = Set() + // input let context: AppContext + let authContext: AuthContext var mastodonAuthenticationController: MastodonAuthenticationController? - // input let setting: CurrentValueSubject var updateDisposeBag = Set() var createDisposeBag = Set() @@ -42,15 +43,13 @@ class SettingsViewModel { let updateSubscriptionSubject = PassthroughSubject<(triggerBy: String, values: [Bool?]), Never>() lazy var privacyURL: URL? = { - guard let box = AppContext.shared.authenticationService.activeMastodonAuthenticationBox.value else { - return nil - } - - return Mastodon.API.privacyURL(domain: box.domain) + let domain = authContext.mastodonAuthenticationBox.domain + return Mastodon.API.privacyURL(domain: domain) }() - init(context: AppContext, setting: Setting) { + init(context: AppContext, authContext: AuthContext, setting: Setting) { self.context = context + self.authContext = authContext self.setting = CurrentValueSubject(setting) self.setting @@ -60,10 +59,7 @@ class SettingsViewModel { }) .store(in: &disposeBag) - context.authenticationService.activeMastodonAuthenticationBox - .compactMap { $0?.domain } - .map { context.apiService.instance(domain: $0) } - .switchToLatest() + context.apiService.instance(domain: authContext.mastodonAuthenticationBox.domain) .sink { [weak self] completion in guard let self = self else { return } switch completion { diff --git a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift index ed5e68dc7..98d06fd92 100644 --- a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift @@ -162,42 +162,39 @@ extension NotificationView { } } .store(in: &disposeBag) + + let authContext = viewModel.authContext // isMuting - Publishers.CombineLatest( - viewModel.$userIdentifier, - author.publisher(for: \.mutingBy) - ) - .map { userIdentifier, mutingBy in - guard let userIdentifier = userIdentifier else { return false } - return mutingBy.contains(where: { - $0.id == userIdentifier.userID && $0.domain == userIdentifier.domain - }) - } - .assign(to: \.isMuting, on: viewModel) - .store(in: &disposeBag) + author.publisher(for: \.mutingBy) + .map { mutingBy in + guard let authContext = authContext else { return false } + return mutingBy.contains(where: { + $0.id == authContext.mastodonAuthenticationBox.userID + && $0.domain == authContext.mastodonAuthenticationBox.domain + }) + } + .assign(to: \.isMuting, on: viewModel) + .store(in: &disposeBag) // isBlocking - Publishers.CombineLatest( - viewModel.$userIdentifier, - author.publisher(for: \.blockingBy) - ) - .map { userIdentifier, blockingBy in - guard let userIdentifier = userIdentifier else { return false } - return blockingBy.contains(where: { - $0.id == userIdentifier.userID && $0.domain == userIdentifier.domain - }) - } - .assign(to: \.isBlocking, on: viewModel) - .store(in: &disposeBag) + author.publisher(for: \.blockingBy) + .map { blockingBy in + guard let authContext = authContext else { return false } + return blockingBy.contains(where: { + $0.id == authContext.mastodonAuthenticationBox.userID + && $0.domain == authContext.mastodonAuthenticationBox.domain + }) + } + .assign(to: \.isBlocking, on: viewModel) + .store(in: &disposeBag) // isMyself - Publishers.CombineLatest3( - viewModel.$userIdentifier, + Publishers.CombineLatest( author.publisher(for: \.domain), author.publisher(for: \.id) ) - .map { userIdentifier, domain, id in - guard let userIdentifier = userIdentifier else { return false } - return userIdentifier.domain == domain - && userIdentifier.userID == id + .map { domain, id in + guard let authContext = authContext else { return false } + return authContext.mastodonAuthenticationBox.domain == domain + && authContext.mastodonAuthenticationBox.userID == id } .assign(to: \.isMyself, on: viewModel) .store(in: &disposeBag) diff --git a/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift b/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift index 7d1baa188..334c9ce15 100644 --- a/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift @@ -57,13 +57,13 @@ extension PollOptionView { option.publisher(for: \.poll), option.publisher(for: \.votedBy), option.publisher(for: \.isSelected), - viewModel.$userIdentifier + viewModel.$authContext ) - .sink { [weak self] poll, optionVotedBy, isSelected, userIdentifier in + .sink { [weak self] poll, optionVotedBy, isSelected, authContext in guard let self = self else { return } - let domain = userIdentifier?.domain ?? "" - let userID = userIdentifier?.userID ?? "" + let domain = authContext?.mastodonAuthenticationBox.domain ?? "" + let userID = authContext?.mastodonAuthenticationBox.userID ?? "" let options = poll.options let pollVoteBy = poll.votedBy ?? Set() diff --git a/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift b/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift index 78ca7b88d..590b85652 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift @@ -122,7 +122,7 @@ extension StatusView { let header = createHeader(name: nil, emojis: nil) viewModel.header = header - if let authenticationBox = AppContext.shared.authenticationService.activeMastodonAuthenticationBox.value { + if let authenticationBox = viewModel.authContext?.mastodonAuthenticationBox { Just(inReplyToAccountID) .asyncMap { userID in return try await AppContext.shared.apiService.accountInfo( diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift index 3dfffa540..6e3e73b8a 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift @@ -159,7 +159,7 @@ extension SuggestionAccountViewController: UITableViewDelegate { switch item { case .account(let record): guard let account = record.object(in: context.managedObjectContext) else { return } - let cachedProfileViewModel = CachedProfileViewModel(context: context, mastodonUser: account) + let cachedProfileViewModel = CachedProfileViewModel(context: context, authContext: viewModel.authContext, mastodonUser: account) coordinator.present( scene: .profile(viewModel: cachedProfileViewModel), from: self, @@ -169,6 +169,12 @@ extension SuggestionAccountViewController: UITableViewDelegate { } } +// MARK: - AuthContextProvider +extension SuggestionAccountViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + +// MARK: - SuggestionAccountTableViewCellDelegate extension SuggestionAccountViewController: SuggestionAccountTableViewCellDelegate { func suggestionAccountTableViewCell( _ cell: SuggestionAccountTableViewCell, @@ -177,7 +183,6 @@ extension SuggestionAccountViewController: SuggestionAccountTableViewCellDelegat guard let tableViewDiffableDataSource = viewModel.tableViewDiffableDataSource else { return } guard let indexPath = tableView.indexPath(for: cell) else { return } guard let item = tableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return } - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } switch item { case .account(let user): @@ -186,8 +191,7 @@ extension SuggestionAccountViewController: SuggestionAccountTableViewCellDelegat do { try await DataSourceFacade.responseToUserFollowAction( dependency: self, - user: user, - authenticationBox: authenticationBox + user: user ) } catch { // do noting diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel+Diffable.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel+Diffable.swift index 4496b9f0a..35ba305bc 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel+Diffable.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel+Diffable.swift @@ -17,6 +17,7 @@ extension SuggestionAccountViewModel { tableView: tableView, context: context, configuration: RecommendAccountSection.Configuration( + authContext: authContext, suggestionAccountTableViewCellDelegate: suggestionAccountTableViewCellDelegate ) ) diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift index 70676bf0f..b8af80bb4 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift @@ -25,6 +25,7 @@ final class SuggestionAccountViewModel: NSObject { // input let context: AppContext + let authContext: AuthContext let userFetchedResultsController: UserFetchedResultsController let selectedUserFetchedResultsController: UserFetchedResultsController @@ -35,9 +36,11 @@ final class SuggestionAccountViewModel: NSObject { var tableViewDiffableDataSource: UITableViewDiffableDataSource? init( - context: AppContext + context: AppContext, + authContext: AuthContext ) { self.context = context + self.authContext = authContext self.userFetchedResultsController = UserFetchedResultsController( managedObjectContext: context.managedObjectContext, domain: nil, @@ -50,14 +53,11 @@ final class SuggestionAccountViewModel: NSObject { ) super.init() - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } - userFetchedResultsController.domain = authenticationBox.domain - selectedUserFetchedResultsController.domain = authenticationBox.domain + userFetchedResultsController.domain = authContext.mastodonAuthenticationBox.domain + selectedUserFetchedResultsController.domain = authContext.mastodonAuthenticationBox.domain selectedUserFetchedResultsController.additionalPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: [ - MastodonUser.predicate(followingBy: authenticationBox.userID), - MastodonUser.predicate(followRequestedBy: authenticationBox.userID) + MastodonUser.predicate(followingBy: authContext.mastodonAuthenticationBox.userID), + MastodonUser.predicate(followRequestedBy: authContext.mastodonAuthenticationBox.userID) ]) // fetch recomment users @@ -66,13 +66,13 @@ final class SuggestionAccountViewModel: NSObject { do { let response = try await context.apiService.suggestionAccountV2( query: nil, - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) userIDs = response.value.map { $0.account.id } } catch let error as Mastodon.API.Error where error.httpResponseStatus == .notFound { let response = try await context.apiService.suggestionAccount( query: nil, - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) userIDs = response.value.map { $0.id } } catch { @@ -90,12 +90,9 @@ final class SuggestionAccountViewModel: NSObject { .sink { [weak self] records in guard let _ = self else { return } Task { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } _ = try await context.apiService.relationship( records: records, - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) } } diff --git a/Mastodon/Scene/Thread/CachedThreadViewModel.swift b/Mastodon/Scene/Thread/CachedThreadViewModel.swift index 0a39db590..00c29e157 100644 --- a/Mastodon/Scene/Thread/CachedThreadViewModel.swift +++ b/Mastodon/Scene/Thread/CachedThreadViewModel.swift @@ -10,10 +10,11 @@ import CoreDataStack import MastodonCore final class CachedThreadViewModel: ThreadViewModel { - init(context: AppContext, status: Status) { + init(context: AppContext, authContext: AuthContext, status: Status) { let threadContext = StatusItem.Thread.Context(status: .init(objectID: status.objectID)) super.init( context: context, + authContext: authContext, optionalRoot: .root(context: threadContext) ) } diff --git a/Mastodon/Scene/Thread/RemoteThreadViewModel.swift b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift index da5a9bba0..e22b11961 100644 --- a/Mastodon/Scene/Thread/RemoteThreadViewModel.swift +++ b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift @@ -15,22 +15,20 @@ final class RemoteThreadViewModel: ThreadViewModel { init( context: AppContext, + authContext: AuthContext, statusID: Mastodon.Entity.Status.ID ) { super.init( context: context, + authContext: authContext, optionalRoot: nil ) - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } - Task { @MainActor in - let domain = authenticationBox.domain + let domain = authContext.mastodonAuthenticationBox.domain let response = try await context.apiService.status( statusID: statusID, - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) let managedObjectContext = context.managedObjectContext @@ -49,22 +47,20 @@ final class RemoteThreadViewModel: ThreadViewModel { init( context: AppContext, + authContext: AuthContext, notificationID: Mastodon.Entity.Notification.ID ) { super.init( context: context, + authContext: authContext, optionalRoot: nil ) - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } - Task { @MainActor in - let domain = authenticationBox.domain + let domain = authContext.mastodonAuthenticationBox.domain let response = try await context.apiService.notification( notificationID: notificationID, - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) guard let statusID = response.value.status?.id else { return } diff --git a/Mastodon/Scene/Thread/ThreadViewController.swift b/Mastodon/Scene/Thread/ThreadViewController.swift index 7915df6e5..fc158919b 100644 --- a/Mastodon/Scene/Thread/ThreadViewController.swift +++ b/Mastodon/Scene/Thread/ThreadViewController.swift @@ -112,13 +112,12 @@ extension ThreadViewController { @objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") guard case let .root(threadContext) = viewModel.root else { return } - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } let composeViewModel = ComposeViewModel( context: context, composeKind: .reply(status: threadContext.status), - authenticationBox: authenticationBox + authContext: viewModel.authContext ) - coordinator.present( + _ = coordinator.present( scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil) @@ -126,8 +125,10 @@ extension ThreadViewController { } } -//// MARK: - StatusTableViewControllerAspect -//extension ThreadViewController: StatusTableViewControllerAspect { } +// MARK: - AuthContextProvider +extension ThreadViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} // MARK: - UITableViewDelegate extension ThreadViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { @@ -178,7 +179,6 @@ extension ThreadViewController: UITableViewDelegate, AutoGenerateTableViewDelega // MARK: - StatusTableViewCellDelegate extension ThreadViewController: StatusTableViewCellDelegate { } - extension ThreadViewController { override var keyCommands: [UIKeyCommand]? { return navigationKeyCommands + statusNavigationKeyCommands diff --git a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift index a865dd8f0..7040818e9 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift @@ -23,6 +23,7 @@ extension ThreadViewModel { tableView: tableView, context: context, configuration: StatusSection.Configuration( + authContext: authContext, statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, filterContext: .thread, diff --git a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift index 86fdc2111..4917aacb5 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift @@ -69,11 +69,7 @@ extension ThreadViewModel.LoadThreadState { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - stateMachine.enter(Fail.self) - return - } - + guard let threadContext = viewModel.threadContext else { stateMachine.enter(Fail.self) return @@ -83,7 +79,7 @@ extension ThreadViewModel.LoadThreadState { do { let response = try await viewModel.context.apiService.statusContext( statusID: threadContext.statusID, - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) await enter(state: NoMore.self) diff --git a/Mastodon/Scene/Thread/ThreadViewModel.swift b/Mastodon/Scene/Thread/ThreadViewModel.swift index 54c9d1599..735d85cd4 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel.swift @@ -26,6 +26,7 @@ class ThreadViewModel { // input let context: AppContext + let authContext: AuthContext let mastodonStatusThreadViewModel: MastodonStatusThreadViewModel // let cellFrameCache = NSCache() @@ -54,9 +55,11 @@ class ThreadViewModel { init( context: AppContext, + authContext: AuthContext, optionalRoot: StatusItem.Thread? ) { self.context = context + self.authContext = authContext self.root = optionalRoot self.mastodonStatusThreadViewModel = MastodonStatusThreadViewModel(context: context) // self.rootNode = CurrentValueSubject(optionalStatus.flatMap { RootNode(domain: $0.domain, statusID: $0.id, replyToID: $0.inReplyToID) }) diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index c1e6d7abe..f368fa246 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -146,11 +146,11 @@ extension SceneDelegate { if coordinator?.tabBarController.topMost is ComposeViewController { logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): composing…") } else { - if let authenticationBox = AppContext.shared.authenticationService.activeMastodonAuthenticationBox.value { + if let authContext = coordinator?.authContext { let composeViewModel = ComposeViewModel( context: AppContext.shared, composeKind: .post, - authenticationBox: authenticationBox + authContext: authContext ) coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): present compose scene") diff --git a/MastodonSDK/Sources/CoreDataStack/Utility/ManagedObjectRecord.swift b/MastodonSDK/Sources/CoreDataStack/Utility/ManagedObjectRecord.swift index ea087d894..4910145cf 100644 --- a/MastodonSDK/Sources/CoreDataStack/Utility/ManagedObjectRecord.swift +++ b/MastodonSDK/Sources/CoreDataStack/Utility/ManagedObjectRecord.swift @@ -30,3 +30,9 @@ public class ManagedObjectRecord: Hashable { } } + +extension Managed where Self: NSManagedObject { + public var asRecrod: ManagedObjectRecord { + return .init(objectID: objectID) + } +} diff --git a/MastodonSDK/Sources/MastodonCommon/Preference/Preference+Notification.swift b/MastodonSDK/Sources/MastodonCommon/Preference/Preference+Notification.swift index 7b2ac57a1..38ed3aa5e 100644 --- a/MastodonSDK/Sources/MastodonCommon/Preference/Preference+Notification.swift +++ b/MastodonSDK/Sources/MastodonCommon/Preference/Preference+Notification.swift @@ -1,6 +1,6 @@ // // UserDefaults+Notification.swift -// AppShared +// MastodonCommon // // Created by Cirno MainasuK on 2021-10-9. // diff --git a/MastodonSDK/Sources/MastodonCore/AppSecret.swift b/MastodonSDK/Sources/MastodonCore/AppSecret.swift index 59c7a7cb5..687cb6fca 100644 --- a/MastodonSDK/Sources/MastodonCore/AppSecret.swift +++ b/MastodonSDK/Sources/MastodonCore/AppSecret.swift @@ -1,6 +1,6 @@ // // AppSecret.swift -// MastodonCommon +// MastodonCore // // Created by MainasuK Cirno on 2021-4-27. // diff --git a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift index 8d69c3558..afb4e63a9 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift @@ -23,10 +23,8 @@ public final class AuthenticationService: NSObject { let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController // output - public let mastodonAuthentications = CurrentValueSubject<[MastodonAuthentication], Never>([]) - public let mastodonAuthenticationBoxes = CurrentValueSubject<[MastodonAuthenticationBox], Never>([]) - public let activeMastodonAuthentication = CurrentValueSubject(nil) - public let activeMastodonAuthenticationBox = CurrentValueSubject(nil) + @Published public var mastodonAuthentications: [ManagedObjectRecord] = [] + @Published public var mastodonAuthenticationBoxes: [MastodonAuthenticationBox] = [] init( managedObjectContext: NSManagedObjectContext, @@ -53,38 +51,23 @@ public final class AuthenticationService: NSObject { mastodonAuthenticationFetchedResultsController.delegate = self // TODO: verify credentials for active authentication - - // bind data - mastodonAuthentications - .map { $0.sorted(by: { $0.activedAt > $1.activedAt }).first } - .assign(to: \.value, on: activeMastodonAuthentication) - .store(in: &disposeBag) - mastodonAuthentications + $mastodonAuthentications .map { authentications -> [MastodonAuthenticationBox] in return authentications + .compactMap { $0.object(in: managedObjectContext) } .sorted(by: { $0.activedAt > $1.activedAt }) .compactMap { authentication -> MastodonAuthenticationBox? in - return 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) - ) + return MastodonAuthenticationBox(authentication: authentication) } } - .assign(to: \.value, on: mastodonAuthenticationBoxes) - .store(in: &disposeBag) - - mastodonAuthenticationBoxes - .map { $0.first } - .assign(to: \.value, on: activeMastodonAuthenticationBox) - .store(in: &disposeBag) - + .assign(to: &$mastodonAuthenticationBoxes) + do { try mastodonAuthenticationFetchedResultsController.performFetch() - mastodonAuthentications.value = mastodonAuthenticationFetchedResultsController.fetchedObjects ?? [] + mastodonAuthentications = mastodonAuthenticationFetchedResultsController.fetchedObjects? + .sorted(by: { $0.activedAt > $1.activedAt }) + .compactMap { $0.asRecrod } ?? [] } catch { assertionFailure(error.localizedDescription) } @@ -94,52 +77,28 @@ public final class AuthenticationService: NSObject { extension AuthenticationService { - public func activeMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher, Never> { + public func activeMastodonUser(domain: String, userID: MastodonUser.ID) async throws -> Bool { var isActive = false var _mastodonAuthentication: MastodonAuthentication? - return backgroundManagedObjectContext.performChanges { [weak self] in - guard let self = self else { return } - + let managedObjectContext = backgroundManagedObjectContext + + try await managedObjectContext.performChanges { let request = MastodonAuthentication.sortedFetchRequest request.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID) request.fetchLimit = 1 - guard let mastodonAuthentication = try? self.backgroundManagedObjectContext.fetch(request).first else { + guard let mastodonAuthentication = try? managedObjectContext.fetch(request).first else { return } mastodonAuthentication.update(activedAt: Date()) _mastodonAuthentication = mastodonAuthentication isActive = true + } - } - .receive(on: DispatchQueue.main) - .map { [weak self] result in - switch result { - case .success: - if let self = self, - let mastodonAuthentication = _mastodonAuthentication - { - // force set to avoid delay - self.activeMastodonAuthentication.value = mastodonAuthentication - self.activeMastodonAuthenticationBox.value = MastodonAuthenticationBox( - authenticationRecord: .init(objectID: mastodonAuthentication.objectID), - domain: mastodonAuthentication.domain, - userID: mastodonAuthentication.userID, - appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.appAccessToken), - userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken) - ) - } - case .failure: - break - } - return result.map { isActive } - } - .eraseToAnyPublisher() + return isActive } - public func signOutMastodonUser( - authenticationBox: MastodonAuthenticationBox - ) async throws { + public func signOutMastodonUser(authenticationBox: MastodonAuthenticationBox) async throws { let managedObjectContext = backgroundManagedObjectContext try await managedObjectContext.performChanges { // remove Feed @@ -176,7 +135,6 @@ extension AuthenticationService { } - // MARK: - NSFetchedResultsControllerDelegate extension AuthenticationService: NSFetchedResultsControllerDelegate { @@ -185,10 +143,14 @@ extension AuthenticationService: NSFetchedResultsControllerDelegate { } public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - if controller === mastodonAuthenticationFetchedResultsController { - mastodonAuthentications.value = mastodonAuthenticationFetchedResultsController.fetchedObjects ?? [] + guard controller === mastodonAuthenticationFetchedResultsController else { + assertionFailure() + return } + + mastodonAuthentications = mastodonAuthenticationFetchedResultsController.fetchedObjects? + .sorted(by: { $0.activedAt > $1.activedAt }) + .compactMap { $0.asRecrod } ?? [] } } - diff --git a/MastodonSDK/Sources/MastodonCore/Service/BlockDomainService.swift b/MastodonSDK/Sources/MastodonCore/Service/BlockDomainService.swift index c7b2c2333..02b8bdccd 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/BlockDomainService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/BlockDomainService.swift @@ -14,6 +14,7 @@ import OSLog import UIKit public final class BlockDomainService { + // input weak var backgroundManagedObjectContext: NSManagedObjectContext? weak var authenticationService: AuthenticationService? @@ -27,21 +28,21 @@ public final class BlockDomainService { ) { self.backgroundManagedObjectContext = backgroundManagedObjectContext self.authenticationService = authenticationService - guard let authorizationBox = authenticationService.activeMastodonAuthenticationBox.value else { return } - backgroundManagedObjectContext.perform { - let _blockedDomains: [DomainBlock] = { - let request = DomainBlock.sortedFetchRequest - request.predicate = DomainBlock.predicate(domain: authorizationBox.domain, userID: authorizationBox.userID) - request.returnsObjectsAsFaults = false - do { - return try backgroundManagedObjectContext.fetch(request) - } catch { - assertionFailure(error.localizedDescription) - return [] - } - }() - self.blockedDomains.value = _blockedDomains.map(\.blockedDomain) - } + +// backgroundManagedObjectContext.perform { +// let _blockedDomains: [DomainBlock] = { +// let request = DomainBlock.sortedFetchRequest +// request.predicate = DomainBlock.predicate(domain: authorizationBox.domain, userID: authorizationBox.userID) +// request.returnsObjectsAsFaults = false +// do { +// return try backgroundManagedObjectContext.fetch(request) +// } catch { +// assertionFailure(error.localizedDescription) +// return [] +// } +// }() +// self.blockedDomains.value = _blockedDomains.map(\.blockedDomain) +// } } // func blockDomain( diff --git a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift index 7632704b0..c63e965bd 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift @@ -33,9 +33,9 @@ public final class InstanceService { self.apiService = apiService self.authenticationService = authenticationService - authenticationService.activeMastodonAuthenticationBox + authenticationService.$mastodonAuthenticationBoxes .receive(on: DispatchQueue.main) - .compactMap { $0?.domain } + .compactMap { $0.first?.domain } .removeDuplicates() // prevent infinity loop .sink { [weak self] domain in guard let self = self else { return } diff --git a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift index fff84cd4e..18ff2e508 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift @@ -39,7 +39,7 @@ public final class NotificationService { self.apiService = apiService self.authenticationService = authenticationService - authenticationService.mastodonAuthentications + authenticationService.$mastodonAuthentications .sink(receiveValue: { [weak self] mastodonAuthentications in guard let self = self else { return } @@ -61,16 +61,16 @@ public final class NotificationService { .store(in: &disposeBag) Publishers.CombineLatest( - authenticationService.mastodonAuthentications, + authenticationService.$mastodonAuthenticationBoxes, applicationIconBadgeNeedsUpdate ) .receive(on: DispatchQueue.main) - .sink { [weak self] mastodonAuthentications, _ in + .sink { [weak self] mastodonAuthenticationBoxes, _ in guard let self = self else { return } var count = 0 - for authentication in mastodonAuthentications { - count += UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: authentication.userAccessToken) + for authenticationBox in mastodonAuthenticationBoxes { + count += UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: authenticationBox.userAuthorization.accessToken) } UserDefaults.shared.notificationBadgeCount = count @@ -143,7 +143,7 @@ extension NotificationService { extension NotificationService { public func clearNotificationCountForActiveUser() { guard let authenticationService = self.authenticationService else { return } - if let accessToken = authenticationService.activeMastodonAuthentication.value?.userAccessToken { + if let accessToken = authenticationService.mastodonAuthenticationBoxes.first?.userAuthorization.accessToken { UserDefaults.shared.setNotificationCountWithAccessToken(accessToken: accessToken, value: 0) } diff --git a/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift b/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift index b0932450b..48c66baef 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift @@ -43,7 +43,7 @@ public final class SettingService { ) // create setting (if non-exist) for authenticated users - authenticationService.mastodonAuthenticationBoxes + authenticationService.$mastodonAuthenticationBoxes .compactMap { [weak self] mastodonAuthenticationBoxes -> AnyPublisher<[MastodonAuthenticationBox], Never>? in guard let self = self else { return nil } guard let authenticationService = self.authenticationService else { return nil } @@ -72,15 +72,15 @@ public final class SettingService { // bind current setting Publishers.CombineLatest( - authenticationService.activeMastodonAuthenticationBox, + authenticationService.$mastodonAuthenticationBoxes, settingFetchedResultController.settings ) - .sink { [weak self] activeMastodonAuthenticationBox, settings in + .sink { [weak self] mastodonAuthenticationBoxes, settings in guard let self = self else { return } - guard let activeMastodonAuthenticationBox = activeMastodonAuthenticationBox else { return } + guard let activeMastodonAuthenticationBox = mastodonAuthenticationBoxes.first else { return } let currentSetting = settings.first(where: { setting in - return setting.domain == activeMastodonAuthenticationBox.domain && - setting.userID == activeMastodonAuthenticationBox.userID + return setting.domain == activeMastodonAuthenticationBox.domain + && setting.userID == activeMastodonAuthenticationBox.userID }) self.currentSetting.value = currentSetting } @@ -114,13 +114,13 @@ public final class SettingService { Publishers.CombineLatest3( notificationService.deviceToken, currentSetting.eraseToAnyPublisher(), - authenticationService.activeMastodonAuthenticationBox + authenticationService.$mastodonAuthenticationBoxes ) - .compactMap { [weak self] deviceToken, setting, activeMastodonAuthenticationBox -> AnyPublisher, Error>? in + .compactMap { [weak self] deviceToken, setting, mastodonAuthenticationBoxes -> AnyPublisher, Error>? in guard let self = self else { return nil } guard let deviceToken = deviceToken else { return nil } guard let setting = setting else { return nil } - guard let authenticationBox = activeMastodonAuthenticationBox else { return nil } + guard let authenticationBox = mastodonAuthenticationBoxes.first else { return nil } guard let subscription = setting.activeSubscription else { return nil } diff --git a/MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift b/MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift index 92bb10def..e752a022e 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift @@ -44,13 +44,12 @@ public final class StatusFilterService { .subscribe(filterUpdatePublisher) .store(in: &disposeBag) - let activeMastodonAuthenticationBox = authenticationService.activeMastodonAuthenticationBox Publishers.CombineLatest( - activeMastodonAuthenticationBox, + authenticationService.$mastodonAuthenticationBoxes, filterUpdatePublisher ) - .flatMap { box, _ -> AnyPublisher, Error>, Never> in - guard let box = box else { + .flatMap { mastodonAuthenticationBoxes, _ -> AnyPublisher, Error>, Never> in + guard let box = mastodonAuthenticationBoxes.first else { return Just(Result { throw APIService.APIError.implicit(.authenticationMissing) }).eraseToAnyPublisher() } return apiService.filters(mastodonAuthenticationBox: box) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift index 974069801..032760983 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift @@ -24,7 +24,7 @@ extension NotificationView { let logger = Logger(subsystem: "NotificationView", category: "ViewModel") - @Published public var userIdentifier: UserIdentifier? // me + @Published public var authContext: AuthContext? @Published public var notificationIndicatorText: MetaContent? @@ -55,11 +55,11 @@ extension NotificationView.ViewModel { bindAuthorMenu(notificationView: notificationView) bindFollowRequest(notificationView: notificationView) - $userIdentifier - .assign(to: \.userIdentifier, on: notificationView.statusView.viewModel) + $authContext + .assign(to: \.authContext, on: notificationView.statusView.viewModel) .store(in: &disposeBag) - $userIdentifier - .assign(to: \.userIdentifier, on: notificationView.quoteStatusView.viewModel) + $authContext + .assign(to: \.authContext, on: notificationView.quoteStatusView.viewModel) .store(in: &disposeBag) } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift index cacb56a8a..2db731971 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift @@ -164,6 +164,8 @@ public final class NotificationView: UIView { disposeBag.removeAll() viewModel.objects.removeAll() + + viewModel.authContext = nil viewModel.authorAvatarImageURL = nil avatarButton.avatarImageView.cancelTask() diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/PollOptionView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/PollOptionView+ViewModel.swift index b6ea11d49..a91f57dc2 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/PollOptionView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/PollOptionView+ViewModel.swift @@ -30,7 +30,7 @@ extension PollOptionView { let layoutDidUpdate = PassthroughSubject() - @Published public var userIdentifier: UserIdentifier? + @Published public var authContext: AuthContext? @Published public var style: PollOptionView.Style? diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index 648f256b6..cb542da7c 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -9,14 +9,14 @@ import os.log import UIKit import Combine import CoreData -import Meta -import MastodonSDK -import MastodonCore -import MastodonAsset -import MastodonLocalization -import MastodonExtension -import MastodonCommon import CoreDataStack +import Meta +import MastodonAsset +import MastodonCore +import MastodonCommon +import MastodonExtension +import MastodonLocalization +import MastodonSDK extension StatusView { public final class ViewModel: ObservableObject { @@ -26,7 +26,7 @@ extension StatusView { let logger = Logger(subsystem: "StatusView", category: "ViewModel") - @Published public var userIdentifier: UserIdentifier? // me + public var authContext: AuthContext? // Header @Published public var header: Header = .none @@ -127,6 +127,8 @@ extension StatusView { } public func prepareForReuse() { + authContext = nil + authorAvatarImageURL = nil isContentSensitive = false diff --git a/Podfile.lock b/Podfile.lock index 453610694..0cec6626a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -37,6 +37,6 @@ SPEC CHECKSUMS: "UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3 XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5 -PODFILE CHECKSUM: d95968ab70ea5121c21dfd801aa36b12bcd59c9d +PODFILE CHECKSUM: 50ec5b2c4aa189024cc5ab41039f983dc5609040 COCOAPODS: 1.11.3 From 5f71acf5ce7507c4cb7482781af26fb33ef138c9 Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 9 Oct 2022 20:30:10 +0800 Subject: [PATCH 037/658] chore: set CI build device --- .github/scripts/build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh index 3c570b1af..d4fc4acc7 100755 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -7,5 +7,6 @@ set -eo pipefail xcodebuild -workspace Mastodon.xcworkspace \ -scheme Mastodon \ + -destination "platform=iOS Simulator,name=iPhone SE (2nd generation)" \ clean \ build | xcpretty From 56f04db40f999052042c3d7460bf6a429e33aec5 Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 9 Oct 2022 21:40:02 +0800 Subject: [PATCH 038/658] chore: fix compile issue --- Mastodon.xcodeproj/project.pbxproj | 37 ++--- .../xcschemes/Mastodon - Release.xcscheme | 2 +- .../xcschemes/Mastodon - Snapshot.xcscheme | 2 +- .../xcshareddata/xcschemes/Mastodon.xcscheme | 2 +- .../Scene/Account/AccountViewController.swift | 4 +- ...ComposeStatusAttachmentTableViewCell.swift | 4 +- .../HomeTimelineViewController.swift | 2 +- .../Bookmark/BookmarkViewModel+State.swift | 4 +- .../Content/StatusView+Configuration.swift | 137 ++++++++---------- MastodonSDK/Package.swift | 2 +- 10 files changed, 94 insertions(+), 102 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 30f9c0c2d..9a38be2b6 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -345,7 +345,7 @@ DB8F7076279E954700E1225B /* DataSourceFacade+Follow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8F7075279E954700E1225B /* DataSourceFacade+Follow.swift */; }; DB8FABC726AEC7B2008E5AF4 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB8FAB9E26AEC3A2008E5AF4 /* Intents.framework */; }; DB8FABCA26AEC7B2008E5AF4 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8FABC926AEC7B2008E5AF4 /* IntentHandler.swift */; }; - DB8FABCE26AEC7B2008E5AF4 /* MastodonIntent.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DB8FABC626AEC7B2008E5AF4 /* MastodonIntent.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DB8FABCE26AEC7B2008E5AF4 /* MastodonIntent.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DB8FABC626AEC7B2008E5AF4 /* MastodonIntent.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */; }; DB938EE62623F50700E5B6C1 /* ThreadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938EE52623F50700E5B6C1 /* ThreadViewController.swift */; }; DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */; }; @@ -420,7 +420,7 @@ DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */; }; DBC6461526A170AB00B0E31B /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6461426A170AB00B0E31B /* ShareViewController.swift */; }; DBC6461826A170AB00B0E31B /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DBC6461626A170AB00B0E31B /* MainInterface.storyboard */; }; - DBC6461C26A170AB00B0E31B /* ShareActionExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DBC6461C26A170AB00B0E31B /* ShareActionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 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 */; }; @@ -466,7 +466,7 @@ DBF3B73F2733EAED00E21627 /* local-codes.json in Resources */ = {isa = PBXBuildFile; fileRef = DBF3B73E2733EAED00E21627 /* local-codes.json */; }; DBF3B7412733EB9400E21627 /* MastodonLocalCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF3B7402733EB9400E21627 /* MastodonLocalCode.swift */; }; DBF8AE16263293E400C9C23C /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF8AE15263293E400C9C23C /* NotificationService.swift */; }; - DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBF8AE13263293E400C9C23C /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DBF8AE13263293E400C9C23C /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBF96325262EC0A6001D8D25 /* AuthenticationServices.framework */; }; DBF9814A265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */; }; DBF9814C265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */; }; @@ -533,17 +533,17 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - DBF8AE1B263293E400C9C23C /* Embed App Extensions */ = { + DBF8AE1B263293E400C9C23C /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( - DB8FABCE26AEC7B2008E5AF4 /* MastodonIntent.appex in Embed App Extensions */, - DBC6461C26A170AB00B0E31B /* ShareActionExtension.appex in Embed App Extensions */, - DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed App Extensions */, + DB8FABCE26AEC7B2008E5AF4 /* MastodonIntent.appex in Embed Foundation Extensions */, + DBC6461C26A170AB00B0E31B /* ShareActionExtension.appex in Embed Foundation Extensions */, + DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed Foundation Extensions */, ); - name = "Embed App Extensions"; + name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ @@ -2916,7 +2916,7 @@ DB427DCE25BAA00100D1B89D /* Sources */, DB427DCF25BAA00100D1B89D /* Frameworks */, DB89BA0825C10FD0008580ED /* Embed Frameworks */, - DBF8AE1B263293E400C9C23C /* Embed App Extensions */, + DBF8AE1B263293E400C9C23C /* Embed Foundation Extensions */, DB3D100425BAA71500EAA174 /* ShellScript */, DB025B8E278D6448002F581E /* ShellScript */, DB697DD2278F48D5004EF2F7 /* ShellScript */, @@ -3042,7 +3042,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1250; - LastUpgradeCheck = 1250; + LastUpgradeCheck = 1400; TargetAttributes = { DB427DD125BAA00100D1B89D = { CreatedOnToolsVersion = 12.4; @@ -3237,7 +3237,8 @@ }; DB025B8E278D6448002F581E /* ShellScript */ = { isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; + alwaysOutOfDate = 1; + buildActionMask = 8; files = ( ); inputFileListPaths = ( @@ -3248,13 +3249,14 @@ ); outputPaths = ( ); - runOnlyForDeploymentPostprocessing = 0; + runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/sh; shellScript = "if [[ -f \"${PODS_ROOT}/Sourcery/bin/sourcery\" ]]; then\n \"${PODS_ROOT}/Sourcery/bin/sourcery\" --config ./MastodonSDK/Sources/CoreDataStack\nelse\n echo \"warning: Sourcery is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; }; DB3D100425BAA71500EAA174 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; + alwaysOutOfDate = 1; + buildActionMask = 8; files = ( ); inputFileListPaths = ( @@ -3265,13 +3267,14 @@ ); outputPaths = ( ); - runOnlyForDeploymentPostprocessing = 0; + runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/sh; shellScript = "if [[ -f \"${PODS_ROOT}/SwiftGen/bin/swiftgen\" ]]; then\n \"${PODS_ROOT}/SwiftGen/bin/swiftgen\" \nelse\n echo \"warning: SwiftGen is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; }; DB697DD2278F48D5004EF2F7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; + alwaysOutOfDate = 1; + buildActionMask = 8; files = ( ); inputFileListPaths = ( @@ -3282,7 +3285,7 @@ ); outputPaths = ( ); - runOnlyForDeploymentPostprocessing = 0; + runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/sh; shellScript = "if [[ -f \"${PODS_ROOT}/Sourcery/bin/sourcery\" ]]; then\n \"${PODS_ROOT}/Sourcery/bin/sourcery\" --config ./Mastodon\nelse\n echo \"warning: Sourcery is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; }; diff --git a/Mastodon.xcodeproj/xcshareddata/xcschemes/Mastodon - Release.xcscheme b/Mastodon.xcodeproj/xcshareddata/xcschemes/Mastodon - Release.xcscheme index f88978596..87410f779 100644 --- a/Mastodon.xcodeproj/xcshareddata/xcschemes/Mastodon - Release.xcscheme +++ b/Mastodon.xcodeproj/xcshareddata/xcschemes/Mastodon - Release.xcscheme @@ -1,6 +1,6 @@ UICollectionViewCell? in - guard let self = self else { return UICollectionViewCell() } + guard let _ = self else { return UICollectionViewCell() } switch item { - case .attachment(let attachmentService): + case .attachment: let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell cell.contentConfiguration = UIHostingConfigurationBackport { HStack { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 095a362bf..b287f3b71 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -384,7 +384,7 @@ extension HomeTimelineViewController { @objc private func manuallySearchButtonPressed(_ sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - let searchDetailViewModel = SearchDetailViewModel() + let searchDetailViewModel = SearchDetailViewModel(authContext: viewModel.authContext) coordinator.present(scene: .searchDetail(viewModel: searchDetailViewModel), from: self, transition: .modal(animated: true, completion: nil)) } diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift index 21cb8e021..8c9fd1150 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift @@ -48,7 +48,7 @@ extension BookmarkViewModel { extension BookmarkViewModel.State { class Initial: BookmarkViewModel.State { override func isValidNextState(_ stateClass: AnyClass) -> Bool { - guard let viewModel = viewModel else { return false } + guard let _ = viewModel else { return false } switch stateClass { case is Reloading.Type: return true @@ -132,7 +132,7 @@ extension BookmarkViewModel.State { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + guard let viewModel = viewModel, let _ = stateMachine else { return } if previousState is Reloading { maxID = nil diff --git a/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift b/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift index 590b85652..bea08059d 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift @@ -185,41 +185,36 @@ extension StatusView { .assign(to: \.locked, on: viewModel) .store(in: &disposeBag) // isMuting - Publishers.CombineLatest( - viewModel.$userIdentifier, - author.publisher(for: \.mutingBy) - ) - .map { userIdentifier, mutingBy in - guard let userIdentifier = userIdentifier else { return false } - return mutingBy.contains(where: { - $0.id == userIdentifier.userID && $0.domain == userIdentifier.domain - }) - } - .assign(to: \.isMuting, on: viewModel) - .store(in: &disposeBag) + author.publisher(for: \.mutingBy) + .map { [weak viewModel] mutingBy in + guard let viewModel = viewModel else { return false } + guard let authContext = viewModel.authContext else { return false } + return mutingBy.contains(where: { + $0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain + }) + } + .assign(to: \.isMuting, on: viewModel) + .store(in: &disposeBag) // isBlocking - Publishers.CombineLatest( - viewModel.$userIdentifier, - author.publisher(for: \.blockingBy) - ) - .map { userIdentifier, blockingBy in - guard let userIdentifier = userIdentifier else { return false } - return blockingBy.contains(where: { - $0.id == userIdentifier.userID && $0.domain == userIdentifier.domain - }) - } - .assign(to: \.isBlocking, on: viewModel) - .store(in: &disposeBag) + author.publisher(for: \.blockingBy) + .map { [weak viewModel] blockingBy in + guard let viewModel = viewModel else { return false } + guard let authContext = viewModel.authContext else { return false } + return blockingBy.contains(where: { + $0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain + }) + } + .assign(to: \.isBlocking, on: viewModel) + .store(in: &disposeBag) // isMyself - Publishers.CombineLatest3( - viewModel.$userIdentifier, + Publishers.CombineLatest( author.publisher(for: \.domain), author.publisher(for: \.id) ) - .map { userIdentifier, domain, id in - guard let userIdentifier = userIdentifier else { return false } - return userIdentifier.domain == domain - && userIdentifier.userID == id + .map { [weak viewModel] domain, id in + guard let viewModel = viewModel else { return false } + guard let authContext = viewModel.authContext else { return false } + return authContext.mastodonAuthenticationBox.domain == domain && authContext.mastodonAuthenticationBox.userID == id } .assign(to: \.isMyself, on: viewModel) .store(in: &disposeBag) @@ -317,15 +312,15 @@ extension StatusView { .store(in: &disposeBag) // isVotable if let poll = status.poll { - Publishers.CombineLatest3( + Publishers.CombineLatest( poll.publisher(for: \.votedBy), - poll.publisher(for: \.expired), - viewModel.$userIdentifier + poll.publisher(for: \.expired) ) - .map { votedBy, expired, userIdentifier in - guard let userIdentifier = userIdentifier else { return false } - let domain = userIdentifier.domain - let userID = userIdentifier.userID + .map { [weak viewModel] votedBy, expired in + guard let viewModel = viewModel else { return false } + guard let authContext = viewModel.authContext else { return false } + let domain = authContext.mastodonAuthenticationBox.domain + let userID = authContext.mastodonAuthenticationBox.userID let isVoted = votedBy?.contains(where: { $0.domain == domain && $0.id == userID }) ?? false return !isVoted && !expired } @@ -372,44 +367,38 @@ extension StatusView { .store(in: &disposeBag) // relationship - Publishers.CombineLatest( - viewModel.$userIdentifier, - status.publisher(for: \.rebloggedBy) - ) - .map { userIdentifier, rebloggedBy in - guard let userIdentifier = userIdentifier else { return false } - return rebloggedBy.contains(where: { - $0.id == userIdentifier.userID && $0.domain == userIdentifier.domain - }) - } - .assign(to: \.isReblog, on: viewModel) - .store(in: &disposeBag) + status.publisher(for: \.rebloggedBy) + .map { [weak viewModel] rebloggedBy in + guard let viewModel = viewModel else { return false } + guard let authContext = viewModel.authContext else { return false } + return rebloggedBy.contains(where: { + $0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain + }) + } + .assign(to: \.isReblog, on: viewModel) + .store(in: &disposeBag) - Publishers.CombineLatest( - viewModel.$userIdentifier, - status.publisher(for: \.favouritedBy) - ) - .map { userIdentifier, favouritedBy in - guard let userIdentifier = userIdentifier else { return false } - return favouritedBy.contains(where: { - $0.id == userIdentifier.userID && $0.domain == userIdentifier.domain - }) - } - .assign(to: \.isFavorite, on: viewModel) - .store(in: &disposeBag) - - Publishers.CombineLatest( - viewModel.$userIdentifier, - status.publisher(for: \.bookmarkedBy) - ) - .map { userIdentifier, bookmarkedBy in - guard let userIdentifier = userIdentifier else { return false } - return bookmarkedBy.contains(where: { - $0.id == userIdentifier.userID && $0.domain == userIdentifier.domain - }) - } - .assign(to: \.isBookmark, on: viewModel) - .store(in: &disposeBag) + status.publisher(for: \.favouritedBy) + .map { [weak viewModel]favouritedBy in + guard let viewModel = viewModel else { return false } + guard let authContext = viewModel.authContext else { return false } + return favouritedBy.contains(where: { + $0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain + }) + } + .assign(to: \.isFavorite, on: viewModel) + .store(in: &disposeBag) + + status.publisher(for: \.bookmarkedBy) + .map { [weak viewModel] bookmarkedBy in + guard let viewModel = viewModel else { return false } + guard let authContext = viewModel.authContext else { return false } + return bookmarkedBy.contains(where: { + $0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain + }) + } + .assign(to: \.isBookmark, on: viewModel) + .store(in: &disposeBag) } private func configureFilter(status: Status) { diff --git a/MastodonSDK/Package.swift b/MastodonSDK/Package.swift index 7b0fe6af6..ac968754d 100644 --- a/MastodonSDK/Package.swift +++ b/MastodonSDK/Package.swift @@ -42,7 +42,7 @@ let package = Package( .package(url: "https://github.com/slackhq/PanModal.git", from: "1.2.7"), .package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"), .package(url: "https://github.com/TimOliver/TOCropViewController.git", from: "2.6.1"), - .package(url: "https://github.com/TwidereProject/MetaTextKit.git", .exact("2.2.5")), + .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "2.2.5"), .package(url: "https://github.com/TwidereProject/TabBarPager.git", from: "0.1.0"), .package(url: "https://github.com/uias/Tabman", from: "2.13.0"), .package(url: "https://github.com/vtourraine/ThirdPartyMailer.git", from: "2.1.0"), From 02e3ad9a16c4eb3f579ce08100d3c10a2c0070be Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 10 Oct 2022 19:14:52 +0800 Subject: [PATCH 039/658] chore: [WIP] restore the replyTo entry for compose --- Mastodon.xcodeproj/project.pbxproj | 254 +- .../xcschemes/xcschememanagement.plist | 4 +- .../Compose/CustomEmojiPickerSection.swift | 62 - .../Notification/NotificationSection.swift | 1 + Mastodon/Diffiable/Report/ReportSection.swift | 1 + Mastodon/Diffiable/User/UserSection.swift | 3 +- .../MastodonSDK/Mastodon+Entity+Account.swift | 51 - .../MastodonSDK/Mastodon+Entity+Tag.swift | 18 - Mastodon/Protocol/NamingState.swift | 12 - .../Provider/DataSourceFacade+Status.swift | 4 +- ...tatusTableViewControllerNavigateable.swift | 6 +- .../AutoCompleteViewModel+Diffable.swift | 22 - ...sPollExpiresOptionCollectionViewCell.swift | 126 +- .../Scene/Compose/ComposeViewController.swift | 2069 +++++++++-------- .../Compose/ComposeViewModel+DataSource.swift | 954 ++++---- .../ComposeViewModel+PublishState.swift | 298 +-- Mastodon/Scene/Compose/ComposeViewModel.swift | 663 +++--- ...ComposeStatusAttachmentTableViewCell.swift | 171 -- .../ComposeStatusContentTableViewCell.swift | 172 -- .../ComposeStatusPollTableViewCell.swift | 209 -- .../CustomEmojiPickerInputViewModel.swift | 2 + .../DiscoveryCommunityViewModel+State.swift | 14 +- .../News/DiscoveryNewsViewModel+State.swift | 14 +- .../Posts/DiscoveryPostsViewModel+State.swift | 14 +- .../HashtagTimelineViewController.swift | 7 +- .../HashtagTimelineViewModel+State.swift | 11 +- .../HashtagTimelineViewModel.swift | 1 - .../HomeTimelineViewModel+Diffable.swift | 1 + ...omeTimelineViewModel+LoadOldestState.swift | 14 +- .../HomeTimeline/HomeTimelineViewModel.swift | 1 + ...ionTimelineViewModel+LoadOldestState.swift | 14 +- .../PickServerLoaderTableViewCell.swift | 4 +- ...ckServerServerSectionTableHeaderView.swift | 1 + .../Bookmark/BookmarkViewController.swift | 1 + .../Bookmark/BookmarkViewModel+State.swift | 14 +- .../Favorite/FavoriteViewController.swift | 1 + .../Favorite/FavoriteViewModel+State.swift | 14 +- .../Follower/FollowerListViewController.swift | 1 + .../FollowerListViewModel+State.swift | 10 +- .../FollowingListViewController.swift | 1 + .../FollowingListViewModel+State.swift | 14 +- .../Scene/Profile/ProfileViewController.swift | 12 +- .../UserTimelineViewModel+State.swift | 14 +- .../UserLIst/UserListViewModel+State.swift | 14 +- .../ReportStatusViewController.swift | 1 + .../ReportSupplementaryViewController.swift | 1 + .../Root/MainTab/MainTabBarController.swift | 8 +- .../Root/Sidebar/SidebarViewController.swift | 4 +- .../SearchResultViewModel+State.swift | 13 +- .../SuggestionAccountViewController.swift | 1 + .../Scene/Thread/ThreadViewController.swift | 5 +- .../Thread/ThreadViewModel+Diffable.swift | 1 + .../ThreadViewModel+LoadThreadState.swift | 14 +- Mastodon/Supporting Files/SceneDelegate.swift | 6 +- .../Mastodon+API+Subscriptions+Policy.swift | 2 +- .../MastodonSDK/Mastodon+Entity+Account.swift | 33 +- .../Mastodon+Entity+Error+Detail.swift | 14 +- .../MastodonSDK/Mastodon+Entity+Error.swift | 0 .../MastodonSDK/Mastodon+Entity+Field.swift | 0 .../MastodonSDK/Mastodon+Entity+History.swift | 0 .../Mastodon+Entity+Notification+Type.swift | 0 .../Model}/Compose/AutoCompleteItem.swift | 6 +- .../Model/Compose/AutoCompleteSection.swift | 16 + .../Compose/ComposeStatusAttachmentItem.swift | 1 - .../ComposeStatusAttachmentSection.swift | 0 .../Model}/Compose/ComposeStatusItem.swift | 1 - .../Compose/ComposeStatusPollItem.swift | 0 .../Compose/ComposeStatusPollSection.swift | 0 .../Model}/Compose/ComposeStatusSection.swift | 61 +- .../Compose/CustomEmojiPickerItem.swift | 0 .../Compose/CustomEmojiPickerSection.swift | 12 + .../Model/Poll/PollItem.swift | 0 .../Model/Poll/PollSection.swift | 0 .../Service/API/APIService+Favorite.swift | 1 - .../Service/AuthenticationService.swift | 2 - .../AutoCompleteSection+Diffable.swift | 21 +- .../CustomEmojiPickerSection+Diffable.swift | 59 + .../AutoCompleteViewController.swift | 2 +- .../AutoCompleteViewModel+Diffable.swift | 22 + .../AutoCompleteViewModel+State.swift | 14 +- .../AutoComplete/AutoCompleteViewModel.swift | 0 .../Cell/AutoCompleteTableViewCell.swift | 10 +- .../View/AutoCompleteTopChevronView.swift | 0 .../ComposeContent/ComposeContentView.swift | 92 +- .../ComposeContentViewController.swift | 31 +- .../ComposeContentViewModel+DataSource.swift | 90 + .../ComposeContentViewModel.swift | 32 +- .../ComposeReplyToTableViewCell.swift | 11 +- ...ComposeStatusAttachmentTableViewCell.swift | 172 ++ .../ComposeStatusContentTableViewCell.swift | 172 ++ .../ComposeStatusPollTableViewCell.swift | 209 ++ .../View/ComposeTableView.swift | 0 .../ControlContainableScrollViews.swift | 12 +- .../Content/MediaView+Configuration.swift | 57 + .../Content/StatusView+Configuration.swift | 7 +- .../View/Decoration/SawToothView.swift | 4 +- .../TimelineBottomLoaderTableViewCell.swift | 7 +- .../TimelineLoaderTableViewCell.swift | 31 +- ...eMiddleLoaderTableViewCell+ViewModel.swift | 6 +- .../TimelineMiddleLoaderTableViewCell.swift | 7 +- .../TimelineTopLoaderTableViewCell.swift | 6 +- .../Scene/ComposeViewController.swift | 327 +++ .../Scene/ComposeViewModel.swift | 417 ++++ .../Scene/ShareViewController.swift | 325 --- .../Scene/ShareViewModel.swift | 412 ---- 105 files changed, 4011 insertions(+), 4041 deletions(-) delete mode 100644 Mastodon/Diffiable/Compose/CustomEmojiPickerSection.swift delete mode 100644 Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift delete mode 100644 Mastodon/Extension/MastodonSDK/Mastodon+Entity+Tag.swift delete mode 100644 Mastodon/Protocol/NamingState.swift delete mode 100644 Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+Diffable.swift delete mode 100644 Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift delete mode 100644 Mastodon/Scene/Compose/TableViewCell/ComposeStatusContentTableViewCell.swift delete mode 100644 Mastodon/Scene/Compose/TableViewCell/ComposeStatusPollTableViewCell.swift rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Extension/MastodonSDK/Mastodon+API+Subscriptions+Policy.swift (95%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Extension/MastodonSDK/Mastodon+Entity+Error+Detail.swift (92%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Extension/MastodonSDK/Mastodon+Entity+Error.swift (100%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Extension/MastodonSDK/Mastodon+Entity+Field.swift (100%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Extension/MastodonSDK/Mastodon+Entity+History.swift (100%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift (100%) rename {Mastodon/Diffiable => MastodonSDK/Sources/MastodonCore/Model}/Compose/AutoCompleteItem.swift (90%) create mode 100644 MastodonSDK/Sources/MastodonCore/Model/Compose/AutoCompleteSection.swift rename {Mastodon/Diffiable => MastodonSDK/Sources/MastodonCore/Model}/Compose/ComposeStatusAttachmentItem.swift (93%) rename {Mastodon/Diffiable => MastodonSDK/Sources/MastodonCore/Model}/Compose/ComposeStatusAttachmentSection.swift (100%) rename {Mastodon/Diffiable => MastodonSDK/Sources/MastodonCore/Model}/Compose/ComposeStatusItem.swift (99%) rename {Mastodon/Diffiable => MastodonSDK/Sources/MastodonCore/Model}/Compose/ComposeStatusPollItem.swift (100%) rename {Mastodon/Diffiable => MastodonSDK/Sources/MastodonCore/Model}/Compose/ComposeStatusPollSection.swift (100%) rename {Mastodon/Diffiable => MastodonSDK/Sources/MastodonCore/Model}/Compose/ComposeStatusSection.swift (54%) rename {Mastodon/Diffiable => MastodonSDK/Sources/MastodonCore/Model}/Compose/CustomEmojiPickerItem.swift (100%) create mode 100644 MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerSection.swift rename MastodonSDK/Sources/{MastodonUI => MastodonCore}/Model/Poll/PollItem.swift (100%) rename MastodonSDK/Sources/{MastodonUI => MastodonCore}/Model/Poll/PollSection.swift (100%) rename Mastodon/Diffiable/Compose/AutoCompleteSection.swift => MastodonSDK/Sources/MastodonUI/DataSource/AutoCompleteSection+Diffable.swift (94%) create mode 100644 MastodonSDK/Sources/MastodonUI/DataSource/CustomEmojiPickerSection+Diffable.swift rename {Mastodon/Scene/Compose => MastodonSDK/Sources/MastodonUI/Scene/ComposeContent}/AutoComplete/AutoCompleteViewController.swift (98%) create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+Diffable.swift rename {Mastodon/Scene/Compose => MastodonSDK/Sources/MastodonUI/Scene/ComposeContent}/AutoComplete/AutoCompleteViewModel+State.swift (95%) rename {Mastodon/Scene/Compose => MastodonSDK/Sources/MastodonUI/Scene/ComposeContent}/AutoComplete/AutoCompleteViewModel.swift (100%) rename {Mastodon/Scene/Compose => MastodonSDK/Sources/MastodonUI/Scene/ComposeContent}/AutoComplete/Cell/AutoCompleteTableViewCell.swift (95%) rename {Mastodon/Scene/Compose => MastodonSDK/Sources/MastodonUI/Scene/ComposeContent}/AutoComplete/View/AutoCompleteTopChevronView.swift (100%) create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift rename Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift => MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeReplyToTableViewCell.swift (83%) create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusAttachmentTableViewCell.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusContentTableViewCell.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusPollTableViewCell.swift rename {Mastodon/Scene/Compose => MastodonSDK/Sources/MastodonUI/Scene/ComposeContent}/View/ComposeTableView.swift (100%) rename {Mastodon/Vender => MastodonSDK/Sources/MastodonUI/Vendor}/ControlContainableScrollViews.swift (80%) rename {Mastodon/Scene/Share => MastodonSDK/Sources/MastodonUI}/View/Content/StatusView+Configuration.swift (99%) rename {Mastodon/Scene/Share => MastodonSDK/Sources/MastodonUI}/View/Decoration/SawToothView.swift (95%) rename {Mastodon/Scene/Share/View/TableviewCell => MastodonSDK/Sources/MastodonUI/View/TableViewCell}/TimelineBottomLoaderTableViewCell.swift (81%) rename {Mastodon/Scene/Share/View/TableviewCell => MastodonSDK/Sources/MastodonUI/View/TableViewCell}/TimelineLoaderTableViewCell.swift (84%) rename {Mastodon/Scene/Share/View/TableviewCell => MastodonSDK/Sources/MastodonUI/View/TableViewCell}/TimelineMiddleLoaderTableViewCell+ViewModel.swift (90%) rename {Mastodon/Scene/Share/View/TableviewCell => MastodonSDK/Sources/MastodonUI/View/TableViewCell}/TimelineMiddleLoaderTableViewCell.swift (93%) rename {Mastodon/Scene/Share/View/TableviewCell => MastodonSDK/Sources/MastodonUI/View/TableViewCell}/TimelineTopLoaderTableViewCell.swift (83%) create mode 100644 ShareActionExtension/Scene/ComposeViewController.swift create mode 100644 ShareActionExtension/Scene/ComposeViewModel.swift delete mode 100644 ShareActionExtension/Scene/ShareViewController.swift delete mode 100644 ShareActionExtension/Scene/ShareViewModel.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 9a38be2b6..d2eda9b89 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -28,7 +28,6 @@ 2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; }; 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; }; 2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */; }; - 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; }; 2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D35237926256D920031AF25 /* NotificationSection.swift */; }; 2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */; }; 2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */; }; @@ -44,12 +43,10 @@ 2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */; }; 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */; }; 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */; }; - 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */; }; 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */; }; 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */; }; 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */; }; 2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D607AD726242FC500B70763 /* NotificationViewModel.swift */; }; - 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; }; 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */; }; 2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */; }; 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76319E25C1521200929FB9 /* StatusSection.swift */; }; @@ -62,13 +59,9 @@ 2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D84350425FF858100EECE90 /* UIScrollView.swift */; }; 2D939AB525EDD8A90076FA61 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; }; 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */; }; - 2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA504682601ADE7008F4E6C /* SawToothView.swift */; }; - 2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */; }; - 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; }; 2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */; }; 2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC9E3D262FC2400062E1A6 /* SuggestionAccountViewModel.swift */; }; 2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */; }; - 2DB72C8C262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */; }; 2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */; }; 2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */; }; 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; }; @@ -84,9 +77,6 @@ 5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; }; 5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; }; 5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DA732CB2629CEF500A92342 /* UIView+Remove.swift */; }; - 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */; }; - 5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */; }; - 5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */; }; 5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */; }; 5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; }; 5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; }; @@ -110,8 +100,6 @@ DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; DB03A793272A7E5700EE37C5 /* SidebarListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03A792272A7E5700EE37C5 /* SidebarListHeaderView.swift */; }; DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */; }; - DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */; }; - DB03F7F52689B782007B274C /* ComposeTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F42689B782007B274C /* ComposeTableView.swift */; }; DB0617EB277EF3820030EE79 /* GradientBorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617EA277EF3820030EE79 /* GradientBorderView.swift */; }; DB0617ED277F02C50030EE79 /* OnboardingNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617EC277F02C50030EE79 /* OnboardingNavigationController.swift */; }; DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617EE277F12720030EE79 /* NavigationActionView.swift */; }; @@ -130,8 +118,6 @@ DB0F9D54283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F9D53283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift */; }; DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F9D55283EB46200379AF8 /* ProfileHeaderView+Configuration.swift */; }; DB0FCB68279507EF006C02E2 /* DataSourceFacade+Meta.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */; }; - DB0FCB7027951368006C02E2 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB6F27951368006C02E2 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift */; }; - DB0FCB7227952986006C02E2 /* NamingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB7127952986006C02E2 /* NamingState.swift */; }; DB0FCB7427956939006C02E2 /* DataSourceFacade+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB7327956939006C02E2 /* DataSourceFacade+Status.swift */; }; DB0FCB76279571C5006C02E2 /* ThreadViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB75279571C5006C02E2 /* ThreadViewController+DataSourceProvider.swift */; }; DB0FCB7827957678006C02E2 /* DataSourceProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB7727957678006C02E2 /* DataSourceProvider+UITableViewDelegate.swift */; }; @@ -174,14 +160,6 @@ DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; }; DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */; }; DB336F3F278E668C0031E64B /* StatusTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F3E278E668C0031E64B /* StatusTableViewCell+ViewModel.swift */; }; - DB336F41278E68480031E64B /* StatusView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F40278E68480031E64B /* StatusView+Configuration.swift */; }; - DB336F43278EB1690031E64B /* MediaView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F42278EB1680031E64B /* MediaView+Configuration.swift */; }; - DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */; }; - DB36679F268ABAF20027D07F /* ComposeStatusAttachmentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */; }; - DB3667A1268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */; }; - DB3667A4268AE2370027D07F /* ComposeStatusPollTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */; }; - DB3667A6268AE2620027D07F /* ComposeStatusPollSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */; }; - DB3667A8268AE2900027D07F /* ComposeStatusPollItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */; }; DB3E6FDD2806A40F00B035AE /* DiscoveryHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FDC2806A40F00B035AE /* DiscoveryHashtagsViewController.swift */; }; DB3E6FE02806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FDF2806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift */; }; DB3E6FE22806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FE12806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift */; }; @@ -208,8 +186,6 @@ 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 */; }; - DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */; }; - DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.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 */; }; @@ -282,8 +258,6 @@ 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 */; }; - DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729525F9F91600D60309 /* ComposeStatusSection.swift */; }; - DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729B25F9F91F00D60309 /* ComposeStatusItem.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 */; }; @@ -311,14 +285,10 @@ DB6B74FE272FF59000C70B6E /* UserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74FD272FF59000C70B6E /* UserItem.swift */; }; DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */; }; DB6B750427300B4000C70B6E /* TimelineFooterTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */; }; - DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; }; - DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */; }; DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */; }; DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F7C26358ED4008423CD /* SettingsSection.swift */; }; DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F8326358EEC008423CD /* SettingsItem.swift */; }; DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; }; - DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */; }; - DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */; }; DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; }; DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; }; DB7274F4273BB9B200577D95 /* ListBatchFetchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */; }; @@ -353,7 +323,6 @@ DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */; }; DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */; }; DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */; }; - DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */; }; DB98EB4727B0DFAA0082E365 /* ReportStatusViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB4627B0DFAA0082E365 /* ReportStatusViewModel+State.swift */; }; DB98EB4927B0F0CD0082E365 /* ReportStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB4827B0F0CD0082E365 /* ReportStatusTableViewCell.swift */; }; DB98EB4C27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB4B27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift */; }; @@ -387,7 +356,6 @@ DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */; }; DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */; }; DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */; }; - DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */; }; DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; }; DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; }; @@ -408,20 +376,12 @@ 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 */; }; - DBBC24A826A52F9000398BB9 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */; }; - DBBC24AC26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */; }; DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */; }; DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; }; - DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */; }; - DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */; }; - DBBF1DC5265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC4265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift */; }; - DBBF1DC7265251D400E5B703 /* AutoCompleteViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC6265251D400E5B703 /* AutoCompleteViewModel+State.swift */; }; - DBBF1DC92652538500E5B703 /* AutoCompleteSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */; }; - DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */; }; - DBC6461526A170AB00B0E31B /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6461426A170AB00B0E31B /* ShareViewController.swift */; }; + DBC6461526A170AB00B0E31B /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6461426A170AB00B0E31B /* ComposeViewController.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 /* ShareViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6462226A1712000B0E31B /* ShareViewModel.swift */; }; + DBC6462326A1712000B0E31B /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6462226A1712000B0E31B /* ComposeViewModel.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 */; }; @@ -474,14 +434,6 @@ DBFEEC99279BDCDE004F81DD /* ProfileAboutViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEEC98279BDCDE004F81DD /* ProfileAboutViewModel.swift */; }; DBFEEC9B279BDDD9004F81DD /* ProfileAboutViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEEC9A279BDDD9004F81DD /* ProfileAboutViewModel+Diffable.swift */; }; DBFEEC9D279C12C1004F81DD /* ProfileFieldEditCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEEC9C279C12C1004F81DD /* ProfileFieldEditCollectionViewCell.swift */; }; - DBFEF05B26A57715006D7ED1 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05726A576EE006D7ED1 /* ComposeViewModel.swift */; }; - DBFEF05C26A57715006D7ED1 /* StatusEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05526A576EE006D7ED1 /* StatusEditorView.swift */; }; - DBFEF05D26A57715006D7ED1 /* ContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05826A576EE006D7ED1 /* ContentWarningEditorView.swift */; }; - DBFEF05E26A57715006D7ED1 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05626A576EE006D7ED1 /* ComposeView.swift */; }; - DBFEF05F26A57715006D7ED1 /* StatusAuthorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05926A576EE006D7ED1 /* StatusAuthorView.swift */; }; - DBFEF06026A57715006D7ED1 /* StatusAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */; }; - DBFEF06326A577F2006D7ED1 /* StatusAttachmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */; }; - DBFEF06D26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF06C26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -572,7 +524,6 @@ 2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; 2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = ""; }; 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Gesture.swift"; sourceTree = ""; }; - 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = ""; }; 2D35237926256D920031AF25 /* NotificationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSection.swift; sourceTree = ""; }; 2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewController.swift; sourceTree = ""; }; 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModel.swift; sourceTree = ""; }; @@ -588,12 +539,10 @@ 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarProgressView.swift; sourceTree = ""; }; 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewController.swift; sourceTree = ""; }; 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewModel.swift; sourceTree = ""; }; - 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlContainableScrollViews.swift; sourceTree = ""; }; 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+Diffable.swift"; sourceTree = ""; }; 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewContainer.swift; sourceTree = ""; }; 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+DebugAction.swift"; sourceTree = ""; }; 2D607AD726242FC500B70763 /* NotificationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewModel.swift; sourceTree = ""; }; - 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = ""; }; 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningOverlayView.swift; sourceTree = ""; }; 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; 2D76319E25C1521200929FB9 /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = ""; }; @@ -606,14 +555,10 @@ 2D84350425FF858100EECE90 /* UIScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollView.swift; sourceTree = ""; }; 2D939AB425EDD8A90076FA61 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonRegisterViewController+Avatar.swift"; sourceTree = ""; }; - 2DA504682601ADE7008F4E6C /* SawToothView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SawToothView.swift; sourceTree = ""; }; - 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLoaderTableViewCell.swift; sourceTree = ""; }; - 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = ""; }; 2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; 2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountViewController.swift; sourceTree = ""; }; 2DAC9E3D262FC2400062E1A6 /* SuggestionAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountViewModel.swift; sourceTree = ""; }; 2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountTableViewCell.swift; sourceTree = ""; }; - 2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Notification+Type.swift"; sourceTree = ""; }; 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendCollectionHeader.swift; sourceTree = ""; }; 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendAccountSection.swift; sourceTree = ""; }; 2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = ""; }; @@ -638,9 +583,6 @@ 5D0393952612D266007FE196 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = ""; }; 5DA732CB2629CEF500A92342 /* UIView+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Remove.swift"; sourceTree = ""; }; 5DA82A9B4ABDAFA3AB9A49C7 /* Pods-MastodonTests.asdk.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.asdk.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.asdk.xcconfig"; sourceTree = ""; }; - 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Account.swift"; sourceTree = ""; }; - 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Tag.swift"; sourceTree = ""; }; - 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+History.swift"; sourceTree = ""; }; 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = ""; }; 6130CBE4B26E3C976ACC1688 /* Pods-ShareActionExtension.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareActionExtension.asdk - debug.xcconfig"; path = "Target Support Files/Pods-ShareActionExtension/Pods-ShareActionExtension.asdk - debug.xcconfig"; sourceTree = ""; }; 6213AF5928939C8400BCADB6 /* BookmarkViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkViewModel.swift; sourceTree = ""; }; @@ -684,8 +626,6 @@ DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = ""; }; DB03A792272A7E5700EE37C5 /* SidebarListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListHeaderView.swift; sourceTree = ""; }; DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSplitViewController.swift; sourceTree = ""; }; - DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToStatusContentTableViewCell.swift; sourceTree = ""; }; - DB03F7F42689B782007B274C /* ComposeTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTableView.swift; sourceTree = ""; }; DB0617EA277EF3820030EE79 /* GradientBorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientBorderView.swift; sourceTree = ""; }; DB0617EC277F02C50030EE79 /* OnboardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationController.swift; sourceTree = ""; }; DB0617EE277F12720030EE79 /* NavigationActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationActionView.swift; sourceTree = ""; }; @@ -707,8 +647,6 @@ DB0F9D53283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileHeaderView+ViewModel.swift"; sourceTree = ""; }; DB0F9D55283EB46200379AF8 /* ProfileHeaderView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileHeaderView+Configuration.swift"; sourceTree = ""; }; DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Meta.swift"; sourceTree = ""; }; - DB0FCB6F27951368006C02E2 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimelineMiddleLoaderTableViewCell+ViewModel.swift"; sourceTree = ""; }; - DB0FCB7127952986006C02E2 /* NamingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NamingState.swift; sourceTree = ""; }; DB0FCB7327956939006C02E2 /* DataSourceFacade+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status.swift"; sourceTree = ""; }; DB0FCB75279571C5006C02E2 /* ThreadViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewController+DataSourceProvider.swift"; sourceTree = ""; }; DB0FCB7727957678006C02E2 /* DataSourceProvider+UITableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+UITableViewDelegate.swift"; sourceTree = ""; }; @@ -748,14 +686,6 @@ 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 = ""; }; DB336F3E278E668C0031E64B /* StatusTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusTableViewCell+ViewModel.swift"; sourceTree = ""; }; - DB336F40278E68480031E64B /* StatusView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusView+Configuration.swift"; sourceTree = ""; }; - DB336F42278EB1680031E64B /* MediaView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaView+Configuration.swift"; sourceTree = ""; }; - DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentTableViewCell.swift; sourceTree = ""; }; - DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentSection.swift; sourceTree = ""; }; - DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentItem.swift; sourceTree = ""; }; - DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollTableViewCell.swift; sourceTree = ""; }; - DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollSection.swift; sourceTree = ""; }; - DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollItem.swift; sourceTree = ""; }; DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = ""; }; DB3E6FDC2806A40F00B035AE /* DiscoveryHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryHashtagsViewController.swift; sourceTree = ""; }; DB3E6FDF2806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryHashtagsViewModel.swift; sourceTree = ""; }; @@ -789,8 +719,6 @@ 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 = ""; }; - DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerSection.swift; sourceTree = ""; }; - DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerItem.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 = ""; }; @@ -893,8 +821,6 @@ 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 = ""; }; - DB66729525F9F91600D60309 /* ComposeStatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusSection.swift; sourceTree = ""; }; - DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusItem.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 = ""; }; @@ -922,14 +848,10 @@ DB6B74FD272FF59000C70B6E /* UserItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserItem.swift; sourceTree = ""; }; DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTableViewCell.swift; sourceTree = ""; }; DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFooterTableViewCell.swift; sourceTree = ""; }; - DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = ""; }; - DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+API+Subscriptions+Policy.swift"; sourceTree = ""; }; DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationService+Decrypt.swift"; sourceTree = ""; }; DB6D9F7C26358ED4008423CD /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = ""; }; DB6D9F8326358EEC008423CD /* SettingsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItem.swift; sourceTree = ""; }; DB6D9F9626367249008423CD /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; - DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewController.swift; sourceTree = ""; }; - DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTopChevronView.swift; sourceTree = ""; }; DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = ""; }; DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = ""; }; DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListBatchFetchViewModel.swift; sourceTree = ""; }; @@ -976,7 +898,6 @@ DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteThreadViewModel.swift; sourceTree = ""; }; DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+LoadThreadState.swift"; sourceTree = ""; }; DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+Diffable.swift"; sourceTree = ""; }; - DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTopLoaderTableViewCell.swift; sourceTree = ""; }; DB98EB4627B0DFAA0082E365 /* ReportStatusViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportStatusViewModel+State.swift"; sourceTree = ""; }; DB98EB4827B0F0CD0082E365 /* ReportStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusTableViewCell.swift; sourceTree = ""; }; DB98EB4B27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportStatusTableViewCell+ViewModel.swift"; sourceTree = ""; }; @@ -1022,7 +943,6 @@ DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldSection.swift; sourceTree = ""; }; DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldItem.swift; sourceTree = ""; }; DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewCell.swift; sourceTree = ""; }; - DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Field.swift"; sourceTree = ""; }; DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = ""; }; DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = ""; }; DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLAnimatedImageView.swift; sourceTree = ""; }; @@ -1042,20 +962,13 @@ 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 = ""; }; - DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentTableViewCell.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 = ""; }; - DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewModel.swift; sourceTree = ""; }; - DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTableViewCell.swift; sourceTree = ""; }; - DBBF1DC4265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AutoCompleteViewModel+Diffable.swift"; sourceTree = ""; }; - DBBF1DC6265251D400E5B703 /* AutoCompleteViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AutoCompleteViewModel+State.swift"; sourceTree = ""; }; - DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteSection.swift; sourceTree = ""; }; - DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteItem.swift; sourceTree = ""; }; DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - DBC6461426A170AB00B0E31B /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; + 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 /* ShareViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareViewModel.swift; sourceTree = ""; }; + DBC6462226A1712000B0E31B /* ComposeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.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 = ""; }; @@ -1320,8 +1233,6 @@ 2D152A8A25C295B8009AA50C /* Content */ = { isa = PBXGroup; children = ( - DB336F40278E68480031E64B /* StatusView+Configuration.swift */, - DB336F42278EB1680031E64B /* MediaView+Configuration.swift */, DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */, DB0FCB992797F7AD006C02E2 /* UserView+Configuration.swift */, DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */, @@ -1406,7 +1317,6 @@ 2D5A3D0125CF8640002347D6 /* Vender */ = { isa = PBXGroup; children = ( - 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */, DB6180EC26391C6C0018D199 /* TransitioningMath.swift */, DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */, DBF156E32702DB3F00EC00B7 /* HandleTapAction.swift */, @@ -1420,7 +1330,6 @@ isa = PBXGroup; children = ( DB697DD7278F4C34004EF2F7 /* Provider */, - DB0FCB7127952986006C02E2 /* NamingState.swift */, 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */, DB4AA6B227BA34B6009EC082 /* CellFrameCacheContainer.swift */, 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */, @@ -1443,7 +1352,6 @@ DB0FCB892796BE1E006C02E2 /* RecommandAccount */, DB4F097926A039C400D62E92 /* Status */, DB65C63527A2AF52008BAC2E /* Report */, - DB4F097626A0398000D62E92 /* Compose */, DB0617F727855B010030EE79 /* Notification */, DB4F097726A039A200D62E92 /* Search */, DB3E6FE52806A5BA00B035AE /* Discovery */, @@ -1467,7 +1375,6 @@ 2D7631A525C1532D00929FB9 /* View */ = { isa = PBXGroup; children = ( - 2DA504672601ADBA008F4E6C /* Decoration */, 2D42FF8325C82245004A627A /* Button */, DBA9B90325F1D4420012E7B6 /* Control */, 2D152A8A25C295B8009AA50C /* Content */, @@ -1487,11 +1394,6 @@ DB0FCB7D27958957006C02E2 /* StatusThreadRootTableViewCell+ViewModel.swift */, DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */, DB0FCB972797F6BF006C02E2 /* UserTableViewCell+ViewModel.swift */, - 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */, - DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */, - 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */, - 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */, - DB0FCB6F27951368006C02E2 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift */, DBE3CDBA261C427900430CC6 /* TimelineHeaderTableViewCell.swift */, DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */, DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */, @@ -1499,14 +1401,6 @@ path = TableviewCell; sourceTree = ""; }; - 2DA504672601ADBA008F4E6C /* Decoration */ = { - isa = PBXGroup; - children = ( - 2DA504682601ADE7008F4E6C /* SawToothView.swift */, - ); - path = Decoration; - sourceTree = ""; - }; 2DAC9E36262FC20B0062E1A6 /* SuggestionAccount */ = { isa = PBXGroup; children = ( @@ -1638,17 +1532,6 @@ path = Onboarding; sourceTree = ""; }; - DB03F7F1268990A2007B274C /* TableViewCell */ = { - isa = PBXGroup; - children = ( - DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */, - DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */, - DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */, - DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */, - ); - path = TableViewCell; - sourceTree = ""; - }; DB0617F727855B010030EE79 /* Notification */ = { isa = PBXGroup; children = ( @@ -1808,13 +1691,6 @@ path = Discovery; sourceTree = ""; }; - DB3E6FEA2806BD2500B035AE /* MastodonUI */ = { - isa = PBXGroup; - children = ( - ); - path = MastodonUI; - sourceTree = ""; - }; DB3E6FED2806D7FC00B035AE /* News */ = { isa = PBXGroup; children = ( @@ -1934,23 +1810,6 @@ path = SearchResult; sourceTree = ""; }; - DB4F097626A0398000D62E92 /* Compose */ = { - isa = PBXGroup; - children = ( - DB66729525F9F91600D60309 /* ComposeStatusSection.swift */, - DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */, - DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */, - DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */, - DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */, - DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */, - DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */, - DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */, - DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */, - DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */, - ); - path = Compose; - sourceTree = ""; - }; DB4F097726A039A200D62E92 /* Search */ = { isa = PBXGroup; children = ( @@ -2016,7 +1875,6 @@ DB55D32225FB4D320002F825 /* View */ = { isa = PBXGroup; children = ( - DB03F7F42689B782007B274C /* ComposeTableView.swift */, DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */, DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */, DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */, @@ -2260,34 +2118,6 @@ path = Follower; sourceTree = ""; }; - DB6C8C0525F0921200AAA452 /* MastodonSDK */ = { - isa = PBXGroup; - children = ( - DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */, - 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */, - 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */, - 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */, - 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */, - 2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */, - DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */, - DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */, - ); - path = MastodonSDK; - sourceTree = ""; - }; - DB6F5E36264E78EA009108F4 /* AutoComplete */ = { - isa = PBXGroup; - children = ( - DBBF1DC02652402000E5B703 /* View */, - DBBF1DC326524D3100E5B703 /* Cell */, - DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */, - DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */, - DBBF1DC4265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift */, - DBBF1DC6265251D400E5B703 /* AutoCompleteViewModel+State.swift */, - ); - path = AutoComplete; - sourceTree = ""; - }; DB72602125E36A2500235243 /* ServerRules */ = { isa = PBXGroup; children = ( @@ -2312,10 +2142,8 @@ DB789A1025F9F29B0071ACA0 /* Compose */ = { isa = PBXGroup; children = ( - DB6F5E36264E78EA009108F4 /* AutoComplete */, DB55D32225FB4D320002F825 /* View */, DB789A2125F9F76D0071ACA0 /* CollectionViewCell */, - DB03F7F1268990A2007B274C /* TableViewCell */, DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */, DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */, DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */, @@ -2404,8 +2232,6 @@ DB8AF56225C138BC002E6C99 /* Extension */ = { isa = PBXGroup; children = ( - DB3E6FEA2806BD2500B035AE /* MastodonUI */, - DB6C8C0525F0921200AAA452 /* MastodonSDK */, 2DF123A625C3B0210020F248 /* ActiveLabel.swift */, 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */, 2D206B8525F5FB0900143C56 /* Double.swift */, @@ -2700,22 +2526,6 @@ path = Helper; sourceTree = ""; }; - DBBF1DC02652402000E5B703 /* View */ = { - isa = PBXGroup; - children = ( - DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */, - ); - path = View; - sourceTree = ""; - }; - DBBF1DC326524D3100E5B703 /* Cell */ = { - isa = PBXGroup; - children = ( - DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */, - ); - path = Cell; - sourceTree = ""; - }; DBC6461326A170AB00B0E31B /* ShareActionExtension */ = { isa = PBXGroup; children = ( @@ -2897,8 +2707,8 @@ isa = PBXGroup; children = ( DBFEF05426A576EE006D7ED1 /* View */, - DBC6462226A1712000B0E31B /* ShareViewModel.swift */, - DBC6461426A170AB00B0E31B /* ShareViewController.swift */, + DBC6462226A1712000B0E31B /* ComposeViewModel.swift */, + DBC6461426A170AB00B0E31B /* ComposeViewController.swift */, ); path = Scene; sourceTree = ""; @@ -3347,19 +3157,14 @@ DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */, DB5B54B22833C24B00DEF8B2 /* RebloggedByViewController+DataSourceProvider.swift in Sources */, 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */, - DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */, - DBBF1DC7265251D400E5B703 /* AutoCompleteViewModel+State.swift in Sources */, DB03A793272A7E5700EE37C5 /* SidebarListHeaderView.swift in Sources */, DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */, DB5B7298273112C800081888 /* FollowingListViewModel.swift in Sources */, 0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */, DB5B54AE2833C15F00DEF8B2 /* UserListViewModel+Diffable.swift in Sources */, - DB336F43278EB1690031E64B /* MediaView+Configuration.swift in Sources */, - DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */, DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */, DB3E6FE02806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift in Sources */, 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */, - DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */, DB0FCB7427956939006C02E2 /* DataSourceFacade+Status.swift in Sources */, DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */, DB63F75A279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift in Sources */, @@ -3395,13 +3200,11 @@ DB697DD9278F4CED004EF2F7 /* HomeTimelineViewController+DataSourceProvider.swift in Sources */, DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */, DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */, - 2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */, 2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */, DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */, DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */, 62FD27D12893707600B205C5 /* BookmarkViewController.swift in Sources */, DBD5B1F627BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */, - 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */, DB63F767279A5EB300455B82 /* NotificationTimelineViewModel.swift in Sources */, 2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */, DB5B54AB2833C12A00DEF8B2 /* RebloggedByViewController.swift in Sources */, @@ -3447,7 +3250,6 @@ DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */, 2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */, 2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */, - DB3667A8268AE2900027D07F /* ComposeStatusPollItem.swift in Sources */, DB603113279EBEBA00A935FE /* DataSourceFacade+Block.swift in Sources */, DB63F777279A9A2A00455B82 /* NotificationView+Configuration.swift in Sources */, DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */, @@ -3457,7 +3259,6 @@ DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */, DB938EE62623F50700E5B6C1 /* ThreadViewController.swift in Sources */, DB6180F426391D110018D199 /* MediaPreviewImageView.swift in Sources */, - DB336F41278E68480031E64B /* StatusView+Configuration.swift in Sources */, DBF9814A265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift in Sources */, 2D939AB525EDD8A90076FA61 /* String.swift in Sources */, DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */, @@ -3470,12 +3271,9 @@ DB5B549F2833A72500DEF8B2 /* FamiliarFollowersViewModel+Diffable.swift in Sources */, DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */, DB8F7076279E954700E1225B /* DataSourceFacade+Follow.swift in Sources */, - DB36679F268ABAF20027D07F /* ComposeStatusAttachmentSection.swift in Sources */, - 2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */, DB63F7542799491600455B82 /* DataSourceFacade+SearchHistory.swift in Sources */, DB7A9F912818EAF10016AF98 /* MastodonRegisterView.swift in Sources */, DBF1572F27046F1A00EC00B7 /* SecondaryPlaceholderViewController.swift in Sources */, - DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */, 2D4AD8A826316D3500613EFC /* SelectedAccountItem.swift in Sources */, DBE3CDFB261C6CA500430CC6 /* FavoriteViewModel.swift in Sources */, DBE3CE01261D623D00430CC6 /* FavoriteViewModel+State.swift in Sources */, @@ -3487,7 +3285,6 @@ DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */, DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */, DB3EA8EB281B7E0700598866 /* DiscoveryCommunityViewModel+State.swift in Sources */, - DB0FCB7227952986006C02E2 /* NamingState.swift in Sources */, DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */, DB98EB5327B0F9890082E365 /* ReportHeadlineTableViewCell.swift in Sources */, DB5B729C273113C200081888 /* FollowingListViewModel+Diffable.swift in Sources */, @@ -3496,14 +3293,12 @@ DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */, DB5B729E273113F300081888 /* FollowingListViewModel+State.swift in Sources */, 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */, - 5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */, DBF9814C265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift in Sources */, DB63F76227996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift in Sources */, DB73BF4927140BA300781945 /* UICollectionViewDiffableDataSource.swift in Sources */, DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */, DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */, DB63F7492799126300455B82 /* FollowerListViewController+DataSourceProvider.swift in Sources */, - DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */, 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */, 2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */, DB66728C25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift in Sources */, @@ -3529,14 +3324,12 @@ DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */, 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */, DBDFF1932805554900557A48 /* DiscoveryPostsViewModel.swift in Sources */, - DBBF1DC92652538500E5B703 /* AutoCompleteSection.swift in Sources */, DB3E6FE72806A7A200B035AE /* DiscoveryItem.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, DBEFCD79282A147000C0ABEA /* ReportStatusViewModel.swift in Sources */, DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */, DB0A322E280EE9FD001729D2 /* DiscoveryIntroBannerView.swift in Sources */, 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */, - 5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */, 5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */, 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */, DB0FCB7827957678006C02E2 /* DataSourceProvider+UITableViewDelegate.swift in Sources */, @@ -3550,12 +3343,9 @@ 0F20222D261457EE000C64BF /* HashtagTimelineViewModel+State.swift in Sources */, DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */, 5B90C462262599800002E742 /* SettingsSectionHeader.swift in Sources */, - DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */, 5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */, DB3E6FEF2806D82600B035AE /* DiscoveryNewsViewModel.swift in Sources */, - DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */, DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */, - DB3667A6268AE2620027D07F /* ComposeStatusPollSection.swift in Sources */, DB6B750427300B4000C70B6E /* TimelineFooterTableViewCell.swift in Sources */, DB98EB4C27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift in Sources */, DB852D1F26FB037800FC9D81 /* SidebarViewModel.swift in Sources */, @@ -3568,7 +3358,6 @@ DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */, DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */, 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */, - DB3667A1268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift in Sources */, DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */, DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */, DBCA0EBC282BB38A0029E2B0 /* PageboyNavigateable.swift in Sources */, @@ -3587,11 +3376,8 @@ DB023D2C27A10464005AC798 /* NotificationTimelineViewController+DataSourceProvider.swift in Sources */, DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */, DBF1D257269DBAC600C1C08A /* SearchDetailViewModel.swift in Sources */, - DB03F7F52689B782007B274C /* ComposeTableView.swift in Sources */, DBB45B5927B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift in Sources */, - DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */, DB0FCB76279571C5006C02E2 /* ThreadViewController+DataSourceProvider.swift in Sources */, - DB0FCB7027951368006C02E2 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift in Sources */, DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */, DB7274F4273BB9B200577D95 /* ListBatchFetchViewModel.swift in Sources */, DB0618052785A73D0030EE79 /* RegisterItem.swift in Sources */, @@ -3605,7 +3391,6 @@ DB3E6FDD2806A40F00B035AE /* DiscoveryHashtagsViewController.swift in Sources */, DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */, DB6988DE2848D11C002398EF /* PagerTabStripNavigateable.swift in Sources */, - DBBC24AC26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */, 2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */, DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */, DB697DD1278F4871004EF2F7 /* AutoGenerateTableViewDelegate.swift in Sources */, @@ -3619,7 +3404,6 @@ DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */, DB63F779279ABF9C00455B82 /* DataSourceFacade+Reblog.swift in Sources */, DB4F0963269ED06300D62E92 /* SearchResultViewController.swift in Sources */, - DBBF1DC5265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift in Sources */, DB603111279EB38500A935FE /* DataSourceFacade+Mute.swift in Sources */, DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */, 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */, @@ -3636,7 +3420,6 @@ DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */, DBF156E22702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m in Sources */, 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */, - DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */, DB697DE1278F5296004EF2F7 /* DataSourceFacade+Model.swift in Sources */, DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */, DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */, @@ -3650,9 +3433,7 @@ DB9F58F126EF512300E7BBE9 /* AccountListTableViewCell.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */, - 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */, DBA5A53526F0A36A00CACBAA /* AddAccountTableViewCell.swift in Sources */, - 2DB72C8C262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift in Sources */, 2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */, 2D4AD89C263165B500613EFC /* SuggestionAccountCollectionViewCell.swift in Sources */, DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */, @@ -3672,8 +3453,6 @@ DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */, DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */, DBEFCD74282A130400C0ABEA /* ReportReasonViewModel.swift in Sources */, - 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */, - DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */, DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */, DBFEEC96279BDC67004F81DD /* ProfileAboutViewController.swift in Sources */, DB63F74F2799405600455B82 /* SearchHistoryViewModel+Diffable.swift in Sources */, @@ -3689,10 +3468,8 @@ DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */, DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */, DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */, - 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */, DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */, DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */, - DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */, DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */, 2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */, DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */, @@ -3708,7 +3485,6 @@ DB3E6FE42806A5B800B035AE /* DiscoverySection.swift in Sources */, DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */, DB697DDB278F4DE3004EF2F7 /* DataSourceProvider+StatusTableViewCellDelegate.swift in Sources */, - 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */, DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */, DBB45B5627B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift in Sources */, DB0FCB8027968F70006C02E2 /* MastodonStatusThreadViewModel.swift in Sources */, @@ -3724,7 +3500,6 @@ DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */, DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */, DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */, - DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */, DB6746F0278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift in Sources */, DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */, DBDFF19A28055A1400557A48 /* DiscoveryViewController.swift in Sources */, @@ -3738,10 +3513,7 @@ DB63F74B279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift in Sources */, DBEFCD7D282A2A3B00C0ABEA /* ReportServerRulesViewController.swift in Sources */, DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */, - DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */, DB98EB4927B0F0CD0082E365 /* ReportStatusTableViewCell.swift in Sources */, - DB3667A4268AE2370027D07F /* ComposeStatusPollTableViewCell.swift in Sources */, - DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */, DBF3B7412733EB9400E21627 /* MastodonLocalCode.swift in Sources */, DB98EB6527B216500082E365 /* ReportResultViewModel.swift in Sources */, DB4F096A269EDAD200D62E92 /* SearchResultViewModel+State.swift in Sources */, @@ -3751,7 +3523,6 @@ DB023D2A27A0FE5C005AC798 /* DataSourceProvider+NotificationTableViewCellDelegate.swift in Sources */, DB98EB6027B10E150082E365 /* ReportCommentTableViewCell.swift in Sources */, DB0FCB962797E6C2006C02E2 /* SearchResultViewController+DataSourceProvider.swift in Sources */, - DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */, DB6180E326391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift in Sources */, DBD5B1F827BCFD9D00BD6B38 /* DataSourceProvider+TableViewControllerNavigateable.swift in Sources */, 0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */, @@ -3796,18 +3567,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DBFEF05E26A57715006D7ED1 /* ComposeView.swift in Sources */, - DBFEF06026A57715006D7ED1 /* StatusAttachmentView.swift in Sources */, - DBC6462326A1712000B0E31B /* ShareViewModel.swift in Sources */, - DBFEF06D26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift in Sources */, - DBFEF05F26A57715006D7ED1 /* StatusAuthorView.swift in Sources */, - DBFEF05D26A57715006D7ED1 /* ContentWarningEditorView.swift in Sources */, - DBFEF05C26A57715006D7ED1 /* StatusEditorView.swift in Sources */, + DBC6462326A1712000B0E31B /* ComposeViewModel.swift in Sources */, DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */, - DBBC24A826A52F9000398BB9 /* ComposeToolbarView.swift in Sources */, - DBFEF05B26A57715006D7ED1 /* ComposeViewModel.swift in Sources */, - DBFEF06326A577F2006D7ED1 /* StatusAttachmentViewModel.swift in Sources */, - DBC6461526A170AB00B0E31B /* ShareViewController.swift in Sources */, + DBC6461526A170AB00B0E31B /* ComposeViewController.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 21906eb03..33eda54f1 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -112,12 +112,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 25 + 18 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 24 + 17 SuppressBuildableAutocreation diff --git a/Mastodon/Diffiable/Compose/CustomEmojiPickerSection.swift b/Mastodon/Diffiable/Compose/CustomEmojiPickerSection.swift deleted file mode 100644 index e55e8849d..000000000 --- a/Mastodon/Diffiable/Compose/CustomEmojiPickerSection.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// CustomEmojiPickerSection.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-3-24. -// - -import UIKit - -enum CustomEmojiPickerSection: Equatable, Hashable { - case emoji(name: String) -} - -extension CustomEmojiPickerSection { - static func collectionViewDiffableDataSource( - for 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 - } -} diff --git a/Mastodon/Diffiable/Notification/NotificationSection.swift b/Mastodon/Diffiable/Notification/NotificationSection.swift index a67d407ee..387affbc7 100644 --- a/Mastodon/Diffiable/Notification/NotificationSection.swift +++ b/Mastodon/Diffiable/Notification/NotificationSection.swift @@ -15,6 +15,7 @@ import MetaTextKit import MastodonMeta import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization enum NotificationSection: Equatable, Hashable { diff --git a/Mastodon/Diffiable/Report/ReportSection.swift b/Mastodon/Diffiable/Report/ReportSection.swift index b815975b0..ba3c5525a 100644 --- a/Mastodon/Diffiable/Report/ReportSection.swift +++ b/Mastodon/Diffiable/Report/ReportSection.swift @@ -14,6 +14,7 @@ import UIKit import os.log import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization enum ReportSection: Equatable, Hashable { diff --git a/Mastodon/Diffiable/User/UserSection.swift b/Mastodon/Diffiable/User/UserSection.swift index 6bb402b3a..20812b7e8 100644 --- a/Mastodon/Diffiable/User/UserSection.swift +++ b/Mastodon/Diffiable/User/UserSection.swift @@ -10,8 +10,9 @@ import UIKit import CoreData import CoreDataStack import MastodonCore -import MetaTextKit +import MastodonUI import MastodonMeta +import MetaTextKit enum UserSection: Hashable { case main diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift deleted file mode 100644 index 09bbb3d8a..000000000 --- a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// Mastodon+Entity+Account.swift -// Mastodon -// -// Created by xiaojian sun on 2021/4/2. -// - -import UIKit -import MastodonSDK -import MastodonMeta - -extension Mastodon.Entity.Account: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - } - - public static func == (lhs: Mastodon.Entity.Account, rhs: Mastodon.Entity.Account) -> Bool { - return lhs.id == rhs.id - } -} - -extension Mastodon.Entity.Account { - - var displayNameWithFallback: String { - return !displayName.isEmpty ? displayName : username - } - -} - -extension Mastodon.Entity.Account { - public func avatarImageURL() -> URL? { - let string = UserDefaults.shared.preferredStaticAvatar ? avatarStatic ?? avatar : avatar - return URL(string: string) - } - - public func avatarImageURLWithFallback(domain: String) -> URL { - return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")! - } -} - -extension Mastodon.Entity.Account { - var emojiMeta: MastodonContent.Emojis { - let isAnimated = !UserDefaults.shared.preferredStaticEmoji - - var dict = MastodonContent.Emojis() - for emoji in emojis ?? [] { - dict[emoji.shortcode] = isAnimated ? emoji.url : emoji.staticURL - } - return dict - } -} diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Tag.swift b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Tag.swift deleted file mode 100644 index 6251d1814..000000000 --- a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Tag.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Mastodon+Entity+Tag.swift -// Mastodon -// -// Created by xiaojian sun on 2021/4/2. -// - -import MastodonSDK - -//extension Mastodon.Entity.Tag: Hashable { -// public func hash(into hasher: inout Hasher) { -// hasher.combine(name) -// } -// -// public static func == (lhs: Mastodon.Entity.Tag, rhs: Mastodon.Entity.Tag) -> Bool { -// return lhs.name == rhs.name -// } -//} diff --git a/Mastodon/Protocol/NamingState.swift b/Mastodon/Protocol/NamingState.swift deleted file mode 100644 index edf6265e8..000000000 --- a/Mastodon/Protocol/NamingState.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// NamingState.swift -// Mastodon -// -// Created by MainasuK on 2022-1-17. -// - -import Foundation - -protocol NamingState { - var name: String { get } -} diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index aecfe6a8d..53755e00a 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -103,8 +103,8 @@ extension DataSourceFacade { let composeViewModel = ComposeViewModel( context: provider.context, - composeKind: .reply(status: status), - authContext: provider.authContext + authContext: provider.authContext, + kind: .reply(status: status) ) _ = provider.coordinator.present( scene: .compose(viewModel: composeViewModel), diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift index 390f246d4..e7b55f91c 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift @@ -99,10 +99,10 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid let composeViewModel = ComposeViewModel( context: self.context, - composeKind: .reply(status: status), - authContext: authContext + authContext: authContext, + kind: .reply(status: status) ) - self.coordinator.present( + _ = self.coordinator.present( scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil) diff --git a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+Diffable.swift b/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+Diffable.swift deleted file mode 100644 index 742188726..000000000 --- a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+Diffable.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// AutoCompleteViewModel+Diffable.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-5-17. -// - -import UIKit - -extension AutoCompleteViewModel { - - func setupDiffableDataSource( - for tableView: UITableView - ) { - diffableDataSource = AutoCompleteSection.tableViewDiffableDataSource(for: tableView) - - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - diffableDataSource?.apply(snapshot) - } - -} diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift index ac32129cc..e5b043adb 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift @@ -13,66 +13,66 @@ import MastodonCore import MastodonUI import MastodonLocalization -protocol ComposeStatusPollExpiresOptionCollectionViewCellDelegate: AnyObject { - func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption) -} - -final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCell { - - var disposeBag = Set() - weak var delegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate? - - let durationButton: UIButton = { - let button = HighlightDimmableButton() - button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12)) - button.expandEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: -20, right: -20) - button.setTitle(L10n.Scene.Compose.Poll.durationTime(L10n.Scene.Compose.Poll.thirtyMinutes), for: .normal) - button.setTitleColor(Asset.Colors.brand.color, for: .normal) - return button - }() - - override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension ComposeStatusPollExpiresOptionCollectionViewCell { - - private typealias ExpiresOption = ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption - - private func _init() { - durationButton.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(durationButton) - NSLayoutConstraint.activate([ - durationButton.topAnchor.constraint(equalTo: contentView.topAnchor), - durationButton.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor, constant: PollOptionView.checkmarkBackgroundLeadingMargin), - durationButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) - - let children = ExpiresOption.allCases.map { expiresOption -> UIAction in - UIAction(title: expiresOption.title, image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in - guard let self = self else { return } - self.expiresOptionActionHandler(action, expiresOption: expiresOption) - } - } - durationButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children) - durationButton.showsMenuAsPrimaryAction = true - } - -} - -extension ComposeStatusPollExpiresOptionCollectionViewCell { - - private func expiresOptionActionHandler(_ sender: UIAction, expiresOption: ExpiresOption) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, expiresOption.title) - delegate?.composeStatusPollExpiresOptionCollectionViewCell(self, didSelectExpiresOption: expiresOption) - } - -} +//protocol ComposeStatusPollExpiresOptionCollectionViewCellDelegate: AnyObject { +// func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption) +//} +// +//final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCell { +// +// var disposeBag = Set() +// weak var delegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate? +// +// let durationButton: UIButton = { +// let button = HighlightDimmableButton() +// button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12)) +// button.expandEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: -20, right: -20) +// button.setTitle(L10n.Scene.Compose.Poll.durationTime(L10n.Scene.Compose.Poll.thirtyMinutes), for: .normal) +// button.setTitleColor(Asset.Colors.brand.color, for: .normal) +// return button +// }() +// +// override init(frame: CGRect) { +// super.init(frame: frame) +// _init() +// } +// +// required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } +// +//} +// +//extension ComposeStatusPollExpiresOptionCollectionViewCell { +// +// private typealias ExpiresOption = ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption +// +// private func _init() { +// durationButton.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(durationButton) +// NSLayoutConstraint.activate([ +// durationButton.topAnchor.constraint(equalTo: contentView.topAnchor), +// durationButton.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor, constant: PollOptionView.checkmarkBackgroundLeadingMargin), +// durationButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// ]) +// +// let children = ExpiresOption.allCases.map { expiresOption -> UIAction in +// UIAction(title: expiresOption.title, image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in +// guard let self = self else { return } +// self.expiresOptionActionHandler(action, expiresOption: expiresOption) +// } +// } +// durationButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children) +// durationButton.showsMenuAsPrimaryAction = true +// } +// +//} +// +//extension ComposeStatusPollExpiresOptionCollectionViewCell { +// +// private func expiresOptionActionHandler(_ sender: UIAction, expiresOption: ExpiresOption) { +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, expiresOption.title) +// delegate?.composeStatusPollExpiresOptionCollectionViewCell(self, didSelectExpiresOption: expiresOption) +// } +// +//} diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index b9605bb78..1fd3b0670 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -30,56 +30,65 @@ final class ComposeViewController: UIViewController, NeedsDependency { let logger = Logger(subsystem: "ComposeViewController", category: "logic") - 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 + lazy var composeContentViewModel: ComposeContentViewModel = { + return ComposeContentViewModel(context: context, kind: viewModel.kind) }() - private(set) lazy var characterCountBarButtonItem: UIBarButtonItem = { - let barButtonItem = UIBarButtonItem(customView: characterCountLabel) - return barButtonItem + private(set) lazy var composeContentViewController: ComposeContentViewController = { + let composeContentViewController = ComposeContentViewController() + composeContentViewController.viewModel = composeContentViewModel + return composeContentViewController }() - 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(set) lazy var publishBarButtonItem: UIBarButtonItem = { - configurePublishButtonApperance() - let shadowBackgroundContainer = ShadowBackgroundContainer() - publishButton.translatesAutoresizingMaskIntoConstraints = false - shadowBackgroundContainer.addSubview(publishButton) - NSLayoutConstraint.activate([ - publishButton.topAnchor.constraint(equalTo: shadowBackgroundContainer.topAnchor), - publishButton.leadingAnchor.constraint(equalTo: shadowBackgroundContainer.leadingAnchor), - publishButton.trailingAnchor.constraint(equalTo: shadowBackgroundContainer.trailingAnchor), - publishButton.bottomAnchor.constraint(equalTo: shadowBackgroundContainer.bottomAnchor), - ]) - let barButtonItem = UIBarButtonItem(customView: shadowBackgroundContainer) - return barButtonItem - }() - - 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(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 +// }() +// +// 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(set) lazy var publishBarButtonItem: UIBarButtonItem = { +// configurePublishButtonApperance() +// let shadowBackgroundContainer = ShadowBackgroundContainer() +// publishButton.translatesAutoresizingMaskIntoConstraints = false +// shadowBackgroundContainer.addSubview(publishButton) +// NSLayoutConstraint.activate([ +// publishButton.topAnchor.constraint(equalTo: shadowBackgroundContainer.topAnchor), +// publishButton.leadingAnchor.constraint(equalTo: shadowBackgroundContainer.leadingAnchor), +// publishButton.trailingAnchor.constraint(equalTo: shadowBackgroundContainer.trailingAnchor), +// publishButton.bottomAnchor.constraint(equalTo: shadowBackgroundContainer.bottomAnchor), +// ]) +// let barButtonItem = UIBarButtonItem(customView: shadowBackgroundContainer) +// return barButtonItem +// }() +// +// 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) +// } - let scrollView: UIScrollView = { - let scrollView = UIScrollView() - scrollView.alwaysBounceVertical = true - return scrollView - }() +// let scrollView: UIScrollView = { +// let scrollView = UIScrollView() +// scrollView.alwaysBounceVertical = true +// return scrollView +// }() // let tableView: ComposeTableView = { // let tableView = ComposeTableView() @@ -92,56 +101,56 @@ final class ComposeViewController: UIViewController, NeedsDependency { // return tableView // }() - var systemKeyboardHeight: CGFloat = .zero { - didSet { - // note: some system AutoLayout warning here - let height = max(300, systemKeyboardHeight) - customEmojiPickerInputView.frame.size.height = height - } - } - - // 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! - let composeToolbarBackgroundView = UIView() - - static func createPhotoLibraryPickerConfiguration(selectionLimit: Int = 4) -> PHPickerConfiguration { - var configuration = PHPickerConfiguration() - configuration.filter = .any(of: [.images, .videos]) - configuration.selectionLimit = selectionLimit - return configuration - } - - private(set) lazy var photoLibraryPicker: PHPickerViewController = { - let imagePicker = PHPickerViewController(configuration: ComposeViewController.createPhotoLibraryPickerConfiguration()) - imagePicker.delegate = self - return imagePicker - }() - private(set) lazy var imagePickerController: UIImagePickerController = { - let imagePickerController = UIImagePickerController() - imagePickerController.sourceType = .camera - imagePickerController.delegate = self - return imagePickerController - }() - - private(set) lazy var documentPickerController: UIDocumentPickerViewController = { - let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image, .movie]) - documentPickerController.delegate = self - return documentPickerController - }() - - 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 - }() +// var systemKeyboardHeight: CGFloat = .zero { +// didSet { +// // note: some system AutoLayout warning here +// let height = max(300, systemKeyboardHeight) +// customEmojiPickerInputView.frame.size.height = height +// } +// } +// +// // 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! +// let composeToolbarBackgroundView = UIView() +// +// static func createPhotoLibraryPickerConfiguration(selectionLimit: Int = 4) -> PHPickerConfiguration { +// var configuration = PHPickerConfiguration() +// configuration.filter = .any(of: [.images, .videos]) +// configuration.selectionLimit = selectionLimit +// return configuration +// } +// +// private(set) lazy var photoLibraryPicker: PHPickerViewController = { +// let imagePicker = PHPickerViewController(configuration: ComposeViewController.createPhotoLibraryPickerConfiguration()) +// imagePicker.delegate = self +// return imagePicker +// }() +// private(set) lazy var imagePickerController: UIImagePickerController = { +// let imagePickerController = UIImagePickerController() +// imagePickerController.sourceType = .camera +// imagePickerController.delegate = self +// return imagePickerController +// }() +// +// private(set) lazy var documentPickerController: UIDocumentPickerViewController = { +// let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image, .movie]) +// documentPickerController.delegate = self +// return documentPickerController +// }() +// +// 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) @@ -167,77 +176,88 @@ extension ComposeViewController { override func viewDidLoad() { super.viewDidLoad() - - 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) - self.setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) - ThemeService.shared.currentTheme - .receive(on: RunLoop.main) - .sink { [weak self] theme in - guard let self = self else { return } - self.setupBackgroundColor(theme: theme) - } - .store(in: &disposeBag) - navigationItem.leftBarButtonItem = cancelBarButtonItem - navigationItem.rightBarButtonItem = publishBarButtonItem - viewModel.traitCollectionDidChangePublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - 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) - } - self.navigationItem.rightBarButtonItems = items - } - .store(in: &disposeBag) - publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) + 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) - - scrollView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(scrollView) - NSLayoutConstraint.activate([ - scrollView.topAnchor.constraint(equalTo: view.topAnchor), - scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - - 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), - ]) +// 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) +// self.setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) +// ThemeService.shared.currentTheme +// .receive(on: RunLoop.main) +// .sink { [weak self] theme in +// guard let self = self else { return } +// self.setupBackgroundColor(theme: theme) +// } +// .store(in: &disposeBag) +// navigationItem.leftBarButtonItem = cancelBarButtonItem +// navigationItem.rightBarButtonItem = publishBarButtonItem +// viewModel.traitCollectionDidChangePublisher +// .receive(on: DispatchQueue.main) +// .sink { [weak self] _ in +// 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) +// } +// self.navigationItem.rightBarButtonItems = items +// } +// .store(in: &disposeBag) +// publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) +// +// +// scrollView.translatesAutoresizingMaskIntoConstraints = false +// view.addSubview(scrollView) +// NSLayoutConstraint.activate([ +// scrollView.topAnchor.constraint(equalTo: view.topAnchor), +// scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), +// scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), +// scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), +// ]) +// +// 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), +// ]) // tableView.delegate = self // viewModel.setupDataSource( @@ -558,14 +578,14 @@ extension ComposeViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - viewModel.isViewAppeared = true +// viewModel.isViewAppeared = true } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) - configurePublishButtonApperance() - viewModel.traitCollectionDidChangePublisher.send() +// configurePublishButtonApperance() +// viewModel.traitCollectionDidChangePublisher.send() } override func viewDidLayoutSubviews() { @@ -576,508 +596,508 @@ extension ComposeViewController { 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 { - - 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 - } - - 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 - } - } - - private func configureNavigationBarTitleStyle() { - switch traitCollection.userInterfaceIdiom { - case .pad: - navigationController?.navigationBar.prefersLargeTitles = traitCollection.horizontalSizeClass == .regular - default: - break - } - } - -} - -extension ComposeViewController { - - @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - guard viewModel.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) - assertionFailure() - - dismiss(animated: true, completion: nil) - } - -} - -// 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 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 { - 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, mediaButtonDidPressed sender: Any, mediaSelectionType type: ComposeToolbarView.MediaSelectionType) { - switch type { - case .photoLibrary: - present(photoLibraryPicker, animated: true, completion: nil) - case .camera: - present(imagePickerController, animated: true, completion: nil) - case .browse: - #if SNAPSHOT - guard let image = UIImage(named: "Athens") else { return } - - let attachmentService = MastodonAttachmentService( - context: context, - image: image, - initialAuthenticationBox: viewModel.authenticationBox - ) - viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService] - #else - present(documentPickerController, animated: true, completion: nil) - #endif - } - } - - 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: - UIScrollViewDelegate -extension ComposeViewController { -// func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { -// guard scrollView === tableView else { return } -// -// let repliedToCellFrame = viewModel.repliedToCellFrame -// guard repliedToCellFrame != .zero else { return } -// -// // try to find some patterns: -// // print(""" -// // repliedToCellFrame: \(viewModel.repliedToCellFrame.value.height) -// // scrollView.contentOffset.y: \(scrollView.contentOffset.y) -// // scrollView.contentSize.height: \(scrollView.contentSize.height) -// // scrollView.frame: \(scrollView.frame) -// // scrollView.adjustedContentInset.top: \(scrollView.adjustedContentInset.top) -// // scrollView.adjustedContentInset.bottom: \(scrollView.adjustedContentInset.bottom) -// // """) -// -// switch viewModel.collectionViewState { -// case .fold: -// os_log("%{public}s[%{public}ld], %{public}s: fold", ((#file as NSString).lastPathComponent), #line, #function) -// guard velocity.y < 0 else { return } -// let offsetY = scrollView.contentOffset.y + scrollView.adjustedContentInset.top -// if offsetY < -44 { -// tableView.contentInset.top = 0 -// targetContentOffset.pointee = CGPoint(x: 0, y: -scrollView.adjustedContentInset.top) -// viewModel.collectionViewState = .expand +// 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 { +// +// 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 +// } // -// case .expand: -// os_log("%{public}s[%{public}ld], %{public}s: expand", ((#file as NSString).lastPathComponent), #line, #function) -// guard velocity.y > 0 else { return } -// // check if top across -// let topOffset = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) - repliedToCellFrame.height +// 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 +// } // -// // check if bottom bounce -// let bottomOffsetY = scrollView.contentOffset.y + (scrollView.frame.height - scrollView.adjustedContentInset.bottom) -// let bottomOffset = bottomOffsetY - scrollView.contentSize.height +// guard let item = firstPollItem else { +// return nil +// } // -// if topOffset > 44 { -// // do not interrupt user scrolling -// viewModel.collectionViewState = .fold -// } else if bottomOffset > 44 { -// tableView.contentInset.top = -repliedToCellFrame.height -// targetContentOffset.pointee = CGPoint(x: 0, y: -repliedToCellFrame.height) -// viewModel.collectionViewState = .fold +// 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 +// } +// +// 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 // } // } -} - -// 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 - } - } -} +// +// private func configureNavigationBarTitleStyle() { +// switch traitCollection.userInterfaceIdiom { +// case .pad: +// navigationController?.navigationBar.prefersLargeTitles = traitCollection.horizontalSizeClass == .regular +// default: +// break +// } +// } +// +//} +// +//extension ComposeViewController { +// +// @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) { +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) +// guard viewModel.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) +// assertionFailure() +// +// dismiss(animated: true, completion: nil) +// } +// +//} +// +//// 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 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 { +// 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, mediaButtonDidPressed sender: Any, mediaSelectionType type: ComposeToolbarView.MediaSelectionType) { +// switch type { +// case .photoLibrary: +// present(photoLibraryPicker, animated: true, completion: nil) +// case .camera: +// present(imagePickerController, animated: true, completion: nil) +// case .browse: +// #if SNAPSHOT +// guard let image = UIImage(named: "Athens") else { return } +// +// let attachmentService = MastodonAttachmentService( +// context: context, +// image: image, +// initialAuthenticationBox: viewModel.authenticationBox +// ) +// viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService] +// #else +// present(documentPickerController, animated: true, completion: nil) +// #endif +// } +// } +// +// 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: - UIScrollViewDelegate +//extension ComposeViewController { +//// func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { +//// guard scrollView === tableView else { return } +//// +//// let repliedToCellFrame = viewModel.repliedToCellFrame +//// guard repliedToCellFrame != .zero else { return } +//// +//// // try to find some patterns: +//// // print(""" +//// // repliedToCellFrame: \(viewModel.repliedToCellFrame.value.height) +//// // scrollView.contentOffset.y: \(scrollView.contentOffset.y) +//// // scrollView.contentSize.height: \(scrollView.contentSize.height) +//// // scrollView.frame: \(scrollView.frame) +//// // scrollView.adjustedContentInset.top: \(scrollView.adjustedContentInset.top) +//// // scrollView.adjustedContentInset.bottom: \(scrollView.adjustedContentInset.bottom) +//// // """) +//// +//// switch viewModel.collectionViewState { +//// case .fold: +//// os_log("%{public}s[%{public}ld], %{public}s: fold", ((#file as NSString).lastPathComponent), #line, #function) +//// guard velocity.y < 0 else { return } +//// let offsetY = scrollView.contentOffset.y + scrollView.adjustedContentInset.top +//// if offsetY < -44 { +//// tableView.contentInset.top = 0 +//// targetContentOffset.pointee = CGPoint(x: 0, y: -scrollView.adjustedContentInset.top) +//// viewModel.collectionViewState = .expand +//// } +//// +//// case .expand: +//// os_log("%{public}s[%{public}ld], %{public}s: expand", ((#file as NSString).lastPathComponent), #line, #function) +//// guard velocity.y > 0 else { return } +//// // check if top across +//// let topOffset = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) - repliedToCellFrame.height +//// +//// // check if bottom bounce +//// let bottomOffsetY = scrollView.contentOffset.y + (scrollView.frame.height - scrollView.adjustedContentInset.bottom) +//// let bottomOffset = bottomOffsetY - scrollView.contentSize.height +//// +//// if topOffset > 44 { +//// // do not interrupt user scrolling +//// viewModel.collectionViewState = .fold +//// } else if bottomOffset > 44 { +//// tableView.contentInset.top = -repliedToCellFrame.height +//// targetContentOffset.pointee = CGPoint(x: 0, y: -repliedToCellFrame.height) +//// viewModel.collectionViewState = .fold +//// } +//// } +//// } +//} +// +//// 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 { @@ -1091,15 +1111,14 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate { } } - func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { - return viewModel.shouldDismiss - } +// 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 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) @@ -1107,357 +1126,357 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate { } -// MARK: - PHPickerViewControllerDelegate -extension ComposeViewController: PHPickerViewControllerDelegate { - func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - picker.dismiss(animated: true, completion: nil) - - let attachmentServices: [MastodonAttachmentService] = results.map { result in - let service = MastodonAttachmentService( - context: context, - pickerResult: result, - initialAuthenticationBox: viewModel.authenticationBox - ) - return service - } - viewModel.attachmentServices = viewModel.attachmentServices + attachmentServices - } -} - -// MARK: - UIImagePickerControllerDelegate -extension ComposeViewController: UIImagePickerControllerDelegate & UINavigationControllerDelegate { - - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { - picker.dismiss(animated: true, completion: nil) - - guard let image = info[.originalImage] as? UIImage else { return } - - let attachmentService = MastodonAttachmentService( - context: context, - image: image, - initialAuthenticationBox: viewModel.authenticationBox - ) - viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService] - } - - func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - picker.dismiss(animated: true, completion: nil) - } -} - -// MARK: - UIDocumentPickerDelegate -extension ComposeViewController: UIDocumentPickerDelegate { - 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] - } -} - -// 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 - } -} - -// 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 - } -} - -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 - } - } - -} +//// MARK: - PHPickerViewControllerDelegate +//extension ComposeViewController: PHPickerViewControllerDelegate { +// func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { +// picker.dismiss(animated: true, completion: nil) +// +// let attachmentServices: [MastodonAttachmentService] = results.map { result in +// let service = MastodonAttachmentService( +// context: context, +// pickerResult: result, +// initialAuthenticationBox: viewModel.authenticationBox +// ) +// return service +// } +// viewModel.attachmentServices = viewModel.attachmentServices + attachmentServices +// } +//} +// +//// MARK: - UIImagePickerControllerDelegate +//extension ComposeViewController: UIImagePickerControllerDelegate & UINavigationControllerDelegate { +// +// func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { +// picker.dismiss(animated: true, completion: nil) +// +// guard let image = info[.originalImage] as? UIImage else { return } +// +// let attachmentService = MastodonAttachmentService( +// context: context, +// image: image, +// initialAuthenticationBox: viewModel.authenticationBox +// ) +// viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService] +// } +// +// func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { +// os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) +// picker.dismiss(animated: true, completion: nil) +// } +//} +// +//// MARK: - UIDocumentPickerDelegate +//extension ComposeViewController: UIDocumentPickerDelegate { +// 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] +// } +//} +// +//// 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 +// } +//} +// +//// 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 +// } +//} +// +//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 +// } +// } +// +//} diff --git a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift b/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift index f8694376a..b3d8f52dc 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift @@ -18,497 +18,473 @@ 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 - } - - 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) - } +// 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 +// } +// +// 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) +// } } -// MARK: - UITableViewDataSource -extension ComposeViewModel: UITableViewDataSource { +//// MARK: - UITableViewDataSource +//extension ComposeViewModel: UITableViewDataSource { - enum Section: CaseIterable { - case repliedTo - case status - case attachment - case poll - } +// 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 +// } +// } +//} - func numberOfSections(in tableView: UITableView) -> Int { - return Section.allCases.count - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - switch Section.allCases[section] { - case .repliedTo: - switch composeKind { - case .reply: return 1 - default: return 0 - } - case .status: return 1 - case .attachment: return 1 - case .poll: return 1 - } - } - - 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) - } -} +//// 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 index 761391814..b9ed18c45 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift @@ -12,153 +12,153 @@ 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 { +// 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 - } - } - -} +//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 de088c68b..6e9a50fcc 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -28,159 +28,159 @@ final class ComposeViewModel: NSObject { // input let context: AppContext - let composeKind: ComposeStatusSection.ComposeKind 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 - @Published var autoCompleteRetryLayoutTimes = 0 - @Published var autoCompleteInfo: ComposeViewController.AutoCompleteInfo? = nil +// 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 +// @Published var autoCompleteRetryLayoutTimes = 0 +// @Published var autoCompleteInfo: ComposeViewController.AutoCompleteInfo? = nil - let traitCollectionDidChangePublisher = CurrentValueSubject(Void()) // use CurrentValueSubject to make initial event emit - var isViewAppeared = false +// 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? - - // custom emojis - let customEmojiViewModel: EmojiService.CustomEmojiViewModel? - let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel() - @Published var isLoadingCustomEmoji = false - - // attachment - @Published var attachmentServices: [MastodonAttachmentService] = [] - - // polls - @Published var pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute] = [] - let pollExpiresOptionAttribute = ComposeStatusPollItem.PollExpiresOptionAttribute() +// 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? +// +// // custom emojis +// let customEmojiViewModel: EmojiService.CustomEmojiViewModel? +// let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel() +// @Published var isLoadingCustomEmoji = false +// +// // attachment +// @Published var attachmentServices: [MastodonAttachmentService] = [] +// +// // polls +// @Published var pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute] = [] +// let pollExpiresOptionAttribute = ComposeStatusPollItem.PollExpiresOptionAttribute() init( context: AppContext, - composeKind: ComposeStatusSection.ComposeKind, - authContext: AuthContext + authContext: AuthContext, + kind: ComposeContentViewModel.Kind ) { self.context = context - self.composeKind = composeKind self.authContext = authContext + self.kind = kind - 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 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) } deinit { @@ -190,199 +190,192 @@ final class ComposeViewModel: NSObject { } extension ComposeViewModel { - enum CollectionViewState { - case fold // snap to input - case expand // snap to reply - } +// func createNewPollOptionIfPossible() { +// guard pollOptionAttributes.count < maxPollOptions else { return } +// +// let attribute = ComposeStatusPollItem.PollOptionAttribute() +// pollOptionAttributes = pollOptionAttributes + [attribute] +// } +// +// func updatePublishDate() { +// publishDate = Date() +// } } -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 - } -} +//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/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift deleted file mode 100644 index 8ff4e7242..000000000 --- a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift +++ /dev/null @@ -1,171 +0,0 @@ -// -// ComposeStatusAttachmentTableViewCell.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-29. -// - -import UIKit -import SwiftUI -import Combine -import AlamofireImage -import MastodonAsset -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/Mastodon/Scene/Compose/TableViewCell/ComposeStatusContentTableViewCell.swift b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusContentTableViewCell.swift deleted file mode 100644 index 814d79c0e..000000000 --- a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusContentTableViewCell.swift +++ /dev/null @@ -1,172 +0,0 @@ -// -// ComposeStatusContentTableViewCell.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-28. -// - -import os.log -import UIKit -import Combine -import MetaTextKit -import UITextView_Placeholder -import MastodonAsset -import MastodonLocalization -import MastodonUI - -protocol ComposeStatusContentTableViewCellDelegate: AnyObject { - func composeStatusContentTableViewCell(_ cell: ComposeStatusContentTableViewCell, textViewShouldBeginEditing textView: UITextView) -> Bool -} - -final class ComposeStatusContentTableViewCell: UITableViewCell { - - let logger = Logger(subsystem: "ComposeStatusContentTableViewCell", 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() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension ComposeStatusContentTableViewCell { - - private func _init() { - selectionStyle = .none - layer.zPosition = 999 - backgroundColor = .clear - preservesSuperviewLayoutMargins = true - - 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/Mastodon/Scene/Compose/TableViewCell/ComposeStatusPollTableViewCell.swift b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusPollTableViewCell.swift deleted file mode 100644 index f33a35c3e..000000000 --- a/Mastodon/Scene/Compose/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 - } -} diff --git a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift b/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift index d258ff7b8..496c8191b 100644 --- a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift +++ b/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift @@ -8,6 +8,8 @@ import UIKit import Combine import MetaTextKit +import MastodonCore +import MastodonUI final class CustomEmojiPickerInputViewModel { diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift index cbf292e3b..476832b69 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift @@ -11,15 +11,11 @@ import GameplayKit import MastodonSDK extension DiscoveryCommunityViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "DiscoveryCommunityViewModel.State", category: "StateMachine") let id = UUID() - - var name: String { - String(describing: Self.self) - } weak var viewModel: DiscoveryCommunityViewModel? @@ -29,8 +25,10 @@ extension DiscoveryCommunityViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? DiscoveryCommunityViewModel.State - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -39,7 +37,7 @@ extension DiscoveryCommunityViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel+State.swift b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel+State.swift index 09c3c57bb..7c802cde3 100644 --- a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel+State.swift +++ b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel+State.swift @@ -11,16 +11,12 @@ import GameplayKit import MastodonSDK extension DiscoveryNewsViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "DiscoveryNewsViewModel.State", category: "StateMachine") let id = UUID() - var name: String { - String(describing: Self.self) - } - weak var viewModel: DiscoveryNewsViewModel? init(viewModel: DiscoveryNewsViewModel) { @@ -29,8 +25,10 @@ extension DiscoveryNewsViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? DiscoveryNewsViewModel.State - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -39,7 +37,7 @@ extension DiscoveryNewsViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift index 9dc1e1f29..3ed245e99 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift @@ -12,16 +12,12 @@ import MastodonSDK import MastodonCore extension DiscoveryPostsViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "DiscoveryPostsViewModel.State", category: "StateMachine") let id = UUID() - var name: String { - String(describing: Self.self) - } - weak var viewModel: DiscoveryPostsViewModel? init(viewModel: DiscoveryPostsViewModel) { @@ -30,8 +26,10 @@ extension DiscoveryPostsViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? DiscoveryPostsViewModel.State - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -40,7 +38,7 @@ extension DiscoveryPostsViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift index a4818aa71..42079a91e 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift @@ -13,6 +13,7 @@ import GameplayKit import CoreData import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization final class HashtagTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { @@ -168,10 +169,10 @@ extension HashtagTimelineViewController { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) let composeViewModel = ComposeViewModel( context: context, - composeKind: .hashtag(hashtag: viewModel.hashtag), - authContext: viewModel.authContext + authContext: viewModel.authContext, + kind: .hashtag(hashtag: viewModel.hashtag) ) - coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) + _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) } } diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift index 57d8c99dd..deb9de0a2 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift @@ -11,7 +11,7 @@ import GameplayKit import CoreDataStack extension HashtagTimelineViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "HashtagTimelineViewModel.LoadOldestState", category: "StateMachine") @@ -28,10 +28,11 @@ extension HashtagTimelineViewModel { } override func didEnter(from previousState: GKState?) { - let previousState = previousState as? HashtagTimelineViewModel.State - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "")") - - viewModel?.loadOldestStateMachinePublisher.send(self) + super.didEnter(from: previousState) + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift index aa57a3930..af4d2a01a 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift @@ -51,7 +51,6 @@ final class HashtagTimelineViewModel { stateMachine.enter(State.Initial.self) return stateMachine }() - lazy var loadOldestStateMachinePublisher = CurrentValueSubject(nil) init(context: AppContext, authContext: AuthContext, hashtag: String) { self.context = context diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index 9412abad1..cabc655c9 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -9,6 +9,7 @@ import os.log import UIKit import CoreData import CoreDataStack +import MastodonUI extension HomeTimelineViewModel { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift index 2a0396f20..e88b5ed5f 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift @@ -11,15 +11,11 @@ import GameplayKit import MastodonSDK extension HomeTimelineViewModel { - class LoadOldestState: GKState, NamingState { + class LoadOldestState: GKState { let logger = Logger(subsystem: "HomeTimelineViewModel.LoadOldestState", category: "StateMachine") let id = UUID() - - var name: String { - String(describing: Self.self) - } weak var viewModel: HomeTimelineViewModel? @@ -29,10 +25,10 @@ extension HomeTimelineViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? HomeTimelineViewModel.LoadOldestState - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "")") - viewModel?.loadOldestStateMachinePublisher.send(self) + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -41,7 +37,7 @@ extension HomeTimelineViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 4cdd1da55..edd431e52 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -16,6 +16,7 @@ import GameplayKit import AlamofireImage import DateToolsSwift import MastodonCore +import MastodonUI final class HomeTimelineViewModel: NSObject { diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift index 346692adc..ff23d8d6e 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift @@ -12,16 +12,12 @@ import MastodonSDK import os.log extension NotificationTimelineViewModel { - class LoadOldestState: GKState, NamingState { + class LoadOldestState: GKState { let logger = Logger(subsystem: "NotificationTimelineViewModel.LoadOldestState", category: "StateMachine") let id = UUID() - var name: String { - String(describing: Self.self) - } - weak var viewModel: NotificationTimelineViewModel? init(viewModel: NotificationTimelineViewModel) { @@ -30,8 +26,10 @@ extension NotificationTimelineViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? NotificationTimelineViewModel.LoadOldestState - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -40,7 +38,7 @@ extension NotificationTimelineViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift index 3b0ebca8c..b8dbf994e 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift @@ -12,7 +12,7 @@ import MastodonCore import MastodonUI import MastodonLocalization -final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell { +public final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell { let containerView: UIView = { let view = UIView() @@ -30,7 +30,7 @@ final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell { return label }() - override func _init() { + public override func _init() { super._init() diff --git a/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift b/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift index 4e757cd1a..d50c62afe 100644 --- a/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift +++ b/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Tabman import MastodonAsset +import MastodonUI import MastodonLocalization protocol PickServerServerSectionTableHeaderViewDelegate: AnyObject { diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift index b91b90dad..5edec2618 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift @@ -12,6 +12,7 @@ import Combine import GameplayKit import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization final class BookmarkViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift index 8c9fd1150..e86ee92cc 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift @@ -12,16 +12,12 @@ import MastodonSDK import MastodonCore extension BookmarkViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "BookmarkViewModel.State", category: "StateMachine") let id = UUID() - var name: String { - String(describing: Self.self) - } - weak var viewModel: BookmarkViewModel? init(viewModel: BookmarkViewModel) { @@ -30,8 +26,10 @@ extension BookmarkViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? BookmarkViewModel.State - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -40,7 +38,7 @@ extension BookmarkViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift index 0b113345e..c15adcf83 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift @@ -15,6 +15,7 @@ import Combine import GameplayKit import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization final class FavoriteViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift index b583ec139..803a9d45e 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift @@ -12,15 +12,11 @@ import MastodonCore import MastodonSDK extension FavoriteViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "FavoriteViewModel.State", category: "StateMachine") let id = UUID() - - var name: String { - String(describing: Self.self) - } weak var viewModel: FavoriteViewModel? @@ -30,8 +26,10 @@ extension FavoriteViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? FavoriteViewModel.State - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -40,7 +38,7 @@ extension FavoriteViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift index 57e068c72..190fa27e5 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift @@ -10,6 +10,7 @@ import UIKit import GameplayKit import Combine import MastodonCore +import MastodonUI import MastodonLocalization final class FollowerListViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift index fe336f39f..045def7b7 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift @@ -12,7 +12,7 @@ import MastodonSDK import MastodonCore extension FollowerListViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "FollowerListViewModel.State", category: "StateMachine") @@ -30,8 +30,10 @@ extension FollowerListViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? FollowerListViewModel.State - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -40,7 +42,7 @@ extension FollowerListViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift index ccf0f1819..e16b600c2 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift @@ -11,6 +11,7 @@ import GameplayKit import Combine import MastodonLocalization import MastodonCore +import MastodonUI final class FollowingListViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift index d8c1f765a..723e66c8e 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift @@ -11,15 +11,11 @@ import GameplayKit import MastodonSDK extension FollowingListViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "FollowingListViewModel.State", category: "StateMachine") let id = UUID() - - var name: String { - String(describing: Self.self) - } weak var viewModel: FollowingListViewModel? @@ -29,8 +25,10 @@ extension FollowingListViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? FollowingListViewModel.State - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -39,7 +37,7 @@ extension FollowingListViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 30c37473e..9dd06b22c 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -499,7 +499,7 @@ extension ProfileViewController { user: record ) guard let activityViewController = _activityViewController else { return } - self.coordinator.present( + _ = self.coordinator.present( scene: .activityViewController( activityViewController: activityViewController, sourceView: nil, @@ -514,13 +514,13 @@ extension ProfileViewController { @objc private func favoriteBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) let favoriteViewModel = FavoriteViewModel(context: context, authContext: viewModel.authContext) - coordinator.present(scene: .favorite(viewModel: favoriteViewModel), from: self, transition: .show) + _ = coordinator.present(scene: .favorite(viewModel: favoriteViewModel), from: self, transition: .show) } @objc private func bookmarkBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) let bookmarkViewModel = BookmarkViewModel(context: context, authContext: viewModel.authContext) - coordinator.present(scene: .bookmark(viewModel: bookmarkViewModel), from: self, transition: .show) + _ = coordinator.present(scene: .bookmark(viewModel: bookmarkViewModel), from: self, transition: .show) } @objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) { @@ -528,10 +528,10 @@ extension ProfileViewController { guard let mastodonUser = viewModel.user else { return } let composeViewModel = ComposeViewModel( context: context, - composeKind: .mention(user: .init(objectID: mastodonUser.objectID)), - authContext: viewModel.authContext + authContext: viewModel.authContext, + kind: .mention(user: mastodonUser.asRecrod) ) - coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) + _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) } @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift index b87d4305e..4ed266c2e 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift @@ -12,16 +12,12 @@ import MastodonCore import MastodonSDK extension UserTimelineViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "UserTimelineViewModel.State", category: "StateMachine") let id = UUID() - var name: String { - String(describing: Self.self) - } - weak var viewModel: UserTimelineViewModel? init(viewModel: UserTimelineViewModel) { @@ -30,8 +26,10 @@ extension UserTimelineViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? UserTimelineViewModel.State - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -40,7 +38,7 @@ extension UserTimelineViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift index 18c37c403..c7b3e20cd 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift @@ -11,15 +11,11 @@ import GameplayKit import MastodonSDK extension UserListViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "UserListViewModel.State", category: "StateMachine") let id = UUID() - - var name: String { - String(describing: Self.self) - } weak var viewModel: UserListViewModel? @@ -29,8 +25,10 @@ extension UserListViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? UserListViewModel.State - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -39,7 +37,7 @@ extension UserListViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift index 7ed704434..d45c196cd 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift @@ -11,6 +11,7 @@ import Combine import CoreDataStack import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization protocol ReportStatusViewControllerDelegate: AnyObject { diff --git a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift index 8e44479d3..e644c29ea 100644 --- a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift +++ b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift @@ -10,6 +10,7 @@ import UIKit import Combine import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization protocol ReportSupplementaryViewControllerDelegate: AnyObject { diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 31d7d9fde..83860046e 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -372,8 +372,8 @@ extension MainTabBarController { guard let authContext = self.authContext else { return } let composeViewModel = ComposeViewModel( context: context, - composeKind: .post, - authContext: authContext + authContext: authContext, + kind: .post ) _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) } @@ -741,8 +741,8 @@ extension MainTabBarController { guard let authContext = self.authContext else { return } let composeViewModel = ComposeViewModel( context: context, - composeKind: .post, - authContext: authContext + authContext: authContext, + kind: .post ) _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) } diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift index 98006d4c0..70e1239b6 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift @@ -207,8 +207,8 @@ extension SidebarViewController: UICollectionViewDelegate { case .compose: let composeViewModel = ComposeViewModel( context: context, - composeKind: .post, - authContext: authContext + authContext: authContext, + kind: .post ) _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) default: diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift index b5deb777c..9b12e1af0 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift @@ -12,15 +12,12 @@ import MastodonSDK import MastodonCore extension SearchResultViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "SearchResultViewModel.State", category: "StateMachine") let id = UUID() - var name: String { - String(describing: Self.self) - } weak var viewModel: SearchResultViewModel? init(viewModel: SearchResultViewModel) { @@ -29,8 +26,10 @@ extension SearchResultViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? SearchResultViewModel.State - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -39,7 +38,7 @@ extension SearchResultViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift index 6e3e73b8a..13c8311c7 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift @@ -13,6 +13,7 @@ import OSLog import UIKit import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization class SuggestionAccountViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Thread/ThreadViewController.swift b/Mastodon/Scene/Thread/ThreadViewController.swift index fc158919b..6599f449e 100644 --- a/Mastodon/Scene/Thread/ThreadViewController.swift +++ b/Mastodon/Scene/Thread/ThreadViewController.swift @@ -13,6 +13,7 @@ import AVKit import MastodonMeta import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization final class ThreadViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { @@ -114,8 +115,8 @@ extension ThreadViewController { guard case let .root(threadContext) = viewModel.root else { return } let composeViewModel = ComposeViewModel( context: context, - composeKind: .reply(status: threadContext.status), - authContext: viewModel.authContext + authContext: viewModel.authContext, + kind: .reply(status: threadContext.status) ) _ = coordinator.present( scene: .compose(viewModel: composeViewModel), diff --git a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift index 7040818e9..834d478e6 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift @@ -10,6 +10,7 @@ import Combine import CoreData import CoreDataStack import MastodonCore +import MastodonUI import MastodonSDK extension ThreadViewModel { diff --git a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift index 4917aacb5..050670be7 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift @@ -13,15 +13,11 @@ import CoreDataStack import MastodonSDK extension ThreadViewModel { - class LoadThreadState: GKState, NamingState { + class LoadThreadState: GKState { let logger = Logger(subsystem: "ThreadViewModel.LoadThreadState", category: "StateMachine") let id = UUID() - - var name: String { - String(describing: Self.self) - } weak var viewModel: ThreadViewModel? @@ -31,8 +27,10 @@ extension ThreadViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? ThreadViewModel.LoadThreadState - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -41,7 +39,7 @@ extension ThreadViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index f368fa246..a2dd1625a 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -149,10 +149,10 @@ extension SceneDelegate { if let authContext = coordinator?.authContext { let composeViewModel = ComposeViewModel( context: AppContext.shared, - composeKind: .post, - authContext: authContext + authContext: authContext, + kind: .post ) - coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) + _ = coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): present compose scene") } else { logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): not authenticated") diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+API+Subscriptions+Policy.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+API+Subscriptions+Policy.swift similarity index 95% rename from Mastodon/Extension/MastodonSDK/Mastodon+API+Subscriptions+Policy.swift rename to MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+API+Subscriptions+Policy.swift index e85c8263e..78d7f2e81 100644 --- a/Mastodon/Extension/MastodonSDK/Mastodon+API+Subscriptions+Policy.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+API+Subscriptions+Policy.swift @@ -11,7 +11,7 @@ import MastodonAsset import MastodonLocalization extension Mastodon.API.Subscriptions.Policy { - var title: String { + public var title: String { switch self { case .all: return L10n.Scene.Settings.Section.Notifications.Trigger.anyone case .follower: return L10n.Scene.Settings.Section.Notifications.Trigger.follower diff --git a/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift index 2441ebfd0..059f09420 100644 --- a/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift @@ -7,14 +7,15 @@ import Foundation import MastodonSDK +import MastodonMeta -extension Mastodon.Entity.Account { - public var displayNameWithFallback: String { - if displayName.isEmpty { - return username - } else { - return displayName - } +extension Mastodon.Entity.Account: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: Mastodon.Entity.Account, rhs: Mastodon.Entity.Account) -> Bool { + return lhs.id == rhs.id } } @@ -28,3 +29,21 @@ extension Mastodon.Entity.Account { return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")! } } + +extension Mastodon.Entity.Account { + public var displayNameWithFallback: String { + return !displayName.isEmpty ? displayName : username + } +} + +extension Mastodon.Entity.Account { + public var emojiMeta: MastodonContent.Emojis { + let isAnimated = !UserDefaults.shared.preferredStaticEmoji + + var dict = MastodonContent.Emojis() + for emoji in emojis ?? [] { + dict[emoji.shortcode] = isAnimated ? emoji.url : emoji.staticURL + } + return dict + } +} diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Error+Detail.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Error+Detail.swift similarity index 92% rename from Mastodon/Extension/MastodonSDK/Mastodon+Entity+Error+Detail.swift rename to MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Error+Detail.swift index b3771632c..1dbfbd24f 100644 --- a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Error+Detail.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Error+Detail.swift @@ -36,7 +36,7 @@ extension Mastodon.Entity.Error.Detail: LocalizedError { extension Mastodon.Entity.Error.Detail { - enum Item: String { + public enum Item: String { case username case email case password @@ -82,32 +82,32 @@ extension Mastodon.Entity.Error.Detail { } } - var usernameErrorDescriptions: [String] { + public var usernameErrorDescriptions: [String] { guard let username = username, !username.isEmpty else { return [] } return username.map { Mastodon.Entity.Error.Detail.localizeError(item: .username, for: $0) } } - var emailErrorDescriptions: [String] { + public var emailErrorDescriptions: [String] { guard let email = email, !email.isEmpty else { return [] } return email.map { Mastodon.Entity.Error.Detail.localizeError(item: .email, for: $0) } } - var passwordErrorDescriptions: [String] { + public var passwordErrorDescriptions: [String] { guard let password = password, !password.isEmpty else { return [] } return password.map { Mastodon.Entity.Error.Detail.localizeError(item: .password, for: $0) } } - var agreementErrorDescriptions: [String] { + public var agreementErrorDescriptions: [String] { guard let agreement = agreement, !agreement.isEmpty else { return [] } return agreement.map { Mastodon.Entity.Error.Detail.localizeError(item: .agreement, for: $0) } } - var localeErrorDescriptions: [String] { + public var localeErrorDescriptions: [String] { guard let locale = locale, !locale.isEmpty else { return [] } return locale.map { Mastodon.Entity.Error.Detail.localizeError(item: .locale, for: $0) } } - var reasonErrorDescriptions: [String] { + public var reasonErrorDescriptions: [String] { guard let reason = reason, !reason.isEmpty else { return [] } return reason.map { Mastodon.Entity.Error.Detail.localizeError(item: .reason, for: $0) } } diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Error.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Error.swift similarity index 100% rename from Mastodon/Extension/MastodonSDK/Mastodon+Entity+Error.swift rename to MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Error.swift diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Field.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Field.swift similarity index 100% rename from Mastodon/Extension/MastodonSDK/Mastodon+Entity+Field.swift rename to MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Field.swift diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+History.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+History.swift similarity index 100% rename from Mastodon/Extension/MastodonSDK/Mastodon+Entity+History.swift rename to MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+History.swift diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift similarity index 100% rename from Mastodon/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift rename to MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift diff --git a/Mastodon/Diffiable/Compose/AutoCompleteItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/AutoCompleteItem.swift similarity index 90% rename from Mastodon/Diffiable/Compose/AutoCompleteItem.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/AutoCompleteItem.swift index ee296ba71..21bf9d759 100644 --- a/Mastodon/Diffiable/Compose/AutoCompleteItem.swift +++ b/MastodonSDK/Sources/MastodonCore/Model/Compose/AutoCompleteItem.swift @@ -8,7 +8,7 @@ import Foundation import MastodonSDK -enum AutoCompleteItem { +public enum AutoCompleteItem { case hashtag(tag: Mastodon.Entity.Tag) case hashtagV1(tag: String) case account(account: Mastodon.Entity.Account) @@ -17,7 +17,7 @@ enum AutoCompleteItem { } extension AutoCompleteItem: Equatable { - static func == (lhs: AutoCompleteItem, rhs: AutoCompleteItem) -> Bool { + public static func == (lhs: AutoCompleteItem, rhs: AutoCompleteItem) -> Bool { switch (lhs, rhs) { case (.hashtag(let tagLeft), hashtag(let tagRight)): return tagLeft.name == tagRight.name @@ -36,7 +36,7 @@ extension AutoCompleteItem: Equatable { } extension AutoCompleteItem: Hashable { - func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { switch self { case .hashtag(let tag): hasher.combine(tag.name) diff --git a/MastodonSDK/Sources/MastodonCore/Model/Compose/AutoCompleteSection.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/AutoCompleteSection.swift new file mode 100644 index 000000000..d7c9d07e9 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Model/Compose/AutoCompleteSection.swift @@ -0,0 +1,16 @@ +// +// AutoCompleteSection.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-5-17. +// + +import UIKit +import MastodonSDK +import MastodonMeta +import MastodonAsset +import MastodonLocalization + +public enum AutoCompleteSection: Equatable, Hashable { + case main +} diff --git a/Mastodon/Diffiable/Compose/ComposeStatusAttachmentItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusAttachmentItem.swift similarity index 93% rename from Mastodon/Diffiable/Compose/ComposeStatusAttachmentItem.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusAttachmentItem.swift index 07ae4e5df..834e1da49 100644 --- a/Mastodon/Diffiable/Compose/ComposeStatusAttachmentItem.swift +++ b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusAttachmentItem.swift @@ -6,7 +6,6 @@ // import Foundation -import MastodonCore enum ComposeStatusAttachmentItem { case attachment(attachmentService: MastodonAttachmentService) diff --git a/Mastodon/Diffiable/Compose/ComposeStatusAttachmentSection.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusAttachmentSection.swift similarity index 100% rename from Mastodon/Diffiable/Compose/ComposeStatusAttachmentSection.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusAttachmentSection.swift diff --git a/Mastodon/Diffiable/Compose/ComposeStatusItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusItem.swift similarity index 99% rename from Mastodon/Diffiable/Compose/ComposeStatusItem.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusItem.swift index aad93c2d2..65650dcdc 100644 --- a/Mastodon/Diffiable/Compose/ComposeStatusItem.swift +++ b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusItem.swift @@ -8,7 +8,6 @@ import Foundation import Combine import CoreData -import MastodonCore import MastodonMeta import CoreDataStack diff --git a/Mastodon/Diffiable/Compose/ComposeStatusPollItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusPollItem.swift similarity index 100% rename from Mastodon/Diffiable/Compose/ComposeStatusPollItem.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusPollItem.swift diff --git a/Mastodon/Diffiable/Compose/ComposeStatusPollSection.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusPollSection.swift similarity index 100% rename from Mastodon/Diffiable/Compose/ComposeStatusPollSection.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusPollSection.swift diff --git a/Mastodon/Diffiable/Compose/ComposeStatusSection.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusSection.swift similarity index 54% rename from Mastodon/Diffiable/Compose/ComposeStatusSection.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusSection.swift index 7b4596267..12dc88053 100644 --- a/Mastodon/Diffiable/Compose/ComposeStatusSection.swift +++ b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusSection.swift @@ -13,7 +13,7 @@ import MetaTextKit import MastodonMeta import AlamofireImage -enum ComposeStatusSection: Equatable, Hashable { +public enum ComposeStatusSection: Equatable, Hashable { case replyTo case status case attachment @@ -21,20 +21,11 @@ enum ComposeStatusSection: Equatable, Hashable { } extension ComposeStatusSection { - public enum ComposeKind { - case post - case hashtag(hashtag: String) - case mention(user: ManagedObjectRecord) - case reply(status: ManagedObjectRecord) - } -} -extension ComposeStatusSection { - - static func configure( - cell: ComposeStatusContentTableViewCell, - attribute: ComposeStatusItem.ComposeStatusAttribute - ) { +// static func configure( +// cell: ComposeStatusContentTableViewCell, +// attribute: ComposeStatusItem.ComposeStatusAttribute +// ) { // cell.prepa // // set avatar // attribute.avatarURL @@ -62,18 +53,18 @@ extension ComposeStatusSection { // cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " " // } // .store(in: &cell.disposeBag) - } +// } } -protocol CustomEmojiReplaceableTextInput: UITextInput & UIResponder { +public protocol CustomEmojiReplaceableTextInput: UITextInput & UIResponder { var inputView: UIView? { get set } } -class CustomEmojiReplaceableTextInputReference { - weak var value: CustomEmojiReplaceableTextInput? +public class CustomEmojiReplaceableTextInputReference { + public weak var value: CustomEmojiReplaceableTextInput? - init(value: CustomEmojiReplaceableTextInput? = nil) { + public init(value: CustomEmojiReplaceableTextInput? = nil) { self.value = value } } @@ -83,21 +74,21 @@ extension UITextView: CustomEmojiReplaceableTextInput { } extension ComposeStatusSection { - static func configureCustomEmojiPicker( - viewModel: CustomEmojiPickerInputViewModel?, - customEmojiReplaceableTextInput: CustomEmojiReplaceableTextInput, - disposeBag: inout Set - ) { - guard let viewModel = viewModel else { return } - viewModel.isCustomEmojiComposing - .receive(on: DispatchQueue.main) - .sink { [weak viewModel] isCustomEmojiComposing in - guard let viewModel = viewModel else { return } - customEmojiReplaceableTextInput.inputView = isCustomEmojiComposing ? viewModel.customEmojiPickerInputView : nil - customEmojiReplaceableTextInput.reloadInputViews() - viewModel.append(customEmojiReplaceableTextInput: customEmojiReplaceableTextInput) - } - .store(in: &disposeBag) - } +// static func configureCustomEmojiPicker( +// viewModel: CustomEmojiPickerInputViewModel?, +// customEmojiReplaceableTextInput: CustomEmojiReplaceableTextInput, +// disposeBag: inout Set +// ) { +// guard let viewModel = viewModel else { return } +// viewModel.isCustomEmojiComposing +// .receive(on: DispatchQueue.main) +// .sink { [weak viewModel] isCustomEmojiComposing in +// guard let viewModel = viewModel else { return } +// customEmojiReplaceableTextInput.inputView = isCustomEmojiComposing ? viewModel.customEmojiPickerInputView : nil +// customEmojiReplaceableTextInput.reloadInputViews() +// viewModel.append(customEmojiReplaceableTextInput: customEmojiReplaceableTextInput) +// } +// .store(in: &disposeBag) +// } } diff --git a/Mastodon/Diffiable/Compose/CustomEmojiPickerItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerItem.swift similarity index 100% rename from Mastodon/Diffiable/Compose/CustomEmojiPickerItem.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerItem.swift diff --git a/MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerSection.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerSection.swift new file mode 100644 index 000000000..5556a41ee --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerSection.swift @@ -0,0 +1,12 @@ +// +// CustomEmojiPickerSection.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-24. +// + +import Foundation + +public enum CustomEmojiPickerSection: Equatable, Hashable { + case emoji(name: String) +} diff --git a/MastodonSDK/Sources/MastodonUI/Model/Poll/PollItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Poll/PollItem.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/Model/Poll/PollItem.swift rename to MastodonSDK/Sources/MastodonCore/Model/Poll/PollItem.swift diff --git a/MastodonSDK/Sources/MastodonUI/Model/Poll/PollSection.swift b/MastodonSDK/Sources/MastodonCore/Model/Poll/PollSection.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/Model/Poll/PollSection.swift rename to MastodonSDK/Sources/MastodonCore/Model/Poll/PollSection.swift diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift index f7e595b81..4abe9ba5f 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift @@ -12,7 +12,6 @@ import MastodonSDK import CoreData import CoreDataStack import CommonOSLog -import MastodonCore extension APIService { diff --git a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift index afb4e63a9..48da254c6 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift @@ -79,7 +79,6 @@ extension AuthenticationService { public func activeMastodonUser(domain: String, userID: MastodonUser.ID) async throws -> Bool { var isActive = false - var _mastodonAuthentication: MastodonAuthentication? let managedObjectContext = backgroundManagedObjectContext @@ -91,7 +90,6 @@ extension AuthenticationService { return } mastodonAuthentication.update(activedAt: Date()) - _mastodonAuthentication = mastodonAuthentication isActive = true } diff --git a/Mastodon/Diffiable/Compose/AutoCompleteSection.swift b/MastodonSDK/Sources/MastodonUI/DataSource/AutoCompleteSection+Diffable.swift similarity index 94% rename from Mastodon/Diffiable/Compose/AutoCompleteSection.swift rename to MastodonSDK/Sources/MastodonUI/DataSource/AutoCompleteSection+Diffable.swift index 1260f398b..3022899f1 100644 --- a/Mastodon/Diffiable/Compose/AutoCompleteSection.swift +++ b/MastodonSDK/Sources/MastodonUI/DataSource/AutoCompleteSection+Diffable.swift @@ -1,25 +1,20 @@ // -// AutoCompleteSection.swift -// Mastodon +// AutoCompleteSection+Diffable.swift +// // -// Created by MainasuK Cirno on 2021-5-17. +// Created by MainasuK on 22/10/10. // import UIKit -import MastodonSDK -import MastodonMeta -import MastodonAsset -import MastodonLocalization import MastodonCore - -enum AutoCompleteSection: Equatable, Hashable { - case main -} +import MastodonSDK +import MastodonLocalization +import MastodonMeta extension AutoCompleteSection { - static func tableViewDiffableDataSource( - for tableView: UITableView + public static func tableViewDiffableDataSource( + tableView: UITableView ) -> UITableViewDiffableDataSource { UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in switch item { diff --git a/MastodonSDK/Sources/MastodonUI/DataSource/CustomEmojiPickerSection+Diffable.swift b/MastodonSDK/Sources/MastodonUI/DataSource/CustomEmojiPickerSection+Diffable.swift new file mode 100644 index 000000000..ca3658e95 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/DataSource/CustomEmojiPickerSection+Diffable.swift @@ -0,0 +1,59 @@ +// +// CustomEmojiPickerSection+Diffable.swift +// +// +// Created by MainasuK on 22/10/10. +// + +import Foundation +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 +// } +} diff --git a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewController.swift similarity index 98% rename from Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewController.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewController.swift index 16f65f702..aa21057d1 100644 --- a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewController.swift @@ -88,7 +88,7 @@ extension AutoCompleteViewController { ]) tableView.delegate = self - viewModel.setupDiffableDataSource(for: 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 new file mode 100644 index 000000000..adbf6ac09 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+Diffable.swift @@ -0,0 +1,22 @@ +// +// AutoCompleteViewModel+Diffable.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-5-17. +// + +import UIKit + +extension AutoCompleteViewModel { + +// func setupDiffableDataSource( +// tableView: UITableView +// ) { +// diffableDataSource = AutoCompleteSection.tableViewDiffableDataSource(for: tableView) +// +// var snapshot = NSDiffableDataSourceSnapshot() +// snapshot.appendSections([.main]) +// diffableDataSource?.apply(snapshot) +// } + +} diff --git a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+State.swift similarity index 95% rename from Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+State.swift index 1d46fb214..b1f5f3187 100644 --- a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+State.swift @@ -12,16 +12,12 @@ import MastodonSDK import MastodonCore extension AutoCompleteViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "AutoCompleteViewModel.State", category: "StateMachine") let id = UUID() - var name: String { - String(describing: Self.self) - } - weak var viewModel: AutoCompleteViewModel? init(viewModel: AutoCompleteViewModel) { @@ -30,8 +26,10 @@ extension AutoCompleteViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? AutoCompleteViewModel.State - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -40,7 +38,7 @@ extension AutoCompleteViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift similarity index 100% rename from Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift diff --git a/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/Cell/AutoCompleteTableViewCell.swift similarity index 95% rename from Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/Cell/AutoCompleteTableViewCell.swift index b7c8fcecc..c5db611d0 100644 --- a/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/Cell/AutoCompleteTableViewCell.swift @@ -10,10 +10,8 @@ import FLAnimatedImage import MetaTextKit import MastodonAsset import MastodonLocalization -import MastodonUI - -final class AutoCompleteTableViewCell: UITableViewCell { +public final class AutoCompleteTableViewCell: UITableViewCell { static let avatarImageSize = CGSize(width: 42, height: 42) static let avatarImageCornerRadius: CGFloat = 4 @@ -51,17 +49,17 @@ final class AutoCompleteTableViewCell: UITableViewCell { let separatorLine = UIView.separatorLine - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) _init() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { super.init(coder: coder) _init() } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { + public override func setHighlighted(_ highlighted: Bool, animated: Bool) { super.setHighlighted(highlighted, animated: animated) // workaround for hitTest trigger highlighted issue diff --git a/Mastodon/Scene/Compose/AutoComplete/View/AutoCompleteTopChevronView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/View/AutoCompleteTopChevronView.swift similarity index 100% rename from Mastodon/Scene/Compose/AutoComplete/View/AutoCompleteTopChevronView.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/View/AutoCompleteTopChevronView.swift diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentView.swift index 21161a0a6..886634d7e 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentView.swift @@ -8,11 +8,93 @@ import SwiftUI public struct ComposeContentView: View { + + @ObservedObject var viewModel: ComposeContentViewModel + + @State var contentOffsetDelta: CGFloat = .zero + public var body: some View { ScrollView { - VStack { - Text("Hello") - } - } - } + VStack(spacing: .zero) { + GeometryReader { geometry in + Color.clear.preference( + key: ScrollOffsetPreferenceKey.self, + value: geometry.frame(in: .named("scrollView")).origin + ) + }.frame(width: 0, height: 0) + .onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in + print("contentOffset: \(offset)") + } + VStack { + Text("Reply") + } + .frame(height: 100) + .frame(maxWidth: .infinity) + .background(Color.blue) + .background( + GeometryReader { geometry in + Color.clear.preference( + key: ViewFramePreferenceKey.self, + value: geometry.frame(in: .named("scrollView")) + ) + } + .onPreferenceChange(ViewFramePreferenceKey.self) { frame in + print("reply frame: \(frame)") + } + ) + VStack { + Text("Content") + } + .frame(maxWidth: .infinity) + .background(Color.orange) + } // end VStack + .offset(y: contentOffsetDelta) + } // end ScrollView + .coordinateSpace(name: "scrollView") + } // end body } + +private struct ScrollOffsetPreferenceKey: PreferenceKey { + static var defaultValue: CGPoint = .zero + + static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { } +} + +private struct ViewFramePreferenceKey: PreferenceKey { + static var defaultValue: CGRect = .zero + + static func reduce(value: inout CGRect, nextValue: () -> CGRect) { } +} + +//struct ScrollView: View { +// let axes: Axis.Set +// let showsIndicators: Bool +// let offsetChanged: (CGPoint) -> Void +// let content: Content +// +// init( +// axes: Axis.Set = .vertical, +// showsIndicators: Bool = true, +// offsetChanged: @escaping (CGPoint) -> Void = { _ in }, +// @ViewBuilder content: () -> Content +// ) { +// self.axes = axes +// self.showsIndicators = showsIndicators +// self.offsetChanged = offsetChanged +// self.content = content() +// } +// +// var body: some View { +// SwiftUI.ScrollView(axes, showsIndicators: showsIndicators) { +// GeometryReader { geometry in +// Color.clear.preference( +// key: ScrollOffsetPreferenceKey.self, +// value: geometry.frame(in: .named("scrollView")).origin +// ) +// }.frame(width: 0, height: 0) +// content +// } +// .coordinateSpace(name: "scrollView") +// .onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: offsetChanged) +// } +//} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index 18604c4b3..240e756a1 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -7,18 +7,47 @@ import os.log import UIKit +import SwiftUI public final class ComposeContentViewController: UIViewController { let logger = Logger(subsystem: "ComposeContentViewController", category: "ViewController") + public var viewModel: ComposeContentViewModel! - + let tableView: ComposeTableView = { + let tableView = ComposeTableView() + tableView.alwaysBounceVertical = true + tableView.separatorStyle = .none + tableView.tableFooterView = UIView() + return tableView + }() + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + } extension ComposeContentViewController { public override func viewDidLoad() { super.viewDidLoad() + tableView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(tableView) + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + tableView.delegate = self + viewModel.setupDataSource(tableView: tableView) + } } + +// MARK: - UITableViewDelegate +extension ComposeContentViewController: UITableViewDelegate { } + diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift new file mode 100644 index 000000000..e1ad561ef --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift @@ -0,0 +1,90 @@ +// +// ComposeContentViewModel+DataSource.swift +// +// +// Created by MainasuK on 22/10/10. +// + +import UIKit +import MastodonCore +import CoreDataStack + +extension ComposeContentViewModel { + + func setupDataSource( + tableView: UITableView + ) { + tableView.dataSource = self + + setupTableViewCell(tableView: tableView) + } + +} + +extension ComposeContentViewModel { + enum Section: CaseIterable { + case replyTo + case status + case attachment + case poll + } + + private func setupTableViewCell(tableView: UITableView) { + switch kind { + case .post: + break + case .reply(let status): + let cell = composeReplyToTableViewCell + // bind frame publisher +// cell.framePublisher +// .receive(on: DispatchQueue.main) +// .assign(to: \.repliedToCellFrame, on: self) +// .store(in: &cell.disposeBag) + + // set initial width + cell.statusView.frame.size.width = tableView.frame.width + + // configure status + context.managedObjectContext.performAndWait { + guard let replyTo = status.object(in: context.managedObjectContext) else { return } + cell.statusView.configure(status: replyTo) + } + case .hashtag(let hashtag): + break + case .mention(let user): + break + } + } +} + +extension ComposeContentViewModel: UITableViewDataSource { + public func numberOfSections(in tableView: UITableView) -> Int { + return Section.allCases.count + } + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch Section.allCases[section] { + case .replyTo: + switch kind { + case .reply: return 1 + default: return 0 + } + case .status: return 1 + case .attachment: return 1 + case .poll: return 1 + } + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch Section.allCases[indexPath.section] { + case .replyTo: + return composeReplyToTableViewCell + case .status: + return UITableViewCell() + case .attachment: + return UITableViewCell() + case .poll: + return UITableViewCell() + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 9fa94c5e0..9f0eed2fe 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -6,15 +6,41 @@ // import Foundation +import CoreDataStack import MastodonCore -final class ComposeContentViewModel: ObservableObject { +public final class ComposeContentViewModel: NSObject, ObservableObject { + + // tableViewCell + let composeReplyToTableViewCell = ComposeReplyToTableViewCell() // input let context: AppContext - - init(context: AppContext) { + let kind: Kind + + public init( + context: AppContext, + kind: Kind + ) { self.context = context + self.kind = kind + super.init() + // end init } } + +extension ComposeContentViewModel { + public enum Kind { + case post + case hashtag(hashtag: String) + case mention(user: ManagedObjectRecord) + case reply(status: ManagedObjectRecord) + } + + public enum ViewState { + case fold // snap to input + case expand // snap to reply + } +} + diff --git a/Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeReplyToTableViewCell.swift similarity index 83% rename from Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeReplyToTableViewCell.swift index b0fbb0194..773245cbf 100644 --- a/Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeReplyToTableViewCell.swift @@ -1,5 +1,5 @@ // -// ComposeRepliedToStatusContentTableViewCell.swift +// ComposeReplyToTableViewCell.swift // Mastodon // // Created by MainasuK Cirno on 2021-6-28. @@ -7,15 +7,14 @@ import UIKit import Combine -import MastodonUI -final class ComposeRepliedToStatusContentTableViewCell: UITableViewCell { +final class ComposeReplyToTableViewCell: UITableViewCell { var disposeBag = Set() let statusView = StatusView() - let framePublisher = PassthroughSubject() + @Published var framePublisher: CGRect = .zero override func prepareForReuse() { super.prepareForReuse() @@ -36,12 +35,12 @@ final class ComposeRepliedToStatusContentTableViewCell: UITableViewCell { override func layoutSubviews() { super.layoutSubviews() - framePublisher.send(bounds) + framePublisher = bounds } } -extension ComposeRepliedToStatusContentTableViewCell { +extension ComposeReplyToTableViewCell { private func _init() { selectionStyle = .none diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusAttachmentTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusAttachmentTableViewCell.swift new file mode 100644 index 000000000..42a851bf1 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusAttachmentTableViewCell.swift @@ -0,0 +1,172 @@ +// +// 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/ComposeStatusContentTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusContentTableViewCell.swift new file mode 100644 index 000000000..e9b8cf068 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusContentTableViewCell.swift @@ -0,0 +1,172 @@ +// +// ComposeStatusContentTableViewCell.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-28. +// + +import os.log +import UIKit +import Combine +import MetaTextKit +import UITextView_Placeholder +import MastodonAsset +import MastodonLocalization +import MastodonUI + +//protocol ComposeStatusContentTableViewCellDelegate: AnyObject { +// func composeStatusContentTableViewCell(_ cell: ComposeStatusContentTableViewCell, textViewShouldBeginEditing textView: UITextView) -> Bool +//} + +final class ComposeStatusContentTableViewCell: UITableViewCell { + +// let logger = Logger(subsystem: "ComposeStatusContentTableViewCell", 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() +// } +// +// required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } + +} + +extension ComposeStatusContentTableViewCell { + +// private func _init() { +// selectionStyle = .none +// layer.zPosition = 999 +// backgroundColor = .clear +// preservesSuperviewLayoutMargins = true +// +// 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/ComposeStatusPollTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusPollTableViewCell.swift new file mode 100644 index 000000000..27b835a5a --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusPollTableViewCell.swift @@ -0,0 +1,209 @@ +// +// 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 +// } +//} diff --git a/Mastodon/Scene/Compose/View/ComposeTableView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeTableView.swift similarity index 100% rename from Mastodon/Scene/Compose/View/ComposeTableView.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeTableView.swift diff --git a/Mastodon/Vender/ControlContainableScrollViews.swift b/MastodonSDK/Sources/MastodonUI/Vendor/ControlContainableScrollViews.swift similarity index 80% rename from Mastodon/Vender/ControlContainableScrollViews.swift rename to MastodonSDK/Sources/MastodonUI/Vendor/ControlContainableScrollViews.swift index 057527ce2..79bb71c6f 100644 --- a/Mastodon/Vender/ControlContainableScrollViews.swift +++ b/MastodonSDK/Sources/MastodonUI/Vendor/ControlContainableScrollViews.swift @@ -17,9 +17,9 @@ import UIKit // they feel broken. Feel free to add your own exceptions if you have custom // controls that require swiping or dragging to function. -final class ControlContainableScrollView: UIScrollView { +public final class ControlContainableScrollView: UIScrollView { - override func touchesShouldCancel(in view: UIView) -> Bool { + public override func touchesShouldCancel(in view: UIView) -> Bool { if view is UIControl && !(view is UITextInput) && !(view is UISlider) @@ -32,9 +32,9 @@ final class ControlContainableScrollView: UIScrollView { } -final class ControlContainableTableView: UITableView { +public final class ControlContainableTableView: UITableView { - override func touchesShouldCancel(in view: UIView) -> Bool { + public override func touchesShouldCancel(in view: UIView) -> Bool { if view is UIControl && !(view is UITextInput) && !(view is UISlider) @@ -47,9 +47,9 @@ final class ControlContainableTableView: UITableView { } -final class ControlContainableCollectionView: UICollectionView { +public final class ControlContainableCollectionView: UICollectionView { - override func touchesShouldCancel(in view: UIView) -> Bool { + public override func touchesShouldCancel(in view: UIView) -> Bool { if view is UIControl && !(view is UITextInput) && !(view is UISlider) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift index 76897a46f..438baff7e 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift @@ -9,6 +9,7 @@ import UIKit import Combine import CoreData +import CoreDataStack import Photos import AlamofireImage import MastodonCore @@ -178,3 +179,59 @@ extension MediaView.Configuration { } } + +extension MediaView { + public static func configuration(status: Status) -> [MediaView.Configuration] { + func videoInfo(from attachment: MastodonAttachment) -> MediaView.Configuration.VideoInfo { + MediaView.Configuration.VideoInfo( + aspectRadio: attachment.size, + assetURL: attachment.assetURL, + previewURL: attachment.previewURL, + durationMS: attachment.durationMS + ) + } + + let status = status.reblog ?? status + let attachments = status.attachments + let configurations = attachments.map { attachment -> MediaView.Configuration in + let configuration: MediaView.Configuration = { + switch attachment.kind { + case .image: + let info = MediaView.Configuration.ImageInfo( + aspectRadio: attachment.size, + assetURL: attachment.assetURL + ) + return .init( + info: .image(info: info), + blurhash: attachment.blurhash + ) + case .video: + let info = videoInfo(from: attachment) + return .init( + info: .video(info: info), + blurhash: attachment.blurhash + ) + case .gifv: + let info = videoInfo(from: attachment) + return .init( + info: .gif(info: info), + blurhash: attachment.blurhash + ) + case .audio: + let info = videoInfo(from: attachment) + return .init( + info: .video(info: info), + blurhash: attachment.blurhash + ) + } // end switch + }() + + configuration.load() + configuration.isReveal = status.isMediaSensitive ? status.isSensitiveToggled : true + + return configuration + } + + return configurations + } +} diff --git a/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift similarity index 99% rename from Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift rename to MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index bea08059d..353dfc097 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -7,11 +7,9 @@ import UIKit import Combine -import MastodonUI import CoreDataStack import MastodonSDK import MastodonCore -import MastodonUI import MastodonLocalization import MastodonMeta import Meta @@ -125,11 +123,12 @@ extension StatusView { if let authenticationBox = viewModel.authContext?.mastodonAuthenticationBox { Just(inReplyToAccountID) .asyncMap { userID in - return try await AppContext.shared.apiService.accountInfo( + return try await Mastodon.API.Account.accountInfo( + session: .shared, domain: authenticationBox.domain, userID: userID, authorization: authenticationBox.userAuthorization - ) + ).singleOutput() } .sink { completion in // do nothing diff --git a/Mastodon/Scene/Share/View/Decoration/SawToothView.swift b/MastodonSDK/Sources/MastodonUI/View/Decoration/SawToothView.swift similarity index 95% rename from Mastodon/Scene/Share/View/Decoration/SawToothView.swift rename to MastodonSDK/Sources/MastodonUI/View/Decoration/SawToothView.swift index f154a48e6..1a7a977e6 100644 --- a/Mastodon/Scene/Share/View/Decoration/SawToothView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Decoration/SawToothView.swift @@ -10,7 +10,7 @@ import UIKit import Combine import MastodonCore -final class SawToothView: UIView { +public final class SawToothView: UIView { static let widthUint = 8 var disposeBag = Set() @@ -41,7 +41,7 @@ final class SawToothView: UIView { setNeedsDisplay() } - override func draw(_ rect: CGRect) { + public override func draw(_ rect: CGRect) { let bezierPath = UIBezierPath() let bottomY = rect.height let topY = 0 diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineBottomLoaderTableViewCell.swift similarity index 81% rename from Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift rename to MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineBottomLoaderTableViewCell.swift index f6c4596ab..cc16e520d 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineBottomLoaderTableViewCell.swift @@ -8,18 +8,17 @@ import UIKit import Combine import MastodonCore -import MastodonUI -final class TimelineBottomLoaderTableViewCell: TimelineLoaderTableViewCell { +public final class TimelineBottomLoaderTableViewCell: TimelineLoaderTableViewCell { - override func prepareForReuse() { + public override func prepareForReuse() { super.prepareForReuse() loadMoreLabel.isHidden = true loadMoreButton.isHidden = true } - override func _init() { + public override func _init() { super._init() activityIndicatorView.isHidden = false diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineLoaderTableViewCell.swift similarity index 84% rename from Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift rename to MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineLoaderTableViewCell.swift index f9d58da6a..6e396dc6d 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineLoaderTableViewCell.swift @@ -9,23 +9,22 @@ import UIKit import Combine import MastodonAsset import MastodonCore -import MastodonUI import MastodonLocalization -class TimelineLoaderTableViewCell: UITableViewCell { +open class TimelineLoaderTableViewCell: UITableViewCell { - static let buttonHeight: CGFloat = 44 - static let buttonMargin: CGFloat = 12 - static let cellHeight: CGFloat = buttonHeight + 2 * buttonMargin - static let labelFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .medium)) + public static let buttonHeight: CGFloat = 44 + public static let buttonMargin: CGFloat = 12 + public static let cellHeight: CGFloat = buttonHeight + 2 * buttonMargin + public static let labelFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .medium)) var disposeBag = Set() private var _disposeBag = Set() - let stackView = UIStackView() + public let stackView = UIStackView() - let loadMoreButton: UIButton = { + public let loadMoreButton: UIButton = { let button = HighlightDimmableButton() button.titleLabel?.font = TimelineLoaderTableViewCell.labelFont button.setTitleColor(ThemeService.tintColor, for: .normal) @@ -34,49 +33,49 @@ class TimelineLoaderTableViewCell: UITableViewCell { return button }() - let loadMoreLabel: UILabel = { + public let loadMoreLabel: UILabel = { let label = UILabel() label.font = TimelineLoaderTableViewCell.labelFont return label }() - let activityIndicatorView: UIActivityIndicatorView = { + public let activityIndicatorView: UIActivityIndicatorView = { let activityIndicatorView = UIActivityIndicatorView(style: .medium) activityIndicatorView.tintColor = Asset.Colors.Label.secondary.color activityIndicatorView.hidesWhenStopped = true return activityIndicatorView }() - override func prepareForReuse() { + public override func prepareForReuse() { super.prepareForReuse() disposeBag.removeAll() } - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) _init() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { super.init(coder: coder) _init() } - func startAnimating() { + public func startAnimating() { activityIndicatorView.startAnimating() self.loadMoreButton.isEnabled = false self.loadMoreLabel.textColor = Asset.Colors.Label.secondary.color self.loadMoreLabel.text = L10n.Common.Controls.Timeline.Loader.loadingMissingPosts } - func stopAnimating() { + public func stopAnimating() { activityIndicatorView.stopAnimating() self.loadMoreButton.isEnabled = true self.loadMoreLabel.textColor = ThemeService.tintColor self.loadMoreLabel.text = "" } - func _init() { + open func _init() { selectionStyle = .none backgroundColor = .clear diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift similarity index 90% rename from Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift rename to MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift index 406d2a7ec..19f2bbdaa 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift @@ -10,7 +10,7 @@ import Combine import CoreDataStack extension TimelineMiddleLoaderTableViewCell { - class ViewModel { + public class ViewModel { var disposeBag = Set() @Published var isFetching = false @@ -18,7 +18,7 @@ extension TimelineMiddleLoaderTableViewCell { } extension TimelineMiddleLoaderTableViewCell.ViewModel { - func bind(cell: TimelineMiddleLoaderTableViewCell) { + public func bind(cell: TimelineMiddleLoaderTableViewCell) { $isFetching .sink { isFetching in if isFetching { @@ -33,7 +33,7 @@ extension TimelineMiddleLoaderTableViewCell.ViewModel { extension TimelineMiddleLoaderTableViewCell { - func configure( + public func configure( feed: Feed, delegate: TimelineMiddleLoaderTableViewCellDelegate? ) { diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell.swift similarity index 93% rename from Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift rename to MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell.swift index 462a22d48..7cb5f2b44 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell.swift @@ -9,13 +9,12 @@ import Combine import CoreData import os.log import UIKit -import MastodonUI -protocol TimelineMiddleLoaderTableViewCellDelegate: AnyObject { +public protocol TimelineMiddleLoaderTableViewCellDelegate: AnyObject { func timelineMiddleLoaderTableViewCell(_ cell: TimelineMiddleLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton) } -final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCell { +public final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCell { weak var delegate: TimelineMiddleLoaderTableViewCellDelegate? @@ -28,7 +27,7 @@ final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCell { let topSawToothView = SawToothView() let bottomSawToothView = SawToothView() - override func _init() { + public override func _init() { super._init() loadMoreButton.isHidden = false diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineTopLoaderTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineTopLoaderTableViewCell.swift similarity index 83% rename from Mastodon/Scene/Share/View/TableviewCell/TimelineTopLoaderTableViewCell.swift rename to MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineTopLoaderTableViewCell.swift index 20c117eab..742614854 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineTopLoaderTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineTopLoaderTableViewCell.swift @@ -8,11 +8,9 @@ import UIKit import Combine import MastodonCore -import MastodonUI - -final class TimelineTopLoaderTableViewCell: TimelineLoaderTableViewCell { - override func _init() { +public final class TimelineTopLoaderTableViewCell: TimelineLoaderTableViewCell { + public override func _init() { super._init() activityIndicatorView.isHidden = false diff --git a/ShareActionExtension/Scene/ComposeViewController.swift b/ShareActionExtension/Scene/ComposeViewController.swift new file mode 100644 index 000000000..c93c05147 --- /dev/null +++ b/ShareActionExtension/Scene/ComposeViewController.swift @@ -0,0 +1,327 @@ +// +// ComposeViewController.swift +// MastodonShareAction +// +// Created by MainasuK Cirno on 2021-7-16. +// + +import os.log +import UIKit +import Combine +import MastodonUI +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 new file mode 100644 index 000000000..4470cfbe8 --- /dev/null +++ b/ShareActionExtension/Scene/ComposeViewModel.swift @@ -0,0 +1,417 @@ +// +// 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 MastodonUI +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 deleted file mode 100644 index a142aa69b..000000000 --- a/ShareActionExtension/Scene/ShareViewController.swift +++ /dev/null @@ -1,325 +0,0 @@ -// -// ShareViewController.swift -// MastodonShareAction -// -// Created by MainasuK Cirno on 2021-7-16. -// - -import os.log -import UIKit -import Combine -import MastodonUI -import SwiftUI -import MastodonAsset -import MastodonLocalization -import MastodonCore -import MastodonUI - -class ShareViewController: UIViewController { - - let logger = Logger(subsystem: "ShareViewController", category: "UI") - - var disposeBag = Set() - let viewModel = ShareViewModel() - - 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(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 - }() - - - let viewSafeAreaDidChange = PassthroughSubject() - let composeToolbarView = ComposeToolbarView() - var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint! - let composeToolbarBackgroundView = UIView() -} - -extension ShareViewController { - - 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 ShareViewController { - 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 ShareViewController { - @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 ShareViewController: 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 ShareViewController: 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/ShareViewModel.swift b/ShareActionExtension/Scene/ShareViewModel.swift deleted file mode 100644 index 63a7132f6..000000000 --- a/ShareActionExtension/Scene/ShareViewModel.swift +++ /dev/null @@ -1,412 +0,0 @@ -// -// 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 MastodonUI -import SwiftUI -import UniformTypeIdentifiers -import MastodonAsset -import MastodonLocalization -import MastodonUI -import MastodonCore - -final class ShareViewModel { - - let logger = Logger(subsystem: "ShareViewModel", category: "logic") - - var disposeBag = Set() - - static let composeContentLimit: Int = 500 - - // input - 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() { - 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 ShareViewModel { - 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() - } -} From 4367e8eabaf0fd8db87ea21e83dbbf9fc09a9175 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 11 Oct 2022 18:31:40 +0800 Subject: [PATCH 040/658] feat: [WP] restore the content compose via SwiftUI and support expandable reply view for compose scene --- .../Scene/Compose/ComposeViewController.swift | 175 +++++------------- .../CoreDataStack/MastodonUser.swift | 29 ++- .../ComposeContent/ComposeContentView.swift | 100 ---------- .../ComposeContentViewController.swift | 93 ++++++++++ .../ComposeContentViewModel+DataSource.swift | 39 ++-- .../ComposeContentViewModel.swift | 52 +++++- ...wift => ComposeContentTableViewCell.swift} | 45 +++-- .../View/ComposeContentView.swift | 97 ++++++++++ .../SwiftUI/MetaLabelRepresentable.swift | 47 +++++ .../SwiftUI/MetaTextViewRepresentable.swift | 82 ++++++++ .../View/Utility/ViewLayoutFrame.swift | 57 ++++++ 11 files changed, 548 insertions(+), 268 deletions(-) delete mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentView.swift rename MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/{ComposeStatusContentTableViewCell.swift => ComposeContentTableViewCell.swift} (89%) create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift create mode 100644 MastodonSDK/Sources/MastodonUI/SwiftUI/MetaLabelRepresentable.swift create mode 100644 MastodonSDK/Sources/MastodonUI/SwiftUI/MetaTextViewRepresentable.swift create mode 100644 MastodonSDK/Sources/MastodonUI/View/Utility/ViewLayoutFrame.swift diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 1fd3b0670..1d847f040 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -31,7 +31,11 @@ final class ComposeViewController: UIViewController, NeedsDependency { let logger = Logger(subsystem: "ComposeViewController", category: "logic") lazy var composeContentViewModel: ComposeContentViewModel = { - return ComposeContentViewModel(context: context, kind: viewModel.kind) + return ComposeContentViewModel( + context: context, + authContext: viewModel.authContext, + kind: viewModel.kind + ) }() private(set) lazy var composeContentViewController: ComposeContentViewController = { let composeContentViewController = ComposeContentViewController() @@ -39,20 +43,20 @@ final class ComposeViewController: UIViewController, NeedsDependency { return composeContentViewController }() -// 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 -// }() -// + 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 + }() + // let publishButton: UIButton = { // let button = RoundedEdgesButton(type: .custom) // button.cornerRadius = 10 @@ -83,23 +87,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) // } - -// let scrollView: UIScrollView = { -// let scrollView = UIScrollView() -// scrollView.alwaysBounceVertical = true -// return scrollView -// }() - -// let tableView: ComposeTableView = { -// let tableView = ComposeTableView() -// tableView.register(ComposeRepliedToStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self)) -// tableView.register(ComposeStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusContentTableViewCell.self)) -// tableView.register(ComposeStatusAttachmentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusAttachmentTableViewCell.self)) -// tableView.alwaysBounceVertical = true -// tableView.separatorStyle = .none -// tableView.tableFooterView = UIView() -// return tableView -// }() // var systemKeyboardHeight: CGFloat = .zero { // didSet { @@ -177,6 +164,22 @@ extension ComposeViewController { override func viewDidLoad() { super.viewDidLoad() + navigationItem.leftBarButtonItem = cancelBarButtonItem + // navigationItem.rightBarButtonItem = publishBarButtonItem + // viewModel.traitCollectionDidChangePublisher + // .receive(on: DispatchQueue.main) + // .sink { [weak self] _ in + // 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) + // } + // self.navigationItem.rightBarButtonItems = items + // } + // .store(in: &disposeBag) + // publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) + addChild(composeContentViewController) composeContentViewController.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(composeContentViewController.view) @@ -212,21 +215,6 @@ extension ComposeViewController { // self.setupBackgroundColor(theme: theme) // } // .store(in: &disposeBag) -// navigationItem.leftBarButtonItem = cancelBarButtonItem -// navigationItem.rightBarButtonItem = publishBarButtonItem -// viewModel.traitCollectionDidChangePublisher -// .receive(on: DispatchQueue.main) -// .sink { [weak self] _ in -// 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) -// } -// self.navigationItem.rightBarButtonItems = items -// } -// .store(in: &disposeBag) -// publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) // // // scrollView.translatesAutoresizingMaskIntoConstraints = false @@ -533,26 +521,6 @@ extension ComposeViewController { // }) // .store(in: &disposeBag) // -// // setup snap behavior -// Publishers.CombineLatest( -// viewModel.$repliedToCellFrame, -// viewModel.$collectionViewState -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] repliedToCellFrame, collectionViewState in -// guard let self = self else { return } -// guard repliedToCellFrame != .zero else { return } -// switch collectionViewState { -// case .fold: -// self.tableView.contentInset.top = -repliedToCellFrame.height -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: set contentInset.top: -%s", ((#file as NSString).lastPathComponent), #line, #function, repliedToCellFrame.height.description) -// -// case .expand: -// self.tableView.contentInset.top = 0 -// } -// } -// .store(in: &disposeBag) -// // configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar.value) // Publishers.CombineLatest( // keyboardHasShortcutBar, @@ -746,17 +714,17 @@ extension ComposeViewController { // //} // -//extension ComposeViewController { -// -// @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) { -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) +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 // } -// dismiss(animated: true, completion: nil) -// } -// + 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 { @@ -779,9 +747,9 @@ extension ComposeViewController { // // dismiss(animated: true, completion: nil) // } -// -//} -// + +} + //// MARK: - MetaTextDelegate //extension ComposeViewController: MetaTextDelegate { // func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? { @@ -1020,58 +988,7 @@ extension ComposeViewController { // } // //} -// -//// MARK: - UIScrollViewDelegate -//extension ComposeViewController { -//// func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { -//// guard scrollView === tableView else { return } -//// -//// let repliedToCellFrame = viewModel.repliedToCellFrame -//// guard repliedToCellFrame != .zero else { return } -//// -//// // try to find some patterns: -//// // print(""" -//// // repliedToCellFrame: \(viewModel.repliedToCellFrame.value.height) -//// // scrollView.contentOffset.y: \(scrollView.contentOffset.y) -//// // scrollView.contentSize.height: \(scrollView.contentSize.height) -//// // scrollView.frame: \(scrollView.frame) -//// // scrollView.adjustedContentInset.top: \(scrollView.adjustedContentInset.top) -//// // scrollView.adjustedContentInset.bottom: \(scrollView.adjustedContentInset.bottom) -//// // """) -//// -//// switch viewModel.collectionViewState { -//// case .fold: -//// os_log("%{public}s[%{public}ld], %{public}s: fold", ((#file as NSString).lastPathComponent), #line, #function) -//// guard velocity.y < 0 else { return } -//// let offsetY = scrollView.contentOffset.y + scrollView.adjustedContentInset.top -//// if offsetY < -44 { -//// tableView.contentInset.top = 0 -//// targetContentOffset.pointee = CGPoint(x: 0, y: -scrollView.adjustedContentInset.top) -//// viewModel.collectionViewState = .expand -//// } -//// -//// case .expand: -//// os_log("%{public}s[%{public}ld], %{public}s: expand", ((#file as NSString).lastPathComponent), #line, #function) -//// guard velocity.y > 0 else { return } -//// // check if top across -//// let topOffset = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) - repliedToCellFrame.height -//// -//// // check if bottom bounce -//// let bottomOffsetY = scrollView.contentOffset.y + (scrollView.frame.height - scrollView.adjustedContentInset.bottom) -//// let bottomOffset = bottomOffsetY - scrollView.contentSize.height -//// -//// if topOffset > 44 { -//// // do not interrupt user scrolling -//// viewModel.collectionViewState = .fold -//// } else if bottomOffset > 44 { -//// tableView.contentInset.top = -repliedToCellFrame.height -//// targetContentOffset.pointee = CGPoint(x: 0, y: -repliedToCellFrame.height) -//// viewModel.collectionViewState = .fold -//// } -//// } -//// } -//} -// + //// MARK: - UITableViewDelegate //extension ComposeViewController: UITableViewDelegate { } // diff --git a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser.swift index 02a983680..6d952726c 100644 --- a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser.swift @@ -8,6 +8,7 @@ import Foundation import CoreDataStack import MastodonSDK +import MastodonMeta extension MastodonUser { @@ -57,7 +58,7 @@ extension MastodonUser { } extension MastodonUser { - + public var profileURL: URL { if let urlString = self.url, let url = URL(string: urlString) { @@ -72,4 +73,30 @@ extension MastodonUser { items.append(profileURL) return items } + +} + +extension MastodonUser { + public var nameMetaContent: MastodonMetaContent? { + do { + let content = MastodonContent(content: displayNameWithFallback, emojis: emojis.asDictionary) + let metaContent = try MastodonMetaContent.convert(document: content) + return metaContent + } catch { + assertionFailure() + return nil + } + } + + public var bioMetaContent: MastodonMetaContent? { + guard let note = note else { return nil } + do { + let content = MastodonContent(content: note, emojis: emojis.asDictionary) + let metaContent = try MastodonMetaContent.convert(document: content) + return metaContent + } catch { + assertionFailure() + return nil + } + } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentView.swift deleted file mode 100644 index 886634d7e..000000000 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentView.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// ComposeContentView.swift -// -// -// Created by MainasuK on 22/9/30. -// - -import SwiftUI - -public struct ComposeContentView: View { - - @ObservedObject var viewModel: ComposeContentViewModel - - @State var contentOffsetDelta: CGFloat = .zero - - public var body: some View { - ScrollView { - VStack(spacing: .zero) { - GeometryReader { geometry in - Color.clear.preference( - key: ScrollOffsetPreferenceKey.self, - value: geometry.frame(in: .named("scrollView")).origin - ) - }.frame(width: 0, height: 0) - .onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in - print("contentOffset: \(offset)") - } - VStack { - Text("Reply") - } - .frame(height: 100) - .frame(maxWidth: .infinity) - .background(Color.blue) - .background( - GeometryReader { geometry in - Color.clear.preference( - key: ViewFramePreferenceKey.self, - value: geometry.frame(in: .named("scrollView")) - ) - } - .onPreferenceChange(ViewFramePreferenceKey.self) { frame in - print("reply frame: \(frame)") - } - ) - VStack { - Text("Content") - } - .frame(maxWidth: .infinity) - .background(Color.orange) - } // end VStack - .offset(y: contentOffsetDelta) - } // end ScrollView - .coordinateSpace(name: "scrollView") - } // end body -} - -private struct ScrollOffsetPreferenceKey: PreferenceKey { - static var defaultValue: CGPoint = .zero - - static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { } -} - -private struct ViewFramePreferenceKey: PreferenceKey { - static var defaultValue: CGRect = .zero - - static func reduce(value: inout CGRect, nextValue: () -> CGRect) { } -} - -//struct ScrollView: View { -// let axes: Axis.Set -// let showsIndicators: Bool -// let offsetChanged: (CGPoint) -> Void -// let content: Content -// -// init( -// axes: Axis.Set = .vertical, -// showsIndicators: Bool = true, -// offsetChanged: @escaping (CGPoint) -> Void = { _ in }, -// @ViewBuilder content: () -> Content -// ) { -// self.axes = axes -// self.showsIndicators = showsIndicators -// self.offsetChanged = offsetChanged -// self.content = content() -// } -// -// var body: some View { -// SwiftUI.ScrollView(axes, showsIndicators: showsIndicators) { -// GeometryReader { geometry in -// Color.clear.preference( -// key: ScrollOffsetPreferenceKey.self, -// value: geometry.frame(in: .named("scrollView")).origin -// ) -// }.frame(width: 0, height: 0) -// content -// } -// .coordinateSpace(name: "scrollView") -// .onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: offsetChanged) -// } -//} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index 240e756a1..4c0a71f60 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -8,15 +8,18 @@ import os.log import UIKit import SwiftUI +import Combine public final class ComposeContentViewController: UIViewController { let logger = Logger(subsystem: "ComposeContentViewController", category: "ViewController") + var disposeBag = Set() public var viewModel: ComposeContentViewModel! let tableView: ComposeTableView = { let tableView = ComposeTableView() + tableView.estimatedRowHeight = UITableView.automaticDimension tableView.alwaysBounceVertical = true tableView.separatorStyle = .none tableView.tableFooterView = UIView() @@ -45,6 +48,96 @@ extension ComposeContentViewController { tableView.delegate = self viewModel.setupDataSource(tableView: tableView) + + // setup snap behavior + Publishers.CombineLatest( + viewModel.$replyToCellFrame, + viewModel.$scrollViewState + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] replyToCellFrame, scrollViewState in + guard let self = self else { return } + guard replyToCellFrame != .zero else { return } + switch scrollViewState { + case .fold: + self.tableView.contentInset.top = -replyToCellFrame.height + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: set contentInset.top: -%s", ((#file as NSString).lastPathComponent), #line, #function, replyToCellFrame.height.description) + case .expand: + self.tableView.contentInset.top = 0 + } + } + .store(in: &disposeBag) + } + + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + viewModel.viewLayoutFrame.update(view: view) + } + + public override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + + viewModel.viewLayoutFrame.update(view: view) + } + + public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + coordinator.animate { [weak self] coordinatorContext in + guard let self = self else { return } + self.viewModel.viewLayoutFrame.update(view: self.view) + } + } +} + +// MARK: - UIScrollViewDelegate +extension ComposeContentViewController { + public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + guard scrollView === tableView else { return } + + let replyToCellFrame = viewModel.replyToCellFrame + guard replyToCellFrame != .zero else { return } + + // try to find some patterns: + // print(""" + // repliedToCellFrame: \(viewModel.repliedToCellFrame.value.height) + // scrollView.contentOffset.y: \(scrollView.contentOffset.y) + // scrollView.contentSize.height: \(scrollView.contentSize.height) + // scrollView.frame: \(scrollView.frame) + // scrollView.adjustedContentInset.top: \(scrollView.adjustedContentInset.top) + // scrollView.adjustedContentInset.bottom: \(scrollView.adjustedContentInset.bottom) + // """) + + switch viewModel.scrollViewState { + case .fold: + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fold") + guard velocity.y < 0 else { return } + let offsetY = scrollView.contentOffset.y + scrollView.adjustedContentInset.top + if offsetY < -44 { + tableView.contentInset.top = 0 + targetContentOffset.pointee = CGPoint(x: 0, y: -scrollView.adjustedContentInset.top) + viewModel.scrollViewState = .expand + } + + case .expand: + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): expand") + guard velocity.y > 0 else { return } + // check if top across + let topOffset = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) - replyToCellFrame.height + + // check if bottom bounce + let bottomOffsetY = scrollView.contentOffset.y + (scrollView.frame.height - scrollView.adjustedContentInset.bottom) + let bottomOffset = bottomOffsetY - scrollView.contentSize.height + + if topOffset > 44 { + // do not interrupt user scrolling + viewModel.scrollViewState = .fold + } else if bottomOffset > 44 { + tableView.contentInset.top = -replyToCellFrame.height + targetContentOffset.pointee = CGPoint(x: 0, y: -replyToCellFrame.height) + viewModel.scrollViewState = .fold + } + } } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift index e1ad561ef..3f6028b56 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift @@ -8,6 +8,7 @@ import UIKit import MastodonCore import CoreDataStack +import UIHostingConfigurationBackport extension ComposeContentViewModel { @@ -25,21 +26,37 @@ extension ComposeContentViewModel { enum Section: CaseIterable { case replyTo case status - case attachment - case poll } - private func setupTableViewCell(tableView: UITableView) { + private func setupTableViewCell(tableView: UITableView) { + composeContentTableViewCell.contentConfiguration = UIHostingConfigurationBackport { + ComposeContentView(viewModel: self) + } + + $contentCellFrame + .map { $0.height } + .removeDuplicates() + .sink { [weak self] height in + guard let self = self else { return } + guard !tableView.visibleCells.isEmpty else { return } + UIView.performWithoutAnimation { + tableView.beginUpdates() + self.composeContentTableViewCell.frame.size.height = height + tableView.endUpdates() + } + } + .store(in: &disposeBag) + switch kind { case .post: break case .reply(let status): let cell = composeReplyToTableViewCell // bind frame publisher -// cell.framePublisher -// .receive(on: DispatchQueue.main) -// .assign(to: \.repliedToCellFrame, on: self) -// .store(in: &cell.disposeBag) + cell.$framePublisher + .receive(on: DispatchQueue.main) + .assign(to: \.replyToCellFrame, on: self) + .store(in: &cell.disposeBag) // set initial width cell.statusView.frame.size.width = tableView.frame.width @@ -70,8 +87,6 @@ extension ComposeContentViewModel: UITableViewDataSource { default: return 0 } case .status: return 1 - case .attachment: return 1 - case .poll: return 1 } } @@ -80,11 +95,7 @@ extension ComposeContentViewModel: UITableViewDataSource { case .replyTo: return composeReplyToTableViewCell case .status: - return UITableViewCell() - case .attachment: - return UITableViewCell() - case .poll: - return UITableViewCell() + return composeContentTableViewCell } } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 9f0eed2fe..24736cc5e 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -5,29 +5,79 @@ // Created by MainasuK on 22/9/30. // +import os.log import Foundation +import Combine import CoreDataStack import MastodonCore +import Meta +import MastodonMeta public final class ComposeContentViewModel: NSObject, ObservableObject { + let logger = Logger(subsystem: "ComposeContentViewModel", category: "ViewModel") + + var disposeBag = Set() + // tableViewCell let composeReplyToTableViewCell = ComposeReplyToTableViewCell() + let composeContentTableViewCell = ComposeContentTableViewCell() // input let context: AppContext let kind: Kind + + @Published var viewLayoutFrame = ViewLayoutFrame() + @Published var authContext: AuthContext + + // output + + // content + @Published public var initialContent = "" + @Published public var content = "" + @Published public var contentWeightedLength = 0 + @Published public var isContentEmpty = true + @Published public var isContentValid = true + @Published public var isContentEditing = false + + // author + @Published var avatarURL: URL? + @Published var name: MetaContent = PlaintextMetaContent(string: "") + @Published var username: String = "" + + // UI & UX + @Published var replyToCellFrame: CGRect = .zero + @Published var contentCellFrame: CGRect = .zero + @Published var scrollViewState: ScrollViewState = .fold + public init( context: AppContext, + authContext: AuthContext, kind: Kind ) { self.context = context + self.authContext = authContext self.kind = kind super.init() // end init + + // bind author + $authContext + .sink { [weak self] authContext in + guard let self = self else { return } + guard let user = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return } + self.avatarURL = user.avatarImageURL() + self.name = user.nameMetaContent ?? PlaintextMetaContent(string: user.displayNameWithFallback) + self.username = user.acctWithDomain + } + .store(in: &disposeBag) } + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + } extension ComposeContentViewModel { @@ -38,7 +88,7 @@ extension ComposeContentViewModel { case reply(status: ManagedObjectRecord) } - public enum ViewState { + public enum ScrollViewState { case fold // snap to input case expand // snap to reply } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusContentTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeContentTableViewCell.swift similarity index 89% rename from MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusContentTableViewCell.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeContentTableViewCell.swift index e9b8cf068..3a646f1fc 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusContentTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeContentTableViewCell.swift @@ -1,5 +1,5 @@ // -// ComposeStatusContentTableViewCell.swift +// ComposeContentTableViewCell.swift // Mastodon // // Created by MainasuK Cirno on 2021-6-28. @@ -12,16 +12,16 @@ import MetaTextKit import UITextView_Placeholder import MastodonAsset import MastodonLocalization -import MastodonUI +import UIHostingConfigurationBackport //protocol ComposeStatusContentTableViewCellDelegate: AnyObject { // func composeStatusContentTableViewCell(_ cell: ComposeStatusContentTableViewCell, textViewShouldBeginEditing textView: UITextView) -> Bool //} -final class ComposeStatusContentTableViewCell: UITableViewCell { +final class ComposeContentTableViewCell: UITableViewCell { + + let logger = Logger(subsystem: "ComposeContentTableViewCell", category: "View") -// let logger = Logger(subsystem: "ComposeStatusContentTableViewCell", category: "View") -// // var disposeBag = Set() // weak var delegate: ComposeStatusContentTableViewCellDelegate? // @@ -74,27 +74,26 @@ final class ComposeStatusContentTableViewCell: UITableViewCell { // metaText.delegate = nil // metaText.textView.delegate = nil // } -// -// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { -// super.init(style: style, reuseIdentifier: reuseIdentifier) -// _init() -// } -// -// required init?(coder: NSCoder) { -// super.init(coder: coder) -// _init() -// } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } } -extension ComposeStatusContentTableViewCell { +extension ComposeContentTableViewCell { + + private func _init() { + selectionStyle = .none + layer.zPosition = 999 + backgroundColor = .clear -// private func _init() { -// selectionStyle = .none -// layer.zPosition = 999 -// backgroundColor = .clear -// preservesSuperviewLayoutMargins = true -// // let containerStackView = UIStackView() // containerStackView.axis = .vertical // containerStackView.translatesAutoresizingMaskIntoConstraints = false @@ -134,7 +133,7 @@ extension ComposeStatusContentTableViewCell { // metaText.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 64).priority(.defaultHigh), // ]) // statusContentWarningEditorView.textView.delegate = self -// } + } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift new file mode 100644 index 000000000..2b9f79321 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift @@ -0,0 +1,97 @@ +// +// ComposeContentView.swift +// +// +// Created by MainasuK on 22/9/30. +// + +import os.log +import SwiftUI +import MastodonLocalization + +public struct ComposeContentView: View { + + static let logger = Logger(subsystem: "ComposeContentView", category: "View") + var logger: Logger { ComposeContentView.logger } + + static var margin: CGFloat = 16 + + @ObservedObject var viewModel: ComposeContentViewModel + + public var body: some View { + VStack(spacing: .zero) { + Group { + authorView + .padding(.top, 14) + MetaTextViewRepresentable( + string: $viewModel.content, + width: viewModel.viewLayoutFrame.layoutFrame.width - ComposeContentView.margin * 2, + configurationHandler: { metaText in + metaText.textView.attributedPlaceholder = { + var attributes = metaText.textAttributes + attributes[.foregroundColor] = UIColor.secondaryLabel + return NSAttributedString( + string: L10n.Scene.Compose.contentInputPlaceholder, + attributes: attributes + ) + }() + metaText.textView.keyboardType = .twitter + // metaText.textView.tag = ComposeContentViewModel.MetaTextViewKind.content.rawValue + // metaText.textView.delegate = viewModel + // metaText.delegate = viewModel + metaText.textView.becomeFirstResponder() + } + ) + .frame(minHeight: 100) + .fixedSize(horizontal: false, vertical: true) + } + .background( + GeometryReader { proxy in + Color.clear.preference(key: ViewFramePreferenceKey.self, value: proxy.frame(in: .local)) + } + .onPreferenceChange(ViewFramePreferenceKey.self) { frame in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): content frame: \(frame.debugDescription)") + viewModel.contentCellFrame = frame + } + ) + Spacer() + } // end VStack + .padding(.horizontal, ComposeContentView.margin) +// .frame(alignment: .top) + } // end body +} + +extension ComposeContentView { + var authorView: some View { + HStack(spacing: 8) { + AnimatedImage(imageURL: viewModel.avatarURL) + .frame(width: 46, height: 46) + .background(Color(UIColor.systemFill)) + .cornerRadius(12) + VStack(alignment: .leading, spacing: 4) { + Spacer() + MetaLabelRepresentable( + textStyle: .statusName, + metaContent: viewModel.name + ) + Text(viewModel.username) + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + } + Spacer() + } + } +} + +//private struct ScrollOffsetPreferenceKey: PreferenceKey { +// static var defaultValue: CGPoint = .zero +// +// static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { } +//} + +private struct ViewFramePreferenceKey: PreferenceKey { + static var defaultValue: CGRect = .zero + + static func reduce(value: inout CGRect, nextValue: () -> CGRect) { } +} diff --git a/MastodonSDK/Sources/MastodonUI/SwiftUI/MetaLabelRepresentable.swift b/MastodonSDK/Sources/MastodonUI/SwiftUI/MetaLabelRepresentable.swift new file mode 100644 index 000000000..7a671494a --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/SwiftUI/MetaLabelRepresentable.swift @@ -0,0 +1,47 @@ +// +// MetaLabelRepresentable.swift +// +// +// Created by MainasuK on 22/10/11. +// + +import UIKit +import SwiftUI +import MastodonCore +import MetaTextKit + +public struct MetaLabelRepresentable: UIViewRepresentable { + + public let textStyle: MetaLabel.Style + public let metaContent: MetaContent + + public init( + textStyle: MetaLabel.Style, + metaContent: MetaContent + ) { + self.textStyle = textStyle + self.metaContent = metaContent + } + + public func makeUIView(context: Context) -> MetaLabel { + let view = MetaLabel(style: textStyle) + view.isUserInteractionEnabled = false + return view + } + + public func updateUIView(_ view: MetaLabel, context: Context) { + view.configure(content: metaContent) + } + +} + +#if DEBUG +struct MetaLabelRepresentable_Preview: PreviewProvider { + static var previews: some View { + MetaLabelRepresentable( + textStyle: .statusUsername, + metaContent: PlaintextMetaContent(string: "Name") + ) + } +} +#endif diff --git a/MastodonSDK/Sources/MastodonUI/SwiftUI/MetaTextViewRepresentable.swift b/MastodonSDK/Sources/MastodonUI/SwiftUI/MetaTextViewRepresentable.swift new file mode 100644 index 000000000..8796feb06 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/SwiftUI/MetaTextViewRepresentable.swift @@ -0,0 +1,82 @@ +// +// MetaTextViewRepresentable.swift +// +// +// Created by MainasuK Cirno on 2021-7-16. +// + +import UIKit +import SwiftUI +import UITextView_Placeholder +import MetaTextKit +import MastodonAsset +import MastodonCore + +public struct MetaTextViewRepresentable: UIViewRepresentable { + + let metaText = MetaText() + + // input + @Binding var string: String + let width: CGFloat + + // handler + let configurationHandler: (MetaText) -> Void + + public func makeUIView(context: Context) -> MetaTextView { + let textView = metaText.textView + + textView.backgroundColor = .clear // clear background + textView.textContainer.lineFragmentPadding = 0 // remove leading inset + textView.isScrollEnabled = false // enable dynamic height + + // set width constraint + textView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + textView.widthAnchor.constraint(equalToConstant: width).priority(.required - 1) + ]) + // make textView horizontal filled + textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + + // setup editor appearance + let font = UIFont.preferredFont(forTextStyle: .body) + metaText.textView.font = font + metaText.textAttributes = [ + .font: font, + .foregroundColor: UIColor.label, + ] + metaText.linkAttributes = [ + .font: font, + .foregroundColor: Asset.Colors.brand.color, + ] + + configurationHandler(metaText) + + metaText.configure(content: PlaintextMetaContent(string: string)) + + return textView + } + + public func updateUIView(_ metaTextView: MetaTextView, context: Context) { + // update layout + context.coordinator.widthLayoutConstraint.constant = width + } + + public func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + public class Coordinator: NSObject, UITextViewDelegate { + let view: MetaTextViewRepresentable + var widthLayoutConstraint: NSLayoutConstraint! + + init(_ view: MetaTextViewRepresentable) { + self.view = view + super.init() + + widthLayoutConstraint = view.metaText.textView.widthAnchor.constraint(equalToConstant: 100) + widthLayoutConstraint.isActive = true + } + } + +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Utility/ViewLayoutFrame.swift b/MastodonSDK/Sources/MastodonUI/View/Utility/ViewLayoutFrame.swift new file mode 100644 index 000000000..183364abc --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/View/Utility/ViewLayoutFrame.swift @@ -0,0 +1,57 @@ +// +// ViewLayoutFrame.swift +// +// +// Created by MainasuK on 2022-8-17. +// + +import os.log +import UIKit +import CoreGraphics + +public struct ViewLayoutFrame { + let logger = Logger(subsystem: "ViewLayoutFrame", category: "ViewLayoutFrame") + + public var layoutFrame: CGRect + public var safeAreaLayoutFrame: CGRect + public var readableContentLayoutFrame: CGRect + + public init( + layoutFrame: CGRect = .zero, + safeAreaLayoutFrame: CGRect = .zero, + readableContentLayoutFrame: CGRect = .zero + ) { + self.layoutFrame = layoutFrame + self.safeAreaLayoutFrame = safeAreaLayoutFrame + self.readableContentLayoutFrame = readableContentLayoutFrame + } +} + +extension ViewLayoutFrame { + public mutating func update(view: UIView) { + guard view.window != nil else { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): layoutFrame update for a view without attached window. Skip this invalid update") + return + } + + let layoutFrame = view.frame + if self.layoutFrame != layoutFrame { + self.layoutFrame = layoutFrame + } + + let safeAreaLayoutFrame = view.safeAreaLayoutGuide.layoutFrame + if self.safeAreaLayoutFrame != safeAreaLayoutFrame { + self.safeAreaLayoutFrame = safeAreaLayoutFrame + } + + let readableContentLayoutFrame = view.readableContentGuide.layoutFrame + if self.readableContentLayoutFrame != readableContentLayoutFrame { + self.readableContentLayoutFrame = readableContentLayoutFrame + } + + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): layoutFrame: \(layoutFrame.debugDescription)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): safeAreaLayoutFrame: \(safeAreaLayoutFrame.debugDescription)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): readableContentLayoutFrame: \(readableContentLayoutFrame.debugDescription)") + + } +} From f1b5c52815b7ad15ae6d9ba8b8f7f7557ce0f7ae Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 18 Oct 2022 19:01:31 +0800 Subject: [PATCH 041/658] feat: restore the compose toolbar layout using SwiftUI --- Mastodon.xcodeproj/project.pbxproj | 12 +- .../xcschemes/xcschememanagement.plist | 4 +- .../Scene/Compose/ComposeViewController.swift | 102 +---------- .../Scene/Compose/Contents.json | 9 + .../Compose/Earth.imageset/Contents.json | 15 ++ .../Scene/Compose/Earth.imageset/Earth.pdf | 169 ++++++++++++++++++ .../button.tint.colorset/Contents.json | 38 ++++ .../chat.warning.imageset/Contents.json | 15 ++ .../chat.warning.imageset/chat.warning.pdf | 101 +++++++++++ .../Compose/emoji.imageset/Contents.json | 15 ++ .../Scene/Compose/emoji.imageset/emoji.pdf | 99 ++++++++++ .../Compose/media.imageset/Contents.json | 15 ++ .../Scene/Compose/media.imageset/media.pdf | 111 ++++++++++++ .../Scene/Compose/poll.imageset/Contents.json | 15 ++ .../Scene/Compose/poll.imageset/poll.pdf | 113 ++++++++++++ .../MastodonAsset/Generated/Assets.swift | 8 + .../Entity/Mastodon+Entity+Status.swift | 2 +- .../ComposeContentViewController.swift | 135 ++++++++++++++ .../ComposeContentToolbarView+ViewModel.swift | 57 ++++++ .../View/ComposeContentToolbarView.swift | 82 +++++++++ 20 files changed, 1007 insertions(+), 110 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Earth.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Earth.imageset/Earth.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/button.tint.colorset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/chat.warning.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/chat.warning.imageset/chat.warning.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/emoji.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/emoji.imageset/emoji.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/media.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/media.imageset/media.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/poll.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/poll.imageset/poll.pdf create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index d2eda9b89..66c28a399 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -3048,7 +3048,7 @@ DB025B8E278D6448002F581E /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; - buildActionMask = 8; + buildActionMask = 12; files = ( ); inputFileListPaths = ( @@ -3059,14 +3059,14 @@ ); outputPaths = ( ); - runOnlyForDeploymentPostprocessing = 1; + runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "if [[ -f \"${PODS_ROOT}/Sourcery/bin/sourcery\" ]]; then\n \"${PODS_ROOT}/Sourcery/bin/sourcery\" --config ./MastodonSDK/Sources/CoreDataStack\nelse\n echo \"warning: Sourcery is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; }; DB3D100425BAA71500EAA174 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; - buildActionMask = 8; + buildActionMask = 12; files = ( ); inputFileListPaths = ( @@ -3077,14 +3077,14 @@ ); outputPaths = ( ); - runOnlyForDeploymentPostprocessing = 1; + runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "if [[ -f \"${PODS_ROOT}/SwiftGen/bin/swiftgen\" ]]; then\n \"${PODS_ROOT}/SwiftGen/bin/swiftgen\" \nelse\n echo \"warning: SwiftGen is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; }; DB697DD2278F48D5004EF2F7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; - buildActionMask = 8; + buildActionMask = 12; files = ( ); inputFileListPaths = ( @@ -3095,7 +3095,7 @@ ); outputPaths = ( ); - runOnlyForDeploymentPostprocessing = 1; + runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "if [[ -f \"${PODS_ROOT}/Sourcery/bin/sourcery\" ]]; then\n \"${PODS_ROOT}/Sourcery/bin/sourcery\" --config ./Mastodon\nelse\n echo \"warning: Sourcery is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; }; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 33eda54f1..31423978e 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -112,12 +112,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 18 + 17 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 17 + 18 SuppressBuildableAutocreation diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 1d847f040..c94db4def 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -207,24 +207,6 @@ extension ComposeViewController { // self.title = title // } // .store(in: &disposeBag) -// self.setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) -// ThemeService.shared.currentTheme -// .receive(on: RunLoop.main) -// .sink { [weak self] theme in -// guard let self = self else { return } -// self.setupBackgroundColor(theme: theme) -// } -// .store(in: &disposeBag) -// -// -// scrollView.translatesAutoresizingMaskIntoConstraints = false -// view.addSubview(scrollView) -// NSLayoutConstraint.activate([ -// scrollView.topAnchor.constraint(equalTo: view.topAnchor), -// scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), -// scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), -// scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), -// ]) // // composeToolbarView.translatesAutoresizingMaskIntoConstraints = false // view.addSubview(composeToolbarView) @@ -286,89 +268,7 @@ extension ComposeViewController { // // update layout when keyboard show/dismiss // view.layoutIfNeeded() // -// 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( -// keyboardEventPublishers, -// viewModel.$isCustomEmojiComposing, -// viewModel.$autoCompleteInfo -// ) -// .sink(receiveValue: { [weak self] keyboardEvents, isCustomEmojiComposing, 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 = self.composeToolbarView.frame.height -// if autoCompleteInfo != nil { -// margin += ComposeViewController.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 -// } -// -// UIView.animate(withDuration: 0.3) { -// self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom -// if self.view.window != nil { -// self.view.layoutIfNeeded() -// } -// } -// 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 -// -// // adjust inset for tableView -// let contentFrame = self.view.convert(self.tableView.frame, to: nil) -// let padding = contentFrame.maxY + extraMargin - endFrame.minY -// guard padding > 0 else { -// self.tableView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin -// self.tableView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin -// return -// } -// -// self.tableView.contentInset.bottom = padding - self.view.safeAreaInsets.bottom -// self.tableView.verticalScrollIndicatorInsets.bottom = padding - self.view.safeAreaInsets.bottom -// UIView.animate(withDuration: 0.3) { -// self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height -// self.view.layoutIfNeeded() -// } -// }) -// .store(in: &disposeBag) + // // // bind auto-complete // viewModel.$autoCompleteInfo diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Contents.json new file mode 100644 index 000000000..6e965652d --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/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/Earth.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Earth.imageset/Contents.json new file mode 100644 index 000000000..04f310f98 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Earth.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Earth.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Earth.imageset/Earth.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Earth.imageset/Earth.pdf new file mode 100644 index 000000000..4f6b948a3 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Earth.imageset/Earth.pdf @@ -0,0 +1,169 @@ +%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.000122 2.000000 cm +0.000000 0.000000 0.000000 scn +8.945436 19.952877 m +8.950368 19.945572 l +9.295332 19.981556 9.645513 20.000000 10.000000 20.000000 c +15.522847 20.000000 20.000000 15.522847 20.000000 10.000000 c +20.000000 4.477153 15.522847 0.000000 10.000000 0.000000 c +6.790722 0.000000 3.934541 1.511787 2.104761 3.862055 c +2.102194 3.862638 l +2.102672 3.864738 l +0.784842 5.558613 0.000000 7.687653 0.000000 10.000000 c +0.000000 15.161964 3.911163 19.410429 8.931705 19.943607 c +8.945436 19.952877 l +h +10.000000 18.500000 m +9.946768 18.500000 9.893649 18.499510 9.840650 18.498537 c +9.963233 18.254343 10.094863 17.965696 10.214363 17.648212 c +10.561299 16.726484 10.880171 15.367099 10.314304 14.162127 c +9.791718 13.049316 8.889565 12.761408 8.224188 12.589492 c +8.139534 12.567652 l +7.483193 12.398458 7.230809 12.333397 7.046710 12.053902 c +6.877729 11.797358 6.903327 11.471569 7.108089 10.804317 c +7.122483 10.757413 7.138054 10.707805 7.154294 10.656069 c +7.235397 10.397685 7.333165 10.086211 7.384129 9.793275 c +7.447527 9.428862 7.465442 8.965590 7.232083 8.517794 c +7.000589 8.073575 6.693745 7.770780 6.331198 7.573263 c +5.990655 7.387735 5.637942 7.317377 5.374053 7.270582 c +5.281080 7.254177 l +4.766211 7.163565 4.519922 7.120219 4.280048 6.863250 c +4.093846 6.663777 3.973670 6.311463 3.903486 5.785102 c +3.874918 5.570853 3.857739 5.358463 3.839978 5.138891 c +3.830442 5.021761 l +3.810462 4.779471 3.785685 4.500609 3.731205 4.261164 c +3.730906 4.259850 l +5.284881 2.563602 7.518231 1.500000 10.000000 1.500000 c +11.577014 1.500000 13.053720 1.929466 14.319545 2.677820 c +14.221224 2.777969 14.114439 2.895618 14.009129 3.028202 c +13.669640 3.455608 13.224341 4.191939 13.378761 5.060995 c +13.453023 5.478927 13.677018 5.828774 13.893493 6.097116 c +14.114051 6.370518 14.380263 6.623110 14.613050 6.837366 c +14.668355 6.888268 14.721365 6.936671 14.772196 6.983084 c +14.950412 7.145808 15.101837 7.284072 15.231435 7.419845 c +15.404221 7.600864 15.441820 7.682460 15.443790 7.686735 c +15.511713 7.911400 15.428436 8.070621 15.337708 8.140779 c +15.292102 8.176043 15.230948 8.201571 15.147898 8.202232 c +15.064073 8.202900 14.928219 8.177752 14.746777 8.062835 c +14.537054 7.930006 14.232018 7.847993 13.911026 7.977239 c +13.643642 8.084899 13.495515 8.290975 13.424360 8.408617 c +13.280478 8.646499 13.199624 8.954817 13.146976 9.180877 c +13.106362 9.355261 13.067616 9.553251 13.032258 9.733932 c +13.018108 9.806237 13.004500 9.875771 12.991533 9.939910 c +12.941022 10.189741 12.898354 10.368218 12.857431 10.479053 c +12.856843 10.480482 12.851788 10.492748 12.838216 10.517543 c +12.823483 10.544457 12.802644 10.579025 12.774174 10.622349 c +12.716190 10.710581 12.640428 10.814299 12.546493 10.938749 c +12.512385 10.983936 12.475714 11.032014 12.437285 11.082394 c +12.276200 11.293579 12.084242 11.545244 11.920969 11.794048 c +11.725189 12.092388 11.503861 12.482321 11.433911 12.898157 c +11.396839 13.118542 11.397423 13.373061 11.488866 13.631610 c +11.582505 13.896370 11.753467 14.114110 11.975435 14.280597 c +12.458843 14.643174 13.168970 15.453171 13.798772 16.239641 c +14.086361 16.598770 14.343374 16.935246 14.534719 17.190622 c +13.222366 18.019987 11.667240 18.500000 10.000000 18.500000 c +h +15.727353 16.280794 m +15.529826 16.017342 15.265779 15.671862 14.969622 15.302032 c +14.367900 14.550627 13.570269 13.617192 12.920100 13.114614 c +12.945479 13.015734 13.020371 12.852727 13.175053 12.617016 c +13.306167 12.417215 13.456026 12.220543 13.614124 12.013055 c +13.656691 11.957191 13.700173 11.900127 13.743729 11.842422 c +13.916128 11.614019 14.155027 11.295321 14.264583 10.998598 c +14.350787 10.765120 14.412687 10.480006 14.461784 10.237164 c +14.479100 10.151520 14.495111 10.069643 14.510527 9.990811 c +14.536025 9.860416 14.559895 9.738358 14.585339 9.621376 c +15.187048 9.793123 15.787111 9.689411 16.255274 9.327401 c +16.863905 8.856771 17.118000 8.041076 16.879692 7.252826 c +16.770346 6.891145 16.515705 6.592863 16.316484 6.384149 c +16.147512 6.207124 15.944934 6.022339 15.761238 5.854778 c +15.715802 5.813333 15.671200 5.772646 15.628872 5.733688 c +15.398922 5.522042 15.205485 5.334450 15.060962 5.155299 c +14.912354 4.971087 14.865835 4.856014 14.855629 4.798573 c +14.816802 4.580065 14.923186 4.289124 15.183693 3.961153 c +15.301805 3.812452 15.427599 3.687178 15.525196 3.598549 c +15.536726 3.588078 15.547768 3.578207 15.558244 3.568960 c +17.360012 5.127576 18.500000 7.430659 18.500000 10.000000 c +18.500000 12.488004 17.431046 14.726340 15.727353 16.280794 c +h +1.500000 10.000000 m +1.500000 8.601643 1.837670 7.282152 2.435920 6.118621 c +2.520806 6.676047 2.697947 7.366606 3.183541 7.886808 c +3.783359 8.529375 4.519145 8.649875 4.981673 8.725623 c +5.027948 8.733203 5.071755 8.740378 5.112145 8.747540 c +5.359830 8.791461 5.503073 8.830262 5.613584 8.890469 c +5.702090 8.938686 5.801398 9.018201 5.901872 9.211002 c +5.916738 9.239530 5.944188 9.318548 5.906326 9.536173 c +5.873906 9.722527 5.813122 9.917411 5.733576 10.172447 c +5.714817 10.232594 5.694809 10.296745 5.674091 10.364261 c +5.488935 10.967622 5.193007 11.966541 5.794037 12.879015 c +6.315677 13.670959 7.154699 13.873423 7.687307 14.001944 c +7.745055 14.015879 7.799201 14.028945 7.848949 14.041800 c +8.411874 14.187244 8.732245 14.322063 8.956565 14.799735 c +9.251945 15.428725 9.124854 16.284683 8.810515 17.119806 c +8.661468 17.515793 8.486593 17.863750 8.348099 18.113476 c +8.304624 18.191868 8.265136 18.259853 8.231707 18.315813 c +4.385974 17.502075 1.500000 14.088065 1.500000 10.000000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 5594 +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 +0000005684 00000 n +0000005707 00000 n +0000005880 00000 n +0000005954 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +6013 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/button.tint.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/button.tint.colorset/Contents.json new file mode 100644 index 000000000..9bdcecb67 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/button.tint.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x38", + "green" : "0x29", + "red" : "0x2B" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF9", + "green" : "0xF5", + "red" : "0xF5" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/chat.warning.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/chat.warning.imageset/Contents.json new file mode 100644 index 000000000..7ed4f66e6 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/chat.warning.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "chat.warning.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/chat.warning.imageset/chat.warning.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/chat.warning.imageset/chat.warning.pdf new file mode 100644 index 000000000..8b984cc82 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/chat.warning.imageset/chat.warning.pdf @@ -0,0 +1,101 @@ +%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.000000 1.858978 cm +0.000000 0.000000 0.000000 scn +10.000000 15.641022 m +10.414213 15.641022 10.750000 15.305235 10.750000 14.891022 c +10.750000 8.641022 l +10.750000 8.226809 10.414213 7.891022 10.000000 7.891022 c +9.585787 7.891022 9.250000 8.226809 9.250000 8.641022 c +9.250000 14.891022 l +9.250000 15.305235 9.585787 15.641022 10.000000 15.641022 c +h +10.000000 4.643219 m +10.552285 4.643219 11.000000 5.090935 11.000000 5.643219 c +11.000000 6.195504 10.552285 6.643219 10.000000 6.643219 c +9.447715 6.643219 9.000000 6.195504 9.000000 5.643219 c +9.000000 5.090935 9.447715 4.643219 10.000000 4.643219 c +h +10.000000 20.141022 m +15.522848 20.141022 20.000000 15.663870 20.000000 10.141022 c +20.000000 4.618174 15.522848 0.141022 10.000000 0.141022 c +8.381707 0.141022 6.817824 0.526447 5.412859 1.253002 c +1.587041 0.185684 l +0.922123 0.000010 0.232581 0.388512 0.046906 1.053431 c +-0.014536 1.273458 -0.014506 1.506115 0.046948 1.725969 c +1.114612 5.548796 l +0.386366 6.955047 0.000000 8.520757 0.000000 10.141022 c +0.000000 15.663870 4.477152 20.141022 10.000000 20.141022 c +h +10.000000 18.641022 m +5.305580 18.641022 1.500000 14.835442 1.500000 10.141022 c +1.500000 8.671413 1.872775 7.257639 2.573033 6.003563 c +2.723677 5.733776 l +1.610960 1.749634 l +5.597552 2.861805 l +5.867086 2.711519 l +7.120057 2.012886 8.532184 1.641022 10.000000 1.641022 c +14.694420 1.641022 18.500000 5.446602 18.500000 10.141022 c +18.500000 14.835442 14.694420 18.641022 10.000000 18.641022 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 1552 +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 +0000001642 00000 n +0000001665 00000 n +0000001838 00000 n +0000001912 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1971 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/emoji.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/emoji.imageset/Contents.json new file mode 100644 index 000000000..26d6caeb6 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/emoji.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emoji.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/emoji.imageset/emoji.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/emoji.imageset/emoji.pdf new file mode 100644 index 000000000..59180776b --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/emoji.imageset/emoji.pdf @@ -0,0 +1,99 @@ +%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 1.998444 1.997925 cm +0.000000 0.000000 0.000000 scn +10.001551 20.003113 m +15.525254 20.003113 20.003101 15.525267 20.003101 10.001562 c +20.003101 4.477859 15.525254 0.000013 10.001551 0.000013 c +4.477847 0.000013 0.000000 4.477859 0.000000 10.001562 c +0.000000 15.525267 4.477847 20.003113 10.001551 20.003113 c +h +10.001551 18.503113 m +5.306274 18.503113 1.500000 14.696838 1.500000 10.001562 c +1.500000 5.306286 5.306274 1.500013 10.001551 1.500013 c +14.696827 1.500013 18.503101 5.306286 18.503101 10.001562 c +18.503101 14.696838 14.696827 18.503113 10.001551 18.503113 c +h +6.463291 7.218245 m +7.312427 6.140525 8.603443 5.499995 10.001535 5.499995 c +11.397759 5.499995 12.687231 6.138799 13.536489 7.214072 c +13.793221 7.539129 14.264852 7.594518 14.589909 7.337786 c +14.914966 7.081055 14.970354 6.609422 14.713623 6.284365 c +13.582860 4.852667 11.861678 3.999996 10.001535 3.999996 c +8.138899 3.999996 6.415668 4.854965 5.285066 6.289920 c +5.028717 6.615278 5.084659 7.086845 5.410017 7.343195 c +5.735374 7.599545 6.206941 7.543602 6.463291 7.218245 c +h +7.001998 13.250921 m +7.691962 13.250921 8.251287 12.691595 8.251287 12.001632 c +8.251287 11.311668 7.691962 10.752343 7.001998 10.752343 c +6.312035 10.752343 5.752709 11.311668 5.752709 12.001632 c +5.752709 12.691595 6.312035 13.250921 7.001998 13.250921 c +h +13.001999 13.250921 m +13.691962 13.250921 14.251287 12.691595 14.251287 12.001632 c +14.251287 11.311668 13.691962 10.752343 13.001999 10.752343 c +12.312036 10.752343 11.752709 11.311668 11.752709 12.001632 c +11.752709 12.691595 12.312036 13.250921 13.001999 13.250921 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 1663 +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 +0000001753 00000 n +0000001776 00000 n +0000001949 00000 n +0000002023 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2082 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/media.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/media.imageset/Contents.json new file mode 100644 index 000000000..aad419f8f --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/media.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "media.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/media.imageset/media.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/media.imageset/media.pdf new file mode 100644 index 000000000..91bd4fa06 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/media.imageset/media.pdf @@ -0,0 +1,111 @@ +%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 3.000000 3.000000 cm +0.000000 0.000000 0.000000 scn +14.750000 18.000000 m +16.544926 18.000000 18.000000 16.544926 18.000000 14.750000 c +18.000000 3.250000 l +18.000000 1.455074 16.544926 0.000000 14.750000 0.000000 c +3.250000 0.000000 l +1.455075 0.000000 0.000000 1.455074 0.000000 3.250000 c +0.000000 14.750000 l +0.000000 16.544926 1.455075 18.000000 3.250000 18.000000 c +14.750000 18.000000 l +h +15.330538 1.598593 m +9.524668 7.285182 l +9.259618 7.544744 8.850125 7.568357 8.558795 7.356009 c +8.475204 7.285225 l +2.668451 1.598949 l +2.850401 1.534863 3.046129 1.500000 3.250000 1.500000 c +14.750000 1.500000 l +14.953493 1.500000 15.148874 1.534733 15.330538 1.598593 c +9.524668 7.285182 l +15.330538 1.598593 l +h +14.750000 16.500000 m +3.250000 16.500000 l +2.283502 16.500000 1.500000 15.716498 1.500000 14.750000 c +1.500000 3.250000 l +1.500000 3.041599 1.536428 2.841706 1.603264 2.656342 c +7.425784 8.357008 l +8.258866 9.172708 9.567461 9.211498 10.445769 8.473416 c +10.574176 8.356878 l +16.396372 2.655334 l +16.463440 2.840982 16.500000 3.041222 16.500000 3.250000 c +16.500000 14.750000 l +16.500000 15.716498 15.716498 16.500000 14.750000 16.500000 c +h +12.252115 14.500000 m +13.495924 14.500000 14.504230 13.491693 14.504230 12.247885 c +14.504230 11.004076 13.495924 9.995770 12.252115 9.995770 c +11.008307 9.995770 10.000000 11.004076 10.000000 12.247885 c +10.000000 13.491693 11.008307 14.500000 12.252115 14.500000 c +h +12.252115 13.000000 m +11.836734 13.000000 11.500000 12.663266 11.500000 12.247885 c +11.500000 11.832503 11.836734 11.495770 12.252115 11.495770 c +12.667497 11.495770 13.004230 11.832503 13.004230 12.247885 c +13.004230 12.663266 12.667497 13.000000 12.252115 13.000000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 1768 +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 +0000001858 00000 n +0000001881 00000 n +0000002054 00000 n +0000002128 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2187 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/poll.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/poll.imageset/Contents.json new file mode 100644 index 000000000..0840327d8 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/poll.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "poll.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/poll.imageset/poll.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/poll.imageset/poll.pdf new file mode 100644 index 000000000..51f03cd12 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/poll.imageset/poll.pdf @@ -0,0 +1,113 @@ +%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.000000 1.998138 cm +0.000000 0.000000 0.000000 scn +9.751870 20.002289 m +11.271687 20.002289 12.503741 18.770235 12.503741 17.250418 c +12.503741 2.751883 l +12.503741 1.232067 11.271687 0.000013 9.751870 0.000013 c +8.232054 0.000013 7.000000 1.232067 7.000000 2.751883 c +7.000000 17.250418 l +7.000000 18.770235 8.232054 20.002289 9.751870 20.002289 c +h +16.751871 15.002289 m +18.271687 15.002289 19.503740 13.770235 19.503740 12.250419 c +19.503740 2.751883 l +19.503740 1.232067 18.271687 0.000013 16.751871 0.000013 c +15.232055 0.000013 14.000000 1.232067 14.000000 2.751883 c +14.000000 12.250419 l +14.000000 13.770235 15.232055 15.002289 16.751871 15.002289 c +h +2.751871 10.002289 m +4.271687 10.002289 5.503741 8.770235 5.503741 7.250419 c +5.503741 2.751883 l +5.503741 1.232067 4.271687 0.000013 2.751871 0.000013 c +1.232054 0.000013 0.000000 1.232067 0.000000 2.751883 c +0.000000 7.250419 l +0.000000 8.770235 1.232054 10.002289 2.751871 10.002289 c +h +9.751870 18.502289 m +9.060481 18.502289 8.500000 17.941807 8.500000 17.250418 c +8.500000 2.751883 l +8.500000 2.060493 9.060481 1.500013 9.751870 1.500013 c +10.443259 1.500013 11.003741 2.060493 11.003741 2.751883 c +11.003741 17.250418 l +11.003741 17.941807 10.443259 18.502289 9.751870 18.502289 c +h +16.751871 13.502289 m +16.060482 13.502289 15.500000 12.941808 15.500000 12.250419 c +15.500000 2.751883 l +15.500000 2.060493 16.060482 1.500013 16.751871 1.500013 c +17.443260 1.500013 18.003740 2.060493 18.003740 2.751883 c +18.003740 12.250419 l +18.003740 12.941808 17.443260 13.502289 16.751871 13.502289 c +h +2.751871 8.502289 m +2.060482 8.502289 1.500000 7.941808 1.500000 7.250419 c +1.500000 2.751883 l +1.500000 2.060493 2.060482 1.500013 2.751871 1.500013 c +3.443260 1.500013 4.003741 2.060493 4.003741 2.751883 c +4.003741 7.250419 l +4.003741 7.941808 3.443260 8.502289 2.751871 8.502289 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 1919 +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 +0000002009 00000 n +0000002032 00000 n +0000002205 00000 n +0000002279 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2338 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift index edbf78a0b..efdc2164e 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift @@ -129,6 +129,14 @@ public enum Asset { public static let star = ImageAsset(name: "ObjectsAndTools/star") } public enum Scene { + public enum Compose { + public static let buttonTint = ColorAsset(name: "Scene/Compose/button.tint") + public static let chatWarning = ImageAsset(name: "Scene/Compose/chat.warning") + public static let earth = ImageAsset(name: "Scene/Compose/earth") + public static let emoji = ImageAsset(name: "Scene/Compose/emoji") + public static let media = ImageAsset(name: "Scene/Compose/media") + public static let poll = ImageAsset(name: "Scene/Compose/poll") + } public enum Discovery { public static let profileCardBackground = ColorAsset(name: "Scene/Discovery/profile.card.background") } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift index 7f8a4fd4e..29ffcbeb9 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift @@ -102,7 +102,7 @@ extension Mastodon.Entity { } extension Mastodon.Entity.Status { - public enum Visibility: RawRepresentable, Codable { + public enum Visibility: RawRepresentable, Codable, Hashable { case `public` case unlisted case `private` diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index 4c0a71f60..51dfb3ace 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import SwiftUI import Combine +import MastodonCore public final class ComposeContentViewController: UIViewController { @@ -16,6 +17,7 @@ public final class ComposeContentViewController: UIViewController { var disposeBag = Set() public var viewModel: ComposeContentViewModel! + let composeContentToolbarViewModel = ComposeContentToolbarView.ViewModel() let tableView: ComposeTableView = { let tableView = ComposeTableView() @@ -25,6 +27,10 @@ public final class ComposeContentViewController: UIViewController { tableView.tableFooterView = UIView() return tableView }() + + lazy var composeContentToolbarView = ComposeContentToolbarView(viewModel: composeContentToolbarViewModel) + var composeContentToolbarViewBottomLayoutConstraint: NSLayoutConstraint! + let composeContentToolbarBackgroundView = UIView() deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -36,6 +42,17 @@ extension ComposeContentViewController { public override func viewDidLoad() { super.viewDidLoad() + // setup view + self.setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) + ThemeService.shared.currentTheme + .receive(on: RunLoop.main) + .sink { [weak self] theme in + guard let self = self else { return } + self.setupBackgroundColor(theme: theme) + } + .store(in: &disposeBag) + + // setup tableView tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) NSLayoutConstraint.activate([ @@ -48,6 +65,110 @@ extension ComposeContentViewController { tableView.delegate = self viewModel.setupDataSource(tableView: tableView) + let toolbarHostingView = UIHostingController(rootView: composeContentToolbarView) + toolbarHostingView.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(toolbarHostingView.view) + composeContentToolbarViewBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: toolbarHostingView.view.bottomAnchor) + NSLayoutConstraint.activate([ + toolbarHostingView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + toolbarHostingView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + composeContentToolbarViewBottomLayoutConstraint, + toolbarHostingView.view.heightAnchor.constraint(equalToConstant: ComposeContentToolbarView.toolbarHeight), + ]) + toolbarHostingView.view.preservesSuperviewLayoutMargins = true + //composeToolbarView.delegate = self + + composeContentToolbarBackgroundView.translatesAutoresizingMaskIntoConstraints = false + view.insertSubview(composeContentToolbarBackgroundView, belowSubview: toolbarHostingView.view) + NSLayoutConstraint.activate([ + composeContentToolbarBackgroundView.topAnchor.constraint(equalTo: toolbarHostingView.view.topAnchor), + composeContentToolbarBackgroundView.leadingAnchor.constraint(equalTo: toolbarHostingView.view.leadingAnchor), + composeContentToolbarBackgroundView.trailingAnchor.constraint(equalTo: toolbarHostingView.view.trailingAnchor), + view.bottomAnchor.constraint(equalTo: composeContentToolbarBackgroundView.bottomAnchor), + ]) + + 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 + 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 +// } + 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 +// } + + UIView.animate(withDuration: 0.3) { + self.composeContentToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom + if self.view.window != nil { + self.view.layoutIfNeeded() + } + } + 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 + + // adjust inset for tableView + let contentFrame = self.view.convert(self.tableView.frame, to: nil) + let padding = contentFrame.maxY + extraMargin - endFrame.minY + guard padding > 0 else { + self.tableView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin + self.tableView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin + return + } + + self.tableView.contentInset.bottom = padding - self.view.safeAreaInsets.bottom + self.tableView.verticalScrollIndicatorInsets.bottom = padding - self.view.safeAreaInsets.bottom + UIView.animate(withDuration: 0.3) { + self.composeContentToolbarViewBottomLayoutConstraint.constant = endFrame.height + self.view.layoutIfNeeded() + } + }) + .store(in: &disposeBag) // setup snap behavior Publishers.CombineLatest( @@ -90,6 +211,20 @@ extension ComposeContentViewController { } } +extension ComposeContentViewController { + 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 + composeContentToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor + } +} + // MARK: - UIScrollViewDelegate extension ComposeContentViewController { public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift new file mode 100644 index 000000000..ed412fbea --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift @@ -0,0 +1,57 @@ +// +// ComposeContentToolbarView.swift +// +// +// Created by MainasuK on 22/10/18. +// + +import SwiftUI +import MastodonCore +import MastodonAsset +import MastodonSDK + +extension ComposeContentToolbarView { + class ViewModel: ObservableObject { + + // input + @Published var backgroundColor = ThemeService.shared.currentTheme.value.composeToolbarBackgroundColor + @Published var visibility: Mastodon.Entity.Status.Visibility = .public + var allVisibilities: [Mastodon.Entity.Status.Visibility] { + return [.public, .private, .direct] + } + + // output + + init() { + ThemeService.shared.currentTheme + .map { $0.composeToolbarBackgroundColor } + .assign(to: &$backgroundColor) + } + + } +} + +extension ComposeContentToolbarView.ViewModel { + enum Action: CaseIterable { + case attachment + case poll + case emoji + case contentWarning + case visibility + + var image: UIImage { + switch self { + case .attachment: + return Asset.Scene.Compose.media.image + case .poll: + return Asset.Scene.Compose.poll.image + case .emoji: + return Asset.Scene.Compose.emoji.image + case .contentWarning: + return Asset.Scene.Compose.chatWarning.image + case .visibility: + return Asset.Scene.Compose.earth.image + } + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift new file mode 100644 index 000000000..775a6f4f0 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift @@ -0,0 +1,82 @@ +// +// ComposeContentToolbarView.swift +// +// +// Created by MainasuK on 22/10/18. +// + +import SwiftUI +import MastodonAsset + +struct ComposeContentToolbarView: View { + + static var toolbarHeight: CGFloat { 48 } + + @ObservedObject var viewModel: ViewModel + + var body: some View { + HStack(spacing: .zero) { + ForEach(ComposeContentToolbarView.ViewModel.Action.allCases, id: \.self) { action in + switch action { + case .attachment: + Menu { + Button { + + } label: { + Label("Photo Library", systemImage: "photo.on.rectangle") + } + Button { + + } label: { + Label("Camera", systemImage: "camera") + } + Button { + + } label: { + Label("Browse", systemImage: "ellipsis") + } + } label: { + label(for: action) + } + .frame(width: 48, height: 48) + case .visibility: + Menu { + Picker(selection: $viewModel.visibility) { + ForEach(viewModel.allVisibilities, id: \.self) { visibility in + Label(visibility.rawValue, systemImage: "photo.on.rectangle") + } + } label: { + Text("Select Visibility") + } + } label: { + label(for: action) + } + .frame(width: 48, height: 48) + default: + Button { + + } label: { + label(for: action) + } + .frame(width: 48, height: 48) + } + } + Spacer() + Text("Hello") + } + .padding(.leading, 4) // 4 + 12 = 16 + .padding(.trailing, 16) + .frame(height: ComposeContentToolbarView.toolbarHeight) + .background(Color(viewModel.backgroundColor)) + } + +} + + +extension ComposeContentToolbarView { + func label(for action: ComposeContentToolbarView.ViewModel.Action) -> some View { + Image(uiImage: action.image.withRenderingMode(.alwaysTemplate)) + .foregroundColor(Color(Asset.Scene.Compose.buttonTint.color)) + .frame(width: 24, height: 24, alignment: .center) + } +} From 44a8b818e4aa6a60c06fbd3f8d940766ca48dc12 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 21 Oct 2022 19:12:44 +0800 Subject: [PATCH 042/658] feat: [WIP] restore compose poll view --- .../Scene/Compose/ComposeViewController.swift | 102 +----------- .../Compose/Mention.imageset/Contents.json | 15 ++ .../Compose/Mention.imageset/Mention.pdf | 102 ++++++++++++ .../Scene/Compose/More.imageset/Contents.json | 15 ++ .../Scene/Compose/More.imageset/More.pdf | 83 ++++++++++ .../Compose/People.imageset/Contents.json | 15 ++ .../Scene/Compose/People.imageset/People.pdf | 140 ++++++++++++++++ .../chat.warning.fill.imageset/Contents.json | 15 ++ .../chat.warning.fill.pdf | 90 +++++++++++ .../Compose/emoji.fill.imageset/Contents.json | 15 ++ .../emoji.fill.imageset/emoji.fill.pdf | 93 +++++++++++ .../Compose/people.add.imageset/Contents.json | 15 ++ .../people.add.imageset/People Add.pdf | 150 ++++++++++++++++++ .../Compose/poll.fill.imageset/Contents.json | 15 ++ .../Compose/poll.fill.imageset/poll.fill.pdf | 89 +++++++++++ .../MastodonAsset/Generated/Assets.swift | 7 + .../Model/Poll/PollComposeItem.swift | 104 ++++++++++++ .../Model/Poll/PollComposeSection.swift | 12 ++ .../ComposeContentViewController.swift | 138 +++++++++++++++- .../ComposeContentViewModel.swift | 76 +++++++++ .../ComposeContent/Poll/PollOptionRow.swift | 35 ++++ .../Poll/PollOptionTextField.swift | 101 ++++++++++++ .../ComposeContentToolbarView+ViewModel.swift | 76 ++++++++- .../View/ComposeContentToolbarView.swift | 79 ++++++--- .../View/ComposeContentView.swift | 110 ++++++++++++- .../Vendor/ReorderableForEach.swift | 108 +++++++++++++ .../MastodonUI/Vendor/VectorImageView.swift | 44 +++++ .../DeleteBackwardResponseTextField.swift | 10 ++ 28 files changed, 1722 insertions(+), 132 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Mention.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Mention.imageset/Mention.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/More.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/More.imageset/More.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/People.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/People.imageset/People.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/chat.warning.fill.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/chat.warning.fill.imageset/chat.warning.fill.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/emoji.fill.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/emoji.fill.imageset/emoji.fill.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/people.add.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/people.add.imageset/People Add.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/poll.fill.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/poll.fill.imageset/poll.fill.pdf create mode 100644 MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeItem.swift create mode 100644 MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeSection.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionRow.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Vendor/ReorderableForEach.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Vendor/VectorImageView.swift diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index c94db4def..54f3903b6 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -106,30 +106,7 @@ final class ComposeViewController: UIViewController, NeedsDependency { // var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint! // let composeToolbarBackgroundView = UIView() // -// static func createPhotoLibraryPickerConfiguration(selectionLimit: Int = 4) -> PHPickerConfiguration { -// var configuration = PHPickerConfiguration() -// configuration.filter = .any(of: [.images, .videos]) -// configuration.selectionLimit = selectionLimit -// return configuration -// } -// -// private(set) lazy var photoLibraryPicker: PHPickerViewController = { -// let imagePicker = PHPickerViewController(configuration: ComposeViewController.createPhotoLibraryPickerConfiguration()) -// imagePicker.delegate = self -// return imagePicker -// }() -// private(set) lazy var imagePickerController: UIImagePickerController = { -// let imagePickerController = UIImagePickerController() -// imagePickerController.sourceType = .camera -// imagePickerController.delegate = self -// return imagePickerController -// }() -// -// private(set) lazy var documentPickerController: UIDocumentPickerViewController = { -// let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image, .movie]) -// documentPickerController.delegate = self -// return documentPickerController -// }() + // // private(set) lazy var autoCompleteViewController: AutoCompleteViewController = { // let viewController = AutoCompleteViewController() @@ -814,29 +791,7 @@ extension ComposeViewController { // //// MARK: - ComposeToolbarViewDelegate //extension ComposeViewController: ComposeToolbarViewDelegate { -// -// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, mediaButtonDidPressed sender: Any, mediaSelectionType type: ComposeToolbarView.MediaSelectionType) { -// switch type { -// case .photoLibrary: -// present(photoLibraryPicker, animated: true, completion: nil) -// case .camera: -// present(imagePickerController, animated: true, completion: nil) -// case .browse: -// #if SNAPSHOT -// guard let image = UIImage(named: "Athens") else { return } -// -// let attachmentService = MastodonAttachmentService( -// context: context, -// image: image, -// initialAuthenticationBox: viewModel.authenticationBox -// ) -// viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService] -// #else -// present(documentPickerController, animated: true, completion: nil) -// #endif -// } -// } -// + // func composeToolbarView(_ composeToolbarView: ComposeToolbarView, pollButtonDidPressed sender: Any) { // // toggle poll composing state // viewModel.isPollComposing.toggle() @@ -943,59 +898,6 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate { } -//// MARK: - PHPickerViewControllerDelegate -//extension ComposeViewController: PHPickerViewControllerDelegate { -// func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { -// picker.dismiss(animated: true, completion: nil) -// -// let attachmentServices: [MastodonAttachmentService] = results.map { result in -// let service = MastodonAttachmentService( -// context: context, -// pickerResult: result, -// initialAuthenticationBox: viewModel.authenticationBox -// ) -// return service -// } -// viewModel.attachmentServices = viewModel.attachmentServices + attachmentServices -// } -//} -// -//// MARK: - UIImagePickerControllerDelegate -//extension ComposeViewController: UIImagePickerControllerDelegate & UINavigationControllerDelegate { -// -// func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { -// picker.dismiss(animated: true, completion: nil) -// -// guard let image = info[.originalImage] as? UIImage else { return } -// -// let attachmentService = MastodonAttachmentService( -// context: context, -// image: image, -// initialAuthenticationBox: viewModel.authenticationBox -// ) -// viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService] -// } -// -// func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { -// os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) -// picker.dismiss(animated: true, completion: nil) -// } -//} -// -//// MARK: - UIDocumentPickerDelegate -//extension ComposeViewController: UIDocumentPickerDelegate { -// 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] -// } -//} -// //// MARK: - ComposeStatusAttachmentTableViewCellDelegate //extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelegate { // diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Mention.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Mention.imageset/Contents.json new file mode 100644 index 000000000..776af5644 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Mention.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Mention.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Mention.imageset/Mention.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Mention.imageset/Mention.pdf new file mode 100644 index 000000000..e797d12c6 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Mention.imageset/Mention.pdf @@ -0,0 +1,102 @@ +%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.000000 2.000000 cm +0.000000 0.000000 0.000000 scn +20.000000 10.000000 m +20.000000 8.250000 l +20.000000 6.178932 18.321068 4.500000 16.250000 4.500000 c +14.745829 4.500000 13.448502 5.385603 12.850986 6.663840 c +12.032894 5.644792 10.840015 5.000000 9.500000 5.000000 c +6.992370 5.000000 5.000000 7.258018 5.000000 10.000000 c +5.000000 12.741982 6.992370 15.000000 9.500000 15.000000 c +10.659005 15.000000 11.707939 14.517641 12.500963 13.728080 c +12.500000 14.250000 l +12.500000 14.664213 12.835787 15.000000 13.250000 15.000000 c +13.629696 15.000000 13.943491 14.717846 13.993154 14.351770 c +14.000000 14.250000 l +14.000000 8.250000 l +14.000000 7.007360 15.007360 6.000000 16.250000 6.000000 c +17.440865 6.000000 18.415646 6.925161 18.494810 8.095951 c +18.500000 8.250000 l +18.500000 10.000000 l +18.500000 14.694420 14.694420 18.500000 10.000000 18.500000 c +5.305580 18.500000 1.500000 14.694420 1.500000 10.000000 c +1.500000 5.305580 5.305580 1.500000 10.000000 1.500000 c +11.032966 1.500000 12.039467 1.683977 12.985156 2.038752 c +13.372977 2.184242 13.805312 1.987795 13.950803 1.599974 c +14.096293 1.212152 13.899846 0.779818 13.512025 0.634327 c +12.398500 0.216589 11.213587 0.000000 10.000000 0.000000 c +4.477152 0.000000 0.000000 4.477152 0.000000 10.000000 c +0.000000 15.522848 4.477152 20.000000 10.000000 20.000000 c +15.429239 20.000000 19.847933 15.673328 19.996159 10.279904 c +20.000000 10.000000 l +20.000000 8.250000 l +20.000000 10.000000 l +h +9.500000 13.500000 m +7.865495 13.500000 6.500000 11.952439 6.500000 10.000000 c +6.500000 8.047561 7.865495 6.500000 9.500000 6.500000 c +11.134505 6.500000 12.500000 8.047561 12.500000 10.000000 c +12.500000 11.952439 11.134505 13.500000 9.500000 13.500000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 1791 +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 +0000001881 00000 n +0000001904 00000 n +0000002077 00000 n +0000002151 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2210 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/More.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/More.imageset/Contents.json new file mode 100644 index 000000000..990bf0cbd --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/More.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "More.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/More.imageset/More.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/More.imageset/More.pdf new file mode 100644 index 000000000..8ae9c73f2 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/More.imageset/More.pdf @@ -0,0 +1,83 @@ +%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.250000 10.250000 cm +0.000000 0.000000 0.000000 scn +3.500000 1.750000 m +3.500000 0.783502 2.716498 0.000000 1.750000 0.000000 c +0.783502 0.000000 0.000000 0.783502 0.000000 1.750000 c +0.000000 2.716498 0.783502 3.500000 1.750000 3.500000 c +2.716498 3.500000 3.500000 2.716498 3.500000 1.750000 c +h +9.500000 1.750000 m +9.500000 0.783502 8.716498 0.000000 7.750000 0.000000 c +6.783502 0.000000 6.000000 0.783502 6.000000 1.750000 c +6.000000 2.716498 6.783502 3.500000 7.750000 3.500000 c +8.716498 3.500000 9.500000 2.716498 9.500000 1.750000 c +h +13.750000 0.000000 m +14.716498 0.000000 15.500000 0.783502 15.500000 1.750000 c +15.500000 2.716498 14.716498 3.500000 13.750000 3.500000 c +12.783502 3.500000 12.000000 2.716498 12.000000 1.750000 c +12.000000 0.783502 12.783502 0.000000 13.750000 0.000000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 877 +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 +0000000967 00000 n +0000000989 00000 n +0000001162 00000 n +0000001236 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1295 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/People.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/People.imageset/Contents.json new file mode 100644 index 000000000..8f5a17a88 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/People.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "People.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/People.imageset/People.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/People.imageset/People.pdf new file mode 100644 index 000000000..c5345615f --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/People.imageset/People.pdf @@ -0,0 +1,140 @@ +%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.000000 2.000000 cm +0.000000 0.000000 0.000000 scn +2.000000 8.001000 m +11.000000 8.000000 l +12.053818 8.000000 12.918116 7.184515 12.994511 6.149316 c +13.000000 6.000000 l +13.000000 4.500000 l +12.999000 1.000000 9.284000 0.000000 6.500000 0.000000 c +3.777867 0.000000 0.164695 0.956049 0.005454 4.270353 c +0.000000 4.500000 l +0.000000 6.001000 l +0.000000 7.054819 0.816397 7.919116 1.850808 7.995511 c +2.000000 8.001000 l +h +13.220000 8.000000 m +18.000000 8.000000 l +19.053818 8.000000 19.918116 7.183603 19.994511 6.149192 c +20.000000 6.000000 l +20.000000 5.000000 l +19.999001 1.938000 17.142000 1.000000 15.000000 1.000000 c +14.320000 1.000000 13.568999 1.096001 12.860000 1.322001 c +13.196000 1.708000 13.467000 2.149000 13.662000 2.649000 c +14.205000 2.524000 14.715000 2.500000 15.000000 2.500000 c +15.266544 2.505959 l +16.251810 2.549091 18.352863 2.869398 18.492661 4.795017 c +18.500000 5.000000 l +18.500000 6.000000 l +18.500000 6.245334 18.322222 6.449580 18.089575 6.491940 c +18.000000 6.500000 l +13.949000 6.500000 l +13.865001 7.001375 13.655437 7.456812 13.354479 7.840185 c +13.220000 8.000000 l +18.000000 8.000000 l +13.220000 8.000000 l +h +2.000000 6.501000 m +1.899344 6.491000 l +1.774960 6.465720 1.690000 6.398199 1.646000 6.355000 c +1.602800 6.311000 1.535280 6.226681 1.510000 6.102040 c +1.500000 6.001000 l +1.500000 4.500000 l +1.500000 3.491000 1.950000 2.778000 2.917000 2.257999 c +3.743154 1.813076 4.919508 1.543680 6.182578 1.504868 c +6.500000 1.500000 l +6.817405 1.504868 l +8.080349 1.543680 9.255923 1.813076 10.083000 2.257999 c +10.988626 2.745499 11.441613 3.402630 11.494699 4.315081 c +11.500000 4.500999 l +11.500000 6.000000 l +11.500000 6.245334 11.322222 6.449580 11.089575 6.491940 c +11.000000 6.500000 l +2.000000 6.501000 l +h +6.500000 19.000000 m +8.985000 19.000000 11.000000 16.985001 11.000000 14.500000 c +11.000000 12.015000 8.985000 10.000000 6.500000 10.000000 c +4.015000 10.000000 2.000000 12.015000 2.000000 14.500000 c +2.000000 16.985001 4.015000 19.000000 6.500000 19.000000 c +h +15.500000 17.000000 m +17.433001 17.000000 19.000000 15.433001 19.000000 13.500000 c +19.000000 11.566999 17.433001 10.000000 15.500000 10.000000 c +13.567000 10.000000 12.000000 11.566999 12.000000 13.500000 c +12.000000 15.433001 13.567000 17.000000 15.500000 17.000000 c +h +6.500000 17.500000 m +4.846000 17.500000 3.500000 16.153999 3.500000 14.500000 c +3.500000 12.846000 4.846000 11.500000 6.500000 11.500000 c +8.154000 11.500000 9.500000 12.846000 9.500000 14.500000 c +9.500000 16.153999 8.154000 17.500000 6.500000 17.500000 c +h +15.500000 15.500000 m +14.397000 15.500000 13.500000 14.603001 13.500000 13.500000 c +13.500000 12.396999 14.397000 11.500000 15.500000 11.500000 c +16.603001 11.500000 17.500000 12.396999 17.500000 13.500000 c +17.500000 14.603001 16.603001 15.500000 15.500000 15.500000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 2893 +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 +0000002983 00000 n +0000003006 00000 n +0000003179 00000 n +0000003253 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3312 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/chat.warning.fill.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/chat.warning.fill.imageset/Contents.json new file mode 100644 index 000000000..e4babf4c7 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/chat.warning.fill.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "chat.warning.fill.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/chat.warning.fill.imageset/chat.warning.fill.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/chat.warning.fill.imageset/chat.warning.fill.pdf new file mode 100644 index 000000000..f6adcc07c --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/chat.warning.fill.imageset/chat.warning.fill.pdf @@ -0,0 +1,90 @@ +%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.000000 1.907806 cm +0.000000 0.000000 0.000000 scn +20.000000 10.093658 m +20.000000 15.616507 15.522848 20.093658 10.000000 20.093658 c +4.477152 20.093658 0.000000 15.616507 0.000000 10.093658 c +0.000000 8.450871 0.397199 6.864305 1.144898 5.443409 c +0.028547 1.155022 l +-0.008018 1.014606 -0.008014 0.867102 0.028576 0.726627 c +0.146904 0.272343 0.611098 -0.000004 1.065382 0.118324 c +5.355775 1.235390 l +6.775161 0.489723 8.359558 0.093658 10.000000 0.093658 c +15.522848 0.093658 20.000000 4.570810 20.000000 10.093658 c +h +10.000000 15.592194 m +10.414213 15.592194 10.750000 15.256407 10.750000 14.842194 c +10.750000 8.592194 l +10.750000 8.177980 10.414213 7.842194 10.000000 7.842194 c +9.585787 7.842194 9.250000 8.177980 9.250000 8.592194 c +9.250000 14.842194 l +9.250000 15.256407 9.585787 15.592194 10.000000 15.592194 c +h +11.000000 5.594330 m +11.000000 5.042046 10.552285 4.594330 10.000000 4.594330 c +9.447715 4.594330 9.000000 5.042046 9.000000 5.594330 c +9.000000 6.146615 9.447715 6.594330 10.000000 6.594330 c +10.552285 6.594330 11.000000 6.146615 11.000000 5.594330 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 1155 +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 +0000001245 00000 n +0000001268 00000 n +0000001441 00000 n +0000001515 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1574 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/emoji.fill.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/emoji.fill.imageset/Contents.json new file mode 100644 index 000000000..27b869ea8 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/emoji.fill.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emoji.fill.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/emoji.fill.imageset/emoji.fill.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/emoji.fill.imageset/emoji.fill.pdf new file mode 100644 index 000000000..b8c544137 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/emoji.fill.imageset/emoji.fill.pdf @@ -0,0 +1,93 @@ +%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 1.998444 1.997925 cm +0.000000 0.000000 0.000000 scn +10.001551 20.003113 m +15.525254 20.003113 20.003101 15.525267 20.003101 10.001562 c +20.003101 4.477859 15.525254 0.000013 10.001551 0.000013 c +4.477847 0.000013 0.000000 4.477859 0.000000 10.001562 c +0.000000 15.525267 4.477847 20.003113 10.001551 20.003113 c +h +6.463291 7.218245 m +6.206941 7.543602 5.735374 7.599545 5.410017 7.343195 c +5.084659 7.086845 5.028717 6.615278 5.285066 6.289920 c +6.415668 4.854965 8.138899 3.999996 10.001535 3.999996 c +11.861678 3.999996 13.582860 4.852667 14.713623 6.284365 c +14.970354 6.609422 14.914966 7.081055 14.589909 7.337786 c +14.264852 7.594518 13.793221 7.539129 13.536489 7.214072 c +12.687231 6.138799 11.397759 5.499995 10.001535 5.499995 c +8.603443 5.499995 7.312427 6.140525 6.463291 7.218245 c +h +7.001998 13.250921 m +6.312035 13.250921 5.752709 12.691595 5.752709 12.001632 c +5.752709 11.311668 6.312035 10.752343 7.001998 10.752343 c +7.691962 10.752343 8.251287 11.311668 8.251287 12.001632 c +8.251287 12.691595 7.691962 13.250921 7.001998 13.250921 c +h +13.001999 13.250921 m +12.312036 13.250921 11.752709 12.691595 11.752709 12.001632 c +11.752709 11.311668 12.312036 10.752343 13.001999 10.752343 c +13.691962 10.752343 14.251287 11.311668 14.251287 12.001632 c +14.251287 12.691595 13.691962 13.250921 13.001999 13.250921 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 1401 +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 +0000001491 00000 n +0000001514 00000 n +0000001687 00000 n +0000001761 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1820 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/people.add.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/people.add.imageset/Contents.json new file mode 100644 index 000000000..b7086c903 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/people.add.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "People Add.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/people.add.imageset/People Add.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/people.add.imageset/People Add.pdf new file mode 100644 index 000000000..a25e5685a --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/people.add.imageset/People Add.pdf @@ -0,0 +1,150 @@ +%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.000000 1.000000 cm +0.000000 0.000000 0.000000 scn +15.500000 11.000000 m +18.537567 11.000000 21.000000 8.537566 21.000000 5.500000 c +21.000000 2.462433 18.537567 0.000000 15.500000 0.000000 c +12.462434 0.000000 10.000000 2.462433 10.000000 5.500000 c +10.000000 8.537566 12.462434 11.000000 15.500000 11.000000 c +h +2.000000 10.001000 m +10.809396 9.999816 l +10.383152 9.555614 10.019394 9.050992 9.732251 8.500078 c +2.000000 8.501000 l +1.899344 8.491000 l +1.774960 8.465720 1.690000 8.398199 1.646000 8.355000 c +1.602800 8.311000 1.535280 8.226681 1.510000 8.102040 c +1.500000 8.001000 l +1.500000 6.500000 l +1.500000 5.491000 1.950000 4.778000 2.917000 4.257999 c +3.743154 3.813076 4.919508 3.543680 6.182578 3.504868 c +6.500000 3.500000 l +6.817405 3.504868 l +7.681085 3.531410 8.503904 3.665789 9.202351 3.890396 c +9.326429 3.397497 9.508213 2.927378 9.739013 2.486986 c +8.688712 2.137030 7.530568 2.000000 6.500000 2.000000 c +3.777867 2.000000 0.164695 2.956049 0.005454 6.270353 c +0.000000 6.500000 l +0.000000 8.001000 l +0.000000 9.105000 0.896000 10.001000 2.000000 10.001000 c +h +15.500000 8.998419 m +15.410124 8.990363 l +15.206031 8.953320 15.045100 8.792387 15.008057 8.588294 c +15.000000 8.498419 l +15.000000 6.000420 l +12.500000 6.000000 l +12.410125 5.991943 l +12.206032 5.954900 12.045099 5.793969 12.008056 5.589876 c +12.000000 5.500000 l +12.008056 5.410124 l +12.045099 5.206031 12.206032 5.045100 12.410125 5.008057 c +12.500000 5.000000 l +15.000000 5.000420 l +15.000000 2.500000 l +15.008057 2.410124 l +15.045100 2.206030 15.206031 2.045101 15.410124 2.008057 c +15.500000 2.000000 l +15.589876 2.008057 l +15.793969 2.045101 15.954900 2.206030 15.991943 2.410124 c +16.000000 2.500000 l +16.000000 5.000420 l +18.500000 5.000000 l +18.589876 5.008057 l +18.793970 5.045100 18.954899 5.206031 18.991943 5.410124 c +19.000000 5.500000 l +18.991943 5.589876 l +18.954899 5.793969 18.793970 5.954900 18.589876 5.991943 c +18.500000 6.000000 l +16.000000 6.000420 l +16.000000 8.498419 l +15.991943 8.588294 l +15.954900 8.792387 15.793969 8.953320 15.589876 8.990363 c +15.500000 8.998419 l +h +6.500000 21.000000 m +8.985000 21.000000 11.000000 18.985001 11.000000 16.500000 c +11.000000 14.015000 8.985000 12.000000 6.500000 12.000000 c +4.015000 12.000000 2.000000 14.015000 2.000000 16.500000 c +2.000000 18.985001 4.015000 21.000000 6.500000 21.000000 c +h +15.500000 19.000000 m +17.433001 19.000000 19.000000 17.433001 19.000000 15.500000 c +19.000000 13.566999 17.433001 12.000000 15.500000 12.000000 c +13.567000 12.000000 12.000000 13.566999 12.000000 15.500000 c +12.000000 17.433001 13.567000 19.000000 15.500000 19.000000 c +h +6.500000 19.500000 m +4.846000 19.500000 3.500000 18.153999 3.500000 16.500000 c +3.500000 14.846000 4.846000 13.500000 6.500000 13.500000 c +8.154000 13.500000 9.500000 14.846000 9.500000 16.500000 c +9.500000 18.153999 8.154000 19.500000 6.500000 19.500000 c +h +15.500000 17.500000 m +14.397000 17.500000 13.500000 16.603001 13.500000 15.500000 c +13.500000 14.396999 14.397000 13.500000 15.500000 13.500000 c +16.603001 13.500000 17.500000 14.396999 17.500000 15.500000 c +17.500000 16.603001 16.603001 17.500000 15.500000 17.500000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 3220 +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 +0000003310 00000 n +0000003333 00000 n +0000003506 00000 n +0000003580 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3639 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/poll.fill.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/poll.fill.imageset/Contents.json new file mode 100644 index 000000000..8b3f008b8 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/poll.fill.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "poll.fill.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/poll.fill.imageset/poll.fill.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/poll.fill.imageset/poll.fill.pdf new file mode 100644 index 000000000..bc645aaab --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/poll.fill.imageset/poll.fill.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 2.000000 1.998138 cm +0.000000 0.000000 0.000000 scn +9.751870 20.002289 m +11.271687 20.002289 12.503741 18.770235 12.503741 17.250418 c +12.503741 2.751883 l +12.503741 1.232067 11.271687 0.000013 9.751870 0.000013 c +8.232054 0.000013 7.000000 1.232067 7.000000 2.751883 c +7.000000 17.250418 l +7.000000 18.770235 8.232054 20.002289 9.751870 20.002289 c +h +16.751871 15.002289 m +18.271687 15.002289 19.503740 13.770235 19.503740 12.250419 c +19.503740 2.751883 l +19.503740 1.232067 18.271687 0.000013 16.751871 0.000013 c +15.232055 0.000013 14.000000 1.232067 14.000000 2.751883 c +14.000000 12.250419 l +14.000000 13.770235 15.232055 15.002289 16.751871 15.002289 c +h +2.751871 10.002289 m +4.271687 10.002289 5.503741 8.770235 5.503741 7.250419 c +5.503741 2.751883 l +5.503741 1.232067 4.271687 0.000013 2.751871 0.000013 c +1.232054 0.000013 0.000000 1.232067 0.000000 2.751883 c +0.000000 7.250419 l +0.000000 8.770235 1.232054 10.002289 2.751871 10.002289 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 1024 +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 +0000001114 00000 n +0000001137 00000 n +0000001310 00000 n +0000001384 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1443 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift index efdc2164e..7fda6061f 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift @@ -131,10 +131,17 @@ public enum Asset { public enum Scene { public enum Compose { public static let buttonTint = ColorAsset(name: "Scene/Compose/button.tint") + public static let chatWarningFill = ImageAsset(name: "Scene/Compose/chat.warning.fill") public static let chatWarning = ImageAsset(name: "Scene/Compose/chat.warning") public static let earth = ImageAsset(name: "Scene/Compose/earth") + public static let emojiFill = ImageAsset(name: "Scene/Compose/emoji.fill") public static let emoji = ImageAsset(name: "Scene/Compose/emoji") public static let media = ImageAsset(name: "Scene/Compose/media") + public static let mention = ImageAsset(name: "Scene/Compose/mention") + public static let more = ImageAsset(name: "Scene/Compose/more") + public static let peopleAdd = ImageAsset(name: "Scene/Compose/people.add") + public static let people = ImageAsset(name: "Scene/Compose/people") + public static let pollFill = ImageAsset(name: "Scene/Compose/poll.fill") public static let poll = ImageAsset(name: "Scene/Compose/poll") } public enum Discovery { diff --git a/MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeItem.swift new file mode 100644 index 000000000..5673f600d --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeItem.swift @@ -0,0 +1,104 @@ +// +// PollComposeItem.swift +// +// +// Created by MainasuK on 2021-11-29. +// + +import UIKit +import Combine +import MastodonLocalization + +public enum PollComposeItem: Hashable { + case option(Option) + case expireConfiguration(ExpireConfiguration) + case multipleConfiguration(MultipleConfiguration) +} + +extension PollComposeItem { + public final class Option: NSObject, Identifiable, ObservableObject { + public let id = UUID() + + public weak var textField: UITextField? + + @Published public var text = "" + @Published public var shouldBecomeFirstResponder = false + + public override init() { + super.init() + } + } +} + +extension PollComposeItem { + public final class ExpireConfiguration: Identifiable, Hashable, ObservableObject { + public let id = UUID() + + @Published public var option: Option = .oneDay // Mastodon + + public init() { + // end init + } + + public static func == (lhs: ExpireConfiguration, rhs: ExpireConfiguration) -> Bool { + return lhs.id == rhs.id + && lhs.option == rhs.option + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public enum Option: String, Hashable, CaseIterable { + case thirtyMinutes + case oneHour + case sixHours + case oneDay + case threeDays + case sevenDays + + public var title: String { + switch self { + case .thirtyMinutes: return L10n.Scene.Compose.Poll.thirtyMinutes + case .oneHour: return L10n.Scene.Compose.Poll.oneHour + case .sixHours: return L10n.Scene.Compose.Poll.sixHours + case .oneDay: return L10n.Scene.Compose.Poll.oneDay + case .threeDays: return L10n.Scene.Compose.Poll.threeDays + case .sevenDays: return L10n.Scene.Compose.Poll.sevenDays + } + } + + public var seconds: Int { + switch self { + case .thirtyMinutes: return 60 * 30 + case .oneHour: return 60 * 60 * 1 + case .sixHours: return 60 * 60 * 6 + case .oneDay: return 60 * 60 * 24 + case .threeDays: return 60 * 60 * 24 * 3 + case .sevenDays: return 60 * 60 * 24 * 7 + } + } + } + } +} + +extension PollComposeItem { + public final class MultipleConfiguration: Hashable, ObservableObject { + private let id = UUID() + + @Published public var isMultiple = false + + public init() { + // end init + } + + public static func == (lhs: MultipleConfiguration, rhs: MultipleConfiguration) -> Bool { + return lhs.id == rhs.id + && lhs.isMultiple == rhs.isMultiple + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + } +} diff --git a/MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeSection.swift b/MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeSection.swift new file mode 100644 index 000000000..3279bc064 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeSection.swift @@ -0,0 +1,12 @@ +// +// PollComposeSection.swift +// +// +// Created by MainasuK on 2021-11-29. +// + +import Foundation + +public enum PollComposeSection: Hashable { + case main +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index 51dfb3ace..09553d0f9 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import SwiftUI import Combine +import PhotosUI import MastodonCore public final class ComposeContentViewController: UIViewController { @@ -17,7 +18,7 @@ public final class ComposeContentViewController: UIViewController { var disposeBag = Set() public var viewModel: ComposeContentViewModel! - let composeContentToolbarViewModel = ComposeContentToolbarView.ViewModel() + private(set) lazy var composeContentToolbarViewModel = ComposeContentToolbarView.ViewModel(delegate: self) let tableView: ComposeTableView = { let tableView = ComposeTableView() @@ -31,6 +32,34 @@ public final class ComposeContentViewController: UIViewController { lazy var composeContentToolbarView = ComposeContentToolbarView(viewModel: composeContentToolbarViewModel) var composeContentToolbarViewBottomLayoutConstraint: NSLayoutConstraint! let composeContentToolbarBackgroundView = UIView() + + // media picker + + static func createPhotoLibraryPickerConfiguration(selectionLimit: Int = 4) -> PHPickerConfiguration { + var configuration = PHPickerConfiguration() + configuration.filter = .any(of: [.images, .videos]) + configuration.selectionLimit = selectionLimit + return configuration + } + + private(set) lazy var photoLibraryPicker: PHPickerViewController = { + let imagePicker = PHPickerViewController(configuration: ComposeContentViewController.createPhotoLibraryPickerConfiguration()) + imagePicker.delegate = self + return imagePicker + }() + + private(set) lazy var imagePickerController: UIImagePickerController = { + let imagePickerController = UIImagePickerController() + imagePickerController.sourceType = .camera + imagePickerController.delegate = self + return imagePickerController + }() + + private(set) lazy var documentPickerController: UIDocumentPickerViewController = { + let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image, .movie]) + documentPickerController.delegate = self + return documentPickerController + }() deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -188,6 +217,9 @@ extension ComposeContentViewController { } } .store(in: &disposeBag) + + // bind toolbar + bindToolbarViewModel() } public override func viewDidLayoutSubviews() { @@ -223,6 +255,12 @@ extension ComposeContentViewController { tableView.backgroundColor = backgroundColor composeContentToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor } + + private func bindToolbarViewModel() { + viewModel.$isPollActive.assign(to: &composeContentToolbarViewModel.$isPollActive) + viewModel.$isEmojiActive.assign(to: &composeContentToolbarViewModel.$isEmojiActive) + viewModel.$isContentWarningActive.assign(to: &composeContentToolbarViewModel.$isContentWarningActive) + } } // MARK: - UIScrollViewDelegate @@ -279,3 +317,101 @@ extension ComposeContentViewController { // MARK: - UITableViewDelegate extension ComposeContentViewController: UITableViewDelegate { } +// MARK: - PHPickerViewControllerDelegate +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 + } +} + +// MARK: - UIImagePickerControllerDelegate +extension ComposeContentViewController: UIImagePickerControllerDelegate & UINavigationControllerDelegate { + public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + picker.dismiss(animated: true, completion: nil) + + guard let image = info[.originalImage] as? UIImage else { return } + +// let attachmentService = MastodonAttachmentService( +// context: context, +// image: image, +// initialAuthenticationBox: viewModel.authenticationBox +// ) +// viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService] + } + + public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + picker.dismiss(animated: true, completion: nil) + } +} + +// MARK: - UIDocumentPickerDelegate +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] + } +} + +// MARK: - ComposeContentToolbarViewDelegate +extension ComposeContentViewController: ComposeContentToolbarViewDelegate { + func composeContentToolbarView( + _ viewModel: ComposeContentToolbarView.ViewModel, + toolbarItemDidPressed action: ComposeContentToolbarView.ViewModel.Action + ) { + switch action { + case .attachment: + assertionFailure() + case .poll: + self.viewModel.isPollActive.toggle() + case .emoji: + self.viewModel.isEmojiActive.toggle() + case .contentWarning: + self.viewModel.isContentWarningActive.toggle() + case .visibility: + assertionFailure() + } + } + + func composeContentToolbarView( + _ viewModel: ComposeContentToolbarView.ViewModel, + attachmentMenuDidPressed action: ComposeContentToolbarView.ViewModel.AttachmentAction + ) { + switch action { + case .photoLibrary: + present(photoLibraryPicker, animated: true, completion: nil) + case .camera: + present(imagePickerController, animated: true, completion: nil) + case .browse: + #if SNAPSHOT + guard let image = UIImage(named: "Athens") else { return } + + let attachmentService = MastodonAttachmentService( + context: context, + image: image, + initialAuthenticationBox: viewModel.authenticationBox + ) + viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService] + #else + present(documentPickerController, animated: true, completion: nil) + #endif + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 24736cc5e..ca78649e1 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -30,6 +30,20 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { @Published var viewLayoutFrame = ViewLayoutFrame() @Published var authContext: AuthContext + // poll + @Published var isPollActive = false + @Published public var pollOptions: [PollComposeItem.Option] = { + // initial with 2 options + var options: [PollComposeItem.Option] = [] + options.append(PollComposeItem.Option()) + options.append(PollComposeItem.Option()) + return options + }() + @Published public var maxPollOptionLimit = 4 + + @Published var isEmojiActive = false + @Published var isContentWarningActive = false + // output // content @@ -94,3 +108,65 @@ extension ComposeContentViewModel { } } +extension ComposeContentViewModel { + func createNewPollOptionIfCould() { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + + guard pollOptions.count < maxPollOptionLimit else { return } + let option = PollComposeItem.Option() + option.shouldBecomeFirstResponder = true + pollOptions.append(option) + } +} + +// MARK: - DeleteBackwardResponseTextFieldRelayDelegate +extension ComposeContentViewModel: DeleteBackwardResponseTextFieldRelayDelegate { + + func deleteBackwardResponseTextFieldDidReturn(_ textField: DeleteBackwardResponseTextField) { + let index = textField.tag + if index + 1 == pollOptions.count { + createNewPollOptionIfCould() + } else if index < pollOptions.count { + pollOptions[index + 1].textField?.becomeFirstResponder() + } + } + + func deleteBackwardResponseTextField(_ textField: DeleteBackwardResponseTextField, textBeforeDelete: String?) { + guard (textBeforeDelete ?? "").isEmpty else { + // do nothing when not empty + return + } + + let index = textField.tag + guard index > 0 else { + // do nothing at first row + return + } + + func optionBeforeRemoved() -> PollComposeItem.Option? { + guard index > 0 else { return nil } + let indexBeforeRemoved = pollOptions.index(before: index) + let itemBeforeRemoved = pollOptions[indexBeforeRemoved] + return itemBeforeRemoved + + } + + func optionAfterRemoved() -> PollComposeItem.Option? { + guard index < pollOptions.count - 1 else { return nil } + let indexAfterRemoved = pollOptions.index(after: index) + let itemAfterRemoved = pollOptions[indexAfterRemoved] + return itemAfterRemoved + } + + // move first responder + let _option = optionBeforeRemoved() ?? optionAfterRemoved() + _option?.textField?.becomeFirstResponder() + + guard pollOptions.count > 2 else { + // remove item when more then 2 options + return + } + pollOptions.remove(at: index) + } + +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionRow.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionRow.swift new file mode 100644 index 000000000..89482ddef --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionRow.swift @@ -0,0 +1,35 @@ +// +// PollOptionRow.swift +// +// +// Created by MainasuK on 2022-5-31. +// + +import SwiftUI +import MastodonCore + +public struct PollOptionRow: View { + + @ObservedObject var viewModel: PollComposeItem.Option + + let index: Int? + let deleteBackwardResponseTextFieldRelayDelegate: DeleteBackwardResponseTextFieldRelayDelegate? + let configurationHandler: (DeleteBackwardResponseTextField) -> Void + + public var body: some View { + PollOptionTextField( + text: $viewModel.text, + index: index ?? -1, + delegate: deleteBackwardResponseTextFieldRelayDelegate + ) { textField in + viewModel.textField = textField + configurationHandler(textField) + } + .onReceive(viewModel.$shouldBecomeFirstResponder) { shouldBecomeFirstResponder in + guard shouldBecomeFirstResponder else { return } + viewModel.shouldBecomeFirstResponder = false + viewModel.textField?.becomeFirstResponder() + } + } + +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift new file mode 100644 index 000000000..0b2065008 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift @@ -0,0 +1,101 @@ +// +// PollOptionTextField.swift +// +// +// Created by MainasuK on 2022-5-27. +// + +import os.log +import UIKit +import SwiftUI +import Combine +import MastodonCore +import MastodonLocalization + +public struct PollOptionTextField: UIViewRepresentable { + + let textField = DeleteBackwardResponseTextField() + + @Binding var text: String + + let index: Int + let delegate: DeleteBackwardResponseTextFieldRelayDelegate? + let configurationHandler: (DeleteBackwardResponseTextField) -> Void + + public func makeUIView(context: Context) -> DeleteBackwardResponseTextField { + textField.setContentHuggingPriority(.defaultHigh, for: .vertical) + textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + textField.textInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + textField.borderStyle = .roundedRect + textField.returnKeyType = .next + return textField + } + + public func updateUIView(_ textField: DeleteBackwardResponseTextField, context: Context) { + textField.tag = index + textField.text = text + textField.placeholder = { + if index >= 0 { + return L10n.Scene.Compose.Poll.optionNumber(index) + } else { + assertionFailure() + return "" + } + }() + textField.delegate = context.coordinator + textField.deleteBackwardDelegate = context.coordinator + context.coordinator.delegate = delegate + configurationHandler(textField) + } + + public func makeCoordinator() -> Coordinator { + Coordinator(self) + } + +} + +protocol DeleteBackwardResponseTextFieldRelayDelegate: AnyObject { + func deleteBackwardResponseTextFieldDidReturn(_ textField: DeleteBackwardResponseTextField) + func deleteBackwardResponseTextField(_ textField: DeleteBackwardResponseTextField, textBeforeDelete: String?) +} + +extension PollOptionTextField { + public class Coordinator: NSObject { + let logger = Logger(subsystem: "DeleteBackwardResponseTextFieldRepresentable.Coordinator", category: "Coordinator") + + var disposeBag = Set() + weak var delegate: DeleteBackwardResponseTextFieldRelayDelegate? + + let view: PollOptionTextField + + init(_ view: PollOptionTextField) { + self.view = view + super.init() + + NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: view.textField) + .sink { [weak self] _ in + guard let self = self else { return } + self.view.text = view.textField.text ?? "" + } + .store(in: &disposeBag) + } + } +} + +// MARK: - UITextFieldDelegate +extension PollOptionTextField.Coordinator: UITextFieldDelegate { + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + guard let textField = textField as? DeleteBackwardResponseTextField else { + return true + } + delegate?.deleteBackwardResponseTextFieldDidReturn(textField) + return true + } +} + +extension PollOptionTextField.Coordinator: DeleteBackwardResponseTextFieldDelegate { + public func deleteBackwardResponseTextField(_ textField: DeleteBackwardResponseTextField, textBeforeDelete: String?) { + delegate?.deleteBackwardResponseTextField(textField, textBeforeDelete: textBeforeDelete) + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift index ed412fbea..e04648c09 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift @@ -8,11 +8,14 @@ import SwiftUI import MastodonCore import MastodonAsset +import MastodonLocalization import MastodonSDK extension ComposeContentToolbarView { class ViewModel: ObservableObject { + weak var delegate: ComposeContentToolbarViewDelegate? + // input @Published var backgroundColor = ThemeService.shared.currentTheme.value.composeToolbarBackgroundColor @Published var visibility: Mastodon.Entity.Status.Visibility = .public @@ -20,9 +23,16 @@ extension ComposeContentToolbarView { return [.public, .private, .direct] } + @Published var isPollActive = false + @Published var isEmojiActive = false + @Published var isContentWarningActive = false + // output - init() { + init(delegate: ComposeContentToolbarViewDelegate) { + self.delegate = delegate + // end init + ThemeService.shared.currentTheme .map { $0.composeToolbarBackgroundColor } .assign(to: &$backgroundColor) @@ -39,19 +49,71 @@ extension ComposeContentToolbarView.ViewModel { case contentWarning case visibility - var image: UIImage { + var activeImage: UIImage { switch self { case .attachment: - return Asset.Scene.Compose.media.image + return Asset.Scene.Compose.media.image.withRenderingMode(.alwaysTemplate) case .poll: - return Asset.Scene.Compose.poll.image + return Asset.Scene.Compose.pollFill.image.withRenderingMode(.alwaysTemplate) case .emoji: - return Asset.Scene.Compose.emoji.image + return Asset.Scene.Compose.emojiFill.image.withRenderingMode(.alwaysTemplate) case .contentWarning: - return Asset.Scene.Compose.chatWarning.image + return Asset.Scene.Compose.chatWarningFill.image.withRenderingMode(.alwaysTemplate) case .visibility: - return Asset.Scene.Compose.earth.image + return Asset.Scene.Compose.earth.image.withRenderingMode(.alwaysTemplate) + } + } + + var inactiveImage: UIImage { + switch self { + case .attachment: + return Asset.Scene.Compose.media.image.withRenderingMode(.alwaysTemplate) + case .poll: + return Asset.Scene.Compose.poll.image.withRenderingMode(.alwaysTemplate) + case .emoji: + return Asset.Scene.Compose.emoji.image.withRenderingMode(.alwaysTemplate) + case .contentWarning: + return Asset.Scene.Compose.chatWarning.image.withRenderingMode(.alwaysTemplate) + case .visibility: + return Asset.Scene.Compose.earth.image.withRenderingMode(.alwaysTemplate) + } + } + } + + enum AttachmentAction: CaseIterable { + case photoLibrary + case camera + case browse + + var title: String { + switch self { + case .photoLibrary: return L10n.Scene.Compose.MediaSelection.photoLibrary + case .camera: return L10n.Scene.Compose.MediaSelection.camera + case .browse: return L10n.Scene.Compose.MediaSelection.browse + } + } + + var image: UIImage { + switch self { + case .photoLibrary: return UIImage(systemName: "photo.on.rectangle")! + case .camera: return UIImage(systemName: "camera")! + case .browse: return UIImage(systemName: "ellipsis")! } } } } + +extension ComposeContentToolbarView.ViewModel { + func image(for action: Action) -> UIImage { + switch action { + case .poll: + return isPollActive ? action.activeImage : action.inactiveImage + case .emoji: + return isEmojiActive ? action.activeImage : action.inactiveImage + case .contentWarning: + return isContentWarningActive ? action.activeImage : action.inactiveImage + default: + return action.inactiveImage + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift index 775a6f4f0..679df22c0 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift @@ -5,11 +5,21 @@ // Created by MainasuK on 22/10/18. // +import os.log import SwiftUI import MastodonAsset +import MastodonLocalization +import MastodonSDK + +protocol ComposeContentToolbarViewDelegate: AnyObject { + func composeContentToolbarView(_ viewModel: ComposeContentToolbarView.ViewModel, toolbarItemDidPressed action: ComposeContentToolbarView.ViewModel.Action) + func composeContentToolbarView(_ viewModel: ComposeContentToolbarView.ViewModel, attachmentMenuDidPressed action: ComposeContentToolbarView.ViewModel.AttachmentAction) +} struct ComposeContentToolbarView: View { + let logger = Logger(subsystem: "ComposeContentToolbarView", category: "View") + static var toolbarHeight: CGFloat { 48 } @ObservedObject var viewModel: ViewModel @@ -20,20 +30,17 @@ struct ComposeContentToolbarView: View { switch action { case .attachment: Menu { - Button { - - } label: { - Label("Photo Library", systemImage: "photo.on.rectangle") - } - Button { - - } label: { - Label("Camera", systemImage: "camera") - } - Button { - - } label: { - Label("Browse", systemImage: "ellipsis") + ForEach(ComposeContentToolbarView.ViewModel.AttachmentAction.allCases, id: \.self) { attachmentAction in + Button { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public), \(attachmentAction.title)") + viewModel.delegate?.composeContentToolbarView(viewModel, attachmentMenuDidPressed: attachmentAction) + } label: { + Label { + Text(attachmentAction.title) + } icon: { + Image(uiImage: attachmentAction.image) + } + } } } label: { label(for: action) @@ -43,18 +50,23 @@ struct ComposeContentToolbarView: View { Menu { Picker(selection: $viewModel.visibility) { ForEach(viewModel.allVisibilities, id: \.self) { visibility in - Label(visibility.rawValue, systemImage: "photo.on.rectangle") + Label { + Text(visibility.title) + } icon: { + Image(uiImage: visibility.image) + } } } label: { - Text("Select Visibility") + Text(viewModel.visibility.title) } } label: { - label(for: action) + label(for: viewModel.visibility.image) } .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))") + viewModel.delegate?.composeContentToolbarView(viewModel, toolbarItemDidPressed: action) } label: { label(for: action) } @@ -72,11 +84,38 @@ struct ComposeContentToolbarView: View { } - extension ComposeContentToolbarView { func label(for action: ComposeContentToolbarView.ViewModel.Action) -> some View { - Image(uiImage: action.image.withRenderingMode(.alwaysTemplate)) + Image(uiImage: viewModel.image(for: action)) + .foregroundColor(Color(Asset.Scene.Compose.buttonTint.color)) + .frame(width: 24, height: 24, alignment: .center) + } + + func label(for image: UIImage) -> some View { + Image(uiImage: image) .foregroundColor(Color(Asset.Scene.Compose.buttonTint.color)) .frame(width: 24, height: 24, alignment: .center) } } + +extension Mastodon.Entity.Status.Visibility { + fileprivate var title: String { + switch self { + case .public: return L10n.Scene.Compose.Visibility.public + case .unlisted: return L10n.Scene.Compose.Visibility.unlisted + case .private: return L10n.Scene.Compose.Visibility.private + case .direct: return L10n.Scene.Compose.Visibility.direct + case ._other(let value): return value + } + } + + fileprivate var image: UIImage { + switch self { + case .public: return Asset.Scene.Compose.earth.image.withRenderingMode(.alwaysTemplate) + case .unlisted: return Asset.Scene.Compose.people.image.withRenderingMode(.alwaysTemplate) + case .private: return Asset.Scene.Compose.peopleAdd.image.withRenderingMode(.alwaysTemplate) + case .direct: return Asset.Scene.Compose.mention.image.withRenderingMode(.alwaysTemplate) + case ._other: return Asset.Scene.Compose.more.image.withRenderingMode(.alwaysTemplate) + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift index 2b9f79321..91f3923aa 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift @@ -7,6 +7,7 @@ import os.log import SwiftUI +import MastodonCore import MastodonLocalization public struct ComposeContentView: View { @@ -21,8 +22,10 @@ public struct ComposeContentView: View { public var body: some View { VStack(spacing: .zero) { Group { + // author authorView .padding(.top, 14) + // content editor MetaTextViewRepresentable( string: $viewModel.content, width: viewModel.viewLayoutFrame.layoutFrame.width - ComposeContentView.margin * 2, @@ -44,15 +47,17 @@ public struct ComposeContentView: View { ) .frame(minHeight: 100) .fixedSize(horizontal: false, vertical: true) + // poll + pollView } .background( GeometryReader { proxy in Color.clear.preference(key: ViewFramePreferenceKey.self, value: proxy.frame(in: .local)) } - .onPreferenceChange(ViewFramePreferenceKey.self) { frame in - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): content frame: \(frame.debugDescription)") - viewModel.contentCellFrame = frame - } + .onPreferenceChange(ViewFramePreferenceKey.self) { frame in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): content frame: \(frame.debugDescription)") + viewModel.contentCellFrame = frame + } ) Spacer() } // end VStack @@ -84,6 +89,81 @@ extension ComposeContentView { } } +extension ComposeContentView { + // MARK: - poll + var pollView: some View { + VStack { + if viewModel.isPollActive { + // poll option TextField + ReorderableForEach( + items: $viewModel.pollOptions + ) { $pollOption in + let _index = viewModel.pollOptions.firstIndex(of: pollOption) + PollOptionRow( + viewModel: pollOption, + index: _index, + deleteBackwardResponseTextFieldRelayDelegate: viewModel + ) { textField in + // viewModel.customEmojiPickerInputViewModel.configure(textInput: textField) + } + } + } + VStack(spacing: .zero) { + // expire configuration + Menu { + ForEach(PollComposeItem.ExpireConfiguration.Option.allCases, id: \.self) { option in + Button { + // viewModel.pollExpireConfiguration.option = option + // viewModel.pollExpireConfiguration = viewModel.pollExpireConfiguration + } label: { + Text(option.title) + } + } + } label: { + HStack { +// VectorImageView( +// image: Asset.ObjectTools.clock.image.withRenderingMode(.alwaysTemplate), +// tintColor: .secondaryLabel +// ) +// .frame(width: 24, height: 24) +// .padding(.vertical, 12) +// let text = viewModel.pollExpireConfigurationFormatter.string(from: TimeInterval(viewModel.pollExpireConfiguration.option.seconds)) ?? "-" +// Text(text) +// .font(.callout) +// .foregroundColor(.primary) +// Spacer() +// VectorImageView( +// image: Asset.Arrows.tablerChevronDown.image.withRenderingMode(.alwaysTemplate), +// tintColor: .secondaryLabel +// ) +// .frame(width: 24, height: 24) +// .padding(.vertical, 12) + } + } + // multi-selection configuration +// Button { +// viewModel.pollMultipleConfiguration.isMultiple.toggle() +// viewModel.pollMultipleConfiguration = viewModel.pollMultipleConfiguration +// } label: { +// HStack { +// let selectionImage = viewModel.pollMultipleConfiguration.isMultiple ? Asset.Indices.checkmarkSquare.image.withRenderingMode(.alwaysTemplate) : Asset.Indices.square.image.withRenderingMode(.alwaysTemplate) +// VectorImageView( +// image: selectionImage, +// tintColor: .secondaryLabel +// ) +// .frame(width: 24, height: 24) +// .padding(.vertical, 12) +// Text(L10n.Scene.Compose.Vote.multiple) +// .font(.callout) +// .foregroundColor(.primary) +// Spacer() +// } +// } + } + } // end VStack + } +} + //private struct ScrollOffsetPreferenceKey: PreferenceKey { // static var defaultValue: CGPoint = .zero // @@ -95,3 +175,25 @@ private struct ViewFramePreferenceKey: PreferenceKey { static func reduce(value: inout CGRect, nextValue: () -> CGRect) { } } + +// MARK: - TypeIdentifiedItemProvider +extension PollComposeItem.Option: TypeIdentifiedItemProvider { + public static var typeIdentifier: String { + return Bundle(for: PollComposeItem.Option.self).bundleIdentifier! + String(describing: type(of: PollComposeItem.Option.self)) + } +} + +// MARK: - NSItemProviderWriting +extension PollComposeItem.Option: NSItemProviderWriting { + public func loadData( + withTypeIdentifier typeIdentifier: String, + forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void + ) -> Progress? { + completionHandler(nil, nil) + return nil + } + + public static var writableTypeIdentifiersForItemProvider: [String] { + return [Self.typeIdentifier] + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Vendor/ReorderableForEach.swift b/MastodonSDK/Sources/MastodonUI/Vendor/ReorderableForEach.swift new file mode 100644 index 000000000..9cbb5bcab --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Vendor/ReorderableForEach.swift @@ -0,0 +1,108 @@ +// +// ReorderableForEach.swift +// +// +// Created by MainasuK on 2022-5-23. +// + +import SwiftUI +import UniformTypeIdentifiers + +// Ref +// https://stackoverflow.com/a/68963988/3797903 + +struct ReorderableForEach: View { + + @State var currentReorderItem: Item? = nil + @State var isCurrentReorderItemOutside: Bool = false + + @Binding var items: [Item] + @ViewBuilder let content: (Binding) -> Content + + var body: some View { + ForEach($items) { $item in + content($item) + .zIndex(currentReorderItem == item ? 1 : 0) + .onDrop( + of: [Item.typeIdentifier], + delegate: DropRelocateDelegate( + item: item, + items: $items, + current: $currentReorderItem, + isOutside: $isCurrentReorderItemOutside + ) + ) + .onDrag { + currentReorderItem = item + isCurrentReorderItemOutside = false + return NSItemProvider(object: item) + } + } + .onDrop( + of: [Item.typeIdentifier], + delegate: DropOutsideDelegate( + current: $currentReorderItem, + isOutside: $isCurrentReorderItemOutside + ) + ) + } +} + +struct DropRelocateDelegate: DropDelegate { + let item: Item + @Binding var items: [Item] + + @Binding var current: Item? + @Binding var isOutside: Bool + + func dropEntered(info: DropInfo) { + guard item != current, let current = current else { return } + guard let from = items.firstIndex(of: current), let to = items.firstIndex(of: item) else { return } + + if items[to] != current { + withAnimation { + items.move( + fromOffsets: IndexSet(integer: from), + toOffset: to > from ? to + 1 : to + ) + } + } + } + + func dropUpdated(info: DropInfo) -> DropProposal? { + DropProposal(operation: .move) + } + + func performDrop(info: DropInfo) -> Bool { + current = nil + isOutside = false + return true + } +} + +struct DropOutsideDelegate: DropDelegate { + @Binding var current: Item? + @Binding var isOutside: Bool + + func dropEntered(info: DropInfo) { + isOutside = false + } + + func dropExited(info: DropInfo) { + isOutside = true + } + + func dropUpdated(info: DropInfo) -> DropProposal? { + return DropProposal(operation: .cancel) + } + + func performDrop(info: DropInfo) -> Bool { + current = nil + isOutside = false + return false + } +} + +public protocol TypeIdentifiedItemProvider { + static var typeIdentifier: String { get } +} diff --git a/MastodonSDK/Sources/MastodonUI/Vendor/VectorImageView.swift b/MastodonSDK/Sources/MastodonUI/Vendor/VectorImageView.swift new file mode 100644 index 000000000..901e6dcdc --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Vendor/VectorImageView.swift @@ -0,0 +1,44 @@ +// +// VectorImageView.swift +// +// +// Created by MainasuK on 2022-4-29. +// + +import UIKit +import SwiftUI + +// workaround SwiftUI vector image scale problem +// https://stackoverflow.com/a/61178828/3797903 +public struct VectorImageView: UIViewRepresentable { + + public var image: UIImage + public var contentMode: UIView.ContentMode = .scaleAspectFit + public var tintColor: UIColor = .black + + public init( + image: UIImage, + contentMode: UIView.ContentMode = .scaleAspectFit, + tintColor: UIColor = .black + ) { + self.image = image + self.contentMode = contentMode + self.tintColor = tintColor + } + + public func makeUIView(context: Context) -> UIImageView { + let imageView = UIImageView() + imageView.setContentCompressionResistancePriority( + .fittingSizeLevel, + for: .vertical + ) + return imageView + } + + public func updateUIView(_ imageView: UIImageView, context: Context) { + imageView.contentMode = contentMode + imageView.tintColor = tintColor + imageView.image = image + } + +} diff --git a/MastodonSDK/Sources/MastodonUI/View/TextField/DeleteBackwardResponseTextField.swift b/MastodonSDK/Sources/MastodonUI/View/TextField/DeleteBackwardResponseTextField.swift index 6fd760430..a6e1bf02c 100644 --- a/MastodonSDK/Sources/MastodonUI/View/TextField/DeleteBackwardResponseTextField.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TextField/DeleteBackwardResponseTextField.swift @@ -15,10 +15,20 @@ public final class DeleteBackwardResponseTextField: UITextField { public weak var deleteBackwardDelegate: DeleteBackwardResponseTextFieldDelegate? + public var textInset: UIEdgeInsets = .zero + public override func deleteBackward() { let text = self.text super.deleteBackward() deleteBackwardDelegate?.deleteBackwardResponseTextField(self, textBeforeDelete: text) } + public override func textRect(forBounds bounds: CGRect) -> CGRect { + return bounds.inset(by: textInset) + } + + public override func editingRect(forBounds bounds: CGRect) -> CGRect { + return bounds.inset(by: textInset) + } + } From 0a3f19bdd3f0996b7988fda229a6df838339bb83 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 26 Oct 2022 18:35:10 +0800 Subject: [PATCH 043/658] feat: restore compose poll options --- .../xcschemes/xcschememanagement.plist | 4 +- .../reorder.dot.imageset/Contents.json | 15 +++ .../reorder.dot.imageset/reorder.dot.pdf | 101 ++++++++++++++++++ .../Contents.json | 38 +++++++ .../Contents.json | 38 +++++++ .../MastodonAsset/Generated/Assets.swift | 3 + .../Model/Poll/PollComposeItem.swift | 8 ++ .../Service/Theme/MastodonTheme.swift | 1 + .../Service/Theme/SystemTheme.swift | 1 + .../MastodonCore/Service/Theme/Theme.swift | 1 + .../Poll/PollAddOptionRow.swift | 59 ++++++++++ .../ComposeContent/Poll/PollOptionRow.swift | 40 ++++--- .../Poll/PollOptionTextField.swift | 5 +- .../View/ComposeContentToolbarView.swift | 1 + .../View/ComposeContentView.swift | 8 +- 15 files changed, 307 insertions(+), 16 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/reorder.dot.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/reorder.dot.imageset/reorder.dot.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Theme/Mastodon/Background/compose.poll.row.background.colorset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Theme/system/Background/compose.poll.row.background.colorset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollAddOptionRow.swift diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 31423978e..33eda54f1 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -112,12 +112,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 17 + 18 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 18 + 17 SuppressBuildableAutocreation diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/reorder.dot.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/reorder.dot.imageset/Contents.json new file mode 100644 index 000000000..0331c75f2 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/reorder.dot.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "reorder.dot.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/reorder.dot.imageset/reorder.dot.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/reorder.dot.imageset/reorder.dot.pdf new file mode 100644 index 000000000..3a8ee867d --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/reorder.dot.imageset/reorder.dot.pdf @@ -0,0 +1,101 @@ +%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 7.000000 4.000000 cm +0.000000 0.000000 0.000000 scn +8.500000 3.000000 m +9.328427 3.000000 10.000000 2.328427 10.000000 1.500000 c +10.000000 0.671574 9.328427 0.000000 8.500000 0.000000 c +7.671573 0.000000 7.000000 0.671574 7.000000 1.500000 c +7.000000 2.328427 7.671573 3.000000 8.500000 3.000000 c +h +1.500000 3.000000 m +2.328427 3.000000 3.000000 2.328427 3.000000 1.500000 c +3.000000 0.671574 2.328427 0.000000 1.500000 0.000000 c +0.671573 0.000000 0.000000 0.671574 0.000000 1.500000 c +0.000000 2.328427 0.671573 3.000000 1.500000 3.000000 c +h +8.500000 10.000000 m +9.328427 10.000000 10.000000 9.328427 10.000000 8.500000 c +10.000000 7.671573 9.328427 7.000000 8.500000 7.000000 c +7.671573 7.000000 7.000000 7.671573 7.000000 8.500000 c +7.000000 9.328427 7.671573 10.000000 8.500000 10.000000 c +h +1.500000 10.000000 m +2.328427 10.000000 3.000000 9.328427 3.000000 8.500000 c +3.000000 7.671573 2.328427 7.000000 1.500000 7.000000 c +0.671573 7.000000 0.000000 7.671573 0.000000 8.500000 c +0.000000 9.328427 0.671573 10.000000 1.500000 10.000000 c +h +8.500000 17.000000 m +9.328427 17.000000 10.000000 16.328426 10.000000 15.500000 c +10.000000 14.671573 9.328427 14.000000 8.500000 14.000000 c +7.671573 14.000000 7.000000 14.671573 7.000000 15.500000 c +7.000000 16.328426 7.671573 17.000000 8.500000 17.000000 c +h +1.500000 17.000000 m +2.328427 17.000000 3.000000 16.328426 3.000000 15.500000 c +3.000000 14.671573 2.328427 14.000000 1.500000 14.000000 c +0.671573 14.000000 0.000000 14.671573 0.000000 15.500000 c +0.000000 16.328426 0.671573 17.000000 1.500000 17.000000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 1644 +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 +0000001734 00000 n +0000001757 00000 n +0000001930 00000 n +0000002004 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2063 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Theme/Mastodon/Background/compose.poll.row.background.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Theme/Mastodon/Background/compose.poll.row.background.colorset/Contents.json new file mode 100644 index 000000000..bc3fb38b9 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Theme/Mastodon/Background/compose.poll.row.background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF7", + "green" : "0xF2", + "red" : "0xF2" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.263", + "green" : "0.208", + "red" : "0.192" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Theme/system/Background/compose.poll.row.background.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Theme/system/Background/compose.poll.row.background.colorset/Contents.json new file mode 100644 index 000000000..bc3fb38b9 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Theme/system/Background/compose.poll.row.background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF7", + "green" : "0xF2", + "red" : "0xF2" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.263", + "green" : "0.208", + "red" : "0.192" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift index 7fda6061f..f9db9d32f 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift @@ -143,6 +143,7 @@ public enum Asset { public static let people = ImageAsset(name: "Scene/Compose/people") public static let pollFill = ImageAsset(name: "Scene/Compose/poll.fill") public static let poll = ImageAsset(name: "Scene/Compose/poll") + public static let reorderDot = ImageAsset(name: "Scene/Compose/reorder.dot") } public enum Discovery { public static let profileCardBackground = ColorAsset(name: "Scene/Discovery/profile.card.background") @@ -209,6 +210,7 @@ public enum Asset { } public enum Theme { public enum Mastodon { + public static let composePollRowBackground = ColorAsset(name: "Theme/Mastodon/compose.poll.row.background") public static let composeToolbarBackground = ColorAsset(name: "Theme/Mastodon/compose.toolbar.background") public static let contentWarningOverlayBackground = ColorAsset(name: "Theme/Mastodon/content.warning.overlay.background") public static let navigationBarBackground = ColorAsset(name: "Theme/Mastodon/navigation.bar.background") @@ -229,6 +231,7 @@ public enum Asset { public static let tabBarItemInactiveIconColor = ColorAsset(name: "Theme/Mastodon/tab.bar.item.inactive.icon.color") } public enum System { + public static let composePollRowBackground = ColorAsset(name: "Theme/system/compose.poll.row.background") public static let composeToolbarBackground = ColorAsset(name: "Theme/system/compose.toolbar.background") public static let contentWarningOverlayBackground = ColorAsset(name: "Theme/system/content.warning.overlay.background") public static let navigationBarBackground = ColorAsset(name: "Theme/system/navigation.bar.background") diff --git a/MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeItem.swift index 5673f600d..53f25a036 100644 --- a/MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeItem.swift +++ b/MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeItem.swift @@ -21,11 +21,19 @@ extension PollComposeItem { public weak var textField: UITextField? + // input @Published public var text = "" @Published public var shouldBecomeFirstResponder = false + // output + @Published public var backgroundColor = ThemeService.shared.currentTheme.value.composePollRowBackgroundColor + public override init() { super.init() + + ThemeService.shared.currentTheme + .map { $0.composePollRowBackgroundColor } + .assign(to: &$backgroundColor) } } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/Theme/MastodonTheme.swift b/MastodonSDK/Sources/MastodonCore/Service/Theme/MastodonTheme.swift index 76173590e..563c3d48a 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Theme/MastodonTheme.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Theme/MastodonTheme.swift @@ -41,5 +41,6 @@ struct MastodonTheme: Theme { let contentWarningOverlayBackgroundColor = Asset.Theme.Mastodon.contentWarningOverlayBackground.color let profileFieldCollectionViewBackgroundColor = Asset.Theme.Mastodon.profileFieldCollectionViewBackground.color let composeToolbarBackgroundColor = Asset.Theme.Mastodon.composeToolbarBackground.color + let composePollRowBackgroundColor = Asset.Theme.Mastodon.composePollRowBackground.color let notificationStatusBorderColor = Asset.Theme.System.notificationStatusBorderColor.color } diff --git a/MastodonSDK/Sources/MastodonCore/Service/Theme/SystemTheme.swift b/MastodonSDK/Sources/MastodonCore/Service/Theme/SystemTheme.swift index cea10a281..ab297780b 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Theme/SystemTheme.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Theme/SystemTheme.swift @@ -41,5 +41,6 @@ struct SystemTheme: Theme { let contentWarningOverlayBackgroundColor = Asset.Theme.System.contentWarningOverlayBackground.color let profileFieldCollectionViewBackgroundColor = Asset.Theme.System.profileFieldCollectionViewBackground.color let composeToolbarBackgroundColor = Asset.Theme.System.composeToolbarBackground.color + let composePollRowBackgroundColor = Asset.Theme.System.composePollRowBackground.color let notificationStatusBorderColor = Asset.Theme.System.notificationStatusBorderColor.color } diff --git a/MastodonSDK/Sources/MastodonCore/Service/Theme/Theme.swift b/MastodonSDK/Sources/MastodonCore/Service/Theme/Theme.swift index ae555da00..bfe8e9c6e 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Theme/Theme.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Theme/Theme.swift @@ -40,6 +40,7 @@ public protocol Theme { var contentWarningOverlayBackgroundColor: UIColor { get } var profileFieldCollectionViewBackgroundColor: UIColor { get } var composeToolbarBackgroundColor: UIColor { get } + var composePollRowBackgroundColor: UIColor { get } var notificationStatusBorderColor: UIColor { get } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollAddOptionRow.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollAddOptionRow.swift new file mode 100644 index 000000000..5a8ae58d1 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollAddOptionRow.swift @@ -0,0 +1,59 @@ +// +// PollAddOptionRow.swift +// +// +// Created by MainasuK on 2022/10/26. +// + +import SwiftUI +import MastodonAsset +import MastodonCore + +public struct PollAddOptionRow: View { + + @StateObject var viewModel = ViewModel() + + public var body: some View { + HStack(alignment: .center, spacing: 16) { + HStack(alignment: .center, spacing: .zero) { + Image(systemName: "plus.circle") + .frame(width: 20, height: 20) + .padding(.leading, 16) + .padding(.trailing, 16 - 10) // 8pt for TextField leading + .font(.system(size: 17)) + PollOptionTextField( + text: $viewModel.text, + index: 999, + delegate: nil + ) { textField in + // do nothing + } + .hidden() + } + .background(Color(viewModel.backgroundColor)) + .cornerRadius(10) + .shadow(color: .black.opacity(0.3), radius: 2, x: 0, y: 1) + Image(uiImage: Asset.Scene.Compose.reorderDot.image.withRenderingMode(.alwaysTemplate)) + .foregroundColor(Color(UIColor.label)) + .hidden() + } + .background(Color.clear) + } + +} + +extension PollAddOptionRow { + public class ViewModel: ObservableObject { + // input + @Published public var text: String = "" + + // output + @Published public var backgroundColor = ThemeService.shared.currentTheme.value.composePollRowBackgroundColor + + public init() { + ThemeService.shared.currentTheme + .map { $0.composePollRowBackgroundColor } + .assign(to: &$backgroundColor) + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionRow.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionRow.swift index 89482ddef..0cf451d8d 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionRow.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionRow.swift @@ -6,6 +6,7 @@ // import SwiftUI +import MastodonAsset import MastodonCore public struct PollOptionRow: View { @@ -17,19 +18,34 @@ public struct PollOptionRow: View { let configurationHandler: (DeleteBackwardResponseTextField) -> Void public var body: some View { - PollOptionTextField( - text: $viewModel.text, - index: index ?? -1, - delegate: deleteBackwardResponseTextFieldRelayDelegate - ) { textField in - viewModel.textField = textField - configurationHandler(textField) - } - .onReceive(viewModel.$shouldBecomeFirstResponder) { shouldBecomeFirstResponder in - guard shouldBecomeFirstResponder else { return } - viewModel.shouldBecomeFirstResponder = false - viewModel.textField?.becomeFirstResponder() + HStack(alignment: .center, spacing: 16) { + HStack(alignment: .center, spacing: .zero) { + Image(systemName: "circle") + .frame(width: 20, height: 20) + .padding(.leading, 16) + .padding(.trailing, 16 - 10) // 8pt for TextField leading + .font(.system(size: 17)) + PollOptionTextField( + text: $viewModel.text, + index: index ?? -1, + delegate: deleteBackwardResponseTextFieldRelayDelegate + ) { textField in + viewModel.textField = textField + configurationHandler(textField) + } + .onReceive(viewModel.$shouldBecomeFirstResponder) { shouldBecomeFirstResponder in + guard shouldBecomeFirstResponder else { return } + viewModel.shouldBecomeFirstResponder = false + viewModel.textField?.becomeFirstResponder() + } + } + .background(Color(viewModel.backgroundColor)) + .cornerRadius(10) + .shadow(color: .black.opacity(0.3), radius: 2, x: 0, y: 1) + Image(uiImage: Asset.Scene.Compose.reorderDot.image.withRenderingMode(.alwaysTemplate)) + .foregroundColor(Color(UIColor.label)) } + .background(Color.clear) } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift index 0b2065008..5143bea35 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift @@ -26,8 +26,11 @@ public struct PollOptionTextField: UIViewRepresentable { textField.setContentHuggingPriority(.defaultHigh, for: .vertical) textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) textField.textInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) - textField.borderStyle = .roundedRect + textField.borderStyle = .none + textField.backgroundColor = .clear textField.returnKeyType = .next + textField.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 16, weight: .regular)) + textField.adjustsFontForContentSizeCategory = true return textField } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift index 679df22c0..732e3b509 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift @@ -75,6 +75,7 @@ struct ComposeContentToolbarView: View { } Spacer() Text("Hello") + .font(.system(size: 16, weight: .regular)) } .padding(.leading, 4) // 4 + 12 = 16 .padding(.trailing, 16) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift index 91f3923aa..569bd7a8e 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift @@ -80,7 +80,7 @@ extension ComposeContentView { metaContent: viewModel.name ) Text(viewModel.username) - .font(.subheadline) + .font(.system(size: 15, weight: .regular)) .foregroundColor(.secondary) Spacer() } @@ -107,6 +107,12 @@ extension ComposeContentView { // viewModel.customEmojiPickerInputViewModel.configure(textInput: textField) } } + if viewModel.maxPollOptionLimit != viewModel.pollOptions.count { + PollAddOptionRow() + .onTapGesture { + viewModel.createNewPollOptionIfCould() + } + } } VStack(spacing: .zero) { // expire configuration From b12825a96ac8a827856c5ffbc569c4e5f4a7f829 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 26 Oct 2022 18:58:25 +0800 Subject: [PATCH 044/658] feat: restore compose poll expire option --- .../ComposeContentViewModel.swift | 3 +- .../View/ComposeContentView.swift | 59 +++++-------------- 2 files changed, 16 insertions(+), 46 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index ca78649e1..cf67e8981 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -39,8 +39,9 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { options.append(PollComposeItem.Option()) return options }() + @Published public var pollExpireConfigurationOption: PollComposeItem.ExpireConfiguration.Option = .oneDay @Published public var maxPollOptionLimit = 4 - + @Published var isEmojiActive = false @Published var isContentWarningActive = false diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift index 569bd7a8e..4aeb65689 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift @@ -56,7 +56,11 @@ public struct ComposeContentView: View { } .onPreferenceChange(ViewFramePreferenceKey.self) { frame in logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): content frame: \(frame.debugDescription)") - viewModel.contentCellFrame = frame + let rect = frame.standardized + viewModel.contentCellFrame = CGRect( + origin: frame.origin, + size: CGSize(width: floor(rect.width), height: floor(rect.height)) + ) } ) Spacer() @@ -113,58 +117,23 @@ extension ComposeContentView { viewModel.createNewPollOptionIfCould() } } - } - VStack(spacing: .zero) { - // expire configuration Menu { - ForEach(PollComposeItem.ExpireConfiguration.Option.allCases, id: \.self) { option in - Button { - // viewModel.pollExpireConfiguration.option = option - // viewModel.pollExpireConfiguration = viewModel.pollExpireConfiguration - } label: { + Picker(selection: $viewModel.pollExpireConfigurationOption) { + ForEach(PollComposeItem.ExpireConfiguration.Option.allCases, id: \.self) { option in Text(option.title) } + } label: { + Text(L10n.Scene.Compose.Poll.durationTime(viewModel.pollExpireConfigurationOption.title)) } } label: { HStack { -// VectorImageView( -// image: Asset.ObjectTools.clock.image.withRenderingMode(.alwaysTemplate), -// tintColor: .secondaryLabel -// ) -// .frame(width: 24, height: 24) -// .padding(.vertical, 12) -// let text = viewModel.pollExpireConfigurationFormatter.string(from: TimeInterval(viewModel.pollExpireConfiguration.option.seconds)) ?? "-" -// Text(text) -// .font(.callout) -// .foregroundColor(.primary) -// Spacer() -// VectorImageView( -// image: Asset.Arrows.tablerChevronDown.image.withRenderingMode(.alwaysTemplate), -// tintColor: .secondaryLabel -// ) -// .frame(width: 24, height: 24) -// .padding(.vertical, 12) + Text(L10n.Scene.Compose.Poll.durationTime(viewModel.pollExpireConfigurationOption.title)) + .foregroundColor(Color(UIColor.label.withAlphaComponent(0.8))) // Gray/800 + .font(Font(UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 13, weight: .semibold)))) + Spacer() } + .padding(.vertical, 8) } - // multi-selection configuration -// Button { -// viewModel.pollMultipleConfiguration.isMultiple.toggle() -// viewModel.pollMultipleConfiguration = viewModel.pollMultipleConfiguration -// } label: { -// HStack { -// let selectionImage = viewModel.pollMultipleConfiguration.isMultiple ? Asset.Indices.checkmarkSquare.image.withRenderingMode(.alwaysTemplate) : Asset.Indices.square.image.withRenderingMode(.alwaysTemplate) -// VectorImageView( -// image: selectionImage, -// tintColor: .secondaryLabel -// ) -// .frame(width: 24, height: 24) -// .padding(.vertical, 12) -// Text(L10n.Scene.Compose.Vote.multiple) -// .font(.callout) -// .foregroundColor(.primary) -// Spacer() -// } -// } } } // end VStack } From 3100c59a3b8d7b9a35a33fd8c9dd3e79aaa99ff0 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 28 Oct 2022 19:06:18 +0800 Subject: [PATCH 045/658] feat: restore content warning input with black-yellow strip edges --- .../xcshareddata/swiftpm/Package.resolved | 9 ++ MastodonSDK/Package.swift | 2 + .../ComposeContentViewController.swift | 13 ++ ...oseContentViewModel+MetaTextDelegate.swift | 57 +++++++ .../ComposeContentViewModel.swift | 142 +++++++++++++++--- .../ComposeContentToolbarView+ViewModel.swift | 4 + .../View/ComposeContentToolbarView.swift | 14 +- .../View/ComposeContentView.swift | 67 ++++++++- 8 files changed, 284 insertions(+), 24 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4ee0ebeb7..9b6674434 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -126,6 +126,15 @@ "version" : "5.12.5" } }, + { + "identity" : "stripes", + "kind" : "remoteSourceControl", + "location" : "https://github.com/eneko/Stripes.git", + "state" : { + "revision" : "d533fd44b8043a3abbf523e733599173d6f98c11", + "version" : "0.2.0" + } + }, { "identity" : "swift-collections", "kind" : "remoteSourceControl", diff --git a/MastodonSDK/Package.swift b/MastodonSDK/Package.swift index ac968754d..0d3e562c0 100644 --- a/MastodonSDK/Package.swift +++ b/MastodonSDK/Package.swift @@ -48,6 +48,7 @@ let package = Package( .package(url: "https://github.com/vtourraine/ThirdPartyMailer.git", from: "2.1.0"), .package(url: "https://github.com/woxtu/UIHostingConfigurationBackport.git", from: "0.1.0"), .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.12.0"), + .package(url: "https://github.com/eneko/Stripes.git", from: "0.2.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -122,6 +123,7 @@ let package = Package( .product(name: "MetaTextKit", package: "MetaTextKit"), .product(name: "CropViewController", package: "TOCropViewController"), .product(name: "PanModal", package: "PanModal"), + .product(name: "Stripes", package: "Stripes"), ] ), .testTarget( diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index 09553d0f9..3417ed935 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -260,6 +260,9 @@ extension ComposeContentViewController { viewModel.$isPollActive.assign(to: &composeContentToolbarViewModel.$isPollActive) viewModel.$isEmojiActive.assign(to: &composeContentToolbarViewModel.$isEmojiActive) viewModel.$isContentWarningActive.assign(to: &composeContentToolbarViewModel.$isContentWarningActive) + viewModel.$maxTextInputLimit.assign(to: &composeContentToolbarViewModel.$maxTextInputLimit) + viewModel.$contentWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWeightedLength) + viewModel.$contentWarningWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWarningWeightedLength) } } @@ -385,6 +388,16 @@ extension ComposeContentViewController: ComposeContentToolbarViewDelegate { self.viewModel.isEmojiActive.toggle() case .contentWarning: self.viewModel.isContentWarningActive.toggle() + if self.viewModel.isContentWarningActive { + Task { @MainActor in + try? await Task.sleep(nanoseconds: .second / 20) // 0.05s + self.viewModel.setContentWarningTextViewFirstResponderIfNeeds() + } // end Task + } else { + if self.viewModel.contentWarningMetaText?.textView.isFirstResponder == true { + self.viewModel.setContentTextViewFirstResponderIfNeeds() + } + } case .visibility: assertionFailure() } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift new file mode 100644 index 000000000..80cc033e8 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift @@ -0,0 +1,57 @@ +// +// ComposeContentViewModel+MetaTextDelegate.swift +// +// +// Created by MainasuK on 2022/10/28. +// + +import os.log +import UIKit +import MetaTextKit +import TwitterMeta +import MastodonMeta + +// MARK: - MetaTextDelegate +extension ComposeContentViewModel: MetaTextDelegate { + + public enum MetaTextViewKind: Int { + case none + case content + case contentWarning + } + + public func metaText( + _ metaText: MetaText, + processEditing textStorage: MetaTextStorage + ) -> MetaContent? { + let kind = MetaTextViewKind(rawValue: metaText.textView.tag) ?? .none + + switch kind { + case .none: + assertionFailure() + return nil + + case .content: + let textInput = textStorage.string + self.content = textInput + + let content = MastodonContent( + content: textInput, + emojis: [:] // TODO: emojiViewModel?.emojis.asDictionary ?? [:] + ) + let metaContent = MastodonMetaContent.convert(text: content) + return metaContent + + case .contentWarning: + let textInput = textStorage.string.replacingOccurrences(of: "\n", with: " ") + self.contentWarning = textInput + + let content = MastodonContent( + content: textInput, + emojis: [:] // emojiViewModel?.emojis.asDictionary ?? [:] + ) + let metaContent = MastodonMetaContent.convert(text: content) + return metaContent + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index cf67e8981..53db9ab30 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -6,12 +6,13 @@ // import os.log -import Foundation +import UIKit import Combine import CoreDataStack import MastodonCore import Meta import MastodonMeta +import MetaTextKit public final class ComposeContentViewModel: NSObject, ObservableObject { @@ -30,6 +31,42 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { @Published var viewLayoutFrame = ViewLayoutFrame() @Published var authContext: AuthContext + // output + + // limit + @Published public var maxTextInputLimit = 500 + + // content + public weak var contentMetaText: MetaText? { + didSet { +// guard let textView = contentMetaText?.textView else { return } +// customEmojiPickerInputViewModel.configure(textInput: textView) + } + } + @Published public var initialContent = "" + @Published public var content = "" + @Published public var contentWeightedLength = 0 + @Published public var isContentEmpty = true + @Published public var isContentValid = true + @Published public var isContentEditing = false + + // content warning + weak var contentWarningMetaText: MetaText? { + didSet { + //guard let textView = contentWarningMetaText?.textView else { return } + //customEmojiPickerInputViewModel.configure(textInput: textView) + } + } + @Published public var isContentWarningActive = false + @Published public var contentWarning = "" + @Published public var contentWarningWeightedLength = 0 // set 0 when not composing + @Published public var isContentWarningEditing = false + + // author + @Published var avatarURL: URL? + @Published var name: MetaContent = PlaintextMetaContent(string: "") + @Published var username: String = "" + // poll @Published var isPollActive = false @Published public var pollOptions: [PollComposeItem.Option] = { @@ -42,23 +79,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { @Published public var pollExpireConfigurationOption: PollComposeItem.ExpireConfiguration.Option = .oneDay @Published public var maxPollOptionLimit = 4 + // emoji @Published var isEmojiActive = false - @Published var isContentWarningActive = false - - // output - - // content - @Published public var initialContent = "" - @Published public var content = "" - @Published public var contentWeightedLength = 0 - @Published public var isContentEmpty = true - @Published public var isContentValid = true - @Published public var isContentEditing = false - - // author - @Published var avatarURL: URL? - @Published var name: MetaContent = PlaintextMetaContent(string: "") - @Published var username: String = "" // UI & UX @Published var replyToCellFrame: CGRect = .zero @@ -87,6 +109,26 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { self.username = user.acctWithDomain } .store(in: &disposeBag) + + // bind text + $content + .map { $0.count } + .assign(to: &$contentWeightedLength) + + Publishers.CombineLatest( + $contentWarning, + $isContentWarningActive + ) + .map { $1 ? $0.count : 0 } + .assign(to: &$contentWarningWeightedLength) + + Publishers.CombineLatest3( + $contentWeightedLength, + $contentWarningWeightedLength, + $maxTextInputLimit + ) + .map { $0 + $1 <= $2 } + .assign(to: &$isContentValid) } deinit { @@ -120,6 +162,72 @@ extension ComposeContentViewModel { } } +// MARK: - UITextViewDelegate +extension ComposeContentViewModel: UITextViewDelegate { + public func textViewDidBeginEditing(_ textView: UITextView) { + 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/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift index e04648c09..4a34c77d4 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift @@ -27,6 +27,10 @@ extension ComposeContentToolbarView { @Published var isEmojiActive = false @Published var isContentWarningActive = false + @Published public var maxTextInputLimit = 500 + @Published public var contentWeightedLength = 0 + @Published public var contentWarningWeightedLength = 0 + // output init(delegate: ComposeContentToolbarViewDelegate) { diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift index 732e3b509..52026c636 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift @@ -74,8 +74,18 @@ struct ComposeContentToolbarView: View { } } Spacer() - Text("Hello") - .font(.system(size: 16, weight: .regular)) + let count: Int = { + if viewModel.isContentWarningActive { + return viewModel.contentWeightedLength + viewModel.contentWarningWeightedLength + } else { + return viewModel.contentWeightedLength + } + }() + let remains = viewModel.maxTextInputLimit - count + let isOverflow = remains < 0 + Text("\(remains)") + .foregroundColor(Color(isOverflow ? UIColor.systemRed : UIColor.secondaryLabel)) + .font(.system(size: isOverflow ? 18 : 16, weight: isOverflow ? .medium : .regular)) } .padding(.leading, 4) // 4 + 12 = 16 .padding(.trailing, 16) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift index 4aeb65689..25584848a 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift @@ -7,8 +7,10 @@ import os.log import SwiftUI +import MastodonAsset import MastodonCore import MastodonLocalization +import Stripes public struct ComposeContentView: View { @@ -22,14 +24,69 @@ public struct ComposeContentView: View { public var body: some View { VStack(spacing: .zero) { Group { + // content warning + if viewModel.isContentWarningActive { + MetaTextViewRepresentable( + string: $viewModel.contentWarning, + width: viewModel.viewLayoutFrame.layoutFrame.width - ComposeContentView.margin * 2, + configurationHandler: { metaText in + viewModel.contentWarningMetaText = metaText + metaText.textView.attributedPlaceholder = { + var attributes = metaText.textAttributes + attributes[.foregroundColor] = UIColor.secondaryLabel + return NSAttributedString( + string: L10n.Scene.Compose.contentInputPlaceholder, + attributes: attributes + ) + }() + metaText.textView.returnKeyType = .next + metaText.textView.tag = ComposeContentViewModel.MetaTextViewKind.contentWarning.rawValue + metaText.textView.delegate = viewModel + metaText.delegate = viewModel + } + ) + .fixedSize(horizontal: false, vertical: true) + .padding(.horizontal, ComposeContentView.margin) + .background( + Color(UIColor.systemBackground) + .overlay( + HStack { + Stripes(config: StripesConfig( + background: Color.yellow, + foreground: Color.black, + degrees: 45, + barWidth: 2.5, + barSpacing: 3.5 + )) + .frame(width: ComposeContentView.margin * 0.5) + .frame(maxHeight: .infinity) + .id(UUID()) + Spacer() + Stripes(config: StripesConfig( + background: Color.yellow, + foreground: Color.black, + degrees: 45, + barWidth: 2.5, + barSpacing: 3.5 + )) + .frame(width: ComposeContentView.margin * 0.5) + .frame(maxHeight: .infinity) + .scaleEffect(x: -1, y: 1, anchor: .center) + .id(UUID()) + } + ) + ) + } // end if viewModel.isContentWarningActive // author authorView .padding(.top, 14) + .padding(.horizontal, ComposeContentView.margin) // content editor MetaTextViewRepresentable( string: $viewModel.content, width: viewModel.viewLayoutFrame.layoutFrame.width - ComposeContentView.margin * 2, configurationHandler: { metaText in + viewModel.contentMetaText = metaText metaText.textView.attributedPlaceholder = { var attributes = metaText.textAttributes attributes[.foregroundColor] = UIColor.secondaryLabel @@ -39,16 +96,18 @@ public struct ComposeContentView: View { ) }() metaText.textView.keyboardType = .twitter - // metaText.textView.tag = ComposeContentViewModel.MetaTextViewKind.content.rawValue - // metaText.textView.delegate = viewModel - // metaText.delegate = viewModel + metaText.textView.tag = ComposeContentViewModel.MetaTextViewKind.content.rawValue + metaText.textView.delegate = viewModel + metaText.delegate = viewModel metaText.textView.becomeFirstResponder() } ) .frame(minHeight: 100) .fixedSize(horizontal: false, vertical: true) + .padding(.horizontal, ComposeContentView.margin) // poll pollView + .padding(.horizontal, ComposeContentView.margin) } .background( GeometryReader { proxy in @@ -65,8 +124,6 @@ public struct ComposeContentView: View { ) Spacer() } // end VStack - .padding(.horizontal, ComposeContentView.margin) -// .frame(alignment: .top) } // end body } From dbd72b3523adbc5e5ed5fec09a3897a3318bb4d7 Mon Sep 17 00:00:00 2001 From: NanoSector Date: Sun, 30 Oct 2022 17:50:15 +0100 Subject: [PATCH 046/658] 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 047/658] 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 668a1d28e27ab0c86fac30ceeaa0b3558d27d78a Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 31 Oct 2022 14:47:13 +0800 Subject: [PATCH 048/658] fix: AccountList scene not display items issue --- Mastodon/Coordinator/SceneCoordinator.swift | 2 +- Mastodon/Scene/Account/AccountListViewModel.swift | 6 ++++++ .../Entity/Mastodon/MastodonAuthentication.swift | 12 ++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index ae8d81d25..58323cc4e 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -216,7 +216,7 @@ extension SceneCoordinator { let rootViewController: UIViewController do { - let request = MastodonAuthentication.sortedFetchRequest + let request = MastodonAuthentication.activeSortedFetchRequest // use active order let _authentication = try appContext.managedObjectContext.fetch(request).first let _authContext = _authentication.flatMap { AuthContext(authentication: $0) } self.authContext = _authContext diff --git a/Mastodon/Scene/Account/AccountListViewModel.swift b/Mastodon/Scene/Account/AccountListViewModel.swift index 17eab07ea..e0aaf97fc 100644 --- a/Mastodon/Scene/Account/AccountListViewModel.swift +++ b/Mastodon/Scene/Account/AccountListViewModel.swift @@ -50,6 +50,12 @@ final class AccountListViewModel: NSObject { // end init mastodonAuthenticationFetchedResultsController.delegate = self + do { + try mastodonAuthenticationFetchedResultsController.performFetch() + authentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?.compactMap { $0.asRecrod } ?? [] + } catch { + assertionFailure(error.localizedDescription) + } $authentications .receive(on: DispatchQueue.main) diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonAuthentication.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonAuthentication.swift index 1c5c40851..2d8a97fad 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonAuthentication.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonAuthentication.swift @@ -148,6 +148,18 @@ extension MastodonAuthentication: Managed { public static var defaultSortDescriptors: [NSSortDescriptor] { return [NSSortDescriptor(keyPath: \MastodonAuthentication.createdAt, ascending: false)] } + + public static var activeSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \MastodonAuthentication.activedAt, ascending: false)] + } +} + +extension MastodonAuthentication { + public static var activeSortedFetchRequest: NSFetchRequest { + let request = NSFetchRequest(entityName: entityName) + request.sortDescriptors = activeSortDescriptors + return request + } } extension MastodonAuthentication { From a7d5e23406593b108f09391c171f2e43d80bda0a Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 31 Oct 2022 20:41:19 +0800 Subject: [PATCH 049/658] feat: [WIP] restore compose status publish function with background task support --- .../xcshareddata/swiftpm/Package.resolved | 9 + .../Scene/Compose/ComposeViewController.swift | 121 +++--- Mastodon/Scene/Compose/ComposeViewModel.swift | 2 +- ...tachmentContainerView+EmptyStateView.swift | 245 ++++++----- .../View/AttachmentContainerView.swift | 84 ++-- .../HomeTimelineViewController.swift | 5 +- ...eTimelineNavigationBarTitleViewModel.swift | 93 ++-- MastodonSDK/Package.swift | 2 + .../Sources/MastodonCore/AppContext.swift | 5 +- .../Sources/MastodonCore/AppError.swift | 13 + .../MastodonCore/Extension/FileManager.swift | 28 ++ .../Extension/NSItemProvider.swift | 145 +++++++ .../Extension/NSKeyValueObservation.swift | 15 + .../Model/Poll/PollComposeItem.swift | 4 +- .../MastodonCore/Service/API/APIService.swift | 2 +- .../PublisherService/PublisherService.swift | 109 +++++ .../StatusPublishResult.swift | 13 + .../PublisherService/StatusPublisher.swift | 14 + .../StatusPublisherReactor.swift | 10 + .../StatusPublisherState.swift | 14 + .../MastodonSDK/API/Mastodon+API+Media.swift | 20 + .../MastodonSDK/Query/MediaAttachment.swift | 12 + .../MastodonSDK/Query/SerialStream.swift | 16 +- .../Extension/UIAlertController.swift | 37 ++ .../Attachment/AttachmentView.swift | 246 +++++++++++ .../AttachmentViewModel+Upload.swift | 316 ++++++++++++++ .../Attachment/AttachmentViewModel.swift | 401 ++++++++++++++++++ .../ComposeContentViewModel.swift | 117 ++++- .../Publisher/MastodonStatusPublisher.swift | 180 ++++++++ .../View/Container/AttachmentView.swift | 29 -- 30 files changed, 2013 insertions(+), 294 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonCore/AppError.swift create mode 100644 MastodonSDK/Sources/MastodonCore/Extension/FileManager.swift create mode 100644 MastodonSDK/Sources/MastodonCore/Extension/NSItemProvider.swift create mode 100644 MastodonSDK/Sources/MastodonCore/Extension/NSKeyValueObservation.swift create mode 100644 MastodonSDK/Sources/MastodonCore/Service/PublisherService/PublisherService.swift create mode 100644 MastodonSDK/Sources/MastodonCore/Service/PublisherService/StatusPublishResult.swift create mode 100644 MastodonSDK/Sources/MastodonCore/Service/PublisherService/StatusPublisher.swift create mode 100644 MastodonSDK/Sources/MastodonCore/Service/PublisherService/StatusPublisherReactor.swift create mode 100644 MastodonSDK/Sources/MastodonCore/Service/PublisherService/StatusPublisherState.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Extension/UIAlertController.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift delete mode 100644 MastodonSDK/Sources/MastodonUI/View/Container/AttachmentView.swift diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9b6674434..34ffd227b 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -72,6 +72,15 @@ "version" : "4.2.2" } }, + { + "identity" : "kingfisher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Kingfisher.git", + "state" : { + "revision" : "44e891bdb61426a95e31492a67c7c0dfad1f87c5", + "version" : "7.4.1" + } + }, { "identity" : "metatextkit", "kind" : "remoteSourceControl", diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 54f3903b6..bf9145d6c 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -57,36 +57,35 @@ final class ComposeViewController: UIViewController, NeedsDependency { return barButtonItem }() -// 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(set) lazy var publishBarButtonItem: UIBarButtonItem = { -// configurePublishButtonApperance() -// let shadowBackgroundContainer = ShadowBackgroundContainer() -// publishButton.translatesAutoresizingMaskIntoConstraints = false -// shadowBackgroundContainer.addSubview(publishButton) -// NSLayoutConstraint.activate([ -// publishButton.topAnchor.constraint(equalTo: shadowBackgroundContainer.topAnchor), -// publishButton.leadingAnchor.constraint(equalTo: shadowBackgroundContainer.leadingAnchor), -// publishButton.trailingAnchor.constraint(equalTo: shadowBackgroundContainer.trailingAnchor), -// publishButton.bottomAnchor.constraint(equalTo: shadowBackgroundContainer.bottomAnchor), -// ]) -// let barButtonItem = UIBarButtonItem(customView: shadowBackgroundContainer) -// return barButtonItem -// }() -// -// 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) -// } + 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(set) lazy var publishBarButtonItem: UIBarButtonItem = { + configurePublishButtonApperance() + let shadowBackgroundContainer = ShadowBackgroundContainer() + publishButton.translatesAutoresizingMaskIntoConstraints = false + shadowBackgroundContainer.addSubview(publishButton) + NSLayoutConstraint.activate([ + publishButton.topAnchor.constraint(equalTo: shadowBackgroundContainer.topAnchor), + publishButton.leadingAnchor.constraint(equalTo: shadowBackgroundContainer.leadingAnchor), + publishButton.trailingAnchor.constraint(equalTo: shadowBackgroundContainer.trailingAnchor), + publishButton.bottomAnchor.constraint(equalTo: shadowBackgroundContainer.bottomAnchor), + ]) + let barButtonItem = UIBarButtonItem(customView: shadowBackgroundContainer) + return barButtonItem + }() + 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) + } // var systemKeyboardHeight: CGFloat = .zero { // didSet { @@ -106,7 +105,6 @@ final class ComposeViewController: UIViewController, NeedsDependency { // var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint! // let composeToolbarBackgroundView = UIView() // - // // private(set) lazy var autoCompleteViewController: AutoCompleteViewController = { // let viewController = AutoCompleteViewController() @@ -142,20 +140,20 @@ extension ComposeViewController { super.viewDidLoad() navigationItem.leftBarButtonItem = cancelBarButtonItem - // navigationItem.rightBarButtonItem = publishBarButtonItem - // viewModel.traitCollectionDidChangePublisher - // .receive(on: DispatchQueue.main) - // .sink { [weak self] _ in - // 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) - // } - // self.navigationItem.rightBarButtonItems = items - // } - // .store(in: &disposeBag) - // publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) + navigationItem.rightBarButtonItem = publishBarButtonItem + viewModel.traitCollectionDidChangePublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + 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) + } + self.navigationItem.rightBarButtonItems = items + } + .store(in: &disposeBag) + publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) addChild(composeContentViewController) composeContentViewController.view.translatesAutoresizingMaskIntoConstraints = false @@ -602,8 +600,8 @@ extension ComposeViewController { 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) + @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 { @@ -613,17 +611,32 @@ extension ComposeViewController { // 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) -// assertionFailure() -// -// dismiss(animated: true, completion: nil) -// } + + // context.statusPublishService.publish(composeViewModel: viewModel) + + do { + let statusPublisher = try composeContentViewModel.statusPublisher() + // let result = try await statusPublisher.publish(api: context.apiService, authContext: viewModel.authContext) + // if let reactor = presentingViewController?.topMostNotModal as? StatusPublisherReactor { + // statusPublisher.reactor = reactor + // } + viewModel.context.publisherService.enqueue( + statusPublisher: statusPublisher, + authContext: viewModel.authContext + ) + } catch { + let alertController = UIAlertController.standardAlert(of: error) + present(alertController, animated: true) + return + } + + dismiss(animated: true, completion: nil) + } } diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 6e9a50fcc..45c9f1e93 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -44,7 +44,7 @@ final class ComposeViewModel: NSObject { // @Published var autoCompleteRetryLayoutTimes = 0 // @Published var autoCompleteInfo: ComposeViewController.AutoCompleteInfo? = nil -// let traitCollectionDidChangePublisher = CurrentValueSubject(Void()) // use CurrentValueSubject to make initial event emit + let traitCollectionDidChangePublisher = CurrentValueSubject(Void()) // use CurrentValueSubject to make initial event emit // var isViewAppeared = false // output diff --git a/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift b/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift index 0dabe7790..d976cbff7 100644 --- a/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift +++ b/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift @@ -11,128 +11,127 @@ import MastodonCore import MastodonUI import MastodonLocalization -extension AttachmentContainerView { - final class EmptyStateView: UIView { - - 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 imageView: UIImageView = { - let imageView = UIImageView() - imageView.tintColor = Asset.Colors.Label.secondary.color - imageView.image = AttachmentContainerView.EmptyStateView.photoFillSplitImage - return imageView - }() - let label: UILabel = { - let label = UILabel() - label.font = .preferredFont(forTextStyle: .body) - label.textColor = Asset.Colors.Label.secondary.color - label.textAlignment = .center - label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) - label.numberOfLines = 2 - label.adjustsFontSizeToFitWidth = true - label.minimumScaleFactor = 0.3 - return label - }() - - override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - - } -} +//extension AttachmentContainerView { +// final class EmptyStateView: UIView { +// +// 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 imageView: UIImageView = { +// let imageView = UIImageView() +// imageView.tintColor = Asset.Colors.Label.secondary.color +// imageView.image = AttachmentContainerView.EmptyStateView.photoFillSplitImage +// return imageView +// }() +// let label: UILabel = { +// let label = UILabel() +// label.font = .preferredFont(forTextStyle: .body) +// label.textColor = Asset.Colors.Label.secondary.color +// label.textAlignment = .center +// label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) +// label.numberOfLines = 2 +// label.adjustsFontSizeToFitWidth = true +// label.minimumScaleFactor = 0.3 +// return label +// }() +// +// override init(frame: CGRect) { +// super.init(frame: frame) +// _init() +// } +// +// required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } +// +// } +//} -extension AttachmentContainerView.EmptyStateView { - private func _init() { - layer.masksToBounds = true - layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius - layer.cornerCurve = .continuous - backgroundColor = ThemeService.shared.currentTheme.value.systemGroupedBackgroundColor - - let stackView = UIStackView() - stackView.axis = .vertical - stackView.alignment = .center - stackView.translatesAutoresizingMaskIntoConstraints = false - addSubview(stackView) - NSLayoutConstraint.activate([ - stackView.topAnchor.constraint(equalTo: topAnchor), - stackView.leadingAnchor.constraint(equalTo: leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: trailingAnchor), - stackView.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - let topPaddingView = UIView() - let middlePaddingView = UIView() - let bottomPaddingView = UIView() - - topPaddingView.translatesAutoresizingMaskIntoConstraints = false - stackView.addArrangedSubview(topPaddingView) - imageView.translatesAutoresizingMaskIntoConstraints = false - stackView.addArrangedSubview(imageView) - NSLayoutConstraint.activate([ - imageView.widthAnchor.constraint(equalToConstant: 92).priority(.defaultHigh), - imageView.heightAnchor.constraint(equalToConstant: 76).priority(.defaultHigh), - ]) - imageView.setContentHuggingPriority(.required - 1, for: .vertical) - middlePaddingView.translatesAutoresizingMaskIntoConstraints = false - stackView.addArrangedSubview(middlePaddingView) - stackView.addArrangedSubview(label) - bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false - stackView.addArrangedSubview(bottomPaddingView) - NSLayoutConstraint.activate([ - topPaddingView.heightAnchor.constraint(equalTo: middlePaddingView.heightAnchor, multiplier: 1.5), - bottomPaddingView.heightAnchor.constraint(equalTo: middlePaddingView.heightAnchor, multiplier: 1.5), - ]) - } -} - -#if canImport(SwiftUI) && DEBUG -import SwiftUI - -struct AttachmentContainerView_EmptyStateView_Previews: PreviewProvider { - - static var previews: some View { - Group { - UIViewPreview(width: 375) { - let emptyStateView = AttachmentContainerView.EmptyStateView() - NSLayoutConstraint.activate([ - emptyStateView.heightAnchor.constraint(equalToConstant: 205) - ]) - return emptyStateView - } - .previewLayout(.fixed(width: 375, height: 205)) - UIViewPreview(width: 375) { - let emptyStateView = AttachmentContainerView.EmptyStateView() - NSLayoutConstraint.activate([ - emptyStateView.heightAnchor.constraint(equalToConstant: 205) - ]) - return emptyStateView - } - .preferredColorScheme(.dark) - .previewLayout(.fixed(width: 375, height: 205)) - UIViewPreview(width: 375) { - let emptyStateView = AttachmentContainerView.EmptyStateView() - emptyStateView.imageView.image = AttachmentContainerView.EmptyStateView.videoSplashImage - emptyStateView.label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video) - - NSLayoutConstraint.activate([ - emptyStateView.heightAnchor.constraint(equalToConstant: 205) - ]) - return emptyStateView - } - .previewLayout(.fixed(width: 375, height: 205)) - } - } - -} - -#endif +//extension AttachmentContainerView.EmptyStateView { +// private func _init() { +// layer.masksToBounds = true +// layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius +// layer.cornerCurve = .continuous +// backgroundColor = ThemeService.shared.currentTheme.value.systemGroupedBackgroundColor +// +// let stackView = UIStackView() +// stackView.axis = .vertical +// stackView.alignment = .center +// stackView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(stackView) +// NSLayoutConstraint.activate([ +// stackView.topAnchor.constraint(equalTo: topAnchor), +// stackView.leadingAnchor.constraint(equalTo: leadingAnchor), +// stackView.trailingAnchor.constraint(equalTo: trailingAnchor), +// stackView.bottomAnchor.constraint(equalTo: bottomAnchor), +// ]) +// let topPaddingView = UIView() +// let middlePaddingView = UIView() +// let bottomPaddingView = UIView() +// +// topPaddingView.translatesAutoresizingMaskIntoConstraints = false +// stackView.addArrangedSubview(topPaddingView) +// imageView.translatesAutoresizingMaskIntoConstraints = false +// stackView.addArrangedSubview(imageView) +// NSLayoutConstraint.activate([ +// imageView.widthAnchor.constraint(equalToConstant: 92).priority(.defaultHigh), +// imageView.heightAnchor.constraint(equalToConstant: 76).priority(.defaultHigh), +// ]) +// imageView.setContentHuggingPriority(.required - 1, for: .vertical) +// middlePaddingView.translatesAutoresizingMaskIntoConstraints = false +// stackView.addArrangedSubview(middlePaddingView) +// stackView.addArrangedSubview(label) +// bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false +// stackView.addArrangedSubview(bottomPaddingView) +// NSLayoutConstraint.activate([ +// topPaddingView.heightAnchor.constraint(equalTo: middlePaddingView.heightAnchor, multiplier: 1.5), +// bottomPaddingView.heightAnchor.constraint(equalTo: middlePaddingView.heightAnchor, multiplier: 1.5), +// ]) +// } +//} +//#if canImport(SwiftUI) && DEBUG +//import SwiftUI +// +//struct AttachmentContainerView_EmptyStateView_Previews: PreviewProvider { +// +// static var previews: some View { +// Group { +// UIViewPreview(width: 375) { +// let emptyStateView = AttachmentContainerView.EmptyStateView() +// NSLayoutConstraint.activate([ +// emptyStateView.heightAnchor.constraint(equalToConstant: 205) +// ]) +// return emptyStateView +// } +// .previewLayout(.fixed(width: 375, height: 205)) +// UIViewPreview(width: 375) { +// let emptyStateView = AttachmentContainerView.EmptyStateView() +// NSLayoutConstraint.activate([ +// emptyStateView.heightAnchor.constraint(equalToConstant: 205) +// ]) +// return emptyStateView +// } +// .preferredColorScheme(.dark) +// .previewLayout(.fixed(width: 375, height: 205)) +// UIViewPreview(width: 375) { +// let emptyStateView = AttachmentContainerView.EmptyStateView() +// emptyStateView.imageView.image = AttachmentContainerView.EmptyStateView.videoSplashImage +// emptyStateView.label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video) +// +// NSLayoutConstraint.activate([ +// emptyStateView.heightAnchor.constraint(equalToConstant: 205) +// ]) +// return emptyStateView +// } +// .previewLayout(.fixed(width: 375, height: 205)) +// } +// } +// +//} +// +//#endif diff --git a/Mastodon/Scene/Compose/View/AttachmentContainerView.swift b/Mastodon/Scene/Compose/View/AttachmentContainerView.swift index dd6a2b0a9..4e8fe4e7c 100644 --- a/Mastodon/Scene/Compose/View/AttachmentContainerView.swift +++ b/Mastodon/Scene/Compose/View/AttachmentContainerView.swift @@ -9,10 +9,10 @@ import UIKit import SwiftUI import MastodonUI -final class AttachmentContainerView: UIView { - - static let containerViewCornerRadius: CGFloat = 4 - +//final class AttachmentContainerView: UIView { +// +// static let containerViewCornerRadius: CGFloat = 4 +// // var descriptionBackgroundViewFrameObservation: NSKeyValueObservation? // // let activityIndicatorView: UIActivityIndicatorView = { @@ -60,35 +60,35 @@ final class AttachmentContainerView: UIView { // textView.returnKeyType = .done // return textView // }() - - private(set) lazy var contentView = AttachmentView(viewModel: viewModel) - public var viewModel: AttachmentView.ViewModel! - - override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} +// +// private(set) lazy var contentView = AttachmentView(viewModel: viewModel) +// public var viewModel: AttachmentView.ViewModel! +// +// override init(frame: CGRect) { +// super.init(frame: frame) +// _init() +// } +// +// required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } +// +//} -extension AttachmentContainerView { - - private func _init() { - let hostingViewController = UIHostingController(rootView: contentView) - hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false - addSubview(hostingViewController.view) - NSLayoutConstraint.activate([ - hostingViewController.view.topAnchor.constraint(equalTo: topAnchor), - hostingViewController.view.leadingAnchor.constraint(equalTo: leadingAnchor), - hostingViewController.view.trailingAnchor.constraint(equalTo: trailingAnchor), - hostingViewController.view.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - +//extension AttachmentContainerView { +// +// private func _init() { +// let hostingViewController = UIHostingController(rootView: contentView) +// hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false +// addSubview(hostingViewController.view) +// NSLayoutConstraint.activate([ +// hostingViewController.view.topAnchor.constraint(equalTo: topAnchor), +// hostingViewController.view.leadingAnchor.constraint(equalTo: leadingAnchor), +// hostingViewController.view.trailingAnchor.constraint(equalTo: trailingAnchor), +// hostingViewController.view.bottomAnchor.constraint(equalTo: bottomAnchor), +// ]) +// // previewImageView.translatesAutoresizingMaskIntoConstraints = false // addSubview(previewImageView) // NSLayoutConstraint.activate([ @@ -144,24 +144,24 @@ extension AttachmentContainerView { // activityIndicatorView.startAnimating() // // descriptionTextView.delegate = self - } - -// override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { +// } +// +//// override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { // super.traitCollectionDidChange(previousTraitCollection) // // setupBroader() // } - -} - -extension AttachmentContainerView { - +// +//} +// +//extension AttachmentContainerView { +// // private func setupBroader() { // emptyStateView.layer.borderWidth = 1 // emptyStateView.layer.borderColor = traitCollection.userInterfaceStyle == .dark ? ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor.cgColor : nil // } - -} +// +//} //// MARK: - UITextViewDelegate //extension AttachmentContainerView: UITextViewDelegate { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index b287f3b71..3efcd5cbe 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -165,6 +165,7 @@ extension HomeTimelineViewController { tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) + // // layout publish progress publishProgressView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(publishProgressView) NSLayoutConstraint.activate([ @@ -204,10 +205,12 @@ extension HomeTimelineViewController { } .store(in: &disposeBag) - viewModel.homeTimelineNavigationBarTitleViewModel.publishingProgress + context.publisherService.$currentPublishProgress .receive(on: DispatchQueue.main) .sink { [weak self] progress in guard let self = self else { return } + let progress = Float(progress) + guard progress > 0 else { let dismissAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeInOut) dismissAnimator.addAnimations { diff --git a/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift b/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift index 09e750ed1..79f568edf 100644 --- a/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift @@ -49,6 +49,29 @@ final class HomeTimelineNavigationBarTitleViewModel { .assign(to: \.value, on: isOffline) .store(in: &disposeBag) + Publishers.CombineLatest( + context.publisherService.$statusPublishers, + context.publisherService.statusPublishResult.prepend(.failure(AppError.badRequest)) + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] statusPublishers, publishResult in + guard let self = self else { return } + + if statusPublishers.isEmpty { + self.isPublishingPost.value = false + self.isPublished.value = false + } else { + self.isPublishingPost.value = true + switch publishResult { + case .success: + self.isPublished.value = true + case .failure: + self.isPublished.value = false + } + } + } + .store(in: &disposeBag) + // context.statusPublishService.latestPublishingComposeViewModel // .receive(on: DispatchQueue.main) // .sink { [weak self] composeViewModel in @@ -82,19 +105,19 @@ final class HomeTimelineNavigationBarTitleViewModel { .assign(to: \.value, on: state) .store(in: &disposeBag) - state - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak self] state in - guard let self = self else { return } - switch state { - case .publishingPostLabel: - self.setupPublishingProgress() - default: - self.suspendPublishingProgress() - } - } - .store(in: &disposeBag) +// state +// .removeDuplicates() +// .receive(on: DispatchQueue.main) +// .sink { [weak self] state in +// guard let self = self else { return } +// switch state { +// case .publishingPostLabel: +// self.setupPublishingProgress() +// default: +// self.suspendPublishingProgress() +// } +// } +// .store(in: &disposeBag) } } @@ -150,26 +173,26 @@ extension HomeTimelineNavigationBarTitleViewModel { } // MARK: Publish post state -extension HomeTimelineNavigationBarTitleViewModel { - - func setupPublishingProgress() { - let progressUpdatePublisher = Timer.publish(every: 0.016, on: .main, in: .common) // ~ 60FPS - .autoconnect() - .share() - .eraseToAnyPublisher() - - publishingProgressSubscription = progressUpdatePublisher - .map { _ in Float(0) } - .scan(0.0) { progress, _ -> Float in - return 0.95 * progress + 0.05 // progress + 0.05 * (1.0 - progress). ~ 1 sec to 0.95 (under 60FPS) - } - .subscribe(publishingProgress) - } - - func suspendPublishingProgress() { - publishingProgressSubscription = nil - publishingProgress.send(0) - } - -} +//extension HomeTimelineNavigationBarTitleViewModel { +// +// func setupPublishingProgress() { +// let progressUpdatePublisher = Timer.publish(every: 0.016, on: .main, in: .common) // ~ 60FPS +// .autoconnect() +// .share() +// .eraseToAnyPublisher() +// +// publishingProgressSubscription = progressUpdatePublisher +// .map { _ in Float(0) } +// .scan(0.0) { progress, _ -> Float in +// return 0.95 * progress + 0.05 // progress + 0.05 * (1.0 - progress). ~ 1 sec to 0.95 (under 60FPS) +// } +// .subscribe(publishingProgress) +// } +// +// func suspendPublishingProgress() { +// publishingProgressSubscription = nil +// publishingProgress.send(0) +// } +// +//} diff --git a/MastodonSDK/Package.swift b/MastodonSDK/Package.swift index 0d3e562c0..a9c9ab9db 100644 --- a/MastodonSDK/Package.swift +++ b/MastodonSDK/Package.swift @@ -49,6 +49,7 @@ let package = Package( .package(url: "https://github.com/woxtu/UIHostingConfigurationBackport.git", from: "0.1.0"), .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"), ], 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: "CropViewController", package: "TOCropViewController"), .product(name: "PanModal", package: "PanModal"), .product(name: "Stripes", package: "Stripes"), + .product(name: "Kingfisher", package: "Kingfisher"), ] ), .testTarget( diff --git a/MastodonSDK/Sources/MastodonCore/AppContext.swift b/MastodonSDK/Sources/MastodonCore/AppContext.swift index 1965a91e2..d44c1ea5a 100644 --- a/MastodonSDK/Sources/MastodonCore/AppContext.swift +++ b/MastodonSDK/Sources/MastodonCore/AppContext.swift @@ -24,7 +24,8 @@ public class AppContext: ObservableObject { public let apiService: APIService public let authenticationService: AuthenticationService public let emojiService: EmojiService - public let statusPublishService = StatusPublishService() + // public let statusPublishService = StatusPublishService() + public let publisherService: PublisherService public let notificationService: NotificationService public let settingService: SettingService public let instanceService: InstanceService @@ -67,6 +68,8 @@ public class AppContext: ObservableObject { apiService: apiService ) + publisherService = .init(apiService: _apiService) + let _notificationService = NotificationService( apiService: _apiService, authenticationService: _authenticationService diff --git a/MastodonSDK/Sources/MastodonCore/AppError.swift b/MastodonSDK/Sources/MastodonCore/AppError.swift new file mode 100644 index 000000000..a8aea55b9 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/AppError.swift @@ -0,0 +1,13 @@ +// +// AppError.swift +// +// +// Created by MainasuK on 2022-8-8. +// + +import Foundation + +public enum AppError: Error { + case badRequest + case badAuthentication +} diff --git a/MastodonSDK/Sources/MastodonCore/Extension/FileManager.swift b/MastodonSDK/Sources/MastodonCore/Extension/FileManager.swift new file mode 100644 index 000000000..9a7ee6601 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Extension/FileManager.swift @@ -0,0 +1,28 @@ +// +// FileManager.swift +// +// +// Created by MainasuK on 2022-1-15. +// + +import os.log +import Foundation + +extension FileManager { + static let logger = Logger(subsystem: "FileManager", category: "File") + + public func createTemporaryFileURL( + filename: String, + pathExtension: String + ) throws -> URL { + let tempDirectoryURL = FileManager.default.temporaryDirectory + let fileURL = tempDirectoryURL + .appendingPathComponent(filename) + .appendingPathExtension(pathExtension) + try FileManager.default.createDirectory(at: tempDirectoryURL, withIntermediateDirectories: true, attributes: nil) + + Self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): create temporary file at: \(fileURL.debugDescription)") + + return fileURL + } +} diff --git a/MastodonSDK/Sources/MastodonCore/Extension/NSItemProvider.swift b/MastodonSDK/Sources/MastodonCore/Extension/NSItemProvider.swift new file mode 100644 index 000000000..c6fbff4f5 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Extension/NSItemProvider.swift @@ -0,0 +1,145 @@ +// +// NSItemProvider.swift +// +// +// Created by MainasuK on 2021/11/19. +// + +import os.log +import Foundation +import UniformTypeIdentifiers +import MobileCoreServices +import PhotosUI + +// load image with low memory usage +// Refs: https://christianselig.com/2020/09/phpickerviewcontroller-efficiently/ + +extension NSItemProvider { + + static let logger = Logger(subsystem: "NSItemProvider", category: "Logic") + + public struct ImageLoadResult { + public let data: Data + public let type: UTType? + + public init(data: Data, type: UTType?) { + self.data = data + self.type = type + } + } + + public func loadImageData() async throws -> ImageLoadResult? { + try await withCheckedThrowingContinuation { continuation in + loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { url, error in + if let error = error { + continuation.resume(with: .failure(error)) + return + } + + guard let url = url else { + continuation.resume(with: .success(nil)) + assertionFailure() + return + } + + let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary + guard let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions) else { + return + } + + #if APP_EXTENSION + let maxPixelSize: Int = 4096 // not limit but may upload fail + #else + let maxPixelSize: Int = 1536 // fit 120MB RAM limit + #endif + + let downsampleOptions = [ + kCGImageSourceCreateThumbnailFromImageAlways: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceThumbnailMaxPixelSize: maxPixelSize, + ] as CFDictionary + + guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else { + continuation.resume(with: .success(nil)) + return + } + + let data = NSMutableData() + guard let imageDestination = CGImageDestinationCreateWithData(data, kUTTypeJPEG, 1, nil) else { + continuation.resume(with: .success(nil)) + assertionFailure() + return + } + + let isPNG: Bool = { + guard let utType = cgImage.utType else { return false } + return (utType as String) == UTType.png.identifier + + }() + + let destinationProperties = [ + kCGImageDestinationLossyCompressionQuality: isPNG ? 1.0 : 0.75 + ] as CFDictionary + + CGImageDestinationAddImage(imageDestination, cgImage, destinationProperties) + CGImageDestinationFinalize(imageDestination) + + let dataSize = ByteCountFormatter.string(fromByteCount: Int64(data.length), countStyle: .memory) + NSItemProvider.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): load image \(dataSize)") + + let result = ImageLoadResult( + data: data as Data, + type: cgImage.utType.flatMap { UTType($0 as String) } + ) + + continuation.resume(with: .success(result)) + } + } + } + +} + +extension NSItemProvider { + + public struct VideoLoadResult { + public let url: URL + public let sizeInBytes: UInt64 + } + + public func loadVideoData() async throws -> VideoLoadResult? { + try await withCheckedThrowingContinuation { continuation in + loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in + if let error = error { + continuation.resume(with: .failure(error)) + return + } + + guard let url = url, + let attribute = try? FileManager.default.attributesOfItem(atPath: url.path), + let sizeInBytes = attribute[.size] as? UInt64 + else { + continuation.resume(with: .success(nil)) + assertionFailure() + return + } + + do { + let fileURL = try FileManager.default.createTemporaryFileURL( + filename: UUID().uuidString, + pathExtension: url.pathExtension + ) + try FileManager.default.copyItem(at: url, to: fileURL) + let result = VideoLoadResult( + url: fileURL, + sizeInBytes: sizeInBytes + ) + + continuation.resume(with: .success(result)) + } catch { + continuation.resume(with: .failure(error)) + } + } // end loadFileRepresentation + } // end try await withCheckedThrowingContinuation + } // end func + +} diff --git a/MastodonSDK/Sources/MastodonCore/Extension/NSKeyValueObservation.swift b/MastodonSDK/Sources/MastodonCore/Extension/NSKeyValueObservation.swift new file mode 100644 index 000000000..9d1088ead --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Extension/NSKeyValueObservation.swift @@ -0,0 +1,15 @@ +// +// NSKeyValueObservation.swift +// Twidere +// +// Created by Cirno MainasuK on 2020-7-20. +// Copyright © 2020 Twidere. All rights reserved. +// + +import Foundation + +extension NSKeyValueObservation { + public func store(in set: inout Set) { + set.insert(self) + } +} diff --git a/MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeItem.swift index 53f25a036..384cb49d2 100644 --- a/MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeItem.swift +++ b/MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeItem.swift @@ -94,12 +94,14 @@ extension PollComposeItem { public final class MultipleConfiguration: Hashable, ObservableObject { private let id = UUID() - @Published public var isMultiple = false + @Published public var isMultiple: Option = false public init() { // end init } + public typealias Option = Bool + public static func == (lhs: MultipleConfiguration, rhs: MultipleConfiguration) -> Bool { return lhs.id == rhs.id && lhs.isMultiple == rhs.isMultiple diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService.swift index d89760576..44eb9938e 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService.swift @@ -25,7 +25,7 @@ public final class APIService { let session: URLSession // input - let backgroundManagedObjectContext: NSManagedObjectContext + public let backgroundManagedObjectContext: NSManagedObjectContext // output public let error = PassthroughSubject() diff --git a/MastodonSDK/Sources/MastodonCore/Service/PublisherService/PublisherService.swift b/MastodonSDK/Sources/MastodonCore/Service/PublisherService/PublisherService.swift new file mode 100644 index 000000000..1c62a38d2 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Service/PublisherService/PublisherService.swift @@ -0,0 +1,109 @@ +// +// PublisherService.swift +// +// +// Created by MainasuK on 2021-12-2. +// + +import os.log +import UIKit +import Combine + +public final class PublisherService { + + var disposeBag = Set() + + let logger = Logger(subsystem: "PublisherService", category: "Service") + + // input + let apiService: APIService + + @Published public private(set) var statusPublishers: [StatusPublisher] = [] + + // output + public let statusPublishResult = PassthroughSubject, Never>() + + var currentPublishProgressObservation: NSKeyValueObservation? + @Published public var currentPublishProgress: Double = 0 + + public init( + apiService: APIService + ) { + self.apiService = apiService + + $statusPublishers + .receive(on: DispatchQueue.main) + .sink { [weak self] publishers in + guard let self = self else { return } + guard let last = publishers.last else { + self.currentPublishProgressObservation = nil + return + } + + self.currentPublishProgressObservation = last.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.currentPublishProgress = progress.fractionCompleted + } + } + .store(in: &disposeBag) + + $statusPublishers + .filter { $0.isEmpty } + .delay(for: 1, scheduler: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + self.currentPublishProgress = 0 + } + .store(in: &disposeBag) + + statusPublishResult + .receive(on: DispatchQueue.main) + .sink { result in + switch result { + case .success: + break + // TODO: + // update store review count trigger + // UserDefaults.shared.storeReviewInteractTriggerCount += 1 + case .failure: + break + } + } + .store(in: &disposeBag) + } + +} + +extension PublisherService { + + @MainActor + public func enqueue(statusPublisher publisher: StatusPublisher, authContext: AuthContext) { + guard !statusPublishers.contains(where: { $0 === publisher }) else { + assertionFailure() + return + } + statusPublishers.append(publisher) + + Task { + do { + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): publish status…") + let result = try await publisher.publish(api: apiService, authContext: authContext) + + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): publish status success") + self.statusPublishResult.send(.success(result)) + self.statusPublishers.removeAll(where: { $0 === publisher }) + + } catch is CancellationError { + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): publish cancelled") + self.statusPublishers.removeAll(where: { $0 === publisher }) + + } catch { + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): publish failure: \(error.localizedDescription)") + self.statusPublishResult.send(.failure(error)) + self.currentPublishProgress = 0 + } + } + } +} diff --git a/MastodonSDK/Sources/MastodonCore/Service/PublisherService/StatusPublishResult.swift b/MastodonSDK/Sources/MastodonCore/Service/PublisherService/StatusPublishResult.swift new file mode 100644 index 000000000..63b8650f2 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Service/PublisherService/StatusPublishResult.swift @@ -0,0 +1,13 @@ +// +// StatusPublishResult.swift +// +// +// Created by MainasuK on 2021-11-26. +// + +import Foundation +import MastodonSDK + +public enum StatusPublishResult { + case mastodon(Mastodon.Response.Content) +} diff --git a/MastodonSDK/Sources/MastodonCore/Service/PublisherService/StatusPublisher.swift b/MastodonSDK/Sources/MastodonCore/Service/PublisherService/StatusPublisher.swift new file mode 100644 index 000000000..e87a6bad1 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Service/PublisherService/StatusPublisher.swift @@ -0,0 +1,14 @@ +// +// StatusPublisher.swift +// +// +// Created by MainasuK on 2021-11-26. +// + +import Foundation + +public protocol StatusPublisher: ProgressReporting { + var state: Published.Publisher { get } + var reactor: StatusPublisherReactor? { get set } + func publish(api: APIService, authContext: AuthContext) async throws -> StatusPublishResult +} diff --git a/MastodonSDK/Sources/MastodonCore/Service/PublisherService/StatusPublisherReactor.swift b/MastodonSDK/Sources/MastodonCore/Service/PublisherService/StatusPublisherReactor.swift new file mode 100644 index 000000000..03d65f94c --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Service/PublisherService/StatusPublisherReactor.swift @@ -0,0 +1,10 @@ +// +// StatusPublisherReactor.swift +// +// +// Created by MainasuK on 2022/10/27. +// + +import Foundation + +public protocol StatusPublisherReactor: AnyObject { } diff --git a/MastodonSDK/Sources/MastodonCore/Service/PublisherService/StatusPublisherState.swift b/MastodonSDK/Sources/MastodonCore/Service/PublisherService/StatusPublisherState.swift new file mode 100644 index 000000000..745217ee4 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Service/PublisherService/StatusPublisherState.swift @@ -0,0 +1,14 @@ +// +// StatusPublisherState.swift +// +// +// Created by MainasuK on 2021-11-26. +// + +import Foundation + +public enum StatusPublisherState { + case pending + case failure(Error) + case success +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift index da77c65a1..1bb4014df 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift @@ -44,6 +44,20 @@ extension Mastodon.API.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) @@ -62,6 +76,12 @@ extension Mastodon.API.Media { public let description: String? public let focus: String? + public let progress: Progress = { + let progress = Progress() + progress.totalUnitCount = 100 + return progress + }() + public init( file: Mastodon.Query.MediaAttachment?, thumbnail: Mastodon.Query.MediaAttachment?, diff --git a/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift b/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift index ca9388cac..f1fdac8bb 100644 --- a/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift +++ b/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift @@ -53,6 +53,18 @@ extension Mastodon.Query.MediaAttachment { var base64EncondedString: String? { return data.map { "data:" + mimeType + ";base64," + $0.base64EncodedString() } } + + var sizeInByte: Int? { + switch self { + case .jpeg(let data), .gif(let data), .png(let data): + return data?.count + case .other(let url, _, _): + guard let url = url else { return nil } + guard let attribute = try? FileManager.default.attributesOfItem(atPath: url.path) else { return nil } + guard let size = attribute[.size] as? UInt64 else { return nil } + return Int(size) + } + } } extension Mastodon.Query.MediaAttachment: MultipartFormValue { diff --git a/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift b/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift index cde09b71b..5d806b6ba 100644 --- a/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift +++ b/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift @@ -15,6 +15,10 @@ import Combine // - https://gist.github.com/khanlou/b5e07f963bedcb6e0fcc5387b46991c3 final class SerialStream: NSObject { + + let logger = Logger(subsystem: "SerialStream", category: "Stream") + + public let progress = Progress() var writingTimerSubscriber: AnyCancellable? // serial stream source @@ -70,10 +74,14 @@ final class SerialStream: NSObject { var baseAddress = 0 var remainsBytes = readBytesCount while remainsBytes > 0 { - let result = self.boundStreams.output.write(&self.buffer[baseAddress], maxLength: remainsBytes) - baseAddress += result - remainsBytes -= result - os_log(.debug, "%{public}s[%{public}ld], %{public}s: write %ld/%ld bytes. write result: %ld", ((#file as NSString).lastPathComponent), #line, #function, baseAddress, readBytesCount, result) + let writeResult = self.boundStreams.output.write(&self.buffer[baseAddress], maxLength: remainsBytes) + baseAddress += writeResult + remainsBytes -= writeResult + + os_log(.debug, "%{public}s[%{public}ld], %{public}s: write %ld/%ld bytes. write result: %ld", ((#file as NSString).lastPathComponent), #line, #function, baseAddress, readBytesCount, writeResult) + + 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)") } } diff --git a/MastodonSDK/Sources/MastodonUI/Extension/UIAlertController.swift b/MastodonSDK/Sources/MastodonUI/Extension/UIAlertController.swift new file mode 100644 index 000000000..5467a026b --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Extension/UIAlertController.swift @@ -0,0 +1,37 @@ +// +// UIAlertController.swift +// TwidereX +// +// Created by Cirno MainasuK on 2020-7-1. +// Copyright © 2020 Dimension. All rights reserved. +// + +import UIKit + +extension UIAlertController { + + public static func standardAlert(of error: Error) -> UIAlertController { + let title: String? = { + if let error = error as? LocalizedError { + return error.errorDescription + } else { + return "Error" + } + }() + + let message: String? = { + if let error = error as? LocalizedError { + return [error.failureReason, error.recoverySuggestion].compactMap { $0 }.joined(separator: "\n") + } else { + return error.localizedDescription + } + }() + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) + alertController.addAction(okAction) + + return alertController + } + +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift new file mode 100644 index 000000000..f4d1397a9 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift @@ -0,0 +1,246 @@ +// +// AttachmentView.swift +// +// +// Created by MainasuK on 2022-5-20. +// + +import os.log +import UIKit +import SwiftUI +import Introspect +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") +// 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 + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift new file mode 100644 index 000000000..0a4aadec3 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift @@ -0,0 +1,316 @@ +// +// AttachmentViewModel+Upload.swift +// +// +// Created by MainasuK on 2021-11-26. +// + +import os.log +import UIKit +import Kingfisher +import UniformTypeIdentifiers +import MastodonCore +import MastodonSDK + +// objc.io +// ref: https://talk.objc.io/episodes/S01E269-swift-concurrency-async-sequences-part-1 +struct Chunked: AsyncSequence where Base.Element == UInt8 { + var base: Base + var chunkSize: Int = 1 * 1024 * 1024 // 1 MiB + typealias Element = Data + + struct AsyncIterator: AsyncIteratorProtocol { + var base: Base.AsyncIterator + var chunkSize: Int + + mutating func next() async throws -> Data? { + var result = Data() + while let element = try await base.next() { + result.append(element) + if result.count == chunkSize { return result } + } + return result.isEmpty ? nil : result + } + } + + func makeAsyncIterator() -> AsyncIterator { + AsyncIterator(base: base.makeAsyncIterator(), chunkSize: chunkSize) + } +} + +extension AsyncSequence where Element == UInt8 { + var chunked: Chunked { + Chunked(base: self) + } +} + +extension Data { + fileprivate func chunks(size: Int) -> [Data] { + return stride(from: 0, to: count, by: size).map { + Data(self[$0.. +// 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) + } +} + +extension AttachmentViewModel { + func upload(context: UploadContext) async throws -> UploadResult { + return try await uploadMastodonMedia( + context: context + ) + } + + private func uploadMastodonMedia( + context: UploadContext + ) async throws -> UploadResult { + guard let output = self.output else { + throw AppError.badRequest + } + + let attachment = output.asAttachment + + let query = Mastodon.API.Media.UploadMediaQuery( + file: attachment, + thumbnail: nil, + description: { + let caption = caption.trimmingCharacters(in: .whitespacesAndNewlines) + return caption.isEmpty ? nil : caption + }(), + focus: nil // TODO: + ) + + // upload + N * check upload + // upload : check = 9 : 1 + let uploadTaskCount: Int64 = 540 + let checkUploadTaskCount: Int64 = 1 + let checkUploadTaskRetryLimit: Int64 = 60 + + progress.totalUnitCount = uploadTaskCount + checkUploadTaskCount * checkUploadTaskRetryLimit + progress.completedUnitCount = 0 + + let attachmentUploadResponse: Mastodon.Response.Content = try await { + do { + AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [V2] upload attachment...") + + progress.addChild(query.progress, withPendingUnitCount: uploadTaskCount) + return try await context.apiService.uploadMedia( + domain: context.authContext.mastodonAuthenticationBox.domain, + query: query, + mastodonAuthenticationBox: context.authContext.mastodonAuthenticationBox, + needsFallback: false + ).singleOutput() + } catch { + // check needs fallback + guard let apiError = error as? Mastodon.API.Error, + apiError.httpResponseStatus == .notFound + else { throw error } + + AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [V1] upload attachment...") + + progress.addChild(query.progress, withPendingUnitCount: uploadTaskCount) + return try await context.apiService.uploadMedia( + domain: context.authContext.mastodonAuthenticationBox.domain, + query: query, + mastodonAuthenticationBox: context.authContext.mastodonAuthenticationBox, + needsFallback: true + ).singleOutput() + } + }() + + // check needs wait processing (until get the `url`) + if attachmentUploadResponse.statusCode == 202 { + // note: + // the Mastodon server append the attachments in order by upload time + // can not upload concurrency + let waitProcessRetryLimit = checkUploadTaskRetryLimit + var waitProcessRetryCount: Int64 = 0 + + repeat { + defer { + // make sure always count + 1 + waitProcessRetryCount += checkUploadTaskCount + } + + AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): check attachment process status") + + let attachmentStatusResponse = try await context.apiService.getMedia( + attachmentID: attachmentUploadResponse.value.id, + mastodonAuthenticationBox: context.authContext.mastodonAuthenticationBox + ).singleOutput() + progress.completedUnitCount += checkUploadTaskCount + + if let url = attachmentStatusResponse.value.url { + AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): attachment process finish: \(url)") + + // escape here + progress.completedUnitCount = progress.totalUnitCount + return .mastodon(attachmentStatusResponse) + + } else { + AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): attachment processing. Retry \(waitProcessRetryCount)/\(waitProcessRetryLimit)") + await Task.sleep(1_000_000_000 * 3) // 3s + } + } while waitProcessRetryCount < waitProcessRetryLimit + + AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): attachment processing result discard due to exceed retry limit") + throw AppError.badRequest + } 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) + } + } +} + +extension AttachmentViewModel.Output { + var asAttachment: Mastodon.Query.MediaAttachment { + switch self { + case .image(let data, let kind): + switch kind { + case .png: return .png(data) + case .jpg: return .jpeg(data) + } + case .video(let url, _): + return .other(url, fileExtension: url.pathExtension, mimeType: "video/mp4") + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift new file mode 100644 index 000000000..7d0e8c859 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift @@ -0,0 +1,401 @@ +// +// AttachmentViewModel.swift +// +// +// Created by MainasuK on 2021/11/19. +// + +import os.log +import UIKit +import Combine +import PhotosUI +import Kingfisher +import MastodonCore + +final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable { + + static let logger = Logger(subsystem: "AttachmentViewModel", category: "ViewModel") + + public let id = UUID() + + var disposeBag = Set() + var observations = Set() + + // input + 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 var error: Error? + let progress = Progress() // upload progress + + public init(input: Input) { + self.input = input + super.init() + // end init + + defer { + load(input: input) + } + + $output + .map { output -> UIImage? in + switch output { + case .image(let data, _): + return UIImage(data: data) + case .video(let url, _): + return AttachmentViewModel.createThumbnailForVideo(url: url) + case .none: + return nil + } + } + .assign(to: &$thumbnail) + } + + deinit { + switch output { + case .image: + // FIXME: + break + case .video(let url, _): + try? FileManager.default.removeItem(at: url) + case nil : + break + } + } +} + +extension AttachmentViewModel { + public enum Input: Hashable { + case image(UIImage) + case url(URL) + case pickerResult(PHPickerResult) + case itemProvider(NSItemProvider) + } + + public enum Output { + case image(Data, imageKind: ImageKind) + // case gif(Data) + case video(URL, mimeType: String) // assert use file for video only + + public enum ImageKind { + case png + case jpg + } + + public var twitterMediaCategory: TwitterMediaCategory { + switch self { + case .image: return .image + case .video: return .amplifyVideo + } + } + } + + 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 + ) { + self.image = image + self.gif = gif + self.video = video + } + } + + public enum AttachmentError: Error { + 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) { + 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): + Task { @MainActor in + do { + let output = try await AttachmentViewModel.load(url: url) + self.output = output + } catch { + self.error = error + } + } // end Task + 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 + 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 + } + } + + 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 + } + } +} + +// 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/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 53db9ab30..73272b419 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -9,10 +9,11 @@ import os.log import UIKit import Combine import CoreDataStack -import MastodonCore import Meta -import MastodonMeta import MetaTextKit +import MastodonMeta +import MastodonCore +import MastodonSDK public final class ComposeContentViewModel: NSObject, ObservableObject { @@ -29,6 +30,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { let kind: Kind @Published var viewLayoutFrame = ViewLayoutFrame() + + // author (me) @Published var authContext: AuthContext // output @@ -67,6 +70,11 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { @Published var name: MetaContent = PlaintextMetaContent(string: "") @Published var username: String = "" + // attachment + @Published public var attachmentViewModels: [AttachmentViewModel] = [] + @Published public var maxMediaAttachmentLimit = 4 + // @Published public internal(set) var isMediaValid = true + // poll @Published var isPollActive = false @Published public var pollOptions: [PollComposeItem.Option] = { @@ -77,11 +85,16 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { return options }() @Published public var pollExpireConfigurationOption: PollComposeItem.ExpireConfiguration.Option = .oneDay + @Published public var pollMultipleConfigurationOption: PollComposeItem.MultipleConfiguration.Option = false + @Published public var maxPollOptionLimit = 4 // emoji @Published var isEmojiActive = false + // visibility + @Published var visibility: Mastodon.Entity.Status.Visibility + // UI & UX @Published var replyToCellFrame: CGRect = .zero @Published var contentCellFrame: CGRect = .zero @@ -96,6 +109,41 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { self.context = context self.authContext = authContext self.kind = kind + self.visibility = { + // default private when user locked + var visibility: Mastodon.Entity.Status.Visibility = { + 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 kind { + 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 + }() super.init() // end init @@ -162,6 +210,71 @@ extension ComposeContentViewModel { } } +extension ComposeContentViewModel { + public enum ComposeError: LocalizedError { + case pollHasEmptyOption + + public var errorDescription: String? { + switch self { + case .pollHasEmptyOption: + return "The post poll is invalid" // TODO: i18n + } + } + + public var failureReason: String? { + switch self { + case .pollHasEmptyOption: + return "The poll has empty option" // TODO: i18n + } + } + } + + public func statusPublisher() throws -> StatusPublisher { + let authContext = self.authContext + + // author + let managedObjectContext = self.context.managedObjectContext + var _author: ManagedObjectRecord? + managedObjectContext.performAndWait { + _author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user.asRecrod + } + guard let author = _author else { + throw AppError.badAuthentication + } + + // poll + _ = try { + guard isPollActive else { return } + let isAllNonEmpty = pollOptions + .map { $0.text } + .allSatisfy { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } + guard isAllNonEmpty else { + throw ComposeError.pollHasEmptyOption + } + }() + + return MastodonStatusPublisher( + author: author, + replyTo: { + switch self.kind { + case .reply(let status): return status + default: return nil + } + }(), + isContentWarningComposing: isContentWarningActive, + contentWarning: contentWarning, + content: content, + isMediaSensitive: isContentWarningActive, + attachmentViewModels: attachmentViewModels, + isPollComposing: isPollActive, + pollOptions: pollOptions, + pollExpireConfigurationOption: pollExpireConfigurationOption, + pollMultipleConfigurationOption: pollMultipleConfigurationOption, + visibility: visibility + ) + } // end func publisher() +} + // MARK: - UITextViewDelegate extension ComposeContentViewModel: UITextViewDelegate { public func textViewDidBeginEditing(_ textView: UITextView) { diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift new file mode 100644 index 000000000..ea3be18a8 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift @@ -0,0 +1,180 @@ +// +// MastodonStatusPublisher.swift +// +// +// Created by MainasuK on 2021-12-1. +// + +import os.log +import Foundation +import CoreData +import CoreDataStack +import MastodonCore +import MastodonSDK + +public final class MastodonStatusPublisher: NSObject, ProgressReporting { + + let logger = Logger(subsystem: "MastodonStatusPublisher", category: "Publisher") + + // Input + + // author + public let author: ManagedObjectRecord + // refer + public let replyTo: ManagedObjectRecord? + // content warning + public let isContentWarningComposing: Bool + public let contentWarning: String + // status content + public let content: String + // media + public let isMediaSensitive: Bool + public let attachmentViewModels: [AttachmentViewModel] + // poll + public let isPollComposing: Bool + public let pollOptions: [PollComposeItem.Option] + public let pollExpireConfigurationOption: PollComposeItem.ExpireConfiguration.Option + public let pollMultipleConfigurationOption: PollComposeItem.MultipleConfiguration.Option + // visibility + public let visibility: Mastodon.Entity.Status.Visibility + + // Output + let _progress = Progress() + public var progress: Progress { _progress } + @Published var _state: StatusPublisherState = .pending + public var state: Published.Publisher { $_state } + + public var reactor: StatusPublisherReactor? + + public init( + author: ManagedObjectRecord, + replyTo: ManagedObjectRecord?, + isContentWarningComposing: Bool, + contentWarning: String, + content: String, + isMediaSensitive: Bool, + attachmentViewModels: [AttachmentViewModel], + isPollComposing: Bool, + pollOptions: [PollComposeItem.Option], + pollExpireConfigurationOption: PollComposeItem.ExpireConfiguration.Option, + pollMultipleConfigurationOption: PollComposeItem.MultipleConfiguration.Option, + visibility: Mastodon.Entity.Status.Visibility + ) { + self.author = author + self.replyTo = replyTo + self.isContentWarningComposing = isContentWarningComposing + self.contentWarning = contentWarning + self.content = content + self.isMediaSensitive = isMediaSensitive + self.attachmentViewModels = attachmentViewModels + self.isPollComposing = isPollComposing + self.pollOptions = pollOptions + self.pollExpireConfigurationOption = pollExpireConfigurationOption + self.pollMultipleConfigurationOption = pollMultipleConfigurationOption + self.visibility = visibility + } + +} + +// MARK: - StatusPublisher +extension MastodonStatusPublisher: StatusPublisher { + + public func publish( + api: APIService, + authContext: AuthContext + ) async throws -> StatusPublishResult { + let idempotencyKey = UUID().uuidString + + let publishStatusTaskStartDelayWeight: Int64 = 20 + let publishStatusTaskStartDelayCount: Int64 = publishStatusTaskStartDelayWeight + + let publishAttachmentTaskWeight: Int64 = 100 + let publishAttachmentTaskCount: Int64 = Int64(attachmentViewModels.count) * publishAttachmentTaskWeight + + let publishStatusTaskWeight: Int64 = 20 + let publishStatusTaskCount: Int64 = publishStatusTaskWeight + + let taskCount = [ + publishStatusTaskStartDelayCount, + publishAttachmentTaskCount, + publishStatusTaskCount + ].reduce(0, +) + progress.totalUnitCount = taskCount + progress.completedUnitCount = 0 + + // start delay + try? await Task.sleep(nanoseconds: 1 * .second) + progress.completedUnitCount += publishStatusTaskStartDelayWeight + + // Task: attachment + + let uploadContext = AttachmentViewModel.UploadContext( + apiService: api, + authContext: authContext + ) + + var attachmentIDs: [Mastodon.Entity.Attachment.ID] = [] + for attachmentViewModel in attachmentViewModels { + // set progress + 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 + } + let attachmentID = response.value.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) + throw error + } + } + + let pollOptions: [String]? = { + guard self.isPollComposing else { return nil } + let options = self.pollOptions.compactMap { $0.text.trimmingCharacters(in: .whitespacesAndNewlines) } + return options.isEmpty ? nil : options + }() + let pollExpiresIn: Int? = { + guard self.isPollComposing else { return nil } + guard pollOptions != nil else { return nil } + return self.pollExpireConfigurationOption.seconds + }() + let pollMultiple: Bool? = { + guard self.isPollComposing else { return nil } + guard pollOptions != nil else { return nil } + return self.pollMultipleConfigurationOption + }() + let inReplyToID: Mastodon.Entity.Status.ID? = try await api.backgroundManagedObjectContext.perform { + guard let replyTo = self.replyTo?.object(in: api.backgroundManagedObjectContext) else { return nil } + return replyTo.id + } + + let query = Mastodon.API.Statuses.PublishStatusQuery( + status: content, + mediaIDs: attachmentIDs.isEmpty ? nil : attachmentIDs, + pollOptions: pollOptions, + pollExpiresIn: pollExpiresIn, + inReplyToID: inReplyToID, + sensitive: isMediaSensitive, + spoilerText: isContentWarningComposing ? contentWarning : nil, + visibility: visibility + ) + + let publishResponse = try await api.publishStatus( + domain: authContext.mastodonAuthenticationBox.domain, + idempotencyKey: idempotencyKey, + query: query, + authenticationBox: authContext.mastodonAuthenticationBox + ) + progress.completedUnitCount += publishStatusTaskCount + _state = .success + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): status published: \(publishResponse.value.id)") + + return .mastodon(publishResponse) + } + +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Container/AttachmentView.swift b/MastodonSDK/Sources/MastodonUI/View/Container/AttachmentView.swift deleted file mode 100644 index 150ff5f51..000000000 --- a/MastodonSDK/Sources/MastodonUI/View/Container/AttachmentView.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// AttachmentView.swift -// -// -// Created by MainasuK on 22/9/30. -// - -import SwiftUI - -public struct AttachmentView: View { - - @ObservedObject public var viewModel: ViewModel - - public init(viewModel: ViewModel) { - self.viewModel = viewModel - } - - public var body: some View { - Text("Hi") - } - -} - -extension AttachmentView { - public class ViewModel: ObservableObject { - - public init() { } - } -} From c36939468e458fcdaefcf55921cfd273dc48c6c5 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 31 Oct 2022 09:12:23 -0400 Subject: [PATCH 050/658] Fix accessibility for Compose button --- .../Root/MainTab/MainTabBarController.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 8970e2f29..24b6127ab 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -33,6 +33,7 @@ class MainTabBarController: UITabBarController { button.layer.masksToBounds = true button.layer.cornerCurve = .continuous button.layer.cornerRadius = 8 + button.isAccessibilityElement = false return button }() @@ -185,14 +186,6 @@ extension MainTabBarController { viewController.tabBarItem.largeContentSizeImage = tab.largeImage.imageWithoutBaseline() viewController.tabBarItem.accessibilityLabel = tab.title viewController.tabBarItem.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0) - - switch tab { - case .compose: - viewController.tabBarItem.isEnabled = false - default: - break - } - return viewController } _viewControllers = viewControllers @@ -363,7 +356,7 @@ extension MainTabBarController { extension MainTabBarController { - @objc private func composeButtonDidPressed(_ sender: UIButton) { + @objc private func composeButtonDidPressed(_ sender: Any) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } let composeViewModel = ComposeViewModel( @@ -504,6 +497,13 @@ extension MainTabBarController { // MARK: - UITabBarControllerDelegate extension MainTabBarController: UITabBarControllerDelegate { + func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { + if let tab = Tab(rawValue: viewController.tabBarItem.tag), tab == .compose { + composeButtonDidPressed(tabBarController) + return false + } + return true + } func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, viewController.debugDescription) defer { From da3a83d38724a32c1bdbbee50c36a1eb37675811 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 31 Oct 2022 09:14:45 -0400 Subject: [PATCH 051/658] Adjust whitespace --- Mastodon/Scene/Root/MainTab/MainTabBarController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 24b6127ab..192a5a5dc 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -504,6 +504,7 @@ extension MainTabBarController: UITabBarControllerDelegate { } return true } + func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, viewController.debugDescription) defer { From f6c1f6c443ae76c42bf5a6dae32edeccd9d83d95 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 31 Oct 2022 10:01:12 -0400 Subject: [PATCH 052/658] Add support for entering text to search via drag-and-drop --- Mastodon/Scene/Root/RootSplitViewController.swift | 2 +- .../Scene/Search/Search/SearchViewController.swift | 13 +++++++++---- .../SearchDetail/SearchDetailViewController.swift | 1 + Mastodon/Supporting Files/SceneDelegate.swift | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Mastodon/Scene/Root/RootSplitViewController.swift b/Mastodon/Scene/Root/RootSplitViewController.swift index f19282936..83eb6375a 100644 --- a/Mastodon/Scene/Root/RootSplitViewController.swift +++ b/Mastodon/Scene/Root/RootSplitViewController.swift @@ -157,7 +157,7 @@ extension RootSplitViewController: ContentSplitViewControllerDelegate { } guard let navigationController = searchViewController.navigationController else { return } if navigationController.viewControllers.count == 1 { - searchViewController.searchBarTapPublisher.send() + searchViewController.searchBarTapPublisher.send("") } else { navigationController.popToRootViewController(animated: true) } diff --git a/Mastodon/Scene/Search/Search/SearchViewController.swift b/Mastodon/Scene/Search/Search/SearchViewController.swift index 982844f51..28ec11996 100644 --- a/Mastodon/Scene/Search/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/Search/SearchViewController.swift @@ -46,7 +46,8 @@ final class SearchViewController: UIViewController, NeedsDependency { // return collectionView // }() - let searchBarTapPublisher = PassthroughSubject() + // value is the initial search text to set + let searchBarTapPublisher = PassthroughSubject() private(set) lazy var discoveryViewController: DiscoveryViewController = { let viewController = DiscoveryViewController() @@ -139,10 +140,10 @@ extension SearchViewController { searchBarTapPublisher .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: false) - .sink { [weak self] in + .sink { [weak self] initialText in guard let self = self else { return } // push to search detail - let searchDetailViewModel = SearchDetailViewModel() + let searchDetailViewModel = SearchDetailViewModel(initialSearchText: initialText) searchDetailViewModel.needsBecomeFirstResponder = true self.navigationController?.delegate = self.searchTransitionController // FIXME: @@ -159,9 +160,13 @@ extension SearchViewController { extension SearchViewController: UISearchBarDelegate { func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool { os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - searchBarTapPublisher.send() + searchBarTapPublisher.send("") return false } + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + searchBar.text = "" + searchBarTapPublisher.send(searchText) + } } // MARK: - UISearchControllerDelegate diff --git a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift index 701dc4fa6..3ebc7f179 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift @@ -301,6 +301,7 @@ extension SearchDetailViewController { searchController.searchBar.sizeToFit() } + searchBar.text = viewModel.searchText.value searchBar.delegate = self } diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 54b1fd57e..a24f43266 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -163,7 +163,7 @@ extension SceneDelegate { logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): select search tab") if let searchViewController = coordinator?.tabBarController.topMost as? SearchViewController { - searchViewController.searchBarTapPublisher.send() + searchViewController.searchBarTapPublisher.send("") logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): trigger search") } default: From 87e05ecdabf9e2b9a8ccd015d8aa61076e3c91b6 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 31 Oct 2022 11:32:31 -0400 Subject: [PATCH 053/658] Add support for UIAccessibilityCustomAction in MastodonMenu --- .../MastodonUI/View/Menu/MastodonMenu.swift | 107 ++++++++++++------ 1 file changed, 71 insertions(+), 36 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift b/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift index de4bc403d..d06876637 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift @@ -20,10 +20,22 @@ public enum MastodonMenu { var children: [UIMenuElement] = [] for action in actions { let element = action.build(delegate: delegate) - children.append(element) + children.append(element.menuElement) } return UIMenu(title: "", options: [], children: children) } + + public static func setupAccessibilityActions( + actions: [Action], + delegate: MastodonMenuDelegate + ) -> [UIAccessibilityCustomAction] { + var accessibilityActions: [UIAccessibilityCustomAction] = [] + for action in actions { + let element = action.build(delegate: delegate) + accessibilityActions.append(element.accessibilityCustomAction) + } + return accessibilityActions + } } extension MastodonMenu { @@ -34,69 +46,50 @@ extension MastodonMenu { case shareUser(ShareUserActionContext) case deleteStatus - func build(delegate: MastodonMenuDelegate) -> UIMenuElement { + func build(delegate: MastodonMenuDelegate) -> BuiltAction { switch self { case .muteUser(let context): - let muteAction = UIAction( + let muteAction = BuiltAction( title: context.isMuting ? L10n.Common.Controls.Friendship.unmuteUser(context.name) : L10n.Common.Controls.Friendship.muteUser(context.name), - image: context.isMuting ? UIImage(systemName: "speaker.wave.2") : UIImage(systemName: "speaker.slash"), - identifier: nil, - discoverabilityTitle: nil, - attributes: [], - state: .off - ) { [weak delegate] _ in + image: context.isMuting ? UIImage(systemName: "speaker.wave.2") : UIImage(systemName: "speaker.slash") + ) { [weak delegate] in guard let delegate = delegate else { return } delegate.menuAction(self) } return muteAction case .blockUser(let context): - let blockAction = UIAction( + let blockAction = BuiltAction( title: context.isBlocking ? L10n.Common.Controls.Friendship.unblockUser(context.name) : L10n.Common.Controls.Friendship.blockUser(context.name), - image: context.isBlocking ? UIImage(systemName: "hand.raised") : UIImage(systemName: "hand.raised"), - identifier: nil, - discoverabilityTitle: nil, - attributes: [], - state: .off - ) { [weak delegate] _ in + image: context.isBlocking ? UIImage(systemName: "hand.raised") : UIImage(systemName: "hand.raised") + ) { [weak delegate] in guard let delegate = delegate else { return } delegate.menuAction(self) } return blockAction case .reportUser(let context): - let reportAction = UIAction( + let reportAction = BuiltAction( title: L10n.Common.Controls.Actions.reportUser(context.name), - image: UIImage(systemName: "flag"), - identifier: nil, - discoverabilityTitle: nil, - attributes: [], - state: .off - ) { [weak delegate] _ in + image: UIImage(systemName: "flag") + ) { [weak delegate] in guard let delegate = delegate else { return } delegate.menuAction(self) } return reportAction case .shareUser(let context): - let shareAction = UIAction( + let shareAction = BuiltAction( title: L10n.Common.Controls.Actions.shareUser(context.name), - image: UIImage(systemName: "square.and.arrow.up"), - identifier: nil, - discoverabilityTitle: nil, - attributes: [], - state: .off - ) { [weak delegate] _ in + image: UIImage(systemName: "square.and.arrow.up") + ) { [weak delegate] in guard let delegate = delegate else { return } delegate.menuAction(self) } return shareAction case .deleteStatus: - let deleteAction = UIAction( + let deleteAction = BuiltAction( title: L10n.Common.Controls.Actions.delete, image: UIImage(systemName: "minus.circle"), - identifier: nil, - discoverabilityTitle: nil, - attributes: .destructive, - state: .off - ) { [weak delegate] _ in + attributes: .destructive + ) { [weak delegate] in guard let delegate = delegate else { return } delegate.menuAction(self) } @@ -104,6 +97,48 @@ extension MastodonMenu { } // end switch } // end func build } // end enum Action + + struct BuiltAction { + init( + title: String, + image: UIImage? = nil, + attributes: UIMenuElement.Attributes = [], + state: UIMenuElement.State = .off, + handler: @escaping () -> Void + ) { + self.title = title + self.image = image + self.attributes = attributes + self.state = state + self.handler = handler + } + + let title: String + let image: UIImage? + let attributes: UIMenuElement.Attributes + let state: UIMenuElement.State + let handler: () -> Void + + var menuElement: UIMenuElement { + UIAction( + title: title, + image: image, + identifier: nil, + discoverabilityTitle: nil, + attributes: attributes, + state: .off + ) { _ in + handler() + } + } + + var accessibilityCustomAction: UIAccessibilityCustomAction { + UIAccessibilityCustomAction(name: title, image: image) { _ in + handler() + return true + } + } + } } extension MastodonMenu { From 3876855bc9eb864c27bb6ee6e239a9ae1c34be86 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 31 Oct 2022 12:01:14 -0400 Subject: [PATCH 054/658] Move the post author information to a custom subview that handles accessibility --- .../StatusThreadRootTableViewCell.swift | 11 +- .../View/Content/NotificationView.swift | 2 +- .../View/Content/StatusAuthorView.swift | 299 ++++++++++++++++++ .../View/Content/StatusView+ViewModel.swift | 42 ++- .../MastodonUI/View/Content/StatusView.swift | 205 +----------- 5 files changed, 340 insertions(+), 219 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonUI/View/Content/StatusAuthorView.swift diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift index 115175800..0049539b0 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift @@ -97,12 +97,7 @@ extension StatusThreadRootTableViewCell { get { var elements = [ statusView.headerContainerView, - statusView.avatarButton, - statusView.authorNameLabel, - statusView.menuButton, - statusView.authorUsernameLabel, - statusView.dateLabel, - statusView.contentSensitiveeToggleButton, + statusView.authorView, statusView.spoilerOverlayView, statusView.contentMetaText.textView, statusView.mediaGridContainerView, @@ -112,10 +107,6 @@ extension StatusThreadRootTableViewCell { statusView.statusMetricView ] - if !statusView.viewModel.isMediaSensitive { - elements.removeAll(where: { $0 === statusView.contentSensitiveeToggleButton }) - } - if statusView.viewModel.isContentReveal { elements.removeAll(where: { $0 === statusView.spoilerOverlayView }) } else { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift index daf14b96e..096eb4cf4 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift @@ -431,7 +431,7 @@ extension NotificationView: AdaptiveContainerView { } extension NotificationView { - public typealias AuthorMenuContext = StatusView.AuthorMenuContext + public typealias AuthorMenuContext = StatusAuthorView.AuthorMenuContext public func setupAuthorMenu(menuContext: AuthorMenuContext) -> UIMenu { var actions: [MastodonMenu.Action] = [] diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusAuthorView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusAuthorView.swift new file mode 100644 index 000000000..a4c7c28d7 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusAuthorView.swift @@ -0,0 +1,299 @@ +// +// StatusAuthorView.swift +// +// +// Created by Jed Fox on 2022-10-31. +// + +import os.log +import UIKit +import Combine +import Meta +import MetaTextKit +import MastodonAsset +import MastodonLocalization + +public class StatusAuthorView: UIStackView { + let logger = Logger(subsystem: "StatusAuthorView", category: "View") + private var _disposeBag = Set() // which lifetime same to view scope + + weak var statusView: StatusView? + + // accessibility actions + var authorActions = [UIAccessibilityCustomAction]() + + // avatar + public let avatarButton = AvatarButton() + + // author name + public let authorNameLabel = MetaLabel(style: .statusName) + + // author username + public let authorUsernameLabel = MetaLabel(style: .statusUsername) + + public let usernameTrialingDotLabel: MetaLabel = { + let label = MetaLabel(style: .statusUsername) + label.configure(content: PlaintextMetaContent(string: "·")) + return label + }() + + // timestamp + public let dateLabel = MetaLabel(style: .statusUsername) + + public let menuButton: UIButton = { + let button = HitTestExpandedButton(type: .system) + button.expandEdgeInsets = UIEdgeInsets(top: -20, left: -10, bottom: -5, right: -10) + button.tintColor = Asset.Colors.Label.secondary.color + let image = UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(font: .systemFont(ofSize: 15))) + button.setImage(image, for: .normal) + button.accessibilityLabel = L10n.Common.Controls.Status.Actions.menu + return button + }() + + public let contentSensitiveeToggleButton: UIButton = { + let button = HitTestExpandedButton(type: .system) + button.expandEdgeInsets = UIEdgeInsets(top: -5, left: -10, bottom: -20, right: -10) + button.tintColor = Asset.Colors.Label.secondary.color + button.imageView?.contentMode = .scaleAspectFill + button.imageView?.clipsToBounds = false + let image = UIImage(systemName: "eye.slash.fill", withConfiguration: UIImage.SymbolConfiguration(font: .systemFont(ofSize: 15))) + button.setImage(image, for: .normal) + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init(coder: NSCoder) { + super.init(coder: coder) + _init() + } + + func layout(style: StatusView.Style) { + switch style { + case .inline: layoutBase() + case .plain: layoutBase() + case .report: layoutReport() + case .notification: layoutBase() + case .notificationQuote: layoutNotificationQuote() + case .composeStatusReplica: layoutComposeStatusReplica() + case .composeStatusAuthor: layoutComposeStatusAuthor() + } + } + + public override var accessibilityElements: [Any]? { + get { [] } + set {} + } + + public override var accessibilityCustomActions: [UIAccessibilityCustomAction]? { + get { + var actions = authorActions + if !contentSensitiveeToggleButton.isHidden { + actions.append(UIAccessibilityCustomAction( + name: contentSensitiveeToggleButton.accessibilityLabel!, + image: contentSensitiveeToggleButton.image(for: .normal), + actionHandler: { _ in + self.contentSensitiveeToggleButtonDidPressed(self.contentSensitiveeToggleButton) + return true + } + )) + } + return actions + } + set {} + } + + public override func accessibilityActivate() -> Bool { + guard let statusView = statusView else { return false } + statusView.delegate?.statusView(statusView, authorAvatarButtonDidPressed: avatarButton) + return true + } +} + +extension StatusAuthorView { + func _init() { + axis = .horizontal + spacing = 12 + isAccessibilityElement = true + + UIContentSizeCategory.publisher + .sink { [unowned self] category in + axis = category > .accessibilityLarge ? .vertical : .horizontal + alignment = category > .accessibilityLarge ? .leading : .center + } + .store(in: &_disposeBag) + + // avatar button + avatarButton.addTarget(self, action: #selector(StatusAuthorView.authorAvatarButtonDidPressed(_:)), for: .touchUpInside) + authorNameLabel.isUserInteractionEnabled = false + authorUsernameLabel.isUserInteractionEnabled = false + + // contentSensitiveeToggleButton + contentSensitiveeToggleButton.addTarget(self, action: #selector(StatusAuthorView.contentSensitiveeToggleButtonDidPressed(_:)), for: .touchUpInside) + + // dateLabel + dateLabel.isUserInteractionEnabled = false + } +} + +extension StatusAuthorView { + + public struct AuthorMenuContext { + public let name: String + + public let isMuting: Bool + public let isBlocking: Bool + public let isMyself: Bool + } + + public func setupAuthorMenu(menuContext: AuthorMenuContext) -> (UIMenu, [UIAccessibilityCustomAction]) { + var actions: [MastodonMenu.Action] = [] + + actions = [ + .muteUser(.init( + name: menuContext.name, + isMuting: menuContext.isMuting + )), + .blockUser(.init( + name: menuContext.name, + isBlocking: menuContext.isBlocking + )), + .reportUser( + .init(name: menuContext.name) + ), + ] + + if menuContext.isMyself { + actions.append(.deleteStatus) + } + + + let menu = MastodonMenu.setupMenu( + actions: actions, + delegate: self.statusView! + ) + + let accessibilityActions = MastodonMenu.setupAccessibilityActions( + actions: actions, + delegate: self.statusView! + ) + + return (menu, accessibilityActions) + } + +} + +extension StatusAuthorView { + @objc private func authorAvatarButtonDidPressed(_ sender: UIButton) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + guard let statusView = statusView else { return } + statusView.delegate?.statusView(statusView, authorAvatarButtonDidPressed: avatarButton) + } + + @objc private func contentSensitiveeToggleButtonDidPressed(_ sender: UIButton) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + guard let statusView = statusView else { return } + statusView.delegate?.statusView(statusView, contentSensitiveeToggleButtonDidPressed: sender) + } +} + +extension StatusAuthorView { + // author container: H - [ avatarButton | authorMetaContainer ] + private func layoutBase() { + // avatarButton + let authorAvatarButtonSize = CGSize(width: 46, height: 46) + avatarButton.size = authorAvatarButtonSize + avatarButton.avatarImageView.imageViewSize = authorAvatarButtonSize + avatarButton.translatesAutoresizingMaskIntoConstraints = false + addArrangedSubview(avatarButton) + NSLayoutConstraint.activate([ + avatarButton.widthAnchor.constraint(equalToConstant: authorAvatarButtonSize.width).priority(.required - 1), + avatarButton.heightAnchor.constraint(equalToConstant: authorAvatarButtonSize.height).priority(.required - 1), + ]) + avatarButton.setContentHuggingPriority(.required - 1, for: .vertical) + avatarButton.setContentCompressionResistancePriority(.required - 1, for: .vertical) + + // authorMetaContainer: V - [ authorPrimaryMetaContainer | authorSecondaryMetaContainer ] + let authorMetaContainer = UIStackView() + authorMetaContainer.axis = .vertical + authorMetaContainer.spacing = 4 + addArrangedSubview(authorMetaContainer) + + // authorPrimaryMetaContainer: H - [ authorNameLabel | (padding) | menuButton ] + let authorPrimaryMetaContainer = UIStackView() + authorPrimaryMetaContainer.axis = .horizontal + authorPrimaryMetaContainer.spacing = 10 + authorMetaContainer.addArrangedSubview(authorPrimaryMetaContainer) + + // authorNameLabel + authorPrimaryMetaContainer.addArrangedSubview(authorNameLabel) + authorNameLabel.setContentHuggingPriority(.required - 10, for: .horizontal) + authorNameLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal) + authorPrimaryMetaContainer.addArrangedSubview(UIView()) + // menuButton + authorPrimaryMetaContainer.addArrangedSubview(menuButton) + menuButton.setContentHuggingPriority(.required - 2, for: .horizontal) + menuButton.setContentCompressionResistancePriority(.required - 2, for: .horizontal) + + // authorSecondaryMetaContainer: H - [ authorUsername | usernameTrialingDotLabel | dateLabel | (padding) | contentSensitiveeToggleButton ] + let authorSecondaryMetaContainer = UIStackView() + authorSecondaryMetaContainer.axis = .horizontal + authorSecondaryMetaContainer.spacing = 4 + authorMetaContainer.addArrangedSubview(authorSecondaryMetaContainer) + + authorSecondaryMetaContainer.addArrangedSubview(authorUsernameLabel) + authorUsernameLabel.setContentHuggingPriority(.required - 8, for: .horizontal) + authorUsernameLabel.setContentCompressionResistancePriority(.required - 8, for: .horizontal) + authorSecondaryMetaContainer.addArrangedSubview(usernameTrialingDotLabel) + usernameTrialingDotLabel.setContentHuggingPriority(.required - 2, for: .horizontal) + usernameTrialingDotLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal) + authorSecondaryMetaContainer.addArrangedSubview(dateLabel) + dateLabel.setContentHuggingPriority(.required - 1, for: .horizontal) + dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal) + authorSecondaryMetaContainer.addArrangedSubview(UIView()) + contentSensitiveeToggleButton.translatesAutoresizingMaskIntoConstraints = false + authorSecondaryMetaContainer.addArrangedSubview(contentSensitiveeToggleButton) + NSLayoutConstraint.activate([ + contentSensitiveeToggleButton.heightAnchor.constraint(equalTo: authorUsernameLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), + contentSensitiveeToggleButton.widthAnchor.constraint(equalTo: contentSensitiveeToggleButton.heightAnchor, multiplier: 1.0).priority(.required - 1), + ]) + authorUsernameLabel.setContentHuggingPriority(.required - 1, for: .vertical) + authorUsernameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) + contentSensitiveeToggleButton.setContentHuggingPriority(.defaultLow, for: .vertical) + contentSensitiveeToggleButton.setContentHuggingPriority(.defaultLow, for: .horizontal) + contentSensitiveeToggleButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + contentSensitiveeToggleButton.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + } + + func layoutReport() { + layoutBase() + + menuButton.removeFromSuperview() + } + + func layoutNotificationQuote() { + layoutBase() + + contentSensitiveeToggleButton.removeFromSuperview() + menuButton.removeFromSuperview() + } + + func layoutComposeStatusReplica() { + layoutBase() + + avatarButton.isUserInteractionEnabled = false + menuButton.removeFromSuperview() + } + + func layoutComposeStatusAuthor() { + layoutBase() + + avatarButton.isUserInteractionEnabled = false + menuButton.removeFromSuperview() + usernameTrialingDotLabel.removeFromSuperview() + dateLabel.removeFromSuperview() + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index f3d9f6f80..af2bba14a 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -208,6 +208,7 @@ extension StatusView.ViewModel { } private func bindAuthor(statusView: StatusView) { + let authorView = statusView.authorView // avatar Publishers.CombineLatest( $authorAvatarImage.removeDuplicates(), @@ -221,30 +222,31 @@ extension StatusView.ViewModel { return AvatarImageView.Configuration(url: url) } }() - statusView.avatarButton.avatarImageView.configure(configuration: configuration) - statusView.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 12))) + authorView.avatarButton.avatarImageView.configure(configuration: configuration) + authorView.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 12))) } .store(in: &disposeBag) // name $authorName .sink { metaContent in let metaContent = metaContent ?? PlaintextMetaContent(string: " ") - statusView.authorNameLabel.configure(content: metaContent) + authorView.authorNameLabel.configure(content: metaContent) } .store(in: &disposeBag) // username - $authorUsername + let usernamePublisher = $authorUsername .map { text -> String in guard let text = text else { return "" } return "@\(text)" } + usernamePublisher .sink { username in let metaContent = PlaintextMetaContent(string: username) - statusView.authorUsernameLabel.configure(content: metaContent) + authorView.authorUsernameLabel.configure(content: metaContent) } .store(in: &disposeBag) // timestamp - Publishers.CombineLatest( + let timestampPublisher = Publishers.CombineLatest( $timestamp, timestampUpdatePublisher.prepend(Date()).eraseToAnyPublisher() ) @@ -256,14 +258,23 @@ extension StatusView.ViewModel { return text } .removeDuplicates() - .assign(to: &$timestampText) + + timestampPublisher.assign(to: &$timestampText) $timestampText .sink { [weak self] text in guard let _ = self else { return } - statusView.dateLabel.configure(content: PlaintextMetaContent(string: text)) + authorView.dateLabel.configure(content: PlaintextMetaContent(string: text)) } .store(in: &disposeBag) + + // accessibility label + Publishers.CombineLatest3($authorName, usernamePublisher, timestampPublisher) + .map { name, username, timestamp in + "\(name?.string ?? "") \(username), \(timestamp)" + } + .assign(to: \.accessibilityLabel, on: authorView) + .store(in: &disposeBag) } private func bindContent(statusView: StatusView) { @@ -326,7 +337,7 @@ extension StatusView.ViewModel { // eye: when media is hidden // eye-slash: when media display let image = isSensitiveToggled ? UIImage(systemName: "eye.slash.fill") : UIImage(systemName: "eye.fill") - statusView.contentSensitiveeToggleButton.setImage(image, for: .normal) + statusView.authorView.contentSensitiveeToggleButton.setImage(image, for: .normal) } .store(in: &disposeBag) } @@ -566,6 +577,7 @@ extension StatusView.ViewModel { } private func bindMenu(statusView: StatusView) { + let authorView = statusView.authorView Publishers.CombineLatest4( $authorName, $isMuting, @@ -574,18 +586,20 @@ extension StatusView.ViewModel { ) .sink { authorName, isMuting, isBlocking, isMyself in guard let name = authorName?.string else { - statusView.menuButton.menu = nil + statusView.authorView.menuButton.menu = nil return } - let menuContext = StatusView.AuthorMenuContext( + let menuContext = StatusAuthorView.AuthorMenuContext( name: name, isMuting: isMuting, isBlocking: isBlocking, isMyself: isMyself ) - statusView.menuButton.menu = statusView.setupAuthorMenu(menuContext: menuContext) - statusView.menuButton.showsMenuAsPrimaryAction = true + let (menu, actions) = authorView.setupAuthorMenu(menuContext: menuContext) + authorView.menuButton.menu = menu + authorView.authorActions = actions + authorView.menuButton.showsMenuAsPrimaryAction = true } .store(in: &disposeBag) } @@ -653,7 +667,7 @@ extension StatusView.ViewModel { isContentReveal ? L10n.Scene.Compose.Accessibility.enableContentWarning : L10n.Scene.Compose.Accessibility.disableContentWarning } .sink { label in - statusView.contentSensitiveeToggleButton.accessibilityLabel = label + statusView.authorView.contentSensitiveeToggleButton.accessibilityLabel = label } .store(in: &disposeBag) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index 4c983df34..1c9ed673d 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -75,52 +75,8 @@ public final class StatusView: UIView { // author let authorAdaptiveMarginContainerView = AdaptiveMarginContainerView() - let authorContainerView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.spacing = 12 - return stackView - }() - - // avatar - public let avatarButton = AvatarButton() - - // author name - public let authorNameLabel = MetaLabel(style: .statusName) - - // author username - public let authorUsernameLabel = MetaLabel(style: .statusUsername) - - public let usernameTrialingDotLabel: MetaLabel = { - let label = MetaLabel(style: .statusUsername) - label.configure(content: PlaintextMetaContent(string: "·")) - return label - }() + public let authorView = StatusAuthorView() - // timestamp - public let dateLabel = MetaLabel(style: .statusUsername) - - public let menuButton: UIButton = { - let button = HitTestExpandedButton(type: .system) - button.expandEdgeInsets = UIEdgeInsets(top: -20, left: -10, bottom: -5, right: -10) - button.tintColor = Asset.Colors.Label.secondary.color - let image = UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(font: .systemFont(ofSize: 15))) - button.setImage(image, for: .normal) - button.accessibilityLabel = L10n.Common.Controls.Status.Actions.menu - return button - }() - - public let contentSensitiveeToggleButton: UIButton = { - let button = HitTestExpandedButton(type: .system) - button.expandEdgeInsets = UIEdgeInsets(top: -5, left: -10, bottom: -20, right: -10) - button.tintColor = Asset.Colors.Label.secondary.color - button.imageView?.contentMode = .scaleAspectFill - button.imageView?.clipsToBounds = false - let image = UIImage(systemName: "eye.slash.fill", withConfiguration: UIImage.SymbolConfiguration(font: .systemFont(ofSize: 15))) - button.setImage(image, for: .normal) - return button - }() - // content let contentAdaptiveMarginContainerView = AdaptiveMarginContainerView() let contentContainer = UIStackView() @@ -239,7 +195,7 @@ public final class StatusView: UIView { viewModel.objects.removeAll() viewModel.prepareForReuse() - avatarButton.avatarImageView.cancelTask() + authorView.avatarButton.avatarImageView.cancelTask() if var snapshot = pollTableViewDiffableDataSource?.snapshot() { snapshot.deleteAllItems() if #available(iOS 15.0, *) { @@ -288,18 +244,10 @@ extension StatusView { let headerTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer headerTapGestureRecognizer.addTarget(self, action: #selector(StatusView.headerDidPressed(_:))) headerContainerView.addGestureRecognizer(headerTapGestureRecognizer) - - // avatar button - avatarButton.addTarget(self, action: #selector(StatusView.authorAvatarButtonDidPressed(_:)), for: .touchUpInside) - authorNameLabel.isUserInteractionEnabled = false - authorUsernameLabel.isUserInteractionEnabled = false - - // contentSensitiveeToggleButton - contentSensitiveeToggleButton.addTarget(self, action: #selector(StatusView.contentSensitiveeToggleButtonDidPressed(_:)), for: .touchUpInside) - - // dateLabel - dateLabel.isUserInteractionEnabled = false - + + // author view + authorView.statusView = self + // content warning let spoilerOverlayViewTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer spoilerOverlayView.addGestureRecognizer(spoilerOverlayViewTapGestureRecognizer) @@ -336,16 +284,6 @@ extension StatusView { assert(sender.view === headerContainerView) delegate?.statusView(self, headerDidPressed: headerContainerView) } - - @objc private func authorAvatarButtonDidPressed(_ sender: UIButton) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - delegate?.statusView(self, authorAvatarButtonDidPressed: avatarButton) - } - - @objc private func contentSensitiveeToggleButtonDidPressed(_ sender: UIButton) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - delegate?.statusView(self, contentSensitiveeToggleButtonDidPressed: sender) - } @objc private func pollVoteButtonDidPressed(_ sender: UIButton) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") @@ -394,6 +332,8 @@ extension StatusView.Style { case .composeStatusReplica: composeStatusReplica(statusView: statusView) case .composeStatusAuthor: composeStatusAuthor(statusView: statusView) } + + statusView.authorView.layout(style: self) } private func base(statusView: StatusView) { @@ -425,81 +365,9 @@ extension StatusView.Style { statusView.headerIconImageView.setContentCompressionResistancePriority(.defaultLow - 100, for: .vertical) statusView.headerIconImageView.setContentCompressionResistancePriority(.defaultLow - 100, for: .horizontal) - // author container: H - [ avatarButton | author meta container | contentWarningToggleButton ] - statusView.authorAdaptiveMarginContainerView.contentView = statusView.authorContainerView + statusView.authorAdaptiveMarginContainerView.contentView = statusView.authorView statusView.authorAdaptiveMarginContainerView.margin = StatusView.containerLayoutMargin statusView.containerStackView.addArrangedSubview(statusView.authorAdaptiveMarginContainerView) - - UIContentSizeCategory.publisher - .sink { category in - statusView.authorContainerView.axis = category > .accessibilityLarge ? .vertical : .horizontal - statusView.authorContainerView.alignment = category > .accessibilityLarge ? .leading : .center - } - .store(in: &statusView._disposeBag) - - // avatarButton - let authorAvatarButtonSize = CGSize(width: 46, height: 46) - statusView.avatarButton.size = authorAvatarButtonSize - statusView.avatarButton.avatarImageView.imageViewSize = authorAvatarButtonSize - statusView.avatarButton.translatesAutoresizingMaskIntoConstraints = false - statusView.authorContainerView.addArrangedSubview(statusView.avatarButton) - NSLayoutConstraint.activate([ - statusView.avatarButton.widthAnchor.constraint(equalToConstant: authorAvatarButtonSize.width).priority(.required - 1), - statusView.avatarButton.heightAnchor.constraint(equalToConstant: authorAvatarButtonSize.height).priority(.required - 1), - ]) - statusView.avatarButton.setContentHuggingPriority(.required - 1, for: .vertical) - statusView.avatarButton.setContentCompressionResistancePriority(.required - 1, for: .vertical) - - // authrMetaContainer: V - [ authorPrimaryMetaContainer | authorSecondaryMetaContainer ] - let authorMetaContainer = UIStackView() - authorMetaContainer.axis = .vertical - authorMetaContainer.spacing = 4 - statusView.authorContainerView.addArrangedSubview(authorMetaContainer) - - // authorPrimaryMetaContainer: H - [ authorNameLabel | (padding) | menuButton ] - let authorPrimaryMetaContainer = UIStackView() - authorPrimaryMetaContainer.axis = .horizontal - authorPrimaryMetaContainer.spacing = 10 - authorMetaContainer.addArrangedSubview(authorPrimaryMetaContainer) - - // authorNameLabel - authorPrimaryMetaContainer.addArrangedSubview(statusView.authorNameLabel) - statusView.authorNameLabel.setContentHuggingPriority(.required - 10, for: .horizontal) - statusView.authorNameLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - authorPrimaryMetaContainer.addArrangedSubview(UIView()) - // menuButton - authorPrimaryMetaContainer.addArrangedSubview(statusView.menuButton) - statusView.menuButton.setContentHuggingPriority(.required - 2, for: .horizontal) - statusView.menuButton.setContentCompressionResistancePriority(.required - 2, for: .horizontal) - - // authorSecondaryMetaContainer: H - [ authorUsername | usernameTrialingDotLabel | dateLabel | (padding) | contentSensitiveeToggleButton ] - let authorSecondaryMetaContainer = UIStackView() - authorSecondaryMetaContainer.axis = .horizontal - authorSecondaryMetaContainer.spacing = 4 - authorMetaContainer.addArrangedSubview(authorSecondaryMetaContainer) - - authorSecondaryMetaContainer.addArrangedSubview(statusView.authorUsernameLabel) - statusView.authorUsernameLabel.setContentHuggingPriority(.required - 8, for: .horizontal) - statusView.authorUsernameLabel.setContentCompressionResistancePriority(.required - 8, for: .horizontal) - authorSecondaryMetaContainer.addArrangedSubview(statusView.usernameTrialingDotLabel) - statusView.usernameTrialingDotLabel.setContentHuggingPriority(.required - 2, for: .horizontal) - statusView.usernameTrialingDotLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal) - authorSecondaryMetaContainer.addArrangedSubview(statusView.dateLabel) - statusView.dateLabel.setContentHuggingPriority(.required - 1, for: .horizontal) - statusView.dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal) - authorSecondaryMetaContainer.addArrangedSubview(UIView()) - statusView.contentSensitiveeToggleButton.translatesAutoresizingMaskIntoConstraints = false - authorSecondaryMetaContainer.addArrangedSubview(statusView.contentSensitiveeToggleButton) - NSLayoutConstraint.activate([ - statusView.contentSensitiveeToggleButton.heightAnchor.constraint(equalTo: statusView.authorUsernameLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), - statusView.contentSensitiveeToggleButton.widthAnchor.constraint(equalTo: statusView.contentSensitiveeToggleButton.heightAnchor, multiplier: 1.0).priority(.required - 1), - ]) - statusView.authorUsernameLabel.setContentHuggingPriority(.required - 1, for: .vertical) - statusView.authorUsernameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) - statusView.contentSensitiveeToggleButton.setContentHuggingPriority(.defaultLow, for: .vertical) - statusView.contentSensitiveeToggleButton.setContentHuggingPriority(.defaultLow, for: .horizontal) - statusView.contentSensitiveeToggleButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - statusView.contentSensitiveeToggleButton.setContentCompressionResistancePriority(.defaultLow, for: .vertical) // content container: V - [ contentMetaText ] statusView.contentContainer.axis = .vertical @@ -605,7 +473,6 @@ extension StatusView.Style { func report(statusView: StatusView) { base(statusView: statusView) // override the base style - statusView.menuButton.removeFromSuperview() statusView.actionToolbarAdaptiveMarginContainerView.removeFromSuperview() } @@ -621,26 +488,18 @@ extension StatusView.Style { statusView.contentAdaptiveMarginContainerView.bottomLayoutConstraint?.constant = 16 // fix bottom margin missing issue statusView.pollAdaptiveMarginContainerView.bottomLayoutConstraint?.constant = 16 // fix bottom margin missing issue - statusView.contentSensitiveeToggleButton.removeFromSuperview() - statusView.menuButton.removeFromSuperview() statusView.actionToolbarAdaptiveMarginContainerView.removeFromSuperview() } func composeStatusReplica(statusView: StatusView) { base(statusView: statusView) - statusView.avatarButton.isUserInteractionEnabled = false - statusView.menuButton.removeFromSuperview() statusView.actionToolbarAdaptiveMarginContainerView.removeFromSuperview() } func composeStatusAuthor(statusView: StatusView) { base(statusView: statusView) - statusView.avatarButton.isUserInteractionEnabled = false - statusView.menuButton.removeFromSuperview() - statusView.usernameTrialingDotLabel.removeFromSuperview() - statusView.dateLabel.removeFromSuperview() statusView.contentAdaptiveMarginContainerView.removeFromSuperview() statusView.spoilerOverlayView.removeFromSuperview() statusView.mediaContainerView.removeFromSuperview() @@ -656,7 +515,7 @@ extension StatusView { } func setContentSensitiveeToggleButtonDisplay(isDisplay: Bool = true) { - contentSensitiveeToggleButton.isHidden = !isDisplay + authorView.contentSensitiveeToggleButton.isHidden = !isDisplay } func setSpoilerOverlayViewHidden(isHidden: Bool) { @@ -696,48 +555,6 @@ extension StatusView: AdaptiveContainerView { } } -extension StatusView { - - public struct AuthorMenuContext { - public let name: String - - public let isMuting: Bool - public let isBlocking: Bool - public let isMyself: Bool - } - - public func setupAuthorMenu(menuContext: AuthorMenuContext) -> UIMenu { - var actions: [MastodonMenu.Action] = [] - - actions = [ - .muteUser(.init( - name: menuContext.name, - isMuting: menuContext.isMuting - )), - .blockUser(.init( - name: menuContext.name, - isBlocking: menuContext.isBlocking - )), - .reportUser( - .init(name: menuContext.name) - ), - ] - - if menuContext.isMyself { - actions.append(.deleteStatus) - } - - - let menu = MastodonMenu.setupMenu( - actions: actions, - delegate: self - ) - - return menu - } - -} - // MARK: - UITextViewDelegate extension StatusView: UITextViewDelegate { @@ -823,7 +640,7 @@ extension StatusView: StatusMetricViewDelegate { extension StatusView: MastodonMenuDelegate { public func menuAction(_ action: MastodonMenu.Action) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - delegate?.statusView(self, menuButton: menuButton, didSelectAction: action) + delegate?.statusView(self, menuButton: authorView.menuButton, didSelectAction: action) } } From 8f3caba0891a9da53c6e589dd1aacb45e436ea31 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 31 Oct 2022 12:40:18 -0400 Subject: [PATCH 055/658] Remove the status metric view from the accessibility hierarchy --- .../View/Content/StatusMetricView.swift | 2 ++ .../View/Content/StatusView+ViewModel.swift | 13 ++++++---- .../MastodonUI/View/Content/StatusView.swift | 8 +++++++ .../View/Control/ActionToolbarContainer.swift | 24 +++++++++++++++++-- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusMetricView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusMetricView.swift index d5f6a0709..f1ca95b3e 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusMetricView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusMetricView.swift @@ -100,6 +100,8 @@ extension StatusMetricView { reblogButton.addTarget(self, action: #selector(StatusMetricView.reblogButtonDidPressed(_:)), for: .touchUpInside) favoriteButton.addTarget(self, action: #selector(StatusMetricView.favoriteButtonDidPressed(_:)), for: .touchUpInside) + + accessibilityElementsHidden = true } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index af2bba14a..0e005f3a5 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -258,8 +258,7 @@ extension StatusView.ViewModel { return text } .removeDuplicates() - - timestampPublisher.assign(to: &$timestampText) + .assign(to: &$timestampText) $timestampText .sink { [weak self] text in @@ -269,9 +268,13 @@ extension StatusView.ViewModel { .store(in: &disposeBag) // accessibility label - Publishers.CombineLatest3($authorName, usernamePublisher, timestampPublisher) - .map { name, username, timestamp in - "\(name?.string ?? "") \(username), \(timestamp)" + Publishers.CombineLatest4($authorName, usernamePublisher, $timestampText, $timestamp) + .map { name, username, timestampText, timestamp in + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .short + let longTimestamp = timestamp.map { formatter.string(from: $0) } ?? "" + return "\(name?.string ?? "") \(username), \(timestampText). \(longTimestamp)" } .assign(to: \.accessibilityLabel, on: authorView) .store(in: &disposeBag) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index 1c9ed673d..6740715ca 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -623,6 +623,14 @@ extension StatusView: ActionToolbarContainerDelegate { public func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action) { delegate?.statusView(self, actionToolbarContainer: actionToolbarContainer, buttonDidPressed: button, action: action) } + + public func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, showReblogs action: UIAccessibilityCustomAction) { + delegate?.statusView(self, statusMetricView: statusMetricView, reblogButtonDidPressed: statusMetricView.reblogButton) + } + + public func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, showFavorites action: UIAccessibilityCustomAction) { + delegate?.statusView(self, statusMetricView: statusMetricView, favoriteButtonDidPressed: statusMetricView.favoriteButton) + } } // MARK: - StatusMetricViewDelegate diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift b/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift index 4a5c44850..be6506642 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift @@ -12,6 +12,8 @@ import MastodonLocalization public protocol ActionToolbarContainerDelegate: AnyObject { func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action) + func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, showReblogs action: UIAccessibilityCustomAction) + func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, showFavorites action: UIAccessibilityCustomAction) } public final class ActionToolbarContainer: UIView { @@ -222,6 +224,7 @@ extension ActionToolbarContainer { public func configureReblog(count: Int, isEnabled: Bool, isHighlighted: Bool) { let title = ActionToolbarContainer.title(from: count) reblogButton.setTitle(title, for: .normal) + reblogButton.accessibilityValue = L10n.Plural.Count.reblog(count) reblogButton.isEnabled = isEnabled reblogButton.setImage(ActionToolbarContainer.reblogImage, for: .normal) let tintColor = isHighlighted ? Asset.Colors.successGreen.color : Asset.Colors.Button.actionToolbar.color @@ -231,15 +234,24 @@ extension ActionToolbarContainer { if isHighlighted { reblogButton.accessibilityTraits.insert(.selected) + reblogButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.unreblog } else { reblogButton.accessibilityTraits.remove(.selected) + reblogButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.reblog } - reblogButton.accessibilityLabel = L10n.Plural.Count.reblog(count) + reblogButton.accessibilityCustomActions = [ + UIAccessibilityCustomAction(name: "Show All Reblogs") { [weak self] action in + guard let self = self else { return false } + self.delegate?.actionToolbarContainer(self, showReblogs: action) + return true + } + ] } public func configureFavorite(count: Int, isEnabled: Bool, isHighlighted: Bool) { let title = ActionToolbarContainer.title(from: count) favoriteButton.setTitle(title, for: .normal) + favoriteButton.accessibilityValue = L10n.Plural.Count.favorite(count) favoriteButton.isEnabled = isEnabled let image = isHighlighted ? ActionToolbarContainer.starFillImage : ActionToolbarContainer.starImage favoriteButton.setImage(image, for: .normal) @@ -250,10 +262,18 @@ extension ActionToolbarContainer { if isHighlighted { favoriteButton.accessibilityTraits.insert(.selected) + favoriteButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.unfavorite } else { favoriteButton.accessibilityTraits.remove(.selected) + favoriteButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.favorite } - favoriteButton.accessibilityLabel = L10n.Plural.Count.favorite(count) + favoriteButton.accessibilityCustomActions = [ + UIAccessibilityCustomAction(name: "Show All Favorites") { [weak self] action in + guard let self = self else { return false } + self.delegate?.actionToolbarContainer(self, showFavorites: action) + return true + } + ] } } From 1c236859ab88caebb9452bae609711de30450d92 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 31 Oct 2022 12:52:15 -0400 Subject: [PATCH 056/658] Jump to thread table view when view appears MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes the issue where people can’t get past the navigation title --- Mastodon/Scene/Thread/ThreadViewController.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Mastodon/Scene/Thread/ThreadViewController.swift b/Mastodon/Scene/Thread/ThreadViewController.swift index bd90fb370..d67a42b45 100644 --- a/Mastodon/Scene/Thread/ThreadViewController.swift +++ b/Mastodon/Scene/Thread/ThreadViewController.swift @@ -104,6 +104,12 @@ extension ThreadViewController { tableView.deselectRow(with: transitionCoordinator, animated: animated) } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + UIAccessibility.post(notification: .screenChanged, argument: tableView) + } } From 715aa8c2489552bf38c9ba7570b9341df0fc4e74 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 31 Oct 2022 13:13:52 -0400 Subject: [PATCH 057/658] Mark post content as an accessibility element --- .../View/TableviewCell/StatusThreadRootTableViewCell.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift index 0049539b0..3738bf8a1 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift @@ -79,7 +79,7 @@ extension StatusThreadRootTableViewCell { statusView.delegate = self // a11y - statusView.contentMetaText.textView.isAccessibilityElement = false + statusView.contentMetaText.textView.isAccessibilityElement = true statusView.contentMetaText.textView.isSelectable = true } @@ -98,8 +98,9 @@ extension StatusThreadRootTableViewCell { var elements = [ statusView.headerContainerView, statusView.authorView, - statusView.spoilerOverlayView, - statusView.contentMetaText.textView, + statusView.viewModel.isContentReveal + ? statusView.contentMetaText.textView + : statusView.spoilerOverlayView, statusView.mediaGridContainerView, statusView.pollTableView, statusView.pollStatusStackView, From 4da11c9dfb9cf79f3133cbff8e58008c41b29c9c Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 31 Oct 2022 13:15:29 -0400 Subject: [PATCH 058/658] Mark the spoiler overlay view as a button --- .../Sources/MastodonUI/View/Control/SpoilerOverlayView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/SpoilerOverlayView.swift b/MastodonSDK/Sources/MastodonUI/View/Control/SpoilerOverlayView.swift index 17360d545..98139b00e 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Control/SpoilerOverlayView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Control/SpoilerOverlayView.swift @@ -72,6 +72,7 @@ extension SpoilerOverlayView { spoilerMetaLabel.isUserInteractionEnabled = false isAccessibilityElement = true + accessibilityTraits.insert(.button) } public func setComponentHidden(_ isHidden: Bool) { From 98b87a0b2021e7023f72505d94446a5fea58270d Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 31 Oct 2022 13:20:53 -0400 Subject: [PATCH 059/658] fix removing status metric view from a11y heirarchy --- .../View/TableviewCell/StatusThreadRootTableViewCell.swift | 4 ++-- .../Sources/MastodonUI/View/Content/StatusMetricView.swift | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift index 3738bf8a1..64f3456b5 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift @@ -104,8 +104,8 @@ extension StatusThreadRootTableViewCell { statusView.mediaGridContainerView, statusView.pollTableView, statusView.pollStatusStackView, - statusView.actionToolbarContainer, - statusView.statusMetricView + statusView.actionToolbarContainer + // statusMetricView is intentionally excluded ] if statusView.viewModel.isContentReveal { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusMetricView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusMetricView.swift index f1ca95b3e..d5f6a0709 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusMetricView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusMetricView.swift @@ -100,8 +100,6 @@ extension StatusMetricView { reblogButton.addTarget(self, action: #selector(StatusMetricView.reblogButtonDidPressed(_:)), for: .touchUpInside) favoriteButton.addTarget(self, action: #selector(StatusMetricView.favoriteButtonDidPressed(_:)), for: .touchUpInside) - - accessibilityElementsHidden = true } } From 211ff344fb10bbe61efe8d205699678a7a28b8b4 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 31 Oct 2022 15:07:27 -0400 Subject: [PATCH 060/658] Update reply button labelling to match the others --- .../MastodonUI/View/Control/ActionToolbarContainer.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift b/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift index be6506642..ca7ca83a9 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift @@ -218,7 +218,8 @@ extension ActionToolbarContainer { public func configureReply(count: Int, isEnabled: Bool) { let title = ActionToolbarContainer.title(from: count) replyButton.setTitle(title, for: .normal) - replyButton.accessibilityLabel = L10n.Plural.Count.reply(count) + replyButton.accessibilityLabel = L10n.Common.Controls.Actions.reply + replyButton.accessibilityValue = L10n.Plural.Count.reply(count) } public func configureReblog(count: Int, isEnabled: Bool, isHighlighted: Bool) { From 0b0d7fcd48b8b12f505bbd161e20edfb0c528853 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 15 Sep 2022 21:28:40 +0800 Subject: [PATCH 061/658] chore: cherry pick 00eddc2aae79591ba0c1fae12b68d0b5010f29b9 from feature-post-edit branch --- .../Provider/DataSourceFacade+Bookmark.swift | 2 +- .../Provider/DataSourceFacade+Status.swift | 43 ++++++++++++++++--- .../Content/NotificationView+ViewModel.swift | 3 +- .../View/Content/StatusView+ViewModel.swift | 29 +++++++------ .../MastodonUI/View/Content/StatusView.swift | 5 +++ .../View/Control/ActionToolbarContainer.swift | 38 ++-------------- .../MastodonUI/View/Menu/MastodonMenu.swift | 38 +++++++++++++++- 7 files changed, 102 insertions(+), 56 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift index b7a793551..2c54653ba 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift @@ -12,7 +12,7 @@ import MastodonCore extension DataSourceFacade { public static func responseToStatusBookmarkAction( - provider: DataSourceProvider & AuthContextProvider, + provider: UIViewController & NeedsDependency & AuthContextProvider, status: ManagedObjectRecord ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 53755e00a..23e2022ea 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -36,7 +36,7 @@ extension DataSourceFacade { button: UIButton ) async throws { let activityViewController = try await createActivityViewController( - provider: provider, + dependency: provider, status: status ) provider.coordinator.present( @@ -51,19 +51,19 @@ extension DataSourceFacade { } private static func createActivityViewController( - provider: DataSourceProvider, + dependency: NeedsDependency, status: ManagedObjectRecord ) async throws -> UIActivityViewController { - var activityItems: [Any] = try await provider.context.managedObjectContext.perform { - guard let status = status.object(in: provider.context.managedObjectContext) else { return [] } + var activityItems: [Any] = try await dependency.context.managedObjectContext.perform { + guard let status = status.object(in: dependency.context.managedObjectContext) else { return [] } let url = status.url ?? status.uri return [URL(string: url)].compactMap { $0 } as [Any] } var applicationActivities: [UIActivity] = [ - SafariActivity(sceneCoordinator: provider.coordinator), // open URL + SafariActivity(sceneCoordinator: dependency.coordinator), // open URL ] - if let provider = provider as? ShareActivityProvider { + if let provider = dependency as? ShareActivityProvider { activityItems.append(contentsOf: provider.activities) applicationActivities.append(contentsOf: provider.applicationActivities) } @@ -247,6 +247,37 @@ extension DataSourceFacade { from: dependency, transition: .activityViewControllerPresent(animated: true, completion: nil) ) + case .bookmarkStatus: + Task { + guard let status = menuContext.status else { + assertionFailure() + return + } + try await DataSourceFacade.responseToStatusBookmarkAction( + provider: dependency, + status: status + ) + } // end Task + case .shareStatus: + Task { + guard let status = menuContext.status else { + assertionFailure() + return + } + let activityViewController = try await DataSourceFacade.createActivityViewController( + dependency: dependency, + status: status + ) + await dependency.coordinator.present( + scene: .activityViewController( + activityViewController: activityViewController, + sourceView: menuContext.button, + barButtonItem: menuContext.barButtonItem + ), + from: dependency, + transition: .activityViewControllerPresent(animated: true, completion: nil) + ) + } // end Task case .deleteStatus: let alertController = UIAlertController( title: "Delete Post", diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift index 032760983..d7ba51e17 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift @@ -144,7 +144,8 @@ extension NotificationView.ViewModel { name: name, isMuting: isMuting, isBlocking: isBlocking, - isMyself: isMyself + isMyself: isMyself, + isBookmarking: false // no bookmark action display for notification item ) notificationView.menuButton.menu = notificationView.setupAuthorMenu(menuContext: menuContext) notificationView.menuButton.showsMenuAsPrimaryAction = true diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index cb542da7c..202d70d91 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -514,13 +514,6 @@ extension StatusView.ViewModel { ) } .store(in: &disposeBag) - $isBookmark - .sink { isHighlighted in - statusView.actionToolbarContainer.configureBookmark( - isHighlighted: isHighlighted - ) - } - .store(in: &disposeBag) } private func bindMetric(statusView: StatusView) { @@ -577,13 +570,24 @@ extension StatusView.ViewModel { } private func bindMenu(statusView: StatusView) { - Publishers.CombineLatest4( + let publisherOne = Publishers.CombineLatest( $authorName, - $isMuting, - $isBlocking, $isMyself ) - .sink { authorName, isMuting, isBlocking, isMyself in + let publishersTwo = Publishers.CombineLatest3( + $isMuting, + $isBlocking, + $isBookmark + ) + + Publishers.CombineLatest( + publisherOne.eraseToAnyPublisher(), + publishersTwo.eraseToAnyPublisher() + ).eraseToAnyPublisher() + .sink { tupleOne, tupleTwo in + let (authorName, isMyself) = tupleOne + let (isMuting, isBlocking, isBookmark) = tupleTwo + guard let name = authorName?.string else { statusView.menuButton.menu = nil return @@ -593,7 +597,8 @@ extension StatusView.ViewModel { name: name, isMuting: isMuting, isBlocking: isBlocking, - isMyself: isMyself + isMyself: isMyself, + isBookmarking: isBookmark ) statusView.menuButton.menu = statusView.setupAuthorMenu(menuContext: menuContext) statusView.menuButton.showsMenuAsPrimaryAction = true diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index 084dca3d1..4c4318bad 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -705,6 +705,7 @@ extension StatusView { public let isMuting: Bool public let isBlocking: Bool public let isMyself: Bool + public let isBookmarking: Bool } public func setupAuthorMenu(menuContext: AuthorMenuContext) -> UIMenu { @@ -722,6 +723,10 @@ extension StatusView { .reportUser( .init(name: menuContext.name) ), + .bookmarkStatus( + .init(isBookmarking: menuContext.isBookmarking) + ), + .shareStatus ] if menuContext.isMyself { diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift b/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift index ccfc8020e..617e29a75 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift @@ -22,14 +22,11 @@ public final class ActionToolbarContainer: UIView { static let reblogImage = Asset.Arrow.repeat.image.withRenderingMode(.alwaysTemplate) static let starImage = Asset.ObjectsAndTools.star.image.withRenderingMode(.alwaysTemplate) static let starFillImage = Asset.ObjectsAndTools.starFill.image.withRenderingMode(.alwaysTemplate) - static let bookmarkImage = Asset.ObjectsAndTools.bookmark.image.withRenderingMode(.alwaysTemplate) - static let bookmarkFillImage = Asset.ObjectsAndTools.bookmarkFill.image.withRenderingMode(.alwaysTemplate) - static let shareImage = Asset.Communication.share.image.withRenderingMode(.alwaysTemplate) + static let shareImage = Asset.Arrow.squareAndArrowUp.image.withRenderingMode(.alwaysTemplate) public let replyButton = HighlightDimmableButton() public let reblogButton = HighlightDimmableButton() public let favoriteButton = HighlightDimmableButton() - public let bookmarkButton = HighlightDimmableButton() public let shareButton = HighlightDimmableButton() public weak var delegate: ActionToolbarContainerDelegate? @@ -64,7 +61,6 @@ extension ActionToolbarContainer { replyButton.addTarget(self, action: #selector(ActionToolbarContainer.buttonDidPressed(_:)), for: .touchUpInside) reblogButton.addTarget(self, action: #selector(ActionToolbarContainer.buttonDidPressed(_:)), for: .touchUpInside) favoriteButton.addTarget(self, action: #selector(ActionToolbarContainer.buttonDidPressed(_:)), for: .touchUpInside) - bookmarkButton.addTarget(self, action: #selector(ActionToolbarContainer.buttonDidPressed(_:)), for: .touchUpInside) shareButton.addTarget(self, action: #selector(ActionToolbarContainer.buttonDidPressed(_:)), for: .touchUpInside) } @@ -79,7 +75,7 @@ extension ActionToolbarContainer { subview.removeFromSuperview() } - let buttons = [replyButton, reblogButton, favoriteButton, bookmarkButton, shareButton] + let buttons = [replyButton, reblogButton, favoriteButton, shareButton] buttons.forEach { button in button.tintColor = Asset.Colors.Button.actionToolbar.color button.titleLabel?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular) @@ -94,7 +90,6 @@ extension ActionToolbarContainer { replyButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.reply reblogButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.reblog // needs update to follow state favoriteButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.favorite // needs update to follow state - bookmarkButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.bookmark // needs update to follow state shareButton.accessibilityLabel = L10n.Common.Controls.Actions.share switch style { @@ -105,7 +100,6 @@ extension ActionToolbarContainer { replyButton.setImage(ActionToolbarContainer.replyImage, for: .normal) reblogButton.setImage(ActionToolbarContainer.reblogImage, for: .normal) favoriteButton.setImage(ActionToolbarContainer.starImage, for: .normal) - bookmarkButton.setImage(ActionToolbarContainer.bookmarkImage, for: .normal) shareButton.setImage(ActionToolbarContainer.shareImage, for: .normal) container.axis = .horizontal @@ -114,22 +108,18 @@ extension ActionToolbarContainer { replyButton.translatesAutoresizingMaskIntoConstraints = false reblogButton.translatesAutoresizingMaskIntoConstraints = false favoriteButton.translatesAutoresizingMaskIntoConstraints = false - bookmarkButton.translatesAutoresizingMaskIntoConstraints = false shareButton.translatesAutoresizingMaskIntoConstraints = false container.addArrangedSubview(replyButton) container.addArrangedSubview(reblogButton) container.addArrangedSubview(favoriteButton) - container.addArrangedSubview(bookmarkButton) container.addArrangedSubview(shareButton) NSLayoutConstraint.activate([ replyButton.heightAnchor.constraint(equalToConstant: 36).priority(.defaultHigh), replyButton.heightAnchor.constraint(equalTo: reblogButton.heightAnchor).priority(.defaultHigh), replyButton.heightAnchor.constraint(equalTo: favoriteButton.heightAnchor).priority(.defaultHigh), - replyButton.heightAnchor.constraint(equalTo: bookmarkButton.heightAnchor).priority(.defaultHigh), replyButton.heightAnchor.constraint(equalTo: shareButton.heightAnchor).priority(.defaultHigh), replyButton.widthAnchor.constraint(equalTo: reblogButton.widthAnchor).priority(.defaultHigh), replyButton.widthAnchor.constraint(equalTo: favoriteButton.widthAnchor).priority(.defaultHigh), - replyButton.widthAnchor.constraint(equalTo: bookmarkButton.widthAnchor).priority(.defaultHigh), ]) shareButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) shareButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) @@ -141,7 +131,6 @@ extension ActionToolbarContainer { replyButton.setImage(ActionToolbarContainer.replyImage, for: .normal) reblogButton.setImage(ActionToolbarContainer.reblogImage, for: .normal) favoriteButton.setImage(ActionToolbarContainer.starImage, for: .normal) - bookmarkButton.setImage(ActionToolbarContainer.bookmarkImage, for: .normal) container.axis = .horizontal container.spacing = 8 @@ -150,7 +139,6 @@ extension ActionToolbarContainer { container.addArrangedSubview(replyButton) container.addArrangedSubview(reblogButton) container.addArrangedSubview(favoriteButton) - container.addArrangedSubview(bookmarkButton) } } @@ -197,11 +185,6 @@ extension ActionToolbarContainer { favoriteButton.setTitleColor(tintColor, for: .highlighted) } - private func isBookmarkButtonHighlightStateDidChange(to isHighlight: Bool) { - let tintColor = isHighlight ? Asset.Colors.brand.color : Asset.Colors.Button.actionToolbar.color - bookmarkButton.tintColor = tintColor - } - } extension ActionToolbarContainer { @@ -214,7 +197,6 @@ extension ActionToolbarContainer { case replyButton: _action = .reply case reblogButton: _action = .reblog case favoriteButton: _action = .like - case bookmarkButton: _action = .bookmark case shareButton: _action = .share default: _action = nil } @@ -275,20 +257,6 @@ extension ActionToolbarContainer { favoriteButton.accessibilityLabel = L10n.Plural.Count.favorite(count) } - public func configureBookmark(isHighlighted: Bool) { - let image = isHighlighted ? ActionToolbarContainer.bookmarkFillImage : ActionToolbarContainer.bookmarkImage - bookmarkButton.setImage(image, for: .normal) - let tintColor = isHighlighted ? Asset.Colors.brand.color : Asset.Colors.Button.actionToolbar.color - bookmarkButton.tintColor = tintColor - - if isHighlighted { - bookmarkButton.accessibilityTraits.insert(.selected) - } else { - bookmarkButton.accessibilityTraits.remove(.selected) - } - bookmarkButton.accessibilityLabel = isHighlighted ? L10n.Common.Controls.Status.Actions.unbookmark : L10n.Common.Controls.Status.Actions.bookmark - } - } extension ActionToolbarContainer { @@ -300,7 +268,7 @@ extension ActionToolbarContainer { extension ActionToolbarContainer { public override var accessibilityElements: [Any]? { - get { [replyButton, reblogButton, favoriteButton, bookmarkButton, shareButton] } + get { [replyButton, reblogButton, favoriteButton, shareButton] } set { } } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift b/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift index de4bc403d..f5763f638 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift @@ -32,6 +32,8 @@ extension MastodonMenu { case blockUser(BlockUserActionContext) case reportUser(ReportUserActionContext) case shareUser(ShareUserActionContext) + case bookmarkStatus(BookmarkStatusActionContext) + case shareStatus case deleteStatus func build(delegate: MastodonMenuDelegate) -> UIMenuElement { @@ -88,6 +90,32 @@ extension MastodonMenu { delegate.menuAction(self) } return shareAction + case .bookmarkStatus(let context): + let action = UIAction( + title: context.isBookmarking ? "Remove Bookmark" : "Bookmark", // TODO: i18n + image: context.isBookmarking ? UIImage(systemName: "bookmark.slash.fill") : UIImage(systemName: "bookmark"), + identifier: nil, + discoverabilityTitle: nil, + attributes: [], + state: .off + ) { [weak delegate] _ in + guard let delegate = delegate else { return } + delegate.menuAction(self) + } + return action + case .shareStatus: + let action = UIAction( + title: "Share", // TODO: i18n + image: UIImage(systemName: "square.and.arrow.up"), + identifier: nil, + discoverabilityTitle: nil, + attributes: [], + state: .off + ) { [weak delegate] _ in + guard let delegate = delegate else { return } + delegate.menuAction(self) + } + return action case .deleteStatus: let deleteAction = UIAction( title: L10n.Common.Controls.Actions.delete, @@ -100,7 +128,7 @@ extension MastodonMenu { guard let delegate = delegate else { return } delegate.menuAction(self) } - return deleteAction + return UIMenu(options: .displayInline, children: [deleteAction]) } // end switch } // end func build } // end enum Action @@ -127,6 +155,14 @@ extension MastodonMenu { } } + public struct BookmarkStatusActionContext { + public let isBookmarking: Bool + + public init(isBookmarking: Bool) { + self.isBookmarking = isBookmarking + } + } + public struct ReportUserActionContext { public let name: String From e5bfed50bb5f50f9de7ed5adf564896e1d42d378 Mon Sep 17 00:00:00 2001 From: Jordan Kay Date: Tue, 1 Nov 2022 14:47:08 -0400 Subject: [PATCH 062/658] Fix typos in Setup.md --- Documentation/Setup.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Documentation/Setup.md b/Documentation/Setup.md index 1c2f0a6c5..61d8f435b 100644 --- a/Documentation/Setup.md +++ b/Documentation/Setup.md @@ -7,7 +7,7 @@ - iOS 14.0+ -Intell the latest version of Xcode from the App Store or Apple Developer Download website. Also, we assert you have the [Homebrew](https://brew.sh) package manager. +Install the latest version of Xcode from the App Store or Apple Developer Download website. Also, we assert you have the [Homebrew](https://brew.sh) package manager. This guide may not suit your machine and actually setup procedure may change in the future. Please file the issue or Pull Request if there are any problems. @@ -59,12 +59,12 @@ bundle exec pod install --repo-update open Mastodon.xcworkspace ``` -The CocoaPods-Key plugin will request the push notification endpoint. You can fufill the empty string and set it later. To setup the push notification. Please check section `Push Notification` below. +The CocoaPods-Keys plugin will request the push notification endpoint. You can fulfill the empty string and set it later. To setup the push notification. Please check section `Push Notification` below. The app requires the `App Group` capability. To make sure it works for your developer membership. Please check [AppSecret.swift](../AppShared/AppSecret.swift) file and set another unique `groupID` and update `App Group` settings. #### Push Notification (Optional) -The app is compatible with [toot-relay](https://github.com/DagAgren/toot-relay) APNs. You can set your push notification endpoint via Cocoapod-Keys. There are two endpoints: +The app is compatible with [toot-relay](https://github.com/DagAgren/toot-relay) APNs. You can set your push notification endpoint via CocoaPods-Keys. There are two endpoints: - notification_endpoint: for `RELEASE` usage - notification_endpoint_debug: for `DEBUG` usage @@ -82,4 +82,4 @@ Please check and set the `notification.Topic` to the app BundleID in [toot-relay ## What's next -We welcome contributions! And if you have an interest to contribute codes. Here is a document that describes the app architecture and what's tech stack it uses. \ No newline at end of file +We welcome contributions! And if you have an interest to contribute codes. Here is a document that describes the app architecture and what's tech stack it uses. From 9d7614a4037f98f26cb1bc0c5bf461c501b9cb2f Mon Sep 17 00:00:00 2001 From: NanoSector Date: Tue, 1 Nov 2022 19:55:51 +0100 Subject: [PATCH 063/658] 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 bd851e4e1c49ddc1b3a9c74e6cb076aaf030a6e3 Mon Sep 17 00:00:00 2001 From: tejuamirthi Date: Wed, 2 Nov 2022 22:13:58 +0530 Subject: [PATCH 064/658] update menu action title message i18n string --- .../Protocol/Provider/DataSourceFacade+Status.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 23e2022ea..175ebe474 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -156,8 +156,8 @@ extension DataSourceFacade { switch action { case .muteUser(let actionContext): let alertController = UIAlertController( - title: actionContext.isMuting ? "Unmute Account" : "Mute Account", - message: actionContext.isMuting ? "Confirm to unmute \(actionContext.name)" : "Confirm to mute \(actionContext.name)", + title: actionContext.isMuting ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title : L10n.Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.title, + message: actionContext.isMuting ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(actionContext.name) : L10n.Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.message(actionContext.name), preferredStyle: .alert ) let confirmAction = UIAlertAction( @@ -184,8 +184,8 @@ extension DataSourceFacade { dependency.present(alertController, animated: true, completion: nil) case .blockUser(let actionContext): let alertController = UIAlertController( - title: actionContext.isBlocking ? "Unblock Account" : "Block Account", - message: actionContext.isBlocking ? "Confirm to unblock \(actionContext.name)" : "Confirm to block \(actionContext.name)", + title: actionContext.isBlocking ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title : L10n.Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.title, + message: actionContext.isBlocking ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.message(actionContext.name) : L10n.Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.message(actionContext.name), preferredStyle: .alert ) let confirmAction = UIAlertAction( @@ -280,8 +280,8 @@ extension DataSourceFacade { } // end Task case .deleteStatus: let alertController = UIAlertController( - title: "Delete Post", - message: "Are you sure you want to delete this post?", + title: L10n.Common.Alerts.DeletePost.title, + message: L10n.Common.Alerts.DeletePost.message, preferredStyle: .alert ) let confirmAction = UIAlertAction( From 8114b7d2acc91c491d274d34055bc80ed1165973 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Thu, 3 Nov 2022 09:24:30 -0400 Subject: [PATCH 065/658] Add support for scrolling the discovery tab to the top/first tab --- Mastodon/Protocol/ScrollViewContainer.swift | 8 +++++++- Mastodon/Scene/Discovery/DiscoveryViewController.swift | 7 +++++++ .../Scene/Search/Search/SearchViewController.swift | 10 ++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Mastodon/Protocol/ScrollViewContainer.swift b/Mastodon/Protocol/ScrollViewContainer.swift index ae79d0e0f..8e3fda06e 100644 --- a/Mastodon/Protocol/ScrollViewContainer.swift +++ b/Mastodon/Protocol/ScrollViewContainer.swift @@ -14,6 +14,12 @@ protocol ScrollViewContainer: UIViewController { extension ScrollViewContainer { func scrollToTop(animated: Bool) { - scrollView.scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: animated) + scrollView.scrollToTop(animated: animated) + } +} + +extension UIScrollView { + func scrollToTop(animated: Bool) { + scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: animated) } } diff --git a/Mastodon/Scene/Discovery/DiscoveryViewController.swift b/Mastodon/Scene/Discovery/DiscoveryViewController.swift index 33bbefaef..969ba5534 100644 --- a/Mastodon/Scene/Discovery/DiscoveryViewController.swift +++ b/Mastodon/Scene/Discovery/DiscoveryViewController.swift @@ -131,6 +131,13 @@ extension DiscoveryViewController: ScrollViewContainer { var scrollView: UIScrollView { return (currentViewController as? ScrollViewContainer)?.scrollView ?? UIScrollView() } + func scrollToTop(animated: Bool) { + if scrollView.contentOffset.y <= 0 { + scrollToPage(.first, animated: animated) + } else { + scrollView.scrollToTop(animated: animated) + } + } } extension DiscoveryViewController { diff --git a/Mastodon/Scene/Search/Search/SearchViewController.swift b/Mastodon/Scene/Search/Search/SearchViewController.swift index b5259dcc4..7efef2c00 100644 --- a/Mastodon/Scene/Search/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/Search/SearchViewController.swift @@ -190,6 +190,16 @@ extension SearchViewController: UISearchControllerDelegate { } } +// MARK: - ScrollViewContainer +extension SearchViewController: ScrollViewContainer { + var scrollView: UIScrollView { + discoveryViewController?.scrollView ?? UIScrollView() + } + func scrollToTop(animated: Bool) { + discoveryViewController?.scrollToTop(animated: animated) + } +} + // MARK: - UICollectionViewDelegate //extension SearchViewController: UICollectionViewDelegate { // func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { From ceece731a4498c8380bc4b9d1db0b19a1ca635e8 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 31 Oct 2022 16:01:19 -0400 Subject: [PATCH 066/658] Use LPLinkMetadata to improve sharing behavior --- .../Provider/DataSourceFacade+Status.swift | 55 ++++++++++++++++++- .../View/Content/NotificationView.swift | 9 ++- .../View/Content/StatusAuthorView.swift | 9 ++- .../MastodonUI/View/Content/StatusView.swift | 4 ++ 4 files changed, 65 insertions(+), 12 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 175ebe474..9d9e73eb3 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -7,9 +7,13 @@ import UIKit import CoreDataStack +import Alamofire +import AlamofireImage import MastodonCore import MastodonUI import MastodonLocalization +import LinkPresentation +import UniformTypeIdentifiers // Delete extension DataSourceFacade { @@ -56,8 +60,7 @@ extension DataSourceFacade { ) async throws -> UIActivityViewController { var activityItems: [Any] = try await dependency.context.managedObjectContext.perform { guard let status = status.object(in: dependency.context.managedObjectContext) else { return [] } - let url = status.url ?? status.uri - return [URL(string: url)].compactMap { $0 } as [Any] + return [StatusActivityItem(status: status)].compactMap { $0 } as [Any] } var applicationActivities: [UIActivity] = [ SafariActivity(sceneCoordinator: dependency.coordinator), // open URL @@ -74,6 +77,54 @@ extension DataSourceFacade { ) return activityViewController } + + private class StatusActivityItem: NSObject, UIActivityItemSource { + init?(status: Status) { + guard let url = URL(string: status.url ?? status.uri) else { return nil } + self.url = url + self.metadata = LPLinkMetadata() + metadata.url = url + metadata.title = "\(status.author.displayName) (@\(status.author.username)@\(status.author.domain))" + metadata.iconProvider = NSItemProvider(object: IconProvider(url: status.author.avatarImageURLWithFallback(domain: status.author.domain))) + } + + let url: URL + let metadata: LPLinkMetadata + + func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { + url + } + + func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { + url + } + + func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { + metadata + } + + private class IconProvider: NSObject, NSItemProviderWriting { + let url: URL + init(url: URL) { + self.url = url + } + + static var writableTypeIdentifiersForItemProvider: [String] { + [UTType.png.identifier] + } + + func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping @Sendable (Data?, Error?) -> Void) -> Progress? { + let filter = ScaledToSizeFilter(size: CGSize.authorAvatarButtonSize) + let receipt = UIImageView.af.sharedImageDownloader.download(URLRequest(url: url), filter: filter, completion: { response in + switch response.result { + case .failure(let error): completionHandler(nil, error) + case .success(let image): completionHandler(image.pngData(), nil) + } + }) + return receipt?.request.downloadProgress + } + } + } } // ActionToolBar diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift index 45e19cc77..e52422770 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift @@ -226,14 +226,13 @@ extension NotificationView { .store(in: &_disposeBag) // avatarButton - let authorAvatarButtonSize = CGSize(width: 46, height: 46) - avatarButton.size = authorAvatarButtonSize - avatarButton.avatarImageView.imageViewSize = authorAvatarButtonSize + avatarButton.size = CGSize.authorAvatarButtonSize + avatarButton.avatarImageView.imageViewSize = CGSize.authorAvatarButtonSize avatarButton.translatesAutoresizingMaskIntoConstraints = false authorContainerView.addArrangedSubview(avatarButton) NSLayoutConstraint.activate([ - avatarButton.widthAnchor.constraint(equalToConstant: authorAvatarButtonSize.width).priority(.required - 1), - avatarButton.heightAnchor.constraint(equalToConstant: authorAvatarButtonSize.height).priority(.required - 1), + avatarButton.widthAnchor.constraint(equalToConstant: CGSize.authorAvatarButtonSize.width).priority(.required - 1), + avatarButton.heightAnchor.constraint(equalToConstant: CGSize.authorAvatarButtonSize.height).priority(.required - 1), ]) avatarButton.setContentHuggingPriority(.required - 1, for: .vertical) avatarButton.setContentCompressionResistancePriority(.required - 1, for: .vertical) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusAuthorView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusAuthorView.swift index aac1543a4..e2879d1e8 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusAuthorView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusAuthorView.swift @@ -210,14 +210,13 @@ extension StatusAuthorView { // author container: H - [ avatarButton | authorMetaContainer ] private func layoutBase() { // avatarButton - let authorAvatarButtonSize = CGSize(width: 46, height: 46) - avatarButton.size = authorAvatarButtonSize - avatarButton.avatarImageView.imageViewSize = authorAvatarButtonSize + avatarButton.size = CGSize.authorAvatarButtonSize + avatarButton.avatarImageView.imageViewSize = CGSize.authorAvatarButtonSize avatarButton.translatesAutoresizingMaskIntoConstraints = false addArrangedSubview(avatarButton) NSLayoutConstraint.activate([ - avatarButton.widthAnchor.constraint(equalToConstant: authorAvatarButtonSize.width).priority(.required - 1), - avatarButton.heightAnchor.constraint(equalToConstant: authorAvatarButtonSize.height).priority(.required - 1), + avatarButton.widthAnchor.constraint(equalToConstant: CGSize.authorAvatarButtonSize.width).priority(.required - 1), + avatarButton.heightAnchor.constraint(equalToConstant: CGSize.authorAvatarButtonSize.height).priority(.required - 1), ]) avatarButton.setContentHuggingPriority(.required - 1, for: .vertical) avatarButton.setContentCompressionResistancePriority(.required - 1, for: .vertical) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index efc4afee8..8ab34ce4f 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -14,6 +14,10 @@ import MastodonAsset import MastodonCore import MastodonLocalization +public extension CGSize { + static let authorAvatarButtonSize = CGSize(width: 46, height: 46) +} + public protocol StatusViewDelegate: AnyObject { func statusView(_ statusView: StatusView, headerDidPressed header: UIView) func statusView(_ statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) From 1ce756a84996e76a8ed53eb454e8afab15056e1f Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Thu, 3 Nov 2022 14:59:58 -0400 Subject: [PATCH 067/658] Add accessibility actions for links/mentions/hashtags in posts --- .../TableviewCell/StatusTableViewCell.swift | 4 +++ .../Extension/MetaEntity+Accessibility.swift | 26 +++++++++++++++++++ .../View/Content/StatusView+ViewModel.swift | 20 +++++++++++--- .../MastodonUI/View/Content/StatusView.swift | 7 +++++ 4 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonUI/Extension/MetaEntity+Accessibility.swift diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index a3315211e..c9850a0d3 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -99,6 +99,10 @@ extension StatusTableViewCell { return true } + override var accessibilityCustomActions: [UIAccessibilityCustomAction]? { + get { statusView.accessibilityCustomActions } + set { } + } } // MARK: - AdaptiveContainerMarginTableViewCell diff --git a/MastodonSDK/Sources/MastodonUI/Extension/MetaEntity+Accessibility.swift b/MastodonSDK/Sources/MastodonUI/Extension/MetaEntity+Accessibility.swift new file mode 100644 index 000000000..8c818e7d4 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Extension/MetaEntity+Accessibility.swift @@ -0,0 +1,26 @@ +// +// MetaEntity+Accessibility.swift +// +// +// Created by Jed Fox on 2022-11-03. +// + +import Meta + +extension Meta.Entity { + var accessibilityCustomActionLabel: String? { + switch meta { + case .url(_, trimmed: _, url: let url, userInfo: _): + return "Link: \(url)" + case .hashtag(_, hashtag: let hashtag, userInfo: _): + return "Hashtag \(hashtag)" + case .mention(_, mention: let mention, userInfo: _): + return "Show Profile: @\(mention)" + case .email(let email, userInfo: _): + return "Email address: \(email)" + // emoji are not actionable + case .emoji: + return nil + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index d0b5daa6f..416226cbb 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -315,7 +315,6 @@ extension StatusView.ViewModel { statusView.contentMetaText.configure( content: content ) - statusView.contentMetaText.textView.accessibilityLabel = content.string statusView.contentMetaText.textView.accessibilityTraits = [.staticText] statusView.contentMetaText.textView.accessibilityElementsHidden = false } else { @@ -727,8 +726,23 @@ extension StatusView.ViewModel { statusView.accessibilityLabel = accessibilityLabel } .store(in: &disposeBag) + + Publishers.CombineLatest( + $content, + $isContentReveal.removeDuplicates() + ) + .map { content, isRevealed in + guard isRevealed, let entities = content?.entities else { return [] } + return entities.compactMap { entity in + guard let name = entity.accessibilityCustomActionLabel else { return nil } + return UIAccessibilityCustomAction(name: name) { action in + statusView.delegate?.statusView(statusView, metaText: statusView.contentMetaText, didSelectMeta: entity.meta) + return true + } + } + } + .assign(to: \.accessibilityCustomActions, on: statusView.contentMetaText.textView) + .store(in: &disposeBag) } } - - diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index 8ab34ce4f..563bc7e3d 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -547,6 +547,13 @@ extension StatusView { } +extension StatusView { + public override var accessibilityCustomActions: [UIAccessibilityCustomAction]? { + get { contentMetaText.textView.accessibilityCustomActions } + set { } + } +} + // MARK: - AdaptiveContainerView extension StatusView: AdaptiveContainerView { public func updateContainerViewComponentsLayoutMarginsRelativeArrangementBehavior(isEnabled: Bool) { From e2f505fa67016f7c35527d8e39b9db2386464123 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 4 Nov 2022 12:20:27 +0800 Subject: [PATCH 068/658] feat: add TestFlight deploy workflow --- .github/scripts/build-release.sh | 65 +++++++++++++++++++++++++++++ .github/scripts/setup.sh | 2 +- .github/support/ExportOptions.plist | 8 ++++ .github/workflows/develop-build.yml | 56 +++++++++++++++++++++++++ Podfile | 7 ++++ Podfile.lock | 2 +- 6 files changed, 138 insertions(+), 2 deletions(-) create mode 100755 .github/scripts/build-release.sh create mode 100644 .github/support/ExportOptions.plist create mode 100644 .github/workflows/develop-build.yml diff --git a/.github/scripts/build-release.sh b/.github/scripts/build-release.sh new file mode 100755 index 000000000..fc081428f --- /dev/null +++ b/.github/scripts/build-release.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +set -xeu +set -o pipefail + +function finish() { + ditto -c -k --sequesterRsrc --keepParent "${RESULT_BUNDLE_PATH}" "${RESULT_BUNDLE_PATH}.zip" + rm -rf "${RESULT_BUNDLE_PATH}" +} + +trap finish EXIT + +SDK="${SDK:-iphoneos}" +WORKSPACE="${WORKSPACE:-Mastodon.xcworkspace}" +SCHEME="${SCHEME:-Mastodon}" +CONFIGURATION=${CONFIGURATION:-Release} + +BUILD_DIR=${BUILD_DIR:-.build} +ARTIFACT_PATH=${RESULT_PATH:-${BUILD_DIR}/Artifacts} +RESULT_BUNDLE_PATH="${ARTIFACT_PATH}/${SCHEME}.xcresult" +ARCHIVE_PATH=${ARCHIVE_PATH:-${BUILD_DIR}/Archives/${SCHEME}.xcarchive} +DERIVED_DATA_PATH=${DERIVED_DATA_PATH:-${BUILD_DIR}/DerivedData} +CURRENT_PROJECT_VERSION=${BUILD_NUMBER:-0} +EXPORT_OPTIONS_FILE=".github/support/ExportOptions.plist" + +WORK_DIR=$(pwd) +API_PRIVATE_KEYS_PATH="${WORK_DIR}/${BUILD_DIR}/private_keys" +API_KEY_FILE="${API_PRIVATE_KEYS_PATH}/api_key.p8" + +rm -rf "${RESULT_BUNDLE_PATH}" + +rm -rf "${API_PRIVATE_KEYS_PATH}" +mkdir -p "${API_PRIVATE_KEYS_PATH}" +echo -n "${ENV_API_PRIVATE_KEY}" | base64 --decode > "${API_KEY_FILE}" + +xcrun xcodebuild clean \ + -workspace "${WORKSPACE}" \ + -scheme "${SCHEME}" \ + -configuration "${CONFIGURATION}" + +xcrun xcodebuild archive \ + -workspace "${WORKSPACE}" \ + -scheme "${SCHEME}" \ + -configuration "${CONFIGURATION}" \ + -destination generic/platform=iOS \ + -sdk "${SDK}" \ + -parallelizeTargets \ + -showBuildTimingSummary \ + -derivedDataPath "${DERIVED_DATA_PATH}" \ + -archivePath "${ARCHIVE_PATH}" \ + -resultBundlePath "${RESULT_BUNDLE_PATH}" \ + -allowProvisioningUpdates \ + -authenticationKeyPath "${API_KEY_FILE}" \ + -authenticationKeyID "${ENV_API_KEY_ID}" \ + -authenticationKeyIssuerID "${ENV_ISSUER_ID}" + +xcrun xcodebuild \ + -exportArchive \ + -archivePath "${ARCHIVE_PATH}" \ + -exportOptionsPlist "${EXPORT_OPTIONS_FILE}" \ + -exportPath "${ARTIFACT_PATH}/${SCHEME}.ipa" \ + -allowProvisioningUpdates + +# Zip up the Xcode Archive into Artifacts folder. +ditto -c -k --sequesterRsrc --keepParent "${ARCHIVE_PATH}" "${ARTIFACT_PATH}/${SCHEME}.xcarchive.zip" \ No newline at end of file diff --git a/.github/scripts/setup.sh b/.github/scripts/setup.sh index a630e28cb..69c1dbd54 100755 --- a/.github/scripts/setup.sh +++ b/.github/scripts/setup.sh @@ -1,7 +1,7 @@ #!/bin/bash # workaround https://github.com/CocoaPods/CocoaPods/issues/11355 -sed -i '' $'1s/^/source "https:\\/\\/github.com\\/CocoaPods\\/Specs.git"\\\n\\\n/' Podfile +# sed -i '' $'1s/^/source "https:\\/\\/github.com\\/CocoaPods\\/Specs.git"\\\n\\\n/' Podfile # Install Ruby Bundler gem install bundler:2.3.11 diff --git a/.github/support/ExportOptions.plist b/.github/support/ExportOptions.plist new file mode 100644 index 000000000..50ba270b0 --- /dev/null +++ b/.github/support/ExportOptions.plist @@ -0,0 +1,8 @@ + + + + + method + app-store + + \ No newline at end of file diff --git a/.github/workflows/develop-build.yml b/.github/workflows/develop-build.yml new file mode 100644 index 000000000..0734b00f8 --- /dev/null +++ b/.github/workflows/develop-build.yml @@ -0,0 +1,56 @@ +name: Build for Develop TestFlight + +on: + push: + branches: + - develop + - ci-test + +jobs: + build: + name: Build + runs-on: macOS-12 + steps: + - name: checkout + uses: actions/checkout@v2 + + - name: setup + env: + NotificationEndpointDebug: ${{ secrets.NotificationEndpointDebug }} + NotificationEndpointRelease: ${{ secrets.NotificationEndpointRelease }} + run: exec ./.github/scripts/setup.sh + + - name: Import Code-Signing Certificates + uses: Apple-Actions/import-codesign-certs@v1 # https://github.com/Apple-Actions/import-codesign-certs + with: + keychain: build-p12 + p12-file-base64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + p12-password: ${{ secrets.P12_PASSWORD }} + + - name: Download Provisioning Profiles + uses: Apple-Actions/download-provisioning-profiles@v1 # https://github.com/Apple-Actions/download-provisioning-profiles + with: + bundle-id: org.joinmastodon.app + issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} + api-key-id: ${{ secrets.APPSTORE_KEY_ID }} + api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }} + + - name: build + env: + ENV_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }} + ENV_API_KEY_ID: ${{ secrets.APPSTORE_KEY_ID }} + ENV_API_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_KEY_BASE64 }} + run: exec ./.github/scripts/build-release.sh + + - name: Upload TestFlight Build + uses: Apple-Actions/upload-testflight-build@master + with: + app-path: .build/Artifacts/Mastodon.ipa/Mastodon.ipa + issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} + api-key-id: ${{ secrets.APPSTORE_KEY_ID }} + api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }} + + - name: Clean up keychain and provisioning profile + if: ${{ always() }} + run: | + security delete-keychain build-p12.keychain-db diff --git a/Podfile b/Podfile index 3c482446a..28757d528 100644 --- a/Podfile +++ b/Podfile @@ -1,3 +1,4 @@ +source 'https://cdn.cocoapods.org/' platform :ios, '14.0' target 'Mastodon' do @@ -35,5 +36,11 @@ post_install do |installer| target.build_configurations.each do |config| config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' end + # https://github.com/CocoaPods/CocoaPods/issues/11402#issuecomment-1201464693 + if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle" + target.build_configurations.each do |config| + config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' + end + end end end diff --git a/Podfile.lock b/Podfile.lock index 0cec6626a..12680db21 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -37,6 +37,6 @@ SPEC CHECKSUMS: "UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3 XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5 -PODFILE CHECKSUM: 50ec5b2c4aa189024cc5ab41039f983dc5609040 +PODFILE CHECKSUM: 8b15fb6d4e801b7a7e7761a2e2fe40a89b1da4ff COCOAPODS: 1.11.3 From eb86b5a9d833c82c7c176b891ca85722ed1a9450 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 4 Nov 2022 12:26:38 +0800 Subject: [PATCH 069/658] chore: update to version 1.4.7 --- Mastodon.xcodeproj/project.pbxproj | 46 ------------------- .../xcschemes/xcschememanagement.plist | 4 +- Mastodon/Info.plist | 2 +- MastodonIntent/Info.plist | 2 +- MastodonTests/Info.plist | 2 +- MastodonUITests/Info.plist | 2 +- NotificationService/Info.plist | 2 +- ShareActionExtension/Info.plist | 2 +- 8 files changed, 8 insertions(+), 54 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 68aae2d4e..c4291c3d3 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -3874,7 +3874,6 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4.5; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3904,7 +3903,6 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4.5; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4077,7 +4075,6 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4.5; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4144,7 +4141,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.7; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4167,7 +4163,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.7; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4191,7 +4186,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.7; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.MastodonIntent; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4215,7 +4209,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.7; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.MastodonIntent; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4239,7 +4232,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.7; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.MastodonIntent; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4263,7 +4255,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.7; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4287,7 +4278,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.7; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4374,7 +4364,6 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4.5; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4426,36 +4415,6 @@ }; name = "Release Snapshot"; }; - DBEB19E527E4658E00B0E80E /* Release Snapshot */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3E08A432F40BA7B9CAA9DB68 /* Pods-AppShared.release snapshot.xcconfig */; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 147; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = AppShared/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.AppShared; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = "Release Snapshot"; - }; DBEB19E627E4658E00B0E80E /* Release Snapshot */ = { isa = XCBuildConfiguration; buildSettings = { @@ -4470,7 +4429,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.7; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4493,7 +4451,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.7; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4517,7 +4474,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.7; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.MastodonIntent; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4542,7 +4498,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.7; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4566,7 +4521,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.7; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index de5069e58..979c8c0e6 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -117,12 +117,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 24 + 18 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 25 + 17 SuppressBuildableAutocreation diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index cfdbb923d..ace07bf2b 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.6 + 1.4.7 CFBundleURLTypes diff --git a/MastodonIntent/Info.plist b/MastodonIntent/Info.plist index 3816b660b..c82d6b9a1 100644 --- a/MastodonIntent/Info.plist +++ b/MastodonIntent/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.6 + 1.4.7 CFBundleVersion 147 NSExtension diff --git a/MastodonTests/Info.plist b/MastodonTests/Info.plist index 9f09d489c..a1baa064c 100644 --- a/MastodonTests/Info.plist +++ b/MastodonTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.6 + 1.4.7 CFBundleVersion 147 diff --git a/MastodonUITests/Info.plist b/MastodonUITests/Info.plist index 9f09d489c..a1baa064c 100644 --- a/MastodonUITests/Info.plist +++ b/MastodonUITests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.6 + 1.4.7 CFBundleVersion 147 diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 854a8a529..d8e24838e 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.6 + 1.4.7 CFBundleVersion 147 NSExtension diff --git a/ShareActionExtension/Info.plist b/ShareActionExtension/Info.plist index 4372c5048..73944fe84 100644 --- a/ShareActionExtension/Info.plist +++ b/ShareActionExtension/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.6 + 1.4.7 CFBundleVersion 147 NSExtension From a7cbbc02396372b69ed76b656a64de893943d24e Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 4 Nov 2022 13:28:09 +0800 Subject: [PATCH 070/658] fix: compile failure issue --- Mastodon/Scene/Report/Report/ReportViewModel.swift | 2 +- .../Sources/MastodonCore/Service/API/APIService+Report.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/Report/Report/ReportViewModel.swift b/Mastodon/Scene/Report/Report/ReportViewModel.swift index c368ce42c..05f1cfef8 100644 --- a/Mastodon/Scene/Report/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/Report/ReportViewModel.swift @@ -162,7 +162,7 @@ extension ReportViewModel { #else let _ = try await context.apiService.report( query: query, - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) #endif isReportSuccess = true diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Report.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Report.swift index aa7393070..81a612120 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Report.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Report.swift @@ -11,7 +11,7 @@ import Combine extension APIService { - func report( + public func report( query: Mastodon.API.Reports.FileReportQuery, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { From 99bef412811d31abf8da07ea73820b505212439b Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 4 Nov 2022 13:49:10 +0800 Subject: [PATCH 071/658] fix: connect api key missing for export archive --- .github/scripts/build-release.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/scripts/build-release.sh b/.github/scripts/build-release.sh index fc081428f..63aca3455 100755 --- a/.github/scripts/build-release.sh +++ b/.github/scripts/build-release.sh @@ -59,7 +59,10 @@ xcrun xcodebuild \ -archivePath "${ARCHIVE_PATH}" \ -exportOptionsPlist "${EXPORT_OPTIONS_FILE}" \ -exportPath "${ARTIFACT_PATH}/${SCHEME}.ipa" \ - -allowProvisioningUpdates + -allowProvisioningUpdates \ + -authenticationKeyPath "${API_KEY_FILE}" \ + -authenticationKeyID "${ENV_API_KEY_ID}" \ + -authenticationKeyIssuerID "${ENV_ISSUER_ID}" # Zip up the Xcode Archive into Artifacts folder. ditto -c -k --sequesterRsrc --keepParent "${ARCHIVE_PATH}" "${ARTIFACT_PATH}/${SCHEME}.xcarchive.zip" \ No newline at end of file From 20b54df37cbe0f2325ca2085c0388c9a0c8a4f88 Mon Sep 17 00:00:00 2001 From: Maximilian Szengel Date: Sun, 6 Nov 2022 13:38:11 +0100 Subject: [PATCH 072/658] Improve CONTRIBUTING.md - Fix typos - Try to improve readability --- Documentation/CONTRIBUTING.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Documentation/CONTRIBUTING.md b/Documentation/CONTRIBUTING.md index cc018445d..b07f70520 100644 --- a/Documentation/CONTRIBUTING.md +++ b/Documentation/CONTRIBUTING.md @@ -1,30 +1,30 @@ # Contributing -- File the issue for bug report and feature request +- File an issue to report a bug or feature request - Translate the project in our [Crowdin](https://crowdin.com/project/mastodon-for-ios) project - Make the Pull Request to contribute ## Bug Report -File the issue about the bug. Make sure you are installing the latest version app from TestFlight or App Store. +File an issue about the bug or feature request. Make sure you are installing the latest version of the app from TestFlight or App Store. ## Translation [![Crowdin](https://badges.crowdin.net/mastodon-for-ios/localized.svg)](https://crowdin.com/project/mastodon-for-ios) -The translation will update regularly. Please request language if not listed via issue. +The translation will update regularly. Please request the language if it is not listed via an issue. ## Pull Request -You can make a pull request directly with small block code changes for bugfix or feature implementations. Before making a pull request with hundred lines of changes to this repository, please first discuss the change you wish to make via issue. +You can create a pull request directly with small block code changes for bugfix or feature implementations. Before making a pull request with hundred lines of changes to this repository, please first discuss the change you wish to make via an issue. Also, there are lots of existing feature request issues that could be a good-first-issue discussing place. Follow the git-flow pattern to make your pull request. -1. Ensure you are checkout on the `develop` branch. -2. Write your codes and test them on **iPad and iPhone**. -3. Merge the `develop` into your branch then make a Pull Request. Please merge the branch and resolve any conflicts when the `develop` updates. **Do not force push your codes.** -4. Make sure the permission for your folk is open to the reviewer. Code style fix, conflict resolution, and other changes may be committed by the reviewer directly. +1. Ensure you have started a new branch based on the `develop` branch. +2. Write your changes and test them on **iPad and iPhone**. +3. Merge the `develop` branch into your branch then make a Pull Request. Please merge the branch and resolve any conflicts if `develop` updates. **Do not force push your commits.** +4. Make sure the permission for your fork is open to the reviewer. Code style fix, conflict resolution, and other changes may be committed by the reviewer directly. 5. Request a code review and wait for approval. The PR will be merged when it is approved. ## Documentation -The documents for this app is list under the [Documentation](../Documentation/) folder. We are also welcome contributions for documentation. \ No newline at end of file +The documentation for this app is listed under the [Documentation](../Documentation/) folder. We are also welcoming contributions for documentation. From c8ae76af4b14daca29630ddf31f90c78323d7074 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sun, 6 Nov 2022 10:56:23 -0500 Subject: [PATCH 073/658] Mark sidebar cells as buttons --- Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index 9f0eb1899..7921daa05 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -141,6 +141,7 @@ extension SidebarViewModel { cell.setNeedsUpdateConfiguration() cell.isAccessibilityElement = true cell.accessibilityLabel = item.title + cell.accessibilityTraits.insert(.button) } // header From fe98dfe4cae898558e5cc007457333170b09c69a Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sun, 6 Nov 2022 20:01:30 -0500 Subject: [PATCH 074/658] DragIndicatorView now handles a11y itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …and also gains the button trait …also the escape gesture now works! --- .../Scene/Account/AccountViewController.swift | 17 ++++++----------- .../Scene/Account/View/DragIndicatorView.swift | 18 +++++++++++++----- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/Mastodon/Scene/Account/AccountViewController.swift b/Mastodon/Scene/Account/AccountViewController.swift index e25c75b01..7a0e529cc 100644 --- a/Mastodon/Scene/Account/AccountViewController.swift +++ b/Mastodon/Scene/Account/AccountViewController.swift @@ -34,7 +34,9 @@ final class AccountListViewController: UIViewController, NeedsDependency { return barButtonItem }() - let dragIndicatorView = DragIndicatorView() + lazy var dragIndicatorView = DragIndicatorView { [weak self] in + self?.dismiss(animated: true, completion: nil) + } var hasLoaded = false private(set) lazy var tableView: UITableView = { @@ -130,14 +132,6 @@ extension AccountListViewController { self.panModalTransition(to: .shortForm) } .store(in: &disposeBag) - - if UIAccessibility.isVoiceOverRunning { - let dragIndicatorTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer - dragIndicatorView.addGestureRecognizer(dragIndicatorTapGestureRecognizer) - dragIndicatorTapGestureRecognizer.addTarget(self, action: #selector(AccountListViewController.dragIndicatorTapGestureRecognizerHandler(_:))) - dragIndicatorView.isAccessibilityElement = true - dragIndicatorView.accessibilityLabel = L10n.Scene.AccountList.dismissAccountSwitcher - } } private func setupBackgroundColor(theme: Theme) { @@ -160,10 +154,11 @@ extension AccountListViewController { logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") _ = coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil)) } - - @objc private func dragIndicatorTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { + + override func accessibilityPerformEscape() -> Bool { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") dismiss(animated: true, completion: nil) + return true } } diff --git a/Mastodon/Scene/Account/View/DragIndicatorView.swift b/Mastodon/Scene/Account/View/DragIndicatorView.swift index 9e0ab77d5..a04d9cd8c 100644 --- a/Mastodon/Scene/Account/View/DragIndicatorView.swift +++ b/Mastodon/Scene/Account/View/DragIndicatorView.swift @@ -15,17 +15,17 @@ final class DragIndicatorView: UIView { let barView = UIView() let separatorLine = UIView.separatorLine + let onDismiss: () -> Void - override init(frame: CGRect) { - super.init(frame: frame) + init(onDismiss: @escaping () -> Void) { + self.onDismiss = onDismiss + super.init(frame: .zero) _init() } required init?(coder: NSCoder) { - super.init(coder: coder) - _init() + fatalError("init(coder:) is not supported") } - } extension DragIndicatorView { @@ -52,6 +52,14 @@ extension DragIndicatorView { separatorLine.bottomAnchor.constraint(equalTo: bottomAnchor), separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)), ]) + + isAccessibilityElement = true + accessibilityTraits = .button + accessibilityLabel = L10n.Scene.AccountList.dismissAccountSwitcher } + override func accessibilityActivate() -> Bool { + self.onDismiss() + return true + } } From 547129ec95a04cf1c5d75ba1bdfab594a8d22208 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sun, 6 Nov 2022 20:09:55 -0500 Subject: [PATCH 075/658] Hide the avatars in the account list from VO --- Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift index 66f49efe8..f0673293a 100644 --- a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift +++ b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift @@ -69,6 +69,7 @@ extension AccountListTableViewCell { ]) avatarButton.setContentHuggingPriority(.defaultLow, for: .horizontal) avatarButton.setContentHuggingPriority(.defaultLow, for: .vertical) + avatarButton.isAccessibilityElement = false let labelContainerStackView = UIStackView() labelContainerStackView.axis = .vertical From 6c6508cdfb9920fbfeca1ec84216b12421d62795 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sun, 6 Nov 2022 20:10:10 -0500 Subject: [PATCH 076/658] Mark account list rows as buttons --- Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift | 2 ++ Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift index f0673293a..cd214f2c7 100644 --- a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift +++ b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift @@ -125,6 +125,8 @@ extension AccountListTableViewCell { badgeButton.setBadge(number: 0) checkmarkImageView.isHidden = true + + accessibilityTraits.insert(.button) } } diff --git a/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift b/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift index 3ff3066a2..bc47aef43 100644 --- a/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift +++ b/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift @@ -108,6 +108,8 @@ extension AddAccountTableViewCell { separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), ]) + + accessibilityTraits.insert(.button) } } From c3d7357456af98a54e642042bb8d7da1be96db00 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sun, 6 Nov 2022 20:14:52 -0500 Subject: [PATCH 077/658] Try to fix strings for a11y.plural.count.unread.notification --- Localization/Localizable.stringsdict | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Localization/Localizable.stringsdict b/Localization/Localizable.stringsdict index 051bb50ef..93d566f70 100644 --- a/Localization/Localizable.stringsdict +++ b/Localization/Localizable.stringsdict @@ -13,15 +13,15 @@ NSStringFormatValueTypeKey ld zero - no unread notification + no unread notifications one 1 unread notification few %ld unread notifications many - %ld unread notification + %ld unread notifications other - %ld unread notification + %ld unread notifications a11y.plural.count.input_limit_exceeds From fd31e08089141493094ae7f00790fc144a086f5f Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sun, 6 Nov 2022 20:15:09 -0500 Subject: [PATCH 078/658] Clarify separation between name/username/badge --- Mastodon/Scene/Account/AccountListViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/Account/AccountListViewModel.swift b/Mastodon/Scene/Account/AccountListViewModel.swift index e0aaf97fc..75797fd47 100644 --- a/Mastodon/Scene/Account/AccountListViewModel.swift +++ b/Mastodon/Scene/Account/AccountListViewModel.swift @@ -166,7 +166,7 @@ extension AccountListViewModel { cell.badgeButton.accessibilityLabel ] .compactMap { $0 } - .joined(separator: " ") + .joined(separator: ", ") } } From d489943b45991025a6e3f4b78613f629294e0493 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sun, 6 Nov 2022 20:22:06 -0500 Subject: [PATCH 079/658] 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 080/658] 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 081/658] 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 082/658] 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 083/658] 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 084/658] 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 f9daeea4d3ad66a5387493b67682c4e27396d2bf Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 7 Nov 2022 13:32:15 -0500 Subject: [PATCH 085/658] =?UTF-8?q?Add=20a=20custom=20action=20for=20?= =?UTF-8?q?=E2=80=9Cswitch=20accounts=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Localization/app.json | 6 ++-- .../Root/ContentSplitViewController.swift | 1 + .../Root/MainTab/MainTabBarController.swift | 30 ++++++++++++------- .../Root/Sidebar/SidebarViewController.swift | 13 ++++++++ .../Scene/Root/Sidebar/SidebarViewModel.swift | 21 +++++++++++-- 5 files changed, 55 insertions(+), 16 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index a965b23ae..47d0e63a5 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -676,9 +676,9 @@ } }, "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" + "add_account": "Add Account", + "switch_accounts": "Switch Accounts" }, "wizard": { "new_in_mastodon": "New in Mastodon", @@ -686,4 +686,4 @@ "accessibility_hint": "Double tap to dismiss this wizard" } } -} \ No newline at end of file +} diff --git a/Mastodon/Scene/Root/ContentSplitViewController.swift b/Mastodon/Scene/Root/ContentSplitViewController.swift index 3f4758e8e..4ae7204ca 100644 --- a/Mastodon/Scene/Root/ContentSplitViewController.swift +++ b/Mastodon/Scene/Root/ContentSplitViewController.swift @@ -33,6 +33,7 @@ final class ContentSplitViewController: UIViewController, NeedsDependency { sidebarViewController.context = context sidebarViewController.coordinator = coordinator sidebarViewController.viewModel = SidebarViewModel(context: context, authContext: authContext) + sidebarViewController.viewModel.delegate = sidebarViewController sidebarViewController.delegate = self return sidebarViewController }() diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index c49dcc1a1..d56a18aed 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -298,6 +298,7 @@ extension MainTabBarController { } .store(in: &disposeBag) + let profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag } if let user = authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user { self.avatarURLObserver = user.publisher(for: \.avatar) .sink { [weak self, weak user] _ in @@ -306,15 +307,20 @@ extension MainTabBarController { guard user.managedObjectContext != nil else { return } self.avatarURL = user.avatarImageURL() } - - // a11y - let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag } - guard let profileTabItem = _profileTabItem else { return } - let currentUserDisplayName = user.displayNameWithFallback ?? "no user" - profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName) - + if let profileTabItem { + profileTabItem.accessibilityCustomActions = [ + // TODO: i18n (scene.account_list.switch_accounts) + UIAccessibilityCustomAction(name: "Switch Accounts") { [weak self] _ in + self?.showAccountSwitcher() + return true + } + ] + } } else { self.avatarURLObserver = nil + if let profileTabItem { + profileTabItem.accessibilityCustomActions = [] + } } let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer() @@ -390,13 +396,17 @@ extension MainTabBarController { switch tab { case .me: - guard let authContext = self.authContext else { return } - let accountListViewModel = AccountListViewModel(context: context, authContext: authContext) - _ = coordinator.present(scene: .accountList(viewModel: accountListViewModel), from: self, transition: .panModal) + showAccountSwitcher() default: break } } + + private func showAccountSwitcher() { + guard let authContext = self.authContext else { return } + let accountListViewModel = AccountListViewModel(context: context, authContext: authContext) + _ = coordinator.present(scene: .accountList(viewModel: accountListViewModel), from: self, transition: .panModal) + } } extension MainTabBarController { diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift index 70e1239b6..28e205047 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift @@ -162,6 +162,19 @@ extension SidebarViewController { } +extension SidebarViewController: SidebarViewModelDelegate { + func sidebarViewModelDidSwitchAccounts(_ sidebarViewModel: SidebarViewModel) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let indexPath = diffableDataSource.indexPath(for: .tab(.me)) else { return } + guard let cell = collectionView.cellForItem(at: indexPath) else { return } + delegate?.sidebarViewController( + self, + didLongPressItem: .tab(.me), + sourceView: cell + ) + } +} + extension SidebarViewController { @objc private func sidebarLongPressGestureRecognizerHandler(_ sender: UILongPressGestureRecognizer) { guard sender.state == .began else { return } diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index 9f0eb1899..22be9e6e0 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -15,6 +15,10 @@ import MastodonAsset import MastodonCore import MastodonLocalization +protocol SidebarViewModelDelegate: AnyObject { + func sidebarViewModelDidSwitchAccounts(_ sidebarViewModel: SidebarViewModel) +} + final class SidebarViewModel { var disposeBag = Set() @@ -31,6 +35,8 @@ final class SidebarViewModel { var secondaryDiffableDataSource: UICollectionViewDiffableDataSource? @Published private(set) var isReadyForWizardAvatarButton = false + weak var delegate: SidebarViewModelDelegate? + init(context: AppContext, authContext: AuthContext?) { self.context = context self.authContext = authContext @@ -127,9 +133,18 @@ extension SidebarViewModel { } .store(in: &cell.disposeBag) case .me: - guard let user = self.authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return } - let currentUserDisplayName = user.displayNameWithFallback - cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName) + if self.authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user != nil { + cell.accessibilityCustomActions = [ + // TODO: i18n (scene.account_list.switch_accounts) + UIAccessibilityCustomAction(name: "Switch Accounts") { [weak self] _ in + guard let self, let delegate = self.delegate else { return false } + delegate.sidebarViewModelDidSwitchAccounts(self) + return true + } + ] + } else { + cell.accessibilityCustomActions = [] + } default: break } From aa87340345080e3d5b004158a804473e36a34ee1 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 7 Nov 2022 18:21:19 -0500 Subject: [PATCH 086/658] set up translations for link labels --- Localization/app.json | 10 +++++++++- .../Extension/MetaEntity+Accessibility.swift | 6 +++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index a965b23ae..707faf3ef 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -685,5 +685,13 @@ "multiple_account_switch_intro_description": "Switch between multiple accounts by holding the profile button.", "accessibility_hint": "Double tap to dismiss this wizard" } + }, + "a11y": { + "meta_entity": { + "url": "Link: %s", + "hashtag": "Hastag %s", + "mention": "Show Profile: %s", + "email": "Email address: %s", + } } -} \ No newline at end of file +} diff --git a/MastodonSDK/Sources/MastodonUI/Extension/MetaEntity+Accessibility.swift b/MastodonSDK/Sources/MastodonUI/Extension/MetaEntity+Accessibility.swift index 8c818e7d4..9319617bb 100644 --- a/MastodonSDK/Sources/MastodonUI/Extension/MetaEntity+Accessibility.swift +++ b/MastodonSDK/Sources/MastodonUI/Extension/MetaEntity+Accessibility.swift @@ -11,12 +11,16 @@ extension Meta.Entity { var accessibilityCustomActionLabel: String? { switch meta { case .url(_, trimmed: _, url: let url, userInfo: _): + // TODO: i18n (a11y.meta_entity.url) return "Link: \(url)" case .hashtag(_, hashtag: let hashtag, userInfo: _): + // TODO: i18n (a11y.meta_entity.hashtag) return "Hashtag \(hashtag)" case .mention(_, mention: let mention, userInfo: _): - return "Show Profile: @\(mention)" + // TODO: i18n (a11y.meta_entity.mention) + return "Show Profile: \("@" + mention)" case .email(let email, userInfo: _): + // TODO: i18n (a11y.meta_entity.email) return "Email address: \(email)" // emoji are not actionable case .emoji: From 78ce26b8894601831f1a44dd01270b127253edb9 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 8 Nov 2022 14:17:43 +0800 Subject: [PATCH 087/658] fix: manually bump the version to workaround auto version management not working issue --- .github/scripts/build-release.sh | 7 +++++-- .github/support/ExportOptions.plist | 2 ++ .github/workflows/develop-build.yml | 12 +++++++++++- Mastodon.xcodeproj/project.pbxproj | 24 ++++++++---------------- Mastodon/Info.plist | 4 ++-- MastodonIntent/Info.plist | 4 ++-- MastodonTests/Info.plist | 4 ++-- MastodonUITests/Info.plist | 4 ++-- NotificationService/Info.plist | 4 ++-- ShareActionExtension/Info.plist | 12 ++++++------ 10 files changed, 42 insertions(+), 35 deletions(-) diff --git a/.github/scripts/build-release.sh b/.github/scripts/build-release.sh index 63aca3455..5ca5569f6 100755 --- a/.github/scripts/build-release.sh +++ b/.github/scripts/build-release.sh @@ -20,7 +20,6 @@ ARTIFACT_PATH=${RESULT_PATH:-${BUILD_DIR}/Artifacts} RESULT_BUNDLE_PATH="${ARTIFACT_PATH}/${SCHEME}.xcresult" ARCHIVE_PATH=${ARCHIVE_PATH:-${BUILD_DIR}/Archives/${SCHEME}.xcarchive} DERIVED_DATA_PATH=${DERIVED_DATA_PATH:-${BUILD_DIR}/DerivedData} -CURRENT_PROJECT_VERSION=${BUILD_NUMBER:-0} EXPORT_OPTIONS_FILE=".github/support/ExportOptions.plist" WORK_DIR=$(pwd) @@ -31,7 +30,11 @@ rm -rf "${RESULT_BUNDLE_PATH}" rm -rf "${API_PRIVATE_KEYS_PATH}" mkdir -p "${API_PRIVATE_KEYS_PATH}" -echo -n "${ENV_API_PRIVATE_KEY}" | base64 --decode > "${API_KEY_FILE}" +echo -n "${ENV_API_PRIVATE_KEY_BASE64}" | base64 --decode > "${API_KEY_FILE}" + +BUILD_NUMBER=$(app-store-connect get-latest-testflight-build-number $ENV_APP_ID --issuer-id $ENV_ISSUER_ID --key-id $ENV_API_KEY_ID --private-key @file:$API_KEY_FILE) +BUILD_NUMBER=$((BUILD_NUMBER+1)) +CURRENT_PROJECT_VERSION=${BUILD_NUMBER:-0} xcrun xcodebuild clean \ -workspace "${WORKSPACE}" \ diff --git a/.github/support/ExportOptions.plist b/.github/support/ExportOptions.plist index 50ba270b0..15d77c344 100644 --- a/.github/support/ExportOptions.plist +++ b/.github/support/ExportOptions.plist @@ -4,5 +4,7 @@ method app-store + manageAppVersionAndBuildNumber + \ No newline at end of file diff --git a/.github/workflows/develop-build.yml b/.github/workflows/develop-build.yml index 0734b00f8..c37f00731 100644 --- a/.github/workflows/develop-build.yml +++ b/.github/workflows/develop-build.yml @@ -20,6 +20,14 @@ jobs: NotificationEndpointRelease: ${{ secrets.NotificationEndpointRelease }} run: exec ./.github/scripts/setup.sh + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - run: | + pip3 install codemagic-cli-tools + - run: | + codemagic-cli-tools --version || true + - name: Import Code-Signing Certificates uses: Apple-Actions/import-codesign-certs@v1 # https://github.com/Apple-Actions/import-codesign-certs with: @@ -37,9 +45,11 @@ jobs: - name: build env: + ENV_APP_ID: ${{ secrets.APP_ID }} ENV_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }} ENV_API_KEY_ID: ${{ secrets.APPSTORE_KEY_ID }} - ENV_API_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_KEY_BASE64 }} + ENV_API_PRIVATE_KEY: ${{ secrets.APPSTORE_PRIVATE_KEY }} + ENV_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_KEY_BASE64 }} run: exec ./.github/scripts/build-release.sh - name: Upload TestFlight Build diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index c4291c3d3..380f21eac 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -3771,6 +3771,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -3834,6 +3835,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -3866,7 +3868,6 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3874,6 +3875,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1.4.7; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3895,7 +3897,6 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3903,6 +3904,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1.4.7; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4030,6 +4032,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -4067,7 +4070,6 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4075,6 +4077,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1.4.7; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4133,7 +4136,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4155,7 +4157,6 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4178,7 +4179,6 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4201,7 +4201,6 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4224,7 +4223,6 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4247,7 +4245,6 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4270,7 +4267,6 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4323,6 +4319,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -4356,7 +4353,6 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4364,6 +4360,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1.4.7; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4421,7 +4418,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4443,7 +4439,6 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4466,7 +4461,6 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4490,7 +4484,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4513,7 +4506,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index ace07bf2b..df221e66d 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.7 + $(MARKETING_VERSION) CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 147 + $(CURRENT_PROJECT_VERSION) ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/MastodonIntent/Info.plist b/MastodonIntent/Info.plist index c82d6b9a1..3c4a6e453 100644 --- a/MastodonIntent/Info.plist +++ b/MastodonIntent/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.7 + $(MARKETING_VERSION) CFBundleVersion - 147 + $(CURRENT_PROJECT_VERSION) NSExtension NSExtensionAttributes diff --git a/MastodonTests/Info.plist b/MastodonTests/Info.plist index a1baa064c..c0701c6d7 100644 --- a/MastodonTests/Info.plist +++ b/MastodonTests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.7 + $(MARKETING_VERSION) CFBundleVersion - 147 + $(CURRENT_PROJECT_VERSION) diff --git a/MastodonUITests/Info.plist b/MastodonUITests/Info.plist index a1baa064c..c0701c6d7 100644 --- a/MastodonUITests/Info.plist +++ b/MastodonUITests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.7 + $(MARKETING_VERSION) CFBundleVersion - 147 + $(CURRENT_PROJECT_VERSION) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index d8e24838e..e28de53ce 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.7 + $(MARKETING_VERSION) CFBundleVersion - 147 + $(CURRENT_PROJECT_VERSION) NSExtension NSExtensionPointIdentifier diff --git a/ShareActionExtension/Info.plist b/ShareActionExtension/Info.plist index 73944fe84..52924beed 100644 --- a/ShareActionExtension/Info.plist +++ b/ShareActionExtension/Info.plist @@ -17,23 +17,23 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.7 + $(MARKETING_VERSION) CFBundleVersion - 147 + $(CURRENT_PROJECT_VERSION) NSExtension NSExtensionAttributes NSExtensionActivationRule - NSExtensionActivationSupportsText - - NSExtensionActivationSupportsWebURLWithMaxCount - 1 NSExtensionActivationSupportsImageWithMaxCount 4 NSExtensionActivationSupportsMovieWithMaxCount 1 + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 NSExtensionMainStoryboard From c317f97dc4612878fc6d6a63f13973eacdc0b9cb Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 8 Nov 2022 14:37:28 +0800 Subject: [PATCH 088/658] fix: CURRENT_PROJECT_VERSION not update issue --- .github/scripts/build-release.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/scripts/build-release.sh b/.github/scripts/build-release.sh index 5ca5569f6..e3efc59df 100755 --- a/.github/scripts/build-release.sh +++ b/.github/scripts/build-release.sh @@ -36,6 +36,8 @@ BUILD_NUMBER=$(app-store-connect get-latest-testflight-build-number $ENV_APP_ID BUILD_NUMBER=$((BUILD_NUMBER+1)) CURRENT_PROJECT_VERSION=${BUILD_NUMBER:-0} +agvtool new-version -all $CURRENT_PROJECT_VERSION + xcrun xcodebuild clean \ -workspace "${WORKSPACE}" \ -scheme "${SCHEME}" \ From fc3750c377d3f1db91fdc01b0526d1f30da02b36 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 8 Nov 2022 16:39:19 +0800 Subject: [PATCH 089/658] 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 090/658] 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 d96f189980917992d9b832d5dd9251893792dc04 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 8 Nov 2022 10:28:49 -0500 Subject: [PATCH 091/658] =?UTF-8?q?Consistently=20handle=20=E2=80=9CA11y?= =?UTF-8?q?=E2=80=9D=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StringsConvertor/Sources/StringsConvertor/Parser.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/Sources/StringsConvertor/Parser.swift b/Localization/StringsConvertor/Sources/StringsConvertor/Parser.swift index ba9750cd4..c355493fe 100644 --- a/Localization/StringsConvertor/Sources/StringsConvertor/Parser.swift +++ b/Localization/StringsConvertor/Sources/StringsConvertor/Parser.swift @@ -43,7 +43,12 @@ extension Parser { .map { switch keyStyle { case .infoPlist: return $0 - case .swiftgen: return $0.capitalized + case .swiftgen: + if $0 == "a11y" { + return "A11y" + } else { + return $0.capitalized + } } } .joined() From ff65f50689c1ed5637d487da09d72b572214f3a2 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 3 Nov 2022 17:17:02 +0100 Subject: [PATCH 092/658] Add menu-entry for show/hide reblogs (#365) translations are still missing, as well as viewModel/networking-stuff. --- .../Provider/DataSourceFacade+Status.swift | 3 +++ .../Scene/Profile/ProfileViewController.swift | 1 + .../Generated/Strings.swift | 4 ++++ .../Resources/en.lproj/Localizable.strings | 2 ++ .../MastodonUI/View/Menu/MastodonMenu.swift | 21 ++++++++++++++++++- .../ViewModel/RelationshipViewModel.swift | 1 + 6 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 9d9e73eb3..412079293 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -205,6 +205,9 @@ extension DataSourceFacade { menuContext: MenuContext ) async throws { switch action { + case .hideReblogs(_): + //TODO: Implement. Alert. Toggle on Server. + return case .muteUser(let actionContext): let alertController = UIAlertController( title: actionContext.isMuting ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title : L10n.Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.title, diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 9dd06b22c..34c2775c8 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -378,6 +378,7 @@ extension ProfileViewController { let _ = ManagedObjectRecord(objectID: user.objectID) let menu = MastodonMenu.setupMenu( actions: [ + .hideReblogs(.init(showReblogs: self.viewModel.relationshipViewModel.showReblogs)), .muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)), .blockUser(.init(name: name, isBlocking: self.viewModel.relationshipViewModel.isBlocking)), .reportUser(.init(name: name)), diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index c64a50fa2..1d5b9895b 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -198,6 +198,8 @@ public enum L10n { public static let follow = L10n.tr("Localizable", "Common.Controls.Friendship.Follow") /// Following public static let following = L10n.tr("Localizable", "Common.Controls.Friendship.Following") + /// Hide Reblogs + public static let hideReblogs = L10n.tr("Localizable", "Common.Controls.Friendship.HideReblogs") /// Mute public static let mute = L10n.tr("Localizable", "Common.Controls.Friendship.Mute") /// Muted @@ -210,6 +212,8 @@ public enum L10n { public static let pending = L10n.tr("Localizable", "Common.Controls.Friendship.Pending") /// Request public static let request = L10n.tr("Localizable", "Common.Controls.Friendship.Request") + /// Show Reblogs + public static let showReblogs = L10n.tr("Localizable", "Common.Controls.Friendship.ShowReblogs") /// Unblock public static let unblock = L10n.tr("Localizable", "Common.Controls.Friendship.Unblock") /// Unblock %@ diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings index 1c40bf855..94bee697c 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings @@ -77,6 +77,8 @@ Please check your internet connection."; "Common.Controls.Friendship.UnblockUser" = "Unblock %@"; "Common.Controls.Friendship.Unmute" = "Unmute"; "Common.Controls.Friendship.UnmuteUser" = "Unmute %@"; +"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; +"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; "Common.Controls.Keyboard.Common.ComposeNewPost" = "Compose New Post"; "Common.Controls.Keyboard.Common.OpenSettings" = "Open Settings"; "Common.Controls.Keyboard.Common.ShowFavorites" = "Show Favorites"; diff --git a/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift b/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift index d6a5bdbac..b85e71138 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift @@ -45,11 +45,23 @@ extension MastodonMenu { case reportUser(ReportUserActionContext) case shareUser(ShareUserActionContext) case bookmarkStatus(BookmarkStatusActionContext) + case hideReblogs(HideReblogsActionContext) case shareStatus case deleteStatus func build(delegate: MastodonMenuDelegate) -> BuiltAction { switch self { + case .hideReblogs(let context): + let title = context.showReblogs ? L10n.Common.Controls.Friendship.hideReblogs : L10n.Common.Controls.Friendship.hideReblogs + let reblogAction = BuiltAction( + title: title, + image: UIImage(systemName: "arrow.2.squarepath") + ) { [weak delegate] in + guard let delegate = delegate else { return } + delegate.menuAction(self) + } + + return reblogAction case .muteUser(let context): let muteAction = BuiltAction( title: context.isMuting ? L10n.Common.Controls.Friendship.unmuteUser(context.name) : L10n.Common.Controls.Friendship.muteUser(context.name), @@ -205,5 +217,12 @@ extension MastodonMenu { self.name = name } } - + + public struct HideReblogsActionContext { + public let showReblogs: Bool + + public init(showReblogs: Bool) { + self.showReblogs = showReblogs + } + } } diff --git a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift index a19de5138..5b032aa90 100644 --- a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift @@ -114,6 +114,7 @@ public final class RelationshipViewModel { @Published public var isFollowing = false @Published public var isFollowingBy = false @Published public var isMuting = false + @Published public var showReblogs = false @Published public var isBlocking = false @Published public var isBlockingBy = false @Published public var isSuspended = false From 13b849449450bb38d936e5505c7caa4ba5e05c17 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 3 Nov 2022 17:17:28 +0100 Subject: [PATCH 093/658] Consider old-school intel macs for development --- Gemfile.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile.lock b/Gemfile.lock index 873f725e5..e0ed91c5b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,6 +100,7 @@ GEM PLATFORMS arm64-darwin-21 + x86_64-darwin-21 DEPENDENCIES arkana From 8f8ae7d6a25e059a43d978df7fceb0bee2568c4f Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 3 Nov 2022 17:18:03 +0100 Subject: [PATCH 094/658] Have xcode update dependencies --- .../xcshareddata/swiftpm/Package.resolved | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 34ffd227b..f15e4a422 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Alamofire/Alamofire.git", "state" : { - "revision" : "354dda32d89fc8cd4f5c46487f64957d355f53d8", - "version" : "5.6.1" + "revision" : "8dd85aee02e39dd280c75eef88ffdb86eed4b07b", + "version" : "5.6.2" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Flipboard/FLAnimatedImage.git", "state" : { - "revision" : "e7f9fd4681ae41bf6f3056db08af4f401d61da52", - "version" : "1.0.16" + "revision" : "d4f07b6f164d53c1212c3e54d6460738b1981e9f", + "version" : "1.0.17" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/kean/Nuke.git", "state" : { - "revision" : "0ea7545b5c918285aacc044dc75048625c8257cc", - "version" : "10.8.0" + "revision" : "a002b7fd786f2df2ed4333fe73a9727499fd9d97", + "version" : "10.11.2" } }, { @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/uias/Pageboy", "state" : { - "revision" : "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6", - "version" : "3.6.2" + "revision" : "af8fa81788b893205e1ff42ddd88c5b0b315d7c5", + "version" : "3.7.0" } }, { @@ -131,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SDWebImage/SDWebImage.git", "state" : { - "revision" : "2e63d0061da449ad0ed130768d05dceb1496de44", - "version" : "5.12.5" + "revision" : "318cca556b0489aede0cd98d8d0c7f1408ab7bd6", + "version" : "5.13.5" } }, { @@ -176,8 +176,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/scinfu/SwiftSoup.git", "state" : { - "revision" : "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886", - "version" : "2.4.2" + "revision" : "6778575285177365cbad3e5b8a72f2a20583cfec", + "version" : "2.4.3" } }, { From ce0e56b84e3c140074c6bd6c7c7fbc5fd2aa7885 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 4 Nov 2022 16:25:11 +0100 Subject: [PATCH 095/658] Add showsReblog to CoreData/persistence (#365) --- .../CoreData.xcdatamodeld/.xccurrentversion | 2 +- .../CoreData 4.xcdatamodel/contents | 254 ++++++++++++++++++ .../Entity/Mastodon/MastodonUser.swift | 15 +- .../Persistence+MastodonUser.swift | 1 + 4 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 4.xcdatamodel/contents diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion index cdd244c9c..1d5ea989f 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion +++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - CoreData 3.xcdatamodel + CoreData 4.xcdatamodel diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 4.xcdatamodel/contents b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 4.xcdatamodel/contents new file mode 100644 index 000000000..7604028f1 --- /dev/null +++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 4.xcdatamodel/contents @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift index 85e844b09..760985d68 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift @@ -89,7 +89,8 @@ final public class MastodonUser: NSManagedObject { @NSManaged public private(set) var endorsedBy: Set @NSManaged public private(set) var domainBlocking: Set @NSManaged public private(set) var domainBlockingBy: Set - + @NSManaged public private(set) var showingReblogs: Set + @NSManaged public private(set) var showingReblogsBy: Set } extension MastodonUser { @@ -521,6 +522,7 @@ extension MastodonUser: AutoUpdatableObject { } } } + public func update(isDomainBlocking: Bool, by mastodonUser: MastodonUser) { if isDomainBlocking { if !self.domainBlockingBy.contains(mastodonUser) { @@ -533,4 +535,15 @@ extension MastodonUser: AutoUpdatableObject { } } + public func update(isShowingReblogs: Bool, by mastodonUser: MastodonUser) { + if isShowingReblogs { + if !self.showingReblogsBy.contains(mastodonUser) { + self.mutableSetValue(forKey: #keyPath(MastodonUser.showingReblogsBy)).add(mastodonUser) + } + } else { + if self.showingReblogsBy.contains(mastodonUser) { + self.mutableSetValue(forKey: #keyPath(MastodonUser.showingReblogsBy)).remove(mastodonUser) + } + } + } } diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift index eb69c36d1..8571a11cf 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift @@ -157,5 +157,6 @@ extension Persistence.MastodonUser { user.update(isBlocking: relationship.blocking, by: me) relationship.domainBlocking.flatMap { user.update(isDomainBlocking: $0, by: me) } relationship.blockedBy.flatMap { me.update(isBlocking: $0, by: user) } + relationship.showingReblogs.flatMap { me.update(isShowingReblogs: $0, by: user) } } } From b719d84d3fdd0e29c28f14318177c95d2cbdf883 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 4 Nov 2022 16:28:14 +0100 Subject: [PATCH 096/658] [WIP] Toggle showReblogs-status on mastodon-server --- .../Provider/DataSourceFacade+Follow.swift | 11 ++++ .../Provider/DataSourceFacade+Status.swift | 52 +++++++++++++++---- .../Scene/Profile/ProfileViewController.swift | 6 ++- .../Service/API/APIService+Follow.swift | 52 ++++++++++++++++++- 4 files changed, 108 insertions(+), 13 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index c6e40e7d9..b3812f198 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -132,3 +132,14 @@ extension DataSourceFacade { } } // end func } + +extension DataSourceFacade { + static func responseToShowHideReblogAction( + dependency: NeedsDependency & AuthContextProvider, + user: ManagedObjectRecord + ) async throws { + _ = try await dependency.context.apiService.toggleShowReblogs( + for: user, + authenticationBox: dependency.authContext.mastodonAuthenticationBox) + } +} diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 412079293..ff41cc126 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -205,9 +205,42 @@ extension DataSourceFacade { menuContext: MenuContext ) async throws { switch action { - case .hideReblogs(_): - //TODO: Implement. Alert. Toggle on Server. - return + case .hideReblogs(let actionContext): + //FIXME: Add localized strings + let alertController = UIAlertController( + title: actionContext.showReblogs ? "Really hide?" : "Really show?", + message: actionContext.showReblogs ? "Really??" : "Really??", + preferredStyle: .alert + ) + + let showHideReblogsAction = UIAlertAction( + title: actionContext.showReblogs ? "Show" : "Hide", + style: .default + ) { [weak dependency] _ in + guard let dependency else { return } + + Task { + let managedObjectContext = dependency.context.managedObjectContext + let _user: ManagedObjectRecord? = try? await managedObjectContext.perform { + guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil } + return ManagedObjectRecord(objectID: user.objectID) + } + + guard let user = _user else { return } + + try await DataSourceFacade.responseToShowHideReblogAction( + dependency: dependency, + user: user + ) + } + } + + alertController.addAction(showHideReblogsAction) + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) + alertController.addAction(cancelAction) + + dependency.present(alertController, animated: true) case .muteUser(let actionContext): let alertController = UIAlertController( title: actionContext.isMuting ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title : L10n.Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.title, @@ -233,9 +266,9 @@ extension DataSourceFacade { } // end Task } alertController.addAction(confirmAction) - let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) + let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel) alertController.addAction(cancelAction) - dependency.present(alertController, animated: true, completion: nil) + dependency.present(alertController, animated: true) case .blockUser(let actionContext): let alertController = UIAlertController( title: actionContext.isBlocking ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title : L10n.Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.title, @@ -261,9 +294,9 @@ extension DataSourceFacade { } // end Task } alertController.addAction(confirmAction) - let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) + let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel) alertController.addAction(cancelAction) - dependency.present(alertController, animated: true, completion: nil) + dependency.present(alertController, animated: true) case .reportUser: Task { guard let user = menuContext.author else { return } @@ -352,9 +385,9 @@ extension DataSourceFacade { } // end Task } alertController.addAction(confirmAction) - let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) + let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel) alertController.addAction(cancelAction) - dependency.present(alertController, animated: true, completion: nil) + dependency.present(alertController, animated: true) } } // end func @@ -374,3 +407,4 @@ extension DataSourceFacade { } } + diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 34c2775c8..dcaefccb7 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -378,8 +378,8 @@ extension ProfileViewController { let _ = ManagedObjectRecord(objectID: user.objectID) let menu = MastodonMenu.setupMenu( actions: [ - .hideReblogs(.init(showReblogs: self.viewModel.relationshipViewModel.showReblogs)), .muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)), + .hideReblogs(.init(showReblogs: self.viewModel.relationshipViewModel.showReblogs)), .blockUser(.init(name: name, isBlocking: self.viewModel.relationshipViewModel.isBlocking)), .reportUser(.init(name: name)), .shareUser(.init(name: name)), @@ -398,7 +398,9 @@ extension ProfileViewController { } } receiveValue: { [weak self] menu in guard let self = self else { return } - self.moreMenuBarButtonItem.menu = menu + OperationQueue.main.addOperation { + self.moreMenuBarButtonItem.menu = menu + } } .store(in: &disposeBag) } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index cfb5b8ee2..38c49970c 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -13,13 +13,14 @@ import CommonOSLog import MastodonSDK extension APIService { - + private struct MastodonFollowContext { let sourceUserID: MastodonUser.ID let targetUserID: MastodonUser.ID let isFollowing: Bool let isPending: Bool let needsUnfollow: Bool + let showsReblogs: Bool } /// Toggle friendship between target MastodonUser and current MastodonUser @@ -121,5 +122,52 @@ extension APIService { let response = try result.get() return response } - + + public func toggleShowReblogs( + for user: ManagedObjectRecord, + authenticationBox: MastodonAuthenticationBox + ) async throws -> Mastodon.Response.Content { + + let managedObjectContext = backgroundManagedObjectContext + guard let user = user.object(in: managedObjectContext) else { throw APIError.implicit(.badRequest) } + + let result: Result, Error> + let showReblogs = false //FIXME: Use showReblogs-value from data + let oldShowReblogs = true + + do { + let response = try await Mastodon.API.Account.follow( + session: session, + domain: authenticationBox.domain, + accountID: user.id, + followQueryType: .follow(query: .init(reblogs: showReblogs)), + authorization: authenticationBox.userAuthorization + ).singleOutput() + + result = .success(response) + } catch { + result = .failure(error) + } + + try await managedObjectContext.performChanges { + guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } + + switch result { + case .success(let response): + Persistence.MastodonUser.update( + mastodonUser: user, + context: Persistence.MastodonUser.RelationshipContext( + entity: response.value, + me: me, + networkDate: response.networkDate + ) + ) + case .failure: + // rollback + user.update(isShowingReblogs: oldShowReblogs, by: me) + } + } + + return try result.get() + } } From 28749b5029c8c952556eeb4df50d5eeea953fc4e Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 4 Nov 2022 16:41:59 +0100 Subject: [PATCH 097/658] Fix build (#365) :see_no_evil: --- .../Sources/MastodonCore/Service/API/APIService+Follow.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index 38c49970c..ffcfae046 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -20,7 +20,6 @@ extension APIService { let isFollowing: Bool let isPending: Bool let needsUnfollow: Bool - let showsReblogs: Bool } /// Toggle friendship between target MastodonUser and current MastodonUser From 18720a9a51acab5d7f0d889d60e77cbb6fcc85c3 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 6 Nov 2022 09:22:26 +0100 Subject: [PATCH 098/658] Add localized strings (#365) --- .../Provider/DataSourceFacade+Status.swift | 61 ++++++++++--------- .../Generated/Strings.swift | 12 ++++ .../Resources/en.lproj/Localizable.strings | 5 ++ .../MastodonUI/View/Menu/MastodonMenu.swift | 2 +- 4 files changed, 50 insertions(+), 30 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index ff41cc126..332ed75e4 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -205,42 +205,45 @@ extension DataSourceFacade { menuContext: MenuContext ) async throws { switch action { - case .hideReblogs(let actionContext): - //FIXME: Add localized strings - let alertController = UIAlertController( - title: actionContext.showReblogs ? "Really hide?" : "Really show?", - message: actionContext.showReblogs ? "Really??" : "Really??", - preferredStyle: .alert - ) + case .hideReblogs(let actionContext): + let title = actionContext.showReblogs ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.title : L10n.Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.title + let message = actionContext.showReblogs ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.message : L10n.Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.message - let showHideReblogsAction = UIAlertAction( - title: actionContext.showReblogs ? "Show" : "Hide", - style: .default - ) { [weak dependency] _ in - guard let dependency else { return } + let alertController = UIAlertController( + title: title, + message: message, + preferredStyle: .alert + ) - Task { - let managedObjectContext = dependency.context.managedObjectContext - let _user: ManagedObjectRecord? = try? await managedObjectContext.perform { - guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil } - return ManagedObjectRecord(objectID: user.objectID) - } + let actionTitle = actionContext.showReblogs ? L10n.Common.Controls.Friendship.hideReblogs : L10n.Common.Controls.Friendship.showReblogs + let showHideReblogsAction = UIAlertAction( + title: actionTitle, + style: .destructive + ) { [weak dependency] _ in + guard let dependency else { return } - guard let user = _user else { return } + Task { + let managedObjectContext = dependency.context.managedObjectContext + let _user: ManagedObjectRecord? = try? await managedObjectContext.perform { + guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil } + return ManagedObjectRecord(objectID: user.objectID) + } - try await DataSourceFacade.responseToShowHideReblogAction( - dependency: dependency, - user: user - ) - } - } + guard let user = _user else { return } - alertController.addAction(showHideReblogsAction) + try await DataSourceFacade.responseToShowHideReblogAction( + dependency: dependency, + user: user + ) + } + } - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) - alertController.addAction(cancelAction) + alertController.addAction(showHideReblogsAction) - dependency.present(alertController, animated: true) + let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel) + alertController.addAction(cancelAction) + + dependency.present(alertController, animated: true) case .muteUser(let actionContext): let alertController = UIAlertController( title: actionContext.isMuting ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title : L10n.Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.title, diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 1d5b9895b..52ed59c09 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -713,6 +713,12 @@ public enum L10n { /// Block Account public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title") } + public enum ConfirmHideReblogs { + /// Confirm to hide reblogs + public static let message = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message") + /// Hide reblogs + public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title") + } public enum ConfirmMuteUser { /// Confirm to mute %@ public static func message(_ p1: Any) -> String { @@ -721,6 +727,12 @@ public enum L10n { /// Mute Account public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title") } + public enum ConfirmShowReblogs { + /// Confirm to show reblogs + public static let message = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message") + /// Show Reblogs + public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title") + } public enum ConfirmUnblockUser { /// Confirm to unblock %@ public static func message(_ p1: Any) -> String { diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings index 94bee697c..6917eb0c7 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings @@ -79,6 +79,7 @@ Please check your internet connection."; "Common.Controls.Friendship.UnmuteUser" = "Unmute %@"; "Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; "Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; + "Common.Controls.Keyboard.Common.ComposeNewPost" = "Compose New Post"; "Common.Controls.Keyboard.Common.OpenSettings" = "Open Settings"; "Common.Controls.Keyboard.Common.ShowFavorites" = "Show Favorites"; @@ -264,6 +265,10 @@ uploaded to Mastodon."; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Unblock Account"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirm to unmute %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title" = "Unmute Account"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide reblogs"; "Scene.Profile.SegmentedControl.About" = "About"; "Scene.Profile.SegmentedControl.Media" = "Media"; "Scene.Profile.SegmentedControl.Posts" = "Posts"; diff --git a/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift b/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift index b85e71138..422494328 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift @@ -52,7 +52,7 @@ extension MastodonMenu { func build(delegate: MastodonMenuDelegate) -> BuiltAction { switch self { case .hideReblogs(let context): - let title = context.showReblogs ? L10n.Common.Controls.Friendship.hideReblogs : L10n.Common.Controls.Friendship.hideReblogs + let title = context.showReblogs ? L10n.Common.Controls.Friendship.hideReblogs : L10n.Common.Controls.Friendship.showReblogs let reblogAction = BuiltAction( title: title, image: UIImage(systemName: "arrow.2.squarepath") From ee523c098efa68a9cdc963adb6d4baf3413f8c1c Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 6 Nov 2022 09:25:26 +0100 Subject: [PATCH 099/658] Make show/hide reblogs finally work (#365) oh, and also indent to 4 spaces. I needed some time to wrap my head around the data model and especially the various view-models, but hey, in the end it works. I still feel like this "I have no idea what I'm doing"-dog :D --- .../Service/API/APIService+Follow.swift | 74 ++++++++++--------- .../ViewModel/RelationshipViewModel.swift | 41 ++++++---- 2 files changed, 64 insertions(+), 51 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index ffcfae046..05d1302b6 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -127,46 +127,50 @@ extension APIService { authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - let managedObjectContext = backgroundManagedObjectContext - guard let user = user.object(in: managedObjectContext) else { throw APIError.implicit(.badRequest) } + let managedObjectContext = backgroundManagedObjectContext + guard let user = user.object(in: managedObjectContext), + let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext) + else { throw APIError.implicit(.badRequest) } - let result: Result, Error> - let showReblogs = false //FIXME: Use showReblogs-value from data - let oldShowReblogs = true + let me = authentication.user + let result: Result, Error> - do { - let response = try await Mastodon.API.Account.follow( - session: session, - domain: authenticationBox.domain, - accountID: user.id, - followQueryType: .follow(query: .init(reblogs: showReblogs)), - authorization: authenticationBox.userAuthorization - ).singleOutput() + let oldShowReblogs = me.showingReblogsBy.contains(user) + let newShowReblogs = (oldShowReblogs == false) - result = .success(response) - } catch { - result = .failure(error) - } + do { + let response = try await Mastodon.API.Account.follow( + session: session, + domain: authenticationBox.domain, + accountID: user.id, + followQueryType: .follow(query: .init(reblogs: showReblogs)), + authorization: authenticationBox.userAuthorization + ).singleOutput() - try await managedObjectContext.performChanges { - guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } - - switch result { - case .success(let response): - Persistence.MastodonUser.update( - mastodonUser: user, - context: Persistence.MastodonUser.RelationshipContext( - entity: response.value, - me: me, - networkDate: response.networkDate - ) - ) - case .failure: - // rollback - user.update(isShowingReblogs: oldShowReblogs, by: me) + result = .success(response) + } catch { + result = .failure(error) } - } - return try result.get() + try await managedObjectContext.performChanges { + guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } + + switch result { + case .success(let response): + Persistence.MastodonUser.update( + mastodonUser: user, + context: Persistence.MastodonUser.RelationshipContext( + entity: response.value, + me: me, + networkDate: response.networkDate + ) + ) + case .failure: + // rollback + user.update(isShowingReblogs: oldShowReblogs, by: me) + } + } + + return try result.get() } } diff --git a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift index 5b032aa90..d51737b1e 100644 --- a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift @@ -27,6 +27,7 @@ public enum RelationshipAction: Int, CaseIterable { case edit case editing case updating + case showReblogs public var option: RelationshipActionOptionSet { return RelationshipActionOptionSet(rawValue: 1 << rawValue) @@ -57,6 +58,7 @@ public struct RelationshipActionOptionSet: OptionSet { public static let edit = RelationshipAction.edit.option public static let editing = RelationshipAction.editing.option public static let updating = RelationshipAction.updating.option + public static let showReblogs = RelationshipAction.showReblogs.option public static let editOptions: RelationshipActionOptionSet = [.edit, .editing, .updating] @@ -75,24 +77,24 @@ public struct RelationshipActionOptionSet: OptionSet { return " " } switch highPriorityAction { - case .isMyself: return "" - case .followingBy: return " " - case .blockingBy: return " " - case .none: return " " - case .follow: return L10n.Common.Controls.Friendship.follow - case .request: return L10n.Common.Controls.Friendship.request - case .pending: return L10n.Common.Controls.Friendship.pending - case .following: return L10n.Common.Controls.Friendship.following - case .muting: return L10n.Common.Controls.Friendship.muted - case .blocked: return L10n.Common.Controls.Friendship.follow // blocked by user (deprecated) - case .blocking: return L10n.Common.Controls.Friendship.blocked - case .suspended: return L10n.Common.Controls.Friendship.follow - case .edit: return L10n.Common.Controls.Friendship.editInfo - case .editing: return L10n.Common.Controls.Actions.done - case .updating: return " " + case .isMyself: return "" + case .followingBy: return " " + case .blockingBy: return " " + case .none: return " " + case .follow: return L10n.Common.Controls.Friendship.follow + case .request: return L10n.Common.Controls.Friendship.request + case .pending: return L10n.Common.Controls.Friendship.pending + case .following: return L10n.Common.Controls.Friendship.following + case .muting: return L10n.Common.Controls.Friendship.muted + case .blocked: return L10n.Common.Controls.Friendship.follow // blocked by user (deprecated) + case .blocking: return L10n.Common.Controls.Friendship.blocked + case .suspended: return L10n.Common.Controls.Friendship.follow + case .edit: return L10n.Common.Controls.Friendship.editInfo + case .editing: return L10n.Common.Controls.Actions.done + case .updating: return " " + case .showReblogs: return "" } } - } public final class RelationshipViewModel { @@ -185,6 +187,7 @@ extension RelationshipViewModel { self.isBlockingBy = optionSet.contains(.blockingBy) self.isBlocking = optionSet.contains(.blocking) self.isSuspended = optionSet.contains(.suspended) + self.showReblogs = optionSet.contains(.showReblogs) self.optionSet = optionSet } @@ -197,6 +200,7 @@ extension RelationshipViewModel { isBlockingBy = false isBlocking = false optionSet = nil + showReblogs = false } } @@ -215,6 +219,7 @@ extension RelationshipViewModel { let isMuting = user.mutingBy.contains(me) let isBlockingBy = me.blockingBy.contains(user) let isBlocking = user.blockingBy.contains(me) + let isShowingReblogs = me.showingReblogsBy.contains(user)// user.showingReblogsBy.contains(me) var optionSet: RelationshipActionOptionSet = [.follow] @@ -253,6 +258,10 @@ extension RelationshipViewModel { if user.suspended { optionSet.insert(.suspended) } + + if isShowingReblogs { + optionSet.insert(.showReblogs) + } return optionSet } From 1ac9e5c73044c632ede1daa63560c43a3008d566 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 6 Nov 2022 09:33:55 +0100 Subject: [PATCH 100/658] Fix build (again) (#365) :facepalm: --- .../Sources/MastodonCore/Service/API/APIService+Follow.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index 05d1302b6..442d293ce 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -143,7 +143,7 @@ extension APIService { session: session, domain: authenticationBox.domain, accountID: user.id, - followQueryType: .follow(query: .init(reblogs: showReblogs)), + followQueryType: .follow(query: .init(reblogs: newShowReblogs)), authorization: authenticationBox.userAuthorization ).singleOutput() From 746d70f3e061207b556ec91e0675b2fbee5e55a8 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 6 Nov 2022 10:16:56 +0100 Subject: [PATCH 101/658] [WIP] Show show/hide-reblog-menu-entry only for people you already follow (#365) Please consider this WIP, as the breaks the ProfileRelationshipActionButton, somethingsomething RelationshipActionOptionSet for whatever reason, I assume. Also: fixed some typos and warnings. --- .../Scene/Profile/ProfileViewController.swift | 30 +++++++++++-------- .../ViewModel/RelationshipViewModel.swift | 4 +-- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index dcaefccb7..c1c5ccea7 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -376,14 +376,20 @@ extension ProfileViewController { } let name = user.displayNameWithFallback let _ = ManagedObjectRecord(objectID: user.objectID) + + var menuActions: [MastodonMenu.Action] = [ + .muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)), + .blockUser(.init(name: name, isBlocking: self.viewModel.relationshipViewModel.isBlocking)), + .reportUser(.init(name: name)), + .shareUser(.init(name: name)), + ] + + if let me = self.viewModel?.me, me.following.contains(user) { + menuActions.insert(.hideReblogs(.init(showReblogs: self.viewModel.relationshipViewModel.showReblogs)), at: 1) + } + let menu = MastodonMenu.setupMenu( - actions: [ - .muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)), - .hideReblogs(.init(showReblogs: self.viewModel.relationshipViewModel.showReblogs)), - .blockUser(.init(name: name, isBlocking: self.viewModel.relationshipViewModel.isBlocking)), - .reportUser(.init(name: name)), - .shareUser(.init(name: name)), - ], + actions: menuActions, delegate: self ) return menu @@ -743,7 +749,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { let alertController = UIAlertController(for: error, title: L10n.Common.Alerts.EditProfileFailure.title, preferredStyle: .alert) let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) alertController.addAction(okAction) - self.coordinator.present( + _ = self.coordinator.present( scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil) @@ -766,11 +772,11 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { break case .follow, .request, .pending, .following: guard let user = viewModel.user else { return } - let reocrd = ManagedObjectRecord(objectID: user.objectID) + let record = ManagedObjectRecord(objectID: user.objectID) Task { try await DataSourceFacade.responseToUserFollowAction( dependency: self, - user: reocrd + user: record ) } case .muting: @@ -819,10 +825,8 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) alertController.addAction(cancelAction) present(alertController, animated: true, completion: nil) - case .blocked: + case .blocked, .showReblogs, .isMyself,.followingBy, .blockingBy, .suspended, .edit, .editing, .updating: break - default: - assertionFailure() } } diff --git a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift index d51737b1e..059ef0413 100644 --- a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift @@ -92,7 +92,7 @@ public struct RelationshipActionOptionSet: OptionSet { case .edit: return L10n.Common.Controls.Friendship.editInfo case .editing: return L10n.Common.Controls.Actions.done case .updating: return " " - case .showReblogs: return "" + case .showReblogs: return " " } } } @@ -219,7 +219,7 @@ extension RelationshipViewModel { let isMuting = user.mutingBy.contains(me) let isBlockingBy = me.blockingBy.contains(user) let isBlocking = user.blockingBy.contains(me) - let isShowingReblogs = me.showingReblogsBy.contains(user)// user.showingReblogsBy.contains(me) + let isShowingReblogs = me.showingReblogsBy.contains(user) var optionSet: RelationshipActionOptionSet = [.follow] From 143a9b32948fa4d02c03085825174a8461e75052 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 7 Nov 2022 17:51:09 +0100 Subject: [PATCH 102/658] Get rid of SwiftyJSON it's not used anymore. --- .../xcshareddata/swiftpm/Package.resolved | 9 --------- MastodonSDK/Package.swift | 2 -- 2 files changed, 11 deletions(-) diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index f15e4a422..3d914a3f1 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -189,15 +189,6 @@ "version" : "0.1.4" } }, - { - "identity" : "swiftyjson", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SwiftyJSON/SwiftyJSON.git", - "state" : { - "revision" : "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07", - "version" : "5.0.1" - } - }, { "identity" : "tabbarpager", "kind" : "remoteSourceControl", diff --git a/MastodonSDK/Package.swift b/MastodonSDK/Package.swift index 852817c20..ca241038b 100644 --- a/MastodonSDK/Package.swift +++ b/MastodonSDK/Package.swift @@ -40,7 +40,6 @@ let package = Package( .package(url: "https://github.com/MainasuK/CommonOSLog", from: "0.1.1"), .package(url: "https://github.com/MainasuK/FPSIndicator.git", from: "1.0.0"), .package(url: "https://github.com/slackhq/PanModal.git", from: "1.2.7"), - .package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"), .package(url: "https://github.com/TimOliver/TOCropViewController.git", from: "2.6.1"), .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "2.2.5"), .package(url: "https://github.com/TwidereProject/TabBarPager.git", from: "0.1.0"), @@ -103,7 +102,6 @@ let package = Package( .target( name: "MastodonSDK", dependencies: [ - .product(name: "SwiftyJSON", package: "SwiftyJSON"), .product(name: "NIOHTTP1", package: "swift-nio"), ] ), From 00ab7ac2b0e193913e82c806c84481bf75cc5ee5 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 7 Nov 2022 18:52:02 +0100 Subject: [PATCH 103/658] Remove RelationshipActionOption for reblogs again (#365) --- Mastodon/Scene/Profile/ProfileViewController.swift | 4 +++- .../MastodonUI/ViewModel/RelationshipViewModel.swift | 11 ++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index c1c5ccea7..3ce1fd33a 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -385,7 +385,9 @@ extension ProfileViewController { ] if let me = self.viewModel?.me, me.following.contains(user) { - menuActions.insert(.hideReblogs(.init(showReblogs: self.viewModel.relationshipViewModel.showReblogs)), at: 1) + let showReblogs = me.showingReblogsBy.contains(user) + let context = MastodonMenu.HideReblogsActionContext(showReblogs: showReblogs) + menuActions.insert(.hideReblogs(context), at: 1) } let menu = MastodonMenu.setupMenu( diff --git a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift index 059ef0413..3815568ab 100644 --- a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift @@ -58,8 +58,6 @@ public struct RelationshipActionOptionSet: OptionSet { public static let edit = RelationshipAction.edit.option public static let editing = RelationshipAction.editing.option public static let updating = RelationshipAction.updating.option - public static let showReblogs = RelationshipAction.showReblogs.option - public static let editOptions: RelationshipActionOptionSet = [.edit, .editing, .updating] public func highPriorityAction(except: RelationshipActionOptionSet) -> RelationshipAction? { @@ -187,7 +185,7 @@ extension RelationshipViewModel { self.isBlockingBy = optionSet.contains(.blockingBy) self.isBlocking = optionSet.contains(.blocking) self.isSuspended = optionSet.contains(.suspended) - self.showReblogs = optionSet.contains(.showReblogs) + self.showReblogs = me.showingReblogsBy.contains(user) self.optionSet = optionSet } @@ -219,8 +217,7 @@ extension RelationshipViewModel { let isMuting = user.mutingBy.contains(me) let isBlockingBy = me.blockingBy.contains(user) let isBlocking = user.blockingBy.contains(me) - let isShowingReblogs = me.showingReblogsBy.contains(user) - + var optionSet: RelationshipActionOptionSet = [.follow] if isMyself { @@ -259,10 +256,6 @@ extension RelationshipViewModel { optionSet.insert(.suspended) } - if isShowingReblogs { - optionSet.insert(.showReblogs) - } - return optionSet } } From 822ea5d843db1b8784852e22cc1a1a4fa7095fc5 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 7 Nov 2022 22:23:00 +0100 Subject: [PATCH 104/658] Add localization keys to app.json (#365) --- Localization/app.json | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index a965b23ae..1f04cf65f 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -180,7 +180,9 @@ "unmute": "Unmute", "unmute_user": "Unmute %s", "muted": "Muted", - "edit_info": "Edit Info" + "edit_info": "Edit Info", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs", }, "timeline": { "filtered": "Filtered", @@ -455,7 +457,15 @@ "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", @@ -686,4 +696,4 @@ "accessibility_hint": "Double tap to dismiss this wizard" } } -} \ No newline at end of file +} From 3751cd172c1252b23fa0453581b8b1b024376578 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 8 Nov 2022 08:08:30 +0100 Subject: [PATCH 105/658] Make json valid again (#365) :see_no_evil: --- Localization/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 1f04cf65f..c44029040 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -460,11 +460,11 @@ }, "confirm_show_reblogs": { "title": "Show Reblogs", - "message": "Confirm to show reblogs + "message": "Confirm to show reblogs" }, "confirm_hide_reblogs": { "title": "Hide Reblogs", - "message": "Confirm to hide reblogs + "message": "Confirm to hide reblogs" }, }, "accessibility": { From 21800a4c81ed26e4d6a67f5d30cf13344ecb35f9 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 8 Nov 2022 15:26:32 +0100 Subject: [PATCH 106/658] Add optionSet for reblog again (#365) --- .../MastodonUI/ViewModel/RelationshipViewModel.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift index 3815568ab..99cff19be 100644 --- a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift @@ -12,6 +12,7 @@ import MastodonLocalization import CoreDataStack public enum RelationshipAction: Int, CaseIterable { + case showReblogs case isMyself case followingBy case blockingBy @@ -27,8 +28,7 @@ public enum RelationshipAction: Int, CaseIterable { case edit case editing case updating - case showReblogs - + public var option: RelationshipActionOptionSet { return RelationshipActionOptionSet(rawValue: 1 << rawValue) } @@ -58,6 +58,7 @@ public struct RelationshipActionOptionSet: OptionSet { public static let edit = RelationshipAction.edit.option public static let editing = RelationshipAction.editing.option public static let updating = RelationshipAction.updating.option + public static let showReblogs = RelationshipAction.showReblogs.option public static let editOptions: RelationshipActionOptionSet = [.edit, .editing, .updating] public func highPriorityAction(except: RelationshipActionOptionSet) -> RelationshipAction? { @@ -185,7 +186,7 @@ extension RelationshipViewModel { self.isBlockingBy = optionSet.contains(.blockingBy) self.isBlocking = optionSet.contains(.blocking) self.isSuspended = optionSet.contains(.suspended) - self.showReblogs = me.showingReblogsBy.contains(user) + self.showReblogs = optionSet.contains(.showReblogs) self.optionSet = optionSet } @@ -217,6 +218,7 @@ extension RelationshipViewModel { let isMuting = user.mutingBy.contains(me) let isBlockingBy = me.blockingBy.contains(user) let isBlocking = user.blockingBy.contains(me) + let isShowingReblogs = me.showingReblogsBy.contains(user) var optionSet: RelationshipActionOptionSet = [.follow] @@ -256,6 +258,10 @@ extension RelationshipViewModel { optionSet.insert(.suspended) } + if isShowingReblogs { + optionSet.insert(.showReblogs) + } + return optionSet } } From 4912d84d4653b14136ff351e3d68b8441e29e244 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 8 Nov 2022 16:48:17 +0100 Subject: [PATCH 107/658] Fix pods --- .../xcshareddata/swiftpm/Package.resolved | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3d914a3f1..64dc691bb 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Alamofire/Alamofire.git", "state" : { - "revision" : "8dd85aee02e39dd280c75eef88ffdb86eed4b07b", - "version" : "5.6.2" + "revision" : "354dda32d89fc8cd4f5c46487f64957d355f53d8", + "version" : "5.6.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Flipboard/FLAnimatedImage.git", "state" : { - "revision" : "d4f07b6f164d53c1212c3e54d6460738b1981e9f", - "version" : "1.0.17" + "revision" : "e7f9fd4681ae41bf6f3056db08af4f401d61da52", + "version" : "1.0.16" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/kean/Nuke.git", "state" : { - "revision" : "a002b7fd786f2df2ed4333fe73a9727499fd9d97", - "version" : "10.11.2" + "revision" : "0ea7545b5c918285aacc044dc75048625c8257cc", + "version" : "10.8.0" } }, { @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/uias/Pageboy", "state" : { - "revision" : "af8fa81788b893205e1ff42ddd88c5b0b315d7c5", - "version" : "3.7.0" + "revision" : "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6", + "version" : "3.6.2" } }, { @@ -131,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SDWebImage/SDWebImage.git", "state" : { - "revision" : "318cca556b0489aede0cd98d8d0c7f1408ab7bd6", - "version" : "5.13.5" + "revision" : "2e63d0061da449ad0ed130768d05dceb1496de44", + "version" : "5.12.5" } }, { @@ -176,8 +176,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/scinfu/SwiftSoup.git", "state" : { - "revision" : "6778575285177365cbad3e5b8a72f2a20583cfec", - "version" : "2.4.3" + "revision" : "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886", + "version" : "2.4.2" } }, { From 4621a86df5e8c01eae7c9d05f91de95ae768dde3 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 8 Nov 2022 16:49:58 +0100 Subject: [PATCH 108/658] Mark missing SidebarCells as buttons (#516) Credit where credit is due: Thanks to @j-f1 et al. --- Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index 7921daa05..c3f9e3e36 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -88,6 +88,7 @@ extension SidebarViewModel { cell.setNeedsUpdateConfiguration() cell.isAccessibilityElement = true cell.accessibilityLabel = item.title + cell.accessibilityTraits.insert(.button) self.$currentTab .receive(on: DispatchQueue.main) From b988a74f6a4f359a08fdeda5f6b08950470c0e2d Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 8 Nov 2022 17:46:48 +0100 Subject: [PATCH 109/658] Remove commas from app.json --- Localization/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index c44029040..bfb3a89c1 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -182,7 +182,7 @@ "muted": "Muted", "edit_info": "Edit Info", "show_reblogs": "Show Reblogs", - "hide_reblogs": "Hide Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Filtered", @@ -465,7 +465,7 @@ "confirm_hide_reblogs": { "title": "Hide Reblogs", "message": "Confirm to hide reblogs" - }, + } }, "accessibility": { "show_avatar_image": "Show avatar image", From 24c426f7f3260052dc34d301a5510827ccc70ab9 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 8 Nov 2022 13:30:17 -0500 Subject: [PATCH 110/658] Add localization info --- .../StringsConvertor/input/en.lproj/app.json | 6 ++++++ Localization/app.json | 14 ++++++-------- .../Generated/Strings.swift | 18 ++++++++++++++++++ .../Resources/en.lproj/Localizable.strings | 4 ++++ .../Extension/MetaEntity+Accessibility.swift | 13 +++++-------- 5 files changed, 39 insertions(+), 16 deletions(-) diff --git a/Localization/StringsConvertor/input/en.lproj/app.json b/Localization/StringsConvertor/input/en.lproj/app.json index a965b23ae..702811c14 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", diff --git a/Localization/app.json b/Localization/app.json index 707faf3ef..31972b075 100644 --- a/Localization/app.json +++ b/Localization/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", @@ -685,13 +691,5 @@ "multiple_account_switch_intro_description": "Switch between multiple accounts by holding the profile button.", "accessibility_hint": "Double tap to dismiss this wizard" } - }, - "a11y": { - "meta_entity": { - "url": "Link: %s", - "hashtag": "Hastag %s", - "mention": "Show Profile: %s", - "email": "Email address: %s", - } } } diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index c64a50fa2..1bf067226 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -314,6 +314,24 @@ public enum L10n { /// Undo reblog public static let unreblog = L10n.tr("Localizable", "Common.Controls.Status.Actions.Unreblog") } + public enum MetaEntity { + /// Email address: %@ + public static func email(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Status.MetaEntity.Email", String(describing: p1)) + } + /// Hastag %@ + public static func hashtag(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Status.MetaEntity.Hashtag", String(describing: p1)) + } + /// Show Profile: %@ + public static func mention(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Status.MetaEntity.Mention", String(describing: p1)) + } + /// Link: %@ + public static func url(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Status.MetaEntity.Url", String(describing: p1)) + } + } public enum Poll { /// Closed public static let closed = L10n.tr("Localizable", "Common.Controls.Status.Poll.Closed") diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings index 1c40bf855..6b7d2348f 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings @@ -108,6 +108,10 @@ Please check your internet connection."; "Common.Controls.Status.Actions.Unbookmark" = "Unbookmark"; "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.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"; diff --git a/MastodonSDK/Sources/MastodonUI/Extension/MetaEntity+Accessibility.swift b/MastodonSDK/Sources/MastodonUI/Extension/MetaEntity+Accessibility.swift index 9319617bb..b604a3870 100644 --- a/MastodonSDK/Sources/MastodonUI/Extension/MetaEntity+Accessibility.swift +++ b/MastodonSDK/Sources/MastodonUI/Extension/MetaEntity+Accessibility.swift @@ -6,22 +6,19 @@ // import Meta +import MastodonLocalization extension Meta.Entity { var accessibilityCustomActionLabel: String? { switch meta { case .url(_, trimmed: _, url: let url, userInfo: _): - // TODO: i18n (a11y.meta_entity.url) - return "Link: \(url)" + return L10n.Common.Controls.Status.MetaEntity.url(url) case .hashtag(_, hashtag: let hashtag, userInfo: _): - // TODO: i18n (a11y.meta_entity.hashtag) - return "Hashtag \(hashtag)" + return L10n.Common.Controls.Status.MetaEntity.hashtag(hashtag) case .mention(_, mention: let mention, userInfo: _): - // TODO: i18n (a11y.meta_entity.mention) - return "Show Profile: \("@" + mention)" + return L10n.Common.Controls.Status.MetaEntity.mention(mention) case .email(let email, userInfo: _): - // TODO: i18n (a11y.meta_entity.email) - return "Email address: \(email)" + return L10n.Common.Controls.Status.MetaEntity.email(email) // emoji are not actionable case .emoji: return nil From b2d26078c10aaf9780e0ce05b3fa868c229e1ed2 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 8 Nov 2022 13:37:11 -0500 Subject: [PATCH 111/658] Update localization --- Localization/StringsConvertor/input/en.lproj/app.json | 4 ++-- Mastodon/Scene/Root/MainTab/MainTabBarController.swift | 3 +-- Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift | 3 +-- .../Sources/MastodonLocalization/Generated/Strings.swift | 6 ++---- .../Resources/en.lproj/Localizable.strings | 2 +- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/Localization/StringsConvertor/input/en.lproj/app.json b/Localization/StringsConvertor/input/en.lproj/app.json index a965b23ae..21c8ab481 100644 --- a/Localization/StringsConvertor/input/en.lproj/app.json +++ b/Localization/StringsConvertor/input/en.lproj/app.json @@ -676,9 +676,9 @@ } }, "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" + "add_account": "Add Account", + "switch_accounts": "Switch Accounts" }, "wizard": { "new_in_mastodon": "New in Mastodon", diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index d56a18aed..0eea7288e 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -309,8 +309,7 @@ extension MainTabBarController { } if let profileTabItem { profileTabItem.accessibilityCustomActions = [ - // TODO: i18n (scene.account_list.switch_accounts) - UIAccessibilityCustomAction(name: "Switch Accounts") { [weak self] _ in + UIAccessibilityCustomAction(name: L10n.Scene.AccountList.switchAccounts) { [weak self] _ in self?.showAccountSwitcher() return true } diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index 97284e7b8..2a8082e47 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -136,8 +136,7 @@ extension SidebarViewModel { case .me: if self.authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user != nil { cell.accessibilityCustomActions = [ - // TODO: i18n (scene.account_list.switch_accounts) - UIAccessibilityCustomAction(name: "Switch Accounts") { [weak self] _ in + UIAccessibilityCustomAction(name: L10n.Scene.AccountList.switchAccounts) { [weak self] _ in guard let self, let delegate = self.delegate else { return false } delegate.sidebarViewModelDidSwitchAccounts(self) return true diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 52ed59c09..00c3747d7 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -406,10 +406,8 @@ public enum L10n { public static let addAccount = L10n.tr("Localizable", "Scene.AccountList.AddAccount") /// Dismiss Account Switcher public static let dismissAccountSwitcher = L10n.tr("Localizable", "Scene.AccountList.DismissAccountSwitcher") - /// 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)) - } + /// Switch Accounts + public static let switchAccounts = L10n.tr("Localizable", "Scene.AccountList.SwitchAccounts") } public enum Bookmark { /// Your Bookmarks diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings index 6917eb0c7..f54ea8442 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings @@ -153,7 +153,7 @@ Your profile looks like this to them."; "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.AccountList.SwitchAccounts" = "Switch Accounts"; "Scene.Bookmark.Title" = "Your Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "Add Attachment"; "Scene.Compose.Accessibility.AppendPoll" = "Add Poll"; From 60b69ca2e539a86d0e8f6ffee40655f0e74c57e3 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 8 Nov 2022 13:50:23 -0500 Subject: [PATCH 112/658] 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 d70dc1c1394a690b13580f10d1940a4216c7f53f Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 9 Nov 2022 07:25:57 +0100 Subject: [PATCH 113/658] Add missing title for bookmark-scene --- Localization/app.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Localization/app.json b/Localization/app.json index bfb3a89c1..80b0882d9 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -694,6 +694,9 @@ "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" } } } From 1e27b2b838bfd4fd0d8da8c128e979b5cb084d83 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 07:33:08 +0100 Subject: [PATCH 114/658] New Crowdin updates (#356) * New translations app.json (Italian) * New translations app.json (Spanish, Argentina) * New translations app.json (Spanish, Argentina) * New translations app.json (Galician) * New translations app.json (Kabyle) * New translations app.json (Kabyle) * New translations Localizable.stringsdict (Kabyle) * New translations app.json (Kurmanji (Kurdish)) * New translations app.json (Chinese Traditional) * New translations app.json (Arabic) * New translations app.json (French) * New translations app.json (Spanish) * New translations Localizable.stringsdict (Spanish) * New translations app.json (Thai) * New translations Localizable.stringsdict (Japanese) * New translations app.json (Thai) * New translations app.json (English, United States) * New translations app.json (Welsh) * New translations app.json (Scottish Gaelic) * New translations app.json (Kurmanji (Kurdish)) * New translations app.json (Sorani (Kurdish)) * New translations app.json (Kabyle) * New translations app.json (Spanish, Argentina) * New translations app.json (Hindi) * New translations app.json (Indonesian) * New translations app.json (Japanese) * New translations app.json (Romanian) * New translations app.json (French) * New translations app.json (Spanish) * New translations app.json (Arabic) * New translations app.json (Catalan) * New translations app.json (Danish) * New translations app.json (German) * New translations app.json (Basque) * New translations app.json (Finnish) * New translations app.json (Italian) * New translations app.json (Korean) * New translations app.json (Portuguese, Brazilian) * New translations app.json (Dutch) * New translations app.json (Portuguese) * New translations app.json (Russian) * New translations app.json (Swedish) * New translations app.json (Turkish) * New translations app.json (Chinese Simplified) * New translations app.json (Chinese Traditional) * New translations app.json (English) * New translations app.json (Vietnamese) * New translations app.json (Galician) * New translations app.json (Spanish, Argentina) * New translations app.json (Italian) * New translations app.json (Chinese Simplified) * New translations app.json (Catalan) * New translations app.json (Japanese) * New translations app.json (Korean) * New translations app.json (Arabic) * New translations app.json (Vietnamese) * New translations app.json (Spanish) * New translations app.json (Japanese) * New translations app.json (Japanese) * New translations app.json (Chinese Traditional) * New translations app.json (Galician) * New translations app.json (Kurmanji (Kurdish)) * New translations app.json (Thai) * New translations app.json (French) * New translations Localizable.stringsdict (Arabic) * New translations Localizable.stringsdict (Arabic) * New translations app.json (Turkish) * New translations app.json (Scottish Gaelic) * New translations app.json (Scottish Gaelic) * New translations Localizable.stringsdict (Scottish Gaelic) * New translations app.json (Latvian) * New translations ios-infoPlist.json (Latvian) * New translations Localizable.stringsdict (Latvian) * New translations Intents.strings (Latvian) * New translations Intents.stringsdict (Latvian) * New translations app.json (Latvian) * New translations app.json (Latvian) * New translations app.json (Latvian) * New translations app.json (Latvian) * New translations app.json (Latvian) * New translations app.json (Latvian) * New translations Intents.strings (Latvian) * New translations app.json (Latvian) * New translations app.json (Czech) * New translations ios-infoPlist.json (Czech) * New translations Localizable.stringsdict (Czech) * New translations Intents.strings (Czech) * New translations Intents.stringsdict (Czech) * New translations app.json (Slovenian) * New translations ios-infoPlist.json (Slovenian) * New translations Localizable.stringsdict (Slovenian) * New translations Intents.strings (Slovenian) * New translations Intents.stringsdict (Slovenian) * New translations app.json (Sinhala) * New translations ios-infoPlist.json (Sinhala) * New translations Localizable.stringsdict (Sinhala) * New translations Intents.strings (Sinhala) * New translations Intents.stringsdict (Sinhala) * New translations app.json (Sinhala) * New translations app.json (Slovenian) * New translations Intents.strings (Slovenian) * New translations app.json (Slovenian) * New translations ios-infoPlist.json (Slovenian) * New translations app.json (Slovenian) * New translations app.json (Czech) * New translations app.json (German) * New translations app.json (German) * New translations app.json (Czech) * New translations app.json (Czech) * New translations app.json (Czech) * New translations app.json (Czech) * New translations app.json (Slovenian) * New translations ios-infoPlist.json (Slovenian) * New translations app.json (Czech) * New translations app.json (Czech) * New translations app.json (Slovenian) * New translations Localizable.stringsdict (Turkish) * New translations app.json (Slovenian) * New translations ios-infoPlist.json (Slovenian) * New translations app.json (Slovenian) * New translations Intents.strings (Slovenian) * New translations app.json (Slovenian) * New translations app.json (Slovenian) * New translations app.json (Slovenian) * New translations app.json (Chinese Traditional) * New translations app.json (Vietnamese) * New translations app.json (Kabyle) * New translations app.json (Korean) * New translations app.json (Korean) * New translations ios-infoPlist.json (Korean) * New translations Localizable.stringsdict (Korean) * New translations Intents.strings (Korean) * New translations Intents.stringsdict (Korean) * New translations app.json (Swedish) * New translations app.json (Slovenian) * New translations app.json (Slovenian) * New translations app.json (Swedish) * New translations Localizable.stringsdict (Swedish) * New translations app.json (Swedish) * New translations Localizable.stringsdict (Swedish) * New translations app.json (Slovenian) * New translations app.json (Vietnamese) * New translations Localizable.stringsdict (Slovenian) * New translations Intents.strings (Vietnamese) * New translations app.json (Vietnamese) * New translations Localizable.stringsdict (German) * New translations Localizable.stringsdict (German) * New translations app.json (French) * New translations app.json (Turkish) * New translations app.json (Czech) * New translations app.json (Ukrainian) * New translations ios-infoPlist.json (Ukrainian) * New translations Localizable.stringsdict (Ukrainian) * New translations Intents.strings (Ukrainian) * New translations Intents.stringsdict (Ukrainian) * New translations Localizable.stringsdict (Slovenian) * New translations app.json (Slovenian) * New translations Intents.stringsdict (Slovenian) * New translations app.json (Slovenian) * New translations Localizable.stringsdict (Slovenian) * New translations app.json (Czech) * New translations app.json (Slovenian) * New translations app.json (Czech) * New translations app.json (Scottish Gaelic) * New translations Localizable.stringsdict (Scottish Gaelic) * New translations app.json (Slovenian) * New translations app.json (Indonesian) * New translations app.json (Portuguese) * New translations app.json (Russian) * New translations app.json (Chinese Simplified) * New translations app.json (English) * New translations app.json (Galician) * New translations app.json (Portuguese, Brazilian) * New translations app.json (Spanish, Argentina) * New translations app.json (Japanese) * New translations app.json (Thai) * New translations app.json (Latvian) * New translations app.json (Hindi) * New translations app.json (English, United States) * New translations app.json (Welsh) * New translations app.json (Sinhala) * New translations app.json (Kurmanji (Kurdish)) * New translations app.json (Dutch) * New translations app.json (Italian) * New translations app.json (Chinese Traditional) * New translations app.json (Ukrainian) * New translations app.json (Vietnamese) * New translations app.json (Kabyle) * New translations app.json (Korean) * New translations app.json (Swedish) * New translations app.json (French) * New translations app.json (Turkish) * New translations app.json (Czech) * New translations app.json (Scottish Gaelic) * New translations app.json (Finnish) * New translations app.json (Romanian) * New translations app.json (Spanish) * New translations app.json (Arabic) * New translations app.json (Catalan) * New translations app.json (Danish) * New translations app.json (German) * New translations app.json (Basque) * New translations app.json (Sorani (Kurdish)) * New translations app.json (Swedish) * New translations app.json (Italian) * New translations Intents.strings (Swedish) * New translations app.json (Catalan) * New translations app.json (Slovenian) * New translations app.json (Spanish, Argentina) * New translations app.json (Chinese Traditional) * New translations app.json (Korean) * New translations app.json (Vietnamese) * New translations app.json (Slovenian) * New translations app.json (Indonesian) * New translations app.json (Portuguese) * New translations app.json (Russian) * New translations app.json (Chinese Simplified) * New translations app.json (English) * New translations app.json (Galician) * New translations app.json (Portuguese, Brazilian) * New translations app.json (Spanish, Argentina) * New translations app.json (Japanese) * New translations app.json (Thai) * New translations app.json (Latvian) * New translations app.json (Hindi) * New translations app.json (English, United States) * New translations app.json (Welsh) * New translations app.json (Sinhala) * New translations app.json (Kurmanji (Kurdish)) * New translations app.json (Dutch) * New translations app.json (Italian) * New translations app.json (Chinese Traditional) * New translations app.json (Ukrainian) * New translations app.json (Vietnamese) * New translations app.json (Kabyle) * New translations app.json (Korean) * New translations app.json (Swedish) * New translations app.json (French) * New translations app.json (Turkish) * New translations app.json (Czech) * New translations app.json (Scottish Gaelic) * New translations app.json (Finnish) * New translations app.json (Romanian) * New translations app.json (Spanish) * New translations app.json (Arabic) * New translations app.json (Catalan) * New translations app.json (Danish) * New translations app.json (German) * New translations app.json (Basque) * New translations app.json (Sorani (Kurdish)) --- .../Intents/input/cs.lproj/Intents.strings | 51 ++ .../input/cs.lproj/Intents.stringsdict | 46 ++ .../Intents/input/ko.lproj/Intents.strings | 14 +- .../input/ko.lproj/Intents.stringsdict | 4 +- .../Intents/input/lv.lproj/Intents.strings | 51 ++ .../input/lv.lproj/Intents.stringsdict | 42 ++ .../Intents/input/si.lproj/Intents.strings | 51 ++ .../input/si.lproj/Intents.stringsdict | 38 + .../Intents/input/sl.lproj/Intents.strings | 51 ++ .../input/sl.lproj/Intents.stringsdict | 46 ++ .../Intents/input/sv.lproj/Intents.strings | 2 +- .../Intents/input/uk.lproj/Intents.strings | 51 ++ .../input/uk.lproj/Intents.stringsdict | 46 ++ .../Intents/input/vi.lproj/Intents.strings | 2 +- .../StringsConvertor/input/ar.lproj/app.json | 17 +- .../StringsConvertor/input/ca.lproj/app.json | 17 +- .../StringsConvertor/input/ckb.lproj/app.json | 17 +- .../input/cs.lproj/Localizable.stringsdict | 561 ++++++++++++++ .../StringsConvertor/input/cs.lproj/app.json | 702 ++++++++++++++++++ .../input/cs.lproj/ios-infoPlist.json | 6 + .../StringsConvertor/input/cy.lproj/app.json | 17 +- .../StringsConvertor/input/da.lproj/app.json | 17 +- .../input/de.lproj/Localizable.stringsdict | 30 +- .../StringsConvertor/input/de.lproj/app.json | 113 +-- .../input/en-US.lproj/app.json | 17 +- .../StringsConvertor/input/en.lproj/app.json | 17 +- .../input/es-AR.lproj/app.json | 17 +- .../StringsConvertor/input/es.lproj/app.json | 17 +- .../StringsConvertor/input/eu.lproj/app.json | 17 +- .../StringsConvertor/input/fi.lproj/app.json | 17 +- .../StringsConvertor/input/fr.lproj/app.json | 29 +- .../input/gd.lproj/Localizable.stringsdict | 24 +- .../StringsConvertor/input/gd.lproj/app.json | 149 ++-- .../StringsConvertor/input/gl.lproj/app.json | 17 +- .../StringsConvertor/input/hi.lproj/app.json | 17 +- .../StringsConvertor/input/id.lproj/app.json | 17 +- .../StringsConvertor/input/it.lproj/app.json | 17 +- .../StringsConvertor/input/ja.lproj/app.json | 17 +- .../StringsConvertor/input/kab.lproj/app.json | 19 +- .../StringsConvertor/input/kmr.lproj/app.json | 17 +- .../input/ko.lproj/Localizable.stringsdict | 38 +- .../StringsConvertor/input/ko.lproj/app.json | 315 ++++---- .../input/ko.lproj/ios-infoPlist.json | 2 +- .../input/lv.lproj/Localizable.stringsdict | 505 +++++++++++++ .../StringsConvertor/input/lv.lproj/app.json | 702 ++++++++++++++++++ .../input/lv.lproj/ios-infoPlist.json | 6 + .../StringsConvertor/input/nl.lproj/app.json | 17 +- .../input/pt-BR.lproj/app.json | 17 +- .../StringsConvertor/input/pt.lproj/app.json | 17 +- .../StringsConvertor/input/ro.lproj/app.json | 17 +- .../StringsConvertor/input/ru.lproj/app.json | 17 +- .../input/si.lproj/Localizable.stringsdict | 449 +++++++++++ .../StringsConvertor/input/si.lproj/app.json | 702 ++++++++++++++++++ .../input/si.lproj/ios-infoPlist.json | 6 + .../input/sl.lproj/Localizable.stringsdict | 561 ++++++++++++++ .../StringsConvertor/input/sl.lproj/app.json | 702 ++++++++++++++++++ .../input/sl.lproj/ios-infoPlist.json | 6 + .../input/sv.lproj/Localizable.stringsdict | 6 +- .../StringsConvertor/input/sv.lproj/app.json | 43 +- .../StringsConvertor/input/th.lproj/app.json | 17 +- .../input/tr.lproj/Localizable.stringsdict | 2 +- .../StringsConvertor/input/tr.lproj/app.json | 37 +- .../input/uk.lproj/Localizable.stringsdict | 561 ++++++++++++++ .../StringsConvertor/input/uk.lproj/app.json | 702 ++++++++++++++++++ .../input/uk.lproj/ios-infoPlist.json | 6 + .../StringsConvertor/input/vi.lproj/app.json | 39 +- .../input/zh-Hans.lproj/app.json | 17 +- .../input/zh-Hant.lproj/app.json | 19 +- 68 files changed, 7512 insertions(+), 433 deletions(-) create mode 100644 Localization/StringsConvertor/Intents/input/cs.lproj/Intents.strings create mode 100644 Localization/StringsConvertor/Intents/input/cs.lproj/Intents.stringsdict create mode 100644 Localization/StringsConvertor/Intents/input/lv.lproj/Intents.strings create mode 100644 Localization/StringsConvertor/Intents/input/lv.lproj/Intents.stringsdict create mode 100644 Localization/StringsConvertor/Intents/input/si.lproj/Intents.strings create mode 100644 Localization/StringsConvertor/Intents/input/si.lproj/Intents.stringsdict create mode 100644 Localization/StringsConvertor/Intents/input/sl.lproj/Intents.strings create mode 100644 Localization/StringsConvertor/Intents/input/sl.lproj/Intents.stringsdict create mode 100644 Localization/StringsConvertor/Intents/input/uk.lproj/Intents.strings create mode 100644 Localization/StringsConvertor/Intents/input/uk.lproj/Intents.stringsdict create mode 100644 Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict create mode 100644 Localization/StringsConvertor/input/cs.lproj/app.json create mode 100644 Localization/StringsConvertor/input/cs.lproj/ios-infoPlist.json create mode 100644 Localization/StringsConvertor/input/lv.lproj/Localizable.stringsdict create mode 100644 Localization/StringsConvertor/input/lv.lproj/app.json create mode 100644 Localization/StringsConvertor/input/lv.lproj/ios-infoPlist.json create mode 100644 Localization/StringsConvertor/input/si.lproj/Localizable.stringsdict create mode 100644 Localization/StringsConvertor/input/si.lproj/app.json create mode 100644 Localization/StringsConvertor/input/si.lproj/ios-infoPlist.json create mode 100644 Localization/StringsConvertor/input/sl.lproj/Localizable.stringsdict create mode 100644 Localization/StringsConvertor/input/sl.lproj/app.json create mode 100644 Localization/StringsConvertor/input/sl.lproj/ios-infoPlist.json create mode 100644 Localization/StringsConvertor/input/uk.lproj/Localizable.stringsdict create mode 100644 Localization/StringsConvertor/input/uk.lproj/app.json create mode 100644 Localization/StringsConvertor/input/uk.lproj/ios-infoPlist.json diff --git a/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.strings new file mode 100644 index 000000000..6877490ba --- /dev/null +++ b/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.strings @@ -0,0 +1,51 @@ +"16wxgf" = "Post on Mastodon"; + +"751xkl" = "Text Content"; + +"CsR7G2" = "Post on Mastodon"; + +"HZSGTr" = "What content to post?"; + +"HdGikU" = "Posting failed"; + +"KDNTJ4" = "Failure Reason"; + +"RHxKOw" = "Send Post with text content"; + +"RxSqsb" = "Post"; + +"WCIR3D" = "Post ${content} on Mastodon"; + +"ZKJSNu" = "Post"; + +"ZS1XaK" = "${content}"; + +"ZbSjzC" = "Visibility"; + +"Zo4jgJ" = "Post Visibility"; + +"apSxMG-dYQ5NN" = "There are ${count} options matching ‘Public’."; + +"apSxMG-ehFLjY" = "There are ${count} options matching ‘Followers Only’."; + +"ayoYEb-dYQ5NN" = "${content}, Public"; + +"ayoYEb-ehFLjY" = "${content}, Followers Only"; + +"dUyuGg" = "Post on Mastodon"; + +"dYQ5NN" = "Public"; + +"ehFLjY" = "Followers Only"; + +"gfePDu" = "Posting failed. ${failureReason}"; + +"k7dbKQ" = "Post was sent successfully."; + +"oGiqmY-dYQ5NN" = "Just to confirm, you wanted ‘Public’?"; + +"oGiqmY-ehFLjY" = "Just to confirm, you wanted ‘Followers Only’?"; + +"rM6dvp" = "URL"; + +"ryJLwG" = "Post was sent successfully. "; diff --git a/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.stringsdict new file mode 100644 index 000000000..a739f778f --- /dev/null +++ b/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.stringsdict @@ -0,0 +1,46 @@ + + + + + There are ${count} options matching ‘${content}’. - 2 + + NSStringLocalizedFormatKey + There are %#@count_option@ matching ‘${content}’. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + one + 1 option + few + %ld options + many + %ld options + other + %ld options + + + There are ${count} options matching ‘${visibility}’. + + NSStringLocalizedFormatKey + There are %#@count_option@ matching ‘${visibility}’. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + one + 1 option + few + %ld options + many + %ld options + other + %ld options + + + + diff --git a/Localization/StringsConvertor/Intents/input/ko.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/ko.lproj/Intents.strings index 6877490ba..5ea6fd363 100644 --- a/Localization/StringsConvertor/Intents/input/ko.lproj/Intents.strings +++ b/Localization/StringsConvertor/Intents/input/ko.lproj/Intents.strings @@ -34,18 +34,18 @@ "dUyuGg" = "Post on Mastodon"; -"dYQ5NN" = "Public"; +"dYQ5NN" = "공개"; -"ehFLjY" = "Followers Only"; +"ehFLjY" = "팔로워 전용"; -"gfePDu" = "Posting failed. ${failureReason}"; +"gfePDu" = "게시를 실패했습니다. ${failureReason}"; -"k7dbKQ" = "Post was sent successfully."; +"k7dbKQ" = "성공적으로 게시물을 전송했습니다."; -"oGiqmY-dYQ5NN" = "Just to confirm, you wanted ‘Public’?"; +"oGiqmY-dYQ5NN" = "확인차 물어보건데, '공개'로 게시하시길 원합니까?"; -"oGiqmY-ehFLjY" = "Just to confirm, you wanted ‘Followers Only’?"; +"oGiqmY-ehFLjY" = "확인차 물어보건데, '팔로워 전용'으로 게시하시길 원합니까?"; "rM6dvp" = "URL"; -"ryJLwG" = "Post was sent successfully. "; +"ryJLwG" = "성공적으로 게시물을 전송했습니다. "; diff --git a/Localization/StringsConvertor/Intents/input/ko.lproj/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/ko.lproj/Intents.stringsdict index a14f8b9ff..86a8eac72 100644 --- a/Localization/StringsConvertor/Intents/input/ko.lproj/Intents.stringsdict +++ b/Localization/StringsConvertor/Intents/input/ko.lproj/Intents.stringsdict @@ -13,7 +13,7 @@ NSStringFormatValueTypeKey %ld other - %ld options + %ld 개의 옵션 There are ${count} options matching ‘${visibility}’. @@ -27,7 +27,7 @@ NSStringFormatValueTypeKey %ld other - %ld options + %ld 개의 옵션 diff --git a/Localization/StringsConvertor/Intents/input/lv.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/lv.lproj/Intents.strings new file mode 100644 index 000000000..eddd2026f --- /dev/null +++ b/Localization/StringsConvertor/Intents/input/lv.lproj/Intents.strings @@ -0,0 +1,51 @@ +"16wxgf" = "Post on Mastodon"; + +"751xkl" = "Text Content"; + +"CsR7G2" = "Post on Mastodon"; + +"HZSGTr" = "What content to post?"; + +"HdGikU" = "Posting failed"; + +"KDNTJ4" = "Failure Reason"; + +"RHxKOw" = "Send Post with text content"; + +"RxSqsb" = "Ziņa"; + +"WCIR3D" = "Post ${content} on Mastodon"; + +"ZKJSNu" = "Post"; + +"ZS1XaK" = "${content}"; + +"ZbSjzC" = "Visibility"; + +"Zo4jgJ" = "Post Visibility"; + +"apSxMG-dYQ5NN" = "There are ${count} options matching ‘Public’."; + +"apSxMG-ehFLjY" = "There are ${count} options matching ‘Followers Only’."; + +"ayoYEb-dYQ5NN" = "${content}, Public"; + +"ayoYEb-ehFLjY" = "${content}, Followers Only"; + +"dUyuGg" = "Post on Mastodon"; + +"dYQ5NN" = "Publisks"; + +"ehFLjY" = "Tikai sekotājiem"; + +"gfePDu" = "Posting failed. ${failureReason}"; + +"k7dbKQ" = "Post was sent successfully."; + +"oGiqmY-dYQ5NN" = "Just to confirm, you wanted ‘Public’?"; + +"oGiqmY-ehFLjY" = "Just to confirm, you wanted ‘Followers Only’?"; + +"rM6dvp" = "URL"; + +"ryJLwG" = "Post was sent successfully. "; diff --git a/Localization/StringsConvertor/Intents/input/lv.lproj/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/lv.lproj/Intents.stringsdict new file mode 100644 index 000000000..8926678aa --- /dev/null +++ b/Localization/StringsConvertor/Intents/input/lv.lproj/Intents.stringsdict @@ -0,0 +1,42 @@ + + + + + There are ${count} options matching ‘${content}’. - 2 + + NSStringLocalizedFormatKey + There are %#@count_option@ matching ‘${content}’. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + zero + %ld options + one + 1 option + other + %ld options + + + There are ${count} options matching ‘${visibility}’. + + NSStringLocalizedFormatKey + There are %#@count_option@ matching ‘${visibility}’. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + zero + %ld options + one + 1 option + other + %ld options + + + + diff --git a/Localization/StringsConvertor/Intents/input/si.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/si.lproj/Intents.strings new file mode 100644 index 000000000..6877490ba --- /dev/null +++ b/Localization/StringsConvertor/Intents/input/si.lproj/Intents.strings @@ -0,0 +1,51 @@ +"16wxgf" = "Post on Mastodon"; + +"751xkl" = "Text Content"; + +"CsR7G2" = "Post on Mastodon"; + +"HZSGTr" = "What content to post?"; + +"HdGikU" = "Posting failed"; + +"KDNTJ4" = "Failure Reason"; + +"RHxKOw" = "Send Post with text content"; + +"RxSqsb" = "Post"; + +"WCIR3D" = "Post ${content} on Mastodon"; + +"ZKJSNu" = "Post"; + +"ZS1XaK" = "${content}"; + +"ZbSjzC" = "Visibility"; + +"Zo4jgJ" = "Post Visibility"; + +"apSxMG-dYQ5NN" = "There are ${count} options matching ‘Public’."; + +"apSxMG-ehFLjY" = "There are ${count} options matching ‘Followers Only’."; + +"ayoYEb-dYQ5NN" = "${content}, Public"; + +"ayoYEb-ehFLjY" = "${content}, Followers Only"; + +"dUyuGg" = "Post on Mastodon"; + +"dYQ5NN" = "Public"; + +"ehFLjY" = "Followers Only"; + +"gfePDu" = "Posting failed. ${failureReason}"; + +"k7dbKQ" = "Post was sent successfully."; + +"oGiqmY-dYQ5NN" = "Just to confirm, you wanted ‘Public’?"; + +"oGiqmY-ehFLjY" = "Just to confirm, you wanted ‘Followers Only’?"; + +"rM6dvp" = "URL"; + +"ryJLwG" = "Post was sent successfully. "; diff --git a/Localization/StringsConvertor/Intents/input/si.lproj/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/si.lproj/Intents.stringsdict new file mode 100644 index 000000000..18422c772 --- /dev/null +++ b/Localization/StringsConvertor/Intents/input/si.lproj/Intents.stringsdict @@ -0,0 +1,38 @@ + + + + + There are ${count} options matching ‘${content}’. - 2 + + NSStringLocalizedFormatKey + There are %#@count_option@ matching ‘${content}’. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + one + 1 option + other + %ld options + + + There are ${count} options matching ‘${visibility}’. + + NSStringLocalizedFormatKey + There are %#@count_option@ matching ‘${visibility}’. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + one + 1 option + other + %ld options + + + + diff --git a/Localization/StringsConvertor/Intents/input/sl.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/sl.lproj/Intents.strings new file mode 100644 index 000000000..72de87df2 --- /dev/null +++ b/Localization/StringsConvertor/Intents/input/sl.lproj/Intents.strings @@ -0,0 +1,51 @@ +"16wxgf" = "Objavi na Mastodonu"; + +"751xkl" = "besedilo"; + +"CsR7G2" = "Objavi na Mastodonu"; + +"HZSGTr" = "Kakšno vsebino želite objaviti?"; + +"HdGikU" = "Objava ni uspela"; + +"KDNTJ4" = "Vzrok za neuspeh"; + +"RHxKOw" = "Pošlji objavo z besedilom"; + +"RxSqsb" = "Objavi"; + +"WCIR3D" = "Objavite ${content} na Mastodonu"; + +"ZKJSNu" = "Objavi"; + +"ZS1XaK" = "${content}"; + +"ZbSjzC" = "Vidnost"; + +"Zo4jgJ" = "Vidnost objave"; + +"apSxMG-dYQ5NN" = "Z \"Javno\" se ujema ${count} možnosti."; + +"apSxMG-ehFLjY" = "S \"Samo sledilci\" se ujema ${count} možnosti."; + +"ayoYEb-dYQ5NN" = "${content}, javno"; + +"ayoYEb-ehFLjY" = "${content}, samo sledilci"; + +"dUyuGg" = "Objavi na Mastodonu"; + +"dYQ5NN" = "Javno"; + +"ehFLjY" = "Samo sledilci"; + +"gfePDu" = "Objava je spodletela. ${failureReason}"; + +"k7dbKQ" = "Uspešno poslana objava."; + +"oGiqmY-dYQ5NN" = "Da ne bo nesporazuma - želeli ste \"Javno\"?"; + +"oGiqmY-ehFLjY" = "Da ne bo nesporazuma - želeli ste \"Samo sledilci\"?"; + +"rM6dvp" = "URL"; + +"ryJLwG" = "Uspešno poslana objava. "; diff --git a/Localization/StringsConvertor/Intents/input/sl.lproj/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/sl.lproj/Intents.stringsdict new file mode 100644 index 000000000..5b8deba51 --- /dev/null +++ b/Localization/StringsConvertor/Intents/input/sl.lproj/Intents.stringsdict @@ -0,0 +1,46 @@ + + + + + There are ${count} options matching ‘${content}’. - 2 + + NSStringLocalizedFormatKey + Na voljo: %#@count_option@, ki se ujema z "${content}". + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + one + %ld možnost + two + %ld možnosti + few + %ld možnosti + other + %ld možnosti + + + There are ${count} options matching ‘${visibility}’. + + NSStringLocalizedFormatKey + Na voljo: %#@count_option@, ki se ujema z "${visibility}". + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + one + %ld možnost + two + %ld možnosti + few + %ld možnosti + other + %ld možnosti + + + + diff --git a/Localization/StringsConvertor/Intents/input/sv.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/sv.lproj/Intents.strings index 526e495d2..793922506 100644 --- a/Localization/StringsConvertor/Intents/input/sv.lproj/Intents.strings +++ b/Localization/StringsConvertor/Intents/input/sv.lproj/Intents.strings @@ -38,7 +38,7 @@ "ehFLjY" = "Endast följare"; -"gfePDu" = "Publicering misslyckades. ${failureReason}"; +"gfePDu" = "Kunde inte publicera. ${failureReason}"; "k7dbKQ" = "Inlägget har publicerats."; diff --git a/Localization/StringsConvertor/Intents/input/uk.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/uk.lproj/Intents.strings new file mode 100644 index 000000000..6877490ba --- /dev/null +++ b/Localization/StringsConvertor/Intents/input/uk.lproj/Intents.strings @@ -0,0 +1,51 @@ +"16wxgf" = "Post on Mastodon"; + +"751xkl" = "Text Content"; + +"CsR7G2" = "Post on Mastodon"; + +"HZSGTr" = "What content to post?"; + +"HdGikU" = "Posting failed"; + +"KDNTJ4" = "Failure Reason"; + +"RHxKOw" = "Send Post with text content"; + +"RxSqsb" = "Post"; + +"WCIR3D" = "Post ${content} on Mastodon"; + +"ZKJSNu" = "Post"; + +"ZS1XaK" = "${content}"; + +"ZbSjzC" = "Visibility"; + +"Zo4jgJ" = "Post Visibility"; + +"apSxMG-dYQ5NN" = "There are ${count} options matching ‘Public’."; + +"apSxMG-ehFLjY" = "There are ${count} options matching ‘Followers Only’."; + +"ayoYEb-dYQ5NN" = "${content}, Public"; + +"ayoYEb-ehFLjY" = "${content}, Followers Only"; + +"dUyuGg" = "Post on Mastodon"; + +"dYQ5NN" = "Public"; + +"ehFLjY" = "Followers Only"; + +"gfePDu" = "Posting failed. ${failureReason}"; + +"k7dbKQ" = "Post was sent successfully."; + +"oGiqmY-dYQ5NN" = "Just to confirm, you wanted ‘Public’?"; + +"oGiqmY-ehFLjY" = "Just to confirm, you wanted ‘Followers Only’?"; + +"rM6dvp" = "URL"; + +"ryJLwG" = "Post was sent successfully. "; diff --git a/Localization/StringsConvertor/Intents/input/uk.lproj/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/uk.lproj/Intents.stringsdict new file mode 100644 index 000000000..a739f778f --- /dev/null +++ b/Localization/StringsConvertor/Intents/input/uk.lproj/Intents.stringsdict @@ -0,0 +1,46 @@ + + + + + There are ${count} options matching ‘${content}’. - 2 + + NSStringLocalizedFormatKey + There are %#@count_option@ matching ‘${content}’. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + one + 1 option + few + %ld options + many + %ld options + other + %ld options + + + There are ${count} options matching ‘${visibility}’. + + NSStringLocalizedFormatKey + There are %#@count_option@ matching ‘${visibility}’. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + one + 1 option + few + %ld options + many + %ld options + other + %ld options + + + + diff --git a/Localization/StringsConvertor/Intents/input/vi.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/vi.lproj/Intents.strings index a95337317..80c01c640 100644 --- a/Localization/StringsConvertor/Intents/input/vi.lproj/Intents.strings +++ b/Localization/StringsConvertor/Intents/input/vi.lproj/Intents.strings @@ -30,7 +30,7 @@ "ayoYEb-dYQ5NN" = "${content}, Công khai"; -"ayoYEb-ehFLjY" = "${content}, Riêng tư"; +"ayoYEb-ehFLjY" = "${content}, Chỉ người theo dõi"; "dUyuGg" = "Đăng lên Mastodon"; diff --git a/Localization/StringsConvertor/input/ar.lproj/app.json b/Localization/StringsConvertor/input/ar.lproj/app.json index 732b5acb2..ce68229ac 100644 --- a/Localization/StringsConvertor/input/ar.lproj/app.json +++ b/Localization/StringsConvertor/input/ar.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "رفع الكتم", "unmute_user": "رفع الكتم عن %s", "muted": "مكتوم", - "edit_info": "تَحريرُ المَعلُومات" + "edit_info": "تَحريرُ المَعلُومات", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "مُصفَّى", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "رَفعُ الحَظرِ عَنِ الحِساب", "message": "تأكيدُ رَفع الحَظرِ عَن %s" + }, + "confirm_show_reblogs": { + "title": "Show Reblogs", + "message": "Confirm to show reblogs" + }, + "confirm_hide_reblogs": { + "title": "Hide Reblogs", + "message": "Confirm to hide reblogs" } }, "accessibility": { @@ -684,6 +694,9 @@ "new_in_mastodon": "جديد في ماستودون", "multiple_account_switch_intro_description": "بدِّل بين حسابات متعددة عبر الاستمرار بالضغط على زر الملف الشخصي.", "accessibility_hint": "انقر نقرًا مزدوجًا لتجاهُل النافذة المنبثقة" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/ca.lproj/app.json b/Localization/StringsConvertor/input/ca.lproj/app.json index da78cf285..4766eb31b 100644 --- a/Localization/StringsConvertor/input/ca.lproj/app.json +++ b/Localization/StringsConvertor/input/ca.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Deixa de silenciar", "unmute_user": "Treure silenci de %s", "muted": "Silenciat", - "edit_info": "Edita" + "edit_info": "Edita", + "show_reblogs": "Mostra els impulsos", + "hide_reblogs": "Amaga els impulsos" }, "timeline": { "filtered": "Filtrat", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "Desbloqueja el Compte", "message": "Confirma per a desbloquejar %s" + }, + "confirm_show_reblogs": { + "title": "Mostra els Impulsos", + "message": "Confirma per a mostrar els impulsos" + }, + "confirm_hide_reblogs": { + "title": "Amaga Impulsos", + "message": "Confirma per a amagar els impulsos" } }, "accessibility": { @@ -684,6 +694,9 @@ "new_in_mastodon": "Nou a Mastodon", "multiple_account_switch_intro_description": "Commuta entre diversos comptes mantenint premut el botó del perfil.", "accessibility_hint": "Toca dues vegades per descartar l'assistent" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/ckb.lproj/app.json b/Localization/StringsConvertor/input/ckb.lproj/app.json index 926590d74..49f72d7a3 100644 --- a/Localization/StringsConvertor/input/ckb.lproj/app.json +++ b/Localization/StringsConvertor/input/ckb.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "بێدەنگی مەکە", "unmute_user": "%s بێدەنگ مەکە", "muted": "بێدەنگ کراوە", - "edit_info": "دەستکاری" + "edit_info": "دەستکاری", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "پاڵێوراو", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "ئاستەنگی مەکە", "message": "دڵنیا ببەوە بۆ لابردنی ئاستەنگی %s" + }, + "confirm_show_reblogs": { + "title": "Show Reblogs", + "message": "Confirm to show reblogs" + }, + "confirm_hide_reblogs": { + "title": "Hide Reblogs", + "message": "Confirm to hide reblogs" } }, "accessibility": { @@ -684,6 +694,9 @@ "new_in_mastodon": "نوێ", "multiple_account_switch_intro_description": "هەژمارەکەت بگۆڕە بە دەستڕاگرتن لەسەر دوگمەی پرۆفایلەکە.", "accessibility_hint": "دوو جار دەستی پیا بنێ بۆ داخستنی" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict new file mode 100644 index 000000000..cdf35477e --- /dev/null +++ b/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict @@ -0,0 +1,561 @@ + + + + + a11y.plural.count.unread.notification + + NSStringLocalizedFormatKey + %#@notification_count_unread_notification@ + notification_count_unread_notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 unread notification + few + %ld unread notification + 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 + 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 + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + + plural.count.followed_by_and_mutual + + NSStringLocalizedFormatKey + %#@names@%#@count_mutual@ + names + + one + + few + + many + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + + + count_mutual + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + 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 + one + post + few + posts + many + posts + other + posts + + + plural.count.media + + NSStringLocalizedFormatKey + %#@media_count@ + media_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 media + few + %ld media + many + %ld media + other + %ld media + + + plural.count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 post + few + %ld posts + many + %ld posts + other + %ld posts + + + plural.count.favorite + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 favorite + few + %ld favorites + many + %ld favorites + other + %ld favorites + + + plural.count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reblog + few + %ld reblogs + many + %ld reblogs + other + %ld reblogs + + + plural.count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reply + few + %ld replies + many + %ld replies + other + %ld replies + + + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 vote + few + %ld votes + many + %ld votes + other + %ld votes + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + 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 + 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 + one + 1 following + few + %ld following + many + %ld following + other + %ld following + + + plural.count.follower + + NSStringLocalizedFormatKey + %#@count_follower@ + count_follower + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + one + 1s ago + few + %lds ago + many + %lds ago + other + %lds ago + + + + diff --git a/Localization/StringsConvertor/input/cs.lproj/app.json b/Localization/StringsConvertor/input/cs.lproj/app.json new file mode 100644 index 000000000..550f71808 --- /dev/null +++ b/Localization/StringsConvertor/input/cs.lproj/app.json @@ -0,0 +1,702 @@ +{ + "common": { + "alerts": { + "common": { + "please_try_again": "Zkuste to prosím znovu.", + "please_try_again_later": "Zkuste to prosím znovu později." + }, + "sign_up_failure": { + "title": "Registrace selhala" + }, + "server_error": { + "title": "Chyba serveru" + }, + "vote_failure": { + "title": "Selhání hlasování", + "poll_ended": "Anketa skončila" + }, + "discard_post_content": { + "title": "Zahodit koncept", + "message": "Potvrďte odstranění obsahu složeného příspěvku." + }, + "publish_post_failure": { + "title": "Publikování selhalo", + "message": "Nepodařilo se publikovat příspěvek.\nZkontrolujte prosím připojení k internetu.", + "attachments_message": { + "video_attach_with_photo": "K příspěvku, který již obsahuje obrázky, nelze připojit video.", + "more_than_one_video": "Nelze připojit více než jedno video." + } + }, + "edit_profile_failure": { + "title": "Chyba při úpravě profilu", + "message": "Nelze upravit profil. Zkuste to prosím znovu." + }, + "sign_out": { + "title": "Odhlásit se", + "message": "Opravdu se chcete odhlásit?", + "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.", + "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." + }, + "delete_post": { + "title": "Odstranit příspěvek", + "message": "Opravdu chcete smazat tento příspěvek?" + }, + "clean_cache": { + "title": "Vyčistit mezipaměť", + "message": "Úspěšně vyčištěno %s mezipaměti." + } + }, + "controls": { + "actions": { + "back": "Zpět", + "next": "Další", + "previous": "Předchozí", + "open": "Otevřít", + "add": "Přidat", + "remove": "Odstranit", + "edit": "Upravit", + "save": "Uložit", + "ok": "OK", + "done": "Hotovo", + "confirm": "Potvrdit", + "continue": "Pokračovat", + "compose": "Napsat", + "cancel": "Zrušit", + "discard": "Zahodit", + "try_again": "Zkusit znovu", + "take_photo": "Vyfotit", + "save_photo": "Uložit fotku", + "copy_photo": "Kopírovat fotografii", + "sign_in": "Přihlásit se", + "sign_up": "Zaregistrovat se", + "see_more": "Zobrazit více", + "preview": "Náhled", + "share": "Sdílet", + "share_user": "Share %s", + "share_post": "Sdílet příspěvek", + "open_in_safari": "Otevřít v Safari", + "open_in_browser": "Otevřít v prohlížeči", + "find_people": "Najít lidi ke sledování", + "manually_search": "Místo toho ručně vyhledat", + "skip": "Přeskočit", + "reply": "Odpovědět", + "report_user": "Nahlásit %s", + "block_domain": "Blokovat %s", + "unblock_domain": "Odblokovat %s", + "settings": "Nastavení", + "delete": "Smazat" + }, + "tabs": { + "home": "Domů", + "search": "Hledat", + "notification": "Oznamování", + "profile": "Profil" + }, + "keyboard": { + "common": { + "switch_to_tab": "Přepnout na %s", + "compose_new_post": "Vytvořit nový příspěvek", + "show_favorites": "Zobrazit Oblíbené", + "open_settings": "Otevřít Nastavení" + }, + "timeline": { + "previous_status": "Předchozí příspěvek", + "next_status": "Další příspěvek", + "open_status": "Otevřít příspěvek", + "open_author_profile": "Otevřít profil autora", + "open_reblogger_profile": "Otevřít rebloggerův profil", + "reply_status": "Odpovědět na příspěvek", + "toggle_reblog": "Toggle Reblog on Post", + "toggle_favorite": "Toggle Favorite on Post", + "toggle_content_warning": "Přepnout varování obsahu", + "preview_image": "Náhled obrázku" + }, + "segmented_control": { + "previous_section": "Předchozí sekce", + "next_section": "Další sekce" + } + }, + "status": { + "user_reblogged": "%s reblogged", + "user_replied_to": "Replied to %s", + "show_post": "Zobrazit příspěvek", + "show_user_profile": "Zobrazit profil uživatele", + "content_warning": "Varování o obsahu", + "sensitive_content": "Citlivý obsah", + "media_content_warning": "Klepnutím kdekoli zobrazíte", + "tap_to_reveal": "Klepnutím zobrazit", + "poll": { + "vote": "Hlasovat", + "closed": "Uzavřeno" + }, + "actions": { + "reply": "Odpovědět", + "reblog": "Boostnout", + "unreblog": "Undo reblog", + "favorite": "Favorite", + "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" + }, + "tag": { + "url": "URL", + "mention": "Zmínka", + "link": "Odkaz", + "hashtag": "Hashtag", + "email": "E-mail", + "emoji": "Emoji" + }, + "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." + } + }, + "friendship": { + "follow": "Sledovat", + "following": "Following", + "request": "Request", + "pending": "Čekající", + "block": "Blokovat", + "block_user": "Blokovat %s", + "block_domain": "Blokovat %s", + "unblock": "Odblokovat", + "unblock_user": "Odblokovat %s", + "blocked": "Blocked", + "mute": "Skrýt", + "mute_user": "Skrýt %s", + "unmute": "Odkrýt", + "unmute_user": "Odkrýt %s", + "muted": "Skrytý", + "edit_info": "Upravit informace", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" + }, + "timeline": { + "filtered": "Filtered", + "timestamp": { + "now": "Nyní" + }, + "loader": { + "load_missing_posts": "Načíst chybějící příspěvky", + "loading_missing_posts": "Načíst chybějící příspěvky...", + "show_more_replies": "Zobrazit více odovědí" + }, + "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.", + "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": "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.", + "button": { + "category": { + "all": "Vše", + "all_accessiblity_description": "Kategorie: Vše", + "academia": "akademická sféra", + "activism": "aktivismus", + "food": "jídlo", + "furry": "furry", + "games": "games", + "general": "obecné", + "journalism": "žurnalistika", + "lgbt": "lgbt", + "regional": "regionální", + "art": "umění", + "music": "hudba", + "tech": "technologie" + }, + "see_less": "Zobrazit méně", + "see_more": "Zobrazit více" + }, + "label": { + "language": "JAZYK", + "users": "UŽIVATELÉ", + "category": "KATEGORIE" + }, + "input": { + "placeholder": "Hledat servery", + "search_servers_or_enter_url": "Hledat servery nebo zadat URL" + }, + "empty_state": { + "finding_servers": "Hledání dostupných serverů...", + "bad_network": "Při načítání dat nastala chyba. Zkontrolujte připojení k internetu.", + "no_results": "Žádné výsledky" + } + }, + "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": "Smazat" + }, + "username": { + "placeholder": "uživatelské jméno", + "duplicate_prompt": "Toto uživatelské jméno je použito." + }, + "display_name": { + "placeholder": "zobrazované jméno" + }, + "email": { + "placeholder": "e-mail" + }, + "password": { + "placeholder": "heslo", + "require": "Heslo musí být alespoň:", + "character_limit": "8 znaků", + "accessibility": { + "checked": "zaškrtnuto", + "unchecked": "nezaškrtnuto" + }, + "hint": "Vaše heslo musí obsahovat alespoň 8 znaků" + }, + "invite": { + "registration_user_invite_request": "Proč se chcete připojit?" + } + }, + "error": { + "item": { + "username": "Uživatelské jméno", + "email": "E-mail", + "password": "Heslo", + "agreement": "Souhlas", + "locale": "Jazyk", + "reason": "Důvod" + }, + "reason": { + "blocked": "%s contains a disallowed email provider", + "unreachable": "%s does not seem to exist", + "taken": "%s se již používá", + "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" + }, + "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": "Toto není platná e-mailová adresa", + "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": "Pošta", + "open_email_client": "Otevřít e-mailového klienta" + } + }, + "home_timeline": { + "title": "Domů", + "navigation_bar_state": { + "offline": "Offline", + "new_posts": "Nové příspěvky", + "published": "Publikováno!", + "Publishing": "Publikování příspěvku...", + "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": "Nový příspěvek", + "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..." + }, + "poll": { + "duration_time": "Duration: %s", + "thirty_minutes": "30 minut", + "one_hour": "1 hodina", + "six_hours": "6 hodin", + "one_day": "1 den", + "three_days": "3 dny", + "seven_days": "7 dní", + "option_number": "Možnost %ld" + }, + "content_warning": { + "placeholder": "Write an accurate warning here..." + }, + "visibility": { + "public": "Veřejný", + "unlisted": "Neuvedeno", + "private": "Pouze sledující", + "direct": "Pouze lidé, které zmíním" + }, + "auto_complete": { + "space_to_add": "Space to add" + }, + "accessibility": { + "append_attachment": "Přidat přílohu", + "append_poll": "Přidat anketu", + "remove_poll": "Odstranit anketu", + "custom_emoji_picker": "Vlastní výběr Emoji", + "enable_content_warning": "Povolit upozornění na obsah", + "disable_content_warning": "Vypnout upozornění na obsah", + "post_visibility_menu": "Menu viditelnosti příspěvku" + }, + "keyboard": { + "discard_post": "Zahodit příspěvek", + "publish_post": "Publikovat příspěvek", + "toggle_poll": "Přepnout anketu", + "toggle_content_warning": "Toggle Content Warning", + "append_attachment_entry": "Přidat přílohu - %s", + "select_visibility_entry": "Vyberte viditelnost - %s" + } + }, + "profile": { + "header": { + "follows_you": "Sleduje vás" + }, + "dashboard": { + "posts": "příspěvky", + "following": "following", + "followers": "sledující" + }, + "fields": { + "add_row": "Přidat řádek", + "placeholder": { + "label": "Label", + "content": "Obsah" + } + }, + "segmented_control": { + "posts": "Příspěvky", + "replies": "Odpovědí", + "posts_and_replies": "Příspěvky a odpovědi", + "media": "Média", + "about": "About" + }, + "relationship_action_alert": { + "confirm_mute_user": { + "title": "Skrýt účet", + "message": "Potvrdit skrytí %s" + }, + "confirm_unmute_user": { + "title": "Zrušit skrytí účtu", + "message": "Confirm to unmute %s" + }, + "confirm_block_user": { + "title": "Blokovat účet", + "message": "Potvrdit blokování %s" + }, + "confirm_unblock_user": { + "title": "Odblokovat účet", + "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" + } + } +} diff --git a/Localization/StringsConvertor/input/cs.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/cs.lproj/ios-infoPlist.json new file mode 100644 index 000000000..c6db73de0 --- /dev/null +++ b/Localization/StringsConvertor/input/cs.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/StringsConvertor/input/cy.lproj/app.json b/Localization/StringsConvertor/input/cy.lproj/app.json index 710f42b93..bc7f75d96 100644 --- a/Localization/StringsConvertor/input/cy.lproj/app.json +++ b/Localization/StringsConvertor/input/cy.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Unmute", "unmute_user": "Unmute %s", "muted": "Muted", - "edit_info": "Edit Info" + "edit_info": "Edit Info", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Filtered", @@ -455,6 +457,14 @@ "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": { @@ -684,6 +694,9 @@ "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/da.lproj/app.json b/Localization/StringsConvertor/input/da.lproj/app.json index a965b23ae..80b0882d9 100644 --- a/Localization/StringsConvertor/input/da.lproj/app.json +++ b/Localization/StringsConvertor/input/da.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Unmute", "unmute_user": "Unmute %s", "muted": "Muted", - "edit_info": "Edit Info" + "edit_info": "Edit Info", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Filtered", @@ -455,6 +457,14 @@ "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": { @@ -684,6 +694,9 @@ "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/de.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/de.lproj/Localizable.stringsdict index 3ea0fd0e3..c6a8a4297 100644 --- a/Localization/StringsConvertor/input/de.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/de.lproj/Localizable.stringsdict @@ -21,7 +21,7 @@ a11y.plural.count.input_limit_exceeds NSStringLocalizedFormatKey - Eingabelimit überschritten %#@character_count@ + Zeichenanzahl um %#@character_count@ überschritten character_count NSStringFormatSpecTypeKey @@ -37,7 +37,7 @@ a11y.plural.count.input_limit_remains NSStringLocalizedFormatKey - Eingabelimit eingehalten %#@character_count@ + Noch %#@character_count@ übrig character_count NSStringFormatSpecTypeKey @@ -72,9 +72,9 @@ NSStringFormatValueTypeKey ld one - Followed by %1$@, and another mutual + Gefolgt von %1$@ und einer weiteren Person, der du folgst other - Followed by %1$@, and %ld mutuals + Gefolgt von %1$@ und %ld weiteren Personen, denen du folgst plural.count.metric_formatted.post @@ -104,9 +104,9 @@ NSStringFormatValueTypeKey ld one - 1 media + 1 Datei other - %ld media + %ld Dateien plural.count.post @@ -200,7 +200,7 @@ NSStringFormatValueTypeKey ld one - 1 Wähler + 1 Wähler:in other %ld Wähler @@ -216,9 +216,9 @@ NSStringFormatValueTypeKey ld one - 1 Mensch spricht + Eine Person redet other - %ld Leute reden + %ld Personen reden plural.count.following @@ -360,7 +360,7 @@ NSStringFormatValueTypeKey ld one - vor 1 Jahr + vor einem Jahr other vor %ld Jahren @@ -376,7 +376,7 @@ NSStringFormatValueTypeKey ld one - vor 1 M + vor einem Monat other vor %ld Monaten @@ -392,7 +392,7 @@ NSStringFormatValueTypeKey ld one - vor 1 Tag + vor einem Tag other vor %ld Tagen @@ -408,7 +408,7 @@ NSStringFormatValueTypeKey ld one - vor 1 Stunde + vor einer Stunde other vor %ld Stunden @@ -424,7 +424,7 @@ NSStringFormatValueTypeKey ld one - vor 1 Minute + vor einer Minute other vor %ld Minuten @@ -440,7 +440,7 @@ NSStringFormatValueTypeKey ld one - vor 1 Sekunde + vor einer Sekunde other vor %ld Sekuden diff --git a/Localization/StringsConvertor/input/de.lproj/app.json b/Localization/StringsConvertor/input/de.lproj/app.json index 1633a7aba..355bfcc1b 100644 --- a/Localization/StringsConvertor/input/de.lproj/app.json +++ b/Localization/StringsConvertor/input/de.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Nicht mehr stummschalten", "unmute_user": "%s nicht mehr stummschalten", "muted": "Stummgeschaltet", - "edit_info": "Information bearbeiten" + "edit_info": "Information bearbeiten", + "show_reblogs": "Reblogs anzeigen", + "hide_reblogs": "Reblogs ausblenden" }, "timeline": { "filtered": "Gefiltert", @@ -241,7 +243,7 @@ }, "input": { "placeholder": "Nach Server suchen oder URL eingeben", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Nach Server suchen oder URL eingeben" }, "empty_state": { "finding_servers": "Verfügbare Server werden gesucht...", @@ -251,7 +253,7 @@ }, "register": { "title": "Erzähle uns von dir.", - "lets_get_you_set_up_on_domain": "Let’s get you set up on %s", + "lets_get_you_set_up_on_domain": "Okay, lass uns mit %s anfangen", "input": { "avatar": { "delete": "Löschen" @@ -322,7 +324,7 @@ "confirm_email": { "title": "Noch eine letzte Sache.", "subtitle": "Schaue kurz in dein E-Mail-Postfach und tippe den Link an, den wir dir gesendet haben.", - "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": "Schaue kurz in dein E-Mail-Postfach und tippe den Link an, den wir dir gesendet haben", "button": { "open_email_app": "E-Mail-App öffnen", "resend": "Erneut senden" @@ -347,8 +349,8 @@ "published": "Veröffentlicht!", "Publishing": "Beitrag wird veröffentlicht...", "accessibility": { - "logo_label": "Logo Button", - "logo_hint": "Tap to scroll to top and tap again to previous location" + "logo_label": "Logo-Button", + "logo_hint": "Zum Scrollen nach oben tippen und zum vorherigen Ort erneut tippen" } } }, @@ -418,7 +420,7 @@ }, "profile": { "header": { - "follows_you": "Follows You" + "follows_you": "Folgt dir" }, "dashboard": { "posts": "Beiträge", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "Konto entsperren", "message": "Bestätige %s zu entsperren" + }, + "confirm_show_reblogs": { + "title": "Reblogs anzeigen", + "message": "Confirm to show reblogs" + }, + "confirm_hide_reblogs": { + "title": "Reblogs ausblenden", + "message": "Confirm to hide reblogs" } }, "accessibility": { @@ -465,22 +475,22 @@ } }, "follower": { - "title": "follower", + "title": "Follower", "footer": "Follower von anderen Servern werden nicht angezeigt." }, "following": { - "title": "following", + "title": "Folgende", "footer": "Wem das Konto folgt wird von anderen Servern werden nicht angezeigt." }, "familiarFollowers": { - "title": "Followers you familiar", - "followed_by_names": "Followed by %s" + "title": "Follower, die dir bekannt vorkommen", + "followed_by_names": "Gefolgt von %s" }, "favorited_by": { - "title": "Favorited By" + "title": "Favorisiert von" }, "reblogged_by": { - "title": "Reblogged By" + "title": "Geteilt von" }, "search": { "title": "Suche", @@ -546,10 +556,10 @@ "show_mentions": "Erwähnungen anzeigen" }, "follow_request": { - "accept": "Accept", - "accepted": "Accepted", - "reject": "reject", - "rejected": "Rejected" + "accept": "Akzeptieren", + "accepted": "Akzeptiert", + "reject": "Ablehnen", + "rejected": "Abgelehnt" } }, "thread": { @@ -626,46 +636,46 @@ "text_placeholder": "Zusätzliche Kommentare eingeben oder einfügen", "reported": "GEMELDET", "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": "Schritt 1 von 4", + "whats_wrong_with_this_post": "Was stimmt mit diesem Beitrag nicht?", + "whats_wrong_with_this_account": "Was stimmt mit diesem Konto nicht?", + "whats_wrong_with_this_username": "Was ist los mit %s?", + "select_the_best_match": "Wähle die passende Kategorie", + "i_dont_like_it": "Mir gefällt das nicht", + "it_is_not_something_you_want_to_see": "Das ist etwas, das man nicht sehen möchte", + "its_spam": "Das ist Spam", + "malicious_links_fake_engagement_or_repetetive_replies": "Bösartige Links, gefälschtes Engagement oder wiederholte Antworten", + "it_violates_server_rules": "Es verstößt gegen Serverregeln", + "you_are_aware_that_it_breaks_specific_rules": "Du weißt, welche Regeln verletzt werden", + "its_something_else": "Das ist was anderes", + "the_issue_does_not_fit_into_other_categories": "Das Problem passt nicht in die Kategorien" }, "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": "Schritt 2 von 4", + "which_rules_are_being_violated": "Welche Regeln werden verletzt?", + "select_all_that_apply": "Alles Zutreffende auswählen", + "i_just_don’t_like_it": "Das gefällt mir einfach nicht" }, "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": "Schritt 3 von 4", + "are_there_any_posts_that_back_up_this_report": "Gibt es Beiträge, die diesen Bericht unterstützen?", + "select_all_that_apply": "Alles Zutreffende auswählen" }, "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": "Schritt 4 von 4", + "is_there_anything_else_we_should_know": "Gibt es etwas anderes, was wir wissen sollten?" }, "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" + "dont_want_to_see_this": "Du willst das nicht mehr sehen?", + "when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "Wenn du etwas auf Mastodon nicht sehen willst, kannst du den Nutzer aus deiner Erfahrung streichen.", + "unfollow": "Entfolgen", + "unfollowed": "Entfolgt", + "unfollow_user": "%s entfolgen", + "mute_user": "%s stummschalten", + "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "Du wirst die Beiträge vom Konto nicht mehr sehen. Das Konto kann dir immer noch folgen, und die Person hinter dem Konto wird deine Beiträge sehen können und nicht wissen, dass du sie stummgeschaltet hast.", + "block_user": "%s blockieren", + "they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "Du wirst die Beiträge von diesem Konto nicht sehen. Das Konto wird nicht in der Lage sein, deine Beiträge zu sehen oder dir zu folgen. Die Person hinter dem Konto wird wissen, dass du das Konto blockiert hast.", + "while_we_review_this_you_can_take_action_against_user": "Während wir dies überprüfen, kannst du gegen %s vorgehen" } }, "preview": { @@ -684,6 +694,9 @@ "new_in_mastodon": "Neu in Mastodon", "multiple_account_switch_intro_description": "Wechsel zwischen mehreren Konten durch Drücken der Profil-Schaltfläche.", "accessibility_hint": "Doppeltippen, um diesen Assistenten zu schließen" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/en-US.lproj/app.json b/Localization/StringsConvertor/input/en-US.lproj/app.json index a965b23ae..80b0882d9 100644 --- a/Localization/StringsConvertor/input/en-US.lproj/app.json +++ b/Localization/StringsConvertor/input/en-US.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Unmute", "unmute_user": "Unmute %s", "muted": "Muted", - "edit_info": "Edit Info" + "edit_info": "Edit Info", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Filtered", @@ -455,6 +457,14 @@ "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": { @@ -684,6 +694,9 @@ "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/en.lproj/app.json b/Localization/StringsConvertor/input/en.lproj/app.json index a965b23ae..80b0882d9 100644 --- a/Localization/StringsConvertor/input/en.lproj/app.json +++ b/Localization/StringsConvertor/input/en.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Unmute", "unmute_user": "Unmute %s", "muted": "Muted", - "edit_info": "Edit Info" + "edit_info": "Edit Info", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Filtered", @@ -455,6 +457,14 @@ "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": { @@ -684,6 +694,9 @@ "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/es-AR.lproj/app.json b/Localization/StringsConvertor/input/es-AR.lproj/app.json index 9b9f8de5a..62d439a3c 100644 --- a/Localization/StringsConvertor/input/es-AR.lproj/app.json +++ b/Localization/StringsConvertor/input/es-AR.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Dejar de silenciar", "unmute_user": "Dejar de silenciar a %s", "muted": "Silenciado", - "edit_info": "Editar" + "edit_info": "Editar", + "show_reblogs": "Mostrar adhesiones", + "hide_reblogs": "Ocultar adhesiones" }, "timeline": { "filtered": "Filtrado", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "Desbloquear cuenta", "message": "Confirmá para desbloquear a %s" + }, + "confirm_show_reblogs": { + "title": "Mostrar adhesiones", + "message": "Confirmá para mostrar adhesiones" + }, + "confirm_hide_reblogs": { + "title": "Ocultar adhesiones", + "message": "Confirmá para ocultar adhesiones" } }, "accessibility": { @@ -684,6 +694,9 @@ "new_in_mastodon": "Novedad en Mastodon", "multiple_account_switch_intro_description": "Cambiá entre varias cuentas manteniendo presionado el botón del perfil.", "accessibility_hint": "Tocá dos veces para descartar este asistente" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/es.lproj/app.json b/Localization/StringsConvertor/input/es.lproj/app.json index a6d0225d3..39e0f37d1 100644 --- a/Localization/StringsConvertor/input/es.lproj/app.json +++ b/Localization/StringsConvertor/input/es.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Desmutear", "unmute_user": "Desmutear a %s", "muted": "Silenciado", - "edit_info": "Editar Info" + "edit_info": "Editar Info", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Filtrado", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "Desbloquear cuenta", "message": "Confirmar para desbloquear a %s" + }, + "confirm_show_reblogs": { + "title": "Show Reblogs", + "message": "Confirm to show reblogs" + }, + "confirm_hide_reblogs": { + "title": "Hide Reblogs", + "message": "Confirm to hide reblogs" } }, "accessibility": { @@ -684,6 +694,9 @@ "new_in_mastodon": "Nuevo en Mastodon", "multiple_account_switch_intro_description": "Cambie entre varias cuentas manteniendo presionado el botón de perfil.", "accessibility_hint": "Haz doble toque para descartar este asistente" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/eu.lproj/app.json b/Localization/StringsConvertor/input/eu.lproj/app.json index 40b4bd623..5c2e16601 100644 --- a/Localization/StringsConvertor/input/eu.lproj/app.json +++ b/Localization/StringsConvertor/input/eu.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Desmututu", "unmute_user": "Desmututu %s", "muted": "Mutututa", - "edit_info": "Editatu informazioa" + "edit_info": "Editatu informazioa", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Iragazita", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "Desblokeatu kontua", "message": "Berretsi %s desblokeatzea" + }, + "confirm_show_reblogs": { + "title": "Show Reblogs", + "message": "Confirm to show reblogs" + }, + "confirm_hide_reblogs": { + "title": "Hide Reblogs", + "message": "Confirm to hide reblogs" } }, "accessibility": { @@ -684,6 +694,9 @@ "new_in_mastodon": "Berria Mastodonen", "multiple_account_switch_intro_description": "Aldatu hainbat konturen artean profilaren botoia sakatuta edukiz.", "accessibility_hint": "Ukitu birritan morroi hau baztertzeko" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/fi.lproj/app.json b/Localization/StringsConvertor/input/fi.lproj/app.json index 353ba989f..d6210c4d5 100644 --- a/Localization/StringsConvertor/input/fi.lproj/app.json +++ b/Localization/StringsConvertor/input/fi.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Poista mykistys", "unmute_user": "Poista mykistys tililtä %s", "muted": "Mykistetty", - "edit_info": "Muokkaa profiilia" + "edit_info": "Muokkaa profiilia", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Suodatettu", @@ -455,6 +457,14 @@ "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": { @@ -684,6 +694,9 @@ "new_in_mastodon": "Uutta Mastodonissa", "multiple_account_switch_intro_description": "Vaihda useiden tilien välillä pitämällä profiilipainiketta painettuna.", "accessibility_hint": "Hylkää tämä ohjattu toiminto kaksoisnapauttamalla" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/fr.lproj/app.json b/Localization/StringsConvertor/input/fr.lproj/app.json index 6ae2ebb2b..ed53d1096 100644 --- a/Localization/StringsConvertor/input/fr.lproj/app.json +++ b/Localization/StringsConvertor/input/fr.lproj/app.json @@ -9,10 +9,10 @@ "title": "Échec de l'inscription" }, "server_error": { - "title": "Erreur du serveur" + "title": "Erreur serveur" }, "vote_failure": { - "title": "Le vote n’a pas pu être enregistré", + "title": "Échec du vote", "poll_ended": "Le sondage est terminé" }, "discard_post_content": { @@ -180,7 +180,9 @@ "unmute": "Ne plus ignorer", "unmute_user": "Ne plus masquer %s", "muted": "Masqué", - "edit_info": "Éditer les infos" + "edit_info": "Éditer les infos", + "show_reblogs": "Afficher les Reblogs", + "hide_reblogs": "Masquer les Reblogs" }, "timeline": { "filtered": "Filtré", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "Débloquer le compte", "message": "Confirmer le déblocage de %s" + }, + "confirm_show_reblogs": { + "title": "Afficher les Reblogs", + "message": "Confirmer pour afficher les reblogs" + }, + "confirm_hide_reblogs": { + "title": "Masquer les Reblogs", + "message": "Confirmer pour masquer les reblogs" } }, "accessibility": { @@ -469,7 +479,7 @@ "footer": "Les abonné·e·s issus des autres serveurs ne sont pas affiché·e·s." }, "following": { - "title": "following", + "title": "abonnement", "footer": "Les abonnés issus des autres serveurs ne sont pas affichés." }, "familiarFollowers": { @@ -477,10 +487,10 @@ "followed_by_names": "Suivi·e par %s" }, "favorited_by": { - "title": "Favorited By" + "title": "Favoris par" }, "reblogged_by": { - "title": "Reblogged By" + "title": "Reblogué par" }, "search": { "title": "Rechercher", @@ -659,7 +669,7 @@ "dont_want_to_see_this": "Vous ne voulez pas voir cela ?", "when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "Quand vous voyez quelque chose que vous n’aimez pas sur Mastodon, vous pouvez retirer la personne de votre expérience.", "unfollow": "Se désabonner", - "unfollowed": "Unfollowed", + "unfollowed": "Non-suivi", "unfollow_user": "Ne plus suivre %s", "mute_user": "Masquer %s", "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "Vous ne verrez plus leurs messages ou leurs partages dans votre flux personnel. Iels ne sauront pas qu’iels ont été mis en sourdine.", @@ -684,6 +694,9 @@ "new_in_mastodon": "Nouveau dans Mastodon", "multiple_account_switch_intro_description": "Basculez entre plusieurs comptes en appuyant de maniere prolongée sur le bouton profil.", "accessibility_hint": "Tapotez deux fois pour fermer cet assistant" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/gd.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/gd.lproj/Localizable.stringsdict index f041677fa..d0ccb5f41 100644 --- a/Localization/StringsConvertor/input/gd.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/gd.lproj/Localizable.stringsdict @@ -88,13 +88,13 @@ NSStringFormatValueTypeKey ld one - Followed by %1$@, and another mutual + ’Ga leantainn le %1$@ ’s %ld eile an cumantas two - Followed by %1$@, and %ld mutuals + ’Ga leantainn le %1$@ ’s %ld eile an cumantas few - Followed by %1$@, and %ld mutuals + ’Ga leantainn le %1$@ ’s %ld eile an cumantas other - Followed by %1$@, and %ld mutuals + ’Ga leantainn le %1$@ ’s %ld eile an cumantas plural.count.metric_formatted.post @@ -128,13 +128,13 @@ NSStringFormatValueTypeKey ld one - 1 media + %ld mheadhan two - %ld media + %ld mheadhan few - %ld media + %ld meadhanan other - %ld media + %ld meadhan plural.count.post @@ -308,13 +308,13 @@ NSStringFormatValueTypeKey ld one - Tha %ld a’ leantainn air + Tha %ld ’ga leantainn two - Tha %ld a’ leantainn air + Tha %ld ’ga leantainn few - Tha %ld a’ leantainn air + Tha %ld ’ga leantainn other - Tha %ld a’ leantainn air + Tha %ld ’ga leantainn date.year.left diff --git a/Localization/StringsConvertor/input/gd.lproj/app.json b/Localization/StringsConvertor/input/gd.lproj/app.json index e2da7d6a9..a2062a89b 100644 --- a/Localization/StringsConvertor/input/gd.lproj/app.json +++ b/Localization/StringsConvertor/input/gd.lproj/app.json @@ -37,7 +37,7 @@ "confirm": "Clàraich a-mach" }, "block_domain": { - "title": "A bheil thu cinnteach dha-rìribh gu bheil thu airson an àrainn %s a bhacadh uile gu lèir? Mar as trice, foghnaidh gun dèan thu bacadh no mùchadh no dhà gu sònraichte agus bhiod sin na b’ fheàrr. Chan fhaic thu susbaint on àrainn ud agus thèid an luchd-leantainn agad on àrainn ud a thoirt air falbh.", + "title": "A bheil thu cinnteach dha-rìribh gu bheil thu airson an àrainn %s a bhacadh uile gu lèir? Mar as trice, foghnaidh gun dèan thu bacadh no mùchadh no dhà gu sònraichte agus bhiodh sin na b’ fheàrr. Chan fhaic thu susbaint on àrainn ud agus thèid an luchd-leantainn agad on àrainn ud a thoirt air falbh.", "block_entire_domain": "Bac an àrainn" }, "save_photo_failure": { @@ -45,7 +45,7 @@ "message": "Cuir cead inntrigidh do thasg-lann nan dealbhan an comas gus an dealbh a shàbhaladh." }, "delete_post": { - "title": "A bheil thu cinnteach gu bheil thu airson am post seo a sguabadh às?", + "title": "Sguab às am post", "message": "A bheil thu cinnteach gu bheil thu airson am post seo a sguabadh às?" }, "clean_cache": { @@ -165,7 +165,7 @@ } }, "friendship": { - "follow": "Lean air", + "follow": "Lean", "following": "’Ga leantainn", "request": "Iarrtas", "pending": "Ri dhèiligeadh", @@ -180,7 +180,9 @@ "unmute": "Dì-mhùch", "unmute_user": "Dì-mhùch %s", "muted": "’Ga mhùchadh", - "edit_info": "Deasaich" + "edit_info": "Deasaich", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Criathraichte", @@ -211,9 +213,9 @@ "log_in": "Clàraich a-steach" }, "server_picker": { - "title": "Tagh frithealaiche sam bith.", - "subtitle": "Tagh coimhearsnachd stèidhichte air d’ ùidhean no an roinn-dùthcha agad no tè choitcheann.", - "subtitle_extend": "Tagh coimhearsnachd stèidhichte air d’ ùidhean no an roinn-dùthcha agad no tè choitcheann. Tha gach coimhearsnachd ’ga stiùireadh le buidheann no neach gu neo-eisimeileach.", + "title": "Tha cleachdaichean Mhastodon air iomadh frithealaiche eadar-dhealaichte.", + "subtitle": "Tagh frithealaiche stèidhichte air d’ ùidhean, air far a bheil thu no fear coitcheann.", + "subtitle_extend": "Tagh frithealaiche stèidhichte air d’ ùidhean, air far a bheil thu no fear coitcheann. Tha gach frithealaiche fo stiùireadh buidhinn no neach neo-eisimeilich fa leth.", "button": { "category": { "all": "Na h-uile", @@ -240,8 +242,8 @@ "category": "ROINN-SEÒRSA" }, "input": { - "placeholder": "Lorg frithealaiche no gabh pàirt san fhear agad fhèin…", - "search_servers_or_enter_url": "Search servers or enter URL" + "placeholder": "Lorg frithealaiche", + "search_servers_or_enter_url": "Lorg frithealaiche no cuir a-steach URL" }, "empty_state": { "finding_servers": "A’ lorg nam frithealaichean ri am faighinn…", @@ -250,8 +252,8 @@ } }, "register": { - "title": "Innis dhuinn mu do dhèidhinn.", - "lets_get_you_set_up_on_domain": "Let’s get you set up on %s", + "title": "’Gad rèiteachadh air %s", + "lets_get_you_set_up_on_domain": "’Gad rèiteachadh air %s", "input": { "avatar": { "delete": "Sguab às" @@ -310,8 +312,8 @@ } }, "server_rules": { - "title": "Riaghailt bhunasach no dhà.", - "subtitle": "Shuidhich rianairean %s na riaghailtean seo.", + "title": "Riaghailtean bunasach.", + "subtitle": "Tha na riaghailtean seo ’gan stèidheachadh is a chur an gnìomh leis na maoir aig %s.", "prompt": "Ma leanas tu air adhart, bidh thu fo bhuaidh teirmichean seirbheise is poileasaidh prìobhaideachd %s.", "terms_of_service": "teirmichean na seirbheise", "privacy_policy": "poileasaidh prìobhaideachd", @@ -321,8 +323,8 @@ }, "confirm_email": { "title": "Aon rud eile.", - "subtitle": "Tha sinn air post-d a chur gu %s,\nthoir gnogag air a’ chunntas a dhearbhadh a’ chunntais agad.", - "tap_the_link_we_emailed_to_you_to_verify_your_account": "Tap the link we emailed to you to verify your account", + "subtitle": "Thoir gnogag air a’ cheangal a chuir sinn thugad air a’ phost-d airson an cunntas agad a dhearbhadh.", + "tap_the_link_we_emailed_to_you_to_verify_your_account": "Thoir gnogag air a’ cheangal a chuir sinn thugad air a’ phost-d airson an cunntas agad a dhearbhadh", "button": { "open_email_app": "Fosgail aplacaid a’ phuist-d", "resend": "Ath-chuir" @@ -347,14 +349,14 @@ "published": "Chaidh fhoillseachadh!", "Publishing": "A’ foillseachadh a’ phuist…", "accessibility": { - "logo_label": "Logo Button", - "logo_hint": "Tap to scroll to top and tap again to previous location" + "logo_label": "Putan an t-suaicheantais", + "logo_hint": "Thoir gnogag a sgroladh dhan bhàrr is thoir gnogag a-rithist a dhol dhan ionad roimhe" } } }, "suggestion_account": { "title": "Lorg daoine a leanas tu", - "follow_explain": "Nuair a leanas tu air cuideigin, chì thu na puist aca air inbhir na dachaigh agad." + "follow_explain": "Nuair a leanas tu cuideigin, chì thu na puist aca air inbhir na dachaigh agad." }, "compose": { "title": { @@ -418,7 +420,7 @@ }, "profile": { "header": { - "follows_you": "Follows You" + "follows_you": "’Gad leantainn" }, "dashboard": { "posts": "postaichean", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "Dì-bhac an cunntas", "message": "Dearbh dì-bhacadh %s" + }, + "confirm_show_reblogs": { + "title": "Show Reblogs", + "message": "Confirm to show reblogs" + }, + "confirm_hide_reblogs": { + "title": "Hide Reblogs", + "message": "Confirm to hide reblogs" } }, "accessibility": { @@ -465,22 +475,22 @@ } }, "follower": { - "title": "follower", + "title": "neach-leantainn", "footer": "Cha dèid luchd-leantainn o fhrithealaichean eile a shealltainn." }, "following": { - "title": "following", - "footer": "Cha dèid cò air a leanas tu air frithealaichean eile a shealltainn." + "title": "’ga leantainn", + "footer": "Cha dèid cò a leanas tu air frithealaichean eile a shealltainn." }, "familiarFollowers": { - "title": "Followers you familiar", - "followed_by_names": "Followed by %s" + "title": "Luchd-leantainn aithnichte", + "followed_by_names": "’Ga leantainn le %s" }, "favorited_by": { - "title": "Favorited By" + "title": "’Na annsachd aig" }, "reblogged_by": { - "title": "Reblogged By" + "title": "’Ga bhrosnachadh le" }, "search": { "title": "Lorg", @@ -497,8 +507,8 @@ }, "accounts": { "title": "Cunntasan a chòrdas riut ma dh’fhaoidte", - "description": "Saoil am bu toigh leat leantainn air na cunntasan seo?", - "follow": "Lean air" + "description": "Saoil am bu toigh leat na cunntasan seo a leantainn?", + "follow": "Lean" } }, "searching": { @@ -538,7 +548,7 @@ "favorited_your_post": "– is annsa leotha am post agad", "reblogged_your_post": "– ’s iad air am post agad a bhrosnachadh", "mentioned_you": "– ’s iad air iomradh a thoirt ort", - "request_to_follow_you": "iarrtas leantainn ort", + "request_to_follow_you": "iarrtas leantainn", "poll_has_ended": "thàinig cunntas-bheachd gu crìoch" }, "keyobard": { @@ -546,10 +556,10 @@ "show_mentions": "Seall na h-iomraidhean" }, "follow_request": { - "accept": "Accept", - "accepted": "Accepted", - "reject": "reject", - "rejected": "Rejected" + "accept": "Gabh ris", + "accepted": "Air a ghabhail ris", + "reject": "diùlt", + "rejected": "Chaidh a dhiùltadh" } }, "thread": { @@ -575,13 +585,13 @@ "notifications": { "title": "Brathan", "favorites": "Nuair as annsa leotha am post agam", - "follows": "Nuair a leanas iad orm", + "follows": "Nuair a leanas iad mi", "boosts": "Nuair a bhrosnaicheas iad post uam", "mentions": "Nuair a bheir iad iomradh orm", "trigger": { "anyone": "Airson duine sam bith, cuir brath thugam", "follower": "Airson luchd-leantainn, cuir brath thugam", - "follow": "Airson daoine air a leanas mi, cuir brath thugam", + "follow": "Airson daoine a leanas mi, cuir brath thugam", "noone": "Na cuir brath thugam idir", "title": " " } @@ -626,46 +636,46 @@ "text_placeholder": "Sgrìobh no cuir ann beachdan a bharrachd", "reported": "CHAIDH GEARAN A DHÈANAMH", "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": "Ceum 1 à 4", + "whats_wrong_with_this_post": "Dè tha ceàrr leis a’ phost seo?", + "whats_wrong_with_this_account": "Dè tha ceàrr leis an cunntas seo?", + "whats_wrong_with_this_username": "Dè tha ceàrr le %s?", + "select_the_best_match": "Tagh a’ mhaids as fheàrr", + "i_dont_like_it": "Cha toigh leam e", + "it_is_not_something_you_want_to_see": "Chan eil thu airson seo fhaicinn", + "its_spam": "’S e spama a th’ ann", + "malicious_links_fake_engagement_or_repetetive_replies": "Ceanglaichean droch-rùnach, conaltradh fuadain no an dearbh fhreagairt a-rithist ’s a-rithist", + "it_violates_server_rules": "Tha e a’ briseadh riaghailtean an fhrithealaiche", + "you_are_aware_that_it_breaks_specific_rules": "Mhothaich thu gu bheil e a’ briseadh riaghailtean sònraichte", + "its_something_else": "’S rud eile a tha ann", + "the_issue_does_not_fit_into_other_categories": "Chan eil na roinnean-seòrsa eile iomchaidh dhan chùis" }, "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": "Ceum 2 à 4", + "which_rules_are_being_violated": "Dè na riaghailtean a tha ’gam briseadh?", + "select_all_that_apply": "Tagh a h-uile gin a tha iomchaidh", + "i_just_don’t_like_it": "’S ann nach toigh leam e" }, "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": "Ceum 3 à 4", + "are_there_any_posts_that_back_up_this_report": "A bheil postaichean sam bith ann a tha ’nam fianais dhan ghearan seo?", + "select_all_that_apply": "Tagh a h-uile gin a tha iomchaidh" }, "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": "Ceum 4 à 4", + "is_there_anything_else_we_should_know": "A bheil rud sam bith eile a bu toigh leat innse dhuinn?" }, "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" + "dont_want_to_see_this": "Nach eil thu airson seo fhaicinn?", + "when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "Nuair a chì thu rudeigin nach toigh leat air Mastodon, ’s urrainn dhut an neach a chumail fad air falbh uat.", + "unfollow": "Na lean tuilleadh", + "unfollowed": "Chan eil thu ’ga leantainn tuilleadh", + "unfollow_user": "Na lean %s tuilleadh", + "mute_user": "Mùch %s", + "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "Chan fhaic thu na postaichean aca is dè a bhrosnaich iad air inbhir na dachaigh agad tuilleadh. Cha bhi fios aca gun do mhùch thu iad.", + "block_user": "Bac %s", + "they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "Chan urrainn dhaibh ’gad leantainn is chan fhaic iad na postaichean agad tuilleadh ach chì iad gun deach am bacadh.", + "while_we_review_this_you_can_take_action_against_user": "Fhad ’s a bhios sinn a’ toirt sùil air, seo nas urrainn dhut dèanamh an aghaidh %s" } }, "preview": { @@ -684,6 +694,9 @@ "new_in_mastodon": "Na tha ùr ann am Mastodon", "multiple_account_switch_intro_description": "Geàrr leum eadar iomadh cunntas le cumail sìos putan na pròifil.", "accessibility_hint": "Thoir gnogag dhùbailte a’ leigeil seachad an draoidh seo" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index 3df2d70bc..513573f79 100644 --- a/Localization/StringsConvertor/input/gl.lproj/app.json +++ b/Localization/StringsConvertor/input/gl.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Non Acalar", "unmute_user": "Deixar de acalar a @%s", "muted": "Acalada", - "edit_info": "Editar info" + "edit_info": "Editar info", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Filtrado", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "Desbloquear Conta", "message": "Confirma o desbloqueo de %s" + }, + "confirm_show_reblogs": { + "title": "Show Reblogs", + "message": "Confirm to show reblogs" + }, + "confirm_hide_reblogs": { + "title": "Hide Reblogs", + "message": "Confirm to hide reblogs" } }, "accessibility": { @@ -684,6 +694,9 @@ "new_in_mastodon": "Novidade en Mastodon", "multiple_account_switch_intro_description": "Cambia dunha conta a outra mantendo preso o botón do perfil.", "accessibility_hint": "Dobre toque para desbotar este asistente" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/hi.lproj/app.json b/Localization/StringsConvertor/input/hi.lproj/app.json index 23cfa481f..d9ef32b3a 100644 --- a/Localization/StringsConvertor/input/hi.lproj/app.json +++ b/Localization/StringsConvertor/input/hi.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Unmute", "unmute_user": "Unmute %s", "muted": "Muted", - "edit_info": "Edit Info" + "edit_info": "Edit Info", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Filtered", @@ -455,6 +457,14 @@ "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": { @@ -684,6 +694,9 @@ "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/id.lproj/app.json b/Localization/StringsConvertor/input/id.lproj/app.json index f56306e5d..607e9a638 100644 --- a/Localization/StringsConvertor/input/id.lproj/app.json +++ b/Localization/StringsConvertor/input/id.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Berhenti membisukan", "unmute_user": "Berhenti membisukan %s", "muted": "Dibisukan", - "edit_info": "Sunting Info" + "edit_info": "Sunting Info", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Tersaring", @@ -455,6 +457,14 @@ "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": { @@ -684,6 +694,9 @@ "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/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json index b5121b23f..269d299ec 100644 --- a/Localization/StringsConvertor/input/it.lproj/app.json +++ b/Localization/StringsConvertor/input/it.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Riattiva", "unmute_user": "Riattiva %s", "muted": "Silenziato", - "edit_info": "Modifica info" + "edit_info": "Modifica info", + "show_reblogs": "Mostra le condivisioni", + "hide_reblogs": "Nascondi le condivisioni" }, "timeline": { "filtered": "Filtrato", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "Sblocca account", "message": "Conferma per sbloccare %s" + }, + "confirm_show_reblogs": { + "title": "Mostra le condivisioni", + "message": "Conferma di mostrare le condivisioni" + }, + "confirm_hide_reblogs": { + "title": "Nascondi le condivisioni", + "message": "Conferma di nascondere le condivisioni" } }, "accessibility": { @@ -684,6 +694,9 @@ "new_in_mastodon": "Nuovo su Mastodon", "multiple_account_switch_intro_description": "Passa tra più account tenendo premuto il pulsante del profilo.", "accessibility_hint": "Doppio tocco per eliminare questa procedura guidata" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/ja.lproj/app.json b/Localization/StringsConvertor/input/ja.lproj/app.json index 039ef19fc..b7615abf3 100644 --- a/Localization/StringsConvertor/input/ja.lproj/app.json +++ b/Localization/StringsConvertor/input/ja.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "ミュートを解除", "unmute_user": "%sのミュートを解除", "muted": "ミュート済み", - "edit_info": "編集" + "edit_info": "編集", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "フィルター済み", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "アカウントのブロックを解除", "message": "%sのブロックを解除しますか?" + }, + "confirm_show_reblogs": { + "title": "Show Reblogs", + "message": "Confirm to show reblogs" + }, + "confirm_hide_reblogs": { + "title": "Hide Reblogs", + "message": "Confirm to hide reblogs" } }, "accessibility": { @@ -684,6 +694,9 @@ "new_in_mastodon": "Mastodon の新機能", "multiple_account_switch_intro_description": "プロフィールボタンを押して複数のアカウントを切り替えます。", "accessibility_hint": "チュートリアルを閉じるには、ダブルタップしてください" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/kab.lproj/app.json b/Localization/StringsConvertor/input/kab.lproj/app.json index 4ca5e0411..2cff3d68d 100644 --- a/Localization/StringsConvertor/input/kab.lproj/app.json +++ b/Localization/StringsConvertor/input/kab.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Kkes asgugem", "unmute_user": "Kkes asgugem ɣef %s", "muted": "Yettwasgugem", - "edit_info": "Ẓreg talɣut" + "edit_info": "Ẓreg talɣut", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Yettwasizdeg", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "Kkes asewḥel i umiḍan", "message": "Sentem tukksa n usgugem i %s" + }, + "confirm_show_reblogs": { + "title": "Show Reblogs", + "message": "Confirm to show reblogs" + }, + "confirm_hide_reblogs": { + "title": "Hide Reblogs", + "message": "Confirm to hide reblogs" } }, "accessibility": { @@ -548,7 +558,7 @@ "follow_request": { "accept": "Accept", "accepted": "Accepted", - "reject": "reject", + "reject": "agi", "rejected": "Rejected" } }, @@ -684,6 +694,9 @@ "new_in_mastodon": "Amaynut deg Maṣṭudun", "multiple_account_switch_intro_description": "Beddel gar waṭas n yimiḍanen s tussda ɣezzifen ɣef tqeffalt n umaɣnu.", "accessibility_hint": "Sin isitiyen i usefsex n umarag-a" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/kmr.lproj/app.json b/Localization/StringsConvertor/input/kmr.lproj/app.json index df87daffe..d48edf3ae 100644 --- a/Localization/StringsConvertor/input/kmr.lproj/app.json +++ b/Localization/StringsConvertor/input/kmr.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Bêdeng neke", "unmute_user": "%s bêdeng neke", "muted": "Bêdengkirî", - "edit_info": "Zanyariyan serrast bike" + "edit_info": "Zanyariyan serrast bike", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Parzûnkirî", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "Astengiyê li ser ajimêr rake", "message": "Ji bo rakirina astengkirinê %s bipejirîne" + }, + "confirm_show_reblogs": { + "title": "Show Reblogs", + "message": "Confirm to show reblogs" + }, + "confirm_hide_reblogs": { + "title": "Hide Reblogs", + "message": "Confirm to hide reblogs" } }, "accessibility": { @@ -684,6 +694,9 @@ "new_in_mastodon": "Nû di Mastodon de", "multiple_account_switch_intro_description": "Dest bide ser bişkoja profîlê da ku di navbera gelek ajimêrann de biguherînî.", "accessibility_hint": "Du caran bitikîne da ku çarçoveyahilpekok ji holê rakî" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/ko.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/ko.lproj/Localizable.stringsdict index d03431fa0..9628be614 100644 --- a/Localization/StringsConvertor/input/ko.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/ko.lproj/Localizable.stringsdict @@ -13,7 +13,7 @@ NSStringFormatValueTypeKey ld other - %ld unread notification + 읽지 않은 알림 %ld개 a11y.plural.count.input_limit_exceeds @@ -27,7 +27,7 @@ NSStringFormatValueTypeKey ld other - %ld characters + %ld 글자 a11y.plural.count.input_limit_remains @@ -41,7 +41,7 @@ NSStringFormatValueTypeKey ld other - %ld characters + %ld 글자 plural.count.followed_by_and_mutual @@ -106,7 +106,7 @@ NSStringFormatValueTypeKey ld other - %ld posts + %ld 개의 게시물 plural.count.favorite @@ -148,7 +148,7 @@ NSStringFormatValueTypeKey ld other - %ld replies + %ld 개의 답글 plural.count.vote @@ -204,7 +204,7 @@ NSStringFormatValueTypeKey ld other - %ld following + %ld 팔로잉 plural.count.follower @@ -218,7 +218,7 @@ NSStringFormatValueTypeKey ld other - %ld followers + %ld 팔로워 date.year.left @@ -232,7 +232,7 @@ NSStringFormatValueTypeKey ld other - %ld years left + %ld 년 남음 date.month.left @@ -246,7 +246,7 @@ NSStringFormatValueTypeKey ld other - %ld months left + %ld 달 남음 date.day.left @@ -260,7 +260,7 @@ NSStringFormatValueTypeKey ld other - %ld days left + %ld 일 남음 date.hour.left @@ -274,7 +274,7 @@ NSStringFormatValueTypeKey ld other - %ld hours left + %ld 시간 남음 date.minute.left @@ -288,7 +288,7 @@ NSStringFormatValueTypeKey ld other - %ld minutes left + %ld 분 남음 date.second.left @@ -302,7 +302,7 @@ NSStringFormatValueTypeKey ld other - %ld seconds left + %ld 초 남음 date.year.ago.abbr @@ -316,7 +316,7 @@ NSStringFormatValueTypeKey ld other - %ldy ago + %ld 년 전 date.month.ago.abbr @@ -330,7 +330,7 @@ NSStringFormatValueTypeKey ld other - %ldM ago + %ld 달 전 date.day.ago.abbr @@ -344,7 +344,7 @@ NSStringFormatValueTypeKey ld other - %ldd ago + %ld 일 전 date.hour.ago.abbr @@ -358,7 +358,7 @@ NSStringFormatValueTypeKey ld other - %ldh ago + %ld 시간 전 date.minute.ago.abbr @@ -372,7 +372,7 @@ NSStringFormatValueTypeKey ld other - %ldm ago + %ld 분 전 date.second.ago.abbr @@ -386,7 +386,7 @@ NSStringFormatValueTypeKey ld other - %lds ago + %ld 초 전 diff --git a/Localization/StringsConvertor/input/ko.lproj/app.json b/Localization/StringsConvertor/input/ko.lproj/app.json index b66808919..bbb4d1dea 100644 --- a/Localization/StringsConvertor/input/ko.lproj/app.json +++ b/Localization/StringsConvertor/input/ko.lproj/app.json @@ -130,7 +130,7 @@ "show_user_profile": "사용자 프로필 보기", "content_warning": "열람 주의", "sensitive_content": "Sensitive Content", - "media_content_warning": "Tap anywhere to reveal", + "media_content_warning": "아무 곳이나 눌러서 보기", "tap_to_reveal": "눌러서 확인", "poll": { "vote": "투표", @@ -145,8 +145,8 @@ "menu": "메뉴", "hide": "숨기기", "show_image": "이미지 표시", - "show_gif": "Show GIF", - "show_video_player": "Show video player", + "show_gif": "GIF 보기", + "show_video_player": "비디오 플레이어 보기", "tap_then_hold_to_show_menu": "Tap then hold to show menu" }, "tag": { @@ -180,7 +180,9 @@ "unmute": "뮤트 해제", "unmute_user": "%s 뮤트 해제", "muted": "뮤트됨", - "edit_info": "정보 편집" + "edit_info": "정보 편집", + "show_reblogs": "리블로그 보기", + "hide_reblogs": "리블로그 가리기" }, "timeline": { "filtered": "필터됨", @@ -207,8 +209,8 @@ "scene": { "welcome": { "slogan": "소셜 네트워킹을\n여러분의 손에 돌려드립니다.", - "get_started": "Get Started", - "log_in": "Log In" + "get_started": "시작하기", + "log_in": "로그인" }, "server_picker": { "title": "서버를 고르세요,\n아무 서버나 좋습니다.", @@ -240,18 +242,18 @@ "category": "분류" }, "input": { - "placeholder": "Search servers", - "search_servers_or_enter_url": "Search servers or enter URL" + "placeholder": "서버 검색", + "search_servers_or_enter_url": "서버를 검색하거나 URL을 입력하세요" }, "empty_state": { - "finding_servers": "Finding available servers...", + "finding_servers": "사용 가능한 서버를 찾는 중입니다...", "bad_network": "Something went wrong while loading the data. Check your internet connection.", "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", + "title": "%s에 가입하기 위한 정보들을 입력하세요", + "lets_get_you_set_up_on_domain": "%s에 가입하기 위한 정보들을 입력하세요", "input": { "avatar": { "delete": "삭제" @@ -268,11 +270,11 @@ }, "password": { "placeholder": "암호", - "require": "Your password needs at least:", - "character_limit": "8 characters", + "require": "암호의 최소 요구사항:", + "character_limit": "8글자", "accessibility": { - "checked": "checked", - "unchecked": "unchecked" + "checked": "확인됨", + "unchecked": "확인되지 않음" }, "hint": "암호는 최소 8글자 이상이어야 합니다" }, @@ -285,7 +287,7 @@ "username": "사용자명", "email": "이메일", "password": "암호", - "agreement": "Agreement", + "agreement": "약관", "locale": "지역설정", "reason": "사유" }, @@ -295,26 +297,26 @@ "taken": "%s는 이미 사용 중입니다", "reserved": "%s는 예약된 키워드입니다", "accepted": "%s는 반드시 동의해야 합니다", - "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" + "blank": "%s 항목은 필수입니다", + "invalid": "%s 항목이 잘못되었습니다", + "too_long": "%s 항목이 너무 깁니다", + "too_short": "%s 항목이 너무 짧습니다", + "inclusion": "%s 는 지원되는 값이 아닙니다" }, "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)" + "username_invalid": "사용자명은 라틴문자와 숫자 그리고 밑줄만 사용할 수 있습니다", + "username_too_long": "사용자명이 너무 깁니다 (30글자를 넘을 수 없습니다)", + "email_invalid": "올바른 이메일 주소가 아닙니다", + "password_too_short": "암호가 너무 짧습니다 (최소 8글자 이상이어야 합니다)" } } }, "server_rules": { - "title": "Some ground rules.", - "subtitle": "These are set and enforced by the %s moderators.", + "title": "몇 개의 규칙이 있습니다.", + "subtitle": "다음은 %s의 중재자들에 의해 설정되고 적용되는 규칙들입니다.", "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", + "terms_of_service": "이용약관", + "privacy_policy": "개인정보 정책", "button": { "confirm": "동의합니다" } @@ -325,29 +327,29 @@ "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" + "resend": "재전송" }, "dont_receive_email": { - "title": "Check your email", + "title": "이메일을 확인하세요", "description": "Check if your email address is correct as well as your junk folder if you haven’t.", - "resend_email": "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" + "title": "받은 편지함을 확인하세요.", + "description": "이메일을 보냈습니다. 만약 받지 못했다면 스팸메일함을 확인하세요.", + "mail": "메일", + "open_email_client": "이메일 앱 열기" } }, "home_timeline": { - "title": "Home", + "title": "홈", "navigation_bar_state": { "offline": "오프라인", "new_posts": "새 글 보기", "published": "게시됨!", - "Publishing": "Publishing post...", + "Publishing": "게시물 게시중...", "accessibility": { - "logo_label": "Logo Button", + "logo_label": "로고 버튼", "logo_hint": "Tap to scroll to top and tap again to previous location" } } @@ -358,20 +360,20 @@ }, "compose": { "title": { - "new_post": "New Post", - "new_reply": "New Reply" + "new_post": "새 게시물", + "new_reply": "새 답글" }, "media_selection": { - "camera": "Take Photo", - "photo_library": "Photo Library", - "browse": "Browse" + "camera": "사진 촬영", + "photo_library": "사진 보관함", + "browse": "둘러보기" }, - "content_input_placeholder": "Type or paste what’s on your mind", - "compose_action": "Publish", - "replying_to_user": "replying to %s", + "content_input_placeholder": "무슨 생각을 하고 있는지 입력하거나 붙여넣으세요", + "compose_action": "게시", + "replying_to_user": "%s 님에게 답장 중", "attachment": { - "photo": "photo", - "video": "video", + "photo": "사진", + "video": "동영상", "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", "description_photo": "시각장애인을 위한 사진 설명…", "description_video": "시각장애인을 위한 영상 설명…" @@ -437,7 +439,7 @@ "replies": "답글", "posts_and_replies": "Posts and Replies", "media": "미디어", - "about": "About" + "about": "정보" }, "relationship_action_alert": { "confirm_mute_user": { @@ -449,38 +451,46 @@ "message": "%s 뮤트 해제 확인" }, "confirm_block_user": { - "title": "Block Account", + "title": "계정 차단", "message": "Confirm to block %s" }, "confirm_unblock_user": { - "title": "Unblock Account", + "title": "계정 차단 해제", "message": "Confirm to unblock %s" + }, + "confirm_show_reblogs": { + "title": "리블로그 보기", + "message": "리블로그를 보기 전 확인" + }, + "confirm_hide_reblogs": { + "title": "리블로그 가리기", + "message": "리블로그를 가리기 전 확인" } }, "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" + "edit_avatar_image": "아바타 이미지 변경", + "show_banner_image": "배너 이미지 표시", + "double_tap_to_open_the_list": "두 번 탭하여 리스트 표시" } }, "follower": { - "title": "follower", - "footer": "Followers from other servers are not displayed." + "title": "팔로워", + "footer": "다른 서버의 팔로워 표시는 할 수 없습니다." }, "following": { - "title": "following", + "title": "팔로잉", "footer": "Follows from other servers are not displayed." }, "familiarFollowers": { "title": "Followers you familiar", - "followed_by_names": "Followed by %s" + "followed_by_names": "%s 님이 팔로우함" }, "favorited_by": { - "title": "Favorited By" + "title": "마음에 들어한 사람들" }, "reblogged_by": { - "title": "Reblogged By" + "title": "리블로그한 사람들" }, "search": { "title": "검색", @@ -489,61 +499,61 @@ "cancel": "취소" }, "recommend": { - "button_text": "See All", + "button_text": "모두 보기", "hash_tag": { - "title": "Trending on Mastodon", + "title": "마스토돈에서 유행 중", "description": "Hashtags that are getting quite a bit of attention", - "people_talking": "%s people are talking" + "people_talking": "%s 명의 사람들이 말하고 있음" }, "accounts": { - "title": "Accounts you might like", + "title": "마음에 들어할만한 계정", "description": "You may like to follow these accounts", - "follow": "Follow" + "follow": "팔로우" } }, "searching": { "segment": { - "all": "All", - "people": "People", - "hashtags": "Hashtags", - "posts": "Posts" + "all": "전부", + "people": "사람", + "hashtags": "해시태그", + "posts": "게시물" }, "empty_state": { - "no_results": "No results" + "no_results": "결과가 없습니다" }, - "recent_search": "Recent searches", - "clear": "Clear" + "recent_search": "최근 검색", + "clear": "모두 지우기" } }, "discovery": { "tabs": { - "posts": "Posts", - "hashtags": "Hashtags", - "news": "News", - "community": "Community", - "for_you": "For You" + "posts": "게시물", + "hashtags": "해시태그", + "news": "소식", + "community": "커뮤니티", + "for_you": "당신을 위한 추천" }, "intro": "These are the posts gaining traction in your corner of Mastodon." }, "favorite": { - "title": "Your Favorites" + "title": "내 즐겨찾기" }, "notification": { "title": { - "Everything": "Everything", - "Mentions": "Mentions" + "Everything": "모두", + "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" + "followed_you": "나를 팔로우 했습니다", + "favorited_your_post": "내 게시물을 마음에 들어했습니다", + "reblogged_your_post": "내 게시물을 리블로그 했습니다", + "mentioned_you": "나를 언급했습니다", + "request_to_follow_you": "팔로우를 요청합니다", + "poll_has_ended": "투표가 끝났습니다" }, "keyobard": { - "show_everything": "Show Everything", - "show_mentions": "Show Mentions" + "show_everything": "모두 보기", + "show_mentions": "멘션 보기" }, "follow_request": { "accept": "수락", @@ -553,37 +563,37 @@ } }, "thread": { - "back_title": "Post", - "title": "Post from %s" + "back_title": "게시물", + "title": "%s 님의 게시물" }, "settings": { - "title": "Settings", + "title": "설정", "section": { "appearance": { - "title": "Appearance", - "automatic": "Automatic", - "light": "Always Light", - "dark": "Always Dark" + "title": "외관", + "automatic": "자동", + "light": "항상 밝음", + "dark": "항상 어두움" }, "look_and_feel": { - "title": "Look and Feel", - "use_system": "Use System", - "really_dark": "Really Dark", - "sorta_dark": "Sorta Dark", - "light": "Light" + "title": "인터페이스", + "use_system": "시스템 설정 사용", + "really_dark": "진짜 어두움", + "sorta_dark": "좀 어두움", + "light": "밝음" }, "notifications": { - "title": "Notifications", - "favorites": "Favorites my post", - "follows": "Follows me", - "boosts": "Reblogs my post", - "mentions": "Mentions me", + "title": "알림", + "favorites": "내 게시물을 마음에 들어할 때", + "follows": "나를 팔로우 할 때", + "boosts": "내 게시물을 리블로그 할 때", + "mentions": "나를 언급할 때", "trigger": { - "anyone": "anyone", - "follower": "a follower", - "follow": "anyone I follow", - "noone": "no one", - "title": "Notify me when" + "anyone": "누구든", + "follower": "팔로워가", + "follow": "내가 팔로우하는 사람이", + "noone": "아무도 못 하게", + "title": "알림을 보낼 조건은" } }, "preference": { @@ -592,7 +602,7 @@ "disable_avatar_animation": "움직이는 아바타 비활성화", "disable_emoji_animation": "움직이는 에모지 비활성화", "using_default_browser": "기본 브라우저로 링크 열기", - "open_links_in_mastodon": "Open links in Mastodon" + "open_links_in_mastodon": "마스토돈에서 링크 열기" }, "boring_zone": { "title": "지루한 영역", @@ -614,57 +624,57 @@ } }, "report": { - "title_report": "Report", + "title_report": "신고", "title": "%s 신고하기", "step1": "1단계 (총 2단계)", "step2": "2단계 (총 2단계)", "content1": "신고에 추가하고 싶은 다른 게시물이 존재하나요?", "content2": "이 신고에 대해 중재자들이 알아야 할 것이 있나요?", - "report_sent_title": "Thanks for reporting, we’ll look into this.", + "report_sent_title": "신고해주셔서 감사합니다, 중재자분들이 확인할 예정입니다.", "send": "신고 전송", "skip_to_send": "추가설명 없이 보내기", "text_placeholder": "추가 설명을 적거나 붙여넣으세요", - "reported": "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": "1단계 (총 4단계)", + "whats_wrong_with_this_post": "이 게시물에 어떤 문제가 있나요?", + "whats_wrong_with_this_account": "이 계정에 어떤 문제가 있나요?", + "whats_wrong_with_this_username": "%s에 어떤 문제가 있나요?", + "select_the_best_match": "가장 알맞은 것을 선택하세요", + "i_dont_like_it": "마음에 안 듭니다", + "it_is_not_something_you_want_to_see": "내가 보기 싫은 종류에 속합니다", + "its_spam": "스팸입니다", + "malicious_links_fake_engagement_or_repetetive_replies": "악성 링크, 반응 스팸, 또는 반복적인 답글", + "it_violates_server_rules": "서버 규칙을 위반합니다", + "you_are_aware_that_it_breaks_specific_rules": "특정 규칙을 위반합니다", + "its_something_else": "기타", + "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_2_of_4": "2단계 (총 4단계)", + "which_rules_are_being_violated": "어떤 규칙을 위반했나요?", + "select_all_that_apply": "해당하는 사항을 모두 선택하세요", + "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_3_of_4": "3단계 (총 4단계)", + "are_there_any_posts_that_back_up_this_report": "이 신고에 대해서 더 참고해야 할 게시물이 있나요?", + "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_4_of_4": "4단계 (총 4단계)", + "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.", + "dont_want_to_see_this": "이런 것을 보지 않길 원하나요?", + "when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "마스토돈에서 보기 싫은 것을 보았다면, 해당하는 사람을 지울 수 있습니다.", + "unfollow": "팔로우 해제", + "unfollowed": "팔로우 해제함", + "unfollow_user": "%s 팔로우 해제", + "mute_user": "%s 뮤트", + "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "그의 게시물이나 리블로그가 내 홈 피드에 보이지 않습니다. 그는 뮤트 당했다는 사실을 알지 못합니다.", + "block_user": "%s 차단", + "they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "그는 나를 팔로우 하거나 내 게시물을 볼 수 없게 됩니다, 하지만 내가 차단한 사실은 알 수 있습니다.", "while_we_review_this_you_can_take_action_against_user": "서버의 중재자들이 이것을 심사하는 동안, 당신은 %s에 대한 행동을 취할 수 있습니다" } }, @@ -678,12 +688,15 @@ "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" + "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" + "new_in_mastodon": "마스토돈의 새 기능", + "multiple_account_switch_intro_description": "프로필 버튼을 꾹 눌러서 여러 계정 사이를 전환할 수 있습니다.", + "accessibility_hint": "두 번 탭하여 팝업 닫기" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/ko.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/ko.lproj/ios-infoPlist.json index 1b073eb0f..23194a120 100644 --- a/Localization/StringsConvertor/input/ko.lproj/ios-infoPlist.json +++ b/Localization/StringsConvertor/input/ko.lproj/ios-infoPlist.json @@ -1,5 +1,5 @@ { - "NSCameraUsageDescription": "Used to take photo for post status", + "NSCameraUsageDescription": "게시물에 사용할 사진을 찍기 위해 쓰임", "NSPhotoLibraryAddUsageDescription": "갤러리에 사진을 저장하기 위해 쓰임", "NewPostShortcutItemTitle": "새 글", "SearchShortcutItemTitle": "검색" diff --git a/Localization/StringsConvertor/input/lv.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/lv.lproj/Localizable.stringsdict new file mode 100644 index 000000000..25f32c98d --- /dev/null +++ b/Localization/StringsConvertor/input/lv.lproj/Localizable.stringsdict @@ -0,0 +1,505 @@ + + + + + a11y.plural.count.unread.notification + + NSStringLocalizedFormatKey + %#@notification_count_unread_notification@ + notification_count_unread_notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld unread notification + one + 1 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 + %ld characters + one + 1 character + other + %ld characters + + + a11y.plural.count.input_limit_remains + + NSStringLocalizedFormatKey + Input limit remains %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld characters + one + 1 character + other + %ld characters + + + plural.count.followed_by_and_mutual + + NSStringLocalizedFormatKey + %#@names@%#@count_mutual@ + names + + zero + + one + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + + + count_mutual + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + Followed by %1$@, and %ld mutuals + one + Followed by %1$@, and another mutual + 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 + other + posts + + + plural.count.media + + NSStringLocalizedFormatKey + %#@media_count@ + media_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld media + one + 1 media + other + %ld media + + + plural.count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld posts + one + 1 post + other + %ld posts + + + plural.count.favorite + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld favorites + one + 1 favorite + other + %ld favorites + + + plural.count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld reblogs + one + 1 reblog + other + %ld reblogs + + + plural.count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld replies + one + 1 reply + other + %ld replies + + + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld votes + one + 1 vote + other + %ld votes + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld voters + one + 1 voter + other + %ld voters + + + plural.people_talking + + NSStringLocalizedFormatKey + %#@count_people_talking@ + count_people_talking + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld people talking + one + 1 people talking + other + %ld people talking + + + plural.count.following + + NSStringLocalizedFormatKey + %#@count_following@ + count_following + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld following + one + 1 following + other + %ld following + + + plural.count.follower + + NSStringLocalizedFormatKey + %#@count_follower@ + count_follower + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld followers + one + 1 follower + other + %ld followers + + + date.year.left + + NSStringLocalizedFormatKey + %#@count_year_left@ + count_year_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld years left + one + 1 year left + other + %ld years left + + + date.month.left + + NSStringLocalizedFormatKey + %#@count_month_left@ + count_month_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld months left + one + 1 months left + other + %ld months left + + + date.day.left + + NSStringLocalizedFormatKey + %#@count_day_left@ + count_day_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld days left + one + 1 day left + other + %ld days left + + + date.hour.left + + NSStringLocalizedFormatKey + %#@count_hour_left@ + count_hour_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld hours left + one + 1 hour left + other + %ld hours left + + + date.minute.left + + NSStringLocalizedFormatKey + %#@count_minute_left@ + count_minute_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld minutes left + one + 1 minute left + other + %ld minutes left + + + date.second.left + + NSStringLocalizedFormatKey + %#@count_second_left@ + count_second_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld seconds left + one + 1 second left + other + %ld seconds left + + + date.year.ago.abbr + + NSStringLocalizedFormatKey + %#@count_year_ago_abbr@ + count_year_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ldy ago + one + 1y ago + other + %ldy ago + + + date.month.ago.abbr + + NSStringLocalizedFormatKey + %#@count_month_ago_abbr@ + count_month_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ldM ago + one + 1M ago + other + %ldM ago + + + date.day.ago.abbr + + NSStringLocalizedFormatKey + %#@count_day_ago_abbr@ + count_day_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ldd ago + one + 1d ago + other + %ldd ago + + + date.hour.ago.abbr + + NSStringLocalizedFormatKey + %#@count_hour_ago_abbr@ + count_hour_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ldh ago + one + 1h ago + other + %ldh ago + + + date.minute.ago.abbr + + NSStringLocalizedFormatKey + %#@count_minute_ago_abbr@ + count_minute_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ldm ago + one + 1m ago + other + %ldm ago + + + date.second.ago.abbr + + NSStringLocalizedFormatKey + %#@count_second_ago_abbr@ + count_second_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %lds ago + one + 1s ago + other + %lds ago + + + + diff --git a/Localization/StringsConvertor/input/lv.lproj/app.json b/Localization/StringsConvertor/input/lv.lproj/app.json new file mode 100644 index 000000000..0051383db --- /dev/null +++ b/Localization/StringsConvertor/input/lv.lproj/app.json @@ -0,0 +1,702 @@ +{ + "common": { + "alerts": { + "common": { + "please_try_again": "Lūdzu, mēģiniet vēlreiz.", + "please_try_again_later": "Lūdzu, mēģiniet vēlreiz vēlāk." + }, + "sign_up_failure": { + "title": "Sign Up Failure" + }, + "server_error": { + "title": "Servera kļūda" + }, + "vote_failure": { + "title": "Vote Failure", + "poll_ended": "Balsošana beidzās" + }, + "discard_post_content": { + "title": "Atmest malnrakstu", + "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": "Iziet", + "message": "Vai tiešām vēlaties iziet?", + "confirm": "Iziet" + }, + "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": "Dzēst ierakstu", + "message": "Vai tiešām vēlies dzēst ierakstu?" + }, + "clean_cache": { + "title": "Clean Cache", + "message": "Successfully cleaned %s cache." + } + }, + "controls": { + "actions": { + "back": "Atpakaļ", + "next": "Nākamais", + "previous": "Iepriekšējais", + "open": "Atvērt", + "add": "Pievienot", + "remove": "Noņemt", + "edit": "Rediģēt", + "save": "Saglabāt", + "ok": "Labi", + "done": "Pabeigts", + "confirm": "Apstiprināt", + "continue": "Turpināt", + "compose": "Rakstīt", + "cancel": "Atcelt", + "discard": "Atmest", + "try_again": "Mēģināt vēlreiz", + "take_photo": "Uzņemt bildi", + "save_photo": "Saglabāt bildi", + "copy_photo": "Kopēt bildi", + "sign_in": "Pieteikties", + "sign_up": "Reģistrēties", + "see_more": "Skatīt vairāk", + "preview": "Priekšskatījums", + "share": "Dalīties", + "share_user": "Share %s", + "share_post": "Share Post", + "open_in_safari": "Atvērt Safari", + "open_in_browser": "Atvērt pārlūkprogrammā", + "find_people": "Atrodi cilvēkus kam sekot", + "manually_search": "Manually search instead", + "skip": "Izlaist", + "reply": "Atbildēt", + "report_user": "Ziņot par lietotāju @%s", + "block_domain": "Bloķēt %s", + "unblock_domain": "Atbloķēt %s", + "settings": "Iestatījumi", + "delete": "Dzēst" + }, + "tabs": { + "home": "Sākums", + "search": "Meklēšana", + "notification": "Paziņojums", + "profile": "Profils" + }, + "keyboard": { + "common": { + "switch_to_tab": "Pārslēgties uz: %s", + "compose_new_post": "Veidot jaunu ziņu", + "show_favorites": "Show Favorites", + "open_settings": "Atvērt iestatījumus" + }, + "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": "Priekšskata attēls" + }, + "segmented_control": { + "previous_section": "Iepriekšējā sadaļa", + "next_section": "Nākamā sadaļa" + } + }, + "status": { + "user_reblogged": "%s reblogged", + "user_replied_to": "Replied to %s", + "show_post": "Show Post", + "show_user_profile": "Parādīt lietotāja profilu", + "content_warning": "Satura brīdinājums", + "sensitive_content": "Sensitīvs saturs", + "media_content_warning": "Tap anywhere to reveal", + "tap_to_reveal": "Tap to reveal", + "poll": { + "vote": "Balsot", + "closed": "Aizvērts" + }, + "actions": { + "reply": "Atbildēt", + "reblog": "Reblogot", + "unreblog": "Undo reblog", + "favorite": "Izlase", + "unfavorite": "Izņemt no izlases", + "menu": "Izvēlne", + "hide": "Slēpt", + "show_image": "Rādīt attēlu", + "show_gif": "Rādīt GIF", + "show_video_player": "Show video player", + "tap_then_hold_to_show_menu": "Tap then hold to show menu" + }, + "tag": { + "url": "URL", + "mention": "Pieminēt", + "link": "Saite", + "hashtag": "Hashtag", + "email": "E-pasts", + "emoji": "Emocijzīmes" + }, + "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": "Sekot", + "following": "Seko", + "request": "Pieprasījums", + "pending": "Gaida", + "block": "Bloķēt", + "block_user": "Bloķēt %s", + "block_domain": "Bloķēt %s", + "unblock": "Atbloķēt", + "unblock_user": "Atbloķēt %s", + "blocked": "Bloķēts", + "mute": "Apklusināt", + "mute_user": "Aplusināt %s", + "unmute": "Noņemt apklusinājumu", + "unmute_user": "Noņemt apklusinājumu @%s", + "muted": "Apklusināts", + "edit_info": "Edit Info", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" + }, + "timeline": { + "filtered": "Filtrēts", + "timestamp": { + "now": "Tagad" + }, + "loader": { + "load_missing_posts": "Load missing posts", + "loading_missing_posts": "Loading missing posts...", + "show_more_replies": "Rādīt vairāk atbildes" + }, + "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": "Pieteikties" + }, + "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": "Visi", + "all_accessiblity_description": "Katekorija: Visi", + "academia": "academia", + "activism": "activism", + "food": "ēdiens", + "furry": "furry", + "games": "spēles", + "general": "general", + "journalism": "žurnālisms", + "lgbt": "lgbt", + "regional": "regionāli", + "art": "māksla", + "music": "mūzika", + "tech": "tehnoloģija" + }, + "see_less": "Rādīt mazāk", + "see_more": "Skatīt vairāk" + }, + "label": { + "language": "VALODA", + "users": "LIETOTĀJI", + "category": "KATEGORIJA" + }, + "input": { + "placeholder": "Meklēt serverus", + "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": "Nav rezultātu" + } + }, + "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": "Dzēst" + }, + "username": { + "placeholder": "lietotājvārds", + "duplicate_prompt": "Šis lietotājvārds jau ir aizņemts." + }, + "display_name": { + "placeholder": "parādāmais vārds" + }, + "email": { + "placeholder": "e-pasts" + }, + "password": { + "placeholder": "parole", + "require": "Your password needs at least:", + "character_limit": "8 rakstzīmes", + "accessibility": { + "checked": "atzīmēts", + "unchecked": "neatzīmēts" + }, + "hint": "Parolei jābūt vismaz 8 simboliem" + }, + "invite": { + "registration_user_invite_request": "Kāpēc tu vēlies pievienoties?" + } + }, + "error": { + "item": { + "username": "Lietotājvārds", + "email": "E-pasts", + "password": "Parole", + "agreement": "Līgums", + "locale": "Lokalizācija", + "reason": "Iemesls" + }, + "reason": { + "blocked": "%s contains a disallowed email provider", + "unreachable": "%s šķiet, ka neeksistē", + "taken": "%s jau tiek izmantots", + "reserved": "%s is a reserved keyword", + "accepted": "%s must be accepted", + "blank": "%s ir obligāts", + "invalid": "%s ir nederīgs", + "too_long": "%s ir pārāk garaš", + "too_short": "%s ir pārāk īs", + "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": "pakalpojuma noteikumi", + "privacy_policy": "privātuma nosacījumi", + "button": { + "confirm": "Es piekrītu" + } + }, + "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": "Nosūtīt atkārtoti" + }, + "dont_receive_email": { + "title": "Pārbaudi savu e-pastu", + "description": "Check if your email address is correct as well as your junk folder if you haven’t.", + "resend_email": "Atkārtoti nosūtīt e-pastu" + }, + "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": "Jauna atbilde" + }, + "media_selection": { + "camera": "Uzņemt bildi", + "photo_library": "Attēlu krātuve", + "browse": "Pārlūkot" + }, + "content_input_placeholder": "Type or paste what’s on your mind", + "compose_action": "Publicēt", + "replying_to_user": "replying to %s", + "attachment": { + "photo": "attēls", + "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..." + }, + "poll": { + "duration_time": "Duration: %s", + "thirty_minutes": "30 minūtes", + "one_hour": "1 Stunda", + "six_hours": "6 stundas", + "one_day": "1 Diena", + "three_days": "3 Dienas", + "seven_days": "7 Dienas", + "option_number": "Option %ld" + }, + "content_warning": { + "placeholder": "Write an accurate warning here..." + }, + "visibility": { + "public": "Publisks", + "unlisted": "Neiekļautie", + "private": "Tikai sekotājiem", + "direct": "Tikai cilvēki, kurus es pieminu" + }, + "auto_complete": { + "space_to_add": "Space to add" + }, + "accessibility": { + "append_attachment": "Pievienot pielikumu", + "append_poll": "Pievienot aptauju", + "remove_poll": "Noņemt aptauju", + "custom_emoji_picker": "Custom Emoji Picker", + "enable_content_warning": "Enable Content Warning", + "disable_content_warning": "Disable Content Warning", + "post_visibility_menu": "Post Visibility Menu" + }, + "keyboard": { + "discard_post": "Discard Post", + "publish_post": "Publish Post", + "toggle_poll": "Toggle Poll", + "toggle_content_warning": "Toggle Content Warning", + "append_attachment_entry": "Pievienot pielikumu - %s", + "select_visibility_entry": "Select Visibility - %s" + } + }, + "profile": { + "header": { + "follows_you": "Seko tev" + }, + "dashboard": { + "posts": "posts", + "following": "seko", + "followers": "sekottāji" + }, + "fields": { + "add_row": "Pievienot rindu", + "placeholder": { + "label": "Label", + "content": "Saturs" + } + }, + "segmented_control": { + "posts": "Posts", + "replies": "Atbildes", + "posts_and_replies": "Ziņas un atbildes", + "media": "Multivide", + "about": "Par" + }, + "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": "Bloķēts kontu", + "message": "Confirm to block %s" + }, + "confirm_unblock_user": { + "title": "Atbloķēt kontu", + "message": "Apstiprini lai atbloķētu %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": "sekottājs", + "footer": "Followers from other servers are not displayed." + }, + "following": { + "title": "seko", + "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": "Meklēt", + "search_bar": { + "placeholder": "Search hashtags and users", + "cancel": "Atcelt" + }, + "recommend": { + "button_text": "Skatīt visu", + "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": "Ziņas", + "hashtags": "Hashtags", + "news": "Ziņas", + "community": "Community", + "for_you": "Priekš tevis" + }, + "intro": "These are the posts gaining traction in your corner of Mastodon." + }, + "favorite": { + "title": "Tava izlase" + }, + "notification": { + "title": { + "Everything": "Visi", + "Mentions": "Pieminējumi" + }, + "notification_description": { + "followed_you": "followed you", + "favorited_your_post": "favorited your post", + "reblogged_your_post": "reblogged your post", + "mentioned_you": "pieminēja tevi", + "request_to_follow_you": "request to follow you", + "poll_has_ended": "balsošana beidzās" + }, + "keyobard": { + "show_everything": "Parādīt man visu", + "show_mentions": "Show Mentions" + }, + "follow_request": { + "accept": "Pieņemt", + "accepted": "Pieņemts", + "reject": "noraidīt", + "rejected": "Noraidīts" + } + }, + "thread": { + "back_title": "Ziņa", + "title": "Post from %s" + }, + "settings": { + "title": "Iestatījumi", + "section": { + "appearance": { + "title": "Izskats", + "automatic": "Automātiski", + "light": "Vienmēr gaišs", + "dark": "Vienmēr tumšs" + }, + "look_and_feel": { + "title": "Izskats", + "use_system": "Use System", + "really_dark": "Ļoti tumšs", + "sorta_dark": "Itkā tumšs", + "light": "Gaišs" + }, + "notifications": { + "title": "Paziņojumi", + "favorites": "Favorites my post", + "follows": "Seko man", + "boosts": "Reblogs my post", + "mentions": "Pieminējumi", + "trigger": { + "anyone": "jebkurš", + "follower": "sekottājs", + "follow": "anyone I follow", + "noone": "neviens", + "title": "Notify me when" + } + }, + "preference": { + "title": "Uzstādījumi", + "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": "Konta iestatījumi", + "terms": "Pakalpojuma noteikumi", + "privacy": "Privātuma politika" + }, + "spicy_zone": { + "title": "The Spicy Zone", + "clear": "Clear Media Cache", + "signout": "Iziet" + } + }, + "footer": { + "mastodon_description": "Mastodon ir atvērtā koda programmatūra. Tu vari ziņot par problēmām GitHub %s (%s)" + }, + "keyboard": { + "close_settings_window": "Close Settings Window" + } + }, + "report": { + "title_report": "Ziņot", + "title": "Ziņot %s", + "step1": "1. solis no 2", + "step2": "2. solis no 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": "Nosūtīt Sūdzību", + "skip_to_send": "Sūtīt bez komentāra", + "text_placeholder": "Type or paste additional comments", + "reported": "REPORTED", + "step_one": { + "step_1_of_4": "1. solis no 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": "Izvēlieties labāko atbilstību", + "i_dont_like_it": "Man tas nepatīk", + "it_is_not_something_you_want_to_see": "Tas nav kaut kas, ko tu vēlies redzēt", + "its_spam": "Tas ir spams", + "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": "Tu zini, ka tas pārkāpj īpašus noteikumus", + "its_something_else": "Tas ir kaut kas cits", + "the_issue_does_not_fit_into_other_categories": "Šis jautājums neietilpst citās kategorijās" + }, + "step_two": { + "step_2_of_4": "2. solis no 4", + "which_rules_are_being_violated": "Kuri noteikumi tiek pārkāpti?", + "select_all_that_apply": "Atlasi visus atbilstošos", + "i_just_don’t_like_it": "I just don’t like it" + }, + "step_three": { + "step_3_of_4": "3. solis no 4", + "are_there_any_posts_that_back_up_this_report": "Vai ir kādas ziņas, kas atbalsta šo ziņojumu?", + "select_all_that_apply": "Atlasi visus atbilstošos" + }, + "step_four": { + "step_4_of_4": "4. solis no 4", + "is_there_anything_else_we_should_know": "Vai ir vēl kas mums būtu jāzina?" + }, + "step_final": { + "dont_want_to_see_this": "Vai nevēlies to redzēt?", + "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": "Atsekot", + "unfollowed": "Atsekoja", + "unfollow_user": "Atsekot %s", + "mute_user": "Apklusināt %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": "Bloķēt %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": "Kamēr mēs to izskatām, tu vari veikt darbības pret @%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": "Pievienot kontu" + }, + "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" + } + } +} diff --git a/Localization/StringsConvertor/input/lv.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/lv.lproj/ios-infoPlist.json new file mode 100644 index 000000000..c6db73de0 --- /dev/null +++ b/Localization/StringsConvertor/input/lv.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/StringsConvertor/input/nl.lproj/app.json b/Localization/StringsConvertor/input/nl.lproj/app.json index 91e651e04..649fe5064 100644 --- a/Localization/StringsConvertor/input/nl.lproj/app.json +++ b/Localization/StringsConvertor/input/nl.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Niet langer negeren", "unmute_user": "%s niet langer negeren", "muted": "Genegeerd", - "edit_info": "Bewerken" + "edit_info": "Bewerken", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Gefilterd", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "Deblokkeer Account", "message": "Bevestig om %s te deblokkeren" + }, + "confirm_show_reblogs": { + "title": "Show Reblogs", + "message": "Confirm to show reblogs" + }, + "confirm_hide_reblogs": { + "title": "Hide Reblogs", + "message": "Confirm to hide reblogs" } }, "accessibility": { @@ -684,6 +694,9 @@ "new_in_mastodon": "Nieuw in Mastodon", "multiple_account_switch_intro_description": "Schakel tussen meerdere accounts door de profielknop in te drukken en vast te houden.", "accessibility_hint": "Tik tweemaal om het popup-scherm te sluiten" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/app.json b/Localization/StringsConvertor/input/pt-BR.lproj/app.json index 872be790b..063ed346c 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/app.json +++ b/Localization/StringsConvertor/input/pt-BR.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Unmute", "unmute_user": "Unmute %s", "muted": "Muted", - "edit_info": "Edit Info" + "edit_info": "Edit Info", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Filtered", @@ -455,6 +457,14 @@ "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": { @@ -684,6 +694,9 @@ "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/pt.lproj/app.json b/Localization/StringsConvertor/input/pt.lproj/app.json index a965b23ae..80b0882d9 100644 --- a/Localization/StringsConvertor/input/pt.lproj/app.json +++ b/Localization/StringsConvertor/input/pt.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Unmute", "unmute_user": "Unmute %s", "muted": "Muted", - "edit_info": "Edit Info" + "edit_info": "Edit Info", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Filtered", @@ -455,6 +457,14 @@ "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": { @@ -684,6 +694,9 @@ "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/ro.lproj/app.json b/Localization/StringsConvertor/input/ro.lproj/app.json index 3a97d2222..8b9da0903 100644 --- a/Localization/StringsConvertor/input/ro.lproj/app.json +++ b/Localization/StringsConvertor/input/ro.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Unmute", "unmute_user": "Unmute %s", "muted": "Muted", - "edit_info": "Edit Info" + "edit_info": "Edit Info", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Filtered", @@ -455,6 +457,14 @@ "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": { @@ -684,6 +694,9 @@ "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/ru.lproj/app.json b/Localization/StringsConvertor/input/ru.lproj/app.json index fa6377a2c..7a4833554 100644 --- a/Localization/StringsConvertor/input/ru.lproj/app.json +++ b/Localization/StringsConvertor/input/ru.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Убрать из игнорируемых", "unmute_user": "Убрать %s из игнорируемых", "muted": "В игнорируемых", - "edit_info": "Изменить" + "edit_info": "Изменить", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Отфильтровано", @@ -455,6 +457,14 @@ "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": { @@ -684,6 +694,9 @@ "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/si.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/si.lproj/Localizable.stringsdict new file mode 100644 index 000000000..bdcae6ac9 --- /dev/null +++ b/Localization/StringsConvertor/input/si.lproj/Localizable.stringsdict @@ -0,0 +1,449 @@ + + + + + a11y.plural.count.unread.notification + + NSStringLocalizedFormatKey + %#@notification_count_unread_notification@ + notification_count_unread_notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 unread notification + other + %ld unread notification + + + a11y.plural.count.input_limit_exceeds + + NSStringLocalizedFormatKey + Input limit exceeds %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + + a11y.plural.count.input_limit_remains + + NSStringLocalizedFormatKey + Input limit remains %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + + plural.count.followed_by_and_mutual + + NSStringLocalizedFormatKey + %#@names@%#@count_mutual@ + names + + one + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + + + count_mutual + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + Followed by %1$@, and another mutual + other + Followed by %1$@, and %ld mutuals + + + plural.count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + post + other + posts + + + plural.count.media + + NSStringLocalizedFormatKey + %#@media_count@ + media_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 media + other + %ld media + + + plural.count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 post + other + %ld posts + + + plural.count.favorite + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 favorite + other + %ld favorites + + + plural.count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reblog + other + %ld reblogs + + + plural.count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reply + other + %ld replies + + + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 vote + other + %ld votes + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 voter + other + %ld voters + + + plural.people_talking + + NSStringLocalizedFormatKey + %#@count_people_talking@ + count_people_talking + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 people talking + other + %ld people talking + + + plural.count.following + + NSStringLocalizedFormatKey + %#@count_following@ + count_following + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 following + other + %ld following + + + plural.count.follower + + NSStringLocalizedFormatKey + %#@count_follower@ + count_follower + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 follower + other + %ld followers + + + date.year.left + + NSStringLocalizedFormatKey + %#@count_year_left@ + count_year_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 year left + other + %ld years left + + + date.month.left + + NSStringLocalizedFormatKey + %#@count_month_left@ + count_month_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 months left + other + %ld months left + + + date.day.left + + NSStringLocalizedFormatKey + %#@count_day_left@ + count_day_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 day left + other + %ld days left + + + date.hour.left + + NSStringLocalizedFormatKey + %#@count_hour_left@ + count_hour_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 hour left + other + %ld hours left + + + date.minute.left + + NSStringLocalizedFormatKey + %#@count_minute_left@ + count_minute_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 minute left + other + %ld minutes left + + + date.second.left + + NSStringLocalizedFormatKey + %#@count_second_left@ + count_second_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 second left + other + %ld seconds left + + + date.year.ago.abbr + + NSStringLocalizedFormatKey + %#@count_year_ago_abbr@ + count_year_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1y ago + other + %ldy ago + + + date.month.ago.abbr + + NSStringLocalizedFormatKey + %#@count_month_ago_abbr@ + count_month_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1M ago + other + %ldM ago + + + date.day.ago.abbr + + NSStringLocalizedFormatKey + %#@count_day_ago_abbr@ + count_day_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1d ago + other + %ldd ago + + + date.hour.ago.abbr + + NSStringLocalizedFormatKey + %#@count_hour_ago_abbr@ + count_hour_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1h ago + other + %ldh ago + + + date.minute.ago.abbr + + NSStringLocalizedFormatKey + %#@count_minute_ago_abbr@ + count_minute_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1m ago + other + %ldm ago + + + date.second.ago.abbr + + NSStringLocalizedFormatKey + %#@count_second_ago_abbr@ + count_second_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1s ago + other + %lds ago + + + + diff --git a/Localization/StringsConvertor/input/si.lproj/app.json b/Localization/StringsConvertor/input/si.lproj/app.json new file mode 100644 index 000000000..f42e91ae1 --- /dev/null +++ b/Localization/StringsConvertor/input/si.lproj/app.json @@ -0,0 +1,702 @@ +{ + "common": { + "alerts": { + "common": { + "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": "ආපසු", + "next": "ඊළඟ", + "previous": "කලින්", + "open": "අරින්න", + "add": "එකතු", + "remove": "ඉවත් කරන්න", + "edit": "සංස්කරණය", + "save": "සුරකින්න", + "ok": "හරි", + "done": "අහවරයි", + "confirm": "තහවුරු", + "continue": "ඉදිරියට", + "compose": "Compose", + "cancel": "අවලංගු", + "discard": "ඉවතලන්න", + "try_again": "Try Again", + "take_photo": "Take Photo", + "save_photo": "Save Photo", + "copy_photo": "Copy Photo", + "sign_in": "පිවිසෙන්න", + "sign_up": "ලියාපදිංචිය", + "see_more": "තව බලන්න", + "preview": "පෙරදසුන", + "share": "බෙදාගන්න", + "share_user": "%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": "ඡන්දය", + "closed": "වසා ඇත" + }, + "actions": { + "reply": "පිළිතුරු", + "reblog": "Reblog", + "unreblog": "Undo reblog", + "favorite": "ප්‍රියතමය", + "unfavorite": "Unfavorite", + "menu": "Menu", + "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": "ඒ.ස.නි.", + "mention": "සැඳහුම", + "link": "සබැඳිය", + "hashtag": "Hashtag", + "email": "වි-තැපෑල", + "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": "අනුගමනය", + "following": "Following", + "request": "Request", + "pending": "Pending", + "block": "අවහිර", + "block_user": "%s අවහිර", + "block_domain": "%s අවහිර", + "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": "පටන් ගන්න", + "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_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": "තව බලන්න" + }, + "label": { + "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": "ප්‍රතිඵල නැත" + } + }, + "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": "මකන්න" + }, + "username": { + "placeholder": "පරිශීලක නාමය", + "duplicate_prompt": "නම දැනටමත් ගෙන ඇත." + }, + "display_name": { + "placeholder": "display name" + }, + "email": { + "placeholder": "වි-තැපෑල" + }, + "password": { + "placeholder": "මුරපදය", + "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": "පරිශීලක නාමය", + "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 දිග වැඩිය", + "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": "සේවාවේ නියම", + "privacy_policy": "රහස්‍යතා ප්‍රතිපත්තිය", + "button": { + "confirm": "මම එකඟයි" + } + }, + "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": "මුල් පිටුව", + "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": "පිරික්සන්න" + }, + "content_input_placeholder": "Type or paste what’s on your mind", + "compose_action": "ප්‍රකාශනය", + "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..." + }, + "poll": { + "duration_time": "Duration: %s", + "thirty_minutes": "විනාඩි 30", + "one_hour": "පැය 1", + "six_hours": "6 Hours", + "one_day": "1 Day", + "three_days": "3 Days", + "seven_days": "7 Days", + "option_number": "Option %ld" + }, + "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" + }, + "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": "නම්පත", + "content": "අන්තර්ගතය" + } + }, + "segmented_control": { + "posts": "Posts", + "replies": "Replies", + "posts_and_replies": "Posts and Replies", + "media": "මාධ්‍ය", + "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_bar": { + "placeholder": "Search hashtags and users", + "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": "සියල්ල", + "people": "මිනිසුන්", + "hashtags": "Hashtags", + "posts": "Posts" + }, + "empty_state": { + "no_results": "ප්‍රතිඵල නැත" + }, + "recent_search": "Recent searches", + "clear": "මකන්න" + } + }, + "discovery": { + "tabs": { + "posts": "Posts", + "hashtags": "Hashtags", + "news": "පුවත්", + "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": "පිළිගන්න", + "accepted": "Accepted", + "reject": "ප්‍රතික්‍ෂේප", + "rejected": "Rejected" + } + }, + "thread": { + "back_title": "Post", + "title": "Post from %s" + }, + "settings": { + "title": "සැකසුම්", + "section": { + "appearance": { + "title": "පෙනුම", + "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": "දැනුම්දීම්", + "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": "නික්මෙන්න" + } + }, + "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": "වාර්තාව", + "title": "%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" + } + } +} diff --git a/Localization/StringsConvertor/input/si.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/si.lproj/ios-infoPlist.json new file mode 100644 index 000000000..c6db73de0 --- /dev/null +++ b/Localization/StringsConvertor/input/si.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/StringsConvertor/input/sl.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/sl.lproj/Localizable.stringsdict new file mode 100644 index 000000000..8f0bcb42b --- /dev/null +++ b/Localization/StringsConvertor/input/sl.lproj/Localizable.stringsdict @@ -0,0 +1,561 @@ + + + + + a11y.plural.count.unread.notification + + NSStringLocalizedFormatKey + %#@notification_count_unread_notification@ + notification_count_unread_notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld neprebrano obvestilo + two + %ld neprebrani obvestili + few + %ld neprebrana obvestila + other + %ld neprebranih obvestil + + + a11y.plural.count.input_limit_exceeds + + NSStringLocalizedFormatKey + Omejitev vnosa presega %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld znak + two + %ld znaka + few + %ld znaki + other + %ld znakov + + + a11y.plural.count.input_limit_remains + + NSStringLocalizedFormatKey + Omejitev vnosa ostaja %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld znak + two + %ld znaka + few + %ld znaki + other + %ld znakov + + + plural.count.followed_by_and_mutual + + NSStringLocalizedFormatKey + %#@names@%#@count_mutual@ + names + + one + + two + + few + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + + + count_mutual + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + Sledijo %1$@ in %ld skupni + two + Sledijo %1$@ in %ld skupna + few + Sledijo %1$@ in %ld skupni + other + Sledijo %1$@ in %ld skupnih + + + plural.count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + objava + two + objavi + few + objave + other + objav + + + plural.count.media + + NSStringLocalizedFormatKey + %#@media_count@ + media_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld medij + two + %ld medija + few + %ld mediji + other + %ld medijev + + + plural.count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld objava + two + %ld objavi + few + %ld objave + other + %ld objav + + + plural.count.favorite + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld priljubljeni + two + %ld priljubljena + few + %ld priljubljeni + other + %ld priljubljenih + + + plural.count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld poobjava + two + %ld poobjavi + few + %ld poobjave + other + %ld poobjav + + + plural.count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld odgovor + two + %ld odgovora + few + %ld odgovori + other + %ld odgovorov + + + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld glas + two + %ld glasova + few + %ld glasovi + other + %ld glasov + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld glasovalec + two + %ld glasovalca + few + %ld glasovalci + other + %ld glasovalcev + + + plural.people_talking + + NSStringLocalizedFormatKey + %#@count_people_talking@ + count_people_talking + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld oseba se pogovarja + two + %ld osebi se pogovarjata + few + %ld osebe se pogovarjajo + other + %ld oseb se pogovarja + + + plural.count.following + + NSStringLocalizedFormatKey + %#@count_following@ + count_following + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld sledi + two + %ld sledita + few + %ld sledijo + other + %ld sledijo + + + plural.count.follower + + NSStringLocalizedFormatKey + %#@count_follower@ + count_follower + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld sledilec + two + %ld sledilca + few + %ld sledilci + other + %ld sledilcev + + + date.year.left + + NSStringLocalizedFormatKey + %#@count_year_left@ + count_year_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + na voljo še %ld leto + two + na voljo še %ld leti + few + na voljo še %ld leta + other + na voljo še %ld let + + + date.month.left + + NSStringLocalizedFormatKey + %#@count_month_left@ + count_month_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + na voljo še %ld mesec + two + na voljo še %ld meseca + few + na voljo še %ld mesece + other + na voljo še %ld mesecev + + + date.day.left + + NSStringLocalizedFormatKey + %#@count_day_left@ + count_day_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + še %ld dan + two + še %ld dneva + few + še %ld dnevi + other + še %ld dni + + + date.hour.left + + NSStringLocalizedFormatKey + %#@count_hour_left@ + count_hour_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + na voljo še %ld uro + two + na voljo še %ld uri + few + na voljo še %ld ure + other + na voljo še %ld ur + + + date.minute.left + + NSStringLocalizedFormatKey + %#@count_minute_left@ + count_minute_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + Še %ld min. + two + Še %ld min. + few + Še %ld min. + other + Še %ld min. + + + date.second.left + + NSStringLocalizedFormatKey + %#@count_second_left@ + count_second_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + Preostane še %ld s + two + Preostaneta še %ld s + few + Preostanejo še %ld s + other + Preostane še %ld s + + + date.year.ago.abbr + + NSStringLocalizedFormatKey + %#@count_year_ago_abbr@ + count_year_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + pred %ld letom + two + pred %ld letoma + few + pred %ld leti + other + pred %ld leti + + + date.month.ago.abbr + + NSStringLocalizedFormatKey + %#@count_month_ago_abbr@ + count_month_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + pred %ld mesecem + two + pred %ld mesecema + few + pred %ld meseci + other + pred %ld meseci + + + date.day.ago.abbr + + NSStringLocalizedFormatKey + %#@count_day_ago_abbr@ + count_day_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + pred %ld dnem + two + pred %ld dnevoma + few + pred %ld dnemi + other + pred %ld dnemi + + + date.hour.ago.abbr + + NSStringLocalizedFormatKey + %#@count_hour_ago_abbr@ + count_hour_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + pred %ld uro + two + pred %ld urama + few + pred %ld urami + other + pred %ld urami + + + date.minute.ago.abbr + + NSStringLocalizedFormatKey + %#@count_minute_ago_abbr@ + count_minute_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + pred %ld min + two + pred %ld min + few + pred %ld min + other + pred %ld min + + + date.second.ago.abbr + + NSStringLocalizedFormatKey + %#@count_second_ago_abbr@ + count_second_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + pred %ld s + two + pred %ld s + few + pred %ld s + other + pred %ld s + + + + diff --git a/Localization/StringsConvertor/input/sl.lproj/app.json b/Localization/StringsConvertor/input/sl.lproj/app.json new file mode 100644 index 000000000..99a823feb --- /dev/null +++ b/Localization/StringsConvertor/input/sl.lproj/app.json @@ -0,0 +1,702 @@ +{ + "common": { + "alerts": { + "common": { + "please_try_again": "Poskusite znova.", + "please_try_again_later": "Poskusite znova pozneje." + }, + "sign_up_failure": { + "title": "Neuspela registracija" + }, + "server_error": { + "title": "Napaka strežnika" + }, + "vote_failure": { + "title": "Napaka glasovanja", + "poll_ended": "Anketa je zaključena" + }, + "discard_post_content": { + "title": "Zavrzi osnutek", + "message": "Potrdite za opustitev sestavljene vsebine objave." + }, + "publish_post_failure": { + "title": "Spodletela objava", + "message": "Objava je spodletela.\nPreverite svojo internetno povezavo.", + "attachments_message": { + "video_attach_with_photo": "Videoposnetka ni mogoče priložiti objavi, ki že vsebuje slike.", + "more_than_one_video": "Ni možno priložiti več kot enega videoposnetka." + } + }, + "edit_profile_failure": { + "title": "Napaka urejanja profila", + "message": "Profila ni mogoče urejati. Poskusite znova." + }, + "sign_out": { + "title": "Odjava", + "message": "Ali ste prepričani, da se želite odjaviti?", + "confirm": "Odjava" + }, + "block_domain": { + "title": "Ali ste res, res prepričani, da želite blokirati celotno %s? V večini primerov je nekaj ciljnih blokiranj ali utišanj dovolj in boljše. Vsebine iz te domene ne boste videli na javnih časovnicah ali obvestilih. Vaši sledilci iz te domene bodo odstranjeni.", + "block_entire_domain": "Blokiraj domeno" + }, + "save_photo_failure": { + "title": "Neuspelo shranjevanje fotografije", + "message": "Za shranjevanje fotografije omogočite pravice za dostop do knjižnice fotografij." + }, + "delete_post": { + "title": "Izbriši objavo", + "message": "Ali ste prepričani, da želite izbrisati to objavo?" + }, + "clean_cache": { + "title": "Počisti predpomnilnik", + "message": "Uspešno počiščem predpomnilnik %s." + } + }, + "controls": { + "actions": { + "back": "Nazaj", + "next": "Naslednji", + "previous": "Prejšnji", + "open": "Odpri", + "add": "Dodaj", + "remove": "Odstrani", + "edit": "Uredi", + "save": "Shrani", + "ok": "V redu", + "done": "Opravljeno", + "confirm": "Potrdi", + "continue": "Nadaljuj", + "compose": "Sestavi", + "cancel": "Prekliči", + "discard": "Opusti", + "try_again": "Poskusi ponovno", + "take_photo": "Posnemi fotografijo", + "save_photo": "Shrani fotografijo", + "copy_photo": "Kopiraj fotografijo", + "sign_in": "Prijava", + "sign_up": "Registracija", + "see_more": "Pokaži več", + "preview": "Predogled", + "share": "Deli", + "share_user": "Deli %s", + "share_post": "Deli objavo", + "open_in_safari": "Odpri v Safariju", + "open_in_browser": "Odpri v brskalniku", + "find_people": "Poiščite osebe, ki jim želite slediti", + "manually_search": "Raje išči ročno", + "skip": "Preskoči", + "reply": "Odgovori", + "report_user": "Prijavi %s", + "block_domain": "Blokiraj %s", + "unblock_domain": "Odblokiraj %s", + "settings": "Nastavitve", + "delete": "Izbriši" + }, + "tabs": { + "home": "Domov", + "search": "Iskanje", + "notification": "Obvestilo", + "profile": "Profil" + }, + "keyboard": { + "common": { + "switch_to_tab": "Preklopi na %s", + "compose_new_post": "Sestavi novo objavo", + "show_favorites": "Pokaži priljubljene", + "open_settings": "Odpri nastavitve" + }, + "timeline": { + "previous_status": "Prejšnja objava", + "next_status": "Naslednja objava", + "open_status": "Odpri objavo", + "open_author_profile": "Pokaži profil avtorja", + "open_reblogger_profile": "Odpri profil poobjavitelja", + "reply_status": "Odgovori", + "toggle_reblog": "Preklopi poobjavo za objavo", + "toggle_favorite": "Preklopi priljubljenost objave", + "toggle_content_warning": "Preklopi opozorilo o vsebini", + "preview_image": "Predogled slike" + }, + "segmented_control": { + "previous_section": "Prejšnji odsek", + "next_section": "Naslednji odsek" + } + }, + "status": { + "user_reblogged": "%s je poobjavil_a", + "user_replied_to": "Odgovarja %s", + "show_post": "Pokaži objavo", + "show_user_profile": "Prikaži uporabnikov profil", + "content_warning": "Opozorilo o vsebini", + "sensitive_content": "Občutljiva vsebina", + "media_content_warning": "Tapnite kamorkoli, da razkrijete", + "tap_to_reveal": "Tapnite za razkritje", + "poll": { + "vote": "Glasuj", + "closed": "Zaprto" + }, + "actions": { + "reply": "Odgovori", + "reblog": "Poobjavi", + "unreblog": "Razveljavi poobjavo", + "favorite": "Priljubljen", + "unfavorite": "Odstrani iz priljubljenih", + "menu": "Meni", + "hide": "Skrij", + "show_image": "Pokaži sliko", + "show_gif": "Pokaži GIF", + "show_video_player": "Pokaži predvajalnik", + "tap_then_hold_to_show_menu": "Tapnite, nato držite, da se pojavi meni" + }, + "tag": { + "url": "URL", + "mention": "Omeni", + "link": "Povezava", + "hashtag": "Ključnik", + "email": "E-naslov", + "emoji": "Emotikon" + }, + "visibility": { + "unlisted": "Vsak lahko vidi to objavo, ni pa prikazana na javni časovnici.", + "private": "Samo sledilci osebe lahko vidijo to objavo.", + "private_from_me": "Samo moji sledilci lahko vidijo to objavo.", + "direct": "Samo omenjeni uporabnik lahko vidi to objavo." + } + }, + "friendship": { + "follow": "Sledi", + "following": "Sledi", + "request": "Zahteva", + "pending": "Na čakanju", + "block": "Blokiraj", + "block_user": "Blokiraj %s", + "block_domain": "Blokiraj %s", + "unblock": "Odblokiraj", + "unblock_user": "Odblokiraj %s", + "blocked": "Blokirano", + "mute": "Utišaj", + "mute_user": "Utišaj %s", + "unmute": "Odtišaj", + "unmute_user": "Odtišaj %s", + "muted": "Utišan", + "edit_info": "Uredi podatke", + "show_reblogs": "Pokaži poobjave", + "hide_reblogs": "Skrij poobjave" + }, + "timeline": { + "filtered": "Filtrirano", + "timestamp": { + "now": "Zdaj" + }, + "loader": { + "load_missing_posts": "Naloži manjkajoče objave", + "loading_missing_posts": "Nalaganje manjkajočih objav ...", + "show_more_replies": "Pokaži več odgovorov" + }, + "header": { + "no_status_found": "Ne najdem nobenih objav", + "blocking_warning": "Profila tega uporabnika ne morete\nvideti, dokler jih ne odblokirate.\nVaš profil je zanje videti tako.", + "user_blocking_warning": "Profila uporabnika %s ne morete\nvideti, dokler jih ne odblokirate.\nVaš profil je zanje videti tako.", + "blocked_warning": "Profila tega uporabnika ne morete\nvideti, dokler vas ne odblokirajo.", + "user_blocked_warning": "Profila uporabnika %s ne morete\nvideti, dokler vas ne odblokirajo.", + "suspended_warning": "Ta oseba je bila suspendirana.", + "user_suspended_warning": "Račun osebe %s je suspendiran." + } + } + } + }, + "scene": { + "welcome": { + "slogan": "Družbeno mreženje\nspet v vaših rokah.", + "get_started": "Začnite", + "log_in": "Prijava" + }, + "server_picker": { + "title": "Mastodon tvorijo uporabniki z različnih strežnikov.", + "subtitle": "Strežnik izberite glede na svoje interese, regijo ali pa izberite splošnega.", + "subtitle_extend": "Strežnik izberite glede na svoje interese, regijo ali pa izberite splošnega. Z vsakim strežnikom upravlja povsem neodvisna organizacija ali posameznik.", + "button": { + "category": { + "all": "Vse", + "all_accessiblity_description": "Kategorija: vse", + "academia": "akademsko", + "activism": "aktivizem", + "food": "hrana", + "furry": "Kosmato", + "games": "igre", + "general": "splošno", + "journalism": "novinarstvo", + "lgbt": "lgbq+", + "regional": "regionalno", + "art": "umetnost", + "music": "glasba", + "tech": "tehnologija" + }, + "see_less": "Pokaži manj", + "see_more": "Pokaži več" + }, + "label": { + "language": "JEZIK", + "users": "UPORABNIKI", + "category": "KATEGORIJA" + }, + "input": { + "placeholder": "Išči strežnike", + "search_servers_or_enter_url": "Iščite strežnike ali vnesite URL" + }, + "empty_state": { + "finding_servers": "Iskanje razpoložljivih strežnikov ...", + "bad_network": "Med nalaganjem podatkov je prišlo do napake. Preverite svojo internetno povezavo.", + "no_results": "Ni rezultatov" + } + }, + "register": { + "title": "Naj vas namestimo na %s", + "lets_get_you_set_up_on_domain": "Naj vas namestimo na %s", + "input": { + "avatar": { + "delete": "Izbriši" + }, + "username": { + "placeholder": "uporabniško ime", + "duplicate_prompt": "To ime je že zasedeno." + }, + "display_name": { + "placeholder": "pojavno ime" + }, + "email": { + "placeholder": "e-pošta" + }, + "password": { + "placeholder": "geslo", + "require": "Vaše geslo potrebuje vsaj:", + "character_limit": "8 znakov", + "accessibility": { + "checked": "potrjeno", + "unchecked": "nepotrjeno" + }, + "hint": "Geslo mora biti dolgo najmanj 8 znakov." + }, + "invite": { + "registration_user_invite_request": "Zakaj se želite pridružiti?" + } + }, + "error": { + "item": { + "username": "Uporabniško ime", + "email": "E-pošta", + "password": "Geslo", + "agreement": "Sporazum", + "locale": "Krajevne nastavitve", + "reason": "Razlog" + }, + "reason": { + "blocked": "%s vsebuje nedovoljenega ponudnika e-poštnih storitev", + "unreachable": "%s kot kaže ne obstaja", + "taken": "%s je že v uporabi", + "reserved": "%s je rezervirana ključna beseda", + "accepted": "%s mora biti sprejet", + "blank": "%s je zahtevan", + "invalid": "%s ni veljavno", + "too_long": "%s je predolgo", + "too_short": "%s je prekratko", + "inclusion": "%s ni podprta vrednost" + }, + "special": { + "username_invalid": "Uporabniško ime lahko vsebuje samo alfanumerične znake ter podčrtaje.", + "username_too_long": "Uporabniško ime je predolgo (ne more biti daljše od 30 znakov)", + "email_invalid": "E-naslov ni veljaven", + "password_too_short": "Geslo je prekratko (dolgo mora biti vsaj 8 znakov)" + } + } + }, + "server_rules": { + "title": "Nekaj osnovnih pravil.", + "subtitle": "Slednje določajo in njihovo spoštovanje zagotavljajo moderatorji %s.", + "prompt": "Če boste nadaljevali, za vas veljajo pogoji storitve in pravilnik o zasebnosti za %s.", + "terms_of_service": "pogoji uporabe", + "privacy_policy": "pravilnik o zasebnosti", + "button": { + "confirm": "Strinjam se" + } + }, + "confirm_email": { + "title": "Še zadnja stvar.", + "subtitle": "Tapnite povezavo, ki smo vam jo poslali po e-pošti, da overite svoj račun.", + "tap_the_link_we_emailed_to_you_to_verify_your_account": "Tapnite povezavo, ki smo vam jo poslali po e-pošti, da overite svoj račun", + "button": { + "open_email_app": "Odpri aplikacijo za e-pošto", + "resend": "Ponovno pošlji" + }, + "dont_receive_email": { + "title": "Preverite svojo e-pošto", + "description": "Preverite, ali je vaš e-naslov pravilen, pa tudi vsebino mape neželene pošte, če tega še niste storili.", + "resend_email": "Ponovno pošlji e-pošto" + }, + "open_email_app": { + "title": "Preverite svojo dohodno e-pošto.", + "description": "Ravnokar smo vam poslali e-sporočilo. Preverite neželeno pošto, če sporočila ne najdete med dohodno pošto.", + "mail": "E-pošta", + "open_email_client": "Odpri odjemalca e-pošte" + } + }, + "home_timeline": { + "title": "Domov", + "navigation_bar_state": { + "offline": "Nepovezan", + "new_posts": "Pokaži nove objave", + "published": "Objavljeno!", + "Publishing": "Objavljanje objave ...", + "accessibility": { + "logo_label": "Gumb logotipa", + "logo_hint": "Tapnite, da podrsate na vrh; tapnite znova, da se pomaknete na prejšnji položaj" + } + } + }, + "suggestion_account": { + "title": "Poiščite osebe, ki jim želite slediti", + "follow_explain": "Ko nekomu sledite, vidite njihove objave v svojem domačem viru." + }, + "compose": { + "title": { + "new_post": "Nova objava", + "new_reply": "Nov odgovor" + }, + "media_selection": { + "camera": "Posnemi fotografijo", + "photo_library": "Knjižnica fotografij", + "browse": "Prebrskaj" + }, + "content_input_placeholder": "Vnesite ali prilepite, kar vam leži na duši", + "compose_action": "Objavi", + "replying_to_user": "odgovarja %s", + "attachment": { + "photo": "fotografija", + "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 ..." + }, + "poll": { + "duration_time": "Trajanje: %s", + "thirty_minutes": "30 minut", + "one_hour": "1 ura", + "six_hours": "6 ur", + "one_day": "1 dan", + "three_days": "3 dni", + "seven_days": "7 dni", + "option_number": "Možnost %ld" + }, + "content_warning": { + "placeholder": "Tukaj zapišite opozorilo ..." + }, + "visibility": { + "public": "Javno", + "unlisted": "Ni prikazano", + "private": "Samo sledilci", + "direct": "Samo osebe, ki jih omenjam" + }, + "auto_complete": { + "space_to_add": "Preslednica za dodajanje" + }, + "accessibility": { + "append_attachment": "Dodaj priponko", + "append_poll": "Dodaj anketo", + "remove_poll": "Odstrani anketo", + "custom_emoji_picker": "Izbirnik čustvenčkov po meri", + "enable_content_warning": "Omogoči opozorilo o vsebini", + "disable_content_warning": "Onemogoči opozorilo o vsebini", + "post_visibility_menu": "Meni vidnosti objave" + }, + "keyboard": { + "discard_post": "Opusti objavo", + "publish_post": "Objavi objavo", + "toggle_poll": "Preklopi anketo", + "toggle_content_warning": "Preklopi opozorilo o vsebini", + "append_attachment_entry": "Dodaj priponko - %s", + "select_visibility_entry": "Izberite vidnost - %s" + } + }, + "profile": { + "header": { + "follows_you": "Vam sledi" + }, + "dashboard": { + "posts": "Objave", + "following": "sledi", + "followers": "sledilcev" + }, + "fields": { + "add_row": "Dodaj vrstico", + "placeholder": { + "label": "Oznaka", + "content": "Vsebina" + } + }, + "segmented_control": { + "posts": "Objave", + "replies": "Odgovori", + "posts_and_replies": "Objave in odgovori", + "media": "Mediji", + "about": "O programu" + }, + "relationship_action_alert": { + "confirm_mute_user": { + "title": "Utišaj račun", + "message": "Potrdite utišanje %s" + }, + "confirm_unmute_user": { + "title": "Odtišaj račun", + "message": "Potrdite umik utišanja %s" + }, + "confirm_block_user": { + "title": "Blokiraj račun", + "message": "Potrdite za blokado %s" + }, + "confirm_unblock_user": { + "title": "Odblokiraj račun", + "message": "Potrdite za umik blokade %s" + }, + "confirm_show_reblogs": { + "title": "Pokaži poobjave", + "message": "Potrdite, da bodo poobjave prikazane" + }, + "confirm_hide_reblogs": { + "title": "Skrij poobjave", + "message": "Potrdite, da poobjave ne bodo prikazane" + } + }, + "accessibility": { + "show_avatar_image": "Pokaži sliko avatarja", + "edit_avatar_image": "Uredi sliko avatarja", + "show_banner_image": "Pokaži sliko pasice", + "double_tap_to_open_the_list": "Dvakrat tapnite, da se odpre seznam" + } + }, + "follower": { + "title": "sledilec", + "footer": "Sledilci z drugih strežnikov niso prikazani." + }, + "following": { + "title": "sledi", + "footer": "Sledenje z drugih strežnikov ni prikazano." + }, + "familiarFollowers": { + "title": "Znani sledilci", + "followed_by_names": "Sledijo %s" + }, + "favorited_by": { + "title": "Med priljubljene dal_a" + }, + "reblogged_by": { + "title": "Poobjavil_a" + }, + "search": { + "title": "Iskanje", + "search_bar": { + "placeholder": "Išči ključnike in uporabnike", + "cancel": "Prekliči" + }, + "recommend": { + "button_text": "Prikaži vse", + "hash_tag": { + "title": "V trendu na Mastodonu", + "description": "Ključniki, ki imajo veliko pozornosti", + "people_talking": "%s oseb se pogovarja" + }, + "accounts": { + "title": "Računi, ki vam bi bili morda všeč", + "description": "Morda želite slediti tem računom", + "follow": "Sledi" + } + }, + "searching": { + "segment": { + "all": "Vse", + "people": "Ljudje", + "hashtags": "Ključniki", + "posts": "Objave" + }, + "empty_state": { + "no_results": "Ni rezultatov" + }, + "recent_search": "Nedavna iskanja", + "clear": "Počisti" + } + }, + "discovery": { + "tabs": { + "posts": "Objave", + "hashtags": "Ključniki", + "news": "Novice", + "community": "Skupnost", + "for_you": "Za vas" + }, + "intro": "To so objave, ki plenijo pozornost na vašem koncu Mastodona." + }, + "favorite": { + "title": "Vaši priljubljeni" + }, + "notification": { + "title": { + "Everything": "Vse", + "Mentions": "Omembe" + }, + "notification_description": { + "followed_you": "vam sledi", + "favorited_your_post": "je vzljubil/a vašo objavo", + "reblogged_your_post": "je poobjavil_a vašo objavo", + "mentioned_you": "vas je omenil/a", + "request_to_follow_you": "vas je zaprosil za sledenje", + "poll_has_ended": "anketa je zaključena" + }, + "keyobard": { + "show_everything": "Pokaži vse", + "show_mentions": "Pokaži omembe" + }, + "follow_request": { + "accept": "Sprejmi", + "accepted": "Sprejeto", + "reject": "Zavrni", + "rejected": "Zavrnjeno" + } + }, + "thread": { + "back_title": "Objavi", + "title": "Objavil/a" + }, + "settings": { + "title": "Nastavitve", + "section": { + "appearance": { + "title": "Videz", + "automatic": "Samodejno", + "light": "Vedno svetlo", + "dark": "Vedno temno" + }, + "look_and_feel": { + "title": "Videz in občutek", + "use_system": "Uporabi sistemsko", + "really_dark": "Zares temno", + "sorta_dark": "Nekako temno", + "light": "Svetlo" + }, + "notifications": { + "title": "Obvestila", + "favorites": "mojo objavo da med priljubljene", + "follows": "me sledi", + "boosts": "prepošlje mojo objavo", + "mentions": "me omeni", + "trigger": { + "anyone": "kdor koli", + "follower": "sledilec/ka", + "follow": "nekdo, ki mu sledim,", + "noone": "nihče", + "title": "Obvesti me, ko" + } + }, + "preference": { + "title": "Nastavitve", + "true_black_dark_mode": "Resnično črni temni način", + "disable_avatar_animation": "Onemogoči animirane avatarje", + "disable_emoji_animation": "Onemogoči animirane emotikone", + "using_default_browser": "Uporabi privzeti brskalnik za odpiranje povezav", + "open_links_in_mastodon": "Odpri povezave v Mastodonu" + }, + "boring_zone": { + "title": "Cona dolgočasja", + "account_settings": "Nastavitve računa", + "terms": "Pogoji uporabe", + "privacy": "Pravilnik o zasebnosti" + }, + "spicy_zone": { + "title": "Pikantna cona", + "clear": "Počisti medijski predpomnilnik", + "signout": "Odjava" + } + }, + "footer": { + "mastodon_description": "Mastodon je odprtokodna programska oprema. Na GitHubu na %s (%s) lahko poročate o napakah" + }, + "keyboard": { + "close_settings_window": "Zapri okno nastavitev" + } + }, + "report": { + "title_report": "Poročaj", + "title": "Prijavi %s", + "step1": "Korak 1 od 2", + "step2": "Korak 2 od 2", + "content1": "Ali so še kakšne druge objave, ki bi jih želeli dodati k prijavi?", + "content2": "Je kaj, kar bi moderatorji morali vedeti o tem poročilu?", + "report_sent_title": "Hvala za poročilo, bomo preverili.", + "send": "Pošlji poročilo", + "skip_to_send": "Pošlji brez komentarja", + "text_placeholder": "Vnesite ali prilepite dodatne komentarje", + "reported": "PRIJAVLJEN", + "step_one": { + "step_1_of_4": "Korak 1 od 4", + "whats_wrong_with_this_post": "Kaj je narobe s to objavo?", + "whats_wrong_with_this_account": "Kaj je narobe s tem računom?", + "whats_wrong_with_this_username": "Kaj je narobe s/z %s?", + "select_the_best_match": "Izberite najboljši zadetek", + "i_dont_like_it": "Ni mi všeč", + "it_is_not_something_you_want_to_see": "To ni tisto, kar želite videti", + "its_spam": "To je neželena vsebina", + "malicious_links_fake_engagement_or_repetetive_replies": "Škodljive povezave, lažno prizadevanje ali ponavljajoči se odgovori", + "it_violates_server_rules": "Krši strežniška pravila", + "you_are_aware_that_it_breaks_specific_rules": "Zavedate se, da krši določena pravila", + "its_something_else": "Gre za nekaj drugega", + "the_issue_does_not_fit_into_other_categories": "Težava ne sodi v druge kategorije" + }, + "step_two": { + "step_2_of_4": "Korak 2 od 4", + "which_rules_are_being_violated": "Katera pravila so kršena?", + "select_all_that_apply": "Izberite vse, kar ustreza", + "i_just_don’t_like_it": "Ni mi všeč" + }, + "step_three": { + "step_3_of_4": "Korak 3 od 4", + "are_there_any_posts_that_back_up_this_report": "Ali so kakšne objave, ki dokazujejo trditve iz tega poročila?", + "select_all_that_apply": "Izberite vse, kar ustreza" + }, + "step_four": { + "step_4_of_4": "Korak 4 od 4", + "is_there_anything_else_we_should_know": "Je še kaj, za kar menite, da bi morali vedeti?" + }, + "step_final": { + "dont_want_to_see_this": "Ne želite videti tega?", + "when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "Če vidite nekaj, česar na Masodonu ne želite, lahko odstranite osebo iz svoje izkušnje.", + "unfollow": "Prenehaj slediti", + "unfollowed": "Ne sledi več", + "unfollow_user": "Prenehaj slediti %s", + "mute_user": "Utišaj %s", + "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "Njihovih objav ali poobjav ne boste videli v svojem domačem viru. Ne bodo vedeli, da so utišani.", + "block_user": "Blokiraj %s", + "they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "Nič več ne bodo mogli slediti ali videti vaše objave, lahko pa vidijo, če so blokirani.", + "while_we_review_this_you_can_take_action_against_user": "Medtem, ko to pregledujemo, lahko proti %s ukrepate" + } + }, + "preview": { + "keyboard": { + "close_preview": "Zapri predogled", + "show_next": "Pokaži naslednje", + "show_previous": "Pokaži prejšnje" + } + }, + "account_list": { + "tab_bar_hint": "Trenutno izbran profil: %s. Dvakrat tapnite, nato držite, da se pojavi preklopnik med računi.", + "dismiss_account_switcher": "Umakni preklopnik med računi", + "add_account": "Dodaj račun" + }, + "wizard": { + "new_in_mastodon": "Novo v Mastodonu", + "multiple_account_switch_intro_description": "Preklopite med več računi s pritiskom gumba profila.", + "accessibility_hint": "Dvakrat tapnite, da zapustite tega čarovnika" + }, + "bookmark": { + "title": "Bookmarks" + } + } +} diff --git a/Localization/StringsConvertor/input/sl.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/sl.lproj/ios-infoPlist.json new file mode 100644 index 000000000..82c65a50a --- /dev/null +++ b/Localization/StringsConvertor/input/sl.lproj/ios-infoPlist.json @@ -0,0 +1,6 @@ +{ + "NSCameraUsageDescription": "Uporabljeno za zajem fotografij za stanje objave", + "NSPhotoLibraryAddUsageDescription": "Uporabljeno za shranjevanje fotografije v knjižnico fotografij", + "NewPostShortcutItemTitle": "Nova objava", + "SearchShortcutItemTitle": "Iskanje" +} diff --git a/Localization/StringsConvertor/input/sv.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/sv.lproj/Localizable.stringsdict index 27ef9fb53..048af4732 100644 --- a/Localization/StringsConvertor/input/sv.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/sv.lproj/Localizable.stringsdict @@ -90,7 +90,7 @@ one inlägg other - inläggen + inlägg plural.count.media @@ -152,9 +152,9 @@ NSStringFormatValueTypeKey ld one - %ld ompostning + %ld puff other - %ld ompostningar + %ld puffar plural.count.reply diff --git a/Localization/StringsConvertor/input/sv.lproj/app.json b/Localization/StringsConvertor/input/sv.lproj/app.json index 537d6a270..85f243b03 100644 --- a/Localization/StringsConvertor/input/sv.lproj/app.json +++ b/Localization/StringsConvertor/input/sv.lproj/app.json @@ -28,7 +28,7 @@ } }, "edit_profile_failure": { - "title": "Profilredigering misslyckades", + "title": "Kunde inte redigera profil", "message": "Kan inte redigera profil. Var god försök igen." }, "sign_out": { @@ -113,7 +113,7 @@ "open_author_profile": "Öppna författarens profil", "open_reblogger_profile": "Öppna ompostarens profil", "reply_status": "Svara på inlägg", - "toggle_reblog": "Växla ompostning på inlägg", + "toggle_reblog": "Växla puff 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 ompostade", + "user_reblogged": "%s puffade", "user_replied_to": "Svarade på %s", "show_post": "Visa inlägg", "show_user_profile": "Visa användarprofil", @@ -138,8 +138,8 @@ }, "actions": { "reply": "Svara", - "reblog": "Omposta", - "unreblog": "Ångra ompostning", + "reblog": "Puffa", + "unreblog": "Ångra puff", "favorite": "Favorit", "unfavorite": "Ta bort favorit", "menu": "Meny", @@ -180,7 +180,9 @@ "unmute": "Avtysta", "unmute_user": "Avtysta %s", "muted": "Tystad", - "edit_info": "Redigera info" + "edit_info": "Redigera info", + "show_reblogs": "Visa knuffar", + "hide_reblogs": "Dölj puffar" }, "timeline": { "filtered": "Filtrerat", @@ -418,7 +420,7 @@ }, "profile": { "header": { - "follows_you": "Follows You" + "follows_you": "Följer dig" }, "dashboard": { "posts": "inlägg", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "Avblockera konto", "message": "Bekräfta för att avblockera %s" + }, + "confirm_show_reblogs": { + "title": "Visa puffar", + "message": "Bekräfta för att visa puffar" + }, + "confirm_hide_reblogs": { + "title": "Dölj puffar", + "message": "Bekräfta för att dölja puffar" } }, "accessibility": { @@ -480,7 +490,7 @@ "title": "Favoriserad av" }, "reblogged_by": { - "title": "Ompostat av" + "title": "Puffat av" }, "search": { "title": "Sök", @@ -536,7 +546,7 @@ "notification_description": { "followed_you": "följde dig", "favorited_your_post": "favoriserade ditt inlägg", - "reblogged_your_post": "ompostade ditt inlägg", + "reblogged_your_post": "puffade 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" @@ -546,10 +556,10 @@ "show_mentions": "Visa omnämningar" }, "follow_request": { - "accept": "Accept", - "accepted": "Accepted", - "reject": "reject", - "rejected": "Rejected" + "accept": "Godkänn", + "accepted": "Godkänd", + "reject": "avvisa", + "rejected": "Avvisad" } }, "thread": { @@ -577,7 +587,7 @@ "favorites": "Favoriserar mitt inlägg", "follows": "Följer mig", "boosts": "Ompostar mitt inlägg", - "mentions": "Omnämner mig", + "mentions": "Nämner mig", "trigger": { "anyone": "alla", "follower": "en följare", @@ -684,6 +694,9 @@ "new_in_mastodon": "Nytt i Mastodon", "multiple_account_switch_intro_description": "Växla mellan flera konton genom att hålla inne profilknappen.", "accessibility_hint": "Dubbeltryck för att avvisa den här guiden" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/th.lproj/app.json b/Localization/StringsConvertor/input/th.lproj/app.json index 85d2d52ae..763b827cd 100644 --- a/Localization/StringsConvertor/input/th.lproj/app.json +++ b/Localization/StringsConvertor/input/th.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "เลิกซ่อน", "unmute_user": "เลิกซ่อน %s", "muted": "ซ่อนอยู่", - "edit_info": "แก้ไขข้อมูล" + "edit_info": "แก้ไขข้อมูล", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "กรองอยู่", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "เลิกปิดกั้นบัญชี", "message": "ยืนยันเพื่อเลิกปิดกั้น %s" + }, + "confirm_show_reblogs": { + "title": "Show Reblogs", + "message": "Confirm to show reblogs" + }, + "confirm_hide_reblogs": { + "title": "Hide Reblogs", + "message": "Confirm to hide reblogs" } }, "accessibility": { @@ -684,6 +694,9 @@ "new_in_mastodon": "มาใหม่ใน Mastodon", "multiple_account_switch_intro_description": "สลับระหว่างหลายบัญชีโดยกดปุ่มโปรไฟล์ค้างไว้", "accessibility_hint": "แตะสองครั้งเพื่อปิดตัวช่วยสร้างนี้" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/tr.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/tr.lproj/Localizable.stringsdict index 3da12ee4e..29df92c2b 100644 --- a/Localization/StringsConvertor/input/tr.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/tr.lproj/Localizable.stringsdict @@ -234,7 +234,7 @@ one 1 takip edilen other - %ld takip edilen + %ld takip plural.count.follower diff --git a/Localization/StringsConvertor/input/tr.lproj/app.json b/Localization/StringsConvertor/input/tr.lproj/app.json index 27fb4e2b6..cef7fd7f4 100644 --- a/Localization/StringsConvertor/input/tr.lproj/app.json +++ b/Localization/StringsConvertor/input/tr.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Susturmayı kaldır", "unmute_user": "Sesini aç %s", "muted": "Susturuldu", - "edit_info": "Bilgiyi Düzenle" + "edit_info": "Bilgiyi Düzenle", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "Filtrelenmiş", @@ -241,7 +243,7 @@ }, "input": { "placeholder": "Toplulukları ara", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Sunucuları ara ya da bir bağlantı gir" }, "empty_state": { "finding_servers": "Mevcut sunucular aranıyor...", @@ -251,7 +253,7 @@ }, "register": { "title": "%s için kurulumunuzu yapalım", - "lets_get_you_set_up_on_domain": "Let’s get you set up on %s", + "lets_get_you_set_up_on_domain": "%s için kurulumunuzu yapalım", "input": { "avatar": { "delete": "Sil" @@ -286,7 +288,7 @@ "email": "E-posta", "password": "Parola", "agreement": "Anlaşma", - "locale": "Locale", + "locale": "Yerel", "reason": "Sebep" }, "reason": { @@ -322,7 +324,7 @@ "confirm_email": { "title": "Son bir şey.", "subtitle": "Hesabınızı doğrulamak için size e-postayla gönderdiğimiz bağlantıya dokunun.", - "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": "Hesabınızı doğrulamak için size e-postayla gönderdiğimiz bağlantıya dokunun", "button": { "open_email_app": "E-posta Uygulamasını Aç", "resend": "Yeniden gönder" @@ -347,7 +349,7 @@ "published": "Yayınlandı!", "Publishing": "Gönderi yayınlanıyor...", "accessibility": { - "logo_label": "Logo Button", + "logo_label": "Logo Düğmesi", "logo_hint": "Tap to scroll to top and tap again to previous location" } } @@ -418,7 +420,7 @@ }, "profile": { "header": { - "follows_you": "Follows You" + "follows_you": "Seni takip ediyor" }, "dashboard": { "posts": "gönderiler", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "Hesabın Engelini Kaldır", "message": "%s engellemeyi kaldırmayı onaylayın" + }, + "confirm_show_reblogs": { + "title": "Show Reblogs", + "message": "Confirm to show reblogs" + }, + "confirm_hide_reblogs": { + "title": "Hide Reblogs", + "message": "Confirm to hide reblogs" } }, "accessibility": { @@ -465,11 +475,11 @@ } }, "follower": { - "title": "follower", + "title": "takipçi", "footer": "Diğer sunucudaki takipçiler gösterilemiyor." }, "following": { - "title": "following", + "title": "takip", "footer": "Diğer sunucudaki takip edilenler gösterilemiyor." }, "familiarFollowers": { @@ -660,8 +670,8 @@ "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": "Takibi bırak", "unfollowed": "Unfollowed", - "unfollow_user": "Unfollow %s", - "mute_user": "Mute %s", + "unfollow_user": "Takipten çık %s", + "mute_user": "Sustur %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.", @@ -684,6 +694,9 @@ "new_in_mastodon": "Mastodon'da Yeni", "multiple_account_switch_intro_description": "Profil butonuna basılı tutarak birden fazla hesap arasında geçiş yapın.", "accessibility_hint": "Bu yardımı kapatmak için çift tıklayın" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/uk.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/uk.lproj/Localizable.stringsdict new file mode 100644 index 000000000..cdf35477e --- /dev/null +++ b/Localization/StringsConvertor/input/uk.lproj/Localizable.stringsdict @@ -0,0 +1,561 @@ + + + + + a11y.plural.count.unread.notification + + NSStringLocalizedFormatKey + %#@notification_count_unread_notification@ + notification_count_unread_notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 unread notification + few + %ld unread notification + 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 + 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 + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + + plural.count.followed_by_and_mutual + + NSStringLocalizedFormatKey + %#@names@%#@count_mutual@ + names + + one + + few + + many + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + + + count_mutual + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + 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 + one + post + few + posts + many + posts + other + posts + + + plural.count.media + + NSStringLocalizedFormatKey + %#@media_count@ + media_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 media + few + %ld media + many + %ld media + other + %ld media + + + plural.count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 post + few + %ld posts + many + %ld posts + other + %ld posts + + + plural.count.favorite + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 favorite + few + %ld favorites + many + %ld favorites + other + %ld favorites + + + plural.count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reblog + few + %ld reblogs + many + %ld reblogs + other + %ld reblogs + + + plural.count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reply + few + %ld replies + many + %ld replies + other + %ld replies + + + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 vote + few + %ld votes + many + %ld votes + other + %ld votes + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + 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 + 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 + one + 1 following + few + %ld following + many + %ld following + other + %ld following + + + plural.count.follower + + NSStringLocalizedFormatKey + %#@count_follower@ + count_follower + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + one + 1s ago + few + %lds ago + many + %lds ago + other + %lds ago + + + + diff --git a/Localization/StringsConvertor/input/uk.lproj/app.json b/Localization/StringsConvertor/input/uk.lproj/app.json new file mode 100644 index 000000000..80b0882d9 --- /dev/null +++ b/Localization/StringsConvertor/input/uk.lproj/app.json @@ -0,0 +1,702 @@ +{ + "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" + }, + "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..." + }, + "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" + }, + "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" + }, + "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" + } + } +} diff --git a/Localization/StringsConvertor/input/uk.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/uk.lproj/ios-infoPlist.json new file mode 100644 index 000000000..c6db73de0 --- /dev/null +++ b/Localization/StringsConvertor/input/uk.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/StringsConvertor/input/vi.lproj/app.json b/Localization/StringsConvertor/input/vi.lproj/app.json index 5d6df6910..b857399b3 100644 --- a/Localization/StringsConvertor/input/vi.lproj/app.json +++ b/Localization/StringsConvertor/input/vi.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "Bỏ ẩn", "unmute_user": "Bỏ ẩn %s", "muted": "Đã ẩn", - "edit_info": "Chỉnh sửa" + "edit_info": "Chỉnh sửa", + "show_reblogs": "Hiện đăng lại", + "hide_reblogs": "Ẩn đăng lại" }, "timeline": { "filtered": "Bộ lọc", @@ -198,7 +200,7 @@ "user_blocking_warning": "Bạn không thể xem trang %s\ncho tới khi bạn bỏ chặn họ.\nHọ sẽ thấy trang của bạn như thế này.", "blocked_warning": "Bạn không thể xem trang người này\ncho tới khi họ bỏ chặn bạn.", "user_blocked_warning": "Bạn không thể xem trang %s\ncho tới khi họ bỏ chặn bạn.", - "suspended_warning": "Người dùng đã bị vô hiệu hóa.", + "suspended_warning": "Người này đã bị vô hiệu hóa.", "user_suspended_warning": "%s đã bị vô hiệu hóa." } } @@ -236,7 +238,7 @@ }, "label": { "language": "NGÔN NGỮ", - "users": "NGƯỜI DÙNG", + "users": "NGƯỜI", "category": "PHÂN LOẠI" }, "input": { @@ -261,7 +263,7 @@ "duplicate_prompt": "Tên người dùng đã tồn tại." }, "display_name": { - "placeholder": "tên hiển thị" + "placeholder": "biệt danh" }, "email": { "placeholder": "email" @@ -392,7 +394,7 @@ "visibility": { "public": "Công khai", "unlisted": "Hạn chế", - "private": "Riêng tư", + "private": "Chỉ người theo dõi", "direct": "Nhắn riêng" }, "auto_complete": { @@ -441,20 +443,28 @@ }, "relationship_action_alert": { "confirm_mute_user": { - "title": "Ẩn người dùng", + "title": "Ẩn người này", "message": "Xác nhận ẩn %s" }, "confirm_unmute_user": { - "title": "Bỏ ẩn người dùng", + "title": "Bỏ ẩn người này", "message": "Xác nhận bỏ ẩn %s" }, "confirm_block_user": { - "title": "Chặn người dùng", + "title": "Chặn người này", "message": "Xác nhận chặn %s" }, "confirm_unblock_user": { - "title": "Bỏ chặn người dùng", + "title": "Bỏ chặn người này", "message": "Xác nhận bỏ chặn %s" + }, + "confirm_show_reblogs": { + "title": "Hiện đăng lại", + "message": "Xác nhận hiện đăng lại" + }, + "confirm_hide_reblogs": { + "title": "Ẩn đăng lại", + "message": "Xác nhận ẩn đăng lại" } }, "accessibility": { @@ -485,13 +495,13 @@ "search": { "title": "Tìm kiếm", "search_bar": { - "placeholder": "Tìm hashtag và người dùng", + "placeholder": "Tìm hashtag và mọi người", "cancel": "Hủy bỏ" }, "recommend": { "button_text": "Xem tất cả", "hash_tag": { - "title": "Xu hướng trên Mastodon", + "title": "Nổi bật trên Mastodon", "description": "Những hashtag đang được sử dụng nhiều nhất", "people_talking": "%s người đang thảo luận" }, @@ -504,7 +514,7 @@ "searching": { "segment": { "all": "Tất cả", - "people": "Người dùng", + "people": "Mọi người", "hashtags": "Hashtag", "posts": "Tút" }, @@ -684,6 +694,9 @@ "new_in_mastodon": "Mới trên Mastodon", "multiple_account_switch_intro_description": "Chuyển đổi giữa nhiều tài khoản bằng cách đè giữ nút tài khoản.", "accessibility_hint": "Nhấn hai lần để bỏ qua" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json index 90dbfc60e..7f3703b8a 100644 --- a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json @@ -180,7 +180,9 @@ "unmute": "取消静音", "unmute_user": "取消静音 %s", "muted": "已静音", - "edit_info": "编辑" + "edit_info": "编辑", + "show_reblogs": "Show Reblogs", + "hide_reblogs": "Hide Reblogs" }, "timeline": { "filtered": "已过滤", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "解除屏蔽帐户", "message": "确认取消屏蔽 %s" + }, + "confirm_show_reblogs": { + "title": "Show Reblogs", + "message": "Confirm to show reblogs" + }, + "confirm_hide_reblogs": { + "title": "Hide Reblogs", + "message": "Confirm to hide reblogs" } }, "accessibility": { @@ -684,6 +694,9 @@ "new_in_mastodon": "新功能", "multiple_account_switch_intro_description": "按住个人资料标签按钮,即可在多个账户之间进行切换。", "accessibility_hint": "双击关闭此向导" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json index b146a2942..ae497109e 100644 --- a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json @@ -37,7 +37,7 @@ "confirm": "登出" }, "block_domain": { - "title": "真的非常確定封鎖整個 %s 網域嗎?大部分情況下,您只需要封鎖或靜音少數特定的帳帳戶能滿足需求了。您將不能看到來自此網域的內容。您來自該網域的跟隨者也將被移除。", + "title": "真的非常確定要封鎖整個 %s 網域嗎?大部分情況下,您只需要封鎖或靜音少數特定的帳號能滿足需求了。您將不能看到來自此網域的內容。您來自該網域的跟隨者也將被移除。", "block_entire_domain": "封鎖網域" }, "save_photo_failure": { @@ -180,7 +180,9 @@ "unmute": "取消靜音", "unmute_user": "取消靜音 %s", "muted": "已靜音", - "edit_info": "編輯" + "edit_info": "編輯", + "show_reblogs": "顯示轉嘟", + "hide_reblogs": "隱藏轉嘟" }, "timeline": { "filtered": "已過濾", @@ -455,6 +457,14 @@ "confirm_unblock_user": { "title": "取消封鎖", "message": "確認將 %s 取消封鎖" + }, + "confirm_show_reblogs": { + "title": "顯示轉嘟", + "message": "確認顯示轉嘟" + }, + "confirm_hide_reblogs": { + "title": "隱藏轉嘟", + "message": "確認隱藏轉嘟" } }, "accessibility": { @@ -684,6 +694,9 @@ "new_in_mastodon": "Mastodon 新功能", "multiple_account_switch_intro_description": "按住個人檔案按鈕以於多個帳號間切換。", "accessibility_hint": "點兩下以關閉此設定精靈" + }, + "bookmark": { + "title": "Bookmarks" } } -} \ No newline at end of file +} From b5943d48cfe2bbb4ea4f8c939f4ced5d8e4b39e1 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 9 Nov 2022 07:37:37 +0100 Subject: [PATCH 115/658] Generate new localized strings --- MastodonIntent/sv.lproj/Intents.strings | 2 +- MastodonIntent/vi.lproj/Intents.strings | 2 +- .../Generated/Strings.swift | 8 +- .../Resources/ar.lproj/Localizable.strings | 7 + .../Resources/ca.lproj/Localizable.strings | 7 + .../Resources/ckb.lproj/Localizable.strings | 7 + .../Resources/de.lproj/Localizable.strings | 103 +++++++------ .../de.lproj/Localizable.stringsdict | 30 ++-- .../Resources/en.lproj/Localizable.strings | 19 +-- .../Resources/es.lproj/Localizable.strings | 7 + .../Resources/eu.lproj/Localizable.strings | 7 + .../Resources/fi.lproj/Localizable.strings | 7 + .../Resources/fr.lproj/Localizable.strings | 19 ++- .../Resources/gd.lproj/Localizable.strings | 140 +++++++++--------- .../gd.lproj/Localizable.stringsdict | 24 +-- .../Resources/gl.lproj/Localizable.strings | 7 + .../Resources/it.lproj/Localizable.strings | 7 + .../Resources/ja.lproj/Localizable.strings | 7 + .../Resources/kab.lproj/Localizable.strings | 9 +- .../Resources/ku.lproj/Localizable.strings | 7 + .../Resources/nl.lproj/Localizable.strings | 7 + .../Resources/ru.lproj/Localizable.strings | 7 + .../Resources/sv.lproj/Localizable.strings | 33 +++-- .../sv.lproj/Localizable.stringsdict | 6 +- .../Resources/th.lproj/Localizable.strings | 7 + .../Resources/tr.lproj/Localizable.strings | 27 ++-- .../tr.lproj/Localizable.stringsdict | 2 +- .../Resources/vi.lproj/Localizable.strings | 29 ++-- .../zh-Hans.lproj/Localizable.strings | 7 + .../zh-Hant.lproj/Localizable.strings | 9 +- 30 files changed, 353 insertions(+), 207 deletions(-) diff --git a/MastodonIntent/sv.lproj/Intents.strings b/MastodonIntent/sv.lproj/Intents.strings index 526e495d2..793922506 100644 --- a/MastodonIntent/sv.lproj/Intents.strings +++ b/MastodonIntent/sv.lproj/Intents.strings @@ -38,7 +38,7 @@ "ehFLjY" = "Endast följare"; -"gfePDu" = "Publicering misslyckades. ${failureReason}"; +"gfePDu" = "Kunde inte publicera. ${failureReason}"; "k7dbKQ" = "Inlägget har publicerats."; diff --git a/MastodonIntent/vi.lproj/Intents.strings b/MastodonIntent/vi.lproj/Intents.strings index a95337317..80c01c640 100644 --- a/MastodonIntent/vi.lproj/Intents.strings +++ b/MastodonIntent/vi.lproj/Intents.strings @@ -30,7 +30,7 @@ "ayoYEb-dYQ5NN" = "${content}, Công khai"; -"ayoYEb-ehFLjY" = "${content}, Riêng tư"; +"ayoYEb-ehFLjY" = "${content}, Chỉ người theo dõi"; "dUyuGg" = "Đăng lên Mastodon"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 52ed59c09..0ee85cdc8 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -291,8 +291,6 @@ public enum L10n { return L10n.tr("Localizable", "Common.Controls.Status.UserRepliedTo", String(describing: p1)) } public enum Actions { - /// Bookmark - public static let bookmark = L10n.tr("Localizable", "Common.Controls.Status.Actions.Bookmark") /// Favorite public static let favorite = L10n.tr("Localizable", "Common.Controls.Status.Actions.Favorite") /// Hide @@ -311,8 +309,6 @@ public enum L10n { public static let showVideoPlayer = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShowVideoPlayer") /// Tap then hold to show menu public static let tapThenHoldToShowMenu = L10n.tr("Localizable", "Common.Controls.Status.Actions.TapThenHoldToShowMenu") - /// Unbookmark - public static let unbookmark = L10n.tr("Localizable", "Common.Controls.Status.Actions.Unbookmark") /// Unfavorite public static let unfavorite = L10n.tr("Localizable", "Common.Controls.Status.Actions.Unfavorite") /// Undo reblog @@ -412,7 +408,7 @@ public enum L10n { } } public enum Bookmark { - /// Your Bookmarks + /// Bookmarks public static let title = L10n.tr("Localizable", "Scene.Bookmark.Title") } public enum Compose { @@ -716,7 +712,7 @@ public enum L10n { public enum ConfirmHideReblogs { /// Confirm to hide reblogs public static let message = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message") - /// Hide reblogs + /// Hide Reblogs public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title") } public enum ConfirmMuteUser { diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.strings index 3814c14a7..9ecfa450e 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.strings @@ -68,11 +68,13 @@ "Common.Controls.Friendship.EditInfo" = "تَحريرُ المَعلُومات"; "Common.Controls.Friendship.Follow" = "مُتابَعَة"; "Common.Controls.Friendship.Following" = "مُتابَع"; +"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; "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.Unblock" = "رفع الحَظر"; "Common.Controls.Friendship.UnblockUser" = "رفع الحَظر عن %@"; "Common.Controls.Friendship.Unmute" = "رفع الكتم"; @@ -149,6 +151,7 @@ "Scene.AccountList.AddAccount" = "إضافَةُ حِساب"; "Scene.AccountList.DismissAccountSwitcher" = "تجاهُل مبدِّل الحِساب"; "Scene.AccountList.TabBarHint" = "المِلَفُّ المُحدَّدُ حالِيًّا: %@. اُنقُر نَقرًا مُزدَوَجًا مَعَ الاِستِمرارِ لِإظهارِ مُبدِّلِ الحِساب"; +"Scene.Bookmark.Title" = "Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "إضافة مُرفَق"; "Scene.Compose.Accessibility.AppendPoll" = "اضافة استطلاع رأي"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "منتقي الرموز التعبيرية المُخصَّص"; @@ -253,8 +256,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.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.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 5e11b147a..1e691f8a9 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings @@ -68,11 +68,13 @@ Comprova la teva connexió a Internet."; "Common.Controls.Friendship.EditInfo" = "Edita"; "Common.Controls.Friendship.Follow" = "Segueix"; "Common.Controls.Friendship.Following" = "Seguint"; +"Common.Controls.Friendship.HideReblogs" = "Amaga els impulsos"; "Common.Controls.Friendship.Mute" = "Silencia"; "Common.Controls.Friendship.MuteUser" = "Silencia %@"; "Common.Controls.Friendship.Muted" = "Silenciat"; "Common.Controls.Friendship.Pending" = "Pendent"; "Common.Controls.Friendship.Request" = "Petició"; +"Common.Controls.Friendship.ShowReblogs" = "Mostra els impulsos"; "Common.Controls.Friendship.Unblock" = "Desbloqueja"; "Common.Controls.Friendship.UnblockUser" = "Desbloqueja %@"; "Common.Controls.Friendship.Unmute" = "Deixa de silenciar"; @@ -149,6 +151,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.Compose.Accessibility.AppendAttachment" = "Afegeix Adjunt"; "Scene.Compose.Accessibility.AppendPoll" = "Afegir enquesta"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Selector d'Emoji Personalitzat"; @@ -253,8 +256,12 @@ carregat a Mastodon."; "Scene.Profile.Header.FollowsYou" = "Et segueix"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirma per a bloquejar %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Bloqueja el Compte"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirma per a amagar els impulsos"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Amaga Impulsos"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Confirma per a silenciar %@"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Silencia el Compte"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirma per a mostrar els impulsos"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Mostra els Impulsos"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Confirma per a desbloquejar %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Desbloqueja el Compte"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirma deixar de silenciar a %@"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.strings index 3653f3810..053211f28 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.strings @@ -68,11 +68,13 @@ "Common.Controls.Friendship.EditInfo" = "دەستکاری"; "Common.Controls.Friendship.Follow" = "شوێنی بکەوە"; "Common.Controls.Friendship.Following" = "شوێنی دەکەویت"; +"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; "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.Unblock" = "ئاستەنگی مەکە"; "Common.Controls.Friendship.UnblockUser" = "%@ ئاستەنگ مەکە"; "Common.Controls.Friendship.Unmute" = "بێدەنگی مەکە"; @@ -149,6 +151,7 @@ "Scene.AccountList.AddAccount" = "هەژمارێک زیاد بکە"; "Scene.AccountList.DismissAccountSwitcher" = "پێڕستی هەژمارەکان دابخە"; "Scene.AccountList.TabBarHint" = "هەژماری ئێستا: %@. دوو جا دەستی پیا بنێ بۆ کردنەوەی پێڕستی هەژمارەکان."; +"Scene.Bookmark.Title" = "Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "پێوەکراوی پێوە بکە"; "Scene.Compose.Accessibility.AppendPoll" = "دەنگدان زیاد بکە"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "هەڵبژێری ئیمۆجی"; @@ -252,8 +255,12 @@ "Scene.Profile.Header.FollowsYou" = "Follows You"; "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.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.ConfirmUnblockUser.Message" = "دڵنیا ببەوە بۆ لابردنی ئاستەنگی %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "ئاستەنگی مەکە"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "دڵنیا ببەوە بۆ بێدەنگنەکردنی %@"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings index 3b1622945..0a78adc48 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings @@ -68,11 +68,13 @@ Bitte überprüfe deine Internetverbindung."; "Common.Controls.Friendship.EditInfo" = "Information bearbeiten"; "Common.Controls.Friendship.Follow" = "Folgen"; "Common.Controls.Friendship.Following" = "Folge Ich"; +"Common.Controls.Friendship.HideReblogs" = "Reblogs ausblenden"; "Common.Controls.Friendship.Mute" = "Stummschalten"; "Common.Controls.Friendship.MuteUser" = "%@ stummschalten"; "Common.Controls.Friendship.Muted" = "Stummgeschaltet"; "Common.Controls.Friendship.Pending" = "In Warteschlange"; "Common.Controls.Friendship.Request" = "Anfragen"; +"Common.Controls.Friendship.ShowReblogs" = "Reblogs anzeigen"; "Common.Controls.Friendship.Unblock" = "Blockierung aufheben"; "Common.Controls.Friendship.UnblockUser" = "Blockierung von %@ aufheben"; "Common.Controls.Friendship.Unmute" = "Nicht mehr stummschalten"; @@ -149,6 +151,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.Compose.Accessibility.AppendAttachment" = "Anhang hinzufügen"; "Scene.Compose.Accessibility.AppendPoll" = "Umfrage hinzufügen"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Benutzerdefinierter Emojiwähler"; @@ -200,7 +203,7 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "E-Mail-Client öffnen"; "Scene.ConfirmEmail.OpenEmailApp.Title" = "Überprüfe deinen Posteingang."; "Scene.ConfirmEmail.Subtitle" = "Schaue kurz in dein E-Mail-Postfach und tippe den Link an, den wir dir gesendet haben."; -"Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount" = "Tap the link we emailed to you to verify your account"; +"Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount" = "Schaue kurz in dein E-Mail-Postfach und tippe den Link an, den wir dir gesendet haben"; "Scene.ConfirmEmail.Title" = "Noch eine letzte Sache."; "Scene.Discovery.Intro" = "Dies sind die Beiträge, die in deiner Umgebung auf Mastodon beliebter werden."; "Scene.Discovery.Tabs.Community" = "Community"; @@ -208,25 +211,25 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.Discovery.Tabs.Hashtags" = "Hashtags"; "Scene.Discovery.Tabs.News" = "Nachrichten"; "Scene.Discovery.Tabs.Posts" = "Beiträge"; -"Scene.Familiarfollowers.FollowedByNames" = "Followed by %@"; -"Scene.Familiarfollowers.Title" = "Followers you familiar"; +"Scene.Familiarfollowers.FollowedByNames" = "Gefolgt von %@"; +"Scene.Familiarfollowers.Title" = "Follower, die dir bekannt vorkommen"; "Scene.Favorite.Title" = "Deine Favoriten"; -"Scene.FavoritedBy.Title" = "Favorited By"; +"Scene.FavoritedBy.Title" = "Favorisiert von"; "Scene.Follower.Footer" = "Follower von anderen Servern werden nicht angezeigt."; -"Scene.Follower.Title" = "follower"; +"Scene.Follower.Title" = "Follower"; "Scene.Following.Footer" = "Wem das Konto folgt wird von anderen Servern werden nicht angezeigt."; -"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.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"; "Scene.HomeTimeline.NavigationBarState.NewPosts" = "Neue Beiträge anzeigen"; "Scene.HomeTimeline.NavigationBarState.Offline" = "Offline"; "Scene.HomeTimeline.NavigationBarState.Published" = "Veröffentlicht!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Beitrag wird veröffentlicht..."; "Scene.HomeTimeline.Title" = "Startseite"; -"Scene.Notification.FollowRequest.Accept" = "Accept"; -"Scene.Notification.FollowRequest.Accepted" = "Accepted"; -"Scene.Notification.FollowRequest.Reject" = "reject"; -"Scene.Notification.FollowRequest.Rejected" = "Rejected"; +"Scene.Notification.FollowRequest.Accept" = "Akzeptieren"; +"Scene.Notification.FollowRequest.Accepted" = "Akzeptiert"; +"Scene.Notification.FollowRequest.Reject" = "Ablehnen"; +"Scene.Notification.FollowRequest.Rejected" = "Abgelehnt"; "Scene.Notification.Keyobard.ShowEverything" = "Alles anzeigen"; "Scene.Notification.Keyobard.ShowMentions" = "Erwähnungen anzeigen"; "Scene.Notification.NotificationDescription.FavoritedYourPost" = "hat deinen Beitrag favorisiert"; @@ -250,11 +253,15 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.Profile.Fields.AddRow" = "Zeile hinzufügen"; "Scene.Profile.Fields.Placeholder.Content" = "Inhalt"; "Scene.Profile.Fields.Placeholder.Label" = "Bezeichnung"; -"Scene.Profile.Header.FollowsYou" = "Follows You"; +"Scene.Profile.Header.FollowsYou" = "Folgt dir"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Bestätige %@ zu blockieren"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Konto blockieren"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; +"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.Title" = "Reblogs anzeigen"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Bestätige %@ zu entsperren"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Konto entsperren"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Bestätige um %@ nicht mehr stummzuschalten"; @@ -264,7 +271,7 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.Profile.SegmentedControl.Posts" = "Beiträge"; "Scene.Profile.SegmentedControl.PostsAndReplies" = "Beiträge und Antworten"; "Scene.Profile.SegmentedControl.Replies" = "Antworten"; -"Scene.RebloggedBy.Title" = "Reblogged By"; +"Scene.RebloggedBy.Title" = "Geteilt von"; "Scene.Register.Error.Item.Agreement" = "Vereinbarung"; "Scene.Register.Error.Item.Email" = "E-Mail"; "Scene.Register.Error.Item.Locale" = "Sprache"; @@ -297,7 +304,7 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.Register.Input.Password.Require" = "Anforderungen an dein Passwort:"; "Scene.Register.Input.Username.DuplicatePrompt" = "Dieser Benutzername ist vergeben."; "Scene.Register.Input.Username.Placeholder" = "Benutzername"; -"Scene.Register.LetsGetYouSetUpOnDomain" = "Let’s get you set up on %@"; +"Scene.Register.LetsGetYouSetUpOnDomain" = "Okay, lass uns mit %@ anfangen"; "Scene.Register.Title" = "Erzähle uns von dir."; "Scene.Report.Content1" = "Gibt es noch weitere Beiträge, die du der Meldung hinzufügen möchtest?"; "Scene.Report.Content2" = "Gibt es etwas, was die Moderatoren über diese Meldung wissen sollten?"; @@ -307,38 +314,38 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.Report.SkipToSend" = "Ohne Kommentar abschicken"; "Scene.Report.Step1" = "Schritt 1 von 2"; "Scene.Report.Step2" = "Schritt 2 von 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.StepFinal.BlockUser" = "%@ blockieren"; +"Scene.Report.StepFinal.DontWantToSeeThis" = "Du willst das nicht mehr sehen?"; +"Scene.Report.StepFinal.MuteUser" = "%@ stummschalten"; +"Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked" = "Du wirst die Beiträge von diesem Konto nicht sehen. Das Konto wird nicht in der Lage sein, deine Beiträge zu sehen oder dir zu folgen. Die Person hinter dem Konto wird wissen, dass du das Konto blockiert hast."; +"Scene.Report.StepFinal.Unfollow" = "Entfolgen"; +"Scene.Report.StepFinal.UnfollowUser" = "%@ entfolgen"; +"Scene.Report.StepFinal.Unfollowed" = "Entfolgt"; +"Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "Wenn du etwas auf Mastodon nicht sehen willst, kannst du den Nutzer aus deiner Erfahrung streichen."; +"Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser" = "Während wir dies überprüfen, kannst du gegen %@ vorgehen"; +"Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "Du wirst die Beiträge vom Konto nicht mehr sehen. Das Konto kann dir immer noch folgen, und die Person hinter dem Konto wird deine Beiträge sehen können und nicht wissen, dass du sie stummgeschaltet hast."; +"Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "Gibt es etwas anderes, was wir wissen sollten?"; +"Scene.Report.StepFour.Step4Of4" = "Schritt 4 von 4"; +"Scene.Report.StepOne.IDontLikeIt" = "Mir gefällt das nicht"; +"Scene.Report.StepOne.ItIsNotSomethingYouWantToSee" = "Das ist etwas, das man nicht sehen möchte"; +"Scene.Report.StepOne.ItViolatesServerRules" = "Es verstößt gegen Serverregeln"; +"Scene.Report.StepOne.ItsSomethingElse" = "Das ist was anderes"; +"Scene.Report.StepOne.ItsSpam" = "Das ist Spam"; +"Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies" = "Bösartige Links, gefälschtes Engagement oder wiederholte Antworten"; +"Scene.Report.StepOne.SelectTheBestMatch" = "Wähle die passende Kategorie"; +"Scene.Report.StepOne.Step1Of4" = "Schritt 1 von 4"; +"Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories" = "Das Problem passt nicht in die Kategorien"; +"Scene.Report.StepOne.WhatsWrongWithThisAccount" = "Was stimmt mit diesem Konto nicht?"; +"Scene.Report.StepOne.WhatsWrongWithThisPost" = "Was stimmt mit diesem Beitrag nicht?"; +"Scene.Report.StepOne.WhatsWrongWithThisUsername" = "Was ist los mit %@?"; +"Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules" = "Du weißt, welche Regeln verletzt werden"; +"Scene.Report.StepThree.AreThereAnyPostsThatBackUpThisReport" = "Gibt es Beiträge, die diesen Bericht unterstützen?"; +"Scene.Report.StepThree.SelectAllThatApply" = "Alles Zutreffende auswählen"; +"Scene.Report.StepThree.Step3Of4" = "Schritt 3 von 4"; +"Scene.Report.StepTwo.IJustDon’tLikeIt" = "Das gefällt mir einfach nicht"; +"Scene.Report.StepTwo.SelectAllThatApply" = "Alles Zutreffende auswählen"; +"Scene.Report.StepTwo.Step2Of4" = "Schritt 2 von 4"; +"Scene.Report.StepTwo.WhichRulesAreBeingViolated" = "Welche Regeln werden verletzt?"; "Scene.Report.TextPlaceholder" = "Zusätzliche Kommentare eingeben oder einfügen"; "Scene.Report.Title" = "%@ melden"; "Scene.Report.TitleReport" = "Melden"; @@ -379,7 +386,7 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.ServerPicker.EmptyState.FindingServers" = "Verfügbare Server werden gesucht..."; "Scene.ServerPicker.EmptyState.NoResults" = "Keine Ergebnisse"; "Scene.ServerPicker.Input.Placeholder" = "Nach Server suchen oder URL eingeben"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search servers or enter URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Nach Server suchen oder URL eingeben"; "Scene.ServerPicker.Label.Category" = "KATEGORIE"; "Scene.ServerPicker.Label.Language" = "SPRACHE"; "Scene.ServerPicker.Label.Users" = "BENUTZER"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.stringsdict index 3ea0fd0e3..c6a8a4297 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.stringsdict @@ -21,7 +21,7 @@ a11y.plural.count.input_limit_exceeds NSStringLocalizedFormatKey - Eingabelimit überschritten %#@character_count@ + Zeichenanzahl um %#@character_count@ überschritten character_count NSStringFormatSpecTypeKey @@ -37,7 +37,7 @@ a11y.plural.count.input_limit_remains NSStringLocalizedFormatKey - Eingabelimit eingehalten %#@character_count@ + Noch %#@character_count@ übrig character_count NSStringFormatSpecTypeKey @@ -72,9 +72,9 @@ NSStringFormatValueTypeKey ld one - Followed by %1$@, and another mutual + Gefolgt von %1$@ und einer weiteren Person, der du folgst other - Followed by %1$@, and %ld mutuals + Gefolgt von %1$@ und %ld weiteren Personen, denen du folgst plural.count.metric_formatted.post @@ -104,9 +104,9 @@ NSStringFormatValueTypeKey ld one - 1 media + 1 Datei other - %ld media + %ld Dateien plural.count.post @@ -200,7 +200,7 @@ NSStringFormatValueTypeKey ld one - 1 Wähler + 1 Wähler:in other %ld Wähler @@ -216,9 +216,9 @@ NSStringFormatValueTypeKey ld one - 1 Mensch spricht + Eine Person redet other - %ld Leute reden + %ld Personen reden plural.count.following @@ -360,7 +360,7 @@ NSStringFormatValueTypeKey ld one - vor 1 Jahr + vor einem Jahr other vor %ld Jahren @@ -376,7 +376,7 @@ NSStringFormatValueTypeKey ld one - vor 1 M + vor einem Monat other vor %ld Monaten @@ -392,7 +392,7 @@ NSStringFormatValueTypeKey ld one - vor 1 Tag + vor einem Tag other vor %ld Tagen @@ -408,7 +408,7 @@ NSStringFormatValueTypeKey ld one - vor 1 Stunde + vor einer Stunde other vor %ld Stunden @@ -424,7 +424,7 @@ NSStringFormatValueTypeKey ld one - vor 1 Minute + vor einer Minute other vor %ld Minuten @@ -440,7 +440,7 @@ NSStringFormatValueTypeKey ld one - vor 1 Sekunde + vor einer Sekunde other vor %ld Sekuden diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings index 6917eb0c7..5375a81af 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings @@ -68,18 +68,17 @@ Please check your internet connection."; "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.Friendship.HideReblogs" = "Hide Reblogs"; -"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; - "Common.Controls.Keyboard.Common.ComposeNewPost" = "Compose New Post"; "Common.Controls.Keyboard.Common.OpenSettings" = "Open Settings"; "Common.Controls.Keyboard.Common.ShowFavorites" = "Show Favorites"; @@ -107,8 +106,6 @@ Please check your internet connection."; "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.Actions.Bookmark" = "Bookmark"; -"Common.Controls.Status.Actions.Unbookmark" = "Unbookmark"; "Common.Controls.Status.ContentWarning" = "Content Warning"; "Common.Controls.Status.MediaContentWarning" = "Tap anywhere to reveal"; "Common.Controls.Status.Poll.Closed" = "Closed"; @@ -154,7 +151,7 @@ Your profile looks like this to them."; "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" = "Your Bookmarks"; +"Scene.Bookmark.Title" = "Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "Add Attachment"; "Scene.Compose.Accessibility.AppendPoll" = "Add Poll"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Custom Emoji Picker"; @@ -259,16 +256,16 @@ uploaded to Mastodon."; "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.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs"; -"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs"; -"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; -"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide reblogs"; "Scene.Profile.SegmentedControl.About" = "About"; "Scene.Profile.SegmentedControl.Media" = "Media"; "Scene.Profile.SegmentedControl.Posts" = "Posts"; @@ -447,4 +444,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 diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.strings index 0e6ba2582..47ed11bb9 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.strings @@ -68,11 +68,13 @@ Por favor, revise su conexión a internet."; "Common.Controls.Friendship.EditInfo" = "Editar Info"; "Common.Controls.Friendship.Follow" = "Seguir"; "Common.Controls.Friendship.Following" = "Siguiendo"; +"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; "Common.Controls.Friendship.Mute" = "Silenciar"; "Common.Controls.Friendship.MuteUser" = "Silenciar a %@"; "Common.Controls.Friendship.Muted" = "Silenciado"; "Common.Controls.Friendship.Pending" = "Pendiente"; "Common.Controls.Friendship.Request" = "Solicitud"; +"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; "Common.Controls.Friendship.Unblock" = "Desbloquear"; "Common.Controls.Friendship.UnblockUser" = "Desbloquear a %@"; "Common.Controls.Friendship.Unmute" = "Desmutear"; @@ -149,6 +151,7 @@ Tu perfil se ve así para él."; "Scene.AccountList.AddAccount" = "Añadir cuenta"; "Scene.AccountList.DismissAccountSwitcher" = "Descartar el selector de cuentas"; "Scene.AccountList.TabBarHint" = "Perfil seleccionado actualmente: %@. Haz un doble toque y mantén pulsado para mostrar el selector de cuentas"; +"Scene.Bookmark.Title" = "Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "Añadir Adjunto"; "Scene.Compose.Accessibility.AppendPoll" = "Añadir Encuesta"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Selector de Emojis Personalizados"; @@ -254,8 +257,12 @@ pulsa en el enlace para confirmar tu cuenta."; "Scene.Profile.Header.FollowsYou" = "Te sigue"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirmar para bloquear a %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Bloquear cuenta"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Confirmar para silenciar %@"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Silenciar cuenta"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Confirmar para desbloquear a %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Desbloquear cuenta"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirmar para dejar de silenciar a %@"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.strings index aad4ad238..e2be3068d 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.strings @@ -68,11 +68,13 @@ Egiaztatu Interneteko konexioa."; "Common.Controls.Friendship.EditInfo" = "Editatu informazioa"; "Common.Controls.Friendship.Follow" = "Jarraitu"; "Common.Controls.Friendship.Following" = "Jarraitzen"; +"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; "Common.Controls.Friendship.Mute" = "Mututu"; "Common.Controls.Friendship.MuteUser" = "Mututu %@"; "Common.Controls.Friendship.Muted" = "Mutututa"; "Common.Controls.Friendship.Pending" = "Zain"; "Common.Controls.Friendship.Request" = "Eskaera"; +"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; "Common.Controls.Friendship.Unblock" = "Desblokeatu"; "Common.Controls.Friendship.UnblockUser" = "Desblokeatu %@"; "Common.Controls.Friendship.Unmute" = "Desmututu"; @@ -149,6 +151,7 @@ Zure profilak itxura hau du berarentzat."; "Scene.AccountList.AddAccount" = "Gehitu kontua"; "Scene.AccountList.DismissAccountSwitcher" = "Baztertu kontu-aldatzailea"; "Scene.AccountList.TabBarHint" = "Unean hautatutako profila: %@. Ukitu birritan, ondoren eduki sakatuta kontu-aldatzailea erakusteko"; +"Scene.Bookmark.Title" = "Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "Gehitu eranskina"; "Scene.Compose.Accessibility.AppendPoll" = "Gehitu inkesta"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Emoji pertsonalizatuen hautatzailea"; @@ -253,8 +256,12 @@ Mastodonera igo."; "Scene.Profile.Header.FollowsYou" = "Follows You"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Berretsi %@ blokeatzea"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Blokeatu kontua"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Berretsi %@ mututzea"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Mututu kontua"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Berretsi %@ desblokeatzea"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Desblokeatu kontua"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Berretsi %@ desmututzea"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.strings index 3987e7749..fbf48fa83 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.strings @@ -68,11 +68,13 @@ Tarkista internet-yhteytesi."; "Common.Controls.Friendship.EditInfo" = "Muokkaa profiilia"; "Common.Controls.Friendship.Follow" = "Seuraa"; "Common.Controls.Friendship.Following" = "Seurataan"; +"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; "Common.Controls.Friendship.Mute" = "Mykistä"; "Common.Controls.Friendship.MuteUser" = "Mykistä %@"; "Common.Controls.Friendship.Muted" = "Mykistetty"; "Common.Controls.Friendship.Pending" = "Pyydetty"; "Common.Controls.Friendship.Request" = "Pyydä"; +"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; "Common.Controls.Friendship.Unblock" = "Poista esto"; "Common.Controls.Friendship.UnblockUser" = "Unblock %@"; "Common.Controls.Friendship.Unmute" = "Poista mykistys"; @@ -149,6 +151,7 @@ Profiilisi näyttää tältä hänelle."; "Scene.AccountList.AddAccount" = "Lisää tili"; "Scene.AccountList.DismissAccountSwitcher" = "Sulje tilin vaihtaja"; "Scene.AccountList.TabBarHint" = "Nykyinen valittu profiili: %@. Kaksoisnapauta ja pidä sitten painettuna näytääksesi tilin vaihtajan"; +"Scene.Bookmark.Title" = "Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "Lisää liite"; "Scene.Compose.Accessibility.AppendPoll" = "Lisää kysely"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Mukautettu emojivalitsin"; @@ -253,8 +256,12 @@ uploaded to Mastodon."; "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" = "Vahvista, että haluat poistaa mykistyksen tililtä %@"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings index 5e0b9be61..03efc3549 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings @@ -17,13 +17,13 @@ Veuillez vérifier votre accès à Internet."; "Common.Alerts.PublishPostFailure.Title" = "La publication a échoué"; "Common.Alerts.SavePhotoFailure.Message" = "Veuillez activer la permission d'accès à la photothèque pour enregistrer la photo."; "Common.Alerts.SavePhotoFailure.Title" = "Échec de l'enregistrement de la photo"; -"Common.Alerts.ServerError.Title" = "Erreur du serveur"; +"Common.Alerts.ServerError.Title" = "Erreur serveur"; "Common.Alerts.SignOut.Confirm" = "Se déconnecter"; "Common.Alerts.SignOut.Message" = "Voulez-vous vraiment vous déconnecter ?"; "Common.Alerts.SignOut.Title" = "Se déconnecter"; "Common.Alerts.SignUpFailure.Title" = "Échec de l'inscription"; "Common.Alerts.VoteFailure.PollEnded" = "Le sondage est terminé"; -"Common.Alerts.VoteFailure.Title" = "Le vote n’a pas pu être enregistré"; +"Common.Alerts.VoteFailure.Title" = "Échec du vote"; "Common.Controls.Actions.Add" = "Ajouter"; "Common.Controls.Actions.Back" = "Retour"; "Common.Controls.Actions.BlockDomain" = "Bloquer %@"; @@ -68,11 +68,13 @@ Veuillez vérifier votre accès à Internet."; "Common.Controls.Friendship.EditInfo" = "Éditer les infos"; "Common.Controls.Friendship.Follow" = "Suivre"; "Common.Controls.Friendship.Following" = "Suivi"; +"Common.Controls.Friendship.HideReblogs" = "Masquer les Reblogs"; "Common.Controls.Friendship.Mute" = "Masquer"; "Common.Controls.Friendship.MuteUser" = "Ignorer %@"; "Common.Controls.Friendship.Muted" = "Masqué"; "Common.Controls.Friendship.Pending" = "En attente"; "Common.Controls.Friendship.Request" = "Requête"; +"Common.Controls.Friendship.ShowReblogs" = "Afficher les Reblogs"; "Common.Controls.Friendship.Unblock" = "Débloquer"; "Common.Controls.Friendship.UnblockUser" = "Débloquer %@"; "Common.Controls.Friendship.Unmute" = "Ne plus ignorer"; @@ -149,6 +151,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.Compose.Accessibility.AppendAttachment" = "Joindre un document"; "Scene.Compose.Accessibility.AppendPoll" = "Ajouter un Sondage"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Sélecteur d’émojis personnalisés"; @@ -211,11 +214,11 @@ téléversé sur Mastodon."; "Scene.Familiarfollowers.FollowedByNames" = "Suivi·e par %@"; "Scene.Familiarfollowers.Title" = "Abonné·e·s que vous connaissez"; "Scene.Favorite.Title" = "Vos favoris"; -"Scene.FavoritedBy.Title" = "Favorited By"; +"Scene.FavoritedBy.Title" = "Favoris par"; "Scene.Follower.Footer" = "Les abonné·e·s issus des autres serveurs ne sont pas affiché·e·s."; "Scene.Follower.Title" = "abonné·e"; "Scene.Following.Footer" = "Les abonnés issus des autres serveurs ne sont pas affichés."; -"Scene.Following.Title" = "following"; +"Scene.Following.Title" = "abonnement"; "Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint" = "Appuyez pour faire défiler vers le haut et appuyez à nouveau vers l'emplacement précédent"; "Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel" = "Bouton logo"; "Scene.HomeTimeline.NavigationBarState.NewPosts" = "Voir les nouvelles publications"; @@ -253,8 +256,12 @@ téléversé sur Mastodon."; "Scene.Profile.Header.FollowsYou" = "Vous suit"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirmer le blocage de %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Bloquer le compte"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirmer pour masquer les reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Masquer les Reblogs"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Êtes-vous sûr de vouloir mettre en sourdine %@"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Masquer le compte"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirmer pour afficher les reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Afficher les Reblogs"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Confirmer le déblocage de %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Débloquer le compte"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Êtes-vous sûr de vouloir désactiver la sourdine de %@"; @@ -264,7 +271,7 @@ téléversé sur Mastodon."; "Scene.Profile.SegmentedControl.Posts" = "Publications"; "Scene.Profile.SegmentedControl.PostsAndReplies" = "Messages et réponses"; "Scene.Profile.SegmentedControl.Replies" = "Réponses"; -"Scene.RebloggedBy.Title" = "Reblogged By"; +"Scene.RebloggedBy.Title" = "Reblogué par"; "Scene.Register.Error.Item.Agreement" = "Accord"; "Scene.Register.Error.Item.Email" = "Courriel"; "Scene.Register.Error.Item.Locale" = "Lieu"; @@ -313,7 +320,7 @@ téléversé sur Mastodon."; "Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked" = "Ils ne seront plus en mesure de suivre ou de voir vos messages, mais iels peuvent voir s’iels ont été bloqué·e·s."; "Scene.Report.StepFinal.Unfollow" = "Se désabonner"; "Scene.Report.StepFinal.UnfollowUser" = "Ne plus suivre %@"; -"Scene.Report.StepFinal.Unfollowed" = "Unfollowed"; +"Scene.Report.StepFinal.Unfollowed" = "Non-suivi"; "Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "Quand vous voyez quelque chose que vous n’aimez pas sur Mastodon, vous pouvez retirer la personne de votre expérience."; "Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser" = "Pendant que nous étudions votre requête, vous pouvez prendre des mesures contre %@"; "Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "Vous ne verrez plus leurs messages ou leurs partages dans votre flux personnel. Iels ne sauront pas qu’iels ont été mis en sourdine."; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings index 1bb94dc2c..2d1964d81 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings @@ -1,11 +1,11 @@ "Common.Alerts.BlockDomain.BlockEntireDomain" = "Bac an àrainn"; -"Common.Alerts.BlockDomain.Title" = "A bheil thu cinnteach dha-rìribh gu bheil thu airson an àrainn %@ a bhacadh uile gu lèir? Mar as trice, foghnaidh gun dèan thu bacadh no mùchadh no dhà gu sònraichte agus bhiod sin na b’ fheàrr. Chan fhaic thu susbaint on àrainn ud agus thèid an luchd-leantainn agad on àrainn ud a thoirt air falbh."; +"Common.Alerts.BlockDomain.Title" = "A bheil thu cinnteach dha-rìribh gu bheil thu airson an àrainn %@ a bhacadh uile gu lèir? Mar as trice, foghnaidh gun dèan thu bacadh no mùchadh no dhà gu sònraichte agus bhiodh sin na b’ fheàrr. Chan fhaic thu susbaint on àrainn ud agus thèid an luchd-leantainn agad on àrainn ud a thoirt air falbh."; "Common.Alerts.CleanCache.Message" = "Chaidh %@ a thasgadan fhalamhachadh."; "Common.Alerts.CleanCache.Title" = "Falamhaich an tasgadan"; "Common.Alerts.Common.PleaseTryAgain" = "Feuch ris a-rithist."; "Common.Alerts.Common.PleaseTryAgainLater" = "Feuch ris a-rithist an ceann greis."; "Common.Alerts.DeletePost.Message" = "A bheil thu cinnteach gu bheil thu airson am post seo a sguabadh às?"; -"Common.Alerts.DeletePost.Title" = "A bheil thu cinnteach gu bheil thu airson am post seo a sguabadh às?"; +"Common.Alerts.DeletePost.Title" = "Sguab às am post"; "Common.Alerts.DiscardPostContent.Message" = "Dearbh tilgeil air falbh susbaint a’ phuist a sgrìobh thu."; "Common.Alerts.DiscardPostContent.Title" = "Tilg air falbh an dreachd"; "Common.Alerts.EditProfileFailure.Message" = "Cha b’ urrainn dhuinn a’ pròifil a dheasachadh. Feuch ris a-rithist."; @@ -66,13 +66,15 @@ Thoir sùil air a’ cheangal agad ris an eadar-lìon."; "Common.Controls.Friendship.BlockUser" = "Bac %@"; "Common.Controls.Friendship.Blocked" = "’Ga bhacadh"; "Common.Controls.Friendship.EditInfo" = "Deasaich"; -"Common.Controls.Friendship.Follow" = "Lean air"; +"Common.Controls.Friendship.Follow" = "Lean"; "Common.Controls.Friendship.Following" = "’Ga leantainn"; +"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; "Common.Controls.Friendship.Mute" = "Mùch"; "Common.Controls.Friendship.MuteUser" = "Mùch %@"; "Common.Controls.Friendship.Muted" = "’Ga mhùchadh"; "Common.Controls.Friendship.Pending" = "Ri dhèiligeadh"; "Common.Controls.Friendship.Request" = "Iarrtas"; +"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; "Common.Controls.Friendship.Unblock" = "Dì-bhac"; "Common.Controls.Friendship.UnblockUser" = "Dì-bhac %@"; "Common.Controls.Friendship.Unmute" = "Dì-mhùch"; @@ -149,6 +151,7 @@ Seo an coltas a th’ air a’ phròifil agad dhaibh-san."; "Scene.AccountList.AddAccount" = "Cuir cunntas ris"; "Scene.AccountList.DismissAccountSwitcher" = "Leig seachad taghadh a’ chunntais"; "Scene.AccountList.TabBarHint" = "A’ phròifil air a taghadh: %@. Thoir gnogag dhùbailte is cùm sìos a ghearradh leum gu cunntas eile"; +"Scene.Bookmark.Title" = "Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "Cuir ceanglachan ris"; "Scene.Compose.Accessibility.AppendPoll" = "Cuir cunntas-bheachd ris"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Roghnaichear nan Emoji gnàthaichte"; @@ -199,9 +202,8 @@ a luchdadh suas gu Mastodon."; "Scene.ConfirmEmail.OpenEmailApp.Mail" = "Post"; "Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "Fosgail cliant puist-d"; "Scene.ConfirmEmail.OpenEmailApp.Title" = "Thoir sùil air a’ bhogsa a-steach agad."; -"Scene.ConfirmEmail.Subtitle" = "Tha sinn air post-d a chur gu %@, -thoir gnogag air a’ chunntas a dhearbhadh a’ chunntais agad."; -"Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount" = "Tap the link we emailed to you to verify your account"; +"Scene.ConfirmEmail.Subtitle" = "Thoir gnogag air a’ cheangal a chuir sinn thugad air a’ phost-d airson an cunntas agad a dhearbhadh."; +"Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount" = "Thoir gnogag air a’ cheangal a chuir sinn thugad air a’ phost-d airson an cunntas agad a dhearbhadh"; "Scene.ConfirmEmail.Title" = "Aon rud eile."; "Scene.Discovery.Intro" = "Seo na postaichean fèillmhor ’nad cheàrnaidh de Mhastodon."; "Scene.Discovery.Tabs.Community" = "Coimhearsnachd"; @@ -209,25 +211,25 @@ thoir gnogag air a’ chunntas a dhearbhadh a’ chunntais agad."; "Scene.Discovery.Tabs.Hashtags" = "Tagaichean hais"; "Scene.Discovery.Tabs.News" = "Naidheachdan"; "Scene.Discovery.Tabs.Posts" = "Postaichean"; -"Scene.Familiarfollowers.FollowedByNames" = "Followed by %@"; -"Scene.Familiarfollowers.Title" = "Followers you familiar"; +"Scene.Familiarfollowers.FollowedByNames" = "’Ga leantainn le %@"; +"Scene.Familiarfollowers.Title" = "Luchd-leantainn aithnichte"; "Scene.Favorite.Title" = "Na h-annsachdan agad"; -"Scene.FavoritedBy.Title" = "Favorited By"; +"Scene.FavoritedBy.Title" = "’Na annsachd aig"; "Scene.Follower.Footer" = "Cha dèid luchd-leantainn o fhrithealaichean eile a shealltainn."; -"Scene.Follower.Title" = "follower"; -"Scene.Following.Footer" = "Cha dèid cò air a leanas tu air frithealaichean eile a shealltainn."; -"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.Follower.Title" = "neach-leantainn"; +"Scene.Following.Footer" = "Cha dèid cò a leanas tu air frithealaichean eile a shealltainn."; +"Scene.Following.Title" = "’ga leantainn"; +"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint" = "Thoir gnogag a sgroladh dhan bhàrr is thoir gnogag a-rithist a dhol dhan ionad roimhe"; +"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel" = "Putan an t-suaicheantais"; "Scene.HomeTimeline.NavigationBarState.NewPosts" = "Seall na postaichean ùra"; "Scene.HomeTimeline.NavigationBarState.Offline" = "Far loidhne"; "Scene.HomeTimeline.NavigationBarState.Published" = "Chaidh fhoillseachadh!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "A’ foillseachadh a’ phuist…"; "Scene.HomeTimeline.Title" = "Dachaigh"; -"Scene.Notification.FollowRequest.Accept" = "Accept"; -"Scene.Notification.FollowRequest.Accepted" = "Accepted"; -"Scene.Notification.FollowRequest.Reject" = "reject"; -"Scene.Notification.FollowRequest.Rejected" = "Rejected"; +"Scene.Notification.FollowRequest.Accept" = "Gabh ris"; +"Scene.Notification.FollowRequest.Accepted" = "Air a ghabhail ris"; +"Scene.Notification.FollowRequest.Reject" = "diùlt"; +"Scene.Notification.FollowRequest.Rejected" = "Chaidh a dhiùltadh"; "Scene.Notification.Keyobard.ShowEverything" = "Seall a h-uile càil"; "Scene.Notification.Keyobard.ShowMentions" = "Seall na h-iomraidhean"; "Scene.Notification.NotificationDescription.FavoritedYourPost" = "– is annsa leotha am post agad"; @@ -235,7 +237,7 @@ thoir gnogag air a’ chunntas a dhearbhadh a’ chunntais agad."; "Scene.Notification.NotificationDescription.MentionedYou" = "– ’s iad air iomradh a thoirt ort"; "Scene.Notification.NotificationDescription.PollHasEnded" = "thàinig cunntas-bheachd gu crìoch"; "Scene.Notification.NotificationDescription.RebloggedYourPost" = "– ’s iad air am post agad a bhrosnachadh"; -"Scene.Notification.NotificationDescription.RequestToFollowYou" = "iarrtas leantainn ort"; +"Scene.Notification.NotificationDescription.RequestToFollowYou" = "iarrtas leantainn"; "Scene.Notification.Title.Everything" = "A h-uile rud"; "Scene.Notification.Title.Mentions" = "Iomraidhean"; "Scene.Preview.Keyboard.ClosePreview" = "Dùin an ro-shealladh"; @@ -251,11 +253,15 @@ thoir gnogag air a’ chunntas a dhearbhadh a’ chunntais agad."; "Scene.Profile.Fields.AddRow" = "Cuir ràgh ris"; "Scene.Profile.Fields.Placeholder.Content" = "Susbaint"; "Scene.Profile.Fields.Placeholder.Label" = "Leubail"; -"Scene.Profile.Header.FollowsYou" = "Follows You"; +"Scene.Profile.Header.FollowsYou" = "’Gad leantainn"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Dearbh bacadh %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Bac an cunntas"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Dearbh mùchadh %@"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Mùch an cunntas"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Dearbh dì-bhacadh %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Dì-bhac an cunntas"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Dearbh dì-mhùchadh %@"; @@ -265,7 +271,7 @@ thoir gnogag air a’ chunntas a dhearbhadh a’ chunntais agad."; "Scene.Profile.SegmentedControl.Posts" = "Postaichean"; "Scene.Profile.SegmentedControl.PostsAndReplies" = "Postaichean ’s freagairtean"; "Scene.Profile.SegmentedControl.Replies" = "Freagairtean"; -"Scene.RebloggedBy.Title" = "Reblogged By"; +"Scene.RebloggedBy.Title" = "’Ga bhrosnachadh le"; "Scene.Register.Error.Item.Agreement" = "Aonta"; "Scene.Register.Error.Item.Email" = "Post-d"; "Scene.Register.Error.Item.Locale" = "Sgeama ionadail"; @@ -298,8 +304,8 @@ thoir gnogag air a’ chunntas a dhearbhadh a’ chunntais agad."; "Scene.Register.Input.Password.Require" = "Feumaidh am facal-faire agad co-dhiù:"; "Scene.Register.Input.Username.DuplicatePrompt" = "Tha an t-ainm-cleachdaiche seo aig cuideigin eile."; "Scene.Register.Input.Username.Placeholder" = "ainm-cleachdaiche"; -"Scene.Register.LetsGetYouSetUpOnDomain" = "Let’s get you set up on %@"; -"Scene.Register.Title" = "Innis dhuinn mu do dhèidhinn."; +"Scene.Register.LetsGetYouSetUpOnDomain" = "’Gad rèiteachadh air %@"; +"Scene.Register.Title" = "’Gad rèiteachadh air %@"; "Scene.Report.Content1" = "A bheil post sam bith eile ann a bu mhiann leat cur ris a’ ghearan?"; "Scene.Report.Content2" = "A bheil rud sam bith ann a bu mhiann leat innse dha na maoir mun ghearan seo?"; "Scene.Report.ReportSentTitle" = "Mòran taing airson a’ ghearain, bheir sinn sùil air."; @@ -308,43 +314,43 @@ thoir gnogag air a’ chunntas a dhearbhadh a’ chunntais agad."; "Scene.Report.SkipToSend" = "Cuir gun bheachd ris"; "Scene.Report.Step1" = "Ceum 1 à 2"; "Scene.Report.Step2" = "Ceum 2 à 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.StepFinal.BlockUser" = "Bac %@"; +"Scene.Report.StepFinal.DontWantToSeeThis" = "Nach eil thu airson seo fhaicinn?"; +"Scene.Report.StepFinal.MuteUser" = "Mùch %@"; +"Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked" = "Chan urrainn dhaibh ’gad leantainn is chan fhaic iad na postaichean agad tuilleadh ach chì iad gun deach am bacadh."; +"Scene.Report.StepFinal.Unfollow" = "Na lean tuilleadh"; +"Scene.Report.StepFinal.UnfollowUser" = "Na lean %@ tuilleadh"; +"Scene.Report.StepFinal.Unfollowed" = "Chan eil thu ’ga leantainn tuilleadh"; +"Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "Nuair a chì thu rudeigin nach toigh leat air Mastodon, ’s urrainn dhut an neach a chumail fad air falbh uat."; +"Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser" = "Fhad ’s a bhios sinn a’ toirt sùil air, seo nas urrainn dhut dèanamh an aghaidh %@"; +"Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "Chan fhaic thu na postaichean aca is dè a bhrosnaich iad air inbhir na dachaigh agad tuilleadh. Cha bhi fios aca gun do mhùch thu iad."; +"Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "A bheil rud sam bith eile a bu toigh leat innse dhuinn?"; +"Scene.Report.StepFour.Step4Of4" = "Ceum 4 à 4"; +"Scene.Report.StepOne.IDontLikeIt" = "Cha toigh leam e"; +"Scene.Report.StepOne.ItIsNotSomethingYouWantToSee" = "Chan eil thu airson seo fhaicinn"; +"Scene.Report.StepOne.ItViolatesServerRules" = "Tha e a’ briseadh riaghailtean an fhrithealaiche"; +"Scene.Report.StepOne.ItsSomethingElse" = "’S rud eile a tha ann"; +"Scene.Report.StepOne.ItsSpam" = "’S e spama a th’ ann"; +"Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies" = "Ceanglaichean droch-rùnach, conaltradh fuadain no an dearbh fhreagairt a-rithist ’s a-rithist"; +"Scene.Report.StepOne.SelectTheBestMatch" = "Tagh a’ mhaids as fheàrr"; +"Scene.Report.StepOne.Step1Of4" = "Ceum 1 à 4"; +"Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories" = "Chan eil na roinnean-seòrsa eile iomchaidh dhan chùis"; +"Scene.Report.StepOne.WhatsWrongWithThisAccount" = "Dè tha ceàrr leis an cunntas seo?"; +"Scene.Report.StepOne.WhatsWrongWithThisPost" = "Dè tha ceàrr leis a’ phost seo?"; +"Scene.Report.StepOne.WhatsWrongWithThisUsername" = "Dè tha ceàrr le %@?"; +"Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules" = "Mhothaich thu gu bheil e a’ briseadh riaghailtean sònraichte"; +"Scene.Report.StepThree.AreThereAnyPostsThatBackUpThisReport" = "A bheil postaichean sam bith ann a tha ’nam fianais dhan ghearan seo?"; +"Scene.Report.StepThree.SelectAllThatApply" = "Tagh a h-uile gin a tha iomchaidh"; +"Scene.Report.StepThree.Step3Of4" = "Ceum 3 à 4"; +"Scene.Report.StepTwo.IJustDon’tLikeIt" = "’S ann nach toigh leam e"; +"Scene.Report.StepTwo.SelectAllThatApply" = "Tagh a h-uile gin a tha iomchaidh"; +"Scene.Report.StepTwo.Step2Of4" = "Ceum 2 à 4"; +"Scene.Report.StepTwo.WhichRulesAreBeingViolated" = "Dè na riaghailtean a tha ’gam briseadh?"; "Scene.Report.TextPlaceholder" = "Sgrìobh no cuir ann beachdan a bharrachd"; "Scene.Report.Title" = "Dèan gearan mu %@"; "Scene.Report.TitleReport" = "Dèan gearan"; -"Scene.Search.Recommend.Accounts.Description" = "Saoil am bu toigh leat leantainn air na cunntasan seo?"; -"Scene.Search.Recommend.Accounts.Follow" = "Lean air"; +"Scene.Search.Recommend.Accounts.Description" = "Saoil am bu toigh leat na cunntasan seo a leantainn?"; +"Scene.Search.Recommend.Accounts.Follow" = "Lean"; "Scene.Search.Recommend.Accounts.Title" = "Cunntasan a chòrdas riut ma dh’fhaoidte"; "Scene.Search.Recommend.ButtonText" = "Seall na h-uile"; "Scene.Search.Recommend.HashTag.Description" = "Tagaichean hais le aire orra an-dràsta"; @@ -379,20 +385,20 @@ thoir gnogag air a’ chunntas a dhearbhadh a’ chunntais agad."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Chaidh rudeigin ceàrr le luchdadh an dàta. Thoir sùil air a’ cheangal agad ris an eadar-lìon."; "Scene.ServerPicker.EmptyState.FindingServers" = "A’ lorg nam frithealaichean ri am faighinn…"; "Scene.ServerPicker.EmptyState.NoResults" = "Gun toradh"; -"Scene.ServerPicker.Input.Placeholder" = "Lorg frithealaiche no gabh pàirt san fhear agad fhèin…"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search servers or enter URL"; +"Scene.ServerPicker.Input.Placeholder" = "Lorg frithealaiche"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Lorg frithealaiche no cuir a-steach URL"; "Scene.ServerPicker.Label.Category" = "ROINN-SEÒRSA"; "Scene.ServerPicker.Label.Language" = "CÀNAN"; "Scene.ServerPicker.Label.Users" = "CLEACHDAICHEAN"; -"Scene.ServerPicker.Subtitle" = "Tagh coimhearsnachd stèidhichte air d’ ùidhean no an roinn-dùthcha agad no tè choitcheann."; -"Scene.ServerPicker.SubtitleExtend" = "Tagh coimhearsnachd stèidhichte air d’ ùidhean no an roinn-dùthcha agad no tè choitcheann. Tha gach coimhearsnachd ’ga stiùireadh le buidheann no neach gu neo-eisimeileach."; -"Scene.ServerPicker.Title" = "Tagh frithealaiche sam bith."; +"Scene.ServerPicker.Subtitle" = "Tagh frithealaiche stèidhichte air d’ ùidhean, air far a bheil thu no fear coitcheann."; +"Scene.ServerPicker.SubtitleExtend" = "Tagh frithealaiche stèidhichte air d’ ùidhean, air far a bheil thu no fear coitcheann. Tha gach frithealaiche fo stiùireadh buidhinn no neach neo-eisimeilich fa leth."; +"Scene.ServerPicker.Title" = "Tha cleachdaichean Mhastodon air iomadh frithealaiche eadar-dhealaichte."; "Scene.ServerRules.Button.Confirm" = "Gabhaidh mi ris"; "Scene.ServerRules.PrivacyPolicy" = "poileasaidh prìobhaideachd"; "Scene.ServerRules.Prompt" = "Ma leanas tu air adhart, bidh thu fo bhuaidh teirmichean seirbheise is poileasaidh prìobhaideachd %@."; -"Scene.ServerRules.Subtitle" = "Shuidhich rianairean %@ na riaghailtean seo."; +"Scene.ServerRules.Subtitle" = "Tha na riaghailtean seo ’gan stèidheachadh is a chur an gnìomh leis na maoir aig %@."; "Scene.ServerRules.TermsOfService" = "teirmichean na seirbheise"; -"Scene.ServerRules.Title" = "Riaghailt bhunasach no dhà."; +"Scene.ServerRules.Title" = "Riaghailtean bunasach."; "Scene.Settings.Footer.MastodonDescription" = "’S e bathar-bog le bun-tùs fosgailte a th’ ann am Mastodon. ’S urrainn dhut aithris a dhèanamh air duilgheadasan air GitHub fo %@ (%@)"; "Scene.Settings.Keyboard.CloseSettingsWindow" = "Dùin uinneag nan roghainnean"; "Scene.Settings.Section.Appearance.Automatic" = "Fèin-obrachail"; @@ -410,11 +416,11 @@ thoir gnogag air a’ chunntas a dhearbhadh a’ chunntais agad."; "Scene.Settings.Section.LookAndFeel.UseSystem" = "Cleachd coltas an t-siostaim"; "Scene.Settings.Section.Notifications.Boosts" = "Nuair a bhrosnaicheas iad post uam"; "Scene.Settings.Section.Notifications.Favorites" = "Nuair as annsa leotha am post agam"; -"Scene.Settings.Section.Notifications.Follows" = "Nuair a leanas iad orm"; +"Scene.Settings.Section.Notifications.Follows" = "Nuair a leanas iad mi"; "Scene.Settings.Section.Notifications.Mentions" = "Nuair a bheir iad iomradh orm"; "Scene.Settings.Section.Notifications.Title" = "Brathan"; "Scene.Settings.Section.Notifications.Trigger.Anyone" = "Airson duine sam bith, cuir brath thugam"; -"Scene.Settings.Section.Notifications.Trigger.Follow" = "Airson daoine air a leanas mi, cuir brath thugam"; +"Scene.Settings.Section.Notifications.Trigger.Follow" = "Airson daoine a leanas mi, cuir brath thugam"; "Scene.Settings.Section.Notifications.Trigger.Follower" = "Airson luchd-leantainn, cuir brath thugam"; "Scene.Settings.Section.Notifications.Trigger.Noone" = "Na cuir brath thugam idir"; "Scene.Settings.Section.Notifications.Trigger.Title" = " "; @@ -428,7 +434,7 @@ thoir gnogag air a’ chunntas a dhearbhadh a’ chunntais agad."; "Scene.Settings.Section.SpicyZone.Signout" = "Clàraich a-mach"; "Scene.Settings.Section.SpicyZone.Title" = "Gnìomhan"; "Scene.Settings.Title" = "Roghainnean"; -"Scene.SuggestionAccount.FollowExplain" = "Nuair a leanas tu air cuideigin, chì thu na puist aca air inbhir na dachaigh agad."; +"Scene.SuggestionAccount.FollowExplain" = "Nuair a leanas tu cuideigin, chì thu na puist aca air inbhir na dachaigh agad."; "Scene.SuggestionAccount.Title" = "Lorg daoine a leanas tu"; "Scene.Thread.BackTitle" = "Post"; "Scene.Thread.Title" = "Post le %@"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.stringsdict index f041677fa..d0ccb5f41 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.stringsdict @@ -88,13 +88,13 @@ NSStringFormatValueTypeKey ld one - Followed by %1$@, and another mutual + ’Ga leantainn le %1$@ ’s %ld eile an cumantas two - Followed by %1$@, and %ld mutuals + ’Ga leantainn le %1$@ ’s %ld eile an cumantas few - Followed by %1$@, and %ld mutuals + ’Ga leantainn le %1$@ ’s %ld eile an cumantas other - Followed by %1$@, and %ld mutuals + ’Ga leantainn le %1$@ ’s %ld eile an cumantas plural.count.metric_formatted.post @@ -128,13 +128,13 @@ NSStringFormatValueTypeKey ld one - 1 media + %ld mheadhan two - %ld media + %ld mheadhan few - %ld media + %ld meadhanan other - %ld media + %ld meadhan plural.count.post @@ -308,13 +308,13 @@ NSStringFormatValueTypeKey ld one - Tha %ld a’ leantainn air + Tha %ld ’ga leantainn two - Tha %ld a’ leantainn air + Tha %ld ’ga leantainn few - Tha %ld a’ leantainn air + Tha %ld ’ga leantainn other - Tha %ld a’ leantainn air + Tha %ld ’ga leantainn date.year.left diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings index dfb88a006..c76089221 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings @@ -68,11 +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.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.Unblock" = "Desbloquear"; "Common.Controls.Friendship.UnblockUser" = "Desbloquear a %@"; "Common.Controls.Friendship.Unmute" = "Non Acalar"; @@ -149,6 +151,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.Compose.Accessibility.AppendAttachment" = "Engadir anexo"; "Scene.Compose.Accessibility.AppendPoll" = "Engadir enquisa"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Selector emoji personalizado"; @@ -253,8 +256,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.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.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 cff8374cf..c83cb7458 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings @@ -68,11 +68,13 @@ Per favore verifica la tua connessione internet."; "Common.Controls.Friendship.EditInfo" = "Modifica info"; "Common.Controls.Friendship.Follow" = "Segui"; "Common.Controls.Friendship.Following" = "Stai seguendo"; +"Common.Controls.Friendship.HideReblogs" = "Nascondi le condivisioni"; "Common.Controls.Friendship.Mute" = "Silenzia"; "Common.Controls.Friendship.MuteUser" = "Silenzia %@"; "Common.Controls.Friendship.Muted" = "Silenziato"; "Common.Controls.Friendship.Pending" = "In attesa"; "Common.Controls.Friendship.Request" = "Richiesta"; +"Common.Controls.Friendship.ShowReblogs" = "Mostra le condivisioni"; "Common.Controls.Friendship.Unblock" = "Sblocca"; "Common.Controls.Friendship.UnblockUser" = "Sblocca %@"; "Common.Controls.Friendship.Unmute" = "Riattiva"; @@ -149,6 +151,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.Compose.Accessibility.AppendAttachment" = "Aggiungi allegato"; "Scene.Compose.Accessibility.AppendPoll" = "Aggiungi sondaggio"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Selettore Emoji personalizzato"; @@ -253,8 +256,12 @@ caricato su Mastodon."; "Scene.Profile.Header.FollowsYou" = "Ti segue"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confermi di bloccare %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Blocca account"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Conferma di nascondere le condivisioni"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Nascondi le condivisioni"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Confermi di silenziare %@"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Silenzia account"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Conferma di mostrare le condivisioni"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Mostra le condivisioni"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Conferma per sbloccare %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Sblocca account"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confermi di riattivare %@"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings index de61ef1d9..080624f06 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings @@ -68,11 +68,13 @@ "Common.Controls.Friendship.EditInfo" = "編集"; "Common.Controls.Friendship.Follow" = "フォロー"; "Common.Controls.Friendship.Following" = "フォロー中"; +"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; "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.Unblock" = "ブロックを解除"; "Common.Controls.Friendship.UnblockUser" = "%@のブロックを解除"; "Common.Controls.Friendship.Unmute" = "ミュートを解除"; @@ -145,6 +147,7 @@ "Scene.AccountList.AddAccount" = "アカウントを追加"; "Scene.AccountList.DismissAccountSwitcher" = "アカウント切替画面を閉じます"; "Scene.AccountList.TabBarHint" = "現在のアカウント: %@. ダブルタップしてアカウント切替画面を表示します"; +"Scene.Bookmark.Title" = "Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "アタッチメントの追加"; "Scene.Compose.Accessibility.AppendPoll" = "投票を追加"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "カスタム絵文字ピッカー"; @@ -248,8 +251,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.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.ConfirmUnblockUser.Message" = "%@のブロックを解除しますか?"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "アカウントのブロックを解除"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "%@をミュートしますか?"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings index 4db503495..1339af4cf 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings @@ -68,11 +68,13 @@ Ma ulac aɣilif, senqed tuqqna-inek internet."; "Common.Controls.Friendship.EditInfo" = "Ẓreg talɣut"; "Common.Controls.Friendship.Follow" = "Ḍfeṛ"; "Common.Controls.Friendship.Following" = "Yettwaḍfar"; +"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; "Common.Controls.Friendship.Mute" = "Sgugem"; "Common.Controls.Friendship.MuteUser" = "Sgugem %@"; "Common.Controls.Friendship.Muted" = "Yettwasgugem"; "Common.Controls.Friendship.Pending" = "Yegguni"; "Common.Controls.Friendship.Request" = "Tuttra"; +"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; "Common.Controls.Friendship.Unblock" = "Serreḥ"; "Common.Controls.Friendship.UnblockUser" = "Serreḥ i %@"; "Common.Controls.Friendship.Unmute" = "Kkes asgugem"; @@ -149,6 +151,7 @@ Akka i as-d-yettban umaɣnu-inek."; "Scene.AccountList.AddAccount" = "Rnu amiḍan"; "Scene.AccountList.DismissAccountSwitcher" = "Sefsex abeddel n umiḍan"; "Scene.AccountList.TabBarHint" = "Amaɣnu amiran yettwafernen: %@. Sit berdayen syen teǧǧeḍ aḍad-ik·im i uskan abeddel n umiḍan"; +"Scene.Bookmark.Title" = "Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "Rnu taceqquft yeddan"; "Scene.Compose.Accessibility.AppendPoll" = "Rnu asenqed"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Amefran n yimujiten udmawanen"; @@ -225,7 +228,7 @@ Ad d-yettwasali ɣef Mastodon."; "Scene.HomeTimeline.Title" = "Agejdan"; "Scene.Notification.FollowRequest.Accept" = "Accept"; "Scene.Notification.FollowRequest.Accepted" = "Accepted"; -"Scene.Notification.FollowRequest.Reject" = "reject"; +"Scene.Notification.FollowRequest.Reject" = "agi"; "Scene.Notification.FollowRequest.Rejected" = "Rejected"; "Scene.Notification.Keyobard.ShowEverything" = "Sken yal taɣawsa"; "Scene.Notification.Keyobard.ShowMentions" = "Sken tisedmirin"; @@ -253,8 +256,12 @@ Ad d-yettwasali ɣef Mastodon."; "Scene.Profile.Header.FollowsYou" = "Yeṭṭafaṛ-ik•im"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Sentem asewḥel n %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Sewḥel amiḍan"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Sentem asgugem i %@"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Sgugem amiḍan"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Sentem tukksa n usgugem i %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Kkes asewḥel i umiḍan"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Sentem tukksa n usgugem i %@"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings index 6c323adfe..a72543a3e 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings @@ -68,11 +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.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.Unblock" = "Astengiyê rake"; "Common.Controls.Friendship.UnblockUser" = "%@ asteng neke"; "Common.Controls.Friendship.Unmute" = "Bêdeng neke"; @@ -149,6 +151,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.Compose.Accessibility.AppendAttachment" = "Pêvek tevlî bike"; "Scene.Compose.Accessibility.AppendPoll" = "Rapirsî tevlî bike"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Hilbijêrê emojî yên kesanekirî"; @@ -254,8 +257,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.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.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 cea8ce7e7..3bcc33bf5 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.strings @@ -67,11 +67,13 @@ "Common.Controls.Friendship.EditInfo" = "Bewerken"; "Common.Controls.Friendship.Follow" = "Volgen"; "Common.Controls.Friendship.Following" = "Gevolgd"; +"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; "Common.Controls.Friendship.Mute" = "Negeren"; "Common.Controls.Friendship.MuteUser" = "Negeer %@"; "Common.Controls.Friendship.Muted" = "Genegeerd"; "Common.Controls.Friendship.Pending" = "In afwachting"; "Common.Controls.Friendship.Request" = "Verzoeken"; +"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; "Common.Controls.Friendship.Unblock" = "Deblokkeer"; "Common.Controls.Friendship.UnblockUser" = "Deblokkeer %@"; "Common.Controls.Friendship.Unmute" = "Niet langer negeren"; @@ -144,6 +146,7 @@ Uw profiel ziet er zo uit voor hen."; "Scene.AccountList.AddAccount" = "Voeg account toe"; "Scene.AccountList.DismissAccountSwitcher" = "Annuleer account wisselen"; "Scene.AccountList.TabBarHint" = "Huidige geselecteerde profiel: %@. Dubbel-tik en houd vast om account te wisselen"; +"Scene.Bookmark.Title" = "Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "Bijlage Toevoegen"; "Scene.Compose.Accessibility.AppendPoll" = "Peiling Toevoegen"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Eigen Emojikiezer"; @@ -248,8 +251,12 @@ klik op de link om uw account te bevestigen."; "Scene.Profile.Header.FollowsYou" = "Follows You"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Bevestig om %@ te blokkeren"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Blokkeer account"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Bevestig om %@ te negeren"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Negeer account"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Bevestig om %@ te deblokkeren"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Deblokkeer Account"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Bevestig om %@ te negeren"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.strings index 8b5adf626..0513a955b 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.strings @@ -68,11 +68,13 @@ "Common.Controls.Friendship.EditInfo" = "Изменить"; "Common.Controls.Friendship.Follow" = "Подписаться"; "Common.Controls.Friendship.Following" = "В подписках"; +"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; "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.Unblock" = "Разблокировать"; "Common.Controls.Friendship.UnblockUser" = "Разблокировать %@"; "Common.Controls.Friendship.Unmute" = "Убрать из игнорируемых"; @@ -157,6 +159,7 @@ "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" = "Прикрепить файл"; "Scene.Compose.Accessibility.AppendPoll" = "Добавить опрос"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Меню пользовательских эмодзи"; @@ -264,8 +267,12 @@ "Scene.Profile.Header.FollowsYou" = "Подписан(а) на вас"; "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" = "Убрать %@ из игнорируемых?"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.strings index 51b18d908..849d88284 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.strings @@ -9,7 +9,7 @@ "Common.Alerts.DiscardPostContent.Message" = "Bekräfta för att slänga inläggsutkast."; "Common.Alerts.DiscardPostContent.Title" = "Släng utkast"; "Common.Alerts.EditProfileFailure.Message" = "Kan inte redigera profil. Var god försök igen."; -"Common.Alerts.EditProfileFailure.Title" = "Profilredigering misslyckades"; +"Common.Alerts.EditProfileFailure.Title" = "Kunde inte redigera profil"; "Common.Alerts.PublishPostFailure.AttachmentsMessage.MoreThanOneVideo" = "Det går inte att bifoga mer än en video."; "Common.Alerts.PublishPostFailure.AttachmentsMessage.VideoAttachWithPhoto" = "Det går inte att bifoga en video till ett inlägg som redan innehåller bilder."; "Common.Alerts.PublishPostFailure.Message" = "Det gick inte att publicera inlägget. @@ -68,11 +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.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.Unblock" = "Avblockera"; "Common.Controls.Friendship.UnblockUser" = "Avblockera %@"; "Common.Controls.Friendship.Unmute" = "Avtysta"; @@ -92,18 +94,18 @@ Kontrollera din internetanslutning."; "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 ompostning på inlägg"; +"Common.Controls.Keyboard.Timeline.ToggleReblog" = "Växla puff 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" = "Omposta"; +"Common.Controls.Status.Actions.Reblog" = "Puffa"; "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 ompostning"; +"Common.Controls.Status.Actions.Unreblog" = "Ångra puff"; "Common.Controls.Status.ContentWarning" = "Innehållsvarning"; "Common.Controls.Status.MediaContentWarning" = "Tryck var som helst för att visa"; "Common.Controls.Status.Poll.Closed" = "Stängd"; @@ -118,7 +120,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" = "%@ ompostade"; +"Common.Controls.Status.UserReblogged" = "%@ puffade"; "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."; @@ -149,6 +151,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.Compose.Accessibility.AppendAttachment" = "Lägg till bilaga"; "Scene.Compose.Accessibility.AppendPoll" = "Lägg till omröstning"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Anpassad emoji-väljare"; @@ -223,17 +226,17 @@ laddas upp till Mastodon."; "Scene.HomeTimeline.NavigationBarState.Published" = "Publicerat!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Publicerar inlägget..."; "Scene.HomeTimeline.Title" = "Hem"; -"Scene.Notification.FollowRequest.Accept" = "Accept"; -"Scene.Notification.FollowRequest.Accepted" = "Accepted"; -"Scene.Notification.FollowRequest.Reject" = "reject"; -"Scene.Notification.FollowRequest.Rejected" = "Rejected"; +"Scene.Notification.FollowRequest.Accept" = "Godkänn"; +"Scene.Notification.FollowRequest.Accepted" = "Godkänd"; +"Scene.Notification.FollowRequest.Reject" = "avvisa"; +"Scene.Notification.FollowRequest.Rejected" = "Avvisad"; "Scene.Notification.Keyobard.ShowEverything" = "Visa allt"; "Scene.Notification.Keyobard.ShowMentions" = "Visa omnämningar"; "Scene.Notification.NotificationDescription.FavoritedYourPost" = "favoriserade ditt inlägg"; "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" = "ompostade ditt inlägg"; +"Scene.Notification.NotificationDescription.RebloggedYourPost" = "puffade ditt inlägg"; "Scene.Notification.NotificationDescription.RequestToFollowYou" = "begär att följa dig"; "Scene.Notification.Title.Everything" = "Allting"; "Scene.Notification.Title.Mentions" = "Omnämningar"; @@ -250,11 +253,15 @@ laddas upp till Mastodon."; "Scene.Profile.Fields.AddRow" = "Lägg till rad"; "Scene.Profile.Fields.Placeholder.Content" = "Innehåll"; "Scene.Profile.Fields.Placeholder.Label" = "Etikett"; -"Scene.Profile.Header.FollowsYou" = "Follows You"; +"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.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.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 %@"; @@ -264,7 +271,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" = "Ompostat av"; +"Scene.RebloggedBy.Title" = "Puffat av"; "Scene.Register.Error.Item.Agreement" = "Avtal"; "Scene.Register.Error.Item.Email" = "E-post"; "Scene.Register.Error.Item.Locale" = "Språk"; @@ -410,7 +417,7 @@ laddas upp till Mastodon."; "Scene.Settings.Section.Notifications.Boosts" = "Ompostar 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" = "Omnämner mig"; +"Scene.Settings.Section.Notifications.Mentions" = "Nämner mig"; "Scene.Settings.Section.Notifications.Title" = "Notiser"; "Scene.Settings.Section.Notifications.Trigger.Anyone" = "alla"; "Scene.Settings.Section.Notifications.Trigger.Follow" = "någon jag följer"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.stringsdict index 27ef9fb53..048af4732 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.stringsdict @@ -90,7 +90,7 @@ one inlägg other - inläggen + inlägg plural.count.media @@ -152,9 +152,9 @@ NSStringFormatValueTypeKey ld one - %ld ompostning + %ld puff other - %ld ompostningar + %ld puffar plural.count.reply diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings index f29e08d82..15514928c 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings @@ -68,11 +68,13 @@ "Common.Controls.Friendship.EditInfo" = "แก้ไขข้อมูล"; "Common.Controls.Friendship.Follow" = "ติดตาม"; "Common.Controls.Friendship.Following" = "กำลังติดตาม"; +"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; "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.Unblock" = "เลิกปิดกั้น"; "Common.Controls.Friendship.UnblockUser" = "เลิกปิดกั้น %@"; "Common.Controls.Friendship.Unmute" = "เลิกซ่อน"; @@ -149,6 +151,7 @@ "Scene.AccountList.AddAccount" = "เพิ่มบัญชี"; "Scene.AccountList.DismissAccountSwitcher" = "ปิดตัวสลับบัญชี"; "Scene.AccountList.TabBarHint" = "โปรไฟล์ที่เลือกในปัจจุบัน: %@ แตะสองครั้งแล้วกดค้างไว้เพื่อแสดงตัวสลับบัญชี"; +"Scene.Bookmark.Title" = "Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "เพิ่มไฟล์แนบ"; "Scene.Compose.Accessibility.AppendPoll" = "เพิ่มการสำรวจความคิดเห็น"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "ตัวเลือกอีโมจิที่กำหนดเอง"; @@ -253,8 +256,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.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.ConfirmUnblockUser.Message" = "ยืนยันเพื่อเลิกปิดกั้น %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "เลิกปิดกั้นบัญชี"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "ยืนยันเพื่อเลิกซ่อน %@"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.strings index 814969c39..eec4a2940 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.strings @@ -67,11 +67,13 @@ "Common.Controls.Friendship.EditInfo" = "Bilgiyi Düzenle"; "Common.Controls.Friendship.Follow" = "Takip et"; "Common.Controls.Friendship.Following" = "Takip ediliyor"; +"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; "Common.Controls.Friendship.Mute" = "Sessize al"; "Common.Controls.Friendship.MuteUser" = "Sustur %@"; "Common.Controls.Friendship.Muted" = "Susturuldu"; "Common.Controls.Friendship.Pending" = "Bekliyor"; "Common.Controls.Friendship.Request" = "İstek"; +"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; "Common.Controls.Friendship.Unblock" = "Engeli kaldır"; "Common.Controls.Friendship.UnblockUser" = "%@ kişisinin engelini kaldır"; "Common.Controls.Friendship.Unmute" = "Susturmayı kaldır"; @@ -148,6 +150,7 @@ Bu kişiye göre profiliniz böyle gözüküyor."; "Scene.AccountList.AddAccount" = "Hesap Ekle"; "Scene.AccountList.DismissAccountSwitcher" = "Hesap Değiştiriciyi Kapat"; "Scene.AccountList.TabBarHint" = "Şu anki seçili profil: %@. Hesap değiştiriciyi göstermek için iki kez dokunun ve basılı tutun"; +"Scene.Bookmark.Title" = "Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "Dosya Ekle"; "Scene.Compose.Accessibility.AppendPoll" = "Anket Ekle"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Özel Emoji Seçici"; @@ -199,7 +202,7 @@ yüklenemiyor."; "Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "E-posta İstemcisini Aç"; "Scene.ConfirmEmail.OpenEmailApp.Title" = "Gelen kutunuzu kontrol edin."; "Scene.ConfirmEmail.Subtitle" = "Hesabınızı doğrulamak için size e-postayla gönderdiğimiz bağlantıya dokunun."; -"Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount" = "Tap the link we emailed to you to verify your account"; +"Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount" = "Hesabınızı doğrulamak için size e-postayla gönderdiğimiz bağlantıya dokunun"; "Scene.ConfirmEmail.Title" = "Son bir şey."; "Scene.Discovery.Intro" = "Bunlar, Mastodon'un köşesinde ilgi çeken gönderilerdir."; "Scene.Discovery.Tabs.Community" = "Topluluk"; @@ -212,11 +215,11 @@ yüklenemiyor."; "Scene.Favorite.Title" = "Favorilerin"; "Scene.FavoritedBy.Title" = "Favorited By"; "Scene.Follower.Footer" = "Diğer sunucudaki takipçiler gösterilemiyor."; -"Scene.Follower.Title" = "follower"; +"Scene.Follower.Title" = "takipçi"; "Scene.Following.Footer" = "Diğer sunucudaki takip edilenler gösterilemiyor."; -"Scene.Following.Title" = "following"; +"Scene.Following.Title" = "takip"; "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.Accessibility.LogoLabel" = "Logo Düğmesi"; "Scene.HomeTimeline.NavigationBarState.NewPosts" = "Yeni gönderiler gör"; "Scene.HomeTimeline.NavigationBarState.Offline" = "Çevrimdışı"; "Scene.HomeTimeline.NavigationBarState.Published" = "Yayınlandı!"; @@ -249,11 +252,15 @@ yüklenemiyor."; "Scene.Profile.Fields.AddRow" = "Satır Ekle"; "Scene.Profile.Fields.Placeholder.Content" = "İçerik"; "Scene.Profile.Fields.Placeholder.Label" = "Etiket"; -"Scene.Profile.Header.FollowsYou" = "Follows You"; +"Scene.Profile.Header.FollowsYou" = "Seni takip ediyor"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "%@ engellemeyi onayla"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Hesabı Engelle"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "%@ susturmak için onaylayın"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Hesabı sustur"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "%@ engellemeyi kaldırmayı onaylayın"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Hesabın Engelini Kaldır"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "%@ susturmasını kaldırmak için onaylayın"; @@ -266,7 +273,7 @@ yüklenemiyor."; "Scene.RebloggedBy.Title" = "Reblogged By"; "Scene.Register.Error.Item.Agreement" = "Anlaşma"; "Scene.Register.Error.Item.Email" = "E-posta"; -"Scene.Register.Error.Item.Locale" = "Locale"; +"Scene.Register.Error.Item.Locale" = "Yerel"; "Scene.Register.Error.Item.Password" = "Parola"; "Scene.Register.Error.Item.Reason" = "Sebep"; "Scene.Register.Error.Item.Username" = "Kullanıcı adı"; @@ -296,7 +303,7 @@ yüklenemiyor."; "Scene.Register.Input.Password.Require" = "Parolanızda en azından şunlar olmalı:"; "Scene.Register.Input.Username.DuplicatePrompt" = "Bu kullanıcı adı alınmış."; "Scene.Register.Input.Username.Placeholder" = "kullanıcı adı"; -"Scene.Register.LetsGetYouSetUpOnDomain" = "Let’s get you set up on %@"; +"Scene.Register.LetsGetYouSetUpOnDomain" = "%@ için kurulumunuzu yapalım"; "Scene.Register.Title" = "%@ için kurulumunuzu yapalım"; "Scene.Report.Content1" = "Bu rapora eklemek istediğiniz başka gönderiler var mı?"; "Scene.Report.Content2" = "Bu rapor hakkında moderatörlerin bilmesi gerektiği bir şey var mı?"; @@ -308,10 +315,10 @@ yüklenemiyor."; "Scene.Report.Step2" = "Adım 2/2"; "Scene.Report.StepFinal.BlockUser" = "Block %@"; "Scene.Report.StepFinal.DontWantToSeeThis" = "Bunu görmek istemiyor musunuz?"; -"Scene.Report.StepFinal.MuteUser" = "Mute %@"; +"Scene.Report.StepFinal.MuteUser" = "Sustur %@"; "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" = "Takibi bırak"; -"Scene.Report.StepFinal.UnfollowUser" = "Unfollow %@"; +"Scene.Report.StepFinal.UnfollowUser" = "Takipten çık %@"; "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 %@"; @@ -378,7 +385,7 @@ yüklenemiyor."; "Scene.ServerPicker.EmptyState.FindingServers" = "Mevcut sunucular aranıyor..."; "Scene.ServerPicker.EmptyState.NoResults" = "Sonuç yok"; "Scene.ServerPicker.Input.Placeholder" = "Toplulukları ara"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search servers or enter URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Sunucuları ara ya da bir bağlantı gir"; "Scene.ServerPicker.Label.Category" = "KATEGORİ"; "Scene.ServerPicker.Label.Language" = "DİL"; "Scene.ServerPicker.Label.Users" = "KULLANICILAR"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.stringsdict index 3da12ee4e..29df92c2b 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.stringsdict @@ -234,7 +234,7 @@ one 1 takip edilen other - %ld takip edilen + %ld takip plural.count.follower diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings index b3be7f302..14f36c7e7 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings @@ -68,11 +68,13 @@ Vui lòng kiểm tra kết nối mạng."; "Common.Controls.Friendship.EditInfo" = "Chỉnh sửa"; "Common.Controls.Friendship.Follow" = "Theo dõi"; "Common.Controls.Friendship.Following" = "Đang theo dõi"; +"Common.Controls.Friendship.HideReblogs" = "Ẩn đăng lại"; "Common.Controls.Friendship.Mute" = "Ẩn"; "Common.Controls.Friendship.MuteUser" = "Ẩn %@"; "Common.Controls.Friendship.Muted" = "Đã ẩn"; "Common.Controls.Friendship.Pending" = "Đang chờ"; "Common.Controls.Friendship.Request" = "Yêu cầu"; +"Common.Controls.Friendship.ShowReblogs" = "Hiện đăng lại"; "Common.Controls.Friendship.Unblock" = "Bỏ chặn"; "Common.Controls.Friendship.UnblockUser" = "Bỏ chặn %@"; "Common.Controls.Friendship.Unmute" = "Bỏ ẩn"; @@ -135,7 +137,7 @@ cho tới khi họ bỏ chặn bạn."; cho tới khi bạn bỏ chặn họ. Họ sẽ thấy trang của bạn như thế này."; "Common.Controls.Timeline.Header.NoStatusFound" = "Không tìm thấy tút"; -"Common.Controls.Timeline.Header.SuspendedWarning" = "Người dùng đã bị vô hiệu hóa."; +"Common.Controls.Timeline.Header.SuspendedWarning" = "Người này đã bị vô hiệu hóa."; "Common.Controls.Timeline.Header.UserBlockedWarning" = "Bạn không thể xem trang %@ cho tới khi họ bỏ chặn bạn."; "Common.Controls.Timeline.Header.UserBlockingWarning" = "Bạn không thể xem trang %@ @@ -149,6 +151,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.Compose.Accessibility.AppendAttachment" = "Thêm media"; "Scene.Compose.Accessibility.AppendPoll" = "Tạo bình chọn"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Chọn emoji"; @@ -187,7 +190,7 @@ tải lên Mastodon."; "Scene.Compose.Title.NewPost" = "Viết tút"; "Scene.Compose.Title.NewReply" = "Viết trả lời"; "Scene.Compose.Visibility.Direct" = "Nhắn riêng"; -"Scene.Compose.Visibility.Private" = "Riêng tư"; +"Scene.Compose.Visibility.Private" = "Chỉ người theo dõi"; "Scene.Compose.Visibility.Public" = "Công khai"; "Scene.Compose.Visibility.Unlisted" = "Hạn chế"; "Scene.ConfirmEmail.Button.OpenEmailApp" = "Mở ứng dụng email"; @@ -252,13 +255,17 @@ tải lên Mastodon."; "Scene.Profile.Fields.Placeholder.Label" = "Nhãn"; "Scene.Profile.Header.FollowsYou" = "Đang theo dõi bạn"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Xác nhận chặn %@"; -"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Chặn người dùng"; +"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Chặn người này"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Xác nhận ẩn đăng lại"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Ẩn đăng lại"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Xác nhận ẩn %@"; -"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Ẩn người dùng"; +"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Ẩn người này"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Xác nhận hiện đăng lại"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Hiện đăng lại"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Xác nhận bỏ chặn %@"; -"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Bỏ chặn người dùng"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Bỏ chặn người này"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Xác nhận bỏ ẩn %@"; -"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title" = "Bỏ ẩn người dùng"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title" = "Bỏ ẩn người này"; "Scene.Profile.SegmentedControl.About" = "Giới thiệu"; "Scene.Profile.SegmentedControl.Media" = "Media"; "Scene.Profile.SegmentedControl.Posts" = "Tút"; @@ -286,7 +293,7 @@ tải lên Mastodon."; "Scene.Register.Error.Special.UsernameInvalid" = "Tên người dùng chỉ có thể chứa các ký tự chữ và số và dấu gạch dưới"; "Scene.Register.Error.Special.UsernameTooLong" = "Tên người dùng không thể dài hơn 30 ký tự"; "Scene.Register.Input.Avatar.Delete" = "Xóa"; -"Scene.Register.Input.DisplayName.Placeholder" = "tên hiển thị"; +"Scene.Register.Input.DisplayName.Placeholder" = "biệt danh"; "Scene.Register.Input.Email.Placeholder" = "email"; "Scene.Register.Input.Invite.RegistrationUserInviteRequest" = "Vì sao bạn muốn tham gia?"; "Scene.Register.Input.Password.Accessibility.Checked" = "đã ổn"; @@ -348,15 +355,15 @@ tải lên Mastodon."; "Scene.Search.Recommend.ButtonText" = "Xem tất cả"; "Scene.Search.Recommend.HashTag.Description" = "Những hashtag đang được sử dụng nhiều nhất"; "Scene.Search.Recommend.HashTag.PeopleTalking" = "%@ người đang thảo luận"; -"Scene.Search.Recommend.HashTag.Title" = "Xu hướng trên Mastodon"; +"Scene.Search.Recommend.HashTag.Title" = "Nổi bật trên Mastodon"; "Scene.Search.SearchBar.Cancel" = "Hủy bỏ"; -"Scene.Search.SearchBar.Placeholder" = "Tìm hashtag và người dùng"; +"Scene.Search.SearchBar.Placeholder" = "Tìm hashtag và mọi người"; "Scene.Search.Searching.Clear" = "Xóa"; "Scene.Search.Searching.EmptyState.NoResults" = "Không có kết quả"; "Scene.Search.Searching.RecentSearch" = "Tìm kiếm gần đây"; "Scene.Search.Searching.Segment.All" = "Tất cả"; "Scene.Search.Searching.Segment.Hashtags" = "Hashtag"; -"Scene.Search.Searching.Segment.People" = "Người dùng"; +"Scene.Search.Searching.Segment.People" = "Mọi người"; "Scene.Search.Searching.Segment.Posts" = "Tút"; "Scene.Search.Title" = "Tìm kiếm"; "Scene.ServerPicker.Button.Category.Academia" = "học thuật"; @@ -382,7 +389,7 @@ tải lên Mastodon."; "Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Tìm máy chủ hoặc nhập URL"; "Scene.ServerPicker.Label.Category" = "PHÂN LOẠI"; "Scene.ServerPicker.Label.Language" = "NGÔN NGỮ"; -"Scene.ServerPicker.Label.Users" = "NGƯỜI DÙNG"; +"Scene.ServerPicker.Label.Users" = "NGƯỜI"; "Scene.ServerPicker.Subtitle" = "Chọn một máy chủ dựa theo sở thích, tôn giáo, hoặc ý muốn của bạn."; "Scene.ServerPicker.SubtitleExtend" = "Chọn một máy chủ dựa theo sở thích, tôn giáo, hoặc ý muốn của bạn. Mỗi máy chủ có thể được vận hành bởi một cá nhân hoặc một tổ chức."; "Scene.ServerPicker.Title" = "Mastodon gồm nhiều máy chủ với thành viên riêng."; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings index 62117499d..8e5de6b52 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings @@ -68,11 +68,13 @@ "Common.Controls.Friendship.EditInfo" = "编辑"; "Common.Controls.Friendship.Follow" = "关注"; "Common.Controls.Friendship.Following" = "正在关注"; +"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; "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.Unblock" = "解除屏蔽"; "Common.Controls.Friendship.UnblockUser" = "解除屏蔽 %@"; "Common.Controls.Friendship.Unmute" = "取消静音"; @@ -149,6 +151,7 @@ "Scene.AccountList.AddAccount" = "添加账户"; "Scene.AccountList.DismissAccountSwitcher" = "关闭账户切换页面"; "Scene.AccountList.TabBarHint" = "当前账户:%@。 双击并按住来打开账户切换页面"; +"Scene.Bookmark.Title" = "Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "添加附件"; "Scene.Compose.Accessibility.AppendPoll" = "添加投票"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "自定义表情选择器"; @@ -253,8 +256,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.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.ConfirmUnblockUser.Message" = "确认取消屏蔽 %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "解除屏蔽帐户"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "确认取消静音 %@"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings index 10fd6837a..f97926795 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings @@ -1,5 +1,5 @@ "Common.Alerts.BlockDomain.BlockEntireDomain" = "封鎖網域"; -"Common.Alerts.BlockDomain.Title" = "真的非常確定封鎖整個 %@ 網域嗎?大部分情況下,您只需要封鎖或靜音少數特定的帳帳戶能滿足需求了。您將不能看到來自此網域的內容。您來自該網域的跟隨者也將被移除。"; +"Common.Alerts.BlockDomain.Title" = "真的非常確定要封鎖整個 %@ 網域嗎?大部分情況下,您只需要封鎖或靜音少數特定的帳號能滿足需求了。您將不能看到來自此網域的內容。您來自該網域的跟隨者也將被移除。"; "Common.Alerts.CleanCache.Message" = "成功清除 %@ 快取。"; "Common.Alerts.CleanCache.Title" = "清除快取"; "Common.Alerts.Common.PleaseTryAgain" = "請再試一次。"; @@ -68,11 +68,13 @@ "Common.Controls.Friendship.EditInfo" = "編輯"; "Common.Controls.Friendship.Follow" = "跟隨"; "Common.Controls.Friendship.Following" = "跟隨中"; +"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" = "顯示轉嘟"; "Common.Controls.Friendship.Unblock" = "解除封鎖"; "Common.Controls.Friendship.UnblockUser" = "解除封鎖 %@"; "Common.Controls.Friendship.Unmute" = "取消靜音"; @@ -145,6 +147,7 @@ "Scene.AccountList.AddAccount" = "新增帳號"; "Scene.AccountList.DismissAccountSwitcher" = "關閉帳號切換器"; "Scene.AccountList.TabBarHint" = "目前已選擇的個人檔案:%@。點兩下然後按住以顯示帳號切換器"; +"Scene.Bookmark.Title" = "Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "新增附件"; "Scene.Compose.Accessibility.AppendPoll" = "新增投票"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "自訂 emoji 選擇器"; @@ -248,8 +251,12 @@ "Scene.Profile.Header.FollowsYou" = "跟隨了您"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "確認將 %@ 封鎖"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "封鎖"; +"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" = "確認顯示轉嘟"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "顯示轉嘟"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "確認將 %@ 取消封鎖"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "取消封鎖"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "確認將 %@ 取消靜音"; From 1a4f8b795eb1376033e58296edb3d8ca08621086 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 08:27:54 +0100 Subject: [PATCH 116/658] 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 117/658] 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 118/658] 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 119/658] 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 81094c6676e9d24fcf3b4c9569afeed817f5233f Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 9 Nov 2022 12:23:38 +0100 Subject: [PATCH 120/658] Add a little documentation on how to L10n (#539) --- Localization/README.md | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/Localization/README.md b/Localization/README.md index ac32319cc..9bdbae360 100644 --- a/Localization/README.md +++ b/Localization/README.md @@ -1,23 +1,34 @@ # Localization [![Crowdin](https://badges.crowdin.net/mastodon-for-ios/localized.svg)](https://crowdin.com/project/mastodon-for-ios) -Mastodon localization template file +We use Crowdin for translations and some automation. +## How to contribute -## How to contribute? +### Help with translations -Please use the [Crodwin](https://crowdin.com/project/mastodon-for-ios) to contribute. If your language is not in the list. Please feel free to open the issue. +Head over [Crowdin][crowdin-mastodon-ios] for that. To help with translations, select your language and translate :-) If your language is not in the list, please feel free to [open a topic on Crowdin](crowdin-mastodon-ios-discussions). -## How to maintains +Please note: You need to have an account on Crowdin to help with translations. -The project use a script to generate Xcode localized strings files. +### Add new strings -```zsh -// enter workdir -cd Mastodon +This is mainly for developers. -// merge PR from Crowdin bot +1. Add new strings in `Localization/app.json` **and** the `Localizable.strings` for English. +2. Run `swiftgen` to generate the `Strings.swift`-file **or** have Xcode build the app (`swiftgen` is a Build phase, too). +3. Use `import MastodonLocalization` and its (new) `L10n`-enum and its properties where ever you need them in the app. +4. Once the updated `Localization/app.json` hits `develop`, it gets synced to Crowdin, where people can help with translations. `Localization/app.json` must be a valid json. -// update resource -./update_localization.sh -``` \ No newline at end of file +## How to update translations + +If there are new translations, Crowdin pushes new commits to a branch called `l10n_develop` and creates a new Pull Request. Both, the branch and the PR might be updated once an hour. The project itself uses a script to generate the various `Localizable.strings`-files etc. for Xcode. + +To add new strings, the workflow is as follows: + +1. Merge the PR with `l10n_develop` into `develop`. +2. Run `update.localization.sh` +3. Commit the changes and push `develop`. + +[crowdin-mastodon-ios]: https://crowdin.com/project/mastodon-for-ios +[crowdin-mastodon-ios-discussions]: https://crowdin.com/project/mastodon-for-ios/discussions \ No newline at end of file From 5589ad0b61f774b8222b242977eaed4f49110b66 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 9 Nov 2022 12:25:17 +0100 Subject: [PATCH 121/658] Fix typo (#539) --- Localization/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/README.md b/Localization/README.md index 9bdbae360..93bf290bc 100644 --- a/Localization/README.md +++ b/Localization/README.md @@ -24,11 +24,11 @@ This is mainly for developers. If there are new translations, Crowdin pushes new commits to a branch called `l10n_develop` and creates a new Pull Request. Both, the branch and the PR might be updated once an hour. The project itself uses a script to generate the various `Localizable.strings`-files etc. for Xcode. -To add new strings, the workflow is as follows: +To update or add new translations, the workflow is as follows: -1. Merge the PR with `l10n_develop` into `develop`. -2. Run `update.localization.sh` +1. Merge the PR with `l10n_develop` into `develop`. It's usually called `New Crowdin Updates` +2. Run `update.localization.sh` on your computer. 3. Commit the changes and push `develop`. [crowdin-mastodon-ios]: https://crowdin.com/project/mastodon-for-ios -[crowdin-mastodon-ios-discussions]: https://crowdin.com/project/mastodon-for-ios/discussions \ No newline at end of file +[crowdin-mastodon-ios-discussions]: https://crowdin.com/project/mastodon-for-ios/discussions From fcf38a15d69a7659882eb823a5257806109db0bd Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Wed, 9 Nov 2022 07:35:06 -0500 Subject: [PATCH 122/658] =?UTF-8?q?Revert=20"Consistently=20handle=20?= =?UTF-8?q?=E2=80=9CA11y=E2=80=9D=20key"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d96f189980917992d9b832d5dd9251893792dc04. --- .../StringsConvertor/Sources/StringsConvertor/Parser.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Localization/StringsConvertor/Sources/StringsConvertor/Parser.swift b/Localization/StringsConvertor/Sources/StringsConvertor/Parser.swift index c355493fe..ba9750cd4 100644 --- a/Localization/StringsConvertor/Sources/StringsConvertor/Parser.swift +++ b/Localization/StringsConvertor/Sources/StringsConvertor/Parser.swift @@ -43,12 +43,7 @@ extension Parser { .map { switch keyStyle { case .infoPlist: return $0 - case .swiftgen: - if $0 == "a11y" { - return "A11y" - } else { - return $0.capitalized - } + case .swiftgen: return $0.capitalized } } .joined() From 1425f348283de31e5bb8b2ca715944fc7b0d54b7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 15:19:18 +0100 Subject: [PATCH 123/658] 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 124/658] 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 125/658] 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 1aaa0e827e5564f7ad399c962fe51121e4c4a97b Mon Sep 17 00:00:00 2001 From: treeshateorcs Date: Wed, 9 Nov 2022 20:20:16 +0500 Subject: [PATCH 126/658] make sure you have rosetta installed on m1 mac --- Documentation/Setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/Setup.md b/Documentation/Setup.md index ecb518a59..c7130a091 100644 --- a/Documentation/Setup.md +++ b/Documentation/Setup.md @@ -12,7 +12,7 @@ Install the latest version of Xcode from the App Store or Apple Developer Downlo This guide may not suit your machine and actually setup procedure may change in the future. Please file the issue or Pull Request if there are any problems. ## CocoaPods -The app use [CocoaPods]() and [Arkana](https://github.com/rogerluan/arkana). Ruby Gems are managed through Bundler. The M1 Mac needs virtual ruby env to workaround compatibility issues. +The app use [CocoaPods]() and [Arkana](https://github.com/rogerluan/arkana). Ruby Gems are managed through Bundler. The M1 Mac needs virtual ruby env to workaround compatibility issues. Make sure you have Rosetta installed if you are on the M1 Mac. #### Intel Mac From a4f6fea0bf24b16b4b4f897e864d2605814a8f49 Mon Sep 17 00:00:00 2001 From: treeshateorcs Date: Wed, 9 Nov 2022 20:23:45 +0500 Subject: [PATCH 127/658] edit grammar --- Documentation/Setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/Setup.md b/Documentation/Setup.md index c7130a091..bb41f2c02 100644 --- a/Documentation/Setup.md +++ b/Documentation/Setup.md @@ -12,7 +12,7 @@ Install the latest version of Xcode from the App Store or Apple Developer Downlo This guide may not suit your machine and actually setup procedure may change in the future. Please file the issue or Pull Request if there are any problems. ## CocoaPods -The app use [CocoaPods]() and [Arkana](https://github.com/rogerluan/arkana). Ruby Gems are managed through Bundler. The M1 Mac needs virtual ruby env to workaround compatibility issues. Make sure you have Rosetta installed if you are on the M1 Mac. +The app use [CocoaPods]() and [Arkana](https://github.com/rogerluan/arkana). Ruby Gems are managed through Bundler. The M1 Mac needs virtual ruby env to workaround compatibility issues. Make sure you have Rosetta installed if you are using the M1 Mac. #### Intel Mac From 306b611887dff07da804b1274685a73f9d9cfd9d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 16:35:57 +0100 Subject: [PATCH 128/658] 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 129/658] 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 130/658] 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 131/658] 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 132/658] 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 133/658] 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 134/658] 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 135/658] 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 136/658] 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 137/658] 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 138/658] 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 139/658] 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 140/658] 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 141/658] 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 142/658] 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 143/658] 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 144/658] 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 145/658] 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 146/658] 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 147/658] 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 148/658] 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 149/658] 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 150/658] 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 151/658] 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 152/658] 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 153/658] 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 154/658] 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 155/658] 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 156/658] 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 157/658] 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 158/658] 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 159/658] 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 160/658] 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 161/658] 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 162/658] 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 163/658] 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 164/658] 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 165/658] 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 d14966792a6d60d948685e976cb60beaf267273c Mon Sep 17 00:00:00 2001 From: woxtu Date: Thu, 10 Nov 2022 01:50:04 +0900 Subject: [PATCH 166/658] Fix typos --- Documentation/Snapshot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/Snapshot.md b/Documentation/Snapshot.md index 7140f7a0b..857c1b586 100644 --- a/Documentation/Snapshot.md +++ b/Documentation/Snapshot.md @@ -14,7 +14,7 @@ We use `xcodebuild` CLI tool to trigger UITest. Set the `name` in `-destination` option to add device for snapshot. For example: `-destination 'platform=iOS Simulator,name=iPad Pro (12.9-inch) (5th generation)' \` -You can list the avaiable simulator: +You can list the available simulators: ```zsh # list the destinations xcodebuild \ From d1bf623c7636371e02633dcf4bf6dd69c91f7668 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 18:02:06 +0100 Subject: [PATCH 167/658] 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 168/658] 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 169/658] 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 170/658] 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 19af7d2a0b47dc3ac0a8e9cdc517746477714599 Mon Sep 17 00:00:00 2001 From: treeshateorcs Date: Wed, 9 Nov 2022 23:28:07 +0500 Subject: [PATCH 171/658] add link to apple documentation --- Documentation/Setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/Setup.md b/Documentation/Setup.md index bb41f2c02..4da3c41ca 100644 --- a/Documentation/Setup.md +++ b/Documentation/Setup.md @@ -12,7 +12,7 @@ Install the latest version of Xcode from the App Store or Apple Developer Downlo This guide may not suit your machine and actually setup procedure may change in the future. Please file the issue or Pull Request if there are any problems. ## CocoaPods -The app use [CocoaPods]() and [Arkana](https://github.com/rogerluan/arkana). Ruby Gems are managed through Bundler. The M1 Mac needs virtual ruby env to workaround compatibility issues. Make sure you have Rosetta installed if you are using the M1 Mac. +The app use [CocoaPods]() and [Arkana](https://github.com/rogerluan/arkana). Ruby Gems are managed through Bundler. The M1 Mac needs virtual ruby env to workaround compatibility issues. Make sure you have [Rosetta](https://support.apple.com/en-us/HT211861) installed if you are using the M1 Mac. #### Intel Mac From 8e7bdd4aec9ad020395f0c45ce9ee683249da46c Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Wed, 9 Nov 2022 14:01:49 -0500 Subject: [PATCH 172/658] Allow content warning descriptions to be on multiple lines --- MastodonSDK/Sources/MastodonUI/Extension/MetaLabel.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonUI/Extension/MetaLabel.swift b/MastodonSDK/Sources/MastodonUI/Extension/MetaLabel.swift index 22c05a969..3e2c6853c 100644 --- a/MastodonSDK/Sources/MastodonUI/Extension/MetaLabel.swift +++ b/MastodonSDK/Sources/MastodonUI/Extension/MetaLabel.swift @@ -66,7 +66,9 @@ extension MetaLabel { textColor = Asset.Colors.Label.primary.color textAlignment = .center paragraphStyle.alignment = .center - + numberOfLines = 0 + textContainer.maximumNumberOfLines = 0 + case .statusSpoilerBanner: font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)) textColor = Asset.Colors.Label.primary.color From baf62ec2004f30b9c1b0cebdc368af048d5a56be Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 20:14:08 +0100 Subject: [PATCH 173/658] 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 174/658] 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 175/658] 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 287aa7a2db088d779dcc76c1954baa2281f32813 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Thevenet Date: Wed, 9 Nov 2022 21:18:15 +0100 Subject: [PATCH 176/658] Update app.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## EASY FIX Typo fix & consistency 😉 --- Localization/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/app.json b/Localization/app.json index 8867385e2..c5a3dac74 100644 --- a/Localization/app.json +++ b/Localization/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 ad3e8b46ea63438f3eaa3b3a64c3ee0469cd78f6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Nov 2022 22:06:23 +0100 Subject: [PATCH 177/658] 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 178/658] 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 179/658] 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 180/658] 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 181/658] 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 182/658] 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 183/658] 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 184/658] 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 185/658] 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 186/658] 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 187/658] 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 188/658] 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 189/658] 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 190/658] 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 191/658] 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 192/658] 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 193/658] 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 194/658] 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 195/658] 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 196/658] 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 197/658] 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 198/658] 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 199/658] 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 200/658] 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 201/658] 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 202/658] 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 203/658] 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 204/658] 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 205/658] 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 206/658] 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 207/658] 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 208/658] 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 209/658] 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 210/658] 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 211/658] 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 212/658] 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 213/658] 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 214/658] 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 215/658] 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 216/658] 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 217/658] 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 218/658] 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 219/658] 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 220/658] 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 221/658] 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 222/658] 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 223/658] 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 224/658] 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 225/658] 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 5d2f4b68f847bd162bd25df677fc197f44c56e0d Mon Sep 17 00:00:00 2001 From: woxtu Date: Fri, 11 Nov 2022 00:36:39 +0900 Subject: [PATCH 226/658] Remove duplicate imports --- Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift | 1 - Mastodon/Scene/Report/ReportResult/ReportResultView.swift | 1 - ShareActionExtension/Scene/ComposeViewController.swift | 1 - ShareActionExtension/Scene/ComposeViewModel.swift | 1 - ShareActionExtension/Scene/View/ComposeToolbarView.swift | 1 - 5 files changed, 5 deletions(-) diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift index 1b0d505b5..f96a02ddc 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift @@ -7,7 +7,6 @@ import Foundation import Combine -import Combine import CoreData import CoreDataStack import GameplayKit diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultView.swift b/Mastodon/Scene/Report/ReportResult/ReportResultView.swift index 75021934b..361f5db24 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultView.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultView.swift @@ -8,7 +8,6 @@ import UIKit import SwiftUI import MastodonSDK -import MastodonUI import MastodonAsset import MastodonCore import MastodonUI diff --git a/ShareActionExtension/Scene/ComposeViewController.swift b/ShareActionExtension/Scene/ComposeViewController.swift index c93c05147..a7605da17 100644 --- a/ShareActionExtension/Scene/ComposeViewController.swift +++ b/ShareActionExtension/Scene/ComposeViewController.swift @@ -8,7 +8,6 @@ import os.log import UIKit import Combine -import MastodonUI import SwiftUI import MastodonAsset import MastodonLocalization diff --git a/ShareActionExtension/Scene/ComposeViewModel.swift b/ShareActionExtension/Scene/ComposeViewModel.swift index 4470cfbe8..93515b7dc 100644 --- a/ShareActionExtension/Scene/ComposeViewModel.swift +++ b/ShareActionExtension/Scene/ComposeViewModel.swift @@ -11,7 +11,6 @@ import Combine import CoreData import CoreDataStack import MastodonSDK -import MastodonUI import SwiftUI import UniformTypeIdentifiers import MastodonAsset diff --git a/ShareActionExtension/Scene/View/ComposeToolbarView.swift b/ShareActionExtension/Scene/View/ComposeToolbarView.swift index 557706fd8..07833ac90 100644 --- a/ShareActionExtension/Scene/View/ComposeToolbarView.swift +++ b/ShareActionExtension/Scene/View/ComposeToolbarView.swift @@ -9,7 +9,6 @@ import os.log import UIKit import Combine import MastodonSDK -import MastodonUI import MastodonAsset import MastodonLocalization import MastodonCore From 38221e059972776b666e9eb84ccf2d1bb686329a Mon Sep 17 00:00:00 2001 From: Natalia Ossipova Date: Thu, 10 Nov 2022 17:17:22 +0100 Subject: [PATCH 227/658] Remove mute/block/report from menu for own toots (#519) --- .../View/Content/StatusAuthorView.swift | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusAuthorView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusAuthorView.swift index e2879d1e8..0631875c0 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusAuthorView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusAuthorView.swift @@ -152,25 +152,30 @@ extension StatusAuthorView { } public func setupAuthorMenu(menuContext: AuthorMenuContext) -> (UIMenu, [UIAccessibilityCustomAction]) { - var actions: [MastodonMenu.Action] = [] + var actions = [MastodonMenu.Action]() - actions = [ - .muteUser(.init( - name: menuContext.name, - isMuting: menuContext.isMuting - )), - .blockUser(.init( - name: menuContext.name, - isBlocking: menuContext.isBlocking - )), - .reportUser( - .init(name: menuContext.name) - ), + if !menuContext.isMyself { + actions.append(contentsOf: [ + .muteUser(.init( + name: menuContext.name, + isMuting: menuContext.isMuting + )), + .blockUser(.init( + name: menuContext.name, + isBlocking: menuContext.isBlocking + )), + .reportUser( + .init(name: menuContext.name) + ) + ]) + } + + actions.append(contentsOf: [ .bookmarkStatus( .init(isBookmarking: menuContext.isBookmarking) ), .shareStatus - ] + ]) if menuContext.isMyself { actions.append(.deleteStatus) From 002d2796e49d5057b12a32cc5403cf72633ffb2e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 10 Nov 2022 17:23:29 +0100 Subject: [PATCH 228/658] 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 229/658] 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 230/658] 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 231/658] 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 232/658] 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 233/658] 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 234/658] 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 235/658] 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 236/658] 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 237/658] 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 238/658] 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 239/658] 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 240/658] 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 241/658] 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 57380b9989bdd4398710e91e3b755bc46e4289a6 Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Fri, 11 Nov 2022 13:32:17 -0800 Subject: [PATCH 242/658] Fix up README.md (#561) --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bf35b4599..d2a19e715 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Mastodon + [![CI](https://github.com/mastodon/mastodon-ios/actions/workflows/main.yml/badge.svg)](https://github.com/mastodon/mastodon-ios/actions/workflows/main.yml) [![Crowdin](https://badges.crowdin.net/mastodon-for-ios/localized.svg)](https://crowdin.com/project/mastodon-for-ios) @@ -11,12 +12,14 @@ This is the repository for the official iOS App for Mastodon. You can install it Read this blog post for this app to learn more. > [Developing an official iOS app for Mastodon](https://blog.joinmastodon.org/2021/02/developing-an-official-ios-app-for-mastodon/) -## Getting Start +## Getting Started + - Read the setup guide [here](./Documentation/Setup.md) - About [contributing](./Documentation/CONTRIBUTING.md) - [Documentation folder](./Documentation/) ## Acknowledgments + Thanks to these open-sources projects listed [here](./Documentation/Acknowledgments.md). ## License From 407e0ae304e2c68c223781e5d8333a34e0263c3e Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Fri, 11 Nov 2022 14:24:59 -0800 Subject: [PATCH 243/658] Update navigation bar appearance in SearchViewController --- .../Search/Search/SearchViewController.swift | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/Mastodon/Scene/Search/Search/SearchViewController.swift b/Mastodon/Scene/Search/Search/SearchViewController.swift index 7efef2c00..3b9496dab 100644 --- a/Mastodon/Scene/Search/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/Search/SearchViewController.swift @@ -23,15 +23,15 @@ final class HeightFixedSearchBar: UISearchBar { final class SearchViewController: UIViewController, NeedsDependency { let logger = Logger(subsystem: "SearchViewController", category: "ViewController") - + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var searchTransitionController = SearchTransitionController() - + var disposeBag = Set() var viewModel: SearchViewModel! - + // use AutoLayout could set search bar margin automatically to // layout alongside with split mode button (on iPad) let titleViewContainer = UIView() @@ -85,6 +85,7 @@ extension SearchViewController { title = L10n.Scene.Search.title setupSearchBar() + setupNavigationBarAppearance() // collectionView.translatesAutoresizingMaskIntoConstraints = false // view.addSubview(collectionView) @@ -101,7 +102,7 @@ extension SearchViewController { // ) guard let discoveryViewController = self.discoveryViewController else { return } - + addChild(discoveryViewController) discoveryViewController.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(discoveryViewController.view) @@ -111,15 +112,16 @@ extension SearchViewController { discoveryViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), discoveryViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - + // discoveryViewController.view.isHidden = true + } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) viewModel.viewDidAppeared.send() - + // note: // need set alpha because (maybe) SDK forget set alpha back titleViewContainer.alpha = 1 @@ -164,6 +166,20 @@ extension SearchViewController { .store(in: &disposeBag) } + private func setupNavigationBarAppearance() { + let appearance = UINavigationBarAppearance() + appearance.configureWithOpaqueBackground() + appearance.shadowColor = nil + + navigationItem.standardAppearance = appearance + navigationItem.scrollEdgeAppearance = appearance + navigationItem.compactAppearance = appearance + + if #available(iOS 15, *) { + navigationItem.compactScrollEdgeAppearance = appearance + } + } + } // MARK: - UISearchBarDelegate From a5d61072c0182fd06c175c854c6d45ffa9bf8b3e Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Fri, 11 Nov 2022 15:07:31 -0800 Subject: [PATCH 244/658] Fix background in dark mode --- .../Search/Search/SearchViewController.swift | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/Mastodon/Scene/Search/Search/SearchViewController.swift b/Mastodon/Scene/Search/Search/SearchViewController.swift index 3b9496dab..581fa08b3 100644 --- a/Mastodon/Scene/Search/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/Search/SearchViewController.swift @@ -73,20 +73,19 @@ extension SearchViewController { override func viewDidLoad() { super.viewDidLoad() - setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) + setupAppearance(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) + self.setupAppearance(theme: theme) } .store(in: &disposeBag) title = L10n.Scene.Search.title setupSearchBar() - setupNavigationBarAppearance() - + // collectionView.translatesAutoresizingMaskIntoConstraints = false // view.addSubview(collectionView) // NSLayoutConstraint.activate([ @@ -129,8 +128,22 @@ extension SearchViewController { } extension SearchViewController { - private func setupBackgroundColor(theme: Theme) { + private func setupAppearance(theme: Theme) { view.backgroundColor = theme.systemGroupedBackgroundColor + + // Match the DiscoveryViewController tab color and remove the double separator. + let navigationBarAppearance = UINavigationBarAppearance() + navigationBarAppearance.configureWithOpaqueBackground() + navigationBarAppearance.backgroundColor = theme.systemBackgroundColor + navigationBarAppearance.shadowColor = nil + + navigationItem.standardAppearance = navigationBarAppearance + navigationItem.scrollEdgeAppearance = navigationBarAppearance + navigationItem.compactAppearance = navigationBarAppearance + + if #available(iOS 15, *) { + navigationItem.compactScrollEdgeAppearance = navigationBarAppearance + } } private func setupSearchBar() { @@ -166,20 +179,6 @@ extension SearchViewController { .store(in: &disposeBag) } - private func setupNavigationBarAppearance() { - let appearance = UINavigationBarAppearance() - appearance.configureWithOpaqueBackground() - appearance.shadowColor = nil - - navigationItem.standardAppearance = appearance - navigationItem.scrollEdgeAppearance = appearance - navigationItem.compactAppearance = appearance - - if #available(iOS 15, *) { - navigationItem.compactScrollEdgeAppearance = appearance - } - } - } // MARK: - UISearchBarDelegate From 35775a5b4364d94998f3cc4890228e16b5beb5f8 Mon Sep 17 00:00:00 2001 From: David Godfrey Date: Sat, 12 Nov 2022 01:53:12 +0000 Subject: [PATCH 245/658] 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 246/658] 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 247/658] 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 248/658] 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 249/658] 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 250/658] 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 251/658] 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 252/658] 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 253/658] 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 254/658] 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 255/658] 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 256/658] 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 257/658] 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 258/658] 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 259/658] 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 260/658] 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 337e221c206322a81346ef94c8dc4999cad18d0e Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Sat, 12 Nov 2022 19:36:22 -0800 Subject: [PATCH 261/658] Resolve build failure when running tests from Xcode. The build failure was: ``` MastodonTests/MastodonTests.swift:39:27: error build: Cannot find 'AppContext' in scope ``` --- MastodonTests/MastodonTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/MastodonTests/MastodonTests.swift b/MastodonTests/MastodonTests.swift index 7264dde64..ec572fd92 100644 --- a/MastodonTests/MastodonTests.swift +++ b/MastodonTests/MastodonTests.swift @@ -7,6 +7,7 @@ import XCTest @testable import Mastodon +import MastodonCore @MainActor class MastodonTests: XCTestCase { From e7ef0f79c7020fc7cfbe7f99711a3c18e15d3d61 Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 13 Nov 2022 16:04:29 +0800 Subject: [PATCH 262/658] 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 263/658] 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 264/658] 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 265/658] 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 266/658] 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 267/658] 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 268/658] 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 269/658] 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 270/658] 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 271/658] 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 272/658] 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 273/658] 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 274/658] 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 275/658] 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 276/658] 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 277/658] 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 278/658] 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 279/658] 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 280/658] 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 281/658] 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 282/658] 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 283/658] 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 284/658] 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 285/658] 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 286/658] 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 287/658] 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 288/658] 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 289/658] 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 290/658] 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 291/658] 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 292/658] 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 293/658] 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 294/658] 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 295/658] 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 296/658] 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 297/658] 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 298/658] 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 299/658] 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 300/658] 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 301/658] 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 302/658] 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 303/658] 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 304/658] 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 305/658] 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 306/658] 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 307/658] 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 308/658] 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 309/658] 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 310/658] 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 311/658] 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 312/658] 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 313/658] 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 314/658] 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 315/658] 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 316/658] 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 317/658] 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 318/658] 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 319/658] 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 320/658] 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 321/658] 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 322/658] 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 323/658] 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 324/658] 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 325/658] 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 326/658] 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 327/658] 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 328/658] 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 329/658] 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 330/658] 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 331/658] 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 332/658] 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 333/658] 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 334/658] 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 335/658] 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 336/658] 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 337/658] 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 37d77e9cf8773e2bc5cf69bdc5c9244d0bd8d2ca Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:32 +0100 Subject: [PATCH 338/658] New translations Localizable.stringsdict (Czech) --- .../input/cs.lproj/Localizable.stringsdict | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict index 827bd79e6..805ac70f2 100644 --- a/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict @@ -62,6 +62,26 @@ %ld znaků + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 96740d2e20e2a422d76d6cfd34b34f2e9d137f1c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:33 +0100 Subject: [PATCH 339/658] New translations app.json (Romanian) --- Localization/StringsConvertor/input/ro.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ro.lproj/app.json b/Localization/StringsConvertor/input/ro.lproj/app.json index a9d3804fa..e50c741b6 100644 --- a/Localization/StringsConvertor/input/ro.lproj/app.json +++ b/Localization/StringsConvertor/input/ro.lproj/app.json @@ -417,7 +417,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", From 55e8cf845f35e059bf9fccabba11e5f63d2be833 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:34 +0100 Subject: [PATCH 340/658] New translations Localizable.stringsdict (Catalan) --- .../input/ca.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/ca.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/ca.lproj/Localizable.stringsdict index cc28edbc6..06d1f0301 100644 --- a/Localization/StringsConvertor/input/ca.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/ca.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld caràcters + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From dd0685060a8722b006ac57c371e7589e3a17b981 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:35 +0100 Subject: [PATCH 341/658] New translations Localizable.stringsdict (Arabic) --- .../input/ar.lproj/Localizable.stringsdict | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict index 862d98184..a65082272 100644 --- a/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict @@ -74,6 +74,30 @@ %ld حَرف + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld characters + one + 1 character + two + %ld characters + few + %ld characters + many + %ld characters + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From bde6bb0a7b83dc31bd2b5c1d5da6bf91493a5a42 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:36 +0100 Subject: [PATCH 342/658] New translations Localizable.stringsdict (Spanish) --- .../input/es.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/es.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/es.lproj/Localizable.stringsdict index def3d7bba..ca07b6b28 100644 --- a/Localization/StringsConvertor/input/es.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/es.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld caracteres + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From bfa9b0294279340a94d7cd4429d475d18002124a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:37 +0100 Subject: [PATCH 343/658] New translations Localizable.stringsdict (French) --- .../input/fr.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/fr.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/fr.lproj/Localizable.stringsdict index d9d860a47..37f280de4 100644 --- a/Localization/StringsConvertor/input/fr.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/fr.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld caractères + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 839816029a015e9b0a93f92fe71e30c9e0d1e045 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:38 +0100 Subject: [PATCH 344/658] New translations Localizable.stringsdict (Romanian) --- .../input/ro.lproj/Localizable.stringsdict | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Localization/StringsConvertor/input/ro.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/ro.lproj/Localizable.stringsdict index 7ae5a1c79..9df2162b0 100644 --- a/Localization/StringsConvertor/input/ro.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/ro.lproj/Localizable.stringsdict @@ -56,6 +56,24 @@ %ld characters + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + few + %ld characters + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 8438fbf0f8d51774dc8fd5ab04a6224e0c6dc6a8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:39 +0100 Subject: [PATCH 345/658] New translations app.json (Japanese) --- Localization/StringsConvertor/input/ja.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ja.lproj/app.json b/Localization/StringsConvertor/input/ja.lproj/app.json index 9ff2a60a6..a18347581 100644 --- a/Localization/StringsConvertor/input/ja.lproj/app.json +++ b/Localization/StringsConvertor/input/ja.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "カスタム絵文字ピッカー", "enable_content_warning": "閲覧注意を有効にする", "disable_content_warning": "閲覧注意を無効にする", - "post_visibility_menu": "投稿の表示メニュー" + "post_visibility_menu": "投稿の表示メニュー", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "投稿を破棄", From 3241258fbff7e7c3cf27c04fec9604a4052c3162 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:40 +0100 Subject: [PATCH 346/658] New translations app.json (Finnish) --- Localization/StringsConvertor/input/fi.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/fi.lproj/app.json b/Localization/StringsConvertor/input/fi.lproj/app.json index a42642786..b98167c35 100644 --- a/Localization/StringsConvertor/input/fi.lproj/app.json +++ b/Localization/StringsConvertor/input/fi.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Mukautettu emojivalitsin", "enable_content_warning": "Ota sisältövaroitus käyttöön", "disable_content_warning": "Poista sisältövaroitus käytöstä", - "post_visibility_menu": "Julkaisun näkyvyysvalikko" + "post_visibility_menu": "Julkaisun näkyvyysvalikko", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Hylkää julkaisu", From ffbd2ba00a8568c3f8a49342ec60c405fd8cda86 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:41 +0100 Subject: [PATCH 347/658] New translations app.json (Basque) --- Localization/StringsConvertor/input/eu.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/eu.lproj/app.json b/Localization/StringsConvertor/input/eu.lproj/app.json index 3f58f522c..c4cecfe59 100644 --- a/Localization/StringsConvertor/input/eu.lproj/app.json +++ b/Localization/StringsConvertor/input/eu.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Emoji pertsonalizatuen hautatzailea", "enable_content_warning": "Gaitu edukiaren abisua", "disable_content_warning": "Desgaitu edukiaren abisua", - "post_visibility_menu": "Bidalketaren ikusgaitasunaren menua" + "post_visibility_menu": "Bidalketaren ikusgaitasunaren menua", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Baztertu bidalketa", From 6a80afbec6feeac8b7d75de4dfc3450513b15622 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:43 +0100 Subject: [PATCH 348/658] New translations app.json (German) --- Localization/StringsConvertor/input/de.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/de.lproj/app.json b/Localization/StringsConvertor/input/de.lproj/app.json index 481f07a4a..ce5dc4d3a 100644 --- a/Localization/StringsConvertor/input/de.lproj/app.json +++ b/Localization/StringsConvertor/input/de.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Benutzerdefinierter Emojiwähler", "enable_content_warning": "Inhaltswarnung einschalten", "disable_content_warning": "Inhaltswarnung ausschalten", - "post_visibility_menu": "Sichtbarkeitsmenü" + "post_visibility_menu": "Sichtbarkeitsmenü", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Beitrag verwerfen", From 7e9228e0469ab7d4e56c98d25140a56a8a2ab33a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:43 +0100 Subject: [PATCH 349/658] New translations app.json (Danish) --- Localization/StringsConvertor/input/da.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/da.lproj/app.json b/Localization/StringsConvertor/input/da.lproj/app.json index a6a971860..25f06ad83 100644 --- a/Localization/StringsConvertor/input/da.lproj/app.json +++ b/Localization/StringsConvertor/input/da.lproj/app.json @@ -417,7 +417,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", From 828ce82fb081d0fb3a309fb4b6eec2f3174ed27e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:44 +0100 Subject: [PATCH 350/658] New translations app.json (Catalan) --- Localization/StringsConvertor/input/ca.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ca.lproj/app.json b/Localization/StringsConvertor/input/ca.lproj/app.json index 7164d1d12..b27042d0f 100644 --- a/Localization/StringsConvertor/input/ca.lproj/app.json +++ b/Localization/StringsConvertor/input/ca.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Selector d'Emoji Personalitzat", "enable_content_warning": "Activa l'Avís de Contingut", "disable_content_warning": "Desactiva l'Avís de Contingut", - "post_visibility_menu": "Menú de Visibilitat de Publicació" + "post_visibility_menu": "Menú de Visibilitat de Publicació", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Descarta la Publicació", From 946b798abbefea71fb90f7aebd9bf90d47b82990 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:46 +0100 Subject: [PATCH 351/658] New translations app.json (Arabic) --- Localization/StringsConvertor/input/ar.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ar.lproj/app.json b/Localization/StringsConvertor/input/ar.lproj/app.json index 4c5ac4c8b..ebea98b0c 100644 --- a/Localization/StringsConvertor/input/ar.lproj/app.json +++ b/Localization/StringsConvertor/input/ar.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "منتقي الرموز التعبيرية المُخصَّص", "enable_content_warning": "تفعيل تحذير المُحتَوى", "disable_content_warning": "تعطيل تحذير المُحتَوى", - "post_visibility_menu": "قائمة ظهور المنشور" + "post_visibility_menu": "قائمة ظهور المنشور", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "تجاهُل المنشور", From a62e16048aafbe192f61f67973b6c72ec3e5d893 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:47 +0100 Subject: [PATCH 352/658] New translations app.json (Spanish) --- Localization/StringsConvertor/input/es.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/es.lproj/app.json b/Localization/StringsConvertor/input/es.lproj/app.json index 7eaff340d..5398df49f 100644 --- a/Localization/StringsConvertor/input/es.lproj/app.json +++ b/Localization/StringsConvertor/input/es.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Selector de Emojis Personalizados", "enable_content_warning": "Activar Advertencia de Contenido", "disable_content_warning": "Desactivar Advertencia de Contenido", - "post_visibility_menu": "Menú de Visibilidad de la Publicación" + "post_visibility_menu": "Menú de Visibilidad de la Publicación", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Descartar Publicación", From 10a5a9905642eff7cbe06247f652a2bb70ec0d12 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:48 +0100 Subject: [PATCH 353/658] New translations app.json (Italian) --- Localization/StringsConvertor/input/it.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json index 73f42d1eb..bfd7811e2 100644 --- a/Localization/StringsConvertor/input/it.lproj/app.json +++ b/Localization/StringsConvertor/input/it.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Selettore Emoji personalizzato", "enable_content_warning": "Abilita avvertimento contenuti", "disable_content_warning": "Disabilita avviso di contenuti", - "post_visibility_menu": "Menu di visibilità del post" + "post_visibility_menu": "Menu di visibilità del post", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Scarta post", From 9fb3966f1e47d3d0c1927d5d9743a1d2d499fd1d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:49 +0100 Subject: [PATCH 354/658] New translations app.json (French) --- Localization/StringsConvertor/input/fr.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/fr.lproj/app.json b/Localization/StringsConvertor/input/fr.lproj/app.json index 25bb6e511..674d0f8ca 100644 --- a/Localization/StringsConvertor/input/fr.lproj/app.json +++ b/Localization/StringsConvertor/input/fr.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Sélecteur d’émojis personnalisés", "enable_content_warning": "Basculer l’avertissement de contenu", "disable_content_warning": "Désactiver l'avertissement de contenu", - "post_visibility_menu": "Menu de Visibilité de la publication" + "post_visibility_menu": "Menu de Visibilité de la publication", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Rejeter la publication", From 43588148eca59129f47f2190bf3eca49663cf2ad Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:50 +0100 Subject: [PATCH 355/658] New translations app.json (Czech) --- Localization/StringsConvertor/input/cs.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/cs.lproj/app.json b/Localization/StringsConvertor/input/cs.lproj/app.json index 4050352f4..02e7f5cec 100644 --- a/Localization/StringsConvertor/input/cs.lproj/app.json +++ b/Localization/StringsConvertor/input/cs.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Vlastní výběr Emoji", "enable_content_warning": "Povolit upozornění na obsah", "disable_content_warning": "Vypnout upozornění na obsah", - "post_visibility_menu": "Menu viditelnosti příspěvku" + "post_visibility_menu": "Menu viditelnosti příspěvku", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Zahodit příspěvek", From 28bd2dc4c18e3c48eac7597d87d29d2e3bb8eb59 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:51 +0100 Subject: [PATCH 356/658] New translations Localizable.stringsdict (German) --- .../input/de.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/de.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/de.lproj/Localizable.stringsdict index f60c6b0d7..1965fd02b 100644 --- a/Localization/StringsConvertor/input/de.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/de.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld Zeichen + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From d87959321ed0e79c66b117fa51fe3dc984c065e5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:52 +0100 Subject: [PATCH 357/658] New translations Localizable.stringsdict (Scottish Gaelic) --- .../input/gd.lproj/Localizable.stringsdict | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Localization/StringsConvertor/input/gd.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/gd.lproj/Localizable.stringsdict index d0ccb5f41..45ba1e156 100644 --- a/Localization/StringsConvertor/input/gd.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/gd.lproj/Localizable.stringsdict @@ -62,6 +62,26 @@ %ld caractar + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + two + %ld characters + few + %ld characters + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 70f42ac890147f72a4f93486794fd2bac60ea0f8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:53 +0100 Subject: [PATCH 358/658] New translations app.json (Kurmanji (Kurdish)) --- Localization/StringsConvertor/input/kmr.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/kmr.lproj/app.json b/Localization/StringsConvertor/input/kmr.lproj/app.json index 098f514ee..1fa54d8ef 100644 --- a/Localization/StringsConvertor/input/kmr.lproj/app.json +++ b/Localization/StringsConvertor/input/kmr.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Hilbijêrê emojî yên kesanekirî", "enable_content_warning": "Hişyariya naverokê çalak bike", "disable_content_warning": "Hişyariya naverokê neçalak bike", - "post_visibility_menu": "Kulîna xuyabûna şandiyê" + "post_visibility_menu": "Kulîna xuyabûna şandiyê", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Şandî paşguh bike", From 77d753a2353b5acdb93615269fb2c87b59aaaaee Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:55 +0100 Subject: [PATCH 359/658] New translations app.json (Latvian) --- Localization/StringsConvertor/input/lv.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/lv.lproj/app.json b/Localization/StringsConvertor/input/lv.lproj/app.json index 2835f0887..8a4b3e921 100644 --- a/Localization/StringsConvertor/input/lv.lproj/app.json +++ b/Localization/StringsConvertor/input/lv.lproj/app.json @@ -417,7 +417,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", From c74f062b27eac0a916c665834713b582ae129a57 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:56 +0100 Subject: [PATCH 360/658] New translations app.json (Hindi) --- Localization/StringsConvertor/input/hi.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/hi.lproj/app.json b/Localization/StringsConvertor/input/hi.lproj/app.json index f0fedf75f..2c46d023d 100644 --- a/Localization/StringsConvertor/input/hi.lproj/app.json +++ b/Localization/StringsConvertor/input/hi.lproj/app.json @@ -417,7 +417,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", From c5dc8dafdfd8ca69d98bd81586227b7dd265ef45 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:57 +0100 Subject: [PATCH 361/658] New translations app.json (English, United States) --- Localization/StringsConvertor/input/en-US.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/en-US.lproj/app.json b/Localization/StringsConvertor/input/en-US.lproj/app.json index a6a971860..25f06ad83 100644 --- a/Localization/StringsConvertor/input/en-US.lproj/app.json +++ b/Localization/StringsConvertor/input/en-US.lproj/app.json @@ -417,7 +417,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", From efdeab68c79099ddaca4cdedb730557aaf4d9adf Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:58 +0100 Subject: [PATCH 362/658] New translations app.json (Welsh) --- Localization/StringsConvertor/input/cy.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/cy.lproj/app.json b/Localization/StringsConvertor/input/cy.lproj/app.json index f36fe7d16..a5de5143a 100644 --- a/Localization/StringsConvertor/input/cy.lproj/app.json +++ b/Localization/StringsConvertor/input/cy.lproj/app.json @@ -417,7 +417,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", From 5a624b491a645ac7fb779128907ff0684df7c759 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:40:59 +0100 Subject: [PATCH 363/658] New translations app.json (Sinhala) --- Localization/StringsConvertor/input/si.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/si.lproj/app.json b/Localization/StringsConvertor/input/si.lproj/app.json index 816536440..1bc8b3036 100644 --- a/Localization/StringsConvertor/input/si.lproj/app.json +++ b/Localization/StringsConvertor/input/si.lproj/app.json @@ -417,7 +417,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", From c8db5833cd508613ff695604153b7669533f1784 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:00 +0100 Subject: [PATCH 364/658] New translations app.json (Swedish) --- Localization/StringsConvertor/input/sv.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/sv.lproj/app.json b/Localization/StringsConvertor/input/sv.lproj/app.json index c740609c9..938134eaa 100644 --- a/Localization/StringsConvertor/input/sv.lproj/app.json +++ b/Localization/StringsConvertor/input/sv.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Anpassad emoji-väljare", "enable_content_warning": "Aktivera innehållsvarning", "disable_content_warning": "Inaktivera innehållsvarning", - "post_visibility_menu": "Inläggssynlighetsmeny" + "post_visibility_menu": "Inläggssynlighetsmeny", + "post_options": "Inläggsalternativ", + "posting_as": "Postar som %s" }, "keyboard": { "discard_post": "Släng inlägget", From 8964159a7a1d97b6158ccbba2bf274d65420f64c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:01 +0100 Subject: [PATCH 365/658] New translations app.json (Sorani (Kurdish)) --- Localization/StringsConvertor/input/ckb.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ckb.lproj/app.json b/Localization/StringsConvertor/input/ckb.lproj/app.json index 25452f38b..b8cf48fbe 100644 --- a/Localization/StringsConvertor/input/ckb.lproj/app.json +++ b/Localization/StringsConvertor/input/ckb.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "هەڵبژێری ئیمۆجی", "enable_content_warning": "ئاگاداریی ناوەڕۆک چالاک بکە", "disable_content_warning": "ئاگاداریی ناوەڕۆک ناچالاک بکە", - "post_visibility_menu": "پێڕستی شێوازی دەرکەوتنی پۆست" + "post_visibility_menu": "پێڕستی شێوازی دەرکەوتنی پۆست", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "پۆستەکە هەڵوەشێنەوە", From c450c89e749f59108dadbe490c91ef4c4a67936a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:03 +0100 Subject: [PATCH 366/658] New translations app.json (Spanish, Argentina) --- .../StringsConvertor/input/es-AR.lproj/app.json | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Localization/StringsConvertor/input/es-AR.lproj/app.json b/Localization/StringsConvertor/input/es-AR.lproj/app.json index 309cf4d34..e54100c41 100644 --- a/Localization/StringsConvertor/input/es-AR.lproj/app.json +++ b/Localization/StringsConvertor/input/es-AR.lproj/app.json @@ -137,10 +137,10 @@ "closed": "Cerrada" }, "meta_entity": { - "url": "Link: %s", - "hashtag": "Hashtag: %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "url": "Enlace: %s", + "hashtag": "Etiqueta: %s", + "mention": "Mostrar perfil: %s", + "email": "Dirección de correo electrónico: %s" }, "actions": { "reply": "Responder", @@ -417,7 +417,9 @@ "custom_emoji_picker": "Selector de emoji personalizado", "enable_content_warning": "Habilitar advertencia de contenido", "disable_content_warning": "Deshabilitar advertencia de contenido", - "post_visibility_menu": "Menú de visibilidad del mensaje" + "post_visibility_menu": "Menú de visibilidad del mensaje", + "post_options": "Opciones de mensaje", + "posting_as": "Enviar como %s" }, "keyboard": { "discard_post": "Descartar mensaje", @@ -706,7 +708,7 @@ "accessibility_hint": "Tocá dos veces para descartar este asistente" }, "bookmark": { - "title": "Bookmarks" + "title": "Marcadores" } } } From 8cf3f88e6a0bfc1dabf1c224e4f7dea413e1ce51 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:04 +0100 Subject: [PATCH 367/658] New translations Localizable.stringsdict (Korean) --- .../input/ko.lproj/Localizable.stringsdict | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Localization/StringsConvertor/input/ko.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/ko.lproj/Localizable.stringsdict index 9628be614..644fd007a 100644 --- a/Localization/StringsConvertor/input/ko.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/ko.lproj/Localizable.stringsdict @@ -44,6 +44,20 @@ %ld 글자 + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From cfca1ea5526f89058a697612af1e2bf19b7bd344 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:05 +0100 Subject: [PATCH 368/658] New translations app.json (Korean) --- Localization/StringsConvertor/input/ko.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ko.lproj/app.json b/Localization/StringsConvertor/input/ko.lproj/app.json index 826ac389c..fa40d14a7 100644 --- a/Localization/StringsConvertor/input/ko.lproj/app.json +++ b/Localization/StringsConvertor/input/ko.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "커스텀 에모지 선택기", "enable_content_warning": "열람 주의 설정", "disable_content_warning": "열람 주의 해제", - "post_visibility_menu": "게시물 공개범위 메뉴" + "post_visibility_menu": "게시물 공개범위 메뉴", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "글 버리기", From 114cdae5f9083d73b960d2a3a4a4884c42ef4146 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:06 +0100 Subject: [PATCH 369/658] New translations app.json (Kabyle) --- Localization/StringsConvertor/input/kab.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/kab.lproj/app.json b/Localization/StringsConvertor/input/kab.lproj/app.json index 9c5d7659a..194a2c681 100644 --- a/Localization/StringsConvertor/input/kab.lproj/app.json +++ b/Localization/StringsConvertor/input/kab.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Amefran n yimujiten udmawanen", "enable_content_warning": "Rmed alɣu n ugbur", "disable_content_warning": "Sens alɣu n ugbur", - "post_visibility_menu": "Umuɣ n ubani n tsuffeɣt" + "post_visibility_menu": "Umuɣ n ubani n tsuffeɣt", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Sefsex tasuffeɣt", From 020e8add8d4153769c0a5b0126e262fca120db13 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:07 +0100 Subject: [PATCH 370/658] New translations app.json (Vietnamese) --- Localization/StringsConvertor/input/vi.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/vi.lproj/app.json b/Localization/StringsConvertor/input/vi.lproj/app.json index 5b7696727..cd20997c3 100644 --- a/Localization/StringsConvertor/input/vi.lproj/app.json +++ b/Localization/StringsConvertor/input/vi.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Chọn emoji", "enable_content_warning": "Bật nội dung ẩn", "disable_content_warning": "Tắt nội dung ẩn", - "post_visibility_menu": "Menu hiển thị tút" + "post_visibility_menu": "Menu hiển thị tút", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Hủy đăng tút", From 37e24a0eeacc8203f306ab0e3ae670716b39f1a4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:08 +0100 Subject: [PATCH 371/658] New translations app.json (Thai) --- Localization/StringsConvertor/input/th.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/th.lproj/app.json b/Localization/StringsConvertor/input/th.lproj/app.json index 9b4316025..f9c30f01d 100644 --- a/Localization/StringsConvertor/input/th.lproj/app.json +++ b/Localization/StringsConvertor/input/th.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "ตัวเลือกอีโมจิที่กำหนดเอง", "enable_content_warning": "เปิดใช้งานคำเตือนเนื้อหา", "disable_content_warning": "ปิดใช้งานคำเตือนเนื้อหา", - "post_visibility_menu": "เมนูการมองเห็นโพสต์" + "post_visibility_menu": "เมนูการมองเห็นโพสต์", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "ละทิ้งโพสต์", From 16754b1048cdd912fe9bf6dfc32dec3394461732 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:09 +0100 Subject: [PATCH 372/658] New translations app.json (Portuguese, Brazilian) --- Localization/StringsConvertor/input/pt-BR.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/app.json b/Localization/StringsConvertor/input/pt-BR.lproj/app.json index 26e6edb76..c7abb1da1 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/app.json +++ b/Localization/StringsConvertor/input/pt-BR.lproj/app.json @@ -417,7 +417,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", From 40e505a8515f674a3e278e82bedd8ace450af884 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:10 +0100 Subject: [PATCH 373/658] New translations app.json (Indonesian) --- Localization/StringsConvertor/input/id.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/id.lproj/app.json b/Localization/StringsConvertor/input/id.lproj/app.json index d942a22ad..91d31071b 100644 --- a/Localization/StringsConvertor/input/id.lproj/app.json +++ b/Localization/StringsConvertor/input/id.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Custom Emoji Picker", "enable_content_warning": "Aktifkan Peringatan Konten", "disable_content_warning": "Nonaktifkan Peringatan Konten", - "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", From 0da6df18e952eba0843b952747f400002d8428ad Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:11 +0100 Subject: [PATCH 374/658] New translations app.json (Scottish Gaelic) --- Localization/StringsConvertor/input/gd.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/gd.lproj/app.json b/Localization/StringsConvertor/input/gd.lproj/app.json index c1d17f813..214279887 100644 --- a/Localization/StringsConvertor/input/gd.lproj/app.json +++ b/Localization/StringsConvertor/input/gd.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Roghnaichear nan Emoji gnàthaichte", "enable_content_warning": "Cuir rabhadh susbainte an comas", "disable_content_warning": "Cuir rabhadh susbainte à comas", - "post_visibility_menu": "Clàr-taice faicsinneachd a’ phuist" + "post_visibility_menu": "Clàr-taice faicsinneachd a’ phuist", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Tilg air falbh am post", From 1a13afd692ed9246560002acc23670f12800b2a3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:12 +0100 Subject: [PATCH 375/658] New translations app.json (Galician) --- Localization/StringsConvertor/input/gl.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index 3c394be95..0eb82b142 100644 --- a/Localization/StringsConvertor/input/gl.lproj/app.json +++ b/Localization/StringsConvertor/input/gl.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Selector emoji personalizado", "enable_content_warning": "Marcar con Aviso sobre o contido", "disable_content_warning": "Retirar Aviso sobre o contido", - "post_visibility_menu": "Visibilidade da publicación" + "post_visibility_menu": "Visibilidade da publicación", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Descartar publicación", From 04ecfe47362c61a9ba0b5740d743c12d35b6d38e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:13 +0100 Subject: [PATCH 376/658] New translations app.json (English) --- Localization/StringsConvertor/input/en.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/en.lproj/app.json b/Localization/StringsConvertor/input/en.lproj/app.json index a6a971860..25f06ad83 100644 --- a/Localization/StringsConvertor/input/en.lproj/app.json +++ b/Localization/StringsConvertor/input/en.lproj/app.json @@ -417,7 +417,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", From 1a4b27835ec33dddb1629dd2bd75223be4b28e39 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:14 +0100 Subject: [PATCH 377/658] New translations app.json (Chinese Simplified) --- Localization/StringsConvertor/input/zh-Hans.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json index ddf89e159..2bfd04c8d 100644 --- a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "自定义表情选择器", "enable_content_warning": "启用内容警告", "disable_content_warning": "关闭内容警告", - "post_visibility_menu": "帖子可见性" + "post_visibility_menu": "帖子可见性", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "丢弃帖子", From 0134aa8900c3c717c800dbab9961f008bdd2da29 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:15 +0100 Subject: [PATCH 378/658] New translations app.json (Russian) --- Localization/StringsConvertor/input/ru.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ru.lproj/app.json b/Localization/StringsConvertor/input/ru.lproj/app.json index 798cdb4c5..1f82a35db 100644 --- a/Localization/StringsConvertor/input/ru.lproj/app.json +++ b/Localization/StringsConvertor/input/ru.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Меню пользовательских эмодзи", "enable_content_warning": "Добавить предупреждение о содержании", "disable_content_warning": "Убрать предупреждение о содержании", - "post_visibility_menu": "Меню видимости поста" + "post_visibility_menu": "Меню видимости поста", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Удалить пост", From 08d976d022c5b1d806620380e3355ae91ca4a25e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:16 +0100 Subject: [PATCH 379/658] New translations app.json (Portuguese) --- Localization/StringsConvertor/input/pt.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/pt.lproj/app.json b/Localization/StringsConvertor/input/pt.lproj/app.json index a6a971860..25f06ad83 100644 --- a/Localization/StringsConvertor/input/pt.lproj/app.json +++ b/Localization/StringsConvertor/input/pt.lproj/app.json @@ -417,7 +417,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", From 6bc7ce605018d1f1c20e8fcff446620381b8e75a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:18 +0100 Subject: [PATCH 380/658] New translations app.json (Dutch) --- Localization/StringsConvertor/input/nl.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/nl.lproj/app.json b/Localization/StringsConvertor/input/nl.lproj/app.json index e0b2872fb..bc508d5b0 100644 --- a/Localization/StringsConvertor/input/nl.lproj/app.json +++ b/Localization/StringsConvertor/input/nl.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Eigen Emojikiezer", "enable_content_warning": "Inhoudswaarschuwing inschakelen", "disable_content_warning": "Inhoudswaarschuwing Uitschakelen", - "post_visibility_menu": "Berichtzichtbaarheidsmenu" + "post_visibility_menu": "Berichtzichtbaarheidsmenu", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Bericht Verwijderen", From 429b9b846477a9bf830500479559e01f2f776eb0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:19 +0100 Subject: [PATCH 381/658] New translations Localizable.stringsdict (Swedish) --- .../input/sv.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/sv.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/sv.lproj/Localizable.stringsdict index c7317903d..3cbfeae6d 100644 --- a/Localization/StringsConvertor/input/sv.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/sv.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld tecken + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ kvar + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld tecken + other + %ld tecken + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From f284a699f34db1efa88f1cf6ef495e959f4ebb08 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:20 +0100 Subject: [PATCH 382/658] New translations app.json (Chinese Traditional) --- Localization/StringsConvertor/input/zh-Hant.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json index ab7343c99..4b2ea2f0f 100644 --- a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "自訂 emoji 選擇器", "enable_content_warning": "啟用內容警告", "disable_content_warning": "停用內容警告", - "post_visibility_menu": "嘟文可見性選單" + "post_visibility_menu": "嘟文可見性選單", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "捨棄嘟文", From fe45e9ac251d3062062ab2e28910d2ddd44ac615 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:21 +0100 Subject: [PATCH 383/658] New translations Localizable.stringsdict (Slovenian) --- .../input/sl.lproj/Localizable.stringsdict | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Localization/StringsConvertor/input/sl.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/sl.lproj/Localizable.stringsdict index 8f0bcb42b..742b78e87 100644 --- a/Localization/StringsConvertor/input/sl.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/sl.lproj/Localizable.stringsdict @@ -62,6 +62,26 @@ %ld znakov + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + two + %ld characters + few + %ld characters + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 1df93527843a46f2b5fa8bf6f04fecc3400ac9ba Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:22 +0100 Subject: [PATCH 384/658] New translations app.json (Turkish) --- Localization/StringsConvertor/input/tr.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/tr.lproj/app.json b/Localization/StringsConvertor/input/tr.lproj/app.json index 2abb92845..f363e3ab6 100644 --- a/Localization/StringsConvertor/input/tr.lproj/app.json +++ b/Localization/StringsConvertor/input/tr.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Özel Emoji Seçici", "enable_content_warning": "İçerik Uyarısını Etkinleştir", "disable_content_warning": "İçerik Uyarısını Kapat", - "post_visibility_menu": "Gönderi Görünürlüğü Menüsü" + "post_visibility_menu": "Gönderi Görünürlüğü Menüsü", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Gönderiyi İptal Et", From de0ed81d1e9186b1919cf4fbb5468cf8ddb08123 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:23 +0100 Subject: [PATCH 385/658] New translations app.json (Ukrainian) --- Localization/StringsConvertor/input/uk.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/uk.lproj/app.json b/Localization/StringsConvertor/input/uk.lproj/app.json index a6a971860..25f06ad83 100644 --- a/Localization/StringsConvertor/input/uk.lproj/app.json +++ b/Localization/StringsConvertor/input/uk.lproj/app.json @@ -417,7 +417,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", From dfad727965514a5f72dbfaab54ee86a56e270427 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:24 +0100 Subject: [PATCH 386/658] New translations Localizable.stringsdict (Ukrainian) --- .../input/uk.lproj/Localizable.stringsdict | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Localization/StringsConvertor/input/uk.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/uk.lproj/Localizable.stringsdict index cdf35477e..32e4cf9aa 100644 --- a/Localization/StringsConvertor/input/uk.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/uk.lproj/Localizable.stringsdict @@ -62,6 +62,26 @@ %ld characters + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 879211ac85c759b44eb7c22eb922b839ca058e80 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:25 +0100 Subject: [PATCH 387/658] New translations app.json (Slovenian) --- Localization/StringsConvertor/input/sl.lproj/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/sl.lproj/app.json b/Localization/StringsConvertor/input/sl.lproj/app.json index 37b62a45d..eb983b0fa 100644 --- a/Localization/StringsConvertor/input/sl.lproj/app.json +++ b/Localization/StringsConvertor/input/sl.lproj/app.json @@ -417,7 +417,9 @@ "custom_emoji_picker": "Izbirnik čustvenčkov po meri", "enable_content_warning": "Omogoči opozorilo o vsebini", "disable_content_warning": "Onemogoči opozorilo o vsebini", - "post_visibility_menu": "Meni vidnosti objave" + "post_visibility_menu": "Meni vidnosti objave", + "post_options": "Post Options", + "posting_as": "Posting as %s" }, "keyboard": { "discard_post": "Opusti objavo", From 144b83859ddf2aee63a807d1d8e7c407b9770d39 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:26 +0100 Subject: [PATCH 388/658] New translations Localizable.stringsdict (Danish) --- .../input/da.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/da.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/da.lproj/Localizable.stringsdict index bdcae6ac9..eabdc3c32 100644 --- a/Localization/StringsConvertor/input/da.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/da.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld characters + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 4f8c7fee1836573b821ea8b95c874bee3638dd36 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:27 +0100 Subject: [PATCH 389/658] New translations Localizable.stringsdict (Finnish) --- .../input/fi.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/fi.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/fi.lproj/Localizable.stringsdict index 8048edf2d..ccfee35c9 100644 --- a/Localization/StringsConvertor/input/fi.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/fi.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld merkkiä + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 29fc78a8d7d4bb4cb583fbcf909bf099e51aeaac Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:28 +0100 Subject: [PATCH 390/658] New translations Localizable.stringsdict (Italian) --- .../input/it.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/it.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/it.lproj/Localizable.stringsdict index 38f986521..d4fc7d0ce 100644 --- a/Localization/StringsConvertor/input/it.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/it.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld caratteri + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 1ccbdaeafa1acfb469d0584e6a9d9308850a251e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:29 +0100 Subject: [PATCH 391/658] New translations Localizable.stringsdict (Basque) --- .../input/eu.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/eu.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/eu.lproj/Localizable.stringsdict index 0159a7da9..057ca4010 100644 --- a/Localization/StringsConvertor/input/eu.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/eu.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld karaktere + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 3f5688fe1a1e145722ee83c8d2d30c9292bd5ef8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:30 +0100 Subject: [PATCH 392/658] New translations Localizable.stringsdict (Indonesian) --- .../input/id.lproj/Localizable.stringsdict | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Localization/StringsConvertor/input/id.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/id.lproj/Localizable.stringsdict index 2635defb8..9b8aca01c 100644 --- a/Localization/StringsConvertor/input/id.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/id.lproj/Localizable.stringsdict @@ -44,6 +44,20 @@ %ld karakter + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 5825b90ac2ef230fc22c6791ab5395079bfb2526 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:31 +0100 Subject: [PATCH 393/658] New translations Localizable.stringsdict (Sorani (Kurdish)) --- .../input/ckb.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/ckb.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/ckb.lproj/Localizable.stringsdict index 001a8a608..8116226ec 100644 --- a/Localization/StringsConvertor/input/ckb.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/ckb.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld نووسە + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 134f8c4f73a450672d08c4b4cc5abee62baf1dd0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:32 +0100 Subject: [PATCH 394/658] New translations Localizable.stringsdict (Kurmanji (Kurdish)) --- .../input/kmr.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/kmr.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/kmr.lproj/Localizable.stringsdict index 77571439f..45d65bc71 100644 --- a/Localization/StringsConvertor/input/kmr.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/kmr.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld tîp + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From e33c81a1dcaf64ddb2bda7af5701228a5c9b9d82 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:33 +0100 Subject: [PATCH 395/658] New translations Localizable.stringsdict (Sinhala) --- .../input/si.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/si.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/si.lproj/Localizable.stringsdict index bdcae6ac9..eabdc3c32 100644 --- a/Localization/StringsConvertor/input/si.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/si.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld characters + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 0bdd2044474c13c52d8f508d630014b0ca74429f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:34 +0100 Subject: [PATCH 396/658] New translations Localizable.stringsdict (Welsh) --- .../input/cy.lproj/Localizable.stringsdict | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Localization/StringsConvertor/input/cy.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/cy.lproj/Localizable.stringsdict index 038eaffda..9e4c09959 100644 --- a/Localization/StringsConvertor/input/cy.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/cy.lproj/Localizable.stringsdict @@ -74,6 +74,30 @@ %ld characters + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld characters + one + 1 character + two + %ld characters + few + %ld characters + many + %ld characters + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 83822c96c2ed745d543cefa237b0c2c8a67f9e28 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:36 +0100 Subject: [PATCH 397/658] New translations Localizable.stringsdict (English, United States) --- .../input/en-US.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/en-US.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/en-US.lproj/Localizable.stringsdict index bdcae6ac9..eabdc3c32 100644 --- a/Localization/StringsConvertor/input/en-US.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/en-US.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld characters + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 8c2d1c74adbcf9ffbccd0600e136bfda00aff028 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:37 +0100 Subject: [PATCH 398/658] New translations Localizable.stringsdict (Hindi) --- .../input/hi.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/hi.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/hi.lproj/Localizable.stringsdict index bdcae6ac9..eabdc3c32 100644 --- a/Localization/StringsConvertor/input/hi.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/hi.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld characters + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 96e7fd24a46d3de752d2eac4327ed8529133a4a6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:38 +0100 Subject: [PATCH 399/658] New translations Localizable.stringsdict (Latvian) --- .../input/lv.lproj/Localizable.stringsdict | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Localization/StringsConvertor/input/lv.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/lv.lproj/Localizable.stringsdict index 25f32c98d..ac30b4f8b 100644 --- a/Localization/StringsConvertor/input/lv.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/lv.lproj/Localizable.stringsdict @@ -56,6 +56,24 @@ %ld characters + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld characters + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From fd9c040dbb7bc2ec07441e463cd2f2c14ac83866 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:39 +0100 Subject: [PATCH 400/658] New translations Localizable.stringsdict (Thai) --- .../input/th.lproj/Localizable.stringsdict | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Localization/StringsConvertor/input/th.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/th.lproj/Localizable.stringsdict index 897d07eca..725e5c8a2 100644 --- a/Localization/StringsConvertor/input/th.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/th.lproj/Localizable.stringsdict @@ -44,6 +44,20 @@ %ld ตัวอักษร + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 3a8abf63a12765f76d8d2e48738968817212f1c0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:40 +0100 Subject: [PATCH 401/658] New translations Localizable.stringsdict (Spanish, Argentina) --- .../input/es-AR.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/es-AR.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/es-AR.lproj/Localizable.stringsdict index 2bd66395a..fb939a040 100644 --- a/Localization/StringsConvertor/input/es-AR.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/es-AR.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld caracteres + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + Quedan %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 caracter + other + %ld caracteres + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From b3503798b9dfa7a096b92e8f2b5052b72ae86b6c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:41 +0100 Subject: [PATCH 402/658] New translations Localizable.stringsdict (Galician) --- .../input/gl.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/gl.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/gl.lproj/Localizable.stringsdict index ff9d87c18..2224a13b3 100644 --- a/Localization/StringsConvertor/input/gl.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/gl.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld caracteres + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From ba4e2ad110accf239ae676ab8cab14d77439792e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:41 +0100 Subject: [PATCH 403/658] New translations Localizable.stringsdict (Portuguese, Brazilian) --- .../input/pt-BR.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/pt-BR.lproj/Localizable.stringsdict index ba1532740..982e25c0b 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/pt-BR.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld caracteres + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 8808905e035463b6e4360e5a63a6184d9fde5708 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:42 +0100 Subject: [PATCH 404/658] New translations Localizable.stringsdict (Vietnamese) --- .../input/vi.lproj/Localizable.stringsdict | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Localization/StringsConvertor/input/vi.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/vi.lproj/Localizable.stringsdict index 6905b240e..d82694c21 100644 --- a/Localization/StringsConvertor/input/vi.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/vi.lproj/Localizable.stringsdict @@ -44,6 +44,20 @@ %ld ký tự + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From c8c91ad892baeea3b6aa13cbf0577b48b4781ef2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:44 +0100 Subject: [PATCH 405/658] New translations Localizable.stringsdict (English) --- .../input/en.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/en.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/en.lproj/Localizable.stringsdict index bdcae6ac9..eabdc3c32 100644 --- a/Localization/StringsConvertor/input/en.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/en.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld characters + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 519bc5935572d50fa7028ca1cd9d7cc1d6e33d32 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:45 +0100 Subject: [PATCH 406/658] New translations Localizable.stringsdict (Chinese Traditional) --- .../input/zh-Hant.lproj/Localizable.stringsdict | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Localization/StringsConvertor/input/zh-Hant.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/zh-Hant.lproj/Localizable.stringsdict index c0ce0f9a2..936c2d48c 100644 --- a/Localization/StringsConvertor/input/zh-Hant.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/zh-Hant.lproj/Localizable.stringsdict @@ -44,6 +44,20 @@ %ld 個字 + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 02f159d52ad3d631e1fbd5761b9c2133a8d5d4d1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:45 +0100 Subject: [PATCH 407/658] New translations Localizable.stringsdict (Chinese Simplified) --- .../input/zh-Hans.lproj/Localizable.stringsdict | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Localization/StringsConvertor/input/zh-Hans.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/zh-Hans.lproj/Localizable.stringsdict index 5a7af3752..915a524b5 100644 --- a/Localization/StringsConvertor/input/zh-Hans.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/zh-Hans.lproj/Localizable.stringsdict @@ -44,6 +44,20 @@ %ld 个字符 + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From cac5f0df860dee4b2af7eacf6d65d076c1ab9709 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:46 +0100 Subject: [PATCH 408/658] New translations Localizable.stringsdict (Turkish) --- .../input/tr.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/tr.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/tr.lproj/Localizable.stringsdict index 29df92c2b..6ef7f4c75 100644 --- a/Localization/StringsConvertor/input/tr.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/tr.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld karakter + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 532580bce79e170aecc681b9f73dc4292bcec52f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:47 +0100 Subject: [PATCH 409/658] New translations Localizable.stringsdict (Russian) --- .../input/ru.lproj/Localizable.stringsdict | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Localization/StringsConvertor/input/ru.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/ru.lproj/Localizable.stringsdict index afb29a6aa..c9552a9e4 100644 --- a/Localization/StringsConvertor/input/ru.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/ru.lproj/Localizable.stringsdict @@ -62,6 +62,26 @@ %ld символа осталось + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 1d422047e386fc1a77b7432be43c683bb13a0370 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:49 +0100 Subject: [PATCH 410/658] New translations Localizable.stringsdict (Portuguese) --- .../input/pt.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/pt.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/pt.lproj/Localizable.stringsdict index bdcae6ac9..eabdc3c32 100644 --- a/Localization/StringsConvertor/input/pt.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/pt.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld characters + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 8cba22bcf708caf429938bca30b578650d23874f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:50 +0100 Subject: [PATCH 411/658] New translations Localizable.stringsdict (Dutch) --- .../input/nl.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/nl.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/nl.lproj/Localizable.stringsdict index 314600ff7..84769b0c1 100644 --- a/Localization/StringsConvertor/input/nl.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/nl.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld tekens + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 0efd014b3183790df496ff6c508977791ebf0cf4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:50 +0100 Subject: [PATCH 412/658] New translations Localizable.stringsdict (Japanese) --- .../input/ja.lproj/Localizable.stringsdict | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Localization/StringsConvertor/input/ja.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/ja.lproj/Localizable.stringsdict index cbc999738..795a971b7 100644 --- a/Localization/StringsConvertor/input/ja.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/ja.lproj/Localizable.stringsdict @@ -44,6 +44,20 @@ %ld 文字 + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From bea62f6a67cbcf714ac3d64b7c75721453a0d539 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 13:41:51 +0100 Subject: [PATCH 413/658] New translations Localizable.stringsdict (Kabyle) --- .../input/kab.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Localization/StringsConvertor/input/kab.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/kab.lproj/Localizable.stringsdict index 7fc6a50bb..fd7cac605 100644 --- a/Localization/StringsConvertor/input/kab.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/kab.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld yisekkilen + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 548543a8c0a0e70fa43a3d306721cf800ad4ddbe Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 14 Nov 2022 14:15:28 +0100 Subject: [PATCH 414/658] 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 57003b0ca582beaa15d078a7eb6046a02af1883c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 14:44:59 +0100 Subject: [PATCH 415/658] New translations app.json (Catalan) --- Localization/StringsConvertor/input/ca.lproj/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/ca.lproj/app.json b/Localization/StringsConvertor/input/ca.lproj/app.json index b27042d0f..e75aa9ff8 100644 --- a/Localization/StringsConvertor/input/ca.lproj/app.json +++ b/Localization/StringsConvertor/input/ca.lproj/app.json @@ -418,8 +418,8 @@ "enable_content_warning": "Activa l'Avís de Contingut", "disable_content_warning": "Desactiva l'Avís de Contingut", "post_visibility_menu": "Menú de Visibilitat de Publicació", - "post_options": "Post Options", - "posting_as": "Posting as %s" + "post_options": "Opcions del tut", + "posting_as": "Publicant com a %s" }, "keyboard": { "discard_post": "Descarta la Publicació", From 2161f68da4ad3c738dcde5406ea2519a03728ce2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 14:45:00 +0100 Subject: [PATCH 416/658] New translations app.json (Galician) --- Localization/StringsConvertor/input/gl.lproj/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index 0eb82b142..2c518c867 100644 --- a/Localization/StringsConvertor/input/gl.lproj/app.json +++ b/Localization/StringsConvertor/input/gl.lproj/app.json @@ -418,8 +418,8 @@ "enable_content_warning": "Marcar con Aviso sobre o contido", "disable_content_warning": "Retirar Aviso sobre o contido", "post_visibility_menu": "Visibilidade da publicación", - "post_options": "Post Options", - "posting_as": "Posting as %s" + "post_options": "Opcións da publicación", + "posting_as": "Publicando como %s" }, "keyboard": { "discard_post": "Descartar publicación", From aa59bef0b522280b2da00a7a0abf8296aed9316d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 14:45:01 +0100 Subject: [PATCH 417/658] New translations Localizable.stringsdict (Catalan) --- .../StringsConvertor/input/ca.lproj/Localizable.stringsdict | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/ca.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/ca.lproj/Localizable.stringsdict index 06d1f0301..947597417 100644 --- a/Localization/StringsConvertor/input/ca.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/ca.lproj/Localizable.stringsdict @@ -53,7 +53,7 @@ a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ left + resten %#@character_count@ character_count NSStringFormatSpecTypeKey @@ -61,9 +61,9 @@ NSStringFormatValueTypeKey ld one - 1 character + 1 caràcter other - %ld characters + %ld caràcters plural.count.followed_by_and_mutual From 7e458ffb10895326ef59bee670d96268dc870e72 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 14:45:03 +0100 Subject: [PATCH 418/658] New translations Localizable.stringsdict (Galician) --- .../StringsConvertor/input/gl.lproj/Localizable.stringsdict | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/gl.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/gl.lproj/Localizable.stringsdict index 2224a13b3..51b146ed4 100644 --- a/Localization/StringsConvertor/input/gl.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/gl.lproj/Localizable.stringsdict @@ -53,7 +53,7 @@ a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ left + %#@character_count@ restantes character_count NSStringFormatSpecTypeKey @@ -61,9 +61,9 @@ NSStringFormatValueTypeKey ld one - 1 character + 1 caracter other - %ld characters + %ld caracteres plural.count.followed_by_and_mutual From 120065104ccbf0d4516af618adcab97e3f432759 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 14 Nov 2022 08:45:10 -0500 Subject: [PATCH 419/658] =?UTF-8?q?Revert=20=E2=80=9CAdd=20a=20custom=20ac?= =?UTF-8?q?tion=20for=20=E2=80=98switch=20accounts=E2=80=99=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit f9daeea4d3ad66a5387493b67682c4e27396d2bf --- Localization/app.json | 6 ++--- .../Root/ContentSplitViewController.swift | 1 - .../Root/MainTab/MainTabBarController.swift | 27 ++++++------------- .../Root/Sidebar/SidebarViewController.swift | 13 --------- .../Scene/Root/Sidebar/SidebarViewModel.swift | 20 +++----------- 5 files changed, 14 insertions(+), 53 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 4c64d28f3..8fb7933c4 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -686,9 +686,9 @@ } }, "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", - "switch_accounts": "Switch Accounts" + "add_account": "Add Account" }, "wizard": { "new_in_mastodon": "New in Mastodon", @@ -699,4 +699,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file diff --git a/Mastodon/Scene/Root/ContentSplitViewController.swift b/Mastodon/Scene/Root/ContentSplitViewController.swift index 4ae7204ca..3f4758e8e 100644 --- a/Mastodon/Scene/Root/ContentSplitViewController.swift +++ b/Mastodon/Scene/Root/ContentSplitViewController.swift @@ -33,7 +33,6 @@ final class ContentSplitViewController: UIViewController, NeedsDependency { sidebarViewController.context = context sidebarViewController.coordinator = coordinator sidebarViewController.viewModel = SidebarViewModel(context: context, authContext: authContext) - sidebarViewController.viewModel.delegate = sidebarViewController sidebarViewController.delegate = self return sidebarViewController }() diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 0eea7288e..e2510071e 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -298,7 +298,6 @@ extension MainTabBarController { } .store(in: &disposeBag) - let profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag } if let user = authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user { self.avatarURLObserver = user.publisher(for: \.avatar) .sink { [weak self, weak user] _ in @@ -307,19 +306,13 @@ extension MainTabBarController { guard user.managedObjectContext != nil else { return } self.avatarURL = user.avatarImageURL() } - if let profileTabItem { - profileTabItem.accessibilityCustomActions = [ - UIAccessibilityCustomAction(name: L10n.Scene.AccountList.switchAccounts) { [weak self] _ in - self?.showAccountSwitcher() - return true - } - ] - } + + // a11y + let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag } + guard let profileTabItem = _profileTabItem else { return } + profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(user.displayNameWithFallback) } else { self.avatarURLObserver = nil - if let profileTabItem { - profileTabItem.accessibilityCustomActions = [] - } } let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer() @@ -395,17 +388,13 @@ extension MainTabBarController { switch tab { case .me: - showAccountSwitcher() + guard let authContext = self.authContext else { return } + let accountListViewModel = AccountListViewModel(context: context, authContext: authContext) + _ = coordinator.present(scene: .accountList(viewModel: accountListViewModel), from: self, transition: .panModal) default: break } } - - private func showAccountSwitcher() { - guard let authContext = self.authContext else { return } - let accountListViewModel = AccountListViewModel(context: context, authContext: authContext) - _ = coordinator.present(scene: .accountList(viewModel: accountListViewModel), from: self, transition: .panModal) - } } extension MainTabBarController { diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift index 28e205047..70e1239b6 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift @@ -162,19 +162,6 @@ extension SidebarViewController { } -extension SidebarViewController: SidebarViewModelDelegate { - func sidebarViewModelDidSwitchAccounts(_ sidebarViewModel: SidebarViewModel) { - guard let diffableDataSource = viewModel.diffableDataSource else { return } - guard let indexPath = diffableDataSource.indexPath(for: .tab(.me)) else { return } - guard let cell = collectionView.cellForItem(at: indexPath) else { return } - delegate?.sidebarViewController( - self, - didLongPressItem: .tab(.me), - sourceView: cell - ) - } -} - extension SidebarViewController { @objc private func sidebarLongPressGestureRecognizerHandler(_ sender: UILongPressGestureRecognizer) { guard sender.state == .began else { return } diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index 2a8082e47..c3f9e3e36 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -15,10 +15,6 @@ import MastodonAsset import MastodonCore import MastodonLocalization -protocol SidebarViewModelDelegate: AnyObject { - func sidebarViewModelDidSwitchAccounts(_ sidebarViewModel: SidebarViewModel) -} - final class SidebarViewModel { var disposeBag = Set() @@ -35,8 +31,6 @@ final class SidebarViewModel { var secondaryDiffableDataSource: UICollectionViewDiffableDataSource? @Published private(set) var isReadyForWizardAvatarButton = false - weak var delegate: SidebarViewModelDelegate? - init(context: AppContext, authContext: AuthContext?) { self.context = context self.authContext = authContext @@ -134,17 +128,9 @@ extension SidebarViewModel { } .store(in: &cell.disposeBag) case .me: - if self.authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user != nil { - cell.accessibilityCustomActions = [ - UIAccessibilityCustomAction(name: L10n.Scene.AccountList.switchAccounts) { [weak self] _ in - guard let self, let delegate = self.delegate else { return false } - delegate.sidebarViewModelDidSwitchAccounts(self) - return true - } - ] - } else { - cell.accessibilityCustomActions = [] - } + guard let user = self.authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return } + let currentUserDisplayName = user.displayNameWithFallback + cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName) default: break } From 3aac00fa83af05d2fe1cbe631dc9216daedc4e34 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 15:40:15 +0100 Subject: [PATCH 420/658] New translations app.json (Italian) --- Localization/StringsConvertor/input/it.lproj/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json index bfd7811e2..f5e8d9293 100644 --- a/Localization/StringsConvertor/input/it.lproj/app.json +++ b/Localization/StringsConvertor/input/it.lproj/app.json @@ -418,8 +418,8 @@ "enable_content_warning": "Abilita avvertimento contenuti", "disable_content_warning": "Disabilita avviso di contenuti", "post_visibility_menu": "Menu di visibilità del post", - "post_options": "Post Options", - "posting_as": "Posting as %s" + "post_options": "Opzioni del messaggio", + "posting_as": "Pubblicazione come %s" }, "keyboard": { "discard_post": "Scarta post", From 95e90a712d559401ee46434cbc914e3efcdbf8d0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 15:40:16 +0100 Subject: [PATCH 421/658] New translations Localizable.stringsdict (Italian) --- .../StringsConvertor/input/it.lproj/Localizable.stringsdict | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/it.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/it.lproj/Localizable.stringsdict index d4fc7d0ce..3a8549914 100644 --- a/Localization/StringsConvertor/input/it.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/it.lproj/Localizable.stringsdict @@ -53,7 +53,7 @@ a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ left + %#@character_count@ rimanenti character_count NSStringFormatSpecTypeKey @@ -61,9 +61,9 @@ NSStringFormatValueTypeKey ld one - 1 character + 1 carattere other - %ld characters + %ld caratteri plural.count.followed_by_and_mutual From 1938af714693d98e6ffd19bcda7cceb399cd8240 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 16:44:33 +0100 Subject: [PATCH 422/658] New translations app.json (Slovenian) --- .../StringsConvertor/input/sl.lproj/app.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Localization/StringsConvertor/input/sl.lproj/app.json b/Localization/StringsConvertor/input/sl.lproj/app.json index eb983b0fa..1d63d2832 100644 --- a/Localization/StringsConvertor/input/sl.lproj/app.json +++ b/Localization/StringsConvertor/input/sl.lproj/app.json @@ -383,10 +383,10 @@ "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 ...", - "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": "Nalaganje ni uspelo", + "upload_failed": "Nalaganje na strežnik ni uspelo", + "can_not_recognize_this_media_attachment": "Te medijske priponke ni mogoče prepoznati", + "attachment_too_large": "Priponka je prevelika" }, "poll": { "duration_time": "Trajanje: %s", @@ -418,8 +418,8 @@ "enable_content_warning": "Omogoči opozorilo o vsebini", "disable_content_warning": "Onemogoči opozorilo o vsebini", "post_visibility_menu": "Meni vidnosti objave", - "post_options": "Post Options", - "posting_as": "Posting as %s" + "post_options": "Možnosti objave", + "posting_as": "Objavljate kot %s" }, "keyboard": { "discard_post": "Opusti objavo", From 57d80a89975efc77193479aa75a03940668ccba8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 16:44:34 +0100 Subject: [PATCH 423/658] New translations app.json (Vietnamese) --- .../StringsConvertor/input/vi.lproj/app.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Localization/StringsConvertor/input/vi.lproj/app.json b/Localization/StringsConvertor/input/vi.lproj/app.json index cd20997c3..eb1b1c52d 100644 --- a/Localization/StringsConvertor/input/vi.lproj/app.json +++ b/Localization/StringsConvertor/input/vi.lproj/app.json @@ -383,10 +383,10 @@ "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ị...", - "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": "Tải thất bại", + "upload_failed": "Tải lên thất bại", + "can_not_recognize_this_media_attachment": "Không xem được tập tin đính kèm", + "attachment_too_large": "Tập tin đính kèm quá lớn" }, "poll": { "duration_time": "Thời hạn: %s", @@ -418,8 +418,8 @@ "enable_content_warning": "Bật nội dung ẩn", "disable_content_warning": "Tắt nội dung ẩn", "post_visibility_menu": "Menu hiển thị tút", - "post_options": "Post Options", - "posting_as": "Posting as %s" + "post_options": "Tùy chọn đăng", + "posting_as": "Đăng dưới dạng %s" }, "keyboard": { "discard_post": "Hủy đăng tút", From d1d694717dced66a0bccc5e8b86bf5d071b3342f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 16:44:35 +0100 Subject: [PATCH 424/658] New translations Localizable.stringsdict (Slovenian) --- .../input/sl.lproj/Localizable.stringsdict | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Localization/StringsConvertor/input/sl.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/sl.lproj/Localizable.stringsdict index 742b78e87..87cc42142 100644 --- a/Localization/StringsConvertor/input/sl.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/sl.lproj/Localizable.stringsdict @@ -65,7 +65,7 @@ a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ left + preostaja %#@character_count@ character_count NSStringFormatSpecTypeKey @@ -73,13 +73,13 @@ NSStringFormatValueTypeKey ld one - 1 character + %ld znak two - %ld characters + %ld znaka few - %ld characters + %ld znaki other - %ld characters + %ld znakov plural.count.followed_by_and_mutual From dabb1a1502dfab09f179ca7cbd1fd622479ef8ab Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 16:44:36 +0100 Subject: [PATCH 425/658] 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 f9c30f01d..d90786149 100644 --- a/Localization/StringsConvertor/input/th.lproj/app.json +++ b/Localization/StringsConvertor/input/th.lproj/app.json @@ -418,7 +418,7 @@ "enable_content_warning": "เปิดใช้งานคำเตือนเนื้อหา", "disable_content_warning": "ปิดใช้งานคำเตือนเนื้อหา", "post_visibility_menu": "เมนูการมองเห็นโพสต์", - "post_options": "Post Options", + "post_options": "ตัวเลือกโพสต์", "posting_as": "Posting as %s" }, "keyboard": { From 8185adcdd099316f164a6e24376e10d84d3e2e3e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 16:44:37 +0100 Subject: [PATCH 426/658] New translations Localizable.stringsdict (Vietnamese) --- .../StringsConvertor/input/vi.lproj/Localizable.stringsdict | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/vi.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/vi.lproj/Localizable.stringsdict index d82694c21..4c772f014 100644 --- a/Localization/StringsConvertor/input/vi.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/vi.lproj/Localizable.stringsdict @@ -47,7 +47,7 @@ a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ left + %#@character_count@ còn lại character_count NSStringFormatSpecTypeKey @@ -55,7 +55,7 @@ NSStringFormatValueTypeKey ld other - %ld characters + %ld ký tự plural.count.followed_by_and_mutual From 2ba82adac90c0c2648f555ea66c9af7294c26e10 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 16:44:38 +0100 Subject: [PATCH 427/658] New translations Localizable.stringsdict (Thai) --- .../StringsConvertor/input/th.lproj/Localizable.stringsdict | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/th.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/th.lproj/Localizable.stringsdict index 725e5c8a2..f25561ad6 100644 --- a/Localization/StringsConvertor/input/th.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/th.lproj/Localizable.stringsdict @@ -47,7 +47,7 @@ a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ left + เหลืออีก %#@character_count@ character_count NSStringFormatSpecTypeKey @@ -55,7 +55,7 @@ NSStringFormatValueTypeKey ld other - %ld characters + %ld ตัวอักษร plural.count.followed_by_and_mutual From abe6292696277c925dd5c467662ee237bbd9f731 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 15 Nov 2022 00:59:04 +0800 Subject: [PATCH 428/658] 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 d9f24a0ad0784d7e0f069e9f672f674c8b0f3656 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 18:00:59 +0100 Subject: [PATCH 429/658] 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 4b2ea2f0f..1c2e9b9ff 100644 --- a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json @@ -383,10 +383,10 @@ "attachment_broken": "此 %s 已損毀,並無法被上傳至 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 33383de85de8279f230009cbb9ef23213773e29a Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 15 Nov 2022 01:42:14 +0800 Subject: [PATCH 430/658] 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 431/658] 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 f8a15ce5cfd1523e821259ba9069d7c6bf2f3c8d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 19:01:32 +0100 Subject: [PATCH 432/658] New translations app.json (Chinese Traditional) --- Localization/StringsConvertor/input/zh-Hant.lproj/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json index 1c2e9b9ff..131bd3f37 100644 --- a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json @@ -418,8 +418,8 @@ "enable_content_warning": "啟用內容警告", "disable_content_warning": "停用內容警告", "post_visibility_menu": "嘟文可見性選單", - "post_options": "Post Options", - "posting_as": "Posting as %s" + "post_options": "嘟文選項", + "posting_as": "以 %s 發嘟" }, "keyboard": { "discard_post": "捨棄嘟文", From 610e0675fd3a13dffdb68949e592330421f2589d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 19:01:33 +0100 Subject: [PATCH 433/658] New translations app.json (Portuguese, Brazilian) --- .../input/pt-BR.lproj/app.json | 108 +++++++++--------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/app.json b/Localization/StringsConvertor/input/pt-BR.lproj/app.json index c7abb1da1..352025595 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/app.json +++ b/Localization/StringsConvertor/input/pt-BR.lproj/app.json @@ -164,7 +164,7 @@ "emoji": "Emoji" }, "visibility": { - "unlisted": "Everyone can see this post but not display in the public timeline.", + "unlisted": "Todos podem ver esta postagem, mas não são exibidos na linha do tempo pública.", "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." @@ -202,8 +202,8 @@ }, "header": { "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.", + "blocking_warning": "Você não pode ver o perfil deste usuário até desbloqueá-lo.\nSeu perfil aparece assim para esse usuário.", + "user_blocking_warning": "Você não pode ver o perfil de %s até desbloqueá-lo.\nSeu perfil aparece assim para esse usuário.", "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.", @@ -259,7 +259,7 @@ }, "register": { "title": "Vamos configurar você em %s", - "lets_get_you_set_up_on_domain": "Let’s get you set up on %s", + "lets_get_you_set_up_on_domain": "Vamos configurar você em %s", "input": { "avatar": { "delete": "Excluir" @@ -279,13 +279,13 @@ "require": "Sua senha deve ter pelo menos:", "character_limit": "8 carácteres", "accessibility": { - "checked": "checked", - "unchecked": "unchecked" + "checked": "marcado", + "unchecked": "desmarcado" }, "hint": "Sua senha precisa ter pelo menos oito carácteres" }, "invite": { - "registration_user_invite_request": "Why do you want to join?" + "registration_user_invite_request": "Por que você deseja se inscrever?" } }, "error": { @@ -298,51 +298,51 @@ "reason": "Motivo" }, "reason": { - "blocked": "%s contains a disallowed email provider", + "blocked": "%s contém um provedor de e-mail não permitido", "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", - "invalid": "%s is invalid", - "too_long": "%s is too long", - "too_short": "%s is too short", - "inclusion": "%s is not a supported value" + "reserved": "%s é uma palavra-chave reservada", + "accepted": "%s deve ser aceite", + "blank": "%s é obrigatório", + "invalid": "%s é inválido", + "too_long": "%s é muito longo", + "too_short": "%s é muito curto", + "inclusion": "%s não é um valor suportado" }, "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)" + "username_invalid": "O nome de usuário só pode conter caracteres alfanuméricos e underlines (_)", + "username_too_long": "Nome de usuário é muito longo (não pode ter mais de 30 caracteres)", + "email_invalid": "Este não é um endereço de e-mail válido", + "password_too_short": "A senha é muito curta (deve ter pelo menos 8 caracteres)" } } }, "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": "Algumas regras básicas.", + "subtitle": "Estes são definidos e aplicados pelos moderadores da %s.", + "prompt": "Ao continuar, você estará sujeito aos termos de serviço e política de privacidade para %s.", + "terms_of_service": "termos de serviço", + "privacy_policy": "política de privacidade", "button": { - "confirm": "I Agree" + "confirm": "Eu concordo" } }, "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": "Uma última coisa.", + "subtitle": "Clique no link que te enviamos por e-mail para verificar a sua conta.", + "tap_the_link_we_emailed_to_you_to_verify_your_account": "Clique no link que te enviamos por e-mail para verificar a sua conta", "button": { - "open_email_app": "Open Email App", - "resend": "Resend" + "open_email_app": "Abrir aplicativo de e-mail", + "resend": "Reenviar" }, "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": "Verifique o seu e-mail", + "description": "Verifique se o seu endereço de e-mail está correto, e também a sua pasta de spam caso não tenha verificado.", + "resend_email": "Reenviar 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": "Verifique sua caixa de entrada.", + "description": "Enviamos um e-mail para você. Verifique sua pasta de spam caso ainda tenha verificado.", "mail": "Mail", "open_email_client": "Open Email Client" } @@ -350,12 +350,12 @@ "home_timeline": { "title": "Home", "navigation_bar_state": { - "offline": "Offline", - "new_posts": "See new posts", - "published": "Published!", - "Publishing": "Publishing post...", + "offline": "Desconectado", + "new_posts": "Ver novas postagens", + "published": "Publicado!", + "Publishing": "Publicando toot...", "accessibility": { - "logo_label": "Logo Button", + "logo_label": "Botão do logotipo", "logo_hint": "Tap to scroll to top and tap again to previous location" } } @@ -366,12 +366,12 @@ }, "compose": { "title": { - "new_post": "New Post", - "new_reply": "New Reply" + "new_post": "Novo toot", + "new_reply": "Nova resposta" }, "media_selection": { - "camera": "Take Photo", - "photo_library": "Photo Library", + "camera": "Tirar foto", + "photo_library": "Galeria", "browse": "Navegar" }, "content_input_placeholder": "Digite ou cole o que está na sua mente", @@ -418,8 +418,8 @@ "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" + "post_options": "Opções de postagem", + "posting_as": "Publicando como %s" }, "keyboard": { "discard_post": "Discard Post", @@ -443,20 +443,20 @@ "add_row": "Add Row", "placeholder": { "label": "Label", - "content": "Content" + "content": "Conteúdo" } }, "segmented_control": { - "posts": "Posts", - "replies": "Replies", - "posts_and_replies": "Posts and Replies", - "media": "Media", - "about": "About" + "posts": "Toots", + "replies": "Respostas", + "posts_and_replies": "Toots e respostas", + "media": "Mídia", + "about": "Sobre" }, "relationship_action_alert": { "confirm_mute_user": { - "title": "Mute Account", - "message": "Confirm to mute %s" + "title": "Silenciar conta", + "message": "Confirme para silenciar %s" }, "confirm_unmute_user": { "title": "Unmute Account", From 520028ec4cd786b65c7f2e88de4f9e8115a67173 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 19:01:34 +0100 Subject: [PATCH 434/658] 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 d90786149..55c808efa 100644 --- a/Localization/StringsConvertor/input/th.lproj/app.json +++ b/Localization/StringsConvertor/input/th.lproj/app.json @@ -385,7 +385,7 @@ "description_video": "อธิบายวิดีโอสำหรับผู้บกพร่องทางการมองเห็น...", "load_failed": "การโหลดล้มเหลว", "upload_failed": "การอัปโหลดล้มเหลว", - "can_not_recognize_this_media_attachment": "Can not regonize this media attachment", + "can_not_recognize_this_media_attachment": "ไม่สามารถระบุไฟล์แนบสื่อนี้", "attachment_too_large": "ไฟล์แนบใหญ่เกินไป" }, "poll": { @@ -419,7 +419,7 @@ "disable_content_warning": "ปิดใช้งานคำเตือนเนื้อหา", "post_visibility_menu": "เมนูการมองเห็นโพสต์", "post_options": "ตัวเลือกโพสต์", - "posting_as": "Posting as %s" + "posting_as": "กำลังโพสต์เป็น %s" }, "keyboard": { "discard_post": "ละทิ้งโพสต์", From 2428c828de049d4de72973319888140773ac59d2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 19:01:35 +0100 Subject: [PATCH 435/658] New translations Localizable.stringsdict (Chinese Traditional) --- .../input/zh-Hant.lproj/Localizable.stringsdict | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/zh-Hant.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/zh-Hant.lproj/Localizable.stringsdict index 936c2d48c..d545fd6a4 100644 --- a/Localization/StringsConvertor/input/zh-Hant.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/zh-Hant.lproj/Localizable.stringsdict @@ -47,7 +47,7 @@ a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ left + 剩餘 %#@character_count@ 字 character_count NSStringFormatSpecTypeKey @@ -55,7 +55,7 @@ NSStringFormatValueTypeKey ld other - %ld characters + %ld 個字 plural.count.followed_by_and_mutual From 52e7442d994ee597c6a717123b0f9fecf70ebab4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 19:01:36 +0100 Subject: [PATCH 436/658] New translations Localizable.stringsdict (Portuguese, Brazilian) --- .../input/pt-BR.lproj/Localizable.stringsdict | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/pt-BR.lproj/Localizable.stringsdict index 982e25c0b..9a72fe3bb 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/pt-BR.lproj/Localizable.stringsdict @@ -53,7 +53,7 @@ a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ left + %#@character_count@ restantes character_count NSStringFormatSpecTypeKey @@ -61,9 +61,9 @@ NSStringFormatValueTypeKey ld one - 1 character + 1 carácter other - %ld characters + %ld carácteres plural.count.followed_by_and_mutual From 646d099abab7418725c6ee2fbfe09bf8a4b41d0e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:08 +0100 Subject: [PATCH 437/658] New translations app.json (Slovenian) --- .../StringsConvertor/input/sl.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/sl.lproj/app.json b/Localization/StringsConvertor/input/sl.lproj/app.json index 1d63d2832..7cb750c38 100644 --- a/Localization/StringsConvertor/input/sl.lproj/app.json +++ b/Localization/StringsConvertor/input/sl.lproj/app.json @@ -385,8 +385,10 @@ "description_video": "Opiši video za slabovidne in osebe z okvaro vida ...", "load_failed": "Nalaganje ni uspelo", "upload_failed": "Nalaganje na strežnik ni uspelo", - "can_not_recognize_this_media_attachment": "Te medijske priponke ni mogoče prepoznati", - "attachment_too_large": "Priponka je prevelika" + "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "attachment_too_large": "Priponka je prevelika", + "compressing_state": "Compressing...", + "server_processing_state": "Server Processing..." }, "poll": { "duration_time": "Trajanje: %s", @@ -396,7 +398,9 @@ "one_day": "1 dan", "three_days": "3 dni", "seven_days": "7 dni", - "option_number": "Možnost %ld" + "option_number": "Možnost %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "Tukaj zapišite opozorilo ..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Oznaka", "content": "Vsebina" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Zaznamki" } } -} +} \ No newline at end of file From cd223766f746179c7930dd01ecf25dc6fc1fef7b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:09 +0100 Subject: [PATCH 438/658] New translations app.json (Thai) --- .../StringsConvertor/input/th.lproj/app.json | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Localization/StringsConvertor/input/th.lproj/app.json b/Localization/StringsConvertor/input/th.lproj/app.json index 55c808efa..dd5ee212c 100644 --- a/Localization/StringsConvertor/input/th.lproj/app.json +++ b/Localization/StringsConvertor/input/th.lproj/app.json @@ -299,7 +299,7 @@ }, "reason": { "blocked": "%s มีผู้ให้บริการอีเมลที่ไม่ได้รับอนุญาต", - "unreachable": "ดูเหมือนว่า %s จะไม่มีอยู่", + "unreachable": "ดูเหมือนว่าจะไม่มี %s อยู่", "taken": "%s ถูกใช้งานแล้ว", "reserved": "%s เป็นคำสงวน", "accepted": "ต้องยอมรับ %s", @@ -337,12 +337,12 @@ }, "dont_receive_email": { "title": "ตรวจสอบอีเมลของคุณ", - "description": "หากคุณยังไม่ได้รับอีเมล ตรวจสอบว่าที่อยู่อีเมลของคุณถูกต้อง รวมถึงโฟลเดอร์อีเมลขยะของคุณ", + "description": "ตรวจสอบว่าที่อยู่อีเมลของคุณถูกต้องเช่นเดียวกับโฟลเดอร์อีเมลขยะหากคุณยังไม่ได้ทำ", "resend_email": "ส่งอีเมลใหม่" }, "open_email_app": { "title": "ตรวจสอบกล่องขาเข้าของคุณ", - "description": "เราเพิ่งส่งอีเมลหาคุณ หากคุณยังไม่ได้รับอีเมล โปรดตรวจสอบโฟลเดอร์อีเมลขยะ", + "description": "เราเพิ่งส่งอีเมลถึงคุณ ตรวจสอบโฟลเดอร์อีเมลขยะของคุณหากคุณยังไม่ได้ทำ", "mail": "จดหมาย", "open_email_client": "เปิดไคลเอ็นต์อีเมล" } @@ -386,7 +386,9 @@ "load_failed": "การโหลดล้มเหลว", "upload_failed": "การอัปโหลดล้มเหลว", "can_not_recognize_this_media_attachment": "ไม่สามารถระบุไฟล์แนบสื่อนี้", - "attachment_too_large": "ไฟล์แนบใหญ่เกินไป" + "attachment_too_large": "ไฟล์แนบใหญ่เกินไป", + "compressing_state": "กำลังบีบอัด...", + "server_processing_state": "เซิร์ฟเวอร์กำลังประมวลผล..." }, "poll": { "duration_time": "ระยะเวลา: %s", @@ -396,7 +398,9 @@ "one_day": "1 วัน", "three_days": "3 วัน", "seven_days": "7 วัน", - "option_number": "ตัวเลือก %ld" + "option_number": "ตัวเลือก %ld", + "the_poll_is_invalid": "การสำรวจความคิดเห็นไม่ถูกต้อง", + "the_poll_has_empty_option": "การสำรวจความคิดเห็นมีตัวเลือกที่ว่างเปล่า" }, "content_warning": { "placeholder": "เขียนคำเตือนที่ถูกต้องที่นี่..." @@ -444,6 +448,10 @@ "placeholder": { "label": "ป้ายชื่อ", "content": "เนื้อหา" + }, + "verified": { + "short": "ตรวจสอบเมื่อ %s", + "long": "ตรวจสอบความเป็นเจ้าของของลิงก์นี้เมื่อ %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "ที่คั่นหน้า" } } -} +} \ No newline at end of file From 2471dfa7ece2b77b20b229a9daf012c632e170fb Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:10 +0100 Subject: [PATCH 439/658] New translations app.json (Russian) --- .../StringsConvertor/input/ru.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/ru.lproj/app.json b/Localization/StringsConvertor/input/ru.lproj/app.json index 1f82a35db..8505e7f42 100644 --- a/Localization/StringsConvertor/input/ru.lproj/app.json +++ b/Localization/StringsConvertor/input/ru.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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": "Продолжительность: %s", @@ -396,7 +398,9 @@ "one_day": "1 день", "three_days": "3 дня", "seven_days": "7 дней", - "option_number": "Вариант %ld" + "option_number": "Вариант %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "Напишите предупреждение здесь..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Ярлык", "content": "Содержимое" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From c61c529ee757447e046d623639417818086c6b98 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:11 +0100 Subject: [PATCH 440/658] New translations app.json (Chinese Simplified) --- .../input/zh-Hans.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json index 2bfd04c8d..c4b127914 100644 --- a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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": "时长:%s", @@ -396,7 +398,9 @@ "one_day": "1 天", "three_days": "3 天", "seven_days": "7 天", - "option_number": "选项 %ld" + "option_number": "选项 %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "在这里写下内容的警告消息..." @@ -444,6 +448,10 @@ "placeholder": { "label": "标签", "content": "内容" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From cd68fd5e284aeee313dd8d545f8e9673581f6060 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:12 +0100 Subject: [PATCH 441/658] New translations app.json (English) --- .../StringsConvertor/input/en.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/en.lproj/app.json b/Localization/StringsConvertor/input/en.lproj/app.json index 25f06ad83..30566d8d6 100644 --- a/Localization/StringsConvertor/input/en.lproj/app.json +++ b/Localization/StringsConvertor/input/en.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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", @@ -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..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Label", "content": "Content" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 1edde4311767a49dc97f39595c777b82edae26d5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:14 +0100 Subject: [PATCH 442/658] New translations app.json (Galician) --- .../StringsConvertor/input/gl.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index 2c518c867..ee2a7cef7 100644 --- a/Localization/StringsConvertor/input/gl.lproj/app.json +++ b/Localization/StringsConvertor/input/gl.lproj/app.json @@ -385,8 +385,10 @@ "description_video": "Describe o vídeo para persoas con problemas visuais...", "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" + "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "attachment_too_large": "Adxunto demasiado grande", + "compressing_state": "Compressing...", + "server_processing_state": "Server Processing..." }, "poll": { "duration_time": "Duración: %s", @@ -396,7 +398,9 @@ "one_day": "1 Día", "three_days": "3 Días", "seven_days": "7 Días", - "option_number": "Opción %ld" + "option_number": "Opción %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "Escribe o teu aviso aquí..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Etiqueta", "content": "Contido" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Marcadores" } } -} +} \ No newline at end of file From 348577cbd6b3e541eed83eb347304805a81556a7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:15 +0100 Subject: [PATCH 443/658] New translations app.json (Portuguese, Brazilian) --- .../StringsConvertor/input/pt-BR.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/app.json b/Localization/StringsConvertor/input/pt-BR.lproj/app.json index 352025595..9d20f1151 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/app.json +++ b/Localization/StringsConvertor/input/pt-BR.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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": "Duração: %s", @@ -396,7 +398,9 @@ "one_day": "1 dia", "three_days": "3 dias", "seven_days": "7 dias", - "option_number": "Opção %ld" + "option_number": "Opção %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..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Label", "content": "Conteúdo" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From ced67536667aba4bf04586430968509f219f7423 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:16 +0100 Subject: [PATCH 444/658] New translations app.json (Indonesian) --- .../StringsConvertor/input/id.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/id.lproj/app.json b/Localization/StringsConvertor/input/id.lproj/app.json index 91d31071b..45e072a30 100644 --- a/Localization/StringsConvertor/input/id.lproj/app.json +++ b/Localization/StringsConvertor/input/id.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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": "Durasi: %s", @@ -396,7 +398,9 @@ "one_day": "1 Hari", "three_days": "3 Hari", "seven_days": "7 Hari", - "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": "Tulis peringatan yang akurat di sini..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Label", "content": "Isi" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 5e2e1e06346159b14210bd5754c40a272ae676c3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:17 +0100 Subject: [PATCH 445/658] New translations app.json (Spanish, Argentina) --- .../StringsConvertor/input/es-AR.lproj/app.json | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/es-AR.lproj/app.json b/Localization/StringsConvertor/input/es-AR.lproj/app.json index e54100c41..21934a104 100644 --- a/Localization/StringsConvertor/input/es-AR.lproj/app.json +++ b/Localization/StringsConvertor/input/es-AR.lproj/app.json @@ -386,7 +386,9 @@ "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" + "attachment_too_large": "Adjunto demasiado grande", + "compressing_state": "Comprimiendo…", + "server_processing_state": "Servidor procesando…" }, "poll": { "duration_time": "Duración: %s", @@ -396,7 +398,9 @@ "one_day": "1 día", "three_days": "3 días", "seven_days": "7 días", - "option_number": "Opción %ld" + "option_number": "Opción %ld", + "the_poll_is_invalid": "La encuesta no es válida", + "the_poll_has_empty_option": "La encuesta tiene opción vacía" }, "content_warning": { "placeholder": "Escribí una advertencia precisa acá…" @@ -444,6 +448,10 @@ "placeholder": { "label": "Nombre de campo", "content": "Valor de campo" + }, + "verified": { + "short": "Verificado en %s", + "long": "La propiedad de este enlace fue verificada el %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Marcadores" } } -} +} \ No newline at end of file From a2d0e1521804317e808748ea785d739872238755 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:18 +0100 Subject: [PATCH 446/658] New translations app.json (Latvian) --- .../StringsConvertor/input/lv.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/lv.lproj/app.json b/Localization/StringsConvertor/input/lv.lproj/app.json index 8a4b3e921..f21cde453 100644 --- a/Localization/StringsConvertor/input/lv.lproj/app.json +++ b/Localization/StringsConvertor/input/lv.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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", @@ -396,7 +398,9 @@ "one_day": "1 Diena", "three_days": "3 Dienas", "seven_days": "7 Dienas", - "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..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Label", "content": "Saturs" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 80d830d0749c128c0e0835119ea78f4fe4e70f75 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:19 +0100 Subject: [PATCH 447/658] New translations app.json (Dutch) --- .../StringsConvertor/input/nl.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/nl.lproj/app.json b/Localization/StringsConvertor/input/nl.lproj/app.json index bc508d5b0..5241d23bf 100644 --- a/Localization/StringsConvertor/input/nl.lproj/app.json +++ b/Localization/StringsConvertor/input/nl.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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": "Duur: %s", @@ -396,7 +398,9 @@ "one_day": "1 Dag", "three_days": "3 Dagen", "seven_days": "7 Dagen", - "option_number": "Optie %ld" + "option_number": "Optie %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "Schrijf hier een nauwkeurige waarschuwing..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Etiket", "content": "Inhoud" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 881c48d74f064ab4064d873f6e833bd265697d17 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:20 +0100 Subject: [PATCH 448/658] New translations app.json (Hindi) --- .../StringsConvertor/input/hi.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/hi.lproj/app.json b/Localization/StringsConvertor/input/hi.lproj/app.json index 2c46d023d..ec405da4d 100644 --- a/Localization/StringsConvertor/input/hi.lproj/app.json +++ b/Localization/StringsConvertor/input/hi.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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", @@ -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..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Label", "content": "Content" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 0d1e74ae395d0bd43ac9075f64563d1e11e892aa Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:21 +0100 Subject: [PATCH 449/658] New translations app.json (English, United States) --- .../StringsConvertor/input/en-US.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/en-US.lproj/app.json b/Localization/StringsConvertor/input/en-US.lproj/app.json index 25f06ad83..30566d8d6 100644 --- a/Localization/StringsConvertor/input/en-US.lproj/app.json +++ b/Localization/StringsConvertor/input/en-US.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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", @@ -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..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Label", "content": "Content" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From a068ac9efcd0ffa0057f09854e3b08f5e9e62901 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:22 +0100 Subject: [PATCH 450/658] New translations app.json (Welsh) --- .../StringsConvertor/input/cy.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/cy.lproj/app.json b/Localization/StringsConvertor/input/cy.lproj/app.json index a5de5143a..5a962d852 100644 --- a/Localization/StringsConvertor/input/cy.lproj/app.json +++ b/Localization/StringsConvertor/input/cy.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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", @@ -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..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Label", "content": "Content" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 61519750fb30dd359d46dfe5dd303dacb4e098dc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:24 +0100 Subject: [PATCH 451/658] New translations app.json (Sinhala) --- .../StringsConvertor/input/si.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/si.lproj/app.json b/Localization/StringsConvertor/input/si.lproj/app.json index 1bc8b3036..2ac50e06b 100644 --- a/Localization/StringsConvertor/input/si.lproj/app.json +++ b/Localization/StringsConvertor/input/si.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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", @@ -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..." @@ -444,6 +448,10 @@ "placeholder": { "label": "නම්පත", "content": "අන්තර්ගතය" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 25c55ddd0612fde595e05ddb50758c5247f8ff09 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:26 +0100 Subject: [PATCH 452/658] New translations app.json (Kurmanji (Kurdish)) --- .../StringsConvertor/input/kmr.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/kmr.lproj/app.json b/Localization/StringsConvertor/input/kmr.lproj/app.json index 1fa54d8ef..6f5fc8b97 100644 --- a/Localization/StringsConvertor/input/kmr.lproj/app.json +++ b/Localization/StringsConvertor/input/kmr.lproj/app.json @@ -385,8 +385,10 @@ "description_video": "Vîdyoyê ji bo kêmbînên dîtbar bide nasîn...", "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" + "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "attachment_too_large": "Pêvek pir mezin e", + "compressing_state": "Compressing...", + "server_processing_state": "Server Processing..." }, "poll": { "duration_time": "Dirêjî: %s", @@ -396,7 +398,9 @@ "one_day": "1 Roj", "three_days": "3 Roj", "seven_days": "7 Roj", - "option_number": "Vebijêrk %ld" + "option_number": "Vebijêrk %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "Li vir hişyariyek hûrgilî binivîsine..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Nîşan", "content": "Naverok" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Şûnpel" } } -} +} \ No newline at end of file From ab5eb3c285f4013727b33bd3ad0fdd07af7b0ea9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:27 +0100 Subject: [PATCH 453/658] New translations app.json (Sorani (Kurdish)) --- .../StringsConvertor/input/ckb.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/ckb.lproj/app.json b/Localization/StringsConvertor/input/ckb.lproj/app.json index b8cf48fbe..3720f555e 100644 --- a/Localization/StringsConvertor/input/ckb.lproj/app.json +++ b/Localization/StringsConvertor/input/ckb.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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": "کات:‌ %s", @@ -396,7 +398,9 @@ "one_day": "1 ڕۆژ", "three_days": "3 ڕۆژ", "seven_days": "7 ڕۆژ", - "option_number": "بژاردەی %ld" + "option_number": "بژاردەی %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "ئاگادارییەکەت لێرە بنووسە..." @@ -444,6 +448,10 @@ "placeholder": { "label": "ناونیشان", "content": "ناوەڕۆک" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 88489d5cdb219ed4735fe7673b0d6fdfb8733785 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:28 +0100 Subject: [PATCH 454/658] New translations Localizable.stringsdict (French) --- .../StringsConvertor/input/fr.lproj/Localizable.stringsdict | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/fr.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/fr.lproj/Localizable.stringsdict index 37f280de4..4eb068697 100644 --- a/Localization/StringsConvertor/input/fr.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/fr.lproj/Localizable.stringsdict @@ -53,7 +53,7 @@ a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ left + %#@character_count@ restants character_count NSStringFormatSpecTypeKey @@ -61,9 +61,9 @@ NSStringFormatValueTypeKey ld one - 1 character + 1 caractère other - %ld characters + %ld caractères plural.count.followed_by_and_mutual From 8b897ec6a59a64ac26977d3edc54a1242502d5cc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:29 +0100 Subject: [PATCH 455/658] New translations app.json (Portuguese) --- .../StringsConvertor/input/pt.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/pt.lproj/app.json b/Localization/StringsConvertor/input/pt.lproj/app.json index 25f06ad83..30566d8d6 100644 --- a/Localization/StringsConvertor/input/pt.lproj/app.json +++ b/Localization/StringsConvertor/input/pt.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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", @@ -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..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Label", "content": "Content" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 544a0a0ef9920b78b46b7f73a107a3dbde83baa5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:30 +0100 Subject: [PATCH 456/658] New translations app.json (Japanese) --- .../StringsConvertor/input/ja.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/ja.lproj/app.json b/Localization/StringsConvertor/input/ja.lproj/app.json index a18347581..d145ff494 100644 --- a/Localization/StringsConvertor/input/ja.lproj/app.json +++ b/Localization/StringsConvertor/input/ja.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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": "期間: %s", @@ -396,7 +398,9 @@ "one_day": "1日", "three_days": "3日", "seven_days": "7日", - "option_number": "オプション %ld" + "option_number": "オプション %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "ここに警告を書いてください..." @@ -444,6 +448,10 @@ "placeholder": { "label": "ラベル", "content": "コンテンツ" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From b4f41a0b682c64f6688c8ae2a2392dfcdd55fa07 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:31 +0100 Subject: [PATCH 457/658] New translations app.json (Chinese Traditional) --- .../input/zh-Hant.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json index 131bd3f37..d7d02240f 100644 --- a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json @@ -385,8 +385,10 @@ "description_video": "為視障人士提供影片說明...", "load_failed": "讀取失敗", "upload_failed": "上傳失敗", - "can_not_recognize_this_media_attachment": "無法識別此媒體附加檔案", - "attachment_too_large": "附加檔案大小過大" + "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "attachment_too_large": "附加檔案大小過大", + "compressing_state": "Compressing...", + "server_processing_state": "Server Processing..." }, "poll": { "duration_time": "持續時間:%s", @@ -396,7 +398,9 @@ "one_day": "一天", "three_days": "三天", "seven_days": "七天", - "option_number": "選項 %ld" + "option_number": "選項 %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "請於此處寫下精準的警告..." @@ -444,6 +448,10 @@ "placeholder": { "label": "標籤", "content": "內容" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "書籤" } } -} +} \ No newline at end of file From fc001af4c3baa8ab7045e073517478d79283ec9e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:32 +0100 Subject: [PATCH 458/658] New translations app.json (Ukrainian) --- .../StringsConvertor/input/uk.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/uk.lproj/app.json b/Localization/StringsConvertor/input/uk.lproj/app.json index 25f06ad83..30566d8d6 100644 --- a/Localization/StringsConvertor/input/uk.lproj/app.json +++ b/Localization/StringsConvertor/input/uk.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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", @@ -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..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Label", "content": "Content" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 2b2d419b398e87dbb59def2ab9e72648ece254db Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:33 +0100 Subject: [PATCH 459/658] New translations app.json (Vietnamese) --- .../StringsConvertor/input/vi.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/vi.lproj/app.json b/Localization/StringsConvertor/input/vi.lproj/app.json index eb1b1c52d..345ebe85d 100644 --- a/Localization/StringsConvertor/input/vi.lproj/app.json +++ b/Localization/StringsConvertor/input/vi.lproj/app.json @@ -385,8 +385,10 @@ "description_video": "Mô tả video cho người khiếm thị...", "load_failed": "Tải thất bại", "upload_failed": "Tải lên thất bại", - "can_not_recognize_this_media_attachment": "Không xem được tập tin đính kèm", - "attachment_too_large": "Tập tin đính kèm quá lớn" + "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "attachment_too_large": "Tập tin đính kèm quá lớn", + "compressing_state": "Compressing...", + "server_processing_state": "Server Processing..." }, "poll": { "duration_time": "Thời hạn: %s", @@ -396,7 +398,9 @@ "one_day": "1 ngày", "three_days": "3 ngày", "seven_days": "7 ngày", - "option_number": "Lựa chọn %ld" + "option_number": "Lựa chọn %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "Viết nội dung ẩn của bạn ở đây..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Nhãn", "content": "Nội dung" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Tút đã lưu" } } -} +} \ No newline at end of file From b6efb7cb3e3216429b2969c4e7e5b128de673be7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:34 +0100 Subject: [PATCH 460/658] New translations app.json (Kabyle) --- .../StringsConvertor/input/kab.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/kab.lproj/app.json b/Localization/StringsConvertor/input/kab.lproj/app.json index 194a2c681..713cc8959 100644 --- a/Localization/StringsConvertor/input/kab.lproj/app.json +++ b/Localization/StringsConvertor/input/kab.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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": "Tangazt: %s", @@ -396,7 +398,9 @@ "one_day": "1 n wass", "three_days": "3 n wussan", "seven_days": "7 n wussan", - "option_number": "Taxtiṛt %ld" + "option_number": "Taxtiṛt %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "Aru alɣu-inek s telqeyt da..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Tabzimt", "content": "Agbur" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 3b311f8a338e9149db109d4e5a90657cb6c546e2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:35 +0100 Subject: [PATCH 461/658] New translations app.json (Korean) --- .../StringsConvertor/input/ko.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/ko.lproj/app.json b/Localization/StringsConvertor/input/ko.lproj/app.json index fa40d14a7..2c4316f07 100644 --- a/Localization/StringsConvertor/input/ko.lproj/app.json +++ b/Localization/StringsConvertor/input/ko.lproj/app.json @@ -385,8 +385,10 @@ "description_video": "시각장애인을 위한 영상 설명…", "load_failed": "불러오기 실패", "upload_failed": "업로드 실패", - "can_not_recognize_this_media_attachment": "이 미디어 첨부파일을 인식할 수 없습니다", - "attachment_too_large": "첨부파일이 너무 큽니다" + "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "attachment_too_large": "첨부파일이 너무 큽니다", + "compressing_state": "Compressing...", + "server_processing_state": "Server Processing..." }, "poll": { "duration_time": "기간: %s", @@ -396,7 +398,9 @@ "one_day": "1일", "three_days": "3일", "seven_days": "7일", - "option_number": "옵션 %ld" + "option_number": "옵션 %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "정확한 경고 문구를 여기에 작성하세요…" @@ -444,6 +448,10 @@ "placeholder": { "label": "라벨", "content": "내용" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 11960613f2c32185fe88ec33088f20b51d2e3cb0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:36 +0100 Subject: [PATCH 462/658] New translations app.json (Swedish) --- .../StringsConvertor/input/sv.lproj/app.json | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/sv.lproj/app.json b/Localization/StringsConvertor/input/sv.lproj/app.json index 938134eaa..6375771ff 100644 --- a/Localization/StringsConvertor/input/sv.lproj/app.json +++ b/Localization/StringsConvertor/input/sv.lproj/app.json @@ -386,7 +386,9 @@ "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": "Bilagan är för stor" + "attachment_too_large": "Bilagan är för stor", + "compressing_state": "Komprimerar...", + "server_processing_state": "Behandlas av servern..." }, "poll": { "duration_time": "Längd: %s", @@ -396,7 +398,9 @@ "one_day": "1 dag", "three_days": "3 dagar", "seven_days": "7 dagar", - "option_number": "Alternativ %ld" + "option_number": "Alternativ %ld", + "the_poll_is_invalid": "Undersökningen är ogiltig", + "the_poll_has_empty_option": "Undersökningen har ett tomt alternativ" }, "content_warning": { "placeholder": "Skriv en noggrann varning här..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Etikett", "content": "Innehåll" + }, + "verified": { + "short": "Verifierad på %s", + "long": "Ägarskap för denna länk kontrollerades den %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bokmärken" } } -} +} \ No newline at end of file From 807ec62d4134213b70267c5d226ecb07a10e801f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:37 +0100 Subject: [PATCH 463/658] New translations app.json (French) --- .../StringsConvertor/input/fr.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/fr.lproj/app.json b/Localization/StringsConvertor/input/fr.lproj/app.json index 674d0f8ca..f35ce4483 100644 --- a/Localization/StringsConvertor/input/fr.lproj/app.json +++ b/Localization/StringsConvertor/input/fr.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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": "Durée: %s", @@ -396,7 +398,9 @@ "one_day": "1 Jour", "three_days": "3 jour", "seven_days": "7 jour", - "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": "Écrivez un avertissement précis ici..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Étiquette", "content": "Contenu" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Favoris" } } -} +} \ No newline at end of file From c7964c0e401eccbffb29f66c9acc6da0c29476d7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:39 +0100 Subject: [PATCH 464/658] New translations app.json (Turkish) --- .../StringsConvertor/input/tr.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/tr.lproj/app.json b/Localization/StringsConvertor/input/tr.lproj/app.json index f363e3ab6..27ebf6e0a 100644 --- a/Localization/StringsConvertor/input/tr.lproj/app.json +++ b/Localization/StringsConvertor/input/tr.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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": "Süre: %s", @@ -396,7 +398,9 @@ "one_day": "1 Gün", "three_days": "3 Gün", "seven_days": "7 Gün", - "option_number": "Seçenek %ld" + "option_number": "Seçenek %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "Buraya kesin bir uyarı yazın..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Etiket", "content": "İçerik" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 8b7ee10642b08f96f08016c0d7a62b1ca62c4007 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:40 +0100 Subject: [PATCH 465/658] New translations app.json (Czech) --- .../StringsConvertor/input/cs.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/cs.lproj/app.json b/Localization/StringsConvertor/input/cs.lproj/app.json index 02e7f5cec..5e742009e 100644 --- a/Localization/StringsConvertor/input/cs.lproj/app.json +++ b/Localization/StringsConvertor/input/cs.lproj/app.json @@ -385,8 +385,10 @@ "description_video": "Popište video pro zrakově postižené...", "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á" + "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "attachment_too_large": "Příloha je příliš velká", + "compressing_state": "Compressing...", + "server_processing_state": "Server Processing..." }, "poll": { "duration_time": "Doba trvání: %s", @@ -396,7 +398,9 @@ "one_day": "1 den", "three_days": "3 dny", "seven_days": "7 dní", - "option_number": "Možnost %ld" + "option_number": "Možnost %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "Zde napište přesné varování..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Označení", "content": "Obsah" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Záložky" } } -} +} \ No newline at end of file From f15baea19e567dc6ed652a037e92385c8fb66734 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:41 +0100 Subject: [PATCH 466/658] New translations app.json (Scottish Gaelic) --- .../StringsConvertor/input/gd.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/gd.lproj/app.json b/Localization/StringsConvertor/input/gd.lproj/app.json index 214279887..6a575afc2 100644 --- a/Localization/StringsConvertor/input/gd.lproj/app.json +++ b/Localization/StringsConvertor/input/gd.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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": "Faide: %s", @@ -396,7 +398,9 @@ "one_day": "Latha", "three_days": "3 làithean", "seven_days": "Seachdain", - "option_number": "Roghainn %ld" + "option_number": "Roghainn %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "Sgrìobh rabhadh pongail an-seo…" @@ -444,6 +448,10 @@ "placeholder": { "label": "Leubail", "content": "Susbaint" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From d2843d8a1c30ec135f7f35f5b6ac0ac9bf60fcfd Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:42 +0100 Subject: [PATCH 467/658] New translations app.json (Italian) --- .../StringsConvertor/input/it.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json index f5e8d9293..108de5506 100644 --- a/Localization/StringsConvertor/input/it.lproj/app.json +++ b/Localization/StringsConvertor/input/it.lproj/app.json @@ -385,8 +385,10 @@ "description_video": "Descrivi il filmato per gli utenti ipovedenti...", "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" + "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "attachment_too_large": "Allegato troppo grande", + "compressing_state": "Compressing...", + "server_processing_state": "Server Processing..." }, "poll": { "duration_time": "Durata: %s", @@ -396,7 +398,9 @@ "one_day": "1 giorno", "three_days": "3 giorni", "seven_days": "7 giorni", - "option_number": "Opzione %ld" + "option_number": "Opzione %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "Scrivi un avviso accurato qui..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Etichetta", "content": "Contenuto" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Segnalibri" } } -} +} \ No newline at end of file From 3f077bc77825680d7941633319f19b9953336c83 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:43 +0100 Subject: [PATCH 468/658] New translations app.json (Romanian) --- .../StringsConvertor/input/ro.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/ro.lproj/app.json b/Localization/StringsConvertor/input/ro.lproj/app.json index e50c741b6..18fcb023b 100644 --- a/Localization/StringsConvertor/input/ro.lproj/app.json +++ b/Localization/StringsConvertor/input/ro.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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", @@ -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..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Label", "content": "Content" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 86a08221b86b5a65751432cbe5850647e661717c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:44 +0100 Subject: [PATCH 469/658] New translations app.json (Spanish) --- .../StringsConvertor/input/es.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/es.lproj/app.json b/Localization/StringsConvertor/input/es.lproj/app.json index 5398df49f..a7500e27f 100644 --- a/Localization/StringsConvertor/input/es.lproj/app.json +++ b/Localization/StringsConvertor/input/es.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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": "Duración: %s", @@ -396,7 +398,9 @@ "one_day": "1 Día", "three_days": "4 Días", "seven_days": "7 Días", - "option_number": "Opción %ld" + "option_number": "Opción %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "Escribe una advertencia precisa aquí..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Nombre para el campo", "content": "Contenido" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 4bd7bde994f179a39eec2f8be721b9fa51fd61e1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:45 +0100 Subject: [PATCH 470/658] New translations app.json (Arabic) --- .../StringsConvertor/input/ar.lproj/app.json | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/ar.lproj/app.json b/Localization/StringsConvertor/input/ar.lproj/app.json index ebea98b0c..c28962a0f 100644 --- a/Localization/StringsConvertor/input/ar.lproj/app.json +++ b/Localization/StringsConvertor/input/ar.lproj/app.json @@ -383,10 +383,12 @@ "attachment_broken": "هذا ال%s مُعطَّل\nويتعذَّرُ رفعُه إلى ماستودون.", "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": "Can not recognize this media attachment", + "attachment_too_large": "المُرفَق كَبيرٌ جِدًّا", + "compressing_state": "يجري الضغط...", + "server_processing_state": "مُعالجة الخادم جارِيَة..." }, "poll": { "duration_time": "المُدَّة: %s", @@ -396,7 +398,9 @@ "one_day": "يومٌ واحِد", "three_days": "ثلاثةُ أيام", "seven_days": "سبعةُ أيام", - "option_number": "الخيار %ld" + "option_number": "الخيار %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "اكتب تَحذيرًا دَقيقًا هُنا..." @@ -419,7 +423,7 @@ "disable_content_warning": "تعطيل تحذير المُحتَوى", "post_visibility_menu": "قائمة ظهور المنشور", "post_options": "Post Options", - "posting_as": "Posting as %s" + "posting_as": "نَشر كَـ %s" }, "keyboard": { "discard_post": "تجاهُل المنشور", @@ -444,6 +448,10 @@ "placeholder": { "label": "التسمية", "content": "المُحتَوى" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "العَلاماتُ المَرجعيَّة" } } -} +} \ No newline at end of file From 60a082577315d2f3b7fc4d0323b9ff47d33bd11d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:46 +0100 Subject: [PATCH 471/658] New translations app.json (Catalan) --- .../StringsConvertor/input/ca.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/ca.lproj/app.json b/Localization/StringsConvertor/input/ca.lproj/app.json index e75aa9ff8..35dd58746 100644 --- a/Localization/StringsConvertor/input/ca.lproj/app.json +++ b/Localization/StringsConvertor/input/ca.lproj/app.json @@ -385,8 +385,10 @@ "description_video": "Descriu el vídeo per als disminuïts visuals...", "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" + "can_not_recognize_this_media_attachment": "No es pot reconèixer aquest adjunt multimèdia", + "attachment_too_large": "El fitxer adjunt és massa gran", + "compressing_state": "Comprimint...", + "server_processing_state": "Servidor processant..." }, "poll": { "duration_time": "Durada: %s", @@ -396,7 +398,9 @@ "one_day": "1 Dia", "three_days": "3 Dies", "seven_days": "7 Dies", - "option_number": "Opció %ld" + "option_number": "Opció %ld", + "the_poll_is_invalid": "L'enquesta no és vàlida", + "the_poll_has_empty_option": "L'enquesta té una opció buida" }, "content_warning": { "placeholder": "Escriu un advertiment precís aquí..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Etiqueta", "content": "Contingut" + }, + "verified": { + "short": "Verificat a %s", + "long": "La propietat d'aquest enllaç es va verificar el dia %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Marcadors" } } -} +} \ No newline at end of file From 50b4919a994b9c6bc3988b6bd5ebe11818e436b9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:47 +0100 Subject: [PATCH 472/658] New translations app.json (Danish) --- .../StringsConvertor/input/da.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/da.lproj/app.json b/Localization/StringsConvertor/input/da.lproj/app.json index 25f06ad83..30566d8d6 100644 --- a/Localization/StringsConvertor/input/da.lproj/app.json +++ b/Localization/StringsConvertor/input/da.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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", @@ -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..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Label", "content": "Content" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 50f971ae5ac44677f238be20162511886baea9bf Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:48 +0100 Subject: [PATCH 473/658] New translations app.json (German) --- .../StringsConvertor/input/de.lproj/app.json | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/de.lproj/app.json b/Localization/StringsConvertor/input/de.lproj/app.json index ce5dc4d3a..2fdfb494e 100644 --- a/Localization/StringsConvertor/input/de.lproj/app.json +++ b/Localization/StringsConvertor/input/de.lproj/app.json @@ -386,7 +386,9 @@ "load_failed": "Laden fehlgeschlagen", "upload_failed": "Upload fehlgeschlagen", "can_not_recognize_this_media_attachment": "Medienanhang wurde nicht erkannt", - "attachment_too_large": "Anhang zu groß" + "attachment_too_large": "Anhang zu groß", + "compressing_state": "Komprimieren...", + "server_processing_state": "Serververarbeitung..." }, "poll": { "duration_time": "Dauer: %s", @@ -396,7 +398,9 @@ "one_day": "1 Tag", "three_days": "3 Tage", "seven_days": "7 Tage", - "option_number": "Auswahlmöglichkeit %ld" + "option_number": "Auswahlmöglichkeit %ld", + "the_poll_is_invalid": "Die Umfrage ist ungültig", + "the_poll_has_empty_option": "Die Umfrage hat eine leere Option" }, "content_warning": { "placeholder": "Schreibe eine Inhaltswarnung hier..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Bezeichnung", "content": "Inhalt" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Lesezeichen" } } -} +} \ No newline at end of file From 8c0619d54d92d60fb5ef4c01cd2cc50f1696ec87 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:49 +0100 Subject: [PATCH 474/658] New translations app.json (Basque) --- .../StringsConvertor/input/eu.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/eu.lproj/app.json b/Localization/StringsConvertor/input/eu.lproj/app.json index c4cecfe59..9f5e925c6 100644 --- a/Localization/StringsConvertor/input/eu.lproj/app.json +++ b/Localization/StringsConvertor/input/eu.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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": "Iraupena: %s", @@ -396,7 +398,9 @@ "one_day": "Egun 1", "three_days": "3 egun", "seven_days": "7 egun", - "option_number": "%ld aukera" + "option_number": "%ld aukera", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "Idatzi abisu zehatz bat hemen..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Etiketa", "content": "Edukia" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From 3c1679239c43c022342ecd784f5dfa392e1bbcf6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:50 +0100 Subject: [PATCH 475/658] New translations app.json (Finnish) --- .../StringsConvertor/input/fi.lproj/app.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/fi.lproj/app.json b/Localization/StringsConvertor/input/fi.lproj/app.json index b98167c35..febfee2ca 100644 --- a/Localization/StringsConvertor/input/fi.lproj/app.json +++ b/Localization/StringsConvertor/input/fi.lproj/app.json @@ -385,8 +385,10 @@ "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" + "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": "Kesto: %s", @@ -396,7 +398,9 @@ "one_day": "1 päivä", "three_days": "3 päivää", "seven_days": "7 päivää", - "option_number": "Vaihtoehto %ld" + "option_number": "Vaihtoehto %ld", + "the_poll_is_invalid": "The poll is invalid", + "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { "placeholder": "Kirjoita tarkka varoitus tähän..." @@ -444,6 +448,10 @@ "placeholder": { "label": "Nimi", "content": "Sisältö" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" } }, "segmented_control": { @@ -711,4 +719,4 @@ "title": "Bookmarks" } } -} +} \ No newline at end of file From e2c120a72f65bdd85ce47ee80e0d86a05c23dc57 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 20:02:51 +0100 Subject: [PATCH 476/658] New translations Localizable.stringsdict (Arabic) --- .../input/ar.lproj/Localizable.stringsdict | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict index a65082272..91368a4fb 100644 --- a/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict @@ -85,17 +85,17 @@ NSStringFormatValueTypeKey ld zero - %ld characters + لَا حَرف one - 1 character + حَرفٌ واحِد two - %ld characters + حَرفانِ اِثنان few %ld characters many %ld characters other - %ld characters + %ld حَرف plural.count.followed_by_and_mutual From 6c97a748040258b99678d4c32bbbfe0205226b57 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 14 Nov 2022 14:08:38 -0500 Subject: [PATCH 477/658] ./update_localization.sh --- .../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 From eaa1358a229647ee89cc6772dc4afa23f5e7ec5f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 21:11:37 +0100 Subject: [PATCH 478/658] New translations app.json (Italian) --- .../StringsConvertor/input/it.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json index 108de5506..c0d33a73b 100644 --- a/Localization/StringsConvertor/input/it.lproj/app.json +++ b/Localization/StringsConvertor/input/it.lproj/app.json @@ -385,10 +385,10 @@ "description_video": "Descrivi il filmato per gli utenti ipovedenti...", "load_failed": "Caricamento fallito", "upload_failed": "Caricamento fallito", - "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "can_not_recognize_this_media_attachment": "Impossibile riconoscere questo allegato multimediale", "attachment_too_large": "Allegato troppo grande", - "compressing_state": "Compressing...", - "server_processing_state": "Server Processing..." + "compressing_state": "Compressione in corso...", + "server_processing_state": "Elaborazione del server in corso..." }, "poll": { "duration_time": "Durata: %s", @@ -399,8 +399,8 @@ "three_days": "3 giorni", "seven_days": "7 giorni", "option_number": "Opzione %ld", - "the_poll_is_invalid": "The poll is invalid", - "the_poll_has_empty_option": "The poll has empty option" + "the_poll_is_invalid": "Il sondaggio non è valido", + "the_poll_has_empty_option": "Il sondaggio ha un'opzione vuota" }, "content_warning": { "placeholder": "Scrivi un avviso accurato qui..." @@ -450,8 +450,8 @@ "content": "Contenuto" }, "verified": { - "short": "Verified on %s", - "long": "Ownership of this link was checked on %s" + "short": "Verificato il %s", + "long": "La proprietà di questo collegamento è stata verificata il %s" } }, "segmented_control": { From 945983c326686891b5330653316e5d22586e99f6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 22:27:48 +0100 Subject: [PATCH 479/658] 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 5e742009e..d6624a26b 100644 --- a/Localization/StringsConvertor/input/cs.lproj/app.json +++ b/Localization/StringsConvertor/input/cs.lproj/app.json @@ -385,10 +385,10 @@ "description_video": "Popište video pro zrakově postižené...", "load_failed": "Načtení se nezdařilo", "upload_failed": "Nahrání selhalo", - "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "can_not_recognize_this_media_attachment": "Nelze rozpoznat toto medium přílohy", "attachment_too_large": "Příloha je příliš velká", - "compressing_state": "Compressing...", - "server_processing_state": "Server Processing..." + "compressing_state": "Probíhá komprese...", + "server_processing_state": "Zpracování serveru..." }, "poll": { "duration_time": "Doba trvání: %s", @@ -399,8 +399,8 @@ "three_days": "3 dny", "seven_days": "7 dní", "option_number": "Možnost %ld", - "the_poll_is_invalid": "The poll is invalid", - "the_poll_has_empty_option": "The poll has empty option" + "the_poll_is_invalid": "Anketa je neplatná", + "the_poll_has_empty_option": "Anketa má prázdnou možnost" }, "content_warning": { "placeholder": "Zde napište přesné varování..." @@ -450,8 +450,8 @@ "content": "Obsah" }, "verified": { - "short": "Verified on %s", - "long": "Ownership of this link was checked on %s" + "short": "Ověřeno na %s", + "long": "Vlastnictví tohoto odkazu bylo zkontrolováno na %s" } }, "segmented_control": { From 009d4e6aa9a495fbac40b6647c04a7835a7c6e63 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 22:27:49 +0100 Subject: [PATCH 480/658] 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 2fdfb494e..54bb81973 100644 --- a/Localization/StringsConvertor/input/de.lproj/app.json +++ b/Localization/StringsConvertor/input/de.lproj/app.json @@ -451,7 +451,7 @@ }, "verified": { "short": "Verified on %s", - "long": "Ownership of this link was checked on %s" + "long": "Besitz des Links wurde überprüft am %s" } }, "segmented_control": { From 303237743bcb582a5f551e1c804e5709d4eaea57 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 22:27:50 +0100 Subject: [PATCH 481/658] New translations Localizable.stringsdict (Czech) --- .../input/cs.lproj/Localizable.stringsdict | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict index 805ac70f2..6e44e9f0a 100644 --- a/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict @@ -73,13 +73,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 @@ -308,13 +308,13 @@ NSStringFormatValueTypeKey ld one - 1 following + 1 sledující few - %ld following + %ld sledující many - %ld following + %ld sledujících other - %ld following + %ld sledujících plural.count.follower @@ -348,13 +348,13 @@ NSStringFormatValueTypeKey ld one - 1 year left + Zbývá 1 rok few - %ld years left + Zbývají %ld roky many - %ld years left + Zbývá %ld roků other - %ld years left + Zbývá %ld roků date.month.left @@ -368,7 +368,7 @@ NSStringFormatValueTypeKey ld one - 1 months left + Zbývá 1 měsíc few %ld months left many From 7b3c7bbd7b4b5a9e1a377a7d2d46e05f5222ed53 Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Mon, 14 Nov 2022 14:09:59 -0800 Subject: [PATCH 482/658] Fix refresh control positioning --- Mastodon.xcodeproj/project.pbxproj | 4 +++ .../DiscoveryCommunityViewController.swift | 4 +-- .../DiscoveryForYouViewController.swift | 4 +-- .../DiscoveryHashtagsViewController.swift | 4 +-- .../News/DiscoveryNewsViewController.swift | 4 +-- .../Posts/DiscoveryPostsViewController.swift | 4 +-- .../HashtagTimelineViewController.swift | 4 +-- .../HomeTimelineViewController.swift | 4 +-- .../NotificationTimelineViewController.swift | 6 ++-- .../Scene/Profile/ProfileViewController.swift | 6 ++-- .../Share/View/Control/RefreshControl.swift | 28 +++++++++++++++++++ 11 files changed, 52 insertions(+), 20 deletions(-) create mode 100644 Mastodon/Scene/Share/View/Control/RefreshControl.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 380f21eac..8338c2225 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -87,6 +87,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 */; }; + C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24C97022922F30500BAE8CB /* RefreshControl.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 */; }; @@ -610,6 +611,7 @@ B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.release.xcconfig"; sourceTree = ""; }; 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 = ""; }; + C24C97022922F30500BAE8CB /* RefreshControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshControl.swift; 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 = ""; }; 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 = ""; }; @@ -2452,6 +2454,7 @@ isa = PBXGroup; children = ( DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */, + C24C97022922F30500BAE8CB /* RefreshControl.swift */, ); path = Control; sourceTree = ""; @@ -3520,6 +3523,7 @@ 5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */, DBEFCD82282A2AB100C0ABEA /* ReportServerRulesView.swift in Sources */, DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */, + C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */, DB023D2A27A0FE5C005AC798 /* DataSourceProvider+NotificationTableViewCellDelegate.swift in Sources */, DB98EB6027B10E150082E365 /* ReportCommentTableViewCell.swift in Sources */, DB0FCB962797E6C2006C02E2 /* SearchResultViewController+DataSourceProvider.swift in Sources */, diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController.swift index d592c3033..31635dc85 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController.swift @@ -33,7 +33,7 @@ final class DiscoveryCommunityViewController: UIViewController, NeedsDependency, return tableView }() - let refreshControl = UIRefreshControl() + let refreshControl = RefreshControl() deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -108,7 +108,7 @@ extension DiscoveryCommunityViewController { extension DiscoveryCommunityViewController { - @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { + @objc private func refreshControlValueChanged(_ sender: RefreshControl) { if !viewModel.stateMachine.enter(DiscoveryCommunityViewModel.State.Reloading.self) { refreshControl.endRefreshing() } diff --git a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift index ce1aadbb4..52e5e1856 100644 --- a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift +++ b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift @@ -32,7 +32,7 @@ final class DiscoveryForYouViewController: UIViewController, NeedsDependency, Me return tableView }() - let refreshControl = UIRefreshControl() + let refreshControl = RefreshControl() deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -93,7 +93,7 @@ extension DiscoveryForYouViewController { extension DiscoveryForYouViewController { - @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { + @objc private func refreshControlValueChanged(_ sender: RefreshControl) { Task { try await viewModel.fetch() } diff --git a/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewController.swift b/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewController.swift index e315f04ee..1c855c254 100644 --- a/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewController.swift +++ b/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewController.swift @@ -32,7 +32,7 @@ final class DiscoveryHashtagsViewController: UIViewController, NeedsDependency, return tableView }() - let refreshControl = UIRefreshControl() + let refreshControl = RefreshControl() deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -88,7 +88,7 @@ extension DiscoveryHashtagsViewController { extension DiscoveryHashtagsViewController { - @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { + @objc private func refreshControlValueChanged(_ sender: RefreshControl) { Task { @MainActor in do { try await viewModel.fetch() diff --git a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewController.swift b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewController.swift index 7f9efb0d9..d1634db82 100644 --- a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewController.swift +++ b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewController.swift @@ -32,7 +32,7 @@ final class DiscoveryNewsViewController: UIViewController, NeedsDependency, Medi return tableView }() - let refreshControl = UIRefreshControl() + let refreshControl = RefreshControl() deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -101,7 +101,7 @@ extension DiscoveryNewsViewController { extension DiscoveryNewsViewController { - @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { + @objc private func refreshControlValueChanged(_ sender: RefreshControl) { guard viewModel.stateMachine.enter(DiscoveryNewsViewModel.State.Reloading.self) else { sender.endRefreshing() return diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift index ee3538885..893f564a3 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift @@ -32,7 +32,7 @@ final class DiscoveryPostsViewController: UIViewController, NeedsDependency, Med return tableView }() - let refreshControl = UIRefreshControl() + let refreshControl = RefreshControl() let discoveryIntroBannerView = DiscoveryIntroBannerView() @@ -119,7 +119,7 @@ extension DiscoveryPostsViewController { extension DiscoveryPostsViewController { - @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { + @objc private func refreshControlValueChanged(_ sender: RefreshControl) { guard viewModel.stateMachine.enter(DiscoveryPostsViewModel.State.Reloading.self) else { sender.endRefreshing() return diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift index 42079a91e..aad97283c 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift @@ -48,7 +48,7 @@ final class HashtagTimelineViewController: UIViewController, NeedsDependency, Me return tableView }() - let refreshControl = UIRefreshControl() + let refreshControl = RefreshControl() deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function) @@ -158,7 +158,7 @@ extension HashtagTimelineViewController { extension HashtagTimelineViewController { - @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { + @objc private func refreshControlValueChanged(_ sender: RefreshControl) { guard viewModel.stateMachine.enter(HashtagTimelineViewModel.State.Reloading.self) else { sender.endRefreshing() return diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 3efcd5cbe..e83101796 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -74,7 +74,7 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media return progressView }() - let refreshControl = UIRefreshControl() + let refreshControl = RefreshControl() deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function) @@ -398,7 +398,7 @@ extension HomeTimelineViewController { coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil)) } - @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { + @objc private func refreshControlValueChanged(_ sender: RefreshControl) { guard viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self) else { sender.endRefreshing() return diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index 00e2a9fc8..d088bb783 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -26,8 +26,8 @@ final class NotificationTimelineViewController: UIViewController, NeedsDependenc var viewModel: NotificationTimelineViewModel! - private(set) lazy var refreshControl: UIRefreshControl = { - let refreshControl = UIRefreshControl() + private(set) lazy var refreshControl: RefreshControl = { + let refreshControl = RefreshControl() refreshControl.addTarget(self, action: #selector(NotificationTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged) return refreshControl }() @@ -137,7 +137,7 @@ extension NotificationTimelineViewController: CellFrameCacheContainer { extension NotificationTimelineViewController { - @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { + @objc private func refreshControlValueChanged(_ sender: RefreshControl) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") Task { diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 3ce1fd33a..99f3042aa 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -99,8 +99,8 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi return barButtonItem }() - let refreshControl: UIRefreshControl = { - let refreshControl = UIRefreshControl() + let refreshControl: RefreshControl = { + let refreshControl = RefreshControl() refreshControl.tintColor = .white return refreshControl }() @@ -545,7 +545,7 @@ extension ProfileViewController { _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) } - @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { + @objc private func refreshControlValueChanged(_ sender: RefreshControl) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) if let userTimelineViewController = profilePagingViewController.currentViewController as? UserTimelineViewController { diff --git a/Mastodon/Scene/Share/View/Control/RefreshControl.swift b/Mastodon/Scene/Share/View/Control/RefreshControl.swift new file mode 100644 index 000000000..afac5a1a1 --- /dev/null +++ b/Mastodon/Scene/Share/View/Control/RefreshControl.swift @@ -0,0 +1,28 @@ +// +// RefreshControl.swift +// Mastodon +// +// Created by Kyle Bashour on 11/14/22. +// + +import UIKit + +/// RefreshControl subclass that properly displays itself behind table view contents. +class RefreshControl: UIRefreshControl { + override init() { + super.init() + } + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override func didMoveToSuperview() { + super.didMoveToSuperview() + layer.zPosition = -1 + } +} From 9e912be7c45a59e477dfa899364e6b99a6614ce8 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 14 Nov 2022 23:19:53 +0100 Subject: [PATCH 483/658] 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 From 8775227aaf1d87cf86306910329005328c04c92d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Nov 2022 23:36:39 +0100 Subject: [PATCH 484/658] New translations app.json (Slovenian) --- .../StringsConvertor/input/sl.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/sl.lproj/app.json b/Localization/StringsConvertor/input/sl.lproj/app.json index 7cb750c38..5f79ef02a 100644 --- a/Localization/StringsConvertor/input/sl.lproj/app.json +++ b/Localization/StringsConvertor/input/sl.lproj/app.json @@ -385,10 +385,10 @@ "description_video": "Opiši video za slabovidne in osebe z okvaro vida ...", "load_failed": "Nalaganje ni uspelo", "upload_failed": "Nalaganje na strežnik ni uspelo", - "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "can_not_recognize_this_media_attachment": "Te medijske priponke ni mogoče prepoznati", "attachment_too_large": "Priponka je prevelika", - "compressing_state": "Compressing...", - "server_processing_state": "Server Processing..." + "compressing_state": "Stiskanje ...", + "server_processing_state": "Obdelovanje na strežniku ..." }, "poll": { "duration_time": "Trajanje: %s", @@ -399,8 +399,8 @@ "three_days": "3 dni", "seven_days": "7 dni", "option_number": "Možnost %ld", - "the_poll_is_invalid": "The poll is invalid", - "the_poll_has_empty_option": "The poll has empty option" + "the_poll_is_invalid": "Anketa je neveljavna", + "the_poll_has_empty_option": "Anketa ima prazno izbiro" }, "content_warning": { "placeholder": "Tukaj zapišite opozorilo ..." @@ -450,8 +450,8 @@ "content": "Vsebina" }, "verified": { - "short": "Verified on %s", - "long": "Ownership of this link was checked on %s" + "short": "Preverjeno %s", + "long": "Lastništvo te povezave je bilo preverjeno %s" } }, "segmented_control": { From 24e9e1b536e37a9640b0510fbcd9696430c50a0f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 15 Nov 2022 03:10:47 +0100 Subject: [PATCH 485/658] New translations app.json (Vietnamese) --- .../StringsConvertor/input/vi.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/vi.lproj/app.json b/Localization/StringsConvertor/input/vi.lproj/app.json index 345ebe85d..ff751d964 100644 --- a/Localization/StringsConvertor/input/vi.lproj/app.json +++ b/Localization/StringsConvertor/input/vi.lproj/app.json @@ -385,10 +385,10 @@ "description_video": "Mô tả video cho người khiếm thị...", "load_failed": "Tải thất bại", "upload_failed": "Tải lên thất bại", - "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "can_not_recognize_this_media_attachment": "Không xem được tập tin đính kèm", "attachment_too_large": "Tập tin đính kèm quá lớn", - "compressing_state": "Compressing...", - "server_processing_state": "Server Processing..." + "compressing_state": "Đang nén...", + "server_processing_state": "Máy chủ đang xử lý..." }, "poll": { "duration_time": "Thời hạn: %s", @@ -399,8 +399,8 @@ "three_days": "3 ngày", "seven_days": "7 ngày", "option_number": "Lựa chọn %ld", - "the_poll_is_invalid": "The poll is invalid", - "the_poll_has_empty_option": "The poll has empty option" + "the_poll_is_invalid": "Bình chọn không hợp lệ", + "the_poll_has_empty_option": "Thiếu lựa chọn" }, "content_warning": { "placeholder": "Viết nội dung ẩn của bạn ở đây..." @@ -450,8 +450,8 @@ "content": "Nội dung" }, "verified": { - "short": "Verified on %s", - "long": "Ownership of this link was checked on %s" + "short": "Đã xác minh %s", + "long": "Liên kết này đã được xác minh trên %s" } }, "segmented_control": { From ed312b2f09a5896448cc0c2b519f77ff885186c6 Mon Sep 17 00:00:00 2001 From: Robert Martin Date: Mon, 14 Nov 2022 22:05:16 -0600 Subject: [PATCH 486/658] Update README.md (#589) Fix minor typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2a19e715..f28caf8bd 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Read this blog post for this app to learn more. ## Acknowledgments -Thanks to these open-sources projects listed [here](./Documentation/Acknowledgments.md). +Thanks to these open-source projects listed [here](./Documentation/Acknowledgments.md). ## License From d99636ded82f7a6026d68dbd337f488e2e4cfd31 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 15 Nov 2022 06:16:39 +0100 Subject: [PATCH 487/658] New translations app.json (Portuguese, Brazilian) --- .../input/pt-BR.lproj/app.json | 218 +++++++++--------- 1 file changed, 109 insertions(+), 109 deletions(-) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/app.json b/Localization/StringsConvertor/input/pt-BR.lproj/app.json index 9d20f1151..d58006a22 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/app.json +++ b/Localization/StringsConvertor/input/pt-BR.lproj/app.json @@ -343,12 +343,12 @@ "open_email_app": { "title": "Verifique sua caixa de entrada.", "description": "Enviamos um e-mail para você. Verifique sua pasta de spam caso ainda tenha verificado.", - "mail": "Mail", - "open_email_client": "Open Email Client" + "mail": "Correio", + "open_email_client": "Abrir Cliente de Email" } }, "home_timeline": { - "title": "Home", + "title": "Início", "navigation_bar_state": { "offline": "Desconectado", "new_posts": "Ver novas postagens", @@ -356,13 +356,13 @@ "Publishing": "Publicando toot...", "accessibility": { "logo_label": "Botão do logotipo", - "logo_hint": "Tap to scroll to top and tap again to previous location" + "logo_hint": "Toque para rolar para o topo e toque novamente para a localização anterior" } } }, "suggestion_account": { - "title": "Find People to Follow", - "follow_explain": "When you follow someone, you’ll see their posts in your home feed." + "title": "Encontre pessoas para seguir", + "follow_explain": "Ao seguir alguém, você verá as publicações dessa pessoa na sua página inicial." }, "compose": { "title": { @@ -380,15 +380,15 @@ "attachment": { "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...", - "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..." + "attachment_broken": "Este %s está quebrado e não pode ser\nenviado para o Mastodon.", + "description_photo": "Descreva a foto para deficientes visuais...", + "description_video": "Descreva o vídeo para os deficientes visuais...", + "load_failed": "Falha ao carregar", + "upload_failed": "Falha no carregamento", + "can_not_recognize_this_media_attachment": "Não é possível reconhecer este anexo de mídia", + "attachment_too_large": "O anexo é muito grande", + "compressing_state": "Compactando...", + "server_processing_state": "Servidor processando..." }, "poll": { "duration_time": "Duração: %s", @@ -399,44 +399,44 @@ "three_days": "3 dias", "seven_days": "7 dias", "option_number": "Opção %ld", - "the_poll_is_invalid": "The poll is invalid", - "the_poll_has_empty_option": "The poll has empty option" + "the_poll_is_invalid": "A enquete é inválida", + "the_poll_has_empty_option": "A enquete tem uma opção vazia" }, "content_warning": { - "placeholder": "Write an accurate warning here..." + "placeholder": "Escreva um aviso de conteúdo preciso aqui..." }, "visibility": { - "public": "Public", - "unlisted": "Unlisted", - "private": "Followers only", - "direct": "Only people I mention" + "public": "Público", + "unlisted": "Não listado", + "private": "Apenas seguidores", + "direct": "Apenas pessoas que menciono" }, "auto_complete": { - "space_to_add": "Space to add" + "space_to_add": "Espaço a adicionar" }, "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", + "append_attachment": "Adicionar anexo", + "append_poll": "Adicionar enquete", + "remove_poll": "Remover enquete", + "custom_emoji_picker": "Seletor de emoji personalizado", + "enable_content_warning": "Ativar Aviso de Conteúdo", + "disable_content_warning": "Desativar Aviso de Conteúdo", + "post_visibility_menu": "Menu de Visibilidade do Post", "post_options": "Opções de postagem", "posting_as": "Publicando como %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" + "discard_post": "Descartar postagem", + "publish_post": "Publicar postagem", + "toggle_poll": "Alternar enquete", + "toggle_content_warning": "Ativar/desativar Aviso de Conteúdo", + "append_attachment_entry": "Adicionar Anexo - %s", + "select_visibility_entry": "Selecionar Visibilidade - %s" } }, "profile": { "header": { - "follows_you": "Follows You" + "follows_you": "Segue você" }, "dashboard": { "posts": "toots", @@ -444,14 +444,14 @@ "followers": "seguidores" }, "fields": { - "add_row": "Add Row", + "add_row": "Adicionar linha", "placeholder": { - "label": "Label", + "label": "Descrição", "content": "Conteúdo" }, "verified": { - "short": "Verified on %s", - "long": "Ownership of this link was checked on %s" + "short": "Verificado em %s", + "long": "O link foi verificado em %s" } }, "segmented_control": { @@ -471,63 +471,63 @@ "message": "Confirm to unmute %s" }, "confirm_block_user": { - "title": "Block Account", - "message": "Confirm to block %s" + "title": "Bloquear conta", + "message": "Confirme para bloquear %s" }, "confirm_unblock_user": { - "title": "Unblock Account", - "message": "Confirm to unblock %s" + "title": "Desbloquear conta", + "message": "Confirme para desbloquear %s" }, "confirm_show_reblogs": { - "title": "Show Reblogs", - "message": "Confirm to show reblogs" + "title": "Mostrar reblogs", + "message": "Confirmar para mostrar reblogs" }, "confirm_hide_reblogs": { - "title": "Hide Reblogs", - "message": "Confirm to hide reblogs" + "title": "Ocultar reblogs", + "message": "Confirmar para ocultar 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" + "show_avatar_image": "Mostrar foto de perfil", + "edit_avatar_image": "Editar foto de perfil", + "show_banner_image": "Mostrar foto de capa", + "double_tap_to_open_the_list": "Toque duas vezes para abrir a lista" } }, "follower": { - "title": "follower", - "footer": "Followers from other servers are not displayed." + "title": "seguidor", + "footer": "Seguidores de outras instâncias não são exibidos." }, "following": { - "title": "following", + "title": "seguindo", "footer": "Follows from other servers are not displayed." }, "familiarFollowers": { - "title": "Followers you familiar", - "followed_by_names": "Followed by %s" + "title": "Seguidores que você conhece", + "followed_by_names": "Seguido por %s" }, "favorited_by": { - "title": "Favorited By" + "title": "Favoritado por" }, "reblogged_by": { - "title": "Reblogged By" + "title": "Reblogado por" }, "search": { - "title": "Search", + "title": "Buscar", "search_bar": { - "placeholder": "Search hashtags and users", - "cancel": "Cancel" + "placeholder": "Buscar hashtags e usuários", + "cancel": "Cancelar" }, "recommend": { - "button_text": "See All", + "button_text": "Ver tudo", "hash_tag": { - "title": "Trending on Mastodon", - "description": "Hashtags that are getting quite a bit of attention", - "people_talking": "%s people are talking" + "title": "Em tendência no Mastodon", + "description": "Hashtags que estão recebendo bastante atenção", + "people_talking": "%s pessoas estão falando" }, "accounts": { - "title": "Accounts you might like", - "description": "You may like to follow these accounts", + "title": "Contas que você deve gostar", + "description": "Você pode gostar de seguir estas contas", "follow": "Seguir" } }, @@ -553,82 +553,82 @@ "community": "Comunidade", "for_you": "Para você" }, - "intro": "These are the posts gaining traction in your corner of Mastodon." + "intro": "Esses são os posts que estão ganhando força no seu canto do Mastodon." }, "favorite": { - "title": "Your Favorites" + "title": "Seus favoritos" }, "notification": { "title": { - "Everything": "Everything", - "Mentions": "Mentions" + "Everything": "Tudo", + "Mentions": "Menções" }, "notification_description": { - "followed_you": "followed you", - "favorited_your_post": "favorited your post", - "reblogged_your_post": "reblogged your post", - "mentioned_you": "mentioned you", + "followed_you": "seguiu você", + "favorited_your_post": "favoritou seu toot", + "reblogged_your_post": "reblogou seu toot", + "mentioned_you": "te mencionou", "request_to_follow_you": "request to follow you", - "poll_has_ended": "poll has ended" + "poll_has_ended": "enquete encerrada" }, "keyobard": { - "show_everything": "Show Everything", - "show_mentions": "Show Mentions" + "show_everything": "Mostrar tudo", + "show_mentions": "Mostrar menções" }, "follow_request": { - "accept": "Accept", - "accepted": "Accepted", - "reject": "reject", - "rejected": "Rejected" + "accept": "Aceitar", + "accepted": "Aceito", + "reject": "rejeitar", + "rejected": "Rejeitado" } }, "thread": { "back_title": "Post", - "title": "Post from %s" + "title": "Publicação de %s" }, "settings": { - "title": "Settings", + "title": "Configurações", "section": { "appearance": { - "title": "Appearance", - "automatic": "Automatic", - "light": "Always Light", - "dark": "Always Dark" + "title": "Aparência", + "automatic": "Automático", + "light": "Sempre Claro", + "dark": "Sempre Escuro" }, "look_and_feel": { "title": "Look and Feel", - "use_system": "Use System", - "really_dark": "Really Dark", - "sorta_dark": "Sorta Dark", - "light": "Light" + "use_system": "Usar configuração do sistema", + "really_dark": "Bem escuro", + "sorta_dark": "Meio escuro", + "light": "Claro" }, "notifications": { - "title": "Notifications", + "title": "Notificações", "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" + "anyone": "qualquer pessoa", + "follower": "um seguidor", + "follow": "qualquer um que eu siga", + "noone": "ninguém", + "title": "Me notificar quando" } }, "preference": { - "title": "Preferences", + "title": "Preferências", "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" + "disable_avatar_animation": "Desativar fotos animadas", + "disable_emoji_animation": "Desativar emojis animados", + "using_default_browser": "Usar o navegador padrão pra abrir links", + "open_links_in_mastodon": "Abrir links no Mastodon" }, "boring_zone": { - "title": "The Boring Zone", - "account_settings": "Account Settings", - "terms": "Terms of Service", - "privacy": "Privacy Policy" + "title": "A zona chata", + "account_settings": "Configurações da conta", + "terms": "Termos de serviço", + "privacy": "Política de privacidade" }, "spicy_zone": { "title": "The Spicy Zone", From b7b7aea4422d2ae877c75ce614038559d5b0587e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 15 Nov 2022 07:18:14 +0100 Subject: [PATCH 488/658] New translations app.json (Portuguese, Brazilian) --- .../input/pt-BR.lproj/app.json | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/app.json b/Localization/StringsConvertor/input/pt-BR.lproj/app.json index d58006a22..ea21865a4 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/app.json +++ b/Localization/StringsConvertor/input/pt-BR.lproj/app.json @@ -637,61 +637,61 @@ } }, "footer": { - "mastodon_description": "Mastodon is open source software. You can report issues on GitHub at %s (%s)" + "mastodon_description": "Mastodon é um software de código aberto. Você pode reportar problemas no GitHub em %s (%s)" }, "keyboard": { - "close_settings_window": "Close Settings Window" + "close_settings_window": "Fechar janela de configurações" } }, "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", + "title_report": "Denunciar", + "title": "Denunciar %s", + "step1": "Passo 1 de 2", + "step2": "Passo 2 de 2", + "content1": "Há outras postagens que você gostaria de adicionar na denúncia?", + "content2": "Há algo que os moderadores deveriam saber sobre esta denúncia?", + "report_sent_title": "Obrigado por denunciar, iremos analisar.", + "send": "Enviar denúncia", + "skip_to_send": "Enviar sem comentário", + "text_placeholder": "Digite ou cole comentários adicionais", + "reported": "DENUNCIADO", "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": "Passo 1 de 4", + "whats_wrong_with_this_post": "O que há de errado com essa publicação?", + "whats_wrong_with_this_account": "O que há de errado com essa conta?", + "whats_wrong_with_this_username": "O que há de errado com %s?", + "select_the_best_match": "Selecione a melhor alternativa", + "i_dont_like_it": "Eu não gosto disso", + "it_is_not_something_you_want_to_see": "Não é algo que você gostaria de ver", + "its_spam": "É spam", + "malicious_links_fake_engagement_or_repetetive_replies": "Links maliciosos, engajamento falso, ou respostas repetitivas", + "it_violates_server_rules": "Isso viola as regras do servidor", + "you_are_aware_that_it_breaks_specific_rules": "Você está ciente que isso quebra regras específicas", + "its_something_else": "É outra coisa", + "the_issue_does_not_fit_into_other_categories": "O problema não se encaixa em outras categorias" }, "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": "Passo 2 de 4", + "which_rules_are_being_violated": "Quais regras estão sendo violadas?", + "select_all_that_apply": "Selecione todas que se aplicam", + "i_just_don’t_like_it": "Simplesmente não gosto" }, "step_three": { - "step_3_of_4": "Step 3 of 4", + "step_3_of_4": "Passo 3 de 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" + "select_all_that_apply": "Selecione todos que se aplicam" }, "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": "Passo 4 de 4", + "is_there_anything_else_we_should_know": "Há algo a mais que deveríamos saber?" }, "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", + "dont_want_to_see_this": "Não quer ver isso?", + "when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "Quando você vê algo que não gosta no Mastodon, você pode remover essa pessoa da sua experiência.", + "unfollow": "Deixar de seguir", + "unfollowed": "Deixou de seguir", + "unfollow_user": "Deixar de seguir %s", + "mute_user": "Silenciar %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.", From 9d716de4535ef2766fc80b2ae660629da0149f1e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 15 Nov 2022 12:59:14 +0100 Subject: [PATCH 489/658] New translations app.json (Korean) --- Localization/StringsConvertor/input/ko.lproj/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/ko.lproj/app.json b/Localization/StringsConvertor/input/ko.lproj/app.json index 2c4316f07..a871d2d00 100644 --- a/Localization/StringsConvertor/input/ko.lproj/app.json +++ b/Localization/StringsConvertor/input/ko.lproj/app.json @@ -422,8 +422,8 @@ "enable_content_warning": "열람 주의 설정", "disable_content_warning": "열람 주의 해제", "post_visibility_menu": "게시물 공개범위 메뉴", - "post_options": "Post Options", - "posting_as": "Posting as %s" + "post_options": "게시물 옵션", + "posting_as": "%s로 게시" }, "keyboard": { "discard_post": "글 버리기", From 143e598c019ff19a26067438ef0b03f85a0768ae Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 15 Nov 2022 12:59:15 +0100 Subject: [PATCH 490/658] New translations Localizable.stringsdict (Korean) --- .../StringsConvertor/input/ko.lproj/Localizable.stringsdict | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/ko.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/ko.lproj/Localizable.stringsdict index 644fd007a..77aac5569 100644 --- a/Localization/StringsConvertor/input/ko.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/ko.lproj/Localizable.stringsdict @@ -47,7 +47,7 @@ a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ left + %#@character_count@ 글자 남음 character_count NSStringFormatSpecTypeKey @@ -55,7 +55,7 @@ NSStringFormatValueTypeKey ld other - %ld characters + %ld 글자 plural.count.followed_by_and_mutual From 6ec16d5a8115aaa5ff3c7d319d623db594e3d5e0 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 15 Nov 2022 07:00:15 -0500 Subject: [PATCH 491/658] Adjustments for new i18n workflow --- Localization/StringsConvertor/input/en.lproj/app.json | 4 ++-- .../Resources/en.lproj/Localizable.strings | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/en.lproj/app.json b/Localization/StringsConvertor/input/en.lproj/app.json index 2b8745ecc..25f06ad83 100644 --- a/Localization/StringsConvertor/input/en.lproj/app.json +++ b/Localization/StringsConvertor/input/en.lproj/app.json @@ -698,9 +698,9 @@ } }, "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", - "switch_accounts": "Switch Accounts" + "add_account": "Add Account" }, "wizard": { "new_in_mastodon": "New in Mastodon", diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings index 66394ee10..07ccd2c1b 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings @@ -154,7 +154,7 @@ Your profile looks like this to them."; "Common.Controls.Timeline.Timestamp.Now" = "Now"; "Scene.AccountList.AddAccount" = "Add Account"; "Scene.AccountList.DismissAccountSwitcher" = "Dismiss Account Switcher"; -"Scene.AccountList.SwitchAccounts" = "Switch Accounts"; +"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"; From 726de75d2e82cd76b2af59cfde1fa1f96ac86606 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 15 Nov 2022 15:43:42 +0100 Subject: [PATCH 492/658] New translations app.json (French) --- .../StringsConvertor/input/fr.lproj/app.json | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Localization/StringsConvertor/input/fr.lproj/app.json b/Localization/StringsConvertor/input/fr.lproj/app.json index f35ce4483..7dffb4aba 100644 --- a/Localization/StringsConvertor/input/fr.lproj/app.json +++ b/Localization/StringsConvertor/input/fr.lproj/app.json @@ -383,12 +383,12 @@ "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...", - "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..." + "load_failed": "Échec du chargement", + "upload_failed": "Échec de l’envoi", + "can_not_recognize_this_media_attachment": "Impossible de reconnaître cette pièce jointe", + "attachment_too_large": "La pièce jointe est trop volumineuse", + "compressing_state": "Compression...", + "server_processing_state": "Traitement du serveur..." }, "poll": { "duration_time": "Durée: %s", @@ -399,8 +399,8 @@ "three_days": "3 jour", "seven_days": "7 jour", "option_number": "Option %ld", - "the_poll_is_invalid": "The poll is invalid", - "the_poll_has_empty_option": "The poll has empty option" + "the_poll_is_invalid": "Le sondage est invalide", + "the_poll_has_empty_option": "Le sondage n'a pas d'options" }, "content_warning": { "placeholder": "Écrivez un avertissement précis ici..." @@ -422,8 +422,8 @@ "enable_content_warning": "Basculer l’avertissement de contenu", "disable_content_warning": "Désactiver l'avertissement de contenu", "post_visibility_menu": "Menu de Visibilité de la publication", - "post_options": "Post Options", - "posting_as": "Posting as %s" + "post_options": "Options de publication", + "posting_as": "Publié en tant que %s" }, "keyboard": { "discard_post": "Rejeter la publication", @@ -450,8 +450,8 @@ "content": "Contenu" }, "verified": { - "short": "Verified on %s", - "long": "Ownership of this link was checked on %s" + "short": "Vérifié le %s", + "long": "La propriété de ce lien a été vérifiée le %s" } }, "segmented_control": { From eb26a4d6c35f9f5f5910541e6ee1273786626d2d Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 15 Nov 2022 15:44:51 +0100 Subject: [PATCH 493/658] fix: Improve media inline appearance --- .../Scene/ComposeContent/View/ComposeContentView.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift index 10ca4e71f..a7f867f60 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift @@ -222,13 +222,17 @@ extension ComposeContentView { VStack(spacing: 16) { ForEach(viewModel.attachmentViewModels, id: \.self) { attachmentViewModel in AttachmentView(viewModel: attachmentViewModel) - .clipShape(Rectangle()) + .clipShape(RoundedRectangle(cornerRadius: 4)) .badgeView( Button { viewModel.attachmentViewModels.removeAll(where: { $0 === attachmentViewModel }) } label: { Image(systemName: "minus.circle.fill") + .resizable() + .frame(width: 20, height: 20) .foregroundColor(.red) + .background(Color.white) + .clipShape(Circle()) } ) } // end ForEach From 88948b40245b61b91c61876fb2453fdba7f088db Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 15 Nov 2022 16:48:16 +0100 Subject: [PATCH 494/658] New translations app.json (Arabic) --- Localization/StringsConvertor/input/ar.lproj/app.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Localization/StringsConvertor/input/ar.lproj/app.json b/Localization/StringsConvertor/input/ar.lproj/app.json index c28962a0f..1a52ccfbb 100644 --- a/Localization/StringsConvertor/input/ar.lproj/app.json +++ b/Localization/StringsConvertor/input/ar.lproj/app.json @@ -385,7 +385,7 @@ "description_video": "صِف المقطع المرئي للمَكفوفين...", "load_failed": "فَشَلَ التَّحميل", "upload_failed": "فَشَلَ الرَّفع", - "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "can_not_recognize_this_media_attachment": "يتعذَّرُ التعرُّفُ على وسائِطِ هذا المُرفَق", "attachment_too_large": "المُرفَق كَبيرٌ جِدًّا", "compressing_state": "يجري الضغط...", "server_processing_state": "مُعالجة الخادم جارِيَة..." @@ -399,8 +399,8 @@ "three_days": "ثلاثةُ أيام", "seven_days": "سبعةُ أيام", "option_number": "الخيار %ld", - "the_poll_is_invalid": "The poll is invalid", - "the_poll_has_empty_option": "The poll has empty option" + "the_poll_is_invalid": "الاِستِطلاعُ غيرُ صالِح", + "the_poll_has_empty_option": "يوجَدُ خِيارٌ فارِغٌ فِي الاِستِطلاع" }, "content_warning": { "placeholder": "اكتب تَحذيرًا دَقيقًا هُنا..." @@ -450,8 +450,8 @@ "content": "المُحتَوى" }, "verified": { - "short": "Verified on %s", - "long": "Ownership of this link was checked on %s" + "short": "تمَّ التَّحقق بِتاريخ %s", + "long": "تمَّ التَّحقق مِن مِلكية هذا الرابِطِ بِتاريخ %s" } }, "segmented_control": { From 23183f62715b2b3ee775c81e03e552671afae16d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 15 Nov 2022 16:48:17 +0100 Subject: [PATCH 495/658] New translations app.json (Portuguese, Brazilian) --- .../input/pt-BR.lproj/app.json | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/app.json b/Localization/StringsConvertor/input/pt-BR.lproj/app.json index ea21865a4..b69c14777 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/app.json +++ b/Localization/StringsConvertor/input/pt-BR.lproj/app.json @@ -2,7 +2,7 @@ "common": { "alerts": { "common": { - "please_try_again": "Por favor tente novamente.", + "please_try_again": "Por favor, tente novamente.", "please_try_again_later": "Tente novamente mais tarde." }, "sign_up_failure": { @@ -467,8 +467,8 @@ "message": "Confirme para silenciar %s" }, "confirm_unmute_user": { - "title": "Unmute Account", - "message": "Confirm to unmute %s" + "title": "Tirar conta do silenciado", + "message": "Confirme para tirar %s do silenciado" }, "confirm_block_user": { "title": "Bloquear conta", @@ -500,7 +500,7 @@ }, "following": { "title": "seguindo", - "footer": "Follows from other servers are not displayed." + "footer": "Contas que você segue de outras instâncias não são exibidas." }, "familiarFollowers": { "title": "Seguidores que você conhece", @@ -568,7 +568,7 @@ "favorited_your_post": "favoritou seu toot", "reblogged_your_post": "reblogou seu toot", "mentioned_you": "te mencionou", - "request_to_follow_you": "request to follow you", + "request_to_follow_you": "solicitação para te seguir", "poll_has_ended": "enquete encerrada" }, "keyobard": { @@ -583,7 +583,7 @@ } }, "thread": { - "back_title": "Post", + "back_title": "Toot", "title": "Publicação de %s" }, "settings": { @@ -596,7 +596,7 @@ "dark": "Sempre Escuro" }, "look_and_feel": { - "title": "Look and Feel", + "title": "Aparência e Comportamento", "use_system": "Usar configuração do sistema", "really_dark": "Bem escuro", "sorta_dark": "Meio escuro", @@ -604,10 +604,10 @@ }, "notifications": { "title": "Notificações", - "favorites": "Favorites my post", - "follows": "Follows me", - "boosts": "Reblogs my post", - "mentions": "Mentions me", + "favorites": "Favoritaram minha publicação", + "follows": "Me segue", + "boosts": "Rebloga minha publicação", + "mentions": "Me menciona", "trigger": { "anyone": "qualquer pessoa", "follower": "um seguidor", @@ -618,7 +618,7 @@ }, "preference": { "title": "Preferências", - "true_black_dark_mode": "True black dark mode", + "true_black_dark_mode": "Modo preto", "disable_avatar_animation": "Desativar fotos animadas", "disable_emoji_animation": "Desativar emojis animados", "using_default_browser": "Usar o navegador padrão pra abrir links", @@ -631,9 +631,9 @@ "privacy": "Política de privacidade" }, "spicy_zone": { - "title": "The Spicy Zone", - "clear": "Clear Media Cache", - "signout": "Sign Out" + "title": "A zona apimentada", + "clear": "Limpar cachê de mídia", + "signout": "Sair" } }, "footer": { @@ -678,7 +678,7 @@ }, "step_three": { "step_3_of_4": "Passo 3 de 4", - "are_there_any_posts_that_back_up_this_report": "Are there any posts that back up this report?", + "are_there_any_posts_that_back_up_this_report": "Existem postagens que apoiam essa denúncia?", "select_all_that_apply": "Selecione todos que se aplicam" }, "step_four": { @@ -692,31 +692,31 @@ "unfollowed": "Deixou de seguir", "unfollow_user": "Deixar de seguir %s", "mute_user": "Silenciar %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" + "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "Você não verá as postagens ou reblogs dessa conta na sua pagina inicial. Essa pessoa não saberá que foi silenciada.", + "block_user": "Bloquear %s", + "they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "Essa conta não poderá mais te seguir ou ver suas postagens, mas ela poderá ver que foi bloqueada.", + "while_we_review_this_you_can_take_action_against_user": "Enquanto revisamos isso, você pode tomar medidas contra %s" } }, "preview": { "keyboard": { - "close_preview": "Close Preview", - "show_next": "Show Next", - "show_previous": "Show Previous" + "close_preview": "Fechar prévia", + "show_next": "Mostrar a próxima", + "show_previous": "Mostrar a anterior" } }, "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": "Perfil selecionado nesse momento: %s. Toque duas vezes e segure para mostrar o alternador de conta", + "dismiss_account_switcher": "Descartar alternador de conta", + "add_account": "Adicionar conta" }, "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": "Novo no Mastodon", + "multiple_account_switch_intro_description": "Alterne entre múltiplas contas segurando o botão de perfil.", + "accessibility_hint": "Toque duas vezes para descartar este assistente" }, "bookmark": { - "title": "Bookmarks" + "title": "Marcados" } } } \ No newline at end of file From 3c190983dc9c3ea25aac3116832912bac46bbc3a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 15 Nov 2022 16:48:18 +0100 Subject: [PATCH 496/658] New translations Localizable.stringsdict (Portuguese, Brazilian) --- .../input/pt-BR.lproj/Localizable.stringsdict | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/pt-BR.lproj/Localizable.stringsdict index 9a72fe3bb..02fbf2d20 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/pt-BR.lproj/Localizable.stringsdict @@ -88,9 +88,9 @@ NSStringFormatValueTypeKey ld one - Followed by %1$@, and another mutual + Seguido por %1$@, e outro em comum other - Followed by %1$@, and %ld mutuals + Seguido por %1$@, e %ld em comum plural.count.metric_formatted.post @@ -120,9 +120,9 @@ NSStringFormatValueTypeKey ld one - 1 media + 1 mídia other - %ld media + %ld mídias plural.count.post From d6aeb81011a3b1b3cff36dc4029efb081a12357f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 15 Nov 2022 16:48:20 +0100 Subject: [PATCH 497/658] New translations Intents.strings (Portuguese, Brazilian) --- .../Intents/input/pt-BR.lproj/Intents.strings | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.strings index 4d4e426c6..3e6806953 100644 --- a/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.strings +++ b/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.strings @@ -4,9 +4,9 @@ "CsR7G2" = "Postar no Mastodon"; -"HZSGTr" = "What content to post?"; +"HZSGTr" = "Qual conteúdo a publicar?"; -"HdGikU" = "Posting failed"; +"HdGikU" = "Falha na publicação"; "KDNTJ4" = "Motivo da falha"; @@ -22,30 +22,30 @@ "ZbSjzC" = "Visibilidade"; -"Zo4jgJ" = "Post Visibility"; +"Zo4jgJ" = "Visibilidade da publicação"; -"apSxMG-dYQ5NN" = "There are ${count} options matching ‘Public’."; +"apSxMG-dYQ5NN" = "Existem ${count} opções correspondentes a ‘Público’."; -"apSxMG-ehFLjY" = "There are ${count} options matching ‘Followers Only’."; +"apSxMG-ehFLjY" = "Existem ${count} opções correspondentes a ‘Apenas para seguidores’."; -"ayoYEb-dYQ5NN" = "${content}, Public"; +"ayoYEb-dYQ5NN" = "${content}, Público"; -"ayoYEb-ehFLjY" = "${content}, Followers Only"; +"ayoYEb-ehFLjY" = "${content}, Apenas para seguidores"; -"dUyuGg" = "Post on Mastodon"; +"dUyuGg" = "Postar no Mastodon"; -"dYQ5NN" = "Public"; +"dYQ5NN" = "Público"; -"ehFLjY" = "Followers Only"; +"ehFLjY" = "Apenas para seguidores"; -"gfePDu" = "Posting failed. ${failureReason}"; +"gfePDu" = "Falha na publicação. ${failureReason}"; -"k7dbKQ" = "Post was sent successfully."; +"k7dbKQ" = "Publicação enviada com sucesso."; -"oGiqmY-dYQ5NN" = "Just to confirm, you wanted ‘Public’?"; +"oGiqmY-dYQ5NN" = "Só para confirmar, você queria ‘Público’?"; -"oGiqmY-ehFLjY" = "Just to confirm, you wanted ‘Followers Only’?"; +"oGiqmY-ehFLjY" = "Só para confirmar, você queria ‘Apenas para seguidores’?"; "rM6dvp" = "URL"; -"ryJLwG" = "Post was sent successfully. "; +"ryJLwG" = "Publicação enviada com sucesso. "; From 25b26e4c7dd1ebed18ceca89c48b22bbf04b07fc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 15 Nov 2022 18:25:04 +0100 Subject: [PATCH 498/658] New translations app.json (Kurmanji (Kurdish)) --- .../StringsConvertor/input/kmr.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/kmr.lproj/app.json b/Localization/StringsConvertor/input/kmr.lproj/app.json index 6f5fc8b97..6e299e016 100644 --- a/Localization/StringsConvertor/input/kmr.lproj/app.json +++ b/Localization/StringsConvertor/input/kmr.lproj/app.json @@ -385,10 +385,10 @@ "description_video": "Vîdyoyê ji bo kêmbînên dîtbar bide nasîn...", "load_failed": "Barkirin têk çû", "upload_failed": "Barkirin têk çû", - "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "can_not_recognize_this_media_attachment": "Nikare ev pêveka medyayê nas bike", "attachment_too_large": "Pêvek pir mezin e", "compressing_state": "Compressing...", - "server_processing_state": "Server Processing..." + "server_processing_state": "Pêvajoya rajekar pêş de diçe..." }, "poll": { "duration_time": "Dirêjî: %s", @@ -399,7 +399,7 @@ "three_days": "3 Roj", "seven_days": "7 Roj", "option_number": "Vebijêrk %ld", - "the_poll_is_invalid": "The poll is invalid", + "the_poll_is_invalid": "Ev dengdayîn ne derbasdar e", "the_poll_has_empty_option": "The poll has empty option" }, "content_warning": { @@ -422,8 +422,8 @@ "enable_content_warning": "Hişyariya naverokê çalak bike", "disable_content_warning": "Hişyariya naverokê neçalak bike", "post_visibility_menu": "Kulîna xuyabûna şandiyê", - "post_options": "Post Options", - "posting_as": "Posting as %s" + "post_options": "Vebijêrkên şandiyê", + "posting_as": "Biweşîne wekî %s" }, "keyboard": { "discard_post": "Şandî paşguh bike", @@ -450,8 +450,8 @@ "content": "Naverok" }, "verified": { - "short": "Verified on %s", - "long": "Ownership of this link was checked on %s" + "short": "Hate piştrastkirin li ser %s", + "long": "Xwedaniya li vê girêdanê di %s de hatiye kontrolkirin" } }, "segmented_control": { From d4cecff07dafb8a483a2b9e80b897f12e2bc057a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 15 Nov 2022 18:25:05 +0100 Subject: [PATCH 499/658] New translations Localizable.stringsdict (Kurmanji (Kurdish)) --- .../input/kmr.lproj/Localizable.stringsdict | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/kmr.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/kmr.lproj/Localizable.stringsdict index 45d65bc71..c904186d8 100644 --- a/Localization/StringsConvertor/input/kmr.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/kmr.lproj/Localizable.stringsdict @@ -53,7 +53,7 @@ a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ left + %#@character_count@ maye character_count NSStringFormatSpecTypeKey @@ -61,9 +61,9 @@ NSStringFormatValueTypeKey ld one - 1 character + 1 peyv other - %ld characters + %ld peyv plural.count.followed_by_and_mutual From 9a08cb194b1e57d9ca840db1f4ff139ff06b7b87 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 15 Nov 2022 19:26:02 +0100 Subject: [PATCH 500/658] New translations app.json (Kurmanji (Kurdish)) --- Localization/StringsConvertor/input/kmr.lproj/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/kmr.lproj/app.json b/Localization/StringsConvertor/input/kmr.lproj/app.json index 6e299e016..f52e1bdae 100644 --- a/Localization/StringsConvertor/input/kmr.lproj/app.json +++ b/Localization/StringsConvertor/input/kmr.lproj/app.json @@ -387,7 +387,7 @@ "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", - "compressing_state": "Compressing...", + "compressing_state": "Tê guvaştin...", "server_processing_state": "Pêvajoya rajekar pêş de diçe..." }, "poll": { @@ -400,7 +400,7 @@ "seven_days": "7 Roj", "option_number": "Vebijêrk %ld", "the_poll_is_invalid": "Ev dengdayîn ne derbasdar e", - "the_poll_has_empty_option": "The poll has empty option" + "the_poll_has_empty_option": "Vebijêrkên vê dengdayînê vala ne" }, "content_warning": { "placeholder": "Li vir hişyariyek hûrgilî binivîsine..." From 26d728731970c5565de03e37d061ec35445870d2 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 9 Nov 2022 14:13:33 +0100 Subject: [PATCH 501/658] Add what needs to be done for better onboarding (#540) --- Mastodon/Coordinator/SceneCoordinator.swift | 1 + Mastodon/Diffable/Onboarding/PickServerSection.swift | 1 + .../PickServer/MastodonPickServerViewController.swift | 7 ++++--- .../PickServer/MastodonPickServerViewModel.swift | 1 + .../PickServer/TableViewCell/PickServerCell.swift | 3 +++ .../PickServer/View/PickServerCategoryView.swift | 1 + .../Cell/MastodonRegisterAvatarTableViewCell.swift | 1 + .../Cell/MastodonRegisterPasswordHintTableViewCell.swift | 1 + .../Cell/MastodonRegisterTextFieldTableViewCell.swift | 4 ++-- .../Scene/Onboarding/Register/MastodonRegisterView.swift | 1 + .../Register/MastodonRegisterViewController.swift | 2 +- .../Register/MastodonRegisterViewModel+Diffable.swift | 1 + .../ServerRules/MastodonServerRulesViewController.swift | 2 +- .../Scene/Onboarding/Welcome/WelcomeViewController.swift | 5 +++-- .../MastodonSDK/Entity/Mastodon+Entity+Server.swift | 1 + 15 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 8a0825969..eca818fe5 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -149,6 +149,7 @@ extension SceneCoordinator { case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel) case mastodonResendEmail(viewModel: MastodonResendEmailViewModel) case mastodonWebView(viewModel: WebViewModel) + //TODO: @zeitschlag new case for welcome back select your server welcome-screen + Screen and ViewModel etc. // search case searchDetail(viewModel: SearchDetailViewModel) diff --git a/Mastodon/Diffable/Onboarding/PickServerSection.swift b/Mastodon/Diffable/Onboarding/PickServerSection.swift index 01a31f6f6..2eb6fa7a3 100644 --- a/Mastodon/Diffable/Onboarding/PickServerSection.swift +++ b/Mastodon/Diffable/Onboarding/PickServerSection.swift @@ -51,6 +51,7 @@ extension PickServerSection { extension PickServerSection { static func configure(cell: PickServerCell, server: Mastodon.Entity.Server, attribute: PickServerItem.ServerItemAttribute) { + //TODO: @zeitschlag configure cell if server doesn't allow registrations cell.domainLabel.text = server.domain cell.descriptionLabel.attributedText = { let content: String = { diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index eb26f75be..006d9bcb8 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -172,7 +172,7 @@ extension MastodonPickServerViewController { let alertController = UIAlertController(for: error, title: "Error", preferredStyle: .alert) let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) alertController.addAction(okAction) - self.coordinator.present( + _ = self.coordinator.present( scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil) @@ -394,7 +394,7 @@ extension MastodonPickServerViewController { instance: response.instance.value, applicationToken: response.applicationToken.value ) - self.coordinator.present(scene: .mastodonServerRules(viewModel: mastodonServerRulesViewModel), from: self, transition: .show) + _ = self.coordinator.present(scene: .mastodonServerRules(viewModel: mastodonServerRulesViewModel), from: self, transition: .show) } else { let mastodonRegisterViewModel = MastodonRegisterViewModel( context: self.context, @@ -403,7 +403,7 @@ extension MastodonPickServerViewController { instance: response.instance.value, applicationToken: response.applicationToken.value ) - self.coordinator.present(scene: .mastodonRegister(viewModel: mastodonRegisterViewModel), from: nil, transition: .show) + _ = self.coordinator.present(scene: .mastodonRegister(viewModel: mastodonRegisterViewModel), from: nil, transition: .show) } } .store(in: &disposeBag) @@ -499,6 +499,7 @@ extension MastodonPickServerViewController: PickServerServerSectionTableHeaderVi } func pickServerServerSectionTableHeaderView(_ headerView: PickServerServerSectionTableHeaderView, searchTextDidChange searchText: String?) { + //TODO: @zeitschlag Deselect server? viewModel.searchText.send(searchText ?? "") } } diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift index 50c1d7aac..719de4cb4 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift @@ -178,6 +178,7 @@ extension MastodonPickServerViewModel { .compactMap { [weak self] searchText -> AnyPublisher, Error>, Never>? in // Check if searchText is a valid mastodon server domain guard let self = self else { return nil } + //TODO: @zeitschlag Also allow search for incomplete URLs? guard let domain = AuthenticationViewModel.parseDomain(from: searchText) else { return Just(Result.failure(APIService.APIError.implicit(.badRequest))).eraseToAnyPublisher() } diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift index 669067770..f33b1d2e2 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift @@ -14,6 +14,7 @@ import Kanna import MastodonAsset import MastodonLocalization +//TODO: @zeitschlag Remove Delegate protocol PickServerCellDelegate: AnyObject { // func pickServerCell(_ cell: PickServerCell, expandButtonPressed button: UIButton) } @@ -88,6 +89,8 @@ class PickServerCell: UITableViewCell { label.adjustsFontForContentSizeCategory = true return label }() + + //TODO: @zeitschlag New label for "Registrations closed" private var collapseConstraints: [NSLayoutConstraint] = [] private var expandConstraints: [NSLayoutConstraint] = [] diff --git a/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift b/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift index 9d6cfc85f..fca4bbf3a 100644 --- a/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift +++ b/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift @@ -19,6 +19,7 @@ class PickServerCategoryView: UIView { return view }() + //TODO: @zeitschlag Remove emojiLabel let emojiLabel: UILabel = { let label = UILabel() label.textAlignment = .center diff --git a/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterAvatarTableViewCell.swift b/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterAvatarTableViewCell.swift index 154385e6a..c4754bb35 100644 --- a/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterAvatarTableViewCell.swift +++ b/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterAvatarTableViewCell.swift @@ -10,6 +10,7 @@ import Combine import MastodonAsset import MastodonLocalization +//TODO: @zeitschlag Remove final class MastodonRegisterAvatarTableViewCell: UITableViewCell { static let containerSize = CGSize(width: 88, height: 88) diff --git a/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterPasswordHintTableViewCell.swift b/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterPasswordHintTableViewCell.swift index 1324c2822..ba088f061 100644 --- a/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterPasswordHintTableViewCell.swift +++ b/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterPasswordHintTableViewCell.swift @@ -9,6 +9,7 @@ import UIKit import MastodonAsset import MastodonLocalization +//TODO: @zeitschlag Remove final class MastodonRegisterPasswordHintTableViewCell: UITableViewCell { let passwordRuleLabel: UILabel = { diff --git a/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterTextFieldTableViewCell.swift b/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterTextFieldTableViewCell.swift index 15f234834..9bbdd8044 100644 --- a/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterTextFieldTableViewCell.swift +++ b/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterTextFieldTableViewCell.swift @@ -11,13 +11,13 @@ import MastodonUI import MastodonAsset import MastodonLocalization -final class MastodonRegisterTextFieldTableViewCell: UITableViewCell { +//TODO: @zeitschlag Removefinal class MastodonRegisterTextFieldTableViewCell: UITableViewCell { static let textFieldHeight: CGFloat = 50 static let textFieldLabelFont = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22) var disposeBag = Set() - + let textFieldShadowContainer = ShadowBackgroundContainer() let textField: UITextField = { let textField = UITextField() diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterView.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterView.swift index 2be7c61d7..787b79bd8 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterView.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterView.swift @@ -175,6 +175,7 @@ struct MastodonRegisterView: View { case .valid: return Color(Asset.Colors.TextField.valid.color) } }() + //TODO: @zeitschlag Replace shadow with border Color(Asset.Scene.Onboarding.textFieldBackground.color) .cornerRadius(10) .shadow(color: shadowColor, radius: 1, x: 0, y: 2) diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index 9b10bd48e..758c20888 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -145,7 +145,7 @@ extension MastodonRegisterViewController { let alertController = UIAlertController(for: error, title: "Sign Up Failure", preferredStyle: .alert) let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) alertController.addAction(okAction) - self.coordinator.present( + _ = self.coordinator.present( scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil) diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel+Diffable.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel+Diffable.swift index dda59843a..f1e0d8903 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel+Diffable.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel+Diffable.swift @@ -11,6 +11,7 @@ import MastodonAsset import MastodonLocalization extension MastodonRegisterViewModel { + //TODO: @zeitschlag Remove, as the screen is based on SwiftUI now func setupDiffableDataSource( tableView: UITableView ) { diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift index 0d4e27d98..e22ea5748 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift @@ -127,7 +127,7 @@ extension MastodonServerRulesViewController { instance: viewModel.instance, applicationToken: viewModel.applicationToken ) - coordinator.present(scene: .mastodonRegister(viewModel: viewModel), from: self, transition: .show) + _ = coordinator.present(scene: .mastodonRegister(viewModel: viewModel), from: self, transition: .show) } } diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift index a2b8df83a..612189243 100644 --- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift +++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift @@ -317,12 +317,13 @@ extension WelcomeViewController { extension WelcomeViewController { @objc private func signUpButtonDidClicked(_ sender: UIButton) { - coordinator.present(scene: .mastodonPickServer(viewMode: MastodonPickServerViewModel(context: context, mode: .signUp)), from: self, transition: .show) + _ = coordinator.present(scene: .mastodonPickServer(viewMode: MastodonPickServerViewModel(context: context, mode: .signUp)), from: self, transition: .show) } @objc private func signInButtonDidClicked(_ sender: UIButton) { - coordinator.present(scene: .mastodonPickServer(viewMode: MastodonPickServerViewModel(context: context, mode: .signIn)), from: self, transition: .show) + //TODO: @zeitschlag Present new login-scene + _ = coordinator.present(scene: .mastodonPickServer(viewMode: MastodonPickServerViewModel(context: context, mode: .signIn)), from: self, transition: .show) } @objc diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift index 2d1f9953f..da1f0dfad 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift @@ -22,6 +22,7 @@ extension Mastodon.Entity { public let approvalRequired: Bool public let language: String public let category: String + //TODO: @zeitschlag Is there a way to figure out in advance if a server accepts new registrations? Right now we'd have to query the server and it responts with a `AuthenticationViewModel.AuthenticationError.registrationClosed` enum CodingKeys: String, CodingKey { case domain From 83de4049d83ee45ca991ac1f12bfb21793a47c08 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 9 Nov 2022 16:05:36 +0100 Subject: [PATCH 502/658] Remove emoji-label (#540) --- .../Diffable/Onboarding/CategoryPickerSection.swift | 1 - .../PickServer/View/PickServerCategoryView.swift | 12 ++---------- .../PickServerServerSectionTableHeaderView.swift | 5 ++--- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/Mastodon/Diffable/Onboarding/CategoryPickerSection.swift b/Mastodon/Diffable/Onboarding/CategoryPickerSection.swift index b53b378d6..191d4d166 100644 --- a/Mastodon/Diffable/Onboarding/CategoryPickerSection.swift +++ b/Mastodon/Diffable/Onboarding/CategoryPickerSection.swift @@ -21,7 +21,6 @@ extension CategoryPickerSection { UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak dependency] collectionView, indexPath, item -> UICollectionViewCell? in guard let _ = dependency else { return nil } let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self), for: indexPath) as! PickServerCategoryCollectionViewCell - cell.categoryView.emojiLabel.text = item.emoji cell.categoryView.titleLabel.text = item.title cell.observe(\.isSelected, options: [.initial, .new]) { cell, _ in cell.categoryView.highlightedIndicatorView.alpha = cell.isSelected ? 1 : 0 diff --git a/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift b/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift index fca4bbf3a..bfa3343bd 100644 --- a/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift +++ b/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift @@ -19,14 +19,6 @@ class PickServerCategoryView: UIView { return view }() - //TODO: @zeitschlag Remove emojiLabel - let emojiLabel: UILabel = { - let label = UILabel() - label.textAlignment = .center - label.font = .systemFont(ofSize: 34, weight: .regular) - return label - }() - let titleLabel: UILabel = { let label = UILabel() label.textAlignment = .center @@ -51,6 +43,7 @@ extension PickServerCategoryView { private func configure() { let container = UIStackView() container.axis = .vertical + container.spacing = 2 container.distribution = .fillProportionally container.translatesAutoresizingMaskIntoConstraints = false @@ -62,12 +55,11 @@ extension PickServerCategoryView { container.bottomAnchor.constraint(equalTo: bottomAnchor), ]) - container.addArrangedSubview(emojiLabel) container.addArrangedSubview(titleLabel) highlightedIndicatorView.translatesAutoresizingMaskIntoConstraints = false container.addArrangedSubview(highlightedIndicatorView) NSLayoutConstraint.activate([ - highlightedIndicatorView.heightAnchor.constraint(equalToConstant: 3).priority(.required - 1), + highlightedIndicatorView.heightAnchor.constraint(equalToConstant: 3)//.priority(.required - 1), ]) titleLabel.setContentHuggingPriority(.required - 1, for: .vertical) } diff --git a/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift b/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift index d50c62afe..ae8c16bb8 100644 --- a/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift +++ b/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift @@ -19,7 +19,7 @@ protocol PickServerServerSectionTableHeaderViewDelegate: AnyObject { final class PickServerServerSectionTableHeaderView: UIView { - static let collectionViewHeight: CGFloat = 88 + static let collectionViewHeight: CGFloat = 30 static let searchTextFieldHeight: CGFloat = 38 static let spacing: CGFloat = 11 @@ -177,7 +177,6 @@ extension PickServerServerSectionTableHeaderView { extension PickServerServerSectionTableHeaderView: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription) collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally) delegate?.pickServerServerSectionTableHeaderView(self, collectionView: collectionView, didSelectItemAt: indexPath) } @@ -205,5 +204,5 @@ extension PickServerServerSectionTableHeaderView: UITextFieldDelegate { textField.resignFirstResponder() return false } - + } From 2d17253c65dea6309e4c7a59a9a52a39582487b0 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 9 Nov 2022 16:13:05 +0100 Subject: [PATCH 503/658] Remove obsolete code (#540) --- Mastodon/Diffable/Onboarding/PickServerSection.swift | 7 ++----- .../PickServer/MastodonPickServerViewController.swift | 8 +------- .../PickServer/MastodonPickServerViewModel+Diffable.swift | 6 ++---- .../PickServer/TableViewCell/PickServerCell.swift | 7 ------- 4 files changed, 5 insertions(+), 23 deletions(-) diff --git a/Mastodon/Diffable/Onboarding/PickServerSection.swift b/Mastodon/Diffable/Onboarding/PickServerSection.swift index 2eb6fa7a3..24e4d2ffb 100644 --- a/Mastodon/Diffable/Onboarding/PickServerSection.swift +++ b/Mastodon/Diffable/Onboarding/PickServerSection.swift @@ -18,16 +18,14 @@ enum PickServerSection: Equatable, Hashable { extension PickServerSection { static func tableViewDiffableDataSource( for tableView: UITableView, - dependency: NeedsDependency, - pickServerCellDelegate: PickServerCellDelegate + dependency: NeedsDependency ) -> UITableViewDiffableDataSource { tableView.register(OnboardingHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: OnboardingHeadlineTableViewCell.self)) tableView.register(PickServerCell.self, forCellReuseIdentifier: String(describing: PickServerCell.self)) tableView.register(PickServerLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: PickServerLoaderTableViewCell.self)) return UITableViewDiffableDataSource(tableView: tableView) { [ - weak dependency, - weak pickServerCellDelegate + weak dependency ] tableView, indexPath, item -> UITableViewCell? in guard let _ = dependency else { return nil } switch item { @@ -37,7 +35,6 @@ extension PickServerSection { case .server(let server, let attribute): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCell.self), for: indexPath) as! PickServerCell PickServerSection.configure(cell: cell, server: server, attribute: attribute) - cell.delegate = pickServerCellDelegate return cell case .loader(let attribute): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerLoaderTableViewCell.self), for: indexPath) as! PickServerLoaderTableViewCell diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 006d9bcb8..a3779dfe6 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -143,8 +143,7 @@ extension MastodonPickServerViewController { viewModel.setupDiffableDataSource( for: tableView, dependency: self, - pickServerServerSectionTableHeaderViewDelegate: self, - pickServerCellDelegate: self + pickServerServerSectionTableHeaderViewDelegate: self ) KeyboardResponderService @@ -504,11 +503,6 @@ extension MastodonPickServerViewController: PickServerServerSectionTableHeaderVi } } -// MARK: - PickServerCellDelegate -extension MastodonPickServerViewController: PickServerCellDelegate { - -} - // MARK: - OnboardingViewControllerAppearance extension MastodonPickServerViewController: OnboardingViewControllerAppearance { } diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+Diffable.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+Diffable.swift index 35de40b8f..7edbfd2ac 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+Diffable.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+Diffable.swift @@ -13,8 +13,7 @@ extension MastodonPickServerViewModel { func setupDiffableDataSource( for tableView: UITableView, dependency: NeedsDependency, - pickServerServerSectionTableHeaderViewDelegate: PickServerServerSectionTableHeaderViewDelegate, - pickServerCellDelegate: PickServerCellDelegate + pickServerServerSectionTableHeaderViewDelegate: PickServerServerSectionTableHeaderViewDelegate ) { // set section header serverSectionHeaderView.diffableDataSource = CategoryPickerSection.collectionViewDiffableDataSource( @@ -34,8 +33,7 @@ extension MastodonPickServerViewModel { // set tableView diffableDataSource = PickServerSection.tableViewDiffableDataSource( for: tableView, - dependency: dependency, - pickServerCellDelegate: pickServerCellDelegate + dependency: dependency ) var snapshot = NSDiffableDataSourceSnapshot() diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift index f33b1d2e2..e199ea96c 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift @@ -14,15 +14,8 @@ import Kanna import MastodonAsset import MastodonLocalization -//TODO: @zeitschlag Remove Delegate -protocol PickServerCellDelegate: AnyObject { -// func pickServerCell(_ cell: PickServerCell, expandButtonPressed button: UIButton) -} - class PickServerCell: UITableViewCell { - weak var delegate: PickServerCellDelegate? - var disposeBag = Set() let containerView: UIStackView = { From 366070d12e1d2838c490ac5ac053db8fe22f86d3 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 9 Nov 2022 16:18:19 +0100 Subject: [PATCH 504/658] Remove obsolete cells (#540) --- Mastodon.xcodeproj/project.pbxproj | 14 -- .../MastodonRegisterAvatarTableViewCell.swift | 117 --------------- ...donRegisterPasswordHintTableViewCell.swift | 51 ------- ...stodonRegisterTextFieldTableViewCell.swift | 142 ------------------ .../MastodonRegisterViewModel+Diffable.swift | 140 ----------------- 5 files changed, 464 deletions(-) delete mode 100644 Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterAvatarTableViewCell.swift delete mode 100644 Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterPasswordHintTableViewCell.swift delete mode 100644 Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterTextFieldTableViewCell.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index c8b2674b5..f622c97da 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -632,7 +632,6 @@ DB0618022785A7100030EE79 /* RegisterSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterSection.swift; sourceTree = ""; }; DB0618042785A73D0030EE79 /* RegisterItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterItem.swift; sourceTree = ""; }; DB0618062785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonRegisterViewModel+Diffable.swift"; sourceTree = ""; }; - DB0618092785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterAvatarTableViewCell.swift; sourceTree = ""; }; DB0A322D280EE9FD001729D2 /* DiscoveryIntroBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryIntroBannerView.swift; sourceTree = ""; }; DB0C947626A7FE840088FB11 /* NotificationAvatarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAvatarButton.swift; sourceTree = ""; }; DB0EF72A26FDB1D200347686 /* SidebarListCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListCollectionViewCell.swift; sourceTree = ""; }; @@ -855,8 +854,6 @@ DB7A9F922818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonServerRulesViewController+Debug.swift"; sourceTree = ""; }; DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderViewModel.swift; sourceTree = ""; }; DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = ""; }; - DB8481142788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterTextFieldTableViewCell.swift; sourceTree = ""; }; - DB84811627883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterPasswordHintTableViewCell.swift; sourceTree = ""; }; DB848E32282B62A800A302CC /* ReportResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultView.swift; sourceTree = ""; }; DB852D1826FAEB6B00FC9D81 /* SidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewController.swift; sourceTree = ""; }; DB852D1B26FB021500FC9D81 /* RootSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootSplitViewController.swift; sourceTree = ""; }; @@ -1564,16 +1561,6 @@ path = Cell; sourceTree = ""; }; - DB06180B2785B2AF0030EE79 /* Cell */ = { - isa = PBXGroup; - children = ( - DB0618092785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift */, - DB8481142788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift */, - DB84811627883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift */, - ); - path = Cell; - sourceTree = ""; - }; DB0A322F280EEA00001729D2 /* View */ = { isa = PBXGroup; children = ( @@ -2540,7 +2527,6 @@ DBE0821A25CD382900FD6BBD /* Register */ = { isa = PBXGroup; children = ( - DB06180B2785B2AF0030EE79 /* Cell */, DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */, 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */, DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */, diff --git a/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterAvatarTableViewCell.swift b/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterAvatarTableViewCell.swift deleted file mode 100644 index c4754bb35..000000000 --- a/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterAvatarTableViewCell.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// MastodonRegisterAvatarTableViewCell.swift -// Mastodon -// -// Created by MainasuK on 2022-1-5. -// - -import UIKit -import Combine -import MastodonAsset -import MastodonLocalization - -//TODO: @zeitschlag Remove -final class MastodonRegisterAvatarTableViewCell: UITableViewCell { - - static let containerSize = CGSize(width: 88, height: 88) - - var disposeBag = Set() - - let containerView: UIView = { - let view = UIView() - view.backgroundColor = .clear - view.layer.masksToBounds = true - view.layer.cornerCurve = .continuous - view.layer.cornerRadius = 22 - return view - }() - - let avatarButton: HighlightDimmableButton = { - let button = HighlightDimmableButton() - button.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color - button.setImage(Asset.Scene.Onboarding.avatarPlaceholder.image, for: .normal) - return button - }() - - let editBannerView: UIView = { - let bannerView = UIView() - bannerView.backgroundColor = UIColor.black.withAlphaComponent(0.5) - bannerView.isUserInteractionEnabled = false - - let label: UILabel = { - let label = UILabel() - label.textColor = .white - label.text = L10n.Common.Controls.Actions.edit - label.font = .systemFont(ofSize: 13, weight: .semibold) - label.textAlignment = .center - label.minimumScaleFactor = 0.5 - label.adjustsFontSizeToFitWidth = true - return label - }() - - label.translatesAutoresizingMaskIntoConstraints = false - bannerView.addSubview(label) - NSLayoutConstraint.activate([ - label.topAnchor.constraint(equalTo: bannerView.topAnchor), - label.leadingAnchor.constraint(equalTo: bannerView.leadingAnchor), - label.trailingAnchor.constraint(equalTo: bannerView.trailingAnchor), - label.bottomAnchor.constraint(equalTo: bannerView.bottomAnchor), - ]) - - return bannerView - }() - - override func prepareForReuse() { - super.prepareForReuse() - - disposeBag.removeAll() - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension MastodonRegisterAvatarTableViewCell { - - private func _init() { - selectionStyle = .none - backgroundColor = .clear - - containerView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(containerView) - NSLayoutConstraint.activate([ - containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 22), - containerView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 8), - containerView.widthAnchor.constraint(equalToConstant: MastodonRegisterAvatarTableViewCell.containerSize.width).priority(.required - 1), - containerView.heightAnchor.constraint(equalToConstant: MastodonRegisterAvatarTableViewCell.containerSize.height).priority(.required - 1), - ]) - - avatarButton.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(avatarButton) - NSLayoutConstraint.activate([ - avatarButton.topAnchor.constraint(equalTo: containerView.topAnchor), - avatarButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - avatarButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), - avatarButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), - ]) - - editBannerView.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(editBannerView) - NSLayoutConstraint.activate([ - editBannerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - editBannerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), - editBannerView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), - editBannerView.heightAnchor.constraint(equalToConstant: 22), - ]) - } - -} diff --git a/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterPasswordHintTableViewCell.swift b/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterPasswordHintTableViewCell.swift deleted file mode 100644 index ba088f061..000000000 --- a/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterPasswordHintTableViewCell.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// MastodonRegisterPasswordHintTableViewCell.swift -// Mastodon -// -// Created by MainasuK on 2022-1-7. -// - -import UIKit -import MastodonAsset -import MastodonLocalization - -//TODO: @zeitschlag Remove -final class MastodonRegisterPasswordHintTableViewCell: UITableViewCell { - - let passwordRuleLabel: UILabel = { - let label = UILabel() - label.font = .preferredFont(forTextStyle: .footnote) - label.textColor = Asset.Colors.Label.secondary.color - label.text = L10n.Scene.Register.Input.Password.hint - return label - }() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension MastodonRegisterPasswordHintTableViewCell { - - private func _init() { - selectionStyle = .none - backgroundColor = .clear - - passwordRuleLabel.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(passwordRuleLabel) - NSLayoutConstraint.activate([ - passwordRuleLabel.topAnchor.constraint(equalTo: contentView.topAnchor), - passwordRuleLabel.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - passwordRuleLabel.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - passwordRuleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) - } - -} diff --git a/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterTextFieldTableViewCell.swift b/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterTextFieldTableViewCell.swift deleted file mode 100644 index 9bbdd8044..000000000 --- a/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterTextFieldTableViewCell.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// MastodonRegisterTextFieldTableViewCell.swift -// Mastodon -// -// Created by MainasuK on 2022-1-7. -// - -import UIKit -import Combine -import MastodonUI -import MastodonAsset -import MastodonLocalization - -//TODO: @zeitschlag Removefinal class MastodonRegisterTextFieldTableViewCell: UITableViewCell { - - static let textFieldHeight: CGFloat = 50 - static let textFieldLabelFont = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22) - - var disposeBag = Set() - - let textFieldShadowContainer = ShadowBackgroundContainer() - let textField: UITextField = { - let textField = UITextField() - textField.font = MastodonRegisterTextFieldTableViewCell.textFieldLabelFont - textField.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color - textField.layer.masksToBounds = true - textField.layer.cornerRadius = 10 - textField.layer.cornerCurve = .continuous - return textField - }() - - override func prepareForReuse() { - super.prepareForReuse() - - disposeBag.removeAll() - textFieldShadowContainer.shadowColor = .black - textFieldShadowContainer.shadowAlpha = 0.25 - resetTextField() - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension MastodonRegisterTextFieldTableViewCell { - - private func _init() { - selectionStyle = .none - backgroundColor = .clear - - textFieldShadowContainer.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(textFieldShadowContainer) - NSLayoutConstraint.activate([ - textFieldShadowContainer.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 6), - textFieldShadowContainer.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - textFieldShadowContainer.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: textFieldShadowContainer.bottomAnchor, constant: 6), - ]) - - textField.translatesAutoresizingMaskIntoConstraints = false - textFieldShadowContainer.addSubview(textField) - NSLayoutConstraint.activate([ - textField.topAnchor.constraint(equalTo: textFieldShadowContainer.topAnchor), - textField.leadingAnchor.constraint(equalTo: textFieldShadowContainer.leadingAnchor), - textField.trailingAnchor.constraint(equalTo: textFieldShadowContainer.trailingAnchor), - textField.bottomAnchor.constraint(equalTo: textFieldShadowContainer.bottomAnchor), - textField.heightAnchor.constraint(equalToConstant: MastodonRegisterTextFieldTableViewCell.textFieldHeight).priority(.required - 1), - ]) - - resetTextField() - } - -} - -extension MastodonRegisterTextFieldTableViewCell { - func resetTextField() { - textField.keyboardType = .default - textField.autocorrectionType = .default - textField.autocapitalizationType = .none - textField.attributedPlaceholder = nil - textField.isSecureTextEntry = false - textField.textAlignment = .natural - textField.semanticContentAttribute = .unspecified - - let paddingRect = CGRect(x: 0, y: 0, width: 16, height: 10) - textField.leftView = UIView(frame: paddingRect) - textField.leftViewMode = .always - textField.rightView = UIView(frame: paddingRect) - textField.rightViewMode = .always - } - - func setupTextViewRightView(text: String) { - textField.rightView = { - let containerView = UIView() - - let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: MastodonRegisterTextFieldTableViewCell.textFieldHeight)) - paddingView.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(paddingView) - NSLayoutConstraint.activate([ - paddingView.topAnchor.constraint(equalTo: containerView.topAnchor), - paddingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - paddingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), - paddingView.widthAnchor.constraint(equalToConstant: 8).priority(.defaultHigh), - ]) - - let label = UILabel() - label.font = MastodonRegisterTextFieldTableViewCell.textFieldLabelFont - label.textColor = Asset.Colors.Label.primary.color - label.text = text - label.lineBreakMode = .byTruncatingMiddle - - label.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(label) - NSLayoutConstraint.activate([ - label.topAnchor.constraint(equalTo: containerView.topAnchor), - label.leadingAnchor.constraint(equalTo: paddingView.trailingAnchor), - containerView.trailingAnchor.constraint(equalTo: label.trailingAnchor, constant: 16), - label.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), - label.widthAnchor.constraint(lessThanOrEqualToConstant: 180).priority(.required - 1), - ]) - return containerView - }() - } - - func setupTextViewPlaceholder(text: String) { - textField.attributedPlaceholder = NSAttributedString( - string: text, - attributes: [ - .foregroundColor: Asset.Colors.Label.secondary.color, - .font: MastodonRegisterTextFieldTableViewCell.textFieldLabelFont - ] - ) - } -} diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel+Diffable.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel+Diffable.swift index f1e0d8903..b3e2a2cdb 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel+Diffable.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel+Diffable.swift @@ -10,146 +10,6 @@ import Combine import MastodonAsset import MastodonLocalization -extension MastodonRegisterViewModel { - //TODO: @zeitschlag Remove, as the screen is based on SwiftUI now - func setupDiffableDataSource( - tableView: UITableView - ) { - tableView.register(OnboardingHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: OnboardingHeadlineTableViewCell.self)) - tableView.register(MastodonRegisterAvatarTableViewCell.self, forCellReuseIdentifier: String(describing: MastodonRegisterAvatarTableViewCell.self)) - tableView.register(MastodonRegisterTextFieldTableViewCell.self, forCellReuseIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self)) - tableView.register(MastodonRegisterPasswordHintTableViewCell.self, forCellReuseIdentifier: String(describing: MastodonRegisterPasswordHintTableViewCell.self)) - - diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in - switch item { - case .header(let domain): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: OnboardingHeadlineTableViewCell.self), for: indexPath) as! OnboardingHeadlineTableViewCell - cell.titleLabel.text = L10n.Scene.Register.letsGetYouSetUpOnDomain(domain) - cell.subTitleLabel.isHidden = true - return cell - case .avatar: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterAvatarTableViewCell.self), for: indexPath) as! MastodonRegisterAvatarTableViewCell - self.configureAvatar(cell: cell) - return cell - case .name: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell - cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.DisplayName.placeholder) - cell.textField.keyboardType = .default - cell.textField.autocapitalizationType = .words - cell.textField.text = self.name - NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField) - .receive(on: DispatchQueue.main) - .compactMap { notification in - guard let textField = notification.object as? UITextField else { - assertionFailure() - return nil - } - return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - } - .assign(to: \.name, on: self) - .store(in: &cell.disposeBag) - return cell - case .username: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell - cell.setupTextViewRightView(text: "@" + self.domain) - cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Username.placeholder) - cell.textField.keyboardType = .alphabet - cell.textField.autocorrectionType = .no - cell.textField.text = self.username - cell.textField.textAlignment = .left - cell.textField.semanticContentAttribute = .forceLeftToRight - NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField) - .receive(on: DispatchQueue.main) - .compactMap { notification in - guard let textField = notification.object as? UITextField else { - assertionFailure() - return nil - } - return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - } - .assign(to: \.username, on: self) - .store(in: &cell.disposeBag) - self.configureTextFieldCell(cell: cell, validateState: self.$usernameValidateState) - return cell - case .email: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell - cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Email.placeholder) - cell.textField.keyboardType = .emailAddress - cell.textField.autocorrectionType = .no - cell.textField.text = self.email - NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField) - .receive(on: DispatchQueue.main) - .compactMap { notification in - guard let textField = notification.object as? UITextField else { - assertionFailure() - return nil - } - return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - } - .assign(to: \.email, on: self) - .store(in: &cell.disposeBag) - self.configureTextFieldCell(cell: cell, validateState: self.$emailValidateState) - return cell - case .password: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell - cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Password.placeholder) - cell.textField.keyboardType = .alphabet - cell.textField.autocorrectionType = .no - cell.textField.isSecureTextEntry = true - cell.textField.text = self.password - cell.textField.textAlignment = .left - cell.textField.semanticContentAttribute = .forceLeftToRight - NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField) - .receive(on: DispatchQueue.main) - .compactMap { notification in - guard let textField = notification.object as? UITextField else { - assertionFailure() - return nil - } - return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - } - .assign(to: \.password, on: self) - .store(in: &cell.disposeBag) - self.configureTextFieldCell(cell: cell, validateState: self.$passwordValidateState) - return cell - case .hint: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterPasswordHintTableViewCell.self), for: indexPath) as! MastodonRegisterPasswordHintTableViewCell - return cell - case .reason: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell - cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Invite.registrationUserInviteRequest) - cell.textField.keyboardType = .default - cell.textField.text = self.reason - NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField) - .receive(on: DispatchQueue.main) - .compactMap { notification in - guard let textField = notification.object as? UITextField else { - assertionFailure() - return nil - } - return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - } - .assign(to: \.reason, on: self) - .store(in: &cell.disposeBag) - self.configureTextFieldCell(cell: cell, validateState: self.$reasonValidateState) - return cell - default: - assertionFailure() - return UITableViewCell() - } - } - - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - snapshot.appendItems([.header(domain: domain)], toSection: .main) - snapshot.appendItems([.avatar, .name, .username, .email, .password, .hint], toSection: .main) - if approvalRequired { - snapshot.appendItems([.reason], toSection: .main) - } - diffableDataSource?.applySnapshot(snapshot, animated: false, completion: nil) - } -} - extension MastodonRegisterViewModel { private func configureAvatar(cell: MastodonRegisterAvatarTableViewCell) { self.$avatarImage From 187bda7ef4ce5f257e95777d4719729bb6e3865b Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 9 Nov 2022 16:41:27 +0100 Subject: [PATCH 505/658] Update strings (#540) --- Localization/app.json | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 30566d8d6..8287a00a5 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -74,8 +74,8 @@ "take_photo": "Take Photo", "save_photo": "Save Photo", "copy_photo": "Copy Photo", - "sign_in": "Sign In", - "sign_up": "Sign Up", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "See More", "preview": "Preview", "share": "Share", @@ -220,8 +220,7 @@ }, "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.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "All", @@ -248,8 +247,7 @@ "category": "CATEGORY" }, "input": { - "placeholder": "Search servers", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Finding available servers...", From 367e759f5003429488cd041ea89ba9a27b15b2c0 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 9 Nov 2022 17:07:06 +0100 Subject: [PATCH 506/658] Use border instead of shadow for validation results (#540) --- .../Register/MastodonRegisterView.swift | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterView.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterView.swift index 787b79bd8..4f28353b0 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterView.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterView.swift @@ -168,22 +168,27 @@ struct MastodonRegisterView: View { func body(content: Content) -> some View { ZStack { - let shadowColor: Color = { + let borderColor: Color = { switch validateState { - case .empty: return .black.opacity(0.125) - case .invalid: return Color(Asset.Colors.TextField.invalid.color) - case .valid: return Color(Asset.Colors.TextField.valid.color) + case .empty: return Color(Asset.Scene.Onboarding.textFieldBackground.color) + case .invalid: return Color(Asset.Colors.TextField.invalid.color) + case .valid: return Color(Asset.Scene.Onboarding.textFieldBackground.color) } }() - //TODO: @zeitschlag Replace shadow with border + Color(Asset.Scene.Onboarding.textFieldBackground.color) .cornerRadius(10) - .shadow(color: shadowColor, radius: 1, x: 0, y: 2) - .animation(.easeInOut, value: validateState) + .shadow(color: .black.opacity(0.125), radius: 1, x: 0, y: 2) + content .padding() .background(Color(Asset.Scene.Onboarding.textFieldBackground.color)) .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(borderColor, lineWidth: 1) + .animation(.easeInOut, value: validateState) + ) } } } From 47401b09408c250d46c0b0e69150b977ba1f0633 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 9 Nov 2022 22:41:54 +0100 Subject: [PATCH 507/658] Add ViewController (#540) --- Mastodon.xcodeproj/project.pbxproj | 12 + .../xcshareddata/swiftpm/Package.resolved | 257 ------------------ Mastodon/Coordinator/SceneCoordinator.swift | 5 +- .../Login/MastodonLoginViewController.swift | 25 ++ .../MastodonPickServerViewController.swift | 3 +- 5 files changed, 43 insertions(+), 259 deletions(-) delete mode 100644 Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index f622c97da..016158b58 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -88,6 +88,7 @@ 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 */; }; C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24C97022922F30500BAE8CB /* RefreshControl.swift */; }; + D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.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 */; }; @@ -608,6 +609,7 @@ 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 = ""; }; 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 = ""; }; + D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginViewController.swift; sourceTree = ""; }; DB0009A826AEE5DC009B9D2D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = ""; }; DB0009AD26AEE5E4009B9D2D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Intents.strings; sourceTree = ""; }; DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; @@ -1494,9 +1496,18 @@ path = Bookmark; sourceTree = ""; }; + D8A6AB68291C50F3003AB663 /* Login */ = { + isa = PBXGroup; + children = ( + D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */, + ); + path = Login; + sourceTree = ""; + }; DB01409B25C40BB600F9F3CF /* Onboarding */ = { isa = PBXGroup; children = ( + D8A6AB68291C50F3003AB663 /* Login */, DB68A03825E900CC00CFDF14 /* Share */, 0FAA0FDD25E0B5700017CCDE /* Welcome */, 0FAA102525E1125D0017CCDE /* PickServer */, @@ -3422,6 +3433,7 @@ DB697DDB278F4DE3004EF2F7 /* DataSourceProvider+StatusTableViewCellDelegate.swift in Sources */, DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */, DBB45B5627B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift in Sources */, + D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */, DB0FCB8027968F70006C02E2 /* MastodonStatusThreadViewModel.swift in Sources */, DB67D08627312E67006A36CF /* WizardViewController.swift in Sources */, DB6746EB278ED8B0008A6B94 /* PollOptionView+Configuration.swift in Sources */, diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 409b8820d..000000000 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,257 +0,0 @@ -{ - "pins" : [ - { - "identity" : "alamofire", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Alamofire/Alamofire.git", - "state" : { - "revision" : "354dda32d89fc8cd4f5c46487f64957d355f53d8", - "version" : "5.6.1" - } - }, - { - "identity" : "alamofireimage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Alamofire/AlamofireImage.git", - "state" : { - "revision" : "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10", - "version" : "4.2.0" - } - }, - { - "identity" : "commonoslog", - "kind" : "remoteSourceControl", - "location" : "https://github.com/MainasuK/CommonOSLog", - "state" : { - "revision" : "c121624a30698e9886efe38aebb36ff51c01b6c2", - "version" : "0.1.1" - } - }, - { - "identity" : "faviconfinder", - "kind" : "remoteSourceControl", - "location" : "https://github.com/will-lumley/FaviconFinder.git", - "state" : { - "revision" : "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a", - "version" : "3.3.0" - } - }, - { - "identity" : "flanimatedimage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Flipboard/FLAnimatedImage.git", - "state" : { - "revision" : "e7f9fd4681ae41bf6f3056db08af4f401d61da52", - "version" : "1.0.16" - } - }, - { - "identity" : "fpsindicator", - "kind" : "remoteSourceControl", - "location" : "https://github.com/MainasuK/FPSIndicator.git", - "state" : { - "revision" : "e4a5067ccd5293b024c767f09e51056afd4a4796", - "version" : "1.1.0" - } - }, - { - "identity" : "fuzi", - "kind" : "remoteSourceControl", - "location" : "https://github.com/cezheng/Fuzi.git", - "state" : { - "revision" : "f08c8323da21e985f3772610753bcfc652c2103f", - "version" : "3.1.3" - } - }, - { - "identity" : "keychainaccess", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", - "state" : { - "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", - "version" : "4.2.2" - } - }, - { - "identity" : "kingfisher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/onevcat/Kingfisher.git", - "state" : { - "revision" : "44e891bdb61426a95e31492a67c7c0dfad1f87c5", - "version" : "7.4.1" - } - }, - { - "identity" : "metatextkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/TwidereProject/MetaTextKit.git", - "state" : { - "revision" : "dcd5255d6930c2fab408dc8562c577547e477624", - "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", - "location" : "https://github.com/kean/Nuke.git", - "state" : { - "revision" : "0ea7545b5c918285aacc044dc75048625c8257cc", - "version" : "10.8.0" - } - }, - { - "identity" : "nuke-flanimatedimage-plugin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", - "state" : { - "revision" : "b59c346a7d536336db3b0f12c72c6e53ee709e16", - "version" : "8.0.0" - } - }, - { - "identity" : "pageboy", - "kind" : "remoteSourceControl", - "location" : "https://github.com/uias/Pageboy", - "state" : { - "revision" : "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6", - "version" : "3.6.2" - } - }, - { - "identity" : "panmodal", - "kind" : "remoteSourceControl", - "location" : "https://github.com/slackhq/PanModal.git", - "state" : { - "revision" : "b012aecb6b67a8e46369227f893c12544846613f", - "version" : "1.2.7" - } - }, - { - "identity" : "sdwebimage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SDWebImage/SDWebImage.git", - "state" : { - "revision" : "2e63d0061da449ad0ed130768d05dceb1496de44", - "version" : "5.12.5" - } - }, - { - "identity" : "stripes", - "kind" : "remoteSourceControl", - "location" : "https://github.com/eneko/Stripes.git", - "state" : { - "revision" : "d533fd44b8043a3abbf523e733599173d6f98c11", - "version" : "0.2.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections.git", - "state" : { - "revision" : "f504716c27d2e5d4144fa4794b12129301d17729", - "version" : "1.0.3" - } - }, - { - "identity" : "swift-nio", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio.git", - "state" : { - "revision" : "546610d52b19be3e19935e0880bb06b9c03f5cef", - "version" : "1.14.4" - } - }, - { - "identity" : "swift-nio-zlib-support", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio-zlib-support.git", - "state" : { - "revision" : "37760e9a52030bb9011972c5213c3350fa9d41fd", - "version" : "1.0.0" - } - }, - { - "identity" : "swiftsoup", - "kind" : "remoteSourceControl", - "location" : "https://github.com/scinfu/SwiftSoup.git", - "state" : { - "revision" : "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886", - "version" : "2.4.2" - } - }, - { - "identity" : "swiftui-introspect", - "kind" : "remoteSourceControl", - "location" : "https://github.com/siteline/SwiftUI-Introspect.git", - "state" : { - "revision" : "f2616860a41f9d9932da412a8978fec79c06fe24", - "version" : "0.1.4" - } - }, - { - "identity" : "tabbarpager", - "kind" : "remoteSourceControl", - "location" : "https://github.com/TwidereProject/TabBarPager.git", - "state" : { - "revision" : "488aa66d157a648901b61721212c0dec23d27ee5", - "version" : "0.1.0" - } - }, - { - "identity" : "tabman", - "kind" : "remoteSourceControl", - "location" : "https://github.com/uias/Tabman", - "state" : { - "revision" : "4a4f7c755b875ffd4f9ef10d67a67883669d2465", - "version" : "2.13.0" - } - }, - { - "identity" : "thirdpartymailer", - "kind" : "remoteSourceControl", - "location" : "https://github.com/vtourraine/ThirdPartyMailer.git", - "state" : { - "revision" : "44c1cfaa6969963f22691aa67f88a69e3b6d651f", - "version" : "2.1.0" - } - }, - { - "identity" : "tocropviewcontroller", - "kind" : "remoteSourceControl", - "location" : "https://github.com/TimOliver/TOCropViewController.git", - "state" : { - "revision" : "d0470491f56e734731bbf77991944c0dfdee3e0e", - "version" : "2.6.1" - } - }, - { - "identity" : "uihostingconfigurationbackport", - "kind" : "remoteSourceControl", - "location" : "https://github.com/woxtu/UIHostingConfigurationBackport.git", - "state" : { - "revision" : "6091f2d38faa4b24fc2ca0389c651e2f666624a3", - "version" : "0.1.0" - } - }, - { - "identity" : "uitextview-placeholder", - "kind" : "remoteSourceControl", - "location" : "https://github.com/MainasuK/UITextView-Placeholder.git", - "state" : { - "revision" : "20f513ded04a040cdf5467f0891849b1763ede3b", - "version" : "1.4.1" - } - } - ], - "version" : 2 -} diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index eca818fe5..0bf1c476e 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -149,7 +149,7 @@ extension SceneCoordinator { case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel) case mastodonResendEmail(viewModel: MastodonResendEmailViewModel) case mastodonWebView(viewModel: WebViewModel) - //TODO: @zeitschlag new case for welcome back select your server welcome-screen + Screen and ViewModel etc. + case mastodonLogin // search case searchDetail(viewModel: SearchDetailViewModel) @@ -200,6 +200,7 @@ extension SceneCoordinator { case .welcome, .mastodonPickServer, .mastodonRegister, + .mastodonLogin, .mastodonServerRules, .mastodonConfirmEmail, .mastodonResendEmail: @@ -404,6 +405,8 @@ private extension SceneCoordinator { let _viewController = MastodonConfirmEmailViewController() _viewController.viewModel = viewModel viewController = _viewController + case .mastodonLogin: + viewController = MastodonLoginViewController() case .mastodonResendEmail(let viewModel): let _viewController = MastodonResendEmailViewController() _viewController.viewModel = viewModel diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift new file mode 100644 index 000000000..9a73317be --- /dev/null +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -0,0 +1,25 @@ +// +// MastodonLoginViewController.swift +// Mastodon +// +// Created by Nathan Mattes on 09.11.22. +// + +import UIKit + +class MastodonLoginViewController: UIViewController { + + // Title, Subtitle + // SearchBox, queries api.joinmastodon.org/servers with domain + // List with (filtered) domains + // back-button, next-button (enabled if user selectes a server or url is valid + // next-button does MastodonPickServerViewController.doSignIn() + + init() { + super.init(nibName: nil, bundle: nil) + view.backgroundColor = .systemGreen + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } +} + diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index a3779dfe6..7ec24a246 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -275,7 +275,8 @@ extension MastodonPickServerViewController { case .signUp: doSignUp() } } - + + //TODO: @zeitschlag Move to MastodonLoginViewController private func doSignIn() { guard let server = viewModel.selectedServer.value else { return } authenticationViewModel.isAuthenticating.send(true) From 9c220a6abc193a9c0cffe2da8a21f0d407507738 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 10 Nov 2022 17:52:48 +0100 Subject: [PATCH 508/658] Add navigation-view (#540) --- Mastodon.xcodeproj/project.pbxproj | 4 ++ Mastodon/Coordinator/SceneCoordinator.swift | 18 ++++- .../Onboarding/Login/MastodonLoginView.swift | 44 +++++++++++++ .../Login/MastodonLoginViewController.swift | 66 +++++++++++++++---- .../MastodonRegisterViewController.swift | 2 +- .../Welcome/WelcomeViewController.swift | 3 +- 6 files changed, 118 insertions(+), 19 deletions(-) create mode 100644 Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 016158b58..9755bb0c7 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -88,6 +88,7 @@ 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 */; }; C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24C97022922F30500BAE8CB /* RefreshControl.swift */; }; + D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; }; D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.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 */; }; @@ -609,6 +610,7 @@ 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 = ""; }; 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 = ""; }; + D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginView.swift; sourceTree = ""; }; D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginViewController.swift; sourceTree = ""; }; DB0009A826AEE5DC009B9D2D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = ""; }; DB0009AD26AEE5E4009B9D2D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Intents.strings; sourceTree = ""; }; @@ -1500,6 +1502,7 @@ isa = PBXGroup; children = ( D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */, + D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */, ); path = Login; sourceTree = ""; @@ -3372,6 +3375,7 @@ DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */, DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */, DB3EA8E9281B7A3700598866 /* DiscoveryCommunityViewModel.swift in Sources */, + D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */, DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */, DBEFCD71282A12B200C0ABEA /* ReportReasonViewController.swift in Sources */, DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 0bf1c476e..bf663b7d8 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -406,7 +406,10 @@ private extension SceneCoordinator { _viewController.viewModel = viewModel viewController = _viewController case .mastodonLogin: - viewController = MastodonLoginViewController() + let loginViewController = MastodonLoginViewController() + loginViewController.delegate = self + + viewController = loginViewController case .mastodonResendEmail(let viewModel): let _viewController = MastodonResendEmailViewController() _viewController.viewModel = viewModel @@ -533,5 +536,16 @@ private extension SceneCoordinator { needs?.context = appContext needs?.coordinator = self } - +} + +//MARK: - MastodonLoginViewControllerDelegate + +extension SceneCoordinator: MastodonLoginViewControllerDelegate { + func backButtonPressed(_ viewController: MastodonLoginViewController) { + viewController.navigationController?.popViewController(animated: true) + } + + func nextButtonPressed(_ viewController: MastodonLoginViewController) { + //TODO: @zeitschlag implement, show ASWebAuthentication and stuff + } } diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift new file mode 100644 index 000000000..88a519d98 --- /dev/null +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift @@ -0,0 +1,44 @@ +// +// MastodonLoginView.swift +// Mastodon +// +// Created by Nathan Mattes on 10.11.22. +// + +import UIKit +import MastodonAsset + +class MastodonLoginView: UIView { + + // Title, Subtitle + // SearchBox, queries api.joinmastodon.org/servers with domain + // List with (filtered) domains + + let navigationActionView: NavigationActionView + + override init(frame: CGRect) { + navigationActionView = NavigationActionView() + navigationActionView.translatesAutoresizingMaskIntoConstraints = false + + super.init(frame: frame) + + addSubview(navigationActionView) + backgroundColor = .systemBackground + + setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupConstraints() { + let constraints = [ + navigationActionView.leadingAnchor.constraint(equalTo: leadingAnchor), + navigationActionView.trailingAnchor.constraint(equalTo: trailingAnchor), + bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor), + ] + NSLayoutConstraint.activate(constraints) + } + +} diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 9a73317be..7a20170e2 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -7,19 +7,57 @@ import UIKit -class MastodonLoginViewController: UIViewController { - - // Title, Subtitle - // SearchBox, queries api.joinmastodon.org/servers with domain - // List with (filtered) domains - // back-button, next-button (enabled if user selectes a server or url is valid - // next-button does MastodonPickServerViewController.doSignIn() - - init() { - super.init(nibName: nil, bundle: nil) - view.backgroundColor = .systemGreen - } - - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } +protocol MastodonLoginViewControllerDelegate: AnyObject { + func backButtonPressed(_ viewController: MastodonLoginViewController) + func nextButtonPressed(_ viewController: MastodonLoginViewController) } +class MastodonLoginViewController: UIViewController { + + // back-button, next-button (enabled if user selectes a server or url is valid + // next-button does MastodonPickServerViewController.doSignIn() + + weak var delegate: MastodonLoginViewControllerDelegate? + + var contentView: MastodonLoginView { + view as! MastodonLoginView + } + + init() { + super.init(nibName: nil, bundle: nil) + + navigationItem.hidesBackButton = true + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func loadView() { + let loginView = MastodonLoginView() + + loginView.navigationActionView.nextButton.addTarget(self, action: #selector(MastodonLoginViewController.nextButtonPressed(_:)), for: .touchUpInside) + loginView.navigationActionView.backButton.addTarget(self, action: #selector(MastodonLoginViewController.backButtonPressed(_:)), for: .touchUpInside) + + view = loginView + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupOnboardingAppearance() + } + + //MARK: - Actions + + @objc func backButtonPressed(_ sender: Any) { + delegate?.backButtonPressed(self) + } + + @objc func nextButtonPressed(_ sender: Any) { + delegate?.nextButtonPressed(self) + } +} + +// MARK: - OnboardingViewControllerAppearance +extension MastodonLoginViewController: OnboardingViewControllerAppearance { } + + diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index 758c20888..dc5152f98 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -322,7 +322,7 @@ extension MastodonRegisterViewController { ) }() let viewModel = MastodonConfirmEmailViewModel(context: self.context, email: email, authenticateInfo: self.viewModel.authenticateInfo, userToken: userToken, updateCredentialQuery: updateCredentialQuery) - self.coordinator.present(scene: .mastodonConfirmEmail(viewModel: viewModel), from: self, transition: .show) + _ = self.coordinator.present(scene: .mastodonConfirmEmail(viewModel: viewModel), from: self, transition: .show) } .store(in: &disposeBag) } diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift index 612189243..36f9bf7e4 100644 --- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift +++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift @@ -322,8 +322,7 @@ extension WelcomeViewController { @objc private func signInButtonDidClicked(_ sender: UIButton) { - //TODO: @zeitschlag Present new login-scene - _ = coordinator.present(scene: .mastodonPickServer(viewMode: MastodonPickServerViewModel(context: context, mode: .signIn)), from: self, transition: .show) + _ = coordinator.present(scene: .mastodonLogin, from: self, transition: .show) } @objc From e05b595aed7e0db0e2fdce27e77af5465d8974bf Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 10 Nov 2022 17:59:28 +0100 Subject: [PATCH 509/658] Remove obsolete package --- MastodonSDK/Package.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/MastodonSDK/Package.swift b/MastodonSDK/Package.swift index 7a4201894..1ac22e6a7 100644 --- a/MastodonSDK/Package.swift +++ b/MastodonSDK/Package.swift @@ -32,7 +32,6 @@ let package = Package( .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"), .package(url: "https://github.com/apple/swift-nio.git", from: "1.0.0"), - .package(url: "https://github.com/cezheng/Fuzi.git", from: "3.1.3"), .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"), .package(url: "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", from: "8.0.0"), .package(url: "https://github.com/kean/Nuke.git", from: "10.3.1"), From 38317d8fa699080566758a85f47fc73bfb14e0b3 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 10 Nov 2022 18:32:00 +0100 Subject: [PATCH 510/658] Add title and subtitle (#540) --- .../Onboarding/Login/MastodonLoginView.swift | 28 ++++++++++++++++++- .../Login/MastodonLoginViewController.swift | 1 + 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift index 88a519d98..8f3ab1beb 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift @@ -10,18 +10,39 @@ import MastodonAsset class MastodonLoginView: UIView { - // Title, Subtitle // SearchBox, queries api.joinmastodon.org/servers with domain // List with (filtered) domains + let titleLabel: UILabel + let subtitleLabel: UILabel + private let headerStackView: UIStackView let navigationActionView: NavigationActionView override init(frame: CGRect) { + + titleLabel = UILabel() + titleLabel.font = MastodonLoginViewController.largeTitleFont + titleLabel.textColor = MastodonLoginViewController.largeTitleTextColor + titleLabel.text = "Welcome Back" //TODO: @zeitschlag localization + titleLabel.numberOfLines = 0 + + subtitleLabel = UILabel() + subtitleLabel.font = MastodonLoginViewController.subTitleFont + subtitleLabel.textColor = MastodonLoginViewController.subTitleTextColor + subtitleLabel.text = "Log you in with the server where you created your account" //TODO: @zeitschlag localization + subtitleLabel.numberOfLines = 0 + + headerStackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel]) + headerStackView.axis = .vertical + headerStackView.spacing = 16 + headerStackView.translatesAutoresizingMaskIntoConstraints = false + navigationActionView = NavigationActionView() navigationActionView.translatesAutoresizingMaskIntoConstraints = false super.init(frame: frame) + addSubview(headerStackView) addSubview(navigationActionView) backgroundColor = .systemBackground @@ -34,6 +55,11 @@ class MastodonLoginView: UIView { private func setupConstraints() { let constraints = [ + + headerStackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + headerStackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), + headerStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), + navigationActionView.leadingAnchor.constraint(equalTo: leadingAnchor), navigationActionView.trailingAnchor.constraint(equalTo: trailingAnchor), bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor), diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 7a20170e2..f0c47885f 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -43,6 +43,7 @@ class MastodonLoginViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + defer { setupNavigationBarBackgroundView() } setupOnboardingAppearance() } From 08302cf2fab64d0fc47b1108960506703adcc6b6 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 11 Nov 2022 18:44:01 +0100 Subject: [PATCH 511/658] Move some code around (#540) and remove an obsolete property --- .../Login/MastodonLoginViewController.swift | 46 ++++++++++++++++ .../MastodonPickServerViewController.swift | 54 ------------------- .../MastodonPickServerViewModel.swift | 15 ++---- .../Welcome/WelcomeViewController.swift | 2 +- 4 files changed, 50 insertions(+), 67 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index f0c47885f..5797c2ae2 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -56,6 +56,52 @@ class MastodonLoginViewController: UIViewController { @objc func nextButtonPressed(_ sender: Any) { delegate?.nextButtonPressed(self) } + + @objc func login(_ sender: Any) { +// guard let server = viewModel.selectedServer.value else { return } +// authenticationViewModel.isAuthenticating.send(true) +// context.apiService.createApplication(domain: server.domain) +// .tryMap { response -> AuthenticationViewModel.AuthenticateInfo in +// let application = response.value +// guard let info = AuthenticationViewModel.AuthenticateInfo( +// domain: server.domain, +// application: application, +// redirectURI: response.value.redirectURI ?? APIService.oauthCallbackURL +// ) else { +// throw APIService.APIError.explicit(.badResponse) +// } +// return info +// } +// .receive(on: DispatchQueue.main) +// .sink { [weak self] completion in +// guard let self = self else { return } +// self.authenticationViewModel.isAuthenticating.send(false) +// +// switch completion { +// case .failure(let error): +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign in fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) +// self.viewModel.error.send(error) +// case .finished: +// break +// } +// } receiveValue: { [weak self] info in +// guard let self = self else { return } +// let authenticationController = MastodonAuthenticationController( +// context: self.context, +// authenticateURL: info.authorizeURL +// ) +// +// self.mastodonAuthenticationController = authenticationController +// authenticationController.authenticationSession?.presentationContextProvider = self +// authenticationController.authenticationSession?.start() +// +// self.authenticationViewModel.authenticate( +// info: info, +// pinCodePublisher: authenticationController.pinCodePublisher +// ) +// } +// .store(in: &disposeBag) + } } // MARK: - OnboardingViewControllerAppearance diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 7ec24a246..2fa46cac9 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -270,60 +270,6 @@ extension MastodonPickServerViewController { } @objc private func nextButtonDidPressed(_ sender: UIButton) { - switch viewModel.mode { - case .signIn: doSignIn() - case .signUp: doSignUp() - } - } - - //TODO: @zeitschlag Move to MastodonLoginViewController - private func doSignIn() { - guard let server = viewModel.selectedServer.value else { return } - authenticationViewModel.isAuthenticating.send(true) - context.apiService.createApplication(domain: server.domain) - .tryMap { response -> AuthenticationViewModel.AuthenticateInfo in - let application = response.value - guard let info = AuthenticationViewModel.AuthenticateInfo( - domain: server.domain, - application: application, - redirectURI: response.value.redirectURI ?? APIService.oauthCallbackURL - ) else { - throw APIService.APIError.explicit(.badResponse) - } - return info - } - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self = self else { return } - self.authenticationViewModel.isAuthenticating.send(false) - - switch completion { - case .failure(let error): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign in fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) - self.viewModel.error.send(error) - case .finished: - break - } - } receiveValue: { [weak self] info in - guard let self = self else { return } - let authenticationController = MastodonAuthenticationController( - context: self.context, - authenticateURL: info.authorizeURL - ) - - self.mastodonAuthenticationController = authenticationController - authenticationController.authenticationSession?.presentationContextProvider = self - authenticationController.authenticationSession?.start() - - self.authenticationViewModel.authenticate( - info: info, - pinCodePublisher: authenticationController.pinCodePublisher - ) - } - .store(in: &disposeBag) - } - - private func doSignUp() { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) guard let server = viewModel.selectedServer.value else { return } authenticationViewModel.isAuthenticating.send(true) diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift index 719de4cb4..ff6806b71 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift @@ -17,12 +17,7 @@ import MastodonCore import MastodonUI class MastodonPickServerViewModel: NSObject { - - enum PickServerMode { - case signUp - case signIn - } - + enum EmptyStateViewState { case none case loading @@ -34,7 +29,6 @@ class MastodonPickServerViewModel: NSObject { let serverSectionHeaderView = PickServerServerSectionTableHeaderView() // input - let mode: PickServerMode let context: AppContext var categoryPickerItems: [CategoryPickerItem] = { var items: [CategoryPickerItem] = [] @@ -72,9 +66,8 @@ class MastodonPickServerViewModel: NSObject { let loadingIndexedServersError = CurrentValueSubject(nil) let emptyStateViewState = CurrentValueSubject(.none) - init(context: AppContext, mode: PickServerMode) { + init(context: AppContext) { self.context = context - self.mode = mode super.init() configure() @@ -115,9 +108,7 @@ extension MastodonPickServerViewModel { .map { indexedServers, selectCategoryItem, searchText -> [Mastodon.Entity.Server] in // ignore approval required servers when sign-up var indexedServers = indexedServers - if self.mode == .signUp { - indexedServers = indexedServers.filter { !$0.approvalRequired } - } + indexedServers = indexedServers.filter { !$0.approvalRequired } // Note: // sort by calculate last week users count // and make medium size (~800) server to top diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift index 36f9bf7e4..5a9af3dd9 100644 --- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift +++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift @@ -317,7 +317,7 @@ extension WelcomeViewController { extension WelcomeViewController { @objc private func signUpButtonDidClicked(_ sender: UIButton) { - _ = coordinator.present(scene: .mastodonPickServer(viewMode: MastodonPickServerViewModel(context: context, mode: .signUp)), from: self, transition: .show) + _ = coordinator.present(scene: .mastodonPickServer(viewMode: MastodonPickServerViewModel(context: context)), from: self, transition: .show) } @objc From cc6ec42c5cdb8cc55cbdab37452bd12ad1e1c244 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 11 Nov 2022 18:46:06 +0100 Subject: [PATCH 512/658] Add TextField and TableView (#540) tableView is green for now, cell's are coming next --- .../Onboarding/Login/MastodonLoginView.swift | 27 +++++++++++++++++++ .../Login/MastodonLoginViewController.swift | 7 +++++ 2 files changed, 34 insertions(+) diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift index 8f3ab1beb..b62fe25c2 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift @@ -16,6 +16,8 @@ class MastodonLoginView: UIView { let titleLabel: UILabel let subtitleLabel: UILabel private let headerStackView: UIStackView + let searchTextField: UITextField + let tableView: UITableView let navigationActionView: NavigationActionView override init(frame: CGRect) { @@ -37,12 +39,27 @@ class MastodonLoginView: UIView { headerStackView.spacing = 16 headerStackView.translatesAutoresizingMaskIntoConstraints = false + searchTextField = UITextField() + searchTextField.translatesAutoresizingMaskIntoConstraints = false + searchTextField.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color + searchTextField.layer.cornerRadius = 10 //TODO: Change mask for + searchTextField.placeholder = "Search for your server" + searchTextField.leftView = UIImageView(image: UIImage(systemName: "magnifyingglass")) + searchTextField.leftViewMode = .always + + tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .green + //TODO: @zeitchlag Cell + navigationActionView = NavigationActionView() navigationActionView.translatesAutoresizingMaskIntoConstraints = false super.init(frame: frame) addSubview(headerStackView) + addSubview(searchTextField) + addSubview(tableView) addSubview(navigationActionView) backgroundColor = .systemBackground @@ -60,6 +77,16 @@ class MastodonLoginView: UIView { headerStackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), headerStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), + searchTextField.topAnchor.constraint(equalTo: headerStackView.bottomAnchor, constant: 32), + searchTextField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + searchTextField.heightAnchor.constraint(equalToConstant: 55), + trailingAnchor.constraint(equalTo: searchTextField.trailingAnchor, constant: 16), + + tableView.topAnchor.constraint(equalTo: searchTextField.bottomAnchor), + tableView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + trailingAnchor.constraint(equalTo: tableView.trailingAnchor, constant: 16), + tableView.bottomAnchor.constraint(equalTo: navigationActionView.topAnchor), + navigationActionView.leadingAnchor.constraint(equalTo: leadingAnchor), navigationActionView.trailingAnchor.constraint(equalTo: trailingAnchor), bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor), diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 5797c2ae2..993aec13d 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -36,6 +36,9 @@ class MastodonLoginViewController: UIViewController { loginView.navigationActionView.nextButton.addTarget(self, action: #selector(MastodonLoginViewController.nextButtonPressed(_:)), for: .touchUpInside) loginView.navigationActionView.backButton.addTarget(self, action: #selector(MastodonLoginViewController.backButtonPressed(_:)), for: .touchUpInside) + loginView.searchTextField.addTarget(self, action: #selector(MastodonLoginViewController.textfieldDidChange(_:)), for: .editingChanged) + + //TODO: Set tableView.delegate and tableView.dataSource view = loginView } @@ -102,6 +105,10 @@ class MastodonLoginViewController: UIViewController { // } // .store(in: &disposeBag) } + + @objc func textfieldDidChange(_ textField: UITextField) { + print(textField.text ?? "---") + } } // MARK: - OnboardingViewControllerAppearance From ea78f884ab5f8de1f307ae5bf2397ab459e52a1f Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sat, 12 Nov 2022 23:25:45 +0100 Subject: [PATCH 513/658] Download and show server list (#540) --- Mastodon.xcodeproj/project.pbxproj | 8 ++ Mastodon/Coordinator/SceneCoordinator.swift | 2 +- .../MastodonLoginServerTableViewCell.swift | 12 +++ .../Onboarding/Login/MastodonLoginView.swift | 1 - .../Login/MastodonLoginViewController.swift | 77 +++++++++++++++++-- .../Login/MastodonLoginViewModel.swift | 45 +++++++++++ .../Service/API/APIService+Onboarding.swift | 4 +- .../Entity/Mastodon+Entity+Server.swift | 4 +- 8 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 Mastodon/Scene/Onboarding/Login/MastodonLoginServerTableViewCell.swift create mode 100644 Mastodon/Scene/Onboarding/Login/MastodonLoginViewModel.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 9755bb0c7..fca6eb8b5 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -89,6 +89,8 @@ 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24C97022922F30500BAE8CB /* RefreshControl.swift */; }; D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; }; + D87BFC8D291EB81200FEE264 /* MastodonLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */; }; + D87BFC8F291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */; }; D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.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 */; }; @@ -611,6 +613,8 @@ 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 = ""; }; D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginView.swift; sourceTree = ""; }; + D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginViewModel.swift; sourceTree = ""; }; + D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginServerTableViewCell.swift; sourceTree = ""; }; D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginViewController.swift; sourceTree = ""; }; DB0009A826AEE5DC009B9D2D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = ""; }; DB0009AD26AEE5E4009B9D2D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Intents.strings; sourceTree = ""; }; @@ -1503,6 +1507,8 @@ children = ( D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */, D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */, + D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */, + D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */, ); path = Login; sourceTree = ""; @@ -3185,6 +3191,7 @@ DB5B54A32833BD1A00DEF8B2 /* UserListViewModel.swift in Sources */, DBF156DF2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift in Sources */, DB0617F1278413D00030EE79 /* PickServerServerSectionTableHeaderView.swift in Sources */, + D87BFC8F291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift in Sources */, DB0FCB7E27958957006C02E2 /* StatusThreadRootTableViewCell+ViewModel.swift in Sources */, DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */, DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */, @@ -3307,6 +3314,7 @@ DB0618012785732C0030EE79 /* ServerRulesTableViewCell.swift in Sources */, DB98EB5C27B10A730082E365 /* ReportSupplementaryViewModel.swift in Sources */, DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */, + D87BFC8D291EB81200FEE264 /* MastodonLoginViewModel.swift in Sources */, DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */, 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */, DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index bf663b7d8..34ac09050 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -406,7 +406,7 @@ private extension SceneCoordinator { _viewController.viewModel = viewModel viewController = _viewController case .mastodonLogin: - let loginViewController = MastodonLoginViewController() + let loginViewController = MastodonLoginViewController(appContext: appContext, authenticationViewModel: AuthenticationViewModel(context: appContext, coordinator: self, isAuthenticationExist: false)) loginViewController.delegate = self viewController = loginViewController diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginServerTableViewCell.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginServerTableViewCell.swift new file mode 100644 index 000000000..2fb599015 --- /dev/null +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginServerTableViewCell.swift @@ -0,0 +1,12 @@ +// +// MastodonLoginServerTableViewCell.swift +// Mastodon +// +// Created by Nathan Mattes on 11.11.22. +// + +import UIKit + +class MastodonLoginServerTableViewCell: UITableViewCell { + static let reuseIdentifier = "MastodonLoginServerTableViewCell" +} diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift index b62fe25c2..cb506a739 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift @@ -49,7 +49,6 @@ class MastodonLoginView: UIView { tableView = UITableView() tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.backgroundColor = .green //TODO: @zeitchlag Cell navigationActionView = NavigationActionView() diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 993aec13d..0c67c8705 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -6,25 +6,38 @@ // import UIKit +import MastodonSDK +import MastodonCore protocol MastodonLoginViewControllerDelegate: AnyObject { func backButtonPressed(_ viewController: MastodonLoginViewController) func nextButtonPressed(_ viewController: MastodonLoginViewController) } +enum MastodonLoginViewSection: Hashable { + case servers +} + class MastodonLoginViewController: UIViewController { - // back-button, next-button (enabled if user selectes a server or url is valid - // next-button does MastodonPickServerViewController.doSignIn() - weak var delegate: MastodonLoginViewControllerDelegate? + var dataSource: UITableViewDiffableDataSource? + let viewModel: MastodonLoginViewModel + let authenticationViewModel: AuthenticationViewModel + weak var appContext: AppContext? var contentView: MastodonLoginView { view as! MastodonLoginView } - init() { + init(appContext: AppContext, authenticationViewModel: AuthenticationViewModel) { + + viewModel = MastodonLoginViewModel(appContext: appContext) + self.authenticationViewModel = authenticationViewModel + self.appContext = appContext + super.init(nibName: nil, bundle: nil) + viewModel.delegate = self navigationItem.hidesBackButton = true } @@ -37,8 +50,9 @@ class MastodonLoginViewController: UIViewController { loginView.navigationActionView.nextButton.addTarget(self, action: #selector(MastodonLoginViewController.nextButtonPressed(_:)), for: .touchUpInside) loginView.navigationActionView.backButton.addTarget(self, action: #selector(MastodonLoginViewController.backButtonPressed(_:)), for: .touchUpInside) loginView.searchTextField.addTarget(self, action: #selector(MastodonLoginViewController.textfieldDidChange(_:)), for: .editingChanged) - - //TODO: Set tableView.delegate and tableView.dataSource + loginView.tableView.delegate = self + loginView.tableView.register(MastodonLoginServerTableViewCell.self, forCellReuseIdentifier: MastodonLoginServerTableViewCell.reuseIdentifier) + loginView.navigationActionView.nextButton.isEnabled = false view = loginView } @@ -46,10 +60,35 @@ class MastodonLoginViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + let dataSource = UITableViewDiffableDataSource(tableView: contentView.tableView) { [weak self] tableView, indexPath, itemIdentifier in + guard let cell = tableView.dequeueReusableCell(withIdentifier: MastodonLoginServerTableViewCell.reuseIdentifier, for: indexPath) as? MastodonLoginServerTableViewCell, + let self = self else { + fatalError("Wrong cell") + } + + let server = self.viewModel.serverList[indexPath.row] + var configuration = cell.defaultContentConfiguration() + configuration.text = server.domain + + cell.contentConfiguration = configuration + cell.accessoryType = .disclosureIndicator + + return cell + } + + contentView.tableView.dataSource = dataSource + self.dataSource = dataSource + defer { setupNavigationBarBackgroundView() } setupOnboardingAppearance() } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + viewModel.updateServers() + } + //MARK: - Actions @objc func backButtonPressed(_ sender: Any) { @@ -108,10 +147,36 @@ class MastodonLoginViewController: UIViewController { @objc func textfieldDidChange(_ textField: UITextField) { print(textField.text ?? "---") + //TODO: @zeitschlag filter server-list, update tableview + + contentView.navigationActionView.nextButton.isEnabled = false } } // MARK: - OnboardingViewControllerAppearance extension MastodonLoginViewController: OnboardingViewControllerAppearance { } +//MARK: - UITableViewDelegate +extension MastodonLoginViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let server = viewModel.serverList[indexPath.row] + viewModel.selectedServer = server + contentView.searchTextField.text = server.domain + //TODO: @zeitschlag filter server list + + contentView.navigationActionView.nextButton.isEnabled = true + tableView.deselectRow(at: indexPath, animated: true) + } +} + +extension MastodonLoginViewController: MastodonLoginViewModelDelegate { + func serversUpdated(_ viewModel: MastodonLoginViewModel) { + var snapshot = NSDiffableDataSourceSnapshot() + + snapshot.appendSections([MastodonLoginViewSection.servers]) + snapshot.appendItems(viewModel.serverList) + + dataSource?.applySnapshot(snapshot, animated: true) + } +} diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewModel.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewModel.swift new file mode 100644 index 000000000..68810dded --- /dev/null +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewModel.swift @@ -0,0 +1,45 @@ +// +// MastodonLoginViewModel.swift +// Mastodon +// +// Created by Nathan Mattes on 11.11.22. +// + +import Foundation +import MastodonSDK +import MastodonCore +import Combine + +protocol MastodonLoginViewModelDelegate: AnyObject { + func serversUpdated(_ viewModel: MastodonLoginViewModel) +} + +class MastodonLoginViewModel { + + var serverList: [Mastodon.Entity.Server] = [] + var selectedServer: Mastodon.Entity.Server? + + weak var appContext: AppContext? + weak var delegate: MastodonLoginViewModelDelegate? + var disposeBag = Set() + + init(appContext: AppContext) { + self.appContext = appContext + } + + func updateServers() { + appContext?.apiService.servers().sink(receiveCompletion: { [weak self] completion in + switch completion { + case .finished: + guard let self = self else { return } + + self.delegate?.serversUpdated(self) + case .failure(let error): + print(error) + } + }, receiveValue: { content in + let servers = content.value + self.serverList = servers + }).store(in: &disposeBag) + } +} diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Onboarding.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Onboarding.swift index 383763359..84104d838 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Onboarding.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Onboarding.swift @@ -12,8 +12,8 @@ import MastodonSDK extension APIService { public func servers( - language: String?, - category: String? + language: String? = nil, + category: String? = nil ) -> AnyPublisher, Error> { let query = Mastodon.API.Onboarding.ServersQuery(language: language, category: category) return Mastodon.API.Onboarding.servers(session: session, query: query) diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift index da1f0dfad..56b4cd327 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift @@ -9,7 +9,7 @@ import Foundation extension Mastodon.Entity { - public struct Server: Codable, Equatable { + public struct Server: Codable, Equatable, Hashable { public let domain: String public let version: String public let description: String @@ -22,7 +22,7 @@ extension Mastodon.Entity { public let approvalRequired: Bool public let language: String public let category: String - //TODO: @zeitschlag Is there a way to figure out in advance if a server accepts new registrations? Right now we'd have to query the server and it responts with a `AuthenticationViewModel.AuthenticationError.registrationClosed` + //TODO: @zeitschlag Is there a way to figure out in advance if a server accepts new registrations? Right now we'd have to query the server and it responds with a `AuthenticationViewModel.AuthenticationError.registrationClosed` enum CodingKeys: String, CodingKey { case domain From bdbcd128fa0d08b2dc8ee80db74cd9c66a62fdbe Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sat, 12 Nov 2022 23:39:58 +0100 Subject: [PATCH 514/658] Set some backgroundcolor (#540) --- .../Scene/Onboarding/Login/MastodonLoginView.swift | 8 ++++---- .../Onboarding/Login/MastodonLoginViewController.swift | 10 ++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift index cb506a739..98d4f7b36 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift @@ -43,14 +43,14 @@ class MastodonLoginView: UIView { searchTextField.translatesAutoresizingMaskIntoConstraints = false searchTextField.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color searchTextField.layer.cornerRadius = 10 //TODO: Change mask for - searchTextField.placeholder = "Search for your server" + searchTextField.placeholder = "Search for your server" //TODO: @zeitschlag Localization searchTextField.leftView = UIImageView(image: UIImage(systemName: "magnifyingglass")) searchTextField.leftViewMode = .always tableView = UITableView() tableView.translatesAutoresizingMaskIntoConstraints = false - //TODO: @zeitchlag Cell - + tableView.backgroundColor = Asset.Scene.Onboarding.background.color + navigationActionView = NavigationActionView() navigationActionView.translatesAutoresizingMaskIntoConstraints = false @@ -60,7 +60,7 @@ class MastodonLoginView: UIView { addSubview(searchTextField) addSubview(tableView) addSubview(navigationActionView) - backgroundColor = .systemBackground + backgroundColor = Asset.Scene.Onboarding.background.color setupConstraints() } diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 0c67c8705..0c4e9558b 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -8,6 +8,7 @@ import UIKit import MastodonSDK import MastodonCore +import MastodonAsset protocol MastodonLoginViewControllerDelegate: AnyObject { func backButtonPressed(_ viewController: MastodonLoginViewController) @@ -73,6 +74,15 @@ class MastodonLoginViewController: UIViewController { cell.contentConfiguration = configuration cell.accessoryType = .disclosureIndicator + if #available(iOS 16.0, *) { + var backgroundConfiguration = cell.defaultBackgroundConfiguration() + backgroundConfiguration.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color + + cell.backgroundConfiguration = backgroundConfiguration + } else { + cell.backgroundColor = .systemBackground + } + return cell } From e987affcc9c094a9bf1515c12cfbf0785d92246d Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 13 Nov 2022 00:22:22 +0100 Subject: [PATCH 515/658] Mask corners of textfield and tableview (#540) --- .../Onboarding/Login/MastodonLoginView.swift | 45 ++++++++++++++++--- .../Login/MastodonLoginViewController.swift | 3 ++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift index 98d4f7b36..9fb5f425a 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift @@ -18,6 +18,7 @@ class MastodonLoginView: UIView { private let headerStackView: UIStackView let searchTextField: UITextField let tableView: UITableView + private var tableViewWrapper: UIView let navigationActionView: NavigationActionView override init(frame: CGRect) { @@ -42,7 +43,6 @@ class MastodonLoginView: UIView { searchTextField = UITextField() searchTextField.translatesAutoresizingMaskIntoConstraints = false searchTextField.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color - searchTextField.layer.cornerRadius = 10 //TODO: Change mask for searchTextField.placeholder = "Search for your server" //TODO: @zeitschlag Localization searchTextField.leftView = UIImageView(image: UIImage(systemName: "magnifyingglass")) searchTextField.leftViewMode = .always @@ -51,6 +51,11 @@ class MastodonLoginView: UIView { tableView.translatesAutoresizingMaskIntoConstraints = false tableView.backgroundColor = Asset.Scene.Onboarding.background.color + tableViewWrapper = UIView() + tableViewWrapper.translatesAutoresizingMaskIntoConstraints = false + tableViewWrapper.backgroundColor = .clear + tableViewWrapper.addSubview(tableView) + navigationActionView = NavigationActionView() navigationActionView.translatesAutoresizingMaskIntoConstraints = false @@ -58,7 +63,7 @@ class MastodonLoginView: UIView { addSubview(headerStackView) addSubview(searchTextField) - addSubview(tableView) + addSubview(tableViewWrapper) addSubview(navigationActionView) backgroundColor = Asset.Scene.Onboarding.background.color @@ -81,10 +86,15 @@ class MastodonLoginView: UIView { searchTextField.heightAnchor.constraint(equalToConstant: 55), trailingAnchor.constraint(equalTo: searchTextField.trailingAnchor, constant: 16), - tableView.topAnchor.constraint(equalTo: searchTextField.bottomAnchor), - tableView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), - trailingAnchor.constraint(equalTo: tableView.trailingAnchor, constant: 16), - tableView.bottomAnchor.constraint(equalTo: navigationActionView.topAnchor), + tableViewWrapper.topAnchor.constraint(equalTo: searchTextField.bottomAnchor), + tableViewWrapper.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + trailingAnchor.constraint(equalTo: tableViewWrapper.trailingAnchor, constant: 16), + tableViewWrapper.bottomAnchor.constraint(equalTo: navigationActionView.topAnchor), + + tableView.topAnchor.constraint(equalTo: tableViewWrapper.topAnchor), + tableView.leadingAnchor.constraint(equalTo: tableViewWrapper.leadingAnchor), + tableViewWrapper.trailingAnchor.constraint(equalTo: tableView.trailingAnchor), + tableViewWrapper.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), navigationActionView.leadingAnchor.constraint(equalTo: leadingAnchor), navigationActionView.trailingAnchor.constraint(equalTo: trailingAnchor), @@ -93,4 +103,27 @@ class MastodonLoginView: UIView { NSLayoutConstraint.activate(constraints) } + func updateCorners(numberOfResults: Int = 0) { + let tableViewPath = UIBezierPath(roundedRect:tableViewWrapper.bounds, + byRoundingCorners: [.bottomLeft, .bottomRight], + cornerRadii: CGSize(width: 10, height: 10)) + + let tableViewMask = CAShapeLayer() + tableViewMask.path = tableViewPath.cgPath + tableViewWrapper.layer.mask = tableViewMask + + let searchFieldCorners: UIRectCorner + if numberOfResults == 0 { + searchFieldCorners = .allCorners + } else { + searchFieldCorners = [.topLeft, .topRight] + } + + let searchFieldPath = UIBezierPath(roundedRect: searchTextField.bounds, + byRoundingCorners: searchFieldCorners, + cornerRadii: CGSize(width: 10, height: 10)) + let searchFieldMask = CAShapeLayer() + searchFieldMask.path = searchFieldPath.cgPath + searchTextField.layer.mask = searchFieldMask + } } diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 0c4e9558b..267b27c15 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -188,5 +188,8 @@ extension MastodonLoginViewController: MastodonLoginViewModelDelegate { snapshot.appendItems(viewModel.serverList) dataSource?.applySnapshot(snapshot, animated: true) + OperationQueue.main.addOperation { + self.contentView.updateCorners(numberOfResults: viewModel.serverList.count) + } } } From f9f14cff3cd2f7f5f74ca6dd5005b698d96c1124 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 13 Nov 2022 13:46:32 +0100 Subject: [PATCH 516/658] Make list of servers look nice (#540) --- Mastodon.xcodeproj/project.pbxproj | 4 +++ .../Login/ContentSizedTableView.swift | 22 ++++++++++++ .../Onboarding/Login/MastodonLoginView.swift | 34 ++++++++----------- .../Login/MastodonLoginViewController.swift | 16 +++++++-- 4 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 Mastodon/Scene/Onboarding/Login/ContentSizedTableView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index fca6eb8b5..ae8410c35 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -91,6 +91,7 @@ D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; }; D87BFC8D291EB81200FEE264 /* MastodonLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */; }; D87BFC8F291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */; }; + D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8916DBF29211BE500124085 /* ContentSizedTableView.swift */; }; D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.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 */; }; @@ -615,6 +616,7 @@ D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginView.swift; sourceTree = ""; }; D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginViewModel.swift; sourceTree = ""; }; D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginServerTableViewCell.swift; sourceTree = ""; }; + D8916DBF29211BE500124085 /* ContentSizedTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSizedTableView.swift; sourceTree = ""; }; D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginViewController.swift; sourceTree = ""; }; DB0009A826AEE5DC009B9D2D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = ""; }; DB0009AD26AEE5E4009B9D2D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Intents.strings; sourceTree = ""; }; @@ -1509,6 +1511,7 @@ D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */, D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */, D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */, + D8916DBF29211BE500124085 /* ContentSizedTableView.swift */, ); path = Login; sourceTree = ""; @@ -3311,6 +3314,7 @@ DB63F769279A5EBB00455B82 /* NotificationTimelineViewModel+Diffable.swift in Sources */, DBFEEC9B279BDDD9004F81DD /* ProfileAboutViewModel+Diffable.swift in Sources */, DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */, + D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */, DB0618012785732C0030EE79 /* ServerRulesTableViewCell.swift in Sources */, DB98EB5C27B10A730082E365 /* ReportSupplementaryViewModel.swift in Sources */, DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */, diff --git a/Mastodon/Scene/Onboarding/Login/ContentSizedTableView.swift b/Mastodon/Scene/Onboarding/Login/ContentSizedTableView.swift new file mode 100644 index 000000000..e0c8a9228 --- /dev/null +++ b/Mastodon/Scene/Onboarding/Login/ContentSizedTableView.swift @@ -0,0 +1,22 @@ +// +// MastodonLoginTableView.swift +// Mastodon +// +// Created by Nathan Mattes on 13.11.22. +// + +import UIKit + +// Source: https://stackoverflow.com/a/48623673 +final class ContentSizedTableView: UITableView { + override var contentSize:CGSize { + didSet { + invalidateIntrinsicContentSize() + } + } + + override var intrinsicContentSize: CGSize { + layoutIfNeeded() + return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height) + } +} diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift index 9fb5f425a..53ed98997 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift @@ -10,7 +10,6 @@ import MastodonAsset class MastodonLoginView: UIView { - // SearchBox, queries api.joinmastodon.org/servers with domain // List with (filtered) domains let titleLabel: UILabel @@ -46,14 +45,17 @@ class MastodonLoginView: UIView { searchTextField.placeholder = "Search for your server" //TODO: @zeitschlag Localization searchTextField.leftView = UIImageView(image: UIImage(systemName: "magnifyingglass")) searchTextField.leftViewMode = .always + searchTextField.layer.cornerRadius = 10 - tableView = UITableView() + tableView = ContentSizedTableView() tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.backgroundColor = Asset.Scene.Onboarding.background.color + tableView.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color tableViewWrapper = UIView() tableViewWrapper.translatesAutoresizingMaskIntoConstraints = false tableViewWrapper.backgroundColor = .clear + tableViewWrapper.layer.cornerRadius = 10 + tableViewWrapper.layer.masksToBounds = true tableViewWrapper.addSubview(tableView) navigationActionView = NavigationActionView() @@ -89,12 +91,12 @@ class MastodonLoginView: UIView { tableViewWrapper.topAnchor.constraint(equalTo: searchTextField.bottomAnchor), tableViewWrapper.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), trailingAnchor.constraint(equalTo: tableViewWrapper.trailingAnchor, constant: 16), - tableViewWrapper.bottomAnchor.constraint(equalTo: navigationActionView.topAnchor), + tableViewWrapper.bottomAnchor.constraint(lessThanOrEqualTo: navigationActionView.topAnchor), tableView.topAnchor.constraint(equalTo: tableViewWrapper.topAnchor), tableView.leadingAnchor.constraint(equalTo: tableViewWrapper.leadingAnchor), tableViewWrapper.trailingAnchor.constraint(equalTo: tableView.trailingAnchor), - tableViewWrapper.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), + tableViewWrapper.bottomAnchor.constraint(greaterThanOrEqualTo: tableView.bottomAnchor), navigationActionView.leadingAnchor.constraint(equalTo: leadingAnchor), navigationActionView.trailingAnchor.constraint(equalTo: trailingAnchor), @@ -104,26 +106,18 @@ class MastodonLoginView: UIView { } func updateCorners(numberOfResults: Int = 0) { - let tableViewPath = UIBezierPath(roundedRect:tableViewWrapper.bounds, - byRoundingCorners: [.bottomLeft, .bottomRight], - cornerRadii: CGSize(width: 10, height: 10)) - let tableViewMask = CAShapeLayer() - tableViewMask.path = tableViewPath.cgPath - tableViewWrapper.layer.mask = tableViewMask + tableView.isHidden = (numberOfResults == 0) + tableViewWrapper.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] // tableViewMask + + let maskedCorners: CACornerMask - let searchFieldCorners: UIRectCorner if numberOfResults == 0 { - searchFieldCorners = .allCorners + maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMaxXMaxYCorner] } else { - searchFieldCorners = [.topLeft, .topRight] + maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] } - let searchFieldPath = UIBezierPath(roundedRect: searchTextField.bounds, - byRoundingCorners: searchFieldCorners, - cornerRadii: CGSize(width: 10, height: 10)) - let searchFieldMask = CAShapeLayer() - searchFieldMask.path = searchFieldPath.cgPath - searchTextField.layer.mask = searchFieldMask + searchTextField.layer.maskedCorners = maskedCorners } } diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 267b27c15..780cc932c 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -83,12 +83,22 @@ class MastodonLoginViewController: UIViewController { cell.backgroundColor = .systemBackground } + if self.viewModel.serverList.last == server { + cell.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + cell.layer.cornerRadius = 10 + cell.layer.masksToBounds = true + } else { + cell.layer.masksToBounds = false + } + return cell } contentView.tableView.dataSource = dataSource self.dataSource = dataSource + contentView.updateCorners() + defer { setupNavigationBarBackgroundView() } setupOnboardingAppearance() } @@ -187,9 +197,11 @@ extension MastodonLoginViewController: MastodonLoginViewModelDelegate { snapshot.appendSections([MastodonLoginViewSection.servers]) snapshot.appendItems(viewModel.serverList) - dataSource?.applySnapshot(snapshot, animated: true) + dataSource?.applySnapshot(snapshot, animated: false) + OperationQueue.main.addOperation { - self.contentView.updateCorners(numberOfResults: viewModel.serverList.count) + let numberOfResults = viewModel.serverList.count + self.contentView.updateCorners(numberOfResults: numberOfResults) } } } From a910d6787655b4a7093ce215d0a7e9d41531d726 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 13 Nov 2022 14:02:28 +0100 Subject: [PATCH 517/658] Filter servers (#540) --- .../Login/MastodonLoginViewController.swift | 16 +++++++--------- .../Login/MastodonLoginViewModel.swift | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 780cc932c..411377019 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -63,11 +63,11 @@ class MastodonLoginViewController: UIViewController { let dataSource = UITableViewDiffableDataSource(tableView: contentView.tableView) { [weak self] tableView, indexPath, itemIdentifier in guard let cell = tableView.dequeueReusableCell(withIdentifier: MastodonLoginServerTableViewCell.reuseIdentifier, for: indexPath) as? MastodonLoginServerTableViewCell, - let self = self else { + let self = self else { fatalError("Wrong cell") } - let server = self.viewModel.serverList[indexPath.row] + let server = self.viewModel.filteredServers[indexPath.row] var configuration = cell.defaultContentConfiguration() configuration.text = server.domain @@ -83,7 +83,7 @@ class MastodonLoginViewController: UIViewController { cell.backgroundColor = .systemBackground } - if self.viewModel.serverList.last == server { + if self.viewModel.filteredServers.last == server { cell.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] cell.layer.cornerRadius = 10 cell.layer.masksToBounds = true @@ -166,9 +166,7 @@ class MastodonLoginViewController: UIViewController { } @objc func textfieldDidChange(_ textField: UITextField) { - print(textField.text ?? "---") - //TODO: @zeitschlag filter server-list, update tableview - + viewModel.filterServers(withText: textField.text) contentView.navigationActionView.nextButton.isEnabled = false } } @@ -179,7 +177,7 @@ extension MastodonLoginViewController: OnboardingViewControllerAppearance { } //MARK: - UITableViewDelegate extension MastodonLoginViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let server = viewModel.serverList[indexPath.row] + let server = viewModel.filteredServers[indexPath.row] viewModel.selectedServer = server contentView.searchTextField.text = server.domain @@ -195,12 +193,12 @@ extension MastodonLoginViewController: MastodonLoginViewModelDelegate { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([MastodonLoginViewSection.servers]) - snapshot.appendItems(viewModel.serverList) + snapshot.appendItems(viewModel.filteredServers) dataSource?.applySnapshot(snapshot, animated: false) OperationQueue.main.addOperation { - let numberOfResults = viewModel.serverList.count + let numberOfResults = viewModel.filteredServers.count self.contentView.updateCorners(numberOfResults: numberOfResults) } } diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewModel.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewModel.swift index 68810dded..061e26c6b 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewModel.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewModel.swift @@ -16,8 +16,9 @@ protocol MastodonLoginViewModelDelegate: AnyObject { class MastodonLoginViewModel { - var serverList: [Mastodon.Entity.Server] = [] + private var serverList: [Mastodon.Entity.Server] = [] var selectedServer: Mastodon.Entity.Server? + var filteredServers: [Mastodon.Entity.Server] = [] weak var appContext: AppContext? weak var delegate: MastodonLoginViewModelDelegate? @@ -42,4 +43,17 @@ class MastodonLoginViewModel { self.serverList = servers }).store(in: &disposeBag) } + + func filterServers(withText query: String?) { + guard let query else { + filteredServers = serverList + delegate?.serversUpdated(self) + return + } + + filteredServers = serverList.filter { $0.domain.contains(query) }.sorted {$0.totalUsers > $1.totalUsers } + delegate?.serversUpdated(self) + +// AuthenticationViewModel.parseDomain(from: query) + } } From f293d178848e0b25df838172ce3ff38a81271783 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 13 Nov 2022 14:22:50 +0100 Subject: [PATCH 518/658] Add login (#540) --- .../Login/MastodonLoginViewController.swift | 120 +++++++++++------- .../MastodonPickServerViewController.swift | 7 - 2 files changed, 75 insertions(+), 52 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 411377019..fa05a13b5 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -9,6 +9,8 @@ import UIKit import MastodonSDK import MastodonCore import MastodonAsset +import Combine +import AuthenticationServices protocol MastodonLoginViewControllerDelegate: AnyObject { func backButtonPressed(_ viewController: MastodonLoginViewController) @@ -25,7 +27,10 @@ class MastodonLoginViewController: UIViewController { var dataSource: UITableViewDiffableDataSource? let viewModel: MastodonLoginViewModel let authenticationViewModel: AuthenticationViewModel + var mastodonAuthenticationController: MastodonAuthenticationController? + weak var appContext: AppContext? + var disposeBag = Set() var contentView: MastodonLoginView { view as! MastodonLoginView @@ -109,6 +114,12 @@ class MastodonLoginViewController: UIViewController { viewModel.updateServers() } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + contentView.searchTextField.becomeFirstResponder() + } + //MARK: - Actions @objc func backButtonPressed(_ sender: Any) { @@ -116,65 +127,76 @@ class MastodonLoginViewController: UIViewController { } @objc func nextButtonPressed(_ sender: Any) { - delegate?.nextButtonPressed(self) + login(sender) } @objc func login(_ sender: Any) { -// guard let server = viewModel.selectedServer.value else { return } -// authenticationViewModel.isAuthenticating.send(true) -// context.apiService.createApplication(domain: server.domain) -// .tryMap { response -> AuthenticationViewModel.AuthenticateInfo in -// let application = response.value -// guard let info = AuthenticationViewModel.AuthenticateInfo( -// domain: server.domain, -// application: application, -// redirectURI: response.value.redirectURI ?? APIService.oauthCallbackURL -// ) else { -// throw APIService.APIError.explicit(.badResponse) -// } -// return info -// } -// .receive(on: DispatchQueue.main) -// .sink { [weak self] completion in -// guard let self = self else { return } -// self.authenticationViewModel.isAuthenticating.send(false) -// -// switch completion { -// case .failure(let error): -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign in fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + guard let server = viewModel.selectedServer else { return } + + authenticationViewModel.isAuthenticating.send(true) + appContext?.apiService.createApplication(domain: server.domain) + .tryMap { response -> AuthenticationViewModel.AuthenticateInfo in + let application = response.value + guard let info = AuthenticationViewModel.AuthenticateInfo( + domain: server.domain, + application: application, + redirectURI: response.value.redirectURI ?? APIService.oauthCallbackURL + ) else { + throw APIService.APIError.explicit(.badResponse) + } + return info + } + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + guard let self = self else { return } + self.authenticationViewModel.isAuthenticating.send(false) + + switch completion { + case .failure(let error): // self.viewModel.error.send(error) -// case .finished: -// break -// } -// } receiveValue: { [weak self] info in -// guard let self = self else { return } -// let authenticationController = MastodonAuthenticationController( -// context: self.context, -// authenticateURL: info.authorizeURL -// ) -// -// self.mastodonAuthenticationController = authenticationController -// authenticationController.authenticationSession?.presentationContextProvider = self -// authenticationController.authenticationSession?.start() -// -// self.authenticationViewModel.authenticate( -// info: info, -// pinCodePublisher: authenticationController.pinCodePublisher -// ) -// } -// .store(in: &disposeBag) + break + case .finished: + break + } + } receiveValue: { [weak self] info in + guard let self, let appContext = self.appContext else { return } + let authenticationController = MastodonAuthenticationController( + context: appContext, + authenticateURL: info.authorizeURL + ) + + self.mastodonAuthenticationController = authenticationController + authenticationController.authenticationSession?.presentationContextProvider = self + authenticationController.authenticationSession?.start() + + self.authenticationViewModel.authenticate( + info: info, + pinCodePublisher: authenticationController.pinCodePublisher + ) + } + .store(in: &disposeBag) } @objc func textfieldDidChange(_ textField: UITextField) { viewModel.filterServers(withText: textField.text) - contentView.navigationActionView.nextButton.isEnabled = false + + + if let text = textField.text, + let domain = AuthenticationViewModel.parseDomain(from: text) { + + viewModel.selectedServer = .init(domain: domain, instance: .init(domain: domain)) + contentView.navigationActionView.nextButton.isEnabled = true + } else { + viewModel.selectedServer = nil + contentView.navigationActionView.nextButton.isEnabled = false + } } } // MARK: - OnboardingViewControllerAppearance extension MastodonLoginViewController: OnboardingViewControllerAppearance { } -//MARK: - UITableViewDelegate +// MARK: - UITableViewDelegate extension MastodonLoginViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let server = viewModel.filteredServers[indexPath.row] @@ -188,6 +210,7 @@ extension MastodonLoginViewController: UITableViewDelegate { } } +// MARK: - MastodonLoginViewModelDelegate extension MastodonLoginViewController: MastodonLoginViewModelDelegate { func serversUpdated(_ viewModel: MastodonLoginViewModel) { var snapshot = NSDiffableDataSourceSnapshot() @@ -203,3 +226,10 @@ extension MastodonLoginViewController: MastodonLoginViewModelDelegate { } } } + +// MARK: - ASWebAuthenticationPresentationContextProviding +extension MastodonLoginViewController: ASWebAuthenticationPresentationContextProviding { + func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + return view.window! + } +} diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 2fa46cac9..11e12f3c8 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -452,10 +452,3 @@ extension MastodonPickServerViewController: PickServerServerSectionTableHeaderVi // MARK: - OnboardingViewControllerAppearance extension MastodonPickServerViewController: OnboardingViewControllerAppearance { } - -// MARK: - ASWebAuthenticationPresentationContextProviding -extension MastodonPickServerViewController: ASWebAuthenticationPresentationContextProviding { - func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { - return view.window! - } -} From e7b8870329be24d98a93cacaf1a5ef457b167432 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 13 Nov 2022 14:30:05 +0100 Subject: [PATCH 519/658] Do the keyboard-dance (#540) --- .../Onboarding/Login/MastodonLoginView.swift | 9 ++++- .../Login/MastodonLoginViewController.swift | 34 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift index 53ed98997..d59e30dd4 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift @@ -19,6 +19,7 @@ class MastodonLoginView: UIView { let tableView: UITableView private var tableViewWrapper: UIView let navigationActionView: NavigationActionView + var bottomConstraint: NSLayoutConstraint? override init(frame: CGRect) { @@ -50,6 +51,7 @@ class MastodonLoginView: UIView { tableView = ContentSizedTableView() tableView.translatesAutoresizingMaskIntoConstraints = false tableView.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color + tableView.keyboardDismissMode = .onDrag tableViewWrapper = UIView() tableViewWrapper.translatesAutoresizingMaskIntoConstraints = false @@ -77,6 +79,9 @@ class MastodonLoginView: UIView { } private func setupConstraints() { + + let bottomConstraint = safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor) + let constraints = [ headerStackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), @@ -100,8 +105,10 @@ class MastodonLoginView: UIView { navigationActionView.leadingAnchor.constraint(equalTo: leadingAnchor), navigationActionView.trailingAnchor.constraint(equalTo: trailingAnchor), - bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor), + bottomConstraint, ] + + self.bottomConstraint = bottomConstraint NSLayoutConstraint.activate(constraints) } diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index fa05a13b5..6ab427e5b 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -46,6 +46,7 @@ class MastodonLoginViewController: UIViewController { viewModel.delegate = self navigationItem.hidesBackButton = true + //TODO: @zeitschlag consider keyboard, do the notification-dance } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -66,6 +67,9 @@ class MastodonLoginViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShowNotification(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHideNotification(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) + let dataSource = UITableViewDiffableDataSource(tableView: contentView.tableView) { [weak self] tableView, indexPath, itemIdentifier in guard let cell = tableView.dequeueReusableCell(withIdentifier: MastodonLoginServerTableViewCell.reuseIdentifier, for: indexPath) as? MastodonLoginServerTableViewCell, let self = self else { @@ -191,6 +195,36 @@ class MastodonLoginViewController: UIViewController { contentView.navigationActionView.nextButton.isEnabled = false } } + + // MARK: - Notifications + @objc func keyboardWillShowNotification(_ notification: Notification) { + + guard let userInfo = notification.userInfo, + let keyboardFrameValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue, + let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber + else { return } + + let adjustmentHeight = keyboardFrameValue.cgRectValue.height - view.safeAreaInsets.bottom + contentView.bottomConstraint?.constant = adjustmentHeight + + UIView.animate(withDuration: duration.doubleValue, delay: 0, options: .curveEaseInOut) { + self.view.layoutIfNeeded() + } + } + + @objc func keyboardWillHideNotification(_ notification: Notification) { + + guard let userInfo = notification.userInfo, + let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber + else { return } + + contentView.bottomConstraint?.constant = 0 + + UIView.animate(withDuration: duration.doubleValue, delay: 0, options: .curveEaseInOut) { + self.view.layoutIfNeeded() + } + } + } // MARK: - OnboardingViewControllerAppearance From 7c14d7163013901ae883979f5bd157c1e3a93a27 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 13 Nov 2022 14:50:45 +0100 Subject: [PATCH 520/658] Show main-screen after login (#540) --- Mastodon/Coordinator/SceneCoordinator.swift | 4 +- .../Login/MastodonLoginViewController.swift | 46 +++++++++++++++---- .../Login/MastodonLoginViewModel.swift | 4 +- .../MastodonPickServerViewModel.swift | 1 - 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 34ac09050..365002256 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -406,7 +406,9 @@ private extension SceneCoordinator { _viewController.viewModel = viewModel viewController = _viewController case .mastodonLogin: - let loginViewController = MastodonLoginViewController(appContext: appContext, authenticationViewModel: AuthenticationViewModel(context: appContext, coordinator: self, isAuthenticationExist: false)) + let loginViewController = MastodonLoginViewController(appContext: appContext, + authenticationViewModel: AuthenticationViewModel(context: appContext, coordinator: self, isAuthenticationExist: false), + sceneCoordinator: self) loginViewController.delegate = self viewController = loginViewController diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 6ab427e5b..ef65ca5e8 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -21,7 +21,7 @@ enum MastodonLoginViewSection: Hashable { case servers } -class MastodonLoginViewController: UIViewController { +class MastodonLoginViewController: UIViewController, NeedsDependency { weak var delegate: MastodonLoginViewControllerDelegate? var dataSource: UITableViewDiffableDataSource? @@ -29,24 +29,26 @@ class MastodonLoginViewController: UIViewController { let authenticationViewModel: AuthenticationViewModel var mastodonAuthenticationController: MastodonAuthenticationController? - weak var appContext: AppContext? + weak var context: AppContext! + weak var coordinator: SceneCoordinator! + var disposeBag = Set() var contentView: MastodonLoginView { view as! MastodonLoginView } - init(appContext: AppContext, authenticationViewModel: AuthenticationViewModel) { + init(appContext: AppContext, authenticationViewModel: AuthenticationViewModel, sceneCoordinator: SceneCoordinator) { viewModel = MastodonLoginViewModel(appContext: appContext) self.authenticationViewModel = authenticationViewModel - self.appContext = appContext + self.context = appContext + self.coordinator = sceneCoordinator super.init(nibName: nil, bundle: nil) viewModel.delegate = self navigationItem.hidesBackButton = true - //TODO: @zeitschlag consider keyboard, do the notification-dance } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -137,8 +139,32 @@ class MastodonLoginViewController: UIViewController { @objc func login(_ sender: Any) { guard let server = viewModel.selectedServer else { return } + authenticationViewModel + .authenticated + .asyncMap { domain, user -> Result in + do { + let result = try await self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id) + return .success(result) + } catch { + return .failure(error) + } + } + .receive(on: DispatchQueue.main) + .sink { [weak self] result in + guard let self = self else { return } + switch result { + case .failure(let error): + assertionFailure(error.localizedDescription) + case .success(let isActived): + assert(isActived) + // self.dismiss(animated: true, completion: nil) + self.coordinator.setup() + } + } + .store(in: &disposeBag) + authenticationViewModel.isAuthenticating.send(true) - appContext?.apiService.createApplication(domain: server.domain) + context.apiService.createApplication(domain: server.domain) .tryMap { response -> AuthenticationViewModel.AuthenticateInfo in let application = response.value guard let info = AuthenticationViewModel.AuthenticateInfo( @@ -157,15 +183,15 @@ class MastodonLoginViewController: UIViewController { switch completion { case .failure(let error): -// self.viewModel.error.send(error) + //TODO: @zeitschlag show error break case .finished: break } } receiveValue: { [weak self] info in - guard let self, let appContext = self.appContext else { return } + guard let self else { return } let authenticationController = MastodonAuthenticationController( - context: appContext, + context: self.context, authenticateURL: info.authorizeURL ) @@ -204,6 +230,7 @@ class MastodonLoginViewController: UIViewController { let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber else { return } + //FIXME: @zeitschlag on iPad let adjustmentHeight = keyboardFrameValue.cgRectValue.height - view.safeAreaInsets.bottom contentView.bottomConstraint?.constant = adjustmentHeight @@ -237,7 +264,6 @@ extension MastodonLoginViewController: UITableViewDelegate { viewModel.selectedServer = server contentView.searchTextField.text = server.domain - //TODO: @zeitschlag filter server list contentView.navigationActionView.nextButton.isEnabled = true tableView.deselectRow(at: indexPath, animated: true) diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewModel.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewModel.swift index 061e26c6b..61311a1b9 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewModel.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewModel.swift @@ -51,9 +51,7 @@ class MastodonLoginViewModel { return } - filteredServers = serverList.filter { $0.domain.contains(query) }.sorted {$0.totalUsers > $1.totalUsers } + filteredServers = serverList.filter { $0.domain.lowercased().contains(query) }.sorted {$0.totalUsers > $1.totalUsers } delegate?.serversUpdated(self) - -// AuthenticationViewModel.parseDomain(from: query) } } diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift index ff6806b71..ebbcfe7fd 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift @@ -169,7 +169,6 @@ extension MastodonPickServerViewModel { .compactMap { [weak self] searchText -> AnyPublisher, Error>, Never>? in // Check if searchText is a valid mastodon server domain guard let self = self else { return nil } - //TODO: @zeitschlag Also allow search for incomplete URLs? guard let domain = AuthenticationViewModel.parseDomain(from: searchText) else { return Just(Result.failure(APIService.APIError.implicit(.badRequest))).eraseToAnyPublisher() } From 378e04c9cbff8dd61e61a127e36dc34017c235bd Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 13 Nov 2022 14:55:59 +0100 Subject: [PATCH 521/658] Don't show a server when you just tapped on one (#540) --- .../Scene/Onboarding/Login/MastodonLoginViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index ef65ca5e8..5811de40b 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -264,6 +264,7 @@ extension MastodonLoginViewController: UITableViewDelegate { viewModel.selectedServer = server contentView.searchTextField.text = server.domain + viewModel.filterServers(withText: " ") contentView.navigationActionView.nextButton.isEnabled = true tableView.deselectRow(at: indexPath, animated: true) From cf2f7850b701e314f62fab73195eac3c737d2760 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 13 Nov 2022 15:04:42 +0100 Subject: [PATCH 522/658] Sprinkle in some localization (#540) --- Localization/app.json | 7 +++++++ .../Scene/Onboarding/Login/MastodonLoginView.swift | 7 ++++--- .../MastodonLocalization/Generated/Strings.swift | 10 ++++++++++ .../Resources/en.lproj/Localizable.strings | 5 ++++- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 8287a00a5..9f6cdecac 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -218,6 +218,13 @@ "get_started": "Get Started", "log_in": "Log In" }, + "login": { + "title": "", + "subtitle": "", + "server_search_field": { + "placeholder": "Search for your server" + } + } "server_picker": { "title": "Mastodon is made of users in different servers.", "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift index d59e30dd4..03c357fca 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift @@ -7,6 +7,7 @@ import UIKit import MastodonAsset +import MastodonLocalization class MastodonLoginView: UIView { @@ -26,13 +27,13 @@ class MastodonLoginView: UIView { titleLabel = UILabel() titleLabel.font = MastodonLoginViewController.largeTitleFont titleLabel.textColor = MastodonLoginViewController.largeTitleTextColor - titleLabel.text = "Welcome Back" //TODO: @zeitschlag localization + titleLabel.text = L10n.Scene.Login.title titleLabel.numberOfLines = 0 subtitleLabel = UILabel() subtitleLabel.font = MastodonLoginViewController.subTitleFont subtitleLabel.textColor = MastodonLoginViewController.subTitleTextColor - subtitleLabel.text = "Log you in with the server where you created your account" //TODO: @zeitschlag localization + subtitleLabel.text = L10n.Scene.Login.subtitle subtitleLabel.numberOfLines = 0 headerStackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel]) @@ -43,7 +44,7 @@ class MastodonLoginView: UIView { searchTextField = UITextField() searchTextField.translatesAutoresizingMaskIntoConstraints = false searchTextField.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color - searchTextField.placeholder = "Search for your server" //TODO: @zeitschlag Localization + searchTextField.placeholder = L10n.Scene.Login.ServerSearchField.placeholder searchTextField.leftView = UIImageView(image: UIImage(systemName: "magnifyingglass")) searchTextField.leftViewMode = .always searchTextField.layer.cornerRadius = 10 diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 1ad98b0ea..af0f4c628 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -665,6 +665,16 @@ public enum L10n { } } } + public enum Login { + /// Log you in with the server where you created your account + public static let subtitle = L10n.tr("Localizable", "Scene.Login.Subtitle") + /// Welcome Back! + public static let title = L10n.tr("Localizable", "Scene.Login.Title") + public enum ServerSearchField { + /// Search for your server + public static let placeholder = L10n.tr("Localizable", "Scene.Login.ServerSearchField.Placeholder") + } + } public enum Notification { public enum FollowRequest { /// Accept diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings index 07ccd2c1b..0ede22a38 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings @@ -315,6 +315,9 @@ uploaded to Mastodon."; "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.Login.Title" = "Welcome Back!"; +"Scene.Login.Subtitle" = "Log you in with the server where you created your account"; +"Scene.Login.ServerSearchField.Placeholder" = "Search for your server"; "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?"; @@ -454,4 +457,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 105a98a3955619157a8c0f8d016d743aa8b6025f Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 14 Nov 2022 17:16:33 +0100 Subject: [PATCH 523/658] No extra orientation for Debug (#571) --- Mastodon/Supporting Files/AppDelegate.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 3bf38f6da..84b819a5c 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -65,11 +65,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { extension AppDelegate { func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { - #if DEBUG - return .all - #else return UIDevice.current.userInterfaceIdiom == .phone ? .portrait : .all - #endif } } From 082e0933d2d029f4b442f55194e9a4525e703261 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 14 Nov 2022 17:53:37 +0100 Subject: [PATCH 524/658] Slash some todos (#540) --- Mastodon/Coordinator/SceneCoordinator.swift | 2 +- .../Login/MastodonLoginViewController.swift | 55 +++++++++---------- .../Resources/en.lproj/Localizable.strings | 2 +- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 365002256..9e6307a38 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -548,6 +548,6 @@ extension SceneCoordinator: MastodonLoginViewControllerDelegate { } func nextButtonPressed(_ viewController: MastodonLoginViewController) { - //TODO: @zeitschlag implement, show ASWebAuthentication and stuff + viewController.login() } } diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 5811de40b..a1daf77bd 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -133,35 +133,34 @@ class MastodonLoginViewController: UIViewController, NeedsDependency { } @objc func nextButtonPressed(_ sender: Any) { - login(sender) + delegate?.nextButtonPressed(self) } - @objc func login(_ sender: Any) { + @objc func login() { guard let server = viewModel.selectedServer else { return } authenticationViewModel - .authenticated - .asyncMap { domain, user -> Result in - do { - let result = try await self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id) - return .success(result) - } catch { - return .failure(error) - } + .authenticated + .asyncMap { domain, user -> Result in + do { + let result = try await self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id) + return .success(result) + } catch { + return .failure(error) } - .receive(on: DispatchQueue.main) - .sink { [weak self] result in - guard let self = self else { return } - switch result { - case .failure(let error): - assertionFailure(error.localizedDescription) - case .success(let isActived): - assert(isActived) - // self.dismiss(animated: true, completion: nil) - self.coordinator.setup() - } + } + .receive(on: DispatchQueue.main) + .sink { [weak self] result in + guard let self = self else { return } + switch result { + case .failure(let error): + assertionFailure(error.localizedDescription) + case .success(let isActived): + assert(isActived) + self.coordinator.setup() } - .store(in: &disposeBag) + } + .store(in: &disposeBag) authenticationViewModel.isAuthenticating.send(true) context.apiService.createApplication(domain: server.domain) @@ -183,9 +182,10 @@ class MastodonLoginViewController: UIViewController, NeedsDependency { switch completion { case .failure(let error): - //TODO: @zeitschlag show error - break + let alert = UIAlertController.standardAlert(of: error) + self.present(alert, animated: true) case .finished: + // do nothing. There's a subscriber above resulting in `coordinator.setup()` break } } receiveValue: { [weak self] info in @@ -251,7 +251,6 @@ class MastodonLoginViewController: UIViewController, NeedsDependency { self.view.layoutIfNeeded() } } - } // MARK: - OnboardingViewControllerAppearance @@ -290,7 +289,7 @@ extension MastodonLoginViewController: MastodonLoginViewModelDelegate { // MARK: - ASWebAuthenticationPresentationContextProviding extension MastodonLoginViewController: ASWebAuthenticationPresentationContextProviding { - func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { - return view.window! - } + func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + return view.window! + } } diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings index 0ede22a38..1a48d8c55 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings @@ -317,7 +317,7 @@ uploaded to Mastodon."; "Scene.Register.LetsGetYouSetUpOnDomain" = "Let’s get you set up on %@"; "Scene.Login.Title" = "Welcome Back!"; "Scene.Login.Subtitle" = "Log you in with the server where you created your account"; -"Scene.Login.ServerSearchField.Placeholder" = "Search for your server"; +"Scene.Login.ServerSearchField.Placeholder" = "Search server or enter URL"; "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?"; From 26d918a28f01bd36d67d1117d1b5e5518ef6fcb8 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 14 Nov 2022 23:40:22 +0100 Subject: [PATCH 525/658] Fix build (again) This time, it's by hand because why not :D --- .../input/Base.lproj/app.json | 19 ++++++++++++------- Localization/app.json | 8 ++++---- .../Generated/Strings.swift | 6 +++--- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json index 30566d8d6..3a1228131 100644 --- a/Localization/StringsConvertor/input/Base.lproj/app.json +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Take Photo", "save_photo": "Save Photo", "copy_photo": "Copy Photo", - "sign_in": "Sign In", - "sign_up": "Sign Up", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "See More", "preview": "Preview", "share": "Share", @@ -218,10 +218,16 @@ "get_started": "Get Started", "log_in": "Log In" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + } "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.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "All", @@ -248,8 +254,7 @@ "category": "CATEGORY" }, "input": { - "placeholder": "Search servers", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Finding available servers...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/Localization/app.json b/Localization/app.json index 9f6cdecac..3a1228131 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -219,10 +219,10 @@ "log_in": "Log In" }, "login": { - "title": "", - "subtitle": "", + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", "server_search_field": { - "placeholder": "Search for your server" + "placeholder": "Enter URL or search for your server" } } "server_picker": { @@ -724,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index af0f4c628..9535b932f 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -667,12 +667,12 @@ public enum L10n { } public enum Login { /// Log you in with the server where you created your account - public static let subtitle = L10n.tr("Localizable", "Scene.Login.Subtitle") + public static let subtitle = L10n.tr("Localizable", "Scene.Login.Subtitle", fallback: "Scene.Login.Subtitle") /// Welcome Back! - public static let title = L10n.tr("Localizable", "Scene.Login.Title") + public static let title = L10n.tr("Localizable", "Scene.Login.Title", fallback: "Welcome") public enum ServerSearchField { /// Search for your server - public static let placeholder = L10n.tr("Localizable", "Scene.Login.ServerSearchField.Placeholder") + public static let placeholder = L10n.tr("Localizable", "Scene.Login.ServerSearchField.Placeholder", fallback: "Scene.Login.ServerSearchField.Placeholder") } } public enum Notification { From 83e0c0e994c9d135697b0147bf2fbe7c29ffde17 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 15 Nov 2022 23:26:58 +0100 Subject: [PATCH 526/658] Slash more todos (#540) - Apply some math for keyboard-dance - Onboarding will be reworked nevertheless --- .../Login/MastodonLoginViewController.swift | 14 +++++++++++--- .../MastodonPickServerViewController.swift | 1 - 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index a1daf77bd..6125955f4 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -129,10 +129,12 @@ class MastodonLoginViewController: UIViewController, NeedsDependency { //MARK: - Actions @objc func backButtonPressed(_ sender: Any) { + contentView.searchTextField.resignFirstResponder() delegate?.backButtonPressed(self) } @objc func nextButtonPressed(_ sender: Any) { + contentView.searchTextField.resignFirstResponder() delegate?.nextButtonPressed(self) } @@ -230,9 +232,15 @@ class MastodonLoginViewController: UIViewController, NeedsDependency { let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber else { return } - //FIXME: @zeitschlag on iPad - let adjustmentHeight = keyboardFrameValue.cgRectValue.height - view.safeAreaInsets.bottom - contentView.bottomConstraint?.constant = adjustmentHeight + // inspired by https://stackoverflow.com/a/30245044 + let keyboardFrame = keyboardFrameValue.cgRectValue + + let keyboardOrigin = view.convert(keyboardFrame.origin, from: nil) + let intersectionY = CGRectGetMaxY(view.frame) - keyboardOrigin.y; + + if intersectionY >= 0 { + contentView.bottomConstraint?.constant = intersectionY - view.safeAreaInsets.bottom + } UIView.animate(withDuration: duration.doubleValue, delay: 0, options: .curveEaseInOut) { self.view.layoutIfNeeded() diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 11e12f3c8..7e832ddca 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -445,7 +445,6 @@ extension MastodonPickServerViewController: PickServerServerSectionTableHeaderVi } func pickServerServerSectionTableHeaderView(_ headerView: PickServerServerSectionTableHeaderView, searchTextDidChange searchText: String?) { - //TODO: @zeitschlag Deselect server? viewModel.searchText.send(searchText ?? "") } } From bd35a01be231ad10f53f8d6643b467e03c87e0f3 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 15 Nov 2022 23:30:46 +0100 Subject: [PATCH 527/658] Fix package.resolved --- .../xcshareddata/swiftpm/Package.resolved | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..409b8820d --- /dev/null +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,257 @@ +{ + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire.git", + "state" : { + "revision" : "354dda32d89fc8cd4f5c46487f64957d355f53d8", + "version" : "5.6.1" + } + }, + { + "identity" : "alamofireimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/AlamofireImage.git", + "state" : { + "revision" : "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10", + "version" : "4.2.0" + } + }, + { + "identity" : "commonoslog", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MainasuK/CommonOSLog", + "state" : { + "revision" : "c121624a30698e9886efe38aebb36ff51c01b6c2", + "version" : "0.1.1" + } + }, + { + "identity" : "faviconfinder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/will-lumley/FaviconFinder.git", + "state" : { + "revision" : "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a", + "version" : "3.3.0" + } + }, + { + "identity" : "flanimatedimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Flipboard/FLAnimatedImage.git", + "state" : { + "revision" : "e7f9fd4681ae41bf6f3056db08af4f401d61da52", + "version" : "1.0.16" + } + }, + { + "identity" : "fpsindicator", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MainasuK/FPSIndicator.git", + "state" : { + "revision" : "e4a5067ccd5293b024c767f09e51056afd4a4796", + "version" : "1.1.0" + } + }, + { + "identity" : "fuzi", + "kind" : "remoteSourceControl", + "location" : "https://github.com/cezheng/Fuzi.git", + "state" : { + "revision" : "f08c8323da21e985f3772610753bcfc652c2103f", + "version" : "3.1.3" + } + }, + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", + "state" : { + "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", + "version" : "4.2.2" + } + }, + { + "identity" : "kingfisher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Kingfisher.git", + "state" : { + "revision" : "44e891bdb61426a95e31492a67c7c0dfad1f87c5", + "version" : "7.4.1" + } + }, + { + "identity" : "metatextkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TwidereProject/MetaTextKit.git", + "state" : { + "revision" : "dcd5255d6930c2fab408dc8562c577547e477624", + "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", + "location" : "https://github.com/kean/Nuke.git", + "state" : { + "revision" : "0ea7545b5c918285aacc044dc75048625c8257cc", + "version" : "10.8.0" + } + }, + { + "identity" : "nuke-flanimatedimage-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", + "state" : { + "revision" : "b59c346a7d536336db3b0f12c72c6e53ee709e16", + "version" : "8.0.0" + } + }, + { + "identity" : "pageboy", + "kind" : "remoteSourceControl", + "location" : "https://github.com/uias/Pageboy", + "state" : { + "revision" : "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6", + "version" : "3.6.2" + } + }, + { + "identity" : "panmodal", + "kind" : "remoteSourceControl", + "location" : "https://github.com/slackhq/PanModal.git", + "state" : { + "revision" : "b012aecb6b67a8e46369227f893c12544846613f", + "version" : "1.2.7" + } + }, + { + "identity" : "sdwebimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SDWebImage/SDWebImage.git", + "state" : { + "revision" : "2e63d0061da449ad0ed130768d05dceb1496de44", + "version" : "5.12.5" + } + }, + { + "identity" : "stripes", + "kind" : "remoteSourceControl", + "location" : "https://github.com/eneko/Stripes.git", + "state" : { + "revision" : "d533fd44b8043a3abbf523e733599173d6f98c11", + "version" : "0.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "f504716c27d2e5d4144fa4794b12129301d17729", + "version" : "1.0.3" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "546610d52b19be3e19935e0880bb06b9c03f5cef", + "version" : "1.14.4" + } + }, + { + "identity" : "swift-nio-zlib-support", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-zlib-support.git", + "state" : { + "revision" : "37760e9a52030bb9011972c5213c3350fa9d41fd", + "version" : "1.0.0" + } + }, + { + "identity" : "swiftsoup", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scinfu/SwiftSoup.git", + "state" : { + "revision" : "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886", + "version" : "2.4.2" + } + }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", + "state" : { + "revision" : "f2616860a41f9d9932da412a8978fec79c06fe24", + "version" : "0.1.4" + } + }, + { + "identity" : "tabbarpager", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TwidereProject/TabBarPager.git", + "state" : { + "revision" : "488aa66d157a648901b61721212c0dec23d27ee5", + "version" : "0.1.0" + } + }, + { + "identity" : "tabman", + "kind" : "remoteSourceControl", + "location" : "https://github.com/uias/Tabman", + "state" : { + "revision" : "4a4f7c755b875ffd4f9ef10d67a67883669d2465", + "version" : "2.13.0" + } + }, + { + "identity" : "thirdpartymailer", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vtourraine/ThirdPartyMailer.git", + "state" : { + "revision" : "44c1cfaa6969963f22691aa67f88a69e3b6d651f", + "version" : "2.1.0" + } + }, + { + "identity" : "tocropviewcontroller", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TimOliver/TOCropViewController.git", + "state" : { + "revision" : "d0470491f56e734731bbf77991944c0dfdee3e0e", + "version" : "2.6.1" + } + }, + { + "identity" : "uihostingconfigurationbackport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/woxtu/UIHostingConfigurationBackport.git", + "state" : { + "revision" : "6091f2d38faa4b24fc2ca0389c651e2f666624a3", + "version" : "0.1.0" + } + }, + { + "identity" : "uitextview-placeholder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MainasuK/UITextView-Placeholder.git", + "state" : { + "revision" : "20f513ded04a040cdf5467f0891849b1763ede3b", + "version" : "1.4.1" + } + } + ], + "version" : 2 +} From d86d613b4aec126c71b3da5b18a0fb8402f4a264 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 15 Nov 2022 23:51:24 +0100 Subject: [PATCH 528/658] Remove todos (#540) After discussion they will be taken care of when we rework the onboarding (spoiler) --- Mastodon/Diffable/Onboarding/PickServerSection.swift | 1 - .../Onboarding/PickServer/TableViewCell/PickServerCell.swift | 2 -- .../Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift | 1 - 3 files changed, 4 deletions(-) diff --git a/Mastodon/Diffable/Onboarding/PickServerSection.swift b/Mastodon/Diffable/Onboarding/PickServerSection.swift index 24e4d2ffb..ef7ca5972 100644 --- a/Mastodon/Diffable/Onboarding/PickServerSection.swift +++ b/Mastodon/Diffable/Onboarding/PickServerSection.swift @@ -48,7 +48,6 @@ extension PickServerSection { extension PickServerSection { static func configure(cell: PickServerCell, server: Mastodon.Entity.Server, attribute: PickServerItem.ServerItemAttribute) { - //TODO: @zeitschlag configure cell if server doesn't allow registrations cell.domainLabel.text = server.domain cell.descriptionLabel.attributedText = { let content: String = { diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift index e199ea96c..4cbe77b9c 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift @@ -83,8 +83,6 @@ class PickServerCell: UITableViewCell { return label }() - //TODO: @zeitschlag New label for "Registrations closed" - private var collapseConstraints: [NSLayoutConstraint] = [] private var expandConstraints: [NSLayoutConstraint] = [] diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift index 56b4cd327..4267810b7 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift @@ -22,7 +22,6 @@ extension Mastodon.Entity { public let approvalRequired: Bool public let language: String public let category: String - //TODO: @zeitschlag Is there a way to figure out in advance if a server accepts new registrations? Right now we'd have to query the server and it responds with a `AuthenticationViewModel.AuthenticationError.registrationClosed` enum CodingKeys: String, CodingKey { case domain From 61b9242df243932d0acef0d932732f32ff6c6e61 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 03:29:17 +0100 Subject: [PATCH 529/658] New translations app.json (Indonesian) --- .../StringsConvertor/input/id.lproj/app.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Localization/StringsConvertor/input/id.lproj/app.json b/Localization/StringsConvertor/input/id.lproj/app.json index 45e072a30..346ac5359 100644 --- a/Localization/StringsConvertor/input/id.lproj/app.json +++ b/Localization/StringsConvertor/input/id.lproj/app.json @@ -129,23 +129,23 @@ "show_post": "Tampilkan Postingan", "show_user_profile": "Tampilkan Profil Pengguna", "content_warning": "Peringatan Konten", - "sensitive_content": "Sensitive Content", + "sensitive_content": "Konten Sensitif", "media_content_warning": "Ketuk di mana saja untuk melihat", - "tap_to_reveal": "Tap to reveal", + "tap_to_reveal": "Ketuk untuk mengungkap", "poll": { - "vote": "Vote", + "vote": "Pilih", "closed": "Ditutup" }, "meta_entity": { - "url": "Link: %s", - "hashtag": "Hashtag: %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "url": "Tautan: %s", + "hashtag": "Tagar: %s", + "mention": "Tampilkan Profile: %s", + "email": "Alamat email: %s" }, "actions": { "reply": "Balas", "reblog": "Reblog", - "unreblog": "Undo reblog", + "unreblog": "Batalkan reblog", "favorite": "Favorit", "unfavorite": "Unfavorite", "menu": "Menu", From 97765f11f619c5cdf63c47c1e4a45a792a39df08 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 04:29:21 +0100 Subject: [PATCH 530/658] New translations app.json (Indonesian) --- .../StringsConvertor/input/id.lproj/app.json | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/Localization/StringsConvertor/input/id.lproj/app.json b/Localization/StringsConvertor/input/id.lproj/app.json index 346ac5359..864283436 100644 --- a/Localization/StringsConvertor/input/id.lproj/app.json +++ b/Localization/StringsConvertor/input/id.lproj/app.json @@ -147,13 +147,13 @@ "reblog": "Reblog", "unreblog": "Batalkan reblog", "favorite": "Favorit", - "unfavorite": "Unfavorite", + "unfavorite": "Batalkan favorit", "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": "Sembunyikan", + "show_image": "Tampilkan gambar", + "show_gif": "Tampilkan GIF", + "show_video_player": "Tampilkan pemutar video", + "tap_then_hold_to_show_menu": "Ketuk lalu tahan untuk menampilkan menu" }, "tag": { "url": "URL", @@ -165,16 +165,16 @@ }, "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": "Hanya pengikut mereka yang dapat melihat postingan ini.", + "private_from_me": "Hanya pengikut saya yang dapat melihat postingan ini.", + "direct": "Hanya pengguna yang disebut yang dapat melihat postingan ini." } }, "friendship": { "follow": "Ikuti", "following": "Mengikuti", - "request": "Request", - "pending": "Pending", + "request": "Minta", + "pending": "Tertunda", "block": "Blokir", "block_user": "Blokir %s", "block_domain": "Blokir %s", @@ -187,8 +187,8 @@ "unmute_user": "Berhenti membisukan %s", "muted": "Dibisukan", "edit_info": "Sunting Info", - "show_reblogs": "Show Reblogs", - "hide_reblogs": "Hide Reblogs" + "show_reblogs": "Tampilkan Reblog", + "hide_reblogs": "Sembunyikan Reblog" }, "timeline": { "filtered": "Tersaring", @@ -196,32 +196,32 @@ "now": "Sekarang" }, "loader": { - "load_missing_posts": "Load missing posts", - "loading_missing_posts": "Loading missing posts...", + "load_missing_posts": "Muat postingan yang hilang", + "loading_missing_posts": "Memuat postingan yang hilang...", "show_more_replies": "Tampilkan lebih banyak balasan" }, "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." + "no_status_found": "Tidak Ditemukan Postingan", + "blocking_warning": "Anda tidak dapat melihat profil pengguna ini sampai Anda membuka blokir mereka.\nProfil Anda terlihat seperti ini bagi mereka.", + "user_blocking_warning": "Anda tidak dapat melihat profil %s sampai Anda membuka blokir mereka.\nProfil Anda terlihat seperti ini bagi mereka.", + "blocked_warning": "Anda tidak dapat melihat profil pengguna ini sampai mereka membuka blokir Anda.", + "user_blocked_warning": "Anda tidak dapat melihat profil %s sampai mereka membuka blokir Anda.", + "suspended_warning": "Pengguna ini telah ditangguhkan.", + "user_suspended_warning": "Akun %s telah ditangguhkan." } } } }, "scene": { "welcome": { - "slogan": "Social networking\nback in your hands.", - "get_started": "Get Started", - "log_in": "Log In" + "slogan": "Jejaring sosial dalam genggaman Anda.", + "get_started": "Mulai", + "log_in": "Login" }, "server_picker": { "title": "Pilih sebuah server,\nserver manapun.", - "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": "Pilih server berdasarkan minat, wilayah, atau tujuan Anda.", + "subtitle_extend": "Pilih server berdasarkan minat, wilayah, atau tujuan Anda. Setiap server dioperasikan oleh organisasi atau individu yang sepenuhnya independen.", "button": { "category": { "all": "Semua", @@ -248,8 +248,8 @@ "category": "KATEGORI" }, "input": { - "placeholder": "Search servers", - "search_servers_or_enter_url": "Search servers or enter URL" + "placeholder": "Cari server", + "search_servers_or_enter_url": "Cari server atau masukkan URL" }, "empty_state": { "finding_servers": "Mencari server yang tersedia...", @@ -259,7 +259,7 @@ }, "register": { "title": "Beritahu kami tentang diri Anda.", - "lets_get_you_set_up_on_domain": "Let’s get you set up on %s", + "lets_get_you_set_up_on_domain": "Mari kita siapkan Anda di %s", "input": { "avatar": { "delete": "Hapus" @@ -269,18 +269,18 @@ "duplicate_prompt": "Nama pengguna ini sudah diambil." }, "display_name": { - "placeholder": "display name" + "placeholder": "nama yang ditampilkan" }, "email": { "placeholder": "surel" }, "password": { "placeholder": "kata sandi", - "require": "Your password needs at least:", - "character_limit": "8 characters", + "require": "Kata sandi Anda harus memiliki setidaknya:", + "character_limit": "8 karakter", "accessibility": { - "checked": "checked", - "unchecked": "unchecked" + "checked": "dicentang", + "unchecked": "tidak dicentang" }, "hint": "Kata sandi Anda harus memiliki sekurang-kurangnya delapan karakter" }, @@ -294,7 +294,7 @@ "email": "Surel", "password": "Kata sandi", "agreement": "Persetujuan", - "locale": "Locale", + "locale": "Lokal", "reason": "Alasan" }, "reason": { From ac3c32b8cc22535e36e62b0c37d6747bf6178112 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 06:19:28 +0100 Subject: [PATCH 531/658] New translations app.json (Galician) --- Localization/StringsConvertor/input/gl.lproj/app.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index ee2a7cef7..58463bdd0 100644 --- a/Localization/StringsConvertor/input/gl.lproj/app.json +++ b/Localization/StringsConvertor/input/gl.lproj/app.json @@ -385,10 +385,10 @@ "description_video": "Describe o vídeo para persoas con problemas visuais...", "load_failed": "Fallou a carga", "upload_failed": "Erro na subida", - "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "can_not_recognize_this_media_attachment": "Non se recoñece o tipo de multimedia", "attachment_too_large": "Adxunto demasiado grande", - "compressing_state": "Compressing...", - "server_processing_state": "Server Processing..." + "compressing_state": "Comprimindo...", + "server_processing_state": "Procesando no servidor..." }, "poll": { "duration_time": "Duración: %s", @@ -399,8 +399,8 @@ "three_days": "3 Días", "seven_days": "7 Días", "option_number": "Opción %ld", - "the_poll_is_invalid": "The poll is invalid", - "the_poll_has_empty_option": "The poll has empty option" + "the_poll_is_invalid": "A enquisa non é válida", + "the_poll_has_empty_option": "A enquisa ten unha opción baleira" }, "content_warning": { "placeholder": "Escribe o teu aviso aquí..." From 99a59f4360ff2b8c2483e93ed05a1e63772a5cff Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 07:22:16 +0100 Subject: [PATCH 532/658] New translations app.json (Galician) --- Localization/StringsConvertor/input/gl.lproj/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index 58463bdd0..e00791796 100644 --- a/Localization/StringsConvertor/input/gl.lproj/app.json +++ b/Localization/StringsConvertor/input/gl.lproj/app.json @@ -450,8 +450,8 @@ "content": "Contido" }, "verified": { - "short": "Verified on %s", - "long": "Ownership of this link was checked on %s" + "short": "Verificada en %s", + "long": "A propiedade desta ligazón foi verificada o %s" } }, "segmented_control": { From 5240006f079ad3817109e00f4d84167b67f379fe Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 16 Nov 2022 07:37:13 +0100 Subject: [PATCH 533/658] Improve textfield-UX (#540) --- Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift index 03c357fca..e641b3c8c 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift @@ -48,6 +48,9 @@ class MastodonLoginView: UIView { searchTextField.leftView = UIImageView(image: UIImage(systemName: "magnifyingglass")) searchTextField.leftViewMode = .always searchTextField.layer.cornerRadius = 10 + searchTextField.keyboardType = .URL + searchTextField.autocorrectionType = .no + searchTextField.autocapitalizationType = .none tableView = ContentSizedTableView() tableView.translatesAutoresizingMaskIntoConstraints = false From 890ba49f4e28305d9d0243fc6b3a3ee634509396 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 16 Nov 2022 07:38:04 +0100 Subject: [PATCH 534/658] Make magnifiying glass in searchTextField look great (#540) Thank you @MainasuK :thumbs_up: --- .../Onboarding/Login/MastodonLoginView.swift | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift index e641b3c8c..066ce454f 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift @@ -16,7 +16,12 @@ class MastodonLoginView: UIView { let titleLabel: UILabel let subtitleLabel: UILabel private let headerStackView: UIStackView + let searchTextField: UITextField + private let searchTextFieldLeftView: UIView + private let searchTextFieldMagnifyingGlass: UIImageView + private let searchContainerLeftPaddingView: UIView + let tableView: UITableView private var tableViewWrapper: UIView let navigationActionView: NavigationActionView @@ -41,11 +46,25 @@ class MastodonLoginView: UIView { headerStackView.spacing = 16 headerStackView.translatesAutoresizingMaskIntoConstraints = false + searchTextFieldMagnifyingGlass = UIImageView(image: UIImage( + systemName: "magnifyingglass", + withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .regular) + )) + searchTextFieldMagnifyingGlass.tintColor = Asset.Colors.Label.secondary.color.withAlphaComponent(0.6) + searchTextFieldMagnifyingGlass.translatesAutoresizingMaskIntoConstraints = false + + searchContainerLeftPaddingView = UIView() + searchContainerLeftPaddingView.translatesAutoresizingMaskIntoConstraints = false + + searchTextFieldLeftView = UIView() + searchTextFieldLeftView.addSubview(searchTextFieldMagnifyingGlass) + searchTextFieldLeftView.addSubview(searchContainerLeftPaddingView) + searchTextField = UITextField() searchTextField.translatesAutoresizingMaskIntoConstraints = false searchTextField.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color searchTextField.placeholder = L10n.Scene.Login.ServerSearchField.placeholder - searchTextField.leftView = UIImageView(image: UIImage(systemName: "magnifyingglass")) + searchTextField.leftView = searchTextFieldLeftView searchTextField.leftViewMode = .always searchTextField.layer.cornerRadius = 10 searchTextField.keyboardType = .URL @@ -97,6 +116,16 @@ class MastodonLoginView: UIView { searchTextField.heightAnchor.constraint(equalToConstant: 55), trailingAnchor.constraint(equalTo: searchTextField.trailingAnchor, constant: 16), + searchTextFieldMagnifyingGlass.topAnchor.constraint(equalTo: searchTextFieldLeftView.topAnchor), + searchTextFieldMagnifyingGlass.leadingAnchor.constraint(equalTo: searchTextFieldLeftView.leadingAnchor, constant: 8), + searchTextFieldMagnifyingGlass.bottomAnchor.constraint(equalTo: searchTextFieldLeftView.bottomAnchor), + + searchContainerLeftPaddingView.topAnchor.constraint(equalTo: searchTextFieldLeftView.topAnchor), + searchContainerLeftPaddingView.leadingAnchor.constraint(equalTo: searchTextFieldMagnifyingGlass.trailingAnchor), + searchContainerLeftPaddingView.trailingAnchor.constraint(equalTo: searchTextFieldLeftView.trailingAnchor), + searchContainerLeftPaddingView.bottomAnchor.constraint(equalTo: searchTextFieldLeftView.bottomAnchor), + searchContainerLeftPaddingView.widthAnchor.constraint(equalToConstant: 4).priority(.defaultHigh), + tableViewWrapper.topAnchor.constraint(equalTo: searchTextField.bottomAnchor), tableViewWrapper.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), trailingAnchor.constraint(equalTo: tableViewWrapper.trailingAnchor, constant: 16), From 9b2b42cddbb1be47e49969ee18d4c255281493bd Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 16 Nov 2022 07:40:23 +0100 Subject: [PATCH 535/658] Add a thin separator between tableView and searchTextField (#540) --- Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift index 066ce454f..16316ff56 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift @@ -126,7 +126,7 @@ class MastodonLoginView: UIView { searchContainerLeftPaddingView.bottomAnchor.constraint(equalTo: searchTextFieldLeftView.bottomAnchor), searchContainerLeftPaddingView.widthAnchor.constraint(equalToConstant: 4).priority(.defaultHigh), - tableViewWrapper.topAnchor.constraint(equalTo: searchTextField.bottomAnchor), + tableViewWrapper.topAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 2), tableViewWrapper.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), trailingAnchor.constraint(equalTo: tableViewWrapper.trailingAnchor, constant: 16), tableViewWrapper.bottomAnchor.constraint(lessThanOrEqualTo: navigationActionView.topAnchor), @@ -148,7 +148,7 @@ class MastodonLoginView: UIView { func updateCorners(numberOfResults: Int = 0) { tableView.isHidden = (numberOfResults == 0) - tableViewWrapper.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] // tableViewMask + tableViewWrapper.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] let maskedCorners: CACornerMask From 901fecc946a8f529423e86c89f0b91a00cc0bfa1 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 16 Nov 2022 07:42:27 +0100 Subject: [PATCH 536/658] Fix json (#540) :facepalm: --- Localization/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/app.json b/Localization/app.json index 3a1228131..96b2150ce 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -224,7 +224,7 @@ "server_search_field": { "placeholder": "Enter URL or search for your server" } - } + }, "server_picker": { "title": "Mastodon is made of users in different servers.", "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", From 1c0dbc806411a61f8b1a2424d15c300ab0491988 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 16 Nov 2022 08:16:32 +0100 Subject: [PATCH 537/658] Polish tableView (#540) --- .../Onboarding/Login/MastodonLoginView.swift | 28 ++++++------------- .../Login/MastodonLoginViewController.swift | 17 +---------- 2 files changed, 9 insertions(+), 36 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift index 16316ff56..d5f8b51d4 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginView.swift @@ -23,7 +23,6 @@ class MastodonLoginView: UIView { private let searchContainerLeftPaddingView: UIView let tableView: UITableView - private var tableViewWrapper: UIView let navigationActionView: NavigationActionView var bottomConstraint: NSLayoutConstraint? @@ -62,7 +61,7 @@ class MastodonLoginView: UIView { searchTextField = UITextField() searchTextField.translatesAutoresizingMaskIntoConstraints = false - searchTextField.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color + searchTextField.backgroundColor = Asset.Scene.Onboarding.searchBarBackground.color searchTextField.placeholder = L10n.Scene.Login.ServerSearchField.placeholder searchTextField.leftView = searchTextFieldLeftView searchTextField.leftViewMode = .always @@ -75,13 +74,7 @@ class MastodonLoginView: UIView { tableView.translatesAutoresizingMaskIntoConstraints = false tableView.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color tableView.keyboardDismissMode = .onDrag - - tableViewWrapper = UIView() - tableViewWrapper.translatesAutoresizingMaskIntoConstraints = false - tableViewWrapper.backgroundColor = .clear - tableViewWrapper.layer.cornerRadius = 10 - tableViewWrapper.layer.masksToBounds = true - tableViewWrapper.addSubview(tableView) + tableView.layer.cornerRadius = 10 navigationActionView = NavigationActionView() navigationActionView.translatesAutoresizingMaskIntoConstraints = false @@ -90,7 +83,7 @@ class MastodonLoginView: UIView { addSubview(headerStackView) addSubview(searchTextField) - addSubview(tableViewWrapper) + addSubview(tableView) addSubview(navigationActionView) backgroundColor = Asset.Scene.Onboarding.background.color @@ -126,15 +119,10 @@ class MastodonLoginView: UIView { searchContainerLeftPaddingView.bottomAnchor.constraint(equalTo: searchTextFieldLeftView.bottomAnchor), searchContainerLeftPaddingView.widthAnchor.constraint(equalToConstant: 4).priority(.defaultHigh), - tableViewWrapper.topAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 2), - tableViewWrapper.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), - trailingAnchor.constraint(equalTo: tableViewWrapper.trailingAnchor, constant: 16), - tableViewWrapper.bottomAnchor.constraint(lessThanOrEqualTo: navigationActionView.topAnchor), - - tableView.topAnchor.constraint(equalTo: tableViewWrapper.topAnchor), - tableView.leadingAnchor.constraint(equalTo: tableViewWrapper.leadingAnchor), - tableViewWrapper.trailingAnchor.constraint(equalTo: tableView.trailingAnchor), - tableViewWrapper.bottomAnchor.constraint(greaterThanOrEqualTo: tableView.bottomAnchor), + tableView.topAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 2), + tableView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + trailingAnchor.constraint(equalTo: tableView.trailingAnchor, constant: 16), + tableView.bottomAnchor.constraint(lessThanOrEqualTo: navigationActionView.topAnchor), navigationActionView.leadingAnchor.constraint(equalTo: leadingAnchor), navigationActionView.trailingAnchor.constraint(equalTo: trailingAnchor), @@ -148,7 +136,7 @@ class MastodonLoginView: UIView { func updateCorners(numberOfResults: Int = 0) { tableView.isHidden = (numberOfResults == 0) - tableViewWrapper.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + tableView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] let maskedCorners: CACornerMask diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 6125955f4..e9965fdde 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -85,22 +85,7 @@ class MastodonLoginViewController: UIViewController, NeedsDependency { cell.contentConfiguration = configuration cell.accessoryType = .disclosureIndicator - if #available(iOS 16.0, *) { - var backgroundConfiguration = cell.defaultBackgroundConfiguration() - backgroundConfiguration.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color - - cell.backgroundConfiguration = backgroundConfiguration - } else { - cell.backgroundColor = .systemBackground - } - - if self.viewModel.filteredServers.last == server { - cell.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] - cell.layer.cornerRadius = 10 - cell.layer.masksToBounds = true - } else { - cell.layer.masksToBounds = false - } + cell.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color return cell } From 70993b31de5f1888eef3bcdfadb7ee6d134c5b2d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:19 +0100 Subject: [PATCH 538/658] New translations app.json (Slovenian) --- .../StringsConvertor/input/sl.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/sl.lproj/app.json b/Localization/StringsConvertor/input/sl.lproj/app.json index 5f79ef02a..252d80def 100644 --- a/Localization/StringsConvertor/input/sl.lproj/app.json +++ b/Localization/StringsConvertor/input/sl.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Posnemi fotografijo", "save_photo": "Shrani fotografijo", "copy_photo": "Kopiraj fotografijo", - "sign_in": "Prijava", - "sign_up": "Registracija", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Pokaži več", "preview": "Predogled", "share": "Deli", @@ -218,10 +218,16 @@ "get_started": "Začnite", "log_in": "Prijava" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Mastodon tvorijo uporabniki z različnih strežnikov.", - "subtitle": "Strežnik izberite glede na svoje interese, regijo ali pa izberite splošnega.", - "subtitle_extend": "Strežnik izberite glede na svoje interese, regijo ali pa izberite splošnega. Z vsakim strežnikom upravlja povsem neodvisna organizacija ali posameznik.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Vse", @@ -248,8 +254,7 @@ "category": "KATEGORIJA" }, "input": { - "placeholder": "Išči strežnike", - "search_servers_or_enter_url": "Iščite strežnike ali vnesite URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Iskanje razpoložljivih strežnikov ...", @@ -719,4 +724,4 @@ "title": "Zaznamki" } } -} \ No newline at end of file +} From 83de7e4f9dd8990bed3d2e38cff3df0ea7c0acab Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:20 +0100 Subject: [PATCH 539/658] New translations app.json (Indonesian) --- .../StringsConvertor/input/id.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/id.lproj/app.json b/Localization/StringsConvertor/input/id.lproj/app.json index 864283436..dba617b0b 100644 --- a/Localization/StringsConvertor/input/id.lproj/app.json +++ b/Localization/StringsConvertor/input/id.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Ambil Foto", "save_photo": "Simpan Foto", "copy_photo": "Salin Foto", - "sign_in": "Masuk", - "sign_up": "Daftar", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Lihat lebih banyak", "preview": "Pratinjau", "share": "Bagikan", @@ -218,10 +218,16 @@ "get_started": "Mulai", "log_in": "Login" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Pilih sebuah server,\nserver manapun.", - "subtitle": "Pilih server berdasarkan minat, wilayah, atau tujuan Anda.", - "subtitle_extend": "Pilih server berdasarkan minat, wilayah, atau tujuan Anda. Setiap server dioperasikan oleh organisasi atau individu yang sepenuhnya independen.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Semua", @@ -248,8 +254,7 @@ "category": "KATEGORI" }, "input": { - "placeholder": "Cari server", - "search_servers_or_enter_url": "Cari server atau masukkan URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Mencari server yang tersedia...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From 34bf78e28cc03a0a4ac3e8d35bb31dcb5c2487d6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:21 +0100 Subject: [PATCH 540/658] New translations app.json (Portuguese) --- .../StringsConvertor/input/pt.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/pt.lproj/app.json b/Localization/StringsConvertor/input/pt.lproj/app.json index 30566d8d6..3113ada74 100644 --- a/Localization/StringsConvertor/input/pt.lproj/app.json +++ b/Localization/StringsConvertor/input/pt.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Take Photo", "save_photo": "Save Photo", "copy_photo": "Copy Photo", - "sign_in": "Sign In", - "sign_up": "Sign Up", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "See More", "preview": "Preview", "share": "Share", @@ -218,10 +218,16 @@ "get_started": "Get Started", "log_in": "Log In" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "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.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "All", @@ -248,8 +254,7 @@ "category": "CATEGORY" }, "input": { - "placeholder": "Search servers", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Finding available servers...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From a24fec4c8da14817ab98f7024a375ba0e1a69816 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:22 +0100 Subject: [PATCH 541/658] New translations app.json (Russian) --- .../StringsConvertor/input/ru.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/ru.lproj/app.json b/Localization/StringsConvertor/input/ru.lproj/app.json index 8505e7f42..25314102a 100644 --- a/Localization/StringsConvertor/input/ru.lproj/app.json +++ b/Localization/StringsConvertor/input/ru.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Сделать фото", "save_photo": "Сохранить изображение", "copy_photo": "Скопировать изображение", - "sign_in": "Войти", - "sign_up": "Зарегистрироваться", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Ещё", "preview": "Предпросмотр", "share": "Поделиться", @@ -218,10 +218,16 @@ "get_started": "Присоединиться", "log_in": "Вход" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Выберите сервер,\nлюбой сервер.", - "subtitle": "Выберите сообщество на основе своих интересов, региона или общей тематики.", - "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": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Все", @@ -248,8 +254,7 @@ "category": "КАТЕГОРИЯ" }, "input": { - "placeholder": "Найдите сервер или присоединитесь к своему...", - "search_servers_or_enter_url": "Поиск по серверам или ссылке" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Ищем доступные сервера...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From 93f6cd4812a45defbb549d23d69c20280067c749 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:23 +0100 Subject: [PATCH 542/658] New translations app.json (Chinese Simplified) --- .../input/zh-Hans.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json index c4b127914..242d9aac2 100644 --- a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "拍照", "save_photo": "保存照片", "copy_photo": "拷贝照片", - "sign_in": "登录", - "sign_up": "注册", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "查看更多", "preview": "预览", "share": "分享", @@ -218,10 +218,16 @@ "get_started": "开始使用", "log_in": "登录" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "挑选一个服务器,\n任意服务器。", - "subtitle": "根据你的兴趣、区域或一般目的选择一个社区。", - "subtitle_extend": "根据你的兴趣、区域或一般目的选择一个社区。每个社区都由完全独立的组织或个人管理。", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "全部", @@ -248,8 +254,7 @@ "category": "类别" }, "input": { - "placeholder": "查找或加入你自己的服务器...", - "search_servers_or_enter_url": "搜索服务器或输入 URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "正在查找可用的服务器...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From 259f3f1ddea602336330353a66c5b60a076fc502 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:24 +0100 Subject: [PATCH 543/658] New translations app.json (English) --- .../StringsConvertor/input/en.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/en.lproj/app.json b/Localization/StringsConvertor/input/en.lproj/app.json index 30566d8d6..3113ada74 100644 --- a/Localization/StringsConvertor/input/en.lproj/app.json +++ b/Localization/StringsConvertor/input/en.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Take Photo", "save_photo": "Save Photo", "copy_photo": "Copy Photo", - "sign_in": "Sign In", - "sign_up": "Sign Up", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "See More", "preview": "Preview", "share": "Share", @@ -218,10 +218,16 @@ "get_started": "Get Started", "log_in": "Log In" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "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.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "All", @@ -248,8 +254,7 @@ "category": "CATEGORY" }, "input": { - "placeholder": "Search servers", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Finding available servers...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From 6f71369c0134112edc481d66a0a6639083004307 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:25 +0100 Subject: [PATCH 544/658] New translations app.json (Galician) --- .../StringsConvertor/input/gl.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index e00791796..f6e01439a 100644 --- a/Localization/StringsConvertor/input/gl.lproj/app.json +++ b/Localization/StringsConvertor/input/gl.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Facer foto", "save_photo": "Gardar foto", "copy_photo": "Copiar foto", - "sign_in": "Acceder", - "sign_up": "Inscribirse", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Ver máis", "preview": "Vista previa", "share": "Compartir", @@ -218,10 +218,16 @@ "get_started": "Crear conta", "log_in": "Acceder" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Mastodon fórmano as persoas das diferentes comunidades.", - "subtitle": "Elixe unha comunidade en función dos teus intereses, rexión ou unha de propósito xeral.", - "subtitle_extend": "Elixe unha comunidade en función dos teus intereses, rexión ou unha de propósito xeral. Cada comunidade está xestionada por unha organización totalmente independente ou unha única persoa.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Todo", @@ -248,8 +254,7 @@ "category": "CATEGORÍA" }, "input": { - "placeholder": "Buscar comunidades", - "search_servers_or_enter_url": "Busca un servidor ou escribe URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Buscando servidores dispoñibles...", @@ -719,4 +724,4 @@ "title": "Marcadores" } } -} \ No newline at end of file +} From e6f8963ae9ec38a9c92aa369765e0cc3dfdac49a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:26 +0100 Subject: [PATCH 545/658] New translations app.json (Portuguese, Brazilian) --- .../input/pt-BR.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/app.json b/Localization/StringsConvertor/input/pt-BR.lproj/app.json index b69c14777..98bb960cc 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/app.json +++ b/Localization/StringsConvertor/input/pt-BR.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Tirar foto", "save_photo": "Salvar foto", "copy_photo": "Copiar foto", - "sign_in": "Entrar", - "sign_up": "Criar conta", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Ver mais", "preview": "Pré-visualização", "share": "Compartilhar", @@ -218,10 +218,16 @@ "get_started": "Comece já", "log_in": "Entrar" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "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.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Todos", @@ -248,8 +254,7 @@ "category": "Categoria" }, "input": { - "placeholder": "Procurar instâncias", - "search_servers_or_enter_url": "Procurar instâncias ou inserir URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Procurando instâncias disponíveis...", @@ -719,4 +724,4 @@ "title": "Marcados" } } -} \ No newline at end of file +} From 4ff233aa56e6b7459e2ef3014e87c55bff0004ea Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:28 +0100 Subject: [PATCH 546/658] New translations app.json (Spanish, Argentina) --- .../input/es-AR.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/es-AR.lproj/app.json b/Localization/StringsConvertor/input/es-AR.lproj/app.json index 21934a104..86762f6d4 100644 --- a/Localization/StringsConvertor/input/es-AR.lproj/app.json +++ b/Localization/StringsConvertor/input/es-AR.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Tomar foto", "save_photo": "Guardar foto", "copy_photo": "Copiar foto", - "sign_in": "Iniciar sesión", - "sign_up": "Registrarse", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Ver más", "preview": "Previsualización", "share": "Compartir", @@ -218,10 +218,16 @@ "get_started": "Comenzá", "log_in": "Iniciar sesión" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Mastodon está compuesto de cuentas en diferentes servidores.", - "subtitle": "Elegí un servidor basado en tus intereses, región, o de propósitos generales.", - "subtitle_extend": "Elegí un servidor basado en tus intereses, región, o de propósitos generales. Cada servidor es operado por una organización o individuo totalmente independientes.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Todas", @@ -248,8 +254,7 @@ "category": "CATEGORÍA" }, "input": { - "placeholder": "Buscar servidores", - "search_servers_or_enter_url": "Buscar servidores o introducir dirección web" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Buscando servidores disponibles…", @@ -719,4 +724,4 @@ "title": "Marcadores" } } -} \ No newline at end of file +} From cc65912ece8ed936883c105ff4a10bccb11f5e3e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:29 +0100 Subject: [PATCH 547/658] New translations app.json (Japanese) --- .../StringsConvertor/input/ja.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/ja.lproj/app.json b/Localization/StringsConvertor/input/ja.lproj/app.json index d145ff494..f73faabd4 100644 --- a/Localization/StringsConvertor/input/ja.lproj/app.json +++ b/Localization/StringsConvertor/input/ja.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "写真を撮る", "save_photo": "写真を撮る", "copy_photo": "写真をコピー", - "sign_in": "サインイン", - "sign_up": "サインアップ", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "もっと見る", "preview": "プレビュー", "share": "共有", @@ -218,10 +218,16 @@ "get_started": "はじめる", "log_in": "ログイン" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "サーバーを選択", - "subtitle": "あなたの興味分野・地域に合ったコミュニティや、汎用のものを選択してください。", - "subtitle_extend": "あなたの興味分野・地域に合ったコミュニティや、汎用のものを選択してください。各コミュニティはそれぞれ完全に独立した組織や個人によって運営されています。", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "すべて", @@ -248,8 +254,7 @@ "category": "カテゴリー" }, "input": { - "placeholder": "サーバーを探す", - "search_servers_or_enter_url": "サーバーを検索またはURLを入力" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "利用可能なサーバーの検索...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From c9c96d5f87245788d5152684d9c0163c57e7cb1e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:30 +0100 Subject: [PATCH 548/658] New translations app.json (Thai) --- .../StringsConvertor/input/th.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/th.lproj/app.json b/Localization/StringsConvertor/input/th.lproj/app.json index dd5ee212c..50f43375e 100644 --- a/Localization/StringsConvertor/input/th.lproj/app.json +++ b/Localization/StringsConvertor/input/th.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "ถ่ายรูป", "save_photo": "บันทึกรูปภาพ", "copy_photo": "คัดลอกรูปภาพ", - "sign_in": "ลงชื่อเข้า", - "sign_up": "ลงทะเบียน", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "ดูเพิ่มเติม", "preview": "แสดงตัวอย่าง", "share": "แบ่งปัน", @@ -218,10 +218,16 @@ "get_started": "เริ่มต้นใช้งาน", "log_in": "เข้าสู่ระบบ" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Mastodon ประกอบด้วยผู้ใช้ในเซิร์ฟเวอร์ต่าง ๆ", - "subtitle": "เลือกเซิร์ฟเวอร์ตามความสนใจ, ภูมิภาค หรือวัตถุประสงค์ทั่วไปของคุณ", - "subtitle_extend": "เลือกเซิร์ฟเวอร์ตามความสนใจ, ภูมิภาค หรือวัตถุประสงค์ทั่วไปของคุณ แต่ละเซิร์ฟเวอร์ได้รับการดำเนินงานโดยองค์กรหรือบุคคลที่เป็นอิสระโดยสิ้นเชิง", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "ทั้งหมด", @@ -248,8 +254,7 @@ "category": "หมวดหมู่" }, "input": { - "placeholder": "ค้นหาเซิร์ฟเวอร์", - "search_servers_or_enter_url": "ค้นหาเซิร์ฟเวอร์หรือป้อน URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "กำลังค้นหาเซิร์ฟเวอร์ที่พร้อมใช้งาน...", @@ -719,4 +724,4 @@ "title": "ที่คั่นหน้า" } } -} \ No newline at end of file +} From c86bf006cd73fa036a9f41c9ccb5e098b1721cf4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:31 +0100 Subject: [PATCH 549/658] New translations app.json (Latvian) --- .../StringsConvertor/input/lv.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/lv.lproj/app.json b/Localization/StringsConvertor/input/lv.lproj/app.json index f21cde453..1ca18400b 100644 --- a/Localization/StringsConvertor/input/lv.lproj/app.json +++ b/Localization/StringsConvertor/input/lv.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Uzņemt bildi", "save_photo": "Saglabāt bildi", "copy_photo": "Kopēt bildi", - "sign_in": "Pieteikties", - "sign_up": "Reģistrēties", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Skatīt vairāk", "preview": "Priekšskatījums", "share": "Dalīties", @@ -218,10 +218,16 @@ "get_started": "Get Started", "log_in": "Pieteikties" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "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.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Visi", @@ -248,8 +254,7 @@ "category": "KATEGORIJA" }, "input": { - "placeholder": "Meklēt serverus", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Finding available servers...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From c32dc91d7caaf680c0ca7ad571f5c6c7f521bb7c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:32 +0100 Subject: [PATCH 550/658] New translations app.json (Hindi) --- .../StringsConvertor/input/hi.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/hi.lproj/app.json b/Localization/StringsConvertor/input/hi.lproj/app.json index ec405da4d..f9414b6c0 100644 --- a/Localization/StringsConvertor/input/hi.lproj/app.json +++ b/Localization/StringsConvertor/input/hi.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Take Photo", "save_photo": "Save Photo", "copy_photo": "Copy Photo", - "sign_in": "Sign In", - "sign_up": "Sign Up", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "See More", "preview": "Preview", "share": "Share", @@ -218,10 +218,16 @@ "get_started": "Get Started", "log_in": "Log In" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "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.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "All", @@ -248,8 +254,7 @@ "category": "CATEGORY" }, "input": { - "placeholder": "Search servers", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Finding available servers...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From ff2663908572ed878b98f9f5aca3257235d119aa Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:33 +0100 Subject: [PATCH 551/658] New translations app.json (English, United States) --- .../input/en-US.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/en-US.lproj/app.json b/Localization/StringsConvertor/input/en-US.lproj/app.json index 30566d8d6..3113ada74 100644 --- a/Localization/StringsConvertor/input/en-US.lproj/app.json +++ b/Localization/StringsConvertor/input/en-US.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Take Photo", "save_photo": "Save Photo", "copy_photo": "Copy Photo", - "sign_in": "Sign In", - "sign_up": "Sign Up", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "See More", "preview": "Preview", "share": "Share", @@ -218,10 +218,16 @@ "get_started": "Get Started", "log_in": "Log In" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "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.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "All", @@ -248,8 +254,7 @@ "category": "CATEGORY" }, "input": { - "placeholder": "Search servers", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Finding available servers...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From a6f26050a6498de56808b144029e4a806aa32e10 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:34 +0100 Subject: [PATCH 552/658] New translations app.json (Welsh) --- .../StringsConvertor/input/cy.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/cy.lproj/app.json b/Localization/StringsConvertor/input/cy.lproj/app.json index 5a962d852..fec3197be 100644 --- a/Localization/StringsConvertor/input/cy.lproj/app.json +++ b/Localization/StringsConvertor/input/cy.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Take Photo", "save_photo": "Save Photo", "copy_photo": "Copy Photo", - "sign_in": "Sign In", - "sign_up": "Sign Up", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "See More", "preview": "Preview", "share": "Share", @@ -218,10 +218,16 @@ "get_started": "Get Started", "log_in": "Log In" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "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.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "All", @@ -248,8 +254,7 @@ "category": "CATEGORY" }, "input": { - "placeholder": "Search servers", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Finding available servers...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From d04313854cef587652730830e2984377c6b056b5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:35 +0100 Subject: [PATCH 553/658] New translations app.json (Sinhala) --- .../StringsConvertor/input/si.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/si.lproj/app.json b/Localization/StringsConvertor/input/si.lproj/app.json index 2ac50e06b..a4542b9e9 100644 --- a/Localization/StringsConvertor/input/si.lproj/app.json +++ b/Localization/StringsConvertor/input/si.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Take Photo", "save_photo": "Save Photo", "copy_photo": "Copy Photo", - "sign_in": "පිවිසෙන්න", - "sign_up": "ලියාපදිංචිය", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "තව බලන්න", "preview": "පෙරදසුන", "share": "බෙදාගන්න", @@ -218,10 +218,16 @@ "get_started": "පටන් ගන්න", "log_in": "පිවිසෙන්න" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "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.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "සියල්ල", @@ -248,8 +254,7 @@ "category": "CATEGORY" }, "input": { - "placeholder": "Search servers", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Finding available servers...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From b16cde910b694add79977bf74fd7fa4cb4ffb232 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:36 +0100 Subject: [PATCH 554/658] New translations app.json (Kurmanji (Kurdish)) --- .../StringsConvertor/input/kmr.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/kmr.lproj/app.json b/Localization/StringsConvertor/input/kmr.lproj/app.json index f52e1bdae..4a2bc3283 100644 --- a/Localization/StringsConvertor/input/kmr.lproj/app.json +++ b/Localization/StringsConvertor/input/kmr.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Wêne bikişîne", "save_photo": "Wêneyê tomar bike", "copy_photo": "Wêneyê jê bigire", - "sign_in": "Têkeve", - "sign_up": "Tomar bibe", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Bêtir bibîne", "preview": "Pêşdîtin", "share": "Parve bike", @@ -218,10 +218,16 @@ "get_started": "Dest pê bike", "log_in": "Têkeve" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Mastodon ji bikarhênerên di civakên cuda de pêk tê.", - "subtitle": "Li gorî berjewendî, herêm, an jî armancek gelemperî civakekê hilbijêre.", - "subtitle_extend": "Li gorî berjewendî, herêm, an jî armancek gelemperî civakekê hilbijêre. Her civakek ji hêla rêxistinek an kesek bi tevahî serbixwe ve tê xebitandin.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Hemû", @@ -248,8 +254,7 @@ "category": "BEŞ" }, "input": { - "placeholder": "Li rajekaran bigere", - "search_servers_or_enter_url": "Li rajekaran bigere an jî girêdanê têxe" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Peydakirina rajekarên berdest...", @@ -719,4 +724,4 @@ "title": "Şûnpel" } } -} \ No newline at end of file +} From 997b856ae8c870ad779cb551c65f97c7a6fe0400 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:37 +0100 Subject: [PATCH 555/658] New translations app.json (Dutch) --- .../StringsConvertor/input/nl.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/nl.lproj/app.json b/Localization/StringsConvertor/input/nl.lproj/app.json index 5241d23bf..589c51d2d 100644 --- a/Localization/StringsConvertor/input/nl.lproj/app.json +++ b/Localization/StringsConvertor/input/nl.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Maak foto", "save_photo": "Bewaar foto", "copy_photo": "Kopieer foto", - "sign_in": "Aanmelden", - "sign_up": "Registreren", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Meer", "preview": "Voorvertoning", "share": "Deel", @@ -218,10 +218,16 @@ "get_started": "Aan de slag", "log_in": "Log in" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Kies een server, welke dan ook.", - "subtitle": "Kies een gemeenschap gebaseerd op jouw interesses, regio of een algemeen doel.", - "subtitle_extend": "Kies een gemeenschap gebaseerd op jouw interesses, regio, of een algemeen doel. Elke gemeenschap wordt beheerd door een volledig onafhankelijke organisatie of individu.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Alles", @@ -248,8 +254,7 @@ "category": "CATEGORIE" }, "input": { - "placeholder": "Zoek uw server of sluit u bij een nieuwe server aan...", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Beschikbare servers zoeken...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From 3e89962373f04fc73312e671fe7bd798e76ded14 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:38 +0100 Subject: [PATCH 556/658] New translations app.json (Italian) --- .../StringsConvertor/input/it.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json index c0d33a73b..81131e857 100644 --- a/Localization/StringsConvertor/input/it.lproj/app.json +++ b/Localization/StringsConvertor/input/it.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Scatta foto", "save_photo": "Salva foto", "copy_photo": "Copia foto", - "sign_in": "Accedi", - "sign_up": "Registrati", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Visualizza altro", "preview": "Anteprima", "share": "Condividi", @@ -218,10 +218,16 @@ "get_started": "Inizia", "log_in": "Accedi" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Mastodon è fatto di utenti in diverse comunità.", - "subtitle": "Scegli una comunità basata sui tuoi interessi, regione o uno scopo generale.", - "subtitle_extend": "Scegli una comunità basata sui tuoi interessi, regione o uno scopo generale. Ogni comunità è gestita da un'organizzazione completamente indipendente o individuale.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Tutti", @@ -248,8 +254,7 @@ "category": "CATEGORIA" }, "input": { - "placeholder": "Cerca comunità", - "search_servers_or_enter_url": "Cerca i server o inserisci l'URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Ricerca server disponibili...", @@ -719,4 +724,4 @@ "title": "Segnalibri" } } -} \ No newline at end of file +} From 58ae3668e2f7114a407a028352dd3189281f9f53 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:39 +0100 Subject: [PATCH 557/658] New translations app.json (Chinese Traditional) --- .../input/zh-Hant.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json index d7d02240f..c7607ba2a 100644 --- a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "拍攝照片", "save_photo": "儲存照片", "copy_photo": "複製照片", - "sign_in": "登入", - "sign_up": "註冊", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "檢視更多", "preview": "預覽", "share": "分享", @@ -218,10 +218,16 @@ "get_started": "新手上路", "log_in": "登入" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Mastodon 由不同伺服器的使用者組成。", - "subtitle": "基於您的興趣、地區、或一般用途選定一個伺服器。", - "subtitle_extend": "基於您的興趣、地區、或一般用途選定一個伺服器。每個伺服器是由完全獨立的組織或個人營運。", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "全部", @@ -248,8 +254,7 @@ "category": "分類" }, "input": { - "placeholder": "搜尋伺服器", - "search_servers_or_enter_url": "搜尋伺服器或輸入網址" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "尋找可用的伺服器...", @@ -719,4 +724,4 @@ "title": "書籤" } } -} \ No newline at end of file +} From 2b933c33270a68c0a7abc05988b921f99a755a9f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:40 +0100 Subject: [PATCH 558/658] New translations app.json (Ukrainian) --- .../StringsConvertor/input/uk.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/uk.lproj/app.json b/Localization/StringsConvertor/input/uk.lproj/app.json index 30566d8d6..3113ada74 100644 --- a/Localization/StringsConvertor/input/uk.lproj/app.json +++ b/Localization/StringsConvertor/input/uk.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Take Photo", "save_photo": "Save Photo", "copy_photo": "Copy Photo", - "sign_in": "Sign In", - "sign_up": "Sign Up", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "See More", "preview": "Preview", "share": "Share", @@ -218,10 +218,16 @@ "get_started": "Get Started", "log_in": "Log In" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "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.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "All", @@ -248,8 +254,7 @@ "category": "CATEGORY" }, "input": { - "placeholder": "Search servers", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Finding available servers...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From dd8c8409ea05bbcfb4ae78dc937a55c8de1d9216 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:42 +0100 Subject: [PATCH 559/658] New translations app.json (Vietnamese) --- .../StringsConvertor/input/vi.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/vi.lproj/app.json b/Localization/StringsConvertor/input/vi.lproj/app.json index ff751d964..e7e5a1372 100644 --- a/Localization/StringsConvertor/input/vi.lproj/app.json +++ b/Localization/StringsConvertor/input/vi.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Chụp ảnh", "save_photo": "Lưu ảnh", "copy_photo": "Sao chép ảnh", - "sign_in": "Đăng nhập", - "sign_up": "Đăng ký", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Xem thêm", "preview": "Xem trước", "share": "Chia sẻ", @@ -218,10 +218,16 @@ "get_started": "Bắt đầu", "log_in": "Đăng nhập" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Mastodon gồm nhiều máy chủ với thành viên riêng.", - "subtitle": "Chọn một máy chủ dựa theo sở thích, tôn giáo, hoặc ý muốn của bạn.", - "subtitle_extend": "Chọn một máy chủ dựa theo sở thích, tôn giáo, hoặc ý muốn của bạn. Mỗi máy chủ có thể được vận hành bởi một cá nhân hoặc một tổ chức.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Toàn bộ", @@ -248,8 +254,7 @@ "category": "PHÂN LOẠI" }, "input": { - "placeholder": "Tìm máy chủ", - "search_servers_or_enter_url": "Tìm máy chủ hoặc nhập URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Đang tìm máy chủ hoạt động...", @@ -719,4 +724,4 @@ "title": "Tút đã lưu" } } -} \ No newline at end of file +} From 1d9f4530a14898d37d2a79735039f124dec15382 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:43 +0100 Subject: [PATCH 560/658] New translations app.json (Kabyle) --- .../StringsConvertor/input/kab.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/kab.lproj/app.json b/Localization/StringsConvertor/input/kab.lproj/app.json index 713cc8959..fb8add87b 100644 --- a/Localization/StringsConvertor/input/kab.lproj/app.json +++ b/Localization/StringsConvertor/input/kab.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Ṭṭef tawlaft", "save_photo": "Sekles tawlaft", "copy_photo": "Nɣel tawlaft", - "sign_in": "Qqen", - "sign_up": "Jerred amiḍan", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Wali ugar", "preview": "Taskant", "share": "Bḍu", @@ -218,10 +218,16 @@ "get_started": "Aha bdu tura", "log_in": "Qqen" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Mastodon yettwaxdem i yiseqdacen deg waṭas n temɣiwnin.", - "subtitle": "Fren tamɣiwent almend n wayen tḥemmleḍ, n tmurt-ik neɣ n yiswi-inek amatu.", - "subtitle_extend": "Fren tamɣiwent almend n wayen tḥemmleḍ, n tmurt-ik neɣ n yiswi-inek amatu. Yal tamɣiwent tsedday-itt tkebbanit neɣ amdan ilelliyen.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Akk", @@ -248,8 +254,7 @@ "category": "TAGGAYT" }, "input": { - "placeholder": "Nadi timɣiwnin", - "search_servers_or_enter_url": "Nadi timɣiwnin neɣ sekcem URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Tifin n yiqeddacen yellan...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From 4952b434b3faf2de93b27da8f7c3b545f33161d4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:44 +0100 Subject: [PATCH 561/658] New translations app.json (Korean) --- .../StringsConvertor/input/ko.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/ko.lproj/app.json b/Localization/StringsConvertor/input/ko.lproj/app.json index a871d2d00..3971b18e9 100644 --- a/Localization/StringsConvertor/input/ko.lproj/app.json +++ b/Localization/StringsConvertor/input/ko.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "사진 촬영", "save_photo": "사진 저장", "copy_photo": "사진 복사", - "sign_in": "로그인", - "sign_up": "회원가입", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "더 보기", "preview": "미리보기", "share": "공유", @@ -218,10 +218,16 @@ "get_started": "시작하기", "log_in": "로그인" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "서버를 고르세요,\n아무 서버나 좋습니다.", - "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": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "모두", @@ -248,8 +254,7 @@ "category": "분류" }, "input": { - "placeholder": "서버 검색", - "search_servers_or_enter_url": "서버를 검색하거나 URL을 입력하세요" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "사용 가능한 서버를 찾는 중입니다...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From cd9209f84beb8b087105cac38ea93686c8533724 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:45 +0100 Subject: [PATCH 562/658] New translations app.json (Swedish) --- .../StringsConvertor/input/sv.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/sv.lproj/app.json b/Localization/StringsConvertor/input/sv.lproj/app.json index 6375771ff..27f931249 100644 --- a/Localization/StringsConvertor/input/sv.lproj/app.json +++ b/Localization/StringsConvertor/input/sv.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Ta foto", "save_photo": "Spara foto", "copy_photo": "Kopiera foto", - "sign_in": "Logga in", - "sign_up": "Registrera dig", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Visa mer", "preview": "Förhandsvisa", "share": "Dela", @@ -218,10 +218,16 @@ "get_started": "Kom igång", "log_in": "Logga in" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Mastodon utgörs av användare på olika servrar.", - "subtitle": "Välj en server baserat på dina intressen, region eller ett allmänt syfte.", - "subtitle_extend": "Välj en server baserat på dina intressen, region eller ett allmänt syfte. Varje server drivs av en helt oberoende organisation eller individ.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Alla", @@ -248,8 +254,7 @@ "category": "KATEGORI" }, "input": { - "placeholder": "Sök gemenskaper", - "search_servers_or_enter_url": "Sök servrar eller ange URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Söker tillgängliga servrar...", @@ -719,4 +724,4 @@ "title": "Bokmärken" } } -} \ No newline at end of file +} From 8fece50f4a5a05a6a54a41f9af02ef66f4f502d0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:46 +0100 Subject: [PATCH 563/658] New translations app.json (French) --- .../StringsConvertor/input/fr.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/fr.lproj/app.json b/Localization/StringsConvertor/input/fr.lproj/app.json index 7dffb4aba..effeafe12 100644 --- a/Localization/StringsConvertor/input/fr.lproj/app.json +++ b/Localization/StringsConvertor/input/fr.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Prendre une photo", "save_photo": "Enregistrer la photo", "copy_photo": "Copier la photo", - "sign_in": "Se connecter", - "sign_up": "Créer un compte", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Voir plus", "preview": "Aperçu", "share": "Partager", @@ -218,10 +218,16 @@ "get_started": "Prise en main", "log_in": "Se connecter" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Choisissez un serveur,\nn'importe quel serveur.", - "subtitle": "Choisissez une communauté en fonction de vos intérêts, de votre région ou de votre objectif général.", - "subtitle_extend": "Choisissez une communauté basée sur vos intérêts, votre région ou un but général. Chaque communauté est gérée par une organisation ou un individu entièrement indépendant.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Tout", @@ -248,8 +254,7 @@ "category": "CATÉGORIE" }, "input": { - "placeholder": "Trouvez un serveur ou rejoignez le vôtre...", - "search_servers_or_enter_url": "Rechercher des serveurs ou entrer une URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Recherche des serveurs disponibles...", @@ -719,4 +724,4 @@ "title": "Favoris" } } -} \ No newline at end of file +} From 687db19201e48f39c73449e5f9496a0c7b3fd443 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:47 +0100 Subject: [PATCH 564/658] New translations app.json (Turkish) --- .../StringsConvertor/input/tr.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/tr.lproj/app.json b/Localization/StringsConvertor/input/tr.lproj/app.json index 27ebf6e0a..37325cf05 100644 --- a/Localization/StringsConvertor/input/tr.lproj/app.json +++ b/Localization/StringsConvertor/input/tr.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Fotoğraf Çek", "save_photo": "Fotoğrafı Kaydet", "copy_photo": "Fotoğrafı Kopyala", - "sign_in": "Giriş Yap", - "sign_up": "Kaydol", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Daha Fazla Gör", "preview": "Önizleme", "share": "Paylaş", @@ -218,10 +218,16 @@ "get_started": "Başlayın", "log_in": "Oturum Aç" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Mastodon, farklı topluluklardaki kullanıcılardan oluşur.", - "subtitle": "İlgi alanlarınıza, bölgenize veya genel amaçlı bir topluluk seçin.", - "subtitle_extend": "İlgi alanlarınıza, bölgenize veya genel amaçlı bir topluluk seçin. Her topluluk tamamen bağımsız bir kuruluş veya kişi tarafından işletilmektedir.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Tümü", @@ -248,8 +254,7 @@ "category": "KATEGORİ" }, "input": { - "placeholder": "Toplulukları ara", - "search_servers_or_enter_url": "Sunucuları ara ya da bir bağlantı gir" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Mevcut sunucular aranıyor...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From ff1899b8ff9e7f8b9f0e6356ad5b41b9dd3717ab Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:48 +0100 Subject: [PATCH 565/658] New translations app.json (Czech) --- .../StringsConvertor/input/cs.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/cs.lproj/app.json b/Localization/StringsConvertor/input/cs.lproj/app.json index d6624a26b..751ae6a61 100644 --- a/Localization/StringsConvertor/input/cs.lproj/app.json +++ b/Localization/StringsConvertor/input/cs.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Vyfotit", "save_photo": "Uložit fotku", "copy_photo": "Kopírovat fotografii", - "sign_in": "Přihlásit se", - "sign_up": "Zaregistrovat se", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Zobrazit více", "preview": "Náhled", "share": "Sdílet", @@ -218,10 +218,16 @@ "get_started": "Začínáme", "log_in": "Přihlásit se" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Mastodon tvoří uživatelé z různých serverů.", - "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.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Vše", @@ -248,8 +254,7 @@ "category": "KATEGORIE" }, "input": { - "placeholder": "Hledat servery", - "search_servers_or_enter_url": "Hledat servery nebo zadat URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Hledání dostupných serverů...", @@ -719,4 +724,4 @@ "title": "Záložky" } } -} \ No newline at end of file +} From 4c5f4c3dfc8286f1ce5c25c97314ebec530bbb7d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:49 +0100 Subject: [PATCH 566/658] New translations app.json (Scottish Gaelic) --- .../StringsConvertor/input/gd.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/gd.lproj/app.json b/Localization/StringsConvertor/input/gd.lproj/app.json index 6a575afc2..f29f0fa17 100644 --- a/Localization/StringsConvertor/input/gd.lproj/app.json +++ b/Localization/StringsConvertor/input/gd.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Tog dealbh", "save_photo": "Sàbhail an dealbh", "copy_photo": "Dèan lethbhreac dhen dealbh", - "sign_in": "Clàraich a-steach", - "sign_up": "Clàraich leinn", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Seall a bharrachd", "preview": "Ro-sheall", "share": "Co-roinn", @@ -218,10 +218,16 @@ "get_started": "Dèan toiseach-tòiseachaidh", "log_in": "Clàraich a-steach" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Tha cleachdaichean Mhastodon air iomadh frithealaiche eadar-dhealaichte.", - "subtitle": "Tagh frithealaiche stèidhichte air d’ ùidhean, air far a bheil thu no fear coitcheann.", - "subtitle_extend": "Tagh frithealaiche stèidhichte air d’ ùidhean, air far a bheil thu no fear coitcheann. Tha gach frithealaiche fo stiùireadh buidhinn no neach neo-eisimeilich fa leth.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Na h-uile", @@ -248,8 +254,7 @@ "category": "ROINN-SEÒRSA" }, "input": { - "placeholder": "Lorg frithealaiche", - "search_servers_or_enter_url": "Lorg frithealaiche no cuir a-steach URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "A’ lorg nam frithealaichean ri am faighinn…", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From 68f7e6f19571abe6bafbab8358f72fa05f91ee8d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:51 +0100 Subject: [PATCH 567/658] New translations app.json (Finnish) --- .../StringsConvertor/input/fi.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/fi.lproj/app.json b/Localization/StringsConvertor/input/fi.lproj/app.json index febfee2ca..7dafe7fd1 100644 --- a/Localization/StringsConvertor/input/fi.lproj/app.json +++ b/Localization/StringsConvertor/input/fi.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Ota kuva", "save_photo": "Tallenna kuva", "copy_photo": "Kopioi kuva", - "sign_in": "Kirjaudu sisään", - "sign_up": "Rekisteröidy", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Näytä lisää", "preview": "Esikatselu", "share": "Jaa", @@ -218,10 +218,16 @@ "get_started": "Get Started", "log_in": "Log In" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Valitse palvelin,\nmikä tahansa palvelin.", - "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": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Kaikki", @@ -248,8 +254,7 @@ "category": "KATEGORIA" }, "input": { - "placeholder": "Etsi palvelin tai liity omaan...", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Etsistään saatavilla olevia palvelimia...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From fced11f09138217eda093c8722a925f605874c6c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:52 +0100 Subject: [PATCH 568/658] New translations app.json (Romanian) --- .../StringsConvertor/input/ro.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/ro.lproj/app.json b/Localization/StringsConvertor/input/ro.lproj/app.json index 18fcb023b..75a77184c 100644 --- a/Localization/StringsConvertor/input/ro.lproj/app.json +++ b/Localization/StringsConvertor/input/ro.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Take Photo", "save_photo": "Save Photo", "copy_photo": "Copy Photo", - "sign_in": "Sign In", - "sign_up": "Sign Up", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "See More", "preview": "Preview", "share": "Share", @@ -218,10 +218,16 @@ "get_started": "Get Started", "log_in": "Log In" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "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.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "All", @@ -248,8 +254,7 @@ "category": "CATEGORY" }, "input": { - "placeholder": "Search servers", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Finding available servers...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From 1de7a45a5369273fa10496d984afc96077a6b388 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:53 +0100 Subject: [PATCH 569/658] New translations app.json (Spanish) --- .../StringsConvertor/input/es.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/es.lproj/app.json b/Localization/StringsConvertor/input/es.lproj/app.json index a7500e27f..1b90bfa10 100644 --- a/Localization/StringsConvertor/input/es.lproj/app.json +++ b/Localization/StringsConvertor/input/es.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Tomar foto", "save_photo": "Guardar foto", "copy_photo": "Copiar foto", - "sign_in": "Iniciar sesión", - "sign_up": "Regístrate", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Ver más", "preview": "Vista previa", "share": "Compartir", @@ -218,10 +218,16 @@ "get_started": "Empezar", "log_in": "Iniciar sesión" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Elige un servidor,\ncualquier servidor.", - "subtitle": "Elige una comunidad relacionada con tus intereses, con tu región o una más genérica.", - "subtitle_extend": "Elige una comunidad relacionada con tus intereses, con tu región o una más genérica. Cada comunidad está operada por una organización o individuo completamente independiente.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Todas", @@ -248,8 +254,7 @@ "category": "CATEGORÍA" }, "input": { - "placeholder": "Encuentra un servidor o únete al tuyo propio...", - "search_servers_or_enter_url": "Buscar servidores o introducir la URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Encontrando servidores disponibles...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From baa8a85fa63c4236000bb0fc04214ee31e2aead3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:54 +0100 Subject: [PATCH 570/658] New translations app.json (Arabic) --- .../StringsConvertor/input/ar.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/ar.lproj/app.json b/Localization/StringsConvertor/input/ar.lproj/app.json index 1a52ccfbb..2ed2be724 100644 --- a/Localization/StringsConvertor/input/ar.lproj/app.json +++ b/Localization/StringsConvertor/input/ar.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "اِلتِقاطُ صُورَة", "save_photo": "حفظ الصورة", "copy_photo": "نسخ الصورة", - "sign_in": "تسجيل الدخول", - "sign_up": "إنشاء حِساب", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "عرض المزيد", "preview": "مُعاينة", "share": "المُشارك", @@ -218,10 +218,16 @@ "get_started": "ابدأ الآن", "log_in": "تسجيلُ الدخول" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "اِختر خادِم،\nأيًّا مِنهُم.", - "subtitle": "اختر مجتمعًا بناءً على اهتماماتك، منطقتك أو يمكنك حتى اختيارُ مجتمعٍ ذي غرضٍ عام.", - "subtitle_extend": "اختر مجتمعًا بناءً على اهتماماتك، منطقتك أو يمكنك حتى اختيارُ مجتمعٍ ذي غرضٍ عام. تُشغَّل جميعُ المجتمعِ مِن قِبَلِ مُنظمَةٍ أو فردٍ مُستقلٍ تمامًا.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "الكُل", @@ -248,8 +254,7 @@ "category": "الفئة" }, "input": { - "placeholder": "اِبحَث عن خادِم أو انضم إلى آخر خاص بك...", - "search_servers_or_enter_url": "اِبحَث فِي الخَوادِم أو أدخِل رابِط" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "يجري إيجاد خوادم متوفِّرَة...", @@ -719,4 +724,4 @@ "title": "العَلاماتُ المَرجعيَّة" } } -} \ No newline at end of file +} From 61fdda2e636b0f06f0f1a314a430f9823e94459b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:55 +0100 Subject: [PATCH 571/658] New translations app.json (Catalan) --- .../StringsConvertor/input/ca.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/ca.lproj/app.json b/Localization/StringsConvertor/input/ca.lproj/app.json index 35dd58746..a417ffdbb 100644 --- a/Localization/StringsConvertor/input/ca.lproj/app.json +++ b/Localization/StringsConvertor/input/ca.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Fes una foto", "save_photo": "Desa la foto", "copy_photo": "Copia la foto", - "sign_in": "Iniciar sessió", - "sign_up": "Registre", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Veure més", "preview": "Vista prèvia", "share": "Comparteix", @@ -218,10 +218,16 @@ "get_started": "Comença", "log_in": "Inicia sessió" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Mastodon està fet d'usuaris en diferents comunitats.", - "subtitle": "Tria una comunitat segons els teus interessos, regió o una de propòsit general.", - "subtitle_extend": "Tria una comunitat segons els teus interessos, regió o una de propòsit general. Cada comunitat és operada per una organització totalment independent o individualment.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Totes", @@ -248,8 +254,7 @@ "category": "CATEGORIA" }, "input": { - "placeholder": "Cerca servidors", - "search_servers_or_enter_url": "Cerca servidors o introdueix l'enllaç" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Cercant els servidors disponibles...", @@ -719,4 +724,4 @@ "title": "Marcadors" } } -} \ No newline at end of file +} From 05863051c9e38ee581222f44433bf90cf82ab226 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:56 +0100 Subject: [PATCH 572/658] New translations app.json (Danish) --- .../StringsConvertor/input/da.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/da.lproj/app.json b/Localization/StringsConvertor/input/da.lproj/app.json index 30566d8d6..3113ada74 100644 --- a/Localization/StringsConvertor/input/da.lproj/app.json +++ b/Localization/StringsConvertor/input/da.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Take Photo", "save_photo": "Save Photo", "copy_photo": "Copy Photo", - "sign_in": "Sign In", - "sign_up": "Sign Up", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "See More", "preview": "Preview", "share": "Share", @@ -218,10 +218,16 @@ "get_started": "Get Started", "log_in": "Log In" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "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.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "All", @@ -248,8 +254,7 @@ "category": "CATEGORY" }, "input": { - "placeholder": "Search servers", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Finding available servers...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From e1c18ebfbe02962f58c1a9e0718d8be3932e39e7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:57 +0100 Subject: [PATCH 573/658] New translations app.json (German) --- .../StringsConvertor/input/de.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/de.lproj/app.json b/Localization/StringsConvertor/input/de.lproj/app.json index 54bb81973..bda8161a1 100644 --- a/Localization/StringsConvertor/input/de.lproj/app.json +++ b/Localization/StringsConvertor/input/de.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Foto aufnehmen", "save_photo": "Foto speichern", "copy_photo": "Foto kopieren", - "sign_in": "Anmelden", - "sign_up": "Registrieren", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Mehr anzeigen", "preview": "Vorschau", "share": "Teilen", @@ -218,10 +218,16 @@ "get_started": "Registrieren", "log_in": "Anmelden" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Wähle einen Server,\nbeliebigen Server.", - "subtitle": "Wähle eine Gemeinschaft, die auf deinen Interessen, Region oder einem allgemeinen Zweck basiert.", - "subtitle_extend": "Wähle eine Gemeinschaft basierend auf deinen Interessen, deiner Region oder einem allgemeinen Zweck. Jede Gemeinschaft wird von einer völlig unabhängigen Organisation oder Einzelperson betrieben.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Alle", @@ -248,8 +254,7 @@ "category": "KATEGORIE" }, "input": { - "placeholder": "Nach Server suchen oder URL eingeben", - "search_servers_or_enter_url": "Nach Server suchen oder URL eingeben" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Verfügbare Server werden gesucht...", @@ -719,4 +724,4 @@ "title": "Lesezeichen" } } -} \ No newline at end of file +} From 54a70a81b5e876cccfa39b2b698f44407beee5d4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:45:59 +0100 Subject: [PATCH 574/658] New translations app.json (Basque) --- .../StringsConvertor/input/eu.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/eu.lproj/app.json b/Localization/StringsConvertor/input/eu.lproj/app.json index 9f5e925c6..3da0d6a00 100644 --- a/Localization/StringsConvertor/input/eu.lproj/app.json +++ b/Localization/StringsConvertor/input/eu.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Atera argazkia", "save_photo": "Gorde argazkia", "copy_photo": "Kopiatu argazkia", - "sign_in": "Hasi saioa", - "sign_up": "Eman Izena", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "Ikusi gehiago", "preview": "Aurrebista", "share": "Partekatu", @@ -218,10 +218,16 @@ "get_started": "Nola hasi", "log_in": "Hasi saioa" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "Aukeratu zerbitzari bat,\nedozein zerbitzari.", - "subtitle": "Aukeratu komunitate bat zure interes edo lurraldearen arabera, edo erabilera orokorreko bat.", - "subtitle_extend": "Aukeratu komunitate bat zure interes edo lurraldearen arabera, edo erabilera orokorreko bat. Komunitate bakoitza erakunde edo norbanako independente batek kudeatzen du.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Guztiak", @@ -248,8 +254,7 @@ "category": "KATEGORIA" }, "input": { - "placeholder": "Bilatu zerbitzari bat edo sortu zurea...", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "Erabilgarri dauden zerbitzariak bilatzen...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From bbc2f3c1652096e52c0c082ea34b4eaf2bda1a1e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 09:46:00 +0100 Subject: [PATCH 575/658] New translations app.json (Sorani (Kurdish)) --- .../StringsConvertor/input/ckb.lproj/app.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/ckb.lproj/app.json b/Localization/StringsConvertor/input/ckb.lproj/app.json index 3720f555e..787bbea36 100644 --- a/Localization/StringsConvertor/input/ckb.lproj/app.json +++ b/Localization/StringsConvertor/input/ckb.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "وێنە بگرە", "save_photo": "هەڵی بگرە", "copy_photo": "لەبەری بگرەوە", - "sign_in": "بچۆ ژوورەوە", - "sign_up": "خۆت تۆمار بکە", + "sign_in": "Log in", + "sign_up": "Create account", "see_more": "زیاتر ببینە", "preview": "پێشبینین", "share": "هاوبەشی بکە", @@ -218,10 +218,16 @@ "get_started": "دەست پێ بکە", "log_in": "بچۆ ژوورەوە" }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, "server_picker": { "title": "ماستۆدۆن لە چەندان بەکارهێنەر پێک دێت کە لە ڕاژەکاری جیاواز دان.", - "subtitle": "ڕاژەکارێکێکی گشتی یان دانەیەک لەسەر بنەمای حەزەکانت و هەرێمەکەت هەڵبژێرە.", - "subtitle_extend": "ڕاژەکارێکێکی گشتی یان دانەیەک لەسەر بنەمای حەزەکانت و هەرێمەکەت هەڵبژێرە. هەر ڕاژەکارێک لەلایەن ڕێکخراوێک یان تاکەکەسێک بەڕێوە دەبرێت.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "هەموو", @@ -248,8 +254,7 @@ "category": "بەش" }, "input": { - "placeholder": "بگەڕێ", - "search_servers_or_enter_url": "Search servers or enter URL" + "search_servers_or_enter_url": "Search communities or enter URL" }, "empty_state": { "finding_servers": "ڕاژەکار دەدۆزرێتەوە...", @@ -719,4 +724,4 @@ "title": "Bookmarks" } } -} \ No newline at end of file +} From 2e7598e4f723e99ef80bce9df4cd7af667be89ea Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 10:42:10 +0100 Subject: [PATCH 576/658] New translations app.json (Slovenian) --- .../StringsConvertor/input/sl.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/sl.lproj/app.json b/Localization/StringsConvertor/input/sl.lproj/app.json index 252d80def..0aed7bc15 100644 --- a/Localization/StringsConvertor/input/sl.lproj/app.json +++ b/Localization/StringsConvertor/input/sl.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Posnemi fotografijo", "save_photo": "Shrani fotografijo", "copy_photo": "Kopiraj fotografijo", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "Prijava", + "sign_up": "Ustvari račun", "see_more": "Pokaži več", "preview": "Predogled", "share": "Deli", @@ -219,15 +219,15 @@ "log_in": "Prijava" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "Dobrodošli nazaj", + "subtitle": "Prijavite se na strežniku, na katerem ste ustvarili račun.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "Vnesite URL ali poiščite svoj strežnik" } }, "server_picker": { "title": "Mastodon tvorijo uporabniki z različnih strežnikov.", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "Strežnik izberite glede na svojo regijo, zanimanje ali pa kar splošno. Še vedno lahko klepetate s komer koli na Mastodonu, ne glede na strežnik.", "button": { "category": { "all": "Vse", @@ -254,7 +254,7 @@ "category": "KATEGORIJA" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "Iščite po skupnostih ali vnesite URL" }, "empty_state": { "finding_servers": "Iskanje razpoložljivih strežnikov ...", From 198411e2f42b9d7f5095e7d626fa360d61fe44c3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 10:42:11 +0100 Subject: [PATCH 577/658] New translations app.json (Vietnamese) --- .../StringsConvertor/input/vi.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/vi.lproj/app.json b/Localization/StringsConvertor/input/vi.lproj/app.json index e7e5a1372..963be39c9 100644 --- a/Localization/StringsConvertor/input/vi.lproj/app.json +++ b/Localization/StringsConvertor/input/vi.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Chụp ảnh", "save_photo": "Lưu ảnh", "copy_photo": "Sao chép ảnh", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "Đăng nhập", + "sign_up": "Tạo tài khoản", "see_more": "Xem thêm", "preview": "Xem trước", "share": "Chia sẻ", @@ -219,15 +219,15 @@ "log_in": "Đăng nhập" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "Chào mừng trở lại!", + "subtitle": "Đăng nhập vào máy chủ mà bạn đã tạo tài khoản.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "Nhập URL hoặc tìm máy chủ" } }, "server_picker": { "title": "Mastodon gồm nhiều máy chủ với thành viên riêng.", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "Chọn một máy chủ dựa theo sở thích, tôn giáo, hoặc ý muốn của bạn. Bạn vẫn có thể giao tiếp với bất cứ ai mà không phụ thuộc vào máy chủ của họ.", "button": { "category": { "all": "Toàn bộ", @@ -254,7 +254,7 @@ "category": "PHÂN LOẠI" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "Tìm một máy chủ hoặc nhập URL" }, "empty_state": { "finding_servers": "Đang tìm máy chủ hoạt động...", From 8d2c1aa4ebae5becc273738b18d260f3354acd0f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 10:42:12 +0100 Subject: [PATCH 578/658] New translations app.json (Swedish) --- .../StringsConvertor/input/sv.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/sv.lproj/app.json b/Localization/StringsConvertor/input/sv.lproj/app.json index 27f931249..7951e1958 100644 --- a/Localization/StringsConvertor/input/sv.lproj/app.json +++ b/Localization/StringsConvertor/input/sv.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Ta foto", "save_photo": "Spara foto", "copy_photo": "Kopiera foto", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "Logga in", + "sign_up": "Skapa konto", "see_more": "Visa mer", "preview": "Förhandsvisa", "share": "Dela", @@ -219,15 +219,15 @@ "log_in": "Logga in" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "Välkommen tillbaka", + "subtitle": "Logga in på servern där du skapade ditt konto.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "Ange URL eller sök efter din server" } }, "server_picker": { "title": "Mastodon utgörs av användare på olika servrar.", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "Välj en server baserat på dina intressen, region eller en allmän server. Du kan fortfarande nå alla, oavsett server.", "button": { "category": { "all": "Alla", @@ -254,7 +254,7 @@ "category": "KATEGORI" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "Sök gemenskaper eller ange URL" }, "empty_state": { "finding_servers": "Söker tillgängliga servrar...", From 5038a49abf67118cdd900ebb5fcb20feb842ea7c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 10:42:14 +0100 Subject: [PATCH 579/658] 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 751ae6a61..f94207c89 100644 --- a/Localization/StringsConvertor/input/cs.lproj/app.json +++ b/Localization/StringsConvertor/input/cs.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Vyfotit", "save_photo": "Uložit fotku", "copy_photo": "Kopírovat fotografii", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "Přihlásit se", + "sign_up": "Vytvořit účet", "see_more": "Zobrazit více", "preview": "Náhled", "share": "Sdílet", @@ -219,15 +219,15 @@ "log_in": "Přihlásit se" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "Vítejte zpět", + "subtitle": "Přihlaste se na serveru, na kterém jste si vytvořili účet.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "Zadejte URL nebo vyhledávejte váš server" } }, "server_picker": { "title": "Mastodon tvoří uživatelé z různých serverů.", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "Vyberte server založený ve vašem regionu, podle zájmů nebo podle obecného účelu. Stále můžete chatovat s kýmkoli na Mastodonu bez ohledu na vaše servery.", "button": { "category": { "all": "Vše", @@ -254,7 +254,7 @@ "category": "KATEGORIE" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "Hledejte komunity nebo zadejte URL" }, "empty_state": { "finding_servers": "Hledání dostupných serverů...", From a9f7caea3d3475ca5ad1e6874a84fd5a89b27694 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 10:42:15 +0100 Subject: [PATCH 580/658] 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 2ed2be724..bf4bf454e 100644 --- a/Localization/StringsConvertor/input/ar.lproj/app.json +++ b/Localization/StringsConvertor/input/ar.lproj/app.json @@ -74,7 +74,7 @@ "take_photo": "اِلتِقاطُ صُورَة", "save_photo": "حفظ الصورة", "copy_photo": "نسخ الصورة", - "sign_in": "Log in", + "sign_in": "تسجيلُ الدخول", "sign_up": "Create account", "see_more": "عرض المزيد", "preview": "مُعاينة", From e3ab9509ec31e070658ad6f6b7743bf3117806b9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 10:42:16 +0100 Subject: [PATCH 581/658] New translations app.json (Italian) --- .../StringsConvertor/input/it.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json index 81131e857..f4f21762b 100644 --- a/Localization/StringsConvertor/input/it.lproj/app.json +++ b/Localization/StringsConvertor/input/it.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Scatta foto", "save_photo": "Salva foto", "copy_photo": "Copia foto", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "Accedi", + "sign_up": "Crea un account", "see_more": "Visualizza altro", "preview": "Anteprima", "share": "Condividi", @@ -219,15 +219,15 @@ "log_in": "Accedi" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "Bentornato/a", + "subtitle": "Accedi al server sul quale hai creato il tuo account.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "Inserisci l'URL o cerca il tuo server" } }, "server_picker": { "title": "Mastodon è fatto di utenti in diverse comunità.", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "Scegli un server in base alla tua regione, ai tuoi interessi o uno generico. Puoi comunque chattare con chiunque su Mastodon, indipendentemente dai tuoi server.", "button": { "category": { "all": "Tutti", @@ -254,7 +254,7 @@ "category": "CATEGORIA" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "Cerca le comunità o inserisci l'URL" }, "empty_state": { "finding_servers": "Ricerca server disponibili...", From 663b7fd70899b17ff7fabd2152f8a89f74657246 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 10:42:17 +0100 Subject: [PATCH 582/658] New translations app.json (Spanish, Argentina) --- .../StringsConvertor/input/es-AR.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/es-AR.lproj/app.json b/Localization/StringsConvertor/input/es-AR.lproj/app.json index 86762f6d4..5be543f98 100644 --- a/Localization/StringsConvertor/input/es-AR.lproj/app.json +++ b/Localization/StringsConvertor/input/es-AR.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Tomar foto", "save_photo": "Guardar foto", "copy_photo": "Copiar foto", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "Iniciar sesión", + "sign_up": "Crear cuenta", "see_more": "Ver más", "preview": "Previsualización", "share": "Compartir", @@ -219,15 +219,15 @@ "log_in": "Iniciar sesión" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "Hola de nuevo", + "subtitle": "Iniciá sesión en el servidor en donde creaste tu cuenta.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "Ingresá la dirección web o buscá tu servidor" } }, "server_picker": { "title": "Mastodon está compuesto de cuentas en diferentes servidores.", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "Elegí un servidor basado en tu región, en tus intereses o uno de propósitos generales. Vas a poder interactuar con cualquier cuenta de Mastodon, independientemente del servidor.", "button": { "category": { "all": "Todas", @@ -254,7 +254,7 @@ "category": "CATEGORÍA" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "Buscá comunidades o ingresá la dirección web" }, "empty_state": { "finding_servers": "Buscando servidores disponibles…", From 7ca0792b5b7096036f3f0f5a7e4c3c1e4fc88ca1 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 16 Nov 2022 18:21:15 +0800 Subject: [PATCH 583/658] feat: update purple logo --- .../input/Base.lproj/app.json | 2 +- .../Contents.json | 12 - .../mastodon.logo.black.pdf | 339 ----- .../Contents.json | 12 - .../mastodon.logo.black.large.pdf | 339 ----- .../mastodon.logo.imageset/Contents.json | 2 +- .../mastodon.logo.imageset/logo.small.pdf | 648 +++++++++ .../mastodon.logo.imageset/logotypeFull1.pdf | 513 ------- .../Contents.json | 2 +- .../logo.large.pdf | 1286 +++++++++++++++++ .../logotypeFull1.large.pdf | Bin 250426 -> 0 bytes .../MastodonAsset/Generated/Assets.swift | 2 - .../Generated/Strings.swift | 32 +- .../Resources/Base.lproj/Localizable.strings | 13 +- 14 files changed, 1958 insertions(+), 1244 deletions(-) delete mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.black.imageset/Contents.json delete mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.black.imageset/mastodon.logo.black.pdf delete mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.black.large.imageset/Contents.json delete mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.black.large.imageset/mastodon.logo.black.large.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.imageset/logo.small.pdf delete mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.imageset/logotypeFull1.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/logo.large.pdf delete mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/logotypeFull1.large.pdf diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json index 3a1228131..96b2150ce 100644 --- a/Localization/StringsConvertor/input/Base.lproj/app.json +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -224,7 +224,7 @@ "server_search_field": { "placeholder": "Enter URL or search for your server" } - } + }, "server_picker": { "title": "Mastodon is made of users in different servers.", "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.black.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.black.imageset/Contents.json deleted file mode 100644 index 3018f4b9a..000000000 --- a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.black.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "mastodon.logo.black.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.black.imageset/mastodon.logo.black.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.black.imageset/mastodon.logo.black.pdf deleted file mode 100644 index 773ed5e77..000000000 --- a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.black.imageset/mastodon.logo.black.pdf +++ /dev/null @@ -1,339 +0,0 @@ -%PDF-1.7 - -1 0 obj - << /BBox [ 0.000000 0.000000 480.000000 119.097778 ] - /Resources << >> - /Subtype /Form - /Length 2 0 R - /Group << /Type /Group - /S /Transparency - >> - /Type /XObject - >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 0.001709 -0.290527 cm -0.188235 0.533333 0.831373 scn -107.682541 47.532677 m -106.063889 39.212074 93.195923 30.106506 78.416977 28.341690 c -70.709908 27.421394 63.121937 26.576881 55.030510 26.946808 c -41.798027 27.553123 31.357124 30.104706 31.357124 30.104706 c -31.357124 28.818085 31.436523 27.591019 31.595320 26.443352 c -33.315022 13.385902 44.544495 12.602745 55.180283 12.238235 c -65.917122 11.870117 75.475624 14.885460 75.475624 14.885460 c -75.917725 5.177185 l -75.917725 5.177185 68.407349 1.147720 55.030510 0.406067 c -47.655472 0.000053 38.495773 0.590118 27.827501 3.414185 c -4.693666 9.538696 0.712913 34.197334 0.106597 59.224106 c --0.079267 66.653275 0.034417 73.660202 0.034417 79.517647 c -0.034417 105.105621 16.798328 112.606972 16.798328 112.606972 c -25.250660 116.488472 39.757122 118.121559 54.837425 118.244263 c -55.207348 118.244263 l -70.287651 118.121559 84.801338 116.488472 93.255470 112.606972 c -93.255470 112.606972 110.019386 105.105621 110.019386 79.517647 c -110.019386 79.517647 110.230507 60.638844 107.682541 47.532677 c -h -f -n -Q -q -1.000000 0.000000 -0.000000 1.000000 23.245789 45.780518 cm -0.121569 0.137255 0.168627 scn -0.000000 39.648720 m -0.000000 43.373230 3.018947 46.392181 6.743458 46.392181 c -10.467969 46.392181 13.486919 43.373230 13.486919 39.648720 c -13.486919 35.924210 10.467969 32.905262 6.743458 32.905262 c -3.018947 32.905262 0.000000 35.924210 0.000000 39.648720 c -h -96.718201 31.461655 m -96.718201 0.478188 l -84.443916 0.478188 l -84.443916 30.553986 l -84.443916 36.893234 81.776840 40.110676 76.440903 40.110676 c -70.541954 40.110676 67.586166 36.292332 67.586166 28.745865 c -67.586166 12.286915 l -55.384064 12.286915 l -55.384064 28.745865 l -55.384064 36.294136 52.426468 40.110676 46.529324 40.110676 c -41.193382 40.110676 38.524509 36.893234 38.524509 30.553986 c -38.524509 0.481804 l -26.252031 0.481804 l -26.252031 31.465263 l -26.252031 37.795486 27.865263 42.828270 31.104361 46.550980 c -34.442707 50.273685 38.816845 52.181053 44.246616 52.181053 c -50.526318 52.181053 55.284817 49.768425 58.430077 44.939552 c -61.486919 39.814735 l -64.543762 44.939552 l -67.689026 49.768425 72.445709 52.182858 78.727219 52.182858 c -84.156990 52.182858 88.529327 50.273685 91.869476 46.552784 c -95.108574 42.828270 96.720009 37.795490 96.720009 31.463459 c -96.718201 31.461655 l -h -139.005112 16.060150 m -141.536835 18.736240 142.758499 22.105263 142.758499 26.170826 c -142.758499 30.236389 141.536835 33.605415 139.005112 36.184063 c -136.565414 38.860153 133.468872 40.148571 129.715500 40.148571 c -125.962112 40.148571 122.867371 38.860153 120.427673 36.184063 c -117.987968 33.605415 116.768120 30.236389 116.768120 26.170826 c -116.768120 22.107067 117.987968 18.736240 120.427673 16.060150 c -122.867371 13.483311 125.962112 12.194881 129.715500 12.194881 c -133.468872 12.194881 136.565414 13.483311 139.005112 16.060150 c -139.005112 16.060150 l -h -142.758499 50.953987 m -154.859543 50.953987 l -154.859543 1.389473 l -142.754898 1.389473 l -142.754898 7.236088 l -139.097153 2.378342 134.030075 -0.000008 127.463463 -0.000008 c -121.176544 -0.000008 115.827965 2.477585 111.323906 7.533829 c -106.915489 12.590069 104.665268 18.835487 104.665268 26.169022 c -104.665268 33.405113 106.915489 39.650528 111.323906 44.706768 c -115.827965 49.763008 121.176544 52.339851 127.463463 52.339851 c -134.030075 52.339851 139.097153 49.959702 142.754898 45.103760 c -142.754898 50.950378 l -142.758499 50.953987 l -h -195.581955 27.062256 m -199.147659 24.387970 200.930527 20.620148 200.836685 15.863457 c -200.836685 10.807217 199.053848 6.840900 195.394302 4.065559 c -191.734756 1.389473 187.326309 0.001801 181.977737 0.001801 c -172.314575 0.001801 165.746170 3.966316 162.274292 11.797894 c -172.783768 18.041504 l -174.191299 13.781052 177.286011 11.599396 181.977737 11.599396 c -186.294128 11.599396 188.452347 12.988873 188.452347 15.863457 c -188.452347 17.944057 185.637283 19.827969 179.913376 21.313084 c -177.755188 21.908573 175.972336 22.504059 174.566620 23.000301 c -172.596085 23.792480 170.907074 24.685715 169.499557 25.775639 c -166.027664 28.451729 164.246628 32.019249 164.246628 36.579250 c -164.246628 41.436996 165.933823 45.302254 169.311874 48.079399 c -172.783752 50.953987 177.004501 52.341656 182.071579 52.341656 c -190.141342 52.341656 196.051117 48.871578 199.898331 41.833984 c -189.578354 35.886318 l -188.076996 39.255341 185.543457 40.940754 182.071579 40.940754 c -178.412018 40.940754 176.630966 39.553085 176.630966 36.876991 c -176.630966 34.796391 179.444214 32.912483 185.168121 31.425564 c -189.578339 30.433083 193.048416 28.947971 195.581955 27.064060 c -195.581955 27.062256 l -h -234.052322 38.661655 m -223.449036 38.661655 l -223.449036 18.043308 l -223.449036 15.563908 224.389175 14.078796 226.170227 13.384060 c -227.483917 12.887817 230.111267 12.788570 234.052322 12.987068 c -234.052322 1.389473 l -225.890518 0.396988 219.978958 1.190971 216.507080 3.868866 c -213.037003 6.445709 211.346176 11.202404 211.346176 18.043308 c -211.346176 38.661655 l -203.184372 38.661655 l -203.184372 50.953987 l -211.346176 50.953987 l -211.346176 60.965416 l -223.449036 64.830681 l -223.449036 50.953987 l -234.052322 50.953987 l -234.052322 38.661655 l -234.052322 38.661655 l -h -272.614716 16.357895 m -275.054413 18.936543 276.274261 22.208122 276.274261 26.172634 c -276.274261 30.137146 275.054413 33.408722 272.614716 35.985565 c -270.176819 38.562408 267.174133 39.850830 263.514587 39.850830 c -259.855011 39.850830 256.854126 38.562408 254.414429 35.985565 c -252.068558 33.309475 250.848709 30.037895 250.848709 26.172634 c -250.848709 22.305565 252.068558 19.033985 254.414429 16.357895 c -256.854126 13.781052 259.855011 12.492626 263.514587 12.492626 c -267.174133 12.492626 270.176819 13.781052 272.614716 16.357895 c -h -245.875488 7.535637 m -241.091721 12.590076 238.745850 18.736240 238.745850 26.172634 c -238.745850 33.507973 241.091721 39.652328 245.875488 44.708572 c -250.661057 49.763008 256.570801 52.341656 263.514587 52.341656 c -270.458344 52.341656 276.368134 49.763008 281.153687 44.708572 c -285.939240 39.652328 288.377136 33.408722 288.377136 26.172634 c -288.377136 18.835491 285.939240 12.590076 281.153687 7.535637 c -276.368134 2.479401 270.552155 0.001801 263.514587 0.001801 c -256.476990 0.001801 250.661057 2.479401 245.875488 7.535637 c -h -328.818054 16.060150 m -331.257751 18.736240 332.475769 22.105263 332.475769 26.170826 c -332.475769 30.236389 331.257751 33.605415 328.818054 36.184063 c -326.378357 38.860153 323.281799 40.148571 319.528412 40.148571 c -315.775024 40.148571 312.680298 38.860153 310.146759 36.184063 c -307.708893 33.605415 306.487213 30.236389 306.487213 26.170826 c -306.487213 22.107067 307.708893 18.736240 310.146759 16.060150 c -312.680298 13.483311 315.870667 12.194881 319.528412 12.194881 c -323.281799 12.194881 326.378357 13.483311 328.818054 16.060150 c -328.818054 16.060150 l -h -332.475769 70.780151 m -344.580475 70.780151 l -344.580475 1.389473 l -332.475769 1.389473 l -332.475769 7.236088 l -328.911865 2.378342 323.844818 -0.000008 317.278198 -0.000008 c -310.991272 -0.000008 305.550690 2.477585 301.046631 7.533829 c -296.636414 12.590069 294.384369 18.835487 294.384369 26.169022 c -294.384369 33.405113 296.636414 39.650528 301.046631 44.706768 c -305.550690 49.763008 310.991272 52.339851 317.278198 52.339851 c -323.844818 52.339851 328.911865 49.959702 332.475769 45.103760 c -332.475769 70.780151 l -332.475769 70.780151 l -h -387.083923 16.357895 m -389.523621 18.936543 390.743469 22.208122 390.743469 26.172634 c -390.743469 30.137146 389.523621 33.408722 387.083923 35.985565 c -384.644226 38.562408 381.643311 39.850830 377.983765 39.850830 c -374.324219 39.850830 371.321503 38.562408 368.881805 35.985565 c -366.535950 33.309475 365.316101 30.037895 365.316101 26.172634 c -365.316101 22.305565 366.535950 19.033985 368.881805 16.357895 c -371.321503 13.781052 374.324219 12.492626 377.983765 12.492626 c -381.643311 12.492626 384.644226 13.781052 387.083923 16.357895 c -387.083923 16.357895 l -h -360.344666 7.535637 m -355.559082 12.590076 353.215027 18.736240 353.215027 26.172634 c -353.215027 33.507973 355.559082 39.652328 360.344666 44.708572 c -365.130219 49.763008 371.040009 52.341656 377.983765 52.341656 c -384.925720 52.341656 390.837280 49.763008 395.622864 44.708572 c -400.408417 39.652328 402.846313 33.408722 402.846313 26.172634 c -402.846313 18.835491 400.408417 12.590076 395.622864 7.535637 c -390.837280 2.479401 385.019562 0.001801 377.983765 0.001801 c -370.946167 0.001801 365.130219 2.479401 360.344666 7.535637 c -360.344666 7.535637 l -h -455.202423 31.822556 m -455.202423 1.389473 l -443.097748 1.389473 l -443.097748 30.236389 l -443.097748 33.507969 442.255035 35.985565 440.566010 37.869476 c -438.970825 39.553085 436.718811 40.446320 433.809937 40.446320 c -426.960022 40.446320 423.489929 36.382557 423.489929 28.153984 c -423.489929 1.389473 l -411.387054 1.389473 l -411.387054 50.953987 l -423.489929 50.953987 l -423.489929 45.403309 l -426.398773 50.060753 430.994904 52.341656 437.469482 52.341656 c -442.630371 52.341656 446.851135 50.556992 450.135345 46.888420 c -453.513397 43.221657 455.202423 38.264664 455.202423 31.820751 c -455.202423 31.822556 l -h -f -n -Q - -endstream -endobj - -2 0 obj - 9224 -endobj - -3 0 obj - << /BBox [ 0.000000 0.000000 480.000000 119.097778 ] - /Resources << >> - /Subtype /Form - /Length 4 0 R - /Group << /Type /Group - /S /Transparency - >> - /Type /XObject - >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm -0.000000 0.000000 0.000000 scn -0.000000 119.097778 m -480.000000 119.097778 l -480.000000 0.000031 l -0.000000 0.000031 l -0.000000 119.097778 l -h -f -n -Q - -endstream -endobj - -4 0 obj - 237 -endobj - -5 0 obj - << /XObject << /X1 1 0 R >> - /ExtGState << /E1 << /SMask << /Type /Mask - /G 3 0 R - /S /Alpha - >> - /Type /ExtGState - >> >> - >> -endobj - -6 0 obj - << /Length 7 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -/E1 gs -/X1 Do -Q - -endstream -endobj - -7 0 obj - 46 -endobj - -8 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 480.000000 119.097778 ] - /Resources 5 0 R - /Contents 6 0 R - /Parent 9 0 R - >> -endobj - -9 0 obj - << /Kids [ 8 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -10 0 obj - << /Type /Catalog - /Pages 9 0 R - >> -endobj - -xref -0 11 -0000000000 65535 f -0000000010 00000 n -0000009484 00000 n -0000009507 00000 n -0000009994 00000 n -0000010016 00000 n -0000010314 00000 n -0000010416 00000 n -0000010437 00000 n -0000010612 00000 n -0000010686 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 10 0 R - /Size 11 ->> -startxref -10746 -%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.black.large.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.black.large.imageset/Contents.json deleted file mode 100644 index fefc19832..000000000 --- a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.black.large.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "mastodon.logo.black.large.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.black.large.imageset/mastodon.logo.black.large.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.black.large.imageset/mastodon.logo.black.large.pdf deleted file mode 100644 index b6244d04e..000000000 --- a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.black.large.imageset/mastodon.logo.black.large.pdf +++ /dev/null @@ -1,339 +0,0 @@ -%PDF-1.7 - -1 0 obj - << /BBox [ 0.000000 0.000000 960.000000 238.195496 ] - /Resources << >> - /Subtype /Form - /Length 2 0 R - /Group << /Type /Group - /S /Transparency - >> - /Type /XObject - >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 0.003357 -0.580933 cm -0.188235 0.533333 0.831373 scn -215.365082 95.065247 m -212.127777 78.424042 186.391846 60.212906 156.833954 56.683273 c -141.419815 54.842682 126.243874 53.153656 110.061020 53.893509 c -83.596054 55.106140 62.714249 60.209290 62.714249 60.209290 c -62.714249 57.636063 62.873047 55.181931 63.190639 52.886597 c -66.630043 26.771698 89.088989 25.205383 110.360565 24.476364 c -131.834244 23.740112 150.951248 29.770813 150.951248 29.770813 c -151.835449 10.354263 l -151.835449 10.354263 136.814697 2.295334 110.061020 0.812027 c -95.310944 0.000000 76.991547 1.180130 55.655003 6.828262 c -9.387332 19.077271 1.425826 68.394562 0.213194 118.448097 c --0.158535 133.306442 0.068834 147.320282 0.068834 159.035172 c -0.068834 210.211121 33.596657 225.213821 33.596657 225.213821 c -50.501320 232.976822 79.514244 236.242996 109.674850 236.488403 c -110.414696 236.488403 l -140.575302 236.242996 169.602676 232.976822 186.510941 225.213821 c -186.510941 225.213821 220.038773 210.211121 220.038773 159.035172 c -220.038773 159.035172 220.461014 121.277573 215.365082 95.065247 c -h -f -n -Q -q -1.000000 0.000000 -0.000000 1.000000 46.491440 91.561096 cm -0.121569 0.137255 0.168627 scn -0.000000 79.297432 m -0.000000 86.746460 6.037893 92.784363 13.486916 92.784363 c -20.935938 92.784363 26.973839 86.746460 26.973839 79.297432 c -26.973839 71.848412 20.935938 65.810516 13.486916 65.810516 c -6.037893 65.810516 0.000000 71.848412 0.000000 79.297432 c -h -193.436401 62.923302 m -193.436401 0.956360 l -168.887833 0.956360 l -168.887833 61.107964 l -168.887833 73.786461 163.553680 80.221344 152.881805 80.221344 c -141.083908 80.221344 135.172333 72.584648 135.172333 57.491714 c -135.172333 24.573814 l -110.768127 24.573814 l -110.768127 57.491714 l -110.768127 72.588257 104.852936 80.221344 93.058647 80.221344 c -82.386765 80.221344 77.049019 73.786461 77.049019 61.107964 c -77.049019 0.963593 l -52.504063 0.963593 l -52.504063 62.930511 l -52.504063 75.590965 55.730526 85.656540 62.208721 93.101944 c -68.885414 100.547363 77.633690 104.362106 88.493233 104.362106 c -101.052635 104.362106 110.569633 99.536835 116.860153 89.879089 c -122.973839 79.629471 l -129.087524 89.879089 l -135.378052 99.536835 144.891418 104.365707 157.454437 104.365707 c -168.313980 104.365707 177.058655 100.547363 183.738953 93.105560 c -190.217148 85.656540 193.440018 75.590973 193.440018 62.926910 c -193.436401 62.923302 l -h -278.010223 32.120285 m -283.073669 37.472473 285.516998 44.210510 285.516998 52.341637 c -285.516998 60.472771 283.073669 67.210823 278.010223 72.368118 c -273.130829 77.720299 266.937744 80.297134 259.431000 80.297134 c -251.924225 80.297134 245.734741 77.720299 240.855347 72.368118 c -235.975937 67.210823 233.536240 60.472771 233.536240 52.341637 c -233.536240 44.214119 235.975937 37.472473 240.855347 32.120285 c -245.734741 26.966606 251.924225 24.389748 259.431000 24.389748 c -266.937744 24.389748 273.130829 26.966606 278.010223 32.120285 c -278.010223 32.120285 l -h -285.516998 101.907967 m -309.719086 101.907967 l -309.719086 2.778915 l -285.509796 2.778915 l -285.509796 14.472160 l -278.194305 4.756668 268.060150 -0.000031 254.926926 -0.000031 c -242.353088 -0.000031 231.655930 4.955154 222.647812 15.067642 c -213.830978 25.180122 209.330536 37.670959 209.330536 52.338036 c -209.330536 66.810219 213.830978 79.301048 222.647812 89.413528 c -231.655930 99.526016 242.353088 104.679695 254.926926 104.679695 c -268.060150 104.679695 278.194305 99.919395 285.509796 90.207512 c -285.509796 101.900742 l -285.516998 101.907967 l -h -391.163910 54.124504 m -398.295319 48.775932 401.861053 41.240288 401.673370 31.726898 c -401.673370 21.614418 398.107697 13.681786 390.788605 8.131104 c -383.469513 2.778931 374.652618 0.003571 363.955475 0.003571 c -344.629150 0.003571 331.492340 7.932617 324.548584 23.595772 c -345.567535 36.082993 l -348.382599 27.562088 354.572021 23.198776 363.955475 23.198776 c -372.588257 23.198776 376.904694 25.977730 376.904694 31.726898 c -376.904694 35.888107 371.274567 39.655930 359.826752 42.626152 c -355.510376 43.817131 351.944672 45.008102 349.133240 46.000587 c -345.192169 47.584946 341.814148 49.371414 338.999115 51.551262 c -332.055328 56.903450 328.493256 64.038490 328.493256 73.158493 c -328.493256 82.873978 331.867645 90.604507 338.623749 96.158791 c -345.567505 101.907967 354.009003 104.683304 364.143158 104.683304 c -380.282684 104.683304 392.102234 97.743149 399.796661 83.667961 c -379.156708 71.772621 l -376.153992 78.510666 371.086914 81.881500 364.143158 81.881500 c -356.824036 81.881500 353.261932 79.106155 353.261932 73.753975 c -353.261932 69.592773 358.888428 65.824951 370.336243 62.851120 c -379.156677 60.866158 386.096832 57.895927 391.163910 54.128113 c -391.163910 54.124504 l -h -468.104645 77.323303 m -446.898071 77.323303 l -446.898071 36.086601 l -446.898071 31.127808 448.778351 28.157578 452.340454 26.768105 c -454.967834 25.775620 460.222534 25.577126 468.104645 25.974121 c -468.104645 2.778915 l -451.781036 0.793961 439.957916 2.381927 433.014160 7.737717 c -426.074005 12.891403 422.692352 22.404793 422.692352 36.086601 c -422.692352 77.323303 l -406.368744 77.323303 l -406.368744 101.907967 l -422.692352 101.907967 l -422.692352 121.930832 l -446.898071 129.661346 l -446.898071 101.907967 l -468.104645 101.907967 l -468.104645 77.323303 l -468.104645 77.323303 l -h -545.229431 32.715775 m -550.108826 37.873070 552.548523 44.416229 552.548523 52.345253 c -552.548523 60.274277 550.108826 66.817436 545.229431 71.971123 c -540.353638 77.124809 534.348267 79.701645 527.029175 79.701645 c -519.710022 79.701645 513.708252 77.124809 508.828857 71.971123 c -504.137115 66.618942 501.697418 60.075783 501.697418 52.345253 c -501.697418 44.611115 504.137115 38.067955 508.828857 32.715775 c -513.708252 27.562088 519.710022 24.985237 527.029175 24.985237 c -534.348267 24.985237 540.353638 27.562088 545.229431 32.715775 c -h -491.750977 15.071259 m -482.183441 25.180138 477.491699 37.472473 477.491699 52.345253 c -477.491699 67.015930 482.183441 79.304657 491.750977 89.417137 c -501.322113 99.526016 513.141602 104.683304 527.029175 104.683304 c -540.916687 104.683304 552.736267 99.526016 562.307373 89.417137 c -571.878479 79.304657 576.754272 66.817436 576.754272 52.345253 c -576.754272 37.670967 571.878479 25.180138 562.307373 15.071259 c -552.736267 4.958771 541.104309 0.003571 527.029175 0.003571 c -512.953979 0.003571 501.322113 4.958771 491.750977 15.071259 c -h -657.636108 32.120285 m -662.515503 37.472473 664.951538 44.210510 664.951538 52.341637 c -664.951538 60.472771 662.515503 67.210823 657.636108 72.368118 c -652.756714 77.720299 646.563599 80.297134 639.056824 80.297134 c -631.550049 80.297134 625.360596 77.720299 620.293518 72.368118 c -615.417786 67.210823 612.974426 60.472771 612.974426 52.341637 c -612.974426 44.214119 615.417786 37.472473 620.293518 32.120285 c -625.360596 26.966606 631.741333 24.389748 639.056824 24.389748 c -646.563599 24.389748 652.756714 26.966606 657.636108 32.120285 c -657.636108 32.120285 l -h -664.951538 141.560303 m -689.160950 141.560303 l -689.160950 2.778915 l -664.951538 2.778915 l -664.951538 14.472160 l -657.823730 4.756668 647.689636 -0.000031 634.556396 -0.000031 c -621.982544 -0.000031 611.101379 4.955154 602.093262 15.067642 c -593.272827 25.180122 588.768738 37.670959 588.768738 52.338036 c -588.768738 66.810219 593.272827 79.301048 602.093262 89.413528 c -611.101379 99.526016 621.982544 104.679695 634.556396 104.679695 c -647.689636 104.679695 657.823730 99.919395 664.951538 90.207512 c -664.951538 141.560303 l -664.951538 141.560303 l -h -774.167847 32.715775 m -779.047241 37.873070 781.486938 44.416229 781.486938 52.345253 c -781.486938 60.274277 779.047241 66.817436 774.167847 71.971123 c -769.288452 77.124809 763.286621 79.701645 755.967529 79.701645 c -748.648438 79.701645 742.643005 77.124809 737.763611 71.971123 c -733.071899 66.618942 730.632202 60.075783 730.632202 52.345253 c -730.632202 44.611115 733.071899 38.067955 737.763611 32.715775 c -742.643005 27.562088 748.648438 24.985237 755.967529 24.985237 c -763.286621 24.985237 769.288452 27.562088 774.167847 32.715775 c -774.167847 32.715775 l -h -720.689331 15.071259 m -711.118164 25.180138 706.430054 37.472473 706.430054 52.345253 c -706.430054 67.015930 711.118164 79.304657 720.689331 89.417137 c -730.260437 99.526016 742.080017 104.683304 755.967529 104.683304 c -769.851440 104.683304 781.674561 99.526016 791.245728 89.417137 c -800.816833 79.304657 805.692627 66.817436 805.692627 52.345253 c -805.692627 37.670967 800.816833 25.180138 791.245728 15.071259 c -781.674561 4.958771 770.039124 0.003571 755.967529 0.003571 c -741.892334 0.003571 730.260437 4.958771 720.689331 15.071259 c -720.689331 15.071259 l -h -910.404846 63.645103 m -910.404846 2.778915 l -886.195496 2.778915 l -886.195496 60.472771 l -886.195496 67.015930 884.510071 71.971123 881.132019 75.738945 c -877.941650 79.106163 873.437622 80.892624 867.619873 80.892624 c -853.920044 80.892624 846.979858 72.765106 846.979858 56.307961 c -846.979858 2.778915 l -822.774109 2.778915 l -822.774109 101.907967 l -846.979858 101.907967 l -846.979858 90.806610 l -852.797546 100.121506 861.989807 104.683304 874.938965 104.683304 c -885.260742 104.683304 893.702271 101.113983 900.270691 93.776840 c -907.026794 86.443298 910.404846 76.529312 910.404846 63.641495 c -910.404846 63.645103 l -h -f -n -Q - -endstream -endobj - -2 0 obj - 9343 -endobj - -3 0 obj - << /BBox [ 0.000000 0.000000 960.000000 238.195496 ] - /Resources << >> - /Subtype /Form - /Length 4 0 R - /Group << /Type /Group - /S /Transparency - >> - /Type /XObject - >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm -0.000000 0.000000 0.000000 scn -0.000000 238.195496 m -960.000000 238.195496 l -960.000000 0.000000 l -0.000000 0.000000 l -0.000000 238.195496 l -h -f -n -Q - -endstream -endobj - -4 0 obj - 237 -endobj - -5 0 obj - << /XObject << /X1 1 0 R >> - /ExtGState << /E1 << /SMask << /Type /Mask - /G 3 0 R - /S /Alpha - >> - /Type /ExtGState - >> >> - >> -endobj - -6 0 obj - << /Length 7 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -/E1 gs -/X1 Do -Q - -endstream -endobj - -7 0 obj - 46 -endobj - -8 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 960.000000 238.195496 ] - /Resources 5 0 R - /Contents 6 0 R - /Parent 9 0 R - >> -endobj - -9 0 obj - << /Kids [ 8 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -10 0 obj - << /Type /Catalog - /Pages 9 0 R - >> -endobj - -xref -0 11 -0000000000 65535 f -0000000010 00000 n -0000009603 00000 n -0000009626 00000 n -0000010113 00000 n -0000010135 00000 n -0000010433 00000 n -0000010535 00000 n -0000010556 00000 n -0000010731 00000 n -0000010805 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 10 0 R - /Size 11 ->> -startxref -10865 -%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.imageset/Contents.json index 6a0bfc87a..76d42c15e 100644 --- a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.imageset/Contents.json +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "logotypeFull1.pdf", + "filename" : "logo.small.pdf", "idiom" : "universal" } ], diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.imageset/logo.small.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.imageset/logo.small.pdf new file mode 100644 index 000000000..581801f38 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.imageset/logo.small.pdf @@ -0,0 +1,648 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 269.000000 75.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.000000 4.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 67.000000 m +261.000000 67.000000 l +261.000000 0.000000 l +0.000000 0.000000 l +0.000000 67.000000 l +h +f +n +Q + +endstream +endobj + +2 0 obj + 234 +endobj + +3 0 obj + << /Length 4 0 R + /Range [ 0.000000 1.000000 0.000000 1.000000 0.000000 1.000000 ] + /Domain [ 0.000000 1.000000 ] + /FunctionType 4 + >> +stream +{ 0.388235 exch 0.392157 exch 1.000000 exch dup 0.000000 gt { exch pop exch pop exch pop dup 0.000000 sub -0.050980 mul 0.388235 add exch dup 0.000000 sub -0.164706 mul 0.392157 add exch dup 0.000000 sub -0.200000 mul 1.000000 add exch } if dup 1.000000 gt { exch pop exch pop exch pop 0.337255 exch 0.227451 exch 0.800000 exch } if pop } +endstream +endobj + +4 0 obj + 339 +endobj + +5 0 obj + << /Type /XObject + /Length 6 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << /Pattern << /P1 << /Matrix [ 0.000000 -65.993195 65.993195 0.000000 -61.993195 70.224548 ] + /Shading << /Coords [ 0.000000 0.000000 1.000000 0.000000 ] + /ColorSpace /DeviceRGB + /Function 3 0 R + /Domain [ 0.000000 1.000000 ] + /ShadingType 2 + /Extend [ true true ] + >> + /PatternType 2 + /Type /Pattern + >> >> >> + /BBox [ 0.000000 0.000000 269.000000 75.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.000000 3.632446 cm +/Pattern cs +/P1 scn +60.826767 51.982300 m +59.885666 59.068882 53.787518 64.663071 46.568073 65.739265 c +45.346546 65.922020 40.730431 66.592102 30.036348 66.592102 c +29.956215 66.592102 l +19.252211 66.592102 16.959164 65.922020 15.737550 65.739265 c +8.708311 64.683380 2.299911 59.667900 0.737860 52.489872 c +-0.003115 48.956699 -0.083220 45.037697 0.056964 41.443653 c +0.257227 36.286026 0.297280 31.148746 0.757885 26.011383 c +1.078305 22.600037 1.629032 19.219128 2.420071 15.889084 c +3.902017 9.736488 9.889900 4.619389 15.757648 2.538147 c +22.035824 0.365425 28.794806 0.000015 35.263187 1.492470 c +35.974140 1.665001 36.675091 1.857872 37.376038 2.081261 c +38.948124 2.588921 40.790466 3.157455 42.152252 4.152424 c +42.172348 4.162594 42.182354 4.182858 42.192364 4.203201 c +42.202370 4.223465 42.212376 4.243816 42.212376 4.274334 c +42.212376 9.249176 l +42.212376 9.249176 42.212376 9.289791 42.192364 9.310051 c +42.192364 9.330315 42.172348 9.350658 42.152252 9.360832 c +42.132240 9.370922 42.112228 9.381180 42.092300 9.391270 c +42.072121 9.391270 42.052189 9.391270 42.032177 9.391270 c +37.886612 8.386127 33.631145 7.878468 29.375511 7.888641 c +22.035824 7.888641 20.063231 11.421898 19.502539 12.883831 c +19.051918 14.152893 18.761566 15.482990 18.641405 16.823097 c +18.641405 16.843441 18.641405 16.863705 18.651411 16.884052 c +18.651411 16.904316 18.671425 16.924664 18.691521 16.934837 c +18.711451 16.944927 18.731462 16.955097 18.751476 16.965271 c +18.821604 16.965271 l +22.896957 15.970303 27.082464 15.462643 31.277977 15.462643 c +32.289371 15.462643 33.290596 15.462646 34.301991 15.493084 c +38.517517 15.614994 42.963440 15.828209 47.118843 16.650486 c +47.218906 16.670830 47.329144 16.691097 47.419201 16.711441 c +53.967884 17.990677 60.196030 21.990898 60.826767 32.123367 c +60.846947 32.519287 60.906982 36.306290 60.906982 36.712379 c +60.906982 38.123699 61.357521 46.692589 60.836857 51.961868 c +60.826767 51.982300 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 16.353134 47.918640 cm +1.000000 1.000000 1.000000 scn +0.000000 3.736245 m +0.000000 5.807401 1.632209 7.472382 3.654834 7.472382 c +5.677542 7.472382 7.309585 5.797228 7.309585 3.736245 c +7.309585 1.675262 5.677542 0.000028 3.654834 0.000028 c +1.632209 0.000028 0.000000 1.675262 0.000000 3.736245 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 32.524460 30.973694 cm +1.000000 1.000000 1.000000 scn +38.200230 17.462723 m +38.200230 0.284256 l +31.541471 0.284256 l +31.541471 16.955149 l +31.541471 20.467976 30.099640 22.244776 27.205791 22.244776 c +24.011585 22.244776 22.399469 20.122755 22.399469 15.950090 c +22.399469 6.822678 l +15.790834 6.822678 l +15.790834 15.950090 l +15.790834 20.143185 14.198734 22.244776 10.984432 22.244776 c +8.100758 22.244776 6.648746 20.467976 6.648746 16.955149 c +6.648746 0.294346 l +0.000000 0.294346 l +0.000000 17.462723 l +0.000000 20.965542 0.871139 23.757576 2.623425 25.828648 c +4.435832 27.899887 6.808930 28.945639 9.742804 28.945639 c +13.147311 28.945639 15.730712 27.605450 17.432966 24.925154 c +19.095194 22.082256 l +20.757338 24.925154 l +22.459507 27.595276 25.032986 28.945639 28.447416 28.945639 c +31.381290 28.945639 33.754387 27.889629 35.566792 25.828648 c +37.319077 23.757576 38.190220 20.985806 38.190220 17.462723 c +38.200230 17.462723 l +h +61.110268 8.924355 m +62.491982 10.416807 63.143234 12.264570 63.143234 14.518509 c +63.143234 16.772449 62.481976 18.640476 61.110268 20.061882 c +59.788589 21.554337 58.106689 22.265121 56.073723 22.265121 c +54.041008 22.265121 52.368858 21.554337 51.037174 20.061882 c +49.715416 18.640476 49.054577 16.772449 49.054577 14.518509 c +49.054577 12.264570 49.715416 10.396461 51.037174 8.924355 c +52.358852 7.502947 54.041008 6.781986 56.073723 6.781986 c +58.106689 6.781986 59.778584 7.492773 61.110268 8.924355 c +h +63.143234 28.255198 m +69.701591 28.255198 l +69.701591 0.781738 l +63.143234 0.781738 l +63.143234 4.020473 l +61.160301 1.330006 58.416882 -0.000011 54.852188 -0.000011 c +51.287495 -0.000011 48.543831 1.370613 46.110611 4.172821 c +43.717499 6.974941 42.505974 10.437069 42.505974 14.498163 c +42.505974 18.559340 43.727505 21.970684 46.110611 24.772808 c +48.553837 27.574930 51.457687 28.996338 54.852188 28.996338 c +58.246773 28.996338 61.160301 27.676495 63.143234 24.996117 c +63.143234 28.234852 l +63.143234 28.255198 l +h +91.770676 15.036257 m +93.703575 13.543886 94.665031 11.462475 94.615005 8.832880 c +94.615005 6.030758 93.653549 3.827599 91.670616 2.304710 c +89.688515 0.812176 87.294495 0.050774 84.390968 0.050774 c +79.154297 0.050774 75.599525 2.253845 73.716660 6.579025 c +79.405289 10.041067 l +80.164940 7.685646 81.837677 6.467285 84.390968 6.467285 c +86.734138 6.467285 87.895714 7.228771 87.895714 8.822790 c +87.895714 9.980196 86.373070 11.025948 83.269424 11.838133 c +82.097839 12.163090 81.127220 12.498138 80.375908 12.772230 c +79.303558 13.208757 78.392975 13.706240 77.631653 14.315380 c +75.749619 15.807833 74.789009 17.777590 74.789009 20.305622 c +74.789009 22.996006 75.699593 25.138290 77.531593 26.681526 c +79.415298 28.275459 81.706749 29.037029 84.451004 29.037029 c +88.826294 29.037029 92.020851 27.118137 94.113853 23.219398 c +88.526115 19.929964 l +87.715591 21.798073 86.333038 22.732088 84.451004 22.732088 c +82.468071 22.732088 81.506630 21.970686 81.506630 20.478231 c +81.506630 19.320744 83.029274 18.274992 86.132919 17.462723 c +88.526115 16.914539 90.408142 16.092180 91.770676 15.036257 c +91.780693 15.036257 l +91.770676 15.036257 l +h +112.618164 21.452854 m +106.870323 21.452854 l +106.870323 10.020803 l +106.870323 8.650179 107.381485 7.817646 108.352104 7.441906 c +109.063393 7.167816 110.485130 7.117115 112.628166 7.218597 c +112.628166 0.791912 l +108.212013 0.243645 105.008308 0.690430 103.126274 2.162537 c +101.243401 3.583942 100.331985 6.233803 100.331985 10.010632 c +100.331985 21.452854 l +95.915833 21.452854 l +95.915833 28.265369 l +100.331985 28.265369 l +100.331985 33.808784 l +106.890335 35.951027 l +106.890335 28.255198 l +112.638176 28.255198 l +112.638176 21.442680 l +112.628166 21.442680 l +112.618164 21.452854 l +h +133.525681 9.086706 m +134.847351 10.508114 135.507782 12.325527 135.507782 14.528599 c +135.507782 16.731840 134.847351 18.528820 133.525681 19.970573 c +132.193161 21.391897 130.571289 22.112776 128.589188 22.112776 c +126.606262 22.112776 124.984390 21.402071 123.651871 19.970573 c +122.381058 18.478121 121.719803 16.680973 121.719803 14.528599 c +121.719803 12.376308 122.381058 10.579159 123.651871 9.086706 c +124.974380 7.665382 126.606262 6.944508 128.589188 6.944508 c +130.571289 6.944508 132.193161 7.655127 133.525681 9.086706 c +h +119.037262 4.203255 m +116.443100 7.005379 115.171455 10.406551 115.171455 14.528599 c +115.171455 18.650732 116.443100 22.001038 119.037262 24.803242 c +121.629745 27.605450 124.834297 29.026855 128.589188 29.026855 c +132.343262 29.026855 135.558640 27.605450 138.141953 24.803242 c +140.725266 22.001038 142.056961 18.538994 142.056961 14.528599 c +142.056961 10.518290 140.725266 7.005379 138.141953 4.203255 c +135.547806 1.401051 132.394119 0.030510 128.589188 0.030510 c +124.784264 0.030510 121.619743 1.401051 119.037262 4.203255 c +h +163.985123 8.934444 m +165.306808 10.426897 165.968063 12.274660 165.968063 14.528599 c +165.968063 16.782539 165.306808 18.650732 163.985123 20.072056 c +162.664291 21.564592 160.982376 22.275211 158.948578 22.275211 c +156.916458 22.275211 155.233719 21.564592 153.862839 20.072056 c +152.540329 18.650732 151.879898 16.782539 151.879898 14.528599 c +151.879898 12.274660 152.540329 10.406551 153.862839 8.934444 c +155.243713 7.513037 156.966492 6.792244 158.948578 6.792244 c +160.932358 6.792244 162.654282 7.502947 163.985123 8.934444 c +h +165.968063 39.260834 m +172.526413 39.260834 l +172.526413 0.791912 l +165.968063 0.791912 l +165.968063 4.030647 l +164.035995 1.340179 161.291748 0.010162 157.727798 0.010162 c +154.163025 0.010162 151.379578 1.380703 148.925522 4.182991 c +146.532318 6.985115 145.321548 10.447161 145.321548 14.508337 c +145.321548 18.569429 146.542328 21.980774 148.925522 24.782898 c +151.358734 27.585186 154.312286 29.006512 157.727798 29.006512 c +161.141647 29.006512 164.035995 27.686666 165.968063 25.006289 c +165.968063 39.250683 l +165.968063 39.260834 l +h +195.566971 9.117228 m +196.888641 10.538635 197.549896 12.355879 197.549896 14.559118 c +197.549896 16.762192 196.888641 18.559340 195.566971 20.001011 c +194.245285 21.422335 192.623413 22.143211 190.630478 22.143211 c +188.638367 22.143211 187.026520 21.432508 185.694000 20.001011 c +184.422363 18.508474 183.761093 16.711493 183.761093 14.559118 c +183.761093 12.406662 184.422363 10.609680 185.694000 9.117228 c +187.016510 7.695736 188.648376 6.974945 190.630478 6.974945 c +192.613403 6.974945 194.235291 7.685648 195.566971 9.117228 c +h +181.077713 4.233692 m +178.495224 7.035900 177.212753 10.437071 177.212753 14.559118 c +177.212753 18.681084 178.485229 22.031557 181.077713 24.833679 c +183.671036 27.635885 186.875580 29.057293 190.630478 29.057293 c +194.385376 29.057293 197.599915 27.635885 200.183228 24.833679 c +202.775726 22.031557 204.098236 18.569429 204.098236 14.559118 c +204.098236 10.548725 202.775726 7.035900 200.183228 4.233692 c +197.589905 1.431572 194.435410 0.060863 190.630478 0.060863 c +186.825546 0.060863 183.661026 1.431572 181.077713 4.233692 c +h +232.475540 17.696289 m +232.475540 0.822433 l +225.917206 0.822433 l +225.917206 16.812975 l +225.917206 18.630386 225.466064 20.001011 224.535477 21.036505 c +223.674088 21.970686 222.452469 22.457994 220.879807 22.457994 c +217.175766 22.457994 215.292923 20.204056 215.292923 15.645479 c +215.292923 0.812172 l +208.734528 0.812172 l +208.734528 28.265369 l +215.292923 28.265369 l +215.292923 25.178900 l +216.864761 27.757797 219.367996 29.026855 222.862732 29.026855 c +225.656174 29.026855 227.949310 28.041977 229.732117 26.011431 c +231.564957 23.980885 232.475540 21.229462 232.475540 17.665854 c +f +n +Q + +endstream +endobj + +6 0 obj + 9965 +endobj + +7 0 obj + << /Type /XObject + /Subtype /Image + /BitsPerComponent 8 + /Length 8 0 R + /Height 148 + /Width 538 + /ColorSpace /DeviceGray + /Filter [ /FlateDecode ] + >> +stream +xC#[ֆ;ݍ;$$@pwwwwww~wW̽LPU[׻׿ }ӿ>_a?CL]0qoAtUǏ>񣯗QoN|ȥ\ Q;iϢАPCa6OP8u!!\@?Qw 3̡̞ǯIL4DFFDEECQ11(66&6oGEGErN 5?%fSHlDEEBDDd$6vdq.11!!!>..N0 <~ՑqDdt\BfіkΗ` .dNMMK@^×E/zҠTt$; 2)1!.&:2tpP;?G&:4&1͕+J O!1q IvGM3|9yy~*r ?//77';;+3ӗ e6~ }gsz|yy>k-$ŧEL|-gfWVVUT։E7TWUUVVfefx)(A+(iчq6WFnQyUM]mUyqn_PZϖddWU646wtvwYo~0/Mu:bPLf|1$<:˯klb"~).z";pxdtl|brrjjzzzffv2 LOMMMNL tu46TWfz]†ShW"#P@K2"cщɉс<#."XCqI73pdlbjfn~~qiyeuu ZX؄:0?7;=59>:4TWU +6#˜|@sI 9bxbDdZ~ Jp:VV:|΄пMfv.ovAiuckW*@;888<<:<:::X萓{{;;[dianzbd('HD02X0s4xkY>| +Jtg5O.mn.M gѼ`8['odrf~emc{wJtͷ!>\svvzrrr R@(!6/ M0X'B DucKͫ<`F;sla1Ca1Ԍ殁sp}ss'7B X&gLJkKC=exEaEF&$=wпn~ +{ +j'wOnNV&k _CfhB#cS<9EUMKG'gW7@Woo|0Ĺ/ϟ??~6`gcynb8ۓ+@a˲Mz}YDbS 1Hw>ڽ뇗,t{ ˇsb¢bmΌÓ˛ϰ~,?'tpngd@{}Y7%)NRE£ѠiB"6eYH5! }2f6XNW3 E_7awgV6u O-n] b4 O?!.7(pwceA+)6ׯ0nT pgW5( &eO1!cvl}Q=6,/odjXT=5wlnm\FPi?}Ï!_<^͍TZ֠)1n ݙm==y丈пo& q2$332^eԶL.n\ DAHx^lLJ˓Bb|#bəARB!3&&B ذ)!SrRK:Wvϯ%.`%?E rws~4eKx7o|vy}}ea`Bb_@B2a!H O`KD-5cxfuᵼy;h{Y(HID!JQ\DWficp[eaE?G dFQ2mſ UY~Wd +o>c]YhgvO.Ӣ|gh@/J(Jɷ't5q8P(?5&º뛫=:\]D*El\կ*M u_ 3eFQd+ϙ&T +hUP G9c *haNеO~e^Eg`ڐZi(r%M􌾙y9x8h9c,S)2S r#sf4 /o 3yNT˹BGk*VVK_giӧPDY(9x `Ɍt;LM +")H̄Ez3G?,I_*Ц,U%CW!Tmd|{|iWb__G_XJPgj +ҝ&M~52M09%Lےb5 + %<$2/6!fUgJJÖ 4R9D%|E]{wZH2snjUi#RI q{hPѥxu#]Lt\W܀#p6y 2U6жLvmqt \*A-)[D=H0y,$UkK8Og T]4OVBSc#,+/I#[+͛efffx=n90lNY9ʀTxDL!iYY>Оde\%hY?tgq8r$iUsEē{sY8ݓLA s+ l +4]Ѥa&M 2eR=Li;# Ño4ՆhU%1AvmՎe WOz`8]:цE&j{VoP%/ O8C`Vh3p%EY-K3L:2r JJ⢂\R^Eb|6ŕz<$H훈N㓒^_N~QqiTZ\ɚF 0ݾΑ/_?ߞNTfzF^S`KIMX,%)+#Hƫ@S,wek4%E9T+5=ypnӊgo*ob MXWTofnA!iAy)> 0 1dgMKu;S$ť.i'i'  +'cta~?//:\$B{ +|l2/A51 v730N—,dX ,/Mb#5=u%'wЁ!.~15.6{[=)&+SHxtlӓ_ZIJchkH̜ںd+r+=o`hlk(I<Ρ[\/g{ks] 5FTm"9p5E/NZCV o'ɇH6I kH J˞peV5vNk6879P,MM Ԑ!ف)"*Ԑ#]ZY֝řɡlaOvW4v M.`'{k 3(#"&%XX;25KkdZjJrt#1)MrysKjZzҢGK3`odn}d F?mwL-nljTˋ3-WWKe!6?&SHL!MMN2G֎_Yh(p +Cr%Z淰<1 6K,8?^)VR0h< + +0=93kۻrxzrr|:?5630:98;TjT)k[?<>=TX+HusK뻆6/Af`wkmif-5nң]=9;=;==>:^_l,A@(Ϗo\P981.nM&# cge$:-12t| mj*Ԗ#:)&3wtziumei~zbtxhxlrvimꁙũ +$ +*P sly6#3`|'H, xIR61/WexLB7aP+lC:?;Z[չC`O)Z;<]{KC Y9EUS[VLoڷ//OWgk=vSkDo;%c.oon뫋ݍ暢T,wT6wfQUs̊յFsCɳեٹ @̠I+@W`fc{ *սٱڒl--1"šΑٕ}hsmyq~nYspr~Ino)~r푟'e52!hMkr^90 .`Q@[uaFJbWy]#3+׷څſ hώvVz3R" O%՝! j(/-,[Җ_%t +Brmy(#9>ٌJ>9Gmw58;]_2MXXwVQMk=Cѣ㽝u9fAegH2DeeS7?m5-[&dosqj4HB!/EKnoNO8"rgq04ĥJq"5g?Wo7G+-yWM ٓW3:w|s߾@̜fpoG*F8ڜl.tX1S2j4;Fh?PsufdmӰgJ|(jїuUPGm W @o=')xL?<T +2H|˞fD}cm4^ w5 &rȌ2EI)/NOYg֧R4>k+mti7QadN/@&ӏQw'zʲ 4L[&6N̂f +4YW15&+5-kbex&?9_g2^OaѶzU(@Jq7of0xFQჴ4C|`.[mvF@⹓ +]QU3!27q~6VWTs%+>)`%Jb 0G5? u7ItAhDݛW:0{|q𙥮^-Ui,1N #3ƞ27/d)ʛ:an YNEvE.#F3(Bw]a]8VV !)Z;x2 $Y]ߠku˞`ѫ "Co $=sxbi Yx|o d0'Q1*3[Wpi0t+ ޅѮʪV;'ZJU3{k}řRe5 +9T|s@Vz/3V]Spem3iI$ƥXq-P0h4 M(l?QSET*D԰h\~K@loe$3,O#h-+z+do +~E߾]+LwseDm {Qx{(3raFB%3bcHhN! +5%Nv8\Xm./nV&5o,uf8IvXA77؜8bsdWfSE_c2%`u-cP;ɌS 54#2B Ci@4Q>蒢3U܃C +KK˫)1Kڲ1&d$ƒ*oÞ ckeafjbjVAʼnڶ=\1 4Nv)1?ph[Gv}~BCwt`7 MfWf1ɯ,U(#|Ehi ud,Ја8e2Loful]p{Mɐmc N2d`0<;|0r7_.Vzғ"pm@e /X( +&oѫ}E}6@߿]Oel梠Ev"^S彾DcT*~ 2$3ܶ8)3X +gS#== +\3@u(&>D vLIqWnX䋓uSgWuۖtHg º!O -*BT>9>:22J܉8~$c4 dhxïBLMOOw":b/X c]Rv!kh8FICN46-Bhk~B N@Fd3mlqmJ?<3~,/@ƽ5%r.oie7fzmm!޳{t,3*@" r49PWOnmhlznanf '\y}[1B&Hꊨ=!x¬=mNWX!`. }]]ݽ +. )2ьނ2Svm~jx[kKkg7ulSp#7x&*2a` G;]Aj[ #%Ȑ6GfoBzMc 1.h 55yiIVd$r*;V f!2V3t4;8}~A 5N#ºq #F{uTt6WWW^::?2+Sڅ=qxe,~H2RK]qwԖHI{3IAIieÖRd=-@+Fz;jkH,SE;Kݵ^ Q ά2k_Н|T$[+mo(++"?>GLʖ̸o\wEm]GBӔoml UExlwe Qa xvw 3xsmJ;`qCXH՝8Ә~ ҁ'|-ଠ$>30o 0L͎4הeggeW `&S~dH$$&7ɞܫS-5yY*X\Ĥ+nOv{&M!*Caj$|uyQ~v;ّ=AU`kqreYqA^^^aIe=ٝUݍ{.-_KVO,g~uE%Pe %ٙMMS1A|*a eDNF X$;za>mWԶtϬ6R +O{HB#1&* # 3~=?ؘ)*gdU5u +t&\ꓡ2>F%5ieLu0TSco_'x|r2U戏#SӁ~J>QCEAvzr=Z}̰̌ Q8 +|f N6Wfgx}%eD%s{Ǘ$:XS6[JZ*U[ }5eY4ח[TIvfuW;\8k`eNK֖ezSN+ UV>Ht2d01,&DƑiNm)l<9$;&R +)>&yXŀ5ۋcݤ|idg/{L#gM|=^n L$]530$7Dq +:,޺2.!'%sq9;he&$$$S^!HQِЈH쬒nc<|FF",QNyxO~~FyHH v7t1eY =u^%9rb  + sniS"-$!'JFLZ>|Lk=RD|?';8'Y{I*c۬DLQOWf9ke ̔eΰ h*+ 1㢴 IrT"tI/EnHǛ@$HXXgtAuyĈAG ~FgYmuPv3(՗)i mG i>}R9#(#ՈGKW)bǒb:ژhIv7f4{0x=wal%Ȱ2HJ_djUAb}:T=YJMIv7w^h.WYPl鴼vɆQ $k۰:`l.YnZAnV#T,p懀NVy_:ܐţM@4H'f,T V&cFVdz'u"PQ<_z&-xAá0/EhS+bRD0WSmcĐdحz`mN%#/$KvEː*HٷWfg"W[B7!΃Ka,֡$,m[Zn0nX^ h"8u gxQ8{a,U4G;p8Y@} d`fڝ$;!tń}o!h;]Ѵ{kD@S;@3CNAR2ŧ̈֡fYe=e 3n޲30DP&ƐLl:rqA|hoC݀'mB|HYV r~L +juqvb ˺:AYx|J' 36@FGо&/bZxV6x2ZX>RA}iA^~y8n +T><Y0pmB&OUN0+[Lr5˥)h^bƾE\IH {QE0?.W" M +D @42 Tw ݥ\;5 XV7qQ@;y!E18d+IR];q7VIv t5ה s_IJV)?l'dhA] )Gɣ'~bGJ# p%y08 8U|$KR"H#2+oB4*f1@)@()4J[tF1g(0 3\) . sl_~21Ƞٳn!C3zT.@O` X'̣drniukW-U/Gj2Ƒ~}bZ`z@LgaZ:>;&r/*R$CyQAQt|u]PpyS{ +†,zd$6l2,>-Rm[{ aAސf}4v|M@FB4O5F)/=YZq9;0!!0Ila& ruGT)m_Z=DgqV ("w1u?7Ԣg ]1DL#At3 +g` 1F_'i4C ;B 3R[Q6qmYm$F xӑbbOXP]DIؖz -O1I Į gRɷE% +25tH|H@:j~ Xk!?athib]̼1:=[BY|L Lц6I?2^@5y\b&^ʓH3"$JIJ*k@`⏁*AA M@V=)(i#*@ao"LxkB-OU<];5C'Ef[$%.I3kpg1ےU26N.9IJW:?Bg@AF˕5V& " ah PҬML0+' ˘,%ד_VnaqyU}sGuˇ<@) 2uԅF&X r%B)H/pIآi1weAvVNɛ7 % h"2jp6IH<5J+o2@ m`6Lb(ΜJ:*7q攓DFf00 #2g dP8ۺ,d~,zD&g? aLqgbY  +4ګ4WlvXPj:$\,vT)0T$~$B+r-dS,,A|+)$$42:ޖJzӽH-+j]ǘ":!3)6Ǡ%30XD1崤Dw}q*(-^nB +!>IJ)ͽE2>:2lj'X dX2d'dV(R< eɌ7 ׼H'+kA(16zBe2ƺX~D|>P+|dW)2|DGfѢT(i]?@0#qNea FGRģ +2k XȨ0Eㆻ-PIkhLln2yt|9Akh3e1DCm*ՑkHТcdn\g>P.ȈMpx zRek3CmE$WNZ{Pi51};Q3:e< |&[2D ew%Q|&O( 2٩DklK&Aw|9d["HO,3Qt%A;gV766qioAeKnL_6t;pí1:ԋT-䤄X&?Sg ,e.LwQf8:Q˨J0C':,KۛSf]#>6֦SB-mLUgݞ_9<i;kY2 6 8EБ ßQÎpg[Q7ahamzLdy#3FZG.H뤓7qeԘM.&jjڻz:Kuksw?BJ +#FfP"RSvwNY*ZPͦ ozfnQݡm]NR#VKVs2v&_w- {ӀU^jJue=P3kj!Ҵ8.:VxbYuBF%3P|D@fLTdb́ 82H>3`~ՠdJ@ܤ[Hw3?!;}`bqֺmPZUl 7pWSmyIQA~AQie}[٦D1< ; g;, !na خrˌHd2'Ή%YG }e@_b=fњ @gk}MeEyEUmlW] 22z;F +^o \XY[[i,-**M [,|%Et_1OVgG;bnFjÑB`,{B2;PWSSgn&Xs5k՚I$؍D+}MMEeu]KkՍƿ&9Р<*lluvl@m[DJ"I6b;@; #44DȘ6QdHfϐ*wd +M~z~zBL?<5s{$3M*sIIނj=qc`oken|NsǺ5[쑭 + GX aQǾ<% m~D ?78:9fnxw2T$2u/<6v/ɵ1=jh;{f6ca$TWyO(@}CDu9x43ЂgŸW5O/> #OhoBh k,P!2'nӏ$.>Af& #:"`/Bc|i%pgcy~zrltz_!mBNglqhocynbo`dR_=`f‚M`N!%Ndy[k3cؙX.e2_1C=?cvzjrzf*iHDasְd @lLzSНݽSs8 +2n!c<܂Mpc{Q]fEDHH9EQţ>ד6*s=xvWyVN<;9>NN/nSVyvdzfqC_Qc3ݽcpqEӥ&X}&vE`x'19!C=8@O3m qj2ׄωceIguת_f2YV !@oî$>?#sq< "YD ThqЌ z(dw{k=& c&)GT/j?ijkd6pcnPN2:S6WOx_ 6m[M$.?>Vhq|TU-}zGAOt^,X[+kIᗰޙ| 8, Cu3lcv6)LSLhB nweNaN_i39\0ab ;d~}y]# =YKXaō~NyWٷD@}P֙ e #YZwLKI1e"}%J\|$xsirli OLRz3+w&z9b#q`5֦w%5º 9z_Xv%ܣTYT +[^Zճ=ON7G:kCf6W4u,o]GZSvg N^c<-3nb{AzVw.lccv9+d6AF0bS9>:eI}e WzuLra9&M7Wh4.\8g\;ZC*//6\vC:nyT,66ĄcGUy\3}p.z"'".-5H 0|=h'.1N3Ssz[ d_&BCA.mw LUcA]h>h{j;G| xC+[;[ks]ڌ4Œ1S;}FǪ]J+ P3:M +:w6 +m-As4XΌ~=aŇ (=ӼiI!gnJqHz1eOqXIT-L\2CJt'wtj~ymc "F6! kV>ț/ɉbKCؼ`P}MKЙ.lMtϜ#\<𵞤FmH$>Oh 𪊓㣣}^hڎ +\twyyASG`Q!F3t(jf5"hɌ DZ~89kȆ`m*dh[QZ=4>mosy憆Ɩξщq#5>xpB#YazYK4QQlcpo[}y~ۆ:6yȨh-;zEss3#Uv*.M6VkJTf-h\w.K;[[V B%ⰹ3 *zh;hP큮ڒt +ԧPd90:1>:M??+~<}cCeH ٟP"@s-?ǠHo$a^Ġy!I ܢf2biYzb8HtX,ili'uPUQMpd簮>/,HO[CeQי 4i-:/oh.͠V&JÕ[R?2>e*Wa͵eYS1[UT>%+&538*QrzbuEiIIYe]S[gWwW{smy~ރa!>> +=Aӝ7mO ͣb#iڔ*hhhgP66+#;kXAfDaL걏FfBnnqe 6$ #[wb 0-ѫ^{鑺d8?[ +K+kjkʊ!Ӷ𞚑[T4(Zi,m!]M3G5ezaoJB1j|-,R=@7IJ+@FkA _O .NR`ca|8d~BHpA ֖榆:LOu9SzsVۑŭXJUm}cSKsKScAPHzzP2 vg7JJJ 3R 1SkoL-%$+j閖z(O/NE̓#]D +F%e赴02j<5fgx6KPXtN *H`:̠Ɔ:.8 hSB2sJŽE&JaVSbfstOnEzI~澤?RPdNhs'A' +MtJ\|$ [N _70qP`I3rԨ%,$c/򿄽>Oj-^ SakԽ>_zǝbҺRǖ;Y%9{^УOzy=56!ɞty4Q",W.,qM5LhNzPn=L%cgu Ye,%Fn&nvEnMGJ}|d }ŹY43qV!١p8N%Ӽ*t@OxE`2&.>fmIpØ(.l0ѱthf/#< ԞDpW>ʰ,i.!H9Iw`#&>L;m q$}H* +#mu>y5ps1 ':6>1[U +"X zi.mq4MJcpO/5\ħ5C^6gfۀ\ +߿=if,wbObhu$ә`'LhL&6!)iV"bg{RҖD+L!Å'bY|8QL42=_5X@ lI& 7w)&ӣ0ȴX U{"U`A%Lぶ)@?i:"rWB ts~0WD1D2mQ,l^~~Rd=,82]c6caV||D2oPp \Z`V,Wi E\JGWsO ft?OxF2E5(6&&:BL&,͊82(KUcj(or э-|1gtEgS'.gQR|%MSLrtE/trML!""RO{ִi"9S)1fʩZŕ (dQ)?R.A2rnX;龹ӿEL72%E,Q?] UWQVTBہP 7EqZOSDpJ Ϸ1~F͆o$| +?I^G!3eߚ]鿔φ(jN2bj+S\ }DJGɔ=*z.8.9R!5qPR:TcqϛU!?#үR%c5E-=)i@|1g"UΏHx߿E ,%-CWw8ީu!s_z_rJn ]bzMW?R/|"A*6OO%J@mќ38Bn$I˟^ӯG4" hTV ?N + ߱1 ~Γ4/)My/Gf +~BO Ll";1mw'h7x_p"SJq}_Cc?o~AD>~ԖnfI$B]q>[Ok + :$zojy@hwun`yHbբkq[ 3L7Db/<=P%!l%%9'InCKWc߿}f^G, N +`km~bN/[0EƯL8'QCzS,W[?W b0["r!H|B!mV,O҇zSCy5â0[Ve4@~ BVLJ󓣃m,bch#;0~e9{̓sʩihϏwחvހGHABnyq~ +*7W8벑F-yGƯMʩ929xDR;ã o,'puuyyOOOv6֖f&GJ|f_>V%$ ߿j?:>9%|C|<bwg{K,/L t͹y>=Z?sA a&[6ۺHp{uavjbbbjvnaiyuu}}sl80?;=5162XWYR'w@Ry&>{c}=]]=C#cS3ss Ylomj*/.iV]b-H/H( [ +?@@`s~o``phxhhhoU%EYVPoA04bt7Ry +I5dƭ0PYZXPXT\Z^QUSSW񧩩Lss2ۙl0,O w{D4S'a ɾʢ\_7=#3;'7!ҽ EEyy9YYnv8lB[B1<;2.2h:&p&?2zrGNx<ތ ϗwϗy4L1[j]C#ƮGl-"4@Ɨކdxd$%řb~`aw{i=:Phk;.nDuUǮ^N5`}'h 4mzB#&V7=ă~&ZR""lGTXxߐ>v#4<\n/f(nnynz|8@DK;Ntg#4f.gГͽC.:uX$8 w=HԣϏ?65)[zy*.Wo>AJHcs(֣WW qBDcܣ$n8o}L̍y!X"͖2MaUWXx&s\Iq{BdrDXQZ`hP;}IqHNM{.ZsSSi|g#IeKiq ՕFpxw~81^_^=))1Id+tR~)գGߝ>~ +sxr˛&/Hl =ZKon w +R<^⡙YyzLPiiIQ^fZqniFhT|7ory6}];ݭ 5eEH g+#V@8+'nh~_OU-@3s7wxosynbںzҮ v6Us5ϷT  )DB??^m,LOa]wfƭGbeߞah|U} 'hW 4<;:\]Zפw/,.mlln-NuTMO|-ԈzG؜rs/E``w{sc}}U[WV76wv6{`wmO IX=:kxfyԼ†9;;6}58;$&PxGߟH zƤyw̵y)|[ЋIno./u ]-gwww7'+2MޑO ?4R%5-zHt׆}8 ϟ??{<@{Ѯ|]3BwfT4t ϭl굹x#_~E߆,^_ V #]=-5C+z9@ra"gB/*;9^+$?TC CILFl׵ M.od} HB9\EeS] 3qq 4*kYЛO2+ac<;=>X]oo(&'G?1/19Eu͝}k{zӝS}q~~mow"M%zܒ CdSm]}CSsz۶Y0i)qpWkcui~G C#E!a1InoV^qyMCkG,Ps}}sc3p`oGKCMyq~e] }JWZB>sȐT_,}#1<$@܈%<YeMwڻzzD=E荈iwKȍP.sY9z7\Yeِ!(/mY>oq=O- +8bwz3|YٹFxwq;SzQ=2G*7jk+JzGg:(Nl՛"ts\|BRf!͖d +RGqV""*L !>, +|-Bt}i + +*u>w@dHND0;BY +endstream +endobj + +8 0 obj + 25939 +endobj + +9 0 obj + << /Type /XObject + /Subtype /Image + /BitsPerComponent 8 + /Length 10 0 R + /Height 148 + /SMask 7 0 R + /Width 538 + /ColorSpace /DeviceRGB + /Filter [ /FlateDecode ] + >> +stream +xK-IrfiCGHӸ=69Df [4uFWQQQ1}{8~G{v5wt!bč?O 7|=7[a1b.Vu]cA q,q, b4k K8&78V8'ψc=oVp+8?nɆznJ%bW\`uWþ}h߳Ho]3\PILmNjfk8=tM7Z(xutPWյ~.)xخl K֟.ǐc{֫v{?wM81젉ɂ4{cwA +8JA7sН|ͯZwJop [4^/nB7ixm:zA#bC#b~61Td O#}ͦ#[*"C|"`X* a֟S@NE" +K޹}q7E87m¹r{;xu@q~WE79` e#)YM1oG*h7)EڰZ?o~-yIopv=1J4c}Mڠo~n8WoV/X^ggİ7?3[s :>+ϊEMyu}jiX4KzHvЁ?u_q-FZ:/DX;F9}rfdy읿$Պ66MjfFX$k"]Ldͻsey;}S?k{qU™;aqX"ki}fu#(lm<(ckRo~z[yڲ]%7u`1½]dSlpE =^8+fxka+a]6z'?H @ t,H#vrfMtq)[l;䰶͟[qf6ћ +<4/şP{_ 7Ek vDNh~gZy_k|Jlk#9 ``wƓɂ np, >^ +me,\>(,=9m7M9^9YMlD69}h|/Efѝꖱ B!{Gf?/v8ןJ4w]4[tEE" F$O-`o Dq!p!9a\~2i–uΡp\ +oJ"!o~͍qGUښuxf_rςq>%2DdV6FA ,F<] qOO4,,HX%MX4AVRC?#:B6_pqڲ9l~"5nl_"?~H(-4Ʋk pn.4zs&8^F򲃕ၽ=y4j45zUuEƹ^04? 0:j; +@wGym%?k.k6aݙ|R;h>o yzbXXC|A!\8枋c؏d_I]/j,αEKBpdkq”ThA 0)A\ۉg~;+cpAnky6K'\ٟGY uaesq-$ȼccʀ8<bxmv=1,j#Ƶ8V1XS6hZji!qp4@ZId4}l: 9хc{ªP +V64?V soRm؏׎#Y )uxsk ;oXOƿ . .6ԎG[cB* THxRczv6xg׬U&`YN)Q 0]!ʀ$pf),k?x8Ry4MS0)eڞHaFƱ0+  ueCX;#XsӳPUP! Ű @ Q AX"kq}<NJwXHȪmlz8%X.=!9kX7r + MC* ~ +4=^<;Ǩ6ŒHC<2."`ȱ{ϔJZ&2j5~$X%,mȀ볣xZ7G}y%]3{nzqlFnM ;1H k jCc1ߩ౳Sb&]( ̒FB hvyzD]%L7EӁ_B$щlH!Ġ ",bD5wOVa`ZuH6Pǂ[{hs=e +:13( T!?í'ƟȂ@" ? offM  vx@,Ň]д#ŻibΞrtDDO`[sg7 p=u2]+]T kh">sDD[7oF/VWć ? lH;xvV3o~|PWkth;HE0[bnza^Uv<ǧ(Gn͝+Br v9y[u)*ؠ4`ߢ+h$XHdwLj GGPm;!;z +ۉASV*iv5{hqcclDՉ,ٝݓb3Cv젃j©ky'cIA+ p$ؠFňA3LuP1xyX60E [sk8kbLXːU`Ƃ9 CM&;L0Z#v#\mX|_ݭw<*(gɂ]c#p @ yVRD ⺮c{`C !&X$XX @ {sClw{ g6l @ Hgc44vYb/+Gk,٠h  @] Bp;hFMΛH5i1F 7h ,0]a4a7"-La.a]C_ fW &rf#4Q4XoZAS7Hg`wAN#BAD4vq#X7r +6vAȹ#\[ɩ&hT78C$Ћ xkAX5%CB]eѨAAsHgA a`1i$>`,Mѓ_ <;#-N 4oɪ Bs4G IX7r ndAQ!jB(R+L3_I}||XyGPbD72:.f9x}M|J0_R`(gt;yX@"a `"h$_d7 bЌ6"Jfi 4'p;w8֛t㜃'UpJ-F&%,I"ȦW;< RTYY&57ӱHsЕ<өT*!İ!hJŖANRᜀ3Cb! opB)d2Čf/@! MqHk-O{c[b`c7Y$؟m<'w)&@Ð2@$c5q]ױ 9TCI$ HEk t=." Ñ``@j@SZ(Ǭlgʳ FL}ЅڼeugQrcQ6ve/ykNۃf Hɡ+[cc2*!Y$tH2Yܾ}l2jJS@Fa^Xj +HHHCM$eE7JS{c<8^:Z,a%DSf]ff]Qʍu] +~&j}xz0PH\kg-҄HXȳbƿ  @DFEb b#fwC oD6zI&rA?u 8O_u0/ @Hk""7?yqC nR{R52>(+ˁ<`Ԅx ~0kV<2\A~bԦH0FY|z9f4|W.?Y]!RUy  <2B j7r* `Rʙ5K .,l6 M쩌 Fb HQ78uXX4V* C3L"KF9!S<17 Hv*wZ׬Z?,Y4! H{cd}ιҲr *c! A#t#bo"1v53t!xF<."h$X,vCv' @̌Jdk,'< фy%! 'L4p^(މ厳?Q$d+Fd]Ps06hͬ2Gf=5{tOGi GoޠA%\V}r2X2nly"m%JBηE;,IAI]"{"' ]Cuq^ ՄY EfM͢f@ SKbj?Xks`Rc^i, F;E6*.f^V [|cZrgD3 kd5-GP0y`A wxvxɓ ݂xK]H `w ' 0-z,h _hr@_ lseqYwK:!NєJBii,X]He`_UbPYH I!)zj;z_xU!,}M 3s]|Xv±~RYVrEc_xR)bH ž[`#s׃KYf,~=i]!R4Y_@VF(Uv1t1jAS{IDzhf-]ˆIk Pu*޼fFZyAO2] aƆC:ղI oH^D ,Z=;S^t4f';<1g 03 $04F ͂čE`;lE M ~Au?A !1/=aHCS{MޱpCI+FpА Bre[s X88ן b +6D/dt` v-Z^0+y¿cMBf[V/DZHqe44rF,0H5e!b V? S녧VFeKV[s\u?):$Y݂B,Q'z_0.ɁiTej% 9D!RQjⲙ5$rBb?Co;!vxMO?"4c1 5~-foՉ[tE46=bm& yX0y5qO-B^aOeVSC3)!!+355"%_#F˥ ˂0)*m"+$ +QO!t֜s4a,F7W sІE/l{w?C$`j򀐖xehl_NU?'$9 &&ؠop1Nb'1͛"FMEMf?C/&;v9* "VT9aHM ~H(X}a=v]ױ& c! 2CfNO e-U%,il +"i] Dz(ӨW0n cY3S{sUc=%%4/] RΧMbPf]& ",?Q^1!7/#3\YMjg^ +E_0Ih}Mg`Q-OiSyPN.j 8rB[Z-@` `A &p4c_ 7~F,gXæ6J8i9QQu\X +\_VuM4JXf,OHu6Jy_tُFQ` dٞk0dTt\B^Dt? -, +w?o$1)](=5mvBSw +tкBxΝs1x?GѫޛI9iSd!ukrIA56 N/ & "<$jげy],FȟXk`&pL;&vDЄMa;`riO 1 y0@9m$ Y ,ޤa^C t4Zk +:oo] a`CB~HB% c%lf1W0X$!LTr"z)(py@Zx%Y +S~\u0ks_HMWنK2 }PjmEК[1~1hAb"`E4zgyәUSJF^V!*4\Sͬc&cc`C) :x)4 9 ~)(ceyI(FI y`F ň콻IHOCM6ugWCͶvNh7k%$3'!2 @j0VU-a5aBb-?h*_ cCA;xS`䄴jP ٠+hX DNMwA l"eCXi ų<<̓V3klvbPm!-*+) C aP  [ځdarD`Cf†;kǻn"iuAQDz%).A^=i>d"XCcYIXA0#@ ` 1V厙b&dDc+9++;QYL6bHАͲ[pgkٶ꯫c` * BSĠ!$F'BX5f>D6pABkv-!ě +) /+~)Nȏ5u/B|%tPEE :hfoudF"7?b欙SHA4A؂ω G8 ^[F@;^0 +feEI<$T7y|JXׇʏ7$H8!XEHP*Fp BB0;ДE=`ԃI A]4'x@ɂIX +Z yfV`5`odŰ0 d&IB&N@QB)8a ؁4CX@"ne$!vxa"9kL([ּ b.aa`Qs5-0/O^ά}dSZhT=hy vx@@ňDБfA [$ϸ~-F$X[ >#GeS 1vG N q#-:G72ȁؠy5 +& c#"11y MVAp*$O5O z CdÈN1;yIeJ,[y];5V5) +gh o,fbbDߨ(d;hgщ,xK4{#gvn"]q#m;=u֜N)m$_P@  >leTCi:<6*4HH&n"  zۆ wT0bςxАf;,Doщ,xKcMHFx;;ǻyNW͞@@rHd`  n]&;uf{˝/|Yy 60[,v,|&v @P5|M*R z}g#<,ɲ%?XX 2wf^ˑO 00223\#jpK{d G9[I*M#L|`͡f +CM F>tne%;kCT?룿1``ӝ"鎈әTܚSZ>G>=,AÀ<8xnz5FXϿj,t3&$5x<*0ӀtL5hfם0`vP0`vDvSFf` 0xh܋kaG9F>4̡1?`U3TRq<2) ':]ܕM|Ԋ>_ַ4F``IAFa +nU|\8s + P1/k9ӝ0O0(.+>eZ7Wgr. A 0C4 0-COrk.IYad(7 +"O!M)ܔ. 6J81NƧ`DY0{dY΅2Sd<"5 A4x0Ȥ̧&0&2V>)nMؗnߵ>&j`NA#E& Y`:|Ajvr *҇u9S0L>vN8IJLxf +O=aq4Y>>ө>!B幵ΖTt(\ 2Xޓ4M0` `"a<9&4#1~7<<}q(gZQC)NCA$n``dF*ys钞c A=[=0Ԕ Bcib hd,> AO +6zR>fYy}ݵ:X%SK d(2ncƷ\u]obq}.$L1Úh)c x݂#AHŖEU625KtC$24x0Ѝ 5 5̥ԗL$1hRlH*7M!BwVɢaLY@n24?4N])O3u"OM0yP0<(2L~45k&0` +wAωGw|= k"&UNo &4M# 5S0xqUzA0P\9)A$r gov +Gw$'? +|:h(̧J +2~1Ww)Nd3lf +&4pGJ :|JSthg¤A vR 14!==?0`f1/%ʰc. +M0` D#0a4x @9Ͼ#IFkpˡYu-c4(|(wi<<ȏ<41fxD4wD0&acv +Ȍw:D1yw +tЄ;#*%W)wh`=Gw>&aF3tA dsOd<{>G2뵲"N +P %C?e1pVuzp}?2@s׏)7@nK<>L}+е\3쮉jc!B0GM&-,]k+aCM}uaC15)4RA"CC3  +]4JzQњ[[[;]F3 4e,G` Aƍ͠}xHx:'&ŘpN! 03ԍFGTaٓQx|p|ҲuJi2L4 <r-;YII[>A eخeR0Q0 qjfDEC}Ph + +ƅ!dΠ-2=H.n`Ew%lh^^(sEPi@tpHz:EXVK*A3t;K01 .1`&1H3h-ZSCtG^? ʀ Du5q+됧NJGιfa@#"2+ҟˡҦ;rS`YSA3v&d `|&4OGf:7-oX`PfzHze=.z1郂Tix\gϞ&5=3TgSOWϭʓH3ч,mR)7:t44\#F멖f5a=_Tz\`K)w mu׬P.0p!dJ%e,/`݌Cp[g Ky% j:4,؊1a2/8Æ&+6:Z_Zk<]`!Cș3h:D~hV֌!swj&+,=frQ&z;Bõ[mz64CH==ú4;څIJJj]"IG& ~7~Ќ<1Gb3U9&aM4AF;ơz钡<:Zq^`RŹF04 AЇ:gAbU?ͥij0ߠ>a\85K"4xC56ڧ[/}0P5xA7;cf20Ks!, 'pW= ccRuH,MH>fq-s b(^)F聩`~ɀ)JwL#"CѪ=nGω;(9VI-8=f A9P8SsZG=T=TSP As؛I)Fn+`d"aa:NsER2r4K*>;'-fFD0tt&`9 +f"C CI01ȧwb̎`5G̠1h+^{Ad4 &?$`~":7zBD|YS  :Ch@Mxl!"[MZND01g(ZI6)2⵨Szע tԅމAO_k チ7;d/@ %5*}TB |j|O;'I 0CtȰ7tjֶ?_z+߳z]kݾpB`>֟R}^34i#Z%P^(݀  44{-Y3 ht^wG`qJLp 5:N4ﵞ ׏_kL j?h)(kf + TBA:LjȘH1;P0`0f(~C`O18ӪJ|%߆y^]1N|M0?f}yrO߃?hj6g7Q=OJ"0)s40ߠ1C|&24C`%aIam˯{qmxW?3tHbe&[/|(`Q0x#SBL`O)> fScaJEb|YzSnB?QhE% 0`"OD>&xdFMd(9|LwD|} D0`aV{~ybr]p8~M(no=Uj#2tGvi2`A&ᇚ4wL$"C &D Lq1Sbx8wl%/[O% "4x0L| v1mfiZFJڣ[^?Q?n9BfM@ePI + "Ȥ蠉1 4ǼPiȧbaa%PJ赾jᏁ}&P'c_>h{|Q`|&Efסf̮;+xg o%W~ص}5k~8Uo7a@ +#T?_9 +f؛`؛<ƀy0x"CA +f+ಕ0ýp8Hx7~tT1TG4vGAGw1:< +L~8 jY6&Qo%Ԫ[ɽ8$T6%.dhpP?5 ERdR0`b||<dQ3]>#. }Ps4p8+kKPJFMi{̧CM + 2tCd؛@Ȥii zGo%6np8TBaT!JUWL +fI4a||ͤ:LtAO$/w^Iv?0J/*W}7WrS@cipi$)2cg),}oZp8Tt|S!<2t݄!H(  P04#OM=Ԥ-06[1Kga0$ٝIϵ_Pc&`j` f* "MdA31}`5l"~,Gzý/=]ً<S:DGưp{Www?Ԥ}G^@&ba?ϾԲx_,p8jO'B +~.f؛<0L1&LK7iu;x0`ٙȘh4cPĘPshZ?A4m"~d-j}߯ӉmRdAs;m%M+&U}}$2av +z>p(3<΅ 755}5ĂBMr-ziH_Pm|/2xE^WC<F> } )p/F]]CKOFPX]Vchp8﨟 +ؽ,n"d8f1lt0{Iʻx ;;iRpJ'¡x2<~ bAv8TU<#7y004a Bg*Ф2R}Vc,p8|*G~6h8( Mo@X3 ^" ^ˤ<V>x}p}k3Z| ?ߊJ|{MCWF"݄&As4>Pqڮkq"#DC\z%I t}p8 ^O}Ag*꿍 4)2 &}j0~A6~F꺮תx=a^A0"סku-^뺨ь  lnr8?%: 'q:ګᨍ 1ȏ`0>5ˆb3j;/(@aנpww?-:̞Җd5oY-U[*4PE /|i;Ϧ .eIW@2CP  +&x0L| G94GBЁA`j eҰ?@ rP|RUɰ ;{3S$~}'bqg:Xz8A%Z9(.aFGx6DC]ã 3m;}˰(6?"a?xbCn1-` ̧̡1GG)a؛|\~:q.\"p8BꗵAjɃ*@1)LOnnB&nr8j)7+/~ڞФPCA>؃ L#y?w/ïJ6m(~Tg0PSd(f1#퉚e7app8~=W^_~k{ކɚ3260`>C ]1 A|WAQ {CE_~_W)PWCD~̎J(DpOzעzu]k'KJc^RDC~b34x0w&€ 9@$$$̻_s1pbCske} +)JBͨ۱{}ŻDy`I#Ofb❋c.p8?ҷ^、}eOGu2 #<(NMd]$$Vҫu]MS^RS) +8(ã v>m: <"ST%,2:6p8~CB}]%*6n{ +sӊ⫭^I|ދFy8߀j}߯cZګ +SVƩ[x0x8i|0kGDoR>o^p8c, ' +;Txu">75B7 l"mg.AV<(y0p ÆBUiۊ=EݶVl+LxSP|^]``w  +42l"~1̦fIȵ~mkMO mrETa/m + +;"pA ;_3D^ȇp8^׏ۊ +ǿ.o41h"CuΥv:;Ț^;{)(bPCg E,^k(fav0p3Qq}{` +\,{CŒp8`z0qo{."e{Epf:1p z +endstream +endobj + +10 0 obj + 32389 +endobj + +11 0 obj + << /ExtGState << /E1 << /SMask << /Type /Mask + /G 1 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + /XObject << /X2 5 0 R + /X1 9 0 R + >> + >> +endobj + +12 0 obj + << /Length 13 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +269.000000 0.000000 -0.000000 74.234528 0.000000 0.000000 cm +/X1 Do +Q +q +/E1 gs +/X2 Do +Q + +endstream +endobj + +13 0 obj + 118 +endobj + +14 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 269.000000 75.000000 ] + /Resources 11 0 R + /Contents 12 0 R + /Parent 15 0 R + >> +endobj + +15 0 obj + << /Kids [ 14 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +16 0 obj + << /Pages 15 0 R + /Type /Catalog + >> +endobj + +xref +0 17 +0000000000 65535 f +0000000010 00000 n +0000000493 00000 n +0000000515 00000 n +0000001038 00000 n +0000001060 00000 n +0000012016 00000 n +0000012039 00000 n +0000038194 00000 n +0000038218 00000 n +0000070841 00000 n +0000070866 00000 n +0000071206 00000 n +0000071382 00000 n +0000071405 00000 n +0000071583 00000 n +0000071659 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 16 0 R + /Size 17 +>> +startxref +71720 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.imageset/logotypeFull1.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.imageset/logotypeFull1.pdf deleted file mode 100644 index 1420a5d7f..000000000 --- a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.imageset/logotypeFull1.pdf +++ /dev/null @@ -1,513 +0,0 @@ -%PDF-1.7 - -1 0 obj - << /BBox [ 0.000000 0.000000 261.000000 67.000000 ] - /Resources << >> - /Subtype /Form - /Length 2 0 R - /Group << /Type /Group - /S /Transparency - >> - /Type /XObject - >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 2.000000 2.000000 cm -0.000000 0.000000 0.000000 scn -0.000000 63.000000 m -257.000000 63.000000 l -257.000000 0.000000 l -0.000000 0.000000 l -0.000000 63.000000 l -h -f -n -Q - -endstream -endobj - -2 0 obj - 234 -endobj - -3 0 obj - << /BBox [ 0.000000 0.000000 261.000000 67.000000 ] - /Resources << >> - /Subtype /Form - /Length 4 0 R - /Group << /Type /Group - /S /Transparency - >> - /Type /XObject - >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 2.000000 1.844727 cm -0.168627 0.564706 0.850980 scn -57.842663 25.387310 m -56.973518 20.943096 50.061783 16.079765 42.122841 15.137188 c -37.982918 14.645527 33.907391 14.194614 29.561213 14.392532 c -22.453136 14.716278 16.844669 16.079762 16.844669 16.079762 c -16.844669 15.391525 16.887453 14.736427 16.972567 14.123863 c -17.896652 7.149250 23.928431 6.731026 29.641823 6.536243 c -35.408352 6.340115 40.542614 7.950329 40.542614 7.950329 c -40.779942 2.765934 l -40.779942 2.765934 36.746300 0.612568 29.561213 0.216728 c -25.598721 0.000004 20.679268 0.315689 14.948763 1.823364 c -2.521337 5.094391 0.384049 18.266270 0.057106 31.631596 c --0.042868 35.599815 0.018828 39.341911 0.018828 42.470993 c -0.018828 56.138123 9.024163 60.143955 9.024163 60.143955 c -13.564885 62.217625 21.356571 63.089451 29.456736 63.155273 c -29.655783 63.155273 l -37.755947 63.089451 45.552582 62.217625 50.093304 60.143955 c -50.093304 60.143955 59.098644 56.138123 59.098644 42.470993 c -59.098644 42.470993 59.211227 32.387894 57.842663 25.387310 c -h -f -n -Q -q -1.000000 0.000000 -0.000000 1.000000 14.485992 26.452484 cm -0.996078 1.000000 0.996078 scn -0.000000 21.175804 m -0.000000 23.165289 1.622104 24.777744 3.622489 24.777744 c -5.623325 24.777744 7.244979 23.165289 7.244979 21.175804 c -7.244979 19.186768 5.623325 17.573866 3.622489 17.573866 c -1.622104 17.573866 0.000000 19.186768 0.000000 21.175804 c -h -51.953178 16.803417 m -51.953178 0.255280 l -45.359833 0.255280 l -45.359833 16.317129 l -45.359833 19.703238 43.926872 21.421368 41.060944 21.421368 c -37.892841 21.421368 36.304512 19.382627 36.304512 15.351715 c -36.304512 6.560461 l -29.749895 6.560461 l -29.749895 15.351715 l -29.749895 19.382627 28.161566 21.421368 24.993464 21.421368 c -22.127537 21.421368 20.694574 19.703238 20.694574 16.317129 c -20.694574 0.255280 l -14.101229 0.255280 l -14.101229 16.803417 l -14.101229 20.185495 14.967223 22.873068 16.706865 24.861656 c -18.500998 26.849798 20.850389 27.868944 23.766304 27.868944 c -27.140659 27.868944 29.695858 26.579786 31.384611 24.000576 c -33.027431 21.262854 l -34.669796 24.000576 l -36.359001 26.579786 38.913750 27.868944 42.288555 27.868944 c -45.204472 27.868944 47.553417 26.849798 49.347549 24.861656 c -51.087193 22.873068 51.953178 20.185495 51.953178 16.803417 c -51.953178 16.803417 l -h -74.667320 8.577215 m -76.027779 10.006529 76.683022 11.806602 76.683022 13.977438 c -76.683022 16.148273 76.027779 17.948345 74.667320 19.324818 c -73.357300 20.754578 71.693764 21.442368 69.678070 21.442368 c -67.661919 21.442368 65.998833 20.754578 64.688812 19.324818 c -63.378338 17.948345 62.723106 16.148273 62.723106 13.977438 c -62.723106 11.806602 63.378338 10.006529 64.688812 8.577215 c -65.998833 7.200743 67.661919 6.512505 69.678070 6.512505 c -71.693764 6.512505 73.357300 7.200743 74.667320 8.577215 c -h -76.683022 27.213799 m -83.184502 27.213799 l -83.184502 0.741074 l -76.683022 0.741074 l -76.683022 3.865234 l -74.717758 1.270798 71.995941 0.000000 68.468475 0.000000 c -65.091866 0.000000 62.219185 1.323635 59.799988 4.024193 c -57.431683 6.724304 56.222076 10.059816 56.222076 13.977438 c -56.222076 17.842222 57.431683 21.178179 59.799988 23.878288 c -62.219185 26.578400 65.091866 27.954872 68.468475 27.954872 c -71.995941 27.954872 74.717758 26.684074 76.683022 24.090088 c -76.683022 27.213799 l -76.683022 27.213799 l -h -105.058311 14.453876 m -106.973137 13.024565 107.931007 11.012690 107.880569 8.471540 c -107.880569 5.770981 106.922699 3.653431 104.957443 2.170834 c -102.991730 0.741074 100.623421 0.000000 97.750740 0.000000 c -92.559738 0.000000 89.031815 2.118000 87.166977 6.300707 c -92.811928 9.635767 l -93.567589 7.359705 95.230667 6.194584 97.750740 6.194584 c -100.068611 6.194584 101.228233 6.936104 101.228233 8.471540 c -101.228233 9.583378 99.716003 10.589090 96.642021 11.383003 c -95.482407 11.700928 94.524994 12.018402 93.769333 12.283487 c -92.711052 12.706638 91.804077 13.183523 91.047966 13.765636 c -89.183136 15.194948 88.225716 17.101593 88.225716 19.536619 c -88.225716 22.131054 89.132698 24.195765 90.947098 25.678362 c -92.811928 27.213799 95.079361 27.954872 97.801178 27.954872 c -102.135201 27.954872 105.310501 26.101961 107.376183 22.342852 c -101.833023 19.166304 l -101.026474 20.965929 99.666016 21.865967 97.801178 21.865967 c -95.835472 21.865967 94.878067 21.124891 94.878067 19.695580 c -94.878067 18.583744 96.389832 17.578032 99.464264 16.783670 c -101.833023 16.254395 103.697403 15.460037 105.058311 14.453876 c -105.058311 14.453876 l -h -125.722229 20.648455 m -120.027290 20.648455 l -120.027290 9.635767 l -120.027290 8.312132 120.531677 7.518219 121.489082 7.147905 c -122.194763 6.882818 123.605659 6.829983 125.722229 6.936106 c -125.722229 0.741074 l -121.338226 0.211800 118.162918 0.635399 116.298080 2.065159 c -114.433701 3.441635 113.526268 5.982782 113.526268 9.635767 c -113.526268 20.648455 l -109.141808 20.648455 l -109.141808 27.213799 l -113.526268 27.213799 l -113.526268 32.561180 l -120.027290 34.625893 l -120.027290 27.213799 l -125.722229 27.213799 l -125.722229 20.648455 l -125.722229 20.648455 l -h -146.436966 8.735956 m -147.747437 10.112877 148.402237 11.860113 148.402237 13.977663 c -148.402237 16.095211 147.747437 17.842445 146.436966 19.218920 c -145.126495 20.595840 143.513840 21.283630 141.548141 21.283630 c -139.582886 21.283630 137.970245 20.595840 136.659760 19.218920 c -135.399734 17.789608 134.744492 16.042374 134.744492 13.977663 c -134.744492 11.912504 135.399734 10.165268 136.659760 8.735956 c -137.970245 7.359482 139.582886 6.671242 141.548141 6.671242 c -143.513840 6.671242 145.126495 7.359482 146.436966 8.735956 c -146.436966 8.735956 l -h -132.073563 4.023972 m -129.503494 6.724081 128.243469 10.006754 128.243469 13.977663 c -128.243469 17.895733 129.503494 21.177954 132.073563 23.878063 c -134.643616 26.578175 137.818924 27.954649 141.548141 27.954649 c -145.277802 27.954649 148.452667 26.578175 151.023178 23.878063 c -153.593689 21.177954 154.903702 17.842447 154.903702 13.977663 c -154.903702 10.059591 153.593689 6.724081 151.023178 4.023972 c -148.452667 1.323414 145.328247 0.000225 141.548141 0.000225 c -137.768478 0.000225 134.643616 1.323414 132.073563 4.023972 c -h -176.626083 8.577215 m -177.936554 10.006529 178.591339 11.806602 178.591339 13.977438 c -178.591339 16.148273 177.936554 17.948345 176.626083 19.324818 c -175.316055 20.754578 173.652527 21.442368 171.636826 21.442368 c -169.620682 21.442368 167.957596 20.754578 166.597137 19.324818 c -165.287109 17.948345 164.631424 16.148273 164.631424 13.977438 c -164.631424 11.806602 165.287109 10.006529 166.597137 8.577215 c -167.957596 7.200743 169.671112 6.512505 171.636826 6.512505 c -173.652527 6.512505 175.316055 7.200743 176.626083 8.577215 c -h -178.591339 37.802887 m -185.092789 37.802887 l -185.092789 0.741074 l -178.591339 0.741074 l -178.591339 3.865234 l -176.676529 1.270798 173.954697 0.000000 170.427216 0.000000 c -167.050613 0.000000 164.127945 1.323635 161.708755 4.024193 c -159.339996 6.724304 158.130402 10.059816 158.130402 13.977438 c -158.130402 17.842222 159.339996 21.178179 161.708755 23.878288 c -164.127945 26.578400 167.050613 27.954872 170.427216 27.954872 c -173.954697 27.954872 176.676529 26.684074 178.591339 24.090088 c -178.591339 37.802887 l -178.591339 37.802887 l -h -207.924301 8.735956 m -209.234329 10.112877 209.889572 11.860113 209.889572 13.977663 c -209.889572 16.095211 209.234329 17.842445 207.924301 19.218920 c -206.613831 20.595840 205.001190 21.283630 203.035477 21.283630 c -201.070221 21.283630 199.457123 20.595840 198.147110 19.218920 c -196.886612 17.789608 196.231812 16.042374 196.231812 13.977663 c -196.231812 11.912504 196.886612 10.165268 198.147110 8.735956 c -199.457123 7.359482 201.070221 6.671242 203.035477 6.671242 c -205.001190 6.671242 206.613831 7.359482 207.924301 8.735956 c -207.924301 8.735956 l -h -193.560883 4.023972 m -190.990372 6.724081 189.730804 10.006754 189.730804 13.977663 c -189.730804 17.895733 190.990372 21.177954 193.560883 23.878063 c -196.131393 26.578175 199.306259 27.954649 203.035477 27.954649 c -206.765137 27.954649 209.940002 26.578175 212.510498 23.878063 c -215.081009 21.177954 216.391037 17.842447 216.391037 13.977663 c -216.391037 10.059591 215.081009 6.724081 212.510498 4.023972 c -209.940002 1.323414 206.815582 0.000225 203.035477 0.000225 c -199.255814 0.000225 196.131393 1.323414 193.560883 4.023972 c -h -244.513977 16.995380 m -244.513977 0.741432 l -238.012482 0.741432 l -238.012482 16.148182 l -238.012482 17.895418 237.559006 19.219053 236.652023 20.224766 c -235.795044 21.124802 234.585434 21.601686 233.023239 21.601686 c -229.343994 21.601686 227.479614 19.430855 227.479614 15.036346 c -227.479614 0.741432 l -220.978149 0.741432 l -220.978149 27.213709 l -227.479614 27.213709 l -227.479614 24.248959 l -229.041824 26.737270 231.511002 27.954782 234.988937 27.954782 c -237.760742 27.954782 240.028625 27.001907 241.792587 25.042873 c -243.606537 23.083838 244.513977 20.436565 244.513977 16.995380 c -f -n -Q - -endstream -endobj - -4 0 obj - 8970 -endobj - -5 0 obj - << /Filter [ /FlateDecode ] - /ColorSpace /DeviceGray - /Width 522 - /Length 6 0 R - /Height 134 - /BitsPerComponent 8 - /Subtype /Image - /Type /XObject - >> -stream -x_T6 1twH H-"ݭ}׽֞ <=\Zk{^9bX[, p>:T*700ɍMMMMLrL - !Q@eGjCϗHrSsKk;'W7wO/o___o/Ow77'G{[kKsSc#C}t*J$bXut Ḽlݽ:x̹/$'?w6))!.6:DhpLzHb}# k;KS}?D}O@Hx3ɗJ '7yQQqqIii벲WEE/?yqڥgc"Bz8Y1\R [ p6j @hpĩ3)d>*(*}˷Ɩή޾ޞ榆ߕ<κ{-l\d\OGgρ[ą[22z[AUK&7u JLEهo -=FF''g恅ŅŅٙ-5?.˺sL.Kykj}Vg?D:2c+g >U7u[XZY[R(m -xT(676Vf&dž:k*^?˹XvH !2p:U5TUzbD"tnuFAUK:T>RoܷCS [ۼ]qq@MH`&L%VۊwOIn2Փ[Yo-i677GM"8,[٠@Py]6Wd'w5FǺFv~AvRX5If֫Bic '09" iGĭw=Jj0j5\L!Yq@ W#TH \L+~|J?$#J ' T߿9{ ZM~< mnR /vj'73U>^d%)A$ׂN0mJFFDPsfjbd8XDpt,@=(ܕt@a7.>coEQ?YnN0\~ n\*p*4Pseٗb('~nW_mlmmkiz7KcsK7nݾ}֭1C3s N((ͳ˧Z`x;]S)mci֝[1Gm ԑ"X*r;v*-#׺֖O/ŇzIOjxaϵM-mBY\ Yy%iI4:p84cp\ZFAɧ,˧C0:,="^Is6A=|K]S P]^{l'Bkx}:ډZ #% F~bHWuÔO+4{N`wɛCS333SC?k=SeenǸkV;*=cIGmڪ_?zޘJ.H~XR^YU3\'z*)wbݵs`fTJ?7uLP3=-__L -q? F"3c 7vPUyt%#s6:HNA]D=?}q'GzZros1ۓ4-_{EG]zHwçwt5\A$ 8 NY2E <)l̂^ލ9P9o{Gg6h_Z~zk'5E0Xj^chCHHomY`'lz"ߵ]d@`_T U`'/?zS54s\_ZD[yolvTQЅD A<%f|W3<J]>6eU dhp㴄wkz6 -|e~N&sd"Gu@ЏtFe~{@`E 8 Qn ~>hc"]kS{׷hb`5V>O^\M/n^Z[][]]}=Rh4chJU% ζbm~ g]˩ܺ`}S8Cn#ʵޏY 2J-nb~ʼns49` i럘QR7:Ơܢ Uܾ{} "}@by*GHzkuaޙ JZ՚|Q - -2q# -ّuX~z9$J*3gMV6Ԁ.ha=H?)m0<8 tсw/^~>N6x!}hja-Vh -K7)ksuEu-DŲ8OI_kUwCU]Z >S]O@Fbcq2 WSJU]>AtUd͋W?ջo3Cs>u"iV"ߨю'ia x 1EtJXjk$( Y `h}~*ktÏMޖ&z G3D`<#'?>$XiAow6>7thWC] - -(׆@ B/,l(UyǏ/H /9%T9v@ |ǔ/}ӫ\haugkQnRJU_7 -b/24QW5³\3:#8P,<,G̼XYj)M|SJLB;fqZd/}u8bʞɆ Odp4\%SA)_s}c\wmReGr*f OJZ_Q6Xy+؊ǠcR;j HË`{K:Z8wIYeɋӚy``C[0f˞SM`rsl*dP,Ccs; 4 AgH# 6y,/ 5bS8% 噉yhTfrsi[sVd4fBIō PF4m\'tAN؀Na OTb ȃ.'$p96@eƗB 큲ʪ1O8 PYBcZ1xqCKDٸ @Jť`$@&'av" 8H6S-ٰ&1oy["GҸ̠̪gTL,ql/x븝) lCO@U"ƖP -:D 'd6? %l,MO.o@ ;ϊ>~ |t^Ojpߡ "Q, _ԌCw\P6!Яa~dE*̽"iH' 6ór{kKHO@ -HJ`%7ΰX[Y\XL\ څ1]kj7f@:.XpFAS -'\ $_/lB4%)#H\A*%P@ ScC/9"BPHrK\n soߚ:7AZ;$淖 :A3sPѰŒkF,%^y64ѕɩY,`bkb&*Q+})[&((oO ,K*AH JwW}LSVfEzV=sWct܁QYrNY`(lT?ZlUn\yPn -J 4K<;Hd'wsQZ\dxĩ䌢ogVg[p ghvMխV\[3OFXИKߛ*˞7`zՂ:jY7&rfU^o<7;'1:W?FIÅVSE'9rh&yl@bs$Fvm{ݛ'WS.\HcRFqs0C.)>uT&AG^UFqBpF -ؚi-L:Eg} ~Р7r5 Ҝ -$Rw?lP<>72IU@wφYXx\,zŷ1'9w/ ;}ރAqBҟmċ9F++*E:%߸ǵhIxRNG;|<'hZ<@ Nj| 7Ͽx*{o?~173.Ƴ uҐzَpwS->,7( g+z^59"t*9}@Z*@,ـnhY+ҎQEGS;fide>Lsq -sA#Abڅg~/ҏOzX"|sEq/cD#82TDŽ]O;cP#qB!L}yv31쨻]X3k;@]qN dKS&H7;lWSd0Pq/eV!?|_^+g0$yؚ MmCdmW@esb:%GKb/POWG+78VPݿB 9<(A>aW6S)$mdjr.6ɂEIW HL/ `)H#%nQwt:ܯo"eGDF;Lpx`E`R=r"̂U2 !uc#ez@zea^TDffB 9;dY5lNF 3 ;l.ąm(60Wl #:bs>vIPn}x`g>Sw_wLdͩf uF`p:/;8 oj-uҝYؠgeR r׸6lMIg'@j/>c _Na4=k~~J#x~mD9 KJ - itь̤`R"@9uE]+o/!+XYi&fݱg -C$'Cڅ: -~JQ@|{dfkhnH̴kh$S&?\bbyqQ+4&D=s=h2*b߻AN?t?copڜk_B+w\2.E%'Pct/ i3~ZIѷI|Xտ iGAhAǽ>q7ܞ "U岞y _ -Hj nrP7jDAm"`} `hᙢ$$Y g~:a8\p\P6 @N @k|r #䰳_]pQ'Kc}҂pvMD SCoW '[_Zvk8D2S ,S"嵑LHdS7!(G=N4%H1^0mgQ% =~Ib}Ej 6mᢆXBa] Îbe)Ր -[|^$r)_$JSC89jRսc^Nfr:J yF޲ޚO[֤F8hi#dڦ[ӭnrvDޔ\:]ܺ5d@e$`;\ PDҏ;pU )\܊ n(zs1єZ6kJF1w`+Kb#ϤgX-9casqs1G-OKJ(UIJ ;rg)Ÿ9 6pW5BVSk&BP<+ ҳ>vE$AYfF ?Sey=?r[<UAqm/t$%cTcMJ]/a6$iky~d̐ *F7 hq W~ QYJH.A[g,l~k^<ꇟ+<2j+Xi,a=n#لe|V08=?I3}9DdUsMFTt,XҔ1P4J@TwCmP܌GGF =*4d) QǨ4P? n/ ]FJj܄.*'"/VNKpňa߁Qzstf%vO4;1MA[#^!*2m549׳|P398fH-@ W+5~;UkDH$ t^#$1Va. -9,03W4dO.(Wb%lt5(72rtɬ2iMdk 78T ZAc% DG ؘlx\ؘ)`\, lfs.;!(@!&:Iѹ9%@ x  gɂ0:@8nJgL\UC=pB#]f(fJ@VN>%ig}wHdryZA[(W3=~ٷt\`2T}izoT!F {(8<:E(3X~XݠE$xCi`gQAȱٗo "6P5U*([4 B 4: -g15ӆ/,' 12bK R 0"=I%]a'G(AD|3t8t>)ohAEhe6W -BNPUbvSBv-Q&Zps`7x[ 4r1:5 :o(3)\#Xj2N-@p -@KtyagxU}^1b:ˮBJ#(+3܅ } c(?êy޷ .^<.7`fIim}{m$F9%OP 1DC.[݉L~ b"2y5%0~%(@ ;P۠L`J 7. nTkB8X,-(̎ h@@spΎb ֠Cb,rJ 9A3:t͜bR=}W;49BXCY#)E/&'2.xCfZ X0"2}9$74~`MpE t膒F6,8wyTj#0')PE< pW`La*FP8`[ hT@'0wl,V9qY8`ۡ8[j71'_f-J-"/(lZ\bJb移a‡EF ă -a Z)B^'V_P "7/oYWDɉ^-3䰍mAIKξ|\>,,+%/İ(bt@{(jl?!Y昢5Z3*,@\N^wߗa??^ v!aS#vx ґ=#W6}XTO+˚0:B -990HL5RVc2F Rˠ|/PB&f)8?Ay%}aEbCO ~ \NE&!tS~,SِC,t#i$FU 9"(3hmVyp@͞q"R='32KPxEtA:!'`'*4$yX"ڜKc{Cbb(Sk9*YT*FiPx%gڱ#O6',1D"2j -5 hAkV-,$Hdq$]a6 $]3*$DlPh >Q,$Ú'di=vQn_A <`1qhXt#3}'`= Z0 'luX W *jA$72518%,\'^N 'h ] wm@p;o߷KNo5\z¤cc[ 4KLДꦜj~zJ# ^1:<K&qO!qR,}GdzAJX`9 BSb ,xQp$g5:#Y؅*~*t3KQ¬ G -}A(m2ʕ8w][F(: EHbԚ;D:^v&`& Ll=Bg/U!e u= -k?0eTaG2p^2vI: :cksbYS2*B({']1"48x=X#qDCy_ ;N"W£aFGHzc6;M6\$FQ^@,cqjK&uBP=Π@1Ha  @@Ybrm\NFڛ"'AvHbTQDǜx#G_E17G;rJ0%Sk[~ݗ^cʏ23TkB䝷tz QE Ggw"JקJwS}+sOjG1@&a<2S}HA5$2Sw 0BAIÉN0V'T#e} [`0 -|\Π6 -  И` dT /;  'Z^^ s6UXg~Ċ%Zcd퇁5̝')!˪"Mȵ7I;ce@ g^vaQ^pГn oF)P~ᄭ%}ܑ}sZ&H;kVֳ*Zd!#hb6{ݍyBWؙ%lzH߆N=VYԺ*Ylz39F/^),2\,LDɁ+JdrM.v `F=q *IGX*2.pk2D,ё:$fbg@ ǀ/CJ"]3t}N - b-o[cC}}==$UO #e{{ur=:t/9,naǓq)Fr'O{XeÖcЙO;o p: Ų\h`n@uef~qK # 7#߲ Q PW D ha_CŠ51A`֤B0ГZO*# do "aHj|%9o.׿pÎ^01vHS AkdM_\8H/JeFo#^dQ]O_&}.іiJv ;[m^wvp󋼐yv|4@8~SŁw⏹SYXYRs+zfXYxŀ'HpDŗ!;S"(i+;YoF({^_ dI%%j3(D[5Dh# Gn ْ - PQUGb^ㄊ)7FZ>^K8o*@ Y>()eu܏y)k{Oj`).+KGhv@ktMuݼ{򄿓Xlx}xG:~ZBTXHHXT|ڽU]ߒp)WGjrNE^瞵R=Je?QT}5rP '2HgNGNx{bhͣ874]<rT PB2F9<\}3/;p!7'h&#JMP1pk %lCoFWn-Oս-#-99ꝜCs  - M -Umsk퍙^O9vxd܊^uvDѳGX,BbQ:{uX\V0삲$'G. -_}xzy([Ko 3o\rΣUmХP9XRCJH% ;y%Uk -3W$PhW* gh?hAx> `Mq j`a\1HȭY2(X_s7%e+:p'N'3>tNmo V=񠠬w1~w5@8®.Qc?Z}HJ9"?}_@mKmKm~#<__yY8 la8bge)+EY;Tea -"%xcDƧB{ZjYMlvg{u[͚[܃Ea>;ts9!iA7׷(lt+1'J B^^%Ҁ@ƫ٘7*mh?L1nte>NtT:x)DRSD!=@7h %UBc/0Y$/}^?Bndt`τ͹ˁر#@0 5 P2k#WD:75& d:wC,//Je!3( -}ΩE Gta7J&6HLo:PҪ²1% d%.hQ`T\ZO.<sbn ̀0 ZK/':G1%"!F#4^C?޵O=*BVnLv}J»WMKo$hyevblj%@NP7vc߶a< ~tQpD<~fWabӾS GA)nJW;X.@731F O`vym?-4m-i5ֈJf>Yc䈬@N7ZJp{>u 5^qe e;VZHjq#yWL(D_[K4qe1wZj*hی8ómF l -CWUH%` N>B*P -S aPӤ4(A#u7^VVQ}^6!o\LYIVMyZEC۠ Oz& R! 'JnE3b}o3ډ0:$z17}F䇁JK`e8=o"lE\}=V\ix~`CJn%8)ሎ]йw̓3mtQ@\ϮGkT!$5sx0A$1peUSγ?N&ѕ֎cN4AkhU\=06=7,M 6Uzv҇ -zSIk|q6'jZKLtl;29e4> RM4UgIZdyS__Wq;0O|QQW߀T>Ҩު\=-/0U - j[p[{a턟m555},*6VA$lZ泷:{z{{:?-H3LQgյ'x7 !{;[{`7 Zh@pܵGEť%%/ 3/jɕ#3sx7[34?=%:L e1s -aSn?@ҨyٷROEZ%/Ϡ'kttԉ w+m&Z{;%{̋VfdPA0ƛX4 - h DO)q'B,5X?,& gbNxؚHb}cK;GGG; /30q 8E!/^ptLX[iD7dmwںG%p19|R\TQW[3+@$-<O$Knk#X9$֪hhH䩤Ɉ\ҩУn ~gPfVYl>^.6Hc*ũo`hdllld$7<@xM]^|@/V 7rd.DG CYr'Y;S>,i3oXDGDH=dYV=Ͼܒڊtʠe<³j҄[rceZ%dRd8"TTXNj=0e/D"r-+7@Yw Db@s7i@MFmZ0 -&Qn)XMCгV}1s\ ٨\z$ ,3ӈAD럦!Hm/Ʃ[4qF\W,B7higgsiJQ0kGP gA7`b\jl]=u$fKph@K"@Ś?̠DgWw!^20/jb.P=ӎrsi"7%i! Л@M: EH[ `!*b+M =(ҧЦh<\ - RȠ%MVG:ވx?RPJ0A7` @;;۠WY珻9;ğs'ݒ ܎T<@Lpء&ώtה>LpH.yx>`m FfBknm) B`M09Q]s96pd!2t}$j@d/²iCx4 VkK E. -t{Uq}=?G襢m$ ~͍Օ兹Oŏo_p:Ep?z{y]eMc[щUz5p X[[]YZTSÛɧB},iC z:C+ -4؞Qs+rZs`hdtlbbrjzrbb|ltdh{og[s}uէ%n% q1LSEGK1Yr5?$"6)ʭ{ٹE%ߗW~ohllhRYmgy9w_JN >Hz͙C9ZP(?s+;'woQg/^q;~VǹdgϸsjsC+T!GߞG4̀(}M_rc3+[{'Wo_'#cbbcOF8|,T}fIG2L;Z Гr+7 @V66t<ё[[Y>=]:"yH@J[DQ0[8}PG/ӓtuu@φXv2#1;ON# Y_؞.I\!wI4^hp8"=_ 92J[R8,tL<E@\CBgrMp3(7g{ާt7;$ 1r{X=".n5v&KoG{X -W 0 -LRZ&װ݌B +c-e N2Z8<"H#5%C 1p {}DwV>䤝w132""U$P(@, Mm]]t!1x=>A -Yi<`fl$7407744곅O`XL\q=_&͍UXƦbcuӍCJC$5v -clq] 6(GRZ\Pl)Rloөw8quV!%!5HWZcjySGF"wo OռSK[$/qPqqf +7-)PKpҝg-?V60f47΀' [% ?Os`|viuC r@'pU`8=1[uC`js̍Gu}C +[L,Dk̐/ 秆zZ><d{x"WplozG&fTf -兙]U EzтOHws~Z}`xtbjfnnaqqq /s/%9[ZHGċ3|󩪺pݫ2^8fKG9D"T_nnv2|<+*~UC7/''Dzؚ(0LM,\< =x셋 ؏ ;hkalM߀X#302c]=<&||<ݜl,͌/ ~!D5]]kddlbjbbblldh'+8ALe> -stream -x͎FaLvmc -/۹\/EO)J,gY| -)"e_.,˲㏆~jO?Qm\.K]weYwrLJ~믿_W*zA4e_T˗W[*QE/jC'_Z:37re6? |SґW㊫7 2H4'iAz,|@O=˳p.EO8TrCSӇFk| -q^/˲ ۹z+_|_PbMA)jA5j`6cgNS˲e - 0h+B;d8X@Y5WOnOeY. - B\>wǑ.j9$Fc8x"㧉7Bg-5ay~7qwG;̕Beu(="Gk2U5 T7\T^VGa;/Ç@)GRAC!lGiDy7Gq_ }uykKoLypUiG+zT3ZS5_%{g_0P7d^k b}vx'G~q4"8h J&xl M{`yY],^³}ySċ %s,8:|H˻լ`Hڌ'ʹz,)=sn D|MM=P_zA;wө{<| L@B5 *Hgp6\'^k(75 !cgU:C TPEnF =ԓ@PvN{ƉM>3S -6W|_ky @ 6X5.U{0P5AH4ĎGSiQ867e -jPA'#3jcZuy巂j^kz g8p5=V%Da4'a6 9@vK~齩G$ǫXS^4@e9j@ź;|ʣ%T=4Ah줂 9$@=vs;W{pz;Gu|.J;p6'|Ȋٴ6&HPkN+ Vx=~p|47gso p"GkjY9ZM{@@]X -@ғ0U5[?/㧘o;O(8A4>&|au'Oq;OET9d 0[eV SI=D?'1jpU!m ~gêwr>nm9 D IyP#p^/[ BVú {f,ח}ƃٹl?< -``Y^}2׊1`bZcEl6[a1c{T|S:?%͋C^Ŝm6^A`ue_M|fqOa%$ GR *&\+=+ _ߠW/oͶ=PrQ+W>Vp@?}o`Bm 0γxSKj'Gl6<4*ٲO##̈́c4Aڬ9촊 M ' O& ꫯί= rapBrM :ڋǯV^ &fzmo>ʑgm`7*3l*ڦF{KZ^JT̙b @ςZOk䔖[KE -N T$ 9A4A5 až蘭zM=kR_97Uܼ_,w='mZVM<쁊oř-3` >;O]mϢņ}-SQ{mI9i_/WKZ_ߡgW3A! ^Kf/%j5^j=hiaj|6 ̈́N^cD\/jgg>oX*az?AXiYIR:x)P>dlR(~Zڀqoѽ =JC1wX+"`>}pk`S9+H]gў<xլcFk>I@jjAf֦u&1XP4 15j5n$[{3= 83{~u*[@+Yu7y Tucx{Pe۶ݜԵc<{2JU͍S] ->Ʒ=Td3^W=o7*\ -qN$3=^nd&LPmAlawlvx|U4 ah!B=n02!Af uL 0FdEzjjC'H9A{ӽ`b65y|o =㣵 B'veYfx 뽉gDu|WkfM0V VZ?MFl'տxzsg3nϑŃffrB66v1 T6?Q]=X*s OuQف QgKNC6s}FVY65A8Є1fT$`/OM >meGއpgT]6NE@@*Wwׄ:gSt` e~gt1 Ԧvk').b/T"0ŭ ǘ:N& Ԫ*rS;0PYsu -Sqp揦ӎfBE{ڣg.AdI~[@0P3fMsWI{lSu -S<j~o:g͢G[$ 9O$LɯN1}igY@`} fYf*+z@8P=ӳwy ?8}4N84]vA8;jSgO,kW\JLGII@ȕkG'W~}+Yf*+vH}:U=Nw8m5>:N2$3ΏlzU; ަ~jҟedLҙФtaCVA* TЂ5L{ 0V'tb*Ua0PpID֘u\m5B0IE2^uA᭲DXȑfB΄&Vn|AکȧfA* TeͣK3PA:?!|D=溂`" -jos&T9h3W,2)rM,"P`E½ ΚQ56 M` M  ȡin̠քISO6͐fZsm#VȘ@ wnmTa|k5LA5a d0yHA½Ռl˶5 *2 4YJydpRLgmf]9⬠\EfKU<X5曜Ep{wDM{Ȧ@ YPSTT3h5j @E_gŽs{3۶] -&'|/dx7V|W!,zyy_Wt&RV3q\*ida۶ˮVm֬f{Y'5ږG} -oԆNz:BLT*z@0PA ʂ*2[5U; T"  vhWSH=-cͬEFrM " ƀ $z^g>I -ƀdRMeg 5eP*O)ME AA2[<0P_޾I'' !/BsSSؽu~̵Ko92^i+eqS5c:mZIa滌 PLUm ֩:4qWS51fgrѡ̣ oњxFUPF"/1MuEmhG#fm|urkfamgϣzUmnxm,ˆIQ{ڪU7;Tk"#_cMwȔ <A֬Cf"7ǝ yOSsΚњo,7Gv.+_'.bo{Z]9Ax#~Nl5 7o{:yscbm;||{⍴եN3*6ub'RH3aO@ P 5h@Ig$ZAp,B65&?ߢ0-\:jj6i -f -BDM*& \$;>#MK 09 ~xLޠ2-{x?B m |L9eՑf+=^N׽e]W`"S<Bcga -cz@PHs@f͢!Qk==!'?VQ0 ceE0(`@@³OfP.n$"L4ZcZOkbxTfT{(MC Χ`"'SL/Ap7Mo.<&$'pfJ8 恁 -^w*@zj۶˿s~kz5P6aW;lJ{7Nji ǫ/\lt֌WpӤ66CB5wZGG{l {PYr#UǓE>ps@4AFm"PTJ'xNrfןSn@⤢] -0Pa\u]eLH ^u?<;/;ͺz(׫Ga*k3vZGp^/zkfc:\[<\ -YbἶT&٪xwFS7XDdռ zD`@AdP)>P8xR՗̞nTh6cg^MnRwA+9E>o 8 -u TxLJ,DͬF\QS'SȭX_RTɎbIvAo@eq* B΄Y:Ah줂71 f={9NZ֓j},X}mUsYd3,Hio|\ sиU7|5`,QYC.S ǐiI@ ˰iA5#̈́L'M*zD*0PE3#xzI #~{pF>c.0S5Vb믹oYEU$xR*ھR[Jir*H. 8ދcD=6A@B88ʍ?؞q lXIPk|&<-fل,kM**SCњSfjҟ7*\aoF۶]^~6^`y"k05PA@½\Њ̾|O?+\ic8"krDjl)T -b<~]=u֧pKh}[7l A& c9x>TsL;Mi4A4j=˿a)<5 q}T^SԪmOlcFWi RE|vڜbfÓם9dcnRahԛk._Yd$5p}xMޚQ<8]iV6hI[0jLQoM#jVzi,(3Ud7 S7@Y72{F}#?SUqSm$lS,a\mN7 S٘d,P1}G(uk~$RA8ͨ_m硂=T>Md#] Si%@Pͨ͛95&Uz=TAЙ,{yģ ..E ymOi ػ;lvw\̽GTY{ V)4g\&2zɄT3})=)PU`Zdy.,x{jcT|AǶm)ǖ%޸V1p Q^ Hpu7x 3 M: 0PtZPjnCcyglZ{68rNM/./ո'#i7m^HAT!pkϖDM8XWktNqPGYU m.сLNUjْ" 4d:d&ȡ9Ah朜^|pRS`;i^U6PQCpdYϒD%#S7`HG;bQƃ0U9{` a@Bi F:3jK$^ӆAdG۶]^n-wMp ~v&T}(> a@q>X Ϩ ` h0 B5v?M/ѣL8`@̟ӻñLR `XCjIᤃy(iUrZ EX/aV&bi Sbf$ymmjR:x_7+ryAjI3qa- MԚ B&QppZAHɀ02Ir݋Mk: 63̈́3`T V]\ -Ud;aӪ#* PNq j'UAtP=J?LOm.Re U֕PΚN*pzNemZgYmPA@lZAwG~w>Wg V4PAH38ڑޜ'pz,/vVN;:-ZӝVf >#1^quOM T%@j='xO aM\ z&5$4Apĝfa44?^$<* -aohhM Toa a4Cjش9_2y'lR#MA< FꪹgI 79jZS\1ƀ rش0fj臁Amaʓaʥ`.n=ZE! *[…ڟ<Z3mMz+jc_78 -'gjpQA|[7KTwK9̵q'0kc 9Z'͊@Z= -Ud T1XAyThMUdqAϮ, QzZZOHd9{|z3[Nb h:eZ$ lO!ӂ2@ _1p#%5Z@[ *[`j5ɭ=Vj=Nܰ9ԃu+J''uuʰ=@@BE'a - hh1l՘a)VwB 1j*'DT 'ۖȴ3[|J\t*WMYN۶]un;.T 6y@/pp?>u kYf+ ֻHWH[gNRz@NLPޅRx  0XAaTVc4 @Ad*z@@hд1<954y0geYJ&\R0&n?kt]0[Us|zdǪA@h{z˂[ LSӋIbE (ȻtMA\=prX#A8ڙ\@%  5Ԛ - @x*3Pcz(6Գ*𰼡rSqf7Ҋ.6S廝թN[tX1 M^n-9& 2#?j=Ys5*32PaְiՃ[!O9 @,W+[(F U]<ߢ<#66y ROba"򸮆u!d`j])9:nTS _ERmvdxFm./\BgmZG{ Tњ\4[Sse SOc -Be.n^^r@W 3|e)Sf_{k-W?ϫqwME~h& |lon SuQ9ٓ *y=l'{1 m٨U^r&ZMQ5c$ ҟ@m65ϨcQ@B]I'qc& a6n MeExQMu>/%W/Wkpg^'9rhVul6[ӡfbWSz^!3%+'׸E y ~:|0@k{8pbp!S@@ pUfi: M5eg* ʹsδ6Ldýyx)%^^$aض1d^:qNƫ -(^]"Y~:՛Rorkע9T\˙xf[5B 5yǶm'O*rbu}^{ka'7Q0NsjiC{1* rBشM5o!=^epwNq (5hf{d? Bz4X+{{5sCáeYώ}5cB@8cihf|0=cczDͲ,gÞ\ A"M#Ǫ+D\eY= Ad:ADzPIEj'T#7փ,%B.h*3j=4aHEB35O jn۶˲,˗Άu87~* 3)3U{6#GeY/ߐP׽RY{2$p#A:ahvg˲,_4 9:x@[5N# eA{,,Oik,@jO2!h>foYA@62~e.˲I* xUa)5A `@nc²,*@ a.T/V_s ڴNVcOj,'70zu -7,rOjS/5 4jW_O)7*l֦}h Ҳ, gEs'hj?o뽣R4,:+?CZei/7qG4 1^G]#wx#} ضmu/Xe9/j{샠iSqyVzaYedS+z@4T5o.˲,#E"c]eY>_~?mO·yyYe,5=ҙ3_T¯,˲z' s8˲,k|~' B,9%=W"LEUGʟA[,rw:Oaޣt۶˛_U,Ȼ=|Z=j,r TՌ4'Tԋy/2s3~,˲ܴ}~i;JNz@T5&\/˲,w \oE\TfkEjh\W*iA>\ eY1?0~.͞4 V$(̈́& ;PmeYeyͥ\+:AڬY7{j\3SX,FN L塄=u^t&TtuwXey/׿ֿ6Se"ajhM=N,˲<,.i&Dz Td6vRA@B=UzY: :3j4AVcC]eYwyfG"= = -KeY|zp/o |;ʖeYxuݶR}i7 .T1+eY-xQ?]i0²,~?mU[_ -1ƀ0]5߽>/,˲C?6p*.@}PJ|Zv@8ǶmeYv.ԹsE>̗W߿?aYѲ,TߟAEr^eY`]eYP@ -endstream -endobj - -8 0 obj - 14846 -endobj - -9 0 obj - << /ExtGState << /E1 << /SMask << /Type /Mask - /G 1 0 R - /S /Alpha - >> - /Type /ExtGState - >> >> - /XObject << /X2 3 0 R - /X1 7 0 R - >> - >> -endobj - -10 0 obj - << /Length 11 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -261.000000 0.000000 -0.000000 67.000000 0.000000 0.000000 cm -/X1 Do -Q -q -/E1 gs -/X2 Do -Q - -endstream -endobj - -11 0 obj - 118 -endobj - -12 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 261.000000 67.000000 ] - /Resources 9 0 R - /Contents 10 0 R - /Parent 13 0 R - >> -endobj - -13 0 obj - << /Kids [ 12 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -14 0 obj - << /Type /Catalog - /Pages 13 0 R - >> -endobj - -xref -0 15 -0000000000 65535 f -0000000010 00000 n -0000000493 00000 n -0000000515 00000 n -0000009734 00000 n -0000009757 00000 n -0000031385 00000 n -0000031409 00000 n -0000046488 00000 n -0000046512 00000 n -0000046851 00000 n -0000047027 00000 n -0000047050 00000 n -0000047227 00000 n -0000047303 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 14 0 R - /Size 15 ->> -startxref -47364 -%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/Contents.json index e26db2fac..ff1493e96 100644 --- a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/Contents.json +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "logotypeFull1.large.pdf", + "filename" : "logo.large.pdf", "idiom" : "universal" } ], diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/logo.large.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/logo.large.pdf new file mode 100644 index 000000000..63f9239a8 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/logo.large.pdf @@ -0,0 +1,1286 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 968.000000 252.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.000000 4.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 244.000000 m +960.000061 244.000000 l +960.000061 0.000000 l +0.000000 0.000000 l +0.000000 244.000000 l +h +f +n +Q + +endstream +endobj + +2 0 obj + 237 +endobj + +3 0 obj + << /Length 4 0 R + /Range [ 0.000000 1.000000 0.000000 1.000000 0.000000 1.000000 ] + /Domain [ 0.000000 1.000000 ] + /FunctionType 4 + >> +stream +{ 0.388235 exch 0.392157 exch 1.000000 exch dup 0.000000 gt { exch pop exch pop exch pop dup 0.000000 sub -0.050980 mul 0.388235 add exch dup 0.000000 sub -0.164706 mul 0.392157 add exch dup 0.000000 sub -0.200000 mul 1.000000 add exch } if dup 1.000000 gt { exch pop exch pop exch pop 0.337255 exch 0.227451 exch 0.800000 exch } if pop } +endstream +endobj + +4 0 obj + 339 +endobj + +5 0 obj + << /Type /XObject + /Length 6 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << /Pattern << /P1 << /Matrix [ 0.000000 -242.733612 242.733612 0.000000 -238.733612 247.147812 ] + /Shading << /Coords [ 0.000000 0.000000 1.000000 0.000000 ] + /ColorSpace /DeviceRGB + /Function 3 0 R + /Domain [ 0.000000 1.000000 ] + /ShadingType 2 + /Extend [ true true ] + >> + /PatternType 2 + /Type /Pattern + >> >> >> + /BBox [ 0.000000 0.000000 968.000000 252.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.000000 2.211426 cm +/Pattern cs +/P1 scn +223.730621 191.199173 m +220.269104 217.264771 197.839127 237.841110 171.284851 241.799530 c +166.791870 242.471710 149.813065 244.936386 110.478516 244.936386 c +110.183769 244.936386 l +70.812729 244.936386 62.378529 242.471710 57.885239 241.799530 c +32.030567 237.915787 8.459440 219.468048 2.713966 193.066116 c +-0.011457 180.070541 -0.306097 165.655823 0.209523 152.436340 c +0.946121 133.465759 1.093443 114.570023 2.787622 95.673965 c +3.966180 83.126495 5.991842 70.690964 8.901408 58.442535 c +14.352245 35.812286 36.376640 16.990784 57.959160 9.335617 c +81.051300 1.344009 105.911919 -0.000015 129.703659 5.489471 c +132.318665 6.124054 134.896866 6.833466 137.475067 7.655151 c +143.257462 9.522385 150.033890 11.613541 155.042755 15.273193 c +155.116684 15.310623 155.153488 15.385147 155.190292 15.459976 c +155.227097 15.534515 155.263901 15.609360 155.263901 15.721619 c +155.263901 34.019882 l +155.263901 34.019882 155.263901 34.169266 155.190292 34.243790 c +155.190292 34.318314 155.116684 34.393158 155.042755 34.430573 c +154.969147 34.467682 154.895538 34.505402 154.822235 34.542511 c +154.748016 34.542511 154.674713 34.542511 154.601089 34.542511 c +139.353043 30.845444 123.700752 28.978195 108.047844 29.015610 c +81.051300 29.015610 73.795784 42.011505 71.733467 47.388733 c +70.076012 52.056534 69.008049 56.948853 68.566086 61.877975 c +68.566086 61.952805 68.566078 62.027344 68.602882 62.102188 c +68.602882 62.176712 68.676498 62.251556 68.750420 62.288971 c +68.823723 62.326080 68.897331 62.363495 68.970940 62.400925 c +69.228882 62.400925 l +84.218689 58.741257 99.613655 56.874008 115.045425 56.874008 c +118.765503 56.874008 122.448158 56.874023 126.168236 56.985977 c +141.673615 57.434372 158.026428 58.218628 173.310669 61.243088 c +173.678726 61.317917 174.084183 61.392456 174.415436 61.467285 c +198.502548 66.172516 221.410675 80.885986 223.730621 118.154839 c +223.804840 119.611092 224.025665 133.540283 224.025665 135.033966 c +224.025665 140.225021 225.682816 171.742767 223.767731 191.124039 c +223.730621 191.199173 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 49.436829 165.103180 cm +1.000000 1.000000 1.000000 scn +0.000000 13.742415 m +0.000000 21.360460 6.003528 27.484528 13.443068 27.484528 c +20.882914 27.484528 26.885828 21.323042 26.885828 13.742415 c +26.885828 6.161789 20.882914 0.000004 13.443068 0.000004 c +6.003528 0.000004 0.000000 6.161789 0.000000 13.742415 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 108.917572 102.776794 cm +1.000000 1.000000 1.000000 scn +140.506592 64.230759 m +140.506592 1.045578 l +116.014610 1.045578 l +116.014610 62.363815 l +116.014610 75.284561 110.711319 81.819916 100.067276 81.819916 c +88.318474 81.819916 82.388847 74.014786 82.388847 58.667053 c +82.388847 25.094963 l +58.081230 25.094963 l +58.081230 58.667053 l +58.081230 74.089928 52.225231 81.819916 40.402508 81.819916 c +29.795889 81.819916 24.455156 75.284561 24.455156 62.363815 c +24.455156 1.082703 l +0.000000 1.082703 l +0.000000 64.230759 l +0.000000 77.114693 3.204189 87.384232 9.649378 95.001968 c +16.315704 102.620323 25.044342 106.466766 35.835602 106.466766 c +48.357925 106.466766 57.860092 101.537338 64.121254 91.678780 c +70.235199 81.222137 l +76.348824 91.678780 l +82.609680 101.499916 92.075348 106.466766 104.634178 106.466766 c +115.425430 106.466766 124.154060 102.582596 130.820389 95.001968 c +137.265579 87.384232 140.469772 77.189224 140.469772 64.230759 c +140.506592 64.230759 l +h +224.773392 32.825264 m +229.855560 38.314743 232.250977 45.111115 232.250977 53.401466 c +232.250977 61.691818 229.818756 68.562721 224.773392 73.790886 c +219.912048 79.280365 213.725754 81.894753 206.248169 81.894753 c +198.771530 81.894753 192.621094 79.280365 187.722946 73.790886 c +182.861298 68.562721 180.430618 61.691818 180.430618 53.401466 c +180.430618 45.111115 182.861298 38.239906 187.722946 32.825264 c +192.584290 27.597099 198.771530 24.945297 206.248169 24.945297 c +213.725754 24.945297 219.875244 27.559677 224.773392 32.825264 c +h +232.250977 103.927216 m +256.373688 103.927216 l +256.373688 2.875412 l +232.250977 2.875412 l +232.250977 14.787994 l +224.957428 4.892029 214.866684 0.000015 201.755173 0.000015 c +188.643661 0.000015 178.552017 5.041397 169.602249 15.348358 c +160.799988 25.655014 156.343811 38.389275 156.343811 53.326630 c +156.343811 68.264290 160.836792 80.811768 169.602249 91.118423 c +178.588821 101.425079 189.269653 106.653244 201.755173 106.653244 c +214.240997 106.653244 224.957428 101.798645 232.250977 91.939789 c +232.250977 103.852371 l +232.250977 103.927216 l +h +337.547302 55.305824 m +344.656830 49.816643 348.193237 42.160881 348.009216 32.488808 c +348.009216 22.182152 344.472839 14.078583 337.179291 8.477158 c +329.888794 2.987366 321.083191 0.186798 310.403564 0.186798 c +291.142242 0.186798 278.067230 8.290054 271.141724 24.198761 c +292.065430 36.932709 l +294.859528 28.269096 301.012146 23.787773 310.403564 23.787773 c +319.022095 23.787773 323.294586 26.588638 323.294586 32.451691 c +323.294586 36.708817 317.694061 40.555260 306.278320 43.542610 c +301.969055 44.737854 298.398956 45.970215 295.635529 46.978371 c +291.691223 48.583992 288.341980 50.413811 285.541718 52.654320 c +278.619293 58.143799 275.085999 65.388893 275.085999 74.687393 c +275.085999 84.583061 278.435272 92.462723 285.173676 98.138992 c +292.102234 104.001740 300.530579 106.802917 310.624390 106.802917 c +326.717407 106.802917 338.467468 99.744926 346.165894 85.404739 c +325.613281 73.305664 l +322.632080 80.176872 317.546814 83.612328 310.624390 83.612328 c +303.330841 83.612328 299.794495 80.811768 299.794495 75.322281 c +299.794495 71.064850 305.395020 67.218414 316.810730 64.230759 c +325.613312 62.214447 332.535675 59.189682 337.547302 55.305824 c +337.584137 55.305824 l +337.547302 55.305824 l +h +414.227722 78.907104 m +393.086243 78.907104 l +393.086243 36.858185 l +393.086243 31.816803 394.966400 28.754616 398.536469 27.372581 c +401.152710 26.364426 406.382080 26.177948 414.264496 26.551216 c +414.264496 2.912827 l +398.021179 0.896210 386.237457 2.539566 379.315033 7.954208 c +372.389526 13.182373 369.037201 22.928986 369.037201 36.820763 c +369.037201 78.907104 l +352.793854 78.907104 l +352.793854 103.964630 l +369.037201 103.964630 l +369.037201 124.354202 l +393.159851 132.233719 l +393.159851 103.927216 l +414.301331 103.927216 l +414.301331 78.869682 l +414.264496 78.869682 l +414.227722 78.907104 l +h +491.128937 33.422424 m +495.990265 38.650589 498.419403 45.335320 498.419403 53.438576 c +498.419403 61.542450 495.990265 68.152031 491.128937 73.455032 c +486.227722 78.682892 480.262238 81.334396 472.971741 81.334396 c +465.678192 81.334396 459.712677 78.720314 454.811462 73.455032 c +450.137238 67.965553 447.705017 61.355354 447.705017 53.438576 c +447.705017 45.522110 450.137238 38.911903 454.811462 33.422424 c +459.675873 28.194565 465.678192 25.543068 472.971741 25.543068 c +480.262238 25.543068 486.227722 28.156837 491.128937 33.422424 c +h +437.838196 15.460297 m +428.296478 25.766960 423.619141 38.277023 423.619141 53.438576 c +423.619141 68.600441 428.296478 80.923401 437.838196 91.230370 c +447.373779 101.537338 459.160614 106.765503 472.971741 106.765503 c +486.779785 106.765503 498.606476 101.537338 508.108337 91.230370 c +517.610168 80.923401 522.508362 68.189453 522.508362 53.438576 c +522.508362 38.688011 517.610168 25.766960 508.108337 15.460297 c +498.566620 5.153336 486.966888 0.112274 472.971741 0.112274 c +458.976593 0.112274 447.336975 5.153336 437.838196 15.460297 c +h +603.163696 32.862373 m +608.025024 38.351860 610.457275 45.148232 610.457275 53.438576 c +610.457275 61.728928 608.025024 68.600441 603.163696 73.828300 c +598.305420 79.318092 592.119080 81.931870 584.638489 81.931870 c +577.163940 81.931870 570.974548 79.318092 565.932251 73.828300 c +561.067871 68.600441 558.638733 61.728928 558.638733 53.438576 c +558.638733 45.148232 561.067871 38.277023 565.932251 32.862373 c +571.011353 27.634209 577.347961 24.983017 584.638489 24.983017 c +591.935059 24.983017 598.268616 27.597099 603.163696 32.862373 c +h +610.457275 144.407715 m +634.579895 144.407715 l +634.579895 2.912827 l +610.457275 2.912827 l +610.457275 14.825424 l +603.350830 4.929443 593.256958 0.037430 580.148254 0.037430 c +567.036438 0.037430 556.798462 5.078506 547.772034 15.385773 c +538.969421 25.692429 534.515991 38.426392 534.515991 53.364052 c +534.515991 68.301407 539.006226 80.848877 547.772034 91.155533 c +556.721802 101.462807 567.585449 106.690666 580.148254 106.690666 c +592.704895 106.690666 603.350830 101.836067 610.457275 91.977203 c +610.457275 144.370377 l +610.457275 144.407715 l +h +719.326782 33.534683 m +724.188110 38.762848 726.620300 45.446960 726.620300 53.550835 c +726.620300 61.654091 724.188110 68.264290 719.326782 73.566986 c +714.465454 78.794846 708.499878 81.446350 701.169556 81.446350 c +693.842285 81.446350 687.913635 78.832268 683.012390 73.566986 c +678.335083 68.077202 675.902893 61.467613 675.902893 53.550835 c +675.902893 45.633751 678.335083 39.024162 683.012390 33.534683 c +687.876831 28.306213 693.879089 25.655014 701.169556 25.655014 c +708.463074 25.655014 714.428650 28.269096 719.326782 33.534683 c +h +666.032959 15.572266 m +656.534180 25.879227 651.817017 38.389275 651.817017 53.550835 c +651.817017 68.712090 656.497375 81.035660 666.032959 91.342316 c +675.571594 101.649284 687.358459 106.877449 701.169556 106.877449 c +714.980652 106.877449 726.804321 101.649284 736.306152 91.342316 c +745.841736 81.035660 750.706177 68.301399 750.706177 53.550835 c +750.706177 38.799957 745.841736 25.879227 736.306152 15.572266 c +726.767517 5.265610 715.164673 0.223923 701.169556 0.223923 c +687.174377 0.223923 675.534790 5.265610 666.032959 15.572266 c +h +855.082458 65.089851 m +855.082458 3.025085 l +830.959839 3.025085 l +830.959839 61.840881 l +830.959839 68.525604 829.300476 73.566986 825.877625 77.375702 c +822.709290 80.811768 818.216003 82.604172 812.431458 82.604172 c +798.807434 82.604172 791.882019 74.313820 791.882019 57.546638 c +791.882019 2.987350 l +767.759216 2.987350 l +767.759216 103.964630 l +791.882019 103.964630 l +791.882019 92.612091 l +797.663452 102.097687 806.870789 106.765503 819.724976 106.765503 c +829.999756 106.765503 838.434265 103.142960 844.991699 95.674278 c +851.733154 88.205605 855.082458 78.085426 855.082458 64.977905 c +f +n +Q + +endstream +endobj + +6 0 obj + 10350 +endobj + +7 0 obj + << /Type /XObject + /Subtype /Image + /BitsPerComponent 8 + /Length 8 0 R + /Height 502 + /Width 1936 + /ColorSpace /DeviceGray + /Filter [ /FlateDecode ] + >> +stream +x읅CJ*8ť8").i{LMvei{}N&9g? +`XzZS)fYi„  M5Ze6„레) GٗĂ Y%␱$UUr#2 &ZiހfJY-"B +ۡHQ-(fJ̊'O9#BKJӓPȏi{H!LPAW*FsWA%5 . +_PP(j=[^^YQYYUUSJ}]u dn+f`8HF*sv]ׯI_ռ:*e]XP %{Zg%&*(eDnÄ &>z_RY[ -jrZhk_e&>{(؄  U&ft4q gfC-/kQh[ZZZ[;::vv}z=߿uJ 2~-t\ggGG{[[kk Fe_TWWUŴFAQbAvVmfQ̱#^6&L0wqd̂M-XͫҼ1I"LOD0N;cEO:ㅘ/9SU]ًE*Nm_q`pphhxdddtttlt||B199959555-̰,YMSS91ёᡡA)}}{޽z5MGhz\nsjrr29.fJ%K59L0ayIT|jnwCc3 fm!ff&GkKS= 0Ac%>4T1&N1".zYZ~]4p^0wl|eB/,,...---/- + +kk +ce +⮮r +/ ..,.,,RQm(~@4;G7\"~tX(TqBXXĥUյuov{Fc_X+5+pk[w`wwwbe:sz!NvwvIp[EP^ZBMϣѢ11Q=J;c;7_*J^j"#=5% 'vlgZ'?aL„ ƃ<QR94{xj>I6eWEa}ԏrR3XUQleU5Bk[gwOoZxj,*xeE_xї΃U('''ˋKbf+Y2_XIL3R&}V=Ծh-(i444f,@oϻΎV+KK +E+;&JLф & E&5_?-#HW]I ]aA2.~PqZFVN^!ʚں}C3X˫m ߽},Z/EwLZqOL`S>f,KLjd 1$3|$1)Rٗ\iQXӢņƀF9c;PytdhPێ|u`*rltT'OJ &LGĤV4M]|Wǫ줘%L=z̀qTL|41޴E OV6D 4h_xQ(]t'zA_oVKBY0Pj2kHC$yr\P JZ ljQТvֺռfb~*Z 7;;YXp &LEƦU8o,Y0ׯ/֦zscq?e,:6.19-=+]\QU[]o?6X[;Z6FъZ+- + ?Ԭ6~o3DHTFઊ/hQhhQW1mD1//.Ltu67(-.LуɢU'L0NqUs[ן? cX߿^m5UF< +*aleᦎGgf.~Hq/Fy4(b0ưX¢oo߿4u&%dZ~2 ?HpT?FII-hp,&3zR~&:eadgɏ~0am['7_ Fo>.M q'2f2/*|U]8xiXbq⾠P{_'Yam(|*Uec_Cx$9VʙIЦrVGdF/lm,-`*l{SWSQR$*yBlԳO:\„ cQD\j^UМ03p5B7W`D0$6~42:.>)5#;م嵍-M|%-jsXibƊَo_ʣ +_됅/Dd̵l1u,?PL_~֊˧'h͵Źɑ]M *K +s_''Eq[?Ľϳ &L_aH+$B|u񡿹<792~hĔ<, 17..MaLQm2_ʮ̂ u} p!Zf֚oRWWgG[+ cC}5%9ib$G>Q^밍&LWUǴ/V"߱g_-$:zyjfNAIūΞyb'gbQ\/gp3l]R rLVc`ogsuinz|uuYqFh<;a0a(}}l?nj70 +Ʊ I/rʪ;'0׷wN/ŷXŨ*Ĕ#?|Ύ=޶dzoH?Dߞ*L~\DeQb+|}}yNZ[F%jm|YhhdT2O1L0v?=1э?8<~FP8"2:619=eE훶ޡ|J3T(W +)P 4>C8yp%~F'a_>tsuyT໎UhTS#r0ahL}\64m-h0 +) lm8%3NcS[8h6[!%$l t:m4KV2Vɛ&z߶Wf$8hpM +_c>>A6t_q{ z68E>O@W׷t^\=<eaUbM-sĭع B@"fI$ 嚶aK!`&ˀ8w֗F[j +sFx8Ä ޯΫ[(},W~J3Ba78#oDLƌ/?1b]VX^^NAʆ(dxCvkuGcĤ;TǛ=сƚ"Ƒb<0a~vm͏o&>Wx_ 'e4Cc4&ʘ 4Oᕐ>qe/a_?[~>?9YW$/3%1V9iFya +"'OFDDF<}~?AƏ=վ?™oMmKL/o6]b +X,c~t{i 8pD@o?]_`#/͌w5(HD58L ?~ +1rݛI|ksI"̿ j<WuJF˪.l]o>bEu /N5!\+̸@E`H(x^G`ŽS8طCgEȋӣM R+aصg*瓧_=OJNz !Oc!E;߯v`kC:i8}HWuNaym۾Ók6N+mޖ"3(ֆ_&<9&'Z(TbTq:.1}F>9^ikz@)jPjƑezNbaFDO3NF߆GO0S^ּx?26AyaU|8ߘR;ja4F0KP\cy'J#:p@{ ?,[s-+ +,9LbGcd)(*.)yY2q}q|hL͐ѦaW#c9U'VN.?~ўj Āo˾mmαB@U *D-+EE ymEmٷH?m,͌u4V`"c06VDZQeյ^Ue<}$WW;Ə1Nq#BSjVA٫7og7ϱmXեI-wW_dodд,'h?b#_Ou./JM}VaBG~ 򱲌2.|Y^]mgkcMInj|a(}bH8 mDnrLXˠ?~㶞ݣ돷JcOG0`mb5Vx 6rLݗ-'&Z\l-enFEY0w*Fe,z\)7mo u,|~O@WeC/ڠ &s}W۠S#_䗽j[>8ئgܑ2c.pm-Ca%03Էom:,|1dľ펜'yݵɡw-e/RL5$2WgU_oK!ƩJB,NގʢS:7TGmGDFYʸPƳ+뛛FKsRb#7x82}guX{`|:.)#՝sc%Î:"A֎rկg +Թ`&?Y13y`v[/-|Ѭ#w0Ș o0ܗ<6rOŢ2VcY(c{Dom-M6UƅiϛX +}~K< _uʋ:9^?Wa\e +O㟄Bʜp/:\PQ\epA>^l. .LAp +]cc,gQ'fdeشӝ˫㽵>Cw|ڎ+r:?ML.j8>Wru*RhֆAމ\ϱo;-S_LÝչ/R#E!Ä.o~<eq(7bO)e|2x{{}q1;V]>;3}K;_}yZ3t<=kpra}Fcdq =~Gƪ4B7a|628Ӵ e22BP[8qxcsg>Wx_QqIe[{gN,ئ4a 'G%{UiXqh|Ɩa%` aw0E:\j.IKD!ؠ_,3K+_7wznɰ22N듭|>x oQK+ ?_\"$j"_TԷͮ]\ƫ!DcnIA3iKə ۹F + 斄K1ܠp{Ҽ1 +9UF=O7uiemʸoh|z^,㳋+_}X}֭a?fhDDǧdV5tO,\~Tߪe˓eľ +!@4wՎajoF%ۏGks=շ +9:~SP2nx7<,݃SQƟQƸt}<.I!]Dr>W1~<']Tkpjqkx(].B.VC +8?-^Xv&̇Ui"x<}xu>?VW.V)/\u<-ee'ƌQRt1I 5~c_6jJB>e/N7'ӟ-0>=*_V׷vL/X2Ac>RWG> ?J\߸cɰ|>$yqznImkϨ:O 7;V_oLm=X, Qn鹅!Y>;\@!g$ńrF%ԶtM/oɘGm+md]e}ZVm; +c$;ۉ6YWwlnm)8GL_# Er`6xqn1L }FZ:Q䯟oPȝ>0r<|Ugxzquk39ʙ[W$>|>䱃jƏ#φI$q^Y]{;MեJ \ߝ<[mS Յ}?r G0.kP~nN-{1z mL )Zqxx@+E۳յW+rSbmFYǨ 1CǶR7R-@Ű6%Jʕ kU,C;%hVKCd6PG# +yz SO3 +_ L/_[1CU?ZcƏu6P탉Xޯ_d!긮rqmHX0!eLől1T,0Vb+Y vl[$?:H@!ȏE!'?0㘤ͣJ[&c}.XW`?u}0zuW^!PԱy[z m(CD潘芿Z߾|P +Y^J/u&Sap1&9g]`EUaش|W}ǒ}:ȇrEgc/m]P,xouغ`7bGceCpawR$!vQR +yY)<9ؔ뇗Ikˋ義aOW]_mfڈ}ϕ+w +(8 xjqK;z6r8UO[-n +AruTuFQB+MKdl Eܪ׷'d) ??T-2~L&C̈́>a}o 鹨ŭs) ? O w4*ZG63p L>n_XKKrR⣞1Zf7\a/~|߯}W i8!~m>` F~:WE) zZqAjlX X/OK`l*I /XQ9%}XJ!ؒxG#*`?6qrC~B>\RSI} W1qԁ+5^?菉.;W>l{^o}=ywbqˇUBG>LҹDYLV!Բ&Q`)*Qȇc +3wȝ0Rhmj/C+c RDF=>6D:M~Q\~laS;}ΓR!!.Mpsq'h+t4_/ 3 \mC8gHwcE~zb4 1}M?@a2,>Rs)}ֿwhu<:fAst{ܓ A(N0on:pw8G|Oho> u֗wU_kc1j] X|7cq|> M}||m_?au 0,0GD'W6vϭ^kuLA017b0?@#xFlc%6×o 8b!(䫓N3I"? ӨܲΡkwd}o~8c/L+pϰ/JV}߿W>Aojyev~ľJ͆nHVR M} i@G;]k3c~Df}Lc=y$eGS;>VO:?3_-8>VyW? 孎NL/z3u|>&0o>V]m1Y@#L6~8l[,>%sxvPRph YЇl0Gofp},Hfp->jXƏYlEF~+|y>>;㈘ůWN?w@ DGT6%?-!pi<6k/lq%c M&AB+:^g=Wtk޿ _c?aOb}.ν;ńf,<|$:!=_~괸}|P +-?<VA +q:(Aߝ< Xq:nMU >Fc+"v_nN6,c~$18bsJmZì>܍J,mu=pk;@;f,/KWdam=?HO WXύ/N|ӿ6W[ +s5ǃ>~!-}LV:(87%щEZWn< 50OרP7 #m#fjf޲m_yV[k+]aˆ>>6Jo11DZ?O;| }c~5I^iT|Z^śզy]罏xd+@-Ҏc_LNmۦ P7ֽ'[|z{qL~,o sL+jK/x|`Z}<8a_m"WL +oyZLȆeA*I +].cT>P6'?VXD? dbYO"5˿V]?<1{* s͘a%kD+A9 s/Td1-?xqaUq?7OVჇ2ak8"Xa{F% aW5/AqS(C}<)>"x[ }ܾF+!;1z>Ih˅&40k[GdyQP7"VYm=q TZ@GLl0 qW/9@>ߜ.O4Uc "["yHj-(9uSxɓ'@8Bp`Z} XcQx^Əw\R=Dy VjW%HE*A5܈rEh? U{).ݍZXZFMgI|9)?J(iY?j7 ]_2Pg9ˌ0bG\>l<ۗ4QBDU6CK]}R~H'QRIXRc'19xAl;4賏cL}'D i"#cbb()&1'&& ys4qR>GܳmQ8RGJB$ S&)O)`ȉ.}_mfj5>V?>] 9|TJ.%$&H&&A2KJ" ܳ]\zȋB"9C$A9A~Gƥ5v-n\߭6P? +BQUVm_{ [Oz~$+)6͒Y14HI)i$tz9L 0]Bӧ>%$<32_x"u&ҥ$%clG ؔց=xcVIJEp<}*e^j)E3|rL(2=yu._>= _lkqQNAeeeggdgge}%9왨*FjRS D^-n"Qj4==UKJzO /g֩mK NlBwHpȖ*Ϻ*U CxlouW}:&BƋ|r Wݧb)IɩYٹyEEEj.(΢ TEoA Y2H%`gf!T˗%%2ssr^dI#Dpɘ>xRZ[? b3~ +rV?z%, cX>84dM=4-UABTTlEPZZ(5$B"tԾZ8@WTL)Tb]hJ#hdt=D~v +aɆmO~%/GҟJ͖żETU-n489EtV^AҲʪꚚW^\S]UQ^^0?7EFZ2J'd]i$}b_=K&ؽ |UR!E*/,.)Euuu _,/+)dSM&Cz!B\f,nj(Tf3>k +2 tƒr $l #Tѐ6N<IJZyRB*+J^KGF=h:6‘o_$8>|-(U0RD&m Fy"ZGp}\>N"Vԅd#zLLol|#466׽EEydHi_IK6 &F*8)U&-]p~kj>Be/ rG&I,)y/hz<.~>8~(=C'Ym@a6 8ՙ +yS)=y`G]Y~FRxwA=|(bxMЄmWW;ymMM u5!张$-97F;"‘HᠣVn62-½෵67IF& +*eHC{ ~+1X(>RO.F7,zUL.RԒ\\Iu ݿufsa/ڲij"z<«@ rh\2]5=Q q"={ qnӛ/e.cׯ(i۱d'0Ԕ̊XXQ 5Cd&O'^dRƥE&R𜚚[@jji@_oOێfjU9MnvR*!(N)ؕ@e1ʟL,X2A8# БOu0Ȩx1bX Mo#$LL ͒-+#% F-rCr]|t FڙYC,~̼1\XQ|P.?ܜ.'d3)x^j2TvĀ(eLH/(*MknxT!-E2 K^Nqp9_m}UZ@םg =*']7zȊ6VZ :4%Ug:߽3.J0=9>:LTВkz|wWG2LR8e,i +FP) liUYJΔF;Jm D}lcjtN~}6N)RoZۻvxlbZ*ykˋs#C"tۛ:WŅ4Wd$Jk{@HOdY*䊔y0cw]I*"pbrzV^Qi+rWZqJ-UyUA%giƇ{u5ս,UrҢ!] e9K;'7޸3D2)g"qy<؞4}/]<ٌlM~N~7g}oj+^_ide"%WuFQr{F'gW77wwv07396X[J~.WQ bP ;Bß7JDB?WU=1qqyMݛVt$b-on!޾¬}W[s=* '3 @ 'o7[^cbaWUsG,6pwӫ2da~-^=_$G/7ڴy<ĝ=)noon,-ΣƆ4Ғ(!s?AOHϢpTnlp ؇9U7(H@;zjuyia~fr|XXK7#BKqzX~V:>>Ǵ9#ʩuAe={ccmeyP[2?[4aF!Bυ},Wp`}lF*nIFQ#o'r@Ʀ>̓JZSp*/ܠ=N|RocKaBORArZb,7NKO<y]բ퉸"/#)(\_YB%#'-b]l-N R_SVD3'%'7 ꖉhzvJޡ1J.͒cj5ܘ\__]RaNwyuzPidq:R,rP4CLZX A27i)+NBAšBm"-x +&F\^^H? ,?~َVH;F@ldfۢ_u-6ycsK6f}Ó g7_=}(>n6&w47cR[ _eYMn@@ 1^_~p[M7E,n>gxuI䈶\O75)z/r#lzj)JsC0@D()(()k4BoB7i5c1va+L?{ B/4!mLnܒT}CZ#S'DW9y~zrx&{J%#' \ЙTYA[kGƎmW%Fo"؄L ቙5UOWg.KAҦ퉠Ri9J#Rrᓨ4ǎO(Ծ=⁹V;~9h}P/8NCY*kw="(|~<75O('#91&J]?Yr9gVQU{Dkn졊TSg;_J>Z_^KP@##! =}}̲k?Q]b<'T-oU^qƶ1Ds#Qo߾~e)ׯ"fpg956uKlq+XRi +  `whxdlznisz)(?SVN7>L ᯖʢ_):V=".75 ~]0X2<²w`tzny}kn[.yH,")EZ[BξzJ{N)iYI]Z1Ymf?!d;2q Nߵ~&{lHRT. ƱL܂#ZbRl)߿}7TU uminjT A!vbx |XY.={?1./jd7=c3 (cKZ,!7$GU E[d\] N:U A? odrÜ? ';W?[Dzٕno.O <",]{CE>}yMg?O0VwӖPG,n!~ O Y@)?f&UM:!Wא'"Y!Z#  !'tji#X璟t_ǿ_M6n|T=(=݃StӍR"(RYZ?6Ԩ*;&O.Y\dU7Wy}|SU V-il78F+{/_Vk)(}F~TrgU-3k瞣~84D*<@Vpe0ۡue<!?OgG>ko.ϒa"S[U$hVlȩe5WSy̾Qɥm.ЭxGWU'zRKmd\rvI]G?f$l¾k2;I^mcFb=O +m<8ai}kյnm ,x"}Uus'HŹuG8:lcbykKގQx~QFm܅'͒+ +`oyXjYóm-f\iG1ޯCTGXZZn [h^1,o2yt:&1oRcLd[f;̍w.JfFӣQK-Mo]Ꞗ]>m5OKUhtՠCv4uoGOooc5$d8TXN0p\E9"J- oGe2pǼ}64/iOv֗fȭuUGD!eYL/]ftYWT!%G(lir`J1?tӣ|!p6ʭh[;<kpokun8A{!U)/J MɠD`%?Dd3&*Y;1Fz;j ugC] RqnycܚZl0~b:nwb'us`_8nʇ2q{Dr7S, JiŇ'gQ;RR6 O +9}-FC]A;6U6PPb.UԾD9)Ǩ'1`R"ϺѦ/ ":!U7Wi+7}>_Əa{_mHӍDZ&?FNb?x.aUm ғw78.-hnyw wBG)|*HT GSM 022ԱqCW|/JuL}8?oXm& rR:`Tu0:TAl`d$g 5m3FP4vWh.&#sbZNqU}uxD^d9xcdb'UҠ!$yH@^I,|Θѕ"FDcel=%/*]l0G}޺7:sel=%/YKCHFM0WU! |+ՕIם^X:%&E{ ZWXGJVZz˰sxn1i&`Rm +n7;^f$<{rg@#D7#&!^KD,]#0B]sVf'hTgCM FTġSK' = 6"FyVqQ$ ?Yt}v6;ZC_1q$l0 Y{ƦV +W%ql̔ĘȈ'jܟѰEAo{7D?Q Eyq +%ZEuC)5Em~$<#/5ZƏE|i3Mǿ\RåmKd WUPGZuK6<F]|$d׾Y:%b/Ȧ^.n.7i.j@~CyF^ɫ׶N/rܿ+c7DN\ heԼ3ZGKԧ22`!o{zL!LA` w^q'1Sƍ?^j]&YO%Tx$UԵҝ[>i+:9^GYQFFD'fӟdE^I#zfX^,ve$D>&Wمm=CFXH[! 8D#o,l)Q.u~@Bc c6f`7 PNRsany]fOťUWŒǏ~]0U> dЦghj~M#| H]C;bd$o|z3@]7O]71jU)L,fOi.g>5_4Of/܎a)}U^A&yY;_{f.a;nFepj-Ylxs#1dߑuU~J8Mʢ)1S\]VInM-lcφTq\D.S /^uO,1"m'PՆ?(_k~[Wy/dH_unI͛+U/Qe&L!ݲB]ed̢owOHg?]^+|,';tgqm-Z0qD^?;m_dSkul\lhdq2_SRnBh0+U ڕ+ژ2k 7y qIDWM[5F'IIw ݐ䧪㷦*pONt/+{k X3f44]_f%'%$gTwͭQrE`9\j%RkJd2>x$*W}_έ n1l44憹v!𐑼 +&>#[}3v6?1XMN@!zm.&!5gxjac*/?TEZb E`2_?-wFGGK7#*N-07IV0"'.UZ fo{CeQ6c* }L$2!F^*lr } +}{F3R03J?3*#ih#t^A!9n1i(Φ%?(zjtA j&WOư@61lonk;Ѧ-Lmw,:>}<$>_Ѐˣ~#he!!:! m 6XJSjia0H2π[DB&Kc+ )4u9iII)/ ++:&6UJԑ/؇-l~l` EDD9e=4{gIMvU#X1@Tܪwa7]3Vp)(W.lq:*חK貦W%x< _#WK9/ňU}z꘰ +ad@[q0_Q v~L\rV[Cn:? +'S!ߏ)R1@#}Wѡ*~w kg?:WR]MF)!n~d4~qW"e'ݿJshu3˛{q`J@f +D02+̚{B͝w`FlƏ}.v73Zcs?RJXۯ[ N-iKml^+W8 +DeROħ7P2']zdq?'i.z||wi,/3=ƷSK[gW#X1@~CS|~Ӵb&%2P~8R6ŭ Ԯ {(9N$0D<lG20\. {'%we=u{Wl (d)}nhJs1"=[&r{}qXY<5[1jWSel 5ba}j!+8.&43L;5Aak}l]ٺ5E=;a2ՏoeFdS2pEoLhc3NBiu~8\WQd[K &=|ebb^, /LbF!ho[]yAfrb"S;DAe7>O9Y?!ӤvveKQzUr%u^p䧃J?DL u7ז=;UD3Es"[rn$ + +sst M/o_꞊+7qA~=/6K]NDVt+_ޡGq][Et ߑ}ۏޒG+Et -К~;.{4O{5SR[ňLyQP^~c:FNdu ~ܢ7ŌG[NkC;3qb:,5KX-qUݼݝ},/C}W?dm`8t_t OyE8)"g۫} jx.? W]<^ĕ)w6du ̯yy.І!wZW(}OAW(Af*KU>:,E[ 0:ߟa& }LǢ?m˓=M%Eը㙕cz ґ>F'ʟq*c&*A!ǨzcV&IqWSTZV+Q&<Tj4,ٱ-C0YpPVzc^~J~Ѧ|Sh:xjFÕU?>{tAQpY~[zG[3M5/^dfKw68 =ޯfr^4wXٖ_V[(+Bfݤ gP)dG-ޢ/+,Z/5㶼A R0:n}G [`N:mt=q񊫓;~~+b?}8tA{e Ѱn7X{_ F6Vde疼;HFZRm{G|8v%} +_<oYY5'e@cX*}j.MToKmeE+wo}Fd_"FG#{sq%D097 j LKơj[Ҷ8d B |3a] z:lNe C>NH.zv`rqs_*+]g>0ű(K +DӴSvB~5)guW=?L~SA4@ BeM]۰ǣX$A백IGԳc(}o{:ơ< UwXv楸j0hֆ:6b"/ ի=Owh㓳 +q m3B/~M6V,,(*[SC'_*:N.j10o*|zDf]smi~fR(ȇM}1/w |uMaKYq_JO|1?uJ|TrHB~8$fKdlp/L>chH}EA9]fW<%,.#0ƌd]'[Q.{Lxi%|Q7 BMVDF(1\6͵+D+e3;%Z`#2S?;EScE'[z,rIrN1#k4QW/.k }~cE1OЇ܊̮xpc>O!GXS0<̈#W6vAߔ%e_&DPڊu[R3au\ +\;I'ֆښW ӫF-\;Ps-8dv+f rLTAXBF ?N([c# +f:+eNw? ]nʊKTQ‘7$Y,.5/pzaٰS^ɤ!]Pkuwǭ1 +BwWg[kkZzƗmsͩcc=~^zmU˻|"=2.RBEuZ5q:+ +_/rz\8CƏ(Tv\4p9pI|17Er3rɳR"cS*fύIk߹M#=VLK.A],81kؿs&FzYhki$'5A_B)V*F Z,zylB6 _w 򦩭k:tWL IH;U\36G+DBLXwY~H :ƅ29D>67D.I!<;N} ڏ'5Fߤ&0(\Ķib͊fpu7epY~ T_3z]B |7\kR=-M-S#S!Es@'eJjlnM0Ԑ5-狓ՁHϊ?}?KVЏ[K3#+;:nZv}O ̤ؠv@vжiKHjX)YA\$ 4=mZS1D!Oz'k &cyzNɵmZx˯5=]o6}d_~PwLafa SԱ B#hLA&}xq4ɘB0l91cվLB[6^M}}L`=*N~↼SQeܥ^*l+!9,+hOo2j`s_5ӸѬkv6-` =uU)KH_j8V(n6*IưmZhGg4=F+d?%c >v*߿'&gVЬ2| Hʂyl!2_w7mK.*~,ֆ?;'1O\-w0~l}$"&!Bh +F[~*)֏+>5:?82,ߍ3+J6V&8|E51I';P#?eᶌ)ԕ椊ΪZ>@"$ *$+X2['V긨S~'vHA3ujID k[_5/qZ(#JNT} < :71<884PǗe},*Q xP$7܀ܑZ ֆ?,dq袮̌t7U֟Cc}B&W7"pNEDڳCo(s]`8Z8  kۅe +6 p en`Q0b z[/ј?I~io} yu}2vOZ3ْr"N)EN},&}cQ eC#'Ђ|v̘B;c +))cW}l$!&Bla}Wo%XPnn՝c= K񨸔ª7]t뇛rNCBMlG9D;.q {'iC=_qJ~@F + ɖl"!w>LNNX r3P*nj[|83 +;&T7G6߉pu|y\SBc^ܙpd{}}ck==.An/OYz{u*ܱCu½{E~/or$%PX }^f&ٿgJ @XX }]KJͪ/!+KA^:X01RX>$Hf/􍚨pʯ-5&NѵNcD_vO`1v5H06NF7Z>^zr\dAmHL}CsXBxLו lu?]ɕE +k$w'{[kmK;gq/߇q `fmCo*uXH5ฅ_Ġ|BhCO~,W9~,]gyݣGQ'pϻ;c#ȘHWCE^e; 2)X3ȳc +g5ֱcyٶXTe#Γ\4-hQ/ *ꉲk;F˰[2;@T"='jP&X{Ubj4 +u\Pv/Ǒ~mJ@{޽ &d$ +#B>,rU'B6}w32);xsuq);H'qtl'7s#]yi-ݤ{itbViûZsq7|=s¹qP֝qr~=wO+Wϛ_X4I+jյ.\_^c <>VuB]jXկ;??1Q#g{;SʩsH1jwهs:Uo>VǢ-rz1ֱR +$m$5lB*O"^V73Ա%RX7+ 6zBvqftAMl3}3~~+Ycg:QuL؏X7땪WÒ=!(dnP{zq|ONQ 8`xs~]oHlJ߲ x󨾦eD[jnU.PY.:TKna\gݍ./쓏ȚW98:fFz~u r?]4bH}6|/q=},&[r^SE.B/.YK!XGw +ԼGWɖ%WG 8+ލ-veÆ/9]!ɟ(?V)}_kMR+ά",yonϟo?}tb;nGEՈNQD~2SrFh9Re"ʏl_?]o/NpSX}>WԻaƖGggji+:םc +U~+ሬ?D|N֖dA?˟D_5<:9|P)_*%aE{dqB5!-uØ Ru#q `!_6Uf&kA}.wpu˖'G# D4qNP_m.v7k'4J߽Nqb{?(λ9 ,'Tr$?MF68 'iҖ>6F@;3P`FPi6̈́F蔳=㺻Ǐk0RQm`fm@'[sNPC+},1XΑYaJAuZX+lGD3~>;P.cU ͬҙAmS#wgvhVtd4~s>,S ۣRTw?>~k{hJׯ 0:QޯVXXtuO-o:k]ч_,]jfEcQ+BU1i\*'΂mOM8> ׾|jV:'YH +HwB$aYQ.{LtA7xTcj}鯖`}V+^X׽iz4Jz XAb)`xߦEhKU +AP:eY>&2}ȸw$|+Fԯ>He%<;G_ckjJMcg㻿uL-ê21 -ƣ0mwaޯyXwM}?Уx:Ű +AxQ5-"G`G͎yw" %Ćy50 $!NpI>>YngBȺu(?U0\[Qȟe=4O̤7koWq\_ Dsm@E]bYʉ!Pآ4k,em 2GbȬ)a%KhXfXc3HWO[1յg|fRmYʉѐE77!>Scy\ӆy|xITX.AVBkeL_5բ[CH_o!B9UJ{4֣'bAҌ/`U4S`4VN[ [tsӐ_x> u֗ҁ(L5B3D*Pd+'!bnn"?2 UU[ %KhR@//]⬤ =x@.,Šgl;pd+HW}IVrS8ܸMsYX:q=K22*1uP-?*ݻ_xWoK:jB VS݃ZxsaTw;6υ}و1Ѝ; ]Ɓ:ST oZ"/ '񣇴( ~ީ᯾R1̵>םjiƯǹҌbU{P o8MMxWİI ?feuǠzZf qCEP›;ҽ$G[M { #;>-Ї= }j5Z!i.̔OnH +~v\4:!- qBG}>VP1 '(i\ڑlfUnA*;.x{ktJtDy+Zw2"w QlY߉Dt#`m_h>_>sxmܗ;P dPApS\4\8^|#Ud=98Nh=c 2WgAHq@K}lc{ˋ_HYS|Z/bGɏR}U_d jeĵAW;K=ohL# ۚHTm^-HC/1),>/h%|#bf|bi[w-TpgvƱC(jEwBԽGhOJ:d70}dPApG>>s$3-"Z:]{Uu*Cm/I(1qǺ7+?:zipG`V_?_o/r4b]~jU^3;o +ۦ-Dbc\[.AH?M~k~m]z_ȖiBa%[M!`c-S A(c$qHޫͣ+ DY~N_+{oo? ~r2V.؄"/ۋ8zL꾊k?u&ﻅ%$}ޯXD7FM R^p-fsm= (.fv{1_jZQ6ԕ~GEŒb-S%}}ƏSB~#F&?߮ +I?wU#X\F-(tfٲ +6Ѽ~㧋5$CTO[7~t!]M{vVpoOv`X8.BYz/WD-6*03xh 4SMy4 +GO@Z[ߴj $  ++ (jaDWƏ>~ &Л/0W{'~xs}uyqqq|!x !ݠQ ,2'diKJ2!޳v^ q H_F_jH[SVvxn@Z-ʬH8TOv*Ov'%P&+l0 ++ u0Np?&?Ǣ_4$}OngexuT^ 5l#fJ>]P/Fӑ>}}Ynx뾜W<]j/0o8ΰvvn㵼b)_j&X}z`v843!kz$ch8_yQsU߯vUdbAffwh]XAa*4t(ˣ1 yM7=]*} 3.b/ uOoJL>k'{ 9W3~}cDtbFAu{Fz0@ZỶq*w" P@sPY7 !v]R?f.Wd 0q}O>,.0T`jx2Jϭ+FƧTY_%e.j6l_Ԗy&c!l)-`۽ro ᨍ4P2:,;96btb4M%;fI$z< ?S.xxYP٧DScar7"^fcDqY|zamI2zK!=DY +ol^\}$uUKTuyȿwXl(/N}L [\ξ{ȣjWxՌ# _e@C) +S`]艑G_cwJ22>ӄ2oދP{"M jGgK!r=?/>>:'kd~d'*@-BGn +}sA/]PDߎG3AUБa:?M~ P_cխS[X2/+T1͵GJ4MiљM~c{ ^>@>:򳂝SԞ%Q}suv0azjjffWַvo?;ǦPf?[7tyv:7X%_Nb1'<"IC>_Ǐݚ?5ou׈Xp?"o<U6Dc1# FY+lj|\B[d,BGC)4gR/]Fx]DZR&("jdlRVտRqA+H!(و&d%7DC*vES?ƿB +cE7;⯶m! +d!W ;<<vquM^뗄- k]ciz{W28 N-m]݈8 */ώv{+S'}է.?Ifh"LWA4bA$(~?#H7+%gǏ7WfHpɤdcT䧩.wyj;>KuLb$ILBJAѐ"&e/SU'JC*qɂ"avcn^g&InˆȊ&FX\ĎqCV~6ZY$ go? pw@onck!u0|A/uyqz|xsoln ;D%KNyJ:\ozlyfIenbۼ@ce#0gD;Gb;8|7#X ѹW&d]P_ejWPY.踜BB(؀_>}Y:;9:8أXoomnn ;t4H_QufiIsmXS?qxi|%y+:<^|o޴}74>=wt~>9~k`W6ՖHM_}FtPoɶ邎n`+.؏@}.]?Өʦ4W;@)ceP4*䘿IyLCn;NsbAӅXІh>R:Ω  e| +jG:9GiȽaK{8ce1n;缈 +p̡#zWWd"zG$#AKJy{svd>z Q߽hwk}uya~V\a͝SjWHX0C:ٖ7피fU3rpnI/=0StbXI96Kوk -pݯϮ'qbtOl5V~ql:7VC~'A⹫|3.?15]uY[a6]0?77;;77?N>8bFww` 8dcqaW[zLR6]@//Nv7FԿ(/+-ҲچխK!`{enb]k}@1#`ŵoGe`)ZI26v!)2fX MrsƏE))}MVc$Hq饝Tk{KMۻJ]awQW+ec䒱2š|'IRqkeK҆uYavdJOpog|iqanÇ33fW)z5ċBsX5\30AQn癸9>$e;-7W瘐{d憂>9yJ,U[e:)m'9/4- ش-u)?}:Q~pxlrjnqusZ/%! Lz,5=^kb'+[طDW|/ttII;֦OSsÞ}A@信44O;c]PZ{9F &뎭+sMjjN7Vfg&FFGF'fbQxC&֦qu2~i~44q*1cv,&mK}uEIQ~^NvVV으ҊW͝Sk;G)<Fww)-JM1g E:Gäc'dV ܽЂif ]\[Y^\~VPW(-|ĖiD1|<ޒ-ם\$q-~cnTL833#ٱ^0GO.,BE0Nԥ4֦cW+}|,^~֥oߵ +qooȟP×1"g!Err6:&kk򁶚DFsrPZcLRX.t3ĭ5FƆww6 +4*gٵxbw ɳCy*5ƀ.F~7!??&7 YQ6<"klg7\֮ 9a"Gmt}/]NSbf{D}ͯs{f}Kw ؑ"-x{ceav־:Z[[Z[[;:޽w3 +v_/·^wm7=A]?Y0 EFzjrrR󤤤̬’M2x2w7>0;=)>:2QzOp cɿ`x9fyALɯM,IR$feR %wM}Iّ1W+n>SK/hogCi!Tmr_6%dd}w 5W.]Hg#6!!񃛤AG(-m-oB?K98F#咜$ l_yW>l-/(b2P._ETWK2rC`ok}ELȉё:t;^[0 mt<4t182N^;<9mVvW+]ұ3#͊~zˆ$x'?떱ʲܜYY/^deW~٨5.W!ԇu+\~Pd`ȸ9r") HD7<э8чB. wA/?ͱNtԘ*qAUk$nܟ-@Og CU%EEyE%eUf7ses_42ͯ[u r_ۉ~{.S]$g&3}HJG m83>ބQE/<779/eiE*lcՔ#!"%wI8$*rSDX +FWOF淤͕IفK>"p7!Hudgwξx :Zѽi-qg_}.wJE614=\_S^R',-559ϟ'3,;o IsB7+d4C" ϫφ#9A/dnx@#@O1{2l@ \wO;t<#a4MʏG4ɡo[k^dȠXS!--=33"]TRQSx jS9~h!A%۩}!+n8٭krL+D5?MuQVJb cڇ{'OF>UXQ~l~}`Sж`2p|834fϼS +1I/^ֶ,ozKr{oގ~ᆄX(Զ,X_[YU_ + |MW/_wǫ}'H66XVoDc,*,|}r6w O|XD#kbv<]~j=^w2z,_7ߘ"HmU`{uaj]{S /;+2"gdP)uM#x;)d%}+rD +}<-oNFQ@i"-?[Jp_wGKC] (RM2ecTké_>]l-{0]֎xJ=mws<;>8fjJĄ8 +vl\|\|<%:9%-En +o5SՍʯJ@U jڇ淍׫r)78ݕޖbr!Fo<|g1 ZzFgN?}LS},㣟=%%_+C2x+;O|X-.3f%op[wn)Lrtmi^^U3үc9356B)̺%%q~u7^@|_ъQ9~m[r܀2GK|WR^{7@d}P+BG+N`q0 ،9<6&쑨Z uuvU5eanVFZ +-yB<>(FΡпoΝCm"H͏WoqlܥK.m-Rnp\%vKCmUit: /3$'Gǻљ21)#tLSHk: @&cJdo}~r$?+#5)!6&Y䳈8„Ϣc'3>UR~S#)z wY{!т=ρvj'B%&3 cr@f78"ƸG:Q_{`yL솕Gd6k +4 hy-R_[ :ܕ=Q:O|:.=0S)Д蘸)Ҫξō׶üaJSʑQQBFQmtCm6?0Q/D&)hTBǘLJC.ºyU|saZYU4GFZ^lUofyI9T]|.ID(Ggy\)O7W(:vfzZrR"=AV>O㑕[Hǣ_yt.Ӟ'""?]o 猴 `'f8> 5xwuv +"-9!.Y$˜ Tzva٫ŭCs![/p!߅P^K#rů>-P.60b0V^=l4~w +gGKǴ'AqiyͽS+yI43ͫ>,N6P|8.jA3G: 6tиgTca\պ.[8Ĥc|*:>9#euc y(sl8 +O͕y)qג/JU= rPn‡}Oo[Nt|P'X{~v~9Oʯb+=k_]-/2Lyyl4Bk}*gL=]ͯ+r3Sc"#hN8@u7t3pTշW~-&bϬ +PjN~dT[L_cx%CqEDŧdc!N,n_ Wj5fsI,(y29u`cNL}WX\ZV^Q!zU50TUtq0Έu]MIH", le%/ r3Sb Jz94kܵ4o.ɰySmyq^Vꥩ!4V,q>F.&<3npW>iަ U`b%@BB.  Npl6|ϻg& sLfZW?}vXXXR-e%OѽpU'ȯ?!Q>)vR&^ڧ)µ;^_|#әyїwߕhpsAWA]ʀE<{醝G0rC+޵sG l\{6>G8@ƞwmN\\ȿ{ĥl9.Ռ1J],ZDrh +썝%8V-w/vLq*-0GrpH~S,Y( |G`fyL׺aP/- n[l\,:<\:H/'5{΂0ǹ?\jqtb nn;F[hn=}?WyıB0nXd>7݊,.")gs'/| ;Rc{<?A~mg5ʧv/9p˻ѶiGBPbkF>lY֐[v᛿n:\lF)V2E-Ǚ< aA*o#yt P4g Oul?o)nܳuD4;^~_^W[lbA#xDI)iBAi ]>࿸/ok|>jULzB[fB/?}P~޽rW&+>:h5P{ADǎvu2($X54A N]Υ B|ܥ\r'{_?p2N|,ȣYm8¨<`&O[3gN7BlC4W{ jw&qPQџ7| tl]Y A5YuUDɃMKC+-m$RHSӦcA.Xjö87r +\ƽKGw.ɪT3tйh_oJ.o?o&GB!h9V*m<-Y74sX$G_<*M}EcC,A.C;@+z|MkU1vt?旖83ҬK8A<@޺f춪!AfEjjb 3wZц +>t +7\yz[WfzcW6\mf[c +a՗37lkLf:0Uƒ_ FX8[nڎ<׌xFۗNعi]VEMqG5h5Yuݠ8KcqjJZń홢%_SRa+GwהTm/q܍/^yCr\VɂjVy)Tԓ3fI8 scنguE +١uKw;r?~AOX,]>s-m]E<!_޿~r7nC"D؜z: ]bٚOC5I~+/:$|э"t]pA[i5Q ƕg3E Bv9{36`meZ/BހyLOO$;} I@IǗO^=sXE]L;N[O,,BHn٥8 ( fˉţ;)0Uǹ睠QC;7>݊fu_ak_;/YUf[GSR32"^h]-/߬V_S*]S?Z{DShFhf%5r;5EO4V5*v¡/9"i<."M=;g'0V-3٢S9~gu_F+dLTq4e UR +&  yrö=G^mc ر~ikRziŻ/`Q4_cLƧ8]ȱ=-ԗNfu-Xf!=2# +z9p@y?ްD+'d͐{ &V~r郃*~|ׂbZѐp ;A< +-'S.hۣoY@UuCR>'5otqt| G7Z8GEs'M5{s7+ j5"[yw0p;wQnG;@vN[#hBElԥ{NǖNvCp4=f_wRrdwQB|g5 %5aB_9t}0ShٗreHx}~yltveN6h{Țݓ|"Ȏo{ީf>xN /aE,?O_]W `|-[mSΟ=}thV|81'1PpcV{N:y}?x( ~hrc=gQG5'R-o[lX&].^N")iȫv^gye_bf=)\Alp' + (oݽriZ-UcRxBPo>a9K ྋȎ}6,B:3ዥߴ巩\ ۗNH3c}%ڑt<m}܍/ZV*0_?zrϦ5&5U޿zxmJ0*5 :ut!P H<7e%5r_[iW詸pAa75)Љ(/D$WڎG +QDIhg(Љ(˕ ?qrO^B}N(GF̏oqVw[6sn;4 wo}0vSyM2H XNRXH1A2q -ٰdM< 68tЁ}r\rU;U=0E¨&aMh1p ֡75)&Q-<tuYӥ5J򪭣{; d!K/r|^_Th8jp:+Vtc1hAPDu9=+q O1+_#9<&~LWf_DfIRF`x?AZk EAI͘=o-! sAK6?5Al|e$<ֿGj? 6,A츥D́m]~_U9#8H>*=:Q *EkKxyLTbb)0^/".8pmGճb!OI?9aJ<~c8(ǂV o8?xlUQ?0пoͲ8kaٸ\1҃?8Uk3Cr yI#_? ݛVtջ#@NޱE瘏{ ŽKGi\YWnOP5&I3dT8NYw/(sl%rPOt"씿C}AI6%'`uM +T%G.~xԽa߷myݑc vBdvȍК,gʵ׭]laWAxRo˳e_@j1$Zb`-C5X?~r"A94=AB`KON>Ga g@'"Nr,w i(Z=p7V)_>zzhG.' 2S ހ2a_8D\׎%RU.t|E\$uqIR;VRzl}G+g۱tl_0mk:1=i~W~btԿVK͜hm37 AN:(I C/MV̤z<6xrjBp5ZQC7>(WcLʯjT0x' ( cȵ ,={1BvI: N +;AaGw*!HT+7.?48`ђ%{u kt T>oȲaxW>xA6&JpZ3L[h=DY>^9HT +<[(8*wbIwHHϚw5 tzp᪍72EOc>v|= v;߻O%,ej˻ܿ}RŢFXf!>zB @P~4ľnX>"r樗V)j*Oď%5̺Q=|*&Puȶ@c-ǥS:pS| |&e@'d@Z* C-tp Aؽ&EĎ;g8ZB^f˞5?$(g$ urXWHy Y*:O"mbRt6-Ϙ9K/l*Q6<ƑZRV(Ȍ Sg]}2̯DTLxֹ;qu<ҒҎ 0wɺ_i9_x:7Onٷx[=AE8';#e"ge]NYҤKf+XkRfRcMGvP7As^v2D@-W{ŧ(rHB%jW,Hw-+^v2D@-W{cY~Ǖ-XBg'/4%l/(z~BMz rf0JgK?`/fV.1:dp3XLMQ᠛4y^{3u!*:=zDѷQ\a>FW| -UZjƥ*r.ŬTN n>ҽ0O-(gk?'^&N%8rLٽs} {+44YCEHVd\"l-{Ӫ]T5)ܽx'w}"(Cx>@Ma- 1FH£Y2楀:/q輽_G1Wҫe&!WOJ`xS){{w>w뉽~|Ҏ4=lW#ۘcSXLvBW߽Q)f;cQ# ~햕I&^(; 8; ovQEJ)尌Fx9ƒWBԞ.C*Wc +қ-+kգ맵>21j4f꘷w@ k:~cq`(N3ŇQBl9d v74Id{+i4Gf@iϒfWibu":kMhbϤ^l\Sgo= W;.۰Z041'NQi#QXjU  !> =\B67D`pkLi<] +y?q2E ,)3W:)yM#NN0ji*l hSgtQݔYc`{I)P pX8)IA*ͯ+*MXXkw8 iPS-꩗4:Zԉ[{4QGEX+<^q7fn ˧}吧?jm3zyrb 0ލZ706Rx]H8&ddJ;IZYwXr10vܲ&_$U;{ "IЌ1Tsv}\r:~y, $R5?85~tP/،OݲzaETKt[ +Gfl*|6++"'>@\^ orpJo$6*F)VD +|B~yա+Dnn00+BKoעDX7vL{WIl/CW6cգP80fs\U߶qoʅgneQ2k,w*5\(gBjJ"eC{bCAފцR#y÷}PȣU;G2@R@q]m򂔌@Nոo,fws\fknۑqEdbуJWl\غj5g y)9?Ú_vdS=W7>~>PR!V~d&8>XE:v5kRw=+l1}IK$M4IŹ$cGO mz l<z,%vEdkX+%v1ؾ+?oOټo5Eh1ISR?LRX]UHo_J&D92u[;w5 #˜ +S59@4DFH;}湃LIQǍk]G+_HW&m f,u&(6r2j nkݦ@IKOQh(*:-`MM4]ѷGt G. I#@>m+ ^ +n}DC4J'rhd &\82`wӴX~Kz=7D5AAwH!U޴b#Ɛ9**vr:yL97eU|+[K} 3׏o_>{H5˗ꘅHڞlj386 D5j $ں>seC7XU" +|\\5[,<_gضG O7lǯs! +R6N۲؉c75RB@'\ Ո|ď%!Ug]~OwIm-Ʃw4rٹpoP,M\;:Hdz'z̙Uُ)wm~uP!K65KRҔʉbSQ 'IS*zbsl>ƅAh{ :%r 2Itd(씻`B1{.ӿ I.ܥnӑ S $Z.?NrP.%A6}Ac|E*wbO/m*'d^edfdxMK̶<9%fV`34F %-u#Zut#rac5ʩ}xwc8ɇWKCR |s +]7@)G_?{Ϟ8o-&L6md2fرYp,{0~_c5- ngzTIkחO5O쪹Z$8g⍶w:~S5i\6dz"Y*#$ETscl*,pF!3t[PCp4 dE'3j58i<|ҫ*JIIpɝS;)uuy 34K^~X*T@⚈gccL䋪}CEs%*Cd>pU~7 .*.UW'7~?op`K+.i2B"L2{F 2JUw[޺<ӒqsnO0G0WD%Ce`>8 ҶJ`Wi%EBAg(`}8&G0.ɿi:hZ-d Ea,Y:S +laP#;Gۨyn?Cnnu`k(]zkYHJn!&Ft/7]v)R`KUM+X'IpK|H8Ǘ_XSFq/'M2!%2wMGe_.chʬwth~ '\+~lKU۷w=Li".}$޿yݛW.>vxmz &/]`ޜv'O2YR$W]r)ۘoc:i#α`DckrP^S"}#j-~߫iCS<&DjLCJ*^зDb4F`DE>g +Bw ZiK$>vWm=pU%W}#~ZcTc dgcvȃƷkYǴ < HRSUVwP]@: W"}s->;2tsEwTMբ*RiT aB{ď%y48]BE**!Gq]l+p%i+LFsVl2X ǻgN)c"_]g\~X#~BϟH$_Ji}"f4/85NY詍(w^,c8 Ĕt?.@9.#qkQ*ypHpFFDϘ}=pSDW Ij I SaiipKL{1MdOUk.1/u (kkO<+B>G߼zݛ.;udr5{kldoYi >_QRnhMOtph4$@kP %^޻xXT$ZK4aΒ KpaazGG~ 5.#; ouk% YSBgibȔ]f沭L]},"-"aAm` Hb6XhųWo;p+~ RPȔAZa-K7 9z|BybQZ6-CHeAeL!A xu +P*׾rpEtj4 +y?Krw@¯`1 ;XRO(CC[,u;}D[z~bz +# ]2[?ߝIvk:UQئz> #zKW6I+(?V~T  y}|aL_>)ƻ{3Gor:4r2f*!~|ޑ?rq^wʥVo_xׯ ܷk`KߺUK-r~k$2CG>]2g>N1 +f] 5D аuшxS8i,e!& { +#yܹh- +=1Unh2'KOa@3\ T9z )Ȕ Sdz*W}'IxԢTGi;8/娔rS >esЮLO_(1s |nIϾfh4'~,"@cW<\qۗ\xLuܾo%Ēe$IJzcک"S43g&ZQs^Y ZM%=x̨ \g9Z[q!j.CzdPXU1pQLuYRt))\@41Uشt.ZlScjF ]H:o2$,^>CbVB JrslT2gǔ"scxpmU(9wRȸ"UkD@&ۮ.IU@xvdw->}y];1-!h9y2e*tUqQ̮%fb$L,)mz+'o>ŦX;n;M/_O N g?8y;ۦM4A- +?A'5:c[I/ W q* '&*^C`WXP^UmBLm[әWь-0x\|^ y|+ i:w# +[JVp?UjyMobTGsX/weprp}!~xnU  y]3 B]LrC +ؔ3*s`:%blZj (?gK)ܯ]+l%=@ tF .y1dg-v3 +e;-cKDZ1KO1% P`v8<63O0^CWKL{5 &W"x> +)"n4{N?La7EDޝ֭қfk=Q&[ yL\Al ?F:fPc<~xXBoΒ;=HՀhi:𚴘^Ch-zC^]fH1P.Q%}]A@>F*`{Zr쁜uP)r+̬J5cTEm@ǾT9:$\cځ2trU,cmϝ{)TujPSӛA59!`(.E +ubk>j/p/RrG#wv*o,5:3Em"pg _?ZK:Ԗ ZB Ax~LрLp+ly@14!2u&r7"J/d/uMq)-e uuD̮VƬc=@ \ 8R! +HM3LvF iSډA9QGZj2*C9opp i!v,}SO%=;( +Id$ʉ<*OIu`l#OhZMDU@%8ㇷ+\:ggަW \/d S-9hwо[{,C"c#F a>ղ"O;3 @ +-aq ?VA>Y)Q2Ȓ{蕇Ry)q^|܀Qv}42b4+3Sk@>?0wܠ@^If/5IV fHN51Dct0O3:oQ5OLyiN9a*DHA&Jq ݴx1+Yr l3Kg&qǶ>WǣF\ + >rj)!;~ǰ:qLFi䠞L_EqFӚڲ__@.[ ^-fk^}wc L&HW߻u#CY82y,NU<,($3Ҙ`֭%JfejP&flui/q AEFh +<}ls!XT)\)z)\+Cw)$B8@ʽ ++7o^| +W%o +.ɟ`_A/Oi %詙GMJB墉㲓^ba'-qt5pYWmdy4zrҢ`=*/y$Ǹa$X + >䉱|WbwǐǴxv!](Pu.O*pU-']*u;2 Ut/@ ɶ.bՀ-kc=Կ4{"Blcc篮ָ(!=+7>v.@.W? u;_ ~wo\:slhҞ30'F&\SejUŬd[i"aK(d-z{wm]x>Nk3k.diZ Vcb7,?k'7k-พWT[VԀcȻ)"M a6,p(e䱙Yl\kr*HQvX!#3/|e<,k/W48ߵѫMz$C);4Ʀ&MCaQ|[KWp;Wӱsl7$ng([p"FعTZDrK_/}KW[+3 X6Bs}-[Bi5`_>_`@1YCn}U@??s`?/_d+@W<56IPH@4x:֋C>|]R2b{BpU;a_ +%)TNsy:nv!*  +bsQF2A+?`Uh矿e#?x٣V-7{ƴX-՝,z1jDHǼ iu#ˏ3uНTfcq6g@4؂J4m@GBc*Pݮ-گ IDHk{kV8F2QuO$ρDn>ŮҰ̝6mաfH[ +/HcZOFA\%^0*X{b)4}hzyÌ X<])>d,@1b￿k*~ $f_aЩ+:[9Wl_?{ 'ܼnjIFR# +Ǘ,˷? EqV(c^ +QU/)Կc4p +').0]/?qj[0E~Z]zyޣSH­K|a#O'j>k W7:cFݢ[YX + &D1P 0WGvxm'hS)kgef/0ʡr\My6+r\@O]~LsX#'6Z +ǵH[XQ!>'.;)Xcq.VX<ք;8p꽧_miS)$__>}3wo[gAd3c1XL,'M1/fk<2c Tm?ſ(<΂a~)yl)tW#XHh*PUvKc#yl0ypqUg,>HJR4})f9+U2VJ)P\a^c?H6qcCX +FK!;6Fc66n \ȅtF +W!J2dU:t"wX~,s<$(ߗc(=8sM5]G $y^L?v\Z 4'5Yѷw!5G?{۷wD &aBI:U`0="ģ$Icjm9..Y`H,8tBe'FɍӚX!E'vjW(m y jQ ~c76s1쉮s->PP!ٖ/OxDG"wv +7fA|{fv >mפ(VO(6D6nvse\Z)T1OJh7p8F^c! >^;p62` +Y8b䮞};r_gtxц݆}[ND^6~8 cmƼtpO >~wA7 +&9 p?o?LyyӶtQ鎮}RDsG) +@GPm4X`ެ)Ac~Mƶc!a(\+@0y#}ǃ__޻(s s6}>yj/g: Q\' Dy1t[yl;jc(4dRhGJ@Ǥ᜿z)ccho4B&`4۴?l+ gJ +#fx'#Wr$h)ʡ4h1 yLxd[d!`o'OKO^ i4ݿ~b"ΕSvnZ@jZLb>LCO"D40k14qt㬔ifݿy%3j!ZKoN'tG9_W\!f}v WCNW5{FƟVl?x$[41*5ߩ>L9Z'Hq +XN~[gmv3  y5tGj>@}Q򘾟>VKv*c +dʿ5Ԟ>Yf\Y_@p{JcSaTe"C pc\=x_z#cCXe?bΌc +c F34|%y|c"[JQ5T_Ͽ}|sfiuYC"&5 &"S0H䱦ވx5ޟvHwL9cXMQ7<~#ycHW})k/s Vϯ~ +~Hͫ'L[_<y զ}*u{m~oTPB8oʔ7<61!wUUY~fTek<҇ T@_j]c{c""2+pAXd50 Q1B 9;Q@$$_&w5yK*HX*svu5HH$PrcJR )Я# 6y +rr2yܴ@B?q-^q`k0?~IFQQ"i]@>{hgߊ36)ᒿ:K~BccuS$@>vy 2 m_c^[c/N~W\YcrwI'y:%4PH7={MWO ":*1AJT' ^mgJ}LWwبSAP ">Nʙ[EP0dKcLM&y/y|cY.]K8@@aID +X<&xgl&ZKVq#ތw(mݎlA +C[rNja# u]rCç/|$&#).vp"{ u5ߝ ~9f1>V]Z$s!e+UVVl-fB@p--!(?ʗ<^9=.TflG%8:;F +#(%n>~Z#˸~Ng!fUӹ\-4hfMA )gu-gcsW$Qjʾq!Ę<ͯv(!|=BuX'ȶAJO;pUǞ}WviG$w[` +"O6s╽<Cx#Q?O~\>gsfjYrh ď]г܋rsylcy<30yv{ @:R~W)GX +cI /<SEBM +yZoOqbK@k/غ.~=gSN3<a%];1X-F>Sg_!(lM9+25~jyy ,ͯF<\+@{ǖ#`"֣U#!oQ2+94{ݿ二?Y$Bf XJgԕ>&e7YL{b X'tB|yiksAig=qsz4 >z>o:QĐ͓[ڸb> YzD+&u9~11fuӸ R|5'djPK4( 6Us +)'PALֹxi. A;=mֿ;o 6)anl*W5q!)LqS=5_ ՞rSv֠6oM%œ*MꬿfE5r~WCbw`oZ 2j͚jsz-P2¢s /_פO~t?OS0J5o}jh#ܠɗupV7gDL f 0SMTd wzjyYH&I{h>)L1YK!]hu! +^}M{ +ȲEV1s&Qh>V\8sJv8( ݫ3T;N1KT~P/𰇚C+I(Cqjnq1LT;Qˉ}F/@dfDNj`hD, ZFh|#1lk*AiE{O#$TdA%*S/< +uy)y5ԢFv䀆elXDL? {fX@r8DŽנj+^t7%22+mvki +=Iw[M._nm\?`1\`K6`lл٫&N9$,\wVֹP0cdvMo-$Z: DfuYw뎽N|'/^ lVrLIw[G m[k& dTh,)NFen^&G Chocp'P Bs8m\Spy´,z^5Hp?vrPL%L򦽧`a&)U?FxiSBZ>b6w? +*<բ=er7עkL8VL|i ilcs' g ]'BF d\ݦ$EzGjn +/3kl(FaŌϨBH|bx|^~|U  &!8&4cB,W *J|* Au8'Mkkݳd庍>~b*?"wLQʱ]o:wh7CVcS1ODW:xx]Pرr "M'Կ&E}h1L,ZxnJ{uq=Ep@Xƭn}vBsPjMs +lJ섭+~v!MZBnxx*MJ̟5nTw䡹z6`z䯖<~h&Q_=6SzŤcS k~5RgpIM(!rW;F '%)JȡKӝt#ړm~5}Γ\`zЗ&ODLA!wo +]cD0ay,^wБgwɊ%c&㹖lYt#I`/\93%_V!$B$Aj0%3;sf bzM ;^}EKd{HdϜuom]oi4t=7l$?)ى`.2Kp]3m`I|Bq-Q +iU$[RT*O +{~(:Бq߫|HI-тD *a^2 +](~lx J9nFW31⎕|#jh"anNt4*4SWԤ\GEϜ?ŗ`?#֘L͚bSP_=^VT:wuނTkZ-~3~4#)|tdM1{C>s&B\.f3B7@TϠR.dCb4H8b%06;ؤ;A`2u7x78f?%േ.lkDSŜ\ +w7 +ә?c@<l&mЕt~ '4PKNt)+7_| WNc涞8yY^<|+o{$$jSyȩbE(l$: @jۓ}WWl Hq2h7lպw:B0[J$5.i2/`T}|] +RzfXɝMY*fҜjz%~M%1X*L\-qYE駖f]SHt~eW_[b0I෴)$7֥#KčGTW/޲ +-ƭOV߰yCPNv%|\#;:$K0w`KarǠ.7Gt(A%FAï`zf4 +Ӆ\ ػKOkּGA"[ Fҿ4}"e;f`6ԗ5 T&9Z;pWq0h'r<6) P5HWp;cf3%W VwSy9AsXǥȡr=03D0N-G iBcsP:63Nifq#5ٳ(R.yxîWZ/,|ܐn箲T#)F!J~}# +ptTHO=s gt)VkKoЕ=/ޤ2`>„+[nTE>'%?A}̠sDHqmV9+V۲}GNx^J^"gy1iF׮ 氮4DŽݜ _cO-cb2@'ωFtϛtVS.ALUeҺ>Q[~!ֻT&mi8wˬV@ݠ;: ,*츫ߕ!Kl%Ml +k:t̓k_F^H@yƙ}[W.`(qQ) ѫxhOomρ- $Q"?Ν<=Lj}lXe~K:w&2ELx1s+%lDB5?vsK$wY; 'd_ι# n7 MR2kŇ>vSZk=1wR]<<!Kq% w;}Ԟ+fUXj0HYu'n߅ldž)te|Wbh)Mne8$ܯLU!m9e W <Ew &;MCp4~bBd@ ;hX\#l ζ~ŜOPqXmú&p~{h#G2qNܞeaLn"'voZ߈+UE`Ƞ'm`U)M_R`Td4 +@@^%$Dc'&S$YpJ΄"ywD1Ďd/u`HVvVALig +^`LoM'1#6n*E +\;Mzt9o +ؤ(~pQ< A&(+yk]4z?w/d2ȧhpɳ2yͯ,.0_Q!Vȅ)̓7.3bMiZꐢ7>aO-}{Hd +'r<p)"w1o (NPy.8(2SvE h[ ܹ %[!: رBxn;/R4c>-Q&bdygvsxŚ;s/|oOx\g=a|R 8c,`$GtcZB֡j +.fiAuv,/*B.v4=ݜ{7ںx ~/Zmా嘦9 Dp:( +B1CHXՖ 'zq o^cAFR|Gzׂܼa2e+t=(46)?2zyΧvbykZ͔?8ԛRwG4de +>~ˏ߼$:E,]=w+ +$Mϵ\ъL"ݧo.\ %m$'7bXP 7L'BYt&?<} +*9PSCƒͥA|=]t Ȍ M ދθef>Ɛ_c/_]QI'Imv2F@O2ws~JX9fIPTT]!2'4n3C[tT3qL=d}X>PH#ڴ +M}pŴ]sW%Z\zmi^Ԥgc~䱷⯼}\G: +tkմ t@^F jz^Nā3}xMH~ I0S{qs13?*Aw v@F>r7a.PʆG.~8SkkUHOX1d -1tk'Nnݳr˞AJoQ9A0yN]z *{ Q86}l:]\A!م<1=(^$ljt Ps>&h*ݛgǒ>Xw֓sᙄ7 ؀ ee o/)0߇0˯aKXPGG*Ø06cƌӦL +FISD"ۼs[^wRg2,¤<$qYYKuɴWi$"Wn±+7>NqEV*Ͽl)XT^\ -A%6d 8N3,^GT_.7#ʐc^W71k델$L`E@>{1Kv]a!ԔdwwO{Ы*2?_+]$GVU]3Կdܤi7V<d-$Z{>cVɍF"IndUocn>| <'_ʊۤI j0j&Ti@G׎5 +a8UdWSL @:ב*zZljx"׹K6 O =VcJـv,XmM||;hܸ|>V* +=ǯ7X֤r<_]]aL" 4v{ ~[@{6\ԛ^sٶԵrJXNR D`ӫ*0yYh@?@4j"y 0@g4y||WHJS3\ 6#H#wl +-tx>l3?Ky' 4Cɤ;Eh@wn`*#ftڼ{*q>FmSEԜ鞅=ȕ?n$Ucȵ? E-HlԄWݼ Zz>' 1;hHh5}\x}/6=A@ ]XyrBt|ØrqNc̊MG)R.ArRE~zmG]VSf̞bc>τ[Ip;Յǫ<{&̟ ~{v!lPHc +}A) k8 4ߦ5֔ՔGl+ړEp{WWY~/xEhW' ?yrܡKm"5*m:rEǚ%N V&@gtYdիW-19?)gu_v˞cn=~Pkib{u%w&f)Ï܍rg_o)IL fUSy&E37qnlfI + N_ V#Ŀwr yzBB[]B +jϟ^lS'F!2J(?M*Rd{xS֝ j~|}[V/ꚉ%4NK{\C6&%d? Bsklݺq݊Eݦep!4ҵ[*G鳽@|&9qչzO`C= A/__?ek tM+G'|i"Kg[O9-3z?{Ѱ!*X.l\ 1oEijԴirBkގ1p&i<VǕY!H'oFێiX=ȑZldU=/}FǗ/gӚ3c+ոS~5F +ժ`GA#_cgZ|&*SRPN"<n_@r(58"C;lLkKE$AhE$_#;$ÆbMH<vzYbyܖG8/s6,?,F +"mfgϊx-DW ;?$fb8AtǏGYoN:qd@rf@2}|,98=v/^<W} >k޲>9V{|xcrT&`Ҵ]=6 zi.//9wWUpD;;c[aqp8ے.~DŽ WmuTykǼʗ߮n)|_y!^\o?@΄}l*yp!lnxStVUT޿|pIbk"llT6o +5(?b +`l +HcU'Vnغs^|^ +M"AtڼWȮ8{ H#ywߕ)Ԃ1a򴙝 Vd 'L2}f܅mڱ7>xpƅc{6jP{(lEZWzÓ-R9E~,u@0+|{Ӣpۻj__dQŃkl_l충G&(fwܸ𹛰J|*U` ih٩O$r +DJX8{hg +iMv"dM4ؾHŵWV;yG{hnޝvoYc) iTvt񊾁gu =ˉ}[YPyl?|qr5y'.*6c7*9ԌW,#i5-1Ш@k&(F%ؽmP~v#vU  ?WD?8OXx!&\7րc s"뾓=7K!`R/^ŏ-@*YtUoC.޼˗7ذD05_$ȉ&/-l!ݔ<}1~1BS# +Ng2y<)S!k3! /e[85\|&K3Lq +'_:YM5H|};3'ROO>iv^-KBBy7QP,pt&چi AwQ;%tFfD=?N0D85 Q4?~|MM/L%nյ`N]!6?r@fWWUEϔH6o]!Y$]z!|l ^ɿ͚͠TW6 h]϶*^ DT#$@+V O#&~\);o"”Lơ+&1riA7/2hJ]v<%'Iyŏ9h;㯮a8wx+_<Ipk'wo\>{ܴ+cb~ȲQOO\:oe!O*#&O5q*Zn;{ع^fUܑKcM1=_I*@GWɡ ˰IOkpxF(nRpȶ*Ncf-KXوzѺ/hX8ܴjќ)+dϜӳbSW>{y]ri ܳyWWU@4VC.bLcT.PZV 'y,O xX7ڄ}^@'_L/K7Bk]FśuB_)?\y,yz֦_Vϕ\^2`ڴz\ XS.n>cc6CpS0ybb+*%xy\媁ߔs#Q}._?Y/v( v]{u$υW+'a;+wCLQ04hPs "kJ &O9{u[pkw!3dGCK9>f^؄<JoUݵ`m/ܸջO_# {b\Sl/;>Fp!衴G_yk(wL<aךֲ ~&b|-Ƿ.ۻmYӧ!'i3f]j#{XUb㐆|>ezݷ|V=f XlM;N%s +B|b=s8OPCNaп)\BAf&wGXԬBrIu#Ŋ#a'8Ѓ5KIV'G:{ʍ;:|)F-f䘕ɍd5ҭ8ZMؗtIzcT:囑ǙWaI`(R[̳ +\3RtO &"v'TQN/J_/0.SŢ9orKrk3Ѐu4y͒yf +A)hNcko>\L'!8G/XFbgu5Wsn>Wvzn=}9o;;#[u.KVo:9U0[]+*!= @|>h8u T{DǿFc2K˷JCp}|!bƎQX7\jD5DXcq) "Wy"Dq?屝i8Ha"LZeŸr1`_ܿ~0v|BX\W=Ub#q}nr sbfvt,[q`ӗn>.Cw =rg(_œ,(#Sh~tPra9z뇣+ gk bDJA=9ugリMxLU|;6^2F huYn#g{aD}irzu+ʋ[\2E ~M59P@~hSN]u^Ni# +<8 ],ߴӷtj3(quYEVPaYՌxP:,Xvˮgޓ8sW9w}hTD?cނ]Qn>4m|02??F09h QyvSCĮ +' pGl+w`fge!vUH?ᇗsU"q6;Tߥ(x~|7 =Mvw̘L* Y4Y'G42AV PF1}z{C'/~׿' +[i)۵8ո8/7kߩeK>ׇHxTȊPk]ܪ*HM"E 4kSe +Pnaɝ+ڼvYOlLd [ 2.!żūz=wH%\I\%D7,dJsMC}u슼+Y*&Jd6R8*P 7~ ݼibKN5HX~/w![^h+n!O8ˆt)o^<1WR@-Zj8.)mY?zpUӘI?*q_Ccd==ԡEp<+/߸8Gj!2סk)h\ewx5&3%}v=['0I(9X)n1uf ;5 YG&NS.w=wՒy]3bZ/^0q8.N<k7}7>+q\ɢ88N- 8ZVsU5P$]={t[l^ep(Jrö _<۳tM[bXRqd!{S_l6o ])VZRNUv8D*cX3JΝ źfCDz/}/z.©b4<."Z LWhce/sczbB D\.($e࠯@zB/#~A.(ѕ6b٦\I="QK'ڲn9>Y30Nx$ۻd mۍClibgIk>#E[qç/jI°α[,A +ՠHinء@0aUd[<^oL-*_o^8{%(J3ONºArL0JdbO71+ agoܼFi5}u BS&A?'*LG/ycJ9BIo_޻|tWҹq@n{aM4sq-iᴡ '4}!c\MW3yw "])\Eun7HIWg @AH4&?7ؤ;0DJx0 /M^=sd✜Ն{TɏXRdMc4s"g18X,g_}~1 Ww_}괩S2>`ɓp&H*2fJHx9Ff}ţ[O޳}m$>u @,&NLَ4^rC_{kRaz١m:rNQ1?:-#M%ǷO\sgu@{DEn!0QqcW7m:d/UOR/Q>.8.[8 {5$1'N4YK)o1tջҎVf&UwH(E5<}&DA3LCa4x(?aj8/mס@G]Br( `CjF~Jt0A84W\;c44Mw~7Nܵm**t1+؈uWvT莮y sԥ_ά&~C)8m2}:Ƅተ}f]b +UYծ/I/<t֖D_Th]~yms׺IThBL׉D$5p?Ei=:gw3h R uzxμKW4`| Aj&(`2{C_<9ٳ{q{ϟKH_:iO];wlhז ȾS}Q!ϐd%ydž7[l]Y3ڰԧ頭 9gn{{Wxy|5_jyDUFŏ }6ETx!oE8J"; U#8 )8L(\}n;Oo[@p='/߷؅M穀>А?_Qnm.+HK>@ttTTTtttll\|BrJzfN~qEmSGl$ޭPoNZ+r";F}*AA6K٬Qq"8FA +l\7JzBY͹1!1xD,} S}namk,aJblQ+#"aT0#bif]j8w4;Bߝ.uf\)i:\>L\.:mo/0@DDoo,/-.,ϥZ:Gq 7ں!H#cA_4s3j9E@v~҈#j@oWG[KSCCShtrvimk7Mfn=4.r [6H+Uj.l|Cث!W +Kf3T<d(St GA-]25aKaZq pa'*7aA50İTC8ð]Iﰽ1ȄlAޠG{m)k#hjk*+*+***+kZ;Ʀ׷Y MH%V_'y<.&)=SJ`ogsc}uueiiqqaqY\oj}=O\P9F8 +싨@l@yEC m//N twuvtttvvML-np LtDOW3T8IW '2oAR +{Z[E禃W֣CgAcesRP\^{{,W.kY?MuԂeS~ܠ}7[+s3#Ã}L_]ƙ嵭Ós-Q6 +@Od{~=F=VlGǮyL7\xH:yLs)9B!@DXFI魽~lE*-^GGp܀tՈpples< +1DI/[0r}}yҏIӥw >)՝K!եɱməyܳ{PSI9F +f.%LZ~ҏ|8zF%f"^ҘChkI<܁u f 3+f JVcF$fpݹ:\?QowW@w)1udhM-:OБo.NOvww]8{hO>2w%y ׉xa <^nCO'E}bbb|brrzfv~qyuck*յ&&\:ןftga =6(&RlV\77H[@n-L Ұj}{G{GgWOo +'IGoPI <1L>қ +M+o +d<??;:@nq~nrjWqqe}k{w뻡v.+W=]Lv71ORcU?>y y4@?Ĝ +cP@`y,cE!mǮJ9TX@dc<[ |4sy[K`"t8؂tm Xw}1 iXsAGNn`s:4l Ҷ(˳#[k[_;8F#~}AN\ k`@#+wj<:4(ڿpA:PHJQnH$mo-/Π1>FOLMSeƣw0BjXIg<$ k% =$3mD:~sa/Oѻ•XxtrvqusJi_L[O-6!ܔhZيǤ+V0oSU!s ͅu~\FArPƏI@px<&e!p:8!,0ʯӏ?,@AΣB<#Á|bH PƎwVWU4ۻ,I4݂}iOz@B4Q=EħuT) +a-MMMX`opd[`π<]2VJAgf!F+_ah֭c %=+H& V0,0ˏ3> ~wssyqvz|||txxpxxxtt|rzvqy +Av %)xzkkn>!i)lzP$-HTaxA?J +@{GxAbw|PfO\8ϥ_lh9=NNnoqLWb2:ȱHu-?̴fp*9njGs!?ՐlmC!4`% ) 1 +{ X`LǸe,P"YW ˏ&.=#Lhxz'Wa~t(;։1o=t_pw9w$7 `txy7 +c}Zwrm\^5!( TdH䫳񱑑0[Zـ&:``?= 2}{i0 I)2 iU%ZrЬ6/aJY`~̗Pm4THCAqfU S-ZhxBzy/ Ť%&  惴_Y7 (Ei*z ( I;>4f0O,,.-mOHcƤ]hoChNȨ[G]OREf}UPjMў~ 3(y.nam, GcS@' nwOZMdS;+i[UIXwٸz/T`D6QO|B_^M'yEqN.kZZpsC8J+ +WfgoMyf=Jd3=̕ } `&yLrzbx^G U "Zc 8o +v =K;1txaB S1"Kc;"*1txaB S>p) ޭCCW 49hyv-⦥f zڠK݅uGcģ\hs+taJt JG4fvM!>TC(.:0|em=~~Vi`S?No+QơUz )((@AN;xSM,֥c텑ꂴpT!м+z !drQIFABQG(&JiWFwRס +&(0RۆgQ\?ĵLutcN93ALV~,7{I{;IKzݭaF1Np^&?UDǰipB$ۗa҂.?7<6 Nd[ɧ7+@R ZGlD )Z(4QƗnZ4ʈ!\~\!`0(+~ 4T.YðaFyLO" !nA7<^ C#]=SFp> +wƝP> +*(  P ا 1*)b!NXNJ#Tq}.,Ê%ÄG>: NhIrG7&=OmALNJ8=vX?EܛC_ӏYn^F=uOoQlvR[\+A V$>v80e1I:'ֿ,1~ʍ+Mhxi4"ˤj[ޔnUX04>v8|L<6H/]&WpLREC$:G=b eCBG?5%}0H}8"nhBnکC>1=N% G>.Gz<-'X{1L8(+cԌӠ@KEnr jm,:!?e[r,?** A+l 2&p@&XktPܢ 9/@ V$Y pu97Vyꥪ~Վ)ndžc`:&q]=N* &64ipqqFQׄ΍ւߪtϪ6-T;!M+nP5d3G?Ll#X~ǐl/o!7L+ʼn`-D +z/*cC.b 0W/uT%{(c<)G Sln?~CC]H+Kxv? >>]4<87j\R8A2]K8 /O4~,jRU><~Iξ;bwL %űκ Κ7z,z0uM@ FO{l܃ 'ocT@ +4/uM(%lACX/-{-cPsT.DdD + e?xi8?_~U11.[HYIɩhʂ!+* ! FM.?DO + + ;f Tr-?c\]Noz#A0A38}j2X4_#" a@FH[ԡ$5epf8ls =o7gK4蘕ER8QPbg[Pmѡ#xco-(kpdBFq]b@ZhTE҂-(q_O|oe @h"cث7g" +B:yT~p,##`n:Z?%!_<%GF T7=cm% efL8x~^K4q56ؔfL8- vsFXP10.~E{ e3&, i%fZeež2WkS=u0WS/ًkRl:X=`փmW6 Ə!¡Gߤ!dXN |g@uS.WX0l7&%f퀬ScfL8eG chֽx#)wbeWkv 819u}{<\hOE{eyb6x$G zPH{cثY_āXngaXW髕ӣɵK܃C fL %-J|+zܣ$O`ƤyaPqO5ys{L*iXI@ fL %?ޟۭ`( 4CP j:FwP*4x{i̘4/S]׌GىNΫ`͍o xu2jfQcK'7Ҙ ş楇}BIĴ#5 @[%#a.{ V\zƚ1nGO̘4/=J"1ӻBgJʖt>p+6ܣ$G`Ƥya/WG5jqϨk 8i< wR<3&1Ə1~ǐQ՝:y +Cצ˥w1{qL +F8nDe1 ϭ>1}r"/'~MɭG>.IY0ޯJ; frZ#<Ə1~\GBv ]h c> +{E"f:u6].S۵1룍h7bZ`uJO!eCā8{j<@u:M(.؃4`VJx8cAFBP F x*p!Y߯XWnzq# 6&k!T +@j:ۀR~`Џa</a/UmPĸvb8&btm3+5º)`tu&:ۄ=hA1۫Y?v 2,ֱl.ȉ5Z O5PmJ5ǹ}E{F7MS٨JX,5Z O5P}y֏id|dubHJMUG btчP}(x}9]i*IQﯣ&7Ck(&$hЏ^MsĤ ǸY\B?Ŧg1 tf APV*ZZk;@Y- +h^@f=1٫=>_MJXu!da?[~b8Ix*f:s?=13TλMOX~"`2sBG-V ͮ0+ -+-YeKьO6*͸cU(0a(?1!~,u@#(,&9{|qڅp8L׊!?Y~hTse:x'^mBQg}ZSgqTRvecثus6~ y14 ȤʎT).l!.1A7 .xE1=* Xf-GW|w"yL#_,5byIŐ3t` E7Xc ̀5_T-̨8xP~j0zte&5{++ljHqF@fŪk,c>`w@kuAzwd5T( +#h~`Cߟ=r&!`([ !#i+䧦/ ݤQ=U@=Ĭ9jyOTyanMA;NΔܲa90^+/hOhk2B|ku҈7*J#BJTux~]+ŋPXC?tD&y, >B?Aie0)) .y8^EzXmĜѥ=$AҬǸmܲ=A,u7[ +n2gdKѬǸmܲ.?L#e*F|er,N*bΞq۸eGA~ >h!oývdzr|ju ef=m=uǩҬ(t:.BlZamʞ3"$gOҏ!N'4PrKHdH$da4n)([Axsn>TZ oQFq7G6y.۫}|մ άSaMZ(z-{lO"7ǺK rwЂ"_ ytA~cs"].?c}.e[PDBVy^, n#8{Hҧw& KΛ!Ps ʯc{ ^GO#G'guosHSC6o9©'CBKѷMA!y.@<4CVP64!3#ؤ]xsn*/FkrRiiS@Mǵ ?'Џ{8#8P`l YɂhΈnS%1GhUu%j34!B34Lc@6o9bIQ>1 |t]!q +qoSE^*1T}~XyKmǻx[^Cf<0!(*\~,aP/7%MaqGQ*z 턞4cBE`5;a?z ϔǞZ=_2Y$ +!eMoOսMۤA hB֏cս3$ɨk=л]p4gh'ٸ>^gQf/s1T(YOz(35P{5cOW 31u`=h ])?f{Bh?[PI`9?X`q `@# X{kpʒ!mQ绫ӣSL ԬCk^ E wAC;."l.83}:Bo.@v I_r}bE?v35"^Mugy {}'Լ𸌒gQ)_%+hډf JOl3tJo)?.=ƏJ2|y92,А?}LWTpgs}m}FiN4k@?A̍tSE ${u]Џ N\,5#H'1>CFRׅ (n>{ 00c  =i/pd^| ][9:717P1O`)>\K?i r\jnYcȨ(>uZ8Y[_:\kM#ך7/2/92[&p@.l[#iVJ>,\#F52w 9^Nuԗ_Ъl;{pd\Z^L֨g7^^jE8^_ܦ7!"3uc>DzVАk?'q\\9QV~sx:]A*'jҎ-qc~Wlah. .0G~Cu`?8&K׏ &hnܲMڦtif\ +ˇ\4\Tfo.Í[K3?HǢ+hw(B$eI@-[L`}vo|i%ifC4r6(6%W<Dz6ZCA;p@[Ma&NKR$pEa5'ZrLkr)@?&{51٫ Mc{@S:#SmڦteE#G[KSuŊ8lf-y LqWNP\lї +$nWGOŒtif3c C~YB@*VLjJю1ܸe %n WnU_+Fc?FrL5 ]ͅ7B럯vЏK3 GolLoh"b|.ʦ /:odڤPWP`[Ї6S,C9iB X~^߯Q?]J$1>*WP6M}v(?$s`lm]:;?#i=ac0mvᤳΆʲڶ9c fW~h؃PSZ14Cϱ'j\g؛jPn/NA;.J;~Mb $ Ce_5^biBW}<#jd]jĖZdyòTE^ѩ[m.L8ej<tJVv.ca׉M6hFߏ&Zkʪz&#=Z'yczXiֿ6]ƮaFJԢs>?XA/:}:ekaDR DžH'<&Cs yj +;A Jɯ\=/~"l|Hӂ|vaaWt+l|@5y%{5*]eshzڂxGu-3v;+N*###n0Ǧ]qwLvx4AhUm|eT{TRry(2&@ß&k&JryUQ=I E&*GHh"W Ω7Y$gqqb,/-!:N(Z:l(ZuK +ہ"BA.q0\US\9n+[@_0̞liUXT|j c?༶S*Uvԗe$pS@cCGs^t=c0l1_8ǰW88sbj1$}0qcH΅9J,,1 &u`I>?+6GsWvpeZ.wd<85|v^~ܤGn?.O זd$%d4l18!&cWx\F$gB ,Z6X~M(H7ԁD? $SrJjqMӐrg3=jҏxX=5 +b3;'7O/""ƣ4DM< }r:;RSB1 raʦUx`BAG1ʰ6^²xPHIL(* y2{j֗ F~T6wM/QdsiL~ xu~47-!nd.5yDQh1[~L_ЏYxG%Uu +::TGpʊ[-H8tKڈՈ}L7XA(-ZymJBua ǿ0\]z_}2ߵϯ -vXZ?`D[{}E҉Mʤ7N4{;>hX{`vQf,Ѵ^/ ÿZu`j%K +YB n~4A{':((#@LWսƘX_k-ԁ0zԋ<G"SXNY"Ü"gENLʆ˃N1g"0(42.%spjagFT q #믇=ya54WP,RJrCv}>8,}:i@D~@GgűGIzE 5]u=*2*6uc4ɈjE4ۤf~ y3'2 wl͸ŗ7-O6} c^nsc}mutc"BjЏ竵cʙˉIـǰWNsy\dЏ)QVp#E,S'#>'0ӹ0|"ƃ?1ߥgωWKvE/hv\q|{Cbap-lP`΍A#N1U1l@C?+HFy7sBMI4e m.5@0\j]QVOgiׄ kD~A$; 1c?竑 A0'[S҃~[1 # +"I;iv&;*SѩyE腎LCE>{'/ ,Bm5%5䜊64dFD[Ys + +J {@(l֙"ͭd(41#v&I9v]ǟB?띆~j' \2yL 4g*_ _@_o`̳ бPp$&Ut(`-Fbjamm(5(R4l[ 8/[3Z}f HXşcCi1 EDvEx`j.4#*:g۰Uw7We%A!QI9BE <"j^ +8;c<꽭@8zV0W5u MD<p`3j1-!ju|{F7]u,HVl>V: y.c40p*th=n6ՍKs;4. JPSSA Ye-#.qU0"'N È`^>E!I?ƕ[*1j Qp(&K*Kܤ\]i.F/0L]s4/?xy<\O! P+mfgWTh.Xmken|QUZuHѡh^||~usiEYBVlY@2zc_HJj!:8tE#{N"EjЊ/ό7@9NA C?݊{LQ9%UCS AKK5 B7P';ѿ  ]:w~G*M[],`.,FBoNf\^8~TZNd@qY"cnrKy[\|:ޒ4Ɲ>֏XCOfٚh*ɤF{P'iV@¤"6xy¼yۑ߬$e~y +m. /j# Gݥ?^bpq}xJCSk]W/7gLR^Jn0+ B/mfic ]&F.0+ڟロZ+A@Nwtj~us +å!}Dh&<>ݞ.Ud'~s >Fŧd׵OrqV'`d]+ƛ˳cT,C>gW~fQn(Ȩ;P&~`E.`4YRSO͍&yp~^BhQPL_cߝ0 yBjPB0%u|=@gSuI9pq7|1~_5Gyyy~}3/0c4D"y YZތ w Gqh 7V1a3y} EF?0<>i3*+*]li 5x]X6`öGL;8uͨq <&{޳[q +Oau +wI3k~B͔H (7WgG;k SC%9U8G7:)}d~ ϦM`;؜n  +M"a^^EtuG ud.ô "&??RGxo{uar +5>Zz?q tM3!&ֶvO/nw\%\wuiJ<]m 6ek_iP3xSTQ3~>4[K48 pFy<= iTSK*/JDTZBs[G膢 ,a[$׌kHca4i(̤' +HQ?C1j8Ck^,OtdžBUd,:E Z& >_HЄ87(2$rf'7-LS ?qG%V o_Եy%}p<'+&!•6M\qp$K^c!&ޢ4][4B4 oݽXE:f[`‹+rQ$6 E +4U~F#  o!Px ⮠glaBY,?|7U5vͲ:yIg4[tX,x\.@GؗI RzF%e7O-C:g.hpaxki4+1— G@WZ݄BQw|vq}[!*_ r- #:'{+CݭEi9ͮי䟮Ixt|rV^IU}k%ZGf O~|xCh{f˸@l(Iyz&V6Q7ptgpSqq!Go`{} 2ip1r++Ϣ =ҚYtCΠ4R(% BT1Eіŷº3zk̴Ý4zf+ Vxodj.$[)FQ& /EF6GkUu5cCĭ,iH*kX=:9%Nhu30 NNO1V0Xʄ4šc?LJ[Ӄmy)׳!Cҋbec"vS'fxJixY}Gǽ@h<4',0Tq9׀㰯|K}71^^c3K;b[=4@"%rN7)[IG+spB9{ԏlwE9ȥ]h4ȹt:j !S*%Aj,٠ X}Y:)J2 bk$h~`'e:K,adT:L4$0"`e+mj_Z;8<:9C~yyuuy}}sC3ؾ8?C㴷2=^SFYCBC~AfnIetq\#śGhW6΢1 s+p +qф$>R89܅h)/@&V9"8(_k[ A'v\Tzk \pPjPy}qz2MuhE%5)y-s{43Z&AoSyNR/9#~~muG%d341 r0xvDPPuXn(0qOQ) 9^,qE+[[[ [<V\_o.H 7hpͽKk|:0V\h͍h L/j:n0':E.\Q\.[sӼB[٢]w{ˏڎK15ZbXS) UM41hj;GNnKzNRD܍7ߟrᛇ[nlw%i\ULQcS*Ƨ%ë%^i K5P1}RLˈA + zg"]@&8PFa8HR-6RGҘZ]u+c ²ڦщ鹅mG8<<8C֡4;5Zq`:'u|JFnIE]KW?ѱH>g]7ACQ@xx Ce9hqJ}5_>84"BH1 nAiT+ ejO)젛61ӊ-8N,+ѩfpŅ֚_*-ָ$+BV(0u0_IF_TW(1nӣq:B݀R〿d904&%9|hI^XLO٩C' YT34>5CA "?Z`5356HN׌y~T卒'y Ip+v#ΠGv~w{w aXW[TpܭLh]PV7C{'0Bx=}+KroJ 7iCU  QQ}v Fhf,idxhhyP4HP(Hp*VaX}!L0F +B`iP7G(-,{[+ cC= 8%rhDt\յu2ٙ-0-PoYHA9:!9#:*d9 l8b4fZЙi|Т6`wsmin]ĸ`T߰(SI6e*I( _TTcEE0|q{MiUb@MdX F'4w`5hĊX<@$;96G[PZ\H%`.d/< +{ns.2n(!7V(- ݾQn퍕qp:o˲0@D1JEUc}eafbNwQn&\$MCG7>-j K1 SáJ*r!ar.(jh[@%>=2TP[XpזgƇa(ʤZ~a1,:1-QЀmu5UeEi ,0UPRk) 44UпZ*/j-c-\1hKEﶇٌ~V԰S֜Pv L W Rѻh6e!韂߹0c`ycuqj83z?ݭd$,{ǦObw.(oБWb,m-Ԧ4ISc"HO-TNU<"6)-;%ੴ01HBAG8l<֮Iň>̽'4{~K4<` =;(`ogKCuYq.PRbSӳr ʹujnmktwwvv46V*sEZE8háQݵ^,x6N"sB?LX\]]D~Տ:Ҥ''@TZqJT\bZV>l(5X(N +vyqqnD*$SkCuiAvzR<>?â?:ZO4c x&d dˋ n"tAZ:zF&1xہL ,n\M޸CfEk*Ja(.8/%zQ<5#'(,0oEy + >(4 +Ь܂Bn@0/'Sm}WyJzar1쪬Gؤ5p+[F#:'ui"mC\>.Cʮʊ|MZ eDcǯDH&^,!:*0T\oHGNH)(yx|jvayk㕇G4\yBӈ%Qt!6V!ƆDc8- +D >!S22ˢY8i WVffFZJl-J;ZH*FG0&4HC`,v;8=<m`ׅ #P;ÊB~B +uUu2J5XSQ {0<%edfSEOXӄVb*FFb.$Vd3J˂Y]܆.*sQfZ>;[q3LOfh(. 8 E(/+@sA_nAKMKKA+LV9I ЩB*U=8q3 M4VڅJΫX=_$:9rz{i!m;G .boL&Zav@XOX.v!i45fLXuw67Âaw+ xᑤeSD=c4`uccsksk #4dJX= fhnRT+8 4cAD,u븸(u4ۺ7hrCèT)yE4IByb0!ڊҢ8t1[9\o[EDFC('gdeC.]GG& +mO5%Ca)ihnIDc^[]l鉱֦:ш%' g8 +OJJc[X =Uzfcs%C_GxldU$S>22rǹB*8qH]8Tl16X(H2 +kAm"I YJ.%;?7=516L,:U| eEH]QG1h 8-ؘ(R!An㊁+Ç#aNi.bpt6 _@pTrnUh miB]8{Iʕ:O[x{y +P-M4 ++ ͘0ՒFM%*hIpA_@ }pxd|brj7,Tt(裛T 7tS0ݥ";"1  %;v8(GV._Tr7wb(6::6>>>1( vwb K mz|"hPHHhxD$XI)*p+)]ؘȈ>w: tlyHY,(.xv#Ƴz4P6#Nc8}A},ti8g$o? Ճ ɰ+W5M`OO̠Ϻ`oW""&dBGLEB;:TqR_ŔԯDrrZ&uQ+jv0sg +U7B T܁px&:g+8\ag +W.H8쁙61QDiਤSGS +gVvJRKBs6#t_wpΖ.Y7`F^1"1"4c +(*LM +> #;QRɂzݎ. VAG'z{:[[g,Df WB8(D&jI$Z1HG*u JGjɦP\ӕSQND_9P.C Uյ Mͭt?w +mqk85)l/f2JO )-DNZE9B}t Bcnp>G&x0SbIÀiӄPOq\^~c!vXÅX̛&܇z}?_ ՖA Ø2Z h[p;Z(,3T&tu k|BJjFbܭހ WdAS$[d RHo۰:i訸fT|b2|sr K1[^YY؊rzʕch~"`Dɐ_t '6,7mz|sB- +) 0Z*8Ȭ +w+(:{@{H~Kd珤2!* '5Y$gP5R"E" +2 +UkPP=P*bQ&^ ""QIfL((T` ^?}z#pQ/|XPyꜮp`wW9=Q\;1񬡦9]ǫõ 4[XЬ(J7+ݭ_Pܤc~ \4KE_<+ ED3:Z"U#b/ #ߢX(B %ŢBDM@aĘP<*#nŅwyV7y~ڄNfp!ҲsF(& ۼek QCb|gi*'?].m7+fZ'&` +3(3ao@Sވ? N$doD@DPPi _!y/yK,P0oP8tyb T/W$j6o +1(~x{9ޘh*ͤWW{ݚ(&Wӈo*:C +O)__@HtjAm;}D%|E],E0 c0y뜂<%F(ѿʁ%C`rA4uN_-5(H$)5]kWo,*Ѕ%%/L@`) 7yS`TẔˀǰaWx&*~=ߞn$EQK$䟄>^9zx>b5X x 8ǡ +paRp8PVb{k-Y6t.Ɍ x\xu6PGOsJ$7/ 82)}lyAC Dz'-PE҈7кC%fX"yQ?ߝ.W%G~D".E$dW.߿ƏU0lGD(Zv4CŰ+`"ÞS>]m7f&DHX"H~Hg /ݙ?hrƥz@w+ElݵX|??_Vƺj +bC~,H$~Y~}Jo +,s2"\:Gsy!ھĿ4z|s57 +X"H~3,9~o:xK!Ba ڑf4!{?Wz2U[$D-0,.>uc[y$B8W墨յV@"ԵXdk~<'>]-cD"mti`aЯH*4n9Bh& -U+5q0C6z|2]W$^vX"H~ĤhW&#hS$!b(Zbnch(i+[l<8>+H$@WRne~чNC Sqy*pshUb++BdVÕ-`PD"VsF/^tiЦN@i{B8p flM+)dhO;4_|4%Fz,H$߂" /\4b 2y6}IJÊNYb/#)XbxcfY>\-H$!~$2K_\wgBV /T P9g=0V%Kй4~ܞ.uz,H$ ?Ɓ٭;"űD"~@G&eW._>^2RV.g>u_47+1 ۼ3\Fa ?Tolm,N +.D" L.o^=<'B~XC,>""sR4.!`8FUNaKLİX|DDFh4 _m Vw$?0<>yh~EE\!. BOnc]brrp e] OGj2\Dy|bzR 2& ;h[&k~K]-&F=mˀDƤymp k$[CI$w0zwz^@Ȫ4.A.]AzzyG}y!2%55]dKta ???>0x|q63R*D=4:[Y"$qqF;!r6oB}DՍ'-TtNs3Z՝5V?_oΏWfG@Ky,H$%!ɹ՝+Wo:Y6͚G֡n{_a@&]J3vhQ́@% CSXW]IVbTH2D"AG&T- O 0JHZ1"XbL + /63u&;ގUq;`R`/^twy,'%&LZ%ŏ)E{ɄCy%>@Qvxa%_1Mcb>F8Ǐק۳< HX"H~W 酧'\5e +d\.o{K( +!l>k5=ZMr UG=H$oaq%“?J@`zkXBm%+h@6R0i}$Fë2E`Ȇ3^ϵHWCy^cK$x\<`X{"A]=c;eoӻX{N:<]/M79x,H$%2JgOo.bR0IT-(ˈA /@4C$Iت_okKrRE''D_N/n<Zмp v0>!Osڑth0vU_o,g%E@cH"H$?~CcR ߼3"u8xj|R_`^i*L K$ɟ@HTr^ukoنY%Շ/& fEX0Ӂ-@sx>Q~kت:3ÂCI$/ 82`k[`ꨭ;jmo8i?v=fxŅ>Gڎ_)'tdܴ()%OJ*kZ9{aR M,FL"4C]` RWVzĒ9@s Ct|-?+/Ǒt7WgG%Ϗ`]?uBXCLKOYق0LVؓ͞NBŒ!?ee 0YaO:.П9=^m,N tWd$Fk%{XlZa}1?,FarkqaCv1҃LB[)[6qy-)?fz ꫓噑UR=H$? +z+&%kb@HQ b._>Gz _z?c0U͏r\$%Oʭh[ٻxxcMsD93: HЯu9*%6> 0x.sT#,ixwcizRK$ɟ߷Db}1S'Zrf8=̨Wium@-XYANBWCKѾ̔x~ZjDGGQ80uzKXd &5EEMlar9vX1Ե I74~di27>TUc\D"iM+8yz')K:Z*! zX<*,1D"H\}M/IKq,H$<!QI9@`-NƘ0v" 7|,QӅq `+DZ[0n*+LMðqp tcK$D)՝Td 7`2@ p⒅2d22d2XRV68;yC1I(>?=>\[Yjk,-LK|X"H$"r*F._@`/Lx " K%;=4eʐ +ؘ bAH Ii _(>ހ,hi(f ;ulTDH0 RK$ČPx|Vi]fT5,6>*Po/WENNN-.P?<>P|zz'Q/@BLX/;Co[`=^n.̀&#rM}ck{S3s ˰`CY&mC7,!I83HXZL *ݠt}04a (Rx{ksc}ueiirxrbtd:qCm )y9$bX/,fX +cD"x[`hLZamՓ *1/T5x)p5m!x<75.*<<"*:6.rvnAaIYE5TΞq(s K˫[ۻ{2 ٧4_C8C<&jD.b@\__CB^@B"e0 U҆f!Gz:[j+K s23RS0Z Q^ i,H${ @_޿4m>s*|(=H?`ށ<.N +.WUr3)}C#cӳsK+k$Y4l>>>E c NH#ؠ>4!7Y//-@@: 19\uVIx1bb)%D%~Ë7uNœ&|wi";1*{@@0):=3R bnk\{rjyaqiͭ흝=>K~.6ϛ|!zIB./--B<3=Mz(zz:;ZjkYBg$%&FGE`ŐŐŘ$D" +?@,uo ,s5lDQYx[kI +ˮo߿PA̬ܼfnh̃CãcS3s KK* +EK\hsӓ$| BtCn!\tu%pIqQA~^n6$cc"`8} +b8D"?DAm6RT:t?7`Rxu:Y R Y(:설d匬"җ!![ {{H@(% 較`/oK3߆ښʊ|Ha҆Y '&BAubR$)H$wUc{@&i +i(N(ж:,>ߟ֧{ +RcC59- { ʬ+GŐXNr93+;77/ҹU 2'xƦFuUPJB ˅"IBFiHxRY AC ֛%D":߂"'` YJP Uzv|=ٜo(Έ gpL&U9 I, ;&Vdfb/(t\RJ" B~:H-.**- ᛓ#oF:iIII 0ICDGGFFa}r8_ȮD"H$?߷鍣'$O(>JGޞj.͌0c@t{dLrXsdٱI2'$Ҝ` 3@"t1c)$|aN Lz0`!Y +i$1P"H$_  w_`mFI L>%ENzzoi"'1*8Y~ BPU4lfM@h+q^hT"# IC`%(6i҆!apVy!H$/CyehaxM#Hh /m;DϏ˃qz+:$@@vQB?oa3fXaj뀈U$s `^#= $0`8,/D"E`^;~|D"9U_Y<5SWh6Xہ(N!! B0Ô ?!*le +&]0AEB$ +H$_ c`[޻|> Bؼ +U\Z&\DK3]X{ I >*I!g +[bń짥D"H$l.k>}~caiFZ#1޶|x;YiN F?v LK^Cb~N%C"H$~0X' ]?JAmPt{>>\wTA<  ,wH$ t!yU{֘Xbͳw8|=]ԘߐD"H`"Jf7Onͯ kTb v8!@ 7dƛ"H$ɟ߷شUz].4۵P+J2|䜤H@H$ɟ rRNyNA +j~K5 wVD~X"H$6~ =SkW/?>T5]G ڠK;>ޟ7¿KD"Hp}?yY9}Q:HR[YuyǏ iH$_@pDBfIcۏOORUʊp9].|~xy_ΓkD"H[`XLj~u(dE<װcF 0Ʀ-|=^O:,_&,H$??YM3BATѻ$S1agh'o:;!*_ID"Hw  eyidHK,@5-ozO)H +D"HhHAL*SW`ȎQaOXJ$DHD"Hlh96 +2Fo ['|;]iMi7s- cYH$AZ;|xXk\Z?P#7A$DǏXW /l<y"H][]dSts57ZLXG$Dg/>3~5[aW/ %Q!!D"HP܊ٍ֡k_%o]E+㍾ 0^]*D"a9>s|iwҎO})俚˙OA>\o*If5"H$߷?eM뇗/GVvƻK2b-dD"@A f/l<kl++RcӖp{uAZ\DԐ%Dg!SClz!YYz GƎ@se~ +2iR"K$반fJ[YjڡT╡-/?ޞnV&{sb¥@H$,Aqi#X_bLBT"X}﯏'[c,SD"auTBfi}ӛ^PSHg]IА@H$ r`XLJnE,GO$Y?JyIE}x{;_-6kG"H$? b^P>2y| + {9mB>_-N@H$߷Sz^m yB >ݜ@:9.2$($D"3񣗐csʚV1 %|5d SEx}9%Y_YlD"H$ Zg7襧7!dS/ xMy)9-=3+3#-9.:,$sDH$oY\92yxyo'̓îMSo7g3muiIHPF~!RaQ + uTtl|brjzfN^AIiIIQ~Vj|t()M"H$ !E'ew-l]ݿ=פ,* T䩡œ䄘`afϊDNɐB'bđ1 )ٹťU M %!SY"H$ ,Hc5DB=k%@l֯O۫s]uEyPc"à%@"P| b14b$KHJNM(/*-onj-I X"H$WAqyMK;'W. Ap8VѻM %i@Cp{ur2;>ZWY{(#XJN,9L8,i'$B)Ȃٙ 9,H$ ~K˯h\٥/YCCv#X xHwE8ރDik)mFjR|ltddr,I."N"3d0 b9LlEFa8.tbHlH⒲j}#cK뛛Kc Y !RK$rd|z~UkACFq "&S /[+S#-e9)IqQ)A`dDz/&o",GF2BOPq]SK[G♹ŕՙ()%D[`hTBFauА 2/Latt !זfƇzI$WLNNTX f 1l0$!IU:&&iLB_PX\R%qWh{U^D"r`XTbfQu>}]b3P ,$P_WkS]uEia~nvfzZJRb||\llLttTTT$31ApJ*0aE%e,Z1R?4:>9=t|z~qu{wps2YF"H$ hIE56وN`b < |suvC"y|d0?/;+3#=555999))111~ p ))iiHgfe2\PtYyEU ֎.#cS3s K+[;{({xz~y}{{}~<\O#y,cD"[OIYŵАh Y0[Xq $ּpE|~~x}yz<;H^]#PWSUQVZRXXPCd,yy%$+kj[ڠM ^^yǏh)%DCrPXLRvI] {"[MU2FA$򏷷닳ݭɱښꪪ +PxQJu MP[1N|bď/Pa O7ݵRK$C߃Å@Y;% e; !a0~:?9:\PhooήN}Ccӳs K+k[;{'gB'xj87=51y.D"@I.!=2KK` E2 ׷חLJ[[ʋs3SSccc###1::;1155Exn2xyyVͭݽãM AA̒,1 >~8&TD"2rxlr^[]$ x>%@&\]nC.o.///---.b!{WVV!~aހف.pptt|rzz}L$!_IkX9XQIH?ccK$5⚶[^`e(ɟ/ϏO:>>9&|NBaÐϤ01 bbF0f_OuZD"GGd <0}|yjxK6HpmBR0[B*pw{{s}u}}yyy q^ +7, Y +&9AbrOCɅ·jsI$4䤬[zKP) +Bm7r`λ!mǮرbCFc7{+v=Yh<ܗ,.+3ÂAOber\yu՞底rXsV~za}"ǫz5}L!Ǡ3Jj7E>S +w#d"ͫ@bo2 `[ MhGᰰq \y``XnGDŽB~ *!'-9ōZj'<ߣ@ qvrQ´b^ pۼX||L!G&w2$W7wϮ_={hCW'Yֿlj8'ՄB~H)=Z54%!"W +oh%7;/yyq=1!L:`J)h\ܔU]>5k+?X#ظw\Wџ)W3BYP^el8g| +_ћ3*g<މQN !PP!'g~]{uI_z^gv=9YvccB!?!G͖-ԬOY{&:-s~#_坳CX߭ (1I!#G'Ubybaq|~}{.]|t|@烏\{\E!&B6Ҳ +FfWN/]ڻ&Y;RNC߭wnņLJPWzg?_WDŽBBYՕgAzzyc)@N8¯E;Tu"=>x||^_MB #If5/ݣ[mi Ⱦ-i |"~M~ Y<| @>ս"! ԬcӲ +>ȲE~uXt>/X_ NTxՁ1 A]CڨWDzBHh 1SJf^Ius,=:n}}/Ay/8iC xXWsBH kum}#BkddL4ѵc~C׊AQ:ݛիDŽBBL"#"'fԴt N,h3{{ āxq=1B!$GD3&gZ+F6u]^ozy?߹0>$8;|L!$P5Ĵ|"wϭʺ[5 }aAonΠ&j*"fY+z׶ϯUSgB^Gz׀]=PG]qvr|tD}L!$rCkcR:UMGdy?bHoHwgo! +*|2p{}tϏ4W Q1!P$,Ek]9^ hhÂ\@>򍭗'd hOk5;%!DŽBBԬQ3&fZu]3;γ+㷧'dd {5 >BߪErz`:q%߻.Nvח{Zkl4!DŽBB0Yim0&e*?u ,1|zysPwl-|]'<]wW`幱*Ŝd@< !(D,rT,% GgֶN/OWt3u ^.7jOKHGw7WNdOuE9Iب?cB!Kca[U2ږwOP2|H'">:=w@uj0m|Y_XeJKicB!L`dsNAIUCk痑B; +8K:Փ9۲/͌ %9cBHhuxf oxbny}{!YfT: T6y$c 28=XYnk.-LMI!8Fl4fdW6wO/n_A0{^7Pw r1ddp뺾bРt(ƪL mo.̌w5֔[s2`cCLؘ:&3!R7%e*jں&fDɻkםv.(Yɏ?#uԀ?Q=,L?=߹nO뫋3S7#eSL1! r$ֆ$)[KHn588:9d2fR9o_wb2~|x@0xvrdud*Q1QBO %GM)9e -P8l;rb2ڥ\d>"r11d|qvrt9ㅙ skcM"81uL!g GHS!9˂5g`h|z~iuc۱/NFNr2 Q'շ}9Ř0V.=jt䋫 ʐ򓒲ͨ#gP.&&~xo./OQvlK0\_]d&uj826&Kum),.ml7821=$N?<>9;U^w"el*jS00pw{þ8`<\WU^\hA27ƲBe@HFZ)ٔjͷTT5}WN^Xox )_@eZe26/f +?be⋳ӓ㣃=qxx UK9-8Nd,ujژBȯdJFJN5gX +5͟:&g7v{++- / 3ðP+;ҏ8 1OL|uP<:!89YlLz_dWV7wtv +7C`ʽ cP/}=ݝm-MuՕb\P&%& =3T1!P]KP7bF99kXْ_\ZV\ 175~jkotwwuuvv|nooX_W[S]Y^^b+h&6xT.R| +B!ɰ2&!e)_)i`g -Ŷ2B ^T4\Xg0&#fbXLB!"HYah% eْ'n.*Złf# [rs26hJ0cjX\$BHPd%HDedXL+ef9 95532233 MAٜ. )OGGEi*x+B! ,Z_p{b3')19Y =C&)!eih8y8F%b)bB!sB0x9VaXhV`8X0D~4B@dxEtC36Y9*0!q*^XR3| zA POB[V̫I61!? +endstream +endobj + +8 0 obj + 137470 +endobj + +9 0 obj + << /Type /XObject + /Subtype /Image + /BitsPerComponent 8 + /Length 10 0 R + /Height 502 + /SMask 7 0 R + /Width 1936 + /ColorSpace /DeviceRGB + /Filter [ /FlateDecode ] + >> +stream +xͮW[➻%%,TX\ %m1sI)-V$%"Ȑ,_.cla;gfeN 3IcL Df1c1A>Ź37^g suwg1Ȼ,2ҙo.Kof6/qy}_^eYe?v?ּLG#mގr<. t'X1c1Be"ngYet]ďq?F0ry6#C G9Tcg+Y4ⶸnr\:x2.gbͧOy\D||e3D>4 S7ЦeXv&"ҙ@daA S_4/ư$J3/=X0U룖i:+Ks1c12>z僘i>@Gdl0% $F;/'Av9>wc1z-;ow,<׾r#Ԧ隈+u ehzk+%…5\giWvA=6揯,IܝvuS B0af?;&x;D7)\/KJyK4%6񥏾D䥂@c1cY\}n1+|(:0gw' VݹHD&W"(h2%Rk*/e@^ ^&#z$Ic1c1|fG|)|1:,YD`3vaD7>0:CCQΧ6Og1cq6\aw/7]uoȺK*,tKӵtmO\ Q\MO$+iˬ.Q&"úñ!fYȿ3ډkfY !W/܈002Hiz%{ڡf$gW6c1|v |zc&gF:3ҙ^gu٨H=cOj>9ˑc1JDWQmF3tFv.y"tw/ *VvbZ7c}DmȦxI,DMAv/'֮l߳Ytnk/7 K"wBT`Wyy gu5MU_{}xz%1c1q')W>=X{_+qH>D#X>v͇1f_\]vr 1{ $ݢ;n`'ڕ1Yv:C7uFeDlM Y,& ,Cm$2Id&&ͲDll$̕_о&W^-+I٦c oyz5>ַc1c'D'||ȧ|l$2?Y^ghę>w|n1_v_]ޝh7 ۆN~ ]̝F52ܮ BL¥0]+љh':e i3>ۉ $2iu' ]e#wNͺ}2-6kYy!Ǜb3l/E[^&yzsewvc1c+#ښ|9|DD;I i3> 3P9<p>C1 +q [Ӹ [~B}#OcNTU]&2dI> >d +p^1Dvܣ޵sĝ=]>ȭ?͕hr-3]e,:su 4,f?I>AEfЈRF ?iSUw,4"$L6^$2#eF" ɰ\y钗׳]#Fz^n_SB1c1?O>tcO?䓋2>c9LhYͲD8$SKW_cD\-k/_?Mqs->Q rE+4.vE]O6 3tY"\;A"l׈겱X}v2#E DhDYk5Hlk BozM +/T7xt /c~7Bwos1c1/pO>mg|>!"LDͻ| 9cznO^GMnֹqdn幭g˓K'*KN>"t.&zH]_e34 &IDe?$:C#+; mgF:lNdnlW]& /Qy^̂ ދ߷vt̛7v~Ce1c1?&)ܯ67>Na3&>ϻ?L>RzcK.fnEn]h&[voUt +Wr cF̅$asM}O4X B@!D<3Lq}ꮂD9h1%#vw~w&̟#uk,qΕd +.[Ox&pC "Yef3˒6iYmoI͓39Ge'!K*;gCGD HdF: LDz4«^׿wvoߐo7}>7Bn]klaF:,qnM%7s?ac1ɕt͵#UGC߿;o禜+t2G]2qfei"B6+KD'"4 ]4Hw""m.KCD آD&D&5hxԟ& s՝D&&>bcAëlf"W^x7oُ?ݻ9n\\r|c1c'>|2|yb}.K;6C%'poc<qzjW=w~astĽ8B.@.Chӭ< :?"^=ڧ b̲DM9OC B!t&Lq^"2tY"j[<iD&A @!ƦWl'mv@񝂷qMy a7oƲ?c1믿V>A[^e a8CkYEUvL4ڬ.&qL޾}L!w}]_.ߤb1c16}`CO%>x4D0|29<rC1u{u9f.zeyCv u +Nؖa,s2a"$2yg> G n&YR#"C,K֠(Z: б#RmoL8o~I[kmy;x W6c1|L.Y>3@Ȍ 9ȍ郏;coYY;.pS{;;BX ҙ$L N sAc$a>q2cmeD gOdeY"Y#ڍ<7ܙ$2k[ګ+;+;e黀iV·U~7ɻwL^^^|Wwc1c "|!w' 3}<18e|O:=18Wv9]st̵C{6;i.(%\n\ʓMkD }}YYveN&&LL4Vf:3 4X;~Y~!OMbDY"8GX"j[ҝA3ΝI P,a"B>2÷5q޼y{4w%qk=c1ࣽj+| =L-LDuXgu0I8̦j1&Wp9@/?w~ +2$:]B0cmX"bk+;X{e:D3I$:Yi@0ecc?ڢ\f>Q~fͶd2mIw՝sDu&a~1#; ex]k]$c1c ++|q#|O\w"$LAhcLs܎NcoxuӧOS힘;co.m悂M\b6;Nt_ Vv\$""Y]#ܬLaǁᦄ5>`g17߭-׫]6s~r7=\2\;pq) dFq(K% &"4loЈH-ko F _vgI4jٖ"6_s2#݉84|ә6͘>JZCv|c1co>ޯP B8["4"&>`ÇW_co+Y/!2=1W ǰ\S:]hٺ @FmxD B#H4$YR#HvwL|664h'LfD],qW2˒,+X{3C#+,M#6}zS~wykg3|Rܸwzc{t:w=>]ܯ=2Dei"I֥F0}qA;Hjrcu>vp;KudeDžG HI@gϤYf>ę}&tfYFڤQ睕G#$։vLG|{uZZ^:=co\v9^#cpV>VGhr,ͺ mGWߎ2vc|Ŝq],w>}Nw0  +i>$ZwtjiM6M3CՇ&"Qwf$Lf$DYV FdYuXD< 6飉L ;&}j{?O?c1W[ͼ^ɺ7혞1F8?_D63i }WjNЎjܯvscz_^r5• \;<ډk +; 4FtȤiv$L @0#Y u"hĶ; Is>bu޹O aByIbږtFЈ.4=;|wH־Mr6c1rE-oMw9_~03;rc@G~L1 3ҙe@X'qK7%~1_%;yWܩ_⢠sZT1c~!nzk_fazd1/EQky,#YFyNb1ro˝׫Ӿ_ݻ\\quדʒkW7aXu2l"bG<̻Pf$:iy7.;ϲMDFڛ<γl|ם(˻<P4XKʎoUyNy箵o ~ʩ@ߎ15 kzuQ;O>S~ ߱ ;&5G"hs1zNwΙ"}FI'D3CuS$p<cA|~sc|rnoνK\.߿=HT4@ $2#Y۲0iĶ\yAcMmqG04gifK9Ȥ']] +aF<_>'7-LNDf3k[}&ؖ+!hlVX=޼y[^,c? )r2/w/ǟtZ䶆{nqW=oqқ^Q: q;~19:s:g: :~")Hh4֥FpHy''vY1Z$!nݝj3o߾}k +.f:e +CŒvcFىvt;kp7'0Id@e:}DmK]9Gd@ hpElkW6376YƣlaF3fmK0XuQv ݹ{oܵR;i{?Z;Epc1H>6\]ܦvSڕNg7@LAq\1͹Y40s>L4Xl"V睰O.8}fqwr1W)=ޯǪ] rQ.U]5b~$$LĪ;56$a0\aնϱIY$2#m"$:4[tȌ Ye 裾Ohlh;I MD.Fn|{k㼓SAO9c_NԮ_Xw]l]\鼹[4ӏrdD ۿڙt4ɧ"llajz`8;1j8;s9ZŻw̼UE*3 3"jшMDhtq!u^ii@ld߬Kl_; fYrڍk{h]Mh74DIܝWFll"B榛3osަ}v +N1&3zuZ{7놳mjW:^cW7gDDD n667xp/c17xo*9}ͼҐb6v4$:6eQYږwy,6ʎi3&a" ݉n֝sgC qf6W9X{ Yϗdnj`h'|_w~7~9-89?λArc1r1+(L?|Rݦ6í 3eUoemp8csFv=:IF>;d"j[9DDDY9sc?5qkrz^˻wݩU[iwU ++fYx+;DYֶx:fhHg'aֶ\MfZkcI¼CIOlOؖwLLę}Ī; 4D?li,i< ,k[{<9!zuc1ƿ4wgj6uЅ+θ ok#93%~ uv\缧#'Rgドt}fmPĹST6c1r7^;YzoU^ ڌv"\;L_?dDY"hNdޕLh$Y۲/꼳( fhfev%Ft_,4ЈۉLD2s3i|5B#e@N ୠpZ0tCB:-k'1,vG}uUty{% +K@Q~?9lϯc|.t^u>)P& 4r~Hhǃ}Hw:4 *e:ڢ,Nt%emYv"bm|ctpB7+vOcSe}yԹt]e'*k_t-1bK+szmps_g?@gœZs AD YQYDei#ɉځc1D ˋs)=6]2]<̌.@D:3"4k H"ڂDfei M@ BFY"Vve O@D[ mf  .0I4LQrff׹yi34Q]tfEh@0DhrNp0 t!s1:r=ڻV%?~/.ag\}9Z~c|z*v3iyiDfh5NcX$=y۝j77/Ý8zp!3vceI#4˒FdI"@ Q2m"bk,H&et&iQYfFDF&j$IduF3ҙgi]CwI¤YeI#$@ eu6Ѧ3.:LLRзrj1Kj|p9n5)ީ~I۷o\wp$W"w*ܯ@"UYf3&2fQPc|r{yy{96GgHgeX{2CG:cpQ&I '\\)|-cY,K7>NnV;w!ZPvrswX5x"O茴I¬,Mwy(tY"H$:c<uGoUvLʲ3#ƪA<4Hgnl"CuQvrswX5x"O茴I¬,Mwy(" +4pZ䏽[ Tc1gǝj>}䆃.+ަIQoV]q_N޵7.R4Б_.c-ȟvi茵#G8T3D=|ո}}^/dŧw~]Vv"4"ҝGhfԕ:gGh3׹ZwtD& 3& t;'D\;[eYWܬkD<갃'tgh|wyS}N +dO ЫdىXe'.!ɣե@Ј.&ADD[OifeI7CYnIceimX!qw쬁 aM$dO ЫdىXe'.!ɣե@K' +Dx떵`e}c1NLS \8ܬ~jGE' F.O]CYn sQ6c->|󡳢L mk?egtqAqeq}c?,'js9ݻwN>Ys|N&zeIyWD&YYv.K;k]j['v# MvcF)2a"3H4ik[MD_ βAڼ+e"BȬ,;C% ҝ.5-ΓFADMg7>ѻ`mӎvcp]z^T=Օ\eLN6U ' ?#1y^r1nu!=>?CFfeƦS̵ﯶt^1p\q>YOk<>Qߕ'$LI!@.$L%k@#Bk}hk_? TK@3Wv$2c'< A @&a~V&,ܝe,sNdageDf}Q@k}&"^:*;&"ҙ+; aF B0?OKlQqβ ]8|3ɏ[ַCNMc1e_u{7.+6M+ ډX;Ȍ_47xܯc|ss3󡳢%ΐΖasv"\DvY;cz%z9p y7\u#*K34U63k[X:w:8M7w!ko6I_m"D@k{ȒDg=Z6h':I&I$aFmKz՝5H'"4[5 3&u8M7w!ko6I_m"D@,g󌳍sNbܲ68M!c|r[򋷩Xv=*..$IŒG]6$L& `˙;_}?_=6\3,N!,It֣evh'jWwBne1kJ8E{v;۳:#m,K ;,CL"$MDh꼃MDf$aehD,:k[X"3sףu iuF$YUwDYՇDIbRŽs39 w\c|üY5]:pe,MfYFD[ βo\r-ӹcs^/c ȉ 3󡳢Iٲh'h I`_91Ɉެ\.MbKN|X!qv!g}H&a""5e`$:i6a@ 6w7#P ]Yf"*,fhΤ2Ie DpD8C"4 (KkF$Ăp-k?8)76Wqi-qplnFZ',Dži2˗*6ׯ17‰:= MgE ӤX"h%txȵṦ'jc,gc7o8u{4ދ Ne34u"C6e턹ʎ&qfA>ڨcsN&  YrN& fYn a"Bu\w4~h&beD&zege'*;IqWʌ(h'UvL03wFuu߹;pA*;4Hef&ځ9=g]D]Y2m0qWJ6IdX}ɦ%:/C#BD@#"m"hĶ\=y^uG%,;i<9essNdH.& ,w99 9#Lؼ vcoӾ߸m],\2"W,ItyzzeIW} cojgD!LfM4V_iI1x,X>b7cA>}8Wfvwgul"64C4HGCWbYc՝A#\' .eyeЬqf?*;$29yLDei"66ID 66C#B#vDli"b퍇h6c3.:Cƪ; 4FdN]6r̃tv?0tgcoA/ƕądc?*;$29p2\~\]8߯YNwmYF g#m0pi$̺4#m~V&Vv:+KsX"oi }#AD&U6Mf]wyb՝@XAx4$YO#aݥi4DYYmqfM)(ܲ|[Ane1Ʒ;Xo_5.+H&Ll$X͵qܥ_c|Ŝrn·sc%|s44HwȤpDm:xs1ƿ3p/ǟQ駟޼y#r p&bMʎI#"i-:IlaF|ģͲ4]}]ЕeY3Xege&u5*Xl"}fMĦbFtE& 3&[t0ŒGek?iP#t-syGzܲvZr1W,zAL]\\vpkGfY"A#$p ;<͚__-sW0iD"tey̲D Vt`4,.c1z^ IErtV/;fHg> ek?ila>GM꼳ʣ\uII4*;ugm,˲̤qv~NfY)˻ll!k Hږw:ҙOxBYOx[Q::Wyh&N*;NDh9H}77.ٰcXW[LdvCv2˲,q~Hj-^Yѹ18af֝,2Yr8;̉AȜ?"zyӉNΦA9iL6bu"H4²,k] MR #^Q7ڑm"67e&"&$:I34 ,,K2,$hF!6\'I#,˲4X.>zn&(ÇXemv)k1k9K;c;o3M<$aFDm$a>$:iIAceYR\a @ȌtflMŒC%D&[Y}ZO` 3֮<$aFDm$a>$:iIAceYR\A8#!Lu@z1_1zǺ\\)ȅ3&a֡A"5I W+{1Ns'I0[Df3ck,c0coa?UwZqӵ79%]6iIila>'fe<$Mα_D .5,CX&>k HtF< PcHw"IM M` >A0+LX}D%mr}rv*޿Tg9cn\\ .4b]jDY&΍Mę}z0Ҹ_-^1xC~.js>N46QA#֥1xܯ =|816j\q66q0EL~$:7Q34@8[ۉHg"ʲ4I$Y6i s+;eNn?} 3 ItfhDY"*N@vcyf\XgY"$:cN& & \gб6$aF@#6f&bL#VwwH>L3C#QYv"tn6$3:),[o޼4z^393 + zkGv2i6I:C0pHf[⎍ ?_Dl$Y"4X;^Mܯ6\=ηYjxϬۈY-q>k?$LYҌtf s쬳,+˻3֎';L+$2C#eY{iHe& Fw6}Bt&G~I$̳웕X;zeCYgYVwgOv4V6q o 8y錇czzmw. aY.{! T{csܝhYѹюl):il @eY>Qx n816N8]72iDk@#g>tws ,K,?"mV& .Y],3&bcYqn ɉR B>Ԉ.נF κ/w}$2C#"YDY~Eڬ,Mf]twyڧOLIF1ZyO ڻYW +a~,3&bcc!?c/}1}y'jưt¬,Y]DDsM16q1RNտ/"7NNhk[>iyfh6C#heh5bm,C`Gme@0"D H#zM@>BTN34#Ga&i34FXF h^#24 ~$fY] -Lg-/r*{D'p3c|eޯ믿{ r p atfM6M;"+T|^/c pv.pKe&A i,za;x1_jg]M󇫝-͜rsws'$:]}Btfu,lm:&k B#Ck$aȬmۦ%"ډrWvHtֈXKDm˳>fROHt"br DY"$2٢3"tMXAFՇ ItNeNhgz3lG3㫑~3󿫀BLM,NdV댶0s`?;6\^1nu9Qun>DΖ&k$aȬmۦ%"cp<Ça9_etʽ +㯭&g'jDۡC#2]CYIdt6lv mF%l^Y4j۱8dȤ%dA"IDllF[ I"4I$Сe,$2ilXmYfrU6;i6C#Q]6h,KXFN_nYw"Lc129ջ6\p8dȤY93qfx1BDsc,gNXFDD[٧p<80;ݯ^1zz˻w~ۿ t6q^A H4VM;Б6$I|d{4X"Vv+;$Лlv A@Ө$2$a,KDY""m,$LDY.M6XA H4VM;Б6$I|d{4X"Vv+;$Лlv AT~uNk8љc1V~#m?_EaJI"(KٺL69g\k,ze,3Idƣejy+DYe3qsBsZ-ˋߧns12={s 3β@D ɦI"a6y;6\7"3󡳢scl)eYD&a,H;9Qkl1WrxmdR  ,DllFޜ7lseFwy; 4 ,A"zeQyfv22t˵QY'k eYO_X"66C#BoΛvC;aeߝ@T '4w}Dt7-0_ UZ D(KyC7CqZ2߀G-fhLDh<}.XjD=ocos'[܏?ZzlLO!:?w7k b6盉2e|f|s#y6Co͵#;&Ov":웫e& Q2kefu`m$5vdgDfeY2d3u>9Xgu"6F0CehĪ;~fY""݉Xar62֎' v"K&f8ݙ߿7e|-\v%@g^.kxr~y9~Zڑ67@܎WKx!e\۵Xn!“/3?./[9?r-uq!DeI"F9W#Gec0oPgVOʹY4#y9~h-uB"N/-ؼO$/_ʼn:s/"GRtyIDȌt'3x̿2Ј< ^p98d:Ǐr5"mc k ό!/+KĪ;~8reuusv6vN.fOԤM$2 t&@<:yСeYYf""D'"4 3@#*;+;g7n/i,CȤYvr7ʒD'H$2W1Id""L` ]y(3tC#ʲDE&NDhfρFvZ#\ݎk3r9 _pC{+Ko'"+]C~`~e95g +1+P/*/3WL|9\d~B 2YוwWې 5"se,&"ڍպv ho_(K:w师%3/ +5p;A[vucz9Wy9^reОz/_ +~Z~f΄8=933@#*;+;gizs16kCW8'_#z8Ӛ酊FdGukY܎׭@ h|'%|>X;ʎ@֝\enj@ *̲D BG:scAM:IgՇ%DDDIXe'sc:WUvLbeg*;fMQYf%:ҙhyl}N:>pfC;y)x+a8C_Q)u'W|__Ѧ%?뻑+z9hs|U_t/bX_EZ;}yO?+ /HU~́ i|;Kw_ո.Զ$;HБ6IlȎ럯EBK-%x9w;ѷMj߳$~GS'~,~lN~r~iPoHw)q9^xIh /4Z,W^NK6==K/<3-.RX'wYѹ1-sHgՇ%1 Me<ˊ@vJx%WתePKY+1(pt^-s+x;n=cUy%_Wf^^ivTsY/K/f䅊@t=?v~f?r`4S^/?x:_-37y>d34Df2kGvʎY ,Necƪ5232ҝg?Ge'%vH,& &~hgN}fh+;$2e& ֎lsssw<QYޝ$2ƦU7 5Nw}7<.4#zKs;z][__Dt :|ɧ\9bs~ 6s,<+S+O5}5W!G'd>'8^ +Joo¨ gYBz(ׁ(m >Ul#m$ +m3ye?s)c{O{~#2Ű[8&8JЌ*&mf6Ocњ W|@|R{x>gK-6[6siT g6fͬv&ڱexȓᯠ&uHx#gfǦ50b4 lyMƯ7NŮ 'XI@N.{4A0g\YARˡJL>ң2M$+zL$eO ~rv2I;I{p\2xjv;.jnZ~/B#)ӂ^ +^++~7~Ѩ/w0Z?[z%2N'\?9;χyxvo~oYۄWulv ldL ëANjۓa!'? _ѣg")|oEsww WxPse̓2x h4~#)AʤB^oϲ۠#7 mt!#=*+K^ Dl0z޳pAC(|_w&mE>th3a/=VzAxew^Ys=î )ZʡJK\+'ʘ2Β #,AC Ayc3.]Jӡ )AK _|&ʡJK\+'ʘ2Β #,AC&8ܺiqV|y_ .$\pK?@ˡ @x2xxYWOw&n!-Uíq (4^D='4o8z5=a' 00y\su =`:(U( Hμ.%t #6/v_ಢi+vmE{]1IfC+Oƃwқzw{ul6Y}^ݨ;7J'̡s&Hpu$8ixcA618xyYoYZ:] S~Cӂc\PzAD4S̓C z G|X"$ R="Op>{$ M81l7P~m-{ Jxm)/hC$^[a@7GdLdJ jc)W\\Y%H>j 8%xe%I%.RXoiCs9 \+)1#/$8ȅXy +.D"A'WC %N yC rRg C9V:AStwwCbݫt+l gѿWc޷17 ׸ʭn]PvPWĻa?ov&_P_W^|>tkm}_RoQ/N.is4hLM +h6[VZөCPi3 H$H>j{ c0xψ ΞTz=ޘ&ȿkб %}xAprd֦,ʺu/]0·$rid`$\|of$8?Vbӽu~>I>4L35qӷP6>.($\;Xı#$xe%ΉNoT3Ag }(煆z=Gn$Vt(g0hUNl;,Ge9 + {ONSo/NwgK\,1aP00AйZ&Nϔ:)'9h.M}2l#FNC90俐&nHt@'$7\C:|h %<.z=~# +'?'^Ϯ vB^ O.J x|֌о햠u40)s7#S#ʀ2rɨ#) H d9S%ȝM$K$ٿPA@0uXI@+G^\:J%%& ': 8\b2qzs +.R Y")A@NC rCDs/>clr^Ņ P8޷Po%ya=58.݂t݅ o,ʽ~h»@Afű2J/MŁ28&+ P;k`$n}(}^+c$M8/q:c؄qG_n9Y\ADRv 01Hi=e;9d=-w7 Mk;qKmRbraY@ #J2 _q>zQ!AF2T6G%"rd ': C\Z`_A6Gg'(g*%,v|L}Mp&ȉ%yso|! Հnqq4}tiV:pL߆5}8Rf:&A;C 'uʡ-"7pCB{8}v4dRez=ӗ96ޅL9VNN&dԑrfxadX{vE-ˈS`qI rR)QABߘ٩l77!հ&VJN.,Gmt#CG%H7K`=8L wA_I h]aoĆ [H {,NuNEJ$1i< oR8AϟwLtcR`X 'gpG $8%x `C9>¥YYXGr杳мf$8Ȩ,JI$OHM9%cPw.*A.IpJA0<)r|,?Kߗ& ;gyGB28Hp89N.F.I΍|tr si& ?Ob&]GGr9^¥YYJghHpu]^.M ᙼT.󙡣ޝCvN珂Ňx,لf׌2⮩EP ~Д 8%x8#yq) fP [vl,2liMYoϧwWϻuFhڡyGB|a% >*%&?S")a0PJi[Ssoy4Z K]vδp,3/Ca LO/hݑ{y0lhѿP dTH1<4ԮY/ܘP]$`?X"M-DR帔<1$M {Fn"j^JV_L}|wxhzkħ$,e4Op@]1-u6vbvх5 &hAzyR&uAv:|]ѐ? -4_ #cG,/>KL.胜;qyrA3WwDG"Ot@0?#.M%9Ygg~3O<)#)3Y^|(# \9w%&Cf?z@ D`~RG: A8%ɹ.R.U.X?_}/Z:i%]Cu͡`@H84 _胜8w>\b2t5/h7a0`{ɣfvr5;?[ {ݲj^[)x;LO_ǜxk@qkc=cqλk%MY `T.q%&[@D1)Q|Oq$ +^oڶсQsIp$D'ndJ5 #p1y(<="ї}f}; M|c^>Ȯ&~Q1YS>7; D3Ww oQGLmR]eLX/u¯m7؍vcG'ȇx 왡rr{YL*l<67lnKl<陬)?߷7/hzsX-<u7tcqmiJG32:C>ɥB_PAD`F;ܔ,iU?7fx_^Fg3@rpi*ȡ >tx_Hx_'# Ja'Nb| +^Y68-egy\brQ$ RXЦurF3}w0wȼLdòe gsɡ+''5Qy&&'5K>9)AgOMDr&ȝəKe^ 1 +O>/ӣ%r,G噘,A2>䤦ӿy>7ApNzddHJFs>\xr.=tЭ|RHκNYGA#㥼$#{f 2''5%c#Fj 1#9/z $K5]m `li  XFwX +IL;W%`$qbq + ^Vڂ!pi{xӱ{T4QL5  f|>s,f-M?3t@8.dDHK̡91 s9h`_bImi%:Ӓт[y !J9 A"?DA"Xi +hFf玨I_rOe;= @e+n9N/蕽E(ȗ/1!Q);H e}OkmCGVX/&pq +&w<dT KK0e3MvrOK·0EMj }lc`SK\nA/1!QY"9D"ƉceRF~ɓu,!u +͏1_)V§wα0Md qJtP HdK'LbM ԇ +dqW*â/~,zk IvϚnP8v ҁ KK S"_F. &evi s|/ +<U:^W4¥D"#_F!y) xEpd BXÏx(8Ȩ,Ap A"?D#_vނN0q.㟉ϯof-]_ZhH$yGL.9V&%zFb.|'rԜvvu^L[%8N䎇w%G;츙ʎLZll&2wD HJ\D" /KLN4cewA2)H\$8 DaI(A@FeyWӱr\ 92,AFeD HJ\D" /KLN4cewA2)H\$8 DaMz9'wrtQs"b'}1qq\݂_H|ᰰׅ[pHDe<}E"0)#fjd_붝;+&{DaI(A@FeiR0/2_z#x^o-l4#"0+qDb w/19 ITFLMӇ#} q 睳mӻ G"Ab0;֜dTw.}e8YF.AyRg|T G\J8:wGjSl;9mi0qDd(1A.c~EJLA# aiY"X *8>Ծw`=_cX=';sZpNڍα)/ptXe%&'12x; C rtl5S2$޴D ^+{;p$$ rGš0QCm"JvY^,0M)KGО$=잖`kO|>a0a>*%Ȩ1' 'uʡJKKJ@:OgfUpOq[t{9)#VcT R`rR< riFLv]h\M$,F',,}B!ܲcR GQY|쌳8rT R 1Mfj.%>$}7hܙЋx.' #/?&8e(A! gA)Eݑ mP:--t$rTJQY|쌳8rT ÌJ5HܲXt\g_v +??~ ӗaSJsrRMAb<m4/uNtJFG>bR'&/O֙:%&'H$&NK1kӇh)ΖP<2?F|>olM F$ɚ2N22꜉A"crٴJi3-;@+}>6N\W^_#w5]=6;(4ri`rC_yO$rKBSjHPzNB4h%ZdP7٬ O|:-u؍pP&Q@YepLx( )/HViODbCz-jŔ[`+''k2*˘axYv=x,syլwwF"AIM$Q@YepLx(tXX B?:|>Cl `) 2꜉A"cr8طFh]ymS'ߖ5Hd|x:#:ѹSӣΙHJo :Ju2x8DI r~9 |d%Vo {y>Hd|x:#:ѹSӣΙHJo ΐ:qtu~c삨y/"j<ޗ6XU.n'?qG·xD"+_ēO!'f:1};jFb}7WE<9?\:/ h)ڢUJ>gEk Z=X=Cby-p&·xD"+_ēO - =y?y?u~uDR|7OЉA*a?.;y6vk伪@;G8 :.z=1ov>ă%_y\0K|bK>youˈAۣaAc@@'HJ; @Q.sMj!$$8X fRF.q +&NH$&G,AC A=sNYeMe x 2>%H#//KɨS"938 {~:K '$28Hp$>N;;v'|N'k|V`X@h1CKG^^X3)#8QDrfp$&X6w`5BBd}%-Vok^a}Me/dD ;MOq^a<kk%6`+o=&4! ADz @z#}!*<*O,AC A=sNYeMeĂ[v@8Aw?.y<57GX4j+ 2, d|,KG^^X3)#8QG;X%ke8:Vha}3lyڼB[8˼ӡ:fV%ioF5Ty! ADz I:\4 r<.<#M½~cvt< KxKHCD{9>xHs,M܁sNJ>:V +ǚjwqiF%h)윎IRW~rNR" RB) 2x;ep k&<9;<8&;kNOt"?#{耀\X`D"윎IRW~rNR" RB) 2x;ep k&<9;<8&;s&4%N9y띴~ńwYVV̺bZFaOANIJ$d\J 8$P~ `5*UnweugۄoI_IsrvxpL#w>ל4w>8m~XUV:[-YAGXI@@@qz>*e|' '$% 2*MI6>xOq}'^76M'g:IDK $a VN> BYCO'KI$<9;<8&;ぁaq' X4fi'xUH2kp-ARqv胜H$ȸA"q +˺*t>NNTYF; rg N28&pyJ2*%Y9p SS_CDbӱ`2ȱ;'磧G#Gd耀ԑA?P.YI@@FDR\:+'W>4Ap +3#wC|(HLp:VLF9Vϡ'|ynr]=zc kbe\r ϱc%I rr鬜\8_)HΌ\5ѷ>2B%N NJɨ#JrR0`QMl/-l!4 f(% NJɨ#Jrruʏx%rbeYx FNdxr鬜\8_)HΌ\ީ/eD"1-AwZ#--3';XΏ?w66dXg=$ R")A@N. g+ 8IPN.)f.QGG_yPJ$QG1t̀caҼ60e5CoP2QDRəw3V_3H$&\bgy4+QfrYOUxBpp69XC<88GI9*KLHp$g9XI~D"#2r 9Ϋowν.>1AxM$dX+XiUe9<},d耀GruŁĻ=-D3H$&\bJcascv߾v|>!YIn}T d4K?إĮS.#?aF2QDRəw3V_3Xఌk࣭ CbaC:X?ZڐlHb%FxBX|(/M2>P2QDRl},uswhˁkzx>T茕WL$28I(ާNx^?6|nϜWɰm,17xBpp69XC<88GI9*KL`=c]ΤC@8"Ph8uɰa׬piVr ral[8\ߣ3A?睯 关ADzAYrV+1lTû8L'|w//h")GJLArP2223/ԔHP4Qɗ/O$$C*:˸{yAI<8Pb2c䎇"yD"2Ww|(ON|yr 8K8ۣˢt)u' &k]/JغM`IArP2223/ԔHPՀ& osӽ:2e˓{gxD117`w?KY[яվ)X=c^Kܽ r(1ʱrrCτ_qv}mw}sWƯz??[xwSɗ/O$$C*epp?* j̶ +Lax( 5%9M|Cyrve䭕5vqG)C8 tk&!eXhC 8P;\F^F^F~慚_Q m9OI ϗ +ȗ'NlP}: (m*۸b4#Y%& C9VNx(ryyjJ$r(r KjaZ=jkC@? +_MEԔy!O\ʡ > x3Iw//h")GJLArP22ri[1iSkvЉ_KM;3fsq)?=|1`H%3#':19_s$H~*#_.'5KryrL\`r rA$NSK9QDr,<щd N#A}Pw9YNI_dF'?18nceg}VGVek(c><)9*K$gF.Otb>9YsӿH[r-oVҦz{Ӥ?| ,ANt@J0N}F Kg%[7kk,.xr\qO:g''%3GeN̉%E4]]o8^|_򕹭ҡ 2%rR9 >*'D%&wz 4A[;8G7[< K 1NiMi^DrfD'擓5'8+zOA>CeK-4[O G)ԁ |ႂN3~ZҵXŔ¡w3̓əDrfD'擓5'8#sJP[LXO#=TF]Nj 'F\w !=쾈Ϋݚ;-;oiAƥg}|yR"9sTHΌ\|rsK7dot8: ^߫ݹq Ece"G;=tfpXsXv&帔uϘO0OJ$gəK,A?-|>o$)8L81W%8,9&|\po)AJs NGC5P")D\]zZ<%rD"8%|\Op:Βc % fd$1' tT9q{^X %r( HPΫN% ߥFK}ws\kU8?gu']%RZ=pL$yެS$3aq f%=_]X %r( HPFҨ|:nhϧo۵㍑HX7óhQg9t%9ApK":Bv#:_vO9x-m.PwEyaM2HʡD"/h"C9*VG\"H$NEG8rH6kd0o.%6A1KsE.D^7 c%9̡ šd(V/"[jnmpP߀4D?V[%knh%2NYrL$yެS$p3>J3 O%{^X %r(ЄC, yiy&m3AY'8g1us\H{N 2VXCvn ?Rv8^8y$|lV(K$14AJϏKq NYrL$"4-sNpfp@?{W|:1Ȇ$ \KyGg9 ?s\JD1A@28r$/t(H.#H C rR3;E?&NMӔMY˥GZq)OHʱDӡD"A@ʻ*ZLKJ*4OJ:'H|QGFiK w|>Zvը7q)/,gK ?&H~R_GR$rv˗nB~}&kzeooH$28P>A@+:%3H$y"A_%,2ݧ }Tu)٤J#'ep$XI"_P" c]G"Al=-/l}c+_Þ|>_[cOb0%RB' ԗc%|CDI$VգЇ*A 0ǨK gPXml^0A_%Y':L8 +I8J$2޶ \py܌=-$ͮH$ygS< !]gr9.=s|q)OHʰ JB:z*z),T0!˨S")A@bH $HM8ewA2)++I CyI(A@FD"Y: j._I:%$ raMD^QY~Gd(1A.c~"2KLA0:dTJtvu;@w&J7~맳YMS(]nɺ"X +ʀX.ep k$&H2x; C rJkE2ZLpO2I 8&C'PJ Cn_eUC=xn\nX4#GR.Ny"A"/\X 4AtTJnaJtk'y"IkxA epLN%&gɇ#{\D\Jb-8g _8|! ϟ}`}w&t%7Y M8ewA2)++I Cs-x.'k(45Okړ:nKǢ< A.I q:* %&e/R"l Pjz%& KLx@`iPQ0NIS ɨS")A@bH $HM8ewA2V:V~?_W$)C=t +."A.h6-&&˨S")A@bH $HM8eaYbڥn܌)a/ _(a<Jɉ&ɽK,rTJ$rTqz茳8ə# ԔH$s|(ԗ .ͳA28HI\<8&˘Oys+'' '.ͳQ)A@QY3HJ$g\`rRS"HdIS_D^^4rN 1'Nδ$N_{KSx8w.7cAiM[rTJ$rTqz茳8ə# ԔH$s|(-ZXз=pi2Abnd'??v妫Q4N6M{Y娔 H,gyq$%3Gn@`=[Xwq?j9h^|>0;L;ީ/A"//\g9' epxpL#1 $DDI DE{>z&-IZKIi +DgőH 䤦D"ȘCypi圄 SO2|O c5*52ېJBSQ)A@QY3HJ$g\`rRS"HdX4 |x_D^^4rN1@6m@bwxaTyUiA;4A@@N\gR CgőH 䤦DbmAZ$|7ts^'y ) x2擡DC)199wpie^J D2NqGR"G:q"?y[|esk# l 2*e\Cg %IšI 2*e\Cee 7~um^'O `F:ˀdTʸxWR~=IyA"`rCa aIamO[M g Vq^ zeoKLR 9|8 ': 3A@| ~aMbdJ"M. BIЇ4MA~C A@ Pxh(A0A:KLR 9|C-B?|IiCJg+UV_I{$eG< K JK:ꄧDAr&NHςכU{ϫt86fڦiߤ̎f-_I{$eG< -SN,N/ʿoeAwK7 aMfgC#=t&P"/a04=I 2*e\v"/HL|P)`=aDg$ :N_"H k^D"2Nu2N kd}rAu&c'wJr[: (awGGb 1gD.cNEӌF'W^7I28&_ ':c%dԑ'u8DR^y%2NBǖ %~&?}~' +ÆϬϻ,?V_i}>neN%7I28&_ ':c%dԑ':^2NAL$Iy%aM w:@~F Fk5lx)3/;H$&ps:':'u |E'H/ WlKg|||})98҃2N?D"k^"A@pcCbM2񕟬OEe wS?ҏ*l#Y`:ӡ<9S+:aDg$ :N_"Hʋ5/2)a +"aȏ{_'jx5,Hx}dD^"|yP)` ~3VLIJ`%Hgbd)GA"1jWZ\LC x9)H$&F—:H,5 8Hp$p$$8PF^31A^fZ}%-o6eKow%AbB~&Ȩ,qI9 'uCpH%耀A"/_yR DR?:ʸt#H'WG#1 S-`MY~ +ߎs`Fh Į .JQYr(AN,I91_K9 D^zÊ86_l&҂_ +C2Oo c%K:$cˑ,*ό\>yD"oa6|^WOn&1GYv=q ld>\)8\5_qRW~rNR ɳs " ^U$|r<3re_`S?ߓ;ȂoOWsH.g^8=?<ŚəO>gF.k"Ot"?s%O1 G/~!('<8 o?0'78Ddʓg 8%8%;D@@@.L|r<3r9o:o>d E||IfOV +x}ˑ,gF./*|i/ހkl0xBxti%1H_qRW~rNR ɳs r̈y!l;ɟ?>I܌Ut9'43YrLt 8$xpLb%dԑc%q:/9q$2~ć&8Kd N282֙\c2H$%YIq De c+ &+ /b ہXo_+~0 O2]o(Ku&:I rruV$r/q&QYӡ|(%HJɨ#Jrba-+w^$H$xpLb%dԑ^PSCO4p?s>Jǽ} ^`ӡ u&:I rruV$r/q&QYӡ|(%HJɨ#aN~?\6.F-][ƚpE Õ O֟t()cɅ>&DR\gyKITa +Ф[A?Y+ahf:X`>}X]mµ^8/9q$2~ć&8Kd N282֙\c2H$%YIȡ40b'{wf2󛂵d{ +ʓ:'gyI$ȸʸ%HQyEI9*KLHp\r9=t\#tA"1 e8լ<$rϓ3Jʓ:'gyI$ȸʸ%HQyEI9*KLHp\r9=t\#tA"1 e8˹]7v6~ˏ/$O(i:1ݮpJõC38 Iqq5+K̋%rT ʓsz GL Db%&0J Jn=@RZ0 ƨ4B$PY>9H$K:$AUլ,A3/jHQYx5tZkT:A^zt*_#ios˵WL哳?O+ r(O,Ӈfp$% *jV G5Ks*Ol[m`Tg\ȗ HK)A HD"U8@NءLL0Oʸ|T Uƚ9S~c=9yA"A" 'WGd؇ݓc6>VF+r]ءLL0Oʸ|T Uƚ9S~c=9yA"A" ':fŠ 7 x>ox[F8A@ȗ8r>&g\l%gbyR売 >Vz4 6bx/Fxɫ=<&% 9: C <8X9?ѹxٌJGe |\eH:78!8&iK$$̻AaEo9UU@|C 'N`rqfPr&&'e\>*K*cD)K&el,3'm?c nϷU1lw PKJA 3<A"_ɳ \}~s; I$O-dh1' W,9P)8+9ʇ٬JLBS Hq93A@@@@Fe9 /qh(ԑp#/OQY"'되yd(A|CDIylY%&)$˸ə J84O{oL6?a×6|F,oEqr9.ʥ)b|CDIylY%&)$˸ə J84Of"qCaMJO'7-ȱyd(A|CDIylY%y(asp/j%:?0yĂ;x@ˑ 2*ˡyCC yyRG'iMڦlVnܞ}LPz %ӱ<|h )/:ˡ$8dy#9dTC rba%84{5siְsb  bN@.YrSp:V's$ųYg9|U-?}{-6 bA{uknr턧\:r|\CH,Iy DS28r%yrH$XH|uJȗ8%8:I̱r~Jˡw|\CH,Iy DS28r%yrH$XH|uJȗ8%8:ILo(ݐ3x>?8=܇ 4Iy DS28r%yrH$XH|uJȗ8%8:ݭZi iYz|7o_pp nxt|\CH,Iy DS28r%yr?i㓷U2qx 䨼d(A"?3%N rNR"9sय़r(A݋,{Kw0'o{@a1?%N?/#)Yr'W_Dʗ\*Jό|S78tq|*?ö]B?(k^d9rN@ Hd1?%N?/#)Yr'W_Dʗ\*Jό|SL=8lVVEmoesp[D.-nס_V +*J$Pe<]r(A@@UΙ': D"AXICs y]~`ɸ'y,Ѳmg7H <8SB 2*%J\K\R%ȸJ|9D$^f.aRG>O88z4 <8Pʘ$eK$%W\Gϒ#q:: SY^g]ӋnR~ox"+ij.Ol%.)d\%yy KJ$O$xp >1'HHJ͑)3wfӏxZqԌ\?}mK.uJJQ)H$Pe<]r(A@@UΙ': D"AXICs yL%gsܛ7ְG"xJ(AFD"C KtKʡW:gD^)& f-Ix ov~hхqqd\2xH$O %ȨH$r(Ap2.qI9 *U0Ё62ڸNsyR+r(AŚIy9*%Hg") 819Y\F.A.jJ$9hD.ANYΓ28ȉHI~q˘OH\~+r(AŚIy9*%Hg") 819Y\F.A.jJ$9hD.ANYΓ28ȉHIvpȶ ?^"~n r <єHQ)A"?Id(Y^I2x2r rQS"HdPD$r rrurANt@@0,7(_ۘܲtcn!rLʡk&e娔 2,/f!aS,1hD.ANYΓ28ȉHI~q˘OH\~" <߳7z7t|}a'mơ4 cL$epgy9&'5%EMD"1'CM%9yR9с ۯD(97 A ,>҅,] pbͤ$3Ar嘜,G.# 5%DƜ epLN4A" 'W,IͰuND^9?1`#'p9Hʱ2)$^^RD~&28Pee䢦D"Ș0q%}I70!aÔ0x._r˘OH\~+r(AŚIy9*%Hg") 819Y+O;a{2I_vQ+G,.A0AJ\+:A'gtT cW@pJ\D'rdy<#A^ҡ28H%ȉK:t&ȓ#8+:Z`|9 C< Wt)O,A@"/ɯ#<8ȉNDIyRGCepK0lho{v}B7 ?2%\2rySQYD^_G)qyp%yI jjۗږoLJ7xX |EGKL/d(#':1N8?%HX9xLS_#.ׂ48DG<;$*P'gtT cW@pJ\D'rdy<#A^ҡ28HpU> ?pe`xoi-K ]J\+:A'gtT cW@pJ\D'rdy<#A^ҡX,ǤVpjoxְxKL/d(#':1N8?%HX9xyԉkz^ÐnN`MZNt@^ҡ3A@}) _ˡNpL$NyrOGe y9VNNok+-w;ge?_iN~_}]'-cH'H$.)ЉJL*3ANtNC'u$Nd %&ό}DїS"/O! H8=t@.&yIJ$2yRD2Nˡ$2j>DN_7t ~RGLPbޗHʋ})8%d0 _ڟ}}=N1 1$_9LA":C'r(199SW &ԑ83 ?3%9bG_ +N n%nc|o#߫u>oU O},KOmehHK$2yRD2Nˡ$2j>DN_7t >:%LZv3z[F3HyɎ~# M$d<ˡA"A"/A@0uD"A@HrRJ$'gG./֜:28jV1o+;LbɆm>>>>00@/tL|pE'J|# M$d<ˡA"A"/A@0uD"A@HrRJ$'gG./֜:28Bb0rPZZ?-E侦IxvcQE^^h `>yɎ~# M$d<ˡA"A"/A@0uD"a"L +5/&||~'طAqˋ5' e\ΉNde^04e25 !gs1 M$d<ˡA"A"/A@0uD"A@HrRJ$'gG./֜:28.̣fMB~Q[ Od!Z\ˌ%;D./4H,#/d9A"q +Ir(,}Xs2ȰZuBzi{vގ o=o@oÆj* H$&O^8HB2r(AHKLF3H$蘸TV)>L?>`>>Oc4 Հ%, /OtNt" ,BKv\^h" YF^% y ie_Z"K]ǃAD^G"1AR" OˡS| \!A7e^^Ԕ O 'uHdȗq +y+v9|ԑ DbD"AԗC|C 'goʼ)A/AN,': /$dW8 LC/m9=筑 UAR" OˡS| \!A7e^^Ԕ O 'uHdȗq +ycIXĒv)"nk[H!֧"O >Q? :e A"/# )H'P)c>瀀D._ Ci +bK4C'֣\/AN,': /$dWr%?#e7gww/$cX zvo瀀D._ 2//jJ'K:K$2H A_]w>Zt0<uhN ': /_P:2xp HLAH$ȓr(1|s@@"/qd4˦-KOY÷=J~C'B-fw ֹ$'qK\#Y~y<#ANt,AJD嘀 epqs2:Hd'O9L$Xs\L$%H|3* 'u'W,_B+Σ<|I rd |TJ$2.d(qA"c>yIg"HŚ2xr{~~ߘɟO=/tJsP] &H_qG3OH$KR"q9& Cd圀@N<D"/j ݾ?}~Ssoe*=4 9S?:g'H_qG3OH$KR"q9& %&d-g4':Hd'O9L$Xs\L$%H|3* 'u{}4CjMV0`d'u$ȉ%HQ)ȸp 2rN@UBC 'uD"fz![K2>aY&uK >G(>*yRG Y1 ,d\%tȘO8rRH$ iX-:lh/i)W y;0 NK\#Y~y<#ANt,AJD嘀 epqsbCiNͬNko7av?h?~i& - #?I _y28I29˗>G(>*yRG Y n˰#hIv*=2<׉@@J娔gb'$DR?yvF$H\D>AR")G^`>$xpLb)dS@09y6yK:d(+@@J娔gb'$DR?yvF$H\D>AR")G^`>$xpLb)dS@09y6HqisE݇/~dz}>MеHLp:Rb2@H$I q9DI y PJ1%N`yX$ֳ߾4кusE<ہ󤞚'$8ȓ'8Q)1N H$ ~I|#B~8&O[y=|(%HSp`rl9DȓuPW' >x_? \ZCOa0#a'$DR?yvF$H\D>AR")G^`>$xpLb)dS@@#LF~?C>Au}ⷾ:\ +d NrTJLƳsD")<;C$.G"胀 )#/A0C <8&\Ғhm߫ @XϹA-@@J娔gb'$DR?yvF$H\D>AR")G^ [;%Oo ߞH=⭱gД'u<%d2 c% d NrTJLƳsD")<;C$.Yfv["_(lX =<8O̓ANtG^~s,AOL$%&C$ˌAɕ?3$ H$ȸʱ~R"938EMD3J\ ':#/9CrT 'Qy&se CC DbJ$d\X?d3K<G$D6`#w CG9 y=JL.O>3 $/3.J$WJ$P Zv.fS~~m43J\ ':#/9CrT 'Qy& ^GlZ$҂Δ <Aɕ?3$ H$ȸʱ~R"938EMD3JE&8_?0h:Yo>D3 $/3.J$WJ$C {u#k7]#4TiXI@<9;D'x8g(1X?Y$^$*DRb2t@bNf38T-?QJlk+p[~ȟH rQS"̱)yrv8ȉNqPbr~%HIT.M4I]k5i|&u?OlU[=P%3q$':t(D:N<2NI'H$r(%r(Ap +3J2*op&ɕH C\/1 P"9,A~K<墾Wنt">wuΠ݈l%;4yIGK/e~Ś8:*KLNep$28&Ot,F>K$VH,f (T--D>Gz.y &8y&G28KvhG qkN^,1:qz bvnGỶoGo- fSPCi$HCS.|':D%Hp$BӋ /)0ܤ_h/a= O6 %סϼXsGGeЉӡ D/H2r S)L7WQ>Ws,e ܵ\,d&/h %סϼXsGGeЉӡ D/H2i`+gӐ4Cvkx Xf%;4yIGK/e~Ś8:*KLNep$28&OC&TM.ǿy'rf .jC1e2OtpJHŚ$N5 /١K:Zu,3/֜QYb25Zր5җ>XBV7?Nz1'g9`NCx6\ ds Nx28ų 'G;Ͽo6|@>t܊9aNg9`N)?y >Hʋg\K6FY/Bwn-st|\%!HٜO':':1'1%r8ӱ2ʤS~'8yyS&Wyҡ Dߔ߻ >$l'HO^I9tXceR)?_ͼ<щ)qXD^K۱}~ґvuGC,)26#4Ni8 D'8D'$擗tDR<gy:V&2X1ʏg3/Otb~J>4- X@rI&x;C4q߻ >$lΉF^C'8{O^I9ten$ ۼ}4w<`,za1lNOǚ$r\I^&||S^g/ a7)ίw?WJCd3Գ':':1'1%r8ӱ2ʤS~'8yyS&)6ѽ"Nk)w5n00IOmak,eqX gs>A^GRĜ|򒎖Hq,OD+2OlN'ג~bl{oux"k`Ji̡*A"/|< <щ9%-,OY >V&e̟bf\[.;XОWo!쥶t_WݹimʓL$2eqX\gs>/Ot#)OtbNb>yIGK$8ӱpQ=$vA~|ufKzVz ep$I D" $%ȉN\Dr(A@@Y'uEv$*%^ +&22d%DR\lJ$2I=sJ)#HJ$%&,ANtr$C 2Β?s/&Q)R0$xp >1',y%estT,>m;@H$nBz!)y 2*KLY H$.)d%:W8$&Q)R0$xp >1',y%^5-Wu[۝ɿϟ¼GSAҳK#W:ep$I D" M%s/&Q)R0$xp >1',y%&ҒҒ+)EZJ)#HJ$%&,ANtr$C 2Β?s/&Q)R07̸q[F&o7*EG%9s\%H$%Օ $%:q9KʡgɃ9񇩷f1GR'%?9* yIyQSD~&&' HdpL?3N''5˓:28N9h Hd 9rTʡ W5': DDD.A^ҡg^Ԕ ɉ&' ~I SNt0&&' H$<8Ȩ,cNFv+ÏnJI'q~X馨!#0l%K:̋$319A"`Ot{LLNj'udprE319A"AFe-oq#mEoyx駟|eCɚM4\"We"A@@" F [zh( e#xϥ DI3xA}k}>: ğ% +ijcrQLLN4A@@H$2xpQYƜJ9*PGReLzY{}fW[?I"\ˆ!/jJD  g?щ9q5K 28N9h Hd uoP8W/_}CPXH%K:̋$319颂`D'擓I`rQLLN4A@@H$2xX--a7-OUC4AP9* yIyQShA"`Otb>9YԑqʉN(M}jO+-a)JKQI𨔣Rep$ʨY?%r|U] nF$K_`+˶-eeT7!4|i\wRYeso>'Tzg^2Kh ɉ&; ~cxNHt?@y>^͝~+ب^+!{\:/ANt&A0o; q: HdpLQ .>.%t"/H'u$Ȩ, 帔'=T~Fʓ:2N{-{\:/ANt&A0o; q: HdpLQ .>.%t"/H'u$Ȩ, 帔.BCK[o2 77 cK ȗ ':HO 8JX<ͻxT`K K$$xI 2* 1'V q|Zgd(F;oH2ЉHeC%cܰJx:bC$N oti&1 يck7:H$H˓:dTAbVr\ʓ*?GIǽ6ܶ|Oa3xC/{Yorb?ILH%ϰ4{`ӽڸЉD"AG^ԑ 󋜋 BW]ylWӻrCiL圄ialO RB'%ȉ$&Xt@t(AA09F^$xہ)?]0ܟNYA"C@N1'':'uʘ?I 5KwKL.O@%&wO$H̓yRF.qR笓|A':eɉI2iB$ { $ A"cHgj_jov.~;A` #OI> HduI2*NHk9C<9i%H;%&' qI;'$tu+K|>?>9Igo` _ws!Y6Q3sa#4[J`:^: +n:e [F2$?^ _tMDL?rAݝ HܽsyR 1O%9t\ʱ[9';:F|_+[-A':eɉI2-2<P ~w$^b9 ~gD<>._AO,7:3P7F76笓|A' cNNNs{ rf N} Ʌ Hܽsb%9@v|>?~% 싵|vt漳]u28Vt_99'uʘ?I 5KwKLEAڝj !wB:aȯcg#i< 1O%9t\ʱ)rbub9':nȚgj^auI2DNsx&(E__dlx<>^8{|[jʡD"A@@ N?_.C'8Hš%P")ǽ y ɸwPSD^b{N",$P ep$ep'u&w/qʸS" c/q$rya d(^d;)A"/1='Hwur(A2828H:ospm@>0~>AL6Oꗸ y 2 /҆2,9cQ /%y9DsﯓC 2AA':p7,߫wDpm\J)HrOK'Ĵ>w~ NBߣ]M^Kgދ{nI9eKB7H ݯjepԞ/>츧mS~xAxD"Y޹Iʡ H OM$2^q)QG[-k/gL8XC'8Hš%P")ǽ y c2tBM y ?s$dy_')d( )Ce\Kqٍ 3?OZt9+~ZJd%2tD./9o~ 2,/ 9= + <1Ʌ$/sdy_')d(FWS /VWi_0l RND~R]NXHN4%&K"d9 {5%H%&_$-' 닞`,ȸ{$.SƥDUD,JIp;A/qq oZDSbJ$帗#/A@0:6bF^@rp,28EJB}9&Hd; 4A@"_qNc$Hdȗ8e(;w.289 ~ 8HqyID"%rJ\$r rNr} /Y8Kr~ ȱrJ$2xK2o;.pvWv=~~"G}}pIsGR"H\/A б +HA04{ŽW9gA/Ap9VNBD|S2,n+3߈;wA~K ; `|e^GR"H!$nI ?AgP+_l +x_ '$gч p#::?^/2l~gH$.R.ˡENKnEVp$gzZЇ g,9ȝ%."I(VbۍxNc_^8d`S$& Yep$% ?aN9"AZ OE$I3K re^ wM.-5AWzU +`h;w.289 ~ 8HpJ 9,gPZDw;i7* +Rdɏ zY/kTD"\yG& %X{*j>[;{$ΒG^q)d9(9I{ԑ C<c|(AY -=ʷ!<3qrT HPF.%."A@0䑗,d\J Y/JrR'u$PF.Ot@FD"☀Jqs|K--{L\e H$9\]]3Z. Ewk$1' RBr|Qr:<12$HߒjЁ7 &<q/%og!Y -=ʷ!<3qrTJjk៹t]<xGAD$ΒG^4ImT ?^9n?xݯNj D?.s6JS{yGxw # eDdTJ{;o9\:dx|+˕ׇCRC9,dxHL\e H$90'/%."A@0m5i'_gP:1.RZc9(9I{ԑ C<F"gA{< J/^V!FzK--{L\e06ZU.W37c䑗,d\J Y/Jrg:aB\YÈnZoOy3G~\};09A%89ARC=DG~&Y,K_FD`ҐH Yqs2.%t,Ji9tv w?%u Xy2*%$P$e9,<ԧ9)e+xaFA"ȡ\"K\D")`gi" -&Br Pږ:ُzz.NԹI 2Q)ȸ860,oY--{xٷxȨC rg N28 2 'JD")A28K|orr9V8ʡ J$Iʱ2 od3'J9 w֟t(# C rr$H$d(c>YO+''c%ӡ ,HD"Hʡ+ HgtY_pnV//?1L%ȝ'8H$P\:+  bdBvؽq`Bω( Gߚog,|. cS5ͱPepHH$P"HJ$el]aC$Z[xn]{n1eOr(Al:sCgp(A<*cW(Pڍ:o* 2 I%/2^ŭ˛:pgn$YxalyAEMA@t(28Hp$N" CD")C9V&0rT'; Omwx{t3\y{AD %ɥ HJ$ΒBҢ>X6'en#WwPepHH$P"HJ$Pfc]~ӗRxY+×˔=wLb`ZQ)ep$d(ANN3KX(K#NHi; M \)mJ R^P9D6 ':qz)1D[OJ$g")V@bN3ɽȗ8$p 2rš%HQ)cg''Hf\D'x5^\_7fcЄ9?˸8ȉN8k<3x:M+AZ7iߏ-;^w({ $ǏIzS~ڕ8dØ +\60{?w|rTF.aESH.cp«v;co-K''HfCp91:OoOq&s7v럳t$0D AHvz[&}(3$ V!{%{'N hp 2rš)4'ң2tKT9VgOOx]NHa:qz)1Dܴ\XEiVڷ>~) ^KɎEo& iȗ8$p 2rĴ.W.4 '/]$}n{Mb$"M.19D6 ':03,K]EK,r6vv9|;T] jx !^˒ޢ &C$=;Ќ|S@b>iEKV}^|M'Nn $*+'ON4͸8ȉNL +*u jp=3|Iwn,-Oॼ H7d̓əHJ$ ~c A|ɯi)׮GO\XsrR >*epḺr̲'u*:4]e%o!x|ݨp\f:}79py_nTa8=ꔘG"A'%3A&bEJυ<׿!Ǐ넗oK L\ %.RrJ$2xi1O䨔 jJSԑt(r LH\-=Zb\"'o;R 99D$8H2rcd耜C yxZ9*%ȅ'u$&8ʡ+'Cp$)1[w?57^^{ Ѿ.II9D$8H2rcd} }v[@|^@{6d|J;ovKi?%Lb0!4KXc)OHLp:C9VN&:vBm_ǹ*j~j D'x[J$g"gTwS|~5lsr-rnmm1m!xHdȥ::"]<-Kx|Vgᤕ`B돟']U?8 +^x/>$11v:P 8QKh]z}}:s?^&H^k?͠4OPY 99D$8H2raNqNB,:pxX'dU"/u{#?^o)WX/‰i BM rrvXyJ:P;&۱+/^+. wSom)AyĨe8Su8m,qIvOKxuFp荶pL^'rTJ 5%c?N!oNrG)1yK HNҴPpBvnX+I +I o-֞Տo+^> K[#h^]KL. ĜN<׫Z_W(muw 0O Kx&4+ GV<eГ}||v|pZ w"3 XIy9axaѵ傝4i.%&ANB009l#^'Nj<+S~ܕK9c!ri Ffd\JYepL,OC) o!6Ӻlk|Ӎ[?_ٖOcf Oo˷yJLN49 KCNLx'|6C_Ku:c7,Qv~͜ %H[]N4K:f5O2,$>NWҶ^JLA@@s@@['Hg8yz 帔qo\"%HNJd\[9wp6Or(΅ϞYdIJ$r(1 o )Ot@pʟ|3Rƽyvr qz;?*%%2ro<ʡs;Å{-/_duu[  o )Ot@p)mURQmVΰ<=L.}nw aNCGZQ),x+'PUy}nfu&?{yG 1JLA@@s@@fB>Ş|N9=919P(eAxMuL}㽼cio|]Y"K&vX4%2r &a'P{1Lln˯sӡ$x'9|?Y )Ot@pʟ|30'p8,ıдcc:NZx<>^\>%Hm'C7V )Vb6iN /#VNӡ;5t,rʹRhlTZkCqh٠/O =ɞHP +2:'Z +/oG.iB5 3eo;C9.eܛg'0=_GR#>t` H2X1GD$N?яydͤDR3YK$2rɉ&HpS@,_8I(cN.9D^KyCo$K,$r|LʘONfy~ }k^#SJ$e''gQ<OÜm>ƣ_ܠZ+uKx<>^ߘHL?~xὺ2L%Mۙc"q +NA ~l'9܊Zܮ8봽aД,$r|LʘO#Aӧ/@x[7|[;͈I&DRz~#^JZ7iR>_+/Yq!B ,qtO S@,.>tXe[8&9sy9f#2Q=drZ /f(5 M>P|E<Ӂ# +˗%XiZZP<Oc,19Y3ilߏ3_*qM&ϰgx{»8( ]0و9UٜOBsra $!om-m3}.|gw E.I<},Ot@ nfu;%lx<sgsgit7N#O -!,D$8N) H/$1X_(>mK!].aÐ0K,$r|LʘON4 v#MB5tuu}Ik0bHil-q\br 66Hh{wR ~<9Q&NH>׉D~;^W>]}R i0?c,19Y3)q`ҜΊ3HXs|F>EJLBITJ >HdȗH D%H̓2~RCzZD$ C9r wOJ oG%s渔c}  C|(/qJ'e9~) H$."A@r<)A 6GJ$p=ꖠ+7 >G/5AJ`JLR eD"G) )A}{-)^Sy_uC'PM6A@@H\D 8yRY?)A@`xXug[j?vt^zB,ǥ\$I$aH_x<m94y nH$>xvqRVo=_#hE"ď2XgnXRid rg9zI`_q04F5a-HD"d(YΓ%,GZ yHz?M;o!sr CNrPGRbrREΌ\\:QYJ<8Ȩ,cN.h"Yep$x[D'rDDŇ-=$cNN֙ ?d(`2IHJLNjșKKG9*"Q)eM$KC. t컣_@ƀH$H\|(C2d AWN[Ծ߅EoMNZHiuKKG9*"Q)n$vElױnt+{ƐOt"HL$H\|xt؆ +3 v[' ; NQu$Ƿb~|,ȉaH(GG`8 +nX,d9*e 91vSKh-e% eR''HN$k(@'N9>+,Ce:gb2t epIgQq 3iaYwko%|qailZ3ꖘ"Q)eIDFx]R,uWsI;fnDDŇ-=$cNFJcak_.14ma!,93r rr(Gp\d9*g'sC.MgjTD'rDDŇ!0󡏼3DFg9prAa=jHcfKNep$%&'5K\%ia җ-֪w޴ I HCI9ޖg?AU;-}6+39ᆷLNp X'9s(4 AeK}is˔|K<1?q/[ ,$#/Ĝ ) H  ( 奲Ÿ/%>^!%>~v>^<_Y<ӏ{A;tHZH9,LD"/,Y^< >8auϟ|z[|>^éҨps!2%%H#/Ĝ )0gnk_ċx?v9:Fz" ^%HD i|܇KYBiii:~z"`#)w ȗGe IyRG"Y^:|?j#19Y. uH\DQ)A@09+ yfy 8H%HQY yzBMD<)Oܛu$r|]bɚwpoC': 8-=TJ ɉXI 5KLA@"/A@G^_[xCjʘ'$Iyr`Mjtv%]]:ox}ehzw3`r\Ot@p +ȝ+MW_ExF2mwDZ, +QY yzBMD!m8>n/.r\J39Y. Φ1c/v)H]_%h\0d=iA9O=DD8]~W;|7lV8(lyBMD<)O.@xtAy</ZlAr;)lo'JLn~:+P9*%&'fm 8}U']| ӣ;[%_|.Ԕ1O$H̓rLĞvW+xر{@taͻ\7!qNK[.?V +?E.g؆'ѺJX/HQY yzBM Bp8{ ,[襜N`4p2ݛj9.dͻ\8v :F g vM |35&vv AKLA@"/A@G^i81H?uEmC졖`ER`r9V$B2ThB81K|</~φ \:d\ʯ%&8!2V&Iw A/1!Eyz|љD'xp$epQY999;y9.%H2.%.J 2.eH[+ r\bK wꗘ<=LBy<828Ȩ,ǜ 5t\}I~^ǝhw5V +%SK5=C<8y"/lݰp;x<>^C|+Eyz|љD'xp$epX;+*N3iï}sMٻDKKRKc' y>-ǁ!a0٨Mfyjw3 BcՎY(OtAReQp҆q[gH[2^gOX21PD耼e2 & +n%,DZinaGKPFi[TX&t:P 2*K%܁4ktR +>DR0d\ʯ%&80XpX'*N狭w`<^(. l[䎇@Egal8(ߑ'6лJԑ8e\J\:d\;f z#.s0:+|_p tOy</:y̸ r~Mk}|5]feB-R'H\999;y9.%H2* vIgTi$ǥZtp7iu`~ ce"1A.KLpw #o'}ꭢ\YS~H$xAR"ʻ\X?)1/d( o^X:?o5^qQ 9s{{@퀵;4<䐁 %3qrT )0t\:7suSKޢ5QyɡgAN)TҨ}B6bE7a<;Nl -pX[J$g",A5'\%v΃| 6;-956OT.#/\4+/9;i阺h[Z<^wϐ/Y Ri\")-{ %iAqqP,֒ॼ8;737ȨC\F^%N`v +͹:7,SV¥ [Ò#H$ +2xpLA@%쿴ro{TwI%r(` hx<>^}0O A8!Mm-ekH9*K c%t}Ê$[Qp2*eg^YPG"ެ#H$%r(/cq/F.qp/ ,ࣲDR")2K"r-; HΌ RF~慚eep$2x>:DR")N22g)A0O  >*K$%r(#$."Z /&|{M%ro 2TF~慚eep$2x 6"_~y BZX{p/ ,ࣲDR")M GG˷o~^9@.@@F &&nl N_uydmoc{}K`OEv2$2,Hʡ 4SAqbD;lg%҇.28dp+{k3<' HΌ RF~f*4;<[5aWpJVC3zt |<&1N-]<_Bh3HEx[wA`09};?}|Zw SifBcQ-S")Cy~D{!:y[N&٣?; _ .Me2e2擓%HQYb9y=t&Nk<'rw֟,AKL$g|h>9wp9ȅKS|f |T`N0A^?wFS@b>ω\F~'r Mp閣w&bS5{]ToԾ +9|.(_1$g@[laNCgOAs"7-E%C|>?iyt[0Og")?Gah0ֽgxWaai  x<>~8_vpt;d|D.#?Np;t8-[O Va->9wp9ȅKS* iTnåh+Z| b,PZ8$peg +(w˱VWpѳ-V{" (?GwAdC::m9af(.@ RpOCgOA!afBoK=Ϗ͹F aÄ#?I=Z*mOq]vj}<:ꔿ3M8@,aHࣲs 2ɽ#Cx}Zݷ|qO;ONj G%G.JrEr@?~ސknlձr(#/#1,A<ҷ|VGr](?8rTJ$g"g\F.1㡡s`D)1yK8H$Hp$K$%Pd`H$%&8J$$> əș': A"KLxh(: #'uJLң%NE$ DI C9A0: "I .P_Jɿ/ _͘oB6 3q3Ot@D.# S9m3᏿Xn׷,vL$%Pd 1$8,A=|~ݠW:n~jz_3I9*%3q3Gl&n_qai{ iѯ0 h!ؒ9(1Pd`H$%<wphߓf?yn!: %&w<4w}F4*GӐ3:[oNC>_ ++tHXe땖 Nr(`2t@0E$i*svsKpkY(ye^@RJ L\ h72Amxb o˷9 L,;Ӈfr%$8xgۼ& >hb L, 9ӱm6$ryI|rC q>zЌ\]&IH7Q.Ͼ,`g9 ύkZ߶e{ܷ9egۑ%%[ -Ӈfr%ĐpR9܍}elq5K$1hb L,>l6b`<~l/?xax_XUsqg9g>4#CiB8F<|\:/􏶥/,$Pbr 82t0 Є>k.>l-Ӝy5EbXl.9 L,;1ıpzкȷ>pܽ0xgۼ& >?_ű]aNkJ;6;ܰL(%H A}>ޖ4 2?˱N!~PzQ)A.hD^ީ_P)Hdp\D^P坳|' K 㭜;8C9w{gO$娔 4A"//A_Y$28."/at(? >{|qP%HpLVNӡ;3\ .#w+<~;ח}a;Kj8 IS4݃im-#xЊ;VjÞ%HpLVNӡ^|-/b镽w^0&H%Hp',+>ю59Vj8x<' VZдp=黃"VNӡ;`Bs:..Xtj ؐvǏ{@.hD^ީ_P)h*C'|Kc:.}Z%ؐ;C9* A0[9wiHӂzOB} q.>fN&HF&$Brg٢4v۱x_h5.S_$4mgHe7{k71_%w:Mjk)<},O.xhZgrge9J ߂9a,G K9AIm8mz)|>?^n|eLu,t䅚ɥrӱVߢ)EX:^߻x/eKLǥ wK0e8j~8 +`Q S724AD.q +&p6O5KMklGBtޮŋ9^kz/@FJyRg`>.%}ն88}HMyxvD\RCn9*党M Kb$AQK |yHqv?{#W]vщB |B-Ȥ/LUDί*toꭹ$|R)aHPt5N4jMu!o[5ׁJ5'OFsiaTƦظ0c;_:ДD. J2*KL,P'% gJD<)?)1'HI 2,) 1OJ8=ꔘ`x<K$%&8K>,I %% R"1OŏG"qJ yRG 8y"q +H̓D$N:%+6\'@}u^injw^DÐHJLb%%&q|(Ҩ`x8x=ۓE>ϏOÞuA#OHg9O$NyRh={_X3׻x/}+ȋ?DR`+ɨFءƞ9=; pH[+Ӕ!{WĂ[y믴GMH$NyRh i`=h쓔#{yp鄩)Yg9OJ$%.1>&Ώ#)||%\{dXX(eS@b ': 17i8=>y -/M^Nw~F%XIFeI( Ƈa;|W6;f'.ŦV:H yRG 8y"q +HCKċ;{`^9yyy"HJL)&k`bZ7~S-wcHsDԑ0*#b.&K5HuJL0OW~^8ۿK1Nؕ%\J\R\M2*%D\_e;үE",oKDCc2tFpʡ dTkd9'$%.YF.Q\DF^2Br(#)/LqDD"O!1:r 8NArP2*DHʓu,# \bKD"#/AFesrdr9x<> +t@(!rTJ$d3* {_t|۟9?0=l`e \bKD"#/AF̃ /:oN:M xC9rH:$8NĹ80 G~g +0pl% +w vJ;H"eɅ&AvT82`sU}neI9b2tFpʡ =T"O1G;e//%8.eIAۜ +re9ޖ$ȸJԑH \M'Ⴜh>Y8q\IT_ I'=|tbJƿh }CFnwA \MsDRC%G~Ƕg{B(SDJ+tYqb:X%H gĜ ) acv w tt/$Gӷg:=5yM›BGIT_4w,.M<i }[%,Ԩ,ADd <8Ht79K9-A鍤70 +⵹yEP q X]SL9\s1#v/v5~հ2m)c||T AU®b:;"- I_+BӐ ́j\=b}qg|Ab~I9kceZp59M&aw:'k$xŚI5զ x[MD"c #/GeSk|cM5& s<K$2NJvNJk$V.jʋ$8&o97 I(%Nʯ6]ԔH$HI˯[KNṋbĝCyU%Hˋ5 kj000[tly~5c +X(-J8e(7tQS^h" 004HcK|~7Gkڪ yy&tVDU,l:X7G 0V@4-# .j M$$$x% ӑ0pYaa˷ȥyAR~ol颦4?>s"&\4Lн%}`h5[l)/4HAy/~ˣKŕ;5Jk)h݉os: ?N=W;î$,U_ V.jJ_sP^0P:λK# ~y9k$j; /GO:nvY;ՐhdpLB$x,q006#4N7{ /5;z_nH #/A?D"D^`40k)<lfìMòXqC r$':s88n=:n{2K8>)cMNC /)KL^)8&e3 LV3oV} +}t$||~<_WaA@KKmuf(Ot)': @10{]АKgsq6)L9yݺ$7c):τ]9AcE aqt$&< 耀28 Ɲ.}]`t#7%&Qx``7qMܑi.P|WhRKtaPbgy: 'LBy<5%1x~y8Y^ɸJ)1A^S" c ҹv,&ˎ[s`5[9 'u8Kn000;H>M>9ޗHFe$^W:%ogYÎA _"˓:%Hpˡ\by"#cd9V|K䨔W_$r(A@"/A@G~X9o;8|6$gƫo٦/ke7[! $N $r(A@"!VLzl3GG8qxYÔ=f&J'$x>+oNZӴ*VB2tNyE(߲M`<{_Cѐ>88;Pa +&BJL0Odr ֍smwsayKia`2vp+Cm"'3<>#I:qvM^[])A@D^%Hp10MmLD)1OV&Z+K9VN&vW.ǘ#.~e`ىlMJo٦|P52\O<(- K4lC J$QYH$vI:.e|BxPD^ &0sS~4]UeBaD )ȷ\ H]#5ii띳N ++HdD"2r9 <5C%N|<Ϗv7 ).)Ge <8PF.Z=:x`wtKA)Jy%%&qD$8Hp$K|MJ\R")2.rq5+og )A0Ӈ&HHHdr$CHYYgGDR8Kr$8%&%.)C3A C$xp$K$2r9VHʡ|K$Hb_MUzqْ;zM$%.)A0 ': a$hTس<|>?{~|6AO:oY, #Y"˱G"={8naɿo }3òxG}@#YR^h")qI I%- 8ZIӔ۳_˂ω_>8?>m”AuqvDDF.JI9/qJÀûzJI ': A#YkR Hqϓyv|l^-- Z%+Ip$PF)]IKBIKJL,9$ Ё?5.\~bܿ{ g K 8H>4A@GD"#0(5*@^1j]'Ơ\gGDRH$ LJx1ϞYtɇ2jXP\ HJLA 10.v._ZHYYD i-ۗ 4&.Hʡ˃\\MaHknlnpQ:l)Exh.AML 2r#YɑZF.#2x|ͣN |TJ$2Po8$pog"IQ >2,AML 2r#YɑZF.#2x|ͣN |TJ$2Po8$pog"I:.ң_r}<e፼rɉ&&W9bTnanjnm+`ҽ^+cBiYgOAcxv\=A/IJxqF8/;z_@C5KࣲDg03MH"p0Gx:tMDibqZǯG +GEbZ[m/KLA\F~&&4i_ۿNl?s}2_lR *>%N8@i`։)??:2]\zY/k |ThbLH`TBLx||ND +2,A0 h,Øe'lYivSi۞i5,HXQ)ȘLC9Q=-;bS߃uț\IQ >2jFtj~x| hu[9QRe2+< xNT:$LAz6 ?J:e\rd %&8ʡy`K$y r%%+gt(2x:%Hp\" ˡJLcP)r$K$gF.1Pe|Cp\"%&K$%Hp$%.)A^9C9)A@\b[9<x>7/G&.[e\rd ȍQAaLݯz21k`5,䕳:C<~'/cPoM$I ʡCpNVKVkv.& Dp\"1knT;Ke'M$|-?Y'O $8+ m6Bs(1O2Pb A.kmR_ Pbg9k[O?F$xp 胀J ͡$r>yK[#>4#CI(ʯ/ jX6' 8 $r> %+F[ݿ{2tws3w +4ŧn5 ^b.Ќ\%& C26g<8?91.8XFs;*ܢYVZPb A.kX"-Qi%ь$&Oч$@ptTyK[a`;}5?k׷ʯַJ}9 +HA霕 +c/];]6]+ޮ< $8+ myfc0FeߥHdc~د]xyR_xPG&B^+.ޮ5Nep$$FF=fήlJqrrrv.?1BG}/Et x,=(=-!I|CD sHLpXd;,8|*Q>8Pv*Og|C忾|_Ė耜Ot^yMn', ذ1*c-1;,?L08!s)w!R"YҔMu"4a}$999;گc$$s +pO;.Y.ofGoȓ:%&k'28#g|_zt=!8q-rT.AU&ʉH t;sqwUÎTpʭ0}9*ouJLV@ҩj 4qNaMee,KWrT.A0Ҩ )Hf!4#ě<9;o}rq+À5W?p5`ö6a"J 5eP4U9LqC7)H˾cEyMH M Krj^%HLN֜|M\ >VWH6q:VNp:Vo $r DF./|M؄ )/Ud N?_y˵I 8Hpc%yOhScc% Jg%>Ww#Z]d A"8B3 w>b=w[X>??MeJG10ǭ({U=~<Ÿx4EyMH M Kr\44١cQx& +S8t~L95>VB +m*tt$cS6 8>#q1' /8y"#vz$:pxSt 2*%|~ bҐAnU-)_[wn],Hke~~D"qJ +K.}uהp`4}x8=N^938Pn ɍ⏿CA8IASn9 2VN:Xy1k+fnh" ޖ$N 5/j 9X3е9&#H":I ̘:aHCj>xdB9LAj8 a,W(-ਔ q%yQ}2ǿgoF%28dArfIpLFd 8=a}d ]"_*˿5`R>W~ű\ЄU%yQeIc0XK6x8`\#J4 1u$%& aTvK:ﳭ,&,.y͋%Ȩ,cN.4ؤqJc~<;4W. 'uGe əK$KA@Nt@Ĝ Y%ȷg ࣲDR"1O AJ8=H$%&:\9yMI2PY%Hp$gF.,A9 sd9 ߦ)Rī/ID^h_Γ'.cߑ"1O AJ Xhw/ŚȖc.lZ)IeQYArf$x&ȉ,~AIEǗn"1*ABZ48HC Gj` Վ0j Ɛq^@a%M-K HΌ\"Yb8a^3} Ik%a邃> qzԑHJL.!9|D8e~M,iش$v|~ +-EbɰDR"1O A}ocs܅D@4ϿyS&彼Ni0Ws6'uCi aT:F({85n.Sq*ҍJ  >*K$%yaHZGwyF{x~]M߻xM\")1d)/0넵ϳSw 00}9AJoϔˮ.G]iS ~Wz%D")A@G^^I@@-4*5%HI8gK$28P%N9щq'6&J̓2x rJ$2ID"cH$%Hˋ5 PۛF9 y,sDGJ )':q:ĦQ[yRPz.2ǟ=q=+ }D" #//$ C c!u9|"pw3~s[X%i@, ȉN>,қzk%D")A@G^^I@@-4*KSlA@رIo Ñ擈[oa7ikʄu;iT./4Vb`p.Pp`R8 xk|˷7H`H9Zs_)w[̲(-u;iT./4Vb08 Ux<>> Mst#7A^ܻD.Ș'I t!/8N\bMCI\eᙵ>e寉plV%P3J)è@E[OazMo.A%&4 Fb،h]x<>>vB,baæ%%P3JC:DAi׻J Qzw$28I S7~i t~]~ͭuUF31yŦ%% #qp:\vo8?Ϗg_/gF.A"/A 'ua4q1wnw|(o H rrv.o +E +s"Ǟ_e+ C )˱r29+Peȉ&8e|Cd(A$$x2X9:>fLVHQYF^y9 Ky+'۝R d"cy;z]pN_˿P}`1G5qJ':%Ā MA \X9o;Fb<1?/ ŎSZ+Kw9#ȓuD0`94-!Gl^kwe+ C l tMP-A!H%Her2vs a: ~˜OLM mF~լ,#/_чRp v8NZqz %PD^n2vG;W|Ư>;hG} G}H$ >*_33a 0u/99)+Peȉ&8Ǩ܅ipEU9TgcS09ʷ\*K$QY DRD.KLcDb|Dۛ"%H A3# RegcS09ʷ\*K$QY DRD.KLcDb|Dۛ"%H AjJޭV٭OӅ'+)˛1)Ԕdxr;qwey~~1k58vGf 1AD"MK\":br WЁ1`0% 22/`rRs9o6U.# HZRiY]kӦDwal/MӔM?L$bS Rk Nh% J:xy)\[Mp0|~㡴,NK䨳``|MK\R$xpQ-v5_vnQeE.Hqy<8& ]OrMPw'e{?'a@dp$K$-NC ՝g1}4}x87}a0(\J9K$dTx7w=]'D+f"/1A^ 8 xc(# Օ}}<^loec  2*ό\ e-N؟kYJ+ljc$L|͚ÚDS0: #ւ; ذ=Gxp`XI`RYr ,q +F/U +Y"c*LNipz\wokXAR`J@' }kd.Ưo,5>G{ТIXFLA"/  Fb60' 5%\"בNzS,/&G"k4 $0wOX+cmǕHJLA"/a W_2?K9g]\ud r2*ˋa<؜V^vl4kXA^/ApL1@yw3#`$?%a0P8$paG. ϿK~AVջ8d(1Bߎ!uu9 lm +o&ߜfg;'Kࣲ$*%&ˡ4sg{=(r(AeWa pPX;хZ3q}w^6 <87%HQY)S|~qn8* 8*% JC}';-?s׻Jۡw$xpa؃˭[++'8ʡ I #b}Uey~~jpxs׻;0$cK$kp$%.Y t(2pL,ttYCxt!OG{8={`6I5Ar(28Hp$%~W14{w†`я;N1Ӂ,AHLp:Ci-OI2T1op-KBY1p 8C$8+ec!9=wq73.Gn4dv]^_%yv8ȉAI;oqk )qI 2VNp:CrrPK8 +.`:7J$EMc񘻎ztiyt$w:VVXKLB  )qI #1΅Qi-y~&-wJ ryn02ZWY~;'?G//I 8. 1yK[110N+ߦfpW8$Β˹ >.A\hyMH^8DxBP`|9<:Ab0%GDܵ:)^ܥ+; |m#hW1PԖ歘F$r>9[Ǖv(}_yxo|66g >L*qvWqxۄ>_xPN[9ylrC>2Nox?y P3GeyQSbDgy&q8;+6k+9qD^: ^ Ż7G"k q1_ NT-EM 8|'bқ:~|K RD<^;?A⭓VN^;8$PKt QZRϖ}Jk]Yh:SإOHdq`W +woaĉlGM228Hu<8ʉ1xZI;ZJ&tXvgh #Y^~I\%tvC=</[l25{;wy5ʼn|EyvB(8}_u4 K`ډC9*EM 81*3yh~oyMqb$\2NoG"@pswnzr3bNnJW-c>+P^ͼyE(GCiҨW]|5M5q~>xwL*qvWhv\nzeUCLM9L28H|EI5 cr;3Vacҏa5Eͷ9^K A}y+pc%WA"e LL*տʡ2^K$}o6U.#/_|6k B$8H𱒼rq6Ont$C>Hos|̗Ab I\W9AkDE6+ݡ ¯{$>op7Z |$00$Շ=C;qr背 GD=rFn Nd8%H M J>`H! +>,A_ D"_ c0Rol%?5Oa%c I\W!9Ѓ//~\'7ݡlPyͷ9Ґ o_/˦JqVq5,7|$ə Q9n]Yk]LE[ o'+ rm\F^Àkh'?tp|V+5uӱ + H99߱G`cҏu|d̓ΙHJ$Ĝ 'uC9r HJ\R X')~D"q$ CmJʘ% y9*HbJLȨHd̓ΙHJ$Ĝ 'uC9r HJ\R X')~D"q$ CmJʘ% y9*7]ww=wU<Ͽd#1ޅΙHJ$Ĝ 'u1pGv_swuR[JʡʷlSRƜD.H؉=<"rŁDsaUBCHʓ:g")HsD)8yR )qI r n% $Kܲ?>Qa.;lHʘ% y9* Gˎ[6l`6jh: 1'HI2P'e+R#pd_dX, lHʘ% y9*ߦaH^5 -692}mqNMk1Oʓ:g")Hsij#S1~oş %K$P [))I;19~{x<:od而JD<)OH( 4OTW">[\c%K H$&a4s~{>_& }9*HbJLHrwՕ./vV#.{> *䖷6q+ qI r$8&C@3 ]*6e~HʓsyWF.J$KLApx-GesrfRep$ŚgF~% 2*epLNues >q2TJ$@$x &4gW]ߚ=!)i%lx+ {9T1N2<r.v9l/ߞWK~6^ǖr2PY")A! lt@b;$HJѫ;D2\L3)A6u$$8H$ d]%H%FR9L9-CdDR C&AvLI ߣWw/Kd2rTJ34 "MT؏u+LJM  <:>`s$:~r?^χ܆ K08 ц;%?3Z\LJJd2ry0ͤ44?AP;*OqA2D.e$1s%U״uީ#l-%6혒 )A@GN+a`1#47: eM3}0nMl3Ų#ޜLF.O$$8He@YL¥#;``%xu/%ɠy6f*؏O'1͟_8ޞWK>1fdrS\"1HGp! JAk|ѫ%^J4 <o?"IeM$$ƓMa']^Uslla6u$H ) T6eKNǽFSF^G5K4(1%f:#Q)Cdp ռ w=t%D^.1!28HHpHJM^npyPSK#Pl32x8D* _A r CgPbK%S29( ޮ]]z@ԍ[pyPSK#1ecTK xoհ&|b֍XC+I"/ApA Hd",Q8M|hOt0&^&/ ̓%^]nd3!28PYEerz_-_|$0 6!.C&ep:21,^D{_K#Pl3J0)x}zڃ=Kd-e!28HHp~f^G5K4%|:r;$8dS|M0ٜY~q3On_H_[H{!'V|8D* ѫ_ s N; v&ДCe9 ot)G{gsH(_2/%28 #eP $r y?y x`7GJK7:Ô#ý9^Ub$/[@J M2s(A peyɼDepWlzswp:Gq{)JK߸yxzx~_~8 pes1xD+X&cx~dMgS+fNeixϡ < |3L92;U%FBy)aѐXI˔g{\?n}Wѧ<~_s 4MpeyɼDǝv}l&é$$x0PaZIGۜW翧ՖBIb7^61Kd0>Dp{F~3=k:_ǥA)2s(>(A@ <L3lGǧ%OfU $8H$#14Ms| v|:~91pA@v3yp4+xƀ2l9sfwpOYC"jC $8Hp0;V0OɼDetr1`S3˴f6yW)#+-a«rPb$/[@$ fA"1rdr ~D"4J H䠌\"R%۱˼ # 39(_rt/%)2xp$%H%)#/1|I Db@P9@IDhA.A+!_!qsel.2xp` ݭ?Zgy<oo%f?59G29TN?DR"1GSGx'vxFx䦕h~۱˼ # 39(_rt/%)2xp$%ZI*]oߞG_vjrgM$%y|PbthHvΣEMA%] J9\?Xe보\ HJ$@3rc8%G濟L x7({AD"c;vc1XI\<>=PlВ H$FL h4S~r'^6W0D"7: CK$2j 7U Oʾ{ Ng؁aZ2F^by$ FGؗY0!>gGC3rD%4Urny!Rr,ARD^<-O:/zs= ,uSZ=zu' Hd;`u#ħD(} DbKlǔ$8F7CteĴ귿3-CΥ}VIrO ri8l Z4'- |P`[@@0FrPt|W:16Gr$d8M/ Ji pMp= 𐎓Y7XɡR"1NL %b9ARnt]nG |SqDR"qņk;GD}x(r'ߢ;҇yP }AR3HMU _~ebc1AaQ)Aԑc-Q|6Ʊd``=7M$lȠ99(Ab !% 堌#% n2ZO+}u8m͕ߡW`ˍ j`(mBlG |SqiY:v< +hH ,3|BY4 3%ȁ&r3r;*%離P0mol8<6\Ėߤ+144fÕ jYJ+ Dr8a@Il ̉flx2!_ؐ!I}$[oNӚvbx!g?* +ܩ/A0\`Kpwʻ+ʝ] +;HG2>hwv|$L$9:Gc`nr;  DRb#'HJ$r8JL'ى-qAH$ 8Db%2>hwv|$L$9:Gc`nr;  DRb#'HJ$r8JL'ى-qA.# >l,n(ĔD^onVin1A HpN[60A~.obl])jwv|$L$9:Gc`nr; ݱ)}WD;lg1&&9 C)7IvbF$xpl13Np9&G }#1e"9 cTW^sSZ(eѠ$;e<83@s"xo} %ǽ4;mlNC~ B&- %$%9%f0H.?ʉ)'t$ 8Db%24[m*ޕ!ḵJܢ$$rG0N4Cs^?}5D :8D"!#/y͟5v + +`nr; hGBW /mx~7ܹwKa$F1FBy9TGwBP>h Je#8|e^KKrPsf:dPީ/A6: H$^ +F]ro@ Wo3A\Gbw|}wBPguG)cʊY7+ dD⥀`|%`hJ\_3,,#mBخ;~@n. /%r`$vqxWVۚ=V#4|f-a$F=̀ҳ3u~ZFdnBsZF;mm9. /%d8=O=(aYk%A@0>쒃|?Z08_}_ǥ]?GwB0y ,Q |]osA)Hh:dPީ/A6: H$^ +F`؏O{?󺮷'I`ePYK9ʆ';{u>3翯&X.b` }s32(ԗ F!]u]o?Jďlt0ˡR)->G{{J[`$v҉eq<8\*=1*]1) &}@6u^p/cF$8HaJr{Ctp/cF$8HaJ3<w= y<oOZX1F6-|/v"c*=ВHpÔ 1U'u&oY(e` ?|oO@ԗfyLt5g 7dSe2oAiC'oC+Yb Ȧ|/x;х - 1mۥ W>GK'A 26]>w<`txneK*Ab;*%\b Ȧe0yɥzn1Y_P%ȝ0eRF.AblaSM ^ϧP+!g9lY +C(]n*% y9KO$>J$%ȁ&ȦN9(4ALʍNp$$8ȁ&{xd8ʡ~H$OJ$rd;aۡA"JDD^cI r SM$r r ^ [ GDGDw(?u]og* [H+73HF' Ȧ4;33+$VZُVrЙ H&F'8 ;T2oN%+a4 i%8rv(AȡR" .yZX@+Ivz^ |=iA"AhhΖU9a sJ3i1N@6svJTzhr;k 'F'8 @dc ݇!U'/a>Sz$OJ$r ݂$}3iӮz;o0ݎҵ  :<A29(7.<jcp\#TGc@)/DrctB͆{67_ 2:>eшl7J$%ȁ&Ȧ4>1jOX  U(epfsI!]^bޞ!Tʃ +DD"A"/]x1C+obt4;uA^F.GJ>L ~^IDntG^'r8DL 2(Ab;*'A6:  Lv")A.AIy0e2{4 5;HF'x堌q2(JCdaD $rdL ~^IDVI',lIҡHV PvTNlt@*'A ֪_C8eJ6ې Ҷ\^ }&i +)Ǵ4u>9\ JX؎IHC$`h']1>EG&q.>A^Ц7~Ui̷<->>8DL 2(Ab;A@wOD~=ߢ;Hdp H 4 1 V.?912[pX'r8DLqa9ȏxnL ǭM Lv")i Hʮʿ)퉽$mi[&2ryGy9nHcrykM>w49,% qPY")qHyGI 2(A@ovg; P)A>D.A@")^-HlLJlL!%F yI/Rb %w $fwCPcL$r|rKĖaʤAщdpR53tyw}{S*% qPY")3!=]^7˚VD"e2)dPntb19GDx<~K<:ỎLnam QQHd>THJRGR J%FB9TJU5=΂__ \C$Y4*KlLHL4'W`Jݒڿ1>THJRGR J؛dM!x<>1sY + B7n2b Ƞd28i w)vMIQo^*$2my,8i|cNRǧeQ:>Ģza=ߣWK$[)[@bΣQs]H8Nl8ȍIR9KH4Fuq{=mYFea-ߣWK$Fg3?12φ;n|z2 AsDATf35| k]y=*aA:lvg; P)A>τse DLJ0moN^ݭM<؉NR;rtqΒ\b˝rh 4 ;{Mcp? +IvnK$ >4l4 f\\GǎۡŔLH;Kv㶩ngx܉MM=t%F6Lay9T `;ʊ[C ۇ;1)vT6;ǕERo\ÆA2xp ud#CnN 3dZz{ <ӶɃE <8 $8H;&wIĹYy(e;Cd,'2xpH# e%FͽW K%2(H$FB 2(H"c AMM`˝#P2q΍PYKOd %FAK : {g3KdPK' ɑHdP"8D8*˃ɑ:2r;%F\ # Tx d8#_W?Dz7_+fHd,jJ$Gn%#͝T9o_{l~d!=P ۾\b$Ƞw6*yɼDetu9 +{p/gJØl%#7udr-wK*"Wm{` eܽIiLN<8S}7hH$FB 2(H"cfsmr|ӁO wK C!1c*Ab*zj7]1t` +@GǼJ@^2/A/w_Av8Y<ᡌy7D"#/yHDb@ry< <8Id' :1Np$# : $ep$28Hp$%r,A@ 'K$2`H$FJy-w #)Hv"KlȠ$G"12`˰NRG"GR5x +1ܡ\]#GA2`H$3V[1+YbdaCdS1$w<"zT⺮:5Ӡes:eʶW"#8ȁP)4@N}y$xp$%IقI<^`fڧ Q~0 +NRG" f$mx+`O+3j~Sb#9*&F'woOfY, +eNRG"Gcta}{{=UߜݸpcD.˃iRb0G/_<لό˝y{N^h:JH$Flv=t`#Ѕ=el'S xPY1N"Hd0~zF'?'ZI&2%`dЉq#Hw:px׶?/,\u>#| Hʡ c`o8L+a~7-\$loXмS_h IDb9+R)].,Kg vV0z+9,o@eP>(A*% Cȏԙɼ42)1$dAy0͑P" H8D,qH$r9,o@eP>(A*% Cȏԙɼ42)1$dAy0͑P" H2m|?]=y6a/֛+}|.ď`ˠ pR„7~߃V̺i2 yd2<&1 d0%z6X9u44iL噼eD ~A <8P)*|F~$Ȧ䠄%Ỉ?v<=?|;$.Q`#D"Aa`300 Wۏ0%$xpRGi~ٰ lYHb%Z`#D"A@:Twǫ/:!HrxYď`ˠ1`΢6A~Zym!-E)-&F6 =tD^" > Da]y~?`I IR8\/rcp;lnR͉_zJBY.(aeK#C#Mn 脓=;S/] ) rP!epcDnfߚ/_:"bKm/A/J9(ë 3i G ].6a8>*KCD"c >( J`<1Ã~ԑArPF^xO w1\K$M${R) V JD8A2|P x"c&X#% 2(*堌AcH䦎 =.p2Ӭ˻|W?}G=jzCo͕>H\CD"c >( Jd/sG~gWXb\vbk8>FAyDnt!ï=x X~D[dӜ> ORpA '28Fpxoҏ:rP" 2H8zgiʽ=M>t?crPF^":28H@"ƓF>sy:­QM& >( J`<1n_7~Ӱ"X +gs)%52ԑApl1HΡTw}#>; ^Ce |Hd$#xvu]ė'A'l7Y %;XF(=)r9(#7DSzt~|GOD.) V $$&o>'ǭS |2rP{Rg$ϐw01뺼տ/o#dt8Iyp4$H$Fl5x\rw~>B;MrSGJ$$3ו/<IYlxI|Ľ9&*%F6َJ <HTJ$I;A"#H&A0\J$%G28Hp$H$ D")AʍPY&X $CFs;Q)Aԑ JD")1rg^"Hd$FA !S dD"AH$%HպӅ «{$%4P)A eh|f̚xDQuk%K2yl:epHǡ#="xtɦL!_{u)mB {aД Ck%A0vTJ7u$FR"HJܙV]}]$|/RS dD"ACqL_MTZI{PFL #lG,xٹl1|5Gc["iǁXu L"H$ ;Η&xu]o_& ~l#q/Aj /vyuu|8dXrP )qd%F=H;k kʵ uc#|"|: M/ mǞ2Y¥Mvur +[;c>VKVC"#H&A0\J$%F!xv$V\A>J$DR c1z#fm/_L"ԑ JD")1r`/tUws4]<#qˌm.pd%ݤs'#/4H$4GDu?-0iX2q#%ȁ&FBàGķ6s뺜zB/ҖvGl`5)X Hpw# <(c >4t&1rPK4(%ƀۜ[xIe VҢ$Il7Ay|}Yx6=kIg'.; + J$Fdrˠ .F9& n3[ բ9-ץ/ay0}d# $ JFB 3c0g{_dX_XI@9楱M-:ҠHJ002 f_i'A>׹N7v)㐳\aT"ĖAH(A#)סk~ຮ߂Rm#N;_FٔtNO}'_7 vPV-fdAzI"10j{1YCTg(ODH| G:%F;ťKx?v]uH3-HXK%#L9k4!_ǯz{~_A|H %0%h8bm}57[߯εmrX%v7:A=׵Q'kEh$aqoUԼ'HlGA"#|/% sϸa- wfp$rcw(NܙFri>z'HlGA"#|f V~x\/K~ԚXd3/p2eGĞR^ǤS_ᜒZgN.nIX b}J ^ :rGPhAba-N8;5f/&@ 9|\Q"w<?t[c]J;EKl >Tʗ9hz'\*6ف +be),eiY(K7|\Q"wvgH7_}WFq0hɃ Hd#&1z{nokeXIk8e܉;HWvC=He c? OY>1da["/ȃfF[|iٰ]\|^ kPntb{N>9e bAA% s]];m}P_r-04rZ CKAFsr AyN:ܚ=:ht!Vļg"12;/'Ab N;ȍNp$$엶t{?D;KD`D"A"/]nԑ AKO$F{%X$H$2rщ|' DN?xȸ{yg|ɼH#H$HsVc7]j~7 J;8Бߤ+#qDF.a$f>?{{O}>(_2/%#HLq 9#"[ yt]qK@c=::OeO=nM2ZE^bF'HF'8 K{:6 +j8=zRۮŴ#S>G{ɼH#H$H場,x0K!NJ^"sS- oۨ$ yDXO#&|lW;#܁ࠚu]o߁mpF;G>K"ce M(DP|6aûXw }$2^}>(_2/%21 \XO`0$UtJ '#ý3D+Nzm'uqt";>:ʢoޏ#q2#LߐN1IP 3D^%#AW<9u﫻ϲ"8 Ke$V`.N')>F6>z y 3\I$؎#H2ƓI$8{'w >H[L?x'7?DG"7:#/7! 7u&#K2drQYbd |Px2CvGR~\b /}F >q_Pj=;q B'|Lԙ\F. '{\dq +!\A胀D.dM̃!3ycx<$&xZ'/2,JH,ىU؎#H2ƓI$xR|aq:6-xg\t0kE.dM>1cY\-.:.&旿&ޞ8#ߴQ%D2gi=#yo+t> 6&_G-r-H&lbya`*gA̴ٷ&ox#[yCo>HntG^nC@oLF.`Ґgx_^}ΎR#/i0۽\>[đù^= yGƛbeq+;HX=$O y<81@2]no?y溳} `})hKm/6ȠO3mep$HRb` ̹|<oG]F`@ .y'#1e;K6 旯 MtqUqߙmS2r|P#|gxdP(h؛a"nԇr3 #/%wOqOw +ML3H&#]'e3qJ~W{79bGbD.OdA UM.f6wV~Yw 3 6K|H&A#sMLBW&{kgo`D"HJ$K$ r*Kt)JVz$ 2W7Idp\Io\f7y<ߒI_\eIԳao\aJD"aԗCe !P)7N[gТ7_diӭ* |nK 08H\Icj\%6Ha$I$N}94̜VN={M亮π-16b[({xu3D\b c@il߃#\iwxA`D"HJ$%lB3Zax'tC%:8yoFG^ 8#%Q:=%E kuanSS G HuUvE'ZCK#%ȦNDx`ۏbfo_$t + R2x8|s<-aǻ?owwyG:gcaP@zxhd^D)EhӜ9!)3 }P\bdSqgle $I R") Aeph\PE2̐ )&%I#ap] q@6S!/)r %F6wv\@"/ *%I dhXyep Ud* {^bR  GX+Cltђo%dSDR")A Vu.M9F w V"A'O$%I#apX98sW~ܚ=+гl=]32yԞM…TivtS:YъI$%I#ap6Tt\-]<]qV =TH]IMJpzrҙp]ϷGn(+1* A& zknv0Ycxh oX'oY`,$F{Sc`xG] ?8/{mlC$80DB \ 1Mk.=Z +u*Y:HJ # T4%ŝZsiCs{S4N@@LlڀPEgCs?*o drVȝi bOpĺ "H'okMᝩTH;S']M6]jЋP$0z%d+$8)/@>ο(V^k\Vd3 )#z~x*ؗ H4~ Ħ /M{ +C%8HH&1~ArI\D\X" A@" apar! >܇c< 1Cd2r9Ħ /M{ +C%8HH&1~ArI\D\X" A@" apar! >܇c p;ךgl/5>9VNsɤ$JpLi;|Z'89Ѽ76Ynߞ2[par! >܇nOuw~;3Ayd}=ilR" ap`weIjɿ_u< H o =p9̐ \/d7:8;{ kod"wM몄 H$EจA"# ˠ&ݬיz oDB}<Ɠ3$H&|w=Evt&:ab"% 䒸 =@?]*INݯ'S~WKIJpCI$e*%28 A.#ىDnH$%HؾqD"w08H#'%6!ܨ DRA$I$[6*A 'o H؂Db d'" 1N@bfI% C`` F×؄drd+ 8wwa߾>tH={qo$pI8IG8Zkfxw X4p` F×؄drzЏ]|bXO Q33J -H$FHJHt-ҧgn*3ߕ8Yl↰i*Kl`LnTYWsgۿt?ˋd$pI  #q >`[@@@@&ÍہjF0W#;ro"H H$lTJHԘLi +L۷oQ&+n2B'K$[pN1½ z6~&7*H&}P|zѕ\g_8psl|YDnH$%H>Ԁ#ʻqgJϴzMyAKlT'9b A@28HlckC$H$ D")AoT@6SقDaF[r+$r <]ߎ%.R")7*Hv"HJܱiSH`r0ARb$ c; L"H$ QLe 9܇;lͮp%Hwv};tbi=ڝ=3{æM= 1\ HJDs84 +p`.wٙfm5mQ +\Zkb=:Qק|ky"w2/<_vӥo0z18,GSV:5l #$D HJI(YϢ<"C  $r <]ߎ~PbKk5&Md0r0ARb$ aa[[u1CBYG. M - +\~g׷aY'hUj_Aqϣ-/ԵѰEJ$FN$6t7s_<Էoj5@l!I$A" CPƴ.nT12ȞB"H94,/( >4,1"ulp; *EʡaDl2qR1.CLe㉌\ ~L"HvHx"shX^P |hXbd3E%ȝ# wA@6U CLe#åb8\ 2+6\ qCv nRа$аf:ўpǜo[9HwXz0&AZ FKp 1'2:nZb;Ќ~qj|>εM:L(~'^W~8t:u:8uM{@:'ҤWZ wB z.CLe㉌\ Np{ւv_ K1ZU S064,1"p90 1)S.3ٙ`}`8\ 2;>[\Li>vt*WM%CbpCDK[vTtB9 LG<[3m -e J" ``dT *ZѤ]Kv/`YfM$zw #aDrϡa _:ٛ$LG68'ێ5 H94,HTF53;pu&2Cp$dgm +Kޔ_i}5ԭulp; *1 $LTťrgO[^Kˍ +u$2^{8x) /ٛ8 # QHa^őHl ۇ;_bv A@H$#ͮpalc+F'c{PDaǓ/%{G"Ac$7*=˸8-cPqKl–!Dbd|0wlK 8q1t =Bz>y$vSDxA'%^ +H$.|>o30#8]^ͭIX%%0`D"(n +%:N'} ?l;2|"6\`I!!-<#;A"|ü#f7 &k~MW<<2D EH$#ͮpakѤA hr@rԼ) +Ɠ/%{G"Acf7:Yx96ևYuD cD"(nv #II< +s?8ħxwByYoF:w/=OJ5Ѥ:e.P3r10ӷVKl–!Dbdr9L3;8^8!Zr_'c{PDF/:sn + +`" ǥG"%xl*|M\3Л {;yn꽼#6 A.#qG~5xU~pwߨ`$0/h]\\`_\NlT@# >܇ H&C'x7AT):O/e2r-%iBg|3u|23$#/F$8}dr0|r!|:HpMή1HrXF.#|h(_2F.T7S'7*\=I |}x#o*kFC$!H Ip \}_m,EKv%–CC4rx5Ime=_,s =ۣo߬ؓS*ơaau *]c3$H.~g9|m͛Ə6I=ο\m/G[ Iy F$)|c⨇wKK`$.C +gjUÆ/);oy>0זhYf*wylMK\PƵűb;y*fAj D./5xl]88]]u)+sѨbJ3m%,I$D.5n2~d/$^ +NH?C?381~&8؂d2EL 坩 Uʡay\ 3L$I v4fW`xaI909̐x$8O  d' ~eS'CCyg#{rhX^(# d$8H `>^bR.7H|J=n>/HϙޅwGRɝq3O#{_ x Qn訰8 d$8H `ڃ#J_io8zlM}Kg<+:~ݏPg}+>ʓΗE;^1u24w>2ԕI۪6&nP]E X={Sټ쩹F9;M\L-81~&8؂d2,/4uIۊο0#|E.;~1q2F < 7Mڳ_g_ui5<ljK.\z_rIH?C?38̈́#}_}xb3NS3Mu3qC ҠXF.A@@&$Hp#곞Ǫّy~> '53̐x$8=4{MSy9?kqAxb&ubY6Uʡay\ 37})5qKuwVC)&0d0CGWz㾯vdtByp4JXv_𲈩3=ʫtyr&|9t~˯Kx5 UؾQG2 ٔH$$0CDu|0QE3LJ3ClLeK58."A&%-Abx"ߣH&A02T)cFd2KfS"H ygy$G90)A> A@e3-จf H$lTq+K[\}KZ^۷on&w,5.#\lJ$  Kah)܃/3 +ï׾n礼 Pakp\D 3LJ$[6*=;KÎ+n>|),!xQvy|6 ӗ<_R'@po킠mhѡ>; Ab{T cLph @,/%FP")AHl٨$/>kћtI'hor)^2DfH;CcVX.pŖïa +.0Y&2kB&%-A$v~nFoHl12T)cFd2KfpdJOz:!O||=_xf')DR =.[g윖qȃGĞM\_K3]!aCiY'."7U&L"8)_QpLvDD]1dݥ6$ +TD2plA2 zZ _r\䞸Tugrsꑙ:PE qqlHJthYfle:\\J G29GR~cnnf<ϗrr:"ֲf@LŃ U$Go.89S߯q#>'N=3a릥ߨ` >4,A%i}8ϷKKG[e /aE * P0DbK;Cap!Hl!Fp=ں#2ryad0D"A@$Hp vq;fHaeph ~e1lAe!Hl|`0 |00HnTG[wbdSQF./L H$P" bt{F2};Nܩ);<΋{ uo'C^æ T n)B{PكX#0 ob~iVX e `yD %HMVnZ+`aqvBy^g?&2Tt8%ElG08'V=VG  CCF%tb LSjb;X% 24H&AB?&ʏ< cjDnT ~aĖwj -2:cհsuKij&2_E A"/HD2 $8tSzVLb_4_l5\OWckbtA#0C-u0lCkT":7K~XT S$ !H%  =>DՎxowA-aeph ~eCGq}emU-HAMxsa.Cb$ 7*ߣ;1(#ג6N1=\?y|*ղxG%4$8ȅ]܎$`IO=B;$<4JoV`r0H >v9]*]T` +qCW :b$X8 I $x0# ST"F0N@."G"13܂D"28FayH4_&D 3$.L1%IeSq<8$8Ha%F8(;E`\EDb$fEdpDBE h(Mn.x_w +o l*1NGR |Е[w@[r|G3xߪCb6׬ MIJ hX^(A@@0 eֶC-=F^byyAQK 1agq<my_Vw)35 b兊#PI$-pVaw‚.b"^YwH䓸˦b$xp$%Hp1I%~@"yz5AE 6wVCb$fEdpDBE }k +&4<ϯq'N + o*_ SLdpl|wT,Ѳݤq:z[:^LupкYE ȅ]H$Fb[H$)ÁG&Yx۟ Ɯ`SD 3$.Tя &y~:#l/LqdSQw 8>ZuoC /:-󽂗A"0/7Sv4H0EQ bA0" A0\bT Q\bdPHbKa9f*#epH A# H$>f*#؎>4H@6*A@0Q <8F6S$!FK *7*K * Cl >!LedS ] x69q2d@t wA=H| =TFӒՐN.Kͷ Wp6~0Q{|gZdJ# +F[k5lt8b9K,- op A# H$>!Yp{Ot#;?%Z}||aM`#da!ݚ}J"g*>vw8* *A#)v8&0Bs*n nBEH +Q bA0Ӟ& <~#R?~==l Q&d0\Hc<#/M{ +y<8H%H\0őA$828F;_b=l Q&d0\Hc<#/M{ +y<8H%H\0őA$828˵K 8<ĺ3gKu=aKl AD6@M?rq+ q4mG_ nܳ;L<,l|[A"/#mby5#$q'cumhL:kÚD qq$GHv+ BfA*q^x؛͌24/ d0٨`$ #4)Ndq.AjLjRi.NyǶ+Vɂx<8H%H\0őAuMu{i&sX*6 SLd5FeC~Ac ZuL߾WsWj'3)G" 6ɗ|[A"/\ః3=ő4t>| +EXB?z䗼0D[Cc)N:*h&zs矰Z1t2;n0A1x%F_ʗVhU=K4ROyX(u^ǭO=hfI#wD?Bo:?>qfa_ 0|[A1ԞK%P;8~$# ܫ a$rD"1N@!F#'$KlB2"/%xpHJ%F*e!D&! CKbKlGr!\"Hd`QF`LnH$r0K $8$xaJ<8F#aD"/ #a-ZV+݉p}aa&Ac[0w@#ҽԷv ho˥%%a9̐D.H$2@aVɩ홤/E-8܈;hU\W%) x)GR>4,h,:Te> ? +Jyk @? #a-H&ЃC[WyN7i%6!TH`+?AQ?'>$W٨cA"0{غQH4]  *JD)*|.r$r3E&c;p{T)G2D H$$` daF#PnvlT@\(A0 K\DR^3I ȑTQ $9 ҭ_{p[,<;hN$ }=lݨ`$J]EMH "8*=;I{V\N7A4wp$r3E&c;p{T)@ 8O}E;GxVAiy{MW6<[}o߾9U`BE hX"%BI@W\{J<$>~TذۣJ$8I$r0Amh޺ ;70 |[7* r+dR>m ǡI9f#烙tfQm&adln*epH&4wȫὼ^ +>$r@rpXJg=~eT0%8ֶbSpϕ{G.r$r3E&c;pVvKYy}x 1 DQǂD 5dH<&?qr]\_8Yfu'ĔK$兊;#T+hUvsW\&u AH$%H0 ==*ao A.T %.R")58:wJMo+Hݲ|K"D2Mm8Hpm뿉)>x ?{uNE֙kH ?~=7v7Az&;6 +FehIikw/ԀN 8H|lцD_~{+f8KS M QA"H$`- BϖEئLiɼf\d7ISf*=*1p7zdּOx~WXW]);Up$H?V&ޠOاD0N4t$X+$D m1LDH35e$I$TVm}Ô'|>5t xSC K#C1iBг m'^֭i񎡇#VvkM'inT@0\F.%HHMZsnst,?M^ޑ{w=k,F HJ!zaj|8wC0e + xC+tk;C'_=)G"1\F^/e2 <8FIl3 A0qqaQQ 6*;1=H A6‘L#A.#/2r #w$A6UE  ø8аܨrODÝzU$ȅ]pvuw l;(\~!ݻ/#s2^e'$xpiV["VO>x?yXs_xp1y`"rd ~`h#pP{v +!v?"8,/,/ +޳5[\F^`)C3/k)6E  ø8Pj/N +#x#[jC֕H|<ԃ"A.(jC5#} +Kq$YD"Aȝ6IM{KCQPD\Cuͮ]KfD H䰇..$|w!]]w2/~#OYN{֤T w HFenvҧxsIO&E%7Oybo_+wnaFa:fWή%YHɏ>E+lyϊ +O-2 +,Atrsh^$w?z;oǿjOB?D0 >\.;|sFln'6yg]#ǃ#Ș AȨ .eaqe_y7io:LDLGiީ^ J;$xz+mwxX|%q8iy~-pSnl,$LOu>d6} H$`7 KK|4#l^֊t2ĽI"AT/APoJvƟ;,M?KЀ6sjLt+?PJ^u>&=w.znDmD]Qc"Hp'e{e3[2 KX4SpHX71'Gqljˑ&#,p^~{?zֶka?bmq>Zu+D.7 + X>"s>m $xp$$?lahYřK_kJ * C=\F~ͥb84 =T@brGIygI90) _renvH`|! ra?{nTuh(L}dPﱷ2{n.áE; H;SOaI9 ˦/s+|0D"C Hp ӵ.ݞ=T/O-۽ts+׻P3{쭹Kph(Ab={2HSO}SL?owst&ͮ A@@3$ A@s)]^ao|Yau"24,7~1zAY!}$Cc?o"]MBÚ3fapmw#BGqHa$8ȅ]'[iOV +9/bsK4MdWF)`T $ +HlAC V^Z|玹Xw#ie y7#NC7L Ԗ6"!Ha$8ȅ]Î||; ׻ءw lT@{쭹K0간Ò;,= »fnpoGNFq< Ֆ\چaHK.ͮ A@@3ԹA4} CCaKԃN֡3AdhXޚᤞƥUk0}k‹{㼮joGlA쐔 r0 %M _&_ަi#x ܒ,] (0>{nTuhGWzst+W׳_qC2S +lA )L=) HXySn~o{|x9 X@6* "HJ\DlT:qI DnH\DrɍʰIDF.ca/"&Db$fH@"akPc٨H\D")q Q$I'%"q%7*'%q{+ =fKx^h+n%qȍpG %> +HจE$K G<>Il'e^;HrIz0Ѽ)Ԑ\r2xR"' ﱷP0C3JXxVp0¡ckPd;x;οeM0}8z ֲp;AbxR &f݈n:|>~ϷV>hYt: ﱷpaz8 +>{v?v:Fb$rU8égmT@E$."٨l9ԥ4.&k_qmtH:T|@W+l못gC=7OJ$rSE"%KnT,ma-|>ƪ^^yw=Pc1~90H$H'#1C臄}-UuEq/|AfFήs'1 ړwB~,`rD"L"r\䞸S]")' HJARnޫH&C/L1)A\Kl A\bT)H0d0`D"H&a9.rO\Dީ.Hd$%F )7UJ$!H H. Db% HvF.1\lTb{p '=~|>> O߸OĞs#ښ$F^b`dg#%4*5Wcd2M^q52oYwU\#%DG2D{qrMbЕc1qLo@׉;NOYpirhfW" A0ŤArI$#cB5'?yO}1 m  #/Aø8FK$ DZ4D7 'c1uɎwԟo^]P3Hf H{`Hd$%F M.8ؙ_G/%c$ 7*Kl\bdSqKڴ3`D"12TH&Am PE[LF~ϡaA29a^^P Qu$d0_fב$˲W#; +D f|4>zoH7=,2j 69QUf~v8&<ʷ\brR̷4tfDR":$8H'g|Q 8& HsTHJ$qyyA$8ȉ]P۫g*}0azo1oiCD*iK +y(_DbJث1 /.Αr˲ + 1U4mEԑ?a2{I d9Vn[DNxE{{)Y\}^JY  I(OtīҭBĽex`fY߾MĜK$K F%1$-Gmk ?߫iܖHJ$qyyA$8ȉLɺ7Ud ]|jɒ?~ٔJXgnZ=l0q|mmM%ЙIDb2H$K&!pN;nL|>^<xdp8" wk;S[0%Q$9*K$%8˼<1IMMp}햿o9l>&Dg(b(rI3TaOKł!֤H Ac/aP5kJ&9ѶpLFF,#, hClVoWQ !㵀]goj۽h>2 'gc|%&alq]Ntm.elåxu`<ϯUZ*?t Hثf'g|Q 8& ~<۟ʔ-ןWL{>&#M&Hp8DJ-fenfhu阳[0ݽvK(R#,A~rv8HǞ71n-HlϓG~[!+x#c>u8=t@#A"AD^%N+ID"t$8%8&'t3 A|DR;2`9S_?9;ɨSE$ $rT.Ap +XI%c%)1?  $%dT141' p—pw/Gܿ9Ó + #^.4%C48=t@#A"AD^%A`[ܫst׺~"x<_[;}D>ÑD:p0_9dOVVDr&28$8HpOHJQYblnE7m] |qc׭2Xr|N|2qׇ5>ys6׶ȿ^zYxvaWҜA%c%)1i]:d-a8L&$#/ 䎡9i>h0[&b]O,0g3;fj2ii8bG]O&G miA"AD^%N+I'/aBvMdp<x~ \ t\3\dkA/Ay%&6[nW<ϯ/δ}hn\b2qzG"q D n%LJ"m֤Ud-Xcvuo箶?՗ҋx)V+ZJojXS|IDRGR   myvNzw~a3S_?9;ɨSDlK?H;k^6 "7|<_#A@<# V;8ڇ$ɘx .t$8%8&'t3фoy<_WN;NDMd`n e NsrG4dXZv`fo3H|Ld2VqwDXI01ϵ &J[ó@B/Xd(A0A.A"/AD.19ѼS 'LNj )28'-O֙18K|^$*e) ': ȱ %&%H%3%&'4qJqdI2xp$e4TD:;cg#o1|D >8owp yj~pAws㡄K)E.A"/AD.19ѼSœaAi/F@Ķa0o|vxz3QL39Y C&# }48|lOr>>ԇ&&|^$*eĚ#kV>xغR)C$_?HG4oCd:D&N >Β3 f -WˆO|_ϯ]~.2JR8(H1vc;zki4tL;cg#o1| f6Aid8qZB9~Tqt[.d(A0A.A"/AD.19Ѽ`g0lѹKm_/ <^n"^[|DDV\a[& L8a'A>hy䎡YCi?Ι(v'xgจ+P`\D^ 93L%dl ^Cg U;*\98,W蘆&AP'u̖bl>aeZNyG^53Qa2P,$bh;$, { 28:e H0ϠO z>_/,! =س9_ʓuͷ\* C <8H%>I$8H%Nš ~dȘ$*%&_XD.G.#F#'uPC.h2TyOJA@G.AI A"/qʨLd'\DƜ %&Q)A0š$ryG?rn8<^WJw; ze0 +y9 |=^aK/e݀o|_o#=mΛ…Ĭ0'\DƜ %&Q)A0|"hXN"Zpmw۽4Y r(O!4bȻD0iVe>(¯40 +y\lZޅ9 I->I$8H%N8j2~^Fe+yk[EJ4$%&_XD.G.obaoa4lcys>>AN%艽NoN!a.JA@G.AI0p=Ol=I 5e8QxxA/f2Ed Pb/I„6]vS~X?kgyUC`#A.h2TyOJ0m؟։Kf>zl' ?~O -Ã=ŕâb,BMf&Mš ~dȘ$*%&h O|~@~I9' o UĄQ"nӤe3-3(a1GrP +^F ޴9,M$8H%Nš ~DDŽA߉Ӆe]iV0ryG?r$0iYt? /-%a# $ep 8eҞ71@ܩ2q>҃=x:&2NA#)C S/eC`K$-",rTc%\"wyHʡsS0Ň-Jʡ| D<98C'xpHJ$P;o)2|89AXI"]b48r(A0Lp|ˆr(#/9ۅ㼚pipUp$%r(ApʝelLB߆H(i;w]1xPGw{4Fzռd$H4[H-F#) N ԆvXzu{Kծv471agyzIޮ&fz-s-iI+WMiL[ E.Y ,Ҕ@CǏm|c~,- 8\|(߲ȗ&Ff /;H$VZ*܇QQtO_W./a/eC`K$-"Ji h3}Ciǖ> HOjroSLF#) N;YEmx<~&LgZgWp\i' $8I9 8N2[6d l`ӠV'aK.p4FSc=KY{i2_?u`&cJ0aH")2%28diC,iͽߙk9?wm eJ$-",&)IT\iHL+x5L\FpiV'$K`gy ' CdS,/c茕$8Hp |$ A"_ )19 <ICȗ .dd 2VL,O A#Y"$$8r(s +xL 2A$8HK՝݇tvky_+dMl rҬ,O!H c%$h -n͌tUv5H0qc QO OuzYBS,/c茕$8Hp 6 6+"?7'b?_O_{fE1| rһKkÄ 3vIm[F7uctT`YzYϭ=aЄ dDHHʡ nJ0=Աg!|~4UYa>$8P >V /AH$@hsO$,(Y?>x>_>ן+naқsQd$Y>4AGD"11yFD;6|(jWt:1/c茕$8Hp |$9hca9HV|~0Ҳԉm>vig\FpiV'$K`V6 v54aY{m8/+!R%$L4 ~)Hw1OtJ$8ll2*ܺytȸ;^.L' CdS,ԗfeKmϬc7n?_XHɘ& $%cn>fEiL~yo;aC$8Hp$K$DG"͡=²mvɃ==XyJ:%Hp$gbSKCg'K$;':/qJəH$: r(':HdkzW8j˒4OSbSKCg'K4eخb#_OPJ$iH$d I9H$2 nc%,34k?,ޖhz0˓u&'5KHo [+p^:Hp =D/eKxs|StLD"AN֙`N@@C ց+t]o2?^w5ac)'LNj 1i@9  1OHwOt"_8+3A 6sf@~ fOl4 H417w}yܭ7l0tIddyG|y+˝fϯ.47"t*DR⭀w-Acb$M['vN12x  HLCӇfS@@28KD"D [D`Mbϲt[>?0.?q]`+'8=tLc(ل'BCkO9Hp0x'‹j^Vz#G':Hy9V$%̳~+c!qaG"HLR2;FA@6N~:D< `W(_L|`[-Q")H"V@; 1':8M63I Gd >Β#H$&Q)O|`=<_|̲䖥疥 LbǥSy&o$NӨ m@wDDO)^ XI d >Β#H$YN:Y? /|>~@ā@8G.A0NHJb@@bn1 %"eXI >-aTå;s7`&cn& 2;FA0?щ9|o>f_uI|UWvU= 8Hg?Eh_^ .΢.^ ,/ /A`gbK;A@NH gDEx[ 5%HdYO5A ș $8&Q 20$8ȉΉ&Qy&&Q䤎p6yHJ$K\DePSɝ MFe8m_I Ig{_"/x}Uˇ POvK ':':D噘D¹ :%ȅ+GR[,#jJ1ɨ s Ųx"?_|> S,H;$]xw%HpLrd?a4,Ap5l tNI()ӉW-a2w􃃀ԑ &Ifk+dg_Py3@0hQgϯgB*%;A@NH g`=Hlqqְ8d=ݫ!H$&QY%ȝw>ʷl"$8 2 H\|w֟\Xr~:2x|rTJ$D7rPwy"HLK;og3/}>oE$Hp$d(AQ99)?Bwud䨔 H$dOt mgoW"gF~ۯ:4e;g[6t ‘HA"G|w֟\Xr~:2x|rTJXX- E [[Ũy"HLKab/aC'+ZyE;."A# C D%ȉΉmӋNj>^/{G {cb-u.3L:0x|rTJ$D7rPwy"ц vcǟ:"Yv?f5Ƕ..sPeC HA"G6 +DSR%&֟\_. -q%ϻ:,k,u5!e)&ITqw rml坳χ-l."A# C DeoT|w֟\Xr~:2xjٚqh1H󸼾s 酩2D"A@02.Aͼo[6D< D %9* ':':'ao)osuu^u %8Kʻ'07iH$%;2N4%n/#_~*e,hKm2 |\DG"AKj}v<sb9<iC`(%r(em.c$8H )A@.|j\YrgCIde.#/qqqH~rO?ARJ[MJ͢#?{u8_^\ c$8H )A@.|j\Yb{/l()eepq<.2ΒOaKnup}ޡ5<c-K40sepo UM| >VU\ Ά288\F^g ,yp{zv6x,!w&X_ GdL SARI9A2T6qr$XI >5AG.? 22K @/[|ݟ[4aБwA>3/#>2fkJ&o|ɰ >5AG.oV=f׷2a`CT:(rxZJL$),ID?Y|C$N kQ)A@@Nt@YreD./M A0 %XINt9y"|L.ʤ2rH.SYΓ ~9ʇ28H:$R而 Љ\^8<8."A`J7s/;rqqO #]D +.\'%dsepmйGJ r2Β.C'rylจ I(AJrbXݾܕh)zewX RF.#?%qنR< ntHx;Fe ΅5ɨ $u :֔~DrZ|}"֤//4$L fA0 %XINt9y"|L.ʤ##惦'6"} ׻K$%dsep8=t.IF9gApp6yp\D$ c%9q|n2;g<_MO)m\F~&KqD"C@>qaK kQ)A@@Nt@YreD./M A0 %gXaȯG~|L5lVf+dTeg"$Ng9OJ$2:( qz\XJ r2Β.p|֡/:y<ޗ!̖˸8HI=cr9\$I{I}Nnl¨ >Aй&&K\ ly8m%D'x%P ~D"A,xP 93r q(Y%&',I ' 1$%&x\_[fl%Hp˓s:t(Ǭt|L(>ɿ)&x<~ؓlR|?D${^gn]^.s +K"$$8Hp%%I%[ e Ns2O)H$."c\`cɨ#JwtTʘs +K"$$8Hp%%I%[ e Ns2O)H$."c\`cɨ#t NI\N-狻?5R"͓ $K")AJQY"YYPƜL0' C<8TD"28&QY%&8 +="=ɹ~=_o!JX|Q)cNc~)H.HJ AdTH 'u o141' 8;^SJw%/hɯgi8*I%&8&:I|$HyG?NGml/_z/o*a&A %Ȩ,,AN,Abh(cN@@p +& d|*OA"q,G\8}||}c Nk_ˏ~97=-K؜uNArI$DR 2 D9y9)`N@2xp<EdpLKX  eѧ?OwYῌG1&?*eIpLO9%\I $8P-2`9AFJ'++Xo#ŋ0,B?6d3&DR+ .Rnw3r +AwVֵUZd惜YPƜ`>qoxNe-!&xxd\Jh;cg#0 +2.%t@09 <8N9gyw DYD")/h"8%+''GI$8r(A@Hq)qv8'uJL +I%\~(ȸD3x8Dd%yRg&DK$dhN:8 + pLQyR䎡Y'K LN4SNt@@Y] 'u/h"H H$NA"aZ*;eW?u33W $5r(A@Hq)qv8'uJL +I%\~(ȸD3x8DZJ7Y_s_.5S2Ds8%+''GI$8r(A@Hp;JF׼'~m+x#oM4PpL,yFAƥ&')': ,.A@ȓ:4H$M$D^ pL_]Տ*}||- Cw7&T[J :%&w $ΒG.?ad\J`rٸ.%]AB$&C-@$2.."O;AM_?ME8!DRQJɅ{N;`$~>tvP:L!sܔ:8殕vj01f+O;Qy&sBD"/~`tz}9pgOL ~wH$&8}%&QYӡg'?Eɽscqi-9l%7ZN~0In56uC S@RB"1p)'Db/܇N^b9Jqyyl~}\DԱ`m:qckx|r4u97Ii %2N:P$帔'HL.u߉# DeyrvN c]Fj%@+Ns+ӓh?*A"cN@#\/MN#eD99|># `>փ)[TW&ۗ;/)AZPFgyOC 1'H$& 'u ߰$2xgL$rT.1yeT9GBM $eegF~XyPFgyOC 1'H$& 'u ߰$2xgL$rT.1yeT9GBM $eNN/ "x6Cp~˧K$p:Ko<3Gr&9*2fy n=ڰn{ES@nZh,x}x˱.| 2 /A@@bNHLA@@N,AaHd IT C2֡5i9//Ԕ {JRF^F~fgwAn{O^k`;v{/:"y YŜ Y|<83A@Q-QPF^'5 5%H8>Gp`!9Q=N[Vw:0mk&p#d> _Ĝ Y|<83A@Q-QPF^'5a=@WwO-_y<_mqY>M҄APFgyOC 1'H$& 'u ߰$2xgL$rT.AAGNM_>_U|fi0J0bn>Ns12s\[y[mΙ ߰$2xg1+͞;&'rPt<ņx:<˂C0D^[?eT.AȓuMSNO'Np{9;ymBkmZ[*o娔 rIoi H$Pbʡx0>fy2! ;^MZ_ +a+-:j 0z9؄ R"Y%'Q)q'-%AH|#$C ֛C:.nW(gs>;N\.n,OՖC {eR~0\oKA胜)/楌͓ .K Kیǚ$8Ʌ|r(Aaܹ ]Om)# H}:Ӽyry¥y)wP"yqXG\͝PLܥ8_ k0q27340mR~0\oKA胜)/楌͓ .K KəV6{EF=[kOKߛ9|r(Aaܹ ]Om)# H}:58L[k'R|_IkYaJ1O+'D6& D|r)hϬo ;U}f/xxA{ޡ{bab: H9ޖ2r d N?9S^=Ko'(A.\:qX9 %eѧIeU[E-fenx<_/5iA¦E,1s?L)RF.A,1' 'u y)m%ȅKRB'8+e`I܍H_˨IM;l.ɝХa: H9ޖ2r d N?9S^=K5pDIɩ ~Vݎ +Wsl/P4s[./I@#1\/q9|@se<{}?+ wnKm A胜)O&fݺ{EkpܨuyrK?{{ WJqa\^8yy{s |GG 5%&ɸ̱r2Ӊ 36 <6O>i@~KpJ$˘_̡JLBDD./ͼǽy DpEi?Hգi]/Tiګ䲑tɽ OM%!CLKsz<&F>X|W~1I4<yy{s |GG 5%&ɸ̱r2F|` - =_eR&> CI(H关q7! /AwtPS`+'i7<'E6{Y/:Jc6PŒΌEJ$H$H8}ܛL H;:Z^)A0KnA$~a@%A;89 p +& ':q?Dd\ʨ)Af$g%H'N N/ARяq3A2NPb>9 p +& ':q?Dd\ʨ)Af$g%ܫݷ < W{w} 5}xKj\%ge%&yg}ys%LANt>ȸQSՇ`WѷͿo@GIxN ^" \b2ꔘ }$/w?$.t(1 %&ȿ;~>KgFzHJ6g7Úsx@g6ftLM )Ot@G~ɸ qwCI(1AEYD^ƜyFD{& |qÔhc0;ΑHJ%&bT=|g/ϟc׏߱Od4kCI(1AEY`' +xw*eU[C~r C rD"ON.ANt@bN,q|J|!qJ'Y~ǣNIdAԗ &AJN}De;I C rD"ON.ANt@bN,q|J|!qJ'Y~ǣNIdAԗ &AJnpVۻβwyo>z O.G2823ܩ/A.Դ#"1/x<>*xZO8W }9HJ$e8J 5%yrvr rsd 8%P p@]•rppqsK>Fck19Dp69PwK$r(߹HJtdnZ|W_omn0 d 8%P P?t;uHJ$ rK#W{rw{|"_VcoOrsB pD"ON.ANt@bN,q|J|!qJ'Y~ǣNIdAo2lx_\`0&mc/R{J4& C rD"ON.ANt@bN,q|J|!qJ'N.]){jVn=opu\>W7m33BJN}De}26ƉVѥ2׸MBZySE 1_HŇ140HOzK/8qkUG\`re ȅKR}\D"Y"1O c%A"JI(|2t%H̓%.",Ș'ep c%$c>:Hd$ z28o="D>2ы/l?> iT\`re ȅKR}\D"Y"1O c%A"Jb3(pu&橕__mCmȹY YJ )1ŃY?RQY^0penP箑iJ{PRE$% 2V$r$ 2x'C\|ҞTEbi).˘  DD"cAJDPd K'K$2x2~thpʟ +E;ҋT00KL.9pi^ʡH$K$2Id$HXI@0 e'.AfqTT[fہYYb~"G >^m8֘=9zvB+o D"cAJD1XyU[O@|"¹AN4Υ/AYq)Iy]tByr(,||LʘO._\x< KGD]y<)O4Υ/AYq)Iy]tByr(,||LʘO._\x< KGD{5H7|ӓn m U߰ȡg;ǥ'w ɥD P red'LFްC|_sߐ]]i08Abq( w8O'剎iʰ> _F~,Z{!]*X4Nyrv9.X?)N(O.%e/IɅK /p- .PWʻ=K#ʿQI_kؓ\4)7wX;g;ǥ'w ɥD P reӥ='Yy v o|xсm']a"cj2͡sKq>7o.H8%2Nk&/28%9*Np$rK/p\ p;PHՓGD~eZ¯6PSC@,#H8%2Nk&/28%9*Np$.\nn J|6ÿet?;"e c% r95KWCoǏmx<>"K P/hHH䨔':q/A~½sa'LAԗC  1\;ۯ+ߞO>XZl>Yۓ\I%׬5Q.#H8%2Nk&/28%9*Np$rK/p\ p;PNeo?_݆I~< 6ӋKU6005H$*H$2NGeǚIyዦ dDJydy#|󅤿kx7<Lǘ)1/ )+IpǒtW0~+b ޤC^fja)/Ԕ,Q)Ot#1W[wOxQ C S\`ra r 2V&ep|Aq%H%!$p$坳Aɉ&;p$8P$) \X\Id$ag&epwG$r |CHd<9;IysWw\[䟈*YE c3BܘAw? OK2);̤ D.A"/q > 'gto)=zDU왏ߘER^t # IJw? OK2)i9pp1EiܤʓAw>+'' '?(1A8 +/MI?\.s@?0B3&֟ ceR+wؙI9\D._|(A@".A.[/6qYFxIC S\`ra r 2V&ep|Aq%H%!$rrGs7m?#q_bTƶ)p d(Ab~ +KYL.?A.Aʤ 2V328,pQ\Ȼ}Y<3}Yc 6~b.}2VNN4ANaI0x ^W{u!z]? b" ceR+DV) jvQ~ډP9)w>lΓ:IS@H\D"g%ȉΨ\B2{䛇N1ő,H;KdDd(ǜ;g`rr6IyAd) H$."3DgT.D^\ ~f=_CHH$ ߳~.? h7M3HO, 0)LN<)/h8E${F^}K a34%ܽײ*BdOJD"=#/A@NtF>H%j%/Yy8$]ׅ*n}+x/r]>'28&'gs4OIJD"=#/A@NtF>H‚@f 5wj>p翲&MDLGC<99O '$%NA"Ap䞑 ':ry냸. +|#yMt n޵0s#Y" w 82,lު~#E-m!E${F^ +/[ <+;j?˯ IT/P |$KrD;D┯qHpL_H;"_ e$,\gɑ8~5s1%*A@q8/DsW.ȓ'H5 Kw~Q$ :cҽMۭ gct l??J{D\Q$*˗tARF^ltΥ_D"OֱԼdO} uB]݊k s0 :cK,9ԗf&՜W8Ϗy\.A;n1oy܅f}ltΥ_D"OND"q8$8&/q$+>JLbzp]G'43tg V@bB|IJ$ee>K'/\K$I$y!xwaH"}_}ndlF~ng^\+sP$*˗tARF^ltΥ_D"OND+U..Rџ +C2B'M;J9˓urTw.H$56#4N}<_d(LY|yү\"'g' IJirn'i'uʗ\)ANwG.A@ H$HgbrRJ%I(A@"/A0J9JEJLF3/|,]F%CJ:K$e% ,$rD~gbJKLR%."#//%.1yt~QLWMې۸gޡ.7?5x<>IJ_Yg6x~WIy DD~&&'5ˡ\"$Cj"P%&8r#q݉m7>OE?WO a2/R)\ )#/AHf9 K$k3 /9ܸ|^xUI0_LaOԶD8LEJLF3/|,]NWdpf}W_-~%st x<>>>a<$ D~&&'5ˡ\"$CˡKKL`]kO7v$^/>;&l:ȼ=3_V:X k: _D.%yD"K$%ed$ H ,pv&,KϏ?;3 M4 4A09)A@@2NG\F.GD I(~|(1A\J$r4 #-A߈ozu]mtK&4MLNjJwtQ)A[a'>)R|q㶐 w0 +%L8G䤦yGG8$r$8&  |(1A\u6O6 mH$党 4A09)A@@2NG\F.GD ˈI(Ù_pKe.=lЭ7onff' H:Iy>MLNjJwtQ)A"Q)aV% y_oOSӾ\)"K$%Ѱ،CW4Pe3aYeJ  c?xlxW|~_m/)O~%ȸ'E. |O@@@^y$xcȉȅP8 $K)A"/ĽC 2.CK$%&d8D^ X#7P7b.av?%!j.A@;QYi: qI}7j8Nt@.,$xpLi00G?{RH6!PKyPI 7%!j.A@;Q)-\wXvܲ|r=t0$BOc=6B<Ԉ4ٯqH8SD^")1{ 2aRPKƅU𥉉^V/r|+q!l$"Z  5 㝇Ȩ,48];D@Nt@.,$xXs}K u풹vp_ 3`^Pr(}DZ:L' /qPs ? .%i0y&Hx2*XyۭM6s幹xF?'O{3%&'u H$&wv$> ~RGXH)\יt(G"K;/5K/\Kyҡ|TJ //ٙI:K$;;t +KY?#AtL$PL.Lp:#H%ȝ%H˗\U.Aƥ ~RGXH)\יt(u+A%;~$xKv0>???xp_>x4 CgJL0'O,A@HL)H.yg}ӱ2 CS@*CXp ܯՕ2Q?Յx/01)n)qH%ȸ'*1:G9(),:h.l!vri/Aԑ q:V&d(q +&u&8H$~$,oW]/]Ϗ(fW) Ob6e kDbrgNAr;'u$HJɅ{ rKʰ낣CD.A@,AG. Ðv op$6vk5lOnfa.I92 ~Rg DbrgNAr;'u$HJɅ{_ǪXmXvK{TeuM#7fd^ʦ٬Aƥ

*%0h=Ku;`{^ŭ|I ce"A`rҲ70o[[xvu+%Y?~tȡ)h 8K w8K:%ȉ&q)Cs(sXH$e||}P" k&g ':HPLN4Ar%tq; NǥDD9_9rL$2 >~Td(c~eQM[> Ż'{e"&bM 9 t\J)ANt0KC5#Gђ@?_{_p?c="*~ȯAPRlJɉ&P1V{uxф>V a%eOM 9 t\J)ANt0KCyb5tN9p~  ƀ+ۜq7t~fl`x~}Im"{LM9L9 t\J)ANt0 x32Fҟ~g kqI9.Y~r? r3VNAHL$B{"`rCeK wOʡJ8%擨5I{r,O?uK9+' H$NAr&yRG`r=wA0ӡ2Nǥ M;'Pn>䁓Tx)xw= x HLqɽD'8^ʉX9A^A"q +3Aȓ: +˨o= ~G;*t\Є!5Nh +{?~_ɇxIJK& w/R\'Nx)':cye,+{- i..1BCuw5lwZm+M;'P%HpITMY IG7…kr脑@'8^ʉX9A^A"q +3Aȓ: DIXP[۷oWeB\&6ys}e-{!mHeBi=_r9t:Ot㥜茕ep$ 9 + (5ba5 Ce"`rCepc04<7%(M Vo<)]'a&.#/R\'Nx)':cy)HV-IoW%\&-3Y?)r(A@㔘'ƀV|<E:މ~m$۩㥜茕ep$Xvx Lv$s^-Օ.ONL]B 2.%y)HqMv2)wK$rT.2NKR"dL+':P~'Gr&P.$˸{&;e;%9*CǥS)A@p 2V&gleLA@@rI0*^ țdŌL-t/2^b2t@N&8%rN}DPV : DBIЙK29sL$&'g3C'ǒ?g1dw-_/`:&EL4aʡi)"/`rrv CMcjE>IK^o= 3˙y bR': d' ~D"Gr(t*%."A2 +K7VQOLaC>'ؐPڎKu^ +eܽd|LpJ2au,%?e H29sL$4=zQt=ϏqK Yvr/edA`C d\JR,%&CdS"ԗH,c-r >\y~j[s/X3'p3\0 Q鬒2ɿ"Fx>ɽo>t@N&8%rHXyiUzod ^FnF?!g$&މeKKL:wə8gy: qq%': q>yrDD)HdA"A kNޱ&މeKKL:wə8gy: qq%': q>yrDD)HdA"Aiu{'rWt0*]ހ Հ_s!1yID 0q)dsXqC ]By傥rKt;K$ K$aǏ{D?RW+b}帅AqD2NǥĥSYKu&cHd.,=iWEݾ^7eӗ+'cNS@.9yļLuti[ˡtƹ+bH+3#q:t(AK(Ot@}$މ%ĺ /jjo^Wx<><=mHf锖qrT RbX.2938,O$. ft`%m9^:'yrDD)H$ J{gr=~~}Ͽ &9;)(Nʘ˱.QY^8yye_fPep7j7mዸwxaぷ!o>xW532/80;O.k9wb}rwRQL,,:H;l(%关w}cмQľc_7>Nrq~Y/{/8 /cNƥ r(? rGމ ,haa-/n>tr9/D9Ѽy9CC8;5ä̎xcgH~5r_u]}7a^ wANXp eIXykXFhZX[ _pp ?f𒑰n65J 'CygɅ}M; 'NO@Nt@Nk2ur&93q y9V%* g3/ cTÓ) x|<1/_L4M&R,cNƥ r(? rGމ ȉ.ZӪZ^;&&\r z¶1G V#r(2N28&q)Qq@x<>_Y٦1 MX; 'NO@Nt@p4<fVyx<>'~]pzG"1Υ(ȅ7/tN//%8P _㜡 rIp帔I\\$;C>ɸt.GA.|ysȗ w)x)A %&%Hp;gND.ǥgNw.}wii{'C|Zr%}>bޡDH,ŔK K~(kqg|}җ1'q;?Opqb ^hj0 |,ʼn>ɸt.GA.|ysȗ w)x)J69AwB3R3gL|$.~ .sÐ06+O} > xBUoXeK;``箮nd(aɼ~lw P%K:$ >r Sƥ/$83Yr$2N{NHu KL0O$w P%K:$ >r Sƥ/$83Yr$2N{NHʡtҳ,7#=f0ˡD"\'g?/q)f+'H+Ct>VHA'Ï*A@#9s%A"1 #O@?+0<0HiM!r OᏀ PCr(C9*q &omB_!;x~A/@M[k5S")ehD.A0l鼔.j-~(A^!9Hd8ep2.eܛV@8J^y B^$)Q%Ȩ 9ΒȆ͍ct~!tk}<IJ,\,,/G@@r(A^!9Hd8ep2VZi`6kִ'/f.>ٵaL֔MsHu Kcp}E|<~Baʲq 8e\J4+o?v5JMo GY?$@/1D^yfPF.1ΉwA@ Db2 +IpLȗCyl~) K\ʓ#g;KL|A@#jy9KLƽs}$g<:%;:*J<3P^8?#Ap +H)nu~ #xb'`^μ1izWZ~PF.1ΉwA@ Db2 =mM咰ĥoA9KF g':!al="ZGb\g|"2$=}V6f rGG"/<3r(#{; di@2Jxfx|???~K'M bqNS@"H9'gG~wKq765gx1 _+B@ӑaL'QyGGXI`gF.-˺Y=+Q͕mi +8-Bn}_,eVw +jy9KLƽs}$g<:A`Y,]geo"؟s`gF gt@# /- a ɻJ;e2eD^yfPF.1ΉwA@ Ă PhбɥMJ`t_Ey﫽 *:&.aYp +H)xs+x<>J9Zy{/t;L'xJސnn/q-ŋ 3ArT4#_ITC 5s&&' _;v4)'g'?3ٙ 5%&/١ ӡ/K<8'ce fDtidIpwtLA0Iל&t`jx+&x<>~~ad?A HdP⥀ 9);䮾/M㉝,7;QHsDZHΌ9 򎎖Rb 8&2)1f}bM4ry{RIt`c*/AVWǔma}š aqE\ wfW1r<+Ti6eiX| c LVi.5KHV3O4帔c<9wW˗;>Edܽ+Id D'8Ȩ\w4ɨ#1A.Q)O 8NK\4/X?)Ot@N q%Opw/DAR"YD< 2*qG28 `2HLK$rTKM{7_rǿDޡ`0q.&%Opw/DAR"Yojʲpx i3(w4⷇GD OoHo传l57`ÆNIyrrĥ/ywh}ȸPZKD-a[+?y}2 +-tJK$rTʓ:%NS 'PYrp2y<`<v1wEs7)DbqrLdp$%%H r,ZIo.opqof-hauڍ倾k%Hܽ|ɻC\D˱2A@F΄E^~Ņb ?3NPF^a1v4j'ɣf˅(] 2t`N q%Opw/D)R2V*-uh2xCYu,rMK$rTʓ:%NG>w_#YxvtG: ce" bHR _pȭf}p?%:p?Ip| C NkNB r$.ehpHK:T^85_ +8C'p8Bμ;O>;sd(_špPbǚP3K q;y&Gd͗2 npv~.寅w`>Tb;/L' +5kp+d(_špPbǚP354a]qWxjf`-?9HDv!}d +Ϗ?{Ojj+b6s91G kN3'A%_ +' %9aAmuJlK_˸hmAxA̽YNU"ˢXɱ':Կ$Hg0$; 4d7?'wvD(evaa0cI(AN֙ĥ M8 sO~uEZxw2n6@`b~ٙ C/>jC Kkbe[أ/q+wϵ |"xVC'CǯcӺ'<_*}fYy;|!iᄡ5'~4+^r_Ϗ'ͅl.PI|]~NʋHL:Hq)>I\@09)AbN@0y1O$& ?8Kc'\N}c%$1˯ɓXy XgIy9.G"1Ky&'5%H &/q4tgqw섋 M=7zq&x8dm +PHq)>I\@09)A%L{gӜ'+cF ##臄wir%]Ntꁊ<?K'+L '&NʋHL:Hq)>̫1qXV8.שi[ miUA4A1w/LAWR^U"[%&%N}c%$a iB+]etn?'.q.]i.](1Ly5ĥWoK7,1ycrA$H:溸/ +/oVJyG$.C ~I#i?+=x|`3$+l5}>k +Fr4Ѽ@e}:Hd''f :a5$4=X~><ؽ˫շ^N|XU+ Dץ/cMÓF~ZjU6G͟"8"^p͈64wL,K||y)x)H$1Sw}-CpNXరF*𮈫#Rg2r9.%tȘON֜ O/ð#AL M-=9jr PߗVrd$%&1C }p${Pv LɺzXrQ@VÚ5Eq)D|rdļGnҬM"VCg/ᷕ#GdI؟S|s7H{q~KۉLfh&,;&ĝit|ofÕy%|>D&KfLN&"tppKin/vUһw$GcyW X<ڏyi;I OǑ&a^\x(r};#!^6ii,xNd&qwF[F,l"BX梛32tH4Hb"f]gsva fYrڍkh.& 4EϹ"ډ$hшMDh\tq"u^ii@,Xd߬Kl;̠A,$c > x'ս:͍߿?z_6aF,l"BX梛EoX"-3뷳Rᳮy::}|7￰$̲ȾYt5[_ywOv.@xr˾O#y|u;4(0#m.Yl"o}rۀ&y3q#$rGN5G ethwG Z΁mz;vE_tO_H/}>}rWw;M; <2=q34He9h7tܒ:/Cf qf2g̞~;YY|Ivh fD;LDYFT&](KDM3옑6ci&>yo_@_Fyv].)LM0[/WXaߌt9+KA/Ɏm7 f_ͦg'U¯u vXGlz^//w)ϗϝy4=1Ph':+KAK?wʎ͸#pArCԯk};D\ %~"A"qf1uF?_6˒# eYrRDH-"yfYrMfX0rQh&D%~"A"qf1uF?_6˒# eYrRDH-"yfY2C"3&bh3tl,Iޫ۾O~%o1.iZo7|~ <,x|'4|i,i< +@n[; K Ω~һ~?*r"w6?haYrM"~.n{,xIU7 INkSDSO.<>bN yoAM _^{YG)d\]A ʲ¥dIny'y:!g#r]yh{د%Pt +š9) /I#On΅#r3fY7?sVvpst7ټ<wDMg?DD:> 4/ҽk^VyMu^/6 SDG  qLiq?mͲDf.1vDx4/Xq$/\@LO7 q6Ї9aɽv a _bF,K1< +(?FyfMOM^J2 KxZ YYiqЕe4wҙ4>^dFh,4h'2E& 4x$49kYV"4;Ki6C0+iY YYiqЕe4wҙ4>^dFh,4h'2E& 4x$Ieu_ B#ix;~R^^?[8B@O{("CG/,;qwG$Džz<(fxOA/ Mbt|>>t <[r|D^ڳ9S=r$tVfhhDD&"teiIFb]О5]>ջ˞5=Wnx|B#̆ƫޫCX#ʎg$<;S5$̲2ˏc~[b9Z rswrohÎrqh-dK._b2#41ĒDLN3 Ϧprsq1<8/g˫;a>zjuhLDfNsG&'܌sSn ]s J<O +RR8\P7i9%InƸI/rO;{r==YtIp9w;]YfNs'm$ܼpqc۝í_r6"Qy@<>^#@ R$L?׫Փe7ؗt;oxpX^;P-=Gc;L_2# G._ OgmAF%СI4lfehnD;YZgىuw @f4Ҭ.3c#u41.,@D&G\N0C&aҘY-ʒDgY"jYֲe'"fMo"$LDxW2lΌُ,Daɯ}=_c:X!s݈v,,Co`醙>n{qg#Ư_+bӯ%/[=hW@u@Mk;D9O?mpK4#n'KkTcqXatfIZ%_ܰ+ŭ5ϮȻD^^ȝN԰Ĝtfř}Dhay׼n7&$LY4"LQ˴ItF:,#mE. L &̌ Li$Id9#h34ynL6I4"K,iD$2,5iKtfYFڌً\dzKff&SM`M4DDLL|sbA7ѯǩGo'cYĬЯaC$|$&>f"9zn@^VkP{i3^2^8M)ח@xI +DY"j.&GɏˏrvcnIDnet+OlyRVEe8ǡ'ݳ  Cl0iDfY"|Oss nv]^-9Woef"tgYF +z|x˽sc>_K| T=ɁT0GZ8E&98h7&L?~Y^eySu`{_t`[%w %.krLڑ`"B#HtK12f"2;8 qN$vwOnI£iY#NL6p9܎w-BC~x;'҄}3tͥt& sʎ: 3ҙSvLayDfY"bif,ىXdD|r.aza34KIw' 34ҌtghZ A|ĥ$LMf>=g*fheyf?ikL'v,?>?{| A^}4ޘ8r:6pD,KD,MM?̫WmDZ[{=Lpw"mV +#ɉAt&:Ye,w7\@0DiYY$2rQD0It\ҹ\쌴ȼݘ)$At&:Ye,w7\@0DiYY$2rQD0It\Q c><ÌQ91&L24 cZ|9Cyx?ik +| -o:rgij"ٕ-v|O$f[V&g.IgRzq{pfmܽSbk86 '#97s,ral=Ï8\ z'8&sw2?gy;^M 2/P!.G\^t?;z<^&^^2^8}E; +yqy5U)27Df~(+g,s$4y9gS' ]Bϻ66 BljcO8\@0_[A~9L7ɌK??·BD:Dg3ҙ%L\jr-Ji!GpD9]>?;#qXNf8hg/13?OnrpSn;+dntYg +&wId"*͏/n^w>ki/a٘r&f+#bܜv"4"ҝGribnrNY\1r9n'2I6iܙ4Ј'\!4w.˘AS3ssvAģ;x҈HwF ɥ+9gGri5 $ueF>YǛ,H4qLƻOwd΃GgKKHjD1XO^O-P&oOl ?pctyqkrIG%Z'z0C㰱JN"vʗyʡronty𳼜݀^㦺aݎ'W_]; |/!Ǐ) c4"ҦK%nBr:.R\:&LԎFrp8V"ڮ}Uh|Cs^5j!mXn9氳mǫ̋[&1s~ƹ  3th&'3~"WD>e'bN].Be'bN].B ,\di3I$\v𖮷r;\^]?1r|(1QaէV| ::v2}/M|}v~b8v|et9?@x0EC1c:~LRg8ЎO~/7q(hivpko>YsM˫F}=^P^\^b~ub˳/[8K;+K*_+|hXNM7rˡ_ +mҗ5};&W2srj9g|Œ/Ltӧy8̡ rQ/ӑir9xe%|g]og!-Alvvz9 K2M2Km6/|yeG^?lL} !\1__""o$2+Ks^7A~z'mM9H^j,Actp:i8zq_啯B[g#nǛj~z-m^f9'c:?ct QYf"Hg';Sv2';xb^Qߕ+$L5wsx@ ecaIX2cNywD e&yf3e'sÌ']I¤Qs'='K  Y]6I4wBM>_ǡ|\$a"$2.˿ShD m?;Y +DoǛLDڼbNmH^z,ۿWiy9z۞q0vehsFl#Dp9=\IDl_o~q\j.l"b4u);&"ҙ̘!H$De,C%41;;<<Df;1̥8^jCO1Μ Hd'\ A @&a~PX&,ܝe,qޡѦz^;`C0o!Gܶ/Njz~1fhmEݶ/dď?W_}=_mk4='1'\9u#*K34),'ehDHg0쇦q"fLGrE6;I$26 Ac`dĜfrQԝFX>,4lfֲ\T#I<ƙ1wڙۣ7IWɶm۶m۶m۶m۶| Y3mYŒI1,$qn&2E& 3f/r2Im"D@kȒDg=Z6h':I&I$a2%sG3y"BK0c6YI<LdޕLf^e0?Dy&yr%:/"%7 s޶m۶m۶m۶m=> OYd1wzt@ 63&βDLvh6%2HIbЈ鼃MDf$aehDdq9#=g-KKqf1AL睻]3DMD猴I,ͩ;,EL"sX&"4b:`mEc ?-Z^K;x{~gq竷m۶m۶m۶mr%b_|pYADdFei0٣}rXd3sYOYYvx2 Fdyr,Hμ+eel2I=ىɌ$a"G"fdmi,5 $d"*K3f&<zr& z՗m۶m۶m۶m۶ws/?o3ET9fYٟШ@xKAMX͏o:#.E%Ad,aF i,#݉@p7hL64웈G\L6dFug#P] +hMDbn~|$Сq),&KdS ߾}H$m۶m۶m۶m۶ϯ_|2NtY41)KYaYYvxu*NdH5iYzQŒI"1$: =uGD@#"m"hIJ\KCODue''\|n\D].Ys6Ie%H$2i=w޽Ͼk?-k>m۶m۶m۶m۶nǿ?|WO>ts8 ]6lp f>siQSwhrN.eyre8isN&LDcF:QYMDhf3ЈD.E6MD^i3f4tu( ͌; 4Fd9' fͲk2Ҧ7=4y'}w?0m۶m۶m۶m۶߉/۷?_mqH<DYe0ҌA<+tVX"/i }1A@ I"ƔMknξSwh&b h"eI#F¬K3jbDYYӲc8ȦIyG\Aozꫯ_~m۶m۶m۶m{pv< ~iW[:up^g 3&bM1eǤ].IŒ$L0#m>RfY'\ qW/jD"tey̲D ̉YflgT1l"fMĢbʎI#\"i%:I,aF|ĥͲO^Ԉ.E7@o ߿?}yyom۶m۶m۶m۶܋믿ly'mip&%ltP1 Wc \j"ΔK眺$$LiƲ,+LgdeY&,Z.겱x@0,9`3t3pOKR1w\:ԝ'&aҨdNsg6ϻ-?/a۶m۶m۶m۶mz\ݻwN8]㼍%D:ģ.B,9'I#,˲fh̦K8UssvdgDfef'Ns&a%"4"ҙe YYf%"ҙ$Јpb9$LaY56C`6] +ę}o #;D,/?\-;zzDo۶m۶m۶m۶m믿O~eW 'm3W` 3f)W0I6Ӳ,#$zD' 4 4s,˚Kzas& XK.KL0AZW` 3f)W0I6Ӳ,#$zD' 4 4s,˚Kzas&{O>{Im۶m۶m۶m۶v?;W㌍A8sHw"v٤&aA[D He&Kyf$Lα_D 1QI̹8 Ј'\qW/j,ND.4$4h 0 d9#b6$94Q.~|n޶m۶m۶m۶m8 TrMryguj.& $ѹX6"bN&lDeei0$2IY6i s }W.,Ke[ M¤A[0e2m0>OesY.r@ d.l"~;LDYV& A"Ĝeix w_mn۶m۶m۶m۶m?o߾uj˜E63i,>0}ҙ4HgF%d"tS7h}sNbβD ItN& & 3t蘍%,I4ЈŲXd3~3 3 ItfhDY"*Nf BG1u&aG<$,KDy_z@m۶m۶m۶m۶ޜq_~O?'mqsL3i5/H0Ig7+K3ҙ1;zsYN"dDfh,yDLvHd~!"mV$LuY + ҙ4~I$̳웕̘ =ٹ|Qv,d'IcI»o޼wvoIZn۶m۶m۶m۶mqZꫯigu54Y⮻ݜ\A"34"ҙe@Z$aե(H-W23m"6Ie6?R."43wFt98@u+ HdFD:, WYY$̺\{}~2ɛm۶m۶m۶mp*v]חZ?_mNfsFԲ|B($2#mF 3b6i0\geyf?tY"$LȬ,3fHg.l^d$aNC#f "DeĜ,q< +34H424Aeh~$YfY] yvM۷Y^m۶m۶m۶m۶n竿>ݻwo޼q'gr̜1C3e9 +ND̾Whά.g  #m2c I$Y2MKD';$:ciDQWhQ]\ݜzD'"f+4HgV3e@dhБ61AFՋf;[:M~+'ٶm۶m۶m۶mn?Ŝϝ9d"'y$a0CFe軖4CȤ~cZ6S6;i6C#Q]6h@,l%Zv,#m"-3cN&LW$2I$Lf ҙ$2+KI QZ. M"Ƣi2sLA (KDu٠=Y4*;]|w}ȶm۶m۶m۶m? r޼y "DLv&i:&Ĝ$GK|n%b0C/Itf,gdet٨ A0cKfhDy2';4';g4$L34J|mK qm۶m۶m۶m۶*竝qZW=_dN8SYf D̎,xȬ,3kYlf9sُ: #;&Ov"ĜY);&DgeID\3c6YfV fe&@2盉2e|fs>:HЋ9;̼߄yoGq;m۶m۶m۶m۶̌S4{#zLYDG\D&]tH$E63';$a2Xdiee9B&  Dee$a%Kd"5AM,K"#.v"A\:gM"Kd0@,l4˲\ng矛ȶm۶m۶m۶m۟|7|g9_]9c.lFzNbnj 3tYFLE3Nd圑62fGvΓw;Y% A"3ҝ4\,36IdXد.Zӓj^gvٌf #g%"ҝ3*9#me̎' v",EFg~O!q.۶m۶m۶m۶m~IW%9I3&);&LD3C#"@ @ܕ2C>;!:4,+LKdD&aFhDۜ99˾vcX&I#n%N6IdN1Id""L`\:yСeYYf"X"D'"4 31ol/z_Nm۶m۶m۶m۶s5?/y;9 H/M9uG dg;cFD efY"#;g:̻zQ@Բ$;HБ6I#Sv26,9eD|&机#SvLb3͝XN1#m"2,Б\D@@O睳y71oh8_d}7?joy'ܶm۶m۶m۶mrpqYv~8C"ީ zC"QYf`vdgSvrihDeywȬ.7n +f ef2ҝg?Ge'%vHy}iqf?4̳y^d34DfLDeIّeN1w7˥I"l,{Bx7ZNV?[ςn۶m۶m۶m۶mW/?Cy'vLzp.WcV1-;Y'3B/l"hшE63C#Hd\3(KwR3& \:'̘}+d34wҙNd'$2e".0p);fu)Ӳy2#&FXd334Dfe:,qW.5C/l0ϼw̼{>NVv۶m۶m۶m۶m*h̹yq.4CeY"DD:%2l6FXF:,=R҈34he1uG0C#B#H̹E˒3KhfDe1˲,DY""n6He#,#YўQsC`ię}]x͛7=Y8Ye۶m۶m۶m۶myyyr '|Aw%"4brKt01n>\9̲D bve3БΌX"t"Kde]f`O6/jHciDME& tfY"B#̬.gD' #˕,K"fW63̘%bN'w-}7OάWo۶m۶m۶m۶i}7wE= $&; eIXƔLgȤA$Y9˄>QYQ] +N'K]H B%,Ө옱4bNα_4jٱЈeYoL$щqLMe8#޲pڻXęjĶm۶m۶m۶m 1vNYONHgFڤQ1C Kd"HtD d%l?겉D0eY]6D<2/=wX""m褁FLv a""iFe ;3, I"Y&bCAfeӒYk.gi/14wډ$2CWs\.]64y'澎I 6C?rM&oVnw?믿>Sm۶m۶m۶mW_J|uj\!tY^ّeeu٘YYIcf&,n7lG^s^A;IbN,hĜ#2 fhB #;,1e3ƔLDYnL41f +mzޥ߾}k:Ym:Ym[9s޶m۶m۶m۶msry圏3?B6h7eyB?K;K@ܕ2dDd$Lf&a-YYh'N"&FAЈ,MC63CG;Ɍ a^v]w(#](sN&9IdNv,IdFhٲe6e_mO&Bo۶m۶m۶m۶mf d'ҝ4f h(4Dp8/t&Y̝tԝF,X6-It'3!*Ls0Ig2Df9uGNh34\QYf"8GЈHɌHwF:AŒ,KN:s㭉+֓՟g@^דb۶m۶m۶m۶m^?~,i6D& 4Ј$LDfhęE7a@Tg˾eM &ĜSv:CW2 ad ItfhD;d3423t3Aڌ$LKdք;UOVu{}z۶m۶m۶m۶/iO{!D@S'Wpz[ܶm۶m۶m۶md:׿NY:#2<;wQɤ1$a"4*;wQs qgO6ϓY]#lL@ *̘e;ϺΜ4 34&iRTg4*;4&$LFe'.!j.ә4\yvT9Y-ķ~?dxsCl۶m۶m۶m۶m9?×_~)pȹ Nѥ@<+'\m^C% 3uA|$W#rd,1;3'L'Wpd,SwDf1{O"q̲D 4.\bN"e&32t웡E79:n6ГHyW/dXtS I,K4wfӥ@ $2cv]Y';L&%vH@.С{1ۑ3Ղw?Ml۶m۶m۶m۶muT?l(>2C#κ`6i<\K@T&LbZvl,.f3Ye,I4MFeǬ.b:Lّ̻\@p7"I SvЈ7ehO,R I".&$挜6q'/}Vdz۶m۶m۶m۶m_/_N +9;$,BT)-~BhDY.FefY]$YYf(;Dhsa5II42&ٵlfYYv"L,34rgB#r175.32thDf&2FA B0cL5ގ~?믿ο oٮmm.۶m۶m۶m8s^>vS]UUUUUUUojkllPfM3c=NO6Q3֏v$̝en2c" &u8wR GÑ㒄9G3W>,3t36I$Lֈ krqzY ~F$a/ss1-„T;~̏Ull?lUUUUUUUUed#dYYiK$p4G9u7:X{{341G陱6$LbGI$2C##bO"M&u0IGsFN8s3fwɣqz:H_DʽD昣XG&p4?v^Q}v~[5eUUUUUUUU]=>>~UX|R # ˥L1rI"鄉tfh\ +KDMCG G&H1LHEЈi 4&ҙL#1r4Ic=x<]\<c&D& Ј$X3C>rL6cZSM ~1ǏM_XUUUUUUUUcYW>|;rPBX4D  9 skwM":CGD X$a0cZG7]i:4b8FĴ]fi3' A"sqb@7.c:sh"$̑cfnz5L i`"aLvWo;TUUUUUUUUr>O4'>VB Ku|<Df3"4A `CpD0AzqiDX'5C69 @ CD # 33IdޙD&bhƮCGEs5$2#yfhG +}cĚAϟ?:VUUUUUUUU˶ ʦY"͢i%L7$a2Ǭ*7L<'03͘`bY\&3^~& D\D ؅kX; vR xWs/;.@fb\t& X?fmWnV `9OŻw|*~MUUUUUUUUJ3A#L˄`MdMDF HgL̑@0`8ެf&`iAsId8"WoVnXL M9fr\&LhDh&h&"4b8F:sN&Hg"Lܘq0?^Z|͟B}͟K說qr+#21]dG*73# ~A "m^! :vtU"ar:4 Df1v#b@ DFı׉@ F"s8"V9IdLs| i*!Vn"CgS=yf6^l]_aQX.VOCfh&5Y#G3{jn:M jbg.`"r kz8С9 +&Fn1G\ }&aQ#Gs?8cn&v31rcD V9 +pBuXSb3Ǐ?rzrr1j',p\ea5h_{ MD\Lcwc&"q;$9rdbAfZo7$vG pDǵ2u""mӉu9 +̣~24 qU2cwe#uLYiM_aM}sscST6ꪪZW.ue=z&*DDX' s+:ҙμ*$8:>HL3qȍ@D:suaw8CDZ;gHg"H;.N.MLu0PH#?̕?"CT37oTqdS}UUUUUUUUUcVF_~} '+)v;Ͼ5&Ϝo߬l.ͱ5b8"8k3Gq֏y˕;L#k\HgƮI@ #ሸC1*7O Un̑c& s57"bgw5b8"8k3Gq֏y˕w̕K8?+Lkjs|1~TtS]UUUUUUUUNAy {v˼C$f8-$2w\"HC*fhc=jޑ@9 +O@ Gs!s丛ለ:#=5B#Vs$1 9 =vǕHqZ HdD0C5b{+34 jz8Q55ijv }˚&\. 씬46QTTY^nqpDM&`3&b>~5?-iCeZ^^k6`gGprL8Z&L"ҙc4yaa6I\\D\2C#"@ +7:^Gd sh& 3#b8F\ s&k_7cm2xa;3 `G[W8QNSEq>/MUUUUUUUUU,읾}* .Ďr?.2cpD쎑DܬrOЫܘLxdmᒄAgkO:Lwѫ\͍@Ȍ bgԈP& q$LvkHgGr쎑VANvڍM jlގz~ݝu~7 êEM>D} Xƌ 'Gh"K&b&"ҙ#7$a"$<va,} G2qoO`g.'Fn$2wv9fƮC3A:sf&&btKWͣ? 4"/M$L60Jh{ +? +7~沭EfUUUUUUUUտe ,I* k4=34 4D& q{s8r t蘞 @DڼCz㹣Vghhɷ9 Lc#U&bl& 3 >H $L@ VnHϼCW1CFFD:34 4D&bmq{s87th&V Ȃz]_I}-}+;j8\6x?';()3ll,pc6՞qObsGuqrc"VnHD9$5hp բEMuwww~-~ċpc!*7ljmRjQ[_[6Z_k|m~ڑ*--3\|>r8"f2&bc$K9r\瘣@p*7@4b&=3F+ yAW9/o[ooo o DZ ‚ڼ\.4"֮3Mey5,̈́͛.f"Br:Ƒ:Icwdn*7#k_0Iq8m[zGF{쫽.ekOD~n13G|nW&$vw2 mGL&q80eb&"hn::(4մ >i1Cp?]./%k+[P.Z6oVv+3C#&ʣWZuU9f218&a7ϳBѲka1ܟ6Y_HNҖ _bv{{kݦUPE > UfH{sx̑չrDhI;s)1.gu=/{ ]UUUUUUUUge5e6iۿb='Gyu$2wM1-Ēu5u%]^5pm!~afy6F)Ƨ7`#bs$aG&< <] t4C{nj9F4//(YMs~4>򲬦MUUUUUUUUUqvY\w}~DYZhAY/'Hg܄&r"'ʣ$2z\6sL#O'S-մGx2c߀,߶fl%vIl Iv65}+7y̰&O)jO2hvxiFkrz `VUUUUUUUU Xvـ L,QS\E0Ic&q<"Vnb&bGzu0`"bj&^Xyq6M#O,-HmJLO*;ٸw}b;\w8zT$̘Nd\>?#n|"Ĭ3,BW03kx?Apq^/=TUUUUUUUUJ6;~:a"HqDDzL\"HLr+71=e'6OkXӶ&j1|ѼCއGsGUnL"O`57>3N>ZayrOOtc}&.5Mfh&.}|>=.UUUUUUUUUU#|ь*–=b57?dD>DD:y+cw$73chʳ𭁗/_:>>>z9_ЬF/ucq̕kGWoxޤMv=ũ%ij*Ȗu829z80CGz&D0#"\7krag61y~%ȩt\~wӗ/_."hEĮUyȌi db LIs8&**"oWqھ`VUUUUUUUUURFH5VxlYCq8is冄i̱w&a*eG34~xr~U#߿~=źƖux97"vr` Uy(sh"L\9 +N.3#t&bU>ߟ6^zU-g28M\A\51DŽiDfM&88}Txz=ɞׯ_}^|U]~/_#/_tojnHd09cgLD?pC;.=cf W#_~QUUUUUUUUU˲)~[Q~ZV@.s8֣Frs"s䘉pIDp5HdzJu^ ]UUUUUUUUU ߛW,W-Z3wrk:ܘ$L$2Q3c:a1sL&րׯ_EX"O޾}k{T+kWgf:=ck1fgFڌU.3GL0-SpfE؎ƴ۬"kri# ǣh*7L\G3Y]$LDh<˗/߼y߿o_}zPG֘ޅh_{ޯEЈЈ^ǫs8""m g}_lbXbzLvH$f":ҙ1̱;>'9LW{וBTUUUUUUUUUjMI|"6NUWv;s)$2G=Ks'&:ҙWMfh&14/i)yi~~/Zz(4x:>:7i3thp :&;s8"VY'x>3yиǪ=P{QQ7۷V/vf6L00#m9N 7fb8^̑13>("ҙϪ7_yiemEҜ~1߿VlSUC۵"L 7$2Јn̑c&"thLcMfM :tL3&DpSUUUUUUUUUUǧ/Za:֣F B#Bs.2Iduh1|LhL<+߶~Qϟ?/O7UUUUUUUUUUcXj>~h=j&6cwId zIx u&zh0cZ0AzҰOY[V9/(tUUUUUUUUUߕ-r9ϧ͇lMNmPrA֭3$i q8snLمI"h&3Id"+7xV[A<ׯM7ڬ.UUUUUUUUU^vnY><<ݽ{.[SjT*®u8"笏=:4"4=1q994m+/_z߾} ߿h^ y B#|紹l UUUUUUUUU٪Y_dmAjMݩ%a̕fhva v˵qG$2cqe:8r?}_WL|>_녨?=4&By ̪BE~z{{oJXZ3YceHnL`U3|q"=O,l!_4OㄵshO{~7 ^η>}OUUUUUUUUU/l+GF߽{gYjk-QaȍIb&h:4biD&IdܰLv13fZGl^RSQm%Ê5\@\g s>Ç‹v5^`UUUUUUUUUrkg[8YYnEXgM3C#+7Ј$fGc2գc =rFXY;www6و_,.˩OHq~ps>OoW3Cp6-߼y# Ц0e{]pVyYPXٚEֶ4X1C#Ǚ;4DLБΌYitĒ¾4KQ{T;uqWz6:/ <3&|x{OO;xonn}ͲGEدqLMcfhj9v#dC%aZf^Ծt^O>YڎZzܜŵ<:fW%0/}ɧe {<9vȞO9>ԾpuA=lr=>??۷o|fUUUUUUUUUXawXYWFVsUD3#m9N߬pj+7L69fsn1HgE5y-EͰMYkŲֳe{)/l1LHseqZ~jڊ0=-O7"7l|b#mD%ZFߒ'Z6|z٦"Xwr9s8^{(Ǚ6$f3C#B$IŒt&3Cf@0 ^ۯbw2Y\Kf=퇮o9o m\A7BЗYC?Դմ˷n<98zl=cSg0{&&9G3`];@UUUUUUUUU+6J۷o®N/+k9;:4+7Ј$fu"HdFDs>D& 4"Ҧ'9txEUck~O57ӶF\6d &va"isWx~:zrhyG_Ԣǰ6=N{gӅoI+̛c=j>? >+g]-ݦ9,,DXa:\^ 4 *HA"M&"$a"B0#9cxgڎf1RY,ij}vYbk rF6ڌ˶mf0q9yoDħi3Ƨwin,庚;1S'1 p8r.ЈЈWyq2X+L=h3&lkbRk'L U__G,m-]1ˍFRFp^\柜_6{d|JS5/Ug2Op?hD J-/Wq_UUUUUUUUUݪFqdYgݡMD\R#B#z2@#u3=+xQ̰EdvXŖU^b>JM3Zpo1ӧi_ʥ/=yBx|1ϵߛOe_-bQUUUUUUUUUs.˗/YYaMgbeg}^}`m({DczfhDL'։@0sܛ r {cG{"2H|4w\bα;zw\"B#B9L|:kyWUUUUUUUUտ5|>?l-->0/pq: .2 "417ߕ$a&@ 1ȌIŒI"̑Lϙg_=?_QUUUUUUUUU ?~\~1cVh;pDf"r4C.13498ʽ:7Ñ5"1]^2DjшDrW?`Lg1\ҼCÑ ezO`WľTUUUUUUUUUaQ+kϟ?[͉C'f{ԑpjJY1r4Lr5ЈЈЈHHg217\{ޛna UUUUUUUUUek~uua$f0#N&q.L2u  aFz:4bhG1Gz&kQfiis\=fAg"@#BǺuo)r_|ŋ }垉@z8FH3U׉=r4I1-H# k LCv79sfh01LWy(z810#9c;ϧk߶ m^X961^D0ADFhD ]dq̽q r3h"B^&3v.LLG Hw2X2s8"b 7I?ϧdeǏ~j݋/lNϴ Gse^jđ{bx iD&LO0-Hd"$̱;27u :4n؅Ib&]L\GΝ\f]n7\.oq˝N+O>{իW.NGcH6`+7LzLg>FǵG.׹1ro"Vn$L Xzw0 ̑cf\퉝7~8/_|>UUUUUUUUU՟߾}7o_<.kE{3"7$̣^e869Un̫~P^ͽ@>s1ibf8"V:k#B#Bgooo竫\\.Z[YZuehk aM&"ҙc= 2$2;.d3Mh "8r]$L\7ӉLML Dx3f~ZϾTUUUUUUUUU.ˏ?㣉۷o__uָC H %HGI+7#9cZ Ǒ.c s8F GDM3+7СG3;sX'Lfh&H$D\N~nF =rWWUUUUUUUUv.[뇇/_ee==Ӗo8"W82#Gs>&&i3G3I1d p\4E& \A*eG5Hc& 1jߦ1}צꪪ[ttM~7޽3gem_-߰ rc\azg.fh9IId?x#mQ͑$Hd:th\ƌt&A$asLd"B&9rDx{? [\C+V? ._~m'~7LlpD$LDhDhD#6:X k3GСId0#I"p 9fȍi\&2zLg1\ҼC-6WWUUUUUUUUEtVwww6_lZXY 7X7yT p-;&a2HdfLcN.Mʍis5ro" mNvΕ13v r@DDxmUUUUUUUUUak}ǏM~753 qy{sehD$2IdX' 3&k01Ĵ]0nXcǵ XM"34 34"4L̑c&1cZD$aF:sg.w&}}uƾ70ƧO޿/ig'L#Gs8>ǣM*73#桉L&V.v t&\N# A"340;d1-"m1y}fUUUUUUUUUׅ>Z j3 ԡW>,ss80p =r4c3chЈ#L6c3Iܘk̝ v昉=r4I1-H =C.ՎW?>>󫪪֎&{_~1 E՟5 Drc"M&L&!AŒIb& pD Hdd#7I$a0$Lqf䘉a3b]:oڛ⬬?~΄>y׳i1`"C#B^nc:aXjn&V.C#B$acpDD0hQMvq&b 3wquF:s% D\Nxsw};$6%~:,փLz+7;K7#$$f s8;y$15#z冉pD9 +DL՞db'Cq8i}/ѷfĻl yAk7?6߾}kZYhYs\q~8 aڱޤg1rN&桉qw7$̣^jz^#ҙ$̝\'"&GAŒﳘo޼?ϗ[eUUUUUUUUU杶t:ߟLhm,CcUHD;Ckw0$8vzUPD0&a0qmIܘЈq&xsU>,331-wUȾ:sᄎʬ~ქׯZ 1;C:pLvNDhD>r#:4=$f޹zz#=s57?cHGI+7#HM7j޴? xWUUUUUUUUU_q~ & @MmѦ! Mb&"4&Hsg.&6C8DG $2c=Ыf:a2sL:ҙs>&QgFjs1?{l|Ťꪪ:|>o߮0VоBܘlM#G3t깛1G:sh&X'1v&aiM& :t31v`I$a&I"33nxW]]UUUUUUUUUu.i[=>>U~AȒж$ah"B#Fɣf&Fnбvs$sg.3CGWFfUBF\HX{*fl&yz[|ꪪr]y ߾}7odk=l 6T 3k H#u"8ZCDF BGDD:qa.'<4^FĴ@ͽ}<4ޯncbcӯ_msUUUUUUUUUúﴱJիW/_|ᬬ3# ƙC#HdF\!aFzLΌt&ML'IŒzg.9CDZ3Id"VF&I":I671M:=ey{{:?\u=>>[gk-6?D [d&b'$LĎK"40f<8"14p9(B1tL'f 13pC;.݇LorxjoHj}˗O>ekmU&[kBN4]D`kii#Јӂ5&*rCb&X{DDz&+7G3Idf<#o}5߾}˲ZWUUUUUUUUߒ5ZZ[ Z;bkmz|Ev+2D&" b57k0i kz6Ckc&$rCb$LLyh$aG&{v +G|$7>|`'鲺oϾ:lickmmŵ#[a'Un@̑c&  cw8vGfi341W>$aXgLOcL'L9rq!)lm|( fgڱެnGI1흠g7DLۦZxg"a+xٜ6www?~|V5V֏62tXZtDıgH6Y#Ѽj:jE蕛8(BG:sQܬ Bz &1yfSwZ6߷_euUUUUUUUUU,M g~m֖ţ $Fh"4WssDf3} 8ÑMWy4t>F8"1/iaMM ?SmSmy~rއUUUUUUUUU+'D\.C[OO[3~Imi-9+ý\=w9rɥi@9&L3Chz\&2s\Ne 1f~w=|Ǐ沭v +|>Z?<W7星rҢ*1#Y#s<]1D&+7FĴ<:3ƑL#7x3{TϦ :ϗś-=='v]( isWn"m210zGX*IdqDF&F0^#y쨱~~8ZS[S|6ñ1t7e5xt6-`ңlVͫPs=Y-UUUUUUUUUU9jbJK,&NkODXb5hl\uc&#7C3f &A$a^!` 3G#Ǚ45"iG/Gfv8z"Mദ6QUUUUUUUUU糟\.ŵk۷o߼ycիWf֡VlM3MY/5ʚ$2Xv&34"4$ aD1A0I %^5: +鬩}Bp\N tUUUUUUUUU՟oKNO,3<߿owmq5vXhwMv v$^':v"1I<ʽ.3&@33ʚ:/JԷfdMmG}>^.8fi5זY\g>|0Eݵŵ&;U,Z3L3V$UnQ 4hDWn؅ aZGfxVܚjOxy·4?6zymyf)\.'Y~y!o/5uV*`3+7$q7бvD .Јz OTNGÚfp/iMk|zQfUUUUUUUUU)Kr6v_ӧO;CSY&V{װ]΍tz*dC&? 4' ah'7''}~ +YYhݴ9ff<AbXLK >m_6}i@_&d_{<>O']Y˶δV`k,MKlK]];^}/` a+mI"9f2_@Xś|9i_ׯ_}ǧTp~YUUUUUUUUUU"Vec+ i!Z%. +lKla6*;tdulbx ȿԿ ǍБ}%k$OTUUUUUUUUUU d#=ZKl]{l}9m,Ƒ83mu9"/W}S5}^zWmUUUUUUUUUUv8e,{M{l`N;a1YhO0do=r韐2db>|r9m& ^=6&䴱[jEhOOi^deLWUUUUUUUUUߒ%0v/bٌOOБ6X]_пqcG9gCa +endstream +endobj + +10 0 obj + 231359 +endobj + +11 0 obj + << /ExtGState << /E1 << /SMask << /Type /Mask + /G 1 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + /XObject << /X2 5 0 R + /X1 9 0 R + >> + >> +endobj + +12 0 obj + << /Length 13 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +968.000000 0.000000 -0.000000 251.184509 0.000000 0.000000 cm +/X1 Do +Q +q +/E1 gs +/X2 Do +Q + +endstream +endobj + +13 0 obj + 119 +endobj + +14 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 968.000000 252.000000 ] + /Resources 11 0 R + /Contents 12 0 R + /Parent 15 0 R + >> +endobj + +15 0 obj + << /Kids [ 14 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +16 0 obj + << /Pages 15 0 R + /Type /Catalog + >> +endobj + +xref +0 17 +0000000000 65535 f +0000000010 00000 n +0000000497 00000 n +0000000519 00000 n +0000001042 00000 n +0000001064 00000 n +0000012410 00000 n +0000012434 00000 n +0000150121 00000 n +0000150146 00000 n +0000381740 00000 n +0000381766 00000 n +0000382106 00000 n +0000382283 00000 n +0000382306 00000 n +0000382485 00000 n +0000382561 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 16 0 R + /Size 17 +>> +startxref +382622 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/logotypeFull1.large.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/logotypeFull1.large.pdf deleted file mode 100644 index d4d478ef612a8388b6768be036f70ba882ebb057..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 250426 zcmX_nWmufS&NlAuh2l`4IK|zySn!y8=zkWt@yX{&!IeXa=&RVCUuI{olY>b2|&j2nS?> zZ~q&TH3wQ)f+#q7IQ};v4g@)?m^(?>+dA0WncIOV{_kNm7ZZ?&17sdKTVo6J|J|+r zpAl_k6DxC55M)eP7{wXnWNvJW;tqY99b!5wOW1ASbZ$4R5I{hW=O*uH?dax~wtfB$ z%#2U(;+D47CUWLQE%#|yvT<$Qt^19egC$dLz5U0Kq2KG1HWNX+m^S?Cs!;%bv?iky zqVkC0;X!W*Dj|zr^Cnav4%F7s{(kJ`+uY;7yU?vf*kQR#?PdU^!O z!Uqr&;eEs*CLkcg{(vuJr)OiM=Phnx;&lC(d{thK`;AU1iVeeF-02hBDcm~}7F`hYtzBX`+Z?ufL!vg2{}@uNuCCU(-(jMmN%y+w>A{50M;s-U zD(3Qeoxeya)Th?yb+}55moTrmoi6`+=(lq=-FFNVN7E+;PpEn{gFf$ODf=fxcS!Mo;Ec zVWw0`3AQJbh==9VXF;?gmS|qDKHe_`Ei4fy|53NEaV^5h))D+MV`>dMg~6cm*NJpt zQ;ebc(-dp&U7Phz=Zm*K8Ko>?udVAj5+3`Fj)%>uoze-zcF))SwYC>bauMIR>*Bn; z+Oo1k-K(bU#Odk!Z`ulxQC&W;O}&AEHHMwuZ;xAj&rS9QU7mlp|IQ3zlCO3-pX@hO z5KiRsdAtne`*^w9tbiTYe(vc+5sqdG37K9OAu~i=?G#Y=io?LZh)#q|nPWBof0-#o z9VkiX|Ex@i(9)>|F6Op?3%F~O)%YcmY~V~694j}b;uO=)RinI|JV zy_B`lccSt(D)b4pM>F3?^}(S2)~qUlds2Si{oa(5rVA0zr|Y|CObUR)NN1Xs`rIs4 z{_^zn{QA_=+}zB{%F4oq2pby`8ag^UwGB}lDFImQyS6R&WgvwFmXncDURPHQCY76; ziwn4^sezk^i*t5PT`TaA`Bn3FPwlNuYl2*)Imzwpx+Q54<9w+O*mr!|e`BGXo53y2 z!19Shkau!p0hLp)EU01)V%#QX+Cs151t#0uJ3HGuY_&n~e48o$=(Q1Xq=eX~CtIz% z&1}fgctOM`JA`;RWTctAzSGI0e5m!B)f-@+i~Jo6R)aQ|_o-i67s9>_Of0X^y$#i! z$O`l4IXRu3zEt1Dym8(LjH65Zhv4FS;%SC9-qzQ1s+F+X713Q9H1I__)9~>9>z}WW z`r3GEvP3*~yU(lmtm_FUGWnQ=xEO@_sW|DOzB^upbEuE}^J^b_wz08<*qzr4`qOEn z2uyL8m;%sYR_IX=%%U+LoWs&t^bW;uwe7q7{>4qjuJWU#a7)`yCMv|XVF`M*{AB@S4P(!`kHYCJkA5Sk} zLz)UZ8a{y5e&%jOp?04H1=u$XhUaaJ_x$+^cM$7Hsnovmuy?BduHMAC0{YTJa z(jT2RCq1}yN-)#XyT6CkE|Yj2Dcq(WRnoqlh!akl8P^gGL>7w1WefV;7o0Y<2;7qk zIsYA4lVM%=WqQ=qG|*X@oQ>SI7D=qpc832;7g(H+T~CA*q`gRDkLN$z5HJd#zcMqo zKEJA>s}GIyaPzz{LAKuM`nH=dq{~w|o+0wZ)Wk_oU7L+e$racH@#Ab!bF9Wa=hhSJ z9X$RBLw?VPy{V&?_6t%2AqcW5cenSbXfcrrIlM0WFXq-xZXbDl&yUY!CN{j8=>Hn% zz<#!o(Q;!&($?%Wfez=1rTASkROuN3dQe2uoCg(pDX8|3cbA6MEoSG)!}M7?l!0Xte*Uay7K`T3n|h+Nd-?lrAqwzhyo)w)89I5E(TzoP1@qk|2)$C&@HOn$lALd~brdRm z-k(;lCxl?h{^BBw@d-ea%SIjQ!i5mDP~)kEqK~temNNMV8U%)cyjl}9seCMdLY+`7 z6-)!FMRX|y`~_}K&dHUTDHN3Rhi%pi1HsWY(Di0)3?t!KMn(n^F4^uuOb%S2U-v&l zf7A||ak5KsUEVC{4XoirnG89&*xH{sNTYjh0RafnB-7B5Vj{^!%=fqP`2;YsTRbn0 zRwnvIejba)ogI-wePArBYo^S1ksP8zVK6Zho|SHlrKWko20sCJ&Nw$Rwt9o9srnbzN9c@y(PkW!TlHZ^AY5h+ozS9bMm5a0C&)GUeGjECk5N$ zP!T%h@0&x-v*o`CYu4KzeuwDB8Tc?gscLMjPOb5<@d~0MU&*>44Q_s^&3Yc zWlNs9ruUwm0PTTg(2mddOKTOSnCFY}LiSI;f0s={*(+nFS^bMv(S)LU6xS0ss4Oc^lbBxks)hZtqUZ!6UT!|#HM>S zPzmsFVIf7{N-op6q6n8Is#il2)^jR9ah^;0c`%4<{|YrSIyb$=a;{P0;F_%KFF1`+KO=Dk}G`1R^+=f*27_G~?# z%fiX&Ygt!Xo5CXGumwWXFdu?RU!vls@lPdK32R~t6fbz z&5s)h8Zq@o^02+bihNl7iI+MQsR@@vAnt%ri)nfQ^R_zZodrb`<3Dw^MM+~@F@mx# z-0Jw<&yE{uVCXVl`NG2&F+qDPuF+QQ`A<_GR#E*n1hJuSsGPH|~g`gNmbi z?RE=S6_^`%-ZgeK)B}^^16VHCfv8p)D7;GJVnb?hM~xlX%q}8bdmm|UAz(Rtchs`~Zpls1-uM^3PLMl9vJ z)eGN?t^oLco9-uu{Tim9cpryQ{_Ab)>d0BsM#%HSZyOIEYvp$=0h&PUl_(s88=MUs z+EdzD7XRtKfRx>cGK7}u$f$#g3XxA@#^_~;WX_K)D?j_bAC62h|FKYvM8CTMbyQ7-M2V2` zdyXc6Q4a6o*1b+!U0(vUna+iK*m1v`+w*ze4G-35%(KX4_SZ`zFQ^7{(xWsS4sMF> z$oa#{!?}LyhVAFj7O*bKD>6qirokkVKDV`h)VVyqU+S;#@>(zP-AY^%Ss4ZeP zrm1o`>Q;#YeQ=ap79otv)KVD@UaD}-?tJ}OTTotTAMS;SHxbj@g-s-|9U~{JU7|W- zEZB_{#WF-*MHSyVssyFQm&FE$gxoiz&&*Z)H;#pLwb@eMyR0Vc#J~qKi{)w{UA~)S z&z=~xYY)i>_uEBU;5#o90gv+g(A6)^3%>VFAE2W3L;8lLP2^OXSWe(|s@I3>yr;!b zPXXeQF2OceUJF%Eo7%WGh51&8E5sdwWQ4@j7U~hGg^oeLPXw47lfl3xfeE3P$HLA+ zE!T>=@SJpx5L4vCFwHo$if-h?;NCYY&MbT~w#d~vb)CzX<9u{yI!VOl(0Kjk#~{iF zGG`C~;T|SXCQjc?^3}Mz+tKLjQ^6SB#s0hHVepd9*}b^;;-@d4(Qta#-FGOK9cOu5 zhbi%AgWY#%P8)m9=SKo9(jKZuzM|YXx~+ZK>+R1)8J!7Ks$d1`)E2pPv~0)qn%rHY zE95lXPsZ?=qi4es_YG(_M+Z&~(9x@@E_11tcVi`q)Ko$TYXz9uD)g6zSv0aJMIqmU zg!E5(BebCwTOMhl-j2W_2J3!QhoSu&!{i7W;7(yq-b>_ptS7AenL9Hxm zr`EI2)a&vQ+S6l3@Gw@46$jqTcIUy(BW!+L(yGe3aX!Sk7O*DN(JrXxjMIgpC&$?3 zKV;*cGtcGuf@oa#6#Qun;J54Q>!DEds2+z>@LqQj5&sn?@;QdWe>DI$xF;N13RWti za@LmMwsJQaYAggZ_xu6MLb7eiBzf)VK1Ya zf9XJ>_+cpbH?6KjZKe5CB;t8(+1Bia@aoF9x3~GNxtX1y4*i1p{=@DXAZmnChM+9~ zpch|SA@oYDEpe$P%%LH5@g%c8a5xQo$1>XGrQkN*>(TuyqWg@Wn+5-)z=mGF4QI+w zUKT{m$BW#K?WR}sH9PR{(~2!@wu_YVxJ^zj&iT$cznfk(QAK8{vM7D{65k)^Mq7gf z?46U0_(v|NYy!Vrf@(wuDgTr_$6?XJLD^Clh>qZRE;fsnNR)*?SDt;0zZ)2(|Uo13-EySatFY_Cp|W%dMRaSz+z?Y<--thFlyX zP4wf7vVr4jIPMZmK>+Gqv4D)KBrccRV&66raZkdps;|iscEl~2Fl_nTz0__&9vNkzulGI4xqC7i#7v}d?b=K?Jy4NRyqLp9k^+r>d!0rp{w_mCp$-w{hMupC!CDZW zT~oPzoE`XqES6JUQHUB=`;9%b#o_M0kI+u!UFF--;=BsT+0?VPV&&f2e_bzuE}9JW zQ1AUx&^GcUwoC*nD$H+mHYM!BxBQ4Wc{$7sN2N_AKCGRVk`Trfk#V^&#WHb`CTMk9 z!6+J``;t{jIdmG@yrh*@3^n+F zA_k&!!2YUDV9sQ~rt9}S;kMw)tc3pHD3k?3+5}j?4yAW~gLM=Z&iH-zdQNIYtTF_| zM#Z)2D^6X{ca0VvhiU@2czsL_fQTtzyzGA=$>j*#B}7o6rP6!4>O9fK^MFh}$gjdD z;c8}0Fk1<7U}A^ikxP!R&472~924zJAYHnyMR(QoYwhsU0Tk6J%(k-iZtn;Dwwd1L z`@?<8=W*Aw5=ib+rT=i zxGia~%FSB%985rUga5JR&@&dfzMe1E4wTPbFB(Wqco{sr{8aORStIKf0U&(JBZ-0S zHThODIyS!Mwm-1b6tdUAy!I+#axt;4m5iUjKAB;zo%m{i;6ET1;?VLSK7YZA0Cb{sCHH(TI*Z- z=Nw%1Puz^^b^_m*+Drd#_|7-DVyf7tih}@YMXy}7CfS-h6&_h9R9b)(O-7wPUwr#QtQ|D% zq<#ZlUO)IU&c^!A<)n2m9Ch<|Tf^fJ__>CCQ$lc;w@^WZbM$-oy3VE=& zTEVST*$mQ#e!7MVM@c&srP9wWU;v@hB&mvluNcL53nqhKJ3}D?timbk+^)A6i?!TQ znkbxt@nAld?0YTf6EN-PY=T&7CcOJ6nF}FTmm*)oGU?2R;%XUYOLGeW%pfnf_XLk$ z-&rQFd5ybM0)^li8&%+Vj4uQi^?1|Ex~ghgPLGDU)C%+=i~S%w8XAA#;rg-5`+Y8r z<5n>4SRkcH6|zQ%>-Zo#UBHUANtOg4 z04Iw{0Jk=bO-gBgjm5ieNr@oiFGY)Ahi_el*F%3AGl@xuH$K1d?f!K4xZJXOp4M7l zg~@Im8ao2`E46aN<#Z&72VTS~Q{Wk)Je^jTARgT0Jgrwa*o8E|37Q0k8~|f_66akm zuI$k)!NU}mBGh6P0ibzmT!0juaW5~2^e}mR%SpE+I|7{*Hv)zrOp*IguqHO8^*{nU z%G>>0V|^)|rg8*GNb5xW_wk1;Hrt~CA-WnNICkBISqiy--oV^JB2m-Z z86GN36H`$6q?P*kp?~{Ba(z4!z(sA6{|Nc`(wj#nfO9b#|NCW#R4^B#EecqWQWYKS zgVlcPZs(64vAk0eUl#Jd8iLx*Okp=4TF8^r&dO9{eR59N1&?kUZtEWS0g~FqmKh?x z2v))JGy6J+C``XKYOliL?q4(qz%gfSImE)6p@t(wPP0ffQy76HsLb+_1Czw`DARG6 z-9qA+T#hBy2I}7$-{$M-1~rRh+}k9pPy;OvcN;8n@HA&l*%iJ^KxwSx|8G(LQ>NJm zz>pwjG1{sM>N_tJ$A`GgjrsAtfI7omWRr3R20n1pnlaNdQRslG0w~U z9vT0gyReq!X&IS%y3nWQPV@^H;f-uhDxqt7RXiO zXL;-XZ$Z_~&;4fm2A+m%G5eNBN7ccO-=nD#C&j*dmx~JEZ^nvwffH30X(aZke$jVd zWk*N{Mt?EIW7qZb@>9493Q|y$(A%hR38-}+WZ``(DN%N04!x)D0mK)LDLTt78^SS? z38m&-V`I=l!iFAgEwY3QK#S+a?f3RRE|<&e5>HCD?4-%%mX;_0cBjy3Q?wWA8{6=2 zspy^_il4+WNZ-g3;uikEABE_uAO4E6(-^V^ zkk({~6165!^BqYMO2l})hP8g)$kG>}onGOBJA^p_Tgjn{en|!prFUagK=l93D7#Ve zZrv1m?k-Likf!wfZ0vXwmEEzkzaZ_YfWV(Eg8?SdhUzm5f3qP%>xRCP4xp<4Ih7IS zA(CuHjI08u4Yg)=8brFMtjv^4be!?i1)9Gqw5WI!A%utMGf+PpYzUiPx`xEh+|tZp zssA3Tw^914Sob9PeOGU!=8XMn%Z==~QC#%gQp10RP@1D$%$JjcgC8L>TvK>im|B8S zXg5|9zKH)mV17F6nUD^?RhWAdMh*bXAqWB>+X@cR!^#2D{{ccn*my9TCzJ3VHL21t z4eZ2BYv&bEWH?OSn- zn!Z|tpP=k?j%cFhdf*;6LOj^=b`etUur@+3xCl?oea9&f;M=y$ag~a+zI}9qRi0k| zY;!~y*Ywfp79T0Lp7*1SRgnNYoV3I5A@9d@B$#w0b1EcQ9`NQ+73_{|42UkYdoir- zvq6p?JNs`gjpwkS1Kh#c^Guysd!5eRT7{4tqgDdK7d zgeNthF1vf-7SzwkF;c;x9L*`q?w@up>idtd{br|Z$f>pUe-4gk`OqWdM0mo6yj7Grh> zs#d@LQ-@9MJlEtYR*`= zB)-R872ytYlauw1xpT9oKBy#_I9sVqT6-dQHC3~K0@{Yv#3Mr@dWw+wk zZY~L2#+;XeI7rGTrv)QS9}ROF%mc#_MSRIUB8@IdHB>0iJxmk0{oh+0za<3p($}sa z0y@{0F?I&4EB49J62NzmLLo6Ss=LG&SUInif7oF4vSaX=N2@y^IK>7oz8{iVT!dvB zuC@zd%CAiERU>CD<8Yu_5t#KgE7V}P;)Nrk1{MK{H5@XcKk>qlwz5m6rjG{Vc9RUO z)jh2|S=~(3D5Bnh?u_2`CIy+NfWR3_TX|eBt)uMK;cBlqKWx#`G3f01&Aja(%&4-N zR+_VmpgBeptMQbH7y#zup?@YEDowze(FB#!dv5_eN>7l88m)pXf&bHF_Mx0xv=|z@ zle(Pf_9FT{lk{B^rdx^#Uo&sdMseOti@%b>0npsC(eHLna6#~l6?SySZgeIAERXpT zOAi5=*$$ZUI<7BLv;wYZ2kcTFFVMqD0Vm<2WLc84=_oWkT~K_;>XtBcxc`3`ZxJyA z)!t?-+Nq;!d!m0k2(&x^ZHe*e@xY$sN2yCgx*_b(87JvD0~A zxB#uKxFNvf_gpQd_Ae+L7PQks4y2GdJWjdxK9UbsX{ttgg4}S{1S)ug>aVs!bN0$L zvgr5z)Z5!}!O>(p9sqo>+!pCNl=}bqEz6u#H2WX^t6rxE{Ow(+1aXt#2Eyx;{^!_` z4(L^*UMYWAjEP5T0RZ$h7bLD#a%RtYh!B;=L0a$uSD;1>IZ1PyVe5l?6a5J&^e-{CWdn zx(f&YSqTZ# zp=((M{Mh!wMvO`bb*WG&dx4Q9>4&f4{;SfHvYCXlP``M{)ztHUFnyp#3Dvw=Z1oiu z0`pF7jJepo=I03%46P9T2!$K6g|pPnn#E@mLv)CPQ$ ze{%7lq2^t!J~MW?hHMFTK8gN*#PVXeinRJN9}z-={HGt%!FHfyAh{(&bDamE`9v;P z1pijh)>lZTbPqO>Acm}s<_wYvBGFWFdNNTuCsfNFGUVd_TLod|y6TS=d%~uISTM%) z6=6IVCLb0N0rpgMVK_HD8%)%z)GoK^d3O`C7qoW}2NO|MrD9&}XPOacm_9113`e}F z5~yR5RkfR^-}SqqZoDikZ-Pd*=|4*Qt3ssCp|~LtXKKd~kqNk0IEd!()<+ zH3@`NV4J$P$&y>QZ`)dhc_$q}3{JpqcPw>`!d^WqUc7bssM1gbQ#4Pca`pgtI+z_X z6~1T`2CionudB1OAFAusNhcF<6EoKrLIYKB_C@=0wExKkq2&H}!wfK()&hKhkqxaT%Gk#?X3t7_-ifqk{(m4bk0)KhH!r@g7v@GCSkeScd;kr+}N6DQ!Ksrv}Fz{Wd zwi#YY^mEm5$RLV9c10R-D8olIjCdP1PL3T8Doc*of}#73igyQ@rB~7~1s%Qtv6b+U zx?!cAon39v912F)n>q9`lxPTB?cm&#)@NxNW)Bi-Tfw3&Aavrrh}#AGiv|=JfyeFf z9m9|g-8UxwI& z{k9%ptxzEw?n<`-?@7A<<*nMR*_qMuW?PcteP(>`&A%Hl*w|;q{!H zM^1nLE@%}E%Nnwx>Pdj3{#8DN3REI9FQ-BK`7A6{n77jE5|)F%%-dxp77xIaKAlu2 zfzsRkr>pn9_$_ZXb$x}8dgPB>*y57tPH;u6p{T638Awlj`VkqIuDG;RSxF(6likJJ z$wcp~x};@ZZVe$=&7xu251RL2bu;<~6%9EmGFDMmUao&mJ~u0e8z^oi|INhatGc^v zyh!x|5ejNUkvTCcm_q8g$?e?&1b9T;x;`bu!o$H9Vqsxt`0yT-MAw(g3+xR}E_z%{ zAG(Ye^=l4(6=#N>LjiahIcS@qaYmWVF4GmtD`vpC$cINoL?a=i_-l&YQoxFq(iRT# zvkj8aoE_Y-dA``Y6p2=93ki1kyueEI7Mg71Bqa?hXC;SXZ0~~QaPvDO9Cr$+AVK2Q zyM1-->+C}p+ARLKyWu-Ub8P%MV%YdS`|@n!e5Rg@ORbU-Q`r>d6xy3`if9r8ZO4Gv#@sg zLJ4oNP3oFky#7ME%h3w5in-j*=LN-ow5oNw*y-qac<3zpAdH|?3+4@8KB>v4(ocV@=Q+s0-F-KTj)ZRcaW@46u7B?8G!h z+u5NM06`vr67zhI<*fJj2jCXwt-lBxruLIT*K%Ldt6Z#?M=-5)T@!E09 zk^jKh?%{Jd;!s7Pbg ztv4Evj=r5OS{$mS5YaQXz^s2`6g6M#@P-MbHrXC5Rm@~}>q$JtuUO~}L7U^Wcx|0y z%q#f?u|CD<7&ynMW4>jpR`?Pokxau4Oc@PV1XR! zJ^f}V6Sws(s+FN#MFjwjhu{{GG+l|(cvh3y-qV=CeSXq2gg2(5 zUsmJbn#=ELG|`yVuoSibRY4&(3N7F~#96)551T0Y=JFmhHd;Y$BC82BH&E4v*_+S- z+Vfmz)|=7gsWI#T-OncY2yj>FG#c$EUpZp;bgmb+A&z?t=J60= zFE+v()Q%)PM_m?Eo`h2sXawIS$HRZXegU~mR&{mMyv@v=MNQ)th`+RFbGowwRNmL= z*u3>dl64g3ZI*p*N9R^Bb2~gAt`xSK5);3DmAh){t;t>Q^0<0jk5MSaI9qLY+KRs2 zbu@l^H#SBsG+%!+o{|IpK&|Jo5pdYF|8sQV>&hl#|l|>U=0)NK^i+Qq#u6 znj;MU;&!*aq&7GhiulBDxR`h`^O>fq-6uTnK90urd^i4m<*=@Q`}~N6hyd1a^X1e< z_km2vneU<}iO~L9#D_W1uNLpEyM8(`al4?Gm3*Vi_w8_fOu-`dbgiDndi_=|V#;{Z z(9!K$iOJ-013k!43)ALhrPp#yPJ2>0J**o|Eg)`BM?NrDJ1(&d-zOQ9S3up7&3$gEjC{ z8;9({LE#gDO{_VB%|3T85wI&fmD-%vPVzRA@f&_OL{?BfD6H>TygL>wfP`NslSh9% z9&$S+=>+wLR5YeSPIafWcsFuI+MVxKr-U*!nR9-m;-PoYKd91ZJv=dCl z(hYFMLi4*MkP@%JG0=vy^SA7`3`xWa`WCML1|&ndaw1}92zpeidRMXNZ#zNcVai4? zwL^;5kKRTSK*o}VHr^Sq7&WW|ZbbZXBkOw8R=@xCvpT=LxT>S$SH&;X)bZH+#?+F~ z*5k+vTEiPdq0u%68!e^#b_NCt8yB!M)>h7Xr*NmRFz8O2gxqj<7nOqA#u(aY2i+=2 z6A%k6-XOnI$L{Rw{;s3_xQt)S+^JX=9@eD`?2m-NhC z1K{-$*OR0;o|cfufT~|Z)j(i=4EtIOp$v8AHnHA(Gt^@Jkkj#|t^HJ3P|}&-Qryr1 z`zG?3FM{-@c zpW&HY{!?LfLZS;;$@|u3uSNCB(PnTw3xwxR*<(u%S*s6KO2JE1Ys|9cjaI;tmbK=tTN7X=cq2$4i z{m2}yKBqxvDTX`241ZwDF8aBV)`iSXB*{bPkDI2~| zPEOX2eJ@?K>67RSf_?4KKVwGc+M~_g5bpH2?@mp*DWQ=;{A)Q2o#E@S+x!Rzw?ws_ zfDNg8VUv@P?q5V>viUD>Z{gyl@Os$Ym%Lio33}g^H@COuw~%UBV;vj!-340bu6+;l zL`TQCy>oQ|bI-ppF}uH?j`tS|%rj*O8|y(RJ0x8u-g&K*s^C_b0(7);XC6aVOG;&! z%^2bPoXFC!R7Iu0A+)PYH5k8-;ZSrhjxY{aHyCJfnei=_V-V7iT`&3LnU{vXv#yMr z{YE#1Ez#w>7?;W)A~NFBi<6^JzuLxY2p>udvpScr8U8`5*JZBTkHnHN$2T7Wvrskd z1)R3Vf4u}wkF=IFj_0iV=6_S6?EW4!9|wS+Er$m5Ye52voesO3)wg6#YeYFzerC5@ z?NEo;psAf3QwHD`k+`9=9?G%Ug^0`9denx3jVk-*$A&4Yx-wn zcXNBRhU>yDK3hYvcM2APwdmrcpJW|vYPE%ocNqvW>KL|nOsE3eH6YYguyI||_y4x7 zuU1%#o2%CzhkElvzK{l4qsTOtR#de#*597A$?zy$tTlrMf*pH^2j{RV>{omHCK$cY z;{>6jP=6r>gmggV#QDPqk>J6CjTet89~OSeZR&;_V;hgjXJUw#D^kJ{+{2U{+mjsH zl+k;>(_D;p#wdJI6RU%l!oXO3hegeb-ns(*O0yU8zB>P-T%2-O1tB0#vH^AGM0H8Y zPA`sZs$X@kulmoN5qZofvWQSe_-ZihI=2{R4wwyE;a)ETwajpvpooU*9lkF&Lt!e3 zV+1I-#yjGEX9-GTxmXIb-)m4Lg}4L>hk7w|PX8#z*^0vA&idOwC(l~3c7|Mj@+M|b zXpCZs`wllT8S#9%>L^yQMzDabCbOgKF}hrD`-PmbgtYNDH^Z~R3a@zuUmRNLm(p()kgnUeRk9 z@0+5A7P_X29#?}0ZmeW$o!^b6<-yV`k0M5n4;bOHvbk+eD^ve%Y*ST{vUE0Ue(uI^ zx;L=I$EVHB*%0=la9gU3sLS_l|B||nZI1hf7tSof`wm~Gbn^QR8}*~aMqXLb@QtRO z5a{iG=B0pqF2+{B1D3+L#R8g{Yu3#&ucob|1*2&4By2(He7%;&nF^FXpj6l3i7-$y z20$7kdWB+xHdBD2t`iTZg#Z#ov^&kwa#5GpKHO3!y`~4y`V8jA=VM)duROIX7c-!g zmw@iEukRl}r=8vLIp=8;2_G36>oti_Rc%iVKcY4G!ZojwH~WHk3K0(?cnW(NirID1 zvOcWy2HG}vRWx+g@Af5G4>r}LClMglyVA(G&-!PfhkqT>2K!ztt+0yt)d0^=0|t;& z!lVe>NHc)SUTQ8!gBDHWr+_aaLc-AthL=bmx+osHBW7?)jqn?tl(L0gjt}qg)u?>b zhj|f%kG5wHYL;9+UmOi4{v5cs1C5K-#RS}7Ds}B=kQ*9qw7FZ(OpjjW^`@sZutjn@ z9;Hf@=6zBjuQ%p%J|Fni>-qM;NNKG-ENxa>^>ViM$AW@ERzVL6ELG7x+_#ciEW>|5 zo12H?QTT3u9(g9iD&l@~=Y=k-^9bv)V6l38B@LGx+VghaIBcXv)|PXs9zEb z1KYFblafsDVsctd4%oftH{rQa%+B5)JJFqS_G*@38x~mw8BFEQ^DE+Rq8Le5yi}Tq zKbyem4YaO^U3hrC_rBeseu_JlKP46NnkdX&YkzsXiN&i-%g;tK<;28AuE=(ulf0#o zVBDt!;ySco7l>UaUHR3BWVIe5Y596NKl}sNGdtqm1C@WukiVKCNPbeDSoXeMPQs*} z8_V_yDs#!G5|Lf$F=&3l>+*9y+8mHl!|Cmh9929Z>v)gP&5hFxz+AkBz7Xj~ zAb{eYu>Yu?%-L#<**XwR_f-eH)!mPH(W$}ddW(j}$e8E%a(18pc0Lc$Z_b8bi%dXbm3 zeY5B)j){C-q^s2ea@@$q;P_Dc&WC4GZ3G?O&MN}R8<-tJq{n(GRb~!t%3rVc_OgF@ zEwGNrMxtH5i6}1y3PT8?SuHkyrcRVaQs=>-0G2;hsnXAb8dg6c+(IP4b1%bHKHEIf zFo%Cp{|y+6C{G~x>17a&CoVzBI)K5oYw~fpx#sW3&nv4-i~`8FD=HN9s2qh-+7Z-D_7}MT9==gn6O=ri%*SZj z%(yl8w>t|70Z^W$`H@mY<(os}19;Bo{dV`d2OV|VBq%>ImP;K{_1rrU=0vkJL*VN^ zMykII3}IjwcbJ#tfx4p6wdH)lr)8I>CRH@sf{&uo>e0dd1eMjgR@JZ}BHRfw>T8c+ z7mGzx4ng)Q7^49M|uO}v|Nr!dP$ttphAq^8|-pA&m zYHZ71)V3ZY+;u2)kqraAFO;(EY`aJl^qWPQ^}G+44W+q$vtQ`4QJCG}T1PFcpd**`k&7Vk=P8U+!4bK%q z(r6o_=<1pM+D!xV@Ar*KO$PYl52(u__l}#eL0uF!`3-&^o_CW2@W=BQi3CUBQNO&< zyWjk_ku$W+quK6%WoVT2!B?BbVHZ)1nU-!^p#_vIu@F7t3 z4lHb49p~52kqDN9NQ+6*G4Wqn!y&AF{KO*k+F5OH>EK}LfRrl6Mqz}CYGAD3+HIukXrQ!APe#$u?14B? z#5~03v?C=ZrKTlPjbFO@k&dx1$aExy_8g zRGasTHHs|09GYjcgTH1Iyo3DyF&*+bUcfi_o6Sv8g`PVXTv(a6LF`E`Mt+z35q%50 zAi+Qd@Yl01SEUS4#w$}Qs+ZUE!)Jdi($8w5p(g`GJ4Q}zOYORt%YkPfj4I5D)%`5b z^v6=*Cu+#_MCiD_`96@z0*Yqj5s9QGA-wKsVD?MeCK1KTIl)P`sBcw7)f$8 zH2>GvvcB8%x0W?VvH4!T$g7A=B0I_?o;3!bjBQ5cA)?o8{TAXUa9|AEqW<6Q$#zEr zEL_v8OLXNQ9p{^OX&al9PF8=)D@TxHt=*ja7wFRNt4F`0aS0g)*FFxg2wvBhXrx_j@?EHp4#vv$JCTRO zVW8*sx!T{Mu&`bcn<&^*(~u>{`Pllj#;gH9!E&L`C&Z2hDAz`g2mcxOwIT~-!-|bM zeOzhl+*sp`M3kL^=Q5wqfYma@(`;~cR^q3Y4S_qARY~645a@6@nr${BdJAT{t$i%5 zYy~VLWb5K*&?d~~Qn7{vyF#jq$*Zeuz-0Rw1U?chcHjMxwS)66SPwICJ{5d*d}#e1 zKt|~kQ7BW8cJkeT!$0P-o~cF^T9C016N8N zR8w3f=4(T{e6^-^R*Mq8mSQN&XYw*HXR{t&`*7rLGu7_qVI2V$L54EBGtyaMaOh%u zY62)h2UW`!kX1DB#yKedSVrdfE*{<=Y@9#jojm!DLP@Vf*amwL=nsSvTX<4v{P znfqhs)iFuyb$pWQ=jOhk*NqJ>^on&r|SwKF-dli{mh;RoK6<;1th|-+NLU;o{ z>Gd`Te+La2b@LyfkYPBTFqQ@%GHCS|*{A_<~u!Fr;br zKsMt#2p@}Q!~VzsVY@1k3N^NRA;u}_h9i9I(rCIx6IUzxFrAI@Eqm@xrmq5BcK+Oe z?+aI8`L8bysAG#yn=(9Rg{algPq2tq2}QL0%vm>-a$gomJ6^XeoF^zS#IdYJ*AA7! z{PHm8<{n?q#w04f;07vUc4-S1en0hmEY!UNhib#vsJO?XjL|i61t5(C$)nJ;Mj*(D z0%friV#IE{5kd~ih#X(f8ljuBx>_6k`oW8$7Q%St0sU(NG`>%fLTNepnhi&Hm!wd~ zlXL0_RiLrxU=_J%34Ufq;NIlZ7f?z-5&DNEL#UlCH`pMpp!Ap$|FSqcswchBQ%gzxDyQ>UfOSoz_40F( zK$#;DKB};r$f@svhXK{l|OcT*fS9vQ>74u;rHK z-E-1LLA`2-vj=cGsLl*aGYOwB+P}T;5g_n0r+|w@n{AM$x{YM`&%Bv{%n*k1B{FK0 zzPo|m1}#QUUN#=~5|Jb(WP9I)RyPdP9(Pao;diLLf7I~_Rr*FKD0f!OKqx#2oG@#u zuFKq~pP{lx&qicvi8uo-2K(b3P~z?!ynA zjgF!R_Am0w8W%m`*8W$z+O<%bI2jh;6ur#uV+%>_ZU;lf#p$53Cw48g{YTLcWS?%t z<_Wr59WB?GK``qq+}=6&ka>^ip)GBvrVJam0zm&TRnll70;aWZktWWG~LzaG{`-7RzROq9+A`#HaONe`k(5+(CUe}EX}bXIfTGd=wh26WJ3!YLF0X3tn^?B%Z@UJTl(=T#bsw8<~XBy-u6~f3H_ae(i)$-O2n#o@527nkR4w--jNYr zj`0xubQcC+OD)&taV;y1i}?9NG*y*|?}b|N57eKggB#q7YmcvUb89+Drbe4+PBYy& zY-D3IiH}9{R3*olj6uC9W%~4z)3J}S^p6jLJ6}>ST3*`A5C*h7mf~^h%k;4}QBXx2 zsTwH{zY+*{)H*LEH~T48!DkN&&^YcdQ#W1z$@EP3UpwOsCUq_--zprBJ_=^qEWEg}qIST1YHqk{~XOe?`8@ z?>L&;A>+W0A(O}@xHeYNhCU6XflyGG&)?WTSjVojmW6b3J*LaFy|M8w7`*PSPjStd zz$V8zUe&tykFGxy1-6r;ywTj$N?}p|e1pTN$Fhu;j%FsF6=h?gM*;A$jSq!u!hVHI zFjAnAejz-Li*?u=TtU?O8WVs^h_ST@)yJIA;OwL<`~rvxou)8_uT0W*NV*H-2hDl= zxSPC=x?gLEQUNm)iuPa=v`^PI#HM47ZvP)!Zy6Uy(ClsF?(QDkJ^11zIKdMf!s71k z7TkinYqDr?fxfKchizvHVo>sE}(ZcacO+YVrIO2*zN}sP zRySSl^?hNUr_M!b3RnAeqL+o$3EoSka9fV!s8K~P$e9}bLwym?92@!CI*wVMm(G8C zUS~l^hI$cPh2<%fF821jv(?9fXdRmyH6stk?%lifd-;ZcWroV%IDrY|Y zIL|?eLpDdSE>DVV#zxjR@g`!0hCi-EwjZK_lqaG*4ajw1zwq(eAJnnccaOE-2aOjE zTD+Xdh^v%xaoT;&AP8a1i7pus;RsGaKe9L*EFQv{p*4}BNT6FAuD3hk^90Eav-S{$ z4ymh|k9_~0{g*M)raM|G6~b@no@GOvhaY@l5(iWMvMpL9FYJQ>Uju3+UGlB(k}swp z3(OzcRx32RG^3xIT~dGEnm(;zX$%&jB>D_1vlYzk=SsuX-BGG$6G{h3MUc}^b2>^u zDvHU^o5;;YDVhh8Ula|*)ZT|y%%rvjl_2KMvlI_GA>AvKax4X0fHmxqDHR36(vwYa z%9QTyn+KRmmZr9D>%sm9{ zQ+p_+f!G?C9#;aoI@`@n-!Wt08u)$nO>icG0m?9|br`4BiZ!R`6QC5wus&{L|Gun&6q>n3+Z(@>dO_U$fq;^j#>==R+70aTB_N z|NcMo_+#cTg)&+u-eU#-;!#}A>A;(4;VF95igJ$Hhp66@7gT{Rz5;4W=($$wO^Agl zlEK^d?Y_rCsOC#c`CbcG4#`1IA*$OM&pyPC(Pwe0bLh(4e-%_b@%HslVFjkHAw*+t zLt@hC;&ikbaU`N>ev44tmkWbxd(jJC(HosJ4qw_~M&Vc&-4}MrFDx@DY8jDMF6rrL zNe@VpXvg&==p7u9b&(#ne~eQC_AUX!&Rq7nv5&(}B)M^jW!Ctg_i_S0e%O$6h$?^3 z+!6GL0Mf4G8mO1KP?F-iT%B$xeFy#MAufrKYd@GjM)umR0~gm$nlTxDJ1lYK!y>Xs zmBinZ-&rJeZy2IV>@!sTc<6DP(4|N9PwPLMH?D}pfXXRH_Icu5WD7OoGfGD_us%T& zB#(+zvBecNE}yrs+$2M07#~}EoNVsH!bLg%iJAroQ%1w>NJda;TMp=Fft5kyYoPs5 zNUDiiHF+G>UbLgREVn$`5u8Y^D+7mHx^&8(y~tHb1(v74k}?B12M*)LG1PM^Ailly`~zV1|#TY)K+sc z&B2MAkeJlJxp=G#XW`~A_w@tzDEj5=3uDL$1J^2MoMKvHl;fql4E!-W)}-U%3&YC6 zZII#UC+Ok4x8m#})gN3F=PN7~{f3k2>a&SeQiC^)AuxxJE66`=yI0qLxWPpkfTpwF zqcfb`{b-AirwT1l$?QCMeosQjFsDh38u~&ZMM#B&euREXIj$Nzi_@eyZNy=>%N$V! z$%&(Uzl^&N(W&ndR*#ruR>+q=OId+T-hU_L24mhe>Fn%LhK*e>swBhccy~=RNB$?%$FTI@@ zlpwPo7P;;g>kFZ5fa#ge_e%vi?k&;M$9xD|hv_BT-G7IPf;ZVYCzl(<5 z0Y&Cx z?6N7_iCdQonBVbC>{Rb~1<_2_d94ns`tQ2>hIu%VwP5hm@Zd?1UwkXb z>qdN~UM^6TGL#H*xPoiDht;n#(AhNQIlzTgjWasb?N^m#R@Suoahdt*C{wW9b*Us$ z^vpS16P8?D4u;%Du0ca$4zfVxIm`O>h1;it`HjP6mKN6-XtL zw;|99a)CDu^v6w$4E5XB<>t2N;9rDIG}gt_-?3L8K8A9Nn}2-$vmVj+gbb?2q|A56 zQZkT2;X2-({2)~xK4q@JibZDsoKvg2u3cmUW>78krja*z{nJ&p6&aaPej&$^3>oz{ z;KA;t0WSG=Aud-Tw8mqibadZ2#4VTI?(|f;@PX6?g;tECjYyh%3g)RW&&q9O^?65q zS64n4F|WMgxLP;w0bft0T$^h>9HN|B$!(}yC%{aFs|VM-0_IauFfAzg~G ztgIye>jO`Iw{f}_7N-Vizqot8qg{SBP6gkX=|!}m*LBlj6CoNKjCVg>Ar=S+4%L_P zCyg*iLvPWSW~P{U9aFZ@Z3{Se_F*a_RCkmR9L2>W8l+xuFeZ_`aGukaPGD=hy@xc+ z)2E6lKrs`W_w8`_rSH+Om*eX+sxy8DdU85C76*{4 z2bvafdI$#(w(4hdb=Md1{r0JVm%43GtdPM8lX~qzM2a=&(urj_nWJL`I~(BO64^t< zHQXR*hNP@amtwV{BDDi+LcAQdXJ4MHJbbjGSwF?7E1TAbk|2~L3hI4Wd|Vmb__WsM z1?O`FpZar2&*X2w;2_`cF_S!mZ^fH z7=MIx;~esc%EABzpncogkrWUf6C6k6zh0Tt4iPOI(KwanXm5{inW5pm?fX01p zfQv)qwS(QCl!DqDN-w7@mlnE7f9wvEi``tTppAqZpWyn9Kucqn=6+Iq-iFIDkM!uQ zqdIY5O;W^sB3fYpQ`TD}4HUK=N+*nl*W`K2@hh|I5iSj4##%*zKrP&Qziy5!pTObJ zwax3w*;ReqH^=*>3WJVbXa0KQ(+P4gG0KDQ?EfjQk|^`KH|PDMKZOqDlTIEQ0;>xXOL= z2WVrD$#4piJ6cBOtnvttHU2Lg4-;# zNR1e*!V+d9yNgpFM~m@~%7&2kt^FCLL{45V9cumb)Sn`~m+y$1caO)|tdCtwezE5m zXp&IxgS5MUq-K+zluOL9Oe!t)!!2p8(FiI-7S)qSXF`p~ z=Lp}S|0-)u(yIp1{6VkitX~YHg6+{j!n>${bK5rvp2~@R7={6n^VzReelF#(qt0hx zVM((t^-xIM!lTEaAzoeo?srf&mJgbqzBWOQ#fq0uv%QJWYQH_MMiX;mN6rYDP~(lB zvgnSdZd5u6LHGLECV#2Ea7;hLj4;rpr``iweIab?Ch9u7 zv0v-{^^i~@bj6uaN_M#Y_zJ>eGV$it2U+Eh?ObQ(+Q>8dbd#Ej4@!)UlGukfq+4vkTVy7MLp*B2#K7<4fSd5tKZe+?Ta^}$XOKJVwlPGy=oo2 zNbMY(+g8l>?*KQ9(Qktl6yv=_3W&F1ZOe{#*zA-ERE4Rb>w{k5t5MceIb{eU&>%gZ zse;(fVt&VBnPeh_lh(KH2W}Q8W+X*w!B5TFOY-*ac&?t zBG!>j6f`Lk=Rh!+!b2C)UbSwt48)A}@Z*8wMUF=(VBvbq|+KCnJPe7KyaU5}| z5ZVn$F`E4YU4+>Ly!bKo5ICz8C%f)V9Sq+$2R#TLS^8iuu(iH%{}+{;ZqXK9m>Sui00%O&kQ<;=MzRazROE9*+*3M8E|G%N#w;{Wc)IAkpZEg5k z!HUc&z?j^g{rsJ0*xgnVw=B4^1M<>WaSkHD^uP)-!Mw}2-Ktkj>s}pC4A_YM{G}RfB%Oq z*R;huhF@^Z_-I}|a4-6kVAJcx8r7q}LJsrs9U+XADxXFXENOVE9#?#hx<776i0XK| z)c|prezDpwNR$H?<^MWF+}>Xy;h=)@d?o+Yao?+YrUa(PXHCRNgC9bKt!sdhzd`=8 z|8%8gTRw1qRc}rYhhV?TUNlvdr6ia5S)yv2tEXLRul0HRDr4ht@+c%KQsV`1+%>A& z0u+mK%g_S*WO)s3MWF3z&0}aqt(Pm9FQ9f!k!_1Ay%F)f!F5Vu1KhA>Yk6CH(frSQ zil9FHf(8t$BgZpa*7hb>Rf^XSGI08R{o0b~^zqaPLeD7Q%diaTij5Ei? z5Q(+@4rt%Zgl;F~OjaS52jHwq%<3B(87xhkZ{NGZD#iW=bY{ID zd>ILcD;tVL5x5hk6AY*^pWnC5q&YSKZ+`9Tb#oX*>Vb>TMFpuAibfY344 zL1`+NShDsm|1wERj&XPm_Q^y|!MEl9xPd?upFmbwp5L10E)vj0WVrH|*>o1|odz1s zF(tqjJfnmtEt)HOeZ@z7O0)_sL~7J6z>H^+`BRjQfS=Ao2PrnC?$DrZLt`{epw%DF zVP4!bu6uRzyvgRPW(q&-F8Ya}b9R2M&6MZwXl_*-_Ay*M!fOm@_89^A7kRWaK|J|L zL&<9}E*XyR?c*5t0IwWqWGueBV!tm`2u>LEpPRo)d8^TY4e%J$bq9|4s;3FBgk;O# zB&P?NkE*vufiF$^c;&-*$rKHc-3)LPcxHt1e6`m>qQ)f2;k=dzx28wfY$jgMv?w($ z7Mvb*Hhd;9q%pe)1y2$lP&WWFW7r8TiQovTd4_a-*WEc@hg`6<*V{$sp+9~>GsURESs0aaDO z<%eBCN&?tkG<5mJ>>vbfLM5p`An7Z0gjpHF4|!GNNcfVHRMa0+_a#Sd6M;2G^LA>; zGDMzD+C}R>O;b&&sg9{>9K{qWLPoX1d+FK1Du)oAjm)JVr$jBZyNE<_4MMP}RjkQD zeW77Qjo8iovX9fLG+%k;-IjG4yWSCirFk3P4cm(_!-dR6xErJ-6Y($a$K8B3da*lO zpt4u@lAV)Z7d0 z3O1c6{*5#cXLY>@M+x{(=&TFv8cf1EQ5DtYk{Af682A$crK)S;G(_JEd^H0Y0fto1 z`#h3IHZ3c5;~M~9p%=v$Kd~Uj=A9x{=!s>*EtlfCu5nsTaAh^u4A=lakpflol0I~> zTvWm?0B`zew4r;I5jZ#$N+nZbWC@bR1i2!=gAOy0Xi2Bm~dz^Lo7K%iK_p<7I5dfw<>oy+3@+$T(HBM47U zFeDJ(+5NDxw&uEfC3L^O9U^Fd5pHsoQgDO(-+l80m2|ePm#Er0E6kvBJF2bg_Li|& ziAr1hwQu+TMMfNQZL%$seHumHQizxnb-7&Taqd9BeUVnU33& zP+t={T^A1U91O2+$460n>213ZViiobLe^T)34eQy#jt;mg9-4fq(Ugo#qOdXUEOa4 zWTAbb31b5*wG<#7i?0Lx&Fhb9x-R7-e;uP#-vb%(H`@O?QPsB+^RZJc4>uheW~o+t zH-kC~p$MnWFX1SJp!a;VU;2(;Jwv}J>0+Tbc|UQvt=8RAY>FjrA(yT%4OgW9;-WTcH*Ht`eV-l;Pqkcxiw|FY{T(!WLoYEg3K> zBC6)R!-ewn@~Sh*+qj&@C!QBGD)bb(L$9ylafA3YWb8=8{pgCh9_E<^%!q@mbZx;( z?kUv7D9q=!vw;=G$A9v?8@q~0Bt7n8vztyH_adl-X~9)duQo3ilo|)UR;+dxXxYiG zwFRi+W4Euu(Az(!RSV@h$`<)mh)U>JkQ2>vc@&JyD#>uv7)_HJx@7W9Xk)0V?JW!S zWW=-%g|C6t>ADIttOX5BY48Ayw-@XL;D0iBM@_Adg>P#0u0C>rp4oDwH`3s2-yCf- zY`gx8SO-iw)cTr);RL11{s!fpQOB7BE*>QD^tJe9jC8-k%ilw0aCkj9?oDmQ4BRD! zes$CiQW;O3q%vXJ+J^Y&oI}K#^+xpgt~xng8ga$0{ZCi)^2~K0JZWy+DG-r{)l}GE zsVLU6go~ob>j7X5nWXW0BGus6SfR*)Lws?Lz3HcA2OJ0*@O9&3N()aGuw{Q_VwZUA zs8aCOAjB7fv5b^Xc~K0iT#mGc%D1-`OqpkvYd#!tJvHsYO4|1&IW&+n1 zU~-6FR-{H1Pt8)(;bSBNu8=4!h9did()-@vH`}2KEH)gcuoqVzYn-#b`|m?`UIJ@h zo!ER_2LR0$;7so8$n`cRf9tu19!(BHJE;Ne6&*FsGvIq`#y~wK19b8f&rB9|Ds25U z94rn0_t5dI`qc~kDeD`T^~HMwk5hB}y9j0np79+^P8<+Mj4M1jJ4|yWZJroE9S*;k zvkh4Q%mVTdPu~Zc5E6ntBtbnGJ1rS8@5R?{j4(8t9aXWuZZ!oeUhu76;7o|OJ zObR~`Fv{R?e-;P}HT1%nWRq1C>$p3UmYTsH8y}Gggu5hD-89N~AciHZGR@Ht`TQI7uOfO4tOUH^FAIqZ1 zFB(8zynRR2FY9{^!6aUvbCR#3(Y`lTWTcmhVm_ApfK?M~U(mGkr?-@4ddY;WWs*g4 zTAGg4`V+~|M=;GJpQS4XZBYaAG5iGLf-4_^;}GH}zp>7<{ah1%=FC)eYAOCC^`f+8 zN>I!g8KSjML+F4$b4v21oWyKI99ji7O_n0_)Vq^-ngPo%aQxzny=Fv)YI9@7vG@vQ zv(gJf4ySdZ1zi;`siRC;iX{J6%OXTlaY0vwRNwOwcinD5^idJHQQ|f&a$eQ~i6Cpb zLRq$^BfeQ9pTOU%!p4OivgP*uuAkriw+T^7!6kAcwdyz=?E5nc+4X)k3RkxzjU@~$ z|92M_t>usXr8x)14|4c4yjyfyTU@*@+ML7XPCSiX7 z{l`)iT+o0<^BH2Mhz27t`M;Y)$#EkywbnpGIl_emEyG0D)eveVG-h1u{Hl;U6H2TR zm{LcholWBKT4(=iAhc{dK4d0B02ShrFZGMXdT4~EG@Cp-*?;Z`>+|~s5UT_#` z=hJ6MkroruKdQs8k@ldLx|mUr&=1a|)8iX{{wZ8T98|JKH!%}m@q+(<&O%6ve?t?q zqpN&t4e=jJ(bwUl<$!ZRS@y4RITE)&$?Y*Z(l*_`@Z^=H?kEP-^fW;2~~`LUpLp~t6_6S-Ie000tlg=1G0XKK+KY`ijh^{ghC}{y#R`936USQ zX_%@6|GC@8W0`vunr($=#cag(wCs9+9kB_kAe{^6w~b8e*5;eF2<2=%V#!nHB(G|E z4SNnel$CwtQj4NtKohe?d`C!H3c+oTm$Vq<)Fk3CQQ7d#=ZhOWhs?MUcJ z^L3D+Pwp}jLoY`iILP}$+tMogo+6S;It^x70nf$MV{C&}9Ki=(+=kpZD&)iW2Ca%r z9IKw)sS=7uVUM;n^8CiYb*>k=Ygb#(3W5Q4z(N8!96z#AZP6T+)rffYeKEbh59z<7 zGV}+6Wo@_mS=I_~=438)Q+p49!*CMs0MX`h8!XBw`mM#CE=Il2JCmh?;* zEus^sZsWM4ifVq!Af-n3kyU*yE!eEm%r+PtZ)k}U0BBUs0?_g9Qq8o%0G#(w|KTEZ!kL->i&~ADhh%HF`aV?Q*Yrlfb z?UJcZ;;9RX5sX;SplM_h67GONgFxe2WJ2A9>@g3bl))rNax)+i7fgpZz_PIY3QR_F zSmfU!LSkmWk*-&vM2Y}Dk@ok9J|y4DgDp&`!z0(`H#Bt5H#d_l{-`n}LaHr)&#;;~ zO<{&!rS&_*t)ad*@Xwv!A{F#rd$?b1r+PnvsRtfIoar zRK)e_OCRGiB_%EB%_h_T-@u5(e2z1LV-EH^1D_qRH&ff$d#UsUfV{`Uz{TAwEd+-H zmk6BR6PDpc`4%6^0LN1@>B-S#F_2B@khDI?9J)Q|oSdO-Wf~NfOq^*_NDhkv3-}T? z`6i!trCX=`|L4YH^ zwzR%HJ-awNJG;8Pw6-EC9JfJL%}R-m)??Cw=_Ujn?)LZ6mWP~Qk^rD6wG~b!16*25 zV2hzR=d1|Fm{;D^irgpnNa~s-|MdFn0`P121-dyoYX-S-@Wsy2TWpQaINHws&NvLY zga*!2A3&CXSe@J1eNL`tjDd3o96cEYvYfliOQf&OdhVx`zl&oYQqvA|iI72(pVL?J z0e_DqS3g0W?#7w_oDOwEFQc;B?!emA42AG13?xehCIe9pA))=&3IsFhAY)ymyoMk` z9gODaVS79?C)nB`5=Q?2r&XZ_@Fv-gbY&Yvo{Vi%s##B(5!|?c=a{Wd%YYS>gbA+` zx6ddn9Oog+V^3NEr2$Wg)#mj`>2(zE9S7=+1Q6rVjhR0GL_zA$>b47FIE(hO*(HWPt#| zU;Av_?Q>;RH@5A)GvE8p&=Z|YzCvzGczSYz8j>j$Cnu7VDCuPx4sNxhNg(n_SVGRp zV?2kWvCC-D7mkW4FUYLNQujy}DGc&-9KoCf)F1XADZzrwEoW z5k)jJWqJ35f(7%%X<3maQ;JB>${B$y>6C!^s1P!iG_4-?-92@EDH->4Qe1;5q2OXn z4VcK&o%c(yyy)20Z5ADbuoF7WbYB!b(ZZzQfLrAtr9%!PJZ1Ht@iOv8b%7^PUJ&W; z)llBl+_Cu*8`sf5Glbtvz4y*9T=80+HGVPXbM}R!vOI>l-(q6bHYM zAOQF07i?+vbW3t(V*%r3(tWq)UDe6S5Eb~4IrRPhCzr&HQ7cLzL&a5)xkok*z*bw9LknmuWaEU?BAudXjoRmN2MID3=ljf6;F zJvP|v6+Sbt!`W5wX5*cgZbqPUwEXy9T-#Jy+Jq>u0+ihV?KAWC@|%Y2?=VUI1!KA! zv=l_!T3!_z6tEBAEh70ER>iDKw7QtR-sUs`1!%?$vYPB34lxaHDa1XmMxM5+(j7qa z$+bb3_bZXeB1i9yH^jb~uK(PJ;X1g*b`vV9n8Bmz2uh9NQPxX^Gv5VZYc~uW?TTFD zF`UylWI-R?@eqxXEMH6=BLjOQ5bM|$O=fy{-&;Hq@@W6RQG zMq0ShWMeodOFKG`m2(=%ZQU=AOrxN`>&xp)b8DepjmoS_N55l7k>SuQ=uH1Tq9fpx z=)0G@CFtWWgk(9w1?v$m0_y~0+B=7DGGUUkMY;g2vQMY0S^VpfGfhElG?U<_Q}Kvn zy=tmS)<`5D5rZRJ)~*acfX&h$d3SboCF#$?4mHwVSzP~8!hs+oRV5M~!UYdt23igu z3ca`etOwYkN=w9DkcMq$7biL|b1H%yD+Lf|5cjA`GerZkh?rGRutGwB!y>WccxcG$ z>!Boq)`hJmJY{CrH?Odg+Rl3oAG{#g3`9qa;1ry{{Gx{Bw$%XvTr8Zd_m{h43w=e& z-!4ZFBmSz+I)SCh-jq}Q{g-pHHZiF7&>CrF_og1SUHHfD=&XXn$9a1(zUjTy=MpuN z{M6LM#Kg>0UhrWY;u85?&Zw`E`qV0$e{&veDKk|dN${z|V_x9h?B9VDa~Z!yQ{G81 zVKR{dukkCc`5E(E287!~%=IbPv3AED`M*Y0*==v#&|TkB{{4G#XK``ycXGydvt`dd~~;Oyjq3I&-CZX*zRv?_w?l?$}fk8Tn2`AW7E2S;XR!x;wJKK$sQfBT-7 z>8CJJNdYYF8bnCWpe0vR2krEGZ1Fd{A3n3AkOT*yX4tw6O>6# zJ&ZymASf1UY%FK+l-ohB%ViAI2g8HhicT(X1afjJ)cQLq7Gqt??pos5kGA$t$(m9ne96D(f7x47VvKz3rtP#0kN~NgqrICC7q_=L$#f7 z1QNO#HRq0BRKgdgB4<>nf$i>k(T$*dGefcxW~z5XOL5_rA#`RXLzQZ=&AUGF3Z7tOSNJ4AkNDYQIMhAHR1YOHd282ik4+gih{)oR*|B@b_73o zp%t54m|2^!3GT+{AgcZm*%GK|F`bG?j=kY$xV|ccu%^p#Ec0?A5dORF*ifABEuBO44GDlYAL!3p8;4(EXX23LXW@6713NrH9lUIcZWDsSP+7whnS`U{jw zb6jtCeZ9C^crcC)lnyK^H>NPxE>hJvfmvrM%&chTP)&v%_VG$aUn#*5sQ<6#Nm^J@KXoqPO#v|}7E(8Q=H_#R{ z^2dp!PCLY9rjA$0C-Dlg<6Bic9F@-DhY{nF!e`8dN9OBZFNdXmh0IQJ+cA^SoPevJ zpf_`C?}zT}3~~hm=OHX${GoCNCZ_Td<;+wG0oPwe$2Tk3xt$LT-L2m$tMff>21+dC z1%w;I2guNUF;s zw@E}vPtcu75XbW{9C>SdQ^xAY`j>FnSl#*vqi1%+}u5T*#5EP zR&=3bJ_&pkU%WnezD$MDz>HT0<)$kMS^QN1MW62 zlk9DYerJl&I)Aj#Q&cyB!DxMactS=%ii}Vgc%zaf$mP4et|gQg2-Vn_;<~vmjYA;* zRI&+^1d$i11rp#+v`I?BQL>`E@%z<>e(y0YC`ZEzqz0OP*Asv4!z-(oaC>rf&nV&e zK15xowWcC@d6u)KRXV^)rpG1#e~z8XXQc!gn=^~<>3eWUq#Q-Y`T%&M`X5`1kr=Z z_rbLsPtRMwcDH?n>h+tQ07x0j!9Iy93dvg@Z*`BB!-Y-7Yys+SLl7Vzc|VsYT?Y!O(!A5+dSI+V->E! zhH+h7YrQ(k#ccQS5HdiH{mBt znUJG!hgHMl8+Nhhn&ex5_r%K}Wm)ythW35!^^sg#BIRrxYElf$g&E3(7Bhg-cuHlo z4(g32+R@$Uz`} zeja|TOzL74NbNm5piXPHxNEx8&Miry3$d?wg~f;Ld2nWzWfhd_`Qc~ecIeKfq)DL0?|%h#Hdi@ z=e0Qvl)-;?6n`~X5Ard>QN!FG-N7PigoaMYagPz3`lSCBx0qgz%_mQ4=43D>B3MUK zXsSA&n@ADW;mS-Wqb9`L-@{Bnm$q#&sGVFE_fxdZN34D6>wo_Cs9O67n?;2(c$MFM z&^bhgtO8pB!Pm*1s9*3-MV(W?CrR0NsAKo@-|Oq;s3!WI1^2SIAJs6qNh2H)+*@%a z#!36e{;|W7+#|R3R=68d_UTYP!qeW+*Iam-;-Nk- z_wFY!!ss-O%BC~&0Je&Nr^ZAx9LFDpB2XWuQ7y=`CV`AXmLBKET>XdB118`!zCW<@ z_?ABsaYQQOy)6JLm5bJdXHe$D*#uJDx;vZi=O(rdzX7|~t+y(9L@30xLj0YiY5bP! zhijkD0+}5Tdvi_VGHEx}Uy@3IWbe?J_DD-RtuKP$kVDB?9hc8THO}A2!{_u?{baQM zI{vuqLIXEJDq0s!$PaDnIeg+7t+h--(l>|!`u_N`$DgTeAxEAw6aOb94-=^$EoYbA zJ=z91O*Ua2CT}oqARh<a9;Q9R%yz^5uMsqXG1H7@gf&GLhdM;{B{I7^s6?Q0Cgh!{?Oo^@WpdOIDp}|e zc9t(7ywBKMNN_NG8)#$WXTf~UY>iBYMN1*$sm5hIo3E&x-DdH!{{3#9XrZ|nq@&82 zn1?X*>bN2`lQBjFLGOMVeA|fFcwBTjB9Z?RIvC1%n*I zqu2gHP-2?s$ACeV%qEwGa3;jLlS!2@1{!odo}ZgiF9-XLAUSdR&&NdFJ$6XG#wRN& zV<}&#Q@{BjzWKG}^QODZR#8Wa!OkS)mNBc~%OB-QG_MF%z0@CP`#gQl_pn13M6QRU z+aW%F78WD6ypxaH+UwqU`F3+OciTg^jz(ETptCu3vx}JVRgZFnK}uTPmu)T>Si}M) ze(3%A=4zh6RXL+vqSxeoJ+CIuuP-=NgF-mSm8kp@pOpZ2H% zYi;lievnP&lJFm?xf`r0BPPE4ap@t*MprPMtaJ}YguH~ue79_7c(OlMtw*<)ZuC&?7nd%=&NwmluP+=2Q8@{!LtyM(8vxo2QD)yJMVEbAZPoHt>!p3s)%v7K*Wqegq>fofAM{RssmbvF>9cYgD%2c{`(7{QMw}imnM(>#RH}PELSRKq5k{y2UmyHk`{)Aoi%w5oZyc-G~+K1Hpoi zPN>O*z_+7o6?@^9xGAZOm;Xrc>p6Pq>|)Io33H-WSmV=ZthMa40xpjG5=}aO9iBF0 z)0L=ytm>xE)wbp(Y;T~|^`m6xH1T3e0zNm?4i9FJEYuau6%ffuH-3i7G`0;&q;AqG)Pg&ZE~@5i&(`TKIbLqg*UppKwdYGl-cM{W`fQWK&JCrT2EPbW-qnf* zqvcEitAOTzy-FgF`l}f94fI`{H_SFLT5eBS{c+G@SlvV^U*$>7-Lz(c?zmw z%I$p};ClPialM6B%gp>1VYzsYVwv{Kr6~=W=#UIK0SNX%;X@ETJ6=vE2`$clz9j*K zP%i@YSichl1T<_fz8HEbs)IFj#KMlJsA>`ZDc_c=t9^AX7$dJ7rgq^dP0tE4m0|&L z{CyLWynN{n%-8S&!$1)qJEx=@GNIRV@+FLD7zXrMgi$mS2bR`vRz;QBEgV$A(N~@u zEQv-ZpNH@v##H$mU&TM!hUmbQg@n)*4Gj$Nq7XTKdH*#RJ(osUH2mjH)Ae8wLB( zu1>$J8x=BpO(w;mD59D+Eh-7z?rBIWjyDepMeoo@`sjkcZ3V+EMf#AvH~P4oCN7S; zQ=^J39f8O>JC#DTljnEd)Gk9pyD*^utM&RPU9Dio2J>M|iuHt5zSMo^5xLym_2J1- z!Ielg9zFcRxj3tj=NjOojgChoRrQfMlfmGHq+$Px9GKU7cy3rq@COP-LF1W8t7bJc z6ipKg{QaHBI#Sg;u^K31e;D8YEPZnDuRN2>f@Nn6R`zAfJI;+}vf+SbsUC4f`m6Cl zwjt&X4uzM~a}@q^QCegsZ!9~v(9ey>VLT@UIoV#;bdb|#?*+Z8b*v7R*(m$Bo>81o z73$xd?|J!(&7HB83T`Sve* zeJQe2qcZxU7+A^-wZW=S8J^EY_-A3n@iChS_ZYL?qHPSltMDU0PGV{)GId6i7KWod;|GWb*R6UBbo;`Itz{CNViwFB$Ibh}Hm47r{Lr_Y05Kx*9?W+8DNuL) z-9|cXXezutFsKU2uDiw6eV*LX0m3;czLwt8$0DY^W{zAkYPeh;zBW)`9~+I?b4^pE zL)Yt4NCYEf)8buDgaKN;1OG##S_}8{U$m$<1-XS3PwEN-N9b+e@j@utO!Qa%`ZP0+ zJGCblBcW=VCb<9gFr)`pFp*HB4^$r&BZV(z1+4;kO=;1GHSaW^*!Hk9OSatd4Tp4hRajwNRSU7p z=RqXq4O;V{IZ?Og%P~htC{`J z8hmY7RP0UWA{E$M3yrsN7;6@8V!OK*|Iar`Zx>CVF1AQ{)Z9Jd-0pAqav0U*QIAl! zxzo<-PmFk3Mt^%Hsv}}lg)@w=Isv*w^8_y?rYM2IwrD}bMf_+Ye34pXv-zb2gF8BA zse``E-`i_naT%fFadVm?P0)r|$-lS0C3;IpI#4v3v#3Dz+rfDI$ z1a)t#sy;Hmsh#6;KDbyA5KAN4Erbng#=a3Gk`(_AHnKt3mrhIu7wiEf7Uw_du8Ks_ zL#TU52QIyN1cr_`W9IMq&q}P$G4h*F9KguwfBhES-bg_qP%n4zQ{OC#q?Szv-0JcI zW#D~nW5z;bD(&NTQIsHzfByXH6@=()m{j!as0!kDd<&hYuCG5fFbkm4f|0M1PO+*J z^?0qm=Oo79*&5w9p|*Q;0Z+(koz1ynk4gQWk!D9K#=z!BLBt4 z7!UToqm5VG-~LM}|GNotzrT_A)^@IO1D+!IU-Br`Tf{?O=1Bt=b&@xLyq(B&MTB{- zq5V_97AdViSn};8y4(tyQIE8A7QZ;Jj}@4@x_L%NpQ;BPoG&aji4d)Y{T-SEvo8>& zv-@-4eY0zt)`wI|P(C%~oRdbiyX7NqQO54)ehcpc&FAmCeQ9JLQx(GK*9`^NKFt&u z&RC}#P208hNr1B%LbK|A zJE{Y9WB94F51@;Y_8pGo=K84$Y4plkUl-!~D6NZjEe4;rS=ViCUI_AcBi5Kk6M5?> z5~ALDzb$^l@1y-pzClNy{}lr0GYQ4-`kGV@Ce?ZRkX$c^UV8)HjETyX$0legGL??) z8}rlzt6a94g6PCQ0nZV8Ax$=Ec*UTn>>ZOa2a)u@Qq<}?gr%s*Pb!54riBOR zRi!A9+Zf0f^pq;WQ{^!SRwS{LhaGD`V&M~|u^Ld@7O2EXuLf*nFoQI{=hKjuU7)lf zp7v~3$A2{qG@A*P=c>W{Luq;DGA7+A!3;#r;PYs7-h?_9kkDI2)CZ$H+^kE@!Jjwb z!^*OZ_Jw+d`@bi6+?f+-KTNCctIL`Vr>*ic6yfcYWHvq*U)vH$LofljP}-mum89G^ zL35A!S1JKxB<75wD8Od7ds|YeBBubTxI)9`2SEU?hDvMlb>bqnJnD#}mc{=4-qBS% z|34(fIeSbJVF0r9;qJnvP#8@f&;id3l?_YVwP%*e$6|a6nOk3byqcLj7!VdwHNT1d z)_yV)J?r8!jso7&@eODwo1e8Cmis`bj19=wI+V0Ion-;jLy-OF(HGToJdEW-knmUT zg4cDArd-tK*Szl93qgOc_|ilvt&1+Mh6M=|3xZpG&U+tLBK`_!=k!uo|KD3+9Y3330pgy_|I|R{1o2rL?Igd z*FfM36ZaqQ8`HIMuK0XD*$*@6kg9a5PKW0@xPF?dEWI75T!65lzw3gGvl}!*wZYoA-=5HFoH3Q`=)LXVDIjKd zaQgcN+@!Ph&QR;yo)Bizw3*GtFwx~AHqPXE@jUNh-6h8!)dlQGG1M#k^ls~|zMo0i zIY{YYtgHGjqLRk}`k*h33f9A%P(O`z0@?`>w>l5+N|KZ?7_ebrWMjVVYP9PJr{&LV z@Ilcq_S!WGEWF+oFm{xZ%kGc5+vY_B#i(AxsEAO(d=Y&eW0tZ+1_(dOi+F73ksp73 z)}p-S!HxyQ+nTxxHLoSM8|kn(5&mdvl0gnbpT9I#;reGszDJzy1PdPq1gC$#(U_FF zLhMD+%TLM9CTRPv*VD~oFN@tb%<6Rz+PR+N)0#(+TVudUXLshv_cVY2gcFG>CC`zU zB^)E)JlJG1fAc3r8u>?@lG*ILXMO#>T!+Q>jnqZFdey1q0+23M+WU^Q)@&e^(8Xa! zt+pmO20h{(&fP*#M~|hcc0+jU%ow6p?hl_cC$TZvOZ0HwTljJ-YZeRafQFZByYPo- zeYTXXthSBjm-p|M(3gWG#GCzovTfrHPl!9pE%ZOG4~ZL)yPgUtVl`UeAtlg+mkE-{ zlhjukcG5o6fyNwwuQsf}K7F8}F)biDwd4^h0?I$0uL_fqi^Q|P`hd30FXTnjVde(c z?mF7(iJR1(f(awT;vMe;Gc_wnTULT`?a z6`Q=@7bnbgz83nDO%Aj&SMjP7dEZh94wlk}(QFSP7$1Yfe4aMXWPpXGClle+RxGre z7E3mbHTadh=es2sWzXMbQEoRY9diP`vt+;}waVpKrZG6ir$zT5`y9R8vpR;~lT(6- zv++Be-1fVkZFub%BsiO`+Q-dr;^VX}ed0{$d#Bq?#d7cy_h3Wv(~dlIDoNckTA%0m z51pq^3tkOCvTCrqqp#l(TeKA?4V=!Ny9cb@RXq3llg98?V3QgSbBeFg!J4HAydfib zh3z)9f;(%W9OQXS7Lrm+ba^VEq@EG`_w91j3ReC1{MY8szZmUr;d}GXm(`XF138G1 z8Q8)J+08M_kzo>TcCORditDs-sim*cY4~XW?{OR*thoPs94ngkV?Y0epF!7oGdc^N z)j;4oP#F1E|V_VB$7XiEpZY+(Hcg+Plmqe81xe`dOSmX6d=owg$e8r6ic~b zr%7q0t~$d{$oUzQh1}|3br9LZx%BYiDXqB2RmPLi?SfDR5m82bU=jTb=YBBEhJS-3 zhkhTjEQloiFE$Jj&~k(Brs4&k@r_^ET7Ox|PZerQ-Ix7QzR^D{KxTX$Q{b`jB~iP$ zsEBO?`PWj|WpQ%Xez4Umf??SRrGWx$c2ty*FOISgno6dn^}XG_u$@rR8MIUqG*DLI z^O%1nGFk?bc621_+uHThQ1BJ!Fw zeEG|iTleE|@cQX-9^qaeHIJURhKq{^t8wC&bFBkUUuYuch7$ZlZxF~(StnlUV}NprPq#B;`!cxeX*3OBQ2mWUt-}TB zAC&y7Y&C8=u_^gV6sl(!H~|}zM~m4D&ecis4_-N^XmJ&1XTFOD5-KHyO5)lgY(2Y0 zb-tUfIu>Wd@jsPo^Z4ZpRJV1s`@FQRyZp>#b5>y&U z6GQvHqWlb#hbOtA4uKL8t^ZZYvC_wsk*lGxQ*j1Vp#v6!k73=mI4CWQnk1ZJ!%=+= za7oRV+a;t@VcO1U)p6oFNp{yv5iasudQZa%70j;#Ux>GMDFwOqw1b?upLazkvqDZV zz_8qO$p3s}jI-}TlarAsdwQ%#(Du9O%tBE^^7%uhkoF-r;W;<@AqpT5Xfd+nXY+HB zy%Avv;>f1$J;qU~za}@kzYnj099xHh&p?`MS-sTFj}pS4bi2;R!EGw*YZA<=d+fMK zwv(YYx|hGo#OU$!%Da2p_ZRBgFVx9U+eDQu~WZ~Q7ffmqt5Rx9ZU*YaX&^bgTrqabKqbOC( zXb{>y5Zy``kE94^Xi$<;0r*{N>6iy-Ur}@imJ_Pr;fb{BT(NhdyT!Nyd4C=YW+{(G!q)ruAuI5fT2 zV`;P}K1FfpHK}o-BX~B&92Rp)e`P9nBz71(;p2^i!TLT<;p2Dg4qGJI9k5v&I0_1_ zzu7#ro4-^-W_D7#WM*|y))1dr0WMM28;iWClZh=H5y8oc#!yAA_L*pAtPnk2k+QCm zlk^KlHdHjr={ym!*5#kJTh%nvExe+BvDHceROi7q_zyq3Yk#`*jJjypw&*)G+M&jt z9j>|>DCV30KJG1nc{mEB0q<*iuUN0IdMfhz6B%+t8JJckQkRiK3NzHps5B}1m0s`A zcPQJtz->{ymW;I(N$$1Zl<)}v4Hlo@Em@y@;;YPp=KBUMM=JkQN(uk?9!*})MHEvA zow6a4v7@A*%M^e)^x98Df*DAJk8q2_jw{bMr4ZRbLPLboJ87Q*W}Kfr87@tl2$7d= zf36$VX^oHXXWtqqTFv*hO*@Cl9&jeiQ9;JH$V>;&xelL8sb^Qpr;;!z747?71M|d~ z-+>%Tl<^wJPlHDJUom$<&z-%uJN@mvru*Ta71!qYoN{Ebrmjc=fe{x7LBoJ5JkjN~ z$)HZbKvjzSCNn1tt6+czv7kW&jdGo?BZ@T_$*<_^yw7vfEQNsGYBR$GX3!f_md(wi zGrT)4B@EY!r$)f&W9$V}6ZA)qOczC;Axxy%tDsaix|>dLa<%4b=0WR5IXcP6kB62Z zE8J}S+y{UeYZ73CV9~Gl;?5wI*$&ila4$Tu1>+6rnl$VEa&!lW8O@ZsSIKg;zbPo# z_O2_U%h3E@1drtbQa@Xe@(rlHjyLBcr=+K2ZQpjqc4RQVD9#&n4^%p~-D?R3adMN@ z$4&e9Gz@4~qOM9FdG=ViC`Wbo!moA`$?+%2UPcYR_so3zlFk18t9lxPyC=>>~gMDmX_?-tFVI-B%HZd&hik&eep+^6Mfj39|6 z@Rr*Whx=1^YKei$Gat=0)QKqhlp5P%`Nq42F7YP&SzAR5B7^SI%2H@*QdJ6}5DGCc62Aq+mF?{nx+oeq z!<3Vi$VYV4SHsk9DSu*GlK$l$-I$qJTtpQoX9?~zqgu=Kn~UxJCmPQa{hHu*rD5Q+ zF|KiG&Co)D7D9lYy0DL^T^oGJzIIRya%!lp+rMZ(`B2Fc@$H`G)}V;PZvH9!G$zb< z8Q)AphJs6^#4Vgj3T}VQllEZy5+=fGT2?H2|9)!8ueKrbw(B?VbaJqJ^)0p%7$BpV zK7GEewod@=6*e~Do%$WbReGNf$gr9*BHWfBf`Hd`m z6>!h*USH19Ro=1zVf3tX9ZzHmIQ7dt(0Vjl0&^W>LjrokWVhu7{L$XT#co%|r zji-NvXWKQ^QtuLJCpi=Em{VIOi{!((IhV;iCFM0P6ogG&=`PXjO82LKTF~GL9^Yjn zs-VORy$r)v#6P6?m3r$LJ*75H;;%#h~m*Z*A?*uGl zZ=lf&g(^nj{8!upwBQcXk=Ku!8do6i)})=cNQOseJGW=+l=946dtDVb`Ut_JCsfbj zb@5RGVT?%Fv}E2-uFexf=J~lniXr|ov$Kp-9ABme#x?+_Jb2@0PHc|-e?vL7R+VpO z;S~HRgwx_SiC1p7ZmmA@>MMD7YFi)B@9XvEUxhM*0e&XQncEd!Z%uJGv(BRQqo#w_ z+QM)A-pXXqJgeXC?aJ`)Eu)b|Y5zOW<4Of(HyXl!;5GXxs8l^tfWQ3?t4dCo*+e6b(}W*CuOH(wB|}F} zoY}K}I`d7j(IPbp4u4~7%zn8-sBdkUL%YtPVD!F1!Z54#LfzvR3Op*=$%f0m)CJq? zdv4(8zUtXvKjUKsd~buu)XnRrCgOQbY>aOhrblDz4F%BsJW!R4+8BQQd{fNEJ8OsA zq2JMlt}*GQNrCH~zPcj1GB-L&)raKqjj}$#u7Z@Miu^m(66~Gu7JLB*ycaOd>^?=f ziF?Mt$B|1BBG9e#XDeSAk?Y@XVcYf6FMF=?OypO}OyGVA@UXP#<1wqJ|MwCxW7m$B zf|{(+OqSe-gd|2_pkYHDtjo3b?rn()&oxEJ;8kM5m78MesPuI4oI0ktMD(Y~si7p2 zyX5LmrR#5)Y22uB5811ijTR{@E)$s{V$ z<fe2_P(GM8uQir~-}(X5ZXynT}T ziJ%JibKB0*F!7t9BIsM0E64_t49ejIasXmJ0*(Mt>}LY1Y=yEhr%44RY5>d|zA=LM z|2_1|;_TtU_0hAt{@bT6%UzlUGR`4{YT#3U})nr;8muEomf zLuimw7hJEGByDG-j^y`Q@JZ;ss>l#=!g6Dr8Y;_c&cCe1QgiH8uqvQEKIaaQ?5b6( zJG@GUlme)0sYYyzx3Q+U>3zM(Z1AGZ$shjW*7B5W^huT%7|Nf#4;-2l4BK!7b{)*r z!SM0_tcJJ=*t+z@$bEEeh_^i*-j$i5V_K5so>aXTMArtN$8dfyZ`xY>Zp0%_9KY_sn!U-vWH1wW-Z)i<` z8Qiv3?u_Myn$g!=%d~&mB@yuEDwURn`YiqO`J`mjILwNWGAyHSR8k;Vdb_ahOnK~F4Zpoz zdw6a)z^q-j@qcUP_P1lZD7Cg-#tH%5D^;g`AK$#H?KhAFlAz*tGY7ys2ALPepzz?2WUE@ zYnwiK{x}$8hA|3`%gL@!JVPM)6FFI$r}r_1ELa*axaLTP$oc*1;kwR)kAB6PB*H@9 zoS;6|d-*O$c+F4dUzAdqFK3lNx4~JV~&sOf=tBnvEbU{)68m$K@plw@)e6A{4l#BF*r}M zuSGc_4bCR8GNya|Q_E+1{iR5ZT_v2%ULjS_tTGaQl?r`;jAwVi|G?}`Y0bpq>c{Pn zRZeEN>Z=bY)5A)>$`UqnznYAnIa_Osq|48|3pA3ClY4eL8TO@26!?+`Ow2{kON=Zt zEK8lm=6XpmDS!6fL zR+u&d`pB1|A^|0Ls*g0K_0meqtKa;zXC$pID^|+mDpm2T%7{{_z`cP*o6p~N#$N@r z%+s64=)(e<$6H?iQB1$vnbjk_ric;YBZyuQ`$t^c}PF9F>P zKXU!^ifm=KZ5h-ESzF)aEu99WSU~9u3R$i>1g|yg@0w5`&PSuFa+@It*n3Q0R4b@T z#+V7z9~E%@9_(y!eg#yza<0tUatJ>*WO(%b?3nj6UVG2^yq{cpF8e{MEf|ffygY|M zR$%1L&?_q%(;pUZA5u2rK^MeC{`D8Ce+n6yqas_Ad4|_{WJajLAF}U;ukMpKDE9rd z(<9SlpOpBHGqGK~ery-y33%s4RXdUNY_QbSrI%M#C#ND{%~%;bkZi)#5|eX7u){ zi)#hUE8xIIkH5!L8;EqC!Q}I)M3`l6B*tkbUL8}$n-L9Fi2P)JSQBp+;oTf#&OxM% zu!3I&_bCvPH#2WT)tHKq=-qjCJ zpH91_F)X)3{UANL=o_rSq0DG$i3k6NT9>H=xywu*83v<^=>7RQ!Tl>k{0WfPh ztWW#1=I^7}JOD8kM^_KMqz@+%S;R0Sl~`Z-KzW9I6Wp4ai$ZD**qB+%l}#lKv6=vo z=yU6<3+GoW(KfBzRIANy4^O`#RwXM*84cy7qsV1_BVQsepj!J7xQ`n@AkR!2zPp?-nGyn)F-d3T-d zkij&iXCY;eY?cg`<7jH8nKb^s^qJ3n(!5!edn~6cJJRx1d3?Y))V#XpX(aut2|K*!5%pJlC$v{em4K+s;*c*H-^%v?Vy^oW1R z4O{?)Ug@;mbLx+&Ksgt_(BD?d9QYCzVw=}L%6RSZdEBJY1MRS|kdwhk#Z+WuTQ1xk z9huTiSUS^3b-`)tuZPtLkfm-GG89Fb?+?-fNK1(xeI!UM7b3M+8fG5Dy zU+{fu$ZdTnU3;wNu;u!Cl_Bc62&tLD?HTkR6fmcB zevcz=AM97q-K8s1VS9RP`2NJUedR0G@n%eN2Om>u_Ars=({ox@xH0AKur6}(^u|Sj zp_ZlhNxw^L|KKQ@FEKGjfx&FfYc9Rs2xi*_jli(%@J!4GJiObl0`qWA_;rrb$_^0W zJ0g!#LMkNoSRcCfIu()!)v1z}Wy|j-Jc5h_qm#DwVLH^2jMv8@$-Uw`mLvwh+^eQt{ zjf)nq%|`_iKkZPV!W=1`g9Zn$kh+eCOVLwF5@`lQ4F_iIc4AVL;%aiQF{VqSz)cT| z+`5UWJY)~Ka;gzT@{Efw2G4+l+2OUZ{zd#)V$rvy<=)l5*;0D>W+;9{=Wx6~JJUsg z+5ZfXPrFH8$7Q$dWWpM;_+^Apw9|uSeC2AKbXKSS0oiG*68^47hmla$p-6O?kBH&Y zz(M;xivH~9kE6k!VqoH%gGw=N2^CBonTxnU+ey=)?Om$UlYB+hK` zZcy`+?!u`bwvN{A)LO9V@^7V7GuT1B6#wulhgTEpSy$yt|by4zXm8V0fvw zSgDZh0@uwx*G*O#j^fzRI|BsN;r4+7)XqdCd3PkVaS9(0s-|Wh$8jj|(&)SffE%de z-MZ|5pQ@0RPhB3(p`%TIECoLT`k4Rlt(sCpwRnKgZD^nOBdN3=J^SxheTlMVPKq8* zz8BG?-rUryjbF8_?Hz&mVUIn>@)=9QzV@|5W^in8c(nf#?0#oCMqUUvRZl;hw#U1@0^@tnn7$x`dH;JU6qAJF|>cA9`e&Zsj2dK}8L%BZQa z0`UC3;JPEI(|@B1K`svmcl&Vqe+c%CQ2!IKl0q8rN^sDs6CT}s;Sc)a{obM|f1_id zV5hLxiJDnhSQTsTas9U?u98V2+#)YSs6;sDzd@ttGE_l~tB9=&OJp1vb3wV&)_JSq z4L%Ylqj6GDQ=B7a(+B+HQAr?pIGcYt>xpCSNL`TRw50UJp)bRvO9!@b<^g|q7yY4u z#f{pS7-|@*`@0(`QkO;l&Hp5FZ{KQOw%PvPJ;mNWi*czFZW`=k?wn8u zE_!V0d%gq8-UA@4GEYZF`^N!oh?TnQM#AiXcH(pl4bRS2Jiabo zq9evx%w8jiJwQKj&W^H-@ormwQJ%)+JwLU8pG3HLs=|3pUgO@I?mGK2E@ZW@j)fIz z8~cqu3XBus(sM)JLjWv}`4vZVeeJ=?sC%0or3~ChJg4QvvF;7GPvF_?rrMt-=)Zxy z`vCa7=wp%{Z{-@R!FNBJstlD0t2k`rJ=fR zND8-m=2g)#-MzQ-_=EZ|5YqGx33YJz+IQ|LBNG8E*xx<0=T_B?%7AHVtq^N5j6>1C zu$x(x=a&fy;0;$>-`7W}_6hB63KE&5f+V^pJJi?6OVw0@tIc9w`DcovoX|5FTLg;m zcZnFrsx%-j#M!_r)qOi9Sxp)Zi@qP10~JxZwifCK6z133K3jrC>gp=kp%r$c*ChCT z)VSDzq;-Vs+ox@PK+61xsTxkomOv1IOn2=VVN~4R^;#uay^}%S+kBpS zx0F_YtnhXYS14zd_mCWK{!Wltczn+E6YarUP9Vcw-EKI3$3v_x7rq zW0Q&&3u6U7xqFz32dbm`lP4G$;F%2Kx8e#a5HceWR9Na%a64-p7Ul&61>FvzC2(Ls zIn^g{vR_*rtAE`U&iz|sPR^xaAJ%~xdPqPozwJ)H(!jGk+S`k!mXYu$P>yVB4DfCS z#UTyeKPeInZV)k*%2~hgW)B>5wr4mvYuZMHWOhHi+LIC|mV1`HJ)7&yAe^)U_DtsO zCV1wq55DhKvQiBL1Z+va@5r-u`oQ$dhHr@~8 zRVg&nBuQxqz-+)ZWhCF}$3(c@CukniZal*HW1+WFPd zudcKe{mC3M!tIj?$`p&SgR(V#%D64ORFXi!sk!yyfqz*)p8n~J>3s-cl8-U`jYoHe zyfG0chK+Y=qL?a(_vLxU0ndjKRx@LACBuN;eu_BdUm z90Ko$ol>(2=#i3dSiuv;1cFkdEnk4S-ZjG*VmFgwtf1|{vIEaD*0*n)g}sk81t(Cy zN*2U*{K(wM|E2>i!RH9_VQFtortP-&AsAmiFz6*i_SY;u+2h5)2KFNnvU&u#h(hE4 zULV@T>%Kx7L*}CpyvF>TP8BI~o*ckLh-O$$)W>2-_0J3(OuG*OK=(iHnM)Rl8gLbU zYVcVk&##$1UxxI1IG3WK@sJku(f;Hy46cFJaD%BhoqHOayw|vdc!M~<@r8wH02@H2F^I1F6Z4Lgz3EQr{T2fcx ziXiF?2qO@0oYuvgRn1lXrXGI2zP=*irCOdMh!D~1H?2?Te>bHl?vpqtY4tPH ze?5id{gFaT6@|oc%dN|z1(u&xaG4=z&-gxWO4-+%64zyE)eW*$d{oPHn;3(w5FI8# z)|Fs5O%-b6ZaQA8JtFiYHmF;c)>_{8AH#XF1lrLWjR{$;@W(LEW%T1*4lAWMm^cf) z;w?Or=%2+9vnx$-Q}wpE^$_GEw8A-f>_hhKq^p>~EZSUjxFpJniesE%8HxQl#4;-T ztPE(Vc%mhEVMf~)+d^EwBL|dZ?Shal2gd*Z&MD(5a@+4_#IobjAHxnU6A=r!9n&Yx z!}9CzOZ#csQUm#HZcfF!fn1KH>;Pbf(}wu}hkessy(G1K1D%(}V*h~ny#%jAvZnEA zE4*o?sbVDBrxUbmpU8BAxtU4#20QLM#?t4rh#5;LH6Ch_%x<4Ki@FkaHO6i4xK147 z@847;W}DB+A+fO1tiZFJal3083uue<+-z9-0n+5teLRcnMWo@qpS!1gQ4a9WW&U|p zJ|G5Gk4Hk~`y}{WqUbUYm24WhjAHxiFBP(8La&EnK7n;yWL;=nQX5(#UXm`x)jZ~) z>J)zCB0GCH?u;JdYm`(LSvt~Zlw3v(q)RA&@T0OEwbfz$BAkMS6{d&&aB>LRwUe#X z^*(#E&=>3ALS%`HW?>Ng2^}_MnLKHDiZdo$Alj{)2m`_#|8&_l!iKB>^+aP`)^h;O zNCW(pIW5h;<2@Wcd7*~Qs(|j3w9AdG{K3;G5ypX#PZC)8)k?G@T6NVObhqwc?!Y0J zf0OdX`qeU&$K<8=v9`d&%r)N66~wWt(Gq;stJ2Y6!@tj4d&gb6({!C2ap>IqBo8m@_>BD#PKqYuwMXdsQ>s{3 ztl3ZD6rYjG|~4Hz2AM*5Ahgf^BJi=zH2!Oo;_c)N6Ivnb4*O3tk%| zzQE8;n#6ZUBNWhVi*nCmLHD2gaIn*3j%u3>J(|vK7RX2IH~S)G9`M;`#GG&ImWAzYG$zh^>N_T_){TTf%q>{8OlcGOCz zva6Se2%JUzUig?OKWUx>jas`l} z^>q2Z0D(I}qWwon7Ggk`qN$;Zo`0D@^2Ure&8!OEtu$-D?te@p;R{_o>u@p2xQyh( z5e$^dI{9uhE6_Nd6(?7Klq~Gn?7E-C*7uJ%C&%kND5baxwJcy*`cLSV3+1sL`~tqP z+00KM*QIzLrhvTsBd`3T7r(W#!_;L*lN&j+m zjU|9S(hQkD=azOk)R1;SC-$Gkg|3;HxM#@B)xwnr0b1$;m*-Dl ze}O{x-3H%3Tr7dP>rGF+lC?lA^?qs8-ETe<*?6;d!npw+s3V_mUgfqOPlvlC241d| z%x%*yE{aUN@V& zIS_hKy|lYWV{>sz4MIMVB1m`DpJhS?nc?|n_vL^7dFMnQI^`pXuhDntzYpoA%m{}k z=i(&^HN&L)L=|QVSLp9qxyp)gq8jHC=;A0!-@`p_Sg#;rsU!;KL1PN$p5%^W(&fR5 zMf?r-%-p^A<&h9Y{u@Ox@VoUvBrQr_yFbN`^0?i=Qxc_RuyzqmzrApICw%)wBkA^P zy4~G2%+n~cOm}YpA@%m_B!-s}_AIT%Tf)@M;>p(D-Ty>BmK=$H2P&Kh_mW;l*9=95 zPW%xAyxWeV_(zt|JiPz+M|;KVjV>f)-X`wd`)t>HmkLo^fh>wA)q`nma}`WeO;;AP z%HD)}VC{bofzRV~SK!iHg6_9$Ul~hS4`M#QmpPuE*H2`nozH)n_QR3PuE-%VvgZz* z=?ZaVqNVe&$ZnatrLwVl<>{z$M#!GQ$4;Vt-Mn;-TN@%0Aa^lMQq<>Y^lZ3ESjLfO zYeZafR4C(QRAM)-DvLIws`V--teVQJh^_t2;r2p9PHeAL?oTcBxYXPd^;1OQMmse@ zt~~IiM=?Y&A_4zkr$tUAaj?mnR%cCHlaY;a8DiQ@gN$eq`)>ER3x2Bak;S0pW~g|L zT(i^G|D7w)-}0WEZf0I(y1J;1*E|z(f=fe zU=S!=&i%``?&YO}y^uDipqQ4QDpkxEbWj;E_hS`&f7y01yu~U5t?ROv(36YLZK27% zfuarJnp9H+(u*mIj#ago=4%oPa*>6e*%zrEhC6-~xP04Wx6a$`Z8!^IA`Esg3UEbG z`(WjusgGZrBZM_dLmPyaLo!nt;d$;dwTX*P%3 zxb$9jK$M@uA`PGA4IZ9XSHT&D0A)lf-5v%Ovxwwn(XC(H|gw&><^T6vDX_wtj zP%%rlk0pL(lKL3bAE7xn(69;<)Q(TgTC%|}M>Ro#o?p2^JqT*zFX>+~@R4-qGtnhw zp{54*FhN7U+ohZe=8%dTGwEoZ8Hgq#YM?Gc3?gugfz%%D1-b{bP?Y=^lK{oLkd}HDLx^A>#}r&G}b3jSo*(XE!IN&2)oW zk;CE*sO|eR7rI;gCE;B?#iDZj6>o&VHh!xWJ4fqLXLYwN6^XPRux0&8g)15Top?rm zl)?#Y<=h{5f?!>vp7k{m822>9_ZN4tmbK-r=kvRxMnyGo$D8?jg&aM+o=!2255Zke zn6}HPl`;00DB#}O{?ZC&Gcuk9h|kMbu_fsFdXj`3|4X$P(KN({L{4ACc6qR*HGrw% zkYEm69c8aCG0Ha}uOk6Ir-Xlm#@jYBe2Zs%7p+y|<4Q4rt|dIQr*#4kKLq7`Z5p3o z;0d}H`wW%ylgdXEtqQj{ddUMv@#JQT(@q>|I2pO<#xv3h1Z`}4Rni24?T1OM?uEnf zN}NSZC2CAI&(033UT(Lqb1uHEeQNgcm6HZ9UoZ3#_dWO0{DBnNc@>6k-c@~?1lcc} zo>rnxxT%R__ENM<7|2XRqGh0RD#WisE0}GhI2$U6x9px@fk#J3L4~d(+gyvw5Tk~W z*@tzL=iMQRs~{tY-Hir|rXqD2eA+L-^Q&IA3u}UWQQ_DAH_i=jYZK_5r-YoIVg#ov z5!?`~A|pbrW;L+y-YOlS6Xj<1eZdpYA=r330};#B%Sb>+<}uVrnIuQekM?TNY*TVT zwk;|Tu>FWgX09BD4j+IKsyTdy&XSOM-7<(qtQXl89_KQU%^%pvK;4_hzaf`Feka!F^u^n5Q4&*jEqit9&U&dF*Mr&K8@Fp`vF{OM3DGwD{?9krI|4}9AlxZ)?!#G{?t zd!iW##NE>g;cw^DAHU;nwMQBNk`!drV8?M&N#~7VASgl`@!N?vd~Em<%r9JE4++oTI2D!Pat?= z)+vTDpf8VbcF8iQNwn-{=~FI=D~e({Zej1%0QBGCg1Y*mimTLjqCC8T0LifNzxj#G z8XEQFSu_XbHD8_~uwOceJB(J?;8Bs=rN_b0kP2N#qYjpa9CxX)vzO)!$-jSAXuTPP zxE3{P6|qWICgW_-D-Q&@mLMVwDqd8)p))sU&isV}e>FA|Y*p%O_Kpf~p*aV`A=$=r zt9feid=Pic{R5VhneOj){TM@2zb(G;XmGxc{=qR%D*%qJ7dM|Gh0vt!TcIj_;$rKi zb~5Tsj6ouNzV_Fq=4k7$04`o$qT9$ODY5Cbp2hf^9Dzg8%t%{pgOIWGG+y)-gg9)? zll`xVV5fR>s*>>Zm?GsXjoM)BB?yZ!w#cVN`hv=oW`M8`QArg8zu#IjBlj+ zgQjVWa9mV@n!C`c$|M7IZm*=lXO^k{;&0f0syrUl5lk-A+s}BwaP!}Zr=ApD8kDJg zzTTl1OO~q->#(ggy_qbmCv1ZvngYhLnasghQ-Nl?eBO7NW@Sy^epOp}HlwVX9N9@# z6l^6>K=zN(0|5JCFNQv~tr7H(Kuku65Ll(iUa>j;84~1QxPXZFYN%p|PbQ%*xW=yd zhD;E+c^8$I^sT`BZ6%}p zvtjJ|##q^m$fN4NPj@QKL=qWqT-x~~ZdjiorrjDG;rY$aqi>gedp3U6N8LIX`-Y2R z;mZr5l(ltI^hk@gIQsPRoLR^JGXh24EF$EPJlX|{QWe<=^6MjHm%ipi3!)!(={7@4 z{wfE{1ZBXQ_zo1uqRUkOJ}3z)5@G4|fB4$^+{OB&9U6?O8nRq%gCc~6g_E-|_y{av zS;qHFiH%`QQPU01w#&xZ&|m!eLpY{>aKZ=sZ04f&o8uCyrf(cF&;d6~y)cr=tOa}l zE(pz|KGc0AgtbVllGI93bb#N6ngn#St-|@fCJIX<*6J^0M}g~ocO|;cYb*4PH~4t* zzgUHbbeLE^Bv!uX>oyCXhE!OBE=;7F{*XOLAUp|G_|m3GyDH$C2Ea>a9Npbhh|ukg zxYsXJ+|U%6_tZZHo*!P7{Is@}%Pc3D^Y@DwUFE^ATKzyfkmjA9^&l)})C);LX1QTTU1)n52iJMoLAPvV^nv_#FJBs^~>JTvdn-1Aw9e2g4EW%6UNskreN@S;cg$_Yg$utch`WE;pylx_HJ)V@ zoYSyg^P60jd@KTPy(LApl~H^$k}Og8+bGM2`d(c@FQ3DAytqQRD1WKF%&xpA$1V>| zXdNXh%o3XvK#t3O@ACXl1UVoS!xh_id=HH+1h8hkIam(09b=k4t{mWzT{e zr15J75k@(-Mzgb_;8D5j`H24(;OBC?nF>@jYCW7N$9+WE7(5IrsPAqq~*diCxJg>L1s$thHhj&nu`Jy&wD^4oFpq z@t)nkOD2B*Xd4*xGPPzHnkVa{{Fs+U;=rTt@gtGqlffd8zu}2drVsMdF;ID4AkGb8sxt@+kufP+QQNUHG;mq zbUcX;0rlr3t>SxR;7>#1LPtM!xpj^$0X&1Hd|k;v;%i$*)& zv`j~Fx2r#Uh0PPwF~1}H|MF}s5W^aZ*m|4zx$ z)sBy}iBWT?vm1O2A5+O2H5EPkZTt9O9$kgs7c3&D&3g8hO~=}7hDfyuJE!H}AGlrE z#TM4mB`TQv_2O$?pk0AYRCuLw_irWY)Zq_rz*uugI#vgob_@D!=o0psHHq&RHv!jF zzO7yPo}VKyJ;|G;>VXbw-Of;{%#;EgE$K^87TcJ9aqb_}YuU4XtphR=-EhR$GP23GKED{+b$q2XN~dt} zR40T&LV?tT%3|ACFR(+E!^W8i@<*08RY8jc`=kKZ9iVn=&t2~>l;=#GaWj9OTPm53 z6ZiqUdTuZZM@2In3lsNvxT`XhD@pr)5zB$dLXETinYEaTP#y^egsh=c=$XSrh@XQD zr2Tv8hP?b!E>Bs;ijN;YtbHcR>tBCW?Dt7aU6=s3qC8pA@#H3UpZbYS05KamC9Obx zg(2xPdc2wsZwi1GV`EL4MX*}@!8vxK+$DB z8vS&^X}Pzj>yI}yrA4pKj2TBr0G_kWNmn>CnT5Kp>y$q}G|^Vr`(n&Ry}_;67P%vI zb%5dW?CjK@wY|+$C3NS3ExC~Y>6d2b#(CQfDM)y?7$mG)GUIr2_I&d^5%o1=1W9dD zOQ(%2V)guCv(}(X&Ub8I`s>1xC8Yy*Gx%ACme1O!df&}5I?qrkBBUtKNRM5=xx`Qst=&%X$mRY??KXkzeSbjEyX~Ka zFpEOyT*sa-==qvXPJ4$3-cSqz7@o_~+k0;(`3hBB*d0o`kk z7f;8>z!&hPM)&g9^Q*Uksy-IIE6F=`>=hudSZ4e)kBM%6=hK za35v6c&{?S@$sqnGMc#@3!aKqV?V(475u|eA#?$5o*FS$1B#Q3Ev`+3MJG^UCCM>A zm?y9tUq#Cls2%YaeH!uprK5r7F1w9iGRkE*q&Vl`I_M)|mGdD$xJj-5PstfhZV`Q*tu5k;gygnys%QW8Doj%cH~V2DjZT9fR01K8Mx;Q zky5;L)#*Ys2*M2gx0_@Eg7pawgK7BXXoMiVKO_Zp?-uKBXh{7Lf5x%{FOISoH8m4s zDi<3){H`yQKnN$tU!2FO5-~{-mDB|fH2LeT5I4g%`0!QO;FSO6tT>P|fKC*ndl0QP+mD#CBG_jC&(9AV&bdbt=z z!iz;d6}0TdU+&HCBN9Sbr1&p@xJb-{N;0*W`Z#}48>J^8{U^VLE-u`@izv_!V7V(k zKoOz5;4A&avGC*O53ttV)|*+>XzQ4d6q=Dn{2T^Katllr*i@2U54$fG4r1V&ho`E= z-G(vy1Au;k!HBE^I=rUnWo6mJQpQQb=W-l&OhDF;xVOaEhSzmOisg6fj#82pRV%Yy zJ_EMxd1?ce!N19t3x>0fvsNQRY0`7qa(JahngzF`fepca1Fma6^BjiM3+80*CSVE&S$+m(~ zjTi5llq8?JRFvb%pjuC-)7|N-Elf2^vN~TMk2eHh*9EgdmC}fXe#6^nu)-AXdzzs6 zZwU36CmX9hbza8U*a!eYF0hZc%M@*^C0Ew=6L@vIDTtuq|Vi+P{MHQ^& zc;_{PYn0O#nG^We%wG@OA7=J#Uvdvz&Ae>$x>p{zILQ~{v{fBDJ_;%SW+syHWt6}Z zF*>!UO4{yYXIMq3@A+g2I8i&eybpo5cG3_x#@6|=;kHlA-wwRSOXuBYdQk|jPnBs2 z4*SV=E?1B`3uh;#=-EZxQBOUrNa{kj7tLaJ{ITAufXm^CAcvWEo3Iw~{C9X*-%fl` zf@f_sN=+5UWj+H6!ulGVPk7PD|KID{ezuFeK1=vnqhpS1dhbg#EqPq{iD7X!o-W;Q zzyPoDdk%Nq4+MG#6OVrf@2p=_ z$0aTCtrjs(7TZ71!>4DzeKys=Ep;}?bsECv-C%|IPxJhE0rNbLY*{W9!=XH=1ybEsva2I@j{KmgFCDfYMWy1**mORO@=!kM>`>(>cp* z%rIGcd4z2qOaXvUr;VaTQBBpwY)%u=ONl&}IW_)~SmtZs~7g5&BXRK91l0WTceT8Of>QuFFw7}F3YhnFBBU9;+sD|dHR8w+d}IfBqoX+ss7la$m|)w2g71XGu!jHhDHE}fv# z*V9m7QsN%QzyNaZ?nV*Yb`e^v>x{2ZqpMPslFbvU?H7?dX?4-9qmHyFae*1ggqnog znduhS#bE%`LBhnpZ>-Z6l<*&eNV14=k=||Dvo*JTH;BLR!K8hGxW+K&$rc1!gEWc) zas2_2rm;62X~CsRt%Vt$a{PV)7JTu9+)^&7Xxx~B7`!6E;{7CD3L)>|-;y8m04)n6 zg(n250&Z(xxGI(1Ke!T%j-S?R(8qK(E$@&>Orw8N=y|-$EtIE(m(I~}Qcej{P)n`* zcJ<0Dh_OnXILqfA-a#4Lk-4|lQMbF2!q{AsXun}bi#rbq{U66<3(Df>;B(vHMiroQjdUDk;>yu))nYE186<2rW~a!YFDm0+ZP?n@xz zVx8a))Le_~bfVO_@4acPID}fZWun1L5-V+x-`{xuS9^o~?>+@$hc1wPlSZ%KzkzZy zJd~4n6QAg9lj7)7Xa5xvt@{B~Z9yEm)Gs%AdoCaD3URXq;G zQshS~A1w}TIH~TqHZi{1^b0=UZTfSYM(p!>+8QuD6UqLV)dO9i97d$ zTiU;;fN3PNkQ-79!vULk9b0u@)0BzoFOPWSm|lCS!CpYu+txfTR{lF`{QRiuz_;D* zqcX4Y!td*NEXVbuWc=T|9(^pXvV30kpi_guI|Ni56AG%*wT?E3@!7PiYV>)wRZ#$- zO#HnZUirEaw&fyq>kjmTkmoV+3fg<#<&zyC?#M{QarD+1+NmgbWu_{3FhmL71o@41 zahUov)67v3TQ#k=N@_Z=2Qy#|xO!6gfqkE_xY9Fql(*{E+BQxAHFJTgn?MP;poyc{v)gm#$$HS)%BRkNC zdaD^p%HbG0O@DwF*aXKCl>VwW3lZ;W+62il)`s4Qy@Jb=PO6F}VH=>23EY@Y6BCzm zP%Os=pr>d~D3Cz0qU}-M)naF2nnp{NtAUzLC>?&nCM_l6UZ1j0$IY-zv zwU%sB>t#2;ujJ4AW$>)2?~vM+*t+BTyr~hcqVe-N{~Tro&=>8nn0k7W`-ni;OBzl$ z#}cPRHE;FHmi@yIFQ{dZ=W}2}zx|c@W&T?;Nage-{Wq*}T)lA5XD0g&{i#ynkP&#m zk=5}>*1(6NRTYN=P}j`!$!){WL?f{)6h+;`d{~15P2g4d#mo50#nE8<0rlV4pM^J- zHy%}C(%Q4I>bN>f+kiUve*_*|BXwjR7#?3rn=-JcG&&G1myA|rlrhZsUa((iLa3Ks z4o=UBnK5BfK^kci3$I`|UY7ec=syC)>CoYZAAL|Qz=p?fDR_EfwF$PP(m#&R8>w68 zg#C=&*h^tkb6Vg2=ffKsmC*(sZ}xKN(BXHtwx`QCGwaKF<{?Z6p%2;ac=q8ZR!%~~ z1rQMik@-$$h&Jh@wH+03LG+aQrr|9&122YcQzP)WUm3KG9^dmkMjghY&Yu6N$_~c2 zsYad5yLA=|DJ@veh3_@sThI4l2d*RsI&SeQ%EKX22me zrGeUXyeq(BF_Zp_2&)lwSX%5`l`=EHvAg;YY~MbtRH62H8Pu1MC?l?N^8Zeez-N39 z*%2LI_WAM|I)qL0*FlE-l0hS2Th{NQ_+Ue;F>vm7b(yxZD zPtOo>ywT0i^0KV+##>)A>d!uLkXmv_%#!h%9X!Oku{!O#3KFj@@^O~vWU^(@DVqu| zGu!6h*_6%sx!m>kASsOZSYE+~O?-|9-1^B=886mWR*E5KXJg`ndz`u>BA)Dv`?@qFm=!$_y5tfG%;}?ieH~=#>QT$!_x3^iZ+WBwW z`;WTLaUZ6Yoq#WgVWDdi_`B#)0hqXdezrHkP0NmU)V(@N-oY4*x%Xu6ztrLi8?=nNOc(3D|HD?|N^FU~1>d2@T9zu`Zb0QYk6>_>xk1bF=3ZT=;4ArCJkeJpA(dj!zINRrvHY zIiC9T%&3!W%DvaxUY}UyRzf@_O_z z>Dgv-6i^vHmg%)`)x4|3R=vsXh!WT)913sChVt zv*Vn%NtZUB%Jk66ng0BPbSjz#<(BU|(XBHKH_%5vzlJs~X+R4fN^VH)7J0j8O+yXX zK};n#LX>quQzzWh z8i4AX`_YYj4w*{T-H4xs|25=2o}}#bJWJ4ub6azTkKjV zy@X>XAcHNKDhoNkH>Pa5YHsHQqg8RP9vsOx(E#La8;5(~A8SNfYW`{MKpP2AxGQ}6 zjIVjJ_Ln$Z^nHZ9A6NH;e(uVm3{yqNO@_T#?oz@bu%-A@Mo?rzj7y^<1@KB(grpOi z!g#~2@Zw_ISzzP?Z&U>QYkHx$WxkcL^1FDFl|V}5!|%a_Gh&HwEIC9`!4yNZl5HBS z8vfhzjme-;Fd3aZe=MVviY4-&b7I(`NhbSur&m%U`<>!}J$ozqT>hIP5J}ZL!fhN`Htx|y7ngr|e4NXeX_`0(>EGtouG$FfG5~jCbhlke5 z1zhw>)?V^xRzC*ayl(d98c5#33(3BNom5YS5y_Bb{6Kr&>@~lEU_#v84`lfA{1q4( zs39ZxJA!Gm(gW06{PbE8#&HjC2KBH~BMaG~L4`+y{1;93ZQ^Y{hkV-}HbkkA12GW~ zgSKx|hVTRWB<&^Q^y*Y5q@eh&HGp_<;8E6LeRr}j%d=NFC8jm*bW4RZ7lBvOU9oR? zD>AhAaVfY<^e8owhL4GZBVO+Xrne4IKb3=+aV(709pj*7BFIScy>~SH4o`g+Ye$O< zR{@$HR;n5>So=;}*Gmj-_*?e8RJWFeS~QvLAhw_(Ww3e~ne`{b0nTx(RB41_>DD8;{rht`-GKT zeUFQ%^KiOYUbJ;sxj zP%*9{ucq*YNh%yssEGmGpI%Sq30f2q*`4tKji_$5i4QpvuA@>5lq$(gQXwX!BvOn{ z)EIo>0Utr)Z9q&r-9bkHYgP;-u&jpJ@5$_lMz=ncfm^`hbYq}kd#p5sjk@Cf-bSo& z?!UOr``~@Q>4Wk)*%7axt`u6mHrbA>YSFiGo-q)-T41t*lySN`R5ACuh>boG;5 zu;Af;?-}92C@G(f$2vy3&+h34UYH2RV>mgXh*`g1?)&idnZ7?W$~sIDeZLYJa5PvL z!s(B1Q|bGMDU+|1LFG@+@`;wiI4n>z7yBhnD1C!vOTgeob4eM?0>j)I37vt4?fQNc z17cO5sy&FbE;G@QcYN^`hTww$!=PA}2PI@|S^X}8Ce>g;0-`=|~)Ru*TAzMjoG-Lw2 zOaM4?L<;TC#~GoRlI&V?*P?_sae^fq#2%}gVgzU-@upG#PMq13*_^KyVZa{WD}%!# zMpEoE-1y`#)IfZ~YFwFB7#&5!Ll|2A0b;!=yHPdX~JegU8X8PnG7tGr!) z>=fs#uhnTG3aqU^pwhgY7p`0gUi)@PHjbEqh!oW~i*E!630v>=ohHbw#n4Y@_);T z)Mq}v>~)sLLpg*tV%UuhR83pALPtu*kMkGc5WLmpkVmxyS&HB4NOH_TO2IYDA#)vk z^sMXzsYTmNM3LuO&t;Rl(QZ8^+i~&%K;fnfHkQfvCOoQVkPL;4JP@%<$Xda26F$Mv)M$`FXs{ zUm(#hNN5vCC5y@^K574h7n)P(`)y9K$0msU7d*^tz>(*K$k<`W0kLO(HC$Oo*~{FK z%Xt&wZq`qcPW$oXriVpEOHq`K#DJoLD|4VK)-(k646{peA}!=}!z$0$qav81%c~CM zNt^Wo*q}R8MzI2AvAbROw|;FaB%{23ul~G0{E0RP#Y6-~X*)Am(prLm6$2gs`}R=@ zG#etFr79iEZjY)&EZE!<3&4^S!D3AGkck1z1mt7K>4p;AUx=MivwI$0#C1kx$k)5Y zSc$4>w-_9qFNzV-({p?C9TjnNuI>d2?V)W(RfkefjN&76Z-ACL)~*C&c;4_Lgu7n) z?Z>zpJdzUk%imHvOu>nMi^Bf^;GP|KtMQHq| zr~R8PUgo&hYj{%JweOSlEwrKEqfj^E$nqv2~*aPfn?gvz8QGsGiJmdi|Um%&iprC803U&Zf z_2>1Zbs*PBOgFIT#E-SNg924) zey;nEQ*aeffa`!$`2cr^paUIOR3yeeIm()q3FGDF@#%h4J!AbNT1H;?_yusJ@4`X~ z)Oc}b;#_jM7@Vs$GP1YJh}-Oe11YMj-5$Th`ZhFW8!zUnSo)sEM397;o0G?^Y;?|U z6|trf2Y1Q%KlXs+mtwv2#l4rIlufE!h&{N|{`^XP1}9o)Tvb_d_#0tAa6DX|Nuu;&Z+x>E z^9w-jnz=l=O{`I-PV@YqU6JW#>+DU)1=$h=vut)2YV}i_GJWQ(?i1&LNgdpP77|co z=YUSWw}+fC2A(3p6FJ?`*C_SMn{9>g_h@xf3^7c%{4&4Ab4CLT-Q>(hCCaH`oOmnK z*>Q$A^D5W)Lrf={rJuN+vju;U70AdQXDs`?w|w%dF}?jR2hv_&zq#`hImkODz6SqC@AJ%3koTzyRoi860uA7Ab7QaZt$)kb950ZjIcRSPMmug+iy|j3lUjCr`uLtS zT`Rqj9>Z_2SRPLX$ZV2F_~0KG-SC}#M2IC!yXvs|K#M=5b$Di9_3B|aJYBv|T?Pu{ z<#XEa4!Vn^n2rq>fiNCc2KAd_a;VBdYq_wPiMR{fTH*LA6p`(*kBh_^Z9q=&N`s`? zRyA3~d?&wsVuz6xgCj?Tk(sN!F8(c2|T6A}%#H zXS-TJ%ICGEg zT^O!AVlr1iz1yz+ffUS$lUa8(yGQQkj%RiNuk&RxIyx_U4H^%h_9MEa@VgSe6!i3F zZQgVW2;;NIwGgDBpVI`~E>{O!f5UO26bCtu^ASX?qelf{#+Im8Newo61SxyF;hm-s z6ES!oY^`^jvQD!}@ytVZ*Dj^kKl`C-VhbZ?D zQ@83U87bYg!_4vE1t2CB1pg;uKj)(FZC6n(%I!}$ZtJT?z@?Q$c6NS^{@JS~G94EE z1X8+|LrjP$69kb7xV)qHldTD+5|z{Ji=&Y~EBcpYN=O&Up+C#^W|4 z*nuV>6JmWK;Kweq&`BW;lnK&M1a_H%{mJ{Qme#Z`m97wO8X&2{mK>RsDJe!Z+MnF8 zNBt?teR78|4LPPBec+yRb4!|^glOFlnYZ~&EG^gUT&i|Q$z||#YzJ`w{ZqCT-CT83 z)JB~E^|0Sl9x-{0^t<)LVz3W*p<0f{o+kk2gJ?C(wFEIul3I#4m_u*TKc!Z01x(sv z=6autN>lU4kj1R`{yM7k(J<%Wlk-WHO6s0#K~0KMiM*Vl4@Ax0zpi0ZpN!w*jCK6= zm%QCTUyyaOCou9by$dXlViIhvP=tWVAmU7KL;0+Urqq&UENu)d_^X$_E8Zg}>~QRhdXi3j(qCe(ubF8ytUFF5 z2;98F7U=G(gD)8gPXVq0$z>v$=r=z|HlVnMnqK`I%pqKA{)~g0FthUf^12E$_+?U5L6C9^ zUn(s@=vtn_%h^`3vXT`cQGt)qE6#VQPzbAM{)jbKejSXa`ra&_LjIA~di~jyixum4 z#ym;nhIJWiUfcZ8MT*2~;YQfcGf9&hbae=MByj=5RH76ny=T4b+x7A>GwxmhgOJro zzrOrszGX5sqq<&ylhlla*xzrq3TolwrL!AY5o*-!#tyGVtg=+W-@D|4jzi9%fq)}D zBH`|-4`;1hepRx>o$9yzH({^0SzKdw>8S~%y9qBbxT;op#Ah8Z_^jwu-QTv4r(NfK z{x&)528JBLP>`Nvg$8lB_nW(W;xFI-N{jHqf{Fp2&2Q>P(-2Hlg2=+S5a6JmsWKL4fU5;*<1gnNnF{v3WUYz$sOOGv0X9n2Xxfg?#Y-)7$jPrI2-{yznVzjP zZ+t;bL~^&I)Qy${&5Bt=6iZr3sun8)Wob3sG}hR?SR^2-@oRWUGKY{O?! zBH(9sZ^uPlpR9A`E8Ma2r_Pz@m(#JqgN@9z^(e<+rr5O~`l8N6b)E94U>STkcQ3<> zyo94dtl%Zc9mXIT;i~(@^o>O(;hgLet4zTm{`A_6rnlG8Jz+wU6$?y&CC&gFGTy}3 zMq8-QNQl&cr~Ey#!n}opOs5pN#66x|`1fh?N{~x)q|bU3qz!c=uaglslVxXriepJqXg7z1tQ}14%C%qrZg^9oN(z*w06X7<#Ie)^qGN#Z% z5${#~FBdc` zF}5C4;1pnhZB*qzx-z~6ednJ_YzvYnO$tD>6h}#$ePhTSDP=(XPen%g0GuyTYpLL> z`JnMg2v*zL=k?zoNwDK{Y{%0IPZPh3$T3MLB_r=ujzwW&U*ERl=zKT(d>VyqK}|AO zBU*+<7`QVeKrXblCI`i-vxIgtxT{&OPHUl7ne@~Y{(QG@{ixySF*(tzel{J<%VRS( zNut%Ciq4u}Shu;*bwH+VyXfDPQ&#C~w^>?VDalUmSb2|l75d)esWp2Y4Vd*;|Ffcj zxr`>Ge^FIdRn>`}m;f6!NThrZ*FskA+ima;DvW%#kqIJrg(<_G54FpN9E^8A0D!CK zz*+`o-}#})tbfVOpQ5l52I%^n-T9uqJ-^Qd{z|FI`6>vOdZAAURaytm@o9G;LupYNans$8yqKeu;vgz?Md#n9WoXBBPVZVXC zy&;SDEVMJEC8Yo1xB(K6=O|ow{a6R$MpD)OCmPflh%b0J%o4OvsI|k;t9AE!-Ay?G zQQmSFW9IjyzQaDaC);8PeOK9pwa!qCTf-VY@Ndn?Bk}D2yT*K@m^KT2g7`D=eP2#| zrdM^rDRXjf)bwrlavl9mr#!jpHrk~{Mm^_u-EEj;^lF(c@-=L;tmzsLkY+Jh*yn(L zJ)F{HHus>IHB2Fjbc5Vea6XpV97NCtPIX6$7c9`^)k#hyl5D%h__C za->VxWwHBeWO0)8Y*-g2Ro31ZEBUSqy+=FK&tbG!E2!}R=pI{@^(%=OG1h1{CbtLu zZvWb(P9PI}UQjDPg>8$-%!z1V$bU9o1SPZ^55mJ((f5jge|*=dL+l*dD3=p|MvsQQMK zk0bvY5r%Wjpybk5jQ<4ertVK9EJ1qlf4}j;k+`^?#gt8AFrtYAK90-vdv&E-+I_?7 z>`U*+{x7z;q;m9;j~GWUc#Ci}GCx4!)V|{7cKi7jIqxDBQl3E>lRc6Ad%46WcG=fx zHJmAm*hDKNGZ7{$oN&_FT(@!*owmQ}t@Tq7M;N;+j_&D;THu}@je6i&=q~o2xVP63 ze}&D~Rq;B=TS;KAHy6%zCeHLR3Tw0|NZxH)WLq;@6L#0NCKP+stwIUo{ZAnblN_D? zWgrhISvIWT?*nq3>4`WwM#vjtf&~QJ{%!9(Rkk6ShCg$Y=Md(6fI;@HuFQiJZSehL z@HJKaSKiu1-D(VXcN;B@t_8cR4O>M+%m$<*H-c`A55{9=lZXOB8 z#7>0;m7%r1Ak%Hf3D|cbqax|=BFB=d?RCx#G$14dh}ti;WkGIqsd`AuE=U)qmR}RgG*<=4yQ~d<=MS1 zZqskxX?|uaux#MP2Jx8$d_YdkpfCaPS?hT|nj&)`DLjWOav9IwL4*;D?R1`hSvz%a^5@^K6D(lX;S*S}@RJH3 zaW~!L3rADA)l0o`)M-P+o!}h@nIJcd2)j_{!8vF}1m>G4|7$GvP$Og$T9%h{+|vft zCyAo?{Vr_W$b3OG9iHx&la(wJ46*EX6&V5Y8M<`EWLRa%xGFhJqhy*_JT_^I&n5 zMkJU#%jbpxX$dZ?-|UGN$5bfn^Or_b7Ki074<+pseKqo)eW{lA*L~j--^FI(B%%$c zd+vwq5+GvGV!z^Z>TtJbYrXaM3!9=A+(k!5pkZR;;9y~4V41!*XKRc zJ}sMwps5^-P8ZfsgR$CVV7`Ra39|aBShE-kYE~7=Wyuz=hwEPpgHHH#>RK9==M*SN zA@YocxMY%k&>9M^h|(x*(CPCWBGXA4RMoj61>%FS6&3)%tWf&zex=)#xZ!OaJx!6}1ufk1Cie^Q<@+d*IL`EDQS}0+PAIz~*svWOt(zv=Bj;9*w zJ<)B%1v&-;f9SL`W?Ku*B7zQ~C$elzSV~c7JjH0Syh5x#&yTP^`ndQh7#2;bFyu{P zaCVBS4zY1m+4PlkFcd zsM#`mE&E298~U@YAG-~*SKb!(6R8&~)$dt8L|z|=I4MO#^L4((dbT)yT8R4&i<$ur z+>nQ}=yClR`7HzIa^tZ`ph6x%L7IW6P|;pA3u@=|_{!?({#V@)!)utWkzt3E6P z+A-R%w0cQbF_0^E<7)|h2Uv2yE^JwT76>fP)S%*nzkadAW@kVP_|NHvq{@MknfV)) zoPQ+A4v933bgN|EPZd_M)lUJJihz#K6FYmWT3>nwHLKCwb@UAfTfFw}DpPpjD+llY zRpT{Pbr+z;qdVGlYr*WKqYDpi_+IIm<8!vx8M#;LH3X5YtO2eKBG zu&QCYp*GYs(Ll=G#%&`Hnr4nY9)5KXDrTU4&*ZXv_+{KzW~l`|b!~0t{NXa9l~mQ8 zXn$qF7kFM->4c28Z^icZe7$?%lcvLwE^c>uUz4YRLm|S)L@IKmv)PxY6zaxTB7YH{x>L4u<(c&r5R4~&onfi~@0*m9>S#FO-kAjh!^@4yKY98|?MnzU}_8EXVL_H9dIuxu`md7Rg@cgox`CC-pm<9eZS?-he`)2I?n2{F;aAj6; zlWp4S(MnVHs4h+v-aIGb;^F=J=r-4OmfDOleclJl?ORu}c;e1&HxSkJ<5DF5O2?xh z!lx#GM@dOZh{V=Zuh7@Kr6r6}IV5gj-|_pV93d+}q!%VNl>s`CXz<=;hQKfl|%-x8-OxOYb*< z-Dk^??BHK8>!%mxa}M&z;^SqmJJ8-p!s~eb^h95vQojzoGepFuU1`v4J#$!pBxq~U zVy5ybcTx$csI+-bw6{9LY);D7q9rvapzZs1e!qM0qOU!YC1EdTKj#0MVr6AjQk{P{ zPR-}K9ei-5V=wG@JDU*8I1^1(S{LB%g|I|5&R%CbB?C1b$qtz=zG0#$c&+?2QBOl- zB--FJv;4CM^?Tv_euacODxjOLj5PYjLMF0^C0+W-!@1^y6nTFNmLt0He=FW(6$B6+ zhdDwx4}?)Mho-9WVWC%>G&$cx7c9M=+z;m-3sbZhk6B?-M{hzy>ZlaINSj50Hi^ zP@(ezv2YaV_wUu9u=jJA!HQ`J2mbU%y2>3GMcYT?mco%j`-*v)23NpR@Ud9CpRZTJdx)KHo$?n3guM zsqC5Zl0Cy7$LC!tRmS*UjETi&I-O%L)A#NY$}S{7ZL12U?g20gIP@`kkD_8)W=0;~ zxwrgXOe-C3_w#%^wCV~HeGsfNYWZ<9R9~i3^|j^4%kJp&bD82CfIE|hjHdm^>Vvs` zX#x*fz}>;(W$qp9p%JO^Tlz~BxjUZu(G7#gt^fEzRl9Pn{cU?k6u3t6R%)Yhr1vp7 zi8isNrRM(jmgPVj0BZUYsL}FZo>PcO(5K&fa%fq>#pUgGHdSMIL_D44PwHVche0yo|8Q@xF_qe*Tz&4m{pPQTk-EN<^rjzC| z5yMKMdYHs~_^cCf27QDU8p0YO^)rW!kPhv6%%jeEVleeZ@!fBH+_^{%Xd^e=bE4l;&wvOd-sdArJy^5Mth%U2_z$lPfP-G0O&=e0Qr zII8mFn_#kvCPZAnJ7e~%9bY_xs3Uq8`BNMemvjV5z`9N8D5`!jbd&cyh&@}2h`_+? zpbrU=m+xC^_i6HS{PZJ#GQb6CV2?xmhMYr^$eEMePYx$R>bsHt>&tLj(VhKQVfVxR zjhCm}la&^Sx17A9aieCpw}Uh;#*THf?6#XGT`7vu746sK`|f3-VI~gJ`kJ6!nh+ed z%RGEnM#13z;DO|Z(9(Dy7mb0Wi^p8(pUB;@beXYRr>*&gcJS3{9?7~b7kh=(XGz7* zv#QSK2O))QfT-Y7E&wyh&1^QR4Ob;!0*;dxyPwYSof~9YC(+yI?OEe>=Bg;RZq0G6 z3l?Dc+U|KaSQfiBR7wGA$&O+AEF^bxR{rAQd9rUdOcRCzD66wdVWyi0A9wucG%D-Q zMzVzF7Gf(=!l!C4i-ctUADbN3soHg)A{+DxLlup}5DyIM7Roe1GY^eK8B|?#Mr$Q} z-Fys!r6N-Q>ybd1)UrKYrV_0Y>Ggk~MvO{M{2M`Iv?lK7cr?^Zpm=+>Q1A29V7K(+ zc-iZG{~&ViOw9MCq^bOGO@-=`X@NY+$RQeq9bN-9#S&yJdq1j1_14hmtL9)kBP~L& zwZFx~Zm9!|X_u^ax`?{yajBh(e_~n>&rMFAZ@Rzl&4-Kx$PRomYQ31)88BjPa+n{+ z!M{9P135b^HJM>vvBSljzw+YNbq&OQ&4TwJ-}=#PV3dUUNpGa z9og*a=a?TmpZ=NVKp0W(HmJDhY}qEo=mrtce0$V*-sxGI(x6r+ukDim5OCZL9>y79dah#NDUAGNW z9L|zI`zkil;hZSBbVn%$KHRKdLave|PMYDXXW44qURxvhj6lXhHwn>(xsjI9l}Q^m zkJD79Ea(R(Oi2TG{QFjB78W)2HE!F^pB!@sKQg9#9XDHXUg@3M1?e9)vONfz*^I`j<2lei3rQ>aVV{5N!)rXUnrOMvYY9P!Az3)4mj@8&8%*tJ0 zB3Z#uz83yH?9BTS*-k%jBsodl)m_m=YcQ;+ag2Jo!TchcdyI#0+njl_PHihnWdn<9}@lJJ8KS^y3+AQnuHuBNRH@oOx5 zz3Thk%#>FPiy7@wgAJ@V05rlRfnZ!IyQS4BO`|VbdKwp)#l_{76%N)`NBgdxcw;X- z=?Oh6E6-b9?F$Q&UjYh~N+R5<1;9fz`J>Vt)Un%!-M=aawtm;^66foa1VT%aM1Ov3 z%?UQT*RuC|j0CZfIZZh{=-P>gwkL#ND=%%-YG>u}u=~-2FfmP=3|BHvDgfB^GuMLU z{%h5R?KRCrS-<%-Olr^~5ACZwe83OYaz`}lSW2XHmg2KL^#8^1pNn8Q0j;*39%qx{ zJGf~`Z(%e=`!1M{4RW5$+r&LKm@VOot!{bgs2I?ATinV=?{vTiD}C^!jtI<(msxW%0Y9>y7FL!;VenN z=X*Vn_E!zVrHNo2#)v%(9P}pf&ZEbm*}vAy;Nz%&$*nK5d)7SE^8T|VJn`@?4FwIY z_dRu9bK_K!)h@%2{AJpex;5TALo-JL4r^Z9BSgoh>{T|54Lks)Ie+a+7~yjgRu_pn zK>Axx73OCKJOnCy$Uvn}>SQ=YnSjVf*@5=Q)X3W&yPvE}GJ*(3g~_JFD&Z65;O&l1 zPb)vE=(xKJHcQJQ=(>13J#7IVMNnBXvpKjVy!UaeP$z26j2h=d(GJCFm6Etryr7@{-EA`Za|Cl|JsCR;)Lf;aSi={xO(fbwt}wR8+UgP zPFt){thj3_?k>fhQk>vW+zJ#Z?k>TLYoTaxcXxNb-RFIu>zwoPXP7IqcCvS7X07}F zEnPdI{zsdOR5$Z0glr;g?+k519C{em!V@+th@GRYqphv2~WkAo1_SZc%WODxeZv0-!6P8=fgsG6I?mWYJz+o~+VChxx{3ncbUwvjIOYv`( z-FRCamPt#EB8nMo=`E0P>@?*5wKFkT^N-Cpjq&#yR8tD`v^W9QKzltbm{6Puba+$j=QD<>RSC!6{ca`e&1LT8Lk9M+9g2m-?Y?|=vsbxgB(QJr$F+R!$bsBx9M^VeB%Z;+A>~v>p+;Pp)#%D zumHOW-CmCEjZ(TZ2hqAnAhm0hWx!pVwY{zk0~kk(G%oVl0=`BV>BTzlM5w7lUB>Zn z?o9pl(&*k%GrIR}vn!^R$zd{uxOyRO$r8P0vm(m({}W4^&1osE*KTGQ29U08fFK2c z&H^VD-eC`L-|zeCBFHGQ2s}r`WR7=!8Ti-+haM6MR+fjY7$~;Jv;)*=J}Vb>j`JB5 z{i!cN@jwFK#8pj=ZhUHLY7)&Ls`$$fQxAj>orq4{KFuu_P16p#d$ymm8cu7xat&9a zv(5EUYI_J4yzC0CowvqQ-H2DU6re^l3mFxGA>Dl<h8y013;`F(y6T@kr)Nm1U_6awu^=_`NmMGd(`I2{Lby?%_9#EE&#?LT{ z6o?+4>#h_^0T*T-hS5N^NsRP+unWOXNb?%%1!K1WMR%cnr^~{=SQH*qa__XA5X)EMw)`#bF-=|^D(Ho#@ zr37R__B7E)2Grb_nN@(I0|we4>`cTWA{gTgRz#b=7f%~Qczjvs`60OP)Cn)}F?!iY z??FB8i=dP{P+SEB$mP}omytlnA@w`KyLa!ZNj5~^PH8Rus?Vx&%*=4OD^1{t$xaP$ zy%AxFPjkXR+kp3gkXi8}Cdp{uH`NV;j1hs|g9_JpOfL zWBt0HC{(N3BXda>$DHsz6UQ|8U5K+FhKkfRR=cV4=UN#}n>#B*co!+o34C!$WkuWi zEFwo?U{6iu^CZ!7uN_uJl9YH9|AS08ZPs5u{?j|92t$RR1AElv+0~=ZB{2 zpUiIpCZ3DzJFW{x%@6G2FuQ$2|IfJK*4Fx`zLM>pHX8wH0OiQ~Y?E1V39XygP5wU+;!RTM0jaV)D=#c(?e5g$zOa7l(^xqHzSe#1hE^2u)#G+nyd#wt~Y)Kc* z9qoVbiQx`-5-#x-!WbSbN=5NU^G9a4Z_MJ5BVYYpbP23D$hy)U?`8SKH;_gI?y7^^ zucISLHgSxqwZZN(*N?;GO!=PkEVwvD4c0WgYES&+bby@bHuc(v9EKr59vLVrFANID z>}5#p3Qzr!AcuV$!TYAq>Tm&h+OGGbVyF$QrZd}?ySr@BzI3qW#cC#B-DDae|a zze62pjQ9$cc-B*J(0l5J>BA-Y$>BL1&0cQ>;}uz5E{RpX)-ikF$bUofF2t4cmtRF@ z;^yho^YX7B>@i~?t7lfROMX?oms}1DD22Nsq_{t9d2UEh!#`y~&JScXv?_3l ze^Mog0Djxb4wNiEu$JC-YvkcTk#1I~N(dd!Vl2qVF`>VJxtRmIN?FwvN31#!{5q8t z9eyDX?Z*E)8NOYVlb7(_=+ zYyT8FeEP0Aent?Ou_OQGq_Y1M8=G}7x>48?5ipLkj_3!7(o^6frp9;De0?0((%5u2ZI^@2kO8e6FGxa#aY87%>1U(Jp#(K#Hojwfl@kWpB;bM7?uW zS&ZPWHPY%je6dl^OV#7c;L@1^G>%rcXeRu$34nHsa8<(TxML z|0j?%Wp^3hiDzF|q&_e2M9plpJ>MTghO^0`K|o}-Qqw*E3C%*hwd`C^cm-%V)inWM z=-6@JEPZ7dfb>7}b1|2avNX50k}kZaM1Mm2(C*VW{xMfpqykm`J-~!?lq84|DkKA1 zcH~^AUP1Y3k^Tz2^@W#xo9^hO<4v^vF%;=@OW>?3mt3)Oi)Ppz41SQue_yRhnmKjn z@rHU~@X<~7S9IJp)Q$|my6B?{p`TGmz%vD$N1>u;pq#E){zb#=HCIOsxNtM$y z!kxX;NV!geQ!D#F|5-`bQ4|>+fpa37&ZEOihj7aA!L#`N@9j`JS+nWW7=-im! zL0s;u%eh{Y8fBxYL_hy`>P)HT)4ucIq0ul>oJe>T!O9#ANg@tB^3;SL`$@VfDhHk?)2 z=Q866-4(F{2*e{;6b#g~zDEm}Rrk`cjCedgGgJQ!k!lalcJFFP<+yeq77M7{uwXN#G?;va}4#=uh$wWn#XPaTzrX+-pw|M@67EA616w^?pi zgg*mArMY1f!XdLu4N8Pk-d*5qLm;N z^^qh&8dO$1Tozi!#Y_3%{@P|b!KAOwu$cjK;>i|A_9-u9P#F;igv$c+j}>X5bRQGf zYcl+~?WurLtzrn-gf@Cz^?&A*)~^sjzZv@*admKyoJPi3cL_f=L_^F1?6o~2ntLfRPU zl}ro;>4`#I;hJ_i>XH5%(my^q1#SpmkD7D^-~AQSYkTsBY4IgqXTjRP;6GKs@&heb z7e3N0w(?rP9an0NnTPCDlTdEy2?OFYxOAdAj&hOzoc2CgQ2FV6Pb0#QyQ?mElh-IH zj%QPoxdd}F6=@yL^ZgAFsLCjj_VHJFrl3pR)VQ9exo=rsH$&Jy6Ix7?iRJ zuST%Re^bq177K7>%uZ_Nq3%m}+Fuq%kZG^24o|r^fZ~|$`N>7$A01(BiFZ!r<4xDZ zEAs3OVnq35)dhEtx3^@9&+`;2M1EYDF`a`~&bDLiEO}L}=ONomaftIltOdk}rzhp? zEv?{_HH6cZ`~v@fKx0Msh-w6aYHM#x$)v#d?y92y$Of1rseWgki0-(DP?S?;Nwo8X zAh&A1@OLpb1MnFbwzA#ZE5tNhpXf6}&jtyT8c-C_3|*?Rh>%tKRE|CT6HPGXJce&u zfql5sC8`o>+r`!8BvG!-TNlIv*6zSA1+1B_4eC<9Qc7$mPRTf2l^uY%1^8IX+ip8b z7Dlab`LO@#%CwXhXj$HR{ORJ?rU>qJ=1y5!{m|Bt!pOR$LtAG->7!rBX@g1g5q4*{ zBORcPk$Hb|LSn*5YZG`ypDluU&%0&P_Ja4HIY`Ej?$#F?_UAbag{$|vbgB#O+A94b z<&5~611E`0w&B=6Y9mJm6>k)O6YTtB%u{h&F`x|1-uwIgkiES`8T>Aem`zN=V+&1Q zFPH)Gzk@~79V~s%dn6*m#ARBs>tPiYbYsPmsNOPdKQWob7Tuk*W`z`O-2&Wv?CUVU z1?U~1-^{UAyA`^ZT8-RYQ%gj^I2$m~$Kb#Sru$1A*|+KQX07USlLM0g7EYu1Mnizp zhsx)_L!K1=x#U-3!r{*L-GL(b5Cf{Hr#ZFbQePiVkEp_R*r?X8EHXBne?OrW&D}8R zPIHs^W5Zj-{M7J>K&RN0uy;szn`~^MB#12n?~H!w@^Ez>-f&vVu1?ri`6Nf_z6o-- zLPvO%gr@msCHIh~r3!#we|76t+t4`5DiTRxz_UKT^cw9J0XD#UAKTgTj3#4+5Z*pg ziu4bzDK|85>*$?f0U{?Grr{>p?JtwPneRFyBm26zD%h{TAq-%o)^4L6Blu$wr~{V; zdq=Vs+uB58O7dnFL~wi?Xr>^|ct11oc{iMM9OAWfoNfJV0Zo`EapPf)ZP8yghO+t3 zYzotqB;E?M@%}7RK>nynWb)G#PkYTMJideDQzb5cK})b}QoQ#knrD8|wIJW@lE}b^`ZTp7NMfb>o^jm%Qem7$NdU#N340C?d=hIk8=Y;m6#jOE zRr2WQ{&L0vU@G2>qhHHx7k8$p0Nk6M#~u_$$H(UVaAOM+oercY)f9EC*3}LE&t-{p zEzh4FpPv)vskVW>eL@K*Z@7`JkrSy%!p+OW*cG}-RAi0~uZ1JPwF5w{O-iyu`OC3G zpX_zf9@e^Z<_l1Su2&X(Ro|h0J)l#LTFuk!F?}12VzIZeCm4>+Mv-O^F|Kx6u{@A4 zl&uB|Fs*;F2fz$%is-ZEPD`Dni`hn>edQALo!7^YRD(0U9FK)dAg!%!1tlfXKGzzc z0D#zO^Usu-{hmThMMtf1^-15>Lu6Z0^($2`GBMT&o7CR-mH)WyMlW4B-6H7nSNG~u zL~wnxOFK|mlDqCtYwwb=8K?!V?{b6OJ7!9r3V;f+>e(i9+oElcaDA=>ICf2G{7B!B-QGqt=iP+yNm>YXjG-y1a9`y52nU@;7laYPwAWNp7E}R&2WhzF9=i{sOVa==ELP*i-zJ&%?_i?GWdnh0}W{6(=_G(Ffw;!+x z_e$VQiLyYgY8h-#Z>}@ruj#8@7RRtBj^a{{s2r%T3@=Lcr1r_wCdj(XlwK`Mf;}s|+z|=N3S70?hs~hR(m&yxS_P zh~)iW2@J9Wc?M0NTXly-I=nR0t-12*+h5F&oExwm@0CTT0lV4Zj<<3>#Lh(w(;u!! zq>sLkN0w-q2TwAY{K2Yb-^0!;DVB&;J$O_fc5*5X?OXQ4+q zBcp`(L!q%|;VPz>kQcy2-M!dqi5Yc$euIbeljDoGUk>uHLTGxT>%Ptx@)0p?B8<*f`XlZhz&6hCJZq5S^ zfJS~;)#%Q)>T$1jeQh2dSsfjEa=>c{=8k2e{(TzPEKt?PoU+1#V@bzSNtM@2#&zxP zHs$O6m4ZdrC_M&t(#wvYHUdekAA$d7E;cRM2h}j)1XgR=o1gaGd*`eE#ntoXAufhs zwXZc>I3nwP3i=P-#@5+is3;964dEd=9{w^h4O9?_9=s`hs66TK~<-oaz?NsZ+gH zdxxLL?Syxqq77>3HL{|DjHJ%%1SDHS!1i&Qw?J`7EhOq+JYWCCgXXXYdgtTxwNKlM z|K^!DY4u`)5J@5LBGh*Zf(We0vtBIMP&`>IMx1ma8A(L?D1Go@`U+mknmnclUYU&B zMsbKIqJlinZ&}o2Y=bgtIUng?y3{N1<@yBm@2iaE{)7_0?YhXZ?R*5fhIHCL1-a?2(a>FDswAJ0) zMOUKY0k2kIchE-;;sF7fsIb5-r)@h{ounl$vUiIF<(Ov#s92jNgTPl6pR;YIrFNLj zFoPai7i@)>m)h+1W73Vt??^(8Rjq`C=jjw9OL;8&chK4WxV|2gy+gRvs8&}eEPZ&b zB2Rlgr84Aik%`cpfKaf2S~O{ZtWnB86!$EI>W(!w9Ph^h+yO{5(({cJZ>VZp*@{_e z(jEsA5xG#|dyTIz;wA+0ioBb!B^Plfp z8phC4IEJrMG-=;b5z~oE2`Cb|zd#GL7&R-1Q6ic0P!So zvQRZ{PaP)qM{8U`ri-1EmWRUfKTvyJH84%uDoDs<%G<+_>)FT6`mmhl`Dy;^C)hF` z`@a!p*b%mi21K!W4IVasC1CA3n%f*-f=do7{jANA)qcsg$#gm1Uu9g}{-(Ph{V<1mRf>?~evvZ~g+KCto6g?O zrgl56YCeHO-%9AE4u)76W$5wJ)X899oaMid8?Eg;`77R3{;_B%S|k4yXliTy=Oc&t z4pDWutN=56qPy)aMwd%!?$e8dmiKd`^OGH(nt0`6tI zci!H-J3HZi`BWVYOQ z4`=zX6XMEi?%WxIA^n+IdLkv@uVWV6-Hu~_7s13uHma7{;>7uEX$ZikSV0k8<~@tw z+SGLsdc?lHE^b8=4hpBl+2e;PQ-bWshL*GSlKMpC-us0K`xv@6oNiki@b{p1y&%g_ zT4Ooqb>)b%f00pE@+d=)i7*x9NE?9Mp+@Ay1-Wi)O;&?U+6=6c-Oo);iss9bge$l> z7T`LmWMte&%zw`P7bryakR#mU4V$C}(ovB?YBbPQ zP$JpzDY&D7zrpd1rxj3AddIt)kIIL=Bc-{Ljh8dejc-9@LCEkSnu|endnbbncDL*I z;1GE#$`qvi21*;_KYo{CO+F^XOZhf8U>nmaq5`3K>F>Tx6|>)03<7)gD}NmsBl)!C zL272YR7JoC#GgUq3}0v`cw3$?rcMWqV(C7BADjdZ1&>g`XL=jE|1&qlR`B;~bzj&C zv8UA?G`Qs3wrVqZTchVv_yu^;&*5@8!myOJt!>JFoF_)b)R(KU@Ro*$eZ z$1j|Jamv3O5q_d^GohHumi-Uh}r-;`mXStm`W_C>iauV=7>_T#%4LPy05v zkZzhzq&y_*zkK5|1@W^HvY^YIW0i+D?~gmg<~g?Jq#eTLwwh?hzAnxcqAseBN)Ue= zKo&0oNqvJV6^;kH^W9u~wwa9Cx}sMlFCS<7d-&6{I?(P$I5s>t+cfR>O>bVeO*Enz zAp;^kvG~)vbz{TXi;6NNUJUoy66C96@5hrMNl3ZUL3rrg{2DNnF4}a61^NBj z-}8{vM3*DbKUZYVxk{mr3FH2uOQvdn{h^zr`<@ql{4#3}LS$*S)wOuF*Z1h~n>D0} zkoFHkfuuBt)Jx0d>h#Ll=JfRPrA+>HwMHj~jIm_L67bvzvzPGhm1__Wp~<6trp`E? zHtNi!%%q^gOGiKc{s}s?cfbyn;%+; z&t&|M%^Z#HjcFU82ZgNzfvwE>yJqYCHv{Zlk8)6cQD{P=kD!Ena2ioZ+5nzS;qUA) zBg}I;YekxgC^6WkR4IaxaV|z+7xi637Iy>(mBudRY3pFb9lqKa5T&4LF4Bb|Z-V;yS~(;v=zFp7ldq zv5Vv73rCG$qW2U;Si4Ar8~5#rGl_ca$q=m1<3x61B6TF)P$;?&x(6(I$0!mAaQA#Y zSvQ?R8AYLYB{EVPb2?=~9uv9gV!5Q3B_xaz(|Oh# zpf*F*aoY^XW`|SWHy%sSLx86k+1grBxwSpLj4ty9u$n4m5j-H!V;FhYY1>2P8SuAP z#U8D_GuQzGog12(SPGE6i{GW|#1_yf4oYWd9bzlBvJ!$$DYOOg2r7Dj(@$}hCJd#w9pG&V zSTKKdY!A{Duh};ZCYXdWE^u@_(i220=K$;PT1^8?|J$9%T1HM@E~%$Sx>B45 znOS4#L!tFK&oVEv1JqsQ#pc3pMi&7ns%M_Wgx0#>hiNGeeLh?5RW=0{4FH72m-L#0 zk*48Bm?-%B?isPEC2e?hdFcaK+eI?F2Q}?xGDSKr%x$2L0yzAKJwp)oNFzol>de{o z=iDwrj$z*>1F0x^Ek92Wug$+p`BX6>xd^?n+C3xv#r#eQOaY;BRXYKSizkBUL?Wcjv!2C`&t59{&`fa1e?EWkk!1RPg+O0e}|H*W%F$ zVI#}V`}gk0LZOT68R5I8D6p3Wg?IZQ_O77?ciTwt{-ug!{D5bcm?7=Y3qv{4w7Vku z7nJBj5sULoX7={!1{btdgv8ZV3U^Bdjddux0O08hN8Pu+&6J_&;;^P8K5BW|(EBx2 zEp-)ua=|BzisI0r%F`T;zT1JC1LQJwgfL)G)?W5N$`7OcMI z)XZqRl3aafc<-$a6%375JA5|$J<8q20%kVrA7;z~tBZ>PyMIWMpFar++WRQJo$fK_Q4p3r1#SY z-W=e@_@4zI$(3QPz9?Jdn2o~8yH!J+2xt7fz5IsD^>5PX7a*UYwW)USqCTvEp^(r{ zcer8zX2CqDuu{Ymls+2e=%U}=4vSVHoVOWmDlm)&94TekOI{o3C;-f zV#{tPvLj#u*Rw>Q3xqhNbJ|qFxz^q`(C+VqIC$y6$~LN?6j`R5ttNw~HNW4k=X`5jRXEd>sYN*3w;`* z3ACl?f_)Hn?!04YWfyu|ZdVAeZQT%;p0NwAFD~&GN%A6K`a=Kn0n(pw7QftFECg&Px;#kVnH0mpavhV6 zL#`}>BQdtB7MOrOLf{1mdI3Aezoba^yu3FBAU(AcV$2F;e{;E`cGh3~LG=_hAn`AX zeRyrGi!<)#4Dwsyy>7(#hH%>7GH0~)-r`JQRbTO|1r+@JA{c?) z;&h)iJj{+^<-hp#tNhAUeVAkWgC=H6@~;6&NR*cniUL#T^7Mqd#?{k8z>mZ*dYSt} zs$*PauChX`U-j|xhqMN^fi3+9LRwn3$BnyM-`Ihg`mmkAzZyQ-U;Uix5MLmrpp-^* zKt>SH=hQtZl#=Olu9ni;(hnIGSy}nm?0jx0TmZv zZQqvQsvvTk`8ozzx&OlXWHl(sUIbHO3xcy}KB&LX#AU3OMQ~D}B;hpI@~(CwCYGc# zyg-sgX=F#}-nYeiL6)T3nbe_&Yx1soDYo_&q>b1>!H66qt8@B?lo%$*Gz@^zo{{b% zAVLLvCS1wj7O)Mp{CCoxabXn2UGi9&{D^hbcPFu_jX&XUh8^z**Mu0oyK99~sKEEr zd)LoXFq-0Ts3CYWDSkdq-yQ=wbWkB0OIdtDw2wQVbQ>O@g0Mww^!QtTw$R#YqDW9@ zWV`}UGeG%*pbNr(h|TP{p8D0eT2_PehD>a~|KGKcXS^D>yK!mwdH(?0Z64v0I)mfI z6apfyN06Rf@2yR~B-$P7_!ImhFoYr5i{1vE{F>v#r)taJMY6P`veMNMVZ}B5`V9Ta z`~*;fI(G}Eaz4yZwVdZR$$J`J;IhzU4?2l$g7*i)EZteCvVM`Q56pl$Z&Gb^livom zKD7gVeSq{dR8P3rI^-F`VylXQ1sv$x;viJWx8$wYhTret>oq6jUk}H3S?;SNE>V6z z1tsko#R^;QY&!IK#y*=s;pRAaJ%)zhwXz5`Be?K~q5KZxb&5Yi#@J2`cxVJvRHTie zqZc+u95+xedng=i4u`c<>_?wx9TO=ty~g#;Anv;3c9 z_#EW7HS?{$$Jvbl0SH5{03kuasn*qy%}(g#dtu+=SM|wkIR*F3w#OT9%#|XsaxgX1 zK%-Wpl7rrRwpOEj&8KC2QdiNdls zKNgl^7Hw5phh8{DP)7?VNKK*sXammefONtS3USiIj8p(|SoTb$vx|O^)5@o|MI{px zYr}4w1Y84Ej+A;LuG;TJJ_pW07W{ODSx=Efp@z3|&`IyV#m(fgD;_u3TJVR7Y6@C6 z<*zT}9Mig55BGnjgKRA@lw$Lqo&5#?o=o@{g|P2-W}UmC-;2uh_(A|Sx+-cx1!{iQ z%1xC!9OQ8ia;~7Sa=%0=E zy{uF9Tcm+6ww)Vfoid?ylpuQ-Qwdgq?1aAN{W$mx#NX| z=Bzmcuw1Nl*6{e!m96*Dg4D|SSCh#yTLbS=Y2T1q;V>jw%EdRJd4W2Wn^3ls=_Q9m z7cRn|NEpQsdIY6Vy$!)44OI9UUGctk18j3XPdkyc*QdU_~k*81^_|XYv8PEU_r`#w*yXA0!&U%@=U*;=6G#U}V+H`W1hcdj!@P{uF z{giXjopN8)!1_~=o|We}FH_ljgrn=d{0wLGMyd|P2jonaf;s~YO>QKyaWCkKzRuIb z4XWT`&V6WE><32o`#z;>gNywrOCGom?2PR@Y^pnKbUT4EBT&}n0I+StPjgod{iOf< zd8=aAi4qcrj4r886AzNW3he$?&#ehG-F@wT;jWYU!qEK%=Ke0e^KRX?5^R6Oh>S=y zh}%iX=ZySBuVXK$x9S#Udv`a4*I4|r{L4O?u&?SgbH2uWV{OZ7G#in^ zgcN!!^rq5Y9XbxfjOE$b{&YQGAYtm;nvf&SA@>pZ1_fP?&mT_-&+a<)zR*JI*PUBi z1oRG>BUXi*AmHN+j(K12f^}`!ulDBR(ksEuW$sk1KBx1K-Jp(A@-#gGet~l=1Z1)Z z1m-&#WB6O@H}n8RZhk`Z{^2TDOvqY@(chVpDl3`y%YKYn<63#KbpohsFbD7XB~*03 zWu3@C$s+WqkVJzi@@>hF%uCuP*!lSAG@V6`#Ohq4bYU+u;z0Cd)_=37k)xtjM zL$>#(1Kk-kFj*%Zi-XjMNHCDN7bo=fH194(=a-o+qlQjq(Nn}k?N%bt9f$YDa3M!-6v7E(Lh%5nF7mvU`Yu0lk5!OCjgqCmP zP50?B`mA?nZMCq-?FXj?sxJ>P#ewKi?P-(j>=#OoM*IxCtUOWRUYTyye#e%xtcAo&mSJE{`TmN$5j+T1xw{J;Rvz!MDTNyR3vmie3?e=b#MjD$ETE|!`DI{%N zd~QMcdh_z~=|ijL2*uB7c7FAfm_t^I(Y5fqB-kFt5xx57@%h8stoCR9tGx2smdcvu zf`W=R$|MKkz{Am)n46oMhUDa({)d*d(uheVckI7I2m9vNvM@EaOsoq&TSGy-Qj+tg z3JqV4xln{~tDo0!%UiC1_9w*H4*Kt%Wo_){Rtqg*Jwj0;6-fpC)|1U_oX%!1@-UKp5uK!s&VdIr3&_!Km*Py)tXA!S3Mf z<*sQz%=J^y2S&@4uEnbL@VK5{ESz$u%X@q(ZWyhp>y!0;4Z6n%1!$<{r_#mJWy4K@h%e2A2wful?7vnTO+V>kXI_ZSN;7?lO{?({Xz2F4SJ7Er!iuieHbtDn zpThLPCH&j@>-F4}QwIUw z3H01LXkbLA^>Jz>urZ`y8k6;S`H4eapMUTkTL$N3{92j-f5Fd9bc~@-+v$D7|hx%^#@HDD?(eFF^u6i>)BE?a~zBftJoqiR7$^6CNB7#PX{t9~qeBYd(+fY&;1Z@!SOb*erR*-PhnP?qo96d7Nb$p70 z+?`R6&a4a8I2aR-&J|W<-EFo{zv%QzLMcyidW~ZC2c=%RcwoE?i zQ6pY^$ZJBK%qIhg-|9YjtfBAQdkyHE_{r3c7D{jrE`5>o{jE|Ja4*zbEE^z5w{Tw(9CAugfdN0U`8Do+TSC z6DRVKER%rUAJ9>Etz8#n-CD&j-SHEmpjNQ-?a1Hs_l_f}x! z>%OjR(fnC&=kc*djra`W@SWfsM37`2H>eE-qn)toarsJmajk^qEPJke=bdCi8nh3g z5aFQLj}2&)^Vj%|Z%n1P%2^(S^8UM2_ce(}I)Zixa4_T1NO@slttu}M1ix1uM4H*c zeqly!>ME$M&E47#(dfZ@0_*U@OXQwKWVCwC)(pF6wX;9zsKBR~BkLzRA;V=uU>uAG zmNDA`k1L|xG_s=%iI)VujiCwFbjXFh-M^57_nkte8LmM|=_%=!80bNY4kv3zQ%%*G z8k6f7C^6v*f2f45U($Zbuy2U~_Fj?$b?h;2%3s+|HJZwV#n|vSe?|(*+A5lBDr?K{ zE)NIpiDOT+xg95~VdZMtz6Ei>CE3eLeic`J@TZ~c|D48QIZ+GP2JM$?ELUG7WMU|U zz8%d?3HGDagM()~5!1@VOj(8Qzef6Oan*X`K-k0Iah z+0ak}{eVapWzSP=6a}p*;KV^$)(OG3Y^WXVceyoMu-Q7xn#TW*iJK}@J(y`=6-GGh zdYFtKHM1wis9nLUXZ>#{GKVg6v+5ecnVlp2FEtI#r~T`^bjw>uLqU6jjAJBhtn5KG zWQ@@tpWRy+oj(6uq6vCnlDdA%bCnhy6S>q>A- z%@Qu057H0z+M33)U*(COtmLT>Y9pET_t0j~^1iHF5y;9KmUn8A%nKR#XMgw{azH#T z5i}y@5r8a4pIZLAmv2NQ`=xTGw3|2um-)RPnhUuhj8oMS?qh>jBeheMJT4m71j`@{;7Du3lQ&!C@)* zhu`DF`WG?bC3#+!pBxslUbJ6L$$s4$Q%Ok<^t$wR1zI(V>f3!%_mCYEkQ7r1mS!ll zz|P|HzAqFSU|;e%I~pmgKWr%HXDWEjfSDaT zv=bBi`MvIVcD1FaD1qiN3DY_G-9_bn99NNrVER3con*Bo)j#R` zn6Bn0K_FLu|GqpUn}4|(`=q3xp!gX;Ji*)^4u+pqkFOcNdA9;%h6k+;i%D;Hn%)tT z88ZM_4}T&XLG8vJ&TlhGU?{pKClgNlj6~YNiZN?H^SpVg^xym1t@uy%TA8GDuVQ49 z-BBR5tw+u{2zXrJO&x46#u^QJ!V(6$Qu23!O@9e@fS<3{kIQyrPUaz|D+GLtkNuJe z>6BL6c9vM$D#+scPL(}KFDB?r-s~VM4`a0GPqL#!ltN!tYOe*bGg~|^NAoPJRyV(# z!Slba%a{|ssK9A;g*5_usKE}Go4FO%GUAbp_kv7$Ms^5-_BSqX&zs^UXnT(1a8Qam zXI8^7w*ri@Mk+S~aA|mk1-aaYR+zQ}zsCYaVyr@^xzHY#c!nvsDR2I(dw6jr(EH)^ z?Jgb)i|;HsMAXOiuU?)Gn}MWIi@zBk@smkEoPmhu61d9;#b=s+a=7^zdSTqA{mu$l zRFXB>kg-2o5!Sf((W z$JN=(GO%yZ7xdm3&$K_(WXV9qtWmC+ldabGI=1o-06>iaVq#r1IVL9ScmTJFrmIU# z%;_5W?zSN>bg6hVkrN4v@`LA0?ho>Y)4Z_vuWT74v02oypst_Jk2}+mmNB4(I=AJa z5Dy!SPp8N-@Ap8?2#9>^6P%oEA3s>m-C19zRIN7Lp8avXvI|qA{Cpc2i0si|)aJA^ z7eGm_1#lxsmC!t#P+s)X_`Hzx7jkfGe4(|jGK#6NzOQ=UKzx1=v`?Wp|Y`4hF4XPp0C11^#_NmKSq5AAkNiSKjnD6l{B04yIRHy02ERur44M+{62NK!^r{ zC!(2p3EJ_|RwGD3z^#J8ouY!H#uAHpBS1p-)8;(}v1<~EWNH!vvX>e{0D;KN>h!-J zN>RUGjPuPc;zG)u8R(9`m|r1lNqf1NCV>}`>-{kaDTpYul79XYaCu7ld6`nKUFSU9 zK-v23biUPnEYm=s!EwIn+jhczUD0O}p|8rX>gv*M#$_-sPxE!c|NarO&V>92o z&HR=c)~Z-;BDJk|Uw!#UQCX>XT};M2)MVA;vwXvY;5kvPUzrbhN> zgW&;VZb8DM4$~8UB=XvH0}&9h=`}k*lkEl0d*>RlA0r#>=bId+ClQ?x24enF%6^Wn z2A|-QAW|6x_SEoF0B{xMvOcGV9?u%@TdI=J&= z*Nb(-p*PVebp9c5uT9xCaIZ;DcRk!JO=uyz`zyky>y=~#Q7OQV-2!`vT-fw)(>Xqq zVkYMo&MUMS@((s1D{DtA4GQLTJSERv*jUehV7^6C5%n451g zQNpw!@(c zex46L2mD@;lb;8+1vxQ6O*J4XDc{c*U#`m7N>-WrO79XFJMX zpxe_u_)pglX6!IZo#Xc)*$3peUE{1IBq(ixuZ9}I*tq$cU1G{vapQ60QoH40&51O$1H*l>1=i8P_HlD32bCvqS)>wkpPEk56`$MxrB z=b?3RIFlk%i2A_jVl6zG$L__olM)#wEIKys!={Kk!fT`(Cw%Fn%=D}ypBWOX@M}%x zXZg{SvtDbIeihTTOoUS&X*p)m*_To=0v6Py2I+C9&w*R?dU*2sQ~&P}<=2zYs#)GB zxr0CIvONF(z;rQ#N1jZpGaI~O=Zsb433D?`7Gtt3g3yJWAdxlcd)>OPP zR1>KmC;Unt+4n{om(Wx`z6V7L*;gp0>XP=&2_#K*(HygC7s?2d521d)d#JrVpEcR_kUl|E8g-EU7RfLZ| zyro+}Z*5POn=3>k9F>EV^{I8ZhYh|83^_j2tPhGWV`=8*aC^W|_;uiPa&aE`b0Hz} zx?4OH9Z_%`uc@wt!WR8q05cHSI=M(A0F?U;P>U6S-w8qU?qXYQzvn*+zh?VD3_?;CY0XVo#9|nJ@uT2KQ#P<^FuN}K2 zSujAhSoTDod46LZDby#KH}1bGrA=^#k9lRuEALf9ERZYV3fp~Emrd%s)H?R%xf7ok zi#&ph-6umeFbu%M_f7X!dDgVVD3_rlT&=rp^)l86{w^`sL+}_+|E8u`)&FrHN9g^u zKOPt!@mq>RcC;`6lG+=+o-Bh{s5Fl###2Bdm|nx+cM0dex7t@!FV7cuLQUU0EmcNF zMvk=Mh`aO-ca7259>5=$YWYn;<3tFf#_+^s*7+dv=||!UZDK5B68fHhw5?U3SO}3Pk(4=I{}nJs9g|x{M%52fNg>p1q8ArG zl*00Q`nA!sV@HLO*0j}tndCCMPaG3Tx`L9Y7Nq5M=s=$T$LGuob3Pt|YhU8n?-23e z2+@Db5p>2+6d{QaUs^;ZIzK1!8U$l~T!C@Z08%5>JZ&$`$X$@LSH7tXOG=0X8yLCzA9*AV*q0fL8YM*oMv zfUs>LYHp5P18RCO7!J(0Y^t)?hQ(rj%{4w=?%s?)oxQ!iJ8RL*M*WkQnFn;O--?GT zWsT#BXb8|`l%Ni>&6!FXrb&%HL+LnZ0Qso|`?Te~mBmLApMWA!eM<~?Ak}?>u@Q@5 zn2w$EK>W6&{aGH{jx0Lq>v;fu<*abc*q{hID#R=7lFc}l-pc_Hy=vCBy28$1XPvYn z@x~Iu_V+QIhi3>EDbe6n&~k`eDE(9(!6^}XSy*2`)$3%^1_|KRe$R19CQsCV7QH?_ zDWGb0HkqjKs?DweiNzWinCg<)5~i5f{3 zY-ufEV2*lE>_>tnO5T4SSo!yxJrRxsvij*hMVQk-|KAXSrGIFTMnz?Jn3*XbxoVLu zYfy;Dw>f2s{q%@lL!PtB43)DXqzqrBz>9UoKDTykR!-N95;j|FVrgMv@gfYG7C57| zFVCSPU$VbsG@qclex_XNXM&BT6vQCu75dOFM2jHD5l~4ktBcPeyJLXM3B7Js{Ni>x z9xIn+Vi`UQ5VXp(Uf;8>G56Y8{Xu z2}v2f?o!M-?y%%o5;tQzKGlby-sL0;guzGT_`1Zz2bhVYK)DZ!p^#-4#8JD4NkUJp zlL)OxYe7&^Ga|1$)y#Q%yE$!L3BvuV6@61{ZGR8_b^Y}4xQgH?qRChJ&}2e8I`W)6mORtg8znC2f&5JVM#+sihWF+KMBh&n7n zeLz0id^NY$)>Zbtm zFG@dfHR6@yYjOsL(=>h_a_V`de`Z=ypmg?$%UKTo$vs51nIHI5xr<>32<~fr~T|gd3%0ttNCY@s7yL|RhtVsrvek;y5B$Y+`$zj%2s>gL% ziCkHZtjiJ^wLyH|V6)u!E=&8839L(n$04PQqzLssS(mAmF+~DS!UITx!jjNB#9yr^ ztnuHlNtxLWVS5_w92KPM^$J^B2Jd_zPOvg^^swwZ4R($+^~HACUAvQVggf zhMB<-J(4li16dWDn$*n0GBuYQbPUv_DyIIms2q+Q`;Tc}%96n%s`ws5<2^ep?{~XR z>hGW_VZWWqAsU2dyRDa(v@NYHDBPT{o?lM|U4>U;l!7KIZ%lEQoe1GEnh_&DicdvC zw)X+1c8Wy#CC$RA-ynckeSS6hg!3ni^yVmYybc%*3C5GJmb-{)Zd zA7^=rJgbD2J`9PjlU`VjVfC>gGeIBho>&z>-rw9*Ix+|4i7ow6e483}ZN*SDYz{On z+2yp=R(vz+Rm5;r7w6z-NqefnL;}6LrD(&mMSqG!6(MzlY0&0G(jE`_3643j{Pgy6 zaC+=yf<)^)mi@Z`lJ%{vn87H=RY`W73FO^v#BW}Q3NT5bPb}}qe7vF0NoWvMB8`Ww z$&GC~+KdsI4Z$gd1jd}$lt1z|HG22_>=@7)UkrN#Jrp7i!_83`{)iwE)8+*ga&$wI z&Y?Z9k)U=odp^F7ZkKS1B?4gOzVQ9>__|d8&^WTcA{g&&!k~hph|=)>Qy=I!{asow zLR}6wVWR(cQy5l{;k%SYC`BlWR4XE=m%yh4tz1J~VYhF94^Ui__xE^3UVS$j|A$GN z@Ebbxp`vmdECfa)Ih?=~|9yvz?(i#b@JpyAjx=unu|E|1)3f1S?n{BOx#Zd!f1Uk6 zcZb9T@eRxc0R0v(Sa>mzI|SZ}$F?W%VPc{EU!n0*h=S(l2w}-_y%0abhtMc~nv*cc zkcKD001u`J>G04@&obucYpyW9|G7M^(zNlWc+n&*mIteShWJ@DpQPA{zNZ$U(nr{< zL4Ef1=B=VPz-8SlWHx4P@>v!4Ll9IIst4nrS&MYL*#(@U+tL)E5CmVR6~~)B|A9SV zcN$7REj_ru_U!cUoalnalAFH2B>v!7AEAPKkpWDk*nHmvF?uKi>{d=rqIOaUPYE@a z>)d)nUe29h-fW0aT;OS3QMrvR#bMe`?Ms2whwjL_Bv?$k?0Q(a_IlI^J*s{=YzOVT zJpun_v5(utEv+wQWjA?6r>7^GOHJ@s7J3F1D}w*9cOzs})#kvmbW)DS96f&ahiBw_ z@ePkq0jo90X$|t?48aQw68IQqg?EFky?fd9nwpkK7(GDt)0G;(y#-YFB6yG?@yDD2 zo3;RGf{p0F#lM^fd{ABd`r}3Aj?#qnXBmxBFxRq$tS=&QkT*8y^Cv;-8u6@!`rNXk z!*KO{C;Ch-7E~Eqz8>Vlt|39qyaPRYLWmsC#ZpqNg?q1-7om_|NCo$84m{ZlJyY6Gt1= z?^|uUz@@+z_!Bn<|7#Nb+QDg%(iU*Ae$hF1J4&bmoII|xxHrGK(}a-rtI@7TX%D0h) zwRg&4Ro7O%*uPJ|46db3<>R7c<@?;7fiehSW-m0CnX=Y>XCB{8iT?+>E;s6tZ=zre zXQu~d=0)Y!7M7<_jtH$J9GXOlAgb>Ve~?xP)G`UmfZKAi-;B&Edc0meo#c`( zC5+*urY^8^(e@KSgIqd&$dNvv4w7tZ*>~sZL9PG}oQwhiXX8s+naJ>O_;IT;9~GNxPX4~ za2t+PH=+t!rs0N>(pnC#iW`cp6<~Cc{6Vt+5f8JR?zon*t=-_%w7xB#ZI!CVEwF+^ zp$JjU6@7H5yQ(1qv7{XMa25#;M-7(JE;qzBqwv$i_401E!CyULEQ67ShLav*x}>Yt z7~~#Wz(!?tPvBa8Wc9Z#?77m!(M{5FHuYFOZqw3UtnoVD0dhlYH9;QKR{U;8D_N>V z{#K_$Xto7fYC@(&fNZ``eE1+*tro#zVKIy_@HAtk$iS*+TBm_FW8CT#TrK9SG+ z)$ne3$tv4=2Kc6#(O3h@W_7Q-2l8X~7oVu(BcGF&(?Qx1ILd8HBtjg2^^* zyn+QV^^v!Qa)C{N@hI2TQ{qHG1T~Vu?@O#@4U|~n#jn)~%L`mo#nx-U{%lZm`e*m^ z13LGpjE1z=^@Sx_QS#h))@$j~<1Cwr_HDUSTO05+bW|^WA3Z!0fzSve+KEcK$Qs3M z@<2kZkIStebF!P+1B`SuhkbhcnVbP;XVEuI~_Bn^>p|C zWkT6P)#EtTcpXY+%fs0==;RG2+lITTQG>pwcY9^Wod;N=S&&;}O=f6(KLdZagarSQ z)xuMHyL>X%TAQ!c^ZxFobjUdRXranzAdN~tXxo6SPdr#ZwX)RcP2CV}VsAZ)xCNWC zyv~u$&oQ27Znjoy&!7E>)GPAvN5Q_;ZOGY%4q!tDArdmX_C6V!vz>C;JS}gl-xBb9 zU!F_aN|=f|S_T*!Ts!C2zm5ERgg7s3t$#G{$f6RKM|2wNq%X8ah6 z^cdrKHKIbjpw7COn184AKtB$L!F+*jT;7;gg~4`;w7T;#w@2FEGB*l*29Njk&T~G4 zc6h7jt6W%ET^0OdlKJ7m!67aVPHGCyz~nrzhperwt*i`GVbOdSWDcB`5|XclA+&is zoSlq>=f6Wa*M`DMSZA@;ayoth^5(((;Ickip}AesP`Ze9G_fbhkeRM5e*} zmIrFLdEGzlPvrwC`&-;^AFhjcfzv*(yY-XwC)_!DO&;fmyBJ8|q->$uYNo2^vfT%t z-)OpGIqm2LyHFw&E=z~BQ%Il-IxDE=o6#HHxWwS&9v)xK_-g$MqqZS9DRY``R`%~9 zNn6I%WGZYRA^n8}x5WCo{bN#7V&Zu}InVbqZBfc5Gr6y3lhY`bu2k3#-DtYs;PcpB z-3`bChmYLe=aacZ%4J$D&>Agv_me#cyJP9x_7BqoTjJ4pJQc=sH>Ig;C7LaI`^Yt_}WG0B6aC(QOg)b&ien-l*e$D=%1=^^=V-E?)c#Etf$U5qC8zdQu5& zS&-~*B4X6JZm&?aEhIpv@c3Y)PGk$!{UpTOVw)JW(F~UfIrPfhx6=rm%AhB%;vq-g>jTogxqd_Lv(?-5@;jCynH{m z$msCUFhmptI8b3KSTKZ-cGaMr^vZKgVC`mE?>o{L_EiKZZ?(1-Q=9fxH1UY;*X8(K zeM+mACMzu)*yn2ulRi;qcGyO{j`7Hlu&~H*P|2j9KT|R?Ciu+-ZrXkQVUVz6Q@KB2 zL3;zIM8SvzJfDn#;^rkKFUi=Zg#!}L4;0;fJ-s5LA_2eLkl?`}KmJ|Bc{BAqW!RA3 zovj0zY+kskE-*n3w+%fgxrUR3P4z=@3MipGUg#^5Jq<9mL`cC?zZ!%Tc9PGZskykg zDA?F!NZ2UYD7dJpsXu=v;o+fWVId>qfL#m%H^GM%NO&_xY3of`$;_%KG1!}{nPHHo6>mpD4WHmP!1Ps3m+(LlgiKqCb8-O!pE5V_zyrIz! z1KB6rzl-MDxDJ`4qzCffQ`}g(usayN8G*xbdRI)HBfDGZ zP>{{)`K$O~W@15gqgK%rgu$2M8?ch;)t6clG_BuheoYn>;S>ItYBJ+b(`4aT4DR^n zV1n&c4`K^QdT>{nVa?YY8c^DK+bi_>j=gi`?)uRjlGqs|Au>9Pox1y4%1V|?ZE5I0pq5#7FxR(hBtDN{^ES_Ue;hr#qG~#Q zUGEj4!ujqbXRo|!__%uZ=v@{xhMBnAv-sbGSc2zW%GwVKebJ$4#NW!e7)Jf=KQGe( zCD|ALdjyYz;$S__E{*bQpk7B2@%a}bRqPz3-UgaH3uc;9GiF9#Bv# z_@!#e3Bidc($ACLZ8Z1z>mBXNyVcOW~M~8J|(QIoG{&loe;FH znSavd5R#{~XnJl@mHU-tyOCK+W>PU2E+}*b-kqA?QlfKO?cgpX1~S=H{>b+F{O6%Rv8J}TLapQSP$4JruQm|i&Tq;P&{^ll>7nQkpZn2*v zt=sp~8sCk>{BU#K$FZ$@aj7u>Dpqef{pQ0y=|YJZjV#MXsO6E5{9q|?B>QzTdQg|2 zxZU$^f%iYTMyNj~ut&2sxvfAU+N;m9j!|z}h9^k;h$Hk&W~av)3E1z!T;C{~ z2NJR5+~n&`+fggzGbknnoB3oc-tH<#A7Pr|{x3n@G3N9VUD5BO*Ra4GnEs-M@ijMH zi+6N1a5CLwefo#Dh!--)U6UUd3Jmw+bJddFLYs9s{~7JQ!r8#0_$`{*U{=P6xnc~cK* zD+vc_c}3ykti4&#wxc%hpU3{d%3w7<4W|}4#g9%GNSG=|k-CPU2KXj6{W&=tIhKbJ zSt$C~raG}R*i4s7vwN+^L#PFKzmq*vTVmVQfBe|ahIVo@Scl0TAsdcZtyl_ztC7qA z(oas~e)?lW>ib!rBE9~~nX7q4H%iBnlqFQ{<|{`5;Sm2mN*Kt@VtDjWoO!@i*jkMk zN^hzR?30E3E$&>IQMtIApL{Q|@qdOz!07(kML06^u%3=JId^sIy?SxQ` zr6kjG<{w>=1L`awYrCFED{@=%v-uUO*2a6m0!8b2D@|QXqzL z;CY=Ps&QN+iXvN*oRp4gFV{fCTtOtRh(AAMQW#SH$(;>Xqpf?zX%cALDv9)6-@E9Ej z)+ypcFn_S|&kOKoqV(Yb4qPiP2HIMje|pd1w-rpwp>dXj-DCz%%1X6Cr4dll zxq^?Bdum0v#Axnfv}=p9hy$N>T25AjMI_)j0F8;H`NR7S835?hJ)cd7X`llg2qT)dCzKhlz!IVef#X;Giv zu*c#ln)DN!`_2is=)B?~q!u}Z`TWX^((NUXea~HK1Sc*II>>T-O=sRO0SO*CJWO(M z7@N&&wo}x;_a+(+!W(3_xT#w0*s)1>O*Se>S_@ z?5_s}#swpN2@QvRrim}@0s@keBf-?!NR8*~_rsN}xg9R!TY|y7Q>!0s((N#EE*mK4&LVKdtGVQKBAB$TdVa~qtS6D`5ju-rswtjVjwvC zQSGAmVkzgD`E!>$rSTghaETbX%nxo-GtGEB1W8OS3zNM*^HWh!Wy;PUXNDh2;0nX z)VUXFvQX{9h^{|BOt=9Va5H3t$7?D@T18^@hY|SnpDqvMhcY6?C zSp8JTf3E-r28O&I{=8{t-Hcc93}Mh^D=e16d{wDRz@*0|6=%Zwb>ZpnH|^cS#v&N@ z@QY}CKsXX1kx_^U@Mm!@?u|{2jU3?tGBQ#}2mW(fc;BA~^F4BLW*2%!R;JVmE9FwR zV!t~das6=4J3BbOSkq#ualAr~k{YESj5jNlY11f6Hf?M3&f#ADh5!b(_Co+K$>y)s zWuj=RoH?$25Ebu;io;U}XZDn-!bMNAt*N!KdeG0ruMp`{9AHCCMUSv$n25x7kkg&f$1SI6sGvX5@1SEvNkWg1ADQOOSUw02BMZS^2 z>7w)A{C*~2^F!2L?pkY;vlGlj__<%5RDWe(>vh@V)=Et}DdR04>6U9igMr-@yT}me z7UmN)W2*tNURC-^lVSex`Q}WBxe)Tjd5cb7ZhC(uy=D8B<{}-(iU|@ES6XeaCwngM zjb#XixYOKq)=o4zxt&k?`@hIaN=V3jU96=uRsa6|>%mR+RYb{A+KT%v;UKpcb=$h$ z`L(*?c6711R&DeB_zX7@esWT>wq7u8uv6LN#4oyx0#k6X_0&yH*P#5&LKo)d0ybGv znbf9-AyLSEtDs(W0t40KX;UgIj3LQO<~V)Q$uUpDrp2|IRx`7LVpp|mb3@VlXLo1o z#MOGG?;iKxCjt@P?RxQv15u+0ftU=MRIN@eS2ry%+wrnRnR9F`HLcI%*Ok^bCJuUT zR&sgX8*#)2`@vp>^v52u+c8)SWOQI)9_k*yj71)jFKRbLryxGRi+dOT+YZc`fSY+- zAYCj+X)w_+{PNFK9J)TKur~DLBj2@yqoaq*ZSC>Mp)#FxBof1+x%M>R*UiqfS(sRO z%`D!W2)iqf<+qM$2Z{AkNJ0GZ2dF$8QBgWXk$g4-a$c)NY z>WX3fd8v|bXae3%=Bq~J*Ib2fd-Qbl!((HkQqs~g5z*06h$tu$x!TZ?zlDXn0FBr} zm!D$(bBPcx`MKfy0ND}Fdr8EYa`nCE>uy;z@Mm+eh21b z9mQ~wU38OJ`zvsU{qdWvggUN@Bh}*;-K-9GktuTHs~|f~K`>UW+k$cnB>QoPa@hLf`BYa zAoGewI36pmhYs>Zu?g#G{iDL&fq-&vXy(rD1C^5><+_~s&h#r6>*0Pt##c2kFclOJ zNeHm^#`SO>(%Q`CxoLhI?dr zQ+L2GO>4h)tRK8R0$urb|&aa#_=)p8zLmcT|Rw_l#MJ}5m2&(MB z@3LIjoG(~Y%o*uP?ijrb0|)wM;Gx8PrQ;sswb;*8oyCe9O#lkL7<>_iG13iU0 zQ(9ci#q(6GA{CMH<^Th0=XziNg)NV;O>>F4wYzyG_=GmdraBCIa>W9w&M=g-3*wfn zSQxQD#)wa_YZMllW=mnKyxcfARBj4j^1}vPT1#gvGREtT5?4MWgLwn-9@fbR^Wk=a z;ni1e6}}o7)|Z{$Q6%~|$;l4)EY@qjLrHa`G^AcpbFEWqW0fK|3pONsv6LDs_C}lO zUQ*|LZ}s-DvP0#pwSMn;>KJ#>4u+aM2lmzgw7cgT?4z=M$cyu|HDZ2I{_5HS{(cxp z@2*8@`d4w$ZZVWtZe{k$XVp52B(EoO>)iT3PJK&Y9jm&cl&SpvsL8zIWo`Yz45UcT z9>3Jr6H4TB$Ch&c44H8^^d48?{Jyb~b&kl9-E3bzNREPeWNRb^iDl(HtvmLt`#Er1 zIiYN7ENLfVDaoV34XHeLS1^Dcl`!nc(-xHQ zdbKnjUw)t?F2A4rbcpAT9_5)3bMHJp>Zpjf{MR?-BN$jKOojmM!jFpE-KLu3$z;R$ z$}(09?2QNzPHf4=7P4@${4u~jx!U-CvHTc3+agC?bS@ikU26Q;po zgW67$+l%oftoq5xA7Svsu?t^S7Z!E)9QIrhg-dm~su6f)FZ7zBuKFMVC5Peh{iAGS zF*`vMjnfotylo~td%#YU1dXmnn@2HIR&B_9y-;g6I+(+0gk0=7EI72T-Ep8}M8tL6 znZFj*;|k);dea7+CYkOzx9m?Is{`zm7*&rR6q;7N-?9xeQxV*~FnWumX+ffK^Dh)? zOT4s=J*`FP=V)(m$j{?DKEuvKygOafC#@+wB^fkFe7H6F^}-YX8AB1!*hn3jik|Ff z$5rZ4q7^*`@=}yo5T_^`H4aY*D=p^=W5touwJ9($dPNQFxw;1FCoq2%qjsNNBu!USadn)YtK{iHco}u{FkYl)B2RZ{o?` zAuYzss_MezD*lg+$xPV&dB$o4jGAu-8(R_Cs-GVU#VmU_7^8{iKmKn4u}yW z^q=GD(0z;wjb|NDnPYf^#&Yk59MtmC@b@j<<+MCVD2X*TBYN}!k_FG8WazcP-=8Np z7)uVCYdIrDNHNgTus&26ZdG$4NgAz01yk*c$mqw%=WvyNXXAcp_|tG8qpNi@`^6h2 z281AuTa^E#sC|5V1x;SmR@+NYgbgnbP&BfeOBeNl2?iH0^Q%se=?9jdn^Z-pQ%gWz~V#Ktr>pDZl7D37)j~ zAX-s`a!dPrx+ryo9X!6yuIAh)TE{ETw0W&D6Xnv>N!sPE^caxpx1D!TyL%=$;M9C? z<=_^idaOM+6*ZkGTt51p6hZ_iKeE}=3u;U_w-i0Ml#GalnvF^Sc0)dJ?nIH#ePQv% zR)C9(v#y?2S>^SIw_azVMQrHbEjPV*C@!*(xbx+^mOu@MKfxT$>zn`GnwrlDiwOD4FG>rev4H5!F_Ql z#k`nsrOE5uVs#b25Kf)M&!go>6~OEAE`2QuQQO0lV{|N>8gf2PQsdW{xZSX_71y&n zQ_HQxI(*(1NbR6Liaev8g8ujcjIZ%(r4uHFvxc#W9Y2d`rwvZH=q3tgsB?wr)qe-1 zXJ7#lA+OW-tu>XkjTc_~@j!XxqooF3CJJiW_TcYIWbN@FStM?rLk%P3E;p0CbZq>r zlQ3OqvX9Q7s&_cP@fUC7D>nC-xoORLlz-jr*odh-A1K^kXt)o|C13a~*N*{DE;k`v zNMzUGp-Q2yrk0eMnw*h>M~I(*jC^(D(CR%kd6xZf-o-t~fRQ%J{Ua_JPnH<>@9y!* zW`r65^)Js!l65j9mBqn{-3*^t?Bp$k>>i>tVWQGF+*=F&?xe$D$QGHK;ijmmt+iU8 z9=+TZmV2*XW+_nP)XTm%@y1O@W1M3RlJaTr9=eg!$BRy3)9A1Wy zw}P9S*VTNu_v{B4A-rk~y4EuQ`mf|XjV?5(lQZfl)j`I2|h`b4{QIsqDL#b5*XaUH@l=GB)SH@@4()|47FT^e) zVja4Yl9ZMZ6W?M!_OdMO+9#XF7w28JF0|2;J)|5KUjDgEqd~!;@=T~>3|PLUb7VMk zIIm^A8XD*2Q;>J}h=>OTLx1mfYitM!e25V32lUh%KbKgsUWUY&Nwh4#yKfBm(A5E` zwbCoCuD7|lg^mV%?jv89p9?Hhj0_D8jSP)`=jZ0-QBaaoSUpqG8x7?Z^aG!M>cFdn zCI+V>Xh!FTBGqnW-zJ1fNv4Mrl&HRzo zp4ATK&F`%p+PyS1)wMFyBTD;mf{TfqmIRQtj`WAdhxy!fM>{*T&`<87B*sBWR-@BI zLI>G8dE|P(X#jUsyEo@47jUwD2uEJQsfEwg;SH?}>>cb3buB@^to6*`;Xe~G5`lgY z5pl9}d}jKe>&*Xiori~rLCn;`!O)IK9r$IT>tHBisBdjxNTdn81^h7xTU%J$$=m4a z8xk>y7&@8i8-5oT`uCo)seywD5fcX|$G;b(46TfTD=a|!-~YYz#n9B)#DR!~{qw&U z z8R|O#*ZBD0{~fjq_|eLco&4^lvu)d}HZm%#?}rbf7?4OfI}7)fQ9YW8|gDFhCmKVNV5gmaxg5s2Fugb}{moGDh8!n&93 zUaGTBo_V}IP3Bl@YKm@(X80EUe_I{jDb90!H$SI;c-Yb!_PAu-#^l{1&eI}}=W@08 z9-aU7$sPwU`OA|-v;c%wJCv`;Ut^i|L~So!w$|;lxy|x$%-jz1iI>jg{Hgtu7gl4` z|DhM4{2agD<4K(0SqMdzL(E+EdSpK=5}t~?-L3C zBK&gItiITm*OGrntQD`gsBD|QczDTeJN@x90tFPig<36hQLelW;d> zV&HG}dVZLB=D!Iy9?M|LXnA0(+2qUD9{qI zbNpU>Mrp~#AsaemFp~LHr6%W&(f2F|nm;Oawb`}H(!SpeiHC)9o@7@us_TX(89aN6*A%wSREjn^e zy=;hl{ZX?smHMwvviH4ux|X~yrCXrYo$2M9<=~%R?N9A`KT{I+0jy`(v};Hk+ih}x zqM|IvXmdIDd%2#|cKub)8#ptE)^iw1q@bN(aUfm2$J?w0FI1C5KW7y75edRvfmYB5 zjJRpkq;D|wv!KN$1*&vHsFD5J>cDY`W6lV#w*c4)bblt!S!~rUI$w(}$g}{BE_*7* z{8ypR9Ur!$`L$X-1`^vH&T3Hp=s^nAq`INgxuMkgp=#SOf#`cxd2|||dJcCl&g6e- z>gLhizAtVSY2J(%L;a#)xqq1ESX8^r(UoJry;`_ z$(>Mdxk)F3YZ6f>DLPPok%E1c8W#czjU!#g8N_P-0|5U{*V-s?w7a{aZ-EoJrAz&3 z<6?|*CTMX~#-uL+xD<$#AsFt{98Mlcb$+nXG9|=O1@G}h{lR=cj+(6;4a5hbhe9?4 z`E+y(X0h>b`E$d;0x!DX7S90TB+03_^ZyZpQf*ygc85dB;pfvKLbA-L%OO}Yp98|& za}=)3vDTNHgPGzR+2_}*DcNQvsl9bW-i%-0a?X2%jTPBh&^EE~YpWMMZ)Lu%JK$n5#mGMq`Cmy4c57?l1u5+a1OoaU?}{!_)ej7`L=G z0Ce*TCha-J% zMedu7m4giAsH*hTC~F9CQgkS^FX-8V?K(<_(6oZfWm?+YvxaOzm+~`&icNYb*?N$G z{CQ2L*Cd};YTHKv&K>mkuFTD5PbI;B3COYJ5?WfMAiF7zz^)E1< z0U~S>UM=S@?>%n!T~>7oF0qRWC6}m{;JQLt{gBLjz<$jViG(9Z18g8l7qDz7WIl+2 zwvA8Z6b!MdA`^iTM^$!JO{cjXU5D+0%htul(`5u%HwM~g<6$PBmgCj`V}3}mxI#%Zi16!qO!rZ#pjFdp{f19ZBw5?+5yn!-M4Mm z(6p;O<$jTi1gQm92ORG4JbQT;(aF$?t$1Y0<$Uwoi_p)8EL(4at!XJcnXHmc46#9nX{ zx!U61hxd;mMDQj)mJfPfyF6#RUk;%rY)rtB+Q)lBly0bZ7WWMUj6+%2ceq$%DRUXs zv)Ys(yY#^{=+sfNO8a7QG0P@M^vh{^%TUJB3BsHkHxaPUtQwPrQR>w0N{s~IbHIV2 zw7Rpel!&_&)^;G;*>ny3Q0bbaQL_aV4k?-kT-&_~7Wi|#&o_Hg*{wrRfIw~#Bmv!S zQMc`x0H|Zu$E&q7ZA8gfP5?M(kXO5z-k)i-w>BlfuO0PY3f@B(@Jr@t$1o4vY!tLY8gP1uQoybBC_(g4({lQ@50dF>EW;(!~hUECVU=uXHWN+nJ2HTJgr$^wk2M? zXopo+CKF%q>bL2z0m1P$EWbKpHM?C8&Q-a%UCfj=&)1kWXhQ#PLi4U4MC24w~S29bcm z0&2D)vo2#yyB4W91YMFlo3$aFUc782n)@vOAka04MjivN_Z@RM*~N~eH$(zHA$aJi z2c>M*!zh=hZ+yGl>_NzL`KS{{`HljOCX-J&M8rY4AfeBH zpEQC^)ZeG$1D)Dcc~}|Xk}S!;uK&q-P1g=4zA73~1@vSPGe@V1JPYiT z&8w#I{cIw@={0mME-MC!XP4J#g_4VAqUh1$14EXOvpt?~3|XT7sQ7sP`ZfV_l(48o zRu0#jPQHCyIRxh6NFcNz(qSa+WBtZmWw8N8oa)++kj9vlt%!U5#UBjZzrpO$NrVk5 z#H7!}{eK%hJ6d4g`PU+J2bE7=AJ<-8_OTq!-iZQ0%6I@EWOpRr{w9brdH{GIj<)Jp zAK15_(S=K8#fAYPVsla;+aSS~9`lUT_fObBi<1cy-=v`I(Q_nd zvIO+cW&I!#v0oV-;1>zFT8(mQ-K^p9y>oOQ))sdHjH_1upgk&h212`S`o@InOymfZ zoS{BP%=pd()sm{AaJ$ZG)S)4B4p6+7JYraOJK8QRVFYk{T7u1W7!{k;ZmrUa8}mWF ze=-IxYU-jio?yF*aXb=WMlKeByHVU&nM_nN8CCZO`F>>e*k_y%NrOwuRlND zN}UJ$^X=b#TRWo#)04H57GVoG0#?5`P=IHgR)kNV-XT|QG7Sa}0q5$F9b{qT;xOn$ zE$vfZq?w?Pgt};6iffQOZ-4C<79MZap;2kALQp(6FNJD`^tK+LtfL^o6>>6~CPe}b zO~&#i2((QB(mChdi#dK`&)XAx^h<~ftS#TmaK8sb2#&CikvPDTf;d`c`o{?38k&}t z4zSyC$9D$fC#B7v5PfqI+78#HS_uwf4KTlx2`F17F(!4qhn6>mV5MdQ&=brzfaAsg z0v$&N%driBq9ZMU>!4!)1Bv^+FlBHuc?4Fg7|*Tvt^SG0@ma&0{j&a8dX#^VFVL3n?=lhz#cw9y4q z4gh)Z*2VcB&sNm4Lx@%}Fu6)_aA446-RFc?57*`(6tI@ztMPic+8^V8mFmyOoiUDF z>_CG;E5KKs1q1I|u%M6Dd?s!nB91ac>Ix0u;nL+$Y&xgNH<|thI157iP-exJi{}C@ zpQQnl^P`c|2NswiZodJ&^)aa`0T-%ssDSc?G80c=6==6%OC8epSnUV-39TsiB{gnW zdlRz!&m#Yivp0{Ya&6nk3mJB7!>WWb7D<^~WLngU%9ICD37L~w=3$w(mdccb7FMxC zh;7PDB4kYFA@dM2x0c!ZowxnG@4NT=ectE&et)0O{;O@>>$9{Zq5%gp|F!z0hXbPgJl0O z{y-aImu}0mESdf_Op!!2c&qeYP*f|;LGvWusHg^wH%x-~SRX;WjhaPFh66>W1xYix zoj^>DRQaiAZ~TnCRq3@B3XzKW=59*W=ib8)0%-GuiaFaf=QaP#jxsY)8Mf&Aq}<|J zOC7eb4x*3XwU9UH=W1L2o}GU!;6)P0MNSAYZmyv*IwHc0Xg!@TBdH=yXx@?^ zXz%C7__y?S)xoN+kbJg&3ZpQ?0;7io8m7A^B8C!t8C-;&O+rk10{Nt7~|9YV#%?U`jDVaVIdT1Ge#H^QWipKE?&`*-2N3 z?Uf2|NA~vnw`#Yg*)#`zEP|pIAjwH)xdoqNnn@=0y#Jg>%V_0vBs@em2!A46F4s2B zIb@6`@IsVU0Ig0QLu5caa+b2-XI4&;}N~T5TVWmyNnx|Y0Hd0pL@FW^M ztx3;rA^%>l%T2#-ECqWbkvS}uIo6H;ASVr5Ynda#`}gSL5QE4BWG`RD>?{Wp&9|f4 z%;fU;#UBSbk0|81S7R)2ZjF zHQ9XuQPopf%_h~RJTR0!F5nE)+f|l^E6tT~dpn-0e%Z3cE<`bndBa zG0x(4z4-z|Kld1}+?DPLFLy3;1?3%y#K^$aJm;w@h`b-2kTNEd$wU#!hfHHqu1b9C zs2|j%gSgzeBBSous*du!deqS`l5=G!#8Gm?4DmTYA8eRL}WXgf+Qf({%^xyQh- zkb_Wavvv9l*CXfk7Bqd>OFW<^Z?XM`ul!C;3ab1yPOIwx>q16ZVUp(Eo?qjD>v`q( z%Q{kOAi;#9Y%EPH_Hg;FaK}c%Srm<@9CgPZG^`c=A0ar>i9{yq`()TPm@@N|BSLkH z5NuJtv6C1V!H|4IIYX(f_t!X$jxpz?{Dc%4FWn?(5`AzO&DXG48Hp<+DaQAEIe;h> zSl2k-qDbl)$mF_>cGWExR!fB;Qe$Sqooc}PUA6Pwv3Oi{rSfZz)Br1;<7XsZ zqKGML- z`1Ovf**%25SE1yx+v^B5q-SR;?${+dO&?rpCbkv8-gV0|21+xu7fSah#P{x;xvCi>=dT~HQzVCOOClL){1K8GtQOj_ixFzYff`6oBwwp9;jQOTC+nUBse!= zjl8=*=vXhZ0U4tlq-G_^4V)uoe18Ts#4;1nxbl+<$-y>;DfP)iwzBtRQ@Lg2S=}zz zI(;Z6mYqJumCTxjT6NDbCZyZqHj(iUl+dF^B)ayHQvG0&B? zc8t_6dyKDUFgprsprj~q#hj@NrjTbFnMYtIoV$`mP9p`Cy=985I$h!20y}7>sMObY z)^jazK+Wa2n`*=bWt)OtdZ7GLRCs~?*0XyqcveR1)Xm{ZJHsGl{yyn*(O4^scU#xOj9paHyl>=5da@v(FAw21M_MIp(v>%B2%3c8yj8|QZrQhPCPMRitB}V{C=oE zqlu63&i2z66*QGqF=g_4xHxw64Z|tD$f9V0gTWl6c}RKOpgWrH$+lcBm#RcGs% zOX?}RHOK)+6Mo&Z|2T3;hJL9PmM^#mW$5x~6noU}uNH`$QF3KlYrK+>U-99)f@XRH8JW+*}@6pD^+j4K%~y@lQOI*QI2ZRw;U2 z0yF8{J&T;4N-k^H3cE2q`@Dk)bw0OO0`<2_01Fd8f~iuu??dW>azqQkRl-PEr7tu! z3pbRp^@()jcRm-#dX%gCSAb86#6*4g4dfJ*eb zXp(_fg6{oPty6^_8oZUE3UW}4GhITXXxEM{T#rZ*K?6D42MP$B4JP%D;ybw}G8xSS z`h!a+dozsWRA7w(xQoY!6MJhK!qc^hReswqU>PNTwezoD;U)$oUgvV-e_d%#<)tnf z*9=6m!YM*7cdaS<7N-VFR%@mDjJ|)^%3QYy0@QPVA!25Z@~!m!8VvPrlJM3sBiJW+ zE7x1j2VvkqQkRxxSEk=B;8tmL&zVaG@B;M+(8}0=fIO}KKkEon+0>KkP8;R&pN7NQ>x>Z*371FN&oWfELa2dPw(NNd9+{pauk zWalPcM@BUaT$pD?Sl`9jtru{ZHf}B-XnWqu7?BUuaE4*fjh6@VT$YS1M?T&v{e0x) z36%_Tn)ktQPQWs-N$zE%R0$b@oN&so4&moj&LrW>ZV`mb?lD010`dqzJ68oqJ+b!* zH9BYrPDj;&1?RoF2$=YetcXlP3NV<^vmrLhoa}ocz<_pI9$|{Ifuvk-7OQ4auoTxP zUbi_uAJQRc`$`q~Og7lurYq@In{>Z9kt_$d8n30+b36Q2qxayr#t36|N&FIm7{nGq z?c9xJ?pl4#71m^#lvP#?srEQw`&_u3jB{t^qCKMCrinf<_@VN_>0{&bRwQE0(FC?4 z6e{fIebQb$k@zu>{Nc6{$PMX-oa-&SE~#qrV}UxfLp_ zIsvH;?xUYlPwN(mkfFG|XVGM}ICeh40D)@rE0dp5Y1+p|bzTZ{x9d=cr7dMOx?nT*kHDIthfXmiuUuk) z-+{Psj49=wFoA1u!~)2Wwb+Cr1g2n@RU?RQ{@sKQJEtLsEcBY@yC9lojem;G7I zNwl-`POj0jWrXjuR#_#C*kvmu9q8~ddlk~;eI~>MCqv%>vFW+iEA)P`u#vBo-uW$p zj9-Z6*e?KdB8!l^8}e`%wn(%e5`>13sXV+8-aK2G?$iawNDYK!2LbM&+0-yn_$iyk z6iez*Ls@*|j-B$4izP+F7V=1?1Ju?>CkI;@FN;t$Aic3&=Jrk@HAn82-s9JKr;biZ z!4hqrQs(D1 zI_m9sJN^5pq2=WOJn0Mpcwc_mn|6i0HIiTguq}A-!SrmBVG%(#=m6`9SS`ETj;*1< zrvv-)ZU8EAb8$op@92mClnY5DY>qq?7(8EjKPe^H9EdyqfoM8Z2WIyt3KiEp<$#17 zd^*F*DJt~9yeX)6vPJ!%Y{cR|*f04kLmu(@G{KE?f6+cD!2FB`B;$M}kKg6zamPkIEH|!XdTUX zcsQNx9eW3KsN-5JS50#P=R#pBAUBY=Drn_omoJ@2Y~@)5-W!1$CD~F6k}6mvH6GOik7#WEd_9mg^fJTn zqf?MFRna_RN0>OK^!Po;iCBlRE@NPWHpYh`*ZiE-rSffKO!<7dxQ0e2FEAQj0Tn>c zY<0kXQE-A}RTaO2wIEjJBG(IEv@;5?IYdJh3Yu7U;6SVokPC=MqkZKFtteTR>}oO0K)1}q*^PT-rJlZ?$$>>rn6;%MT3k_ zO}Y-kS<#drsv)p0&?qLL^~gz5Nk7Cg00fZEftTMWt9THIo2F#&5NMq;bDV%M;dKy&b%LE`hFUQ6ia~{ZuYcn@rG&y3l-jw3j{#?> z2eD1h4l8pjN2mKu0sw@BJWw51m2)jV5YKmvIt0pq7G{!psR2_xa2p39^t{#^lt+Z83gHr z!H5=--wR1V^NveMjY9ne@pwA2XydR`4w<|xC`A4UCjb?Sa3A~xPB?vu-DP3kC?@`h zNWlAb=PU97vnVd~xMRQl&Tzbi+IaMNC9-D#uP{za ztTUG7GPzjVLUf6c;6BAe9o%pY*aIW#U@z09pdU;#5M;&1zmSQc;luoaq|xy&W$fQpt)$ zCj%8$U*@3MVdffS1Mvfto14W$7UDXW?_Yv!m7*donuLVG z|MiL7_RG_)gw3`b1aZ%0HIQ6&i02Y{l}`Y@q7#WmsS4iSu-a(4w!Mylp?*+nzBAbb z*PW;oIddCf56<1F+$- zXvtvnPiZa_z*y8Co(mbnLd|Jo7*wKD8BYrpDxh{MhCa8)p*2sl)L3d*>k)Z3!a3nP zoasyl8$jnQ;E;f*1GO+iw*X*x-TMMkkbE*taJ#Xy6JKFWk80KYDVWSG(q_s^5R_!m z`|2-pRs%39Ywe^-fm8qd&{-MSZc4_L-k*?C`%0JE{c=Fpj8+AujIeJI9*#5fogG+} zx{Mp9|1m_v4{;tyNFOvW*;52nH)qNW3aKDl4Xvux#53TMBmuAk&=@Ee_MyUd37o%O zG}hr&?p=A;`V%c}LBCNbu)-fqgOr)NZ=$iD7Ph(OuEGvftcc0Nrbm&B#i#907s{gs zgGkB}cOH1s|MnHdzgO*tq+^kfG`P9`Iuu(+%)ab`Ye}b`NmDQriV}P-__9nxC<>iu z;LL%~Xc&mLJd7yq;I>K;kpVLdLdx~S(cDKE@CuOn2jDj-$ZoScc00?XC1#N{-$AWG z*jr(l)BhJw^qd@PUgN>-tbkQ>Ocpl2%4We%^U=|3fwzdlcGgZvB`1AO#jZ&+>~VTJ z=;c$7k!@#O;aQWtdgB^CX9$#AJQy@Af}8d`=!_U3xg8Bl6j+8)Rt*f9#pM+LI~9dzyeVTJE;;+XCXv?DoE1_ zVx1r=;XR&@2mx2W=b=+2LPm^&sj>zbQ5eJsS7=o z%W6f?A62N7%f1~4MA3WVSJWa93a3U;w#j$~geJ7Tadl~kj}UB8&_tOc;K*A@MIT@? z)2{4x4|?>9vb%)yXRq<)hL^#7y=uMBI#56P+zl=uazo$PO{aP5F2D=;1N4NptOnl~ zT5Gd%C=%;4S~#}0@-Rdo|UpgtA#0E&5*t2A72jm8r%VVgq%>6I!HBsd4Tm6JRqP zpNHh4p$81m+zXH-2qg^Isd|nDL+2Qqcda2ch!+AeVnKPfuJs0$!4wHRF|Rse{e|Dc zgJpo{9XroiCkQia0QD!8NF%uxZVg9;4#C=&o#c`RvKo{$yS$zlB>*6Vh{IEjob{2s z=Dl^74cX6(5n1m?Ai{hw{4@K(FLt$5T%WL>C3u#EMu&ySJ|6CJfU$#`AkZ8Fdi<#y z@^kPe&6ue|Y>p?04?w-t0ktE#n_%}J&<%)mQ1Tkz(W1cD@qvw2N)~V^S{IE1<6j(+uQ=8kV+BCd%qDaXU-$uv3!<7)3xMkJ>NixF(?m9$0{e6u7Lj#@Gl7V2YB`e%cKw9B z`&o+1Z=S}=;b5A4#3&VnRvJhZZk@2`wvH2kn<2NK<+@a8H6p+hHgs5c;B zOb<9Z0In%2A;9gL8-th{zAYyM%q;*>u$$R{k!DN~!B%u72aw_;6@r(v(SekKLD@WZ z@KzNxg|eI<+e%;KOHf2x ze3yWz{c+LroMMS^2`vp{I`}!AW#D+E`A#@5FCP}rEebvjrl!8ZN9>>`hG8vYSkzd? zI!bMQkh4nzh2e4|5E><~02V?X*SxcMG91ckaQT;d_@DJZg18%Nbfnp8R3CaiL{Kmw zU&IQOc@pu()(yZp zY%C1|x4gBU8aqS3@Soo_;;RmaXUubNrxX{|!_Dj$WkK6OZFB>%V1#^6J3n{_;`9WO zG(IsRH@0$&X6~{ClbQZ{(O5KtnH!kb`5{Tp%pDqw)#%7iLQc2%oZf=%w}ON4P7-); z()qB+;!UY9xg&u6EIP$f{+=u*7W0^af3Cy53Gigl0YnZ4n=2Erj#_YoQH%>;$RKNK}j{7@O?4Pv72cMFmU z{wyL?;eQEsQZR(t)$cPCubCsYx^wp7XGJW7Sy3@)$t&$m^Wq8{j!NXRhqQ=e7J%(d zXttmQK{pU7k6u9i=w`;$1q@Wr;e5Uz0k#Zq5rMZ}m5><@HI~ejAcSK(!9N`@oN-zp zyfb5NN~15|PiYE00K&=t+hb{%bz68ADaTy*mLI^_*Bh1;+p3MmCaT*Hf$|)2K9S%O zGX{>{PMOOU{*n}zPf&ENwv*0spMom)Z-Y}%s{Yf+Jz?^!;SSHEq^$Rj@HT~S7lQIg z0Q+PZ=hYAHXwd}TKSK9iZJ-2$H0gkL%xo^KIr6TuO2@yElg>r+5?J$;FD;bJY~&BU z?29?f2*&8al>-KkkAWmwm)3gcRthcKA4%crg7n$1fd>r*jDcvTgjT|8gYm@cl|yer z*_{x%z%Ss#lz(a~&xLxS`|Bz03Lj!q8iX#w`FgVTdlWhz(5Flj^R>x>sTKSF9VX9G z+{@8h6G*#GYQoj;F1V@VI3ABHi&`byXFT553HtM{iPR92B*reAar2q~b?|qnD22{D zSq{}AE^*U2ZNIyf8f}cdYS_q!yPbwGye5k?nZtq6JR~P0(YVgppFk>eDtwiks&xd{ z+Ho+v0FwkPM2RYh(3Nl9YcK&Pt_t&W6 zz!kWkAR$mO&H#{*r}gQ$`s)2a+DQSQ+ESM#u*fj=Hd;Ak%79~ASiuTy|CtpFj476? z$YAJ~OYcF3-dj={oY>rmsH~h>kE!t8d{+9_U$>0iar~{x&*KD9+^+O0pzap^D*WC; z@ra}Ww9q)r2qkQf<&nA%5LqQvY;rpy_Ql<9Z9;>={Ah7E7>YgJJxO!1+NY~2d-77% z(ISqIJ7>H651TBW+$paAu<2V>UQHP~KPPnFr=(%kymD!GrGcjHa^5jYh+Njgw)u_5#b-v&pgucW-4(ixDC^v!?6zJq9RaeUMPo_d)m@kt!Ft16+?u24Y|DNe! z2X1CwJua}OX25nv(!FQ*ilwg$eTu)o=PjjLj*+r04y)Qn+%#gN@cqUDl(#5BBI9H~ zQQK&K-m}8C^#pV4RBj}BY`ZxBS~J-8OlTb8*?ns$4fGRfWBxZRj42_**Y5+9EV{c@4_=3PEwWYK1xi9bHHI`Jpxxp2BKm@^4+fq2PB66EJUDo zne6EAvRu$KI$#!A43&pN|6p7FX;jE6FB)QcWFXGra2010yTImXkM)AW9sT_eObV&( zJ((CQK@7L6pOj4AP0kA}u?=6F`^L;&svC${Ch+cz6x8OWr`jz8LPX>bnUj~!3`0{O z@i6+YW1SlmuPW)Bh0EP41Ew8CmM&`U()SG49pWW#5Jxd|JaRYvCptIwvT=4bwX(#| zEMtlmQ@c$-VDtpED{1<#x{vtyQSPo&YEHHnKQ4|@yZR;T4*7k7^Jfr;2>T>fF?>6m zqQzv~sX1bm{cy}C9<;v_A!LZ_n;i5w|7F7_nrcG)OXlygPEbYfJ#L>%IO-wpq@5Z) znuBb2WZgD7g329y&iK1-Ftv|FZSA%A9Pm1`Uv1SPh!^3L5Ia(tWx+Tn5SF|S4Wtad z+h|l+qg8ks?EZn|RGWUNf>l&xSi zsa;i%n`ocf_FQ|UGR0^}Qe37on><5?az`GUj`?4L0+pm3Yg*9G^074PJP8Hu{YV zIzRkwuX_1D>RP%rKBZF4R1V{-n|)ZeE#iM9hCnZV0_I&j1Ih>NaQ8!qlt%==R|v@h!^ zJ(MraLKdp8mX4i1uwHVKa z2|9QKa$a1@dJrbJWvWW~tI5lnOp#i^(j%^JWyss&mQoE1zUO{XI=Q-XwnEJ^$Y#|+ z2+Tmx%!UbDw;o0>tA1?-WWgmf`XGTEM)T*(QrHCFiPe;)ng z1VjtSNLcp19SF}2zFSp3a*p)d3MtM6vzS*(v(5v8Q3LyAPu;?fxSxJ5U)C=A)=0Ph zKc}-K@!#gN$i+L!RSN1-i(|JYutkA1tL9#O_|I{HEp(058c7Y-+*t`E0W*0nOb{4O zK(hjGG}yx_pWN-{Ong6JRud)n>?;n;FI8=EK^LkTW<3$Unonzk$EC>~y}-A%G&Ju? zad1vHIFYC#A^c-vg)R@pV(74Y1-7fL*;SIR8($M8rG6FSb6IoRvf zo|?fXZRS?d=iU}EgH6qurK$;&yTcdv#ZK^Dz8-LPOVGl8`@%~e@_axgji)aCkbw$I zTX59~fLjNIw+^re$bL8kt_#}$D1?!cUhp--X@J*4JicM>f;5!UV4wqm7=i&}qSTYm z0bfA|IPa6W95n-Ioq5@hyYY7v`b98&fnKF6Jfj_qbqA9jrq>=*;&UHfP&#;lR2#?a z0S-voyqWqtsL|E~&Z9h=@AE3WKM=1=eJYSb+MIx4s5>ZnK*ushjDN#-dV9nETV zh3e>ejQ^^i!m_{i1|Cs_tH*`Qhnb_9MGsygc!Xv(Kir>EdevzWV+QLW6M;3Ujb};B z!JCc?mDlWv=7=Wu=e+tR%!PR*O)xDJ?P*&bNL2l=g>2-aUG(H7`;t#?m)D6D&(kSJ zn(q&Hsvbair&Vh60cwGQafcJj|`-`3H= zl`H_@9jJIEYA=Xyg3GnhD9AHVs-0j;vrGG-(UIwQlzokbY3o?r(zwo6mOkd*+F;tp$>xl5|U&|L;4J%h1_}AeX}{556`MX1Lm) zw?0QKthwXg&TJhs{*0x1I2oUTj;m_bDs=j2Xk8#W7)qzleHW2j*B`8{J1&|o`+YRR z@c(^DYVKT>pUVB%fKz(zdzXmOK=J|Ch@jFCXsZjPwARB&J4D8iXIxgNJa?|YRB5lo z8t3ZF*l;r=rCdBdKOtWVJh}Qt=e&3PhP9RM>?-&D^K%4i*Em|8758XL)uiRtvA6@5 z_U7VKBAqp3nE(R&Y%f!RWrLs9K1P5K8s)uojLzi>)`p7@Hb_A%JT~cnA4*k`)~WHs zdzWp~JB4wFBV`z|p9E!Z_Cphw`|x>iBk4PwevVEA6-Z#+s}@=|{%dmMpNQ3)P3;EX zfdDygqOhkRhxnJ$2+iDD)J+((a<;Lnt zrUx~_4SwIG6Wqt#=j`V0DKY@zvN>mZW)mG;c4xl1V@OoFPFAt%3d zYp2E9Ap)^%y(NTQ#5r}`4QYJ=HAjE2(EO7oLdHcw%iL$y^8-Mpm?HNr0N?{NZ)=%_ zKv`40(=fjQTL$ZbVK?CiG+|34L}4GIWII1?2FWf6JUU=)!Q+F2EG?m-#QI+LI3=YtiGguj;h6`=L=2Mda_X2>+_i@g^ z^l13ZZxMh5)|u~9?nc;jfS69>1Mh;Y-dv4YK9AfyAwMS;W8wxKTKl4Ro|i?CGPni; zL2u6s(|)~MIlazTT}~->qBK9cVEiFF*ZYv86#c0jTLTyD6Erj+^#vm^*WIqiW_5xVpNtLa*5qI9#2_PGUMW)MzYEd$zl8(RjL*+Q)AJ>Wwr!&I2TyNCTg(h zbFmFK5h>V^KH$^$)8EkfksVKGDrg51IO)u4#Uu~ zOr#D(8b*-WP2@h1_I;kPdUtZtkDe;b3TRzcL@zuQ;mm=!IS3`6O|B*l@y+MP^+qKi zA@1Pdj$e}xGj`)wkSqYGe^z}M8%}vm_VxJCZdW5#$1GZX0{5&@yDC$r|38cY|6C*D z&SXqkUg>T}HxR0DfKJCTT&2L@-%*T*o4t8?(HniLJY%uN}-Qyyz zcuff{y&<=a*&hHq2;U&+4TYeVL?UbgoxlB4nDd|@7DmM0B9dLS^uc0JKW|%?^qC5*|$t=w7$MS0BOa(2+AaKfW|keAa|(^#NbbxZ(jg<^OH?2XPZL z?T6Qdo7h1>-pN|59;Ou8q}T%S<%^sl3wdS`QT}rZJ4Le5)=`z!lJ19b?{|d0ASTeO zcmX^#k@W+cZMI7vq3Ko4`Y+ebB{!njaYS;32WR^;cKPJ^Qm1*9`aZGo1wvZtd&*Rt zi6>dep(B}NvnT-8pZvG=-_YOa*pyTB--*{&r(aH!G+5~Ei|8OPlw`91jw*QrvHlpS zyjgIuge+cgJ0v2~lp49&M7Rt@>}SY*L)7z!`l0n$*FiE-A0S~=U)?+K1*q@_B9s<@ zLMYLzP8yJc9#R$mOdoFm8pj=+XjF6^7Q`ice#kNVUdB0Yt*)s1 zcZnVMYoM*RfY<|E+g~QqJ^OI0T?y}1oV(--_fYhGaDx5y81AwFHN7!s?Sr=p8tXLl zoWO`&d;@+CXA%0%UYfmv$kl405xd~6uAxOOVu;5PC~XY-QM^tpnq-EPJTRUiN)0|o-F@$&mdU8Q4iTaZ(7VFw|OxkS`3*^|j{yz_CCuxa)TVV({$~7HQL*ooCs#Y4l zawwKMY!OrXZKPXOW|lUfh+Lv9SyjBB^|9NTn3Jhq;XEX@0T*PDF3*CuqYwD@ zzt6Aw;O@gk-4)|;YwCr+4XWa_FN+r`-rr{9$a9Vg5oNuqxIbuZ!OLXxXS3tbFTrmM z$LtIrE8W0#7X@P?&6T2ZHW?gix2|)8SaQ8xGs(mQ+L=B z^#`5cLWF|<_Ap`pyBZq~W9)4^K^x!398oLNfj9h`!Fs|1UiJq-??UU858TuMyB?mLj~LJW~ek)&C(R(1Vy3);_^(O{>_$V6S+zmaLt@y?z_4s}6~o;ob_1a>$8AF5|LZ#_f*Ehp!(ku*JOl>%j1& z)(!W~2V?9<;g+o%VATOLEcnqFRnb3=u>=OcV=KEuy&dM$w~F^nD_G3mK>g2QR%ZaG3}J>i`Sfp#t?J#|Z-n z-Xda#$o)@nfgF^Q&@yLJ0G3{>yEz2Sc}u&X3F}7YQ`lOkoPlKXWMx!MdR;NbN=Z4c zy2`ajs3dfqJZ)4|sWtQp^L>hS04cT0-oMT2U>o6Ks_jVEq7eZgQ^F%0@*1vhcEmKn(q zdYYjYXpeIIMR9~O^3xX=feRsib`kRY3J?jI*|m#Shi5I8swHUFr4c8iJOMe8nB$_ zBk@l{Wf~`THXGN+pu#y?fm&6%M#BYSYQuv8XlD21QvMF<#D2xRZicL}ad|a{R}+8i zJW?Yg4l&1nj%m+qe(8JeFOKKwk0Uj{Zub@T2y-T{(G(E`WLXb8HWg1I|bblgk zWpE=9_#jhc_WfQjAyj~2uQ1ifingKtjqYqP^a-V+=Kj$82SJM9ie zy|LtbnpF|z>7C9jsjR%zOp;B~G9L(yyUSmz>HMSFMSO^UnT>stQ$M8)Hx}Hqu6<&= z@{i|~5^PFLeTq-u88z(r_8pP0y*RHYT0^0{9bqNWTrD<-*#yUpml6U^Bh}YzjUTmC ziTDTNei6Y4Br~RX+nt*>epEHd7N*7dWbshS3D%+$0?lnMMivPi3j|YivxI1zNqNl1v|G=1+|Kcn2vcGRmH-uvwe&-X`{^diGQ4b6XvGve>KHi6N_ zRc;-2Zg2kWUrwYfCZu$E*IlZ@hfsZ8OiYaEKdETpHL{$~YkN`kMbcDBqomY;kEhb~ zn>}+rT^`T323+g^$C`AQq*Y;+W(n1=l-~Si{Xn(cd7QS+%w&;ZNRGgBRle5gw@{Ts zseF7&W~8CYGkg)zLVxmpjH+8fgIT{=!@xQkf7QDF$4m8->YedrM_Tai!EaclKT7;u z(^_x$`~UMWRm^n?QON6)DCTvG!3DDBIUlvU@Zm2Rc9H@~VZ8TLeQ2oy4bQ2og|O4G zDot#g`|sT-#D8V0EWDHi%Vvs-eK z0uO)JtE+cjM9@wGpOyNy}QePA3*{lk! ze_mN`nxx)VES}=ye)88f`0L*=(}+j6(jnrX|)pRLmvhpnQ%zRTXBrgfHpf z;*8a_`dx|@zq#1qv9v?E?*FbLt0ni?s+`4N-_G9I7RMv)OJnP)zpMqyhWZcB?v*;q zXblAz{%H0?H4n2or&0o2JA3souX}rYwJDj^C!S2*Z=VWWV(i-_((P zqV-D*6O%OZujvFuv)ky4R{U4vU$e#_s~m(XHL8+K8MtM22okTB&hrsVLU z7Z()^!Ynb7J_P(HqqYc#7mY?1_xaDN6}U)W)<#QzLH;$_fsDW3KN$}%J5*Hz*c~Ae-1!xUst49vkDIOC^Wt;KKfYUqwOn~2nBpQWq9*mz;xcu(VawEyS*ytS z7jfT7CKz_yTC0q)ssu)0TZQE>gL~4_i}QQWV`N;r8}4U?YWC+|N3%XDO+5MSTy$yU zSFB!y)s#(WPQ|$vk5BDHO!Zz@b;<7H$|OU4thr!N2nVM`KQDh0$E@F1Ph;+9pBb&9 z;$8Dq&Yim!Pfx>Sgp;)Jwa<@bB^O4M7-bwpT>Q-+h6f%Tw`g zi?3{aqoH+=-wb{`%egJa=mM*B{K@_w$YJWN5{_e;!s8!KkAI%3@;bx(`P4mRqg}Hw zZhE7IExc0r32yGa<1n9B^CKUt7s>S1wf@JIs=%S@>21uOZ<(+Bw{(!NKDGSMqiN}# zsdu@9%b+c2X46qbUv1|oVBcN$!bEJw{=?r#%j*uAV}S#If8@D3-UW-5b6uHq8Buut z$l{qe>#2(14zaGak-QxSO42%y|3(eCi1{(wm;blvEoa^LKQB7cNtU@+6{y{GSrYaQ zo0Sb)LY&K66stX}eO`&t;)_9VJMr?J2xBF-Hs)s(>-DX6#~T|`8>?TPH~Am^i_x@v zIVCAJ^^dfXbo_EzLdD+3^{PEtKcAZN6vIh1Pn*(r4S&wDZDwb_0LP4RbR6Efn5sxE zu|RNP(20PPCYo%tNO-gwrNM@5@|W1@H#>(<=3=TGDYiR%4LA+IJ*7&oLoUKGy7Blv z`2v@g7M~y2lGA-yCUqp_^ZTs}t$EY@bVd1^SXpOVKE*Wqt$mqi_hKi7NmieTp1J=_ z+@s$*G~(fm#pRFL&F0hNE?&}M89?{2@JWoKzM7g&5&hsmLf#Jk=tjYE0hxaI+{X5ZpAgU ze41K4B}2f+5VA!1u0G^Xv+OiCYvW4COtjOr{Yv;EGo<`6k=**+)N{N<7_9iZsU^t2zB#bGl2Dzd$t7RkW;S-YC~(l9`o~HkEpPIHc-I=f55i*R;QT zUr3en*5oRWL)E!W)2W!N!wga$UT^>1YV4twem=P@mX=`Pg6kT8SUc3dKdGqnJcVwP zz^Z{}9*tG9mNCu0n@(xzT=(Uu^@N>ANA}3Y)ucldH$$aEVq0tr|Milx#C#@AP|MaD zuBAu(JOy3|pz&YuR|%(D?vfK;3w*oSA0h7qI5}>H==J$@ zUAWg}IVmfQF!Sf%bHGpdhsP* zT{L#$xu*5^_+7?z0h`31p&?=MTW$mM?VCDV<}Y!hw>KuMs&78|H^bTKZ%Q}C*|cKF z7rjomD)ca(EFg~g(}c$iM=fix@I3bT*y&iQa7SC|p$$z~%goeXi@GSELkkzclkIC> zR|UiKRW!B*SBbAS*D1b}?2SLG78H$)R!=lJH>F8EZ1E4>W|h|RzWfoZ@lCpkHm^}K zMv=e4PxKtCJdb`0-2S^aLb0|nBn&yU0 z%+45d+Q>lVy%{>;|62P$6xvOQ6{Be~949$d_}9EjF>BA%@my=653O4K|H1w2TB;oc z?yw(Hm7z(UfQB*CGd)D?usfTe*<2dD)hjO&ez1BK`3;G$5i=WQ7TCFe>$7wW{WY97 zdb48aZ`&Z(P;j@WuDyw!JUy8pCGb^{q2L7T>r_R70j94BGA_Yk%Dk5Z()>{UFa@7Q z2+uJb^fYD=4vZn9HA|G)MYSn7$I`yU3IRq@6qNKx_bYjfe<&z*DW);ej^&Bp{o1F< z)^x#!M3A4_$XvA^Ze$Xq3ZxH@Cu>>|ISQ6aQLM8XR=h0u&!Hi(L9yo>zUQmRt<~vs zLbz5^5=f(bz&x+4sPN_VO(BXs={-9jmGX@@$xA>6pa8!#<%KYatn|#2X@mZq^{Mg(&-KN~rS!`h=u^mU zgF+w<7D_1Rt8&bDtVVNM19p0`kl6szvxTY+o8MbZ{vMr^@1nRsyMp7V@F7!v7C5WO zse?V5ERXyd!0mVlX>msTV_h>-t-3;at@IE_lRH=(Q7JR+CbfP28AReKJpIW9jt+;Y zwu)#j4H_y|PGm1BNm-4!3h`HXTUC2>j7%77&@8I`eQec&xw*MGxYrc7F-8~OUSMT1 zw;C#NZ2M~{2NK}_=f|!wP5MW#6|E6umM#wOeEDboh%S|1sFhmr#ZHR+Lp?_6W{6-k zf@=PU$EAqvP zbuvAtP2HQ_f1m}!ER6@m`gsMpU`PCxb*hESfnL!qrqNe*=?IM5!dUKZz8de;>TDh?XY=OM@hMg{dw-K85MKP;H ziC;E|5QM7whs#^3pIw^cKj)mwCIkOElsTEZxC>iyhaF|*VgR6Yp8hxQO{?N}?>XZC z$J%?xHGOaYRj3GMC7=um2!xfZqCgQ4qa*u!*ZL^ewRO-#-2-h0VSJ)ixh(@<746YW=UCL_bDn=KrxZawu)Rqv!DHBR}{u@0{Ydc1g@!YGP!}myPMU<-&Mo= zZo5^1KD@CPL(X;P2iV((60#tJBO0T1W)IygXvpy8EnB$YQ0bT^)!6PnbdcMeS!008 z_6YIB3>_VM*B1R}Gnypzezq_A+>lbe6leCpm~!Kx+iJx#9X(^#+KQ-M1S_MU%)t|y z%bu!u*SxfY8(+lL(4DQjA|1UH`a|S{1e15dRgaa}Nl`(hG7Mx%8oOAizhcb^USm_XXlJAFFs=;h6}8`uQn%Zc%E!GxmU{q;#7@%JSpAIgJ& zZ?}xYa`859lVN~Kh1<|3ZMPQy-=z53Lgc;K^KHmjI<@T@Gc-Vx$2 z=gl*`6BSgj_+@!4cFZK^_rIODu&^+a!_P4jH+0VmH4JjR+6ELfkt8*^^DQIVyOgZ5 zV>SPRlKdw&3S53TMLj&j9F4^6Y!(VBoZ5USH|0>AuzNHG#-oiRmTEesG4#w%yda(p zscK1#HKdoa$Bn}7sD9(0X=PVc@op7TI}0m%`{GoHm+Cd#Qay%ANL@G=+|?Hbh``Lr z;>G1k;*4I}Hn+==Q(DkuGe=@m(}xaH;W_?${zDBnT%G`1r8;G|!Cx)Kx=OpQl@qsA zA{R-Y2@9oW&C+Ma4odn^mQN@$AOR0tO@%C3smyoE)~u-8BsndHJiuK^$&y@591dI+ zZ>vBszMx}O-^a&Zq%*UfI^A$ zdD%u(LUiEl4xZ8+w=KVT-Jj0BS~l)Hb8~~)Sl~h!Z~9sHgi2BPqCt6BK*2etV>Mr7?>wx0pIq;(mBn$6`r*9AoJ^lv$B@%_cjbyIW7UNwdy4 zr<9TC;l1Hq+|JItERJUllldC(Y|dGf zlC#)*;5xKjakjSkb}QE3pH3rC2Qu2a*|qwHiKp-xV@Lhy`42A^!82r`exqkkY!XW@ z^8?x^*(we0r}9YS)b&Vi0tib-=|2`~n`HSfd`THWY3ZFiFWf=(rpRlt#2vqy)8yh} zi*&JNbaXVw^(B&@$+}><{2Kk4C#(oSFbwJ9Fa3M9Fo2i9OMX2+XzNU3x>7|Zp+cm3 zWM;^@aFHHu`5$K2dAc-mOR@oqM%s!TbKm!1ixlfhj$N%#VpBYBXW=Sr=wZA+nQu}Y zRs6jyW^HP==uoWLR}x_hMH0>&nO6M9g)wy_1){GgySu`H02eO=4s(y8L&Hi~EZe5A zA+Xt}lFq{V^lVJC+=;fIyKlsHbhPYx5Kt+ER$-eeKNdQcEq-F|!f)k_%SKJ|RBRM9 zWeH|i(gh8l)<8juSMwfo{y1uYdx7j@WXydvp;h4e_W+yTyY=bT%awN#+Bv*zlg11O zk9Q}Ce|Cg>72%{9RCgPKHn8QoK|-m1{|#av5czEvt87N@(~3i7fglrG;A0jZe49)U{?? zw43N80>GCPqMqu}8JiSLd52pz`8NXRSY6bGOuf~G-2CnGWBOi<^@WVO*zNM=rCD}K zgyEqkQQMDW@f+0_?-)fb?_6|9bx`kA|J$bLFl!!yY8Zv#&*EI`&HW0w>q4SzkZ-jQ?o%^=W(29nx0*54^ik_AJdh zfUSNvjSAg;lGTHG$NO$@>_R?O-WA+5@B&GoysX}`+^gIe3X8SXW8(;@$f)5G-RB3@BLWR}no9#jVw&I*F~_H>RTq^m_MS5sFN z4iq&!Alrtb*V0&xuh^||jrvF(1rQ1pllz8_1~PF7fgrhx?KF9!)Zl3O7cp+Zhq_%ynDOt)8V*-T^tqj>rJ&v)7Eb|XiP{|M;oGFWVTp^X;F|n$$^Z^ zq%*Wi(<6|#rqaaEppS?<5}VZ7R?zZX!_A&WFitf+yNc{5Sr}_jZ)7WRM!b2S$ zO0W!X?g5%dzkH74Llxrq`MZMInW<4pkW8fMs^P>8DXuT6QYhb*vq@gVR)L!vq3`7Z zZ{rDnz(mKUznmD;a04K~B%quYeq+bkxPRlYSVRn3fLA zQ-sNt-N3tjqD#UR=rgBzIPumMH#P$NhmapHZQ^ZLxIe!Qxw}Q(SYtuqMj$CF_uKs zS{>vT|JIG3tvh>p&lUC6LB(UrkjHOLiFXaIY4?`O9ib;w-cGz{a|Y%NX9TBKw(hVI z;D0x{cTAT*RgvQ~+beLZ6rQSxH&0F1%+SUtivz06+T`29YUSP=m8Vw&E|jain-!RH zJN~Vvi7KY-BIontM*n%=7P}XE3+N7S-LNM*wXMp0YDgx5N;gsm8Y+I+-k4I07TE$u zv6`P5A)gKO+FQ-FJ=hy{fUgz+{3g0kYCB1--Fy^a3%%v2oLc>v35K}Z|0P6(;I=qU zSicdJQ&jS?V^`D@_sub6@ZJhsQ-RuEt6hpu?X=4R@X3jeNuE^en{%>34BbsU-5B_? zp;QlAW2Wnyxy~pqM85I(KW3E`eM{XhoQ(3UR!NvpdhP+WMW^SByA=`Af@mQ4j#q9g zFmbDEEho;&l^=w_@&c-lf%rG_k2X4R{rnCl8Lk!H7WAh}HO*F{gMlCGuDKzyoau;z z3XWlTqEC+o3{J`57r@#P)cRk}`}I2%uv|+b-9${K-oA&d!sAI={uVDuCXC`o`;1>W zdS=D%iP3cH4U+rSXp+4zSu9qL^m9?VD*x;7j>K=E?m%iihwck^Ibj9?Clg4w^6PPm zXJjC&A5y^&?hx1XD&xz677u>q7W`z#oChQ5Lr0md^G1ZWpa39=Cx74mv zECw7Wll`Xa#zr6Qf)>15)klhW$>o`CSM#QM7xeo6)q!Pu0Y#=$+W6^;v+Lt_?)c*o z2G?O-q~ig06(izE@&#i;?bt(ee7X59%nMDLz8BT_w0dLyMkv3p#Wk!7oD6Q^L_2Xm zuyFV;g!p)Gspf4GK;EB>exZr*dMgs^^A%A!mHFL|H%@NH4$;>aR`|NYMGIGo=uIEn^&{7U zw5!Ef3?Wt)jlg^G=>LKFd`>HN5C>b?r02LK1elw7`x1TfGjR# zuN%-cfM|~sw=I4%+%Uxa3)55WOif-M(C=}x)G<1BDXCeyYWEZEdmz&v+AcS85Z|St zKje81u;UPP|4Y?6IB>ZE%7Is|Vxe!_U!UU*COX z0;>Cw)*m*4h1}T4(hufz!~?4*uopN_(7cwiK7?j~J3q(2qJ^@(&vfo;vXI7H^pZGy z&5a1?yBopsV@>yxf|3~eyM5&mB<^BSgO@GBoWzl!@Vn}v1?W

u-s?rdnJKC@zt z>34t$aRCT5MAzBlGZtz|Z%7rz-5Gx{r)8?!PHFe0)mvQ^G*~rD=+Peo(FxfzKp%Q6 z6kvVlk-E3$wsYgHgRp7w#y774;%H=8H64{&H`lR8eJ$$+O5Y2kwJdNe&7f2cP! zQ#{H_Srw5kqrgCj)BkxqpBd3NtgIuImK5=*Ym*jmlb_mE0U}cruoh@?ZKNIn(E0SM6!kk>W$FY@DsMc1F9t0^&6TDMnVP!6`|BKZH~Ks*j#o=rBCIV+jWLD@;@np0 zgjCab=f>N%8(t;uxkSe-X1^Ik0J@;a&u5B#REFDG(^1@@>vgL(AS#Ktt?9<7IGfTd z4N+=qod`#gf-+0KNirPH$#!iG?Tex@{T-u6OdbC6`~+*6E?ia{^AU3`z@=Hyk)yoc z{k@w+yZ(n=(>3isIW+!-)rK)Y^nSREm+HOdKiw}ea6L^FjiElFQO`QQs88CdKVkgh+WS%nYx;mvq0+tRhkq>R|dEmlG5jRTM>7YoOwpK^Xb**H>kj~qY4}Jfx=rAfp5CG zCaPLigWa&|Aiu+&4m-@=s-$=@#{*rMI*^NnCm4o+*OrJY>gLA9b-GyqFMfBU9qh3R zPyQ#8(HcYw`cSt7WcWX{dTH|h1FPpNzax1Z2q2#M?0wNE!_P*ODQ_35RVClziYjFO zhxyTUb;)&Rxl`0(RjI>*uTqK!r10?iVgT6*!q<#F+yvT&uaY!@9gri-ED@H%hnBbk zVdg_2JL@8kc$a5!x_4*Kx5;nTf!fOh{XW3z8#Z9t1nd z;*eRwFtF8^|BH<*b{xv<$+MJDcT3nOFBB`IHb790O?|Tw1px$6a{ydTpKG#EIlB{h zKZUb+EhN|ys2*J4Xy@Y6tdtinB=09X)D!cQ$7bvDsz={h%~p;l*6YKMK~!eQZ7()P zyN7f@Vy6Cul_CU*Sd$fjq@Y)uSH#;Z5W*(Yn6|F%l;xdNtCx{&UB6$QJ%PZYbF!&%4Vs3DK5*HQ`@dMi@37tfgoOQsF>Zjj38Xq1W1(G_^ltV* z3_ET#Q7SuqgnXspBC$j0Z9?j8RrQaDyrCC36wob z-ZeK^wWh_JdcR0EU`&7N`h{AwlhC5C>KI^2d)w%#5Z<+NFC&OuAgtn=DI{nZ9n}I| zK~uxAzK^)YG}E>D!!5lfLq;`#0%&3~-`UsXiYyT_TM01KqJW2ZTgl%>^S(iuGYGG!fU6*>)jj{WyfD;ex;xzc{GiZ3&CtJ(2L!1DL{TGtI}2 zy^9-xiQ4=ZSHd%vnQp@NMNUnsOj4|}h?P?zn>5o~4e!7u&y8PVMD}#UdrwTh%bCB( ziUoyHP(h!MMb$-#K@_Cn+`NAluR(TDM0o!S1XO@JYNM!j-W%Eq*!sY)Eg^^g5Oxc# zUXmhfITk9>Uxy3?3@~dMp*+by5}#ETZ*ogjUk1^}+#`+EsWsuY3SphLr>iJ3%G+?x z`)Yi{|M4NV02K!EyH$k&GmmTgNoF*lEvb-MJtNHq|050*AMk|PZb*mNnP@fjytNBquIdPxPTp(_bOLw&Lh z!d&gSRF7|^16ms&K|ix6#_|{Zn|`{=N;D)ms}bfj;z`caF{LLz>wqElpJV68@HUaq@DxuGhnk;_)tt|J&A4KI2B{@}JNujhCTDj4jmB*Q`DAC*g(t(~ZktG>AGa zHL~hU{qGO5@u=6;mdh=o@c_z69kco(=|#8XJ!;<0q|ZQKgRG_d~NE-T0(xb_8Yqm9-|6=5_>9RA|0~@}^ zbG*^$YeEgII{nODK;-~qNzlfqUu)QE(05DDsR~~K%u+~>cu=Lr;>i#C!oP`JlO_8^ z$s@WHVL~aNfR>lpKy%&)w*y2_2NDJ{`o`y#6kIpP~TcY9C}+0({QkhI$5g6s^*vb9RrKm8OYbs?!J9`5P&Ba(*UbX95J zdb9Y&dSI8JufKeGTQo_oKfCueT^lA#LlVzN8&CEL+P#I`YTc?fi@!P>9Gg3-9KDj4 z)zr{8Ai(Z(iTS{q0>HibC~}{`l~3)y`0XlC>{(cK;k5yy3)al3kEG%}S1V3ys+{e( zHmBO!CzS{)1V{SLtP?4{`isl5wNDYw^8Dup(_7zcpl)_g;TV7%@kXl0xX522=qG;f z501MnPkR?Hv?ecLG3OJW7gP14Gpe1D7j_$;2dwh0uWgMX7J=|@;U4QZ-L}Nnl8C4*hUv;dLGSA|sjDE!BJt#19Mpd5p@?1*-x^kbEu$)<%;zg3 zO?QCi0VY4B`i9Ouq<-UCJ#u{+Ljdf22q->+8#LD9ok?4R*-53wr=fmcPLaoSN|lT9 zt`sL7Mlcch|F%Jxavhq(dk>{H=M(rM#ia`*$Jam$KDdKI03myz6>W6RCw1y$3z<`{ zar5cZ!T_7+y5FcYwizKOaeeyIe_Uf8leqz-%3~4jg@J!KWKNQ=fln*ktSQa6P)YC} zV0othYR5`O-u6V6S7-2Yt0WnP`Y0eWa#FTwo3rcVI$4{ z%Qk>54a&v4K3Y_#RGzMgeAuXT676j=Xm9TZnAaCdiuz4WV`a{`d&z#bLHAJ4?L9L~ z4pe0zoTekd_Khj9dmd;VVaeb&eqO)n;!LmTVmBfyRWu*i6iWIS0cl`2eEbm%-#W_I zBh4l;E(8sC%%q3A@R5h~yJ7Wutb$g++4a9)7bzzmlE@W_vE2<@5yT9B)16YraT-A7 zJqoISYr>MFqQnEL(k*L6{1MF*>jBTwr!b-MbSilWW7;zYhq1)t6>RjF(>V(9ylh=u z>rxg=gV(b#r!86^fd?uO6iXq<@|YTs#J8GS6QVlp(A3%n(m8k@y0BXOW@305F!!7= zgwXObt{pp`OL*ZJtSg^wO^3%^Cs`9s07}li_x0SfbCZw70*X|?Q+}A7m*vbGxf5{k zzn+(b6(*jJa1|EcMo(S}bC0{1zRCwPf25eTL!Mo=*@|$@AP@ z7Q~LuHUwtLYh40f592%;y_SDAp+6jlGi>^Fre{DZ?vs9|+6J{4ky{;>d1}TF^?$GM z!+?}mI2`569ijZ~Wq#L2$5eWJ;i7pC{%vI z;guQ^A!288?_ATfXp^z3X^4JxVSR%35R~yyzeMaBXUWDDzrVTRaB5Jhx_cv`86Iw+ zJ|eo`1h8r^7L|6;e#t7<-!?|%(T3P@5Ow}cj(HjbkF_QdcGbtVGHhzoH<8<2B(sI* zh63iAx(9lrj~NkeH9e~=+5`#&!w{YGm4QJPI!#W-Y1uiYK3eQn!zxr@f$Jl;S~SZT z@Ct7{TOz}YweX1P& zPCjs<#W89-&+UtV=aCqX|6#Wn3_(otSRO@u-&%@F+8XdIgA)hsBM_o>klvson0r&8 zukNQacl(#DDaof9E^pH&JKTyC=|38FOTHHqIoo1{Eh-{egRI4|ks@8JTJQJCCv2UX zf#3nu0H|c(_{IVQnA?>wFSzcmP9)>KX_Uo8=g=C^mK_^Y>ji=fC$F-w3fWQj}FcV z*Vny`uuPjl{13^(H%VGgwx=}M;geXO9!HqUWlt~eQdSIdNuv?0u!;LXs&HHY|42NA zkYC>TnQb58x@fWqi^kJZKWVNr8++m#Fplgez9yNB_{H1$qe;#LAT)u(lOenu0XgoA z3d7xH5Ct7PC&~f1ZgULIs_#DC=Qwk8N)jrlGn9Vv1#S<6#(k4Hf!EfdfiVU0^OMGz zGmr6_Sd_622FSqbwK6L(P@J>LJ}zm>JrKXN3__y>l6SGH?Bs#j*+>k)}DA!!#n-w9V3lJe1nrm%B4G#uhUeW`o6Q=d0~z6E<<#xB~n$(8|ut{FQc5} z!nPT)ujwZ>0BeKa1%QZkfQ=_fF=fEO;%KkdfH1&rtWTA0=Gz6QO3y3|_kG!Y3uCdw zpD|4cOJcRw>)YiKm7mF-A$W9rkdn1XS74_t5_vdY)&m>IlWvSwB7nem{8z*`M@*Q* zPd34}jTqmr10HD?CU}@`x?Kda?j=o@Y}c|tkV`gsgg4vpFI#Gt`PweIUYKSVHm?S+ zMC57iP*UWp_bioEEbx9oN0Nc+2__7;>PLJt2V(3GV!NW1kRHUP)y-tkxSJClgFER< zmKO@;(>{m!sy4L7@oed8g=>QDb9c$M>Dj>j{pZoF`O6cMwFoMia6m7BDogZP z@;V9XKq_MoxN<4c9s&4ldq6<$Y8xFT9im9;9xU!HG-H%n6JhItt)-CJAEbNc#}Ek* z{L+?RkH!iVTJ#^?GtI2tzSPf2r^|yUGvLHwrhn`poN0QO_hhStvVbQk~{d333jq>=Iv$ zpr8NeV27A{BeHPa4nGo9_>7qo?)Dqxnsx*7gc`54C@oZkLtT;hUK3stmlIcH5ylH* zJk?Hf8EV{A(*~cBwh@!Vl09pAx(%KuD8s|QR?`7G41$=}lQcTD)`H(0QiaEtBP1qd zxvR}@mRYmOAO`cYKq$D0ae*f_eh)q(xj_MyqgfYC7uWks{&lr@8IW#h`|ut5T8XjT zYXH{8g&@2gqTPXCMy2x)>Fxj>KZwA)-%K`WDxQg(Q=#hWMhP4v^~Fcua|3SU;9Eu4 z=APgGkp%^9{;Qe{LzT49$BLThu0({ugNIFK!)YOn3C_CWw8&u{MUjdm~sZmUQD zwNc;+5<5c&E3@I1-MPnm#ukt4pd7(w?HpFLVOtMmlZm_C&^LO*wuFYD;kmeeut$Rn zd0isNk%6og%taJ5;MH#Q&{YFQB}W%6I;B;C@6)%+)55KRUyY->*!{&pYRY4oSJ$c@ zkc>0$F=a(;pHFHgijrMEi$l0e4ci*)JWcMc>DG$EYkH{X{s?sn+WEb$ccBb&d2i+> zF&}d!xZ=qF)nQd0Se%qf0{Fy*L0lHw>ys7m;)JxtsVL>Ef?_BWt9rS)`*c?QEzm43 zv17wf_jY7Riq%eBFeInDXp;FO~S7 zxvoFX^4|a);hg$vYAv5@@d7SBMTX%Q>1>ErF%UIhXhy(|1033|{Nc)jEVC2yLFv-( zzskC}r{)Eyo=u<`cEQTC5cbM_fV=ES*09~sk}hXWoPgV^|6IBd7cGw;Zp~KrE2z66 zLda9NU6mdM=x~f6<3Om+^&6nnX3r&@oi7DoEvS<-+4|pXQ{VZ8ZK6k0$Dc-*ew~Bm zumA6Mr#D|9xFm=^y#zBMSpLF zDoY(`27Dh#Asf+VCFCa(!|MHzVhP~~xIiQVkD`gW<7?ny1b7X?ED~Ja->$vg_KGET zeoREP*u$+?2QogfA=rutfa;ij*UDL*CwxA?jN8EcNWfLwOV#rLIGTmcEr6>AmUq*CGgjnX>BO|4k20x?t67wMO6CUr zq;vnvBkC|qRBI+d(=Q~O*az$_`R=aDzTt0@c7yF{Tb=O-VY)2ZuzyqWrvG>#Lsq=H z`&wkJk=>`Fi)?E=6$_1(6?3eYA5S_>saXY7Tn?auE-{yip=8V zn;OCM(v=*yRS9Fb%={+TlB%~(z^V-{ec-&qny#NtMj%lD{0Tw3H0`OY$~KNl;$(rF zx{jy^EK@Zx=xa{K1aJ#&csGw@c|`bLx28W*MIAOGc&|8X*_5d1rUSkk5rrGuzx!_l zX3QMad0 zP9h*eJs>fiQgM&gue`Ivf%gD}wIH9{0ob#9L40RMQheL3Gdgu;X71-49ns#12nSCGjW0bah+{P z?SN^y)}&jV_rN;(g-K%6i#;@t9@TtstD+1+6;fCK!qf5=W?P*tT*2xa`-s0D%=LA2 zhZKxMp3$~^O6rht`m5rVVAKJK@V{Cz>$%krk@x4$&GWOQzB;Kx74=^Oy;sPv`io<2 zdUx*7&S+cV&dK%RkAFUj30mR^$*J_jWj)YU-bHBu;%CR^2HR$6$_5+17Nfv>tYD4^ zDK0W4vw7Ua$ZJ`lm!}5c{Y4`V9Z%3xr&%3O0!V*v+a7fw*dU5yQ+F$)5MLqaZLeRe z&){-j>SK4!Y^H%Ps}_Gn{cOzAoBn}&1g8_!f9b(Tmhbe~&C6@16Z34!9`E=!*f3nY#=Wd7wS)^sf? zN8Ozk1@vc;5}=fA-5I;Mme6hiF{RPtc*NT1Eztm=%^aMywvRfdz{o~aiHL>B03Zcq zf)M+&eXgT`p9R83iFGY04zUqJUejvtX%B!aT}gT2a`I7}s&FDuLg1^{Q^Jc(EU4li z9-7Bk){MP3NcoXLR4UyBlH_)i_oR!RZi;EEHUNzb3fqeO?vc0-uPpefaY-YOFHSd~ zOwAeUL^q<$tB@NEfp_n=<%4hS_nY_!k57we?oEKeAy52VA*eSj%%Vj0-<;lp{F&IL>eJ*4 z|K|$brCtj@+xsSVPIt@yOuT|m0wO5*H6bV|hC_RSG^AFG$aw?KS1<4p=R_UG%~Re< zhsBtMqls;tV{Y+uq>dPPC;QH*1}q05a{x-W3|m0ZMr>$Q%g&fg5Kn8mKld8w9eG@u zbzEq8Wl|-JLxvZDX<1z~=!RbdYgG`~B65z;)txLE5$1 zz}ho2BbSh64f1HvRwT)65ODp*YoGMKc66Q?I;7sAPGXxNq&(7qL`QJkXy<7!h7pkN^O< zqV9+We_F%Hi8+S$WVD3W1D5d^r4Kv)?}b6X7fvN}i>&=|VK%6icl>T?Q(rs7S6d@b zOz`r2W?Hn#Vchhv{oWu=PxTnHQK}JXCm&Dpa#(JPSP>OyR8x0j+|8d@#@;#)P{h_F zY|?6T@Gt;-=$-}q(J2kL%k^NlTMhWwYtqFipL1c5Iw1&bq}To>4Huamc;EEfAtK-BoXG^^p)A^L4A%)_Ow3PY zWq*{3QSjjHdH02*@5E%j=Nw`Muu_1B6FYsWH7s1l^&I20aQ8{+(6#Q@dELF)4v00N zZI6RdT*I4vRqWWjKCql~snTRFc0>_~AB0E!4=ZFv3^>&5iWfsae-bdL)%1Y#x#-2J zX~^ejKaF^yhU1y+h(w4V*yl4EGvbP@J1~>*{DVvw<1mr;np&K)+pfmHbBGQMBOuI6 zeJmh%flGRH98W^jd+9bDm6|GpnSRKs(i&8}&OwEQbV33#Ih!iF zKRf6*Xs?)mY|fYf9T|XmK)QWE&WZ0nWnp}|{vvVVkbnKf=rZA#k*}wsF0h7-vKE2# z>Vz~2vKyemJwm(~WRfssJhLKkF<8^Rcr1twMBb88uW@q?!lb^tK+;HAUpRDsCYiEW zz39I9lF!ig*SQ)Y{|Repncr&>`mreuk{xvBBP&~}h$%f8Efov6RRPw8W@=)mJ0@p- znk)0@6py!L+gmO9r|(3bb#yIQy=_HzocDsd9P~m!-!i6cT^nT?`bt*#VGb}h(*8CQwK4UT=n8Mh+l>CjCgZtaXmZ>&L3|mp#>}aWHDzlD^z$e_?;{y z*%rDww5#_SV~{{Vfw(V~q3&$gTd|N$O(z68~&6{4*oF7*@nJ)Vr1Ue{u9X+bviWY35MiN}-JkNL->I^{q5!gyPx` z&oS7001neC-9_ZkOIf{m_!qD(m&T|e^4lM2Kp#qf^osb%mtvX7cPF??<}>jevPpt*K!fx~Mj;45-!hZ5 zIJzlkTiLdV>z0;ON-1FS@ zC(K1P0$8#O>Jmi}&R$T=Bb1~Dc;=9%6Su8{k9^?$F!Df2POnkHC+qCA4o9uS$YejUIFzdc1*wU7qgy5M=hF%zsV442FyqYdISKo-5 ze0wHk;$l%eVlGj4H!}SgWSrZrc#Pq7Gl&bxwW&oWsw&>NbD&minrq-u){U@rJWbDu zNCLi_?rJiq=rw<6ldFcOj@iDDItfyjNo%nR* z-I3%zcvSv$1Eejz%(2?5$H&WbLf!KEm_eNkpANlGJvV*qM0$ml$LdeUhT__nN7u_W zJxSKze*k?t@?+V60Mup673ZwBj{mrSW5=eJ_nufc#|)~_%OegQA=t+Ldjs`UpZm6G z#D5??`*POlEp4LezjQAK6K^4}9h6yxBSUrK-AygS{hhr-z)9C+j0L`1!1;UC8mijN z?^^LZ&ogp!yjqqHCN$bQdi*?D)`mKabR26#+&bPd>ix^-&%>dZw(D29E*_FRLGvg+iTO~dG zt!^;0k8h^fp73-Z`sYJ#He$TG33?o8a+Z0FidRXKJ4Y}hyj~2DU%0S;kdj&vI_xSK zV>bA^v^4*~rmw*wK3+T*+-gr_@%+uaE2QZN?j2*Woz8v4u2~}{XjVQz)H-w7z|K9v$c~ zXjyh$?A$>cfsltd4_`%j1m?>D)jW2M?9GI{#eCq>F^eJV=-pABEoNIN!N$K_f?d@` zU=ZKmlozT54e{fw%~011u_F5}Eh2Asx`y+fTTS%iL5yaNlqGXy4|XjvqUCaIXXty~ zGd8^L8v<74huL~#>4_hx^_H;jo8I?*!Tkt#n0dK@=s@8JSh~#lG%JzPp0@M{x?PRb z3wy@G@BQ&XnkaE;-%`4Qnm|yxso1hBL&qrbxPKa^OExw3?eOtKcxXSTw%+ACZXtK_ zTwmv{gHJMl+>||IWdYY2<{s}2+%Au{TwFAJUcoG6x`w21n{mOzt@)wcY%wGXD-tdI zWlghQIJ$%l600&y=bjA=3{4N^oWJ7G!;)m|W(!^*;zi;&%<>ArbQy(JSnF&kn8*{=1B`29T2g z^2c)RcFnwOQW*n%shjG;mnwP0SCOhw?#2}lwK3~hBr`BUSR%am<%(K%@zl;rmqZg` zIfvJXdjZmIs@x|8W?>~fpjk;dxj*j$?$W9V;tR))SlpHLxzgbReNF*porvU5IGV~x zSQi~3`G$Xbr|MhZ-)!oF^Nu@_zLSTseNRP!EiD{d-nx|Nr1k)fCj*1+T!GWPA}i44 zm3!|k3x}Sur@cUT&1K7R?l?zQwq;~FnkAULDZRBX_wk(iSv+WS0A?iGCEI{en0s(g zyiGMWr}F(;qI6L-J^g$g2H6uPMekMniQcmb&K8<{UKjKwS@F9~QSBk76d%O}YlolT zfkpECX9eBK0FtK|0a(`TlKHCgpL~@A(SLhIb7dbrjr$jOjo7_>K}jdotl0V3ttM|Q zxY~VMT|kJppZ*DpWKzy+d1j?Ti@{xC^+L0@+AHv6xHdOIIvJE-@$CZbWY(9GD3Z3S zy~uE`>*r%#C)%I>+~1b06&1fo!puNRuMHr=-Gt4@YsIranfPZM1go_ylCga$bO2N# zDK2roLHXJ=bzo^5G!gsk6Lq-XvqApv;nX}Z<_JL-x=F490sh9vaBZ4gNoTi01>$qb z2o>91qi_w9wUXs%D198YS)835O==9e-VR}Q-J72kI46N=K`VVpjF1F<|BxKwqL*=U zp-1O@MnU8u(M9KrBOhDn7O-!bci}gZ6m5b*VfuDWT)h`9kGv-9Me+R5^(A6WA|yWL zy;}C-Ir{7s-USaMe@qoBD_;$6o&Tl3!H2=4 zcMaaJ6Q3|MHp}E4-inH1oWH+UJC#Gq;3kb}SBB z;Xz|d;E)IwS>^ak0LcR&wSFA*K{GqxHAUvbcX^ajy43Ht@gS} zt1Dd5C{y(&ZSX9B@p2r^U!P>@82gjT%j7mKW8H(Q(x|PQb#m*js(XKhjW>oD(byf7 zfWFY>#;0>P`u+Ck>oX7P#s^mMs}P+$X3a6R{hlg~c7$OlAPo0M+*4a4+*?J{sp3bL z_o!sLG;qbyl(5UHx6D*bH&L^+hSYP(MAyxKL;KB#T>hmz&E_xu;e|C`ZL|G@UC0SfeA2o&u#g?b ztb$rL$XHZW@dk8pAXx|>O?Q!tN?wR;+to&Ov?wvEBK7 zYq?jS$GMpdLwA+qdgx5?(;lVi&(g42apd#)0e41^1;&ypxH!(|Epw3@22>rS9?w*z zor~uqIhV{(s{5Pv={|yvPQQAdCco@GpA|JUYXh_sVtxldF&+W_t|6YbZC2FvRJ-9H zIcVUQXV)YiHD3HW$Qb;f38gQyp{$2e3G(oMrzgk2q=&T222(hotjNPJT4e*Qu|K?g zz{;YaM+=?^8qOsi-&1iH0oaNyb*+y!hCZ)H!9TJIvIiz)5=U-lN4X2^C;h?t; z^Yzh-m4Wq4Y9vA5w#PIuc*GHf zC7#iiWL9l-)9Y=y|5%xI zb!bVOF{Ma$g?9_7}k zfGayDCG>s)1}W$skk7KyM5vd26PJuT@nu1~y46C6qW8)`3~sM`_Uwn!t^736C8&g~ z)gi?F|1f6Dv!kwrS^hp9UB#yt>2}G9!w0W;a5}6JTCxdJh3Oxf z0vb&)Zpg4Ill3G8+3I{v11ZMilGalhNdzKyI;Bq8Od6}n~uJqolNlL`k^N61!2IpT$N zr50RI#gXQbQ|CIajo)7@=a8F+dr9k)_^CWttg;iXLqbj|F0t6&M(J z(!a5S`-(1iiz1$svd6`<<-V#fqEZD1Up_KEqGGRw5E$&+n&9Arrl3F+pU;ev8UXdX z)S;+t))Is*2vxN_@vh2B%n4%`$BB_RkV1gQ2S9-RK>bpi0bh(GWfY9yLCF9b3$$UP z+}rlc!+_x~DS-^vj2DMMB>R6HGw&I1N%u}^$xDo{J!^T3 zeo$F22&6m^a_l~|LZn?1pm;Oelkz+z(=&N}vos%< z?J8?PSOY?;=b2~JK=NR>3uM1)prX*!J)U>0rDFb~v$c*-^ZCutmEc!9a>71~GgP}f zE3-l|{{8JT65%DO;$e3i3RFMfx$|z>b1Br&^>}(#3NerN4`@7(EI)dC{L)B)*U*tV zQxn}S-@E=JtG}t?Svn%)VsPT1&*oRjXV*X4m`7fRna(Jbc6kl#uQDm@+lBQzW?kSMKwx}8ojomweL8`RV#))d zGIOuuB%R28R!;1VAO0vySD3R_^`@K&(whr;72Bhh+f^;5&eI8*ovM@FMJoaPN`p*) zsbMv_Dg7N$SzKF@C2N*Ymm_eeyz$O-QlY7zH4S;P_2(hPk^WL1jp@v^@7|)+o{9C(fgGgiG3^eX>DkXxn>c71;wGNYPY5$LQ+-w2>*I9To& z2^CSyj>(S$AoeSo?U`urZHxT!=Vdh=+i}*^WprU(W>5HIEvy&x$_0JFxy@&5v%62A zy|u?6g5YBa@wRv%L6_7UD3(4r_uQdV%SmH*AOn#wRE9z%HXnoR1ssCh{v%ju+A@(l zc`9VIGqd}OHEb(lHJ8~COS&I_J^vx_{EDbFXx9YZx3MLGf_5DpY-!N6FP8&~w52x% z#P;qXr?<*qbsz`AONyg%zF$i$&Kz&BV;F+W&8P3%_-}E?UC%>)d`csE3u0;h{TNDB za&Eo+K9%-pYmO#dzCE+b&Hko~=qXI2(z92vKIc+h_kG$@3!3Y0xu(Nsa>e%^n%f$I zr3;#$;Mut@4Z6%lUUT2-%6qun+b3>AA+((Eh;zV?ZQH9xIj%H`aYc}GSrWpTDs=o~ONgi%?^ z&u&UWx;ejy<9rTQKYb-6V^2ILfsAV62SWVguM(=_`DzV+T}5@u&Q}xvnR1@W*7jVO!`aB3LYJ z25InZlplHcSf}V805T+Kn|8+N0IB#qGk>S^kpMqH8#D~^I5@EBxSo{o9?K-|si>*Q zS))8++w7i|u@C81$C}>Evw_c>{Kzy{YcpS?P(GN{Oy9jU^?0^-i{nO}`^CWZ04mc? z$^w&4-VO@+Sd68b3tIKeKvZScxR!xy$-+O>xI3nJ33-c`As_g_>(AFZQ}TOayVa0Z zA18aOdrW3_~i^`reP!J7j8$chwZ<2J8r>yexsMM-zMta4oiphv0 ze801DyTxf!$1Yoqzf1r3A|lOrVPIa=?-l)< zxu&NPqJ1qnom(0{#}q7HT*Dl!f99a_kN$t}zB66dj4N7GdCDM=TKa~P^)R^z8VmLU zmkmAW^l2l;plQam^d5lOhk5tr^gy_Bz0Cw^7vj^u16Dmq0U$P%k0RdI%WbQuY5=gZ$s;1Uz#Jdm{46qc@g-}`v$5zFa$xw zfD%L60A!vH!A7izxNc#!3lP0Vpy}h)p07`fX@GAjfkPbn;TL~kt91k)bm*WEg~I^` zz4i(RnNfDPC+HASh3kk|q#s>yi8&bY`<;lPg;uR})!Wk9Jc=-Lf5p9kfdT2j(OKE{ zXX!h1|0cH{KITDxva=ik((Y8tg4Z53IXRV6mtymqIgqQHbeEasnMs8$1P;%(Mo0Cd zN;lSW!DEYGtU#5-%%u=Z=4abor9;Wmcwe3#2C}}M4=RB2ZY>OfT3i`$hCh&$m!!Lj zD-|DBY0l=HY5&DW%w{^21XSAvcm<{0Z^B<1hHgVhXL;zv52Cj>eP;pPF>6Z5ldW7! z_GYlETl5hG6!6iw&V<5rz{=bkcQDN+do=mCM|sd+?zg+CbGq`R;G+YsB?zvmPHq!C zzbXsN>MeaQW8`Vvf@Q>!$Z=uC(ybRn;>tyuda0h`^n=3kPW6r@SyU|VA5ApQQH{Rc zgPUyEB^Uqdl>b#8sbe5@JV~N55+=nb{kvqlA|mye)t(q-fAH-9MqB_LN<977fcpm; zpgaOY3}_)iTa+dUz(R=)9hM72#_~GjAfpbD-&GeB|&@4CX@$3+=+2>(CU-UF(sv}+r-AY~M3jz|#@1gsQ60jVm23?Sp9 zjs*k-1r($U1VSQ$0s|NkrAWz$iem#ID4j@=8VHC8NC`Edlt4lYA<4f_aAtJ=_gnwJ z-sfAc#ahoiIp^HxKIh!`-q*FSYhPy|I7Xn4X#AL|Y`wKF(Agf%y@M(93pMe0;!*lW z<6wHrza}ejB0iJpF*(nw#M)*(PhcA~RsS>MIzl*c#a^PY+=dM5A+Sam2g4@!gY6(1 zQWV{l2&dKWv1POlR_f%fg?<=F&<;-p`fTBAIu}wtA+*wc#vk8?56E>q*z71Z?0ija z4{dSig63tznW+PJxAFWucyKaa@0`2z@x%;S;C#P871Nmp??ip@X-W-hJ(KhhP%XdI*?Xl? ze|T@pZ#u&W{Di>HE)a}$`Fgi*nHSmYVrW>_eoBxfNh*C@{qf1R8dsguMY0x%(^JT9 zZeRZ}U1-0E0f9RG(h2C0rc{VmM+P$I<5YjQxA^Dst0cpm&^cTuw-6m}Lo2sR6GxjB zqiWIrWSbmHo!+P&;1aCnv44p3aBxaLH~TOrZEO1JY&7+d+us)(UJv2QQ_g4swv_&J za{IXQ)my5R)C8AzV&2s8PXt3roy~Moa3d*Vs;-e6I;sV@K9JMjrjSrtH)S?15Xxe6 zruOOwH&BnIAW`pFDzH_X8>-)_#2P~D+BwmlfQ-(Nk_2zDt_>de#gnF(&e39N|Qg!HDlnapZ5 zrffgLgRnH+@)N| zgFm2WMO%6->Qj)(x{aQ~w?8`c;~+^*TJh@4vK;x%TyiUS-a}4-ajgA+I9f%grEo@~&iOAj_ZDxL1mTA9f_ z87!N<5x5Bz`6*l3ARuenk4ZXc;1XA6I)M1UFNkHnh08=~V4=AaYU!^>8_dQgI= z2i?5{rH@mzY1OLRedFA;#9C*ak)pxnJ1X&GJbBN45%ML78X_o#wvY1v9bF&qQDu z_Z>QE~Id8amu zG_uf6^7RQ3TJo1S|9I~rlI)lBvqil3rJzJ;>W()$p?J#?P`!M7aXW%){$3}D!``rs z(iXIXl+^w^(zoohDN{*)Ix ze>eTnTApP~=iZ%qN;8+F#5i~fn1aBH{9w4Y?92j7l43XBZh-9vu8};rObt0ZLA@^6 z9Pcxn1W#0F2VZh?ME8cvT3PxK?1L*iB%;YFrQomb8v?sZC+ChnAU1Pr8wxz2@t_10 zb^#ZPuK@!9>DDq*HB?xG8?k#(cNjE9Dx;=96e-;P!PzbV@ce5aA$8N+qZ16U8FjI? zEQ-M1g69)s2@L5BWb23Qy=(0EAkh$1@QgsBKYyEX zknu{3;`Idzc#x#>2ABn0cz~@rH9UqmcNHcxUddQT2*v+s_&}QS>ud;l1*Y`>1ra)KR$yw zY&xf_8u}6R*x=q*&=7Sgyuv_rJ?iOGhO$sZ!3ni1@caS&hTd_AT{19Z*#n6nacY|R z1X3rcj76v^=yDa+ZTv6#EEb8g{x6)Ooi<~K(wj%K#;7VNGHi0nCA&fv5bYKJSY(j4@uNEujSt;|5|kb3t>S1I)xY;^$ChMO5Q^uQn)6^q+n z%0DVIWIwAw_YHN{qJJjxiAj5B=I?`UW0wea6Djmh4j8iuX;2Y;$%KGajp_s8@l9n# zgWZf54?O?Fz5Kg?+g$(JsyyhG!{e+@Oz+w1tCgumhNa!wo+{Zr2hR}j5w@3 zS4=EgTceV2wVJNs3p}8;v*~Aer=c4`l!+BmA`{jA(86A>%>k;{^T1&U#K6zZkKG0p zv9=nC#j`J6-`FDpdvk*fY_-aVSb^niD9xwWo{b$4DNoPy50B` z{amY{K4H9R&U3fJwz6N;Pu|h!M3^BHSyEasfmqD1t$4ZM)i>A27C^O1 z4}ldPB`DuY4W4h`mVH%(PmG~?_lGe<9p%>fr>80D1lqk3#vk9`_Gp(PHNTFga_WY{ zg;jUO-WB3HWfuQ{(qu6*;uO``1?rB3+{{bi`%unRq9{ zR?pS}AhlM+l-mRQv`d8@w}*#7z-M;q{uIw zjXqleOz=Qoo><%Nxe&3Uxa=HVxk?+K{5i2*27qF!nbO9D$ogyPA#qG0HsstdOtXI? zB#f2wBSB;`2sJ(e?{CGb9{coCC&+=*uS^#g_S`NG#FI39dnaxuZ{0)GljH`GWi8hq>qQ1`o|DK(sDHTc^gOpQWj>#NEI z#Y`8SUN;i0yx(QNyk>$bp9~BFoZVUUAb79iqqT{$r4^=#`l>?#q?3wZ43f0?fqjb$ zWGjRjE%c9<0CWN23z)IdmKI4wL;Hdq(AEO#@ReqDETGDz%!nw=kSH>j{{DH{XK^)D z96b7Ugi22*=ru{XZ1#=chX6*fYCAweXy+do8+EW=^{qZr)BPCg1NK6*BfU-yC1i>uLIK+eV3notOP3?v zZV?w@M@oR!>YOp7zln~J)v>z1A!D`(GSbMrKrs!6&`NeBhH$&k0z&B*i8X9n?v@p- z{Btf2YHBtcdPq>#3BE_ml+R{NIXv8JEWAeJeMDjc=ulk@giOhUshX6Fti(60l!XW# zpWsD@x{bwUrGIlfJvz;i*`MiIY3(SvQ_iW^NYY*eON~FwXW7#{oR>M5F(F!t}=H`y|Lx^J7Z2yy+cN? zg}50&r!=ThOnkbi+=PkHEuQtlUs8?Ivq)0Up|tYz8ZU@>e`A*`H3S8IPEjq}?9lkYC1EVNdjmLK3{-j; zc8_e{?GV9Q)W8kY;NmDJ(tDY$PPAVdY+b&wN_Cl#>U&F3^-BmP&kTO=p`TZ3I0{~t z8V-)cr`Fl^ttp~{5qYv1xW;}r>ZbHlH$|>fZBKLlsGAMX8ZKCP z4I^e|>T@kxpk8jFY_rkfjzp(Co{JNa*N9Q>`&S-6 zoon#UacIlGirMwbNGN?LK-!Ii<2{|P<^VXU9;)e~+A``szBtjh=U=nmGXE^Qy+OW$ zbD~25(Xyy0HWd}k+`bOYL|i!&IW!MC-7BKo7%J>0-E9E%+X^yuXxPXP*@d*`=Y0H`)O^?kqT>#EdUaj4z%bwECxbs=`kg_<<$4u-u><}x4E zi-pCbbTB8HhM!VuV^eFVe$@3hOUMk=zPxoOjo> z*S{%ES;{V*P2SRuw{XPoy5O@B>f?Jl(+zg3ta1oXVM_Td+*&hT$*S$z2>_E@8rvrr zRxbw;%5|eZky?ts@4ZruQqOevuy629g2d)c6QYHv=xR^`g*q3#izW_CKw?ox3N!E;Fg{ovP z&dBe;h}hEuY||CR0dKxncSiOoETePQ@wj08xvVQLh(&H{-RAuOVr$XGOCbfM{Lb{0 zs~9{CAgFA)++(fm!V;#%($rcJqR5Tv3OQ)jn+i|?ETAu-W7~cnaR9(0uNDZ=;v;6e zfPRh+WL_@p@~Ps7D);}~8U2ecahbCvfJ2ZV{Da*`ac0t;+;0uM>DplEH{pF0Y5G<1 z61w3nAFz#Q@n3j|$FKn64g|%+=#g)s@n-gLqC-{61Bd*+ zJz&3FOfZblIm07|p^B)n=G@I{(2R?oXab z*C{m*hgK3z7QC|Qw!wAcX~WjCz3&&VzG=VCG+Y3SeUGE*^8dPp=}b-I-NtukQRaQG zXsdo4gn8n{Io23$WQ~-ZCnx0dQ1SuUqJqn3C*OX%lgN^s-hq=rD(W#yh5f%kWd0BU z8urfp5X*oo?$#F6 z=(q}xAhF@8ECCb)_$SgOt!H;|yfR74193rKraN8>fa96bJ$;@S6JcxqFPsx7lnAz0 z-BKQvmTbHe>*owDHzMDfKoen8>O)k#*_{{k!_iT+10-6bCuBa5aGbfLTEz$p{M6Z# zt8)g}rr!{jr?(Y=4d`uva4S*>D+qn**MKqOR{C50d)xveIeB>^xt$qYZ(DUyh!JCp znC~K|fd4LECXVDFA|V#Rc2Guhx7Gl%1BmbhA>6RLPg8y`lKzu#qH5#!$IX`9wEGVg zv8N`VfnFrwJpcq1ZqX1c&Ae2PRm-a-+$*g80HmlGd4I9(oQh=Nd>f)r3 zZ2`64#wl;fw$A6pfKI(;AwF{^5$WHycHYr6l8z%v^MqQZslnJdSiHjjf4h_N9FOI6 zj{8n-QCcRJEd1p57uU&!ikxxohH9nq;wY(7EC~K{OHHXahu4F}2ISBn%{qNogIw@4 zyjcx#ArcK`=1A=+%v}d4pb1oV*LTdp@NY;}>YO#WFCxnTFsR_#1J$Xw3fVcyX%Gq+&5VdZ7X394ZJ@77D~iA0Lfa$b|( zH0pGwe0`yVb>z%=X+kM#9Y2EML8H~AhB+vnPrwq`gEh6D7t6MM&w>p9rrwj!n&S9V z9sEC{T_=?HsP76$s^JZd@_9yHj{7>r=G0#_jYWllmhmqP#5rluxXREdVr4(Jg$~JM+qIdfXWwcKeSw;^e2F2QIo!gC{K1ZiFDAbXdZIhY!LQ3v7lt(XLusKr8 zC11^Ed_L3jkF)8bWEn+ghrf>UvS7?IrWcirhy)An%e93#008ON*}@h@g}b-6DZ?V-Iqq+Vo2f)<3jhy{+KmUBBOB=V#dZ|IvMghfZ8&^HDI zJdzMCIb_KiVhC7*7HG@B+xjdPmXI%{%yMO<`&NyUw zgEqIeeQob)XZ*H*AJ1%}O3oGVLn+fPEhzI%(QbYyc@7*B%iT*rg0mN&U0%DBf{SYm zM5ECQ{b4iFw)E6sfI#h%=8-gRnGs=FFWv0I>RQO307FdDGMkgESqw^&0$$OKC3mr% zSkiYGl>i|wJOqZ(WpM9Rw1hu0)8)Qpvm)G=KV1Hz{fBu*Wvm;Qyda{sXli}Nc`}zA zvpne)rp*hx7W1naPjbC(K6EpN9x9iTp^WNUJy( zq}_O_5TVjEq7fS#VrIWdP2t5TksGnA z+tsx6Yi&P2IBdl_0jP~IPg0Ul!*>7De{(8ab(zK}c5;5>(yx~=`%kMWSw z<;|U7aj-nhYKJ~G>>>7LrmdXs1MGfi&I&u~{Y%bvpi@5u_Q~^?vBKlz5B;#UAmowy zms%Ctff=sMur&(5cW3*r4aiWc{h2My%fqPPS=LCRMJ7$ zY%Xy;I2(Sv+FLgUA&vd0dHynI^*FOWm3;a$G+=miC}RD&hPcAcVEeW%)DkV=Lt6AR zSzw8UwH4=(77J2AbjeRtkqCox_aA{TZF66dH8hj3|MMZ`BNw$uNqshKlbX1Jr2-uR z)=vJa_aNFnirBJ`sL75#hF4=9(L?VzJ=b0nG=WL9xrzyL-=qFB(Pk9D?`7=YDHn0^ z;VD}libVvqLevQCDfk2@Bf5_F{kh%03&!fU+RE@uTzJ3J)TcW8BlfzW|A6NRi!D%F z69C$Itz+-N2m#k%<$2XA0r8vQaSGZ4zj~fs)(JH_?T<&~GP-sNwh;OMi+iZ)CP708 zF+^D9B0TMnc5oUX?+IS{{dq&i1UPRPa|gpmg#QBnzwr6wE$3nFN0gw`8c`aa4b$1zN! zzH7tBz~&AZo9c@e6;OnM`Hu_cT^pv7?&yTdg3{LSkD)&thcq7@PRRG+6o9ttE%?P& zXwJL{{ES5_`<64D;Jbt==%fTp2NAI(O84IBI9~yhMFi4f3nbuE@p2=@DZ`aU z0DY8_<%$U@)G|rNp$UqLI{5>7)zdCa7Pmfw(KM16n1jGigvbmnDGmey1EV*fnz&pj_(NYn}7?x!8Zn}sHUE9A@N zVN$G#tFqav;M*8a>G8qKLC0jXtgrf9{0gLfc?fF9qG?+<$$lsk?iFWZ)le-d4p`Uo zB>B>(!$DUNy&S-1dvpku%E=TB&raqh!__dr4n*+OQ}Wd8<5E$D4Nk;3ouv$EgXXo? zg1402DJ1aWSAu4iz&juv?Jbhzw-YT?gth-+CK4g;(*}p6M5oh=J8A$o8*rKlICAlW zAW|h!FBU3T%F{zUOR0Xt_UOD={QpkF{xD2_oZiN&=RSTjT zB+6ETB*htuU>qWuD=F=(W$n|y5h7U=0S(pnlcw)_t|Q9hy2z^;F}vr6bCI`XJb~@A z&eD{5r3OvLX5Tf=fpTu7O2#DI9?{zX88G-6lMmj!Z|6a2Su5#NCbE~km8ooel$*uN z3<)~;)3F`9S;L!v+Ry}mXn{Yl6t({SNAvdYCPXvYo@;F6wu?HS9ykXJ-S@-wyub_y z;3)k2?nNY#8qZz$H`5=tK=YJ0o*ju$W(0ycMtRke&&+=0lZNUW2vijgs$)3J5rkE) z6sSA?z+yF$UV?D#vySH#Y{dJ$3jfUWkd_hi!+e((F@k9ZE74a+3&dmWiXP~M1?^5rF{N4oV0!I=xB?<_h=fbH z*ssf&e7rIeiYWpu9pJ($D4U&_A)iwOEedt*M8ITb|8X+CXL7kLkbK_)U;3}3Y9?HI zJo~*_$LXo#?T7xyF|>bR?k8dmHX11^f_GQPO!*`ehtbj;iU@@I+${N~v zH^OUfg$P^~1{UzvVF~c7Nf~wodfU6Mv2mesqqsV86`n|NU$VZW6UIQkm9!n}h)m$2 z(#r^2kZfG?8%Omi+G?L9>KCwO$OWSm9^29)IWn^N#aj((zo4@y4<<7Bn6kKToJ+(Q zpTK8J^J3jr2=u{tZ2LB6PV0KtcZ8dP*h5B7JJl<#cm$Iws=-278|2(Fi zCCjei;Mn(_G|`4v(DV8|v{G8$$e`^BR6D0YLrEw`N?bK9-acP~2oIpKl08(CJl1lW zyV<=PEQS$k;0nC&z!)VV?Un89y1%LbdXsso=j)LQJ0Jq8#%sZi{xq2N0SqYwh{hk9 zAn8!|4k1U|_Jkw-qky;tw!qn;wg-@J(Rk~|M`q0cJcvbZ=ZMt}Wr_=56@r2N-Q2jU^ID@%r9yU4WQ-WjJR z%g0NmpET9GtAUs=d`Q-0RnWQHr|O1(zI$qI)=#iFMh+JgATxcLvh;cs+crFLGcamQ zbq*2KK%*z72UU_Gr@Jcg$9XqZAC#d+`xE51!qWTWvrA{9`yS+a$6L@!s#2p1ySsqY zMeip5G_48zQXxL1(NHYH=QwP`dXu0ffINyR-e8E-emT6SU5D!LNUg%X&`iJ&x*13M zxgcQ>Aqk!UuVroa@To0E)txkrc|vxf>HxfTAUJ{e?c=|$^F`X&d& zFHGb5B~g_p?L|;?R8WTmbh?yP`H+Vf%n;BreId?Lj+Ja@15F>0F3w_Kpe)uedt?NU z-G>bnvf2FoOwgP-GYgdkXZcoznxxjff*^J1%>&QKGWqnCRW-V-%~Z@Bf&Oq^=@bh| zV2ob`c-#{Pre${95x;~%D6tP;Z}>kj2Wwf`h@7El1AL&+OB4b4tvX3yaftX!1$YR@ z3c^u7$46nw23n60Gy>H-M=t(`yJ5wDcWX&qJ{R{DNH|Ld~P-HFpP(H3$Wj z&Ye+2SdX+a0_VbhnLBL#NBM)e^|0pxXodNW*1ZiMWN@(@scGvE8@G8Wd;=VuYVOHNQ{N5o2hg%QXAI}dtM zAF1ByM){^d4Ba$t`qkx^iUggxlU6M_$rzJcBdo?P7d*9dxS%N@ng)q<6Yf1F|iEEugPI8LF?NkS+j7E4^=M^q(Glj)|oJ znfm#+_8Tq#77U|WJFPois(V4Y3ZSS((yll#{lT5bArfQF<;zZ_Xt~UjdSMv=ALuZo zK`^0d(ix{9pOI`lg<8uF+gN%RT#b-x^UimF$!nWKZE01fZ+a=hD1x!TF})zhmNLFV zaY?iPZ*#M*mYzjn)7 zyP*S@j)D*UQ5PM+a|o>G`>?uxb2bFJ_5&aVz@=Q2M;)xC&AkK*dTut6D_*=LrXs{> z?z9GCTMNR2qf8qdwjg1CfZYib2*!GP#V&KOaHy&DPCl*C$L_yj_*^EmqZ#lDzd8{8vGd5d05$K{b%AnIm-1-4ox z)IQGfQWjjSoM4i*GKh8v0&l2d#TR8IE&m7c?Kk-WJS1RB5MKIkz#05uKgWXYM?F%k z!lMU9!PgFnqjNpi?+!>Yhh9J1MS1pXV1beZcdu<^g1!0=qz}mrk_ABJ;T(AK=^={m zJQ;a);(h4W$Aj#*wiFV~k>qB`i8I#Z#y0H}IeY?#WLsY~FMt^C!$N=M5K;cAXE82O z=l5?%c^NW`7Dvc46oWa{3{Ca_K*QgkpjQKU%Yl6g%%V!HNV7xNGAS^ca+w)K3rV!j57e~~4I%+5IiOwfnhfxV^8r9wNaVHrIpelRthoiTVS2WbK`=r+OjOr2cwSO` zj(xLu87b*B4t%C?1)%=Mhr=Gc3}6@BTCEWi}ylkUxCStDL~RwC149j45%!4+N#S4fG4TZntfZ~`m98qfqGOmTBt$|H_=@0g ztpMNP=X_3Jm9sU}Ynvy8NdsDa$!xL|9$hJD+>gSAatTcCpOz@QU}a?yX6jCmFC z?$0ap<6KBFOCxy*0oQd`Bw(0%plaOBWj@awv`>7!8`V({F?vm$G%%v=*U|?Nejozp z$JQ$9Y;T&pi~|N!(1SGZ|_Kd7>XpR?pH* zs|_poi4@@}!4TXR>>5HKaPJMPpj>`nJWe$}R?B#6`wiI#P=xww?ruN^E@19yfjFZ1 zQa}*crv!HX4$8-pPn`rm-mLAeDd|<@&gnWxEgCuYZeX-G0lp0zb@j4%VVNq+1UMei z*Z?FCV*U9>zd*MHBslpSxL4Pvr9wT`Z?|0#`?bUrh(PTxah1Uw7di@p%;BHkB23u@ zo*3dYZYv425Qm`P(RqK#-Ubpl@+0=SNbP>4G+;Lah|AD$;M2CQ+XRI(5w2`Od+ z36ok;YT{7wH3}hmGz8jMNo7y)aQIRS1+Y|i1h6$nXfq<8(F8mfHUjVo>j8h2p$=e@ zOS;C-CS>owYwbMS0U-fs$i9#eBFdKlxH3{GZ{KzQ$(h>`LGdOhz-~O3XyhodzX@oi zWy~Hi1uKGAw>Ag%B2<3on_Ujqpgi#_bUZNH{S#feoKCR{5oZh%jlYQ+mWL*952i7* z@z)$w<6Lt8Ix-u_pg5NhjkI>ztY`cu5po}BN?q;71L1%56psY+D}Xf3(QbWC2aWow zXhg0DhCw|JJj(D3XrFW@#2fQ7w|;DTT9aEWYQM#qWIl5@YgeNWWPE%XK{G{ru?y!z zRFcm%&`&}ish4FOgmrl!--lpFBJ3V2eh(m=mzSA})NAUuJZZE(qus_!RifK$sLT)2sfxxf!Z;|4pURdQfXkyQwV!{m#41Hc^;$j*X%5Od5S05 zcrX>-8ha5ElmJ7-fuI&SG(E9jAc>l{0X4wIV` zpi}I9u6=Qqx$kI`Ta9<>3Nuj!;e8)|neSfVsW@M$PsYAmjhVWbReI{@?RN*v+0qhD zjpxLpQ#KqvmbY$IRH&ig)*BE4FlXRy$*3#w7*> z9dR|`=G1rMRm}}=k`>N+y0&B-uF=iVd2~2Gr&C^YpPv8q)6-%}b@*sjw4Qo2ZmwiQ zbA9}l^BQkFRmC^$KASfhSlT|X^@S{sTI(ZnAbR8G!N}g2jXl-QvM6a1&233J+NN%# z^LU=?6QNC36J1e!X3-mW@2?i?6jivW-_n~j;@0&E8Ph?;1ZWH-kB!KiO-!X4= z-#(U7wMq?N{vwNWvLSw`E9=hu#)YrCaqpIgs3@~4d>o^qBST}bxn0=ekyE0^BB4tp zdco0_jQc9Q0WNO-K=dl?9GfR~n{`-;zS4mS$Z(QvN%82tG3qk=OQ2YZg5xmb!VMG0 z^CGK+P3zMS_fI;1`nsei|cnCSXXM7B9t+ED_@$fIW;>KmmtNK)wsPW zMONSBWw+J=1t_l{hgGUnYJmZN5nJ1#>omzLst|#uDg1q_Q!2c*`6-W zNb93U-p0ILooK4G1=rN?C1tlpY*2UTO2#F<3Mskd+~kY{rt&qLE7@#z&)lJ!h&usI zm`m^ZO??N~;jB8Xu7xQHc3ROi$Th}sv9t*keo5|fx|VO>UdQx)@#*lIFee;JMUM1bJ5&nBZ*!wbkHFK7VRnB_>N)KTJg<;9R*vz-9gIE=}Q8c zH1R`}4v(DBem-xNQxfI;ropA2<-@d#$t442pHMB!51^xT;Q8pLn(FvE^|};c4>6tJ z1t)hzjy{^OD-2j)I=)dxw4^_&M8CAW{aDDE61%@f9&`T23Ncya7H#`?*1j=ZuxqB>27fPWTI288AO5C z@Z&3JM%~tlp*g@h3@}xXz(;)doc`+h19|Qv)t#%ex*4)23vGinIMgXy`||JZcPBS? zr15J{8JBhJfygeBWL5O)haQ1`6<+Jr(t=Sj*^>4&H=M&S9^-${!bt4GFBtahDT*Qb zK*_J7>NoMi;YIx|LX|E)1RYMgTj}Lqt82!#n7ezMg(=T^-vt(WFV9(uR>{-Q=Ah#B zItstJunaL9BKrn6Nt6su&xT`em5Z!w=|H}WI~}(2`}WBmh~&@l!3#N_7g99XAEva9 zszX=x&RAXbP8>eA&~V)wn?4P9c=2tKHnr6`o#<|&t#j1fvDQd4rqo;|hSb(EAGgu) zHQ)b*Q*x#bO*ed%EN}Svhw~+HkB(5KOV%7%Hx|2D=C*=qnClq-*j?esxze&UIw)d9 zwD4`~22peWU*nkHwuwXzwed(Uf3fbMcH~=+*J_=}XMB3lsi!LpeKF06%~ zAsxIsuB!JODb^cozZ}*j_~Q{zdbuH`3!Nql4EWYj)Z%2tkb5R$@*++u0SnRyiv_LY)q;djoFqY**p_u1D%E3J+vP`aZytSuSTf)Und> zerbP|psM&EVNM2L0*}ezn{^#~OdWF5G;ezi{x|1Z*ax5)4Kot4IIUeR=8+m^sVcrY zSoDbLK8(r=B7dOo=s49&=78z#%?iJD${daHQe639Ub!URI7F6{bHd_#!#xCx2G3fKRtYMU+SiQ(^Y<6G3@EI zCC9=K{l`rbcy}VkI~BobIQRsuv)<%gDy(v35#tEo}Vw+Q}WFvfMi$}st4$7q+aW%Zr z%o*e+R$LX^EzK8MZKl0WRdDOs-Vv0GEw@l3n<|k09+p|bO-EZ4&g*8k;KniD?Dzm0 z^T^Xr{;-kCwMP-WPpVX=jR-JhXYsB^b4Z^E#F{l#SZG{uXP2n*+2I0lW0 z1AHr23B`W9YC3tsO}ZV~ws)3TYLq4$dT(qxWVgyYcKQguZmj=B4SM3{yu^dVz4k>x z7Ga{oN`yl18!JM?tHm&mo!GG;UuDnC<8Jy87vjdM&gfF!k&U^eQ@9XZ>4(Mc4uw!{ z&6&IWTOK&<4JX%7)|9QX_s%SGu;UK(wCe^N1dB%OOZ4);)RmdR2I3b?bK( ziTzh6Bdx{E7BFq67WqEKc7*BX6^0XDjB(_-3jz%4FJ)q<#$qq6UKRDJVN#2oM0h$K z(nFz8*mLro|F^|0|tiec{!k`_xQm^Y!Jren(3 zK@wV-t!Ydvl3`aBl^=ueX5C1ADWUX3fE`w)EO@5tHtfPZj$yYUBDc*ddyZ-8ag>m? z^(-RKI2D!GV9SEgy#0~cSXxFnJh@_vw2-&EyUF!C6D5DjQ;*YEALhL4CCWDAL=v0I zpAarN>58PC^PzqG+*HkP8hEkdyhp0xbwk0IVx3#HZ~J_K{V(Shq-k6E=dDB)Okc>z zCBtWE=QsDd($G@0rDn2!>Tbg|#fRSaEMd#&%+*YdXi_T-xN_A60s(_P7c?M-eJ98F zDbihB+Pu&YqP)|G2H}5%1*-^(X0!?8thU}!QQIHf(a*U=3?fmKwAe8{A`7U8La~qR z_6sC7CB*g{%*0|7mK5o@;00>*Y_)UzCP=rgi@x#*yr|f()t^?My|YXrg3|Wo_^=)> zD!lrbT3VQ;OGC96RHzU2K6{fED;zmh8(!Y68@vK4x2_9nMpz0gk8p7n;KU2v=< zUbMEy(sbXqn?CyTA;B|#!EBJ=>fmj!jSMW!aNLV5Zn_{_1+Nl5CEJq{{=vwBEI;U;ZD;)_?ydaq4CM#yebKIEqY2-g ztc2;$5x6xK8|d#uo{K=gmJpi2dC%2k&=egEHsBc=P9Z<DirnfZ` zn4i4A-Q`J&Kc!a0h{EL-=6u;A8x=ol@7xe{@aTXCpZf_biF$gLL;%v|p` zb=<^~6%y4*LV%l2$g~yiENo=Q`?9X#SOzLReq4H-?t8;7X2O-E(}O1fMu11AEtl4efg`_S!|| zE3|fv{yhe|RUmFN{vsw|tX%|k*J`?`w&`9YUuaeSDE_gnQ>9qd@ig|fxn(@*0H3E@ z=8|+%M5PfQfIVL<;j)-H$SG$ z<#bhA{8Ange&zKx=W1O7)Ww@I0bvny)cg+r4=loF!|Bwvo7GXBiwloJGE~LI;LXNe2ydz&g78odwjbw->n*3Q@ zNDePY{>8`&s|ao~jDy(dnt8&Qd7tFaDI8l%Qaw`gr~MM9W7Yu$ez5(v38&v8k5Hc# zTno`E{&7J4L&+L(#^Pl*7^ft1LS4K-8q8XzzTJL6rVy~DT#Rp>%b0b3kP|$C%109X z5>+vsghvPQlRdtoa(4tNu+D%bnn z*;c&_V?14MAu*^|b8_oC75L{o8mfM`?9*$WE+h{AX&i91 zTOjj?2WcR|J>v|anTSE%cn-&8pGMODvGYQXUUlnkBMWpro3p9Z*ezDr9MIDjfV62` z%Q4q|gwu|U#hA=X$;S&RR%^}$LvC=)&x`Nj?Dn9nHPgwBhnFyoeDfOLA6l{|3JD#9vlkd*z9q!z9g)A~cf33Q z^<|iHWVy1Iu03q+JLg$WGn|)udneJ!-fU+Yt;xnCDi+gLL|#a;64ks7hB2ZGZj#ux z8JSXtN0Z-8!Cc#5EMh7c;qdB8h*R<3zsIJ_W0&IQEe@vOOTN47NzOHIUqhqkfn$r# z_Tyqrki$7XlJAR0S(f?zI87(3-&2%5Z+Xk;{l0Pc@{~oV^$FJuNs2KV7d~hmsM4** zAYb6`>+Od*3*4V$^hHli&ex2x&&boyiJ>m8V)?I!s-S4_pe9SH`{E7J-9bFu zySc&bth=eX{8g7gzw|FJ;04XNurvaf1DW5$x0NZ zWWTiMeL$7uKIV?&oN8G#C)q{KVe`vx$9<09#}`>Tox~RN*mEFqV`+`#Wh{HPeSU7u z5R1hs_GJeli^xBNn6j^7$xSzyFC^})7vh^k=SgMPdP&u0elsopx6DV9 zA@k-WFg>v>nQ7?-?K<3(l7SMt=B9FGn91Fik6hrTmNuO_tL2#U(+>I8ky|* zJI1g~Vc|ovM$(aO=9NOLggQvP;~dN>{@*>d#b3E|jSM~!esg$WfKQ*!&Pqvm5kk zI=9jA;_vO-4E9t$^3}Dg4!YKpXJ@UQXeH`WQh%@UgiiIb(!4YXIJiq>?$QE>BV5E8 z+K94c3?2CEB$WcXbgFh80^6f}OWC$l>rNfe)-hBvM`Ey0&I@LHg5(Yygf5VI ztakMJBAaVRnaiLo{tAElc4>7jPGUPj%b0w*4V&&9T8JS~}?K3{t_@7KlPe zh3$s}6tPDx_x_|3sbznu-ub!A;Gg!N3mFDEOK1DS_**TFb{YHwZ{s$;n$$Aj2i%HS zgIgsAjFO-Hz)FSnv6ix+>UN05BuyaEhN-^Yz(DW$8^0pZ$JduSfl02Atr)^=s^iYA z?%4!ufNRTFqe>@}mrZlQOWR-p8?~O_BDZ3oxu8m{Dw*9j8qs;Q(@OM6UZt|yB9mRYg<}ei-D)xF9c72uJ39WkVyRO}1G|4S_7TUc z(en^$O*;=~H$IzJ601XQ&TsLrcLwllvzoUzP3HDI*OMvTbD#T3S^b2zco+*A*T~o! zj$ACtSJ;T3o^B#5ou8%@sl|HHB0@-`Pt+!w+JEtFAubK8TiLrg+*uU5t_0geP*_Bi znd4OQ!kDJRhN3gmZwJSmO<(5c>G0m|LxbYxqAvDdt_A>#PjI1g&wZA$5=C$}t7|Md zR^hB|e0$<4`Me_X)F#mv1h$;b>Z;^Qe1dW$WRVD9Uak3wy^J19W$%If<3PB%%TQ8* zJ?6;4WyqH-#wVTw;B(C_B|+FOD{D;w-g^h{pw(6rkzI#H&PX5K3LMX-nzsJcfXulg zU(69#S*ISHu(@c)fz;_8-(X*}+fie~5nG4iAgJCuCkwzF0OLLo`ierx8)F5_HeU>B zs@8PX%)6(FOPWIujNcm9sV8!tI!D;t`eD4qi|Q20L(b!aV}?~lvIxi?y-`?6A}YaV z$HLcXfkprNllF1dFV;b_18a!FL+_Yt#ysZ0yZVKd=xR&R{uU`cm8^gTm)QC9C6`cb zCXo=qo2kIEx5~^pa431IKdtZ4bi~4Y`cq?e6eT9IdW;9P9~|5?hCxOwJV1RGnC--$ zFBT3I<%NriExB9I!-A9^1(*?9_I{*7?&fZC0ES*Vt33Bq7wi+Op-4y@PR3H{dBD_Dg9;?H#-hnf&-X zV&yBu_z}`*RG?Wwv1YyKi=Fe3$-yqW26FHIt25mp>Xj83+byQ&Piw>S0QdvIv(lzR^EI=Uk91y? ztSR*j(Yh>pD1k5XaOQVDc2j^_pTv!q~K4 zaFo1%&ngjO|&7DEr)kbG&jwrLIiocA(gFg;srfIuV8<) zN1)@XD}efWyO!P)UOCVl^i0RkNh0L&0lPe-7~%DULF|Rev8$L3|K-VsJa4P3;kkV% zbw|chAzwyLD8hDB#Y^Gu_Sl87s@=O!ibj00gp7UttHhwn))k%y$`q(NJJ7$e&x+Ws zYJpG|_VbJbwf;sz#(c08t<1;~!a+vtdPb#+wL)w0QKPY^59M=$dtKaE)1vF{v3XJH%c(IwMUp@%)y^+7 z`_ML3ny;*50}GYS{tA3q)VK{VSWP4ne(cU27+*x2Pb%de63uIFgWPUak;RpYsEjEpp|bzrW=wY_h4}-}?qtOx#29 z7OEcMJpxnf27emJTc)e<>*ub?21L!7sFTZbk0~63-8H3>d7pR+z1+=}`^7ys8wKRi%1QlVFG|aD z%>~1&EI3U`%Bbs_u$!??4sXgZxaL#UHO=Qw1#i8aw^%KWjZJ7SiW@x?ub%c4NDju` zx)O1|;Slp`xaO(>N!xbj?zO?9*dL%8|Jb*y)ns_^nDQ5s$rc(pIYd=kSx9>A9a#w| zY8q{ac!p@SR=v?59&@=Xo7gn%+LA-cDWLHHSo~S}2mh@|p8y|{YM6`Q-4j};47D3V z@x7CZzwZRd8dQl07i}Ju^n9yn&6S8sOf^)dEy+;xx+z6@y@YlVD$Am~|NmT*U5j-% z^McPs+o*4eS6zE?k5lmc@N2JXGo}3{x3>wYbpq9`d5atzkBj6y?kOotwd%%Q#K}jN{zZH9iW%UcbC+JVr4KxLRbXIg3my?nvVN| z>a#<04+HyUqleE~$2tNT*&zW6BkeS#d8=h+4rXlw=5j4G5!V$mUicwsx}AQcT+Au- zr8b~+07m(YlHX9!50p8je&wyPr-X&KuErs4^s>;Hh&v!Tt-3)kQFL|=J6$?EZ-h|z z?^O&|cJfd&nK^2=0t~e^vPD@$mXfjnqMyYxT!1&MhsK5^s?PyNgL!LPtP@X4ApZfo zxqA;_Va|Q^#&RP57g$c$EBIV0UkCp)@zIeYpD*uw-;A9#uQ9}L^)PLXPXpkAGk=x> zJ#cG@%ID&STjmQ>kO!a>2AkwzO-Eg`3a!YrpvRoj!LfDliHJe#b6B#BO0iEcN=z;K zcY-g^=q2OZl5#v^=~fLkP1&)W&Nfm^GAV;>0-!w1`e&Tq!r7pA(Kf*)$P@Zv=;a02 zH0Ka+D}uSTD%1^c8`+c-_CrBV&v${)WLi9_>o)*QtaSky&R}~?O@9W|l8)O?BS65@ z)JicR`JgmwzOqk%NyF#x!v{~Idax`4K)J@c%-)=Ze(5i}F8AA|G!giwTs+}H!`q5H z3?Pu5J=-E~7(QG7)MF-io9Qh$&aw;~7JGg4Ts~oY2=1n8n2ocUp2u48OsNr2syJ0SR(aMM=xZ6a zd2Wb!_0pp=khOWfaoL+XaE3Yq+gLwj#I_oAAFl?30O;`g{>ugC|OY zUzw6>zBJ=goBHT7LgY$LgFr)!;z?%m7)_V1if&P?Ntyrc)4VP{UWay74qn;$`r|g@ zmW19y<#2j`2Vmxw*bvvM8s4ozd|lm76dTsS8=KWiW-okn*{ibLkm5aH%9ithE>-6~ z!L>&>Qapk2ozO8@TcWOmn@5`kKN{|vpE+M)EO%(Q;-?p{0F1Ch<8Cd60`OVAiQZi@ zh%A|nE2gmt3S7rrq0pMz*L9OB+Bug*?s+^#U0hJr_YZMvSE&4`APxOKfW8Ovy?x-r zfp-6r2=m0!k1^~-spE5bLs6PP(>r)ThFtG1WG8c1*PcYQO+F7=lxsw#Wp*p7C!^>> zhO!9YT<@E+c5zR-EAn45c3^j`qds?X=CU~hwOOVmKb(tI?+!6_rVqF*NY{7m^jI5* zCc#70rSGtmvALsiCz@c@?ONqJyrk67_I|Omckll>+(P!9UbMc+3l_%=bCP&MEvF|o zi^=0o;e(r2`1@>#L3g759wVk8wbW|@!S^|TS=i?l4WAk9VGYP;dz}v!;QJ-D)RkZj z=mjSDWkM0|BjcWoj(~IaL*fH*%rOxFRg>?7)(n%Ym{iLw=HEKbkORvdwT7v@#`$Bx zdw;w9L%of+F-dg1XO_Vr%5|H(<*vkg8K{l!=#z}%&PYLL5dvaTb%eno|b-u zze2QblU9>e(54|k@yH=%n7xWqBmFl)g^_sH`SmtXp9Y!~jtIm;2l{Sg(lGRs zBUg=K4QGwkC>~A#9gHFrK+y2FfJhZyxa#aZ={H=|Y9l{E;oj=f0INGb@%DtZ#iD8N z-@xxjZ_qNp$39)(;hDjF3>1r~94JDZrT&y7-WWfL-8Qo*JaYh)US7j9kFB5`jn1Vt z%rVT+UH!^PhxwP$bG; zxuryW)|meJC!+u$R8Fr4caOx}BSh%&E!LGtj8g|549W@Dwd7ZT%Htg(3SN9MaRk&7 z+Lz6}@dy*T?$}n-a9B!!lk1V;5}V`~RFsvcJz9r+y(W^Q3b*dB^=cDaRPt9;b+|*S znlHXUkEwK2+mdkXN$4DipZZUVGK;v&6Xd1c9bEad!|SX+|P@eCtlQ)4Rn?EW}GMxNa@}g&t*b$VeUNYm8hK3Nq6ziV+DAbn>&u26R<)x1dhu=fK~$vwJe2(e8ePgyuKCY^xn*hn^r zX2%EL1O9%6oZ0CLgs3pEolqHNc8R0utr*^7S0x2YDkoq2Vq%O1)ujIjL;8n_zNW8c zff_szz)!z{ZIrI!$}Ovz!kXbIeE^_)nA%q>4N5%S<7jprmRE^+ht4xPb$_+Q8z6ap zD0sewH9UJ}Vjohd-YKzg*za9`sb>Ej4O3N}XJT72Bvig*&)qCDtlsT>iJUwh_?xF>Cl($NrAUB z)$o~x@c9Gz$NSo?6fJt)WuGR!aO-KU*U?FvN=@*}m*-RHgpqJg2q`I5;kl-%+eMA& z&eZ9d^Ll)Lf4M01pLh+a`xo_NI;~E>aCaT7&q``;n?lS1G}^Qum-Gkgq@o7eDadvD zsE4gx(n`w*&=pjQ0B!?_yk1hXxqTRUuOA$8BM^z8Gf;AWUnyCkbwTAY&*ou?Sy0ns z+&)AB{?Mp%i1@bUxbp(JCUq1$w7p{w=SgS_Y|gUC=@>I_m*S?;8U zq=B6|Urw`3!RB2{FDoR8XS82eJ8C1~kAv@pzKXLpP}7}Gl!$XFbcK?r^XTkjxkz1o z%U<9!m9F&c*4M7JpIU^%Nk;YONhfU=CGY|$TEoh2;ko|l^fiC&=x(L+3eBOJqxX{f zPNO}0Pq(AFMlTDaz8s4gXhC=pvw2rA%NC{OtG6^;+<;bJFP@emaR3I?l6?$HKFf0KAXmXg>!Yrdwpqxj+^EV}ubS>bW12co zorVdB?K)gK+4(B8DFe`*7rVM}ZfJxfGrGB=U+7u|!tPvH$~V!Ya~o181){oX63Zn( zSk>A4&(DKhV0?p~ntZ#Ok>Ndh7Wgy;@0Zls8&WlFR(;dJnjmZTbN_aM#sKI(d8VUG zt|80z7{Wuwq*hAXm%Z;l>RmhhkMy4;?tg0?ITZJl*R)}0hJ++k7quFe;fN?!JpTSr z?||Gax%>KJL82YBFp;37@ww2aN0MORn=7ppu`zWSu9xnFRKDA191BP}ESIPzcrm+i z802fF%s4@LUH?E%qPx|6B9rpw{=sYUCcfm(Y!A?Ofal=e$eEkco&cikzW@pW4O{SQ zoOxRfNI5JWFLIl8bYi)$)sc@+N~IQnUfAD4rZ5Odg1A6NT^4^U9k1P_1PYQ8DU<&2 zAIe8P{=E#^m4lXJ{Q{@F^ai+j8erbgq(z1Nuy_xn+TNLo(mbSXCduucyU@-5B{fR~I60U!coR+4 zI$K)i*}B{1=*Bh0;{;ivxVVo><7(L!vR056WUB4NZP@yZcL0Si-04*SRo6N|fDeW% zz$INlxFrO@cyt`o@|j;)78DCntPl%37|~(2-?xZt0}DE*A@VeHZ8My4@af0)0e@l< zE}qdlwNtsR;vx{}<^iVnpIV`h4W>sG$j>5ceUUPepUeBHv8}mDI5#(Vm93eoP%ltV zB5fifl>VL9RhsAwmFZsP zy1TI<=oV>9es&Le3|YT7-t4W4x?56O67}_z-9yOjCDGb!>=PIbI6;7JWXalu7km>+ z25rmi^72(si+p+eRsH8f4wdaY-4kvJ>q|%_^ovoslYm~--&_TtGNSGHd4dvmysx_O zwu{aS&Pl-Kq;i^7J8$`gy8%$s?eJsh>~=%-~*N`MqB1;Khi zMmcwdZ6DpUeEtnmV^V-A z<=>74DwO$t{xLwto;=4AOmIEREjb_6@JYu(i`AI1>fVbRo0Nb>NV-%0=i6nq&ZAF~ zu9#QQmr7+8h2mclm|{_jN9DB)dugDaIA6VVW~S*@z>gWUe;`lPp+msysoT=4DWPH)ud{MT|kt=c;#QPYl2DSFnVI0MJC}gbrdPk+1i} z;ywarKd>=1Z>$s}Ub&oEsGWY~0skP0Al=vNIK!jpyYc+CzNW?|)#hLt8*x`LIj3B+yz0A^ykXeR@!cVqJGIyRN2X-va4!T;MeyZ#PWr12+Kz2;hgB zvM79p^-=QdgW-&@m5*&TBl=I8D4(VBwDtyFp-_guP1(T={uW%zg-Mknhcjqc?p@_- zdb9Co{J#3Xw`g7Jq4R6{x*$2IUR?$PwfxN0Y+2Bgm86-5(bI<0OK<#+azRR5D@;y7n) z+`sc-iTu&P23MkU`5t~(tCv`7>=(~xa_Qnyf6!_R{$H2HBp<%ATZlZqu%Dc3!n)h? z8($%($`SMc@Exgx0BhyCv?IcxZ_zT;y3!MMK1dq{i>E1Q(NwD*K=T>?XwIp^aYvs} z_@n8>Qrhj()(cM(O3vkdz(!_kC)3&ue+7oqy>!fEaCNe(c2z*VRqHi}1*g+0&7y}u z4m)+H?8;=)Ls}hS=Dw5mo#~Lu6Mjt;&&KH#*Gswx-Ns$eXv(st=qR+BJ6v*@doEoZ zQ%5c9Z0Fxd=jYxdC9VwJF7eVj&EW4#eb3Xe{~g|kR$!4L%Kr-JI!l!2MM>IH>`1c* zZB48v!izLbNYZof>4-#xI@27$vnY3f`0eWLf$3eRR2uS}DNj1lowZHK|6TGN2`=LA z#u+DK6Ug+6rr#@1ENjEsAsFK9Sl^T7qrSe2RbtwpteCx?lqNr)cf}aM2cG=*#CMHr zhQ5c&;~r4J(7K8qAFGDWG?>d9Z+HfCClxKA5&=cdPE>V_#l%_kCF?%|hfX377Zypto1-^M9ng~;xmRrk z|FkSfiAWu z)D0Ams?Na{c!1T9`CKR;t2HRQ&3Pdhuf1?+&9mt|7+!_%Zx`Y7Q5^u0ce@-=NY!R~ zh$jEN@!(Iwed9+LE4D%BsV`?SnJ{{EzfkWZk0~_BLXvKh5@n>S1EQ?}o$m|+TpoVp zX$#=;7D}K|xDo~onzw!V-MQ!f>zX^;iV&E;6O}5VwqQAjH&vOi+KE43ibpVUN28Y@ zloglp^8a=D=h?oo1>YOJr55Y9*}4vqNM7w3to?c!t-LV#D$Q=DR&p$c+YB!I`{~8 z0e>zMt*@^PI2(wTg1?j22j%st)q01(jeRg1C8&K4;4-1K1V|q2TR>mp@Z2BWO=}GD zx!axyohI@YJ_K)^rl!J+s|KR07rD$)1euSr8E!%%)abUW6$?gZrgITNei>!?L3c>B zSIN@GC{k>CLpu7uzcyf;xU|3?s(v?{QN4TwL?HBT{Uml?0^hA+c?~D%3WwXo!_3Z~ zi|=~(0O%4#4L?+AqLQH?M`c3VbI`mIret?x>6M%FmVf`TRc+Fhw#BX=XNUH-dyUS> zT790ux-vf6wZ!)Rm-kjK#yP~GuAe<79OnAn>HNI77K17&xy?~)wV1bA@@43c-+G9< z#iiD+gXLx$tah+`03pH%OVxH*{yZW1c}%5bFT@|RZKzcNnAs;nit2lz-F``AF#R74 zR(?i(_g?_s{X_u(u1IV7-3s_$5pSG@a9?0lLUV`bW0-v4lR-@pCl%X#A8Oe2bs?qgful=`vjcS%9vv96~7 zIpQAauK29@rt}Lzsozi67B9cTxx)T4dQ;il-$?#(R{$lF$2*se-gOOPZ^(cAP6miy zFnqYI7Rbs8kF?ehyy(>D^lL7vgHh4G3&Zs$zs5$<2(IiBM3*+zr6KqQiuFtQam@ZfoW7+;sV6qq2Yb4$_);4wMZ>h5P(_Q z1bzLpL$NJApLvGkePr%0AEVxuUpqLROhXjm3ib=|&Y@5W&FB?v7GQh1RJ8w?LZyXs z_oSI)oIpD6i9Rfxc7+e7|vvvm1<3b@F(3kXpw-{0UTA}8h{-c*kuqW!9)8+vGsko zH(=}j^>tuc0_agt;e~+8LOU7c=5ZU1pEUWbPE_499)%b@qP*^aJ_{C=SJj!H;dKv$ zafwXuQfA45&Kq${gH)xuNHGurpQV=+Tn7)sPAaH=5mkvO1P?rFzaQmO1wsYK!mk82gNk_{eOEgT1kEF zAfuw3GBCec>+;vr0T_{j%8jyU=IPmU!t0_6F3jj4MzVj0p0I_tuLoto*-$wvcC6!visQsnUm2c;)2^@VJU zzQz&d*~Fr~D2q8zwber12;PC2QJ9~fCp1rEWFuZhmA&hGZljkd0E<6r0~rbKi-=b~ z`3O)B_MD=I^-uLe}viL!B8`uNzs52xZETnXrOCB)iD2bXb+ z>$F@sMJV3gp@cNm!PGxMM=vwp*;5OeIziaOqLhi$aKa$>eMW%}k3c)F_~~x}wf9xKmUg6yWMEeDsC(<)+xWsqyAA>Xb39PjQLF}binl?u|R${b7rxhyXG;;9DQl4 zqc#d9Gp@Y+lV6kEO5JhZ$@^_J)YqJ!dp}AI#;+SaRphq=LD0NTXmR_-Um64Aulax! z1EB(du&3`6{C#Sf1Q#8NtD^sXRWTfl`xxt{n0s?&` z!0WXt7uEu(NyArsK>6Hle@1J*77(8HFXZB`m)?V0T1>V@Ah~W5c~$A<*`V}%>urs9 zyY&$>4ze%QGqp_tlTl8GE!#O<3^7)VE>0+|P0B5Lj%WtoO7Kz>j=ej?W;oy%$CM<>e=gmhfU;(bT}Q>*MNj}N`fk}8Hw7Ol zVE%qvC`r+&T{G+z75&-Q0X+M+=(H6E3M3aGH3rHRabrtP2eLjUQ*SK(#@8b}Mv4Xj zSzw*BtmkiHjp6YUgZPL~dbU`!^48+Z9~@r7`#m3t)Bw>+(yJpziAVfb%Cr2XIh(y< zON<{y^b^27yMtFmH{b}5E6=cM?z*0J!xU`*$_(?wh>i&GSXiuh@~vB}0s!~5d$O2y zF()VKii=VB1NiehRIOz44$>Reb53TZ9FSVxpLoYm58(Otie)Ivk>`%?Vr)`*#7>WQ zE3%5IBA{b>-?DwUqzCkWA*R_VzGA&298MPifIsD!= z45&^>F*pP@4{W9Phf!!5re6z2J^kTt=y;wA}~MZgI>^1)`?d@B><2Mn<+@V zbZ1rrtYGa|p>P}|uh6>0Spfi_ep(Rlv))Q({{&RuxrQHF>)Soyt*L)6@~XGTgb;GP zFV0wiF4w9<*9ChCXuTvkrT`1P-K<@^9>lLA(dP6Hz?i|7ou9j@4$W@SAt^m3g#gzf zg~;UXkN!~{m$m2vOa*24@}5j1$Jp(F?1{@n)nX_mx9)|b}L^Cjz`4DBLN3Smuh{%Y!~>Q|g2AyPaAj;uSc`eBw|?m1VMvJ(JPL)6|9)d=GRg4pcsmyKUQ%BcH{#h33pu{N60B!?7VpR z_TrI)m|d8?si=`iX=vR*4E+D{V$5~yJ7q*4r1GN@6kDZK`UP@p$3|bUg}U+le#Kdo zX??Slex=0rNr4+gAhg~~$_MvfO$yOtX}DW@{0_0G^ba^d4dE=7_A9g1mGQ!F+VRhk zjw7c%;ZN}2WFX&8pdC+5anX{Sb}`ExzPBKsi>^(&w4KD}gx6Ryb5C8(@&`a9M33hK zJ1=l0z8EcJYm(cZG9ssxz+eHEzO=FI3r2SQ^wxrjga{NSZl>PdBMoIz5d(1zxG`1M z#5b@4`Qeh$zpDF5k44may_ZHDEK}EP7~d>qmJ_KVHtK*1HBJYtlU|e1JX{;#{TF`i z7xP^@6gWm(R4yQT!0+fs>VgLDAz_|dGL3LMd9(8yAjU?(O9<}G#)V6v&u9W0u-~B8 zKrIy*4^o7RF!=d&!5@MM+XV=Dl+{p&xTd%aYGU>C?`vMQ_c+*pgJA6+jBN5OfB#$YwI`h)`2>6q@d7&N< zUX8kI<$y*Ufy0ISZU7{u3*uxUY$gY3?#VT6XQY?(1aTgJ!}w!b*~HCUyZlQN0RCUj zm$!u-9Z{}_PAgDzX4WlbYHIR$XZUA}bP4Ou@n0^N_{hd>i=G*+5!V0>;v@UUm6>d! zX>YxpLCq`@ee>_lO!6%jJeb9zZE4hu+TEMUo1cGQ{{TtXfNjcvUS7W#^R#?RJo?Ba ze*M2c3a|(*Zy+dg=?F*qCBbZF2>kZWN~uZzjJfRy@&)3ms~O}+xRKBTHNVJJxw$5) zsUTxdpL%^#W}qjBKQMYNki*ql=kH}bqhwkE+98C-{p1xG-bOuDuUoUMGxB!kBpq#_ z!5R;QP4>%)3~QV=wE`s74+)E%U zvxK(R5#pVOcmwNd?ywxg0U(^FZAt>HLE=wP-m~h#3RSxwKAp7=J|2TY7a%pm;I>ta zLaIrJ>TQiTI?sS#gSNHi3e@W$Qu=wRWqJ2d17aQZk!~w|F`d_dKUCkv&r|uc+o?9mCFf1AMdluZL5AC=qXJ9^rl`j3 zQ-bc$$YYHhX4_@3ZK&P@cT=u1j4@1$7hG8W+tLJ%--vlp7Ut#q?~Ih(YiHn9rZsSk z8TF7t)PD!h-59WqJB~%O4`)h!z z6!sL?)hcqyefKd$|zv8ka%|}WX1+$Z> zfrNV7i}0RTdW~)aG7iPnruHOrIuo(OguN3h`ivi&I~RX_>CmD9`|}WlI7yzPw^S0O zf|sU(g{)u9OZm*ZfwD6X1lm1H!ed~w4q6QvJK%rj0t3k9MI#FdHJ zBGITNsnun5mt0Lh5A_9hEyVV)Vwe#7er85V;7s^DK@-)5t5peBK_0fmUE{^S>LM^u zxQl-?d?=g&eVnn$$_jZ@#mxvRwV%N=Uwel1HW@ml($5fd0$%J^9yiI370B}u6G&#E*KRf&0WHW`mf?ncT*9?zeB zGu-i5<)blSWwEvdP51kjvXL{OMkrUj!nrGF1@Zc4Oz9$&^KCC=s-RQS85@~ zVt;WXgsT9EFAEWRXN^D+izuoE3UMiA3D~zmy8v4eQf&+9fNTg++Im{6_{IYo)S5e$ zMTJG8>vPRP{r)^%I}LF=clDfHP+hy(q4B27?p}C?0s(mjK}Ix}s4=U^iV-3R1GEh= zNtH_*bQ^Z1w4XyvvG3bPfRfG`e5t%V{G>JKxflR^KY=%(=h%`vm5*}cRvqO3Lg{j@ z&w2z(`UJ*`^ilenvjM9&feW)}zzM;4m5eOHQELVQX>o-C!T}ke$$#OV<&LPA-S~_9 z`R5x@r|^%+XFmPtJu@V3Ck|uG7lq%YP1EhLVK2SJ!i+z84J?{`8+|iqeo-yKKf?)z zCNQOlxC`F)&shKJd7jj=8M7b-ER3$tI7cQ?&PGlKE=rK)k(WJ*7zS#xD+m{f?Hy!W z@1$hRCXkNn#iIaza6^P$H@2zF1Z7+y5MVocWn1>`M-dr;@d3d7>rHZynbAu|f4$G?2IS+}X>g6ySU?K@H1^jXOe3?Pu>KZEXO z`)Oame;~RAoNx%3CIut`B=vMp)Bw#pr*ZKKy#Jqi&x?IE!#~lT4u?e21fJg=>D~1% zvb0(xutvZiWI=ESAY{q{Yki(r=sriI_mEuVLYBMW21%hWa*y>0g!qVzSuWLzTtgLS z8Lek2$)W#m1^#gk_@2W-Q_huE8{n#0^PY-OSA5OvJN+D+8IrE{e3OMqtE{V8- z)wsY)2bTmS8S}^Zw_0)A=?yDK3<1!$h;>0c1)_w#lyP((ukuaSJ&dtS`t!fd;9NbE zS0#HUvd6v}cr!+4$)*^Q1_lwN%S$(w9jd=DzEc_evVX?;z%xf2?M0#fC#B2#WR!@b zs9N2*(G&Y~Z`5tAipyKtO6$_>l0L3-x{Rcc+aHO)QMRGaby;UYkX=*yJ?a=~1jRRb zPY{-8y5Cp@e91ZcdF0QT_o#tEEO>K?=00hq8(KZ;`7Vrj-w6ZqORK#0?qZ|(sjY{? z;-uIiVup>6@Q!CbyqcXBl48G3HO#(xEz#nqM!u4DEdJ;3a}NHv{$K<6AG(2>nbr+T zmIUWE#{f=@}3U-rmxgqfH{Ru(bI-MX<5bl39 z4>KpM*>X`@LE7_iL>UFh%zj(mWc4#;{eNfS_;Yfc)@s&!L+UoYj9dSm)r4WKa?W0! zDlc`|HJmG)ir*->om5C5)RcAg$1;E9+D+gcIls%0-hlQW5K4rtK$39R8e_3ouGsAE zY9w_t+RNSHlE?H?k5HJ_g%zr?a>=(0zqJU4egPj@jk4c=7Ly_4#91M5o)4}mJ@RGi?$z;(@vIe^?-y$qx}05>6B>oHHn?%sftSVL{k&+|uU4K942L8=Ez>YZti>M=6)~<$6g#t95Vs zLeVW1MUCPv z${0o(^(q{hITxcsUbgftulV8)(h`<~M#=j<0-CmdJ>-Rzz}DXS)sqIdQ2&hAKaczq z@uU{-uK$AXe{Xf}TyP_tB|SSv;&?Vzhjh0t*iC=*(m*S;R<_MmXgM^)p<8n;NUSjX zCk3xibQqEV-Mq#X{Y)%$b2}^qdpKhKqQW%#DL;r2xwKzic(eqq1Q=C z3*fZM4;Al)S`frcx0Pvt{6`0gv@dAXg1+$o)3p*R7_9FP@WuuzO-rs)Ymg*7L1<)S z_)^gYlhX3wJW_EP8nq$cepM>tKvXR%)P=$<+a2KUaNwaY zh<>FskU+%?n6_fVH;mUD_LXHZMT|srK9pfz zBFr5`InYV>TD;&i4N`L~O$baDpaaZ5K3azWk9q;$iXo09y&lnov$<~j#bOe^p1U$l z?(}$9Wv!Uh(C-io04;A= z_5yjhc-=|_=4k)*A5_4TR<9B2fiMI|7z(u}br!u==s?gEipa)gf^!~$-XX1dmy4koxQd?S|Z@xdBD>D{61Dqb*`PaX9x!;(P4v@spsxd8^V*+LDxF;4G+$K z;P@u6>(}BmmNz*4^ZR(t%VOTr9Tkf>F>`$hK}y#TSL!NU`32QgAig(S(YMl|1ro(2ULJqL3)PQ3whWxu=&(djj#%hcfnEM5K zV&h$N@N~$gRO){ql)l)JhiBEBq(h=d6oleFP5KkQ(1Y6)PwMx(AcH_v{G;MO8~2;E0ffRi6z3;#(sv7sf2+kesR;Zl#ld^9Ie( zUxT!D>pLXP^=Xpc{$Gs_Lg9N4Q6S`3b5C6oX@`9I8ecC7`+4nsB8RB-CzK1Q1gSVDsF~({(r1L9HN7)aQx9S-fvG zxf>}2;zM^0+3I3W8OsYvy(KlR+f9D$7-j%QGz(#UIf?ljqC(UH>N@FF&Q0d|xr5^O z>H#w z>ZS3{@D3Q@jj1~q)IWPS)&@ZEO-zkvK3{`(i?wKjEh0jcDaR+woWAd$;Ul4yacg_Z zY@@YClzC1Aws#hrhw~01Hz8a2Z8%sO=KF%@70PPwQN_P}%3=MRSDE}|f{CX(1!TRF z_F5ecU%c~buz={v{sHj=9?AFL_kGkwBK*y2Xzei;s3sF%OW=7&&%%^E27>asr3NI2 zAY@22?WdKFZX}h}MX9%Nu>BEY&Xz%kC?Ey~IA7qPI;;%Q!;#?VY%xB5UKk7qO5kmL zkvr+)2U|P%5)k{1-GW4bAQQ?(Ivfa>ms(6_$|m})Su$+~Tk??WzxwM7T3|ES?pFhf z`<8Lvuk^De6Afp^As-Q<`>QjL_w8PZ0I4U0F)_odvXVrCWh*&9aNIThPn;{ij*N~@ zUtA9XTKa?^t&tNEKsp4Ex%Lvrv}9A+>H3c>|F6d4v*1qsHiJrHo0ks){f@kM@O=Wy zXx%>UVWJ)E*r=sMY^TF1%R!jM2U+8!cTrkzz8nQO9+YbF+SG3UtqI3`9zcLmp%9$) zfLx2ZWyMC%gy*`~WJ~#gc4eK=@EbDhp7Ea+PR}vWAew9mPQOAAw$xbGG^ow1jMJ^n zjk8itABO+|X=ctu>I7=QMlT}`z0b`749kDt%<}}q+owce7j)rs;w*o!gzox75}XJ@ z)1ARIGm>!`Lz?nrc|HFN|9RR5$~@ex-n2XYCzWD&D3}g&-2H z={YbHx}-dyxx07=fho=V?Q9rY+su6uj$x>-N795g{{buJ=Ag;ic}VQmiRTRXvn7^# z6{O$RW||-jv;Ut_Wi{li{-+kf=E!GW&Oi9vyzD2?vX=tbr+}m{K3fg+!Tkpb98+z2 z5Wi^y(Qq`(W*Xz|S;n@6yyo~M<5+b(xtKTPN z_%pc+F5|&{6TSKW3$<0k_0m=`_ zeDn<&8QH0i=S-~UIR7}Kz&%m@?_@I5tV_A5DQN|jcowH@&)+wZnjv2jD&agUA6+1D zCgFzaMoPJkeT61fak>kjN#gv)wH!l>?_O@Ez0YlK_sq`?n6fjWrmgYj4TLJ;l%lrK=4&PKi0kf202P&t?f%Nh z(sl7u%vVTMJ;Whc%CuYdBK}oiQ9bu*3DGtCZC~5XFD)(AEeDT$WAhr(--yFBcBje@ zrOK$b&fH4J)kjN5YlSNFx^}Rx9x?z#15}ZM#2i}fr(;XF89l)oym(eb%y7r{-kxre zI-2Kp3N;xi7x>@%heQ2-f3t_FZ+?Jin##@FnaV# z6uggHv~&;w5@c~_8X;lwh>>#LiZ}%1x|_PYt~h;6bP?{We!Xo91u}r#)%o5{i2WcJ zL6M&9^MU>GP7LSg{pB)lm<=FpEU7_KJ7J@~J-te2<}cM5C#>HFQsrLtgg$ci z{|rlh!1gtjb*r~zf|dHqLw3uq=+Uf`_zokzc)|#AV7Wck?^!<_OlIoKo=W(??nsh>^E76liji|UNk-w+6(Bx1?gvu|&e!Enz`5#A=orp_TZ2<&Q>fM7a z6wbI#xu$d3MyjT_Hv^-$C+@*>YYY7tb1>UOn!L6h2wb#26u_dee*!_1EG!Wv8MtWu zCWo#;sux84R%Xh@)XIv2R}skfmqB@`kqhZTkjsr|9d^^oGi3tJ!uTPyrSs@MaK`9{hCgJwN1161nh<$|D9h|!`vqYl=fRw@VY)Z7d zznxB7$&l?{Y3@QVzKz8DW{`);A{lB89-z7ywXXv|3w+S;HU};ZMA{ApP8RnU>}VE? z(~N>al@7JN!8(?F7k}TZ!Z9BzHHQ~?m+BQ!%0wTMixS9hy)goQ_wFWnw7{AUCrtP^ zX-6RjDh4lf!ps$?HuKGXX;6dwwM*`XfNc6v&z~ON%}6WgodO~LmZ9qZ3;`_mNAFUX zh~Lo-X|+qIU8#+wvxvl^u}gM#-{4|ZQhbWm2Xfd^&v(C&#sb|n_kXgcM7*N+QSnPf zbekni1}tZE|BKYgPk)cq|G;;;g&t1I4=Z@=HZ!B0jMVe;Q@!0Hmg(~;7;R)5O`WyO zi>n*{XF{QMb24bhUuT>$@#-U^@mFi^dEEg!%BMfR5OCNdQbz-6eUPf}{cd#Yg_R7T zaom9j0kgzbzEVXZ1=tV6_~aSA>~+Y$cz%Rz{1Z56gl2aXjC( zWogBhb6{1~$XJXBWH=)S6Zka6&3-7;BymQ(ZOhir_Tz)g{y_E<2~y?o+g0pHsSK6> zxuS|t`G|8on&Hc=D6*(fkmDwFlWfb2F%HVft8xXrl9M*%8rI(&5!%AT{JykjStk;6 z(H?HRq`o&T&Qh2bh!1Y2^0!Su5#!77DR1IL-x<7Gt zEE5^dLSozLrR|4E%w9xW;qDrke&($7{j;|WYEB|c!oh2YoVy{XAN-U^P|%u>;0*>} zK&S1+td58dfs3jm#y9seasWWIvi<&cr0#(3q8-k(`E&*sqghGP` z;D*9NhLj*?JffB<><2Q*D{o8ZPxe02yC*n8;ojx@XLf-=T6~7NsY@lUJaU3OKFEZ; zy&B(7jM8aXv&zOy!*Rjf-h0Rfh#%lFNPR=P-SW&=rhnOiS$FyI)QP)4+!a`%?iLu% zwhT_JCQzzGupd<`szd7O>!8ehF$zKy@v@LKtZ#r{$!7nhKPul=IbPiuSMv;xU_h*A zuu0?F$dwCSPOQUU-7c}jG{kZU3=;_`R2oq*o5YVWe7YUld!rD#7R;_e6l>WqiK?yTUmGkYcm-hYrxQ0T_UX(!S(UcV1=})G{X6zG8 z7P$(N0{=U;%xU(M$O7H+__yIUqZVRNUnHwScJDtIiz2I))z)>8h|4iHwXAGS>5Ag- zXzOiuD#kc5dcbEQ$l22J)P%FQ61YwGNMpXm#@zUVE#gBGa#PM@$XN+8#)Zwgu?YvM zXq*u^{dOpd01eQ7STeB9PPwL{Zs)mP>O!pF{+Cd{guqk3rN3~ZAP2FOX#LLEDDmd7 zeZ6tay~TVvP!_spy+Y@N8PHE^LdwptN?!SH6mn`p3j{_UuA!tck7fB9A^Ovw@g21p z;pV?>q$6$oSC@ z-N#o_7Bv-?KCCIL%_7?ci(bQc4jh>NUb3_x9mE*F8oe(CH|8b>`CD2c+EmfMZY9mGR;DehbB9##LvezYI5Sx6y(d(bi` zpK4I{)7N+PmT_iacS>-<0d8?q9$z@0Bm54jRAdR#gJMaKh?!8K3*GaW*SI+O$m0zD z6)kW~zRq$r^<qq~*R`D~8i>c4(5*h@|u$lR1J!N>B>FMbaVqM-)L2eyxKsfamkd zZ8j5*LBZ*aZ}rbQUW|NLKG{@MK;T|l(rZo+m&4&)p4&&k#`>KYmanri7uJ)NzhvV) zS~>;b7!sKv?S@tU@8mBOPxCOGBpk5;CtJ&XYgw-w(56QqEp~0@P2Sud?E8W!JMZo5 z0*3&W*=;pd_uA6dmQ<6%_+5IKXOO{sX<+u4qOzZ8wtp24J0(>e(sNVS3$%`cd8}( zY1)0LZ1cUgTY?zrU_t37;4J`58Zd~>v~&e~*Ol+99l-~KN7lN{fk?KTI{eRn=n-J6 z_X-I-AaM}6g1l^)D*O<*Qjy->P?mP1p^oOa^t!#RF09UR`S9*kz;UF)A*BkqD<5B< z$pBq7Qt|S^WadeEto%2myH_EFe{SRzvP}Hm=D1{k%|K7t=9m9_)UVg@)mXnL>0%Px z0GF%rQm{$@Nh8)wH28uRBfYoT@2&VY6QBoF+>N6Z@5%UI)S@51HM7 zaT=_38Efx({{0IF7Eu_2n-empkO6+|DNw}g@rvrzH-X+;JmDWc5*&KgA$6i#cy7s9 z!dA*Pd^d4x#zx?bQAwA!Ry{a=xi^)epmH_EwUx5|%a_b(Re7q(N@i$GHC`OpWk4r< z&`6O4hX$?dvYc$`mul5r`3*k_c~5~E4ev!dVicfEUR&zl0ANCRfYV3q_)0j11X^vx z-vimKaHi;_0&KoOxr;%xvIk^@fiOu#EhnKnwGx%)y6~RTouc~dBIZ`M#Gi6VY?9Iy z=1l+=L}#lCos_~$-H;ZWZ7`|TNr=A;U?$}VaiUU7rWu+51OUk{E`rz%Hc)Vh@Jr-%tS?9PTt1EawIBCvR`wpO*cQP2C2z1heklVt&um>)9N2S z+g$bmWh56dnV}MAGlcMcCE6Yeb>!NY6@npdBWG&9@ND!d8Fzv&8K@$qjsQE zefIr#TDd!F+B_a|LfR~enmCCqzZw_RPjEF>@3jUDly*Fmf7 zs^9IXcLt(??GTo841(1x4scTz7O(oPmeHYz1LU%)q%zYmu-QM zt2GU(Fj#QBBx)cQo|__|vmiaf8FW{0pejHNI`L5@b(*FXh15NUokDIsoB{B_eH|LY+B<9t+}0 z@JfK6)W{j1U*`H^6y!9eO(cWav0s9Z)IJC^@J1>aIN@^}h^>EHbRBrTmp1a(QbY}T zw^4W?r-oRc+JtjTU)?5&fJ0uGL1W}x0?-XX+(NGU!nuhqqq6*^6t*IfuhB)l`~u@3 ztOu9N*8(sV1iZwhIIMlYUzo*5#$pzB9H}lmO6-X7C3+wL`)t7qB^hwdm(KCv5aZ#I zLjRKf=*_ukJF4#KInTn6{$0ZNJkH*N?BQBw)Bct1Z5KfP`PE$$t{H|W%&JBb?DwXv zrpj2%q=!dy-fH`Fa<&5esUM*1bv$M0uH{nM*rM2{7YQDPvZI^=| z5@@Yt`p}to!4bOt2LxU!!TF2-x%C?gWZq)Y8_8K?NBq5E*0)AcGL95D_IHAOb43wj!VcGDHRmLzuOI z43QyWCXfID0?GFr?CrJpz4x~L?!W%b=A3=b*?T{0J!`GdIMLr9^78Xd_})*vALcR2 z=h$=+(0%^PlOdj6d3+~!Ip6#mdNWC`DB`Q4yP27pJgvEl%zr$}8Fo>)>}IaK)Mc^Iz2iG@VabOyd!yg3OdYymhM^674Uowj?1=?Urg)s9-(aD(n!07;z z1A%Ed;L7wJr}D5LV?^?kKrhed=sfa*qpFiKxtzQ3b!y3)!J zTy=nVY_eg{PKIy0($cy}|KwRS`tvne6yiQr18v|)nO7z+KDd9Mf@YX5OEjKmL7g^e5O!Wdt@G-w8Purp75ui8B>9 z)5%{N4!hE(l5%evPZ052JNcul(SN+q%mrj~sO}PrLq;7(PR7)3&sZ)mAIhbCqT-F# zd9v)%>6Kk#wnGYAf{y&(JC?eg(`HPEtL)i#1Q&F_=6(N26oZ5Zn>jjaZow{a${P})=P|=v%g;%dR6XvYxJRzwA$O#x) zT=A8;lS=<06V1)r&IvEw&+=F+1lGFbDkT6|SPuwd`~z}3L|q!xhzSV?xvCI;;m5sz z_y?oGWjVCNacnsY%5>iP#F-SD9QF;ti%q;xbWWYif# zFV1#e)`ttk>Bm;nm7kCY2EHYMSgGhx*ws+;B{qFFhmnJXAe0x&?@a?6jW3SY`NJ|0 zJk*2#@Q8SuKfW@3$(mn7dVs_;Cd}3w;Ml9r{`qj8nxca?sTkX=;)hDLD}v=KKD_~X z3&($-sG7|BeNi;dwy>115pu9P#}?KiXeEY|X#8F9I7Ya^GqhGwBo1TkFpVH@ZC?gJ zy1MrDyK8);RM|NjprmGh0&J?Nup9!v5oQ2kF9G3r3p5n80oygSjY1`lXQLgU1TgPeHyOhv)*K7flz;X+N-$FrXc4 z)FY&0E%Kc$s6s$K8(BfNcS$CAK7rh+I_I|r)xC1nKw3sYlzJKf^8_=c-brwAQfjl? zhcqfQU-}$4IS4-TL!PR^!JdBYCvY_B1;%X<)+7)k;O(YW&uV~%ta)Y>aU?;Oj@nDL z#7thU2d;%YGYQt(dQrYJ?ip}|TWekfO6xbW5vr`6jt>i`jt=PqG+zuLXU%r?2yxM< zrQ%+jS7$dv{0u}HG4jG!_pq&G!e>*$wKqNVc<8}MHry^7cd2mg-r0t+&{!yNe79Gte2o zU7HsWpx&@dVyn*@sdJ#K&lDrZe~iN6@E2kf%oW=Ie;!QcYDFVob#N0ijCxJT>^BW8 z@|des+k}?Jmpfmnx-WhG3r1dKO&aL!0S%|FJ#tgzJIx+prmB!T1>fZh#KN}XGnxSC zjcz;!*5Wsf==R2%WGl#oMS36K3`k6Z{u9Zj2aTcN$BHxHH(q@U2p|$5h%lZ=*9jJq znn1?Xd}1ICek|{^zwKn?2W_#Yb845*t}H}5qnU8f4qC@QG-Uvvt$5>50AgPR67Us4 zGQXf#f6y{z>GF0PVel$hcGFJ;C$+~jN8xO=`8Qb{Kpa=33#>g#ZnYu=owWHj2;Bay z=>WKTaf?@+Bn&-_JQ6N%Trt9n^@493yl#HW_1yu!;8=yJl{rLM_U)n}e?aQZs4N1a?6zy`cKnQ4GOJDzEOflA2!vWsL`1kop+zZl^`uCp+$VNX; z<6I`wGL2-63m3F>z)}hUssGM^YlVVX1#P1vdSz)OlS}(%Q16eoyt3&6FLuaty;7}r z-ERUmWn7R^We@qM7qg;zh=1IuRiD)IfgV_<#vqGKVvQLMi9#Y^Oc(Umyo0leW7z9Q z6-pD8BN7mL^_KEFr0fwMeAjL0%H3Wh1N#Nii}$~drpzfzg|hpb#j6{z#?HtA9W&<% z2DFU!I7ILEh391Vf*>^S?ueNTxPmAau?iiCD$XC8sehB#^=lp)<#Cz86?)iR1%D%pO-lnvJ9V(?kZ+&C?*DX~ zdzs%jej9#yxib8`@zW`6c8MPZp&=%DAuq~JlELA0J9pbXq<(Fq_$k#sB=*%M9-_R0 z)P#n_%=fFG7R@SbTXy5U3PE(Je|O@=2zhTc9B+;2z;+rYUF40(<^F7j@VZlZzgA$# z!PaN-!&E29wE|f2Nk*UV3;r}c>CtR53Aa@hu5OLO?vb0zo8Qn9w0IO@)LASKiLQ^=q483i;xa-U6*u7cWgZHs$XX(jr)+@Z!JUOPEd#m1)xdn#Xsg z&$a?h|8H-HOX5YLdpkMx%v~vUPQSU|{gtVyfifj@bu?UHsqUh_c#p(r}mWRq8 zOaLG0FR-8)9QqF6oUjcnZjmIA#6`R-?QOp!aP}l~^h)HET%%AsqgwxYPsx;_oOpN_ zkJ0@R-;DM;1nmGoH|KBAhTs9j)Q%=l5lqF4V?+TSIm2`puXdOgMat?5Tl{sJ zfqsbdDZ;ZVz9^@qy_eX;@P9!meg+{Srpa-pppg2Fy_<|uUKFOj0VFpNK>=HMOfS0L zIYhk&d7L9z6L1k0;B)HdID;^)=X<#2A|Rdgat*m|1&1X)=TzM{V7_)Vl56-O%MRfQ31 z$0N5L){IL-27aL;urC3eF}<;$0Zfxe2wA6cr!;lSgXrz9fOIQM#mz-SF={H=KJ30lOO7n~4KHCFcWmXGk6+cTZQ=XG0p zKYmt>kX0s{54k*msfw=u==p@b7V>_CiN{T}0i1wB_(mjivh@@g2VI8d9fXiwL)IV& zCcgwsFA%q1w^*xmUYv_&0;^OM!-73se|GL4UJjeSlr7Z z2!u>f^!pWhJNu9nZaDWrqdStda>)boDiO^m)SMD)lr09r%D04W2l2pxH4eRPCTfPi z$_?iP@c0=mwvQw_SyvR#A^5B2 z%-OmPs{e5UINa5-iN#Jb@AVd(CnGiNr@wo#OPceNfbr22w19D0Ic3mR+c@c@1JmEC ziwI5D4wGP21+*HH*$x(!kfs2xM@qT}ARz1p2zVr>F2xm=17s={A00?x*e{!|_6@pOJJwPmkp;QJ8x~rUpznR16?$MLMOx`wsD{gp_iK0TWJWItrk`a3bfR%4Xxv z)hl?9-yhDG0h_|kFG0wX2KfZPWr=1u{Vz|0SQCYc_0a-CYoing*n}R~Py~Pxg>K(X zjQ8!kWVc~n7JE9r?DxYtTyE%>L`YLxcPE>FNbSy2{JnqlmXo;mAruDDSng1_>mAye z>w&C(Pcz1iYD6_sqO5Y={K`fLUxbdq6AXI0E5v38!X-Y4Hw2C0nu$%swluJha^%p+ zWixfI`=#;_ngEeSBQigN9O83(tZg)~?X@Sj9UPq5KiMwawLsWY*z!R^w-m{z{~$DL zfanLo@&#z!H6xntn z)_QH6wQSk-`Y*0o#rNC*EuG$?ejXX}Jyukd&u2;tqHle>DBjFwW{f)9!gnhoF^Z9$_&S~b!k z#sUeO6OfQnp9Y!+bhL<;@v_1o;DY>-Y5jg>ywFA)Vd%dOZF>&b+%-XM{mr)N!VN-xU7MPn?&{5hHlD>vh zc!xq`-=z+qmq<8Zi;6g5$j*Lu;h5xomb^OtMS+GhNoMF3BY#*?kzji3b9sycRmCh z(dWW0mC^Q5k}^w*p?eLy> z-;X7Cl=qLG4d9~#4kArNAi;5wk1`a#+$bsuCs`1YK?lT(JQuAs;h!Cm=1Z>3HDmqs z0k+0pzqIYf)qGvvasG=8)-$mB*=%q3uC@ut15i(ZrW8q9#m*(xF2?!tm%}-0eG8oM zJ=+5%peYS8_V}xlXU+{;RpjY9t%PU}n}9t28#yP|^F z9{%D5ZMNV|Zw}o#NP`?4FlsfsAiD=f&?E?@eWQF551fBgjQf^>IBsf(2!dI?eo_%@j%EzH&7OW_7t*C z2opo8g*{+?h4E5PfcpIt&8Jw(uiR$?(5?fDSVG}Y!bQAU<%;5>=ZWx)tjs1-wE$=e zcbDf}aM#78_d!M+^gFmVdpU^#D^a40U=xU>@C>sfrtm@%@U=>d`!MW_Lxo#hW;SWm zV%Ni@fEgYjUsEoTT?j+i27m;JhcC4VpgJj9u0{DmZHsS6OWp>EvRR#Z*yzP?12re; zKe4ABc;XWcJ+JS-je3QIQ7}6F`Ewg7nfw*khpbP6o?;u@FFELzLLVipF`#vtg|?x5B;$#<7`A88T7`eUSRsSe|AoWPfS^536FCn_PQm7XVVIuq zH;dla^ZKEe_b=X;L+^DlBczLLs2ffTfqK6`r>O<|28x2?asn?n=^{=e!xq{HKvlZt zDB|vikW>&hw%bZv?41r#lqs_6`kA8~U)RNW!`@&JE53NdC_R@;A;kM<+7~Y2jS%uv_XV^!<8(Vl>jPCKENZE1UlaX z#z7MtKJ#X{#qhQGvi+a}|3F);J(KDcFnM&{Y&XSq2MGy6RU#dK6o-Wvv-LS?axRyk zyC@oj{3--pD2t%sI^(jlC1EqLVu+YeEGSA+zcqApFlGWk{iwjUc~5{H5JnyLhyQJ~ zxweYn6@KV4^CXXs2|xFPqgqO3b<|pZ+(LnlwuE$FuSV2Rs@=|8ziStZy~lr6wmR$= z==T%IQ{RTx$)$HiOcUQP2Z6&nVy+5J%4>5EO3d+u@=4Kn0W%#P2>ONioqxy!&*M)n ze&w*jNeun`*yJHj)-|~&7$9i|k8~EzzYXAL4M^>0wAwuCZ?!n<=Ucrg0WyHTx&wPF zDi(^tf!ZGn!Rn=LpzX(Lko0l3f02HP6;~)w{s4?)N(~mS^r(47}*}$J9p*lH+UrINKWq&|_H`P(#6I z(Y9a)9Eekp_x%f6X=!(Rz~uOYpzZKN3iP}o(K=ufffT;gCl}iMYjw|i80%GyB7EKn zleSAXdQua5=sciT2DR!nb70x_RJ=LZ!WqsLaKG|eTSwXyW7O6}hah2t*B9F(moKDw zo&Gve;U1sIpgwxGtG z%$c8KEBu=_H5md zW=^eDDa!#v`JsB&Z9uVz(PNOc1<>TWeG%rtqgL9m?Sr{aIVuDmkb}H?H!MruxK`71K^3PLDyVbbO}eiQdTpTPFL8h z2V@6fvJitQgwug6Sm{t})zit)aPa=-kJ?BM{Q?R}B3ld32H-$%uHZ}v`(8C}6w#)^ zxdo_K^-CKN<_uEm9U2W0J9>mFLTtj2Ff+i#G?!D3J%jKHYUNH)7DMGc1sVhRcNBIm zA8-UD4s3y85=#R)?u){bftx%hor^*zm<&Up`@_cy7`ISP55|FY7&Q0?cl_e9$pwP$pJhOfhz-uvyTAS!9E5&AmN+oG zw%VH*jDDb5V0s0--RxL_n!t-dNegObRJZh!{E+D?Y0n+LC7-w6=WfZY&!I1lUTHn2 zdPsR)@I?94ttk)yOdN%Tj+FSLKuLl3`i$E7hf#CA;cXD7#^VDhR<|r_Y zF@-Qi*@=O_;H$Ba>rC*z)u;)yb!`_D3<7J>AUV}LQVo{7$U<^m(@9>LE8y=cXb!n< zkly^_n&>)Zz}o(&!B{A1{1*&@6u*LQ{%XTDbl{Oxjr1vx>Mij#7wK{;N|0ZDJ;yFV zM5$J@HVpU%9k8_vs)vUIMro&~x1YT(6GUA7X+fw$jb;4gO#@_?#swnJBtW2@pdHbW2G)kzwrwlcdT4{DYa0zlWWKVKLZ#y(km~+ljo}xD*1!%xWh1=pmMJGu2kTUPZaMb&iG%C*=V) zcN`1>5<}OQYDFmn7PTl{hZCqU=FuI(==9f-F&MsBoel_51)$Yo)`H3#JYf78f8LS* zI)uZQG{!D)v01C^C0mYV+_;&@@5z6da-v6m6f0 z-Hc%FONC(_@9YiA3!4(8mSg;4-NZ+AeFOJfRqxW5+=ipJ{Cpa%4BZy(&Fz7mAJC4YePu+Y*=@AY%}zK zKu4bvv~=pTs^`=l4ZT+GzM;h}S_GYjY*+*!v=)~ttNd|VXz6JB=qrL@)X>$6?R{`e zJqWd4Q~M0%2oFDdjZaHL9Hz$JF7=OExh!uz8~OLS@78o36gu4w9IV)0u zozu9Wl^BQ$0Lip;2sjl>C&EEzJvd!cA#K^I^L_^yGjy|LJKNNNJJ9 zLh&aj*Om>1mD7f55okmcIUFXy*sw3DMKeV~o;=0s+&s6@-HDmF3ld`>co;0w40}Kw zy{;N|LE&z&R<8x{;ORP){1L}SLx8lfQ3SBKa8f?)w>W(#;7U=AKAOF$r50F?zjlov zq!L>EdK0^v*Gv$Sa}*e=Po1IvpKJ8D0Zr(;fo~nv9QQa?DDzMB83-C*4J|T3ch!9Z zS}F+W^5~R<(nb~bLhy^NmbM++1{oHYwuJ8GKvE-g%R|u}PeZeYE&~N!VasP5^Vz^@ z&GcJvB?Y)smtyq68{m4m`9rts|8YcY|C`F4;+4nep7#P~38#X(wvhp433l46ybDyV zAp52Vl7b+AiIehHh0i6QQ?~^okq~JRgltU(n?_w3fJYFG_=h;{CR3-#D>Id|5s9%J z*VsV&CegzD2!KDK1#~;4^t~4HWN;et&}^63ybNgCCCqdc+JoX9EUd9nVnlbKABXQr+ zfKMUS>U!|AKduJ4^9C0r!ci9i3DJC2usaJl9 zcf})IVTcl9GLQ%^0j|a7Q1wF2f+F^!&RXq=WETvc_zVzu{?u33F;!Poh4MB+UTor_ zcW0q93hA?fMXSy#)q>#UJA^+@bKzi>z=J`VBJi^15^)ry-LOy*e)W3za>>NCLX-Su z(4$OIc?3ts9tI`Jt2$^I1l8-Y9XAd@>Yc{+QLdQB&i zwpBAoWc9rJf4XB#Y70M(n@bK{a#)PqA-NJ3T`#tdVBm_hPaV29bc3`nQJli4UCOR# znvTQH)k6HWh+pOa8tQB~;YNxZi_?~_2T@5!^={VPH_<kA0#|D$Co!F+KcETOXoDr=>ER_*%?HJln_=ISta=ySs zQJgsv2A6lJl@Rpi-;Y+OtgwXp4zI>K9WY#8$K5}|o#z==Y5LvQLa8OzZ%RH1eaR9bQGnHV$0!6!~$E~K52hVTL?o>TfB zO)`69@X(OwQjyJzCEw*&1>wbEvP_g2$CV#?xhVdJ0dnOd`&$XmQ@m1S~Ke>P&6qR}|{W}q#N^ZRXCW)oe*T^6ka zX*qkR=e}3+eW&AHQgokKh9R;u4OKAT%?awH3D?|eN`Ee2eKXq9X+$DB*~xF0ikyH5 zTa%-Bej%GAYitpAJFcrcw&IqRiinmvcb|o`*}Lm=>DNlqL+eH97D~RFq(X(1qA{QA z-T59qRA<5S4KB^66V6#y-Ou+GIo_!Ga!ZnEWb{%>h`44*n^nBR4sru_a?;eXVP{gb z3@KD*$3A6Gv*6~}cFh&TF+MvIOGc&6IXF1n4eL%0UmsL4OYhrpuDz=y`Vn2){)c)& zlPWybkdRV7`k+?qrsD-A|Aj0JePAkcGb7{U!5HyN7}k}P0nt5_lx>YF6l)hn=%4CRuz63d)7Kq2ukPyTV+tRr z9hE*Q#PED6@8rKC;b2%Xob$Bo`Cqv)(~#k&Ix9+0kTJ$4^c~A+LG1Nv%%Em{l-30; z*S(ad|K-N$+mB54iZ6eyp1OltJlUw2C=*=qZ4d*9p~-l>Ia}Z^^H_hSd-rfk{iZ*A zf_3JzwC(Av*it;^B-Q-v!cLuA%5hYfZ=Oe!&G5Lg@<$IGSI^=oa<%T0>(Q4>M7E9T z<(9M&jwGun^?O95EHs@bR|=Xa2XDzz&X6h0y+D>9dpL_O(bIA%l+QPbR>`Nf(u<#B zZz+qg+xvEgNMw|jhL)_$?;`b09w()>Cf@bEBNuR@}S zX2|J<#zZe!W@4I_(}6~m9M(;uag%+*Mk}n|O%2Ta1lxRkTB!3+ugAscCtO2b!}V@8 za}V(>OEoR+{@wE7BK>%)vP#11ib)sZ%iidOx%7@+w;ClYNfhqIA#1KnKJ9GOJyhtk z)wz`LYWr|C2S>+D-t7P(RR5`uucFVsK((!;w1*VX+UYvGG0`~FtRjGLq<_~R$1(h& zHL>}6cSnI^f?r1N;76;_-GCkarFAF>)rd2K01K3kc0MBcqXqfJy_iONT}51DIo&x*@m!3`x$hEGBC#&>U4fIco?=g4Mjxtae-1BE z&E)H{`=&JS&ryXJ)j0=DW6hpYoZ}8DDmiAeF;RFwW1nHwc=w8n9rS*m0+SmWrb!xm z%q2~f3WGfi78YL!ov=Twcz*13Lf7z;c+hutKDjan=eC_dX_hj4a7jyyP&gJWMX-%q ztZZERr90$Q)JvjI`gNbQnGZLX)vXp)Fa8M`XLgq zxSdWY?LDis^_0sr>T(G=Mc#W-YJib0%GSI_OYAG4yMsh_H7i1w|b)F zX5F}tqUYrAoODXhWxAl{I;?{wb^Cl>nT>gMVsH6eGM|eo4^Ge2UZx!idsQ0u-!72# zbDeGE>U>bLqTrB^$7r}4$18O4WcT1@I=SR&q*9|CA?cVz7|HQ#hC7jyIgHJF`w zb|pHfas}SL=}(=Pr4Rhq8E0kZ$1%69T9Zrl?2fdiBwtAIt(~k>DMXjeEgug=kop;i z!06XXObW4tbi1naS$i#jq1Np(BU{mRk} zuUdW}A$Ir=KUlO455GC*ZDPlhu&UWfvPgNZkQ$+wXj!#so4ZzfdTN2b5$ zz;g0Z{-UwcZga`wdUps`+y@@}a=ip11%nIXUJ#y@q*ygwr}z*pmE{{z$GND8d-+ez zZ)iv*6Rgw|H7*+O+hC#u2Zw|0F84_?h2A^bWR{!z-cEliqUEReYZ&&M1eI)f!$}KP z-Ok7Qgat(BoFzR`?r=)EBSWOejuR)|I)zWNe!&J7HZ|znVohuP`FJd$b(p7Wd45M5 zG27FqKgvbii@58Nz4tIiVUVA_G_q$N7I zP-_#huA}+^C)oRcFkT(A2@+j!8h-pMqOuiY*AJB=V<~y6Grrau%I|f8YQhKmCJd{{ z3L4)qa*>d^fPzSt;H6XnGw3vY7}?Z>h-&Hs9Rfi>C00Ua8C z==Q-jh20CZ?Q%(2mGiDG*C}Wk8zQ7MZLaX2c6N5=it#5H!);o$9Oen7q}mPT@HGU9 zBWtJ+R?)(*!hD)qmh?Dv$G(8ONvlp{l2V=Zq1#kIcA^tPvnduv&cw%pAkm?C7OXulH)HU@Z_nZ3qjO_NB3y36{ zU2wWthn+NdVMMwrb!X*jq;BBq%r`X~w=kSSJw6pl5~_qlznZx~Bz7cZxS6V7a>r81 z*OhqEZ`a|Fgw#?BM~LTeD)_{8O`YYy3Vh893Y`Vh)-r!&ye5JL+qa@?WR~S2%Edya&p7p zR0?d78X;x`Y>g0#`1le?QbpAYR!u_TDtk$XDU%d+Cbb>g+Eub;?3iqTkEu(O)4NUO zPo2&5l+G_y?R0@PKAmCK0pFf;ZCdZvgD&O=Quh-JmE|PixNGfPQSV7;h0Xq`1bh_f zNwv)nS~a=(jrxV!C~bfBBQKL`GkR~FVy7H1#i+r{1?9=5IGJl|T5x*SuF98BomDBi zn$Vqi)TL=?vaS{jb)V$wvDvW_d?(8xO&IDCr9R7{V_tPX2U(|ONhMRiFvn6IifgVP zN#;83Cp&VFWytl~CD7}x>y+&Eqb7Ll+k>-I%UfAdT}s<1iHQn4r)6Br@oenzfVt6g zpS1gITcJW}nUqX&q+_mPaqT*({f}>yO4g-q13jH3&j;(e2`t)=4 zKL@36>rN4tZF5p$AN4YgHd9pcyg7s39#)TD@ty3yW)aX{oFupJBD%w6^y*Hn1{56q z)HR(FFEF%q?wIK&D{J)+UsWOE9rqimFU^T;W8wo#{_Pdql%W#{-5VX9_$(4x*myS; z^WwH+nqE`mY>NGSwLUN?C}>$x>^ZKtQGc}DK_x!JD$8lXi4@@CuH-gp_k*8m zyne~{LWZVijKj%&IR-xwy7-zgHm3a)dT1u4J25`)9?DQHo7JW?L_g7pEB&(xRO(XS{>P#w6WNqeW2F-Z) zj?ykHzlZl;0v3&@*fLsKxc^k6mqe<-0~4&2Q+m@$ zHR;;l(`x#Rf-j5MNGBP1hH-mnciO`54oJ4*w)Rhad=m?*7``6Cs8Wge>Z3=^HlTH5 zaN`*(-}%CcQq3F|`KuS8^jtgRr!R(hJxhGA)$Jwlx~WD z;(wbln!_q3nyq0LmPEC1y;EI20!1!+ssFze`Q*F3wAWXn{0X|Vpk-;db*#-%aN z$o_C8B^zm?Sft>TAk-*&7t#9%*>jK-)h*^8_SxIvtUtU0nb~C1D&&-@)SfDg)la4O|Fcg;pxk-q)$6@s@M!tbXa@jH4g;LJUr9T% z_JPCa1S=9r4fR}Jxv{ymt$)L|<(pP(cqyL9(=ID3GZ0<+@mnnIqDfVmR^AJ_fZi*C zwm8WwsZ)0dt%N(~@xt-z3sbU1HiE#>4xqpxLJ6?w z`Q`jj0q(#(=CMDSEu+|6n|c1_(GmKdDDtf>w{x>&x1fSvX`u){SnQb_8c;4x)o#;K z?0?gP9YxS7?QBN%VCf7ii8{e?3}R{Yk1=8~s>JqemDC5W^ z>k0tWw{zkMek$u>t9IKc!N>@D-d8*%Sd$i9&KE?MY#G8GT~E zO>&@3p*;6#_)nR+V$|5|kA72a1h6qLnZ|bfCM+xNUc}~j#WuZe#h)lUt+DsDz|h}Z z%g+tj7T$>@365Dys3`f3>f;IY&K0lQd9?&AofM@=dK-mMI$>-Bhh*?`v76z_P(C3)-(8 zFT^k`tQO24{aR7ouwqN__(PLYlY{K`#>U1DN(+ZX&&-)_`6QGkiM+&M)Sn}BP#f9* zrlKvQ{8U6FQ3vNTC?{6a8FJ5zFS$GP&kCfF2mX1HZDMpx>j}qDI&S9mEuF42UVq{G zSSOF5Ei)S#Bh@20LG?MmU(-!1fMab*>rt|rbi-w`7=Iwqu^3tA!dG(|v@_F(4fmVI zs+?0-%gNAiztOzooYLXg3O3_yt2R1?L)}c2I6pfwtI%%TKes&aqjZHn7oE|fY*0op zSWHb4%?`G!f{)6#Xwp~vUq=b~qSZKow*BL|GLl?6zH*JpFU&VOnYBlZ@TZT_=&PkU z&V#eOi(b4t^((gw0>ACuAn}jcM(%8RF?=(gq{-~38>u9IJj-P=K}lPew#>*RhRkfV zOj$z z250f?ajDEbch*ivv*%u&9AwZRLQAnqQDwE%ztaJ({XTAe(io(k2vAF|_82@9J9 zpm4+9QoX+5YKiNz?5XfA&hw0hoY_9!uG->9@G|X2c!uQx9|0isc%=Of9*o= ziaY6CVk3JY>Yn!qOVCP!;T&~O)9|FrG2g-ffF@cjtI`1?N9w)Nl=8X<)-E?Ra`uWt zQ4d1_z6tO}E8N_3`NtsD3x!)3vtj%bYQ?pC~e7JV*Y^q*U zQ-3M|+NvVH1t!sEu%5wjeGn|uOP54WItV-p%bQjFkJr{%#!c-2JY=G}(|^G+lJ?a1 zPFlH^sd?}*3C8eV6D$wEdhK@RB>_=ZSZbE3vPDXkX@{ccuRcFdf4cS_4}PtF;k$$k zAS!}R?ps~_-iOl?9(I%UdUv?SY_3Pg(EC(n)>CV=HrA~s+X{8i1>>_TNoEJtZDUPy zPi8=LXRPD7u=?eJ;#(ZULZ3+h{CZVkTW&4(4T{={+)rx?8fv+vlcC>b*6}yz<>fi3 zu}GMM24zOSL|dZy@a6+__r@pP?yz0M18PIQWPY7brgnp%$mO4XJBb_qIV;@zv&xIlg=O8~ z(K4zeOpxz^9#a!_3L&uwDXYiWB)1yBlB|fk3!=%lomAqQ?Kz^!3D0B@N_P{iK1xcS zu*^IF!jf*G1G^A_n*jUXi!f_d^i)?vA>Y28mxTeWB{^0Rgl?vGc$}FvNltduopu%= z+MA=jmuY9mEYdWAai%JsOIB8DS5a~S9MSf%f(46RqVT4vp+&WWSJ!nT5EeQ!*boY7 z?mUV36m=b8CT_G|;aHOuEsX1lez~%nEcWsBnN$;fc^Y4Yx#yurPK|hLNd;}vQeb;2 zx!ps-+q=<9K%_8PnItD62A>CPh=Yb=uU+XZ4AbCC;pI`0e{*kx^UC=7MGTE^euEWj z?&wd&>+U0Q{Qh`R+N~WGl?6W39c?9zCtRA1eOB1ZCDyDeuSD#@Y z)4@vM9PK~DQp0kWtnllbg|<87Rn(P*oeVzxl9t!rv0~WxVVH@E%J$*Cr^3Eiji%o& zTJen{23>-6NLcoM(^a7QPcE*c-$ zV#~>S5x$ot{Lx%8D-}*iEv*8=%(?(BtLM=-2Il}4}XTy71Y>oeSL(msVc(Tp9`U1`>`B-T%L$SFpD zI3n1VC#5x1AUBy&QrA9tW>74OLD`AJGeQEx_5agd8p*KRdlSjQ2onBoSkFl*g*g7< zB02(pF53S+4Zj|*yApH&6O6t4D5#yW;aoGvUio3DZON^rLtdjh&K>J^ZZE#|xDgN> zdY~$iD5cjl?l5oBd{c#G^W~YtFM!q+ zHiD#$vNa5@%d+KK=IH;OGKRE1ew_YB zQVc$Q8u?ISjcb>)w_h?oKW?Ux1JHKOn|R@E?n!XaFMViU36)Rd<79$8Ctly{Q1oVd zy*pXCp^^*20@=YHc6Uwy(szU;s$P1W`>?+Z2m>eoUQcTYM)O@6WSp~ror)8Flmqr& z1&W%0qr}LaRDIHky{OV@r0Nvj_R>e&)P}1v2@uMBSFv{9!=~12>T-7e0InGpzrpop z-tQFSufAIH{DPLUa_exFvk~_Hj>0@X+F;&oI>pw+jm{~z zq>om2JrW}F(?~$mP!meyEvr^;EjB6^;+4_@S5jveL&;^|$8}w%L0O0$ZM~f%6Z}r+ z9Pt&G@TlxFtiyyZ&&hzb8e*xirRELs&cB&b+K;t1iL@I2cd!LU8?KLdgq9;Q-agZ* zC85Ggel}LfQ>bWz4r>nXjzO&9=JTxgDSX+gGw7bFwC~6{3rGO{TUs~D zoJy>aA@w~9wZ%CA%QCBncf;?|98+$=i@W^ezgg9IEwQ>3uB(!h1rJW&33`(i(5GrG zupTvL4Q*gJf2U5X^99{ap>V*5uA@`j#Aq2=6Xj&5DK7MQxYIO9&?%NwXLnNlH(;-#(6=3umhfX)y@BX$mz#<6{mSuRxE?ph)9Kk(IP8jImo3OGx{<P1HACcsd~G> zT=D* zjr^^^!bh5^QQmQUb5I=>dU!vU@A13UqbXvY9*pC${g*TUFbd6ti7^T)=dNarYK^QL z8?p`6kWAg!c!ab*qN_Ve1I107wYrKc&-iYv_q%h_S+I(JKa@7(`!Jze&tq z54bf3kng!3)-@r;(9_}DaOSQIduM{eKmd*Js?K0`A9Dt1@#aaM-qcXeuyktJ2TbtVP-V8E3VSj`F&NA$-XJX7;7yXKk&^E8^g{7Bdj7|_ zwfyW;JUfrDFBiy@GeY57HY|Ey%TSUYfK~m~8PM$lhapXU59mesG_pcpl*PMw0yDB_ zoDzQ1oCk&DMWfFWZFCgWBwU7_?tJ`bf4hT4%ZJAClbKP>5lsZ%`FLhCzceB+M(TaL z?kn`}61x>Q<6#oL8CjNP;8TV6iLAy%Gw2Ybr|8l9X7?&*J{5wR!-mFQj1J3PbxHH- z?VIxcQ9g85lfN)PHsFh}qqW)Uu^&Iz#Z$wDY6wm4qD7R2HpS`WRiFEEP{*BYG@>BA zs@mB;RQ%cgd#1{VNltn&t9r0G=$8kveM}Po&u!kk)-3|dgyD4M^Si&jTUZ3$oa4g-SO40??G2tqF% zfSS@x;QUg1pTe+_Bob^<&5N>AP!N-XzC(bsv}oG64q#N-5i6gFBQ8DA%VNCX_ysR3 z?@cIYfUmjRSeITje?{@5VOLToa@dTa%mA$<{Lf9TnrT6Agn^YVnj|Ur2?KaCE~gOm zZZh)}#YU6wyOlcR4zuQ*Ezx^vrzDBHOZs$q=V;RG^769ZPMgJsp&LsN zm)tJ?@qTbxB3eumkwK#IwbC6}!LR}yHNclZo~WuO$M;{3Qxd)#sDG zRD1_s|9O$)txRXC!mQT#1Sj(+^#7ymTcDxN`~Npd*|KhJ*4-j%S0hH`&Ndl6$Ya}G z+7(3xTain-6WU^hCWUcrskGZ}Lc6)^B9~^|Yf&zR8I*_-#$f)huk<|a@0|ZRzu$BA zJZH}y#&dEARNYe&svGLtXXZwu72FZdtd z;QN`75_13azNNiG(zh0Zdy*qP!6M!wv`^@SpA0GuHS$E6%G1}X`5(Lt-bnxAuP-uR z3n(p`iZQ76)1Tx!IqePT=mVeG)0E!ie0Dh(($?2it12^zCivnr~-Uk@Ft=AYpY0@5Bb{xkQ>h31h4K@SR+0A5Z`d9Lp+m!751 zs(^PdewlkJqI2rd0h-x3;~(Gh&0mZ^6((cO>{(hYZCOaRHqXaZX_hm4zL?kgLYZ!z zX&gqmJxjE-ylTmsDn5O1}S?szKks_|1t;){&s7~NcOlCqSx2krMKk3T;jYmH=o8J#+H;9l!Vzu^1xXX!GR#%%X>g6=j z*rRqIEJ<@=J5Tae@ZwTzW|a0;*JD$YQ>~w;YVJwS`R1SNI-SU>*ls=@9vW3RzmJ$x zaiDP*HTEiS7rQG>*`}&b7Z39CevJGrb%GO7$ldwjo9$RA3(Ck1m2-f=LsP!?D*J+3 zOsyHDHbM@Dq!ND+-WtbLuVKaY2By*h!~H4`ZLRq%_37{Ixl_h_0+;^H`xcPSF+_vv zK_!$)IWrOZjEb_(IDd5l+$L>HQ;<}jF+8Cr5%QbHn@k$F2ly}VHn zsKK1=VJ9aN!B4Q&#U<|ce&*z=pbH}l;jGzC>R-z2*$Q8(+3Or#{6|@%T!$^pZwyjXQ*4095}ow|Iq5QPwE{~;4~ft8BHr9oX}UvHmwvyr&^Dbb%=%%D_I2ukB?*~z{5%v&1H0NnzQ z5{1S(t@_=m*>R;~vItU#u*}}u`D+<9+q2^>di*Zc>gHFiEeR_7@q&{E!6F1fW_>zd zh2H1^&V*fB(c|e2eUf!%oK4w9!)=*tYkis_sd$W1>(K)IYlBVgW4qXH^M-|=Ms0-$ zl9a!&)*?R*+a8v7LnGd!edqX4Kb>`_ZL4|3-%HksA|VjF zLa^5rF0C)$JrKoH=}p5K(}ph>a?|mRiR)-4UDe+NTr{`juNs}&#hS8>Gl{bO?{{QJ z1sV<<(~lUK+f{Bp&whA&&|E{iEv$>XH6`j`-Ps3Y;zK##bKG-+X{_e@*{`Dn3uP0i zOQ(b-2V?Qoj-F7_t4I-qacEnp<9eEzFN-WI9H5imi=5eu8UTT_ z+aO`_OhgI5mpK0oipQd1?2riv>Fnq!AbUgB3CgCM^7LFqouuSztDP`Loa}8;IrLp} zJPQ&Y&f$GvCk%e<9_j*EUHXGhgAQOgLTG)5$hWbJlO`Id6oDaR%G&yxqAP79Rc?l6+8K2)8<6=M+ELXcw0mVC)%;YDW1f^=@sNs;{}P5G;Zb8U$H0Kzj< zpM_>cuQMM?U#@}{ZcJ43NrgAG&Hp^3EF&2-Z|VDufogm9pW&7BYs0nef}_8B6KZxc z_k3EKjJ?T=t7Y|3KfM^0$sr%&byaEtEcUJ`Yj6&}k!r52SaTfT+XYuF5@S58B6WU^ zi0#~M*Zk^d-4(%D?o!7Ktc_tB*pkz?!iKV3%nzGXU&zGhP%-J?gL~!=2%*?egp_?R5>eWpZ znQ89HjC0C3UH0bp$c378&$oUGf=|+NFR_1;fh?QoqKK^GmnN_4J^?f{MGW{Q2#jmd zImJ4A2i)*Fazph+U+%DcrFM(Rg$?{)JgJW$-gz@o$Wf=Izd+Y zjF~IsWaZxq8RyNT-qNRIZ8}+mS7?xR=#RPWa_`LC%Z#z1DAjPLHbb80UF zBut_1+YVk(Hu6j9e!HptV^M8_o9PQDE*0wA*Lyy1lKFEgi_wx-1>AUVq%eHjF1o#wE_ZezivubT(p0n7)01Ts%No1 zZ%?`(7KA@mb{lPB>`N1?xl+?}W04m1MUG2-reFA(@F|jDi+j$t7yCDs+i#18ap28* zNu8+SYG)QtP2jh%adE>uhcCFB->E!5n(8aeB^S;AIv=@sY@#w}Vr%Zzr4v?5KT6qu zYF-zY-g1{$n|UC$%G~~W>4JPq8rSjD`j#uMHj}nX50sbA7i4Lc#Cy8uGEzK$-{T@t z2dBlN25o586=|w6NBc_sq;IOZOiS9n`F}Cp%eIl9CK_|k1QIQM-0gUkB6}S1BJxvF z=^|zyBk{E}HQT?_&P0ihX^e8tnxma_S|5N1jrxl&tU86dcNyEEx zB3XjU*H?KxHlBNzhCD)*W_E9@=D*2pL)W*YBM3?!oZC8OcRFpNCV9E=o&2MAdQr$B zBIJs{W_4zI0#BFeH6LQ0~&Srew(1A>*Gn7$g}6h*!rB2{In}#`JIRH*+yF zX>@G~IzQN-@46S*ZU)`cKc{@3{!LZY7Eg1@w^S|H8%=54i)Kl#BHCrO?a57HG{;ni zUngPU>8QLo4nq?mlo*#LYTU*&l>9ZJ5NQab9+RdbWv~sLthed)``-egOfuF zJ^_d`F!{BLm&x`C#(@CCO!iz?Z&MK8xU7GJoqO@JL?=X|!?`&!xhGc^qUr#@vMYRw zrrRV(_ZVUI==1w@Qi<2{-RYW|9P>JbdcXhvlN{pxQ}xDb<<*}?_vLnym59P$54Bv(={?n@iXT3z`TD|g zP_wF!itt@8Y=;(j*p!LoLRBY2ED1`J=H73^D6E{~Tu3L?b(I6eRo`{3vLy`2XPb0) zQ7bINlD$bgh+aR=pe#|uTiO945tuJ{U2M|{*ot74VV8Clk}?4u1dZQ(s^&IxaWPAs zqfY0gJ0=gC2QP#!m3;c)b4`>81Z%X%U9uX@2=hlrwJ5(;oFzYAUJT$ zi+BGmtu*&;wU^`RNGWL?v1OZ_iUj{8r8BKf*5Gu1et$CFhZ7_@7K0sS1z$X#b6 z5B44JyyT%l27~TS>R`a9dSE{d3Pz7I1*VciQ2pu7#HKk+0B#!Gne^X(a8GhYKjRW6 zazOSEpfo5b%-PMV=Y)s%nVSl;@L5k^bUKw z?NA^z-5ovJibjxQx|UbmQ*to^Aa53=?dK=9!FXft`bSxK^_pv=`M5J?wH8*7>ux{-Tzupch={%~&Mhl{qe zZUsq)y*XOgb4!chOy}T1ZAhPQN@WpA3&q)vWH2CQ zZG_E4Z(PejtNzvo#S-n;1}vHL0ti_d|5!NwBFr*#Xyvx07v8pYNwK`>ZUQtt*mz$)a>-pyx$Npt8Ha=72nhg^4h+&=ckLacEs~_s~y9B|v zu}mt_BA{6PYuy3vPQG2${__o#ThyWSkoqTH>p}07j5=T{fD-Nx_pxX_sQ-Wd$eN6Q z29Uz)Pt-Ir(+?1G5YZBj-^u+DalGaq)s;z3vw6oBz!BE8I5-YX9#lkxYJ|1ixjdp% zG{|lz_Y)|+q}HKg>g!CW8N=p1gGGHB|EEvvOeFK5sbCRu!2h09EaF*ylW(N^-&0?+ z{Li7kh46cfW17~BH*vWAG8UeN>hRjp8CX`K7qw;9NkVxBj`LS0MI-2ts++#4zMB%l zTB zV=!(D6_L2^ac5%9+`xJ+tQ0$vKYT*v7ur!grf(GgnShroXiG`+_)1M8E%aA7*|fBF z)$~USst=Jo?U_sCrS%s*mSw1fCVoU0c>|CQxgYxR{A?HpdKBdWP%4mt(yPIavAy5D ztV0KmHo53h*b#p+bvqzge>$k%6kb9?Bx|d$j@|~(KhyWypxs<6@A4PL=`=Q(dO2P7tO8fY6tuc%T@t4)RjyyNGqApnjNDO zD%OBP7$sEo2ENu71=wa~w5;?x-|#y-xc(T3OM}_h$Poy8#OA|q6L3`aFZ~a)p-z6t z_V-_%Mr_`qL1v!0vHY4R>n zP&)?SXDal%wIn{uCmXO20!{dP29>f3<}8YgPFg)Cy0^Gyt<`8LrJ*M~a1R%4XT~NQl-eZ$p29XBa0m*vUHf&Sxwofac0aR-GXC{TAkK7q?*@zkU-&u2VeoZ zsOHw@k$==W9Y&6i4Q#suLCZ$3QqJb_;TBo!j`c#0I}yWLd?44xd{fHoLz` zqBZ4PA(sFORvYQOxnS@mF*dVM@ZH0IY{L91K;x#S5j=Jp9JI~QN)su=hIv(?=}{Y; zls_tY4uWh&i`t$(bJcDnqyA)UAWUJF?suFs>H}$PFIX7QegdrIWVh#D!n%~^|Ku>h zl33J+rQGK)D=#)z4t|jbh>KWl=SOY7D?mz>$MJ*Xk=43SUn9}m_37!GAx906p~qn4 zVqOigoqaMm2NxpipQ{?W_~zxLs&>QSz2f?W2zy@=`!fjj27UUrS8$4ZOCR45O*tLn zT{h&DBtCt{>a{%VWLP+W=PuuqFzDgjH2ccEu}AH{D;R6aBZfSZw7gHLJ?gK=D1vP{ z^cH5pXlD9r0a^W1A&7gXdq>)9DE;ly)v+F%>;1zo8z9J?_uvd&9fW99{JRC*W2`@j zsa6GOum8KM3(l46Vr4v!gJoSV<_^J*y0JWU5bqa?4%-P=vp3I|oS%A*_yf0&dzwtw zhiKeC>*&BuCd?j(>pTG9SydGMK!UP^{TUu%@kaw8$#Q^DNfQ-K}P5lRtrC`1c+)fC>elh>vr3GT-O-N_E zQv)yqAdlmavRzXin20<^$GlPbLLT*HK08gjL?OS5LhE^Z(@{QUb3Cjjj&@}@3ayMvIkIc+uO4N}AVc$*r{mL)S_fjyMA9S@;DFH}>@I^& z-0M)DoGiPDW(3m>{2Jl8f{@uX-P9u&$AZN1pUJMDo&L?@JeoRvctkEVIBPK%<1Ant zM+JX)R~|QdtwHH&r!gi<1ek{8>ocfTn32G|N}z!4=1Y^w-1f=;zEy?#7D77l=tjMZ zk(Jie&vuY-MqAjTY&ip2;AC`&=Tp={YMFbL)+VkEwU50mTh5s*mQN__REI^>mMBW_ zqZCcTsss84;Yk?cn5N#Inr<5mb2(fGH78M9q#u>Q(qUJx<~H93b4=CGHu(fz+Z+*Z za$wV-hryE>7FJe@;*q*b8H=Wg$DPeTkkBpcdUg?Str*kk<~7QnV1|QX)JjBk9DkRf!NFYjLeS`Z|6RPngllAVK~gP*)RPM`#c3r zV*&hX+6+v}!{R{oLuMQ$DbDI3gTUMdFz$_4ZO2su=xlULyVShCF(4x$69bT%Y~&o0 zlXcSMFbO4~+1j&j|4kB=a}B;ANFZ6(69~V~$g)x#_i@Vwfuq2`_QO{@={waDwZzTz zt(M&9a(ydYt!j|oCuqUip?0jAI?*$z8OogvIlhYoRe+=)OuCYB;9X(TcwL*7;L$xO z#rQ>1$9Sgc@eP2wWZGa{9K=IJrN;8lN=;OQQ4`hA?o=q+==%NnLF1FsRXn~)>E~&f z)e=_<te2({)32CO+=$osr2%UXP(ec0 z7Z;Raa0I4a6hN0PLz`RGF>V2+-T&(IUOCpxU#fXA)jkQ0JHKXut*!rI#H){D$+(6e z`p%`iF}x-uDzurX?@FD&yGbLu2U_jeh!yf=&xr%12b&4QJ}`_bv4OoeGvazoikCxi6^F-N( zGzs{dE*Evpx)?2?g8t`*Q|3;+aQ$fZ5vwrr?Ttlc6a8IkfMi@=UKKeec?>a2nq_P z!;Fw$)8uaNzWvaR^i1q&Jjo495fxJHugrzfNS(l5_!|brq0V(?X@Bbds zW1Lbq;M1>7nFRdU_Rj#9F#ofiOM9dGp))s?q%~0FTy(!O8!~$Ds!iIgE9o7W0lPTx zoMqroZJc#ulu*p{_Ox89rx0#n&eag_lO7C^2c!Pjv%sleI;ebw0b#@pl-4Ki_bIJg zGW`*Jv@subbZUvYx!V(C9ME~8TER!z0d2|#C)4~t9t`i$l@TEp^74o=(5HO{VYe^_ z!#~Tv5muM9&=VpMV^{R4!3*S%0_4EW@c{5!v|=hN->P*BQHfmhASYv%2CI5Vgc#Sf zxSFrs2wCtb+K2zm=W!D3t0fkRYUCdckl-dW>ZZ0fM+GR18WRQ|k6g6)1QPhvAdsF1e0lIcuG|zI%qb}%?5UvcJR@`v96A3eC{JCX+s{Xvjm-e zy<@Z!9!EC*C8Qtqx#*THKT~2www3ps@{Qq7W(S-JzU0XL{q5^xyT1NbRMy`8$KT8D zh<^4(;-_ix-A_E!Qq{`651wq=Eaq0PBpi2{-Z0c}s513!y?w5kM|A{0b?crB>U%C+ z5&VJwpu-hNwYj?3#eQT%DV2cT}%jR;&9AIk|mI@bIK|o2(J7kjO(~l8 z=^d3XR#DaA7m;D^O9HR%)<2ficSsqa(`8HBqvuCzH%K_CN+&+jz<$MKybW@-q%e)R zfjC+5dzmVmv|`QfcU&&;nYr#GWvia!v5F~sRqc0L z#=<#pA|HQU<$QzlVM9$Rj4iCmlcGo+~i&4Y?iw1@ubQF*2`fVh{koycM@Ek0Buc>@u>U!{6 zVJSwNhJ`|72kWxsH%Yb)XhyA-=5yE@I7yf2F6Aikqf1crcW|eg;1@r z2AmWU)W9h0J!&B=HsDezlD9L)>6WmL2U&CGF7{RL(pLr)Kg#M7+@Zj3ptxbEkSU2C z;-I`)x6L9$RMksDT3!P5kL9U3{qUo$rJuEFT=Q8+{FelGKP_C>lWkbEN>VQ_Y*H?6 zV4d4NDPkgF*XoiYg`hH{obiPV?(bq#RMLWSqy2Qu7JAtaGt5b0UlGN?);1sKz{#yP zEQ{61IjI=LPeeM<&3uh6Vz^mCGN(BEpok)M(PHtu>E7vVE0!8gqf-*K+4WUN9liQ| z10h5WjxmV+#Aw9JfK)zraWn05bRb97Mq}NWBvZ1=AaZ6*mizkI9ov=T7O?n+{eyC> z)#?&=VoI4A9W%QE{QSzEXIqoCGm>fA!O#bjQa4j29_qB)>TM6J-zPzD{+%6n)X1{d z!QD`V31Q=I8P^6~qQdmYjTa9kaTsK_C8xRxC&*;k>D)Oao=J@XMQ*pK4nClJ-VtpU6#RTQR(_$oKqaX<@kvp4Z7+ zt8_ewNRSYGT9}*^`Y5s)uT$G7^x~pf@|%tEENgOtnw&LRCSZ?a63a=Bq9rCI#v-L! z1FcCcEDrxBDZt8zYn?-@8|yM?xF9N&*c+~A+;s|p0YP=Q$yYJ5R8tRg&&k41-V{n6 zU)e}C_x!216^?+h#OlaWBib6*n|!589s#0oz+4pK;M~>>f zbKS%9wEQkgc|eg?fV0Lry`RP6MhCFZv2U)2T#1d0V>dy59BKF+sL!1*6ZX11my}{y z+6SZ%@s+~DeM8UN(-}5{^~uWQSBL@sdUw|P*OOve!+w!PKq~P+9AlB+bqX@(ZqJyH zqD>(nIEoQw#4u7x?Nd816tFG_iFX%}W%lFt!LfvSp;bbn#IZW*wpZ^Yw{2{MmQ2KP zBEqGUd}8W0NLaJ>yGlrA#FRB+AZc9GfuTO9mFKvctZc3uTcJW@ zCFGyPF2%jg2(83<8fXYNGFB??p8Q10RzgK7X;w#V#G2f8-Dk#0i;wS|dkR%Ji9&C<#0E`$m)T0_Lg8A^G12?< zu|rxWKZ%PM!#e0m*SfjVQjkK32POUz`gG0YP0UOMgz#)26jF%N*iJS7GCM6h4S61M z^r^v51-Bo^71-zMzHm*{e79d0c?&!;IW_F_<409HNNW1e;sbn2>>mpUjS#IGd?UW4C(}=W?iDlrcHdmd{&E?5_-Yu(D}`#=1*9 zZO4D|x2yd_l53qSrGj+u8x>pYW?0&5t0u8`xz8eYXPb;vKwgp4jH$jwEE4LpsLMxw zL{jQyedK_Ol9(I^|Kr|}i`X0VgE^TKtwqQrVUQCp=->#Sm)%;%gD@f(V^ z=W3sR-Y?F5zf&?o3#N~8&K8gduWOgj-FLpm!0cHiGqonXGfcdML>TWxtyeaXRV|~v zc2OcEC0=CYWsy#{7P*L`A;otdV|qN$8M$n!Lg)Qi{W|~0$Y3LK(tKWmHp?SJ)a?-` zoYTIOe6z5Sfm$zuH??{4#e=i>*CxSm%yJ^QP#xAI6CLNgStr)4-WX|nTfNa!N0$be zE84Dmbf`%Nvih0|^x~|^mK&+KNYwV|&x4QcRLP_nr9XYInQhL?ZV=Xao!Rg{cOp@# zda3Ki)@>Lo8U!~@{{Mcg;KjsLlzm)9Ma7pZ!Pc0it-=!fdi-L#3Rv|peU+C3bVQ+m zN)h5{BC6G9A%S3_V{=lQ{Ii0Ekw1jxX20(nirbu8R$3|?Ml3lBMUyocBzrU_2g$*V z(yzWfKXp(~`Kqxc(dXjyK2$@%91a!}#?F?RTo-R>Nih=KD5NGU=_s2=hDSs!PAAuF zEF7;iN`03sWy+j|qGdaZUl17!QOf>}&~gTYb{|ff!*0pG=aZYxU5yo4C4_S0-OnJB zAq$|=&BpP$wblzWTjM=&gVQc8Uw`3A&uOa4cK7dbs}c&HxIhlEt+ZXBZ7rIK&^}+L zN9*h9**-b{_Qz&JB$>Aksu-#(CoCs3xsh^(q0)Ny=yBm_ zC?T}}@p;Z0)$X0k7rf~2>vMVcV)nRBOa0opPmGAWe=1-!*|z#M_Vj~R#`H4$nRwoP zLVgI5czHKsdPcmkbnSzt*$!I?dXBUxt+hxmZVj^F$tL#L!>KJ)4U}i4rK=ni=m+;Q z&!}Mrx9N6y1gkCgCKHB1XeqwH-zmHx-aUL9Gv|b(k$PbIPw;5eL0fetLC0IxwRCRB7F! zZ&^R47UIcJIj#dWNWdcv#3?Un%e9KryN2v&enfdv%3RMU2DXrc7Dk1lu@W|eGTJT@ zeqmu@@6x}z=|`P*D(7u}+bf|tB}9Ho?Be11<_!{EgLyo?-gsfiB3|bo6p3)Cd7RSn z1w4>N>E>FB(@5ZX(uuogg;97BopJYm1)2RcE57=6d8Q7Dbi-bL`AB4|>>XM|yDf@t zrTJ~PtjewW-HRn>7Y+n3SGFNs4Oj2*prD|YXNjl^(Rt#d@L~;6?=oMv?f_xGJZ%`f z5PgaM?JIOVDs5PgI+>Li!=tIfM? zr_#DE$Y;&>19urXnSydM7EBR?htpo+S8`jLQuiHT>N%GWhJf-FxM1X;oF#{M4l)=P)U1 z=XYh#A8jyd&z6=~Pd_o4jwot!A&sGulx4z}T&HeiY0JEpTi&D8AIYojJ0kdk-QY`| zrjqn6bDw+038U-Vmpkr&BuL~zb=_A3e$CKLI3*oijZT10Q@z&%G$-(iv8KND_9MJR zrcQv4;Ip1rgJ~0Xe#5hRE8#WDhE%PqJs=q;&HV{e;UX`GTWiQY6u}o#w zvT?Z+DWVRQwyd#{s2F2dt`!rCXPss88HoC|@k>@J*m=rZT-~#3gv5jTK6}^Uj+zhl zRs=b^R)@W;%w_%b8Df~9uw>wEPf7(^h4{+5Xt^k@D%T>UyD>8-pi&fXb7Y6g>|H+l zP3OSckfjAf2d-|}e8t~UkoZyOyz^Jd#qX8qC?3_Jml;1@YmZ$fO^&fr4RBlSR;1m1 z(CP{{0lKc|T!q)NZ9XbBK@81ETzbXHH1O@Kj<7i;_~_ody5+(Wzf^WL3RUEbbw*F) z4QoDD77{{?y@V@?-*$De9Xwn28DJ}={?KnedZ$0qeedG@VTqoFpd9Dj&0)-gtKHm6 z7TQw>V+yRd+rRm0`G?PrWFi|_A9e|xjx$~NrrRPWBt|ACX~{)hm-}q`TOVKFD$>e} z>R)@u(pc51|&(LiV;C;wGP}wl|Yus@2d(6dfRDOZi3FmquR!5 zJS7(gmUT(D^!f%P|7b(@UjfZ(AsM2PjmwDf=&(Ejr6tInE*sr5w-fA|lX1>6z*-G5{DI>oQ@dTtSgwv8IN z>r`nRRpr`ZL6L7B8nW9*=l8$N4mZnX^@z9FpE&-Y#d-RNO&=u1DBm7GztvXL2*d^O`A9IUHwW0ee7qF{YfqFzUPH$-v{6R;4^$)-a$hOvX0t|Vi3hT1a#!A z5(?}2()Xn5J?C4((P6Q&(?MGOAG^~FE1Se{Bf4Or!`{4VA!ab<8BIf`S*5k`N1thz zJJH=E%zaU;x{KPMiI|&4UZ~w&rcj5*rBe0XZohDnCm5_>m>TR$(*F^hc3=kvd7SP< z^Jk*#dPMr6Wa91_2Cjch6yA4&U;pGtb^nCqcoz3;Vz>4*`=pF8?NnU#hDZt8;u3$t zx4$}KcD(n}N(dIDNAmoJlJv;5wek`;@!9CINOj8ymxDrrJEl5l5=~eDbz=I4%Z`Yp z1s>ww%Y+JZRn+j;?lJK0<9GhJQk)4CWoj(%(R(tW+d!QY1FbO{6qJ>vTatCrSgu3k zijr#i6O|+9qAO;RjfbD@uwL0cLC?npQQJo7&M7#)I>y++8{~lP5LJEH@I>gj+&)~O?d>7X=JtBgghAS~wgy*~UoK~(F-c(^*w?w zdX!?HCvCLZUf?5lNldy{&!FVhz*O{nQKC1=TcU06VWk&arC6i?MSb?nn^lIs{W;!Q zG1qC_#t>-nOcR~Q*%fNk9s7}k0#W_1VGev&vQh($hP}mp))=p{NLn1%lc!x_Bt$fd zhyr@`8VlYy9iJjI<<(47AbJLi=Xz1s`}+D8zS{}vqhXWSoH{|=jExF717OtY)u|1& zdnifF4WQ=xaF^HJb4ErSA>RCa1`j!JrP@)CFvu~Bcy!MBEa#*ipryw>SI9uMXb_4G z=y3D$sbeS%^j8h2Dn(xp37;O3B)i^^_;1;^M-Yx`Y21;;*>v&QV91X0R~@xkUOh*u zyZ3Ilq*VA7R(6XLziW|SzK0dk_D6r^3bN2X%Wh23WR1Muf+*+hda@%y-mfh`ARyqu?=C4CNg8sO6Wp_vac61F=5=~Fk@XE1;$;E0p;O(W zZ+*P`6W`>O-&zbizT@eBh1i=RR}$P=pXgg61yh>b5!xner!=Uj1<0|G1&4Qz%#9fo zu9WrDy^K4THA{D_A^Q8VE@;N*Bo-w=eEf+r#8&TTyVj0k-V3I|{K0v4{mKDA^Zb); z+L889AV=mAro?DxQE;_#U^bw2q;!!Lk8Tbq7lr zGPK}r`ew(Fokm=5af?pCqv6-Xxly$Diyt)j;HtwLq7xk`(j#*UFY=|EVPZ#4ZMG@Z}p+A?P7R zyjT|QFLmz?f6Oa>9ll!P{oib6G3|XQj7yncYtb>nP~k@}F9T%o=V)_G}sRbe{p;w*b zJTz14QWg5}{nAeCsaZ&ucaSIz_9)jbITUjp5_Mdx685+UcS>A|?c2szlDBS&w_*+E zQt@v9P3Cx!n@LM2EXl)PMn|GZ9XDe{fo_W%c@vNK6SrLP)#292ZQU_FE z43vpK3nNZ~eLIPn9t&UGaZy>RN#>Pa&t63*!l$JLp>N0PcX{NZg(Hw%%*r5sTZSzu zP4Q!YqUur^{JyBZ5~b<$Z%zGoTO18~yeS}x^vr~8bP*td_>x2@Prp;&V6d9LlE9zYJl*nZp*F+0TQ^rg+@rZn_q)Tc6ljqs~=P>xGB*CNA%QnDJ`#idTJ}StS zAbg>EwX;fqnw&ygI*Fv|0-jbUnVQIsiy$8eH%cps6_WzGFD$p-%-eQIh{xe@(0&rg z$yssbo$mvKc{B2O*aIGDSB_hacGDSjxo9Ln=Zr_A2Q#bBv~;b_w2ZX%v{@GLO z%uc8z1xIVW+a061YykH!6A~3SV5+}#Zb5~yK9zuUulW$8%%X(S zd=IZGqzlfVcdT8Qn^fEP?i98MMnp6XO_4{~Cj*t1PXlY65$1xS-lNC1kLRP zB^Pdkz3uDPKP(+bbzRaoH3^$J8mU;k^a;wcn@ev1({WH{+E#|mj=vO`uM|261zey4Wd44@4tI9a^QT0x%3Z16SxfT+1xW%&q%I<^=VXHFzYbs?3raEy(c4zIjrxWs938GWH9+*31LNb( z6F)Bu_bv*2jV9-rL06r8&ILHKeAljf6Lzs#@bOBm^RTz{_~0jrnR-HH>zlAS%7Fp` zG#{xP8NtQ!U8@5jv?bCSf!Uyq@+-{moKq)bqCr<__g91a_PUr_}izn!) z_HfEE0Z{wVk(I!G4LBIl z1ujM(xy`-tS3bYJ@M+6tu#=Yc4!P-tS8?ul(t~jML9$V*SUoR3JA#zfDEMN5aX8O$ zWKn&{=Fj{)=Jf`65Nd@i_NiiJA5sxBboT z83gqZrx(!X;a;yM(ZQo3_b=Zc6srb6Nl4VJ2!B^c@cjeFLKmGVTuK<^>U`ipFY%x$ zV0t6wW5!)I<~U8*U%`_DJnk?z<=@USWF0{*EiXEp1NdQpz2w!Wv~swpp@|}-Zi`cK zfk053lMk+j7W$;g@ztt$SoQSD^SD=nesEKL*y}>S=*A5Eu33|>=4UTm1V>@P>pSLyBynG|~wg}D94ESqsTko9c z%HRGCc<1&oYW?n>O3zHwZG5hK#Oug=M-UhZxPPaw2Bdo({TJT${ZcqS+ULMl{4QEB7PETHfmK zc;nKj85X{`u;+Ffn}0eA0@wTo-sIW?+y@hjLo4}K@I3}`$OqgzCJzM(q&+C`En`cs zC#NF9fgZmeV(XcQZAK!I5(-peMEtJU%LBZ+*i_j(}jd!Y9)-5$Rd>)BEQ4Y^APg&X^y|`H~#`S2c>reJ-&<81un%{r+ju2NL z&0`x^`(Nt(VaIU+>~934sQ5aFYbQt9MrAdP?N;*0d1Wjd=Z&%$+Gj#eAQ`D^Lycbz32Ne<}pd5`ZbSbx@bUq zAq-O7T#n+0d*w&g)Ep+b89+S&{3AS3E%Sht%`Q%V^=Fu$_E~hUBG6mZ^}%5QCx+fS zEjn>cSCLlOQRrtaFkb$!n<(0Thz1^UM(&JxDQntuw=BzkaIeU2o}oJ|`l$@@+ciCQ zvCjs1RXbLqj$&i~nZJDxGM^~N?bh!98F3Az86+J-1lnA?E2}1C>LLM~f|IQL2-S=Z zf%BqvkBhf!BIE-NI?@Xq0#2;;cb=~A0dRQR_gZ1-BT3-=XdnHSzFY|mf!a7Q3$on5 zeKPpn!@<(EyF)HQ*upNghk6C{{#mC!=OjNr-nVm-7N>C8Z!O&fQ4oI-q#H4Lp@XsW zXG58QG6QL0srL>20_%uU5?F}X;RgOKnEZzI$$&lwYxQw z_l_w)6cNURbo_+nDT_R?$G{M_W*Wjnif*SQ$V5zzf?2d%mDW_@m2&)Ms^514Xw=`2}QO<5^GHJYo<$RbyjDXqccY?K2hE5+NO7PlB z3o8;^7yEOKje0sY_+xw_-0TM)Vb+?^6(Ql|_0DmpcIhBiGmwvBHcMq2&)|?bFeUk% zJocXS5-Qt_%WmY597GOF({>(}e5LD-{FHhj(4vxPY{bp{D4Hs3_$5-MaxaN?< zA&(>}EVH0#jW7ume$b-9Lt;-289iki_HwG6 zGi1GSGnP6ehVnA6?Pr?;C%sb{=qcU7MiFYLgE7IEdH%#tv4iyp13<_{aNC8&QP%pl z5*=tOw1`{d>cGA^AQ5Q0RI04()rd^bmK>cTXT?tctwE8(nJ6UPyN_=$8UcNF3t-A{ zlW?@vP?zX{3-{0NKecLJf?&K+Z&kF4s35OE<#=~2J@YE$3-X+!gSzuu0# zx1FvGL_3yuS%R_oce5ZW2NWj2jQ|@@C?hD740-&a*qiLQvi(LQ-rZTsMMp!kmR}@l z$r*_C;BJKoN8|j#x)9I2=&OW^=KJbj?yRh~?;DUY8vfCh&K2yhlknw{Y?VCWBT#`P z2@-D@*=Gp(g(AUEz7LXiG~84K91K~^nhfA~`88r|CKaG<$_n17Cr5YZphXD&7|7B9 zFpw2gXS8sNs9b9m72|r_mK2ns^+Yn5F?c0jyp}dN(sj4pl4``QZCfwi6F^awu66xM zS&YK6N&?OewVO$2(6X8-qd%5SVErp6cUfzSM|vn}nX>j&Hxz`!6Epi`sK?V0X2c|8d2&LOpGEM?pA% z^(LajubFkbvMO0>sESmsaqX~7;h&p&g4|V4-;oSvRJGn#&&?dr-UEQhiLd;0$4qMGnOLfB&(I{iTA=v_NS@CxgJk zei|hmTL9SG%8iu|Pt;`VK z!Lb1sYT<#N5@ATw!~7;4Qu!1#B!?M9j>7n_5QnAkmCkBjJ9vJ2v}tZ{!$UEpe?+V5 zzqjZwhqUYlym9SEOomr2uPAAztpIE@L@vg~jo7NfaYp6?hj$U-4@n7u_x+BKdPRW1 z7nG}2u7&(TU8|@Ek+mh$0~T5u@7Tar=pmjr0kw=kJ~ev~a_m=;XXl3L|Q35?Y;cJ2$_ zc~(NOuqw^IJ4h8|OS~|qK_S%YXv$r(!!=GMd1IwD2bh!2F5~9{iSmpXim?(9uXh%@ zDlZO;zh8V&EI#I)pLOPbG4apnHakQ3w)F@Wu$wxySCi0jR%5ut^Unf7z~S~-2O|^- zip%}&&X;bYZuHG1@B7*L{_^f>Y5$w_fY^GFb*;m}1^$OgoT<+hkH*|YfkpraoDMMi z7}iF70V|?EjTUKN))9Kaz01)`0zH@)+Ov8%8}~}eAWNB$t2F2;m{#-!M!Ne6g*9Fij;t%OP*JRM>2ad!TD zDX`y`=kmUESkiQXGIQa3=Xs3Xj4NXh3bj(NkXp%FI=5hltBt2s26Ql^15&f&;LvsC z5tQm2a-aWsxy|XYIc9%`+?*JT?siJLG{M}q@=7%DPWSVo`trPNY#igJsz0_r!8o5&2lr1?}p_6 zceQKv7cTD>rd{ye_<2pzc}o??aBOC1v6h*suN4=8y=W=4+fp(H0h zx+Yz_x|csSB}C*;EwVjJ#!KRfx%Xn>Xa6O%E-Z-{ zFRSa+&ingVb7a!ey8X@Z`CX6|L!lSAv{UDvGI>P>d12b%cDVTvAQf`J*$t%GQnuP} z_0A%Mg#MlmyYFT)dR{M+<%UP))vQx;H_Cr%+1elf5g*uNko@@ znp@C;w&gu(Qd1RfoS=B8UZpkRo)t!GLMd(AttD-<9SitW46Mib1{2-f<~sj{8`Ebu z6XY^53t_p0!l$0^VwCbFiiVP4GBN4Zf8{iLqN0dA0qji6XurL|gx8>>X8W=^wunr_S;q=}65Ea;2Z2tNT(Ml6&qaavaiKqC*N;xpmOtfL> zqTP5m8#FoFRAB~K*6*=;A4a4ysHp`Cmf?Ryev`k%IwL!6@an9=NoGD5eT~InZPf%g zqtovLAD=t$a6R5uc$;|C5NF*iIc*iWkIH63>?UK;OxuQuD$PlxpE#*G`Z6aC~tf@2To8x+q-Iy(3L8ZuD?RI_JVCPzgM7 zclW!UJGB=occ{8iGQymXJ8$$hLCO@TFQ=P0Fc>}Jwe3qQ(Z;WnEgyi=Cyj1h<=2!J*j(kz(eAMKPU^* z+_8^C>4L)&i+vg^p-!&~1`%2Qoxe)A$Xu|MaPrX(U{RpAPMDzv>{<($1Ky<|e#cQ1 z?ErmA^M6t4GDHEt^T(l}!_8`tYysUw_7+`<_eQf|V98-A;h=HOh1=-{lrfx>M*g5- zOx?m4;p&n&q_CFOz3fPxk%;j9syOs^3v2LWMFyOBU(x58_*tItNx3sMIFn0 zs?6rf>Pgui_s;UQ)v5V(PsH4t=#{OuN3f>s#!!24i2;L-zRImHsU0BvYi8M=7Fle3 zQCyURIE-x;Z>#tJ8avxq&917Be}HK61m!hG?T7B7)i#xL_xs)lkQO>X8qvaFw5Dms z>79qpzw2P26LTo!0d7>M;|=rK`{2l zhd#wWBF@*Y{r*$k<>bxF_%AD#8`vE@{l@Em>ZOqn%}*XV^58j-9A5pC_MPQrr_Z@! zVpWj_W|L^0h#=pYK_kQRz z@}A+Ld%tzlMZfcsJ9q9s{NXd-$Y1|yUPthf{rp=F{@R}(`Q$Ud`1-#ok6(P|2QS~g zi_e(m4*J{cZpDm~A6Svswpa2m!uLIO&c1hl`mdk7Ib1(b+-KQS7ZQoZvvhuLGYwPCu2PK!?b^LpGtbF!^XHKVZ?Xed-}-~Z~z-@Eam6HkBag~vY6%lz|CJoTTS+6~VSe&*D}Upsbq-$~Y!ANTi>6sJx z$4-w8)b8-je|q@Ilee4!r2hWmQ~&jWZ+-TM@A}X@<>%$}(PzK#?2D&I|9Co?ZXDR& z*_!TOm!CGTe&=KojWzOvF)q43x$zfi{KacuYgVqf{OWg3S9i?tvonET?lrG_9se6| z$O|{TWx8?A&OuY=7f$AnUD-4JgF~y+y>EVlx%BN5^|#gS$$KV!?7J9KcGcL<4u8z6 z#-Du1o$q6C-lWd;)#YqrS88@4nAP>k$`#J+*_^x$MQ-=*@#dyx{9Wln%E1Sdl@i;z zT(igSF6CG(r|N35eYIwLbz|b4?@CIwB-4j3l#+P%`o!D5ONg^KrF1s9QloJ-b}nRR zt4V$5Vyt~JF2$}7$yH}k>T=1U_hxnCLf;izQlCu)BGGN5k32i4uKH?YZ@fe(`BY7OA{vb+NHKD zeMA#x_4qLrifl$|NLv)N`M=13E4?!`BKjg@GSMVySYack#uCsN?!DvEK9sy%xH`d=wYMY^*Ve8^Kp~h?$!^JC0^~cJTuL*qg-FVB zDbjgm5)FxRAskW52}eL*@nVqHfWn(Ik~;07 z<#Lf0%DS(FLsGAXzyX$(`|9N2qdz{ z%?Mo}O0A@H1!zWLO=M5?y#)(NqMnHMBxT?-vXe6M-jmu*whBmfxt=AZU^91sMonRp zwI_4;cx3?^i?5_$)0iWAI0Y3rKD)_QC!>)~I3A_xYohVU;%t()vppFO0J*N>GE#Y6 zi=|K~NGnyJGigh*(ZH5UGf(lBT3o!i0Bt2kDODblsO4HD@e62OsAK?-x)Yx^pfxNtMHKSH&#@dXBMa z!WPH1Ns|_;K9;~U>5agQB}hXPjb54$7%DO^Ssi00Sm0%*(ZoJyii==jaL9twg1?Ed zS^A>Q8R)D92&OGc3mt`EHGoGlgU3oN?)N@t0G^U1zR7c&OC2Z;wn=yM0&!x115+a` zg!0CF6eWl+5%d6?2e5eZcr{|;T>_yEGtIrXg6beNVIfr72%L~&D$+-^#ggH^fKUr3 zaX1g#5*gDR0nOm}bdI{dy$MMG3N{2#aiYbP7L1T64q9WNBN)zZfpc7}EFkGE z{38pS@cgGA0ow|$k_Lh9E7xYcPSwv!x)xgrO`K!}|qRm;p;3Qm{})B=9MwUDvUL(3M0+6V1g<*&I`>$PVG3o>P{ zq98{QYhw=$0=8oXiSR)?7c`3yA)fhweoullp*V5$R}(AGDy_@%00qm$YmzrD?9PJ3 z*vPZjC&5aKfNe@0j`B4v=S-e*@f4TRvRvvw66y)o&c)bLIlZJGj61@qa0@a@hs>>V8d7M1cq zgrzIYSjqs%y&-_sk{pBr!ZsH!6hsrlv$N1FDynIf9u3t5MU(~9cLgG3E}9IqiURH9 zfH-hV+L=0)V9P)YI79P*lA@w{OUadAK^*ahSk~eT>|s|T254yFh44O~{NN;1GM8vG zOTeETZez9~PZ+zR3{BNU0Wqct5U9_vaDdX92sv?%R2_ts$nty;il#1Twh*28o6i^Q<4MfHd3(pBctsHG)s7r@x&|Yq*Q7{mrT-M6Yzw-1khHO$@qsfvJq!Je|lfL7<=s7rd&( zFw?|HmcS`8nn?>6!2JPoT00hCIGmI5fWt5lFyzU}NIjY9VWME34IGBB5MnR^W@k8| z48Y7;a#-p%!apOk9HJr3#WfbkiUJ-RDuHImK>_tC*VZnQHOw#&dkA$;Ycseg2l(BP z7!)-Lz`!HyqxM|JwXiQ5Xe2hwzHp=-dZvWne$BL)nS$|J+Ah;A-I!}B#Rz|}rZN{< z3%?mGz#Anv!<0#o0O($bXdKd#!J*7l-;&}&&5)B-KWRg}#vi7tuVWfehrrOghWW_Nb%_YL{nm7;^W|uTuoD^0DB(xTC4<$HE)k;jd2Kt+nJ_C3P;3jJv z&^>WT!Y&&L7$T?&YZ1ByhiWIIbj5p&?UIiYD=Liv3O)eeW%wOR|2g1;f@PQ_T@5b^ zU56nU{Lf;9HUe0I`!i|?7a7HCTCA&BT@3+%_pt~g@0q7$$s;U?^JUA($I<9N;Daz5 z1`MiA&;deBGVxcgO)OF<9j?VeS&ladA*&2WRLE%WP}| zJCPb9bCQr4?1Kd*!$Y9T%5{tSn@hEs z2PX+gWYI}i0V9S95B=*CS_X9lGLVFnSEoH;Yz6af)+ExNT*}} z*R;+_1VK<1Q0Rm=5n)DknNa9;fEO0C?8J4f8i6skl|Z=+ITf5mfix*Ho$72t6i5q& zYNuf)-o4gJlLJ783fa{P^XTxCHE1Qs-F3D0ackaH#3@6Nd3Fj*)cjB^Ed3)`143f9i&66M%Yql&@sa4S2=XACFI!uFbw{SO1GkhR$cZa%JO+2+dH21-3H0kSM} znZcP27A&CFd1v8Bs=?Y}T!+d_!~jndU3tAN9?aOFw8>};# zW(yQghBXWv#&zX`CPYE=R}(AGDy_@%00qm$L*BGpkX5IJ$pWS8Y~6S*X5ox7Fpcsx zEgz>Nq)p3mDaksH>tf4pv)dil8ThF2NFBAZpbbKTpd-Vp5c(VpLwrd_!)E9+V*zD0 zp>Pd($1oO9g8Y`=k^2fpsXv+d7&D+Fs?wo|@R}^c$2hKoZ@WTW5K64DrfF?3sw~hj zs*G(gs!pW5LXCI-|sm z&M2*o&Y*DDf))if^v&8vJk+@`a875G51J4K&0kHdJgc-W%L92wup~VMU<#Y8p0-87 zwT`tuVOcBVI^(jc!%RW&wx89iqBBaU^G1mqol#m_9V!cS9g5QXOy@>m(of!=Uh=)&;r33nzbW#%DLnvG$$A_R{3C*aW`4Wke4+YE5|mC zXJx^h&Zy8dnp|5E9~*s63qpPY3TrWcqt%6Z`+-5K-fEg>@6V;+f!tSlA_#FCfOg|&n( z3apm3lMi9BSSp(--Xk*37zsH$GZ+U$Gh+gSC#6X(vD4tKJsM=S#m1DkExgs5?;KR7 zkOmBh@`6kWhxB3hI#aMDt0diKZ~K>gJtdzv&Ay&uZ5zi=q4<1hE|%}5Uca%ixwCDq zyo!&k__Olc?|=8yth{yqHPgA^H>U>A0dBOL%`dIA59Zc=~~3b39x#Oh`q>JSCM^ZiI~}*BdS#V;*C8U z=0#Crr^QEd@&~(iw)P)dn{G|Sy_fC5xfg72u1_yC7aTfp;mpM=dp9@nuefiB*UH-u dT{{(D;N0z<{aZVOcZN@5!sPt(-+1|({tuJ~KT-ey diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift index 25dcc168e..f44e0ca7a 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift @@ -209,8 +209,6 @@ public enum Asset { public static let elephantThreeOnGrassWithTreeThree = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass.with.tree.three") public static let elephantThreeOnGrassWithTreeTwo = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass.with.tree.two") } - public static let mastodonLogoBlack = ImageAsset(name: "Scene/Welcome/mastodon.logo.black") - public static let mastodonLogoBlackLarge = ImageAsset(name: "Scene/Welcome/mastodon.logo.black.large") public static let mastodonLogo = ImageAsset(name: "Scene/Welcome/mastodon.logo") public static let mastodonLogoLarge = ImageAsset(name: "Scene/Welcome/mastodon.logo.large") public static let signInButtonBackground = ColorAsset(name: "Scene/Welcome/sign.in.button.background") diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 9535b932f..ee56f9e31 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -164,10 +164,10 @@ public enum L10n { public static func shareUser(_ p1: Any) -> String { 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", fallback: "Sign In") - /// Sign Up - public static let signUp = L10n.tr("Localizable", "Common.Controls.Actions.SignUp", fallback: "Sign Up") + /// Log in + public static let signIn = L10n.tr("Localizable", "Common.Controls.Actions.SignIn", fallback: "Log in") + /// Create account + public static let signUp = L10n.tr("Localizable", "Common.Controls.Actions.SignUp", fallback: "Create account") /// Skip public static let skip = L10n.tr("Localizable", "Common.Controls.Actions.Skip", fallback: "Skip") /// Take Photo @@ -666,13 +666,13 @@ public enum L10n { } } public enum Login { - /// Log you in with the server where you created your account - public static let subtitle = L10n.tr("Localizable", "Scene.Login.Subtitle", fallback: "Scene.Login.Subtitle") - /// Welcome Back! - public static let title = L10n.tr("Localizable", "Scene.Login.Title", fallback: "Welcome") + /// Log you in on the server you created your account on. + public static let subtitle = L10n.tr("Localizable", "Scene.Login.Subtitle", fallback: "Log you in on the server you created your account on.") + /// Welcome back + public static let title = L10n.tr("Localizable", "Scene.Login.Title", fallback: "Welcome back") public enum ServerSearchField { - /// Search for your server - public static let placeholder = L10n.tr("Localizable", "Scene.Login.ServerSearchField.Placeholder", fallback: "Scene.Login.ServerSearchField.Placeholder") + /// Enter URL or search for your server + public static let placeholder = L10n.tr("Localizable", "Scene.Login.ServerSearchField.Placeholder", fallback: "Enter URL or search for your server") } } public enum Notification { @@ -1110,10 +1110,8 @@ public enum L10n { } } 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", 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", 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.") + /// Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers. + public static let subtitle = L10n.tr("Localizable", "Scene.ServerPicker.Subtitle", fallback: "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.") /// Mastodon is made of users in different servers. public static let title = L10n.tr("Localizable", "Scene.ServerPicker.Title", fallback: "Mastodon is made of users in different servers.") public enum Button { @@ -1161,10 +1159,8 @@ public enum L10n { 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", fallback: "Search servers") - /// Search servers or enter URL - public static let searchServersOrEnterUrl = L10n.tr("Localizable", "Scene.ServerPicker.Input.SearchServersOrEnterUrl", fallback: "Search servers or enter URL") + /// Search communities or enter URL + public static let searchServersOrEnterUrl = L10n.tr("Localizable", "Scene.ServerPicker.Input.SearchServersOrEnterUrl", fallback: "Search communities or enter URL") } public enum Label { /// CATEGORY diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 73bc292cf..2a3f1efbf 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -55,8 +55,8 @@ Please check your internet connection."; "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.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "Skip"; "Common.Controls.Actions.TakePhoto" = "Take Photo"; "Common.Controls.Actions.TryAgain" = "Try Again"; @@ -240,6 +240,9 @@ uploaded to Mastodon."; "Scene.HomeTimeline.NavigationBarState.Published" = "Published!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post..."; "Scene.HomeTimeline.Title" = "Home"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "Accept"; "Scene.Notification.FollowRequest.Accepted" = "Accepted"; "Scene.Notification.FollowRequest.Reject" = "reject"; @@ -401,13 +404,11 @@ uploaded to Mastodon."; "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.Input.SearchServersOrEnterUrl" = "Search communities 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.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Mastodon is made of users in different servers."; "Scene.ServerRules.Button.Confirm" = "I Agree"; "Scene.ServerRules.PrivacyPolicy" = "privacy policy"; From 6384a776977c8a9a9676c58d6a1a64974711d5d1 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 16 Nov 2022 18:45:27 +0800 Subject: [PATCH 584/658] chore: remove large logo --- .../Contents.json | 12 - .../logo.large.pdf | 1286 ----------------- .../MastodonAsset/Generated/Assets.swift | 1 - 3 files changed, 1299 deletions(-) delete mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/Contents.json delete mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/logo.large.pdf diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/Contents.json deleted file mode 100644 index ff1493e96..000000000 --- a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "logo.large.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/logo.large.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/logo.large.pdf deleted file mode 100644 index 63f9239a8..000000000 --- a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/logo.large.pdf +++ /dev/null @@ -1,1286 +0,0 @@ -%PDF-1.7 - -1 0 obj - << /Type /XObject - /Length 2 0 R - /Group << /Type /Group - /S /Transparency - >> - /Subtype /Form - /Resources << >> - /BBox [ 0.000000 0.000000 968.000000 252.000000 ] - >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 4.000000 4.000000 cm -0.000000 0.000000 0.000000 scn -0.000000 244.000000 m -960.000061 244.000000 l -960.000061 0.000000 l -0.000000 0.000000 l -0.000000 244.000000 l -h -f -n -Q - -endstream -endobj - -2 0 obj - 237 -endobj - -3 0 obj - << /Length 4 0 R - /Range [ 0.000000 1.000000 0.000000 1.000000 0.000000 1.000000 ] - /Domain [ 0.000000 1.000000 ] - /FunctionType 4 - >> -stream -{ 0.388235 exch 0.392157 exch 1.000000 exch dup 0.000000 gt { exch pop exch pop exch pop dup 0.000000 sub -0.050980 mul 0.388235 add exch dup 0.000000 sub -0.164706 mul 0.392157 add exch dup 0.000000 sub -0.200000 mul 1.000000 add exch } if dup 1.000000 gt { exch pop exch pop exch pop 0.337255 exch 0.227451 exch 0.800000 exch } if pop } -endstream -endobj - -4 0 obj - 339 -endobj - -5 0 obj - << /Type /XObject - /Length 6 0 R - /Group << /Type /Group - /S /Transparency - >> - /Subtype /Form - /Resources << /Pattern << /P1 << /Matrix [ 0.000000 -242.733612 242.733612 0.000000 -238.733612 247.147812 ] - /Shading << /Coords [ 0.000000 0.000000 1.000000 0.000000 ] - /ColorSpace /DeviceRGB - /Function 3 0 R - /Domain [ 0.000000 1.000000 ] - /ShadingType 2 - /Extend [ true true ] - >> - /PatternType 2 - /Type /Pattern - >> >> >> - /BBox [ 0.000000 0.000000 968.000000 252.000000 ] - >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 4.000000 2.211426 cm -/Pattern cs -/P1 scn -223.730621 191.199173 m -220.269104 217.264771 197.839127 237.841110 171.284851 241.799530 c -166.791870 242.471710 149.813065 244.936386 110.478516 244.936386 c -110.183769 244.936386 l -70.812729 244.936386 62.378529 242.471710 57.885239 241.799530 c -32.030567 237.915787 8.459440 219.468048 2.713966 193.066116 c --0.011457 180.070541 -0.306097 165.655823 0.209523 152.436340 c -0.946121 133.465759 1.093443 114.570023 2.787622 95.673965 c -3.966180 83.126495 5.991842 70.690964 8.901408 58.442535 c -14.352245 35.812286 36.376640 16.990784 57.959160 9.335617 c -81.051300 1.344009 105.911919 -0.000015 129.703659 5.489471 c -132.318665 6.124054 134.896866 6.833466 137.475067 7.655151 c -143.257462 9.522385 150.033890 11.613541 155.042755 15.273193 c -155.116684 15.310623 155.153488 15.385147 155.190292 15.459976 c -155.227097 15.534515 155.263901 15.609360 155.263901 15.721619 c -155.263901 34.019882 l -155.263901 34.019882 155.263901 34.169266 155.190292 34.243790 c -155.190292 34.318314 155.116684 34.393158 155.042755 34.430573 c -154.969147 34.467682 154.895538 34.505402 154.822235 34.542511 c -154.748016 34.542511 154.674713 34.542511 154.601089 34.542511 c -139.353043 30.845444 123.700752 28.978195 108.047844 29.015610 c -81.051300 29.015610 73.795784 42.011505 71.733467 47.388733 c -70.076012 52.056534 69.008049 56.948853 68.566086 61.877975 c -68.566086 61.952805 68.566078 62.027344 68.602882 62.102188 c -68.602882 62.176712 68.676498 62.251556 68.750420 62.288971 c -68.823723 62.326080 68.897331 62.363495 68.970940 62.400925 c -69.228882 62.400925 l -84.218689 58.741257 99.613655 56.874008 115.045425 56.874008 c -118.765503 56.874008 122.448158 56.874023 126.168236 56.985977 c -141.673615 57.434372 158.026428 58.218628 173.310669 61.243088 c -173.678726 61.317917 174.084183 61.392456 174.415436 61.467285 c -198.502548 66.172516 221.410675 80.885986 223.730621 118.154839 c -223.804840 119.611092 224.025665 133.540283 224.025665 135.033966 c -224.025665 140.225021 225.682816 171.742767 223.767731 191.124039 c -223.730621 191.199173 l -h -f -n -Q -q -1.000000 0.000000 -0.000000 1.000000 49.436829 165.103180 cm -1.000000 1.000000 1.000000 scn -0.000000 13.742415 m -0.000000 21.360460 6.003528 27.484528 13.443068 27.484528 c -20.882914 27.484528 26.885828 21.323042 26.885828 13.742415 c -26.885828 6.161789 20.882914 0.000004 13.443068 0.000004 c -6.003528 0.000004 0.000000 6.161789 0.000000 13.742415 c -h -f -n -Q -q -1.000000 0.000000 -0.000000 1.000000 108.917572 102.776794 cm -1.000000 1.000000 1.000000 scn -140.506592 64.230759 m -140.506592 1.045578 l -116.014610 1.045578 l -116.014610 62.363815 l -116.014610 75.284561 110.711319 81.819916 100.067276 81.819916 c -88.318474 81.819916 82.388847 74.014786 82.388847 58.667053 c -82.388847 25.094963 l -58.081230 25.094963 l -58.081230 58.667053 l -58.081230 74.089928 52.225231 81.819916 40.402508 81.819916 c -29.795889 81.819916 24.455156 75.284561 24.455156 62.363815 c -24.455156 1.082703 l -0.000000 1.082703 l -0.000000 64.230759 l -0.000000 77.114693 3.204189 87.384232 9.649378 95.001968 c -16.315704 102.620323 25.044342 106.466766 35.835602 106.466766 c -48.357925 106.466766 57.860092 101.537338 64.121254 91.678780 c -70.235199 81.222137 l -76.348824 91.678780 l -82.609680 101.499916 92.075348 106.466766 104.634178 106.466766 c -115.425430 106.466766 124.154060 102.582596 130.820389 95.001968 c -137.265579 87.384232 140.469772 77.189224 140.469772 64.230759 c -140.506592 64.230759 l -h -224.773392 32.825264 m -229.855560 38.314743 232.250977 45.111115 232.250977 53.401466 c -232.250977 61.691818 229.818756 68.562721 224.773392 73.790886 c -219.912048 79.280365 213.725754 81.894753 206.248169 81.894753 c -198.771530 81.894753 192.621094 79.280365 187.722946 73.790886 c -182.861298 68.562721 180.430618 61.691818 180.430618 53.401466 c -180.430618 45.111115 182.861298 38.239906 187.722946 32.825264 c -192.584290 27.597099 198.771530 24.945297 206.248169 24.945297 c -213.725754 24.945297 219.875244 27.559677 224.773392 32.825264 c -h -232.250977 103.927216 m -256.373688 103.927216 l -256.373688 2.875412 l -232.250977 2.875412 l -232.250977 14.787994 l -224.957428 4.892029 214.866684 0.000015 201.755173 0.000015 c -188.643661 0.000015 178.552017 5.041397 169.602249 15.348358 c -160.799988 25.655014 156.343811 38.389275 156.343811 53.326630 c -156.343811 68.264290 160.836792 80.811768 169.602249 91.118423 c -178.588821 101.425079 189.269653 106.653244 201.755173 106.653244 c -214.240997 106.653244 224.957428 101.798645 232.250977 91.939789 c -232.250977 103.852371 l -232.250977 103.927216 l -h -337.547302 55.305824 m -344.656830 49.816643 348.193237 42.160881 348.009216 32.488808 c -348.009216 22.182152 344.472839 14.078583 337.179291 8.477158 c -329.888794 2.987366 321.083191 0.186798 310.403564 0.186798 c -291.142242 0.186798 278.067230 8.290054 271.141724 24.198761 c -292.065430 36.932709 l -294.859528 28.269096 301.012146 23.787773 310.403564 23.787773 c -319.022095 23.787773 323.294586 26.588638 323.294586 32.451691 c -323.294586 36.708817 317.694061 40.555260 306.278320 43.542610 c -301.969055 44.737854 298.398956 45.970215 295.635529 46.978371 c -291.691223 48.583992 288.341980 50.413811 285.541718 52.654320 c -278.619293 58.143799 275.085999 65.388893 275.085999 74.687393 c -275.085999 84.583061 278.435272 92.462723 285.173676 98.138992 c -292.102234 104.001740 300.530579 106.802917 310.624390 106.802917 c -326.717407 106.802917 338.467468 99.744926 346.165894 85.404739 c -325.613281 73.305664 l -322.632080 80.176872 317.546814 83.612328 310.624390 83.612328 c -303.330841 83.612328 299.794495 80.811768 299.794495 75.322281 c -299.794495 71.064850 305.395020 67.218414 316.810730 64.230759 c -325.613312 62.214447 332.535675 59.189682 337.547302 55.305824 c -337.584137 55.305824 l -337.547302 55.305824 l -h -414.227722 78.907104 m -393.086243 78.907104 l -393.086243 36.858185 l -393.086243 31.816803 394.966400 28.754616 398.536469 27.372581 c -401.152710 26.364426 406.382080 26.177948 414.264496 26.551216 c -414.264496 2.912827 l -398.021179 0.896210 386.237457 2.539566 379.315033 7.954208 c -372.389526 13.182373 369.037201 22.928986 369.037201 36.820763 c -369.037201 78.907104 l -352.793854 78.907104 l -352.793854 103.964630 l -369.037201 103.964630 l -369.037201 124.354202 l -393.159851 132.233719 l -393.159851 103.927216 l -414.301331 103.927216 l -414.301331 78.869682 l -414.264496 78.869682 l -414.227722 78.907104 l -h -491.128937 33.422424 m -495.990265 38.650589 498.419403 45.335320 498.419403 53.438576 c -498.419403 61.542450 495.990265 68.152031 491.128937 73.455032 c -486.227722 78.682892 480.262238 81.334396 472.971741 81.334396 c -465.678192 81.334396 459.712677 78.720314 454.811462 73.455032 c -450.137238 67.965553 447.705017 61.355354 447.705017 53.438576 c -447.705017 45.522110 450.137238 38.911903 454.811462 33.422424 c -459.675873 28.194565 465.678192 25.543068 472.971741 25.543068 c -480.262238 25.543068 486.227722 28.156837 491.128937 33.422424 c -h -437.838196 15.460297 m -428.296478 25.766960 423.619141 38.277023 423.619141 53.438576 c -423.619141 68.600441 428.296478 80.923401 437.838196 91.230370 c -447.373779 101.537338 459.160614 106.765503 472.971741 106.765503 c -486.779785 106.765503 498.606476 101.537338 508.108337 91.230370 c -517.610168 80.923401 522.508362 68.189453 522.508362 53.438576 c -522.508362 38.688011 517.610168 25.766960 508.108337 15.460297 c -498.566620 5.153336 486.966888 0.112274 472.971741 0.112274 c -458.976593 0.112274 447.336975 5.153336 437.838196 15.460297 c -h -603.163696 32.862373 m -608.025024 38.351860 610.457275 45.148232 610.457275 53.438576 c -610.457275 61.728928 608.025024 68.600441 603.163696 73.828300 c -598.305420 79.318092 592.119080 81.931870 584.638489 81.931870 c -577.163940 81.931870 570.974548 79.318092 565.932251 73.828300 c -561.067871 68.600441 558.638733 61.728928 558.638733 53.438576 c -558.638733 45.148232 561.067871 38.277023 565.932251 32.862373 c -571.011353 27.634209 577.347961 24.983017 584.638489 24.983017 c -591.935059 24.983017 598.268616 27.597099 603.163696 32.862373 c -h -610.457275 144.407715 m -634.579895 144.407715 l -634.579895 2.912827 l -610.457275 2.912827 l -610.457275 14.825424 l -603.350830 4.929443 593.256958 0.037430 580.148254 0.037430 c -567.036438 0.037430 556.798462 5.078506 547.772034 15.385773 c -538.969421 25.692429 534.515991 38.426392 534.515991 53.364052 c -534.515991 68.301407 539.006226 80.848877 547.772034 91.155533 c -556.721802 101.462807 567.585449 106.690666 580.148254 106.690666 c -592.704895 106.690666 603.350830 101.836067 610.457275 91.977203 c -610.457275 144.370377 l -610.457275 144.407715 l -h -719.326782 33.534683 m -724.188110 38.762848 726.620300 45.446960 726.620300 53.550835 c -726.620300 61.654091 724.188110 68.264290 719.326782 73.566986 c -714.465454 78.794846 708.499878 81.446350 701.169556 81.446350 c -693.842285 81.446350 687.913635 78.832268 683.012390 73.566986 c -678.335083 68.077202 675.902893 61.467613 675.902893 53.550835 c -675.902893 45.633751 678.335083 39.024162 683.012390 33.534683 c -687.876831 28.306213 693.879089 25.655014 701.169556 25.655014 c -708.463074 25.655014 714.428650 28.269096 719.326782 33.534683 c -h -666.032959 15.572266 m -656.534180 25.879227 651.817017 38.389275 651.817017 53.550835 c -651.817017 68.712090 656.497375 81.035660 666.032959 91.342316 c -675.571594 101.649284 687.358459 106.877449 701.169556 106.877449 c -714.980652 106.877449 726.804321 101.649284 736.306152 91.342316 c -745.841736 81.035660 750.706177 68.301399 750.706177 53.550835 c -750.706177 38.799957 745.841736 25.879227 736.306152 15.572266 c -726.767517 5.265610 715.164673 0.223923 701.169556 0.223923 c -687.174377 0.223923 675.534790 5.265610 666.032959 15.572266 c -h -855.082458 65.089851 m -855.082458 3.025085 l -830.959839 3.025085 l -830.959839 61.840881 l -830.959839 68.525604 829.300476 73.566986 825.877625 77.375702 c -822.709290 80.811768 818.216003 82.604172 812.431458 82.604172 c -798.807434 82.604172 791.882019 74.313820 791.882019 57.546638 c -791.882019 2.987350 l -767.759216 2.987350 l -767.759216 103.964630 l -791.882019 103.964630 l -791.882019 92.612091 l -797.663452 102.097687 806.870789 106.765503 819.724976 106.765503 c -829.999756 106.765503 838.434265 103.142960 844.991699 95.674278 c -851.733154 88.205605 855.082458 78.085426 855.082458 64.977905 c -f -n -Q - -endstream -endobj - -6 0 obj - 10350 -endobj - -7 0 obj - << /Type /XObject - /Subtype /Image - /BitsPerComponent 8 - /Length 8 0 R - /Height 502 - /Width 1936 - /ColorSpace /DeviceGray - /Filter [ /FlateDecode ] - >> -stream -x읅CJ*8ť8").i{LMvei{}N&9g? -`XzZS)fYi„  M5Ze6„레) GٗĂ Y%␱$UUr#2 &ZiހfJY-"B -ۡHQ-(fJ̊'O9#BKJӓPȏi{H!LPAW*FsWA%5 . -_PP(j=[^^YQYYUUSJ}]u dn+f`8HF*sv]ׯI_ռ:*e]XP %{Zg%&*(eDnÄ &>z_RY[ -jrZhk_e&>{(؄  U&ft4q gfC-/kQh[ZZZ[;::vv}z=߿uJ 2~-t\ggGG{[[kk Fe_TWWUŴFAQbAvVmfQ̱#^6&L0wqd̂M-XͫҼ1I"LOD0N;cEO:ㅘ/9SU]ًE*Nm_q`pphhxdddtttlt||B199959555-̰,YMSS91ёᡡA)}}{޽z5MGhz\nsjrr29.fJ%K59L0ayIT|jnwCc3 fm!ff&GkKS= 0Ac%>4T1&N1".zYZ~]4p^0wl|eB/,,...---/- - -kk -ce -⮮r -/ ..,.,,RQm(~@4;G7\"~tX(TqBXXĥUյuov{Fc_X+5+pk[w`wwwbe:sz!NvwvIp[EP^ZBMϣѢ11Q=J;c;7_*J^j"#=5% 'vlgZ'?aL„ ƃ<QR94{xj>I6eWEa}ԏrR3XUQleU5Bk[gwOoZxj,*xeE_xї΃U('''ˋKbf+Y2_XIL3R&}V=Ծh-(i444f,@oϻΎV+KK -E+;&JLф & E&5_?-#HW]I ]aA2.~PqZFVN^!ʚں}C3X˫m ߽},Z/EwLZqOL`S>f,KLjd 1$3|$1)Rٗ\iQXӢņƀF9c;PytdhPێ|u`*rltT'OJ &LGĤV4M]|Wǫ줘%L=z̀qTL|41޴E OV6D 4h_xQ(]t'zA_oVKBY0Pj2kHC$yr\P JZ ljQТvֺռfb~*Z 7;;YXp &LEƦU8o,Y0ׯ/֦zscq?e,:6.19-=+]\QU[]o?6X[;Z6FъZ+- - ?Ԭ6~o3DHTFઊ/hQhhQW1mD1//.Ltu67(-.LуɢU'L0NqUs[ן? cX߿^m5UF< -*aleᦎGgf.~Hq/Fy4(b0ưX¢oo߿4u&%dZ~2 ?HpT?FII-hp,&3zR~&:eadgɏ~0am['7_ Fo>.M q'2f2/*|U]8xiXbq⾠P{_'Yam(|*Uec_Cx$9VʙIЦrVGdF/lm,-`*l{SWSQR$*yBlԳO:\„ cQD\j^UМ03p5B7W`D0$6~42:.>)5#;م嵍-M|%-jsXibƊَo_ʣ -_됅/Dd̵l1u,?PL_~֊˧'h͵Źɑ]M *K -s_''Eq[?Ľϳ &L_aH+$B|u񡿹<792~hĔ<, 17..MaLQm2_ʮ̂ u} p!Zf֚oRWWgG[+ cC}5%9ib$G>Q^밍&LWUǴ/V"߱g_-$:zyjfNAIūΞyb'gbQ\/gp3l]R rLVc`ogsuinz|uuYqFh<;a0a(}}l?nj70 -Ʊ I/rʪ;'0׷wN/ŷXŨ*Ĕ#?|Ύ=޶dzoH?Dߞ*L~\DeQb+|}}yNZ[F%jm|YhhdT2O1L0v?=1э?8<~FP8"2:619=eE훶ޡ|J3T(W -)P 4>C8yp%~F'a_>tsuyT໎UhTS#r0ahL}\64m-h0 -) lm8%3NcS[8h6[!%$l t:m4KV2Vɛ&z߶Wf$8hpM -_c>>A6t_q{ z68E>O@W׷t^\=<eaUbM-sĭع B@"fI$ 嚶aK!`&ˀ8w֗F[j -sFx8Ä ޯΫ[(},W~J3Ba78#oDLƌ/?1b]VX^^NAʆ(dxCvkuGcĤ;TǛ=сƚ"Ƒb<0a~vm͏o&>Wx_ 'e4Cc4&ʘ 4Oᕐ>qe/a_?[~>?9YW$/3%1V9iFya -"'OFDDF<}~?AƏ=վ?™oMmKL/o6]b -X,c~t{i 8pD@o?]_`#/͌w5(HD58L ?~ -1rݛI|ksI"̿ j<WuJF˪.l]o>bEu /N5!\+̸@E`H(x^G`ŽS8طCgEȋӣM R+aصg*瓧_=OJNz !Oc!E;߯v`kC:i8}HWuNaym۾Ók6N+mޖ"3(ֆ_&<9&'Z(TbTq:.1}F>9^ikz@)jPjƑezNbaFDO3NF߆GO0S^ּx?26AyaU|8ߘR;ja4F0KP\cy'J#:p@{ ?,[s-+ -,9LbGcd)(*.)yY2q}q|hL͐ѦaW#c9U'VN.?~ўj Āo˾mmαB@U *D-+EE ymEmٷH?m,͌u4V`"c06VDZQeյ^Ue<}$WW;Ə1Nq#BSjVA٫7og7ϱmXեI-wW_dodд,'h?b#_Ou./JM}VaBG~ 򱲌2.|Y^]mgkcMInj|a(}bH8 mDnrLXˠ?~㶞ݣ돷JcOG0`mb5Vx 6rLݗ-'&Z\l-enFEY0w*Fe,z\)7mo u,|~O@WeC/ڠ &s}W۠S#_䗽j[>8ئgܑ2c.pm-Ca%03Էom:,|1dľ펜'yݵɡw-e/RL5$2WgU_oK!ƩJB,NގʢS:7TGmGDFYʸPƳ+뛛FKsRb#7x82}guX{`|:.)#՝sc%Î:"A֎rկg +Թ`&?Y13y`v[/-|Ѭ#w0Ș o0ܗ<6rOŢ2VcY(c{Dom-M6UƅiϛX -}~K< _uʋ:9^?Wa\e -O㟄Bʜp/:\PQ\epA>^l. .LAp -]cc,gQ'fdeشӝ˫㽵>Cw|ڎ+r:?ML.j8>Wru*RhֆAމ\ϱo;-S_LÝչ/R#E!Ä.o~<eq(7bO)e|2x{{}q1;V]>;3}K;_}yZ3t<=kpra}Fcdq =~Gƪ4B7a|628Ӵ e22BP[8qxcsg>Wx_QqIe[{gN,ئ4a 'G%{UiXqh|Ɩa%` aw0E:\j.IKD!ؠ_,3K+_7wznɰ22N듭|>x oQK+ ?_\"$j"_TԷͮ]\ƫ!DcnIA3iKə ۹F - 斄K1ܠp{Ҽ1 -9UF=O7uiemʸoh|z^,㳋+_}X}֭a?fhDDǧdV5tO,\~Tߪe˓eľ -!@4wՎajoF%ۏGks=շ -9:~SP2nx7<,݃SQƟQƸt}<.I!]Dr>W1~<']Tkpjqkx(].B.VC -8?-^Xv&̇Ui"x<}xu>?VW.V)/\u<-ee'ƌQRt1I 5~c_6jJB>e/N7'ӟ-0>=*_V׷vL/X2Ac>RWG> ?J\߸cɰ|>$yqznImkϨ:O 7;V_oLm=X, Qn鹅!Y>;\@!g$ńrF%ԶtM/oɘGm+md]e}ZVm; -c$;ۉ6YWwlnm)8GL_# Er`6xqn1L }FZ:Q䯟oPȝ>0r<|Ugxzquk39ʙ[W$>|>䱃jƏ#φI$q^Y]{;MեJ \ߝ<[mS Յ}?r G0.kP~nN-{1z mL )Zqxx@+E۳յW+rSbmFYǨ 1CǶR7R-@Ű6%Jʕ kU,C;%hVKCd6PG# -yz SO3 -_ L/_[1CU?ZcƏu6P탉Xޯ_d!긮rqmHX0!eLől1T,0Vb+Y vl[$?:H@!ȏE!'?0㘤ͣJ[&c}.XW`?u}0zuW^!PԱy[z m(CD潘芿Z߾|P -Y^J/u&Sap1&9g]`EUaش|W}ǒ}:ȇrEgc/m]P,xouغ`7bGceCpawR$!vQR -yY)<9ؔ뇗Ikˋ義aOW]_mfڈ}ϕ+w -(8 xjqK;z6r8UO[-n -AruTuFQB+MKdl Eܪ׷'d) ??T-2~L&C̈́>a}o 鹨ŭs) ? O w4*ZG63p L>n_XKKrR⣞1Zf7\a/~|߯}W i8!~m>` F~:WE) zZqAjlX X/OK`l*I /XQ9%}XJ!ؒxG#*`?6qrC~B>\RSI} W1qԁ+5^?菉.;W>l{^o}=ywbqˇUBG>LҹDYLV!Բ&Q`)*Qȇc -3wȝ0Rhmj/C+c RDF=>6D:M~Q\~laS;}ΓR!!.Mpsq'h+t4_/ 3 \mC8gHwcE~zb4 1}M?@a2,>Rs)}ֿwhu<:fAst{ܓ A(N0on:pw8G|Oho> u֗wU_kc1j] X|7cq|> M}||m_?au 0,0GD'W6vϭ^kuLA017b0?@#xFlc%6×o 8b!(䫓N3I"? ӨܲΡkwd}o~8c/L+pϰ/JV}߿W>Aojyev~ľJ͆nHVR M} i@G;]k3c~Df}Lc=y$eGS;>VO:?3_-8>VyW? 孎NL/z3u|>&0o>V]m1Y@#L6~8l[,>%sxvPRph YЇl0Gofp},Hfp->jXƏYlEF~+|y>>;㈘ůWN?w@ DGT6%?-!pi<6k/lq%c M&AB+:^g=Wtk޿ _c?aOb}.ν;ńf,<|$:!=_~괸}|P --?<VA -q:(Aߝ< Xq:nMU >Fc+"v_nN6,c~$18bsJmZì>܍J,mu=pk;@;f,/KWdam=?HO WXύ/N|ӿ6W[ -s5ǃ>~!-}LV:(87%щEZWn< 50OרP7 #m#fjf޲m_yV[k+]aˆ>>6Jo11DZ?O;| }c~5I^iT|Z^śզy]罏xd+@-Ҏc_LNmۦ P7ֽ'[|z{qL~,o sL+jK/x|`Z}<8a_m"WL -oyZLȆeA*I -].cT>P6'?VXD? dbYO"5˿V]?<1{* s͘a%kD+A9 s/Td1-?xqaUq?7OVჇ2ak8"Xa{F% aW5/AqS(C}<)>"x[ }ܾF+!;1z>Ih˅&40k[GdyQP7"VYm=q TZ@GLl0 qW/9@>ߜ.O4Uc "["yHj-(9uSxɓ'@8Bp`Z} XcQx^Əw\R=Dy VjW%HE*A5܈rEh? U{).ݍZXZFMgI|9)?J(iY?j7 ]_2Pg9ˌ0bG\>l<ۗ4QBDU6CK]}R~H'QRIXRc'19xAl;4賏cL}'D i"#cbb()&1'&& ys4qR>GܳmQ8RGJB$ S&)O)`ȉ.}_mfj5>V?>] 9|TJ.%$&H&&A2KJ" ܳ]\zȋB"9C$A9A~Gƥ5v-n\߭6P? -BQUVm_{ [Oz~$+)6͒Y14HI)i$tz9L 0]Bӧ>%$<32_x"u&ҥ$%clG ؔց=xcVIJEp<}*e^j)E3|rL(2=yu._>= _lkqQNAeeeggdgge}%9왨*FjRS D^-n"Qj4==UKJzO /g֩mK NlBwHpȖ*Ϻ*U CxlouW}:&BƋ|r Wݧb)IɩYٹyEEEj.(΢ TEoA Y2H%`gf!T˗%%2ssr^dI#Dpɘ>xRZ[? b3~ +rV?z%, cX>84dM=4-UABTTlEPZZ(5$B"tԾZ8@WTL)Tb]hJ#hdt=D~v -aɆmO~%/GҟJ͖żETU-n489EtV^AҲʪꚚW^\S]UQ^^0?7EFZ2J'd]i$}b_=K&ؽ |UR!E*/,.)Euuu _,/+)dSM&Cz!B\f,nj(Tf3>k -2 tƒr $l #Tѐ6N<IJZyRB*+J^KGF=h:6‘o_$8>|-(U0RD&m Fy"ZGp}\>N"Vԅd#zLLol|#466׽EEydHi_IK6 &F*8)U&-]p~kj>Be/ rG&I,)y/hz<.~>8~(=C'Ym@a6 8ՙ -yS)=y`G]Y~FRxwA=|(bxMЄmWW;ymMM u5!张$-97F;"‘HᠣVn62-½෵67IF& -*eHC{ ~+1X(>RO.F7,zUL.RԒ\\Iu ݿufsa/ڲij"z<«@ rh\2]5=Q q"={ qnӛ/e.cׯ(i۱d'0Ԕ̊XXQ 5Cd&O'^dRƥE&R𜚚[@jji@_oOێfjU9MnvR*!(N)ؕ@e1ʟL,X2A8# БOu0Ȩx1bX Mo#$LL ͒-+#% F-rCr]|t FڙYC,~̼1\XQ|P.?ܜ.'d3)x^j2TvĀ(eLH/(*MknxT!-E2 K^Nqp9_m}UZ@םg =*']7zȊ6VZ :4%Ug:߽3.J0=9>:LTВkz|wWG2LR8e,i -FP) liUYJΔF;Jm D}lcjtN~}6N)RoZۻvxlbZ*ykˋs#C"tۛ:WŅ4Wd$Jk{@HOdY*䊔y0cw]I*"pbrzV^Qi+rWZqJ-UyUA%giƇ{u5ս,UrҢ!] e9K;'7޸3D2)g"qy<؞4}/]<ٌlM~N~7g}oj+^_ide"%WuFQr{F'gW77wwv07396X[J~.WQ bP ;Bß7JDB?WU=1qqyMݛVt$b-on!޾¬}W[s=* '3 @ 'o7[^cbaWUsG,6pwӫ2da~-^=_$G/7ڴy<ĝ=)noon,-ΣƆ4Ғ(!s?AOHϢpTnlp ؇9U7(H@;zjuyia~fr|XXK7#BKqzX~V:>>Ǵ9#ʩuAe={ccmeyP[2?[4aF!Bυ},Wp`}lF*nIFQ#o'r@Ʀ>̓JZSp*/ܠ=N|RocKaBORArZb,7NKO<y]բ퉸"/#)(\_YB%#'-b]l-N R_SVD3'%'7 ꖉhzvJޡ1J.͒cj5ܘ\__]RaNwyuzPidq:R,rP4CLZX A27i)+NBAšBm"-x -&F\^^H? ,?~َVH;F@ldfۢ_u-6ycsK6f}Ó g7_=}(>n6&w47cR[ _eYMn@@ 1^_~p[M7E,n>gxuI䈶\O75)z/r#lzj)JsC0@D()(()k4BoB7i5c1va+L?{ B/4!mLnܒT}CZ#S'DW9y~zrx&{J%#' \ЙTYA[kGƎmW%Fo"؄L ቙5UOWg.KAҦ퉠Ri9J#Rrᓨ4ǎO(Ծ=⁹V;~9h}P/8NCY*kw="(|~<75O('#91&J]?Yr9gVQU{Dkn졊TSg;_J>Z_^KP@##! =}}̲k?Q]b<'T-oU^qƶ1Ds#Qo߾~e)ׯ"fpg956uKlq+XRi -  `whxdlznisz)(?SVN7>L ᯖʢ_):V=".75 ~]0X2<²w`tzny}kn[.yH,")EZ[BξzJ{N)iYI]Z1Ymf?!d;2q Nߵ~&{lHRT. ƱL܂#ZbRl)߿}7TU uminjT A!vbx |XY.={?1./jd7=c3 (cKZ,!7$GU E[d\] N:U A? odrÜ? ';W?[Dzٕno.O <",]{CE>}yMg?O0VwӖPG,n!~ O Y@)?f&UM:!Wא'"Y!Z#  !'tji#X璟t_ǿ_M6n|T=(=݃StӍR"(RYZ?6Ԩ*;&O.Y\dU7Wy}|SU V-il78F+{/_Vk)(}F~TrgU-3k瞣~84D*<@Vpe0ۡue<!?OgG>ko.ϒa"S[U$hVlȩe5WSy̾Qɥm.ЭxGWU'zRKmd\rvI]G?f$l¾k2;I^mcFb=O -m<8ai}kյnm ,x"}Uus'HŹuG8:lcbykKގQx~QFm܅'͒+ -`oyXjYóm-f\iG1ޯCTGXZZn [h^1,o2yt:&1oRcLd[f;̍w.JfFӣQK-Mo]Ꞗ]>m5OKUhtՠCv4uoGOooc5$d8TXN0p\E9"J- oGe2pǼ}64/iOv֗fȭuUGD!eYL/]ftYWT!%G(lir`J1?tӣ|!p6ʭh[;<kpokun8A{!U)/J MɠD`%?Dd3&*Y;1Fz;j ugC] RqnycܚZl0~b:nwb'us`_8nʇ2q{Dr7S, JiŇ'gQ;RR6 O -9}-FC]A;6U6PPb.UԾD9)Ǩ'1`R"ϺѦ/ ":!U7Wi+7}>_Əa{_mHӍDZ&?FNb?x.aUm ғw78.-hnyw wBG)|*HT GSM 022ԱqCW|/JuL}8?oXm& rR:`Tu0:TAl`d$g 5m3FP4vWh.&#sbZNqU}uxD^d9xcdb'UҠ!$yH@^I,|Θѕ"FDcel=%/*]l0G}޺7:sel=%/YKCHFM0WU! |+ՕIם^X:%&E{ ZWXGJVZz˰sxn1i&`Rm -n7;^f$<{rg@#D7#&!^KD,]#0B]sVf'hTgCM FTġSK' = 6"FyVqQ$ ?Yt}v6;ZC_1q$l0 Y{ƦV -W%ql̔ĘȈ'jܟѰEAo{7D?Q Eyq -%ZEuC)5Em~$<#/5ZƏE|i3Mǿ\RåmKd WUPGZuK6<F]|$d׾Y:%b/Ȧ^.n.7i.j@~CyF^ɫ׶N/rܿ+c7DN\ heԼ3ZGKԧ22`!o{zL!LA` w^q'1Sƍ?^j]&YO%Tx$UԵҝ[>i+:9^GYQFFD'fӟdE^I#zfX^,ve$D>&Wمm=CFXH[! 8D#o,l)Q.u~@Bc c6f`7 PNRsany]fOťUWŒǏ~]0U> dЦghj~M#| H]C;bd$o|z3@]7O]71jU)L,fOi.g>5_4Of/܎a)}U^A&yY;_{f.a;nFepj-Ylxs#1dߑuU~J8Mʢ)1S\]VInM-lcφTq\D.S /^uO,1"m'PՆ?(_k~[Wy/dH_unI͛+U/Qe&L!ݲB]ed̢owOHg?]^+|,';tgqm-Z0qD^?;m_dSkul\lhdq2_SRnBh0+U ڕ+ژ2k 7y qIDWM[5F'IIw ݐ䧪㷦*pONt/+{k X3f44]_f%'%$gTwͭQrE`9\j%RkJd2>x$*W}_έ n1l44憹v!𐑼 -&>#[}3v6?1XMN@!zm.&!5gxjac*/?TEZb E`2_?-wFGGK7#*N-07IV0"'.UZ fo{CeQ6c* }L$2!F^*lr } -}{F3R03J?3*#ih#t^A!9n1i(Φ%?(zjtA j&WOư@61lonk;Ѧ-Lmw,:>}<$>_Ѐˣ~#he!!:! m 6XJSjia0H2π[DB&Kc+ )4u9iII)/ -+:&6UJԑ/؇-l~l` EDD9e=4{gIMvU#X1@Tܪwa7]3Vp)(W.lq:*חK貦W%x< _#WK9/ňU}z꘰ -ad@[q0_Q v~L\rV[Cn:? -'S!ߏ)R1@#}Wѡ*~w kg?:WR]MF)!n~d4~qW"e'ݿJshu3˛{q`J@f -D02+̚{B͝w`FlƏ}.v73Zcs?RJXۯ[ N-iKml^+W8 -DeROħ7P2']zdq?'i.z||wi,/3=ƷSK[gW#X1@~CS|~Ӵb&%2P~8R6ŭ Ԯ {(9N$0D<lG20\. {'%we=u{Wl (d)}nhJs1"=[&r{}qXY<5[1jWSel 5ba}j!+8.&43L;5Aak}l]ٺ5E=;a2ՏoeFdS2pEoLhc3NBiu~8\WQd[K &=|ebb^, /LbF!ho[]yAfrb"S;DAe7>O9Y?!ӤvveKQzUr%u^p䧃J?DL u7ז=;UD3Es"[rn$ + -sst M/o_꞊+7qA~=/6K]NDVt+_ޡGq][Et ߑ}ۏޒG+Et -К~;.{4O{5SR[ňLyQP^~c:FNdu ~ܢ7ŌG[NkC;3qb:,5KX-qUݼݝ},/C}W?dm`8t_t OyE8)"g۫} jx.? W]<^ĕ)w6du ̯yy.І!wZW(}OAW(Af*KU>:,E[ 0:ߟa& }LǢ?m˓=M%Eը㙕cz ґ>F'ʟq*c&*A!ǨzcV&IqWSTZV+Q&<Tj4,ٱ-C0YpPVzc^~J~Ѧ|Sh:xjFÕU?>{tAQpY~[zG[3M5/^dfKw68 =ޯfr^4wXٖ_V[(+Bfݤ gP)dG-ޢ/+,Z/5㶼A R0:n}G [`N:mt=q񊫓;~~+b?}8tA{e Ѱn7X{_ F6Vde疼;HFZRm{G|8v%} -_<oYY5'e@cX*}j.MToKmeE+wo}Fd_"FG#{sq%D097 j LKơj[Ҷ8d B |3a] z:lNe C>NH.zv`rqs_*+]g>0ű(K -DӴSvB~5)guW=?L~SA4@ BeM]۰ǣX$A백IGԳc(}o{:ơ< UwXv楸j0hֆ:6b"/ ի=Owh㓳 -q m3B/~M6V,,(*[SC'_*:N.j10o*|zDf]smi~fR(ȇM}1/w |uMaKYq_JO|1?uJ|TrHB~8$fKdlp/L>chH}EA9]fW<%,.#0ƌd]'[Q.{Lxi%|Q7 BMVDF(1\6͵+D+e3;%Z`#2S?;EScE'[z,rIrN1#k4QW/.k }~cE1OЇ܊̮xpc>O!GXS0<̈#W6vAߔ%e_&DPڊu[R3au\ -\;I'ֆښW ӫF-\;Ps-8dv+f rLTAXBF ?N([c# -f:+eNw? ]nʊKTQ‘7$Y,.5/pzaٰS^ɤ!]Pkuwǭ1 -BwWg[kkZzƗmsͩcc=~^zmU˻|"=2.RBEuZ5q:+ -_/rz\8CƏ(Tv\4p9pI|17Er3rɳR"cS*fύIk߹M#=VLK.A],81kؿs&FzYhki$'5A_B)V*F Z,zylB6 _w 򦩭k:tWL IH;U\36G+DBLXwY~H :ƅ29D>67D.I!<;N} ڏ'5Fߤ&0(\Ķib͊fpu7epY~ T_3z]B |7\kR=-M-S#S!Es@'eJjlnM0Ԑ5-狓ՁHϊ?}?KVЏ[K3#+;:nZv}O ̤ؠv@vжiKHjX)YA\$ 4=mZS1D!Oz'k &cyzNɵmZx˯5=]o6}d_~PwLafa SԱ B#hLA&}xq4ɘB0l91cվLB[6^M}}L`=*N~↼SQeܥ^*l+!9,+hOo2j`s_5ӸѬkv6-` =uU)KH_j8V(n6*IưmZhGg4=F+d?%c >v*߿'&gVЬ2| Hʂyl!2_w7mK.*~,ֆ?;'1O\-w0~l}$"&!Bh -F[~*)֏+>5:?82,ߍ3+J6V&8|E51I';P#?eᶌ)ԕ椊ΪZ>@"$ *$+X2['V긨S~'vHA3ujID k[_5/qZ(#JNT} < :71<884PǗe},*Q xP$7܀ܑZ ֆ?,dq袮̌t7U֟Cc}B&W7"pNEDڳCo(s]`8Z8  kۅe -6 p en`Q0b z[/ј?I~io} yu}2vOZ3ْr"N)EN},&}cQ eC#'Ђ|v̘B;c -))cW}l$!&Bla}Wo%XPnn՝c= K񨸔ª7]t뇛rNCBMlG9D;.q {'iC=_qJ~@F - ɖl"!w>LNNX r3P*nj[|83 +;&T7G6߉pu|y\SBc^ܙpd{}}ck==.An/OYz{u*ܱCu½{E~/or$%PX }^f&ٿgJ @XX }]KJͪ/!+KA^:X01RX>$Hf/􍚨pʯ-5&NѵNcD_vO`1v5H06NF7Z>^zr\dAmHL}CsXBxLו lu?]ɕE -k$w'{[kmK;gq/߇q `fmCo*uXH5ฅ_Ġ|BhCO~,W9~,]gyݣGQ'pϻ;c#ȘHWCE^e; 2)X3ȳc -g5ֱcyٶXTe#Γ\4-hQ/ *ꉲk;F˰[2;@T"='jP&X{Ubj4 -u\Pv/Ǒ~mJ@{޽ &d$ -#B>,rU'B6}w32);xsuq);H'qtl'7s#]yi-ݤ{itbViûZsq7|=s¹qP֝qr~=wO+Wϛ_X4I+jյ.\_^c <>VuB]jXկ;??1Q#g{;SʩsH1jwهs:Uo>VǢ-rz1ֱR -$m$5lB*O"^V73Ա%RX7+ 6zBvqftAMl3}3~~+Ycg:QuL؏X7땪WÒ=!(dnP{zq|ONQ 8`xs~]oHlJ߲ x󨾦eD[jnU.PY.:TKna\gݍ./쓏ȚW98:fFz~u r?]4bH}6|/q=},&[r^SE.B/.YK!XGw -ԼGWɖ%WG 8+ލ-veÆ/9]!ɟ(?V)}_kMR+ά",yonϟo?}tb;nGEՈNQD~2SrFh9Re"ʏl_?]o/NpSX}>WԻaƖGggji+:םc -U~+ሬ?D|N֖dA?˟D_5<:9|P)_*%aE{dqB5!-uØ Ru#q `!_6Uf&kA}.wpu˖'G# D4qNP_m.v7k'4J߽Nqb{?(λ9 ,'Tr$?MF68 'iҖ>6F@;3P`FPi6̈́F蔳=㺻Ǐk0RQm`fm@'[sNPC+},1XΑYaJAuZX+lGD3~>;P.cU ͬҙAmS#wgvhVtd4~s>,S ۣRTw?>~k{hJׯ 0:QޯVXXtuO-o:k]ч_,]jfEcQ+BU1i\*'΂mOM8> ׾|jV:'YH -HwB$aYQ.{LtA7xTcj}鯖`}V+^X׽iz4Jz XAb)`xߦEhKU -AP:eY>&2}ȸw$|+Fԯ>He%<;G_ckjJMcg㻿uL-ê21 -ƣ0mwaޯyXwM}?Уx:Ű -AxQ5-"G`G͎yw" %Ćy50 $!NpI>>YngBȺu(?U0\[Qȟe=4O̤7koWq\_ Dsm@E]bYʉ!Pآ4k,em 2GbȬ)a%KhXfXc3HWO[1յg|fRmYʉѐE77!>Scy\ӆy|xITX.AVBkeL_5բ[CH_o!B9UJ{4֣'bAҌ/`U4S`4VN[ [tsӐ_x> u֗ҁ(L5B3D*Pd+'!bnn"?2 UU[ %KhR@//]⬤ =x@.,Šgl;pd+HW}IVrS8ܸMsYX:q=K22*1uP-?*ݻ_xWoK:jB VS݃ZxsaTw;6υ}و1Ѝ; ]Ɓ:ST oZ"/ '񣇴( ~ީ᯾R1̵>םjiƯǹҌbU{P o8MMxWİI ?feuǠzZf qCEP›;ҽ$G[M { #;>-Ї= }j5Z!i.̔OnH -~v\4:!- qBG}>VP1 '(i\ڑlfUnA*;.x{ktJtDy+Zw2"w QlY߉Dt#`m_h>_>sxmܗ;P dPApS\4\8^|#Ud=98Nh=c 2WgAHq@K}lc{ˋ_HYS|Z/bGɏR}U_d jeĵAW;K=ohL# ۚHTm^-HC/1),>/h%|#bf|bi[w-TpgvƱC(jEwBԽGhOJ:d70}dPApG>>s$3-"Z:]{Uu*Cm/I(1qǺ7+?:zipG`V_?_o/r4b]~jU^3;o -ۦ-Dbc\[.AH?M~k~m]z_ȖiBa%[M!`c-S A(c$qHޫͣ+ DY~N_+{oo? ~r2V.؄"/ۋ8zL꾊k?u&ﻅ%$}ޯXD7FM R^p-fsm= (.fv{1_jZQ6ԕ~GEŒb-S%}}ƏSB~#F&?߮ -I?wU#X\F-(tfٲ -6Ѽ~㧋5$CTO[7~t!]M{vVpoOv`X8.BYz/WD-6*03xh 4SMy4 -GO@Z[ߴj $  -+ (jaDWƏ>~ &Л/0W{'~xs}uyqqq|!x !ݠQ ,2'diKJ2!޳v^ q H_F_jH[SVvxn@Z-ʬH8TOv*Ov'%P&+l0 -+ u0Np?&?Ǣ_4$}OngexuT^ 5l#fJ>]P/Fӑ>}}Ynx뾜W<]j/0o8ΰvvn㵼b)_j&X}z`v843!kz$ch8_yQsU߯vUdbAffwh]XAa*4t(ˣ1 yM7=]*} 3.b/ uOoJL>k'{ 9W3~}cDtbFAu{Fz0@ZỶq*w" P@sPY7 !v]R?f.Wd 0q}O>,.0T`jx2Jϭ+FƧTY_%e.j6l_Ԗy&c!l)-`۽ro ᨍ4P2:,;96btb4M%;fI$z< ?S.xxYP٧DScar7"^fcDqY|zamI2zK!=DY -ol^\}$uUKTuyȿwXl(/N}L [\ξ{ȣjWxՌ# _e@C) -S`]艑G_cwJ22>ӄ2oދP{"M jGgK!r=?/>>:'kd~d'*@-BGn -}sA/]PDߎG3AUБa:?M~ P_cխS[X2/+T1͵GJ4MiљM~c{ ^>@>:򳂝SԞ%Q}suv0azjjffWַvo?;ǦPf?[7tyv:7X%_Nb1'<"IC>_Ǐݚ?5ou׈Xp?"o<U6Dc1# FY+lj|\B[d,BGC)4gR/]Fx]DZR&("jdlRVտRqA+H!(و&d%7DC*vES?ƿB -cE7;⯶m! -d!W ;<<vquM^뗄- k]ciz{W28 N-m]݈8 */ώv{+S'}է.?Ifh"LWA4bA$(~?#H7+%gǏ7WfHpɤdcT䧩.wyj;>KuLb$ILBJAѐ"&e/SU'JC*qɂ"avcn^g&InˆȊ&FX\ĎqCV~6ZY$ go? pw@onck!u0|A/uyqz|xsoln ;D%KNyJ:\ozlyfIenbۼ@ce#0gD;Gb;8|7#X ѹW&d]P_ejWPY.踜BB(؀_>}Y:;9:8أXoomnn ;t4H_QufiIsmXS?qxi|%y+:<^|o޴}74>=wt~>9~k`W6ՖHM_}FtPoɶ邎n`+.؏@}.]?Өʦ4W;@)ceP4*䘿IyLCn;NsbAӅXІh>R:Ω  e| -jG:9GiȽaK{8ce1n;缈 -p̡#zWWd"zG$#AKJy{svd>z Q߽hwk}uya~V\a͝SjWHX0C:ٖ7피fU3rpnI/=0StbXI96Kوk -pݯϮ'qbtOl5V~ql:7VC~'A⹫|3.?15]uY[a6]0?77;;77?N>8bFww` 8dcqaW[zLR6]@//Nv7FԿ(/+-ҲچխK!`{enb]k}@1#`ŵoGe`)ZI26v!)2fX MrsƏE))}MVc$Hq饝Tk{KMۻJ]awQW+ec䒱2š|'IRqkeK҆uYavdJOpog|iqanÇ33fW)z5ċBsX5\30AQn癸9>$e;-7W瘐{d憂>9yJ,U[e:)m'9/4- ش-u)?}:Q~pxlrjnqusZ/%! Lz,5=^kb'+[طDW|/ttII;֦OSsÞ}A@信44O;c]PZ{9F &뎭+sMjjN7Vfg&FFGF'fbQxC&֦qu2~i~44q*1cv,&mK}uEIQ~^NvVV으ҊW͝Sk;G)<Fww)-JM1g E:Gäc'dV ܽЂif ]\[Y^\~VPW(-|ĖiD1|<ޒ-ם\$q-~cnTL833#ٱ^0GO.,BE0Nԥ4֦cW+}|,^~֥oߵ +qooȟP×1"g!Err6:&kk򁶚DFsrPZcLRX.t3ĭ5FƆww6 -4*gٵxbw ɳCy*5ƀ.F~7!??&7 YQ6<"klg7\֮ 9a"Gmt}/]NSbf{D}ͯs{f}Kw ؑ"-x{ceav־:Z[[Z[[;:޽w3 -v_/·^wm7=A]?Y0 EFzjrrR󤤤̬’M2x2w7>0;=)>:2QzOp cɿ`x9fyALɯM,IR$feR %wM}Iّ1W+n>SK/hogCi!Tmr_6%dd}w 5W.]Hg#6!!񃛤AG(-m-oB?K98F#咜$ l_yW>l-/(b2P._ETWK2rC`ok}ELȉё:t;^[0 mt<4t182N^;<9mVvW+]ұ3#͊~zˆ$x'?떱ʲܜYY/^deW~٨5.W!ԇu+\~Pd`ȸ9r") HD7<э8чB. wA/?ͱNtԘ*qAUk$nܟ-@Og CU%EEyE%eUf7ses_42ͯ[u r_ۉ~{.S]$g&3}HJG m83>ބQE/<779/eiE*lcՔ#!"%wI8$*rSDX -FWOF淤͕IفK>"p7!Hudgwξx :Zѽi-qg_}.wJE614=\_S^R',-559ϟ'3,;o IsB7+d4C" ϫφ#9A/dnx@#@O1{2l@ \wO;t<#a4MʏG4ɡo[k^dȠXS!--=33"]TRQSx jS9~h!A%۩}!+n8٭krL+D5?MuQVJb cڇ{'OF>UXQ~l~}`Sж`2p|834fϼS -1I/^ֶ,ozKr{oގ~ᆄX(Զ,X_[YU_ - |MW/_wǫ}'H66XVoDc,*,|}r6w O|XD#kbv<]~j=^w2z,_7ߘ"HmU`{uaj]{S /;+2"gdP)uM#x;)d%}+rD +}<-oNFQ@i"-?[Jp_wGKC] (RM2ecTké_>]l-{0]֎xJ=mws<;>8fjJĄ8 -vl\|\|<%:9%-En -o5SՍʯJ@U jڇ淍׫r)78ݕޖbr!Fo<|g1 ZzFgN?}LS},㣟=%%_+C2x+;O|X-.3f%op[wn)Lrtmi^^U3үc9356B)̺%%q~u7^@|_ъQ9~m[r܀2GK|WR^{7@d}P+BG+N`q0 ،9<6&쑨Z uuvU5eanVFZ --yB<>(FΡпoΝCm"H͏WoqlܥK.m-Rnp\%vKCmUit: /3$'Gǻљ21)#tLSHk: @&cJdo}~r$?+#5)!6&Y䳈8„Ϣc'3>UR~S#)z wY{!т=ρvj'B%&3 cr@f78"ƸG:Q_{`yL솕Gd6k -4 hy-R_[ :ܕ=Q:O|:.=0S)Д蘸)Ҫξō׶üaJSʑQQBFQmtCm6?0Q/D&)hTBǘLJC.ºyU|saZYU4GFZ^lUofyI9T]|.ID(Ggy\)O7W(:vfzZrR"=AV>O㑕[Hǣ_yt.Ӟ'""?]o 猴 `'f8> 5xwuv -"-9!.Y$˜ Tzva٫ŭCs![/p!߅P^K#rů>-P.60b0V^=l4~w -gGKǴ'AqiyͽS+yI43ͫ>,N6P|8.jA3G: 6tиgTca\պ.[8Ĥc|*:>9#euc y(sl8 +O͕y)qג/JU= rPn‡}Oo[Nt|P'X{~v~9Oʯb+=k_]-/2Lyyl4Bk}*gL=]ͯ+r3Sc"#hN8@u7t3pTշW~-&bϬ -PjN~dT[L_cx%CqEDŧdc!N,n_ Wj5fsI,(y29u`cNL}WX\ZV^Q!zU50TUtq0Έu]MIH", le%/ r3Sb Jz94kܵ4o.ɰySmyq^Vꥩ!4V,q>F.&<3npW>iަ U`b%@BB.  Npl6|ϻg& sLfZW?}vXXXR-e%OѽpU'ȯ?!Q>)vR&^ڧ)µ;^_|#әyїwߕhpsAWA]ʀE<{醝G0rC+޵sG l\{6>G8@ƞwmN\\ȿ{ĥl9.Ռ1J],ZDrh -썝%8V-w/vLq*-0GrpH~S,Y( |G`fyL׺aP/- n[l\,:<\:H/'5{΂0ǹ?\jqtb nn;F[hn=}?WyıB0nXd>7݊,.")gs'/| ;Rc{<?A~mg5ʧv/9p˻ѶiGBPbkF>lY֐[v᛿n:\lF)V2E-Ǚ< aA*o#yt P4g Oul?o)nܳuD4;^~_^W[lbA#xDI)iBAi ]>࿸/ok|>jULzB[fB/?}P~޽rW&+>:h5P{ADǎvu2($X54A N]Υ B|ܥ\r'{_?p2N|,ȣYm8¨<`&O[3gN7BlC4W{ jw&qPQџ7| tl]Y A5YuUDɃMKC+-m$RHSӦcA.Xjö87r -\ƽKGw.ɪT3tйh_oJ.o?o&GB!h9V*m<-Y74sX$G_<*M}EcC,A.C;@+z|MkU1vt?旖83ҬK8A<@޺f춪!AfEjjb 3wZц ->t -7\yz[WfzcW6\mf[c -a՗37lkLf:0Uƒ_ FX8[nڎ<׌xFۗNعi]VEMqG5h5Yuݠ8KcqjJZń홢%_SRa+GwהTm/q܍/^yCr\VɂjVy)Tԓ3fI8 scنguE -١uKw;r?~AOX,]>s-m]E<!_޿~r7nC"D؜z: ]bٚOC5I~+/:$|э"t]pA[i5Q ƕg3E Bv9{36`meZ/BހyLOO$;} I@IǗO^=sXE]L;N[O,,BHn٥8 ( fˉţ;)0Uǹ睠QC;7>݊fu_ak_;/YUf[GSR32"^h]-/߬V_S*]S?Z{DShFhf%5r;5EO4V5*v¡/9"i<."M=;g'0V-3٢S9~gu_F+dLTq4e UR -&  yrö=G^mc ر~ikRziŻ/`Q4_cLƧ8]ȱ=-ԗNfu-Xf!=2# -z9p@y?ްD+'d͐{ &V~r郃*~|ׂbZѐp ;A< --'S.hۣoY@UuCR>'5otqt| G7Z8GEs'M5{s7+ j5"[yw0p;wQnG;@vN[#hBElԥ{NǖNvCp4=f_wRrdwQB|g5 %5aB_9t}0ShٗreHx}~yltveN6h{Țݓ|"Ȏo{ީf>xN /aE,?O_]W `|-[mSΟ=}thV|81'1PpcV{N:y}?x( ~hrc=gQG5'R-o[lX&].^N")iȫv^gye_bf=)\Alp' - (oݽriZ-UcRxBPo>a9K ྋȎ}6,B:3ዥߴ巩\ ۗNH3c}%ڑt<m}܍/ZV*0_?zrϦ5&5U޿zxmJ0*5 :ut!P H<7e%5r_[iW詸pAa75)Љ(/D$WڎG -QDIhg(Љ(˕ ?qrO^B}N(GF̏oqVw[6sn;4 wo}0vSyM2H XNRXH1A2q -ٰdM< 68tЁ}r\rU;U=0E¨&aMh1p ֡75)&Q-<tuYӥ5J򪭣{; d!K/r|^_Th8jp:+Vtc1hAPDu9=+q O1+_#9<&~LWf_DfIRF`x?AZk EAI͘=o-! sAK6?5Al|e$<ֿGj? 6,A츥D́m]~_U9#8H>*=:Q *EkKxyLTbb)0^/".8pmGճb!OI?9aJ<~c8(ǂV o8?xlUQ?0пoͲ8kaٸ\1҃?8Uk3Cr yI#_? ݛVtջ#@NޱE瘏{ ŽKGi\YWnOP5&I3dT8NYw/(sl%rPOt"씿C}AI6%'`uM -T%G.~xԽa߷myݑc vBdvȍК,gʵ׭]laWAxRo˳e_@j1$Zb`-C5X?~r"A94=AB`KON>Ga g@'"Nr,w i(Z=p7V)_>zzhG.' 2S ހ2a_8D\׎%RU.t|E\$uqIR;VRzl}G+g۱tl_0mk:1=i~W~btԿVK͜hm37 AN:(I C/MV̤z<6xrjBp5ZQC7>(WcLʯjT0x' ( cȵ ,={1BvI: N -;AaGw*!HT+7.?48`ђ%{u kt T>oȲaxW>xA6&JpZ3L[h=DY>^9HT -<[(8*wbIwHHϚw5 tzp᪍72EOc>v|= v;߻O%,ej˻ܿ}RŢFXf!>zB @P~4ľnX>"r樗V)j*Oď%5̺Q=|*&Puȶ@c-ǥS:pS| |&e@'d@Z* C-tp Aؽ&EĎ;g8ZB^f˞5?$(g$ urXWHy Y*:O"mbRt6-Ϙ9K/l*Q6<ƑZRV(Ȍ Sg]}2̯DTLxֹ;qu<ҒҎ 0wɺ_i9_x:7Onٷx[=AE8';#e"ge]NYҤKf+XkRfRcMGvP7As^v2D@-W{ŧ(rHB%jW,Hw-+^v2D@-W{cY~Ǖ-XBg'/4%l/(z~BMz rf0JgK?`/fV.1:dp3XLMQ᠛4y^{3u!*:=zDѷQ\a>FW| -UZjƥ*r.ŬTN n>ҽ0O-(gk?'^&N%8rLٽs} {+44YCEHVd\"l-{Ӫ]T5)ܽx'w}"(Cx>@Ma- 1FH£Y2楀:/q輽_G1Wҫe&!WOJ`xS){{w>w뉽~|Ҏ4=lW#ۘcSXLvBW߽Q)f;cQ# ~햕I&^(; 8; ovQEJ)尌Fx9ƒWBԞ.C*Wc -қ-+kգ맵>21j4f꘷w@ k:~cq`(N3ŇQBl9d v74Id{+i4Gf@iϒfWibu":kMhbϤ^l\Sgo= W;.۰Z041'NQi#QXjU  !> =\B67D`pkLi<] -y?q2E ,)3W:)yM#NN0ji*l hSgtQݔYc`{I)P pX8)IA*ͯ+*MXXkw8 iPS-꩗4:Zԉ[{4QGEX+<^q7fn ˧}吧?jm3zyrb 0ލZ706Rx]H8&ddJ;IZYwXr10vܲ&_$U;{ "IЌ1Tsv}\r:~y, $R5?85~tP/،OݲzaETKt[ -Gfl*|6++"'>@\^ orpJo$6*F)VD -|B~yա+Dnn00+BKoעDX7vL{WIl/CW6cգP80fs\U߶qoʅgneQ2k,w*5\(gBjJ"eC{bCAފцR#y÷}PȣU;G2@R@q]m򂔌@Nոo,fws\fknۑqEdbуJWl\غj5g y)9?Ú_vdS=W7>~>PR!V~d&8>XE:v5kRw=+l1}IK$M4IŹ$cGO mz l<z,%vEdkX+%v1ؾ+?oOټo5Eh1ISR?LRX]UHo_J&D92u[;w5 #˜ -S59@4DFH;}湃LIQǍk]G+_HW&m f,u&(6r2j nkݦ@IKOQh(*:-`MM4]ѷGt G. I#@>m+ ^ -n}DC4J'rhd &\82`wӴX~Kz=7D5AAwH!U޴b#Ɛ9**vr:yL97eU|+[K} 3׏o_>{H5˗ꘅHڞlj386 D5j $ں>seC7XU" -|\\5[,<_gضG O7lǯs! -R6N۲؉c75RB@'\ Ո|ď%!Ug]~OwIm-Ʃw4rٹpoP,M\;:Hdz'z̙Uُ)wm~uP!K65KRҔʉbSQ 'IS*zbsl>ƅAh{ :%r 2Itd(씻`B1{.ӿ I.ܥnӑ S $Z.?NrP.%A6}Ac|E*wbO/m*'d^edfdxMK̶<9%fV`34F %-u#Zut#rac5ʩ}xwc8ɇWKCR |s -]7@)G_?{Ϟ8o-&L6md2fرYp,{0~_c5- ngzTIkחO5O쪹Z$8g⍶w:~S5i\6dz"Y*#$ETscl*,pF!3t[PCp4 dE'3j58i<|ҫ*JIIpɝS;)uuy 34K^~X*T@⚈gccL䋪}CEs%*Cd>pU~7 .*.UW'7~?op`K+.i2B"L2{F 2JUw[޺<ӒqsnO0G0WD%Ce`>8 ҶJ`Wi%EBAg(`}8&G0.ɿi:hZ-d Ea,Y:S -laP#;Gۨyn?Cnnu`k(]zkYHJn!&Ft/7]v)R`KUM+X'IpK|H8Ǘ_XSFq/'M2!%2wMGe_.chʬwth~ '\+~lKU۷w=Li".}$޿yݛW.>vxmz &/]`ޜv'O2YR$W]r)ۘoc:i#α`DckrP^S"}#j-~߫iCS<&DjLCJ*^зDb4F`DE>g -Bw ZiK$>vWm=pU%W}#~ZcTc dgcvȃƷkYǴ < HRSUVwP]@: W"}s->;2tsEwTMբ*RiT aB{ď%y48]BE**!Gq]l+p%i+LFsVl2X ǻgN)c"_]g\~X#~BϟH$_Ji}"f4/85NY詍(w^,c8 Ĕt?.@9.#qkQ*ypHpFFDϘ}=pSDW Ij I SaiipKL{1MdOUk.1/u (kkO<+B>G߼zݛ.;udr5{kldoYi >_QRnhMOtph4$@kP %^޻xXT$ZK4aΒ KpaazGG~ 5.#; ouk% YSBgibȔ]f沭L]},"-"aAm` Hb6XhųWo;p+~ RPȔAZa-K7 9z|BybQZ6-CHeAeL!A xu -P*׾rpEtj4 -y?Krw@¯`1 ;XRO(CC[,u;}D[z~bz -# ]2[?ߝIvk:UQئz> #zKW6I+(?V~T  y}|aL_>)ƻ{3Gor:4r2f*!~|ޑ?rq^wʥVo_xׯ ܷk`KߺUK-r~k$2CG>]2g>N1 -f] 5D аuшxS8i,e!& { -#yܹh- -=1Unh2'KOa@3\ T9z )Ȕ Sdz*W}'IxԢTGi;8/娔rS >esЮLO_(1s |nIϾfh4'~,"@cW<\qۗ\xLuܾo%Ēe$IJzcک"S43g&ZQs^Y ZM%=x̨ \g9Z[q!j.CzdPXU1pQLuYRt))\@41Uشt.ZlScjF ]H:o2$,^>CbVB JrslT2gǔ"scxpmU(9wRȸ"UkD@&ۮ.IU@xvdw->}y];1-!h9y2e*tUqQ̮%fb$L,)mz+'o>ŦX;n;M/_O N g?8y;ۦM4A- -?A'5:c[I/ W q* '&*^C`WXP^UmBLm[әWь-0x\|^ y|+ i:w# -[JVp?UjyMobTGsX/weprp}!~xnU  y]3 B]LrC -ؔ3*s`:%blZj (?gK)ܯ]+l%=@ tF .y1dg-v3 -e;-cKDZ1KO1% P`v8<63O0^CWKL{5 &W"x> -)"n4{N?La7EDޝ֭қfk=Q&[ yL\Al ?F:fPc<~xXBoΒ;=HՀhi:𚴘^Ch-zC^]fH1P.Q%}]A@>F*`{Zr쁜uP)r+̬J5cTEm@ǾT9:$\cځ2trU,cmϝ{)TujPSӛA59!`(.E -ubk>j/p/RrG#wv*o,5:3Em"pg _?ZK:Ԗ ZB Ax~LрLp+ly@14!2u&r7"J/d/uMq)-e uuD̮VƬc=@ \ 8R! -HM3LvF iSډA9QGZj2*C9opp i!v,}SO%=;( -Id$ʉ<*OIu`l#OhZMDU@%8ㇷ+\:ggަW \/d S-9hwо[{,C"c#F a>ղ"O;3 @ --aq ?VA>Y)Q2Ȓ{蕇Ry)q^|܀Qv}42b4+3Sk@>?0wܠ@^If/5IV fHN51Dct0O3:oQ5OLyiN9a*DHA&Jq ݴx1+Yr l3Kg&qǶ>WǣF\ - >rj)!;~ǰ:qLFi䠞L_EqFӚڲ__@.[ ^-fk^}wc L&HW߻u#CY82y,NU<,($3Ҙ`֭%JfejP&flui/q AEFh -<}ls!XT)\)z)\+Cw)$B8@ʽ -+7o^| -W%o -.ɟ`_A/Oi %詙GMJB墉㲓^ba'-qt5pYWmdy4zrҢ`=*/y$Ǹa$X - >䉱|WbwǐǴxv!](Pu.O*pU-']*u;2 Ut/@ ɶ.bՀ-kc=Կ4{"Blcc篮ָ(!=+7>v.@.W? u;_ ~wo\:slhҞ30'F&\SejUŬd[i"aK(d-z{wm]x>Nk3k.diZ Vcb7,?k'7k-พWT[VԀcȻ)"M a6,p(e䱙Yl\kr*HQvX!#3/|e<,k/W48ߵѫMz$C);4Ʀ&MCaQ|[KWp;Wӱsl7$ng([p"FعTZDrK_/}KW[+3 X6Bs}-[Bi5`_>_`@1YCn}U@??s`?/_d+@W<56IPH@4x:֋C>|]R2b{BpU;a_ -%)TNsy:nv!*  -bsQF2A+?`Uh矿e#?x٣V-7{ƴX-՝,z1jDHǼ iu#ˏ3uНTfcq6g@4؂J4m@GBc*Pݮ-گ IDHk{kV8F2QuO$ρDn>ŮҰ̝6mաfH[ -/HcZOFA\%^0*X{b)4}hzyÌ X<])>d,@1b￿k*~ $f_aЩ+:[9Wl_?{ 'ܼnjIFR# -Ǘ,˷? EqV(c^ -QU/)Կc4p -').0]/?qj[0E~Z]zyޣSH­K|a#O'j>k W7:cFݢ[YX - &D1P 0WGvxm'hS)kgef/0ʡr\My6+r\@O]~LsX#'6Z +ǵH[XQ!>'.;)Xcq.VX<ք;8p꽧_miS)$__>}3wo[gAd3c1XL,'M1/fk<2c Tm?ſ(<΂a~)yl)tW#XHh*PUvKc#yl0ypqUg,>HJR4})f9+U2VJ)P\a^c?H6qcCX -FK!;6Fc66n \ȅtF -W!J2dU:t"wX~,s<$(ߗc(=8sM5]G $y^L?v\Z 4'5Yѷw!5G?{۷wD &aBI:U`0="ģ$Icjm9..Y`H,8tBe'FɍӚX!E'vjW(m y jQ ~c76s1쉮s->PP!ٖ/OxDG"wv -7fA|{fv >mפ(VO(6D6nvse\Z)T1OJh7p8F^c! >^;p62` -Y8b䮞};r_gtxц݆}[ND^6~8 cmƼtpO >~wA7 -&9 p?o?LyyӶtQ鎮}RDsG) -@GPm4X`ެ)Ac~Mƶc!a(\+@0y#}ǃ__޻(s s6}>yj/g: Q\' Dy1t[yl;jc(4dRhGJ@Ǥ᜿z)ccho4B&`4۴?l+ gJ -#fx'#Wr$h)ʡ4h1 yLxd[d!`o'OKO^ i4ݿ~b"ΕSvnZ@jZLb>LCO"D40k14qt㬔ifݿy%3j!ZKoN'tG9_W\!f}v WCNW5{FƟVl?x$[41*5ߩ>L9Z'Hq -XN~[gmv3  y5tGj>@}Q򘾟>VKv*c -dʿ5Ԟ>Yf\Y_@p{JcSaTe"C pc\=x_z#cCXe?bΌc -c F34|%y|c"[JQ5T_Ͽ}|sfiuYC"&5 &"S0H䱦ވx5ޟvHwL9cXMQ7<~#ycHW})k/s Vϯ~ -~Hͫ'L[_<y զ}*u{m~oTPB8oʔ7<61!wUUY~fTek<҇ T@_j]c{c""2+pAXd50 Q1B 9;Q@$$_&w5yK*HX*svu5HH$PrcJR )Я# 6y -rr2yܴ@B?q-^q`k0?~IFQQ"i]@>{hgߊ36)ᒿ:K~BccuS$@>vy 2 m_c^[c/N~W\YcrwI'y:%4PH7={MWO ":*1AJT' ^mgJ}LWwبSAP ">Nʙ[EP0dKcLM&y/y|cY.]K8@@aID -X<&xgl&ZKVq#ތw(mݎlA -C[rNja# u]rCç/|$&#).vp"{ u5ߝ ~9f1>V]Z$s!e+UVVl-fB@p--!(?ʗ<^9=.TflG%8:;F -#(%n>~Z#˸~Ng!fUӹ\-4hfMA )gu-gcsW$Qjʾq!Ę<ͯv(!|=BuX'ȶAJO;pUǞ}WviG$w[` -"O6s╽<Cx#Q?O~\>gsfjYrh ď]г܋rsylcy<30yv{ @:R~W)GX -cI /<SEBM -yZoOqbK@k/غ.~=gSN3<a%];1X-F>Sg_!(lM9+25~jyy ,ͯF<\+@{ǖ#`"֣U#!oQ2+94{ݿ二?Y$Bf XJgԕ>&e7YL{b X'tB|yiksAig=qsz4 >z>o:QĐ͓[ڸb> YzD+&u9~11fuӸ R|5'djPK4( 6Us -)'PALֹxi. A;=mֿ;o 6)anl*W5q!)LqS=5_ ՞rSv֠6oM%œ*MꬿfE5r~WCbw`oZ 2j͚jsz-P2¢s /_פO~t?OS0J5o}jh#ܠɗupV7gDL f 0SMTd wzjyYH&I{h>)L1YK!]hu! -^}M{ -ȲEV1s&Qh>V\8sJv8( ݫ3T;N1KT~P/𰇚C+I(Cqjnq1LT;Qˉ}F/@dfDNj`hD, ZFh|#1lk*AiE{O#$TdA%*S/< -uy)y5ԢFv䀆elXDL? {fX@r8DŽנj+^t7%22+mvki -=Iw[M._nm\?`1\`K6`lл٫&N9$,\wVֹP0cdvMo-$Z: DfuYw뎽N|'/^ lVrLIw[G m[k& dTh,)NFen^&G Chocp'P Bs8m\Spy´,z^5Hp?vrPL%L򦽧`a&)U?FxiSBZ>b6w? -*<բ=er7עkL8VL|i ilcs' g ]'BF d\ݦ$EzGjn -/3kl(FaŌϨBH|bx|^~|U  &!8&4cB,W *J|* Au8'Mkkݳd庍>~b*?"wLQʱ]o:wh7CVcS1ODW:xx]Pرr "M'Կ&E}h1L,ZxnJ{uq=Ep@Xƭn}vBsPjMs -lJ섭+~v!MZBnxx*MJ̟5nTw䡹z6`z䯖<~h&Q_=6SzŤcS k~5RgpIM(!rW;F '%)JȡKӝt#ړm~5}Γ\`zЗ&ODLA!wo -]cD0ay,^wБgwɊ%c&㹖lYt#I`/\93%_V!$B$Aj0%3;sf bzM ;^}EKd{HdϜuom]oi4t=7l$?)ى`.2Kp]3m`I|Bq-Q -iU$[RT*O -{~(:Бq߫|HI-тD *a^2 -](~lx J9nFW31⎕|#jh"anNt4*4SWԤ\GEϜ?ŗ`?#֘L͚bSP_=^VT:wuނTkZ-~3~4#)|tdM1{C>s&B\.f3B7@TϠR.dCb4H8b%06;ؤ;A`2u7x78f?%േ.lkDSŜ\ -w7 -ә?c@<l&mЕt~ '4PKNt)+7_| WNc涞8yY^<|+o{$$jSyȩbE(l$: @jۓ}WWl Hq2h7lպw:B0[J$5.i2/`T}|] +RzfXɝMY*fҜjz%~M%1X*L\-qYE駖f]SHt~eW_[b0I෴)$7֥#KčGTW/޲ --ƭOV߰yCPNv%|\#;:$K0w`KarǠ.7Gt(A%FAï`zf4 +Ӆ\ ػKOkּGA"[ Fҿ4}"e;f`6ԗ5 T&9Z;pWq0h'r<6) P5HWp;cf3%W VwSy9AsXǥȡr=03D0N-G iBcsP:63Nifq#5ٳ(R.yxîWZ/,|ܐn箲T#)F!J~}# -ptTHO=s gt)VkKoЕ=/ޤ2`>„+[nTE>'%?A}̠sDHqmV9+V۲}GNx^J^"gy1iF׮ 氮4DŽݜ _cO-cb2@'ωFtϛtVS.ALUeҺ>Q[~!ֻT&mi8wˬV@ݠ;: ,*츫ߕ!Kl%Ml +k:t̓k_F^H@yƙ}[W.`(qQ) ѫxhOomρ- $Q"?Ν<=Lj}lXe~K:w&2ELx1s+%lDB5?vsK$wY; 'd_ι# n7 MR2kŇ>vSZk=1wR]<<!Kq% w;}Ԟ+fUXj0HYu'n߅ldž)te|Wbh)Mne8$ܯLU!m9e W <Ew &;MCp4~bBd@ ;hX\#l ζ~ŜOPqXmú&p~{h#G2qNܞeaLn"'voZ߈+UE`Ƞ'm`U)M_R`Td4 -@@^%$Dc'&S$YpJ΄"ywD1Ďd/u`HVvVALig -^`LoM'1#6n*E -\;Mzt9o -ؤ(~pQ< A&(+yk]4z?w/d2ȧhpɳ2yͯ,.0_Q!Vȅ)̓7.3bMiZꐢ7>aO-}{Hd -'r<p)"w1o (NPy.8(2SvE h[ ܹ %[!: رBxn;/R4c>-Q&bdygvsxŚ;s/|oOx\g=a|R 8c,`$GtcZB֡j -.fiAuv,/*B.v4=ݜ{7ںx ~/Zmా嘦9 Dp:( -B1CHXՖ 'zq o^cAFR|Gzׂܼa2e+t=(46)?2zyΧvbykZ͔?8ԛRwG4de ->~ˏ߼$:E,]=w+ -$Mϵ\ъL"ݧo.\ %m$'7bXP 7L'BYt&?<} -*9PSCƒͥA|=]t Ȍ M ދθef>Ɛ_c/_]QI'Imv2F@O2ws~JX9fIPTT]!2'4n3C[tT3qL=d}X>PH#ڴ -M}pŴ]sW%Z\zmi^Ԥgc~䱷⯼}\G: -tkմ t@^F jz^Nā3}xMH~ I0S{qs13?*Aw v@F>r7a.PʆG.~8SkkUHOX1d -1tk'Nnݳr˞AJoQ9A0yN]z *{ Q86}l:]\A!م<1=(^$ljt Ps>&h*ݛgǒ>Xw֓sᙄ7 ؀ ee o/)0߇0˯aKXPGG*Ø06cƌӦL -FISD"ۼs[^wRg2,¤<$qYYKuɴWi$"Wn±+7>NqEV*Ͽl)XT^\ -A%6d 8N3,^GT_.7#ʐc^W71k델$L`E@>{1Kv]a!ԔdwwO{Ы*2?_+]$GVU]3Կdܤi7V<d-$Z{>cVɍF"IndUocn>| <'_ʊۤI j0j&Ti@G׎5 -a8UdWSL @:ב*zZljx"׹K6 O =VcJـv,XmM||;hܸ|>V* -=ǯ7X֤r<_]]aL" 4v{ ~[@{6\ԛ^sٶԵrJXNR D`ӫ*0yYh@?@4j"y 0@g4y||WHJS3\ 6#H#wl --tx>l3?Ky' 4Cɤ;Eh@wn`*#ftڼ{*q>FmSEԜ鞅=ȕ?n$Ucȵ? E-HlԄWݼ Zz>' 1;hHh5}\x}/6=A@ ]XyrBt|ØrqNc̊MG)R.ArRE~zmG]VSf̞bc>τ[Ip;Յǫ<{&̟ ~{v!lPHc -}A) k8 4ߦ5֔ՔGl+ړEp{WWY~/xEhW' ?yrܡKm"5*m:rEǚ%N V&@gtYdիW-19?)gu_v˞cn=~Pkib{u%w&f)Ï܍rg_o)IL fUSy&E37qnlfI - N_ V#Ŀwr yzBB[]B -jϟ^lS'F!2J(?M*Rd{xS֝ j~|}[V/ꚉ%4NK{\C6&%d? Bsklݺq݊Eݦep!4ҵ[*G鳽@|&9qչzO`C= A/__?ek tM+G'|i"Kg[O9-3z?{Ѱ!*X.l\ 1oEijԴirBkގ1p&i<VǕY!H'oFێiX=ȑZldU=/}FǗ/gӚ3c+ոS~5F -ժ`GA#_cgZ|&*SRPN"<n_@r(58"C;lLkKE$AhE$_#;$ÆbMH<vzYbyܖG8/s6,?,F -"mfgϊx-DW ;?$fb8AtǏGYoN:qd@rf@2}|,98=v/^<W} >k޲>9V{|xcrT&`Ҵ]=6 zi.//9wWUpD;;c[aqp8ے.~DŽ WmuTykǼʗ߮n)|_y!^\o?@΄}l*yp!lnxStVUT޿|pIbk"llT6o -5(?b -`l -HcU'Vnغs^|^ -M"AtڼWȮ8{ H#ywߕ)Ԃ1a򴙝 Vd 'L2}f܅mڱ7>xpƅc{6jP{(lEZWzÓ-R9E~,u@0+|{Ӣpۻj__dQŃkl_l충G&(fwܸ𹛰J|*U` ih٩O$r -DJX8{hg -iMv"dM4ؾHŵWV;yG{hnޝvoYc) iTvt񊾁gu =ˉ}[YPyl?|qr5y'.*6c7*9ԌW,#i5-1Ш@k&(F%ؽmP~v#vU  ?WD?8OXx!&\7րc s"뾓=7K!`R/^ŏ-@*YtUoC.޼˗7ذD05_$ȉ&/-l!ݔ<}1~1BS# -Ng2y<)S!k3! /e[85\|&K3Lq +'_:YM5H|};3'ROO>iv^-KBBy7QP,pt&چi AwQ;%tFfD=?N0D85 Q4?~|MM/L%nյ`N]!6?r@fWWUEϔH6o]!Y$]z!|l ^ɿ͚͠TW6 h]϶*^ DT#$@+V O#&~\);o"”Lơ+&1riA7/2hJ]v<%'Iyŏ9h;㯮a8wx+_<Ipk'wo\>{ܴ+cb~ȲQOO\:oe!O*#&O5q*Zn;{ع^fUܑKcM1=_I*@GWɡ ˰IOkpxF(nRpȶ*Ncf-KXوzѺ/hX8ܴjќ)+dϜӳbSW>{y]ri ܳyWWU@4VC.bLcT.PZV 'y,O xX7ڄ}^@'_L/K7Bk]FśuB_)?\y,yz֦_Vϕ\^2`ڴz\ XS.n>cc6CpS0ybb+*%xy\媁ߔs#Q}._?Y/v( v]{u$υW+'a;+wCLQ04hPs "kJ &O9{u[pkw!3dGCK9>f^؄<JoUݵ`m/ܸջO_# {b\Sl/;>Fp!衴G_yk(wL<aךֲ ~&b|-Ƿ.ۻmYӧ!'i3f]j#{XUb㐆|>ezݷ|V=f XlM;N%s -B|b=s8OPCNaп)\BAf&wGXԬBrIu#Ŋ#a'8Ѓ5KIV'G:{ʍ;:|)F-f䘕ɍd5ҭ8ZMؗtIzcT:囑ǙWaI`(R[̳ -\3RtO &"v'TQN/J_/0.SŢ9orKrk3Ѐu4y͒yf -A)hNcko>\L'!8G/XFbgu5Wsn>Wvzn=}9o;;#[u.KVo:9U0[]+*!= @|>h8u T{DǿFc2K˷JCp}|!bƎQX7\jD5DXcq) "Wy"Dq?屝i8Ha"LZeŸr1`_ܿ~0v|BX\W=Ub#q}nr sbfvt,[q`ӗn>.Cw =rg(_œ,(#Sh~tPra9z뇣+ gk bDJA=9ugリMxLU|;6^2F huYn#g{aD}irzu+ʋ[\2E ~M59P@~hSN]u^Ni# -<8 ],ߴӷtj3(quYEVPaYՌxP:,Xvˮgޓ8sW9w}hTD?cނ]Qn>4m|02??F09h QyvSCĮ -' pGl+w`fge!vUH?ᇗsU"q6;Tߥ(x~|7 =Mvw̘L* Y4Y'G42AV PF1}z{C'/~׿' -[i)۵8ո8/7kߩeK>ׇHxTȊPk]ܪ*HM"E 4kSe -Pnaɝ+ڼvYOlLd [ 2.!żūz=wH%\I\%D7,dJsMC}u슼+Y*&Jd6R8*P 7~ ݼibKN5HX~/w![^h+n!O8ˆt)o^<1WR@-Zj8.)mY?zpUӘI?*q_Ccd==ԡEp<+/߸8Gj!2סk)h\ewx5&3%}v=['0I(9X)n1uf ;5 YG&NS.w=wՒy]3bZ/^0q8.N<k7}7>+q\ɢ88N- 8ZVsU5P$]={t[l^ep(Jrö _<۳tM[bXRqd!{S_l6o ])VZRNUv8D*cX3JΝ źfCDz/}/z.©b4<."Z LWhce/sczbB D\.($e࠯@zB/#~A.(ѕ6b٦\I="QK'ڲn9>Y30Nx$ۻd mۍClibgIk>#E[qç/jI°α[,A -ՠHinء@0aUd[<^oL-*_o^8{%(J3ONºArL0JdbO71+ agoܼFi5}u BS&A?'*LG/ycJ9BIo_޻|tWҹq@n{aM4sq-iᴡ '4}!c\MW3yw "])\Eun7HIWg @AH4&?7ؤ;0DJx0 /M^=sd✜Ն{TɏXRdMc4s"g18X,g_}~1 Ww_}괩S2>`ɓp&H*2fJHx9Ff}ţ[O޳}m$>u @,&NLَ4^rC_{kRaz١m:rNQ1?:-#M%ǷO\sgu@{DEn!0QqcW7m:d/UOR/Q>.8.[8 {5$1'N4YK)o1tջҎVf&UwH(E5<}&DA3LCa4x(?aj8/mס@G]Br( `CjF~Jt0A84W\;c44Mw~7Nܵm**t1+؈uWvT莮y sԥ_ά&~C)8m2}:Ƅተ}f]b +UYծ/I/<t֖D_Th]~yms׺IThBL׉D$5p?Ei=:gw3h R uzxμKW4`| Aj&(`2{C_<9ٳ{q{ϟKH_:iO];wlhז ȾS}Q!ϐd%ydž7[l]Y3ڰԧ頭 9gn{{Wxy|5_jyDUFŏ }6ETx!oE8J"; U#8 )8L(\}n;Oo[@p='/߷؅M穀>А?_Qnm.+HK>@ttTTTtttll\|BrJzfN~qEmSGl$ޭPoNZ+r";F}*AA6K٬Qq"8FA -l\7JzBY͹1!1xD,} S}namk,aJblQ+#"aT0#bif]j8w4;Bߝ.uf\)i:\>L\.:mo/0@DDoo,/-.,ϥZ:Gq 7ں!H#cA_4s3j9E@v~҈#j@oWG[KSCCShtrvimk7Mfn=4.r [6H+Uj.l|Cث!W -Kf3T<d(St GA-]25aKaZq pa'*7aA50İTC8ð]Iﰽ1ȄlAޠG{m)k#hjk*+*+***+kZ;Ʀ׷Y MH%V_'y<.&)=SJ`ogsc}uueiiqqaqY\oj}=O\P9F8 -싨@l@yEC m//N twuvtttvvML-np LtDOW3T8IW '2oAR -{Z[E禃W֣CgAcesRP\^{{,W.kY?MuԂeS~ܠ}7[+s3#Ã}L_]ƙ嵭Ós-Q6 -@Od{~=F=VlGǮyL7\xH:yLs)9B!@DXFI魽~lE*-^GGp܀tՈpples< -1DI/[0r}}yҏIӥw >)՝K!եɱməyܳ{PSI9F -f.%LZ~ҏ|8zF%f"^ҘChkI<܁u f 3+f JVcF$fpݹ:\?QowW@w)1udhM-:OБo.NOvww]8{hO>2w%y ׉xa <^nCO'E}bbb|brrzfv~qyuck*յ&&\:ןftga =6(&RlV\77H[@n-L Ұj}{G{GgWOo -'IGoPI <1L>қ -M+o -d<??;:@nq~nrjWqqe}k{w뻡v.+W=]Lv71ORcU?>y y4@?Ĝ -cP@`y,cE!mǮJ9TX@dc<[ |4sy[K`"t8؂tm Xw}1 iXsAGNn`s:4l Ҷ(˳#[k[_;8F#~}AN\ k`@#+wj<:4(ڿpA:PHJQnH$mo-/Π1>FOLMSeƣw0BjXIg<$ k% =$3mD:~sa/Oѻ•XxtrvqusJi_L[O-6!ܔhZيǤ+V0oSU!s ͅu~\FArPƏI@px<&e!p:8!,0ʯӏ?,@AΣB<#Á|bH PƎwVWU4ۻ,I4݂}iOz@B4Q=EħuT) -a-MMMX`opd[`π<]2VJAgf!F+_ah֭c %=+H& V0,0ˏ3> ~wssyqvz|||txxpxxxtt|rzvqy -Av %)xzkkn>!i)lzP$-HTaxA?J -@{GxAbw|PfO\8ϥ_lh9=NNnoqLWb2:ȱHu-?̴fp*9njGs!?ՐlmC!4`% ) 1 -{ X`LǸe,P"YW ˏ&.=#Lhxz'Wa~t(;։1o=t_pw9w$7 `txy7 -c}Zwrm\^5!( TdH䫳񱑑0[Zـ&:``?= 2}{i0 I)2 iU%ZrЬ6/aJY`~̗Pm4THCAqfU S-ZhxBzy/ Ť%&  惴_Y7 (Ei*z ( I;>4f0O,,.-mOHcƤ]hoChNȨ[G]OREf}UPjMў~ 3(y.nam, GcS@' nwOZMdS;+i[UIXwٸz/T`D6QO|B_^M'yEqN.kZZpsC8J+ -WfgoMyf=Jd3=̕ } `&yLrzbx^G U "Zc 8o -v =K;1txaB S1"Kc;"*1txaB S>p) ޭCCW 49hyv-⦥f zڠK݅uGcģ\hs+taJt JG4fvM!>TC(.:0|em=~~Vi`S?No+QơUz )((@AN;xSM,֥c텑ꂴpT!м+z !drQIFABQG(&JiWFwRס -&(0RۆgQ\?ĵLutcN93ALV~,7{I{;IKzݭaF1Np^&?UDǰipB$ۗa҂.?7<6 Nd[ɧ7+@R ZGlD )Z(4QƗnZ4ʈ!\~\!`0(+~ 4T.YðaFyLO" !nA7<^ C#]=SFp> -wƝP> +*(  P ا 1*)b!NXNJ#Tq}.,Ê%ÄG>: NhIrG7&=OmALNJ8=vX?EܛC_ӏYn^F=uOoQlvR[\+A V$>v80e1I:'ֿ,1~ʍ+Mhxi4"ˤj[ޔnUX04>v8|L<6H/]&WpLREC$:G=b eCBG?5%}0H}8"nhBnکC>1=N% G>.Gz<-'X{1L8(+cԌӠ@KEnr jm,:!?e[r,?** A+l 2&p@&XktPܢ 9/@ V$Y pu97Vyꥪ~Վ)ndžc`:&q]=N* &64ipqqFQׄ΍ւߪtϪ6-T;!M+nP5d3G?Ll#X~ǐl/o!7L+ʼn`-D -z/*cC.b 0W/uT%{(c<)G Sln?~CC]H+Kxv? >>]4<87j\R8A2]K8 /O4~,jRU><~Iξ;bwL %űκ Κ7z,z0uM@ FO{l܃ 'ocT@ -4/uM(%lACX/-{-cPsT.DdD - e?xi8?_~U11.[HYIɩhʂ!+* ! FM.?DO - - ;f Tr-?c\]Noz#A0A38}j2X4_#" a@FH[ԡ$5epf8ls =o7gK4蘕ER8QPbg[Pmѡ#xco-(kpdBFq]b@ZhTE҂-(q_O|oe @h"cث7g" -B:yT~p,##`n:Z?%!_<%GF T7=cm% efL8x~^K4q56ؔfL8- vsFXP10.~E{ e3&, i%fZeež2WkS=u0WS/ًkRl:X=`փmW6 Ə!¡Gߤ!dXN |g@uS.WX0l7&%f퀬ScfL8eG chֽx#)wbeWkv 819u}{<\hOE{eyb6x$G zPH{cثY_āXngaXW髕ӣɵK܃C fL %-J|+zܣ$O`ƤyaPqO5ys{L*iXI@ fL %?ޟۭ`( 4CP j:FwP*4x{i̘4/S]׌GىNΫ`͍o xu2jfQcK'7Ҙ ş楇}BIĴ#5 @[%#a.{ V\zƚ1nGO̘4/=J"1ӻBgJʖt>p+6ܣ$G`Ƥya/WG5jqϨk 8i< wR<3&1Ə1~ǐQ՝:y -Cצ˥w1{qL -F8nDe1 ϭ>1}r"/'~MɭG>.IY0ޯJ; frZ#<Ə1~\GBv ]h c> -{E"f:u6].S۵1룍h7bZ`uJO!eCā8{j<@u:M(.؃4`VJx8cAFBP F x*p!Y߯XWnzq# 6&k!T -@j:ۀR~`Џa</a/UmPĸvb8&btm3+5º)`tu&:ۄ=hA1۫Y?v 2,ֱl.ȉ5Z O5PmJ5ǹ}E{F7MS٨JX,5Z O5P}y֏id|dubHJMUG btчP}(x}9]i*IQﯣ&7Ck(&$hЏ^MsĤ ǸY\B?Ŧg1 tf APV*ZZk;@Y- -h^@f=1٫=>_MJXu!da?[~b8Ix*f:s?=13TλMOX~"`2sBG-V ͮ0+ -+-YeKьO6*͸cU(0a(?1!~,u@#(,&9{|qڅp8L׊!?Y~hTse:x'^mBQg}ZSgqTRvecثus6~ y14 ȤʎT).l!.1A7 .xE1=* Xf-GW|w"yL#_,5byIŐ3t` E7Xc ̀5_T-̨8xP~j0zte&5{++ljHqF@fŪk,c>`w@kuAzwd5T( +#h~`Cߟ=r&!`([ !#i+䧦/ ݤQ=U@=Ĭ9jyOTyanMA;NΔܲa90^+/hOhk2B|ku҈7*J#BJTux~]+ŋPXC?tD&y, >B?Aie0)) .y8^EzXmĜѥ=$AҬǸmܲ=A,u7[ -n2gdKѬǸmܲ.?L#e*F|er,N*bΞq۸eGA~ >h!oývdzr|ju ef=m=uǩҬ(t:.BlZamʞ3"$gOҏ!N'4PrKHdH$da4n)([Axsn>TZ oQFq7G6y.۫}|մ άSaMZ(z-{lO"7ǺK rwЂ"_ ytA~cs"].?c}.e[PDBVy^, n#8{Hҧw& KΛ!Ps ʯc{ ^GO#G'guosHSC6o9©'CBKѷMA!y.@<4CVP64!3#ؤ]xsn*/FkrRiiS@Mǵ ?'Џ{8#8P`l YɂhΈnS%1GhUu%j34!B34Lc@6o9bIQ>1 |t]!q +qoSE^*1T}~XyKmǻx[^Cf<0!(*\~,aP/7%MaqGQ*z 턞4cBE`5;a?z ϔǞZ=_2Y$ -!eMoOսMۤA hB֏cս3$ɨk=л]p4gh'ٸ>^gQf/s1T(YOz(35P{5cOW 31u`=h ])?f{Bh?[PI`9?X`q `@# X{kpʒ!mQ绫ӣSL ԬCk^ E wAC;."l.83}:Bo.@v I_r}bE?v35"^Mugy {}'Լ𸌒gQ)_%+hډf JOl3tJo)?.=ƏJ2|y92,А?}LWTpgs}m}FiN4k@?A̍tSE ${u]Џ N\,5#H'1>CFRׅ (n>{ 00c  =i/pd^| ][9:717P1O`)>\K?i r\jnYcȨ(>uZ8Y[_:\kM#ך7/2/92[&p@.l[#iVJ>,\#F52w 9^Nuԗ_Ъl;{pd\Z^L֨g7^^jE8^_ܦ7!"3uc>DzVАk?'q\\9QV~sx:]A*'jҎ-qc~Wlah. .0G~Cu`?8&K׏ &hnܲMڦtif\ -ˇ\4\Tfo.Í[K3?HǢ+hw(B$eI@-[L`}vo|i%ifC4r6(6%W<Dz6ZCA;p@[Ma&NKR$pEa5'ZrLkr)@?&{51٫ Mc{@S:#SmڦteE#G[KSuŊ8lf-y LqWNP\lї -$nWGOŒtif3c C~YB@*VLjJю1ܸe %n WnU_+Fc?FrL5 ]ͅ7B럯vЏK3 GolLoh"b|.ʦ /:odڤPWP`[Ї6S,C9iB X~^߯Q?]J$1>*WP6M}v(?$s`lm]:;?#i=ac0mvᤳΆʲڶ9c fW~h؃PSZ14Cϱ'j\g؛jPn/NA;.J;~Mb $ Ce_5^biBW}<#jd]jĖZdyòTE^ѩ[m.L8ej<tJVv.ca׉M6hFߏ&Zkʪz&#=Z'yczXiֿ6]ƮaFJԢs>?XA/:}:ekaDR DžH'<&Cs yj +;A Jɯ\=/~"l|Hӂ|vaaWt+l|@5y%{5*]eshzڂxGu-3v;+N*###n0Ǧ]qwLvx4AhUm|eT{TRry(2&@ß&k&JryUQ=I E&*GHh"W Ω7Y$gqqb,/-!:N(Z:l(ZuK -ہ"BA.q0\US\9n+[@_0̞liUXT|j c?༶S*Uvԗe$pS@cCGs^t=c0l1_8ǰW88sbj1$}0qcH΅9J,,1 &u`I>?+6GsWvpeZ.wd<85|v^~ܤGn?.O זd$%d4l18!&cWx\F$gB ,Z6X~M(H7ԁD? $SrJjqMӐrg3=jҏxX=5 -b3;'7O/""ƣ4DM< }r:;RSB1 raʦUx`BAG1ʰ6^²xPHIL(* y2{j֗ F~T6wM/QdsiL~ xu~47-!nd.5yDQh1[~L_ЏYxG%Uu -::TGpʊ[-H8tKڈՈ}L7XA(-ZymJBua ǿ0\]z_}2ߵϯ -vXZ?`D[{}E҉Mʤ7N4{;>hX{`vQf,Ѵ^/ ÿZu`j%K -YB n~4A{':((#@LWսƘX_k-ԁ0zԋ<G"SXNY"Ü"gENLʆ˃N1g"0(42.%spjagFT q #믇=ya54WP,RJrCv}>8,}:i@D~@GgűGIzE 5]u=*2*6uc4ɈjE4ۤf~ y3'2 wl͸ŗ7-O6} c^nsc}mutc"BjЏ竵cʙˉIـǰWNsy\dЏ)QVp#E,S'#>'0ӹ0|"ƃ?1ߥgωWKvE/hv\q|{Cbap-lP`΍A#N1U1l@C?+HFy7sBMI4e m.5@0\j]QVOgiׄ kD~A$; 1c?竑 A0'[S҃~[1 # -"I;iv&;*SѩyE腎LCE>{'/ ,Bm5%5䜊64dFD[Ys + -J {@(l֙"ͭd(41#v&I9v]ǟB?띆~j' \2yL 4g*_ _@_o`̳ бPp$&Ut(`-Fbjamm(5(R4l[ 8/[3Z}f HXşcCi1 EDvEx`j.4#*:g۰Uw7We%A!QI9BE <"j^ -8;c<꽭@8zV0W5u MD<p`3j1-!ju|{F7]u,HVl>V: y.c40p*th=n6ՍKs;4. JPSSA Ye-#.qU0"'N È`^>E!I?ƕ[*1j Qp(&K*Kܤ\]i.F/0L]s4/?xy<\O! P+mfgWTh.Xmken|QUZuHѡh^||~usiEYBVlY@2zc_HJj!:8tE#{N"EjЊ/ό7@9NA C?݊{LQ9%UCS AKK5 B7P';ѿ  ]:w~G*M[],`.,FBoNf\^8~TZNd@qY"cnrKy[\|:ޒ4Ɲ>֏XCOfٚh*ɤF{P'iV@¤"6xy¼yۑ߬$e~y -m. /j# Gݥ?^bpq}xJCSk]W/7gLR^Jn0+ B/mfic ]&F.0+ڟロZ+A@Nwtj~us -å!}Dh&<>ݞ.Ud'~s >Fŧd׵OrqV'`d]+ƛ˳cT,C>gW~fQn(Ȩ;P&~`E.`4YRSO͍&yp~^BhQPL_cߝ0 yBjPB0%u|=@gSuI9pq7|1~_5Gyyy~}3/0c4D"y YZތ w Gqh 7V1a3y} EF?0<>i3*+*]li 5x]X6`öGL;8uͨq <&{޳[q -Oau -wI3k~B͔H (7WgG;k SC%9U8G7:)}d~ ϦM`;؜n  -M"a^^EtuG ud.ô "&??RGxo{uar +5>Zz?q tM3!&ֶvO/nw\%\wuiJ<]m 6ek_iP3xSTQ3~>4[K48 pFy<= iTSK*/JDTZBs[G膢 ,a[$׌kHca4i(̤' -HQ?C1j8Ck^,OtdžBUd,:E Z& >_HЄ87(2$rf'7-LS ?qG%V o_Եy%}p<'+&!•6M\qp$K^c!&ޢ4][4B4 oݽXE:f[`‹+rQ$6 E -4U~F#  o!Px ⮠glaBY,?|7U5vͲ:yIg4[tX,x\.@GؗI RzF%e7O-C:g.hpaxki4+1— G@WZ݄BQw|vq}[!*_ r- #:'{+CݭEi9ͮי䟮Ixt|rV^IU}k%ZGf O~|xCh{f˸@l(Iyz&V6Q7ptgpSqq!Go`{} 2ip1r++Ϣ =ҚYtCΠ4R(% BT1Eіŷº3zk̴Ý4zf+ Vxodj.$[)FQ& /EF6GkUu5cCĭ,iH*kX=:9%Nhu30 NNO1V0Xʄ4šc?LJ[Ӄmy)׳!Cҋbec"vS'fxJixY}Gǽ@h<4',0Tq9׀㰯|K}71^^c3K;b[=4@"%rN7)[IG+spB9{ԏlwE9ȥ]h4ȹt:j !S*%Aj,٠ X}Y:)J2 bk$h~`'e:K,adT:L4$0"`e+mj_Z;8<:9C~yyuuy}}sC3ؾ8?C㴷2=^SFYCBC~AfnIetq\#śGhW6΢1 s+p -qф$>R89܅h)/@&V9"8(_k[ A'v\Tzk \pPjPy}qz2MuhE%5)y-s{43Z&AoSyNR/9#~~muG%d341 r0xvDPPuXn(0qOQ) 9^,qE+[[[ [<V\_o.H 7hpͽKk|:0V\h͍h L/j:n0':E.\Q\.[sӼB[٢]w{ˏڎK15ZbXS) UM41hj;GNnKzNRD܍7ߟrᛇ[nlw%i\ULQcS*Ƨ%ë%^i K5P1}RLˈA - zg"]@&8PFa8HR-6RGҘZ]u+c ²ڦщ鹅mG8<<8C֡4;5Zq`:'u|JFnIE]KW?ѱH>g]7ACQ@xx Ce9hqJ}5_>84"BH1 nAiT+ ejO)젛61ӊ-8N,+ѩfpŅ֚_*-ָ$+BV(0u0_IF_TW(1nӣq:B݀R〿d904&%9|hI^XLO٩C' YT34>5CA "?Z`5356HN׌y~T卒'y Ip+v#ΠGv~w{w aXW[TpܭLh]PV7C{'0Bx=}+KroJ 7iCU  QQ}v Fhf,idxhhyP4HP(Hp*VaX}!L0F -B`iP7G(-,{[+ cC= 8%rhDt\յu2ٙ-0-PoYHA9:!9#:*d9 l8b4fZЙi|Т6`wsmin]ĸ`T߰(SI6e*I( _TTcEE0|q{MiUb@MdX F'4w`5hĊX<@$;96G[PZ\H%`.d/< -{ns.2n(!7V(- ݾQn퍕qp:o˲0@D1JEUc}eafbNwQn&\$MCG7>-j K1 SáJ*r!ar.(jh[@%>=2TP[XpזgƇa(ʤZ~a1,:1-QЀmu5UeEi ,0UPRk) 44UпZ*/j-c-\1hKEﶇٌ~V԰S֜Pv L W Rѻh6e!韂߹0c`ycuqj83z?ݭd$,{ǦObw.(oБWb,m-Ԧ4ISc"HO-TNU<"6)-;%ੴ01HBAG8l<֮Iň>̽'4{~K4<` =;(`ogKCuYq.PRbSӳr ʹujnmktwwvv46V*sEZE8háQݵ^,x6N"sB?LX\]]D~Տ:Ҥ''@TZqJT\bZV>l(5X(N -vyqqnD*$SkCuiAvzR<>?â?:ZO4c x&d dˋ n"tAZ:zF&1xہL ,n\M޸CfEk*Ja(.8/%zQ<5#'(,0oEy - >(4 -Ь܂Bn@0/'Sm}WyJzar1쪬Gؤ5p+[F#:'ui"mC\>.Cʮʊ|MZ eDcǯDH&^,!:*0T\oHGNH)(yx|jvayk㕇G4\yBӈ%Qt!6V!ƆDc8- -D >!S22ˢY8i WVffFZJl-J;ZH*FG0&4HC`,v;8=<m`ׅ #P;ÊB~B -uUu2J5XSQ {0<%edfSEOXӄVb*FFb.$Vd3J˂Y]܆.*sQfZ>;[q3LOfh(. 8 E(/+@sA_nAKMKKA+LV9I ЩB*U=8q3 M4VڅJΫX=_$:9rz{i!m;G .boL&Zav@XOX.v!i45fLXuw67Âaw+ xᑤeSD=c4`uccsksk #4dJX= fhnRT+8 4cAD,u븸(u4ۺ7hrCèT)yE4IByb0!ڊҢ8t1[9\o[EDFC('gdeC.]GG& -mO5%Ca)ihnIDc^[]l鉱֦:ш%' g8 -OJJc[X =Uzfcs%C_GxldU$S>22rǹB*8qH]8Tl16X(H2 -kAm"I YJ.%;?7=516L,:U| eEH]QG1h 8-ؘ(R!An㊁+Ç#aNi.bpt6 _@pTrnUh miB]8{Iʕ:O[x{y -P-M4 -+ ͘0ՒFM%*hIpA_@ }pxd|brj7,Tt(裛T 7tS0ݥ";"1  %;v8(GV._Tr7wb(6::6>>>1( vwb K mz|"hPHHhxD$XI)*p+)]ؘȈ>w: tlyHY,(.xv#Ƴz4P6#Nc8}A},ti8g$o? Ճ ɰ+W5M`OO̠Ϻ`oW""&dBGLEB;:TqR_ŔԯDrrZ&uQ+jv0sg -U7B T܁px&:g+8\ag -W.H8쁙61QDiਤSGS -gVvJRKBs6#t_wpΖ.Y7`F^1"1"4c -(*LM -> #;QRɂzݎ. VAG'z{:[[g,Df WB8(D&jI$Z1HG*u JGjɦP\ӕSQND_9P.C Uյ Mͭt?w -mqk85)l/f2JO )-DNZE9B}t Bcnp>G&x0SbIÀiӄPOq\^~c!vXÅX̛&܇z}?_ ՖA Ø2Z h[p;Z(,3T&tu k|BJjFbܭހ WdAS$[d RHo۰:i訸fT|b2|sr K1[^YY؊rzʕch~"`Dɐ_t '6,7mz|sB- -) 0Z*8Ȭ -w+(:{@{H~Kd珤2!* '5Y$gP5R"E" -2 -UkPP=P*bQ&^ ""QIfL((T` ^?}z#pQ/|XPyꜮp`wW9=Q\;1񬡦9]ǫõ 4[XЬ(J7+ݭ_Pܤc~ \4KE_<+ ED3:Z"U#b/ #ߢX(B %ŢBDM@aĘP<*#nŅwyV7y~ڄNfp!ҲsF(& ۼek QCb|gi*'?].m7+fZ'&` -3(3ao@Sވ? N$doD@DPPi _!y/yK,P0oP8tyb T/W$j6o -1(~x{9ޘh*ͤWW{ݚ(&Wӈo*:C -O)__@HtjAm;}D%|E],E0 c0y뜂<%F(ѿʁ%C`rA4uN_-5(H$)5]kWo,*Ѕ%%/L@`) 7yS`TẔˀǰaWx&*~=ߞn$EQK$䟄>^9zx>b5X x 8ǡ -paRp8PVb{k-Y6t.Ɍ x\xu6PGOsJ$7/ 82)}lyAC Dz'-PE҈7кC%fX"yQ?ߝ.W%G~D".E$dW.߿ƏU0lGD(Zv4CŰ+`"ÞS>]m7f&DHX"H~Hg /ݙ?hrƥz@w+ElݵX|??_Vƺj -bC~,H$~Y~}Jo -,s2"\:Gsy!ھĿ4z|s57 -X"H~3,9~o:xK!Ba ڑf4!{?Wz2U[$D-0,.>uc[y$B8W墨յV@"ԵXdk~<'>]-cD"mti`aЯH*4n9Bh& -U+5q0C6z|2]W$^vX"H~ĤhW&#hS$!b(Zbnch(i+[l<8>+H$@WRne~чNC Sqy*pshUb++BdVÕ-`PD"VsF/^tiЦN@i{B8p flM+)dhO;4_|4%Fz,H$߂" /\4b 2y6}IJÊNYb/#)XbxcfY>\-H$!~$2K_\wgBV /T P9g=0V%Kй4~ܞ.uz,H$ ?Ɓ٭;"űD"~@G&eW._>^2RV.g>u_47+1 ۼ3\Fa ?Tolm,N -.D" L.o^=<'B~XC,>""sR4.!`8FUNaKLİX|DDFh4 _m Vw$?0<>yh~EE\!. BOnc]brrp e] OGj2\Dy|bzR 2& ;h[&k~K]-&F=mˀDƤymp k$[CI$w0zwz^@Ȫ4.A.]AzzyG}y!2%55]dKta ???>0x|q63R*D=4:[Y"$qqF;!r6oB}DՍ'-TtNs3Z՝5V?_oΏWfG@Ky,H$%!ɹ՝+Wo:Y6͚G֡n{_a@&]J3vhQ́@% CSXW]IVbTH2D"AG&T- O 0JHZ1"XbL - /63u&;ގUq;`R`/^twy,'%&LZ%ŏ)E{ɄCy%>@Qvxa%_1Mcb>F8Ǐק۳< HX"H~W 酧'\5e -d\.o{K( -!l>k5=ZMr UG=H$oaq%“?J@`zkXBm%+h@6R0i}$Fë2E`Ȇ3^ϵHWCy^cK$x\<`X{"A]=c;eoӻX{N:<]/M79x,H$%2JgOo.bR0IT-(ˈA /@4C$Iت_okKrRE''D_N/n<Zмp v0>!Osڑth0vU_o,g%E@cH"H$?~CcR ߼3"u8xj|R_`^i*L K$ɟ@HTr^ukoنY%Շ/& fEX0Ӂ-@sx>Q~kت:3ÂCI$/ 82`k[`ꨭ;jmo8i?v=fxŅ>Gڎ_)'tdܴ()%OJ*kZ9{aR M,FL"4C]` RWVzĒ9@s Ct|-?+/Ǒt7WgG%Ϗ`]?uBXCLKOYق0LVؓ͞NBŒ!?ee 0YaO:.П9=^m,N tWd$Fk%{XlZa}1?,FarkqaCv1҃LB[)[6qy-)?fz ꫓噑UR=H$? -z+&%kb@HQ b._>Gz _z?c0U͏r\$%Oʭh[ٻxxcMsD93: HЯu9*%6> 0x.sT#,ixwcizRK$ɟ߷Db}1S'Zrf8=̨Wium@-XYANBWCKѾ̔x~ZjDGGQ80uzKXd &5EEMlar9vX1Ե I74~di27>TUc\D"iM+8yz')K:Z*! zX<*,1D"H\}M/IKq,H$<!QI9@`-NƘ0v" 7|,QӅq `+DZ[0n*+LMðqp tcK$D)՝Td 7`2@ p⒅2d22d2XRV68;yC1I(>?=>\[Yjk,-LK|X"H$"r*F._@`/Lx " K%;=4eʐ -ؘ bAH Ii _(>ހ,hi(f ;ulTDH0 RK$ČPx|Vi]fT5,6>*Po/WENNN-.P?<>P|zz'Q/@BLX/;Co[`=^n.̀&#rM}ck{S3s ˰`CY&mC7,!I83HXZL *ݠt}04a (Rx{ksc}ueiirxrbtd:qCm )y9$bX/,fX -cD"x[`hLZamՓ *1/T5x)p5m!x<75.*<<"*:6.rvnAaIYE5TΞq(s K˫[ۻ{2 ٧4_C8C<&jD.b@\__CB^@B"e0 U҆f!Gz:[j+K s23RS0Z Q^ i,H${ @_޿4m>s*|(=H?`ށ<.N -.WUr3)}C#cӳsK+k$Y4l>>>E c NH#ؠ>4!7Y//-@@: 19\uVIx1bb)%D%~Ë7uNœ&|wi";1*{@@0):=3R bnk\{rjyaqiͭ흝=>K~.6ϛ|!zIB./--B<3=Mz(zz:;ZjkYBg$%&FGE`ŐŐŘ$D" -?@,uo ,s5lDQYx[kI -ˮo߿PA̬ܼfnh̃CãcS3s KK* -EK\hsӓ$| BtCn!\tu%pIqQA~^n6$cc"`8} -b8D"?DAm6RT:t?7`Rxu:Y R Y(:설d匬"җ!![ {{H@(% 較`/oK3߆ښʊ|Ha҆Y '&BAubR$)H$wUc{@&i -i(N(ж:,>ߟ֧{ -RcC59- { ʬ+GŐXNr93+;77/ҹU 2'xƦFuUPJB ˅"IBFiHxRY AC ֛%D":߂"'` YJP Uzv|=ٜo(Έ gpL&U9 I, ;&Vdfb/(t\RJ" B~:H-.**- ᛓ#oF:iIII 0ICDGGFFa}r8_ȮD"H$?߷鍣'$O(>JGޞj.͌0c@t{dLrXsdٱI2'$Ҝ` 3@"t1c)$|aN Lz0`!Y -i$1P"H$_  w_`mFI L>%ENzzoi"'1*8Y~ BPU4lfM@h+q^hT"# IC`%(6i҆!apVy!H$/CyehaxM#Hh /m;DϏ˃qz+:$@@vQB?oa3fXaj뀈U$s `^#= $0`8,/D"E`^;~|D"9U_Y<5SWh6Xہ(N!! B0Ô ?!*le -&]0AEB$ +H$_ c`[޻|> Bؼ -U\Z&\DK3]X{ I >*I!g -[bń짥D"H$l.k>}~caiFZ#1޶|x;YiN F?v LK^Cb~N%C"H$~0X' ]?JAmPt{>>\wTA<  ,wH$ t!yU{֘Xbͳw8|=]ԘߐD"H`"Jf7Onͯ kTb v8!@ 7dƛ"H$ɟ߷شUz].4۵P+J2|䜤H@H$ɟ rRNyNA -j~K5 wVD~X"H$6~ =SkW/?>T5]G ڠK;>ޟ7¿KD"Hp}?yY9}Q:HR[YuyǏ iH$_@pDBfIcۏOORUʊp9].|~xy_ΓkD"H[`XLj~u(dE<װcF 0Ʀ-|=^O:,_&,H$??YM3BATѻ$S1agh'o:;!*_ID"Hw  eyidHK,@5-ozO)H -D"HhHAL*SW`ȎQaOXJ$DHD"Hlh96 -2Fo ['|;]iMi7s- cYH$AZ;|xXk\Z?P#7A$DǏXW /l<y"H][]dSts57ZLXG$Dg/>3~5[aW/ %Q!!D"HP܊ٍ֡k_%o]E+㍾ 0^]*D"a9>s|iwҎO})俚˙OA>\o*If5"H$߷?eM뇗/GVvƻK2b-dD"@A f/l<kl++RcӖp{uAZ\DԐ%Dg!SClz!YYz GƎ@se~ -2iR"K$반fJ[YjڡT╡-/?ޞnV&{sb¥@H$,Aqi#X_bLBT"X}﯏'[c,SD"auTBfi}ӛ^PSHg]IА@H$ r`XLJnE,GO$Y?JyIE}x{;_-6kG"H$? b^P>2y| - {9mB>_-N@H$߷Sz^m yB >ݜ@:9.2$($D"3񣗐csʚV1 %|5d SEx}9%Y_YlD"H$ Zg7襧7!dS/ xMy)9-=3+3#-9.:,$sDH$oY\92yxyo'̓îMSo7g3muiIHPF~!RaQ - uTtl|brjzfN^AIiIIQ~Vj|t()M"H$ !E'ew-l]ݿ=פ,* T䩡œ䄘`afϊDNɐB'bđ1 )ٹťU M %!SY"H$ ,Hc5DB=k%@l֯O۫s]uEyPc"à%@"P| b14b$KHJNM(/*-onj-I X"H$WAqyMK;'W. Ap8VѻM %i@Cp{ur2;>ZWY{(#XJN,9L8,i'$B)Ȃٙ 9,H$ ~K˯h\٥/YCCv#X xHwE8ރDik)mFjR|ltddr,I."N"3d0 b9LlEFa8.tbHlH⒲j}#cK뛛Kc Y !RK$rd|z~UkACFq "&S /[+S#-e9)IqQ)A`dDz/&o",GF2BOPq]SK[G♹ŕՙ()%D[`hTBFauА 2/Latt !זfƇzI$WLNNTX f 1l0$!IU:&&iLB_PX\R%qWh{U^D"r`XTbfQu>}]b3P ,$P_WkS]uEia~nvfzZJRb||\llLttTTT$31ApJ*0aE%e,Z1R?4:>9=t|z~qu{wps2YF"H$ hIE56وN`b < |suvC"y|d0?/;+3#=555999))111~ p ))iiHgfe2\PtYyEU ֎.#cS3s K+[;{({xz~y}{{}~<\O#y,cD"[OIYŵАh Y0[Xq $ּpE|~~x}yz<;H^]#PWSUQVZRXXPCd,yy%$+kj[ڠM ^^yǏh)%DCrPXLRvI] {"[MU2FA$򏷷닳ݭɱښꪪ -PxQJu MP[1N|bď/Pa O7ݵRK$C߃Å@Y;% e; !a0~:?9:\PhooήN}Ccӳs K+k[;{'gB'xj87=51y.D"@I.!=2KK` E2 ׷חLJ[[ʋs3SSccc###1::;1155Exn2xyyVͭݽãM AA̒,1 >~8&TD"2rxlr^[]$ x>%@&\]nC.o.///---.b!{WVV!~aހف.pptt|rzz}L$!_IkX9XQIH?ccK$5⚶[^`e(ɟ/ϏO:>>9&|NBaÐϤ01 bbF0f_OuZD"GGd <0}|yjxK6HpmBR0[B*pw{{s}u}}yyy q^ -7, Y -&9AbrOCɅ·jsI$4䤬[zKP) -Bm7r`λ!mǮرbCFc7{+v=Yh<ܗ,.+3ÂAOber\yu՞底rXsV~za}"ǫz5}L!Ǡ3Jj7E>S -w#d"ͫ@bo2 `[ MhGᰰq \y``XnGDŽB~ *!'-9ōZj'<ߣ@ qvrQ´b^ pۼX||L!G&w2$W7wϮ_={hCW'Yֿlj8'ՄB~H)=Z54%!"W -oh%7;/yyq=1!L:`J)h\ܔU]>5k+?X#ظw\Wџ)W3BYP^el8g| -_ћ3*g<މQN !PP!'g~]{uI_z^gv=9YvccB!?!G͖-ԬOY{&:-s~#_坳CX߭ (1I!#G'Ubybaq|~}{.]|t|@烏\{\E!&B6Ҳ -FfWN/]ڻ&Y;RNC߭wnņLJPWzg?_WDŽBBYՕgAzzyc)@N8¯E;Tu"=>x||^_MB #If5/ݣ[mi Ⱦ-i |"~M~ Y<| @>ս"! ԬcӲ ->ȲE~uXt>/X_ NTxՁ1 A]CڨWDzBHh 1SJf^Ius,=:n}}/Ay/8iC xXWsBH kum}#BkddL4ѵc~C׊AQ:ݛիDŽBBL"#"'fԴt N,h3{{ āxq=1B!$GD3&gZ+F6u]^ozy?߹0>$8;|L!$P5Ĵ|"wϭʺ[5 }aAonΠ&j*"fY+z׶ϯUSgB^Gz׀]=PG]qvr|tD}L!$rCkcR:UMGdy?bHoHwgo! -*|2p{}tϏ4W Q1!P$,Ek]9^ hhÂ\@>򍭗'd hOk5;%!DŽBBԬQ3&fZu]3;γ+㷧'dd {5 >BߪErz`:q%߻.Nvח{Zkl4!DŽBB0Yim0&e*?u ,1|zysPwl-|]'<]wW`幱*Ŝd@< !(D,rT,% GgֶN/OWt3u ^.7jOKHGw7WNdOuE9Iب?cB!Kca[U2ږwOP2|H'">:=w@uj0m|Y_XeJKicB!L`dsNAIUCk痑B; -8K:Փ9۲/͌ %9cBHhuxf oxbny}{!YfT: T6y$c 28=XYnk.-LMI!8Fl4fdW6wO/n_A0{^7Pw r1ddp뺾bРt(ƪL mo.̌w5֔[s2`cCLؘ:&3!R7%e*jں&fDɻkםv.(Yɏ?#uԀ?Q=,L?=߹nO뫋3S7#eSL1! r$ֆ$)[KHn588:9d2fR9o_wb2~|x@0xvrdud*Q1QBO %GM)9e -P8l;rb2ڥ\d>"r11d|qvrt9ㅙ skcM"81uL!g GHS!9˂5g`h|z~iuc۱/NFNr2 Q'շ}9Ř0V.=jt䋫 ʐ򓒲ͨ#gP.&&~xo./OQvlK0\_]d&uj826&Kum),.ml7821=$N?<>9;U^w"el*jS00pw{þ8`<\WU^\hA27ƲBe@HFZ)ٔjͷTT5}WN^Xox )_@eZe26/f +?be⋳ӓ㣃=qxx UK9-8Nd,ujژBȯdJFJN5gX -5͟:&g7v{++- / 3ðP+;ҏ8 1OL|uP<:!89YlLz_dWV7wtv -7C`ʽ cP/}=ݝm-MuՕb\P&%& =3T1!P]KP7bF99kXْ_\ZV\ 175~jkotwwuuvv|nooX_W[S]Y^^b+h&6xT.R| -B!ɰ2&!e)_)i`g -Ŷ2B ^T4\Xg0&#fbXLB!"HYah% eْ'n.*Złf# [rs26hJ0cjX\$BHPd%HDedXL+ef9 95532233 MAٜ. )OGGEi*x+B! ,Z_p{b3')19Y =C&)!eih8y8F%b)bB!sB0x9VaXhV`8X0D~4B@dxEtC36Y9*0!q*^XR3| zA POB[V̫I61!? -endstream -endobj - -8 0 obj - 137470 -endobj - -9 0 obj - << /Type /XObject - /Subtype /Image - /BitsPerComponent 8 - /Length 10 0 R - /Height 502 - /SMask 7 0 R - /Width 1936 - /ColorSpace /DeviceRGB - /Filter [ /FlateDecode ] - >> -stream -xͮW[➻%%,TX\ %m1sI)-V$%"Ȑ,_.cla;gfeN 3IcL Df1c1A>Ź37^g suwg1Ȼ,2ҙo.Kof6/qy}_^eYe?v?ּLG#mގr<. t'X1c1Be"ngYet]ďq?F0ry6#C G9Tcg+Y4ⶸnr\:x2.gbͧOy\D||e3D>4 S7ЦeXv&"ҙ@daA S_4/ư$J3/=X0U룖i:+Ks1c12>z僘i>@Gdl0% $F;/'Av9>wc1z-;ow,<׾r#Ԧ隈+u ehzk+%…5\giWvA=6揯,IܝvuS B0af?;&x;D7)\/KJyK4%6񥏾D䥂@c1cY\}n1+|(:0gw' VݹHD&W"(h2%Rk*/e@^ ^&#z$Ic1c1|fG|)|1:,YD`3vaD7>0:CCQΧ6Og1cq6\aw/7]uoȺK*,tKӵtmO\ Q\MO$+iˬ.Q&"úñ!fYȿ3ډkfY !W/܈002Hiz%{ڡf$gW6c1|v |zc&gF:3ҙ^gu٨H=cOj>9ˑc1JDWQmF3tFv.y"tw/ *VvbZ7c}DmȦxI,DMAv/'֮l߳Ytnk/7 K"wBT`Wyy gu5MU_{}xz%1c1q')W>=X{_+qH>D#X>v͇1f_\]vr 1{ $ݢ;n`'ڕ1Yv:C7uFeDlM Y,& ,Cm$2Id&&ͲDll$̕_о&W^-+I٦c oyz5>ַc1c'D'||ȧ|l$2?Y^ghę>w|n1_v_]ޝh7 ۆN~ ]̝F52ܮ BL¥0]+љh':e i3>ۉ $2iu' ]e#wNͺ}2-6kYy!Ǜb3l/E[^&yzsewvc1c+#ښ|9|DD;I i3> 3P9<p>C1 -q [Ӹ [~B}#OcNTU]&2dI> >d +p^1Dvܣ޵sĝ=]>ȭ?͕hr-3]e,:su 4,f?I>AEfЈRF ?iSUw,4"$L6^$2#eF" ɰ\y钗׳]#Fz^n_SB1c1?O>tcO?䓋2>c9LhYͲD8$SKW_cD\-k/_?Mqs->Q rE+4.vE]O6 3tY"\;A"l׈겱X}v2#E DhDYk5Hlk BozM -/T7xt /c~7Bwos1c1/pO>mg|>!"LDͻ| 9cznO^GMnֹqdn幭g˓K'*KN>"t.&zH]_e34 &IDe?$:C#+; mgF:lNdnlW]& /Qy^̂ ދ߷vt̛7v~Ce1c1?&)ܯ67>Na3&>ϻ?L>RzcK.fnEn]h&[voUt -Wr cF̅$asM}O4X B@!D<3Lq}ꮂD9h1%#vw~w&̟#uk,qΕd -.[Ox&pC "Yef3˒6iYmoI͓39Ge'!K*;gCGD HdF: LDz4«^׿wvoߐo7}>7Bn]klaF:,qnM%7s?ac1ɕt͵#UGC߿;o禜+t2G]2qfei"B6+KD'"4 ]4Hw""m.KCD آD&D&5hxԟ& s՝D&&>bcAëlf"W^x7oُ?ݻ9n\\r|c1c'>|2|yb}.K;6C%'poc<qzjW=w~astĽ8B.@.Chӭ< :?"^=ڧ b̲DM9OC B!t&Lq^"2tY"j[<iD&A @!ƦWl'mv@񝂷qMy a7oƲ?c1믿V>A[^e a8CkYEUvL4ڬ.&qL޾}L!w}]_.ߤb1c16}`CO%>x4D0|29<rC1u{u9f.zeyCv u -Nؖa,s2a"$2yg> G n&YR#"C,K֠(Z: б#RmoL8o~I[kmy;x W6c1|L.Y>3@Ȍ 9ȍ郏;coYY;.pS{;;BX ҙ$L N sAc$a>q2cmeD gOdeY"Y#ڍ<7ܙ$2k[ګ+;+;e黀iV·U~7ɻwL^^^|Wwc1c "|!w' 3}<18e|O:=18Wv9]st̵C{6;i.(%\n\ʓMkD }}YYveN&&LL4Vf:3 4X;~Y~!OMbDY"8GX"j[ҝA3ΝI P,a"B>2÷5q޼y{4w%qk=c1ࣽj+| =L-LDuXgu0I8̦j1&Wp9@/?w~ -2$:]B0cmX"bk+;X{e:D3I$:Yi@0ecc?ڢ\f>Q~fͶd2mIw՝sDu&a~1#; ex]k]$c1c -+|q#|O\w"$LAhcLs܎NcoxuӧOS힘;co.m悂M\b6;Nt_ Vv\$""Y]#ܬLaǁᦄ5>`g17߭-׫]6s~r7=\2\;pq) dFq(K% &"4loЈH-ko F _vgI4jٖ"6_s2#݉84|ә6͘>JZCv|c1co>ޯP B8["4"&>`ÇW_co+Y/!2=1W ǰ\S:]hٺ @FmxD B#H4$YR#HvwL|664h'LfD],qW2˒,+X{3C#+,M#6}zS~wykg3|Rܸwzc{t:w=>]ܯ=2Dei"I֥F0}qA;Hjrcu>vp;KudeDžG HI@gϤYf>ę}&tfYFڤQ睕G#$։vLG|{uZZ^:=co\v9^#cpV>VGhr,ͺ mGWߎ2vc|Ŝq],w>}Nw0  -i>$ZwtjiM6M3CՇ&"Qwf$Lf$DYV FdYuXD< 6飉L ;&}j{?O?c1W[ͼ^ɺ7혞1F8?_D63i }WjNЎjܯvscz_^r5• \;<ډk -; 4FtȤiv$L @0#Y u"hĶ; Is>bu޹O aByIbږtFЈ.4=;|wH־Mr6c1rE-oMw9_~03;rc@G~L1 3ҙe@X'qK7%~1_%;yWܩ_⢠sZT1c~!nzk_fazd1/EQky,#YFyNb1ro˝׫Ӿ_ݻ\\quדʒkW7aXu2l"bG<̻Pf$:iy7.;ϲMDFڛ<γl|ם(˻<P4XKʎoUyNy箵o ~ʩ@ߎ15 kzuQ;O>S~ ߱ ;&5G"hs1zNwΙ"}FI'D3CuS$p<cA|~sc|rnoνK\.߿=HT4@ $2#Y۲0iĶ\yAcMmqG04gifK9Ȥ']] -aF<_>'7-LNDf3k[}&ؖ+!hlVX=޼y[^,c? )r2/w/ǟtZ䶆{nqW=oqқ^Q: q;~19:s:g: :~")Hh4֥FpHy''vY1Z$!nݝj3o߾}k -.f:e -CŒvcFىvt;kp7'0Id@e:}DmK]9Gd@ hpElkW6376YƣlaF3fmK0XuQv ݹ{oܵR;i{?Z;Epc1H>6\]ܦvSڕNg7@LAq\1͹Y40s>L4Xl"V睰O.8}fqwr1W)=ޯǪ] rQ.U]5b~$$LĪ;56$a0\aնϱIY$2#m"$:4[tȌ Ye 裾Ohlh;I MD.Fn|{k㼓SAO9c_NԮ_Xw]l]\鼹[4ӏrdD ۿڙt4ɧ"llajz`8;1j8;s9ZŻw̼UE*3 3"jшMDhtq!u^ii@ld߬Kl_; fYrڍk{h]Mh74DIܝWFll"B榛3osަ}v -N1&3zuZ{7놳mjW:^cW7gDDD n667xp/c17xo*9}ͼҐb6v4$:6eQYږwy,6ʎi3&a" ݉n֝sgC qf6W9X{ Yϗdnj`h'|_w~7~9-89?λArc1r1+(L?|Rݦ6í 3eUoemp8csFv=:IF>;d"j[9DDDY9sc?5qkrz^˻wݩU[iwU -+fYx+;DYֶx:fhHg'aֶ\MfZkcI¼CIOlOؖwLLę}Ī; 4D?li,i< ,k[{<9!zuc1ƿ4wgj6uЅ+θ ok#93%~ uv\缧#'Rgドt}fmPĹST6c1r7^;YzoU^ ڌv"\;L_?dDY"hNdޕLh$Y۲/꼳( fhfev%Ft_,4ЈۉLD2s3i|5B#e@N ୠpZ0tCB:-k'1,vG}uUty{% +K@Q~?9lϯc|.t^u>)P& 4r~Hhǃ}Hw:4 *e:ڢ,Nt%emYv"bm|ctpB7+vOcSe}yԹt]e'*k_t-1bK+szmps_g?@gœZs AD YQYDei#ɉځc1D ˋs)=6]2]<̌.@D:3"4k H"ڂDfei M@ BFY"Vve O@D[ mf  .0I4LQrff׹yi34Q]tfEh@0DhrNp0 t!s1:r=ڻV%?~/.ag\}9Z~c|z*v3iyiDfh5NcX$=y۝j77/Ý8zp!3vceI#4˒FdI"@ Q2m"bk,H&et&iQYfFDF&j$IduF3ҙgi]CwI¤YeI#$@ eu6Ѧ3.:LLRзrj1Kj|p9n5)ީ~I۷o\wp$W"w*ܯ@"UYf3&2fQPc|r{yy{96GgHgeX{2CG:cpQ&I '\\)|-cY,K7>NnV;w!ZPvrswX5x"O茴I¬,Mwy(tY"H$:c<uGoUvLʲ3#ƪA<4Hgnl"CuQvrswX5x"O茴I¬,Mwy(" -4pZ䏽[ Tc1gǝj>}䆃.+ަIQoV]q_N޵7.R4Б_.c-ȟvi茵#G8T3D=|ո}}^/dŧw~]Vv"4"ҝGhfԕ:gGh3׹ZwtD& 3& t;'D\;[eYWܬkD<갃'tgh|wyS}N -dO ЫdىXe'.!ɣե@Ј.&ADD[OifeI7CYnIceimX!qw쬁 aM$dO ЫdىXe'.!ɣե@K' -Dx떵`e}c1NLS \8ܬ~jGE' F.O]CYn sQ6c->|󡳢L mk?egtqAqeq}c?,'js9ݻwN>Ys|N&zeIyWD&YYv.K;k]j['v# MvcF)2a"3H4ik[MD_ βAڼ+e"BȬ,;C% ҝ.5-ΓFADMg7>ѻ`mӎvcp]z^T=Օ\eLN6U ' ?#1y^r1nu!=>?CFfeƦS̵ﯶt^1p\q>YOk<>Qߕ'$LI!@.$L%k@#Bk}hk_? TK@3Wv$2c'< A @&a~V&,ܝe,sNdageDf}Q@k}&"^:*;&"ҙ+; aF B0?OKlQqβ ]8|3ɏ[ַCNMc1e_u{7.+6M+ ډX;Ȍ_47xܯc|ss3󡳢%ΐΖasv"\DvY;cz%z9p y7\u#*K34U63k[X:w:8M7w!ko6I_m"D@k{ȒDg=Z6h':I&I$aFmKz՝5H'"4[5 3&u8M7w!ko6I_m"D@,g󌳍sNbܲ68M!c|r[򋷩Xv=*..$IŒG]6$L& `˙;_}?_=6\3,N!,It֣evh'jWwBne1kJ8E{v;۳:#m,K ;,CL"$MDh꼃MDf$aehD,:k[X"3sףu iuF$YUwDYՇDIbRŽs39 w\c|üY5]:pe,MfYFD[ βo\r-ӹcs^/c ȉ 3󡳢Iٲh'h I`_91Ɉެ\.MbKN|X!qv!g}H&a""5e`$:i6a@ 6w7#P ]Yf"*,fhΤ2Ie DpD8C"4 (KkF$Ăp-k?8)76Wqi-qplnFZ',Dži2˗*6ׯ17‰:= MgE ӤX"h%txȵṦ'jc,gc7o8u{4ދ Ne34u"C6e턹ʎ&qfA>ڨcsN&  YrN& fYn a"Bu\w4~h&beD&zege'*;IqWʌ(h'UvL03wFuu߹;pA*;4Hef&ځ9=g]D]Y2m0qWJ6IdX}ɦ%:/C#BD@#"m"hĶ\=y^uG%,;i<9essNdH.& ,w99 9#Lؼ vcoӾ߸m],\2"W,ItyzzeIW} cojgD!LfM4V_iI1x,X>b7cA>}8Wfvwgul"64C4HGCWbYc՝A#\' .eyeЬqf?*;$29yLDei"66ID 66C#B#vDli"b퍇h6c3.:Cƪ; 4FdN]6r̃tv?0tgcoA/ƕądc?*;$29p2\~\]8߯YNwmYF g#m0pi$̺4#m~V&Vv:+KsX"oi }#AD&U6Mf]wyb՝@XAx4$YO#aݥi4DYYmqfM)(ܲ|[Ane1Ʒ;Xo_5.+H&Ll$X͵qܥ_c|Ŝrn·sc%|s44HwȤpDm:xs1ƿ3p/ǟQ駟޼y#r p&bMʎI#"i-:IlaF|ģͲ4]}]ЕeY3Xege&u5*Xl"}fMĦbFtE& 3&[t0ŒGek?iP#t-syGzܲvZr1W,zAL]\\vpkGfY"A#$p ;<͚__-sW0iD"tey̲D Vt`4,.c1z^ IErtV/;fHg> ek?ila>GM꼳ʣ\uII4*;ugm,˲̤qv~NfY)˻ll!k Hږw:ҙOxBYOx[Q::Wyh&N*;NDh9H}77.ٰcXW[LdvCv2˲,q~Hj-^Yѹ18af֝,2Yr8;̉AȜ?"zyӉNΦA9iL6bu"H4²,k] MR #^Q7ڑm"67e&"&$:I34 ,,K2,$hF!6\'I#,˲4X.>zn&(ÇXemv)k1k9K;c;o3M<$aFDm$a>$:iIAceYR\a @ȌtflMŒC%D&[Y}ZO` 3֮<$aFDm$a>$:iIAceYR\A8#!Lu@z1_1zǺ\\)ȅ3&a֡A"5I W+{1Ns'I0[Df3ck,c0coa?UwZqӵ79%]6iIila>'fe<$Mα_D .5,CX&>k HtF< PcHw"IM M` >A0+LX}D%mr}rv*޿Tg9cn\\ .4b]jDY&΍Mę}z0Ҹ_-^1xC~.js>N46QA#֥1xܯ =|816j\q66q0EL~$:7Q34@8[ۉHg"ʲ4I$Y6i s+;eNn?} 3 ItfhDY"*N@vcyf\XgY"$:cN& & \gб6$aF@#6f&bL#VwwH>L3C#QYv"tn6$3:),[o޼4z^393 - zkGv2i6I:C0pHf[⎍ ?_Dl$Y"4X;^Mܯ6\=ηYjxϬۈY-q>k?$LYҌtf s쬳,+˻3֎';L+$2C#eY{iHe& Fw6}Bt&G~I$̳웕X;zeCYgYVwgOv4V6q o 8y錇czzmw. aY.{! T{csܝhYѹюl):il @eY>Qx n816N8]72iDk@#g>tws ,K,?"mV& .Y],3&bcYqn ɉR B>Ԉ.נF κ/w}$2C#"YDY~Eڬ,Mf]twyڧOLIF1ZyO ڻYW -a~,3&bcc!?c/}1}y'jưt¬,Y]DDsM16q1RNտ/"7NNhk[>iyfh6C#heh5bm,C`Gme@0"D H#zM@>BTN34#Ga&i34FXF h^#24 ~$fY] -Lg-/r*{D'p3c|eޯ믿{ r p atfM6M;"+T|^/c pv.pKe&A i,za;x1_jg]M󇫝-͜rsws'$:]}Btfu,lm:&k B#Ck$aȬmۦ%"ډrWvHtֈXKDm˳>fROHt"br DY"$2٢3"tMXAFՇ ItNeNhgz3lG3㫑~3󿫀BLM,NdV댶0s`?;6\^1nu9Qun>DΖ&k$aȬmۦ%"cp<Ça9_etʽ -㯭&g'jDۡC#2]CYIdt6lv mF%l^Y4j۱8dȤ%dA"IDllF[ I"4I$Сe,$2ilXmYfrU6;i6C#Q]6h,KXFN_nYw"Lc129ջ6\p8dȤY93qfx1BDsc,gNXFDD[٧p<80;ݯ^1zz˻w~ۿ t6q^A H4VM;Б6$I|d{4X"Vv+;$Лlv A@Ө$2$a,KDY""m,$LDY.M6XA H4VM;Б6$I|d{4X"Vv+;$Лlv AT~uNk8љc1V~#m?_EaJI"(KٺL69g\k,ze,3Idƣejy+DYe3qsBsZ-ˋߧns12={s 3β@D ɦI"a6y;6\7"3󡳢scl)eYD&a,H;9Qkl1WrxmdR  ,DllFޜ7lseFwy; 4 ,A"zeQyfv22t˵QY'k eYO_X"66C#BoΛvC;aeߝ@T '4w}Dt7-0_ UZ D(KyC7CqZ2߀G-fhLDh<}.XjD=ocos'[܏?ZzlLO!:?w7k b6盉2e|f|s#y6Co͵#;&Ov":웫e& Q2kefu`m$5vdgDfeY2d3u>9Xgu"6F0CehĪ;~fY""݉Xar62֎' v"K&f8ݙ߿7e|-\v%@g^.kxr~y9~Zڑ67@܎WKx!e\۵Xn!“/3?./[9?r-uq!DeI"F9W#Gec0oPgVOʹY4#y9~h-uB"N/-ؼO$/_ʼn:s/"GRtyIDȌt'3x̿2Ј< ^p98d:Ǐr5"mc k ό!/+KĪ;~8reuusv6vN.fOԤM$2 t&@<:yСeYYf""D'"4 3@#*;+;g7n/i,CȤYvr7ʒD'H$2W1Id""L` ]y(3tC#ʲDE&NDhfρFvZ#\ݎk3r9 _pC{+Ko'"+]C~`~e95g -1+P/*/3WL|9\d~B 2YוwWې 5"se,&"ڍպv ho_(K:w师%3/ -5p;A[vucz9Wy9^reОz/_ -~Z~f΄8=933@#*;+;gizs16kCW8'_#z8Ӛ酊FdGukY܎׭@ h|'%|>X;ʎ@֝\enj@ *̲D BG:scAM:IgՇ%DDDIXe'sc:WUvLbeg*;fMQYf%:ҙhyl}N:>pfC;y)x+a8C_Q)u'W|__Ѧ%?뻑+z9hs|U_t/bX_EZ;}yO?+ /HU~́ i|;Kw_ո.Զ$;HБ6IlȎ럯EBK-%x9w;ѷMj߳$~GS'~,~lN~r~iPoHw)q9^xIh /4Z,W^NK6==K/<3-.RX'wYѹ1-sHgՇ%1 Me<ˊ@vJx%WתePKY+1(pt^-s+x;n=cUy%_Wf^^ivTsY/K/f䅊@t=?v~f?r`4S^/?x:_-37y>d34Df2kGvʎY ,Necƪ5232ҝg?Ge'%vH,& &~hgN}fh+;$2e& ֎lsssw<QYޝ$2ƦU7 5Nw}7<.4#zKs;z][__Dt :|ɧ\9bs~ 6s,<+S+O5}5W!G'd>'8^ -Joo¨ gYBz(ׁ(m >Ul#m$ -m3ye?s)c{O{~#2Ű[8&8JЌ*&mf6Ocњ W|@|R{x>gK-6[6siT g6fͬv&ڱexȓᯠ&uHx#gfǦ50b4 lyMƯ7NŮ 'XI@N.{4A0g\YARˡJL>ң2M$+zL$eO ~rv2I;I{p\2xjv;.jnZ~/B#)ӂ^ -^++~7~Ѩ/w0Z?[z%2N'\?9;χyxvo~oYۄWulv ldL ëANjۓa!'? _ѣg")|oEsww WxPse̓2x h4~#)AʤB^oϲ۠#7 mt!#=*+K^ Dl0z޳pAC(|_w&mE>th3a/=VzAxew^Ys=î )ZʡJK\+'ʘ2Β #,AC Ayc3.]Jӡ )AK _|&ʡJK\+'ʘ2Β #,AC&8ܺiqV|y_ .$\pK?@ˡ @x2xxYWOw&n!-Uíq (4^D='4o8z5=a' 00y\su =`:(U( Hμ.%t #6/v_ಢi+vmE{]1IfC+Oƃwқzw{ul6Y}^ݨ;7J'̡s&Hpu$8ixcA618xyYoYZ:] S~Cӂc\PzAD4S̓C z G|X"$ R="Op>{$ M81l7P~m-{ Jxm)/hC$^[a@7GdLdJ jc)W\\Y%H>j 8%xe%I%.RXoiCs9 \+)1#/$8ȅXy -.D"A'WC %N yC rRg C9V:AStwwCbݫt+l gѿWc޷17 ׸ʭn]PvPWĻa?ov&_P_W^|>tkm}_RoQ/N.is4hLM -h6[VZөCPi3 H$H>j{ c0xψ ΞTz=ޘ&ȿkб %}xAprd֦,ʺu/]0·$rid`$\|of$8?Vbӽu~>I>4L35qӷP6>.($\;Xı#$xe%ΉNoT3Ag }(煆z=Gn$Vt(g0hUNl;,Ge9 - {ONSo/NwgK\,1aP00AйZ&Nϔ:)'9h.M}2l#FNC90俐&nHt@'$7\C:|h %<.z=~# -'?'^Ϯ vB^ O.J x|֌о햠u40)s7#S#ʀ2rɨ#) H d9S%ȝM$K$ٿPA@0uXI@+G^\:J%%& ': 8\b2qzs -.R Y")A@NC rCDs/>clr^Ņ P8޷Po%ya=58.݂t݅ o,ʽ~h»@Afű2J/MŁ28&+ P;k`$n}(}^+c$M8/q:c؄qG_n9Y\ADRv 01Hi=e;9d=-w7 Mk;qKmRbraY@ #J2 _q>zQ!AF2T6G%"rd ': C\Z`_A6Gg'(g*%,v|L}Mp&ȉ%yso|! Հnqq4}tiV:pL߆5}8Rf:&A;C 'uʡ-"7pCB{8}v4dRez=ӗ96ޅL9VNN&dԑrfxadX{vE-ˈS`qI rR)QABߘ٩l77!հ&VJN.,Gmt#CG%H7K`=8L wA_I h]aoĆ [H {,NuNEJ$1i< oR8AϟwLtcR`X 'gpG $8%x `C9>¥YYXGr杳мf$8Ȩ,JI$OHM9%cPw.*A.IpJA0<)r|,?Kߗ& ;gyGB28Hp89N.F.I΍|tr si& ?Ob&]GGr9^¥YYJghHpu]^.M ᙼT.󙡣ޝCvN珂Ňx,لf׌2⮩EP ~Д 8%x8#yq) fP [vl,2liMYoϧwWϻuFhڡyGB|a% >*%&?S")a0PJi[Ssoy4Z K]vδp,3/Ca LO/hݑ{y0lhѿP dTH1<4ԮY/ܘP]$`?X"M-DR帔<1$M {Fn"j^JV_L}|wxhzkħ$,e4Op@]1-u6vbvх5 &hAzyR&uAv:|]ѐ? -4_ #cG,/>KL.胜;qyrA3WwDG"Ot@0?#.M%9Ygg~3O<)#)3Y^|(# \9w%&Cf?z@ D`~RG: A8%ɹ.R.U.X?_}/Z:i%]Cu͡`@H84 _胜8w>\b2t5/h7a0`{ɣfvr5;?[ {ݲj^[)x;LO_ǜxk@qkc=cqλk%MY `T.q%&[@D1)Q|Oq$ -^oڶсQsIp$D'ndJ5 #p1y(<="ї}f}; M|c^>Ȯ&~Q1YS>7; D3Ww oQGLmR]eLX/u¯m7؍vcG'ȇx 왡rr{YL*l<67lnKl<陬)?߷7/hzsX-<u7tcqmiJG32:C>ɥB_PAD`F;ܔ,iU?7fx_^Fg3@rpi*ȡ >tx_Hx_'# Ja'Nb| -^Y68-egy\brQ$ RXЦurF3}w0wȼLdòe gsɡ+''5Qy&&'5K>9)AgOMDr&ȝəKe^ 1 -O>/ӣ%r,G噘,A2>䤦ӿy>7ApNzddHJFs>\xr.=tЭ|RHκNYGA#㥼$#{f 2''5%c#Fj 1#9/z $K5]m `li  XFwX -IL;W%`$qbq - ^Vڂ!pi{xӱ{T4QL5  f|>s,f-M?3t@8.dDHK̡91 s9h`_bImi%:Ӓт[y !J9 A"?DA"Xi -hFf玨I_rOe;= @e+n9N/蕽E(ȗ/1!Q);H e}OkmCGVX/&pq -&w<dT KK0e3MvrOK·0EMj }lc`SK\nA/1!QY"9D"ƉceRF~ɓu,!u -͏1_)V§wα0Md qJtP HdK'LbM ԇ -dqW*â/~,zk IvϚnP8v ҁ KK S"_F. &evi s|/ -<U:^W4¥D"#_F!y) xEpd BXÏx(8Ȩ,Ap A"?D#_vނN0q.㟉ϯof-]_ZhH$yGL.9V&%zFb.|'rԜvvu^L[%8N䎇w%G;츙ʎLZll&2wD HJ\D" /KLN4cewA2)H\$8 DaI(A@FeyWӱr\ 92,AFeD HJ\D" /KLN4cewA2)H\$8 DaMz9'wrtQs"b'}1qq\݂_H|ᰰׅ[pHDe<}E"0)#fjd_붝;+&{DaI(A@FeiR0/2_z#x^o-l4#"0+qDb w/19 ITFLMӇ#} q 睳mӻ G"Ab0;֜dTw.}e8YF.AyRg|T G\J8:wGjSl;9mi0qDd(1A.c~EJLA# aiY"X *8>Ծw`=_cX=';sZpNڍα)/ptXe%&'12x; C rtl5S2$޴D ^+{;p$$ rGš0QCm"JvY^,0M)KGО$=잖`kO|>a0a>*%Ȩ1' 'uʡJKKJ@:OgfUpOq[t{9)#VcT R`rR< riFLv]h\M$,F',,}B!ܲcR GQY|쌳8rT R 1Mfj.%>$}7hܙЋx.' #/?&8e(A! gA)Eݑ mP:--t$rTJQY|쌳8rT ÌJ5HܲXt\g_v -??~ ӗaSJsrRMAb<m4/uNtJFG>bR'&/O֙:%&'H$&NK1kӇh)ΖP<2?F|>olM F$ɚ2N22꜉A"crٴJi3-;@+}>6N\W^_#w5]=6;(4ri`rC_yO$rKBSjHPzNB4h%ZdP7٬ O|:-u؍pP&Q@YepLx( )/HViODbCz-jŔ[`+''k2*˘axYv=x,syլwwF"AIM$Q@YepLx(tXX B?:|>Cl `) 2꜉A"cr8طFh]ymS'ߖ5Hd|x:#:ѹSӣΙHJo :Ju2x8DI r~9 |d%Vo {y>Hd|x:#:ѹSӣΙHJo ΐ:qtu~c삨y/"j<ޗ6XU.n'?qG·xD"+_ēO!'f:1};jFb}7WE<9?\:/ h)ڢUJ>gEk Z=X=Cby-p&·xD"+_ēO - =y?y?u~uDR|7OЉA*a?.;y6vk伪@;G8 :.z=1ov>ă%_y\0K|bK>youˈAۣaAc@@'HJ; @Q.sMj!$$8X fRF.q -&NH$&G,AC A=sNYeMe x 2>%H#//KɨS"938 {~:K '$28Hp$>N;;v'|N'k|V`X@h1CKG^^X3)#8QDrfp$&X6w`5BBd}%-Vok^a}Me/dD ;MOq^a<kk%6`+o=&4! ADz @z#}!*<*O,AC A=sNYeMeĂ[v@8Aw?.y<57GX4j+ 2, d|,KG^^X3)#8QG;X%ke8:Vha}3lyڼB[8˼ӡ:fV%ioF5Ty! ADz I:\4 r<.<#M½~cvt< KxKHCD{9>xHs,M܁sNJ>:V -ǚjwqiF%h)윎IRW~rNR" RB) 2x;ep k&<9;<8&;kNOt"?#{耀\X`D"윎IRW~rNR" RB) 2x;ep k&<9;<8&;s&4%N9y띴~ńwYVV̺bZFaOANIJ$d\J 8$P~ `5*UnweugۄoI_IsrvxpL#w>ל4w>8m~XUV:[-YAGXI@@@qz>*e|' '$% 2*MI6>xOq}'^76M'g:IDK $a VN> BYCO'KI$<9;<8&;ぁaq' X4fi'xUH2kp-ARqv胜H$ȸA"q -˺*t>NNTYF; rg N28&pyJ2*%Y9p SS_CDbӱ`2ȱ;'磧G#Gd耀ԑA?P.YI@@FDR\:+'W>4Ap -3#wC|(HLp:VLF9Vϡ'|ynr]=zc kbe\r ϱc%I rr鬜\8_)HΌ\5ѷ>2B%N NJɨ#JrR0`QMl/-l!4 f(% NJɨ#Jrruʏx%rbeYx FNdxr鬜\8_)HΌ\ީ/eD"1-AwZ#--3';XΏ?w66dXg=$ R")A@N. g+ 8IPN.)f.QGG_yPJ$QG1t̀caҼ60e5CoP2QDRəw3V_3H$&\bgy4+QfrYOUxBpp69XC<88GI9*KLHp$g9XI~D"#2r 9Ϋowν.>1AxM$dX+XiUe9<},d耀GruŁĻ=-D3H$&\bJcascv߾v|>!YIn}T d4K?إĮS.#?aF2QDRəw3V_3Xఌk࣭ CbaC:X?ZڐlHb%FxBX|(/M2>P2QDRl},uswhˁkzx>T茕WL$28I(ާNx^?6|nϜWɰm,17xBpp69XC<88GI9*KL`=c]ΤC@8"Ph8uɰa׬piVr ral[8\ߣ3A?睯 关ADzAYrV+1lTû8L'|w//h")GJLArP2223/ԔHP4Qɗ/O$$C*:˸{yAI<8Pb2c䎇"yD"2Ww|(ON|yr 8K8ۣˢt)u' &k]/JغM`IArP2223/ԔHPՀ& osӽ:2e˓{gxD117`w?KY[яվ)X=c^Kܽ r(1ʱrrCτ_qv}mw}sWƯz??[xwSɗ/O$$C*epp?* j̶ -Lax( 5%9M|Cyrve䭕5vqG)C8 tk&!eXhC 8P;\F^F^F~慚_Q m9OI ϗ -ȗ'NlP}: (m*۸b4#Y%& C9VNx(ryyjJ$r(r KjaZ=jkC@? -_MEԔy!O\ʡ > x3Iw//h")GJLArP22ri[1iSkvЉ_KM;3fsq)?=|1`H%3#':19_s$H~*#_.'5KryrL\`r rA$NSK9QDr,<щd N#A}Pw9YNI_dF'?18nceg}VGVek(c><)9*K$gF.Otb>9YsӿH[r-oVҦz{Ӥ?| ,ANt@J0N}F Kg%[7kk,.xr\qO:g''%3GeN̉%E4]]o8^|_򕹭ҡ 2%rR9 >*'D%&wz 4A[;8G7[< K 1NiMi^DrfD'擓5'8+zOA>CeK-4[O G)ԁ |ႂN3~ZҵXŔ¡w3̓əDrfD'擓5'8#sJP[LXO#=TF]Nj 'F\w !=쾈Ϋݚ;-;oiAƥg}|yR"9sTHΌ\|rsK7dot8: ^߫ݹq Ece"G;=tfpXsXv&帔uϘO0OJ$gəK,A?-|>o$)8L81W%8,9&|\po)AJs NGC5P")D\]zZ<%rD"8%|\Op:Βc % fd$1' tT9q{^X %r( HPΫN% ߥFK}ws\kU8?gu']%RZ=pL$yެS$3aq f%=_]X %r( HPFҨ|:nhϧo۵㍑HX7óhQg9t%9ApK":Bv#:_vO9x-m.PwEyaM2HʡD"/h"C9*VG\"H$NEG8rH6kd0o.%6A1KsE.D^7 c%9̡ šd(V/"[jnmpP߀4D?V[%knh%2NYrL$yެS$p3>J3 O%{^X %r(ЄC, yiy&m3AY'8g1us\H{N 2VXCvn ?Rv8^8y$|lV(K$14AJϏKq NYrL$"4-sNpfp@?{W|:1Ȇ$ \KyGg9 ?s\JD1A@28r$/t(H.#H C rR3;E?&NMӔMY˥GZq)OHʱDӡD"A@ʻ*ZLKJ*4OJ:'H|QGFiK w|>Zvը7q)/,gK ?&H~R_GR$rv˗nB~}&kzeooH$28P>A@+:%3H$y"A_%,2ݧ }Tu)٤J#'ep$XI"_P" c]G"Al=-/l}c+_Þ|>_[cOb0%RB' ԗc%|CDI$VգЇ*A 0ǨK gPXml^0A_%Y':L8 +I8J$2޶ \py܌=-$ͮH$ygS< !]gr9.=s|q)OHʰ JB:z*z),T0!˨S")A@bH $HM8ewA2)++I CyI(A@FD"Y: j._I:%$ raMD^QY~Gd(1A.c~"2KLA0:dTJtvu;@w&J7~맳YMS(]nɺ"X -ʀX.ep k$&H2x; C rJkE2ZLpO2I 8&C'PJ Cn_eUC=xn\nX4#GR.Ny"A"/\X 4AtTJnaJtk'y"IkxA epLN%&gɇ#{\D\Jb-8g _8|! ϟ}`}w&t%7Y M8ewA2)++I Cs-x.'k(45Okړ:nKǢ< A.I q:* %&e/R"l Pjz%& KLx@`iPQ0NIS ɨS")A@bH $HM8ewA2V:V~?_W$)C=t -."A.h6-&&˨S")A@bH $HM8eaYbڥn܌)a/ _(a<Jɉ&ɽK,rTJ$rTqz茳8ə# ԔH$s|(ԗ .ͳA28HI\<8&˘Oys+'' '.ͳQ)A@QY3HJ$g\`rRS"HdIS_D^^4rN 1'Nδ$N_{KSx8w.7cAiM[rTJ$rTqz茳8ə# ԔH$s|(-ZXз=pi2Abnd'??v妫Q4N6M{Y娔 H,gyq$%3Gn@`=[Xwq?j9h^|>0;L;ީ/A"//\g9' epxpL#1 $DDI DE{>z&-IZKIi -DgőH 䤦D"ȘCypi圄 SO2|O c5*52ېJBSQ)A@QY3HJ$g\`rRS"HdX4 |x_D^^4rN1@6m@bwxaTyUiA;4A@@N\gR CgőH 䤦DbmAZ$|7ts^'y ) x2擡DC)199wpie^J D2NqGR"G:q"?y[|esk# l 2*e\Cg %IšI 2*e\Cee 7~um^'O `F:ˀdTʸxWR~=IyA"`rCa aIamO[M g Vq^ zeoKLR 9|8 ': 3A@| ~aMbdJ"M. BIЇ4MA~C A@ Pxh(A0A:KLR 9|C-B?|IiCJg+UV_I{$eG< K JK:ꄧDAr&NHςכU{ϫt86fڦiߤ̎f-_I{$eG< -SN,N/ʿoeAwK7 aMfgC#=t&P"/a04=I 2*e\v"/HL|P)`=aDg$ :N_"H k^D"2Nu2N kd}rAu&c'wJr[: (awGGb 1gD.cNEӌF'W^7I28&_ ':c%dԑ'u8DR^y%2NBǖ %~&?}~' -ÆϬϻ,?V_i}>neN%7I28&_ ':c%dԑ':^2NAL$Iy%aM w:@~F Fk5lx)3/;H$&ps:':'u |E'H/ WlKg|||})98҃2N?D"k^"A@pcCbM2񕟬OEe wS?ҏ*l#Y`:ӡ<9S+:aDg$ :N_"Hʋ5/2)a -"aȏ{_'jx5,Hx}dD^"|yP)` ~3VLIJ`%Hgbd)GA"1jWZ\LC x9)H$&F—:H,5 8Hp$p$$8PF^31A^fZ}%-o6eKow%AbB~&Ȩ,qI9 'uCpH%耀A"/_yR DR?:ʸt#H'WG#1 S-`MY~ -ߎs`Fh Į .JQYr(AN,I91_K9 D^zÊ86_l&҂_ -C2Oo c%K:$cˑ,*ό\>yD"oa6|^WOn&1GYv=q ld>\)8\5_qRW~rNR ɳs " ^U$|r<3re_`S?ߓ;ȂoOWsH.g^8=?<ŚəO>gF.k"Ot"?s%O1 G/~!('<8 o?0'78Ddʓg 8%8%;D@@@.L|r<3r9o:o>d E||IfOV -x}ˑ,gF./*|i/ހkl0xBxti%1H_qRW~rNR ɳs r̈y!l;ɟ?>I܌Ut9'43YrLt 8$xpLb%dԑc%q:/9q$2~ć&8Kd N282֙\c2H$%YIq De c+ &+ /b ہXo_+~0 O2]o(Ku&:I rruV$r/q&QYӡ|(%HJɨ#Jrba-+w^$H$xpLb%dԑ^PSCO4p?s>Jǽ} ^`ӡ u&:I rruV$r/q&QYӡ|(%HJɨ#aN~?\6.F-][ƚpE Õ O֟t()cɅ>&DR\gyKITa -Ф[A?Y+ahf:X`>}X]mµ^8/9q$2~ć&8Kd N282֙\c2H$%YIȡ40b'{wf2󛂵d{ -ʓ:'gyI$ȸʸ%HQyEI9*KLHp\r9=t\#tA"1 e8լ<$rϓ3Jʓ:'gyI$ȸʸ%HQyEI9*KLHp\r9=t\#tA"1 e8˹]7v6~ˏ/$O(i:1ݮpJõC38 Iqq5+K̋%rT ʓsz GL Db%&0J Jn=@RZ0 ƨ4B$PY>9H$K:$AUլ,A3/jHQYx5tZkT:A^zt*_#ios˵WL哳?O+ r(O,Ӈfp$% *jV G5Ks*Ol[m`Tg\ȗ HK)A HD"U8@NءLL0Oʸ|T Uƚ9S~c=9yA"A" 'WGd؇ݓc6>VF+r]ءLL0Oʸ|T Uƚ9S~c=9yA"A" ':fŠ 7 x>ox[F8A@ȗ8r>&g\l%gbyR売 >Vz4 6bx/Fxɫ=<&% 9: C <8X9?ѹxٌJGe |\eH:78!8&iK$$̻AaEo9UU@|C 'N`rqfPr&&'e\>*K*cD)K&el,3'm?c nϷU1lw PKJA 3<A"_ɳ \}~s; I$O-dh1' W,9P)8+9ʇ٬JLBS Hq93A@@@@Fe9 /qh(ԑp#/OQY"'되yd(A|CDIylY%&)$˸ə J84O{oL6?a×6|F,oEqr9.ʥ)b|CDIylY%&)$˸ə J84Of"qCaMJO'7-ȱyd(A|CDIylY%y(asp/j%:?0yĂ;x@ˑ 2*ˡyCC yyRG'iMڦlVnܞ}LPz %ӱ<|h )/:ˡ$8dy#9dTC rba%84{5siְsb  bN@.YrSp:V's$ųYg9|U-?}{-6 bA{uknr턧\:r|\CH,Iy DS28r%yrH$XH|uJȗ8%8:I̱r~Jˡw|\CH,Iy DS28r%yrH$XH|uJȗ8%8:ILo(ݐ3x>?8=܇ 4Iy DS28r%yrH$XH|uJȗ8%8:ݭZi iYz|7o_pp nxt|\CH,Iy DS28r%yr?i㓷U2qx 䨼d(A"?3%N rNR"9sय़r(A݋,{Kw0'o{@a1?%N?/#)Yr'W_Dʗ\*Jό|S78tq|*?ö]B?(k^d9rN@ Hd1?%N?/#)Yr'W_Dʗ\*Jό|SL=8lVVEmoesp[D.-nס_V +*J$Pe<]r(A@@UΙ': D"AXICs y]~`ɸ'y,Ѳmg7H <8SB 2*%J\K\R%ȸJ|9D$^f.aRG>O88z4 <8Pʘ$eK$%W\Gϒ#q:: SY^g]ӋnR~ox"+ij.Ol%.)d\%yy KJ$O$xp >1'HHJ͑)3wfӏxZqԌ\?}mK.uJJQ)H$Pe<]r(A@@UΙ': D"AXICs yL%gsܛ7ְG"xJ(AFD"C KtKʡW:gD^)& f-Ix ov~hхqqd\2xH$O %ȨH$r(Ap2.qI9 *U0Ё62ڸNsyR+r(AŚIy9*%Hg") 819Y\F.A.jJ$9hD.ANYΓ28ȉHI~q˘OH\~+r(AŚIy9*%Hg") 819Y\F.A.jJ$9hD.ANYΓ28ȉHIvpȶ ?^"~n r <єHQ)A"?Id(Y^I2x2r rQS"HdPD$r rrurANt@@0,7(_ۘܲtcn!rLʡk&e娔 2,/f!aS,1hD.ANYΓ28ȉHI~q˘OH\~" <߳7z7t|}a'mơ4 cL$epgy9&'5%EMD"1'CM%9yR9с ۯD(97 A ,>҅,] pbͤ$3Ar嘜,G.# 5%DƜ epLN4A" 'W,IͰuND^9?1`#'p9Hʱ2)$^^RD~&28Pee䢦D"Ș0q%}I70!aÔ0x._r˘OH\~+r(AŚIy9*%Hg") 819Y+O;a{2I_vQ+G,.A0AJ\+:A'gtT cW@pJ\D'rdy<#A^ҡ28H%ȉK:t&ȓ#8+:Z`|9 C< Wt)O,A@"/ɯ#<8ȉNDIyRGCepK0lho{v}B7 ?2%\2rySQYD^_G)qyp%yI jjۗږoLJ7xX |EGKL/d(#':1N8?%HX9xLS_#.ׂ48DG<;$*P'gtT cW@pJ\D'rdy<#A^ҡ28HpU> ?pe`xoi-K ]J\+:A'gtT cW@pJ\D'rdy<#A^ҡX,ǤVpjoxְxKL/d(#':1N8?%HX9xyԉkz^ÐnN`MZNt@^ҡ3A@}) _ˡNpL$NyrOGe y9VNNok+-w;ge?_iN~_}]'-cH'H$.)ЉJL*3ANtNC'u$Nd %&ό}DїS"/O! H8=t@.&yIJ$2yRD2Nˡ$2j>DN_7t ~RGLPbޗHʋ})8%d0 _ڟ}}=N1 1$_9LA":C'r(199SW &ԑ83 ?3%9bG_ -N n%nc|o#߫u>oU O},KOmehHK$2yRD2Nˡ$2j>DN_7t >:%LZv3z[F3HyɎ~# M$d<ˡA"A"/A@0uD"A@HrRJ$'gG./֜:28jV1o+;LbɆm>>>>00@/tL|pE'J|# M$d<ˡA"A"/A@0uD"A@HrRJ$'gG./֜:28Bb0rPZZ?-E侦IxvcQE^^h `>yɎ~# M$d<ˡA"A"/A@0uD"a"L -5/&||~'طAqˋ5' e\ΉNde^04e25 !gs1 M$d<ˡA"A"/A@0uD"A@HrRJ$'gG./֜:28.̣fMB~Q[ Od!Z\ˌ%;D./4H,#/d9A"q -Ir(,}Xs2ȰZuBzi{vގ o=o@oÆj* H$&O^8HB2r(AHKLF3H$蘸TV)>L?>`>>Oc4 Հ%, /OtNt" ,BKv\^h" YF^% y ie_Z"K]ǃAD^G"1AR" OˡS| \!A7e^^Ԕ O 'uHdȗq -y+v9|ԑ DbD"AԗC|C 'goʼ)A/AN,': /$dW8 LC/m9=筑 UAR" OˡS| \!A7e^^Ԕ O 'uHdȗq -ycIXĒv)"nk[H!֧"O >Q? :e A"/# )H'P)c>瀀D._ Ci -bK4C'֣\/AN,': /$dWr%?#e7gww/$cX zvo瀀D._ 2//jJ'K:K$2H A_]w>Zt0<uhN ': /_P:2xp HLAH$ȓr(1|s@@"/qd4˦-KOY÷=J~C'B-fw ֹ$'qK\#Y~y<#ANt,AJD嘀 epqs2:Hd'O9L$Xs\L$%H|3* 'u'W,_B+Σ<|I rd |TJ$2.d(qA"c>yIg"HŚ2xr{~~ߘɟO=/tJsP] &H_qG3OH$KR"q9& Cd圀@N<D"/j ݾ?}~Ssoe*=4 9S?:g'H_qG3OH$KR"q9& %&d-g4':Hd'O9L$Xs\L$%H|3* 'u{}4CjMV0`d'u$ȉ%HQ)ȸp 2rN@UBC 'uD"fz![K2>aY&uK >G(>*yRG Y1 ,d\%tȘO8rRH$ iX-:lh/i)W y;0 NK\#Y~y<#ANt,AJD嘀 epqsbCiNͬNko7av?h?~i& - #?I _y28I29˗>G(>*yRG Y n˰#hIv*=2<׉@@J娔gb'$DR?yvF$H\D>AR")G^`>$xpLb)dS@09y6yK:d(+@@J娔gb'$DR?yvF$H\D>AR")G^`>$xpLb)dS@09y6HqisE݇/~dz}>MеHLp:Rb2@H$I q9DI y PJ1%N`yX$ֳ߾4кusE<ہ󤞚'$8ȓ'8Q)1N H$ ~I|#B~8&O[y=|(%HSp`rl9DȓuPW' >x_? \ZCOa0#a'$DR?yvF$H\D>AR")G^`>$xpLb)dS@@#LF~?C>Au}ⷾ:\ -d NrTJLƳsD")<;C$.G"胀 )#/A0C <8&\Ғhm߫ @XϹA-@@J娔gb'$DR?yvF$H\D>AR")G^ [;%Oo ߞH=⭱gД'u<%d2 c% d NrTJLƳsD")<;C$.Yfv["_(lX =<8O̓ANtG^~s,AOL$%&C$ˌAɕ?3$ H$ȸʱ~R"938EMD3J\ ':#/9CrT 'Qy&se CC DbJ$d\X?d3K<G$D6`#w CG9 y=JL.O>3 $/3.J$WJ$P Zv.fS~~m43J\ ':#/9CrT 'Qy& ^GlZ$҂Δ <Aɕ?3$ H$ȸʱ~R"938EMD3JE&8_?0h:Yo>D3 $/3.J$WJ$C {u#k7]#4TiXI@<9;D'x8g(1X?Y$^$*DRb2t@bNf38T-?QJlk+p[~ȟH rQS"̱)yrv8ȉNqPbr~%HIT.M4I]k5i|&u?OlU[=P%3q$':t(D:N<2NI'H$r(%r(Ap -3J2*op&ɕH C\/1 P"9,A~K<墾Wنt">wuΠ݈l%;4yIGK/e~Ś8:*KLNep$28&Ot,F>K$VH,f (T--D>Gz.y &8y&G28KvhG qkN^,1:qz bvnGỶoGo- fSPCi$HCS.|':D%Hp$BӋ /)0ܤ_h/a= O6 %סϼXsGGeЉӡ D/H2r S)L7WQ>Ws,e ܵ\,d&/h %סϼXsGGeЉӡ D/H2i`+gӐ4Cvkx Xf%;4yIGK/e~Ś8:*KLNep$28&OC&TM.ǿy'rf .jC1e2OtpJHŚ$N5 /١K:Zu,3/֜QYb25Zր5җ>XBV7?Nz1'g9`NCx6\ ds Nx28ų 'G;Ͽo6|@>t܊9aNg9`N)?y >Hʋg\K6FY/Bwn-st|\%!HٜO':':1'1%r8ӱ2ʤS~'8yyS&Wyҡ Dߔ߻ >$l'HO^I9tXceR)?_ͼ<щ)qXD^K۱}~ґvuGC,)26#4Ni8 D'8D'$擗tDR<gy:V&2X1ʏg3/Otb~J>4- X@rI&x;C4q߻ >$lΉF^C'8{O^I9ten$ ۼ}4w<`,za1lNOǚ$r\I^&||S^g/ a7)ίw?WJCd3Գ':':1'1%r8ӱ2ʤS~'8yyS&)6ѽ"Nk)w5n00IOmak,eqX gs>A^GRĜ|򒎖Hq,OD+2OlN'ג~bl{oux"k`Ji̡*A"/|< <щ9%-,OY >V&e̟bf\[.;XОWo!쥶t_WݹimʓL$2eqX\gs>/Ot#)OtbNb>yIGK$8ӱpQ=$vA~|ufKzVz ep$I D" $%ȉN\Dr(A@@Y'uEv$*%^ -&22d%DR\lJ$2I=sJ)#HJ$%&,ANtr$C 2Β?s/&Q)R0$xp >1',y%estT,>m;@H$nBz!)y 2*KLY H$.)d%:W8$&Q)R0$xp >1',y%^5-Wu[۝ɿϟ¼GSAҳK#W:ep$I D" M%s/&Q)R0$xp >1',y%&ҒҒ+)EZJ)#HJ$%&,ANtr$C 2Β?s/&Q)R07̸q[F&o7*EG%9s\%H$%Օ $%:q9KʡgɃ9񇩷f1GR'%?9* yIyQSD~&&' HdpL?3N''5˓:28N9h Hd 9rTʡ W5': DDD.A^ҡg^Ԕ ɉ&' ~I SNt0&&' H$<8Ȩ,cNFv+ÏnJI'q~X馨!#0l%K:̋$319A"`Ot{LLNj'udprE319A"AFe-oq#mEoyx駟|eCɚM4\"We"A@@" F [zh( e#xϥ DI3xA}k}>: ğ% -ijcrQLLN4A@@H$2xpQYƜJ9*PGReLzY{}fW[?I"\ˆ!/jJD  g?щ9q5K 28N9h Hd uoP8W/_}CPXH%K:̋$319颂`D'擓I`rQLLN4A@@H$2xX--a7-OUC4AP9* yIyQShA"`Otb>9YԑqʉN(M}jO+-a)JKQI𨔣Rep$ʨY?%r|U] nF$K_`+˶-eeT7!4|i\wRYeso>'Tzg^2Kh ɉ&; ~cxNHt?@y>^͝~+ب^+!{\:/ANt&A0o; q: HdpLQ .>.%t"/H'u$Ȩ, 帔'=T~Fʓ:2N{-{\:/ANt&A0o; q: HdpLQ .>.%t"/H'u$Ȩ, 帔.BCK[o2 77 cK ȗ ':HO 8JX<ͻxT`K K$$xI 2* 1'V q|Zgd(F;oH2ЉHeC%cܰJx:bC$N oti&1 يck7:H$H˓:dTAbVr\ʓ*?GIǽ6ܶ|Oa3xC/{Yorb?ILH%ϰ4{`ӽڸЉD"AG^ԑ 󋜋 BW]ylWӻrCiL圄ialO RB'%ȉ$&Xt@t(AA09F^$xہ)?]0ܟNYA"C@N1'':'uʘ?I 5KwKL.O@%&wO$H̓yRF.qR笓|A':eɉI2iB$ { $ A"cHgj_jov.~;A` #OI> HduI2*NHk9C<9i%H;%&' qI;'$tu+K|>?>9Igo` _ws!Y6Q3sa#4[J`:^: -n:e [F2$?^ _tMDL?rAݝ HܽsyR 1O%9t\ʱ[9';:F|_+[-A':eɉI2-2<P ~w$^b9 ~gD<>._AO,7:3P7F76笓|A' cNNNs{ rf N} Ʌ Hܽsb%9@v|>?~% 싵|vt漳]u28Vt_99'uʘ?I 5KwKLEAڝj !wB:aȯcg#i< 1O%9t\ʱ)rbub9':nȚgj^auI2DNsx&(E__dlx<>^8{|[jʡD"A@@ N?_.C'8Hš%P")ǽ y ɸwPSD^b{N",$P ep$ep'u&w/qʸS" c/q$rya d(^d;)A"/1='Hwur(A2828H:ospm@>0~>AL6Oꗸ y 2 /҆2,9cQ /%y9DsﯓC 2AA':p7,߫wDpm\J)HrOK'Ĵ>w~ NBߣ]M^Kgދ{nI9eKB7H ݯjepԞ/>츧mS~xAxD"Y޹Iʡ H OM$2^q)QG[-k/gL8XC'8Hš%P")ǽ y c2tBM y ?s$dy_')d( )Ce\Kqٍ 3?OZt9+~ZJd%2tD./9o~ 2,/ 9= - <1Ʌ$/sdy_')d(FWS /VWi_0l RND~R]NXHN4%&K"d9 {5%H%&_$-' 닞`,ȸ{$.SƥDUD,JIp;A/qq oZDSbJ$帗#/A@0:6bF^@rp,28EJB}9&Hd; 4A@"_qNc$Hdȗ8e(;w.289 ~ 8HqyID"%rJ\$r rNr} /Y8Kr~ ȱrJ$2xK2o;.pvWv=~~"G}}pIsGR"H\/A б +HA04{ŽW9gA/Ap9VNBD|S2,n+3߈;wA~K ; `|e^GR"H!$nI ?AgP+_l -x_ '$gч p#::?^/2l~gH$.R.ˡENKnEVp$gzZЇ g,9ȝ%."I(VbۍxNc_^8d`S$& Yep$% ?aN9"AZ OE$I3K re^ wM.-5AWzU -`h;w.289 ~ 8HpJ 9,gPZDw;i7* -Rdɏ zY/kTD"\yG& %X{*j>[;{$ΒG^q)d9(9I{ԑ C<c|(AY -=ʷ!<3qrT HPF.%."A@0䑗,d\J Y/JrR'u$PF.Ot@FD"☀Jqs|K--{L\e H$9\]]3Z. Ewk$1' RBr|Qr:<12$HߒjЁ7 &<q/%og!Y -=ʷ!<3qrTJjk៹t]<xGAD$ΒG^4ImT ?^9n?xݯNj D?.s6JS{yGxw # eDdTJ{;o9\:dx|+˕ׇCRC9,dxHL\e H$90'/%."A@0m5i'_gP:1.RZc9(9I{ԑ C<F"gA{< J/^V!FzK--{L\e06ZU.W37c䑗,d\J Y/Jrg:aB\YÈnZoOy3G~\};09A%89ARC=DG~&Y,K_FD`ҐH Yqs2.%t,Ji9tv w?%u Xy2*%$P$e9,<ԧ9)e+xaFA"ȡ\"K\D")`gi" -&Br Pږ:ُzz.NԹI 2Q)ȸ860,oY--{xٷxȨC rg N28 2 'JD")A28K|orr9V8ʡ J$Iʱ2 od3'J9 w֟t(# C rr$H$d(c>YO+''c%ӡ ,HD"Hʡ+ HgtY_pnV//?1L%ȝ'8H$P\:+  bdBvؽq`Bω( Gߚog,|. cS5ͱPepHH$P"HJ$el]aC$Z[xn]{n1eOr(Al:sCgp(A<*cW(Pڍ:o* 2 I%/2^ŭ˛:pgn$YxalyAEMA@t(28Hp$N" CD")C9V&0rT'; Omwx{t3\y{AD %ɥ HJ$ΒBҢ>X6'en#WwPepHH$P"HJ$Pfc]~ӗRxY+×˔=wLb`ZQ)ep$d(ANN3KX(K#NHi; M \)mJ R^P9D6 ':qz)1D[OJ$g")V@bN3ɽȗ8$p 2rš%HQ)cg''Hf\D'x5^\_7fcЄ9?˸8ȉN8k<3x:M+AZ7iߏ-;^w({ $ǏIzS~ڕ8dØ -\60{?w|rTF.aESH.cp«v;co-K''HfCp91:OoOq&s7v럳t$0D AHvz[&}(3$ V!{%{'N hp 2rš)4'ң2tKT9VgOOx]NHa:qz)1Dܴ\XEiVڷ>~) ^KɎEo& iȗ8$p 2rĴ.W.4 '/]$}n{Mb$"M.19D6 ':03,K]EK,r6vv9|;T] jx !^˒ޢ &C$=;Ќ|S@b>iEKV}^|M'Nn $*+'ON4͸8ȉNL -*u jp=3|Iwn,-Oॼ H7d̓əHJ$ ~c A|ɯi)׮GO\XsrR >*epḺr̲'u*:4]e%o!x|ݨp\f:}79py_nTa8=ꔘG"A'%3A&bEJυ<׿!Ǐ넗oK L\ %.RrJ$2xi1O䨔 jJSԑt(r LH\-=Zb\"'o;R 99D$8H2rcd耜C yxZ9*%ȅ'u$&8ʡ+'Cp$)1[w?57^^{ Ѿ.II9D$8H2rcd} }v[@|^@{6d|J;ovKi?%Lb0!4KXc)OHLp:C9VN&:vBm_ǹ*j~j D'x[J$g"gTwS|~5lsr-rnmm1m!xHdȥ::"]<-Kx|Vgᤕ`B돟']U?8 -^x/>$11v:P 8QKh]z}}:s?^&H^k?͠4OPY 99D$8H2raNqNB,:pxX'dU"/u{#?^o)WX/‰i BM rrvXyJ:P;&۱+/^+. wSom)AyĨe8Su8m,qIvOKxuFp荶pL^'rTJ 5%c?N!oNrG)1yK HNҴPpBvnX+I +I o-֞Տo+^> K[#h^]KL. ĜN<׫Z_W(muw 0O Kx&4+ GV<eГ}||v|pZ w"3 XIy9axaѵ傝4i.%&ANB009l#^'Nj<+S~ܕK9c!ri Ffd\JYepL,OC) o!6Ӻlk|Ӎ[?_ٖOcf Oo˷yJLN49 KCNLx'|6C_Ku:c7,Qv~͜ %H[]N4K:f5O2,$>NWҶ^JLA@@s@@['Hg8yz 帔qo\"%HNJd\[9wp6Or(΅ϞYdIJ$r(1 o )Ot@pʟ|3Rƽyvr qz;?*%%2ro<ʡs;Å{-/_duu[  o )Ot@p)mURQmVΰ<=L.}nw aNCGZQ),x+'PUy}nfu&?{yG 1JLA@@s@@fB>Ş|N9=919P(eAxMuL}㽼cio|]Y"K&vX4%2r &a'P{1Lln˯sӡ$x'9|?Y )Ot@pʟ|30'p8,ıдcc:NZx<>^\>%Hm'C7V )Vb6iN /#VNӡ;5t,rʹRhlTZkCqh٠/O =ɞHP -2:'Z -/oG.iB5 3eo;C9.eܛg'0=_GR#>t` H2X1GD$N?яydͤDR3YK$2rɉ&HpS@,_8I(cN.9D^KyCo$K,$r|LʘONfy~ }k^#SJ$e''gQ<OÜm>ƣ_ܠZ+uKx<>^ߘHL?~xὺ2L%Mۙc"q -NA ~l'9܊Zܮ8봽aД,$r|LʘO#Aӧ/@x[7|[;͈I&DRz~#^JZ7iR>_+/Yq!B ,qtO S@,.>tXe[8&9sy9f#2Q=drZ /f(5 M>P|E<Ӂ# -˗%XiZZP<Oc,19Y3ilߏ3_*qM&ϰgx{»8( ]0و9UٜOBsra $!om-m3}.|gw E.I<},Ot@ nfu;%lx<sgsgit7N#O -!,D$8N) H/$1X_(>mK!].aÐ0K,$r|LʘON4 v#MB5tuu}Ik0bHil-q\br 66Hh{wR ~<9Q&NH>׉D~;^W>]}R i0?c,19Y3)q`ҜΊ3HXs|F>EJLBITJ >HdȗH D%H̓2~RCzZD$ C9r wOJ oG%s渔c}  C|(/qJ'e9~) H$."A@r<)A 6GJ$p=ꖠ+7 >G/5AJ`JLR eD"G) )A}{-)^Sy_uC'PM6A@@H\D 8yRY?)A@`xXug[j?vt^zB,ǥ\$I$aH_x<m94y nH$>xvqRVo=_#hE"ď2XgnXRid rg9zI`_q04F5a-HD"d(YΓ%,GZ yHz?M;o!sr CNrPGRbrREΌ\\:QYJ<8Ȩ,cN.h"Yep$x[D'rDDŇ-=$cNN֙ ?d(`2IHJLNjșKKG9*"Q)eM$KC. t컣_@ƀH$H\|(C2d AWN[Ծ߅EoMNZHiuKKG9*"Q)n$vElױnt+{ƐOt"HL$H\|xt؆ -3 v[' ; NQu$Ƿb~|,ȉaH(GG`8 -nX,d9*e 91vSKh-e% eR''HN$k(@'N9>+,Ce:gb2t epIgQq 3iaYwko%|qailZ3ꖘ"Q)eIDFx]R,uWsI;fnDDŇ-=$cNFJcak_.14ma!,93r rr(Gp\d9*g'sC.MgjTD'rDDŇ!0󡏼3DFg9prAa=jHcfKNep$%&'5K\%ia җ-֪w޴ I HCI9ޖg?AU;-}6+39ᆷLNp X'9s(4 AeK}is˔|K<1?q/[ ,$#/Ĝ ) H  ( 奲Ÿ/%>^!%>~v>^<_Y<ӏ{A;tHZH9,LD"/,Y^< >8auϟ|z[|>^éҨps!2%%H#/Ĝ )0gnk_ċx?v9:Fz" ^%HD i|܇KYBiii:~z"`#)w ȗGe IyRG"Y^:|?j#19Y. uH\DQ)A@09+ yfy 8H%HQY yzBMD<)Oܛu$r|]bɚwpoC': 8-=TJ ɉXI 5KLA@"/A@G^_[xCjʘ'$Iyr`Mjtv%]]:ox}ehzw3`r\Ot@p -ȝ+MW_ExF2mwDZ, -QY yzBMD!m8>n/.r\J39Y. Φ1c/v)H]_%h\0d=iA9O=DD8]~W;|7lV8(lyBMD<)O.@xtAy</ZlAr;)lo'JLn~:+P9*%&'fm 8}U']| ӣ;[%_|.Ԕ1O$H̓rLĞvW+xر{@taͻ\7!qNK[.?V -?E.g؆'ѺJX/HQY yzBM Bp8{ ,[襜N`4p2ݛj9.dͻ\8v :F g vM |35&vv AKLA@"/A@G^i81H?uEmC졖`ER`r9V$B2ThB81K|</~φ \:d\ʯ%&8!2V&Iw A/1!Eyz|љD'xp$epQY999;y9.%H2.%.J 2.eH[+ r\bK wꗘ<=LBy<828Ȩ,ǜ 5t\}I~^ǝhw5V -%SK5=C<8y"/lݰp;x<>^C|+Eyz|љD'xp$epX;+*N3iï}sMٻDKKRKc' y>-ǁ!a0٨Mfyjw3 BcՎY(OtAReQp҆q[gH[2^gOX21PD耼e2 & -n%,DZinaGKPFi[TX&t:P 2*K%܁4ktR ->DR0d\ʯ%&80XpX'*N狭w`<^(. l[䎇@Egal8(ߑ'6лJԑ8e\J\:d\;f z#.s0:+|_p tOy</:y̸ r~Mk}|5]feB-R'H\999;y9.%H2* vIgTi$ǥZtp7iu`~ ce"1A.KLpw #o'}ꭢ\YS~H$xAR"ʻ\X?)1/d( o^X:?o5^qQ 9s{{@퀵;4<䐁 %3qrT )0t\:7suSKޢ5QyɡgAN)TҨ}B6bE7a<;Nl -pX[J$g",A5'\%v΃| 6;-956OT.#/\4+/9;i阺h[Z<^wϐ/Y Ri\")-{ %iAqqP,֒ॼ8;737ȨC\F^%N`v +͹:7,SV¥ [Ò#H$ -2xpLA@%쿴ro{TwI%r(` hx<>^}0O A8!Mm-ekH9*K c%t}Ê$[Qp2*eg^YPG"ެ#H$%r(/cq/F.qp/ ,ࣲDR")2K"r-; HΌ RF~慚eep$2x>:DR")N22g)A0O  >*K$%r(#$."Z /&|{M%ro 2TF~慚eep$2x 6"_~y BZX{p/ ,ࣲDR")M GG˷o~^9@.@@F &&nl N_uydmoc{}K`OEv2$2,Hʡ 4SAqbD;lg%҇.28dp+{k3<' HΌ RF~f*4;<[5aWpJVC3zt |<&1N-]<_Bh3HEx[wA`09};?}|Zw SifBcQ-S")Cy~D{!:y[N&٣?; _ .Me2e2擓%HQYb9y=t&Nk<'rw֟,AKL$g|h>9wp9ȅKS|f |T`N0A^?wFS@b>ω\F~'r Mp閣w&bS5{]ToԾ -9|.(_1$g@[laNCgOAs"7-E%C|>?iyt[0Og")?Gah0ֽgxWaai  x<>~8_vpt;d|D.#?Np;t8-[O Va->9wp9ȅKS* iTnåh+Z| b,PZ8$peg -(w˱VWpѳ-V{" (?GwAdC::m9af(.@ RpOCgOA!afBoK=Ϗ͹F aÄ#?I=Z*mOq]vj}<:ꔿ3M8@,aHࣲs 2ɽ#Cx}Zݷ|qO;ONj G%G.JrEr@?~ސknlձr(#/#1,A<ҷ|VGr](?8rTJ$g"g\F.1㡡s`D)1yK8H$Hp$K$%Pd`H$%&8J$$> əș': A"KLxh(: #'uJLң%NE$ DI C9A0: "I .P_Jɿ/ _͘oB6 3q3Ot@D.# S9m3᏿Xn׷,vL$%Pd 1$8,A=|~ݠW:n~jz_3I9*%3q3Gl&n_qai{ iѯ0 h!ؒ9(1Pd`H$%<wphߓf?yn!: %&w<4w}F4*GӐ3:[oNC>_ -+tHXe땖 Nr(`2t@0E$i*svsKpkY(ye^@RJ L\ h72Amxb o˷9 L,;Ӈfr%$8xgۼ& >hb L, 9ӱm6$ryI|rC q>zЌ\]&IH7Q.Ͼ,`g9 ύkZ߶e{ܷ9egۑ%%[ -Ӈfr%ĐpR9܍}elq5K$1hb L,>l6b`<~l/?xax_XUsqg9g>4#CiB8F<|\:/􏶥/,$Pbr 82t0 Є>k.>l-Ӝy5EbXl.9 L,;1ıpzкȷ>pܽ0xgۼ& >?_ű]aNkJ;6;ܰL(%H A}>ޖ4 2?˱N!~PzQ)A.hD^ީ_P)Hdp\D^P坳|' K 㭜;8C9w{gO$娔 4A"//A_Y$28."/at(? >{|qP%HpLVNӡ;3\ .#w+<~;ח}a;Kj8 IS4݃im-#xЊ;VjÞ%HpLVNӡ^|-/b镽w^0&H%Hp',+>ю59Vj8x<' VZдp=黃"VNӡ;`Bs:..Xtj ؐvǏ{@.hD^ީ_P)h*C'|Kc:.}Z%ؐ;C9* A0[9wiHӂzOB} q.>fN&HF&$Brg٢4v۱x_h5.S_$4mgHe7{k71_%w:Mjk)<},O.xhZgrge9J ߂9a,G K9AIm8mz)|>?^n|eLu,t䅚ɥrӱVߢ)EX:^߻x/eKLǥ wK0e8j~8 -`Q S724AD.q -&p6O5KMklGBtޮŋ9^kz/@FJyRg`>.%}ն88}HMyxvD\RCn9*党M Kb$AQK |yHqv?{#W]vщB |B-Ȥ/LUDί*toꭹ$|R)aHPt5N4jMu!o[5ׁJ5'OFsiaTƦظ0c;_:ДD. J2*KL,P'% gJD<)?)1'HI 2,) 1OJ8=ꔘ`x<K$%&8K>,I %% R"1OŏG"qJ yRG 8y"q -H̓D$N:%+6\'@}u^injw^DÐHJLb%%&q|(Ҩ`x8x=ۓE>ϏOÞuA#OHg9O$NyRh={_X3׻x/}+ȋ?DR`+ɨFءƞ9=; pH[+Ӕ!{WĂ[y믴GMH$NyRh i`=h쓔#{yp鄩)Yg9OJ$%.1>&Ώ#)||%\{dXX(eS@b ': 17i8=>y -/M^Nw~F%XIFeI( Ƈa;|W6;f'.ŦV:H yRG 8y"q -HCKċ;{`^9yyy"HJL)&k`bZ7~S-wcHsDԑ0*#b.&K5HuJL0OW~^8ۿK1Nؕ%\J\R\M2*%D\_e;үE",oKDCc2tFpʡ dTkd9'$%.YF.Q\DF^2Br(#)/LqDD"O!1:r 8NArP2*DHʓu,# \bKD"#/AFesrdr9x<> -t@(!rTJ$d3* {_t|۟9?0=l`e \bKD"#/AF̃ /:oN:M xC9rH:$8NĹ80 G~g -0pl% -w vJ;H"eɅ&AvT82`sU}neI9b2tFpʡ =T"O1G;e//%8.eIAۜ -re9ޖ$ȸJԑH \M'Ⴜh>Y8q\IT_ I'=|tbJƿh }CFnwA \MsDRC%G~Ƕg{B(SDJ+tYqb:X%H gĜ ) acv w tt/$Gӷg:=5yM›BGIT_4w,.M<i }[%,Ԩ,ADd <8Ht79K9-A鍤70 +⵹yEP q X]SL9\s1#v/v5~հ2m)c||T AU®b:;"- I_+BӐ ́j\=b}qg|Ab~I9kceZp59M&aw:'k$xŚI5զ x[MD"c #/GeSk|cM5& s<K$2NJvNJk$V.jʋ$8&o97 I(%Nʯ6]ԔH$HI˯[KNṋbĝCyU%Hˋ5 kj000[tly~5c -X(-J8e(7tQS^h" 004HcK|~7Gkڪ yy&tVDU,l:X7G 0V@4-# .j M$$$x% ӑ0pYaa˷ȥyAR~ol颦4?>s"&\4Lн%}`h5[l)/4HAy/~ˣKŕ;5Jk)h݉os: ?N=W;î$,U_ V.jJ_sP^0P:λK# ~y9k$j; /GO:nvY;ՐhdpLB$x,q006#4N7{ /5;z_nH #/A?D"D^`40k)<lfìMòXqC r$':s88n=:n{2K8>)cMNC /)KL^)8&e3 LV3oV} -}t$||~<_WaA@KKmuf(Ot)': @10{]АKgsq6)L9yݺ$7c):τ]9AcE aqt$&< 耀28 Ɲ.}]`t#7%&Qx``7qMܑi.P|WhRKtaPbgy: 'LBy<5%1x~y8Y^ɸJ)1A^S" c ҹv,&ˎ[s`5[9 'u8Kn000;H>M>9ޗHFe$^W:%ogYÎA _"˓:%Hpˡ\by"#cd9V|K䨔W_$r(A@"/A@G~X9o;8|6$gƫo٦/ke7[! $N $r(A@"!VLzl3GG8qxYÔ=f&J'$x>+oNZӴ*VB2tNyE(߲M`<{_Cѐ>88;Pa -&BJL0Odr ֍smwsayKia`2vp+Cm"'3<>#I:qvM^[])A@D^%Hp10MmLD)1OV&Z+K9VN&vW.ǘ#.~e`ىlMJo٦|P52\O<(- K4lC J$QYH$vI:.e|BxPD^ &0sS~4]UeBaD )ȷ\ H]#5ii띳N -+HdD"2r9 <5C%N|<Ϗv7 ).)Ge <8PF.Z=:x`wtKA)Jy%%&qD$8Hp$K|MJ\R")2.rq5+og )A0Ӈ&HHHdr$CHYYgGDR8Kr$8%&%.)C3A C$xp$K$2r9VHʡ|K$Hb_MUzqْ;zM$%.)A0 ': a$hTس<|>?{~|6AO:oY, #Y"˱G"={8naɿo }3òxG}@#YR^h")qI I%- 8ZIӔ۳_˂ω_>8?>m”AuqvDDF.JI9/qJÀûzJI ': A#YkR Hqϓyv|l^-- Z%+Ip$PF)]IKBIKJL,9$ Ё?5.\~bܿ{ g K 8H>4A@GD"#0(5*@^1j]'Ơ\gGDRH$ LJx1ϞYtɇ2jXP\ HJLA 10.v._ZHYYD i-ۗ 4&.Hʡ˃\\MaHknlnpQ:l)Exh.AML 2r#YɑZF.#2x|ͣN |TJ$2Po8$pog"IQ >2,AML 2r#YɑZF.#2x|ͣN |TJ$2Po8$pog"I:.ң_r}<e፼rɉ&&W9bTnanjnm+`ҽ^+cBiYgOAcxv\=A/IJxqF8/;z_@C5KࣲDg03MH"p0Gx:tMDibqZǯG -GEbZ[m/KLA\F~&&4i_ۿNl?s}2_lR *>%N8@i`։)??:2]\zY/k |ThbLH`TBLx||ND -2,A0 h,Øe'lYivSi۞i5,HXQ)ȘLC9Q=-;bS߃uț\IQ >2jFtj~x| hu[9QRe2+< xNT:$LAz6 ?J:e\rd %&8ʡy`K$y r%%+gt(2x:%Hp\" ˡJLcP)r$K$gF.1Pe|Cp\"%&K$%Hp$%.)A^9C9)A@\b[9<x>7/G&.[e\rd ȍQAaLݯz21k`5,䕳:C<~'/cPoM$I ʡCpNVKVkv.& Dp\"1knT;Ke'M$|-?Y'O $8+ m6Bs(1O2Pb A.kmR_ Pbg9k[O?F$xp 胀J ͡$r>yK[#>4#CI(ʯ/ jX6' 8 $r> %+F[ݿ{2tws3w -4ŧn5 ^b.Ќ\%& C26g<8?91.8XFs;*ܢYVZPb A.kX"-Qi%ь$&Oч$@ptTyK[a`;}5?k׷ʯַJ}9 -HA霕 -c/];]6]+ޮ< $8+ myfc0FeߥHdc~د]xyR_xPG&B^+.ޮ5Nep$$FF=fήlJqrrrv.?1BG}/Et x,=(=-!I|CD sHLpXd;,8|*Q>8Pv*Og|C忾|_Ė耜Ot^yMn', ذ1*c-1;,?L08!s)w!R"YҔMu"4a}$999;گc$$s -pO;.Y.ofGoȓ:%&k'28#g|_zt=!8q-rT.AU&ʉH t;sqwUÎTpʭ0}9*ouJLV@ҩj 4qNaMee,KWrT.A0Ҩ )Hf!4#ě<9;o}rq+À5W?p5`ö6a"J 5eP4U9LqC7)H˾cEyMH M Krj^%HLN֜|M\ >VWH6q:VNp:Vo $r DF./|M؄ )/Ud N?_y˵I 8Hpc%yOhScc% Jg%>Ww#Z]d A"8B3 w>b=w[X>??MeJG10ǭ({U=~<Ÿx4EyMH M Kr\44١cQx& -S8t~L95>VB -m*tt$cS6 8>#q1' /8y"#vz$:pxSt 2*%|~ bҐAnU-)_[wn],Hke~~D"qJ +K.}uהp`4}x8=N^938Pn ɍ⏿CA8IASn9 2VN:Xy1k+fnh" ޖ$N 5/j 9X3е9&#H":I ̘:aHCj>xdB9LAj8 a,W(-ਔ q%yQ}2ǿgoF%28dArfIpLFd 8=a}d ]"_*˿5`R>W~ű\ЄU%yQeIc0XK6x8`\#J4 1u$%& aTvK:ﳭ,&,.y͋%Ȩ,cN.4ؤqJc~<;4W. 'uGe əK$KA@Nt@Ĝ Y%ȷg ࣲDR"1O AJ8=H$%&:\9yMI2PY%Hp$gF.,A9 sd9 ߦ)Rī/ID^h_Γ'.cߑ"1O AJ Xhw/ŚȖc.lZ)IeQYArf$x&ȉ,~AIEǗn"1*ABZ48HC Gj` Վ0j Ɛq^@a%M-K HΌ\"Yb8a^3} Ik%a邃> qzԑHJL.!9|D8e~M,iش$v|~ --EbɰDR"1O A}ocs܅D@4ϿyS&彼Ni0Ws6'uCi aT:F({85n.Sq*ҍJ  >*K$%yaHZGwyF{x~]M߻xM\")1d)/0넵ϳSw 00}9AJoϔˮ.G]iS ~Wz%D")A@G^^I@@-4*5%HI8gK$28P%N9щq'6&J̓2x rJ$2ID"cH$%Hˋ5 PۛF9 y,sDGJ )':q:ĦQ[yRPz.2ǟ=q=+ }D" #//$ C c!u9|"pw3~s[X%i@, ȉN>,қzk%D")A@G^^I@@-4*KSlA@رIo Ñ擈[oa7ikʄu;iT./4Vb`p.Pp`R8 xk|˷7H`H9Zs_)w[̲(-u;iT./4Vb08 Ux<>> Mst#7A^ܻD.Ș'I t!/8N\bMCI\eᙵ>e寉plV%P3J)è@E[OazMo.A%&4 Fb،h]x<>>vB,baæ%%P3JC:DAi׻J Qzw$28I S7~i t~]~ͭuUF31yŦ%% #qp:\vo8?Ϗg_/gF.A"/A 'ua4q1wnw|(o H rrv.o +E -s"Ǟ_e+ C )˱r29+Peȉ&8e|Cd(A$$x2X9:>fLVHQYF^y9 Ky+'۝R d"cy;z]pN_˿P}`1G5qJ':%Ā MA \X9o;Fb<1?/ ŎSZ+Kw9#ȓuD0`94-!Gl^kwe+ C l tMP-A!H%Her2vs a: ~˜OLM mF~լ,#/_чRp v8NZqz %PD^n2vG;W|Ư>;hG} G}H$ >*_33a 0u/99)+Peȉ&8Ǩ܅ipEU9TgcS09ʷ\*K$QY DRD.KLcDb|Dۛ"%H A3# RegcS09ʷ\*K$QY DRD.KLcDb|Dۛ"%H AjJޭV٭OӅ'+)˛1)Ԕdxr;qwey~~1k58vGf 1AD"MK\":br WЁ1`0% 22/`rRs9o6U.# HZRiY]kӦDwal/MӔM?L$bS Rk Nh% J:xy)\[Mp0|~㡴,NK䨳``|MK\R$xpQ-v5_vnQeE.Hqy<8& ]OrMPw'e{?'a@dp$K$-NC ՝g1}4}x87}a0(\J9K$dTx7w=]'D+f"/1A^ 8 xc(# Օ}}<^loec  2*ό\ e-N؟kYJ+ljc$L|͚ÚDS0: #ւ; ذ=Gxp`XI`RYr ,q -F/U -Y"c*LNipz\wokXAR`J@' }kd.Ưo,5>G{ТIXFLA"/  Fb60' 5%\"בNzS,/&G"k4 $0wOX+cmǕHJLA"/a W_2?K9g]\ud r2*ˋa<؜V^vl4kXA^/ApL1@yw3#`$?%a0P8$paG. ϿK~AVջ8d(1Bߎ!uu9 lm -o&ߜfg;'Kࣲ$*%&ˡ4sg{=(r(AeWa pPX;хZ3q}w^6 <87%HQY)S|~qn8* 8*% JC}';-?s׻Jۡw$xpa؃˭[++'8ʡ I #b}Uey~~jpxs׻;0$cK$kp$%.Y t(2pL,ttYCxt!OG{8={`6I5Ar(28Hp$%~W14{w†`я;N1Ӂ,AHLp:Ci-OI2T1op-KBY1p 8C$8+ec!9=wq73.Gn4dv]^_%yv8ȉAI;oqk )qI 2VNp:CrrPK8 -.`:7J$EMc񘻎ztiyt$w:VVXKLB  )qI #1΅Qi-y~&-wJ ryn02ZWY~;'?G//I 8. 1yK[110N+ߦfpW8$Β˹ >.A\hyMH^8DxBP`|9<:Ab0%GDܵ:)^ܥ+; |m#hW1PԖ歘F$r>9[Ǖv(}_yxo|66g >L*qvWqxۄ>_xPN[9ylrC>2Nox?y P3GeyQSbDgy&q8;+6k+9qD^: ^ Ż7G"k q1_ NT-EM 8|'bқ:~|K RD<^;?A⭓VN^;8$PKt QZRϖ}Jk]Yh:SإOHdq`W -woaĉlGM228Hu<8ʉ1xZI;ZJ&tXvgh #Y^~I\%tvC=</[l25{;wy5ʼn|EyvB(8}_u4 K`ډC9*EM 81*3yh~oyMqb$\2NoG"@pswnzr3bNnJW-c>+P^ͼyE(GCiҨW]|5M5q~>xwL*qvWhv\nzeUCLM9L28H|EI5 cr;3Vacҏa5Eͷ9^K A}y+pc%WA"e LL*տʡ2^K$}o6U.#/_|6k B$8H𱒼rq6Ont$C>Hos|̗Ab I\W9AkDE6+ݡ ¯{$>op7Z |$00$Շ=C;qr背 GD=rFn Nd8%H M J>`H! ->,A_ D"_ c0Rol%?5Oa%c I\W!9Ѓ//~\'7ݡlPyͷ9Ґ o_/˦JqVq5,7|$ə Q9n]Yk]LE[ o'+ rm\F^Àkh'?tp|V+5uӱ + H99߱G`cҏu|d̓ΙHJ$Ĝ 'uC9r HJ\R X')~D"q$ CmJʘ% y9*HbJLȨHd̓ΙHJ$Ĝ 'uC9r HJ\R X')~D"q$ CmJʘ% y9*7]ww=wU<Ͽd#1ޅΙHJ$Ĝ 'u1pGv_swuR[JʡʷlSRƜD.H؉=<"rŁDsaUBCHʓ:g")HsD)8yR )qI r n% $Kܲ?>Qa.;lHʘ% y9* Gˎ[6l`6jh: 1'HI2P'e+R#pd_dX, lHʘ% y9*ߦaH^5 -692}mqNMk1Oʓ:g")Hsij#S1~oş %K$P [))I;19~{x<:od而JD<)OH( 4OTW">[\c%K H$&a4s~{>_& }9*HbJLHrwՕ./vV#.{> *䖷6q+ qI r$8&C@3 ]*6e~HʓsyWF.J$KLApx-GesrfRep$ŚgF~% 2*epLNues >q2TJ$@$x &4gW]ߚ=!)i%lx+ {9T1N2<r.v9l/ߞWK~6^ǖr2PY")A! lt@b;$HJѫ;D2\L3)A6u$$8H$ d]%H%FR9L9-CdDR C&AvLI ߣWw/Kd2rTJ34 "MT؏u+LJM  <:>`s$:~r?^χ܆ K08 ц;%?3Z\LJJd2ry0ͤ44?AP;*OqA2D.e$1s%U״uީ#l-%6혒 )A@GN+a`1#47: eM3}0nMl3Ų#ޜLF.O$$8He@YL¥#;``%xu/%ɠy6f*؏O'1͟_8ޞWK>1fdrS\"1HGp! JAk|ѫ%^J4 <o?"IeM$$ƓMa']^Uslla6u$H ) T6eKNǽFSF^G5K4(1%f:#Q)Cdp ռ w=t%D^.1!28HHpHJM^npyPSK#Pl32x8D* _A r CgPbK%S29( ޮ]]z@ԍ[pyPSK#1ecTK xoհ&|b֍XC+I"/ApA Hd",Q8M|hOt0&^&/ ̓%^]nd3!28PYEerz_-_|$0 6!.C&ep:21,^D{_K#Pl3J0)x}zڃ=Kd-e!28HHp~f^G5K4%|:r;$8dS|M0ٜY~q3On_H_[H{!'V|8D* ѫ_ s N; v&ДCe9 ot)G{gsH(_2/%28 #eP $r y?y x`7GJK7:Ô#ý9^Ub$/[@J M2s(A peyɼDepWlzswp:Gq{)JK߸yxzx~_~8 pes1xD+X&cx~dMgS+fNeixϡ < |3L92;U%FBy)aѐXI˔g{\?n}Wѧ<~_s 4MpeyɼDǝv}l&é$$x0PaZIGۜW翧ՖBIb7^61Kd0>Dp{F~3=k:_ǥA)2s(>(A@ <L3lGǧ%OfU $8H$#14Ms| v|:~91pA@v3yp4+xƀ2l9sfwpOYC"jC $8Hp0;V0OɼDetr1`S3˴f6yW)#+-a«rPb$/[@$ fA"1rdr ~D"4J H䠌\"R%۱˼ # 39(_rt/%)2xp$%H%)#/1|I Db@P9@IDhA.A+!_!qsel.2xp` ݭ?Zgy<oo%f?59G29TN?DR"1GSGx'vxFx䦕h~۱˼ # 39(_rt/%)2xp$%ZI*]oߞG_vjrgM$%y|PbthHvΣEMA%] J9\?Xe보\ HJ$@3rc8%G濟L x7({AD"c;vc1XI\<>=PlВ H$FL h4S~r'^6W0D"7: CK$2j 7U Oʾ{ Ng؁aZ2F^by$ FGؗY0!>gGC3rD%4Urny!Rr,ARD^<-O:/zs= ,uSZ=zu' Hd;`u#ħD(} DbKlǔ$8F7CteĴ귿3-CΥ}VIrO ri8l Z4'- |P`[@@0FrPt|W:16Gr$d8M/ Ji pMp= 𐎓Y7XɡR"1NL %b9ARnt]nG |SqDR"qņk;GD}x(r'ߢ;҇yP }AR3HMU _~ebc1AaQ)Aԑc-Q|6Ʊd``=7M$lȠ99(Ab !% 堌#% n2ZO+}u8m͕ߡW`ˍ j`(mBlG |SqiY:v< -hH ,3|BY4 3%ȁ&r3r;*%離P0mol8<6\Ėߤ+144fÕ jYJ+ Dr8a@Il ̉flx2!_ؐ!I}$[oNӚvbx!g?* -ܩ/A0\`Kpwʻ+ʝ] -;HG2>hwv|$L$9:Gc`nr;  DRb#'HJ$r8JL'ى-qAH$ 8Db%2>hwv|$L$9:Gc`nr;  DRb#'HJ$r8JL'ى-qA.# >l,n(ĔD^onVin1A HpN[60A~.obl])jwv|$L$9:Gc`nr; ݱ)}WD;lg1&&9 C)7IvbF$xpl13Np9&G }#1e"9 cTW^sSZ(eѠ$;e<83@s"xo} %ǽ4;mlNC~ B&- %$%9%f0H.?ʉ)'t$ 8Db%24[m*ޕ!ḵJܢ$$rG0N4Cs^?}5D :8D"!#/y͟5v + -`nr; hGBW /mx~7ܹwKa$F1FBy9TGwBP>h Je#8|e^KKrPsf:dPީ/A6: H$^ -F]ro@ Wo3A\Gbw|}wBPguG)cʊY7+ dD⥀`|%`hJ\_3,,#mBخ;~@n. /%r`$vqxWVۚ=V#4|f-a$F=̀ҳ3u~ZFdnBsZF;mm9. /%d8=O=(aYk%A@0>쒃|?Z08_}_ǥ]?GwB0y ,Q |]osA)Hh:dPީ/A6: H$^ -F`؏O{?󺮷'I`ePYK9ʆ';{u>3翯&X.b` }s32(ԗ F!]u]o?Jďlt0ˡR)->G{{J[`$v҉eq<8\*=1*]1) &}@6u^p/cF$8HaJr{Ctp/cF$8HaJ3<w= y<oOZX1F6-|/v"c*=ВHpÔ 1U'u&oY(e` ?|oO@ԗfyLt5g 7dSe2oAiC'oC+Yb Ȧ|/x;х - 1mۥ W>GK'A 26]>w<`txneK*Ab;*%\b Ȧe0yɥzn1Y_P%ȝ0eRF.AblaSM ^ϧP+!g9lY -C(]n*% y9KO$>J$%ȁ&ȦN9(4ALʍNp$$8ȁ&{xd8ʡ~H$OJ$rd;aۡA"JDD^cI r SM$r r ^ [ GDGDw(?u]og* [H+73HF' Ȧ4;33+$VZُVrЙ H&F'8 ;T2oN%+a4 i%8rv(AȡR" .yZX@+Ivz^ |=iA"AhhΖU9a sJ3i1N@6svJTzhr;k 'F'8 @dc ݇!U'/a>Sz$OJ$r ݂$}3iӮz;o0ݎҵ  :<A29(7.<jcp\#TGc@)/DrctB͆{67_ 2:>eшl7J$%ȁ&Ȧ4>1jOX  U(epfsI!]^bޞ!Tʃ -DD"A"/]x1C+obt4;uA^F.GJ>L ~^IDntG^'r8DL 2(Ab;*'A6:  Lv")A.AIy0e2{4 5;HF'x堌q2(JCdaD $rdL ~^IDVI',lIҡHV PvTNlt@*'A ֪_C8eJ6ې Ҷ\^ }&i +)Ǵ4u>9\ JX؎IHC$`h']1>EG&q.>A^Ц7~Ui̷<->>8DL 2(Ab;A@wOD~=ߢ;Hdp H 4 1 V.?912[pX'r8DLqa9ȏxnL ǭM Lv")i Hʮʿ)퉽$mi[&2ryGy9nHcrykM>w49,% qPY")qHyGI 2(A@ovg; P)A>D.A@")^-HlLJlL!%F yI/Rb %w $fwCPcL$r|rKĖaʤAщdpR53tyw}{S*% qPY")3!=]^7˚VD"e2)dPntb19GDx<~K<:ỎLnam QQHd>THJRGR J%FB9TJU5=΂__ \C$Y4*KlLHL4'W`Jݒڿ1>THJRGR J؛dM!x<>1sY - B7n2b Ƞd28i w)vMIQo^*$2my,8i|cNRǧeQ:>Ģza=ߣWK$[)[@bΣQs]H8Nl8ȍIR9KH4Fuq{=mYFea-ߣWK$Fg3?12φ;n|z2 AsDATf35| k]y=*aA:lvg; P)A>τse DLJ0moN^ݭM<؉NR;rtqΒ\b˝rh 4 ;{Mcp? -IvnK$ >4l4 f\\GǎۡŔLH;Kv㶩ngx܉MM=t%F6Lay9T `;ʊ[C ۇ;1)vT6;ǕERo\ÆA2xp ud#CnN 3dZz{ <ӶɃE <8 $8H;&wIĹYy(e;Cd,'2xpH# e%FͽW K%2(H$FB 2(H"c AMM`˝#P2q΍PYKOd %FAK : {g3KdPK' ɑHdP"8D8*˃ɑ:2r;%F\ # Tx d8#_W?Dz7_+fHd,jJ$Gn%#͝T9o_{l~d!=P ۾\b$Ƞw6*yɼDetu9 -{p/gJØl%#7udr-wK*"Wm{` eܽIiLN<8S}7hH$FB 2(H"cfsmr|ӁO wK C!1c*Ab*zj7]1t` -@GǼJ@^2/A/w_Av8Y<ᡌy7D"#/yHDb@ry< <8Id' :1Np$# : $ep$28Hp$%r,A@ 'K$2`H$FJy-w #)Hv"KlȠ$G"12`˰NRG"GR5x -1ܡ\]#GA2`H$3V[1+YbdaCdS1$w<"zT⺮:5Ӡes:eʶW"#8ȁP)4@N}y$xp$%IقI<^`fڧ Q~0 -NRG" f$mx+`O+3j~Sb#9*&F'woOfY, -eNRG"Gcta}{{=UߜݸpcD.˃iRb0G/_<لό˝y{N^h:JH$Flv=t`#Ѕ=el'S xPY1N"Hd0~zF'?'ZI&2%`dЉq#Hw:px׶?/,\u>#| Hʡ c`o8L+a~7-\$loXмS_h IDb9+R)].,Kg vV0z+9,o@eP>(A*% Cȏԙɼ42)1$dAy0͑P" H8D,qH$r9,o@eP>(A*% Cȏԙɼ42)1$dAy0͑P" H2m|?]=y6a/֛+}|.ď`ˠ pR„7~߃V̺i2 yd2<&1 d0%z6X9u44iL噼eD ~A <8P)*|F~$Ȧ䠄%Ỉ?v<=?|;$.Q`#D"Aa`300 Wۏ0%$xpRGi~ٰ lYHb%Z`#D"A@:Twǫ/:!HrxYď`ˠ1`΢6A~Zym!-E)-&F6 =tD^" > Da]y~?`I IR8\/rcp;lnR͉_zJBY.(aeK#C#Mn 脓=;S/] ) rP!epcDnfߚ/_:"bKm/A/J9(ë 3i G ].6a8>*KCD"c >( J`<1Ã~ԑArPF^xO w1\K$M${R) V JD8A2|P x"c&X#% 2(*堌AcH䦎 =.p2Ӭ˻|W?}G=jzCo͕>H\CD"c >( Jd/sG~gWXb\vbk8>FAyDnt!ï=x X~D[dӜ> ORpA '28Fpxoҏ:rP" 2H8zgiʽ=M>t?crPF^":28H@"ƓF>sy:­QM& >( J`<1n_7~Ӱ"X -gs)%52ԑApl1HΡTw}#>; ^Ce |Hd$#xvu]ė'A'l7Y %;XF(=)r9(#7DSzt~|GOD.) V $$&o>'ǭS |2rP{Rg$ϐw01뺼տ/o#dt8Iyp4$H$Fl5x\rw~>B;MrSGJ$$3ו/<IYlxI|Ľ9&*%F6َJ <HTJ$I;A"#H&A0\J$%G28Hp$H$ D")AʍPY&X $CFs;Q)Aԑ JD")1rg^"Hd$FA !S dD"AH$%HպӅ «{$%4P)A eh|f̚xDQuk%K2yl:epHǡ#="xtɦL!_{u)mB {aД Ck%A0vTJ7u$FR"HJܙV]}]$|/RS dD"ACqL_MTZI{PFL #lG,xٹl1|5Gc["iǁXu L"H$ ;Η&xu]o_& ~l#q/Aj /vyuu|8dXrP )qd%F=H;k kʵ uc#|"|: M/ mǞ2Y¥Mvur -[;c>VKVC"#H&A0\J$%F!xv$V\A>J$DR c1z#fm/_L"ԑ JD")1r`/tUws4]<#qˌm.pd%ݤs'#/4H$4GDu?-0iX2q#%ȁ&FBàGķ6s뺜zB/ҖvGl`5)X Hpw# <(c >4t&1rPK4(%ƀۜ[xIe VҢ$Il7Ay|}Yx6=kIg'.; - J$Fdrˠ .F9& n3[ բ9-ץ/ay0}d# $ JFB 3c0g{_dX_XI@9楱M-:ҠHJ002 f_i'A>׹N7v)㐳\aT"ĖAH(A#)סk~ຮ߂Rm#N;_FٔtNO}'_7 vPV-fdAzI"10j{1YCTg(ODH| G:%F;ťKx?v]uH3-HXK%#L9k4!_ǯz{~_A|H %0%h8bm}57[߯εmrX%v7:A=׵Q'kEh$aqoUԼ'HlGA"#|/% sϸa- wfp$rcw(NܙFri>z'HlGA"#|f V~x\/K~ԚXd3/p2eGĞR^ǤS_ᜒZgN.nIX b}J ^ :rGPhAba-N8;5f/&@ 9|\Q"w<?t[c]J;EKl >Tʗ9hz'\*6ف -be),eiY(K7|\Q"wvgH7_}WFq0hɃ Hd#&1z{nokeXIk8e܉;HWvC=He c? OY>1da["/ȃfF[|iٰ]\|^ kPntb{N>9e bAA% s]];m}P_r-04rZ CKAFsr AyN:ܚ=:ht!Vļg"12;/'Ab N;ȍNp$$엶t{?D;KD`D"A"/]nԑ AKO$F{%X$H$2rщ|' DN?xȸ{yg|ɼH#H$HsVc7]j~7 J;8Бߤ+#qDF.a$f>?{{O}>(_2/%#HLq 9#"[ yt]qK@c=::OeO=nM2ZE^bF'HF'8 K{:6 -j8=zRۮŴ#S>G{ɼH#H$H場,x0K!NJ^"sS- oۨ$ yDXO#&|lW;#܁ࠚu]o߁mpF;G>K"ce M(DP|6aûXw }$2^}>(_2/%21 \XO`0$UtJ '#ý3D+Nzm'uqt";>:ʢoޏ#q2#LߐN1IP 3D^%#AW<9u﫻ϲ"8 Ke$V`.N')>F6>z y 3\I$؎#H2ƓI$8{'w >H[L?x'7?DG"7:#/7! 7u&#K2drQYbd |Px2CvGR~\b /}F >q_Pj=;q B'|Lԙ\F. '{\dq -!\A胀D.dM̃!3ycx<$&xZ'/2,JH,ىU؎#H2ƓI$xR|aq:6-xg\t0kE.dM>1cY\-.:.&旿&ޞ8#ߴQ%D2gi=#yo+t> 6&_G-r-H&lbya`*gA̴ٷ&ox#[yCo>HntG^nC@oLF.`Ґgx_^}ΎR#/i0۽\>[đù^= yGƛbeq+;HX=$O y<81@2]no?y溳} `})hKm/6ȠO3mep$HRb` ̹|<oG]F`@ .y'#1e;K6 旯 MtqUqߙmS2r|P#|gxdP(h؛a"nԇr3 #/%wOqOw -ML3H&#]'e3qJ~W{79bGbD.OdA UM.f6wV~Yw 3 6K|H&A#sMLBW&{kgo`D"HJ$K$ r*Kt)JVz$ 2W7Idp\Io\f7y<ߒI_\eIԳao\aJD"aԗCe !P)7N[gТ7_diӭ* |nK 08H\Icj\%6Ha$I$N}94̜VN={M亮π-16b[({xu3D\b c@il߃#\iwxA`D"HJ$%lB3Zax'tC%:8yoFG^ 8#%Q:=%E kuanSS G HuUvE'ZCK#%ȦNDx`ۏbfo_$t - R2x8|s<-aǻ?owwyG:gcaP@zxhd^D)EhӜ9!)3 }P\bdSqgle $I R") Aeph\PE2̐ )&%I#ap] q@6S!/)r %F6wv\@"/ *%I dhXyep Ud* {^bR  GX+Cltђo%dSDR")A Vu.M9F w V"A'O$%I#apX98sW~ܚ=+гl=]32yԞM…TivtS:YъI$%I#ap6Tt\-]<]qV =TH]IMJpzrҙp]ϷGn(+1* A& zknv0Ycxh oX'oY`,$F{Sc`xG] ?8/{mlC$80DB \ 1Mk.=Z -u*Y:HJ # T4%ŝZsiCs{S4N@@LlڀPEgCs?*o drVȝi bOpĺ "H'okMᝩTH;S']M6]jЋP$0z%d+$8)/@>ο(V^k\Vd3 )#z~x*ؗ H4~ Ħ /M{ -C%8HH&1~ArI\D\X" A@" apar! >܇c< 1Cd2r9Ħ /M{ -C%8HH&1~ArI\D\X" A@" apar! >܇c p;ךgl/5>9VNsɤ$JpLi;|Z'89Ѽ76Ynߞ2[par! >܇nOuw~;3Ayd}=ilR" ap`weIjɿ_u< H o =p9̐ \/d7:8;{ kod"wM몄 H$EจA"# ˠ&ݬיz oDB}<Ɠ3$H&|w=Evt&:ab"% 䒸 =@?]*INݯ'S~WKIJpCI$e*%28 A.#ىDnH$%HؾqD"w08H#'%6!ܨ DRA$I$[6*A 'o H؂Db d'" 1N@bfI% C`` F×؄drd+ 8wwa߾>tH={qo$pI8IG8Zkfxw X4p` F×؄drzЏ]|bXO Q33J -H$FHJHt-ҧgn*3ߕ8Yl↰i*Kl`LnTYWsgۿt?ˋd$pI  #q >`[@@@@&ÍہjF0W#;ro"H H$lTJHԘLi -L۷oQ&+n2B'K$[pN1½ z6~&7*H&}P|zѕ\g_8psl|YDnH$%H>Ԁ#ʻqgJϴzMyAKlT'9b A@28HlckC$H$ D")AoT@6SقDaF[r+$r <]ߎ%.R")7*Hv"HJܱiSH`r0ARb$ c; L"H$ QLe 9܇;lͮp%Hwv};tbi=ڝ=3{æM= 1\ HJDs84 -p`.wٙfm5mQ -\Zkb=:Qק|ky"w2/<_vӥo0z18,GSV:5l #$D HJI(YϢ<"C  $r <]ߎ~PbKk5&Md0r0ARb$ aa[[u1CBYG. M - -\~g׷aY'hUj_Aqϣ-/ԵѰEJ$FN$6t7s_<Էoj5@l!I$A" CPƴ.nT12ȞB"H94,/( >4,1"ulp; *EʡaDl2qR1.CLe㉌\ ~L"HvHx"shX^P |hXbd3E%ȝ# wA@6U CLe#åb8\ 2+6\ qCv nRа$аf:ўpǜo[9HwXz0&AZ FKp 1'2:nZb;Ќ~qj|>εM:L(~'^W~8t:u:8uM{@:'ҤWZ wB z.CLe㉌\ Np{ւv_ K1ZU S064,1"p90 1)S.3ٙ`}`8\ 2;>[\Li>vt*WM%CbpCDK[vTtB9 LG<[3m -e J" ``dT *ZѤ]Kv/`YfM$zw #aDrϡa _:ٛ$LG68'ێ5 H94,HTF53;pu&2Cp$dgm -Kޔ_i}5ԭulp; *1 $LTťrgO[^Kˍ -u$2^{8x) /ٛ8 # QHa^őHl ۇ;_bv A@H$#ͮpalc+F'c{PDaǓ/%{G"Ac$7*=˸8-cPqKl–!Dbd|0wlK 8q1t =Bz>y$vSDxA'%^ -H$.|>o30#8]^ͭIX%%0`D"(n -%:N'} ?l;2|"6\`I!!-<#;A"|ü#f7 &k~MW<<2D EH$#ͮpakѤA hr@rԼ) +Ɠ/%{G"Acf7:Yx96ևYuD cD"(nv #II< -s?8ħxwByYoF:w/=OJ5Ѥ:e.P3r10ӷVKl–!Dbdr9L3;8^8!Zr_'c{PDF/:sn - -`" ǥG"%xl*|M\3Л {;yn꽼#6 A.#qG~5xU~pwߨ`$0/h]\\`_\NlT@# >܇ H&C'x7AT):O/e2r-%iBg|3u|23$#/F$8}dr0|r!|:HpMή1HrXF.#|h(_2F.T7S'7*\=I |}x#o*kFC$!H Ip \}_m,EKv%–CC4rx5Ime=_,s =ۣo߬ؓS*ơaau *]c3$H.~g9|m͛Ə6I=ο\m/G[ Iy F$)|c⨇wKK`$.C -gjUÆ/);oy>0זhYf*wylMK\PƵűb;y*fAj D./5xl]88]]u)+sѨbJ3m%,I$D.5n2~d/$^ -NH?C?381~&8؂d2EL 坩 Uʡay\ 3L$I v4fW`xaI909̐x$8O  d' ~eS'CCyg#{rhX^(# d$8H `>^bR.7H|J=n>/HϙޅwGRɝq3O#{_ x Qn訰8 d$8H `ڃ#J_io8zlM}Kg<+:~ݏPg}+>ʓΗE;^1u24w>2ԕI۪6&nP]E X={Sټ쩹F9;M\L-81~&8؂d2,/4uIۊο0#|E.;~1q2F < 7Mڳ_g_ui5<ljK.\z_rIH?C?38̈́#}_}xb3NS3Mu3qC ҠXF.A@@&$Hp#곞Ǫّy~> '53̐x$8=4{MSy9?kqAxb&ubY6Uʡay\ 37})5qKuwVC)&0d0CGWz㾯vdtByp4JXv_𲈩3=ʫtyr&|9t~˯Kx5 UؾQG2 ٔH$$0CDu|0QE3LJ3ClLeK58."A&%-Abx"ߣH&A02T)cFd2KfS"H ygy$G90)A> A@e3-จf H$lTq+K[\}KZ^۷on&w,5.#\lJ$  Kah)܃/3 -ï׾n礼 Pakp\D 3LJ$[6*=;KÎ+n>|),!xQvy|6 ӗ<_R'@po킠mhѡ>; Ab{T cLph @,/%FP")AHl٨$/>kћtI'hor)^2DfH;CcVX.pŖïa -.0Y&2kB&%-A$v~nFoHl12T)cFd2KfpdJOz:!O||=_xf')DR =.[g윖qȃGĞM\_K3]!aCiY'."7U&L"8)_QpLvDD]1dݥ6$ -TD2plA2 zZ _r\䞸Tugrsꑙ:PE qqlHJthYfle:\\J G29GR~cnnf<ϗrr:"ֲf@LŃ U$Go.89S߯q#>'N=3a릥ߨ` >4,A%i}8ϷKKG[e /aE * P0DbK;Cap!Hl!Fp=ں#2ryad0D"A@$Hp vq;fHaeph ~e1lAe!Hl|`0 |00HnTG[wbdSQF./L H$P" bt{F2};Nܩ);<΋{ uo'C^æ T n)B{PكX#0 ob~iVX e `yD %HMVnZ+`aqvBy^g?&2Tt8%ElG08'V=VG  CCF%tb LSjb;X% 24H&AB?&ʏ< cjDnT ~aĖwj -2:cհsuKij&2_E A"/HD2 $8tSzVLb_4_l5\OWckbtA#0C-u0lCkT":7K~XT S$ !H%  =>DՎxowA-aeph ~eCGq}emU-HAMxsa.Cb$ 7*ߣ;1(#ג6N1=\?y|*ղxG%4$8ȅ]܎$`IO=B;$<4JoV`r0H >v9]*]T` -qCW :b$X8 I $x0# ST"F0N@."G"13܂D"28FayH4_&D 3$.L1%IeSq<8$8Ha%F8(;E`\EDb$fEdpDBE h(Mn.x_w -o l*1NGR |Е[w@[r|G3xߪCb6׬ MIJ hX^(A@@0 eֶC-=F^byyAQK 1agq<my_Vw)35 b兊#PI$-pVaw‚.b"^YwH䓸˦b$xp$%Hp1I%~@"yz5AE 6wVCb$fEdpDBE }k -&4<ϯq'N - o*_ SLdpl|wT,Ѳݤq:z[:^LupкYE ȅ]H$Fb[H$)ÁG&Yx۟ Ɯ`SD 3$.Tя &y~:#l/LqdSQw 8>ZuoC /:-󽂗A"0/7Sv4H0EQ bA0" A0\bT Q\bdPHbKa9f*#epH A# H$>f*#؎>4H@6*A@0Q <8F6S$!FK *7*K * Cl >!LedS ] x69q2d@t wA=H| =TFӒՐN.Kͷ Wp6~0Q{|gZdJ# -F[k5lt8b9K,- op A# H$>!Yp{Ot#;?%Z}||aM`#da!ݚ}J"g*>vw8* *A#)v8&0Bs*n nBEH +Q bA0Ӟ& <~#R?~==l Q&d0\Hc<#/M{ -y<8H%H\0őA$828F;_b=l Q&d0\Hc<#/M{ -y<8H%H\0őA$828˵K 8<ĺ3gKu=aKl AD6@M?rq+ q4mG_ nܳ;L<,l|[A"/#mby5#$q'cumhL:kÚD qq$GHv+ BfA*q^x؛͌24/ d0٨`$ #4)Ndq.AjLjRi.NyǶ+Vɂx<8H%H\0őAuMu{i&sX*6 SLd5FeC~Ac ZuL߾WsWj'3)G" 6ɗ|[A"/\ః3=ő4t>| -EXB?z䗼0D[Cc)N:*h&zs矰Z1t2;n0A1x%F_ʗVhU=K4ROyX(u^ǭO=hfI#wD?Bo:?>qfa_ 0|[A1ԞK%P;8~$# ܫ a$rD"1N@!F#'$KlB2"/%xpHJ%F*e!D&! CKbKlGr!\"Hd`QF`LnH$r0K $8$xaJ<8F#aD"/ #a-ZV+݉p}aa&Ac[0w@#ҽԷv ho˥%%a9̐D.H$2@aVɩ홤/E-8܈;hU\W%) x)GR>4,h,:Te> ? -Jyk @? #a-H&ЃC[WyN7i%6!TH`+?AQ?'>$W٨cA"0{غQH4]  *JD)*|.r$r3E&c;p{T)G2D H$$` daF#PnvlT@\(A0 K\DR^3I ȑTQ $9 ҭ_{p[,<;hN$ }=lݨ`$J]EMH "8*=;I{V\N7A4wp$r3E&c;p{T)@ 8O}E;GxVAiy{MW6<[}o߾9U`BE hX"%BI@W\{J<$>~TذۣJ$8I$r0Amh޺ ;70 |[7* r+dR>m ǡI9f#烙tfQm&adln*epH&4wȫὼ^ ->$r@rpXJg=~eT0%8ֶbSpϕ{G.r$r3E&c;pVvKYy}x 1 DQǂD 5dH<&?qr]\_8Yfu'ĔK$兊;#T+hUvsW\&u AH$%H0 ==*ao A.T %.R")58:wJMo+Hݲ|K"D2Mm8Hpm뿉)>x ?{uNE֙kH ?~=7v7Az&;6 -FehIikw/ԀN 8H|lцD_~{+f8KS M QA"H$`- BϖEئLiɼf\d7ISf*=*1p7zdּOx~WXW]);Up$H?V&ޠOاD0N4t$X+$D m1LDH35e$I$TVm}Ô'|>5t xSC K#C1iBг m'^֭i񎡇#VvkM'inT@0\F.%HHMZsnst,?M^ޑ{w=k,F HJ!zaj|8wC0e - xC+tk;C'_=)G"1\F^/e2 <8FIl3 A0qqaQQ 6*;1=H A6‘L#A.#/2r #w$A6UE  ø8аܨrODÝzU$ȅ]pvuw l;(\~!ݻ/#s2^e'$xpiV["VO>x?yXs_xp1y`"rd ~`h#pP{v -!v?"8,/,/ -޳5[\F^`)C3/k)6E  ø8Pj/N -#x#[jC֕H|<ԃ"A.(jC5#} -Kq$YD"Aȝ6IM{KCQPD\Cuͮ]KfD H䰇..$|w!]]w2/~#OYN{֤T w HFenvҧxsIO&E%7Oybo_+wnaFa:fWή%YHɏ>E+lyϊ -O-2 +,Atrsh^$w?z;oǿjOB?D0 >\.;|sFln'6yg]#ǃ#Ș AȨ .eaqe_y7io:LDLGiީ^ J;$xz+mwxX|%q8iy~-pSnl,$LOu>d6} H$`7 KK|4#l^֊t2ĽI"AT/APoJvƟ;,M?KЀ6sjLt+?PJ^u>&=w.znDmD]Qc"Hp'e{e3[2 KX4SpHX71'Gqljˑ&#,p^~{?zֶka?bmq>Zu+D.7 - X>"s>m $xp$$?lahYřK_kJ * C=\F~ͥb84 =T@brGIygI90) _renvH`|! ra?{nTuh(L}dPﱷ2{n.áE; H;SOaI9 ˦/s+|0D"C Hp ӵ.ݞ=T/O-۽ts+׻P3{쭹Kph(Ab={2HSO}SL?owst&ͮ A@@3$ A@s)]^ao|Yau"24,7~1zAY!}$Cc?o"]MBÚ3fapmw#BGqHa$8ȅ]'[iOV -9/bsK4MdWF)`T $ -HlAC V^Z|玹Xw#ie y7#NC7L Ԗ6"!Ha$8ȅ]Î||; ׻ءw lT@{쭹K0간Ò;,= »fnpoGNFq< Ֆ\چaHK.ͮ A@@3ԹA4} CCaKԃN֡3AdhXޚᤞƥUk0}k‹{㼮joGlA쐔 r0 %M _&_ަi#x ܒ,] (0>{nTuhGWzst+W׳_qC2S -lA )L=) HXySn~o{|x9 X@6* "HJ\DlT:qI DnH\DrɍʰIDF.ca/"&Db$fH@"akPc٨H\D")q Q$I'%"q%7*'%q{+ =fKx^h+n%qȍpG %> -HจE$K G<>Il'e^;HrIz0Ѽ)Ԑ\r2xR"' ﱷP0C3JXxVp0¡ckPd;x;οeM0}8z ֲp;AbxR &f݈n:|>~ϷV>hYt: ﱷpaz8 ->{v?v:Fb$rU8égmT@E$."٨l9ԥ4.&k_qmtH:T|@W+l못gC=7OJ$rSE"%KnT,ma-|>ƪ^^yw=Pc1~90H$H'#1C臄}-UuEq/|AfFήs'1 ړwB~,`rD"L"r\䞸S]")' HJARnޫH&C/L1)A\Kl A\bT)H0d0`D"H&a9.rO\Dީ.Hd$%F )7UJ$!H H. Db% HvF.1\lTb{p '=~|>> O߸OĞs#ښ$F^b`dg#%4*5Wcd2M^q52oYwU\#%DG2D{qrMbЕc1qLo@׉;NOYpirhfW" A0ŤArI$#cB5'?yO}1 m  #/Aø8FK$ DZ4D7 'c1uɎwԟo^]P3Hf H{`Hd$%F M.8ؙ_G/%c$ 7*Kl\bdSqKڴ3`D"12TH&Am PE[LF~ϡaA29a^^P Qu$d0_fב$˲W#; -D f|4>zoH7=,2j 69QUf~v8&<ʷ\brR̷4tfDR":$8H'g|Q 8& HsTHJ$qyyA$8ȉ]P۫g*}0azo1oiCD*iK -y(_DbJث1 /.Αr˲ - 1U4mEԑ?a2{I d9Vn[DNxE{{)Y\}^JY  I(OtīҭBĽex`fY߾MĜK$K F%1$-Gmk ?߫iܖHJ$qyyA$8ȉLɺ7Ud ]|jɒ?~ٔJXgnZ=l0q|mmM%ЙIDb2H$K&!pN;nL|>^<xdp8" wk;S[0%Q$9*K$%8˼<1IMMp}햿o9l>&Dg(b(rI3TaOKł!֤H Ac/aP5kJ&9ѶpLFF,#, hClVoWQ !㵀]goj۽h>2 'gc|%&alq]Ntm.elåxu`<ϯUZ*?t Hثf'g|Q 8& ~<۟ʔ-ןWL{>&#M&Hp8DJ-fenfhu阳[0ݽvK(R#,A~rv8HǞ71n-HlϓG~[!+x#c>u8=t@#A"AD^%N+ID"t$8%8&'t3 A|DR;2`9S_?9;ɨSE$ $rT.Ap -XI%c%)1?  $%dT141' p—pw/Gܿ9Ó - #^.4%C48=t@#A"AD^%A`[ܫst׺~"x<_[;}D>ÑD:p0_9dOVVDr&28$8HpOHJQYblnE7m] |qc׭2Xr|N|2qׇ5>ys6׶ȿ^zYxvaWҜA%c%)1i]:d-a8L&$#/ 䎡9i>h0[&b]O,0g3;fj2ii8bG]O&G miA"AD^%N+I'/aBvMdp<x~ \ t\3\dkA/Ay%&6[nW<ϯ/δ}hn\b2qzG"q D n%LJ"m֤Ud-Xcvuo箶?՗ҋx)V+ZJojXS|IDRGR   myvNzw~a3S_?9;ɨSDlK?H;k^6 "7|<_#A@<# V;8ڇ$ɘx .t$8%8&'t3фoy<_WN;NDMd`n e NsrG4dXZv`fo3H|Ld2VqwDXI01ϵ &J[ó@B/Xd(A0A.A"/AD.19ѼS 'LNj )28'-O֙18K|^$*e) ': ȱ %&%H%3%&'4qJqdI2xp$e4TD:;cg#o1|D >8owp yj~pAws㡄K)E.A"/AD.19ѼSœaAi/F@Ķa0o|vxz3QL39Y C&# }48|lOr>>ԇ&&|^$*eĚ#kV>xغR)C$_?HG4oCd:D&N >Β3 f -WˆO|_ϯ]~.2JR8(H1vc;zki4tL;cg#o1| f6Aid8qZB9~Tqt[.d(A0A.A"/AD.19Ѽ`g0lѹKm_/ <^n"^[|DDV\a[& L8a'A>hy䎡YCi?Ι(v'xgจ+P`\D^ 93L%dl ^Cg U;*\98,W蘆&AP'u̖bl>aeZNyG^53Qa2P,$bh;$, { 28:e H0ϠO z>_/,! =س9_ʓuͷ\* C <8H%>I$8H%Nš ~dȘ$*%&_XD.G.#F#'uPC.h2TyOJA@G.AI A"/qʨLd'\DƜ %&Q)A0š$ryG?rn8<^WJw; ze0 -y9 |=^aK/e݀o|_o#=mΛ…Ĭ0'\DƜ %&Q)A0|"hXN"Zpmw۽4Y r(O!4bȻD0iVe>(¯40 -y\lZޅ9 I->I$8H%N8j2~^Fe+yk[EJ4$%&_XD.G.obaoa4lcys>>AN%艽NoN!a.JA@G.AI0p=Ol=I 5e8QxxA/f2Ed Pb/I„6]vS~X?kgyUC`#A.h2TyOJ0m؟։Kf>zl' ?~O -Ã=ŕâb,BMf&Mš ~dȘ$*%&h O|~@~I9' o UĄQ"nӤe3-3(a1GrP -^F ޴9,M$8H%Nš ~DDŽA߉Ӆe]iV0ryG?r$0iYt? /-%a# $ep 8eҞ71@ܩ2q>҃=x:&2NA#)C S/eC`K$-",rTc%\"wyHʡsS0Ň-Jʡ| D<98C'xpHJ$P;o)2|89AXI"]b48r(A0Lp|ˆr(#/9ۅ㼚pipUp$%r(ApʝelLB߆H(i;w]1xPGw{4Fzռd$H4[H-F#) N ԆvXzu{Kծv471agyzIޮ&fz-s-iI+WMiL[ E.Y ,Ҕ@CǏm|c~,- 8\|(߲ȗ&Ff /;H$VZ*܇QQtO_W./a/eC`K$-"Ji h3}Ciǖ> HOjroSLF#) N;YEmx<~&LgZgWp\i' $8I9 8N2[6d l`ӠV'aK.p4FSc=KY{i2_?u`&cJ0aH")2%28diC,iͽߙk9?wm eJ$-",&)IT\iHL+x5L\FpiV'$K`gy ' CdS,/c茕$8Hp |$ A"_ )19 <ICȗ .dd 2VL,O A#Y"$$8r(s -xL 2A$8HK՝݇tvky_+dMl rҬ,O!H c%$h -n͌tUv5H0qc QO OuzYBS,/c茕$8Hp 6 6+"?7'b?_O_{fE1| rһKkÄ 3vIm[F7uctT`YzYϭ=aЄ dDHHʡ nJ0=Աg!|~4UYa>$8P >V /AH$@hsO$,(Y?>x>_>ן+naқsQd$Y>4AGD"11yFD;6|(jWt:1/c茕$8Hp |$9hca9HV|~0Ҳԉm>vig\FpiV'$K`V6 v54aY{m8/+!R%$L4 ~)Hw1OtJ$8ll2*ܺytȸ;^.L' CdS,ԗfeKmϬc7n?_XHɘ& $%cn>fEiL~yo;aC$8Hp$K$DG"͡=²mvɃ==XyJ:%Hp$gbSKCg'K$;':/qJəH$: r(':HdkzW8j˒4OSbSKCg'K4eخb#_OPJ$iH$d I9H$2 nc%,34k?,ޖhz0˓u&'5KHo [+p^:Hp =D/eKxs|StLD"AN֙`N@@C ց+t]o2?^w5ac)'LNj 1i@9  1OHwOt"_8+3A 6sf@~ fOl4 H417w}yܭ7l0tIddyG|y+˝fϯ.47"t*DR⭀w-Acb$M['vN12x  HLCӇfS@@28KD"D [D`Mbϲt[>?0.?q]`+'8=tLc(ل'BCkO9Hp0x'‹j^Vz#G':Hy9V$%̳~+c!qaG"HLR2;FA@6N~:D< `W(_L|`[-Q")H"V@; 1':8M63I Gd >Β#H$&Q)O|`=<_|̲䖥疥 LbǥSy&o$NӨ m@wDDO)^ XI d >Β#H$YN:Y? /|>~@ā@8G.A0NHJb@@bn1 %"eXI >-aTå;s7`&cn& 2;FA0?щ9|o>f_uI|UWvU= 8Hg?Eh_^ .΢.^ ,/ /A`gbK;A@NH gDEx[ 5%HdYO5A ș $8&Q 20$8ȉΉ&Qy&&Q䤎p6yHJ$K\DePSɝ MFe8m_I Ig{_"/x}Uˇ POvK ':':D噘D¹ :%ȅ+GR[,#jJ1ɨ s Ųx"?_|> S,H;$]xw%HpLrd?a4,Ap5l tNI()ӉW-a2w􃃀ԑ &Ifk+dg_Py3@0hQgϯgB*%;A@NH g`=Hlqqְ8d=ݫ!H$&QY%ȝw>ʷl"$8 2 H\|w֟\Xr~:2x|rTJ$D7rPwy"HLK;og3/}>oE$Hp$d(AQ99)?Bwud䨔 H$dOt mgoW"gF~ۯ:4e;g[6t ‘HA"G|w֟\Xr~:2x|rTJXX- E [[Ũy"HLKab/aC'+ZyE;."A# C D%ȉΉmӋNj>^/{G {cb-u.3L:0x|rTJ$D7rPwy"ц vcǟ:"Yv?f5Ƕ..sPeC HA"G6 -DSR%&֟\_. -q%ϻ:,k,u5!e)&ITqw rml坳χ-l."A# C DeoT|w֟\Xr~:2xjٚqh1H󸼾s 酩2D"A@02.Aͼo[6D< D %9* ':':'ao)osuu^u %8Kʻ'07iH$%;2N4%n/#_~*e,hKm2 |\DG"AKj}v<sb9<iC`(%r(em.c$8H )A@.|j\YrgCIde.#/qqqH~rO?ARJ[MJ͢#?{u8_^\ c$8H )A@.|j\Yb{/l()eepq<.2ΒOaKnup}ޡ5<c-K40sepo UM| >VU\ Ά288\F^g ,yp{zv6x,!w&X_ GdL SARI9A2T6qr$XI >5AG.? 22K @/[|ݟ[4aБwA>3/#>2fkJ&o|ɰ >5AG.oV=f׷2a`CT:(rxZJL$),ID?Y|C$N kQ)A@@Nt@YreD./M A0 %XINt9y"|L.ʤ2rH.SYΓ ~9ʇ28H:$R而 Љ\^8<8."A`J7s/;rqqO #]D -.\'%dsepmйGJ r2Β.C'rylจ I(AJrbXݾܕh)zewX RF.#?%qنR< ntHx;Fe ΅5ɨ $u :֔~DrZ|}"֤//4$L fA0 %XINt9y"|L.ʤ##惦'6"} ׻K$%dsep8=t.IF9gApp6yp\D$ c%9q|n2;g<_MO)m\F~&KqD"C@>qaK kQ)A@@Nt@YreD./M A0 %gXaȯG~|L5lVf+dTeg"$Ng9OJ$2:( qz\XJ r2Β.p|֡/:y<ޗ!̖˸8HI=cr9\$I{I}Nnl¨ >Aй&&K\ ly8m%D'x%P ~D"A,xP 93r q(Y%&',I ' 1$%&x\_[fl%Hp˓s:t(Ǭt|L(>ɿ)&x<~ؓlR|?D${^gn]^.s -K"$$8Hp%%I%[ e Ns2O)H$."c\`cɨ#JwtTʘs -K"$$8Hp%%I%[ e Ns2O)H$."c\`cɨ#t NI\N-狻?5R"͓ $K")AJQY"YYPƜL0' C<8TD"28&QY%&8 -="=ɹ~=_o!JX|Q)cNc~)H.HJ AdTH 'u o141' 8;^SJw%/hɯgi8*I%&8&:I|$HyG?NGml/_z/o*a&A %Ȩ,,AN,Abh(cN@@p -& d|*OA"q,G\8}||}c Nk_ˏ~97=-K؜uNArI$DR 2 D9y9)`N@2xp<EdpLKX  eѧ?OwYῌG1&?*eIpLO9%\I $8P-2`9AFJ'++Xo#ŋ0,B?6d3&DR+ .Rnw3r -AwVֵUZd惜YPƜ`>qoxNe-!&xxd\Jh;cg#0 -2.%t@09 <8N9gyw DYD")/h"8%+''GI$8r(A@Hq)qv8'uJL -I%\~(ȸD3x8Dd%yRg&DK$dhN:8 - pLQyR䎡Y'K LN4SNt@@Y] 'u/h"H H$NA"aZ*;eW?u33W $5r(A@Hq)qv8'uJL -I%\~(ȸD3x8DZJ7Y_s_.5S2Ds8%+''GI$8r(A@Hp;JF׼'~m+x#oM4PpL,yFAƥ&')': ,.A@ȓ:4H$M$D^ pL_]Տ*}||- Cw7&T[J :%&w $ΒG.?ad\J`rٸ.%]AB$&C-@$2.."O;AM_?ME8!DRQJɅ{N;`$~>tvP:L!sܔ:8殕vj01f+O;Qy&sBD"/~`tz}9pgOL ~wH$&8}%&QYӡg'?Eɽscqi-9l%7ZN~0In56uC S@RB"1p)'Db/܇N^b9Jqyyl~}\DԱ`m:qckx|r4u97Ii %2N:P$帔'HL.u߉# DeyrvN c]Fj%@+Ns+ӓh?*A"cN@#\/MN#eD99|># `>փ)[TW&ۗ;/)AZPFgyOC 1'H$& 'u ߰$2xgL$rT.1yeT9GBM $eegF~XyPFgyOC 1'H$& 'u ߰$2xgL$rT.1yeT9GBM $eNN/ "x6Cp~˧K$p:Ko<3Gr&9*2fy n=ڰn{ES@nZh,x}x˱.| 2 /A@@bNHLA@@N,AaHd IT C2֡5i9//Ԕ {JRF^F~fgwAn{O^k`;v{/:"y YŜ Y|<83A@Q-QPF^'5 5%H8>Gp`!9Q=N[Vw:0mk&p#d> _Ĝ Y|<83A@Q-QPF^'5a=@WwO-_y<_mqY>M҄APFgyOC 1'H$& 'u ߰$2xgL$rT.AAGNM_>_U|fi0J0bn>Ns12s\[y[mΙ ߰$2xg1+͞;&'rPt<ņx:<˂C0D^[?eT.AȓuMSNO'Np{9;ymBkmZ[*o娔 rIoi H$Pbʡx0>fy2! ;^MZ_ -a+-:j 0z9؄ R"Y%'Q)q'-%AH|#$C ֛C:.nW(gs>;N\.n,OՖC {eR~0\oKA胜)/楌͓ .K Kیǚ$8Ʌ|r(Aaܹ ]Om)# H}:Ӽyry¥y)wP"yqXG\͝PLܥ8_ k0q27340mR~0\oKA胜)/楌͓ .K KəV6{EF=[kOKߛ9|r(Aaܹ ]Om)# H}:58L[k'R|_IkYaJ1O+'D6& D|r)hϬo ;U}f/xxA{ޡ{bab: H9ޖ2r d N?9S^=Ko'(A.\:qX9 %eѧIeU[E-fenx<_/5iA¦E,1s?L)RF.A,1' 'u y)m%ȅKRB'8+e`I܍H_˨IM;l.ɝХa: H9ޖ2r d N?9S^=K5pDIɩ ~Vݎ -Wsl/P4s[./I@#1\/q9|@se<{}?+ wnKm A胜)O&fݺ{EkpܨuyrK?{{ WJqa\^8yy{s |GG 5%&ɸ̱r2Ӊ 36 <6O>i@~KpJ$˘_̡JLBDD./ͼǽy DpEi?Hգi]/Tiګ䲑tɽ OM%!CLKsz<&F>X|W~1I4<yy{s |GG 5%&ɸ̱r2F|` - =_eR&> CI(H关q7! /AwtPS`+'i7<'E6{Y/:Jc6PŒΌEJ$H$H8}ܛL H;:Z^)A0KnA$~a@%A;89 p -& ':q?Dd\ʨ)Af$g%H'N N/ARяq3A2NPb>9 p -& ':q?Dd\ʨ)Af$g%ܫݷ < W{w} 5}xKj\%ge%&yg}ys%LANt>ȸQSՇ`WѷͿo@GIxN ^" \b2ꔘ }$/w?$.t(1 %&ȿ;~>KgFzHJ6g7Úsx@g6ftLM )Ot@G~ɸ qwCI(1AEYD^ƜyFD{& |qÔhc0;ΑHJ%&bT=|g/ϟc׏߱Od4kCI(1AEY`' -xw*eU[C~r C rD"ON.ANt@bN,q|J|!qJ'Y~ǣNIdAԗ &AJN}De;I C rD"ON.ANt@bN,q|J|!qJ'Y~ǣNIdAԗ &AJnpVۻβwyo>z O.G2823ܩ/A.Դ#"1/x<>*xZO8W }9HJ$e8J 5%yrvr rsd 8%P p@]•rppqsK>Fck19Dp69PwK$r(߹HJtdnZ|W_omn0 d 8%P P?t;uHJ$ rK#W{rw{|"_VcoOrsB pD"ON.ANt@bN,q|J|!qJ'Y~ǣNIdAo2lx_\`0&mc/R{J4& C rD"ON.ANt@bN,q|J|!qJ'N.]){jVn=opu\>W7m33BJN}De}26ƉVѥ2׸MBZySE 1_HŇ140HOzK/8qkUG\`re ȅKR}\D"Y"1O c%A"JI(|2t%H̓%.",Ș'ep c%$c>:Hd$ z28o="D>2ы/l?> iT\`re ȅKR}\D"Y"1O c%A"Jb3(pu&橕__mCmȹY YJ )1ŃY?RQY^0penP箑iJ{PRE$% 2V$r$ 2x'C\|ҞTEbi).˘  DD"cAJDPd K'K$2x2~thpʟ -E;ҋT00KL.9pi^ʡH$K$2Id$HXI@0 e'.AfqTT[fہYYb~"G >^m8֘=9zvB+o D"cAJD1XyU[O@|"¹AN4Υ/AYq)Iy]tByr(,||LʘO._\x< KGD]y<)O4Υ/AYq)Iy]tByr(,||LʘO._\x< KGD{5H7|ӓn m U߰ȡg;ǥ'w ɥD P red'LFްC|_sߐ]]i08Abq( w8O'剎iʰ> _F~,Z{!]*X4Nyrv9.X?)N(O.%e/IɅK /p- .PWʻ=K#ʿQI_kؓ\4)7wX;g;ǥ'w ɥD P reӥ='Yy v o|xсm']a"cj2͡sKq>7o.H8%2Nk&/28%9*Np$rK/p\ p;PHՓGD~eZ¯6PSC@,#H8%2Nk&/28%9*Np$.\nn J|6ÿet?;"e c% r95KWCoǏmx<>"K P/hHH䨔':q/A~½sa'LAԗC  1\;ۯ+ߞO>XZl>Yۓ\I%׬5Q.#H8%2Nk&/28%9*Np$rK/p\ p;PNeo?_݆I~< 6ӋKU6005H$*H$2NGeǚIyዦ dDJydy#|󅤿kx7<Lǘ)1/ )+IpǒtW0~+b ޤC^fja)/Ԕ,Q)Ot#1W[wOxQ C S\`ra r 2V&ep|Aq%H%!$p$坳Aɉ&;p$8P$) \X\Id$ag&epwG$r |CHd<9;IysWw\[䟈*YE c3BܘAw? OK2);̤ D.A"/q > 'gto)=zDU왏ߘER^t # IJw? OK2)i9pp1EiܤʓAw>+'' '?(1A8 -/MI?\.s@?0B3&֟ ceR+wؙI9\D._|(A@".A.[/6qYFxIC S\`ra r 2V&ep|Aq%H%!$rrGs7m?#q_bTƶ)p d(Ab~ -KYL.?A.Aʤ 2V328,pQ\Ȼ}Y<3}Yc 6~b.}2VNN4ANaI0x ^W{u!z]? b" ceR+DV) jvQ~ډP9)w>lΓ:IS@H\D"g%ȉΨ\B2{䛇N1ő,H;KdDd(ǜ;g`rr6IyAd) H$."3DgT.D^\ ~f=_CHH$ ߳~.? h7M3HO, 0)LN<)/h8E${F^}K a34%ܽײ*BdOJD"=#/A@NtF>H%j%/Yy8$]ׅ*n}+x/r]>'28&'gs4OIJD"=#/A@NtF>H‚@f 5wj>p翲&MDLGC<99O '$%NA"Ap䞑 ':ry냸. -|#yMt n޵0s#Y" w 82,lު~#E-m!E${F^ +/[ <+;j?˯ IT/P |$KrD;D┯qHpL_H;"_ e$,\gɑ8~5s1%*A@q8/DsW.ȓ'H5 Kw~Q$ :cҽMۭ gct l??J{D\Q$*˗tARF^ltΥ_D"OֱԼdO} uB]݊k s0 :cK,9ԗf&՜W8Ϗy\.A;n1oy܅f}ltΥ_D"OND"q8$8&/q$+>JLbzp]G'43tg V@bB|IJ$ee>K'/\K$I$y!xwaH"}_}ndlF~ng^\+sP$*˗tARF^ltΥ_D"OND+U..Rџ -C2B'M;J9˓urTw.H$56#4N}<_d(LY|yү\"'g' IJirn'i'uʗ\)ANwG.A@ H$HgbrRJ%I(A@"/A0J9JEJLF3/|,]F%CJ:K$e% ,$rD~gbJKLR%."#//%.1yt~QLWMې۸gޡ.7?5x<>IJ_Yg6x~WIy DD~&&'5ˡ\"$Cj"P%&8r#q݉m7>OE?WO a2/R)\ )#/AHf9 K$k3 /9ܸ|^xUI0_LaOԶD8LEJLF3/|,]NWdpf}W_-~%st x<>>>a<$ D~&&'5ˡ\"$CˡKKL`]kO7v$^/>;&l:ȼ=3_V:X k: _D.%yD"K$%ed$ H ,pv&,KϏ?;3 M4 4A09)A@@2NG\F.GD I(~|(1A\J$r4 #-A߈ozu]mtK&4MLNjJwtQ)A[a'>)R|q㶐 w0 -%L8G䤦yGG8$r$8&  |(1A\u6O6 mH$党 4A09)A@@2NG\F.GD ˈI(Ù_pKe.=lЭ7onff' H:Iy>MLNjJwtQ)A"Q)aV% y_oOSӾ\)"K$%Ѱ،CW4Pe3aYeJ  c?xlxW|~_m/)O~%ȸ'E. |O@@@^y$xcȉȅP8 $K)A"/ĽC 2.CK$%&d8D^ X#7P7b.av?%!j.A@;QYi: qI}7j8Nt@.,$xpLi00G?{RH6!PKyPI 7%!j.A@;Q)-\wXvܲ|r=t0$BOc=6B<Ԉ4ٯqH8SD^")1{ 2aRPKƅU𥉉^V/r|+q!l$"Z  5 㝇Ȩ,48];D@Nt@.,$xXs}K u풹vp_ 3`^Pr(}DZ:L' /qPs ? .%i0y&Hx2*XyۭM6s幹xF?'O{3%&'u H$&wv$> ~RGXH)\יt(G"K;/5K/\Kyҡ|TJ //ٙI:K$;;t -KY?#AtL$PL.Lp:#H%ȝ%H˗\U.Aƥ ~RGXH)\יt(u+A%;~$xKv0>???xp_>x4 CgJL0'O,A@HL)H.yg}ӱ2 CS@*CXp ܯՕ2Q?Յx/01)n)qH%ȸ'*1:G9(),:h.l!vri/Aԑ q:V&d(q -&u&8H$~$,oW]/]Ϗ(fW) Ob6e kDbrgNAr;'u$HJɅ{ rKʰ낣CD.A@,AG. Ðv op$6vk5lOnfa.I92 ~Rg DbrgNAr;'u$HJɅ{_ǪXmXvK{TeuM#7fd^ʦ٬Aƥ

*%0h=Ku;`{^ŭ|I ce"A`rҲ70o[[xvu+%Y?~tȡ)h 8K w8K:%ȉ&q)Cs(sXH$e||}P" k&g ':HPLN4Ar%tq; NǥDD9_9rL$2 >~Td(c~eQM[> Ż'{e"&bM 9 t\J)ANt0KC5#Gђ@?_{_p?c="*~ȯAPRlJɉ&P1V{uxф>V a%eOM 9 t\J)ANt0KCyb5tN9p~  ƀ+ۜq7t~fl`x~}Im"{LM9L9 t\J)ANt0 x32Fҟ~g kqI9.Y~r? r3VNAHL$B{"`rCeK wOʡJ8%擨5I{r,O?uK9+' H$NAr&yRG`r=wA0ӡ2Nǥ M;'Pn>䁓Tx)xw= x HLqɽD'8^ʉX9A^A"q -3Aȓ: -˨o= ~G;*t\Є!5Nh -{?~_ɇxIJK& w/R\'Nx)':cye,+{- i..1BCuw5lwZm+M;'P%HpITMY IG7…kr脑@'8^ʉX9A^A"q -3Aȓ: DIXP[۷oWeB\&6ys}e-{!mHeBi=_r9t:Ot㥜茕ep$ 9 - (5ba5 Ce"`rCepc04<7%(M Vo<)]'a&.#/R\'Nx)':cy)HV-IoW%\&-3Y?)r(A@㔘'ƀV|<E:މ~m$۩㥜茕ep$Xvx Lv$s^-Օ.ONL]B 2.%y)HqMv2)wK$rT.2NKR"dL+':P~'Gr&P.$˸{&;e;%9*CǥS)A@p 2V&gleLA@@rI0*^ țdŌL-t/2^b2t@N&8%rN}DPV : DBIЙK29sL$&'g3C'ǒ?g1dw-_/`:&EL4aʡi)"/`rrv CMcjE>IK^o= 3˙y bR': d' ~D"Gr(t*%."A2 -K7VQOLaC>'ؐPڎKu^ -eܽd|LpJ2au,%?e H29sL$4=zQt=ϏqK Yvr/edA`C d\JR,%&CdS"ԗH,c-r >\y~j[s/X3'p3\0 Q鬒2ɿ"Fx>ɽo>t@N&8%rHXyiUzod ^FnF?!g$&މeKKL:wə8gy: qq%': q>yrDD)HdA"A kNޱ&މeKKL:wə8gy: qq%': q>yrDD)HdA"Aiu{'rWt0*]ހ Հ_s!1yID 0q)dsXqC ]By傥rKt;K$ K$aǏ{D?RW+b}帅AqD2NǥĥSYKu&cHd.,=iWEݾ^7eӗ+'cNS@.9yļLuti[ˡtƹ+bH+3#q:t(AK(Ot@}$މ%ĺ /jjo^Wx<><=mHf锖qrT RbX.2938,O$. ft`%m9^:'yrDD)H$ J{gr=~~}Ͽ &9;)(Nʘ˱.QY^8yye_fPep7j7mዸwxaぷ!o>xW532/80;O.k9wb}rwRQL,,:H;l(%关w}cмQľc_7>Nrq~Y/{/8 /cNƥ r(? rGމ ,haa-/n>tr9/D9Ѽy9CC8;5ä̎xcgH~5r_u]}7a^ wANXp eIXykXFhZX[ _pp ?f𒑰n65J 'CygɅ}M; 'NO@Nt@Nk2ur&93q y9V%* g3/ cTÓ) x|<1/_L4M&R,cNƥ r(? rGމ ȉ.ZӪZ^;&&\r z¶1G V#r(2N28&q)Qq@x<>_Y٦1 MX; 'NO@Nt@p4<fVyx<>'~]pzG"1Υ(ȅ7/tN//%8P _㜡 rIp帔I\\$;C>ɸt.GA.|ysȗ w)x)A %&%Hp;gND.ǥgNw.}wii{'C|Zr%}>bޡDH,ŔK K~(kqg|}җ1'q;?Opqb ^hj0 |,ʼn>ɸt.GA.|ysȗ w)x)J69AwB3R3gL|$.~ .sÐ06+O} > xBUoXeK;``箮nd(aɼ~lw P%K:$ >r Sƥ/$83Yr$2N{NHu KL0O$w P%K:$ >r Sƥ/$83Yr$2N{NHʡtҳ,7#=f0ˡD"\'g?/q)f+'H+Ct>VHA'Ï*A@#9s%A"1 #O@?+0<0HiM!r OᏀ PCr(C9*q &omB_!;x~A/@M[k5S")ehD.A0l鼔.j-~(A^!9Hd8ep2.eܛV@8J^y B^$)Q%Ȩ 9ΒȆ͍ct~!tk}<IJ,\,,/G@@r(A^!9Hd8ep2VZi`6kִ'/f.>ٵaL֔MsHu Kcp}E|<~Baʲq 8e\J4+o?v5JMo GY?$@/1D^yfPF.1ΉwA@ Db2 +IpLȗCyl~) K\ʓ#g;KL|A@#jy9KLƽs}$g<:%;:*J<3P^8?#Ap -H)nu~ #xb'`^μ1izWZ~PF.1ΉwA@ Db2 =mM咰ĥoA9KF g':!al="ZGb\g|"2$=}V6f rGG"/<3r(#{; di@2Jxfx|???~K'M bqNS@"H9'gG~wKq765gx1 _+B@ӑaL'QyGGXI`gF.-˺Y=+Q͕mi -8-Bn}_,eVw -jy9KLƽs}$g<:A`Y,]geo"؟s`gF gt@# /- a ɻJ;e2eD^yfPF.1ΉwA@ Ă PhбɥMJ`t_Ey﫽 *:&.aYp -H)xs+x<>J9Zy{/t;L'xJސnn/q-ŋ 3ArT4#_ITC 5s&&' _;v4)'g'?3ٙ 5%&/١ ӡ/K<8'ce fDtidIpwtLA0Iל&t`jx+&x<>~~ad?A HdP⥀ 9);䮾/M㉝,7;QHsDZHΌ9 򎎖Rb 8&2)1f}bM4ry{RIt`c*/AVWǔma}š aqE\ wfW1r<+Ti6eiX| c LVi.5KHV3O4帔c<9wW˗;>Edܽ+Id D'8Ȩ\w4ɨ#1A.Q)O 8NK\4/X?)Ot@N q%Opw/DAR"YD< 2*qG28 `2HLK$rTKM{7_rǿDޡ`0q.&%Opw/DAR"Yojʲpx i3(w4⷇GD OoHo传l57`ÆNIyrrĥ/ywh}ȸPZKD-a[+?y}2 --tJK$rTʓ:%NS 'PYrp2y<`<v1wEs7)DbqrLdp$%%H r,ZIo.opqof-hauڍ倾k%Hܽ|ɻC\D˱2A@F΄E^~Ņb ?3NPF^a1v4j'ɣf˅(] 2t`N q%Opw/D)R2V*-uh2xCYu,rMK$rTʓ:%NG>w_#YxvtG: ce" bHR _pȭf}p?%:p?Ip| C NkNB r$.ehpHK:T^85_ -8C'p8Bμ;O>;sd(_špPbǚP3K q;y&Gd͗2 npv~.寅w`>Tb;/L' -5kp+d(_špPbǚP354a]qWxjf`-?9HDv!}d -Ϗ?{Ojj+b6s91G kN3'A%_ -' %9aAmuJlK_˸hmAxA̽YNU"ˢXɱ':Կ$Hg0$; 4d7?'wvD(evaa0cI(AN֙ĥ M8 sO~uEZxw2n6@`b~ٙ C/>jC Kkbe[أ/q+wϵ |"xVC'CǯcӺ'<_*}fYy;|!iᄡ5'~4+^r_Ϗ'ͅl.PI|]~NʋHL:Hq)>I\@09)AbN@0y1O$& ?8Kc'\N}c%$1˯ɓXy XgIy9.G"1Ky&'5%H &/q4tgqw섋 M=7zq&x8dm -PHq)>I\@09)A%L{gӜ'+cF ##臄wir%]Ntꁊ<?K'+L '&NʋHL:Hq)>̫1qXV8.שi[ miUA4A1w/LAWR^U"[%&%N}c%$a iB+]etn?'.q.]i.](1Ly5ĥWoK7,1ycrA$H:溸/ -/oVJyG$.C ~I#i?+=x|`3$+l5}>k -Fr4Ѽ@e}:Hd''f :a5$4=X~><ؽ˫շ^N|XU+ Dץ/cMÓF~ZjU6G͟"8"^p͈64wL,K||y)x)H$1Sw}-CpNXరF*𮈫#Rg2r9.%tȘON֜ O/ð#AL M-=9jr PߗVrd$%&1C }p${Pv LɺzXrQ@VÚ5Eq)D|rdļGnҬM"VCg/ᷕ#GdI؟S|s7H{q~KۉLfh&,;&ĝit|ofÕy%|>D&KfLN&"tppKin/vUһw$GcyW X<ڏyi;I OǑ&a^\x(r};#!^6ii,xNd&qwF[F,l"BX梛32tH4Hb"f]gsva fYrڍkh.& 4EϹ"ډ$hшMDh\tq"u^ii@,Xd߬Kl;̠A,$c > x'ս:͍߿?z_6aF,l"BX梛EoX"-3뷳Rᳮy::}|7￰$̲ȾYt5[_ywOv.@xr˾O#y|u;4(0#m.Yl"o}rۀ&y3q#$rGN5G ethwG Z΁mz;vE_tO_H/}>}rWw;M; <2=q34He9h7tܒ:/Cf qf2g̞~;YY|Ivh fD;LDYFT&](KDM3옑6ci&>yo_@_Fyv].)LM0[/WXaߌt9+KA/Ɏm7 f_ͦg'U¯u vXGlz^//w)ϗϝy4=1Ph':+KAK?wʎ͸#pArCԯk};D\ %~"A"qf1uF?_6˒# eYrRDH-"yfYrMfX0rQh&D%~"A"qf1uF?_6˒# eYrRDH-"yfY2C"3&bh3tl,Iޫ۾O~%o1.iZo7|~ <,x|'4|i,i< -@n[; K Ω~һ~?*r"w6?haYrM"~.n{,xIU7 INkSDSO.<>bN yoAM _^{YG)d\]A ʲ¥dIny'y:!g#r]yh{د%Pt -š9) /I#On΅#r3fY7?sVvpst7ټ<wDMg?DD:> 4/ҽk^VyMu^/6 SDG  qLiq?mͲDf.1vDx4/Xq$/\@LO7 q6Ї9aɽv a _bF,K1< -(?FyfMOM^J2 KxZ YYiqЕe4wҙ4>^dFh,4h'2E& 4x$49kYV"4;Ki6C0+iY YYiqЕe4wҙ4>^dFh,4h'2E& 4x$Ieu_ B#ix;~R^^?[8B@O{("CG/,;qwG$Džz<(fxOA/ Mbt|>>t <[r|D^ڳ9S=r$tVfhhDD&"teiIFb]О5]>ջ˞5=Wnx|B#̆ƫޫCX#ʎg$<;S5$̲2ˏc~[b9Z rswrohÎrqh-dK._b2#41ĒDLN3 Ϧprsq1<8/g˫;a>zjuhLDfNsG&'܌sSn ]s J<O -RR8\P7i9%InƸI/rO;{r==YtIp9w;]YfNs'm$ܼpqc۝í_r6"Qy@<>^#@ R$L?׫Փe7ؗt;oxpX^;P-=Gc;L_2# G._ OgmAF%СI4lfehnD;YZgىuw @f4Ҭ.3c#u41.,@D&G\N0C&aҘY-ʒDgY"jYֲe'"fMo"$LDxW2lΌُ,Daɯ}=_c:X!s݈v,,Co`醙>n{qg#Ư_+bӯ%/[=hW@u@Mk;D9O?mpK4#n'KkTcqXatfIZ%_ܰ+ŭ5ϮȻD^^ȝN԰Ĝtfř}Dhay׼n7&$LY4"LQ˴ItF:,#mE. L &̌ Li$Id9#h34ynL6I4"K,iD$2,5iKtfYFڌً\dzKff&SM`M4DDLL|sbA7ѯǩGo'cYĬЯaC$|$&>f"9zn@^VkP{i3^2^8M)ח@xI -DY"j.&GɏˏrvcnIDnet+OlyRVEe8ǡ'ݳ  Cl0iDfY"|Oss nv]^-9Woef"tgYF -z|x˽sc>_K| T=ɁT0GZ8E&98h7&L?~Y^eySu`{_t`[%w %.krLڑ`"B#HtK12f"2;8 qN$vwOnI£iY#NL6p9܎w-BC~x;'҄}3tͥt& sʎ: 3ҙSvLayDfY"bif,ىXdD|r.aza34KIw' 34ҌtghZ A|ĥ$LMf>=g*fheyf?ikL'v,?>?{| A^}4ޘ8r:6pD,KD,MM?̫WmDZ[{=Lpw"mV -#ɉAt&:Ye,w7\@0DiYY$2rQD0It\ҹ\쌴ȼݘ)$At&:Ye,w7\@0DiYY$2rQD0It\Q c><ÌQ91&L24 cZ|9Cyx?ik -| -o:rgij"ٕ-v|O$f[V&g.IgRzq{pfmܽSbk86 '#97s,ral=Ï8\ z'8&sw2?gy;^M 2/P!.G\^t?;z<^&^^2^8}E; -yqy5U)27Df~(+g,s$4y9gS' ]Bϻ66 BljcO8\@0_[A~9L7ɌK??·BD:Dg3ҙ%L\jr-Ji!GpD9]>?;#qXNf8hg/13?OnrpSn;+dntYg -&wId"*͏/n^w>ki/a٘r&f+#bܜv"4"ҝGribnrNY\1r9n'2I6iܙ4Ј'\!4w.˘AS3ssvAģ;x҈HwF ɥ+9gGri5 $ueF>YǛ,H4qLƻOwd΃GgKKHjD1XO^O-P&oOl ?pctyqkrIG%Z'z0C㰱JN"vʗyʡronty𳼜݀^㦺aݎ'W_]; |/!Ǐ) c4"ҦK%nBr:.R\:&LԎFrp8V"ڮ}Uh|Cs^5j!mXn9氳mǫ̋[&1s~ƹ  3th&'3~"WD>e'bN].Be'bN].B ,\di3I$\v𖮷r;\^]?1r|(1QaէV| ::v2}/M|}v~b8v|et9?@x0EC1c:~LRg8ЎO~/7q(hivpko>YsM˫F}=^P^\^b~ub˳/[8K;+K*_+|hXNM7rˡ_ -mҗ5};&W2srj9g|Œ/Ltӧy8̡ rQ/ӑir9xe%|g]og!-Alvvz9 K2M2Km6/|yeG^?lL} !\1__""o$2+Ks^7A~z'mM9H^j,Actp:i8zq_啯B[g#nǛj~z-m^f9'c:?ct QYf"Hg';Sv2';xb^Qߕ+$L5wsx@ ecaIX2cNywD e&yf3e'sÌ']I¤Qs'='K  Y]6I4wBM>_ǡ|\$a"$2.˿ShD m?;Y -DoǛLDڼbNmH^z,ۿWiy9z۞q0vehsFl#Dp9=\IDl_o~q\j.l"b4u);&"ҙ̘!H$De,C%41;;<<Df;1̥8^jCO1Μ Hd'\ A @&a~PX&,ܝe,qޡѦz^;`C0o!Gܶ/Njz~1fhmEݶ/dď?W_}=_mk4='1'\9u#*K34),'ehDHg0쇦q"fLGrE6;I$26 Ac`dĜfrQԝFX>,4lfֲ\T#I<ƙ1wڙۣ7IWɶm۶m۶m۶m۶| Y3mYŒI1,$qn&2E& 3f/r2Im"D@kȒDg=Z6h':I&I$a2%sG3y"BK0c6YI<LdޕLf^e0?Dy&yr%:/"%7 s޶m۶m۶m۶m=> OYd1wzt@ 63&βDLvh6%2HIbЈ鼃MDf$aehDdq9#=g-KKqf1AL睻]3DMD猴I,ͩ;,EL"sX&"4b:`mEc ?-Z^K;x{~gq竷m۶m۶m۶mr%b_|pYADdFei0٣}rXd3sYOYYvx2 Fdyr,Hμ+eel2I=ىɌ$a"G"fdmi,5 $d"*K3f&<zr& z՗m۶m۶m۶m۶ws/?o3ET9fYٟШ@xKAMX͏o:#.E%Ad,aF i,#݉@p7hL64웈G\L6dFug#P] -hMDbn~|$Сq),&KdS ߾}H$m۶m۶m۶m۶ϯ_|2NtY41)KYaYYvxu*NdH5iYzQŒI"1$: =uGD@#"m"hIJ\KCODue''\|n\D].Ys6Ie%H$2i=w޽Ͼk?-k>m۶m۶m۶m۶nǿ?|WO>ts8 ]6lp f>siQSwhrN.eyre8isN&LDcF:QYMDhf3ЈD.E6MD^i3f4tu( ͌; 4Fd9' fͲk2Ҧ7=4y'}w?0m۶m۶m۶m۶߉/۷?_mqH<DYe0ҌA<+tVX"/i }1A@ I"ƔMknξSwh&b h"eI#F¬K3jbDYYӲc8ȦIyG\Aozꫯ_~m۶m۶m۶m{pv< ~iW[:up^g 3&bM1eǤ].IŒ$L0#m>RfY'\ qW/jD"tey̲D ̉YflgT1l"fMĢbʎI#\"i%:I,aF|ĥͲO^Ԉ.E7@o ߿?}yyom۶m۶m۶m۶܋믿ly'mip&%ltP1 Wc \j"ΔK眺$$LiƲ,+LgdeY&,Z.겱x@0,9`3t3pOKR1w\:ԝ'&aҨdNsg6ϻ-?/a۶m۶m۶m۶mz\ݻwN8]㼍%D:ģ.B,9'I#,˲fh̦K8UssvdgDfef'Ns&a%"4"ҙe YYf%"ҙ$Јpb9$LaY56C`6] -ę}o #;D,/?\-;zzDo۶m۶m۶m۶m믿O~eW 'm3W` 3f)W0I6Ӳ,#$zD' 4 4s,˚Kzas& XK.KL0AZW` 3f)W0I6Ӳ,#$zD' 4 4s,˚Kzas&{O>{Im۶m۶m۶m۶v?;W㌍A8sHw"v٤&aA[D He&Kyf$Lα_D 1QI̹8 Ј'\qW/j,ND.4$4h 0 d9#b6$94Q.~|n޶m۶m۶m۶m8 TrMryguj.& $ѹX6"bN&lDeei0$2IY6i s }W.,Ke[ M¤A[0e2m0>OesY.r@ d.l"~;LDYV& A"Ĝeix w_mn۶m۶m۶m۶m?o߾uj˜E63i,>0}ҙ4HgF%d"tS7h}sNbβD ItN& & 3t蘍%,I4ЈŲXd3~3 3 ItfhDY"*Nf BG1u&aG<$,KDy_z@m۶m۶m۶m۶ޜq_~O?'mqsL3i5/H0Ig7+K3ҙ1;zsYN"dDfh,yDLvHd~!"mV$LuY - ҙ4~I$̳웕̘ =ٹ|Qv,d'IcI»o޼wvoIZn۶m۶m۶m۶mqZꫯigu54Y⮻ݜ\A"34"ҙe@Z$aե(H-W23m"6Ie6?R."43wFt98@u+ HdFD:, WYY$̺\{}~2ɛm۶m۶m۶mp*v]חZ?_mNfsFԲ|B($2#mF 3b6i0\geyf?tY"$LȬ,3fHg.l^d$aNC#f "DeĜ,q< -34H424Aeh~$YfY] yvM۷Y^m۶m۶m۶m۶n竿>ݻwo޼q'gr̜1C3e9 -ND̾Whά.g  #m2c I$Y2MKD';$:ciDQWhQ]\ݜzD'"f+4HgV3e@dhБ61AFՋf;[:M~+'ٶm۶m۶m۶mn?Ŝϝ9d"'y$a0CFe軖4CȤ~cZ6S6;i6C#Q]6h@,l%Zv,#m"-3cN&LW$2I$Lf ҙ$2+KI QZ. M"Ƣi2sLA (KDu٠=Y4*;]|w}ȶm۶m۶m۶m? r޼y "DLv&i:&Ĝ$GK|n%b0C/Itf,gdet٨ A0cKfhDy2';4';g4$L34J|mK qm۶m۶m۶m۶*竝qZW=_dN8SYf D̎,xȬ,3kYlf9sُ: #;&Ov"ĜY);&DgeID\3c6YfV fe&@2盉2e|fs>:HЋ9;̼߄yoGq;m۶m۶m۶m۶̌S4{#zLYDG\D&]tH$E63';$a2Xdiee9B&  Dee$a%Kd"5AM,K"#.v"A\:gM"Kd0@,l4˲\ng矛ȶm۶m۶m۶m۟|7|g9_]9c.lFzNbnj 3tYFLE3Nd圑62fGvΓw;Y% A"3ҝ4\,36IdXد.Zӓj^gvٌf #g%"ҝ3*9#me̎' v",EFg~O!q.۶m۶m۶m۶m~IW%9I3&);&LD3C#"@ @ܕ2C>;!:4,+LKdD&aFhDۜ99˾vcX&I#n%N6IdN1Id""L`\:yСeYYf"X"D'"4 31ol/z_Nm۶m۶m۶m۶s5?/y;9 H/M9uG dg;cFD efY"#;g:̻zQ@Բ$;HБ6I#Sv26,9eD|&机#SvLb3͝XN1#m"2,Б\D@@O睳y71oh8_d}7?joy'ܶm۶m۶m۶mrpqYv~8C"ީ zC"QYf`vdgSvrihDeywȬ.7n -f ef2ҝg?Ge'%vHy}iqf?4̳y^d34DfLDeIّeN1w7˥I"l,{Bx7ZNV?[ςn۶m۶m۶m۶mW/?Cy'vLzp.WcV1-;Y'3B/l"hшE63C#Hd\3(KwR3& \:'̘}+d34wҙNd'$2e".0p);fu)Ӳy2#&FXd334Dfe:,qW.5C/l0ϼw̼{>NVv۶m۶m۶m۶m*h̹yq.4CeY"DD:%2l6FXF:,=R҈34he1uG0C#B#H̹E˒3KhfDe1˲,DY""n6He#,#YўQsC`ię}]x͛7=Y8Ye۶m۶m۶m۶myyyr '|Aw%"4brKt01n>\9̲D bve3БΌX"t"Kde]f`O6/jHciDME& tfY"B#̬.gD' #˕,K"fW63̘%bN'w-}7OάWo۶m۶m۶m۶i}7wE= $&; eIXƔLgȤA$Y9˄>QYQ] -N'K]H B%,Ө옱4bNα_4jٱЈeYoL$щqLMe8#޲pڻXęjĶm۶m۶m۶m 1vNYONHgFڤQ1C Kd"HtD d%l?겉D0eY]6D<2/=wX""m褁FLv a""iFe ;3, I"Y&bCAfeӒYk.gi/14wډ$2CWs\.]64y'澎I 6C?rM&oVnw?믿>Sm۶m۶m۶mW_J|uj\!tY^ّeeu٘YYIcf&,n7lG^s^A;IbN,hĜ#2 fhB #;,1e3ƔLDYnL41f -mzޥ߾}k:Ym:Ym[9s޶m۶m۶m۶msry圏3?B6h7eyB?K;K@ܕ2dDd$Lf&a-YYh'N"&FAЈ,MC63CG;Ɍ a^v]w(#](sN&9IdNv,IdFhٲe6e_mO&Bo۶m۶m۶m۶mf d'ҝ4f h(4Dp8/t&Y̝tԝF,X6-It'3!*Ls0Ig2Df9uGNh34\QYf"8GЈHɌHwF:AŒ,KN:s㭉+֓՟g@^דb۶m۶m۶m۶m^?~,i6D& 4Ј$LDfhęE7a@Tg˾eM &ĜSv:CW2 ad ItfhD;d3423t3Aڌ$LKdք;UOVu{}z۶m۶m۶m۶/iO{!D@S'Wpz[ܶm۶m۶m۶md:׿NY:#2<;wQɤ1$a"4*;wQs qgO6ϓY]#lL@ *̘e;ϺΜ4 34&iRTg4*;4&$LFe'.!j.ә4\yvT9Y-ķ~?dxsCl۶m۶m۶m۶m9?×_~)pȹ Nѥ@<+'\m^C% 3uA|$W#rd,1;3'L'Wpd,SwDf1{O"q̲D 4.\bN"e&32t웡E79:n6ГHyW/dXtS I,K4wfӥ@ $2cv]Y';L&%vH@.С{1ۑ3Ղw?Ml۶m۶m۶m۶muT?l(>2C#κ`6i<\K@T&LbZvl,.f3Ye,I4MFeǬ.b:Lّ̻\@p7"I SvЈ7ehO,R I".&$挜6q'/}Vdz۶m۶m۶m۶m_/_N -9;$,BT)-~BhDY.FefY]$YYf(;Dhsa5II42&ٵlfYYv"L,34rgB#r175.32thDf&2FA B0cL5ގ~?믿ο oٮmm.۶m۶m۶m8s^>vS]UUUUUUUojkllPfM3c=NO6Q3֏v$̝en2c" &u8wR GÑ㒄9G3W>,3t36I$Lֈ krqzY ~F$a/ss1-„T;~̏Ull?lUUUUUUUUed#dYYiK$p4G9u7:X{{341G陱6$LbGI$2C##bO"M&u0IGsFN8s3fwɣqz:H_DʽD昣XG&p4?v^Q}v~[5eUUUUUUUU]=>>~UX|R # ˥L1rI"鄉tfh\ -KDMCG G&H1LHEЈi 4&ҙL#1r4Ic=x<]\<c&D& Ј$X3C>rL6cZSM ~1ǏM_XUUUUUUUUcYW>|;rPBX4D  9 skwM":CGD X$a0cZG7]i:4b8FĴ]fi3' A"sqb@7.c:sh"$̑cfnz5L i`"aLvWo;TUUUUUUUUr>O4'>VB Ku|<Df3"4A `CpD0AzqiDX'5C69 @ CD # 33IdޙD&bhƮCGEs5$2#yfhG -}cĚAϟ?:VUUUUUUUU˶ ʦY"͢i%L7$a2Ǭ*7L<'03͘`bY\&3^~& D\D ؅kX; vR xWs/;.@fb\t& X?fmWnV `9OŻw|*~MUUUUUUUUJ3A#L˄`MdMDF HgL̑@0`8ެf&`iAsId8"WoVnXL M9fr\&LhDh&h&"4b8F:sN&Hg"Lܘq0?^Z|͟B}͟K說qr+#21]dG*73# ~A "m^! :vtU"ar:4 Df1v#b@ DFı׉@ F"s8"V9IdLs| i*!Vn"CgS=yf6^l]_aQX.VOCfh&5Y#G3{jn:M jbg.`"r kz8С9 -&Fn1G\ }&aQ#Gs?8cn&v31rcD V9 -pBuXSb3Ǐ?rzrr1j',p\ea5h_{ MD\Lcwc&"q;$9rdbAfZo7$vG pDǵ2u""mӉu9 -̣~24 qU2cwe#uLYiM_aM}sscST6ꪪZW.ue=z&*DDX' s+:ҙμ*$8:>HL3qȍ@D:suaw8CDZ;gHg"H;.N.MLu0PH#?̕?"CT37oTqdS}UUUUUUUUUcVF_~} '+)v;Ͼ5&Ϝo߬l.ͱ5b8"8k3Gq֏y˕;L#k\HgƮI@ #ሸC1*7O Un̑c& s57"bgw5b8"8k3Gq֏y˕w̕K8?+Lkjs|1~TtS]UUUUUUUUNAy {v˼C$f8-$2w\"HC*fhc=jޑ@9 -O@ Gs!s丛ለ:#=5B#Vs$1 9 =vǕHqZ HdD0C5b{+34 jz8Q55ijv }˚&\. 씬46QTTY^nqpDM&`3&b>~5?-iCeZ^^k6`gGprL8Z&L"ҙc4yaa6I\\D\2C#"@ +7:^Gd sh& 3#b8F\ s&k_7cm2xa;3 `G[W8QNSEq>/MUUUUUUUUU,읾}* .Ďr?.2cpD쎑DܬrOЫܘLxdmᒄAgkO:Lwѫ\͍@Ȍ bgԈP& q$LvkHgGr쎑VANvڍM jlގz~ݝu~7 êEM>D} Xƌ 'Gh"K&b&"ҙ#7$a"$<va,} G2qoO`g.'Fn$2wv9fƮC3A:sf&&btKWͣ? 4"/M$L60Jh{ +? -7~沭EfUUUUUUUUտe ,I* k4=34 4D& q{s8r t蘞 @DڼCz㹣Vghhɷ9 Lc#U&bl& 3 >H $L@ VnHϼCW1CFFD:34 4D&bmq{s87th&V Ȃz]_I}-}+;j8\6x?';()3ll,pc6՞qObsGuqrc"VnHD9$5hp բEMuwww~-~ċpc!*7ljmRjQ[_[6Z_k|m~ڑ*--3\|>r8"f2&bc$K9r\瘣@p*7@4b&=3F+ yAW9/o[ooo o DZ ‚ڼ\.4"֮3Mey5,̈́͛.f"Br:Ƒ:Icwdn*7#k_0Iq8m[zGF{쫽.ekOD~n13G|nW&$vw2 mGL&q80eb&"hn::(4մ >i1Cp?]./%k+[P.Z6oVv+3C#&ʣWZuU9f218&a7ϳBѲka1ܟ6Y_HNҖ _bv{{kݦUPE > UfH{sx̑չrDhI;s)1.gu=/{ ]UUUUUUUUge5e6iۿb='Gyu$2wM1-Ēu5u%]^5pm!~afy6F)Ƨ7`#bs$aG&< <] t4C{nj9F4//(YMs~4>򲬦MUUUUUUUUUqvY\w}~DYZhAY/'Hg܄&r"'ʣ$2z\6sL#O'S-մGx2c߀,߶fl%vIl Iv65}+7y̰&O)jO2hvxiFkrz `VUUUUUUUU Xvـ L,QS\E0Ic&q<"Vnb&bGzu0`"bj&^Xyq6M#O,-HmJLO*;ٸw}b;\w8zT$̘Nd\>?#n|"Ĭ3,BW03kx?Apq^/=TUUUUUUUUJ6;~:a"HqDDzL\"HLr+71=e'6OkXӶ&j1|ѼCއGsGUnL"O`57>3N>ZayrOOtc}&.5Mfh&.}|>=.UUUUUUUUUU#|ь*–=b57?dD>DD:y+cw$73chʳ𭁗/_:>>>z9_ЬF/ucq̕kGWoxޤMv=ũ%ij*Ȗu829z80CGz&D0#"\7krag61y~%ȩt\~wӗ/_."hEĮUyȌi db LIs8&**"oWqھ`VUUUUUUUUURFH5VxlYCq8is冄i̱w&a*eG34~xr~U#߿~=źƖux97"vr` Uy(sh"L\9 -N.3#t&bU>ߟ6^zU-g28M\A\51DŽiDfM&88}Txz=ɞׯ_}^|U]~/_#/_tojnHd09cgLD?pC;.=cf W#_~QUUUUUUUUU˲)~[Q~ZV@.s8֣Frs"s䘉pIDp5HdzJu^ ]UUUUUUUUU ߛW,W-Z3wrk:ܘ$L$2Q3c:a1sL&րׯ_EX"O޾}k{T+kWgf:=ck1fgFڌU.3GL0-SpfE؎ƴ۬"kri# ǣh*7L\G3Y]$LDh<˗/߼y߿o_}zPG֘ޅh_{ޯEЈЈ^ǫs8""m g}_lbXbzLvH$f":ҙ1̱;>'9LW{וBTUUUUUUUUUjMI|"6NUWv;s)$2G=Ks'&:ҙWMfh&14/i)yi~~/Zz(4x:>:7i3thp :&;s8"VY'x>3yиǪ=P{QQ7۷V/vf6L00#m9N 7fb8^̑13>("ҙϪ7_yiemEҜ~1߿VlSUC۵"L 7$2Јn̑c&"thLcMfM :tL3&DpSUUUUUUUUUUǧ/Za:֣F B#Bs.2Iduh1|LhL<+߶~Qϟ?/O7UUUUUUUUUUcXj>~h=j&6cwId zIx u&zh0cZ0AzҰOY[V9/(tUUUUUUUUUߕ-r9ϧ͇lMNmPrA֭3$i q8snLمI"h&3Id"+7xV[A<ׯM7ڬ.UUUUUUUUU^vnY><<ݽ{.[SjT*®u8"笏=:4"4=1q994m+/_z߾} ߿h^ y B#|紹l UUUUUUUUU٪Y_dmAjMݩ%a̕fhva v˵qG$2cqe:8r?}_WL|>_녨?=4&By ̪BE~z{{oJXZ3YceHnL`U3|q"=O,l!_4OㄵshO{~7 ^η>}OUUUUUUUUU/l+GF߽{gYjk-QaȍIb&h:4biD&IdܰLv13fZGl^RSQm%Ê5\@\g s>Ç‹v5^`UUUUUUUUUrkg[8YYnEXgM3C#+7Ј$fGc2գc =rFXY;www6و_,.˩OHq~ps>OoW3Cp6-߼y# Ц0e{]pVyYPXٚEֶ4X1C#Ǚ;4DLБΌYitĒ¾4KQ{T;uqWz6:/ <3&|x{OO;xonn}ͲGEدqLMcfhj9v#dC%aZf^Ծt^O>YڎZzܜŵ<:fW%0/}ɧe {<9vȞO9>ԾpuA=lr=>??۷o|fUUUUUUUUUXawXYWFVsUD3#m9N߬pj+7L69fsn1HgE5y-EͰMYkŲֳe{)/l1LHseqZ~jڊ0=-O7"7l|b#mD%ZFߒ'Z6|z٦"Xwr9s8^{(Ǚ6$f3C#B$IŒt&3Cf@0 ^ۯbw2Y\Kf=퇮o9o m\A7BЗYC?Դմ˷n<98zl=cSg0{&&9G3`];@UUUUUUUUU+6J۷o®N/+k9;:4+7Ј$fu"HdFDs>D& 4"Ҧ'9txEUck~O57ӶF\6d &va"isWx~:zrhyG_Ԣǰ6=N{gӅoI+̛c=j>? >+g]-ݦ9,,DXa:\^ 4 *HA"M&"$a"B0#9cxgڎf1RY,ij}vYbk rF6ڌ˶mf0q9yoDħi3Ƨwin,庚;1S'1 p8r.ЈЈWyq2X+L=h3&lkbRk'L U__G,m-]1ˍFRFp^\柜_6{d|JS5/Ug2Op?hD J-/Wq_UUUUUUUUUݪFqdYgݡMD\R#B#z2@#u3=+xQ̰EdvXŖU^b>JM3Zpo1ӧi_ʥ/=yBx|1ϵߛOe_-bQUUUUUUUUUs.˗/YYaMgbeg}^}`m({DczfhDL'։@0sܛ r {cG{"2H|4w\bα;zw\"B#B9L|:kyWUUUUUUUUտ5|>?l-->0/pq: .2 "417ߕ$a&@ 1ȌIŒI"̑Lϙg_=?_QUUUUUUUUU ?~\~1cVh;pDf"r4C.13498ʽ:7Ñ5"1]^2DjшDrW?`Lg1\ҼCÑ ezO`WľTUUUUUUUUUaQ+kϟ?[͉C'f{ԑpjJY1r4Lr5ЈЈЈHHg217\{ޛna UUUUUUUUUek~uua$f0#N&q.L2u  aFz:4bhG1Gz&kQfiis\=fAg"@#BǺuo)r_|ŋ }垉@z8FH3U׉=r4I1-H# k LCv79sfh01LWy(z810#9c;ϧk߶ m^X961^D0ADFhD ]dq̽q r3h"B^&3v.LLG Hw2X2s8"b 7I?ϧdeǏ~j݋/lNϴ Gse^jđ{bx iD&LO0-Hd"$̱;27u :4n؅Ib&]L\GΝ\f]n7\.oq˝N+O>{իW.NGcH6`+7LzLg>FǵG.׹1ro"Vn$L Xzw0 ̑cf\퉝7~8/_|>UUUUUUUUU՟߾}7o_<.kE{3"7$̣^e869Un̫~P^ͽ@>s1ibf8"V:k#B#Bgooo竫\\.Z[YZuehk aM&"ҙc= 2$2;.d3Mh "8r]$L\7ӉLML Dx3f~ZϾTUUUUUUUUU.ˏ?㣉۷o__uָC H %HGI+7#9cZ Ǒ.c s8F GDM3+7СG3;sX'Lfh&H$D\N~nF =rWWUUUUUUUUv.[뇇/_ee==Ӗo8"W82#Gs>&&i3G3I1d p\4E& \A*eG5Hc& 1jߦ1}צꪪ[ttM~7޽3gem_-߰ rc\azg.fh9IId?x#mQ͑$Hd:th\ƌt&A$asLd"B&9rDx{? [\C+V? ._~m'~7LlpD$LDhDhD#6:X k3GСId0#I"p 9fȍi\&2zLg1\ҼC-6WWUUUUUUUUEtVwww6_lZXY 7X7yT p-;&a2HdfLcN.Mʍis5ro" mNvΕ13v r@DDxmUUUUUUUUUak}ǏM~753 qy{sehD$2IdX' 3&k01Ĵ]0nXcǵ XM"34 34"4L̑c&1cZD$aF:sg.w&}}uƾ70ƧO޿/ig'L#Gs8>ǣM*73#桉L&V.v t&\N# A"340;d1-"m1y}fUUUUUUUUUׅ>Z j3 ԡW>,ss80p =r4c3chЈ#L6c3Iܘk̝ v昉=r4I1-H =C.ՎW?>>󫪪֎&{_~1 E՟5 Drc"M&L&!AŒIb& pD Hdd#7I$a0$Lqf䘉a3b]:oڛ⬬?~΄>y׳i1`"C#B^nc:aXjn&V.C#B$acpDD0hQMvq&b 3wquF:s% D\Nxsw};$6%~:,փLz+7;K7#$$f s8;y$15#z冉pD9 -DL՞db'Cq8i}/ѷfĻl yAk7?6߾}kZYhYs\q~8 aڱޤg1rN&桉qw7$̣^jz^#ҙ$̝\'"&GAŒﳘo޼?ϗ[eUUUUUUUUU杶t:ߟLhm,CcUHD;Ckw0$8vzUPD0&a0qmIܘЈq&xsU>,331-wUȾ:sᄎʬ~ქׯZ 1;C:pLvNDhD>r#:4=$f޹zz#=s57?cHGI+7#HM7j޴? xWUUUUUUUUU_q~ & @MmѦ! Mb&"4&Hsg.&6C8DG $2c=Ыf:a2sL:ҙs>&QgFjs1?{l|Ťꪪ:|>o߮0VоBܘlM#G3t깛1G:sh&X'1v&aiM& :t31v`I$a&I"33nxW]]UUUUUUUUUu.i[=>>U~AȒж$ah"B#Fɣf&Fnбvs$sg.3CGWFfUBF\HX{*fl&yz[|ꪪr]y ߾}7odk=l 6T 3k H#u"8ZCDF BGDD:qa.'<4^FĴ@ͽ}<4ޯncbcӯ_msUUUUUUUUUúﴱJիW/_|ᬬ3# ƙC#HdF\!aFzLΌt&ML'IŒzg.9CDZ3Id"VF&I":I671M:=ey{{:?\u=>>[gk-6?D [d&b'$LĎK"40f<8"14p9(B1tL'f 13pC;.݇LorxjoHj}˗O>ekmU&[kBN4]D`kii#Јӂ5&*rCb&X{DDz&+7G3Idf<#o}5߾}˲ZWUUUUUUUUߒ5ZZ[ Z;bkmz|Ev+2D&" b57k0i kz6Ckc&$rCb$LLyh$aG&{v -G|$7>|`'鲺oϾ:lickmmŵ#[a'Un@̑c&  cw8vGfi341W>$aXgLOcL'L9rq!)lm|( fgڱެnGI1흠g7DLۦZxg"a+xٜ6www?~|V5V֏62tXZtDıgH6Y#Ѽj:jE蕛8(BG:sQܬ Bz &1yfSwZ6߷_euUUUUUUUUU,M g~m֖ţ $Fh"4WssDf3} 8ÑMWy4t>F8"1/iaMM ?SmSmy~rއUUUUUUUUU+'D\.C[OO[3~Imi-9+ý\=w9rɥi@9&L3Chz\&2s\Ne 1f~w=|Ǐ沭v -|>Z?<W7星rҢ*1#Y#s<]1D&+7FĴ<:3ƑL#7x3{TϦ :ϗś-=='v]( isWn"m210zGX*IdqDF&F0^#y쨱~~8ZS[S|6ñ1t7e5xt6-`ңlVͫPs=Y-UUUUUUUUUU9jbJK,&NkODXb5hl\uc&#7C3f &A$a^!` 3G#Ǚ45"iG/Gfv8z"Mദ6QUUUUUUUUU糟\.ŵk۷o߼ycիWf֡VlM3MY/5ʚ$2Xv&34"4$ aD1A0I %^5: -鬩}Bp\N tUUUUUUUUU՟oKNO,3<߿owmq5vXhwMv v$^':v"1I<ʽ.3&@33ʚ:/JԷfdMmG}>^.8fi5זY\g>|0Eݵŵ&;U,Z3L3V$UnQ 4hDWn؅ aZGfxVܚjOxy·4?6zymyf)\.'Y~y!o/5uV*`3+7$q7бvD .Јz OTNGÚfp/iMk|zQfUUUUUUUUU)Kr6v_ӧO;CSY&V{װ]΍tz*dC&? 4' ah'7''}~ -YYhݴ9ff<AbXLK >m_6}i@_&d_{<>O']Y˶δV`k,MKlK]];^}/` a+mI"9f2_@Xś|9i_ׯ_}ǧTp~YUUUUUUUUUU"Vec+ i!Z%. -lKla6*;tdulbx ȿԿ ǍБ}%k$OTUUUUUUUUUU d#=ZKl]{l}9m,Ƒ83mu9"/W}S5}^zWmUUUUUUUUUUv8e,{M{l`N;a1YhO0do=r韐2db>|r9m& ^=6&䴱[jEhOOi^deLWUUUUUUUUUߒ%0v/bٌOOБ6X]_пqcG9gCa -endstream -endobj - -10 0 obj - 231359 -endobj - -11 0 obj - << /ExtGState << /E1 << /SMask << /Type /Mask - /G 1 0 R - /S /Alpha - >> - /Type /ExtGState - >> >> - /XObject << /X2 5 0 R - /X1 9 0 R - >> - >> -endobj - -12 0 obj - << /Length 13 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -968.000000 0.000000 -0.000000 251.184509 0.000000 0.000000 cm -/X1 Do -Q -q -/E1 gs -/X2 Do -Q - -endstream -endobj - -13 0 obj - 119 -endobj - -14 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 968.000000 252.000000 ] - /Resources 11 0 R - /Contents 12 0 R - /Parent 15 0 R - >> -endobj - -15 0 obj - << /Kids [ 14 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -16 0 obj - << /Pages 15 0 R - /Type /Catalog - >> -endobj - -xref -0 17 -0000000000 65535 f -0000000010 00000 n -0000000497 00000 n -0000000519 00000 n -0000001042 00000 n -0000001064 00000 n -0000012410 00000 n -0000012434 00000 n -0000150121 00000 n -0000150146 00000 n -0000381740 00000 n -0000381766 00000 n -0000382106 00000 n -0000382283 00000 n -0000382306 00000 n -0000382485 00000 n -0000382561 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 16 0 R - /Size 17 ->> -startxref -382622 -%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift index f44e0ca7a..155227685 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift @@ -210,7 +210,6 @@ public enum Asset { public static let elephantThreeOnGrassWithTreeTwo = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass.with.tree.two") } public static let mastodonLogo = ImageAsset(name: "Scene/Welcome/mastodon.logo") - public static let mastodonLogoLarge = ImageAsset(name: "Scene/Welcome/mastodon.logo.large") public static let signInButtonBackground = ColorAsset(name: "Scene/Welcome/sign.in.button.background") } } From 35544b30669e39618755e5e0074baf708896c980 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 16 Nov 2022 18:45:51 +0800 Subject: [PATCH 585/658] fix: aspect ratio --- Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift index 5a9af3dd9..32696d197 100644 --- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift +++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift @@ -243,7 +243,7 @@ extension WelcomeViewController { logoImageView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), logoImageView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: 35), view.readableContentGuide.trailingAnchor.constraint(equalTo: logoImageView.trailingAnchor, constant: 35), - logoImageView.heightAnchor.constraint(equalTo: logoImageView.widthAnchor, multiplier: 65.4/265.1), + logoImageView.heightAnchor.constraint(equalTo: logoImageView.widthAnchor, multiplier: 75.0/269.0), ]) logoImageView.setContentHuggingPriority(.defaultHigh, for: .vertical) } From ea7972c78953b287c77043519442339b771f94b0 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 16 Nov 2022 19:06:09 +0800 Subject: [PATCH 586/658] chore: update i18n resources --- .../xcschemes/xcschememanagement.plist | 4 +-- .../Resources/ar.lproj/Localizable.strings | 29 ++++++++++++------- .../ar.lproj/Localizable.stringsdict | 24 +++++++++++++++ .../Resources/ca.lproj/Localizable.strings | 23 ++++++++++----- .../ca.lproj/Localizable.stringsdict | 16 ++++++++++ .../Resources/ckb.lproj/Localizable.strings | 23 ++++++++++----- .../ckb.lproj/Localizable.stringsdict | 16 ++++++++++ .../Resources/de.lproj/Localizable.strings | 21 ++++++++++---- .../de.lproj/Localizable.stringsdict | 16 ++++++++++ .../Resources/en.lproj/Localizable.strings | 26 ++++++++++------- .../Resources/es.lproj/Localizable.strings | 23 ++++++++++----- .../es.lproj/Localizable.stringsdict | 16 ++++++++++ .../Resources/eu.lproj/Localizable.strings | 23 ++++++++++----- .../eu.lproj/Localizable.stringsdict | 16 ++++++++++ .../Resources/fi.lproj/Localizable.strings | 23 ++++++++++----- .../fi.lproj/Localizable.stringsdict | 16 ++++++++++ .../Resources/fr.lproj/Localizable.strings | 29 ++++++++++++------- .../fr.lproj/Localizable.stringsdict | 16 ++++++++++ .../Resources/gd.lproj/Localizable.strings | 23 ++++++++++----- .../gd.lproj/Localizable.stringsdict | 20 +++++++++++++ .../Resources/gl.lproj/Localizable.strings | 21 ++++++++++---- .../gl.lproj/Localizable.stringsdict | 16 ++++++++++ .../Resources/it.lproj/Localizable.strings | 19 ++++++++---- .../it.lproj/Localizable.stringsdict | 16 ++++++++++ .../Resources/ja.lproj/Localizable.strings | 23 ++++++++++----- .../ja.lproj/Localizable.stringsdict | 14 +++++++++ .../Resources/kab.lproj/Localizable.strings | 23 ++++++++++----- .../kab.lproj/Localizable.stringsdict | 16 ++++++++++ .../Resources/ku.lproj/Localizable.strings | 21 ++++++++++---- .../ku.lproj/Localizable.stringsdict | 16 ++++++++++ .../Resources/nl.lproj/Localizable.strings | 23 ++++++++++----- .../nl.lproj/Localizable.stringsdict | 16 ++++++++++ .../Resources/ru.lproj/Localizable.strings | 23 ++++++++++----- .../ru.lproj/Localizable.stringsdict | 20 +++++++++++++ .../Resources/sv.lproj/Localizable.strings | 19 ++++++++---- .../sv.lproj/Localizable.stringsdict | 16 ++++++++++ .../Resources/th.lproj/Localizable.strings | 29 ++++++++++++------- .../th.lproj/Localizable.stringsdict | 14 +++++++++ .../Resources/tr.lproj/Localizable.strings | 23 ++++++++++----- .../tr.lproj/Localizable.stringsdict | 16 ++++++++++ .../Resources/vi.lproj/Localizable.strings | 27 +++++++++++------ .../vi.lproj/Localizable.stringsdict | 14 +++++++++ .../zh-Hans.lproj/Localizable.strings | 23 ++++++++++----- .../zh-Hans.lproj/Localizable.stringsdict | 14 +++++++++ .../zh-Hant.lproj/Localizable.strings | 29 ++++++++++++------- .../zh-Hant.lproj/Localizable.stringsdict | 14 +++++++++ 46 files changed, 734 insertions(+), 174 deletions(-) diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 78a3a9e70..b2b5b312a 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -117,12 +117,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 16 + 17 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 17 + 16 SuppressBuildableAutocreation diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.strings index 943d56a9f..17d0569c7 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.strings @@ -55,8 +55,8 @@ "Common.Controls.Actions.Share" = "المُشارك"; "Common.Controls.Actions.SharePost" = "مشارك المنشور"; "Common.Controls.Actions.ShareUser" = "مُشارَكَةُ %@"; -"Common.Controls.Actions.SignIn" = "تسجيل الدخول"; -"Common.Controls.Actions.SignUp" = "إنشاء حِساب"; +"Common.Controls.Actions.SignIn" = "تسجيلُ الدخول"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "تخطي"; "Common.Controls.Actions.TakePhoto" = "اِلتِقاطُ صُورَة"; "Common.Controls.Actions.TryAgain" = "المُحاولة مرة أُخرى"; @@ -161,17 +161,21 @@ "Scene.Compose.Accessibility.CustomEmojiPicker" = "منتقي الرموز التعبيرية المُخصَّص"; "Scene.Compose.Accessibility.DisableContentWarning" = "تعطيل تحذير المُحتَوى"; "Scene.Compose.Accessibility.EnableContentWarning" = "تفعيل تحذير المُحتَوى"; +"Scene.Compose.Accessibility.PostOptions" = "Post Options"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "قائمة ظهور المنشور"; +"Scene.Compose.Accessibility.PostingAs" = "نَشر كَـ %@"; "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.AttachmentTooLarge" = "المُرفَق كَبيرٌ جِدًّا"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "يتعذَّرُ التعرُّفُ على وسائِطِ هذا المُرفَق"; +"Scene.Compose.Attachment.CompressingState" = "يجري الضغط..."; "Scene.Compose.Attachment.DescriptionPhoto" = "صِف الصورة للمَكفوفين..."; "Scene.Compose.Attachment.DescriptionVideo" = "صِف المقطع المرئي للمَكفوفين..."; -"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; +"Scene.Compose.Attachment.LoadFailed" = "فَشَلَ التَّحميل"; "Scene.Compose.Attachment.Photo" = "صورة"; -"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; +"Scene.Compose.Attachment.ServerProcessingState" = "مُعالجة الخادم جارِيَة..."; +"Scene.Compose.Attachment.UploadFailed" = "فَشَلَ الرَّفع"; "Scene.Compose.Attachment.Video" = "مقطع مرئي"; "Scene.Compose.AutoComplete.SpaceToAdd" = "انقر على مساحة لإضافتِها"; "Scene.Compose.ComposeAction" = "نَشر"; @@ -192,6 +196,8 @@ "Scene.Compose.Poll.OptionNumber" = "الخيار %ld"; "Scene.Compose.Poll.SevenDays" = "سبعةُ أيام"; "Scene.Compose.Poll.SixHours" = "سِتُّ ساعات"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "يوجَدُ خِيارٌ فارِغٌ فِي الاِستِطلاع"; +"Scene.Compose.Poll.ThePollIsInvalid" = "الاِستِطلاعُ غيرُ صالِح"; "Scene.Compose.Poll.ThirtyMinutes" = "ثلاثون دقيقة"; "Scene.Compose.Poll.ThreeDays" = "ثلاثةُ أيام"; "Scene.Compose.ReplyingToUser" = "رَدًا على %@"; @@ -234,6 +240,9 @@ "Scene.HomeTimeline.NavigationBarState.Published" = "تمَّ النَّشر!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "يَجري نَشر المُشارَكَة..."; "Scene.HomeTimeline.Title" = "الرَّئِيسَة"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "قَبُول"; "Scene.Notification.FollowRequest.Accepted" = "مَقبُول"; "Scene.Notification.FollowRequest.Reject" = "رَفض"; @@ -261,6 +270,8 @@ "Scene.Profile.Fields.AddRow" = "إضافة صف"; "Scene.Profile.Fields.Placeholder.Content" = "المُحتَوى"; "Scene.Profile.Fields.Placeholder.Label" = "التسمية"; +"Scene.Profile.Fields.Verified.Long" = "تمَّ التَّحقق مِن مِلكية هذا الرابِطِ بِتاريخ %@"; +"Scene.Profile.Fields.Verified.Short" = "تمَّ التَّحقق بِتاريخ %@"; "Scene.Profile.Header.FollowsYou" = "يُتابِعُك"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "تأكيدُ حَظر %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "حَظرُ الحِساب"; @@ -393,13 +404,11 @@ "Scene.ServerPicker.EmptyState.BadNetwork" = "حدث خطأٌ ما أثناء تحميل البيانات. تحقَّق من اتصالك بالإنترنت."; "Scene.ServerPicker.EmptyState.FindingServers" = "يجري إيجاد خوادم متوفِّرَة..."; "Scene.ServerPicker.EmptyState.NoResults" = "لا توجد نتائج"; -"Scene.ServerPicker.Input.Placeholder" = "اِبحَث عن خادِم أو انضم إلى آخر خاص بك..."; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "اِبحَث فِي الخَوادِم أو أدخِل رابِط"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "الفئة"; "Scene.ServerPicker.Label.Language" = "اللُّغَة"; "Scene.ServerPicker.Label.Users" = "مُستَخدِم"; -"Scene.ServerPicker.Subtitle" = "اختر مجتمعًا بناءً على اهتماماتك، منطقتك أو يمكنك حتى اختيارُ مجتمعٍ ذي غرضٍ عام."; -"Scene.ServerPicker.SubtitleExtend" = "اختر مجتمعًا بناءً على اهتماماتك، منطقتك أو يمكنك حتى اختيارُ مجتمعٍ ذي غرضٍ عام. تُشغَّل جميعُ المجتمعِ مِن قِبَلِ مُنظمَةٍ أو فردٍ مُستقلٍ تمامًا."; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "اِختر خادِم، أيًّا مِنهُم."; "Scene.ServerRules.Button.Confirm" = "أنا مُوافِق"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.stringsdict index 862d98184..91368a4fb 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.stringsdict @@ -74,6 +74,30 @@ %ld حَرف + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + لَا حَرف + one + حَرفٌ واحِد + two + حَرفانِ اِثنان + few + %ld characters + many + %ld characters + other + %ld حَرف + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings index fca658aef..d8fa6a739 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings @@ -55,8 +55,8 @@ Comprova la teva connexió a Internet."; "Common.Controls.Actions.Share" = "Comparteix"; "Common.Controls.Actions.SharePost" = "Compartir Publicació"; "Common.Controls.Actions.ShareUser" = "Compartir %@"; -"Common.Controls.Actions.SignIn" = "Iniciar sessió"; -"Common.Controls.Actions.SignUp" = "Registre"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "Omet"; "Common.Controls.Actions.TakePhoto" = "Fes una foto"; "Common.Controls.Actions.TryAgain" = "Torna a provar"; @@ -161,16 +161,20 @@ El teu perfil els sembla així."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Selector d'Emoji Personalitzat"; "Scene.Compose.Accessibility.DisableContentWarning" = "Desactiva l'Avís de Contingut"; "Scene.Compose.Accessibility.EnableContentWarning" = "Activa l'Avís de Contingut"; +"Scene.Compose.Accessibility.PostOptions" = "Opcions del tut"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Menú de Visibilitat de Publicació"; +"Scene.Compose.Accessibility.PostingAs" = "Publicant com a %@"; "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.CanNotRecognizeThisMediaAttachment" = "No es pot reconèixer aquest adjunt multimèdia"; +"Scene.Compose.Attachment.CompressingState" = "Comprimint..."; "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.ServerProcessingState" = "Servidor processant..."; "Scene.Compose.Attachment.UploadFailed" = "Pujada fallida"; "Scene.Compose.Attachment.Video" = "vídeo"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Espai per afegir"; @@ -192,6 +196,8 @@ carregat a Mastodon."; "Scene.Compose.Poll.OptionNumber" = "Opció %ld"; "Scene.Compose.Poll.SevenDays" = "7 Dies"; "Scene.Compose.Poll.SixHours" = "6 Hores"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "L'enquesta té una opció buida"; +"Scene.Compose.Poll.ThePollIsInvalid" = "L'enquesta no és vàlida"; "Scene.Compose.Poll.ThirtyMinutes" = "30 minuts"; "Scene.Compose.Poll.ThreeDays" = "3 Dies"; "Scene.Compose.ReplyingToUser" = "responent a %@"; @@ -234,6 +240,9 @@ carregat a Mastodon."; "Scene.HomeTimeline.NavigationBarState.Published" = "Publicat!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "S'està publicant..."; "Scene.HomeTimeline.Title" = "Inici"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "Acceptar"; "Scene.Notification.FollowRequest.Accepted" = "Acceptat"; "Scene.Notification.FollowRequest.Reject" = "rebutjar"; @@ -261,6 +270,8 @@ carregat a Mastodon."; "Scene.Profile.Fields.AddRow" = "Afegeix fila"; "Scene.Profile.Fields.Placeholder.Content" = "Contingut"; "Scene.Profile.Fields.Placeholder.Label" = "Etiqueta"; +"Scene.Profile.Fields.Verified.Long" = "La propietat d'aquest enllaç es va verificar el dia %@"; +"Scene.Profile.Fields.Verified.Short" = "Verificat a %@"; "Scene.Profile.Header.FollowsYou" = "Et segueix"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirma per a bloquejar %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Bloqueja el Compte"; @@ -393,13 +404,11 @@ carregat a Mastodon."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Alguna cosa no ha anat bé en carregar les dades. Comprova la teva connexió a Internet."; "Scene.ServerPicker.EmptyState.FindingServers" = "Cercant els servidors disponibles..."; "Scene.ServerPicker.EmptyState.NoResults" = "No hi ha resultats"; -"Scene.ServerPicker.Input.Placeholder" = "Cerca servidors"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Cerca servidors o introdueix l'enllaç"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "CATEGORIA"; "Scene.ServerPicker.Label.Language" = "LLENGUATGE"; "Scene.ServerPicker.Label.Users" = "USUARIS"; -"Scene.ServerPicker.Subtitle" = "Tria una comunitat segons els teus interessos, regió o una de propòsit general."; -"Scene.ServerPicker.SubtitleExtend" = "Tria una comunitat segons els teus interessos, regió o una de propòsit general. Cada comunitat és operada per una organització totalment independent o individualment."; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Mastodon està fet d'usuaris en diferents comunitats."; "Scene.ServerRules.Button.Confirm" = "Hi estic d'acord"; "Scene.ServerRules.PrivacyPolicy" = "política de privadesa"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.stringsdict index cc28edbc6..947597417 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld caràcters + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + resten %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 caràcter + other + %ld caràcters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.strings index 05c575520..4e76e20fb 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.strings @@ -55,8 +55,8 @@ "Common.Controls.Actions.Share" = "هاوبەشی بکە"; "Common.Controls.Actions.SharePost" = "هاوبەشی بکە"; "Common.Controls.Actions.ShareUser" = "%@ هاوبەش بکە"; -"Common.Controls.Actions.SignIn" = "بچۆ ژوورەوە"; -"Common.Controls.Actions.SignUp" = "خۆت تۆمار بکە"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "بیپەڕێنە"; "Common.Controls.Actions.TakePhoto" = "وێنە بگرە"; "Common.Controls.Actions.TryAgain" = "هەوڵ بدەوە"; @@ -161,15 +161,19 @@ "Scene.Compose.Accessibility.CustomEmojiPicker" = "هەڵبژێری ئیمۆجی"; "Scene.Compose.Accessibility.DisableContentWarning" = "ئاگاداریی ناوەڕۆک ناچالاک بکە"; "Scene.Compose.Accessibility.EnableContentWarning" = "ئاگاداریی ناوەڕۆک چالاک بکە"; +"Scene.Compose.Accessibility.PostOptions" = "Post Options"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "پێڕستی شێوازی دەرکەوتنی پۆست"; +"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; "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.CanNotRecognizeThisMediaAttachment" = "Can not recognize this media attachment"; +"Scene.Compose.Attachment.CompressingState" = "Compressing..."; "Scene.Compose.Attachment.DescriptionPhoto" = "وێنەکەت بۆ نابیناکان باس بکە..."; "Scene.Compose.Attachment.DescriptionVideo" = "ڤیدیۆکەت بۆ نابیناکان باس بکە..."; "Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "وێنە"; +"Scene.Compose.Attachment.ServerProcessingState" = "Server Processing..."; "Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "ڤیدیۆ"; "Scene.Compose.AutoComplete.SpaceToAdd" = "بۆشایی دابنێ بۆ زیادکردن"; @@ -191,6 +195,8 @@ "Scene.Compose.Poll.OptionNumber" = "بژاردەی %ld"; "Scene.Compose.Poll.SevenDays" = "7 ڕۆژ"; "Scene.Compose.Poll.SixHours" = "6 کاتژمێر"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; +"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; "Scene.Compose.Poll.ThirtyMinutes" = "30 خولەک"; "Scene.Compose.Poll.ThreeDays" = "3 ڕۆژ"; "Scene.Compose.ReplyingToUser" = "لە وەڵامدا بۆ %@"; @@ -233,6 +239,9 @@ "Scene.HomeTimeline.NavigationBarState.Published" = "بڵاوکرایەوە!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "پۆستەکە بڵاو دەکرێتەوە..."; "Scene.HomeTimeline.Title" = "ماڵەوە"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "Accept"; "Scene.Notification.FollowRequest.Accepted" = "Accepted"; "Scene.Notification.FollowRequest.Reject" = "reject"; @@ -260,6 +269,8 @@ "Scene.Profile.Fields.AddRow" = "ڕیز زیاد بکە"; "Scene.Profile.Fields.Placeholder.Content" = "ناوەڕۆک"; "Scene.Profile.Fields.Placeholder.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" = "دڵنیا ببەوە بۆ ئاستەنگکردنی %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "ئاستەنگی بکە"; @@ -392,13 +403,11 @@ "Scene.ServerPicker.EmptyState.BadNetwork" = "هەڵەیەک ڕوویدا لە کاتی بارکردن. لە هەبوونی هێڵی ئینتەرنێت دڵنیا بە."; "Scene.ServerPicker.EmptyState.FindingServers" = "ڕاژەکار دەدۆزرێتەوە..."; "Scene.ServerPicker.EmptyState.NoResults" = "ئەنجام نییە"; -"Scene.ServerPicker.Input.Placeholder" = "بگەڕێ"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search servers or enter URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "بەش"; "Scene.ServerPicker.Label.Language" = "زمان"; "Scene.ServerPicker.Label.Users" = "بەکارهێنەر"; -"Scene.ServerPicker.Subtitle" = "ڕاژەکارێکێکی گشتی یان دانەیەک لەسەر بنەمای حەزەکانت و هەرێمەکەت هەڵبژێرە."; -"Scene.ServerPicker.SubtitleExtend" = "ڕاژەکارێکێکی گشتی یان دانەیەک لەسەر بنەمای حەزەکانت و هەرێمەکەت هەڵبژێرە. هەر ڕاژەکارێک لەلایەن ڕێکخراوێک یان تاکەکەسێک بەڕێوە دەبرێت."; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "ماستۆدۆن لە چەندان بەکارهێنەر پێک دێت کە لە ڕاژەکاری جیاواز دان."; "Scene.ServerRules.Button.Confirm" = "ڕازیم"; "Scene.ServerRules.PrivacyPolicy" = "سیاسەتی تایبەتێتی"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.stringsdict index 001a8a608..8116226ec 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld نووسە + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings index 4f9a15906..89e826349 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings @@ -55,8 +55,8 @@ Bitte überprüfe deine Internetverbindung."; "Common.Controls.Actions.Share" = "Teilen"; "Common.Controls.Actions.SharePost" = "Beitrag teilen"; "Common.Controls.Actions.ShareUser" = "%@ teilen"; -"Common.Controls.Actions.SignIn" = "Anmelden"; -"Common.Controls.Actions.SignUp" = "Registrieren"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "Überspringen"; "Common.Controls.Actions.TakePhoto" = "Foto aufnehmen"; "Common.Controls.Actions.TryAgain" = "Nochmals versuchen"; @@ -161,16 +161,20 @@ Dein Profil sieht für diesen Benutzer auch so aus."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Benutzerdefinierter Emojiwähler"; "Scene.Compose.Accessibility.DisableContentWarning" = "Inhaltswarnung ausschalten"; "Scene.Compose.Accessibility.EnableContentWarning" = "Inhaltswarnung einschalten"; +"Scene.Compose.Accessibility.PostOptions" = "Post Options"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Sichtbarkeitsmenü"; +"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; "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.CompressingState" = "Komprimieren..."; "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.ServerProcessingState" = "Serververarbeitung..."; "Scene.Compose.Attachment.UploadFailed" = "Upload fehlgeschlagen"; "Scene.Compose.Attachment.Video" = "Video"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Leerzeichen um hinzuzufügen"; @@ -192,6 +196,8 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.Compose.Poll.OptionNumber" = "Auswahlmöglichkeit %ld"; "Scene.Compose.Poll.SevenDays" = "7 Tage"; "Scene.Compose.Poll.SixHours" = "6 Stunden"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "Die Umfrage hat eine leere Option"; +"Scene.Compose.Poll.ThePollIsInvalid" = "Die Umfrage ist ungültig"; "Scene.Compose.Poll.ThirtyMinutes" = "30 Minuten"; "Scene.Compose.Poll.ThreeDays" = "3 Tage"; "Scene.Compose.ReplyingToUser" = "antwortet auf %@"; @@ -234,6 +240,9 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.HomeTimeline.NavigationBarState.Published" = "Veröffentlicht!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Beitrag wird veröffentlicht..."; "Scene.HomeTimeline.Title" = "Startseite"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "Akzeptieren"; "Scene.Notification.FollowRequest.Accepted" = "Akzeptiert"; "Scene.Notification.FollowRequest.Reject" = "Ablehnen"; @@ -261,6 +270,8 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.Profile.Fields.AddRow" = "Zeile hinzufügen"; "Scene.Profile.Fields.Placeholder.Content" = "Inhalt"; "Scene.Profile.Fields.Placeholder.Label" = "Bezeichnung"; +"Scene.Profile.Fields.Verified.Long" = "Besitz des Links wurde überprüft am %@"; +"Scene.Profile.Fields.Verified.Short" = "Verified on %@"; "Scene.Profile.Header.FollowsYou" = "Folgt dir"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Bestätige %@ zu blockieren"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Konto blockieren"; @@ -393,13 +404,11 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Beim Laden der Daten ist etwas schief gelaufen. Überprüfe deine Internetverbindung."; "Scene.ServerPicker.EmptyState.FindingServers" = "Verfügbare Server werden gesucht..."; "Scene.ServerPicker.EmptyState.NoResults" = "Keine Ergebnisse"; -"Scene.ServerPicker.Input.Placeholder" = "Nach Server suchen oder URL eingeben"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Nach Server suchen oder URL eingeben"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "KATEGORIE"; "Scene.ServerPicker.Label.Language" = "SPRACHE"; "Scene.ServerPicker.Label.Users" = "BENUTZER"; -"Scene.ServerPicker.Subtitle" = "Wähle eine Gemeinschaft, die auf deinen Interessen, Region oder einem allgemeinen Zweck basiert."; -"Scene.ServerPicker.SubtitleExtend" = "Wähle eine Gemeinschaft basierend auf deinen Interessen, deiner Region oder einem allgemeinen Zweck. Jede Gemeinschaft wird von einer völlig unabhängigen Organisation oder Einzelperson betrieben."; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Wähle einen Server, beliebigen Server."; "Scene.ServerRules.Button.Confirm" = "Ich stimme zu"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.stringsdict index f60c6b0d7..1965fd02b 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld Zeichen + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings index 1a48d8c55..2a3f1efbf 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings @@ -55,8 +55,8 @@ Please check your internet connection."; "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.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "Skip"; "Common.Controls.Actions.TakePhoto" = "Take Photo"; "Common.Controls.Actions.TryAgain" = "Try Again"; @@ -168,11 +168,13 @@ Your profile looks like this to them."; "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.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"; @@ -194,6 +196,8 @@ uploaded to Mastodon."; "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 %@"; @@ -236,6 +240,9 @@ uploaded to Mastodon."; "Scene.HomeTimeline.NavigationBarState.Published" = "Published!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post..."; "Scene.HomeTimeline.Title" = "Home"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "Accept"; "Scene.Notification.FollowRequest.Accepted" = "Accepted"; "Scene.Notification.FollowRequest.Reject" = "reject"; @@ -263,6 +270,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"; @@ -315,9 +324,6 @@ uploaded to Mastodon."; "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.Login.Title" = "Welcome Back!"; -"Scene.Login.Subtitle" = "Log you in with the server where you created your account"; -"Scene.Login.ServerSearchField.Placeholder" = "Search server or enter URL"; "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?"; @@ -398,13 +404,11 @@ uploaded to Mastodon."; "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.Input.SearchServersOrEnterUrl" = "Search communities 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.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Mastodon is made of users in different servers."; "Scene.ServerRules.Button.Confirm" = "I Agree"; "Scene.ServerRules.PrivacyPolicy" = "privacy policy"; @@ -457,4 +461,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 diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.strings index c16bec6cf..b2cb18954 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.strings @@ -55,8 +55,8 @@ Por favor, revise su conexión a internet."; "Common.Controls.Actions.Share" = "Compartir"; "Common.Controls.Actions.SharePost" = "Compartir publicación"; "Common.Controls.Actions.ShareUser" = "Compartir %@"; -"Common.Controls.Actions.SignIn" = "Iniciar sesión"; -"Common.Controls.Actions.SignUp" = "Regístrate"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "Omitir"; "Common.Controls.Actions.TakePhoto" = "Tomar foto"; "Common.Controls.Actions.TryAgain" = "Inténtalo de nuevo"; @@ -161,16 +161,20 @@ Tu perfil se ve así para él."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Selector de Emojis Personalizados"; "Scene.Compose.Accessibility.DisableContentWarning" = "Desactivar Advertencia de Contenido"; "Scene.Compose.Accessibility.EnableContentWarning" = "Activar Advertencia de Contenido"; +"Scene.Compose.Accessibility.PostOptions" = "Post Options"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Menú de Visibilidad de la Publicación"; +"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; "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.CanNotRecognizeThisMediaAttachment" = "Can not recognize this media attachment"; +"Scene.Compose.Attachment.CompressingState" = "Compressing..."; "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.ServerProcessingState" = "Server Processing..."; "Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "vídeo"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Espacio para añadir"; @@ -192,6 +196,8 @@ subirse a Mastodon."; "Scene.Compose.Poll.OptionNumber" = "Opción %ld"; "Scene.Compose.Poll.SevenDays" = "7 Días"; "Scene.Compose.Poll.SixHours" = "6 Horas"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; +"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; "Scene.Compose.Poll.ThirtyMinutes" = "30 minutos"; "Scene.Compose.Poll.ThreeDays" = "4 Días"; "Scene.Compose.ReplyingToUser" = "en respuesta a %@"; @@ -235,6 +241,9 @@ pulsa en el enlace para confirmar tu cuenta."; "Scene.HomeTimeline.NavigationBarState.Published" = "¡Publicado!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Publicación en curso..."; "Scene.HomeTimeline.Title" = "Inicio"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "Aceptar"; "Scene.Notification.FollowRequest.Accepted" = "Aceptado"; "Scene.Notification.FollowRequest.Reject" = "rechazar"; @@ -262,6 +271,8 @@ pulsa en el enlace para confirmar tu cuenta."; "Scene.Profile.Fields.AddRow" = "Añadir Fila"; "Scene.Profile.Fields.Placeholder.Content" = "Contenido"; "Scene.Profile.Fields.Placeholder.Label" = "Nombre para el campo"; +"Scene.Profile.Fields.Verified.Long" = "Ownership of this link was checked on %@"; +"Scene.Profile.Fields.Verified.Short" = "Verified on %@"; "Scene.Profile.Header.FollowsYou" = "Te sigue"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirmar para bloquear a %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Bloquear cuenta"; @@ -394,13 +405,11 @@ pulsa en el enlace para confirmar tu cuenta."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Algo ha ido mal al cargar los datos. Comprueba tu conexión a Internet."; "Scene.ServerPicker.EmptyState.FindingServers" = "Encontrando servidores disponibles..."; "Scene.ServerPicker.EmptyState.NoResults" = "Sin resultados"; -"Scene.ServerPicker.Input.Placeholder" = "Encuentra un servidor o únete al tuyo propio..."; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Buscar servidores o introducir la URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "CATEGORÍA"; "Scene.ServerPicker.Label.Language" = "IDIOMA"; "Scene.ServerPicker.Label.Users" = "USUARIOS"; -"Scene.ServerPicker.Subtitle" = "Elige una comunidad relacionada con tus intereses, con tu región o una más genérica."; -"Scene.ServerPicker.SubtitleExtend" = "Elige una comunidad relacionada con tus intereses, con tu región o una más genérica. Cada comunidad está operada por una organización o individuo completamente independiente."; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Elige un servidor, cualquier servidor."; "Scene.ServerRules.Button.Confirm" = "Acepto"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.stringsdict index def3d7bba..ca07b6b28 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld caracteres + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.strings index aef7a7507..e2985112e 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.strings @@ -55,8 +55,8 @@ Egiaztatu Interneteko konexioa."; "Common.Controls.Actions.Share" = "Partekatu"; "Common.Controls.Actions.SharePost" = "Partekatu bidalketa"; "Common.Controls.Actions.ShareUser" = "Partekatu %@"; -"Common.Controls.Actions.SignIn" = "Hasi saioa"; -"Common.Controls.Actions.SignUp" = "Eman Izena"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "Saltatu"; "Common.Controls.Actions.TakePhoto" = "Atera argazkia"; "Common.Controls.Actions.TryAgain" = "Saiatu berriro"; @@ -161,16 +161,20 @@ Zure profilak itxura hau du berarentzat."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Emoji pertsonalizatuen hautatzailea"; "Scene.Compose.Accessibility.DisableContentWarning" = "Desgaitu edukiaren abisua"; "Scene.Compose.Accessibility.EnableContentWarning" = "Gaitu edukiaren abisua"; +"Scene.Compose.Accessibility.PostOptions" = "Post Options"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Bidalketaren ikusgaitasunaren menua"; +"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; "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.CanNotRecognizeThisMediaAttachment" = "Can not recognize this media attachment"; +"Scene.Compose.Attachment.CompressingState" = "Compressing..."; "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.ServerProcessingState" = "Server Processing..."; "Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "bideoa"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Sakatu zuriunea gehitzeko"; @@ -192,6 +196,8 @@ Mastodonera igo."; "Scene.Compose.Poll.OptionNumber" = "%ld aukera"; "Scene.Compose.Poll.SevenDays" = "7 egun"; "Scene.Compose.Poll.SixHours" = "6 ordu"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; +"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; "Scene.Compose.Poll.ThirtyMinutes" = "30 minutu"; "Scene.Compose.Poll.ThreeDays" = "3 egun"; "Scene.Compose.ReplyingToUser" = "%@(r)i erantzuten"; @@ -234,6 +240,9 @@ Mastodonera igo."; "Scene.HomeTimeline.NavigationBarState.Published" = "Argitaratua!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Bidalketa argitaratzen..."; "Scene.HomeTimeline.Title" = "Hasiera"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "Accept"; "Scene.Notification.FollowRequest.Accepted" = "Accepted"; "Scene.Notification.FollowRequest.Reject" = "reject"; @@ -261,6 +270,8 @@ Mastodonera igo."; "Scene.Profile.Fields.AddRow" = "Gehitu errenkada"; "Scene.Profile.Fields.Placeholder.Content" = "Edukia"; "Scene.Profile.Fields.Placeholder.Label" = "Etiketa"; +"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" = "Berretsi %@ blokeatzea"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Blokeatu kontua"; @@ -393,13 +404,11 @@ Mastodonera igo."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Arazoren bat egon da datuak kargatzean. Egiaztatu zure Interneteko konexioa."; "Scene.ServerPicker.EmptyState.FindingServers" = "Erabilgarri dauden zerbitzariak bilatzen..."; "Scene.ServerPicker.EmptyState.NoResults" = "Emaitzarik ez"; -"Scene.ServerPicker.Input.Placeholder" = "Bilatu zerbitzari bat edo sortu zurea..."; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search servers or enter URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "KATEGORIA"; "Scene.ServerPicker.Label.Language" = "HIZKUNTZA"; "Scene.ServerPicker.Label.Users" = "ERABILTZAILEAK"; -"Scene.ServerPicker.Subtitle" = "Aukeratu komunitate bat zure interes edo lurraldearen arabera, edo erabilera orokorreko bat."; -"Scene.ServerPicker.SubtitleExtend" = "Aukeratu komunitate bat zure interes edo lurraldearen arabera, edo erabilera orokorreko bat. Komunitate bakoitza erakunde edo norbanako independente batek kudeatzen du."; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Aukeratu zerbitzari bat, edozein zerbitzari."; "Scene.ServerRules.Button.Confirm" = "Ados nago"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.stringsdict index 0159a7da9..057ca4010 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld karaktere + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.strings index 11259ace5..7902fb6eb 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.strings @@ -55,8 +55,8 @@ Tarkista internet-yhteytesi."; "Common.Controls.Actions.Share" = "Jaa"; "Common.Controls.Actions.SharePost" = "Jaa julkaisu"; "Common.Controls.Actions.ShareUser" = "Jaa %@"; -"Common.Controls.Actions.SignIn" = "Kirjaudu sisään"; -"Common.Controls.Actions.SignUp" = "Rekisteröidy"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "Ohita"; "Common.Controls.Actions.TakePhoto" = "Ota kuva"; "Common.Controls.Actions.TryAgain" = "Yritä uudelleen"; @@ -161,16 +161,20 @@ Profiilisi näyttää tältä hänelle."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Mukautettu emojivalitsin"; "Scene.Compose.Accessibility.DisableContentWarning" = "Poista sisältövaroitus käytöstä"; "Scene.Compose.Accessibility.EnableContentWarning" = "Ota sisältövaroitus käyttöön"; +"Scene.Compose.Accessibility.PostOptions" = "Post Options"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Julkaisun näkyvyysvalikko"; +"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; "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.CanNotRecognizeThisMediaAttachment" = "Can not recognize this media attachment"; +"Scene.Compose.Attachment.CompressingState" = "Compressing..."; "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.ServerProcessingState" = "Server Processing..."; "Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Space to add"; @@ -192,6 +196,8 @@ uploaded to Mastodon."; "Scene.Compose.Poll.OptionNumber" = "Vaihtoehto %ld"; "Scene.Compose.Poll.SevenDays" = "7 päivää"; "Scene.Compose.Poll.SixHours" = "6 tuntia"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; +"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; "Scene.Compose.Poll.ThirtyMinutes" = "30 minuuttia"; "Scene.Compose.Poll.ThreeDays" = "3 päivää"; "Scene.Compose.ReplyingToUser" = "vastaamassa tilille %@"; @@ -234,6 +240,9 @@ uploaded to Mastodon."; "Scene.HomeTimeline.NavigationBarState.Published" = "Julkaistu!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Julkaistaan julkaisua..."; "Scene.HomeTimeline.Title" = "Koti"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "Accept"; "Scene.Notification.FollowRequest.Accepted" = "Accepted"; "Scene.Notification.FollowRequest.Reject" = "reject"; @@ -261,6 +270,8 @@ uploaded to Mastodon."; "Scene.Profile.Fields.AddRow" = "Lisää rivi"; "Scene.Profile.Fields.Placeholder.Content" = "Sisältö"; "Scene.Profile.Fields.Placeholder.Label" = "Nimi"; +"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"; @@ -393,13 +404,11 @@ uploaded to Mastodon."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Jokin meni pieleen dataa ladatessa. Tarkista internet-yhteytesi."; "Scene.ServerPicker.EmptyState.FindingServers" = "Etsistään saatavilla olevia palvelimia..."; "Scene.ServerPicker.EmptyState.NoResults" = "Ei hakutuloksia"; -"Scene.ServerPicker.Input.Placeholder" = "Etsi palvelin tai liity omaan..."; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search servers or enter URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "KATEGORIA"; "Scene.ServerPicker.Label.Language" = "KIELI"; "Scene.ServerPicker.Label.Users" = "TILIÄ"; -"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.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Valitse palvelin, mikä tahansa palvelin."; "Scene.ServerRules.Button.Confirm" = "Hyväksyn"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.stringsdict index 8048edf2d..ccfee35c9 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld merkkiä + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings index 931219c21..59590f612 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings @@ -55,8 +55,8 @@ Veuillez vérifier votre accès à Internet."; "Common.Controls.Actions.Share" = "Partager"; "Common.Controls.Actions.SharePost" = "Partager la publication"; "Common.Controls.Actions.ShareUser" = "Partager %@"; -"Common.Controls.Actions.SignIn" = "Se connecter"; -"Common.Controls.Actions.SignUp" = "Créer un compte"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "Passer"; "Common.Controls.Actions.TakePhoto" = "Prendre une photo"; "Common.Controls.Actions.TryAgain" = "Réessayer"; @@ -161,17 +161,21 @@ Votre profil ressemble à ça pour lui."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Sélecteur d’émojis personnalisés"; "Scene.Compose.Accessibility.DisableContentWarning" = "Désactiver l'avertissement de contenu"; "Scene.Compose.Accessibility.EnableContentWarning" = "Basculer l’avertissement de contenu"; +"Scene.Compose.Accessibility.PostOptions" = "Options de publication"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Menu de Visibilité de la publication"; +"Scene.Compose.Accessibility.PostingAs" = "Publié en tant que %@"; "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.AttachmentTooLarge" = "La pièce jointe est trop volumineuse"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Impossible de reconnaître cette pièce jointe"; +"Scene.Compose.Attachment.CompressingState" = "Compression..."; "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.LoadFailed" = "Échec du chargement"; "Scene.Compose.Attachment.Photo" = "photo"; -"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; +"Scene.Compose.Attachment.ServerProcessingState" = "Traitement du serveur..."; +"Scene.Compose.Attachment.UploadFailed" = "Échec de l’envoi"; "Scene.Compose.Attachment.Video" = "vidéo"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Espace à ajouter"; "Scene.Compose.ComposeAction" = "Publier"; @@ -192,6 +196,8 @@ téléversé sur Mastodon."; "Scene.Compose.Poll.OptionNumber" = "Option %ld"; "Scene.Compose.Poll.SevenDays" = "7 jour"; "Scene.Compose.Poll.SixHours" = "6 Heures"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "Le sondage n'a pas d'options"; +"Scene.Compose.Poll.ThePollIsInvalid" = "Le sondage est invalide"; "Scene.Compose.Poll.ThirtyMinutes" = "30 minutes"; "Scene.Compose.Poll.ThreeDays" = "3 jour"; "Scene.Compose.ReplyingToUser" = "répondre à %@"; @@ -234,6 +240,9 @@ téléversé sur Mastodon."; "Scene.HomeTimeline.NavigationBarState.Published" = "Publié!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Publication en cours ..."; "Scene.HomeTimeline.Title" = "Accueil"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "Accepter"; "Scene.Notification.FollowRequest.Accepted" = "Accepté"; "Scene.Notification.FollowRequest.Reject" = "rejeter"; @@ -261,6 +270,8 @@ téléversé sur Mastodon."; "Scene.Profile.Fields.AddRow" = "Ajouter une rangée"; "Scene.Profile.Fields.Placeholder.Content" = "Contenu"; "Scene.Profile.Fields.Placeholder.Label" = "Étiquette"; +"Scene.Profile.Fields.Verified.Long" = "La propriété de ce lien a été vérifiée le %@"; +"Scene.Profile.Fields.Verified.Short" = "Vérifié le %@"; "Scene.Profile.Header.FollowsYou" = "Vous suit"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirmer le blocage de %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Bloquer le compte"; @@ -393,13 +404,11 @@ téléversé sur Mastodon."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Une erreur s'est produite lors du chargement des données. Vérifiez votre connexion Internet."; "Scene.ServerPicker.EmptyState.FindingServers" = "Recherche des serveurs disponibles..."; "Scene.ServerPicker.EmptyState.NoResults" = "Aucun résultat"; -"Scene.ServerPicker.Input.Placeholder" = "Trouvez un serveur ou rejoignez le vôtre..."; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Rechercher des serveurs ou entrer une URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "CATÉGORIE"; "Scene.ServerPicker.Label.Language" = "LANGUE"; "Scene.ServerPicker.Label.Users" = "UTILISATEUR·RICE·S"; -"Scene.ServerPicker.Subtitle" = "Choisissez une communauté en fonction de vos intérêts, de votre région ou de votre objectif général."; -"Scene.ServerPicker.SubtitleExtend" = "Choisissez une communauté basée sur vos intérêts, votre région ou un but général. Chaque communauté est gérée par une organisation ou un individu entièrement indépendant."; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Choisissez un serveur, n'importe quel serveur."; "Scene.ServerRules.Button.Confirm" = "J’accepte"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.stringsdict index d9d860a47..4eb068697 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld caractères + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ restants + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 caractère + other + %ld caractères + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings index ce1764eac..aea2902fc 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings @@ -55,8 +55,8 @@ Thoir sùil air a’ cheangal agad ris an eadar-lìon."; "Common.Controls.Actions.Share" = "Co-roinn"; "Common.Controls.Actions.SharePost" = "Co-roinn am post"; "Common.Controls.Actions.ShareUser" = "Co-roinn %@"; -"Common.Controls.Actions.SignIn" = "Clàraich a-steach"; -"Common.Controls.Actions.SignUp" = "Clàraich leinn"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "Leum thairis air"; "Common.Controls.Actions.TakePhoto" = "Tog dealbh"; "Common.Controls.Actions.TryAgain" = "Feuch ris a-rithist"; @@ -161,16 +161,20 @@ Seo an coltas a th’ air a’ phròifil agad dhaibh-san."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Roghnaichear nan Emoji gnàthaichte"; "Scene.Compose.Accessibility.DisableContentWarning" = "Cuir rabhadh susbainte à comas"; "Scene.Compose.Accessibility.EnableContentWarning" = "Cuir rabhadh susbainte an comas"; +"Scene.Compose.Accessibility.PostOptions" = "Post Options"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Clàr-taice faicsinneachd a’ phuist"; +"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; "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.CanNotRecognizeThisMediaAttachment" = "Can not recognize this media attachment"; +"Scene.Compose.Attachment.CompressingState" = "Compressing..."; "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.ServerProcessingState" = "Server Processing..."; "Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Brùth air Space gus a chur ris"; @@ -192,6 +196,8 @@ a luchdadh suas gu Mastodon."; "Scene.Compose.Poll.OptionNumber" = "Roghainn %ld"; "Scene.Compose.Poll.SevenDays" = "Seachdain"; "Scene.Compose.Poll.SixHours" = "6 uairean a thìde"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; +"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; "Scene.Compose.Poll.ThirtyMinutes" = "Leth-uair a thìde"; "Scene.Compose.Poll.ThreeDays" = "3 làithean"; "Scene.Compose.ReplyingToUser" = "a’ freagairt gu %@"; @@ -234,6 +240,9 @@ a luchdadh suas gu Mastodon."; "Scene.HomeTimeline.NavigationBarState.Published" = "Chaidh fhoillseachadh!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "A’ foillseachadh a’ phuist…"; "Scene.HomeTimeline.Title" = "Dachaigh"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "Gabh ris"; "Scene.Notification.FollowRequest.Accepted" = "Air a ghabhail ris"; "Scene.Notification.FollowRequest.Reject" = "diùlt"; @@ -261,6 +270,8 @@ a luchdadh suas gu Mastodon."; "Scene.Profile.Fields.AddRow" = "Cuir ràgh ris"; "Scene.Profile.Fields.Placeholder.Content" = "Susbaint"; "Scene.Profile.Fields.Placeholder.Label" = "Leubail"; +"Scene.Profile.Fields.Verified.Long" = "Ownership of this link was checked on %@"; +"Scene.Profile.Fields.Verified.Short" = "Verified on %@"; "Scene.Profile.Header.FollowsYou" = "’Gad leantainn"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Dearbh bacadh %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Bac an cunntas"; @@ -393,13 +404,11 @@ a luchdadh suas gu Mastodon."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Chaidh rudeigin ceàrr le luchdadh an dàta. Thoir sùil air a’ cheangal agad ris an eadar-lìon."; "Scene.ServerPicker.EmptyState.FindingServers" = "A’ lorg nam frithealaichean ri am faighinn…"; "Scene.ServerPicker.EmptyState.NoResults" = "Gun toradh"; -"Scene.ServerPicker.Input.Placeholder" = "Lorg frithealaiche"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Lorg frithealaiche no cuir a-steach URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "ROINN-SEÒRSA"; "Scene.ServerPicker.Label.Language" = "CÀNAN"; "Scene.ServerPicker.Label.Users" = "CLEACHDAICHEAN"; -"Scene.ServerPicker.Subtitle" = "Tagh frithealaiche stèidhichte air d’ ùidhean, air far a bheil thu no fear coitcheann."; -"Scene.ServerPicker.SubtitleExtend" = "Tagh frithealaiche stèidhichte air d’ ùidhean, air far a bheil thu no fear coitcheann. Tha gach frithealaiche fo stiùireadh buidhinn no neach neo-eisimeilich fa leth."; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Tha cleachdaichean Mhastodon air iomadh frithealaiche eadar-dhealaichte."; "Scene.ServerRules.Button.Confirm" = "Gabhaidh mi ris"; "Scene.ServerRules.PrivacyPolicy" = "poileasaidh prìobhaideachd"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.stringsdict index d0ccb5f41..45ba1e156 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.stringsdict @@ -62,6 +62,26 @@ %ld caractar + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + two + %ld characters + few + %ld characters + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings index 3087f33c5..b388ad5c3 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings @@ -55,8 +55,8 @@ Comproba a conexión a internet."; "Common.Controls.Actions.Share" = "Compartir"; "Common.Controls.Actions.SharePost" = "Compartir publicación"; "Common.Controls.Actions.ShareUser" = "Compartir %@"; -"Common.Controls.Actions.SignIn" = "Acceder"; -"Common.Controls.Actions.SignUp" = "Inscribirse"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "Omitir"; "Common.Controls.Actions.TakePhoto" = "Facer foto"; "Common.Controls.Actions.TryAgain" = "Intentar de novo"; @@ -161,16 +161,20 @@ Así se ve o teu perfil."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Selector emoji personalizado"; "Scene.Compose.Accessibility.DisableContentWarning" = "Retirar Aviso sobre o contido"; "Scene.Compose.Accessibility.EnableContentWarning" = "Marcar con Aviso sobre o contido"; +"Scene.Compose.Accessibility.PostOptions" = "Opcións da publicación"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Visibilidade da publicación"; +"Scene.Compose.Accessibility.PostingAs" = "Publicando como %@"; "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.CompressingState" = "Comprimindo..."; "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.ServerProcessingState" = "Procesando no servidor..."; "Scene.Compose.Attachment.UploadFailed" = "Erro na subida"; "Scene.Compose.Attachment.Video" = "vídeo"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Barra de espazo engade"; @@ -192,6 +196,8 @@ ser subido a Mastodon."; "Scene.Compose.Poll.OptionNumber" = "Opción %ld"; "Scene.Compose.Poll.SevenDays" = "7 Días"; "Scene.Compose.Poll.SixHours" = "6 Horas"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "A enquisa ten unha opción baleira"; +"Scene.Compose.Poll.ThePollIsInvalid" = "A enquisa non é válida"; "Scene.Compose.Poll.ThirtyMinutes" = "30 minutos"; "Scene.Compose.Poll.ThreeDays" = "3 Días"; "Scene.Compose.ReplyingToUser" = "en resposta a %@"; @@ -234,6 +240,9 @@ ser subido a Mastodon."; "Scene.HomeTimeline.NavigationBarState.Published" = "Publicado!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Publicando..."; "Scene.HomeTimeline.Title" = "Inicio"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "Aceptar"; "Scene.Notification.FollowRequest.Accepted" = "Aceptada"; "Scene.Notification.FollowRequest.Reject" = "rexeitar"; @@ -261,6 +270,8 @@ ser subido a Mastodon."; "Scene.Profile.Fields.AddRow" = "Engadir fila"; "Scene.Profile.Fields.Placeholder.Content" = "Contido"; "Scene.Profile.Fields.Placeholder.Label" = "Etiqueta"; +"Scene.Profile.Fields.Verified.Long" = "A propiedade desta ligazón foi verificada o %@"; +"Scene.Profile.Fields.Verified.Short" = "Verificada en %@"; "Scene.Profile.Header.FollowsYou" = "Séguete"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirma o bloqueo de %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Bloquear Conta"; @@ -393,13 +404,11 @@ ser subido a Mastodon."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Algo fallou ao cargar os datos. Comproba a conexión a internet."; "Scene.ServerPicker.EmptyState.FindingServers" = "Buscando servidores dispoñibles..."; "Scene.ServerPicker.EmptyState.NoResults" = "Sen resultados"; -"Scene.ServerPicker.Input.Placeholder" = "Buscar comunidades"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Busca un servidor ou escribe URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "CATEGORÍA"; "Scene.ServerPicker.Label.Language" = "IDIOMA"; "Scene.ServerPicker.Label.Users" = "USUARIAS"; -"Scene.ServerPicker.Subtitle" = "Elixe unha comunidade en función dos teus intereses, rexión ou unha de propósito xeral."; -"Scene.ServerPicker.SubtitleExtend" = "Elixe unha comunidade en función dos teus intereses, rexión ou unha de propósito xeral. Cada comunidade está xestionada por unha organización totalmente independente ou unha única persoa."; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Mastodon fórmano as persoas das diferentes comunidades."; "Scene.ServerRules.Button.Confirm" = "Acepto"; "Scene.ServerRules.PrivacyPolicy" = "polícica de privacidade"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.stringsdict index ff9d87c18..51b146ed4 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld caracteres + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ restantes + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 caracter + other + %ld caracteres + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings index 8f99028ed..fab67c38d 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings @@ -56,7 +56,7 @@ Per favore verifica la tua connessione internet."; "Common.Controls.Actions.SharePost" = "Condividi il post"; "Common.Controls.Actions.ShareUser" = "Condividi %@"; "Common.Controls.Actions.SignIn" = "Accedi"; -"Common.Controls.Actions.SignUp" = "Registrati"; +"Common.Controls.Actions.SignUp" = "Crea un account"; "Common.Controls.Actions.Skip" = "Salta"; "Common.Controls.Actions.TakePhoto" = "Scatta foto"; "Common.Controls.Actions.TryAgain" = "Riprova"; @@ -161,16 +161,20 @@ Il tuo profilo sembra questo per loro."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Selettore Emoji personalizzato"; "Scene.Compose.Accessibility.DisableContentWarning" = "Disabilita avviso di contenuti"; "Scene.Compose.Accessibility.EnableContentWarning" = "Abilita avvertimento contenuti"; +"Scene.Compose.Accessibility.PostOptions" = "Opzioni del messaggio"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Menu di visibilità del post"; +"Scene.Compose.Accessibility.PostingAs" = "Pubblicazione come %@"; "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.CompressingState" = "Compressione in corso..."; "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.ServerProcessingState" = "Elaborazione del server in corso..."; "Scene.Compose.Attachment.UploadFailed" = "Caricamento fallito"; "Scene.Compose.Attachment.Video" = "filmato"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Spazio da aggiungere"; @@ -192,6 +196,8 @@ caricato su Mastodon."; "Scene.Compose.Poll.OptionNumber" = "Opzione %ld"; "Scene.Compose.Poll.SevenDays" = "7 giorni"; "Scene.Compose.Poll.SixHours" = "6 ore"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "Il sondaggio ha un'opzione vuota"; +"Scene.Compose.Poll.ThePollIsInvalid" = "Il sondaggio non è valido"; "Scene.Compose.Poll.ThirtyMinutes" = "30 minuti"; "Scene.Compose.Poll.ThreeDays" = "3 giorni"; "Scene.Compose.ReplyingToUser" = "rispondendo a %@"; @@ -234,6 +240,9 @@ caricato su Mastodon."; "Scene.HomeTimeline.NavigationBarState.Published" = "Pubblicato!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Pubblicazione post..."; "Scene.HomeTimeline.Title" = "Inizio"; +"Scene.Login.ServerSearchField.Placeholder" = "Inserisci l'URL o cerca il tuo server"; +"Scene.Login.Subtitle" = "Accedi al server sul quale hai creato il tuo account."; +"Scene.Login.Title" = "Bentornato/a"; "Scene.Notification.FollowRequest.Accept" = "Accetta"; "Scene.Notification.FollowRequest.Accepted" = "Richiesta accettata"; "Scene.Notification.FollowRequest.Reject" = "rifiuta"; @@ -261,6 +270,8 @@ caricato su Mastodon."; "Scene.Profile.Fields.AddRow" = "Aggiungi riga"; "Scene.Profile.Fields.Placeholder.Content" = "Contenuto"; "Scene.Profile.Fields.Placeholder.Label" = "Etichetta"; +"Scene.Profile.Fields.Verified.Long" = "La proprietà di questo collegamento è stata verificata il %@"; +"Scene.Profile.Fields.Verified.Short" = "Verificato il %@"; "Scene.Profile.Header.FollowsYou" = "Ti segue"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confermi di bloccare %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Blocca account"; @@ -393,13 +404,11 @@ caricato su Mastodon."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Qualcosa è andato storto durante il caricamento dei dati. Controlla la tua connessione internet."; "Scene.ServerPicker.EmptyState.FindingServers" = "Ricerca server disponibili..."; "Scene.ServerPicker.EmptyState.NoResults" = "Nessun risultato"; -"Scene.ServerPicker.Input.Placeholder" = "Cerca comunità"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Cerca i server o inserisci l'URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Cerca le comunità o inserisci l'URL"; "Scene.ServerPicker.Label.Category" = "CATEGORIA"; "Scene.ServerPicker.Label.Language" = "LINGUA"; "Scene.ServerPicker.Label.Users" = "UTENTI"; -"Scene.ServerPicker.Subtitle" = "Scegli una comunità basata sui tuoi interessi, regione o uno scopo generale."; -"Scene.ServerPicker.SubtitleExtend" = "Scegli una comunità basata sui tuoi interessi, regione o uno scopo generale. Ogni comunità è gestita da un'organizzazione completamente indipendente o individuale."; +"Scene.ServerPicker.Subtitle" = "Scegli un server in base alla tua regione, ai tuoi interessi o uno generico. Puoi comunque chattare con chiunque su Mastodon, indipendentemente dai tuoi server."; "Scene.ServerPicker.Title" = "Mastodon è fatto di utenti in diverse comunità."; "Scene.ServerRules.Button.Confirm" = "Accetto"; "Scene.ServerRules.PrivacyPolicy" = "privacy policy"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.stringsdict index 38f986521..3a8549914 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld caratteri + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ rimanenti + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 carattere + other + %ld caratteri + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings index cad44f531..01701dfc2 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings @@ -55,8 +55,8 @@ "Common.Controls.Actions.Share" = "共有"; "Common.Controls.Actions.SharePost" = "投稿を共有"; "Common.Controls.Actions.ShareUser" = "%@を共有"; -"Common.Controls.Actions.SignIn" = "サインイン"; -"Common.Controls.Actions.SignUp" = "サインアップ"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "スキップ"; "Common.Controls.Actions.TakePhoto" = "写真を撮る"; "Common.Controls.Actions.TryAgain" = "再実行"; @@ -157,15 +157,19 @@ "Scene.Compose.Accessibility.CustomEmojiPicker" = "カスタム絵文字ピッカー"; "Scene.Compose.Accessibility.DisableContentWarning" = "閲覧注意を無効にする"; "Scene.Compose.Accessibility.EnableContentWarning" = "閲覧注意を有効にする"; +"Scene.Compose.Accessibility.PostOptions" = "Post Options"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "投稿の表示メニュー"; +"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; "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.CanNotRecognizeThisMediaAttachment" = "Can not recognize this media attachment"; +"Scene.Compose.Attachment.CompressingState" = "Compressing..."; "Scene.Compose.Attachment.DescriptionPhoto" = "閲覧が難しいユーザーへの画像説明"; "Scene.Compose.Attachment.DescriptionVideo" = "閲覧が難しいユーザーへの映像説明"; "Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "写真"; +"Scene.Compose.Attachment.ServerProcessingState" = "Server Processing..."; "Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "動画"; "Scene.Compose.AutoComplete.SpaceToAdd" = "スペースを追加"; @@ -187,6 +191,8 @@ "Scene.Compose.Poll.OptionNumber" = "オプション %ld"; "Scene.Compose.Poll.SevenDays" = "7日"; "Scene.Compose.Poll.SixHours" = "6時間"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; +"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; "Scene.Compose.Poll.ThirtyMinutes" = "30分"; "Scene.Compose.Poll.ThreeDays" = "3日"; "Scene.Compose.ReplyingToUser" = "%@に返信"; @@ -229,6 +235,9 @@ "Scene.HomeTimeline.NavigationBarState.Published" = "投稿しました!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "投稿中..."; "Scene.HomeTimeline.Title" = "ホーム"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "承認"; "Scene.Notification.FollowRequest.Accepted" = "承諾済み"; "Scene.Notification.FollowRequest.Reject" = "拒否"; @@ -256,6 +265,8 @@ "Scene.Profile.Fields.AddRow" = "行追加"; "Scene.Profile.Fields.Placeholder.Content" = "コンテンツ"; "Scene.Profile.Fields.Placeholder.Label" = "ラベル"; +"Scene.Profile.Fields.Verified.Long" = "Ownership of this link was checked on %@"; +"Scene.Profile.Fields.Verified.Short" = "Verified on %@"; "Scene.Profile.Header.FollowsYou" = "フォローされています"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "%@をブロックしますか?"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "アカウントをブロック"; @@ -388,13 +399,11 @@ "Scene.ServerPicker.EmptyState.BadNetwork" = "データの読み込み中に何か問題が発生しました。インターネットの接続状況を確認してください。"; "Scene.ServerPicker.EmptyState.FindingServers" = "利用可能なサーバーの検索..."; "Scene.ServerPicker.EmptyState.NoResults" = "なし"; -"Scene.ServerPicker.Input.Placeholder" = "サーバーを探す"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "サーバーを検索またはURLを入力"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "カテゴリー"; "Scene.ServerPicker.Label.Language" = "言語"; "Scene.ServerPicker.Label.Users" = "ユーザー"; -"Scene.ServerPicker.Subtitle" = "あなたの興味分野・地域に合ったコミュニティや、汎用のものを選択してください。"; -"Scene.ServerPicker.SubtitleExtend" = "あなたの興味分野・地域に合ったコミュニティや、汎用のものを選択してください。各コミュニティはそれぞれ完全に独立した組織や個人によって運営されています。"; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "サーバーを選択"; "Scene.ServerRules.Button.Confirm" = "同意する"; "Scene.ServerRules.PrivacyPolicy" = "プライバシーポリシー"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.stringsdict index cbc999738..795a971b7 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.stringsdict @@ -44,6 +44,20 @@ %ld 文字 + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings index 03108a25a..c80c8ef77 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings @@ -55,8 +55,8 @@ Ma ulac aɣilif, senqed tuqqna-inek internet."; "Common.Controls.Actions.Share" = "Bḍu"; "Common.Controls.Actions.SharePost" = "Bḍu tasuffeɣt"; "Common.Controls.Actions.ShareUser" = "Bḍu %@"; -"Common.Controls.Actions.SignIn" = "Qqen"; -"Common.Controls.Actions.SignUp" = "Jerred amiḍan"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "Zgel"; "Common.Controls.Actions.TakePhoto" = "Ṭṭef tawlaft"; "Common.Controls.Actions.TryAgain" = "Ɛreḍ tikkelt-nniḍen"; @@ -161,16 +161,20 @@ Akka i as-d-yettban umaɣnu-inek."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Amefran n yimujiten udmawanen"; "Scene.Compose.Accessibility.DisableContentWarning" = "Sens alɣu n ugbur"; "Scene.Compose.Accessibility.EnableContentWarning" = "Rmed alɣu n ugbur"; +"Scene.Compose.Accessibility.PostOptions" = "Post Options"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Umuɣ n ubani n tsuffeɣt"; +"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; "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.CanNotRecognizeThisMediaAttachment" = "Can not recognize this media attachment"; +"Scene.Compose.Attachment.CompressingState" = "Compressing..."; "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.ServerProcessingState" = "Server Processing..."; "Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "tavidyutt"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Tallunt ara yettwarnun"; @@ -192,6 +196,8 @@ Ad d-yettwasali ɣef Mastodon."; "Scene.Compose.Poll.OptionNumber" = "Taxtiṛt %ld"; "Scene.Compose.Poll.SevenDays" = "7 n wussan"; "Scene.Compose.Poll.SixHours" = "6 n yisragen"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; +"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; "Scene.Compose.Poll.ThirtyMinutes" = "30 n tesdatin"; "Scene.Compose.Poll.ThreeDays" = "3 n wussan"; "Scene.Compose.ReplyingToUser" = "tiririt ɣef %@"; @@ -234,6 +240,9 @@ Ad d-yettwasali ɣef Mastodon."; "Scene.HomeTimeline.NavigationBarState.Published" = "Yettwasuffeɣ!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Asuffeɣ tasuffeɣt..."; "Scene.HomeTimeline.Title" = "Agejdan"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "Accept"; "Scene.Notification.FollowRequest.Accepted" = "Accepted"; "Scene.Notification.FollowRequest.Reject" = "agi"; @@ -261,6 +270,8 @@ Ad d-yettwasali ɣef Mastodon."; "Scene.Profile.Fields.AddRow" = "Rnu izirig"; "Scene.Profile.Fields.Placeholder.Content" = "Agbur"; "Scene.Profile.Fields.Placeholder.Label" = "Tabzimt"; +"Scene.Profile.Fields.Verified.Long" = "Ownership of this link was checked on %@"; +"Scene.Profile.Fields.Verified.Short" = "Verified on %@"; "Scene.Profile.Header.FollowsYou" = "Yeṭṭafaṛ-ik•im"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Sentem asewḥel n %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Sewḥel amiḍan"; @@ -393,13 +404,11 @@ Ad d-yettwasali ɣef Mastodon."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Tella-d tuccḍa lawan n usali n yisefka. Senqed tuqqna-ink internet."; "Scene.ServerPicker.EmptyState.FindingServers" = "Tifin n yiqeddacen yellan..."; "Scene.ServerPicker.EmptyState.NoResults" = "Ulac igemmaḍ"; -"Scene.ServerPicker.Input.Placeholder" = "Nadi timɣiwnin"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Nadi timɣiwnin neɣ sekcem URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "TAGGAYT"; "Scene.ServerPicker.Label.Language" = "TUTLAYT"; "Scene.ServerPicker.Label.Users" = "ISEQDACEN"; -"Scene.ServerPicker.Subtitle" = "Fren tamɣiwent almend n wayen tḥemmleḍ, n tmurt-ik neɣ n yiswi-inek amatu."; -"Scene.ServerPicker.SubtitleExtend" = "Fren tamɣiwent almend n wayen tḥemmleḍ, n tmurt-ik neɣ n yiswi-inek amatu. Yal tamɣiwent tsedday-itt tkebbanit neɣ amdan ilelliyen."; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Mastodon yettwaxdem i yiseqdacen deg waṭas n temɣiwnin."; "Scene.ServerRules.Button.Confirm" = "Qebleɣ"; "Scene.ServerRules.PrivacyPolicy" = "tasertit tabaḍnit"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.stringsdict index 7fc6a50bb..fd7cac605 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld yisekkilen + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings index 10d88488a..758a9190b 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings @@ -55,8 +55,8 @@ Jkx girêdana înternetê xwe kontrol bike."; "Common.Controls.Actions.Share" = "Parve bike"; "Common.Controls.Actions.SharePost" = "Şandiyê parve bike"; "Common.Controls.Actions.ShareUser" = "%@ parve bike"; -"Common.Controls.Actions.SignIn" = "Têkeve"; -"Common.Controls.Actions.SignUp" = "Tomar bibe"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "Derbas bike"; "Common.Controls.Actions.TakePhoto" = "Wêne bikişîne"; "Common.Controls.Actions.TryAgain" = "Dîsa biceribîne"; @@ -161,16 +161,20 @@ Profîla te ji wan ra wiha xuya dike."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Hilbijêrê emojî yên kesanekirî"; "Scene.Compose.Accessibility.DisableContentWarning" = "Hişyariya naverokê neçalak bike"; "Scene.Compose.Accessibility.EnableContentWarning" = "Hişyariya naverokê çalak bike"; +"Scene.Compose.Accessibility.PostOptions" = "Vebijêrkên şandiyê"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Kulîna xuyabûna şandiyê"; +"Scene.Compose.Accessibility.PostingAs" = "Biweşîne wekî %@"; "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.CompressingState" = "Tê guvaştin..."; "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.ServerProcessingState" = "Pêvajoya rajekar pêş de diçe..."; "Scene.Compose.Attachment.UploadFailed" = "Barkirin têk çû"; "Scene.Compose.Attachment.Video" = "vîdyo"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Bicîhkirinê tevlî bike"; @@ -192,6 +196,8 @@ Profîla te ji wan ra wiha xuya dike."; "Scene.Compose.Poll.OptionNumber" = "Vebijêrk %ld"; "Scene.Compose.Poll.SevenDays" = "7 Roj"; "Scene.Compose.Poll.SixHours" = "6 Demjimêr"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "Vebijêrkên vê dengdayînê vala ne"; +"Scene.Compose.Poll.ThePollIsInvalid" = "Ev dengdayîn ne derbasdar e"; "Scene.Compose.Poll.ThirtyMinutes" = "30 xulek"; "Scene.Compose.Poll.ThreeDays" = "3 Roj"; "Scene.Compose.ReplyingToUser" = "bersiv bide %@"; @@ -235,6 +241,9 @@ girêdanê bitikne da ku ajimêra xwe bidî piştrastkirin."; "Scene.HomeTimeline.NavigationBarState.Published" = "Hate weşandin!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Şandî tê weşandin..."; "Scene.HomeTimeline.Title" = "Serrûpel"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "Bipejirîne"; "Scene.Notification.FollowRequest.Accepted" = "Pejirandî"; "Scene.Notification.FollowRequest.Reject" = "nepejirîne"; @@ -262,6 +271,8 @@ girêdanê bitikne da ku ajimêra xwe bidî piştrastkirin."; "Scene.Profile.Fields.AddRow" = "Rêzê tevlî bike"; "Scene.Profile.Fields.Placeholder.Content" = "Naverok"; "Scene.Profile.Fields.Placeholder.Label" = "Nîşan"; +"Scene.Profile.Fields.Verified.Long" = "Xwedaniya li vê girêdanê di %@ de hatiye kontrolkirin"; +"Scene.Profile.Fields.Verified.Short" = "Hate piştrastkirin li ser %@"; "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"; @@ -394,13 +405,11 @@ girêdanê bitikne da ku ajimêra xwe bidî piştrastkirin."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Di dema barkirina daneyan da çewtî derket. Girêdana xwe ya înternetê kontrol bike."; "Scene.ServerPicker.EmptyState.FindingServers" = "Peydakirina rajekarên berdest..."; "Scene.ServerPicker.EmptyState.NoResults" = "Encam tune"; -"Scene.ServerPicker.Input.Placeholder" = "Li rajekaran bigere"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Li rajekaran bigere an jî girêdanê têxe"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "BEŞ"; "Scene.ServerPicker.Label.Language" = "ZIMAN"; "Scene.ServerPicker.Label.Users" = "BIKARHÊNER"; -"Scene.ServerPicker.Subtitle" = "Li gorî berjewendî, herêm, an jî armancek gelemperî civakekê hilbijêre."; -"Scene.ServerPicker.SubtitleExtend" = "Li gorî berjewendî, herêm, an jî armancek gelemperî civakekê hilbijêre. Her civakek ji hêla rêxistinek an kesek bi tevahî serbixwe ve tê xebitandin."; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Mastodon ji bikarhênerên di civakên cuda de pêk tê."; "Scene.ServerRules.Button.Confirm" = "Ez dipejirînim"; "Scene.ServerRules.PrivacyPolicy" = "polîtikaya nihêniyê"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.stringsdict index 77571439f..c904186d8 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld tîp + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ maye + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 peyv + other + %ld peyv + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.strings index e719583aa..0d8f1dd0f 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.strings @@ -54,8 +54,8 @@ "Common.Controls.Actions.Share" = "Deel"; "Common.Controls.Actions.SharePost" = "Deel bericht"; "Common.Controls.Actions.ShareUser" = "Delen %@"; -"Common.Controls.Actions.SignIn" = "Aanmelden"; -"Common.Controls.Actions.SignUp" = "Registreren"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "Overslaan"; "Common.Controls.Actions.TakePhoto" = "Maak foto"; "Common.Controls.Actions.TryAgain" = "Probeer Opnieuw"; @@ -156,15 +156,19 @@ Uw profiel ziet er zo uit voor hen."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Eigen Emojikiezer"; "Scene.Compose.Accessibility.DisableContentWarning" = "Inhoudswaarschuwing Uitschakelen"; "Scene.Compose.Accessibility.EnableContentWarning" = "Inhoudswaarschuwing inschakelen"; +"Scene.Compose.Accessibility.PostOptions" = "Post Options"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Berichtzichtbaarheidsmenu"; +"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; "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.CanNotRecognizeThisMediaAttachment" = "Can not recognize this media attachment"; +"Scene.Compose.Attachment.CompressingState" = "Compressing..."; "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.ServerProcessingState" = "Server Processing..."; "Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Spaties toe te voegen"; @@ -186,6 +190,8 @@ Uw profiel ziet er zo uit voor hen."; "Scene.Compose.Poll.OptionNumber" = "Optie %ld"; "Scene.Compose.Poll.SevenDays" = "7 Dagen"; "Scene.Compose.Poll.SixHours" = "6 Uur"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; +"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; "Scene.Compose.Poll.ThirtyMinutes" = "30 minuten"; "Scene.Compose.Poll.ThreeDays" = "3 Dagen"; "Scene.Compose.ReplyingToUser" = "reageren op %@"; @@ -229,6 +235,9 @@ klik op de link om uw account te bevestigen."; "Scene.HomeTimeline.NavigationBarState.Published" = "Gepubliceerd!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Bericht publiceren..."; "Scene.HomeTimeline.Title" = "Start"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "Accept"; "Scene.Notification.FollowRequest.Accepted" = "Accepted"; "Scene.Notification.FollowRequest.Reject" = "reject"; @@ -256,6 +265,8 @@ klik op de link om uw account te bevestigen."; "Scene.Profile.Fields.AddRow" = "Rij Toevoegen"; "Scene.Profile.Fields.Placeholder.Content" = "Inhoud"; "Scene.Profile.Fields.Placeholder.Label" = "Etiket"; +"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" = "Bevestig om %@ te blokkeren"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Blokkeer account"; @@ -388,13 +399,11 @@ klik op de link om uw account te bevestigen."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Er is een fout opgetreden bij het laden van de gegevens. Controleer uw internetverbinding."; "Scene.ServerPicker.EmptyState.FindingServers" = "Beschikbare servers zoeken..."; "Scene.ServerPicker.EmptyState.NoResults" = "Geen resultaten"; -"Scene.ServerPicker.Input.Placeholder" = "Zoek uw server of sluit u bij een nieuwe server aan..."; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search servers or enter URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "CATEGORIE"; "Scene.ServerPicker.Label.Language" = "TAAL"; "Scene.ServerPicker.Label.Users" = "GEBRUIKERS"; -"Scene.ServerPicker.Subtitle" = "Kies een gemeenschap gebaseerd op jouw interesses, regio of een algemeen doel."; -"Scene.ServerPicker.SubtitleExtend" = "Kies een gemeenschap gebaseerd op jouw interesses, regio, of een algemeen doel. Elke gemeenschap wordt beheerd door een volledig onafhankelijke organisatie of individu."; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Kies een server, welke dan ook."; "Scene.ServerRules.Button.Confirm" = "Ik Ga Akkoord"; "Scene.ServerRules.PrivacyPolicy" = "privacybeleid"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.stringsdict index 314600ff7..84769b0c1 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld tekens + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.strings index 65d4fa65e..2ad16cb77 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.strings @@ -55,8 +55,8 @@ "Common.Controls.Actions.Share" = "Поделиться"; "Common.Controls.Actions.SharePost" = "Поделиться постом"; "Common.Controls.Actions.ShareUser" = "Поделиться %@"; -"Common.Controls.Actions.SignIn" = "Войти"; -"Common.Controls.Actions.SignUp" = "Зарегистрироваться"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "Пропустить"; "Common.Controls.Actions.TakePhoto" = "Сделать фото"; "Common.Controls.Actions.TryAgain" = "Попробовать снова"; @@ -169,16 +169,20 @@ "Scene.Compose.Accessibility.CustomEmojiPicker" = "Меню пользовательских эмодзи"; "Scene.Compose.Accessibility.DisableContentWarning" = "Убрать предупреждение о содержании"; "Scene.Compose.Accessibility.EnableContentWarning" = "Добавить предупреждение о содержании"; +"Scene.Compose.Accessibility.PostOptions" = "Post Options"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Меню видимости поста"; +"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; "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.CanNotRecognizeThisMediaAttachment" = "Can not recognize this media attachment"; +"Scene.Compose.Attachment.CompressingState" = "Compressing..."; "Scene.Compose.Attachment.DescriptionPhoto" = "Опишите фото для людей с нарушениями зрения..."; "Scene.Compose.Attachment.DescriptionVideo" = "Опишите видео для людей с нарушениями зрения..."; "Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "изображение"; +"Scene.Compose.Attachment.ServerProcessingState" = "Server Processing..."; "Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "видео"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Пробел, чтобы добавить"; @@ -200,6 +204,8 @@ "Scene.Compose.Poll.OptionNumber" = "Вариант %ld"; "Scene.Compose.Poll.SevenDays" = "7 дней"; "Scene.Compose.Poll.SixHours" = "6 часов"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; +"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; "Scene.Compose.Poll.ThirtyMinutes" = "30 минут"; "Scene.Compose.Poll.ThreeDays" = "3 дня"; "Scene.Compose.ReplyingToUser" = "ответ %@"; @@ -245,6 +251,9 @@ "Scene.HomeTimeline.NavigationBarState.Published" = "Опубликовано!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Публикуем пост..."; "Scene.HomeTimeline.Title" = "Главная"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "Accept"; "Scene.Notification.FollowRequest.Accepted" = "Accepted"; "Scene.Notification.FollowRequest.Reject" = "reject"; @@ -272,6 +281,8 @@ "Scene.Profile.Fields.AddRow" = "Добавить строку"; "Scene.Profile.Fields.Placeholder.Content" = "Содержимое"; "Scene.Profile.Fields.Placeholder.Label" = "Ярлык"; +"Scene.Profile.Fields.Verified.Long" = "Ownership of this link was checked on %@"; +"Scene.Profile.Fields.Verified.Short" = "Verified on %@"; "Scene.Profile.Header.FollowsYou" = "Подписан(а) на вас"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirm to block %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Block Account"; @@ -404,13 +415,11 @@ "Scene.ServerPicker.EmptyState.BadNetwork" = "Что-то пошло не так при загрузке данных. Проверьте подключение к интернету."; "Scene.ServerPicker.EmptyState.FindingServers" = "Ищем доступные сервера..."; "Scene.ServerPicker.EmptyState.NoResults" = "Нет результатов"; -"Scene.ServerPicker.Input.Placeholder" = "Найдите сервер или присоединитесь к своему..."; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Поиск по серверам или ссылке"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "КАТЕГОРИЯ"; "Scene.ServerPicker.Label.Language" = "ЯЗЫК"; "Scene.ServerPicker.Label.Users" = "ПОЛЬЗОВАТЕЛИ"; -"Scene.ServerPicker.Subtitle" = "Выберите сообщество на основе своих интересов, региона или общей тематики."; -"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.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Выберите сервер, любой сервер."; "Scene.ServerRules.Button.Confirm" = "Принимаю"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.stringsdict index afb29a6aa..c9552a9e4 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.stringsdict @@ -62,6 +62,26 @@ %ld символа осталось + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.strings index dbba5ddda..79781cae7 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.strings @@ -56,7 +56,7 @@ Kontrollera din internetanslutning."; "Common.Controls.Actions.SharePost" = "Dela inlägg"; "Common.Controls.Actions.ShareUser" = "Dela %@"; "Common.Controls.Actions.SignIn" = "Logga in"; -"Common.Controls.Actions.SignUp" = "Registrera dig"; +"Common.Controls.Actions.SignUp" = "Skapa konto"; "Common.Controls.Actions.Skip" = "Hoppa över"; "Common.Controls.Actions.TakePhoto" = "Ta foto"; "Common.Controls.Actions.TryAgain" = "Försök igen"; @@ -161,16 +161,20 @@ Din profil ser ut så här för dem."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Anpassad emoji-väljare"; "Scene.Compose.Accessibility.DisableContentWarning" = "Inaktivera innehållsvarning"; "Scene.Compose.Accessibility.EnableContentWarning" = "Aktivera innehållsvarning"; +"Scene.Compose.Accessibility.PostOptions" = "Inläggsalternativ"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Inläggssynlighetsmeny"; +"Scene.Compose.Accessibility.PostingAs" = "Postar som %@"; "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.CompressingState" = "Komprimerar..."; "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.ServerProcessingState" = "Behandlas av servern..."; "Scene.Compose.Attachment.UploadFailed" = "Uppladdning misslyckades"; "Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Mellanslag för att lägga till"; @@ -192,6 +196,8 @@ laddas upp till Mastodon."; "Scene.Compose.Poll.OptionNumber" = "Alternativ %ld"; "Scene.Compose.Poll.SevenDays" = "7 dagar"; "Scene.Compose.Poll.SixHours" = "6 timmar"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "Undersökningen har ett tomt alternativ"; +"Scene.Compose.Poll.ThePollIsInvalid" = "Undersökningen är ogiltig"; "Scene.Compose.Poll.ThirtyMinutes" = "30 minuter"; "Scene.Compose.Poll.ThreeDays" = "3 dagar"; "Scene.Compose.ReplyingToUser" = "svarar %@"; @@ -234,6 +240,9 @@ laddas upp till Mastodon."; "Scene.HomeTimeline.NavigationBarState.Published" = "Publicerat!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Publicerar inlägget..."; "Scene.HomeTimeline.Title" = "Hem"; +"Scene.Login.ServerSearchField.Placeholder" = "Ange URL eller sök efter din server"; +"Scene.Login.Subtitle" = "Logga in på servern där du skapade ditt konto."; +"Scene.Login.Title" = "Välkommen tillbaka"; "Scene.Notification.FollowRequest.Accept" = "Godkänn"; "Scene.Notification.FollowRequest.Accepted" = "Godkänd"; "Scene.Notification.FollowRequest.Reject" = "avvisa"; @@ -261,6 +270,8 @@ laddas upp till Mastodon."; "Scene.Profile.Fields.AddRow" = "Lägg till rad"; "Scene.Profile.Fields.Placeholder.Content" = "Innehåll"; "Scene.Profile.Fields.Placeholder.Label" = "Etikett"; +"Scene.Profile.Fields.Verified.Long" = "Ägarskap för denna länk kontrollerades den %@"; +"Scene.Profile.Fields.Verified.Short" = "Verifierad på %@"; "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"; @@ -393,13 +404,11 @@ laddas upp till Mastodon."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Något gick fel när data laddades. Försök igen eller kontrollera din internetanslutning."; "Scene.ServerPicker.EmptyState.FindingServers" = "Söker tillgängliga servrar..."; "Scene.ServerPicker.EmptyState.NoResults" = "Inga resultat"; -"Scene.ServerPicker.Input.Placeholder" = "Sök gemenskaper"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Sök servrar eller ange URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Sök gemenskaper eller ange URL"; "Scene.ServerPicker.Label.Category" = "KATEGORI"; "Scene.ServerPicker.Label.Language" = "SPRÅK"; "Scene.ServerPicker.Label.Users" = "ANVÄNDARE"; -"Scene.ServerPicker.Subtitle" = "Välj en server baserat på dina intressen, region eller ett allmänt syfte."; -"Scene.ServerPicker.SubtitleExtend" = "Välj en server baserat på dina intressen, region eller ett allmänt syfte. Varje server drivs av en helt oberoende organisation eller individ."; +"Scene.ServerPicker.Subtitle" = "Välj en server baserat på dina intressen, region eller en allmän server. Du kan fortfarande nå alla, oavsett server."; "Scene.ServerPicker.Title" = "Mastodon utgörs av användare på olika servrar."; "Scene.ServerRules.Button.Confirm" = "Jag godkänner"; "Scene.ServerRules.PrivacyPolicy" = "integritetspolicy"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.stringsdict index c7317903d..3cbfeae6d 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld tecken + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ kvar + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld tecken + other + %ld tecken + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings index de982308d..8e63e0269 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings @@ -55,8 +55,8 @@ "Common.Controls.Actions.Share" = "แบ่งปัน"; "Common.Controls.Actions.SharePost" = "แบ่งปันโพสต์"; "Common.Controls.Actions.ShareUser" = "แบ่งปัน %@"; -"Common.Controls.Actions.SignIn" = "ลงชื่อเข้า"; -"Common.Controls.Actions.SignUp" = "ลงทะเบียน"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "ข้าม"; "Common.Controls.Actions.TakePhoto" = "ถ่ายรูป"; "Common.Controls.Actions.TryAgain" = "ลองอีกครั้ง"; @@ -161,16 +161,20 @@ "Scene.Compose.Accessibility.CustomEmojiPicker" = "ตัวเลือกอีโมจิที่กำหนดเอง"; "Scene.Compose.Accessibility.DisableContentWarning" = "ปิดใช้งานคำเตือนเนื้อหา"; "Scene.Compose.Accessibility.EnableContentWarning" = "เปิดใช้งานคำเตือนเนื้อหา"; +"Scene.Compose.Accessibility.PostOptions" = "ตัวเลือกโพสต์"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "เมนูการมองเห็นโพสต์"; +"Scene.Compose.Accessibility.PostingAs" = "กำลังโพสต์เป็น %@"; "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.CanNotRecognizeThisMediaAttachment" = "ไม่สามารถระบุไฟล์แนบสื่อนี้"; +"Scene.Compose.Attachment.CompressingState" = "กำลังบีบอัด..."; "Scene.Compose.Attachment.DescriptionPhoto" = "อธิบายรูปภาพสำหรับผู้บกพร่องทางการมองเห็น..."; "Scene.Compose.Attachment.DescriptionVideo" = "อธิบายวิดีโอสำหรับผู้บกพร่องทางการมองเห็น..."; "Scene.Compose.Attachment.LoadFailed" = "การโหลดล้มเหลว"; "Scene.Compose.Attachment.Photo" = "รูปภาพ"; +"Scene.Compose.Attachment.ServerProcessingState" = "เซิร์ฟเวอร์กำลังประมวลผล..."; "Scene.Compose.Attachment.UploadFailed" = "การอัปโหลดล้มเหลว"; "Scene.Compose.Attachment.Video" = "วิดีโอ"; "Scene.Compose.AutoComplete.SpaceToAdd" = "เว้นวรรคเพื่อเพิ่ม"; @@ -192,6 +196,8 @@ "Scene.Compose.Poll.OptionNumber" = "ตัวเลือก %ld"; "Scene.Compose.Poll.SevenDays" = "7 วัน"; "Scene.Compose.Poll.SixHours" = "6 ชั่วโมง"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "การสำรวจความคิดเห็นมีตัวเลือกที่ว่างเปล่า"; +"Scene.Compose.Poll.ThePollIsInvalid" = "การสำรวจความคิดเห็นไม่ถูกต้อง"; "Scene.Compose.Poll.ThirtyMinutes" = "30 นาที"; "Scene.Compose.Poll.ThreeDays" = "3 วัน"; "Scene.Compose.ReplyingToUser" = "กำลังตอบกลับ %@"; @@ -203,10 +209,10 @@ "Scene.Compose.Visibility.Unlisted" = "ไม่อยู่ในรายการ"; "Scene.ConfirmEmail.Button.OpenEmailApp" = "เปิดแอปอีเมล"; "Scene.ConfirmEmail.Button.Resend" = "ส่งใหม่"; -"Scene.ConfirmEmail.DontReceiveEmail.Description" = "หากคุณยังไม่ได้รับอีเมล ตรวจสอบว่าที่อยู่อีเมลของคุณถูกต้อง รวมถึงโฟลเดอร์อีเมลขยะของคุณ"; +"Scene.ConfirmEmail.DontReceiveEmail.Description" = "ตรวจสอบว่าที่อยู่อีเมลของคุณถูกต้องเช่นเดียวกับโฟลเดอร์อีเมลขยะหากคุณยังไม่ได้ทำ"; "Scene.ConfirmEmail.DontReceiveEmail.ResendEmail" = "ส่งอีเมลใหม่"; "Scene.ConfirmEmail.DontReceiveEmail.Title" = "ตรวจสอบอีเมลของคุณ"; -"Scene.ConfirmEmail.OpenEmailApp.Description" = "เราเพิ่งส่งอีเมลหาคุณ หากคุณยังไม่ได้รับอีเมล โปรดตรวจสอบโฟลเดอร์อีเมลขยะ"; +"Scene.ConfirmEmail.OpenEmailApp.Description" = "เราเพิ่งส่งอีเมลถึงคุณ ตรวจสอบโฟลเดอร์อีเมลขยะของคุณหากคุณยังไม่ได้ทำ"; "Scene.ConfirmEmail.OpenEmailApp.Mail" = "จดหมาย"; "Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "เปิดไคลเอ็นต์อีเมล"; "Scene.ConfirmEmail.OpenEmailApp.Title" = "ตรวจสอบกล่องขาเข้าของคุณ"; @@ -234,6 +240,9 @@ "Scene.HomeTimeline.NavigationBarState.Published" = "เผยแพร่แล้ว!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "กำลังเผยแพร่โพสต์..."; "Scene.HomeTimeline.Title" = "หน้าแรก"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "ยอมรับ"; "Scene.Notification.FollowRequest.Accepted" = "ยอมรับแล้ว"; "Scene.Notification.FollowRequest.Reject" = "ปฏิเสธ"; @@ -261,6 +270,8 @@ "Scene.Profile.Fields.AddRow" = "เพิ่มแถว"; "Scene.Profile.Fields.Placeholder.Content" = "เนื้อหา"; "Scene.Profile.Fields.Placeholder.Label" = "ป้ายชื่อ"; +"Scene.Profile.Fields.Verified.Long" = "ตรวจสอบความเป็นเจ้าของของลิงก์นี้เมื่อ %@"; +"Scene.Profile.Fields.Verified.Short" = "ตรวจสอบเมื่อ %@"; "Scene.Profile.Header.FollowsYou" = "ติดตามคุณ"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "ยืนยันเพื่อปิดกั้น %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "ปิดกั้นบัญชี"; @@ -295,7 +306,7 @@ "Scene.Register.Error.Reason.Taken" = "%@ ถูกใช้งานแล้ว"; "Scene.Register.Error.Reason.TooLong" = "%@ ยาวเกินไป"; "Scene.Register.Error.Reason.TooShort" = "%@ สั้นเกินไป"; -"Scene.Register.Error.Reason.Unreachable" = "ดูเหมือนว่า %@ จะไม่มีอยู่"; +"Scene.Register.Error.Reason.Unreachable" = "ดูเหมือนว่าจะไม่มี %@ อยู่"; "Scene.Register.Error.Special.EmailInvalid" = "นี่ไม่ใช่ที่อยู่อีเมลที่ถูกต้อง"; "Scene.Register.Error.Special.PasswordTooShort" = "รหัสผ่านสั้นเกินไป (ต้องมีอย่างน้อย 8 ตัวอักษร)"; "Scene.Register.Error.Special.UsernameInvalid" = "ชื่อผู้ใช้ต้องมีเฉพาะตัวอักษรและตัวเลขและขีดล่างเท่านั้น"; @@ -393,13 +404,11 @@ "Scene.ServerPicker.EmptyState.BadNetwork" = "มีบางอย่างผิดพลาดขณะโหลดข้อมูล ตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณ"; "Scene.ServerPicker.EmptyState.FindingServers" = "กำลังค้นหาเซิร์ฟเวอร์ที่พร้อมใช้งาน..."; "Scene.ServerPicker.EmptyState.NoResults" = "ไม่มีผลลัพธ์"; -"Scene.ServerPicker.Input.Placeholder" = "ค้นหาเซิร์ฟเวอร์"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "ค้นหาเซิร์ฟเวอร์หรือป้อน URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "หมวดหมู่"; "Scene.ServerPicker.Label.Language" = "ภาษา"; "Scene.ServerPicker.Label.Users" = "ผู้ใช้"; -"Scene.ServerPicker.Subtitle" = "เลือกเซิร์ฟเวอร์ตามความสนใจ, ภูมิภาค หรือวัตถุประสงค์ทั่วไปของคุณ"; -"Scene.ServerPicker.SubtitleExtend" = "เลือกเซิร์ฟเวอร์ตามความสนใจ, ภูมิภาค หรือวัตถุประสงค์ทั่วไปของคุณ แต่ละเซิร์ฟเวอร์ได้รับการดำเนินงานโดยองค์กรหรือบุคคลที่เป็นอิสระโดยสิ้นเชิง"; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Mastodon ประกอบด้วยผู้ใช้ในเซิร์ฟเวอร์ต่าง ๆ"; "Scene.ServerRules.Button.Confirm" = "ฉันเห็นด้วย"; "Scene.ServerRules.PrivacyPolicy" = "นโยบายความเป็นส่วนตัว"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.stringsdict index 897d07eca..f25561ad6 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.stringsdict @@ -44,6 +44,20 @@ %ld ตัวอักษร + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + เหลืออีก %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld ตัวอักษร + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.strings index 614ea6dc5..fcee4e12e 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.strings @@ -54,8 +54,8 @@ "Common.Controls.Actions.Share" = "Paylaş"; "Common.Controls.Actions.SharePost" = "Gönderiyi Paylaş"; "Common.Controls.Actions.ShareUser" = "%@ ile paylaş"; -"Common.Controls.Actions.SignIn" = "Giriş Yap"; -"Common.Controls.Actions.SignUp" = "Kaydol"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "Atla"; "Common.Controls.Actions.TakePhoto" = "Fotoğraf Çek"; "Common.Controls.Actions.TryAgain" = "Tekrar Deneyin"; @@ -160,16 +160,20 @@ Bu kişiye göre profiliniz böyle gözüküyor."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Özel Emoji Seçici"; "Scene.Compose.Accessibility.DisableContentWarning" = "İçerik Uyarısını Kapat"; "Scene.Compose.Accessibility.EnableContentWarning" = "İçerik Uyarısını Etkinleştir"; +"Scene.Compose.Accessibility.PostOptions" = "Post Options"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Gönderi Görünürlüğü Menüsü"; +"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; "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.CanNotRecognizeThisMediaAttachment" = "Can not recognize this media attachment"; +"Scene.Compose.Attachment.CompressingState" = "Compressing..."; "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.ServerProcessingState" = "Server Processing..."; "Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Eklemek için boşluk tuşuna basın"; @@ -191,6 +195,8 @@ yüklenemiyor."; "Scene.Compose.Poll.OptionNumber" = "Seçenek %ld"; "Scene.Compose.Poll.SevenDays" = "7 Gün"; "Scene.Compose.Poll.SixHours" = "6 Saat"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; +"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; "Scene.Compose.Poll.ThirtyMinutes" = "30 dakika"; "Scene.Compose.Poll.ThreeDays" = "3 Gün"; "Scene.Compose.ReplyingToUser" = "yanıtlanıyor: %@"; @@ -233,6 +239,9 @@ yüklenemiyor."; "Scene.HomeTimeline.NavigationBarState.Published" = "Yayınlandı!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Gönderi yayınlanıyor..."; "Scene.HomeTimeline.Title" = "Ana Sayfa"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "Accept"; "Scene.Notification.FollowRequest.Accepted" = "Accepted"; "Scene.Notification.FollowRequest.Reject" = "reject"; @@ -260,6 +269,8 @@ yüklenemiyor."; "Scene.Profile.Fields.AddRow" = "Satır Ekle"; "Scene.Profile.Fields.Placeholder.Content" = "İçerik"; "Scene.Profile.Fields.Placeholder.Label" = "Etiket"; +"Scene.Profile.Fields.Verified.Long" = "Ownership of this link was checked on %@"; +"Scene.Profile.Fields.Verified.Short" = "Verified on %@"; "Scene.Profile.Header.FollowsYou" = "Seni takip ediyor"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "%@ engellemeyi onayla"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Hesabı Engelle"; @@ -392,13 +403,11 @@ yüklenemiyor."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Veriyi yüklerken bir hata oluştu. Lütfen internet bağlantınızı kontrol edin."; "Scene.ServerPicker.EmptyState.FindingServers" = "Mevcut sunucular aranıyor..."; "Scene.ServerPicker.EmptyState.NoResults" = "Sonuç yok"; -"Scene.ServerPicker.Input.Placeholder" = "Toplulukları ara"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Sunucuları ara ya da bir bağlantı gir"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "KATEGORİ"; "Scene.ServerPicker.Label.Language" = "DİL"; "Scene.ServerPicker.Label.Users" = "KULLANICILAR"; -"Scene.ServerPicker.Subtitle" = "İlgi alanlarınıza, bölgenize veya genel amaçlı bir topluluk seçin."; -"Scene.ServerPicker.SubtitleExtend" = "İlgi alanlarınıza, bölgenize veya genel amaçlı bir topluluk seçin. Her topluluk tamamen bağımsız bir kuruluş veya kişi tarafından işletilmektedir."; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Mastodon, farklı topluluklardaki kullanıcılardan oluşur."; "Scene.ServerRules.Button.Confirm" = "Kabul Ediyorum"; "Scene.ServerRules.PrivacyPolicy" = "gizlilik politikası"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.stringsdict index 29df92c2b..6ef7f4c75 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.stringsdict @@ -50,6 +50,22 @@ %ld karakter + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings index 41ac9aa20..5a14fb2cf 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings @@ -56,7 +56,7 @@ Vui lòng kiểm tra kết nối mạng."; "Common.Controls.Actions.SharePost" = "Chia sẻ tút"; "Common.Controls.Actions.ShareUser" = "Chia sẻ %@"; "Common.Controls.Actions.SignIn" = "Đăng nhập"; -"Common.Controls.Actions.SignUp" = "Đăng ký"; +"Common.Controls.Actions.SignUp" = "Tạo tài khoản"; "Common.Controls.Actions.Skip" = "Bỏ qua"; "Common.Controls.Actions.TakePhoto" = "Chụp ảnh"; "Common.Controls.Actions.TryAgain" = "Thử lại"; @@ -161,17 +161,21 @@ Họ sẽ thấy trang của bạn như thế này."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Chọn emoji"; "Scene.Compose.Accessibility.DisableContentWarning" = "Tắt nội dung ẩn"; "Scene.Compose.Accessibility.EnableContentWarning" = "Bật nội dung ẩn"; +"Scene.Compose.Accessibility.PostOptions" = "Tùy chọn đăng"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Menu hiển thị tút"; +"Scene.Compose.Accessibility.PostingAs" = "Đăng dưới dạng %@"; "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.AttachmentTooLarge" = "Tập tin đính kèm quá lớn"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Không xem được tập tin đính kèm"; +"Scene.Compose.Attachment.CompressingState" = "Đang nén..."; "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.LoadFailed" = "Tải thất bại"; "Scene.Compose.Attachment.Photo" = "ảnh"; -"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; +"Scene.Compose.Attachment.ServerProcessingState" = "Máy chủ đang xử lý..."; +"Scene.Compose.Attachment.UploadFailed" = "Tải lên thất bại"; "Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Khoảng cách để thêm"; "Scene.Compose.ComposeAction" = "Đăng"; @@ -192,6 +196,8 @@ tải lên Mastodon."; "Scene.Compose.Poll.OptionNumber" = "Lựa chọn %ld"; "Scene.Compose.Poll.SevenDays" = "7 ngày"; "Scene.Compose.Poll.SixHours" = "6 giờ"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "Thiếu lựa chọn"; +"Scene.Compose.Poll.ThePollIsInvalid" = "Bình chọn không hợp lệ"; "Scene.Compose.Poll.ThirtyMinutes" = "30 phút"; "Scene.Compose.Poll.ThreeDays" = "3 ngày"; "Scene.Compose.ReplyingToUser" = "trả lời %@"; @@ -234,6 +240,9 @@ tải lên Mastodon."; "Scene.HomeTimeline.NavigationBarState.Published" = "Đã đăng!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Đang đăng tút..."; "Scene.HomeTimeline.Title" = "Bảng tin"; +"Scene.Login.ServerSearchField.Placeholder" = "Nhập URL hoặc tìm máy chủ"; +"Scene.Login.Subtitle" = "Đăng nhập vào máy chủ mà bạn đã tạo tài khoản."; +"Scene.Login.Title" = "Chào mừng trở lại!"; "Scene.Notification.FollowRequest.Accept" = "Chấp nhận"; "Scene.Notification.FollowRequest.Accepted" = "Đã chấp nhận"; "Scene.Notification.FollowRequest.Reject" = "từ chối"; @@ -261,6 +270,8 @@ tải lên Mastodon."; "Scene.Profile.Fields.AddRow" = "Thêm hàng"; "Scene.Profile.Fields.Placeholder.Content" = "Nội dung"; "Scene.Profile.Fields.Placeholder.Label" = "Nhãn"; +"Scene.Profile.Fields.Verified.Long" = "Liên kết này đã được xác minh trên %@"; +"Scene.Profile.Fields.Verified.Short" = "Đã xác minh %@"; "Scene.Profile.Header.FollowsYou" = "Đang theo dõi bạn"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Xác nhận chặn %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Chặn người này"; @@ -393,13 +404,11 @@ tải lên Mastodon."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Đã xảy ra lỗi. Hãy thử lại hoặc kiểm tra kết nối internet của bạn."; "Scene.ServerPicker.EmptyState.FindingServers" = "Đang tìm máy chủ hoạt động..."; "Scene.ServerPicker.EmptyState.NoResults" = "Không có kết quả"; -"Scene.ServerPicker.Input.Placeholder" = "Tìm máy chủ"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Tìm máy chủ hoặc nhập URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Tìm một máy chủ hoặc nhập URL"; "Scene.ServerPicker.Label.Category" = "PHÂN LOẠI"; "Scene.ServerPicker.Label.Language" = "NGÔN NGỮ"; "Scene.ServerPicker.Label.Users" = "NGƯỜI"; -"Scene.ServerPicker.Subtitle" = "Chọn một máy chủ dựa theo sở thích, tôn giáo, hoặc ý muốn của bạn."; -"Scene.ServerPicker.SubtitleExtend" = "Chọn một máy chủ dựa theo sở thích, tôn giáo, hoặc ý muốn của bạn. Mỗi máy chủ có thể được vận hành bởi một cá nhân hoặc một tổ chức."; +"Scene.ServerPicker.Subtitle" = "Chọn một máy chủ dựa theo sở thích, tôn giáo, hoặc ý muốn của bạn. Bạn vẫn có thể giao tiếp với bất cứ ai mà không phụ thuộc vào máy chủ của họ."; "Scene.ServerPicker.Title" = "Mastodon gồm nhiều máy chủ với thành viên riêng."; "Scene.ServerRules.Button.Confirm" = "Tôi đồng ý"; "Scene.ServerRules.PrivacyPolicy" = "chính sách bảo mật"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.stringsdict index 6905b240e..4c772f014 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.stringsdict @@ -44,6 +44,20 @@ %ld ký tự + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ còn lại + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld ký tự + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings index 3883e30df..cb362ea9d 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings @@ -55,8 +55,8 @@ "Common.Controls.Actions.Share" = "分享"; "Common.Controls.Actions.SharePost" = "分享帖子"; "Common.Controls.Actions.ShareUser" = "分享 %@"; -"Common.Controls.Actions.SignIn" = "登录"; -"Common.Controls.Actions.SignUp" = "注册"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "跳过"; "Common.Controls.Actions.TakePhoto" = "拍照"; "Common.Controls.Actions.TryAgain" = "再试一次"; @@ -161,16 +161,20 @@ "Scene.Compose.Accessibility.CustomEmojiPicker" = "自定义表情选择器"; "Scene.Compose.Accessibility.DisableContentWarning" = "关闭内容警告"; "Scene.Compose.Accessibility.EnableContentWarning" = "启用内容警告"; +"Scene.Compose.Accessibility.PostOptions" = "Post Options"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "帖子可见性"; +"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; "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.CanNotRecognizeThisMediaAttachment" = "Can not recognize this media attachment"; +"Scene.Compose.Attachment.CompressingState" = "Compressing..."; "Scene.Compose.Attachment.DescriptionPhoto" = "为视觉障碍人士添加照片的文字说明..."; "Scene.Compose.Attachment.DescriptionVideo" = "为视觉障碍人士添加视频的文字说明..."; "Scene.Compose.Attachment.LoadFailed" = "Load Failed"; "Scene.Compose.Attachment.Photo" = "照片"; +"Scene.Compose.Attachment.ServerProcessingState" = "Server Processing..."; "Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; "Scene.Compose.Attachment.Video" = "视频"; "Scene.Compose.AutoComplete.SpaceToAdd" = "输入空格键入"; @@ -192,6 +196,8 @@ "Scene.Compose.Poll.OptionNumber" = "选项 %ld"; "Scene.Compose.Poll.SevenDays" = "7 天"; "Scene.Compose.Poll.SixHours" = "6 小时"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; +"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; "Scene.Compose.Poll.ThirtyMinutes" = "30 分钟"; "Scene.Compose.Poll.ThreeDays" = "3 天"; "Scene.Compose.ReplyingToUser" = "回复给 %@"; @@ -234,6 +240,9 @@ "Scene.HomeTimeline.NavigationBarState.Published" = "已发送"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "正在发送..."; "Scene.HomeTimeline.Title" = "主页"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "接受"; "Scene.Notification.FollowRequest.Accepted" = "已接受"; "Scene.Notification.FollowRequest.Reject" = "拒绝"; @@ -261,6 +270,8 @@ "Scene.Profile.Fields.AddRow" = "添加"; "Scene.Profile.Fields.Placeholder.Content" = "内容"; "Scene.Profile.Fields.Placeholder.Label" = "标签"; +"Scene.Profile.Fields.Verified.Long" = "Ownership of this link was checked on %@"; +"Scene.Profile.Fields.Verified.Short" = "Verified on %@"; "Scene.Profile.Header.FollowsYou" = "关注了你"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "确认屏蔽 %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "屏蔽帐户"; @@ -393,13 +404,11 @@ "Scene.ServerPicker.EmptyState.BadNetwork" = "出了些问题。请检查你的互联网连接"; "Scene.ServerPicker.EmptyState.FindingServers" = "正在查找可用的服务器..."; "Scene.ServerPicker.EmptyState.NoResults" = "无结果"; -"Scene.ServerPicker.Input.Placeholder" = "查找或加入你自己的服务器..."; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "搜索服务器或输入 URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "类别"; "Scene.ServerPicker.Label.Language" = "语言"; "Scene.ServerPicker.Label.Users" = "用户"; -"Scene.ServerPicker.Subtitle" = "根据你的兴趣、区域或一般目的选择一个社区。"; -"Scene.ServerPicker.SubtitleExtend" = "根据你的兴趣、区域或一般目的选择一个社区。每个社区都由完全独立的组织或个人管理。"; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "挑选一个服务器, 任意服务器。"; "Scene.ServerRules.Button.Confirm" = "我同意"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict index 5a7af3752..915a524b5 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict @@ -44,6 +44,20 @@ %ld 个字符 + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld characters + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings index 34e59a582..60dcd2077 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings @@ -55,8 +55,8 @@ "Common.Controls.Actions.Share" = "分享"; "Common.Controls.Actions.SharePost" = "分享嘟文"; "Common.Controls.Actions.ShareUser" = "分享 %@"; -"Common.Controls.Actions.SignIn" = "登入"; -"Common.Controls.Actions.SignUp" = "註冊"; +"Common.Controls.Actions.SignIn" = "Log in"; +"Common.Controls.Actions.SignUp" = "Create account"; "Common.Controls.Actions.Skip" = "跳過"; "Common.Controls.Actions.TakePhoto" = "拍攝照片"; "Common.Controls.Actions.TryAgain" = "再試一次"; @@ -157,16 +157,20 @@ "Scene.Compose.Accessibility.CustomEmojiPicker" = "自訂 emoji 選擇器"; "Scene.Compose.Accessibility.DisableContentWarning" = "停用內容警告"; "Scene.Compose.Accessibility.EnableContentWarning" = "啟用內容警告"; +"Scene.Compose.Accessibility.PostOptions" = "嘟文選項"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "嘟文可見性選單"; +"Scene.Compose.Accessibility.PostingAs" = "以 %@ 發嘟"; "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.AttachmentTooLarge" = "附加檔案大小過大"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not recognize this media attachment"; +"Scene.Compose.Attachment.CompressingState" = "Compressing..."; "Scene.Compose.Attachment.DescriptionPhoto" = "為視障人士提供圖片說明..."; "Scene.Compose.Attachment.DescriptionVideo" = "為視障人士提供影片說明..."; -"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; +"Scene.Compose.Attachment.LoadFailed" = "讀取失敗"; "Scene.Compose.Attachment.Photo" = "照片"; -"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; +"Scene.Compose.Attachment.ServerProcessingState" = "Server Processing..."; +"Scene.Compose.Attachment.UploadFailed" = "上傳失敗"; "Scene.Compose.Attachment.Video" = "影片"; "Scene.Compose.AutoComplete.SpaceToAdd" = "添加的空白"; "Scene.Compose.ComposeAction" = "嘟出去"; @@ -187,6 +191,8 @@ "Scene.Compose.Poll.OptionNumber" = "選項 %ld"; "Scene.Compose.Poll.SevenDays" = "七天"; "Scene.Compose.Poll.SixHours" = "六小時"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; +"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; "Scene.Compose.Poll.ThirtyMinutes" = "30 分鐘"; "Scene.Compose.Poll.ThreeDays" = "三天"; "Scene.Compose.ReplyingToUser" = "正在回覆 %@"; @@ -229,6 +235,9 @@ "Scene.HomeTimeline.NavigationBarState.Published" = "嘟出去!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "發表嘟文..."; "Scene.HomeTimeline.Title" = "首頁"; +"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; +"Scene.Login.Title" = "Welcome back"; "Scene.Notification.FollowRequest.Accept" = "接受"; "Scene.Notification.FollowRequest.Accepted" = "已接受"; "Scene.Notification.FollowRequest.Reject" = "拒絕"; @@ -256,6 +265,8 @@ "Scene.Profile.Fields.AddRow" = "新增列"; "Scene.Profile.Fields.Placeholder.Content" = "內容"; "Scene.Profile.Fields.Placeholder.Label" = "標籤"; +"Scene.Profile.Fields.Verified.Long" = "Ownership of this link was checked on %@"; +"Scene.Profile.Fields.Verified.Short" = "Verified on %@"; "Scene.Profile.Header.FollowsYou" = "跟隨了您"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "確認將 %@ 封鎖"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "封鎖"; @@ -388,13 +399,11 @@ "Scene.ServerPicker.EmptyState.BadNetwork" = "讀取資料時發生錯誤。請檢查您的網路連線。"; "Scene.ServerPicker.EmptyState.FindingServers" = "尋找可用的伺服器..."; "Scene.ServerPicker.EmptyState.NoResults" = "沒有結果"; -"Scene.ServerPicker.Input.Placeholder" = "搜尋伺服器"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "搜尋伺服器或輸入網址"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; "Scene.ServerPicker.Label.Category" = "分類"; "Scene.ServerPicker.Label.Language" = "語言"; "Scene.ServerPicker.Label.Users" = "使用者"; -"Scene.ServerPicker.Subtitle" = "基於您的興趣、地區、或一般用途選定一個伺服器。"; -"Scene.ServerPicker.SubtitleExtend" = "基於您的興趣、地區、或一般用途選定一個伺服器。每個伺服器是由完全獨立的組織或個人營運。"; +"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; "Scene.ServerPicker.Title" = "Mastodon 由不同伺服器的使用者組成。"; "Scene.ServerRules.Button.Confirm" = "我已閱讀並同意"; "Scene.ServerRules.PrivacyPolicy" = "隱私權政策"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.stringsdict index c0ce0f9a2..d545fd6a4 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.stringsdict @@ -44,6 +44,20 @@ %ld 個字 + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + 剩餘 %#@character_count@ 字 + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld 個字 + + plural.count.followed_by_and_mutual NSStringLocalizedFormatKey From 26576d888e90fe33ef82fdfbfa884b0f72072198 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 16 Nov 2022 12:54:51 +0100 Subject: [PATCH 587/658] feat: Implement double-tap on profile Tab to quickly cycle through logged in Accounts --- .../Root/MainTab/MainTabBarController.swift | 64 +++++++++++++++++-- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 48d974358..158cd769b 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -8,6 +8,7 @@ import os.log import UIKit import Combine +import CoreDataStack import SafariServices import MastodonAsset import MastodonCore @@ -324,7 +325,12 @@ extension MainTabBarController { let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer() tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:))) tabBar.addGestureRecognizer(tabBarLongPressGestureRecognizer) - + + let tabBarDoubleTapGestureRecognizer = UITapGestureRecognizer() + tabBarDoubleTapGestureRecognizer.numberOfTapsRequired = 2 + tabBarDoubleTapGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarDoubleTapGestureRecognizerHandler(_:))) + tabBar.addGestureRecognizer(tabBarDoubleTapGestureRecognizer) + self.isReadyForWizardAvatarButton = authContext != nil $currentTab @@ -375,9 +381,7 @@ extension MainTabBarController { _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) } - @objc private func tabBarLongPressGestureRecognizerHandler(_ sender: UILongPressGestureRecognizer) { - guard sender.state == .began else { return } - + private func touchedTab(by sender: UIGestureRecognizer) -> Tab? { var _tab: Tab? let location = sender.location(in: tabBar) for item in tabBar.items ?? [] { @@ -389,7 +393,57 @@ extension MainTabBarController { break } - guard let tab = _tab else { return } + return _tab + } + + @objc private func tabBarDoubleTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { + guard sender.state == .ended else { return } + guard let tab = touchedTab(by: sender) else { return } + logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): double tap \(tab.title) tab") + + switch tab { + case .me: + guard let authContext = authContext else { return } + assert(Thread.isMainThread) + + let request = MastodonAuthentication.sortedFetchRequest + guard let accounts = try? context.managedObjectContext.fetch(request) else { return } + + let nextSelectedAccountIndex: Int? = { + for (index, account) in accounts.enumerated() { + guard account == authContext.mastodonAuthenticationBox + .authenticationRecord + .object(in: context.managedObjectContext) + else { continue } + + let nextAccountIndex = index + 1 + + if accounts.count > nextAccountIndex { + return nextAccountIndex + } else { + return 0 + } + } + + return nil + }() + + guard let nextSelectedAccountIndex = nextSelectedAccountIndex, accounts.count > nextSelectedAccountIndex else { return } + let nextAccount = accounts[nextSelectedAccountIndex] + + Task { @MainActor in + let isActive = try await context.authenticationService.activeMastodonUser(domain: nextAccount.domain, userID: nextAccount.userID) + guard isActive else { return } + self.coordinator.setup() + } + default: + break + } + } + + @objc private func tabBarLongPressGestureRecognizerHandler(_ sender: UILongPressGestureRecognizer) { + guard sender.state == .began else { return } + guard let tab = touchedTab(by: sender) else { return } logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): long press \(tab.title) tab") switch tab { From 972b82268a76225e3a06fca606c984cce147cd8b Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 16 Nov 2022 19:56:16 +0800 Subject: [PATCH 588/658] feat: new i18n languages. Czech (cs) and Slovenian (sl) --- .../Sources/StringsConvertor/main.swift | 2 + Mastodon.xcodeproj/project.pbxproj | 14 + Mastodon/Resources/cs.lproj/InfoPlist.strings | 4 + Mastodon/Resources/sl.lproj/InfoPlist.strings | 4 + MastodonIntent/cs.lproj/Intents.strings | 51 ++ MastodonIntent/cs.lproj/Intents.stringsdict | 54 ++ MastodonIntent/sl.lproj/Intents.strings | 51 ++ MastodonIntent/sl.lproj/Intents.stringsdict | 54 ++ .../Resources/cs.lproj/Localizable.strings | 459 ++++++++++++++ .../cs.lproj/Localizable.stringsdict | 581 ++++++++++++++++++ .../Resources/sl.lproj/Localizable.strings | 464 ++++++++++++++ .../sl.lproj/Localizable.stringsdict | 581 ++++++++++++++++++ 12 files changed, 2319 insertions(+) create mode 100644 Mastodon/Resources/cs.lproj/InfoPlist.strings create mode 100644 Mastodon/Resources/sl.lproj/InfoPlist.strings create mode 100644 MastodonIntent/cs.lproj/Intents.strings create mode 100644 MastodonIntent/cs.lproj/Intents.stringsdict create mode 100644 MastodonIntent/sl.lproj/Intents.strings create mode 100644 MastodonIntent/sl.lproj/Intents.stringsdict create mode 100644 MastodonSDK/Sources/MastodonLocalization/Resources/cs.lproj/Localizable.strings create mode 100644 MastodonSDK/Sources/MastodonLocalization/Resources/cs.lproj/Localizable.stringsdict create mode 100644 MastodonSDK/Sources/MastodonLocalization/Resources/sl.lproj/Localizable.strings create mode 100644 MastodonSDK/Sources/MastodonLocalization/Resources/sl.lproj/Localizable.stringsdict diff --git a/Localization/StringsConvertor/Sources/StringsConvertor/main.swift b/Localization/StringsConvertor/Sources/StringsConvertor/main.swift index beba6cb3f..665161e2c 100644 --- a/Localization/StringsConvertor/Sources/StringsConvertor/main.swift +++ b/Localization/StringsConvertor/Sources/StringsConvertor/main.swift @@ -53,6 +53,7 @@ private func map(language: String) -> String? { case "ca.lproj": return "ca" // Catalan case "zh-Hans.lproj": return "zh-Hans" // Chinese Simplified case "zh-Hant.lproj": return "zh-Hant" // Chinese Traditional + case "cs.lproj": return "cs" // Czech case "nl.lproj": return "nl" // Dutch case "en.lproj": return "en" case "fi.lproj": return "fi" // Finnish @@ -65,6 +66,7 @@ private func map(language: String) -> String? { case "kmr.lproj": return "ku" // Kurmanji (Kurdish) [intent mapping] case "ru.lproj": return "ru" // Russian case "gd.lproj": return "gd" // Scottish Gaelic + case "sl.lproj": return "sl" // Slovenian case "ckb.lproj": return "ckb" // Sorani (Kurdish) case "es.lproj": return "es" // Spanish case "es_AR.lproj": return "es-AR" // Spanish, Argentina diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index ae8410c35..5724244b6 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -895,6 +895,12 @@ DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteThreadViewModel.swift; sourceTree = ""; }; DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+LoadThreadState.swift"; sourceTree = ""; }; DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+Diffable.swift"; sourceTree = ""; }; + DB96C25D292505FE00F3B85D /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Intents.strings; sourceTree = ""; }; + DB96C25E292505FF00F3B85D /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = ""; }; + DB96C25F292505FF00F3B85D /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Intents.stringsdict; sourceTree = ""; }; + DB96C260292506D600F3B85D /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Intents.strings; sourceTree = ""; }; + DB96C261292506D700F3B85D /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/InfoPlist.strings; sourceTree = ""; }; + DB96C262292506D700F3B85D /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sl; path = sl.lproj/Intents.stringsdict; sourceTree = ""; }; DB98EB4627B0DFAA0082E365 /* ReportStatusViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportStatusViewModel+State.swift"; sourceTree = ""; }; DB98EB4827B0F0CD0082E365 /* ReportStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusTableViewCell.swift; sourceTree = ""; }; DB98EB4B27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportStatusTableViewCell+ViewModel.swift"; sourceTree = ""; }; @@ -2872,6 +2878,8 @@ gd, "es-AR", fi, + cs, + sl, ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( @@ -3607,6 +3615,8 @@ DBC9E3A6282E15190063A4D9 /* gd */, DBC9E3A9282E17DF0063A4D9 /* es-AR */, DB8F40042835EE5E006E7513 /* fi */, + DB96C25D292505FE00F3B85D /* cs */, + DB96C260292506D600F3B85D /* sl */, ); name = Intents.intentdefinition; sourceTree = ""; @@ -3638,6 +3648,8 @@ DBC9E3A7282E15190063A4D9 /* gd */, DBC9E3AA282E17DF0063A4D9 /* es-AR */, DB8F40052835EE5E006E7513 /* fi */, + DB96C25E292505FF00F3B85D /* cs */, + DB96C261292506D700F3B85D /* sl */, ); name = InfoPlist.strings; sourceTree = ""; @@ -3685,6 +3697,8 @@ DBC9E3A8282E15190063A4D9 /* gd */, DBC9E3AB282E17DF0063A4D9 /* es-AR */, DB8F40062835EE5E006E7513 /* fi */, + DB96C25F292505FF00F3B85D /* cs */, + DB96C262292506D700F3B85D /* sl */, ); name = Intents.stringsdict; sourceTree = ""; diff --git a/Mastodon/Resources/cs.lproj/InfoPlist.strings b/Mastodon/Resources/cs.lproj/InfoPlist.strings new file mode 100644 index 000000000..710865573 --- /dev/null +++ b/Mastodon/Resources/cs.lproj/InfoPlist.strings @@ -0,0 +1,4 @@ +"NSCameraUsageDescription" = "Used to take photo for post status"; +"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; +"NewPostShortcutItemTitle" = "New Post"; +"SearchShortcutItemTitle" = "Search"; \ No newline at end of file diff --git a/Mastodon/Resources/sl.lproj/InfoPlist.strings b/Mastodon/Resources/sl.lproj/InfoPlist.strings new file mode 100644 index 000000000..710865573 --- /dev/null +++ b/Mastodon/Resources/sl.lproj/InfoPlist.strings @@ -0,0 +1,4 @@ +"NSCameraUsageDescription" = "Used to take photo for post status"; +"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; +"NewPostShortcutItemTitle" = "New Post"; +"SearchShortcutItemTitle" = "Search"; \ No newline at end of file diff --git a/MastodonIntent/cs.lproj/Intents.strings b/MastodonIntent/cs.lproj/Intents.strings new file mode 100644 index 000000000..6f29830a1 --- /dev/null +++ b/MastodonIntent/cs.lproj/Intents.strings @@ -0,0 +1,51 @@ +"16wxgf" = "Příspěvek na Mastodon"; + +"751xkl" = "Textový obsah"; + +"CsR7G2" = "Příspěvek na Mastodon"; + +"HZSGTr" = "Jaký obsah se má přidat?"; + +"HdGikU" = "Odeslání se nezdařilo"; + +"KDNTJ4" = "Důvod selhání"; + +"RHxKOw" = "Odeslat příspěvek s textovým obsahem"; + +"RxSqsb" = "Příspěvek"; + +"WCIR3D" = "Zveřejnit ${content} na Mastodon"; + +"ZKJSNu" = "Příspěvek"; + +"ZS1XaK" = "${content}"; + +"ZbSjzC" = "Viditelnost"; + +"Zo4jgJ" = "Viditelnost příspěvku"; + +"apSxMG-dYQ5NN" = "Existuje ${count} možností odpovídajících 'Veřejný'."; + +"apSxMG-ehFLjY" = "Existuje ${count} možností, které odpovídají „jen sledujícím“."; + +"ayoYEb-dYQ5NN" = "${content}, veřejné"; + +"ayoYEb-ehFLjY" = "${content}, pouze sledující"; + +"dUyuGg" = "Příspěvek na Mastodon"; + +"dYQ5NN" = "Veřejný"; + +"ehFLjY" = "Pouze sledující"; + +"gfePDu" = "Odeslání se nezdařilo. ${failureReason}"; + +"k7dbKQ" = "Příspěvek byl úspěšně odeslán."; + +"oGiqmY-dYQ5NN" = "Jen pro kontrolu, chtěli jste „Veřejný“?"; + +"oGiqmY-ehFLjY" = "Jen pro kontrolu, chtěli jste „Pouze sledující“?"; + +"rM6dvp" = "URL"; + +"ryJLwG" = "Příspěvek byl úspěšně odeslán. "; diff --git a/MastodonIntent/cs.lproj/Intents.stringsdict b/MastodonIntent/cs.lproj/Intents.stringsdict new file mode 100644 index 000000000..5a39d5e64 --- /dev/null +++ b/MastodonIntent/cs.lproj/Intents.stringsdict @@ -0,0 +1,54 @@ + + + + + There are ${count} options matching ‘${content}’. - 2 + + NSStringLocalizedFormatKey + There are %#@count_option@ matching ‘${content}’. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + zero + 0 options + one + 1 option + two + 2 options + few + %ld options + many + %ld options + other + %ld options + + + There are ${count} options matching ‘${visibility}’. + + NSStringLocalizedFormatKey + There are %#@count_option@ matching ‘${visibility}’. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + zero + 0 options + one + 1 option + two + 2 options + few + %ld options + many + %ld options + other + %ld options + + + + diff --git a/MastodonIntent/sl.lproj/Intents.strings b/MastodonIntent/sl.lproj/Intents.strings new file mode 100644 index 000000000..72de87df2 --- /dev/null +++ b/MastodonIntent/sl.lproj/Intents.strings @@ -0,0 +1,51 @@ +"16wxgf" = "Objavi na Mastodonu"; + +"751xkl" = "besedilo"; + +"CsR7G2" = "Objavi na Mastodonu"; + +"HZSGTr" = "Kakšno vsebino želite objaviti?"; + +"HdGikU" = "Objava ni uspela"; + +"KDNTJ4" = "Vzrok za neuspeh"; + +"RHxKOw" = "Pošlji objavo z besedilom"; + +"RxSqsb" = "Objavi"; + +"WCIR3D" = "Objavite ${content} na Mastodonu"; + +"ZKJSNu" = "Objavi"; + +"ZS1XaK" = "${content}"; + +"ZbSjzC" = "Vidnost"; + +"Zo4jgJ" = "Vidnost objave"; + +"apSxMG-dYQ5NN" = "Z \"Javno\" se ujema ${count} možnosti."; + +"apSxMG-ehFLjY" = "S \"Samo sledilci\" se ujema ${count} možnosti."; + +"ayoYEb-dYQ5NN" = "${content}, javno"; + +"ayoYEb-ehFLjY" = "${content}, samo sledilci"; + +"dUyuGg" = "Objavi na Mastodonu"; + +"dYQ5NN" = "Javno"; + +"ehFLjY" = "Samo sledilci"; + +"gfePDu" = "Objava je spodletela. ${failureReason}"; + +"k7dbKQ" = "Uspešno poslana objava."; + +"oGiqmY-dYQ5NN" = "Da ne bo nesporazuma - želeli ste \"Javno\"?"; + +"oGiqmY-ehFLjY" = "Da ne bo nesporazuma - želeli ste \"Samo sledilci\"?"; + +"rM6dvp" = "URL"; + +"ryJLwG" = "Uspešno poslana objava. "; diff --git a/MastodonIntent/sl.lproj/Intents.stringsdict b/MastodonIntent/sl.lproj/Intents.stringsdict new file mode 100644 index 000000000..5a39d5e64 --- /dev/null +++ b/MastodonIntent/sl.lproj/Intents.stringsdict @@ -0,0 +1,54 @@ + + + + + There are ${count} options matching ‘${content}’. - 2 + + NSStringLocalizedFormatKey + There are %#@count_option@ matching ‘${content}’. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + zero + 0 options + one + 1 option + two + 2 options + few + %ld options + many + %ld options + other + %ld options + + + There are ${count} options matching ‘${visibility}’. + + NSStringLocalizedFormatKey + There are %#@count_option@ matching ‘${visibility}’. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + zero + 0 options + one + 1 option + two + 2 options + few + %ld options + many + %ld options + other + %ld options + + + + diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/cs.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/cs.lproj/Localizable.strings new file mode 100644 index 000000000..fbf2ff2f4 --- /dev/null +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/cs.lproj/Localizable.strings @@ -0,0 +1,459 @@ +"Common.Alerts.BlockDomain.BlockEntireDomain" = "Blokovat doménu"; +"Common.Alerts.BlockDomain.Title" = "Opravdu chcete blokovat celou doménu %@? 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."; +"Common.Alerts.CleanCache.Message" = "Úspěšně vyčištěno %@ mezipaměti."; +"Common.Alerts.CleanCache.Title" = "Vyčistit mezipaměť"; +"Common.Alerts.Common.PleaseTryAgain" = "Zkuste to prosím znovu."; +"Common.Alerts.Common.PleaseTryAgainLater" = "Zkuste to prosím znovu později."; +"Common.Alerts.DeletePost.Message" = "Opravdu chcete smazat tento příspěvek?"; +"Common.Alerts.DeletePost.Title" = "Odstranit příspěvek"; +"Common.Alerts.DiscardPostContent.Message" = "Potvrďte odstranění obsahu složeného příspěvku."; +"Common.Alerts.DiscardPostContent.Title" = "Zahodit koncept"; +"Common.Alerts.EditProfileFailure.Message" = "Nelze upravit profil. Zkuste to prosím znovu."; +"Common.Alerts.EditProfileFailure.Title" = "Chyba při úpravě profilu"; +"Common.Alerts.PublishPostFailure.AttachmentsMessage.MoreThanOneVideo" = "Nelze připojit více než jedno video."; +"Common.Alerts.PublishPostFailure.AttachmentsMessage.VideoAttachWithPhoto" = "K příspěvku, který již obsahuje obrázky, nelze připojit video."; +"Common.Alerts.PublishPostFailure.Message" = "Nepodařilo se publikovat příspěvek. +Zkontrolujte prosím připojení k internetu."; +"Common.Alerts.PublishPostFailure.Title" = "Publikování selhalo"; +"Common.Alerts.SavePhotoFailure.Message" = "Pro uložení fotografie povolte přístup k knihovně fotografií."; +"Common.Alerts.SavePhotoFailure.Title" = "Uložení fotografie se nezdařilo"; +"Common.Alerts.ServerError.Title" = "Chyba serveru"; +"Common.Alerts.SignOut.Confirm" = "Odhlásit se"; +"Common.Alerts.SignOut.Message" = "Opravdu se chcete odhlásit?"; +"Common.Alerts.SignOut.Title" = "Odhlásit se"; +"Common.Alerts.SignUpFailure.Title" = "Registrace selhala"; +"Common.Alerts.VoteFailure.PollEnded" = "Anketa skončila"; +"Common.Alerts.VoteFailure.Title" = "Selhání hlasování"; +"Common.Controls.Actions.Add" = "Přidat"; +"Common.Controls.Actions.Back" = "Zpět"; +"Common.Controls.Actions.BlockDomain" = "Blokovat %@"; +"Common.Controls.Actions.Cancel" = "Zrušit"; +"Common.Controls.Actions.Compose" = "Napsat"; +"Common.Controls.Actions.Confirm" = "Potvrdit"; +"Common.Controls.Actions.Continue" = "Pokračovat"; +"Common.Controls.Actions.CopyPhoto" = "Kopírovat fotografii"; +"Common.Controls.Actions.Delete" = "Smazat"; +"Common.Controls.Actions.Discard" = "Zahodit"; +"Common.Controls.Actions.Done" = "Hotovo"; +"Common.Controls.Actions.Edit" = "Upravit"; +"Common.Controls.Actions.FindPeople" = "Najít lidi ke sledování"; +"Common.Controls.Actions.ManuallySearch" = "Místo toho ručně vyhledat"; +"Common.Controls.Actions.Next" = "Další"; +"Common.Controls.Actions.Ok" = "OK"; +"Common.Controls.Actions.Open" = "Otevřít"; +"Common.Controls.Actions.OpenInBrowser" = "Otevřít v prohlížeči"; +"Common.Controls.Actions.OpenInSafari" = "Otevřít v Safari"; +"Common.Controls.Actions.Preview" = "Náhled"; +"Common.Controls.Actions.Previous" = "Předchozí"; +"Common.Controls.Actions.Remove" = "Odstranit"; +"Common.Controls.Actions.Reply" = "Odpovědět"; +"Common.Controls.Actions.ReportUser" = "Nahlásit %@"; +"Common.Controls.Actions.Save" = "Uložit"; +"Common.Controls.Actions.SavePhoto" = "Uložit fotku"; +"Common.Controls.Actions.SeeMore" = "Zobrazit více"; +"Common.Controls.Actions.Settings" = "Nastavení"; +"Common.Controls.Actions.Share" = "Sdílet"; +"Common.Controls.Actions.SharePost" = "Sdílet příspěvek"; +"Common.Controls.Actions.ShareUser" = "Sdílet %@"; +"Common.Controls.Actions.SignIn" = "Přihlásit se"; +"Common.Controls.Actions.SignUp" = "Vytvořit účet"; +"Common.Controls.Actions.Skip" = "Přeskočit"; +"Common.Controls.Actions.TakePhoto" = "Vyfotit"; +"Common.Controls.Actions.TryAgain" = "Zkusit znovu"; +"Common.Controls.Actions.UnblockDomain" = "Odblokovat %@"; +"Common.Controls.Friendship.Block" = "Blokovat"; +"Common.Controls.Friendship.BlockDomain" = "Blokovat %@"; +"Common.Controls.Friendship.BlockUser" = "Blokovat %@"; +"Common.Controls.Friendship.Blocked" = "Blokovaný"; +"Common.Controls.Friendship.EditInfo" = "Upravit informace"; +"Common.Controls.Friendship.Follow" = "Sledovat"; +"Common.Controls.Friendship.Following" = "Sleduji"; +"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; +"Common.Controls.Friendship.Mute" = "Skrýt"; +"Common.Controls.Friendship.MuteUser" = "Skrýt %@"; +"Common.Controls.Friendship.Muted" = "Skrytý"; +"Common.Controls.Friendship.Pending" = "Čekající"; +"Common.Controls.Friendship.Request" = "Požadavek"; +"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; +"Common.Controls.Friendship.Unblock" = "Odblokovat"; +"Common.Controls.Friendship.UnblockUser" = "Odblokovat %@"; +"Common.Controls.Friendship.Unmute" = "Odkrýt"; +"Common.Controls.Friendship.UnmuteUser" = "Odkrýt %@"; +"Common.Controls.Keyboard.Common.ComposeNewPost" = "Vytvořit nový příspěvek"; +"Common.Controls.Keyboard.Common.OpenSettings" = "Otevřít Nastavení"; +"Common.Controls.Keyboard.Common.ShowFavorites" = "Zobrazit Oblíbené"; +"Common.Controls.Keyboard.Common.SwitchToTab" = "Přepnout na %@"; +"Common.Controls.Keyboard.SegmentedControl.NextSection" = "Další sekce"; +"Common.Controls.Keyboard.SegmentedControl.PreviousSection" = "Předchozí sekce"; +"Common.Controls.Keyboard.Timeline.NextStatus" = "Další příspěvek"; +"Common.Controls.Keyboard.Timeline.OpenAuthorProfile" = "Otevřít profil autora"; +"Common.Controls.Keyboard.Timeline.OpenRebloggerProfile" = "Otevřít rebloggerův profil"; +"Common.Controls.Keyboard.Timeline.OpenStatus" = "Otevřít příspěvek"; +"Common.Controls.Keyboard.Timeline.PreviewImage" = "Náhled obrázku"; +"Common.Controls.Keyboard.Timeline.PreviousStatus" = "Předchozí příspěvek"; +"Common.Controls.Keyboard.Timeline.ReplyStatus" = "Odpovědět na příspěvek"; +"Common.Controls.Keyboard.Timeline.ToggleContentWarning" = "Přepnout varování obsahu"; +"Common.Controls.Keyboard.Timeline.ToggleFavorite" = "Toggle Favorite on Post"; +"Common.Controls.Keyboard.Timeline.ToggleReblog" = "Toggle Reblog on Post"; +"Common.Controls.Status.Actions.Favorite" = "Oblíbit"; +"Common.Controls.Status.Actions.Hide" = "Skrýt"; +"Common.Controls.Status.Actions.Menu" = "Nabídka"; +"Common.Controls.Status.Actions.Reblog" = "Boostnout"; +"Common.Controls.Status.Actions.Reply" = "Odpovědět"; +"Common.Controls.Status.Actions.ShowGif" = "Zobrazit GIF"; +"Common.Controls.Status.Actions.ShowImage" = "Zobrazit obrázek"; +"Common.Controls.Status.Actions.ShowVideoPlayer" = "Zobrazit video přehrávač"; +"Common.Controls.Status.Actions.TapThenHoldToShowMenu" = "Klepnutím podržte pro zobrazení nabídky"; +"Common.Controls.Status.Actions.Unfavorite" = "Odebrat z oblízených"; +"Common.Controls.Status.Actions.Unreblog" = "Undo reblog"; +"Common.Controls.Status.ContentWarning" = "Varování o obsahu"; +"Common.Controls.Status.MediaContentWarning" = "Klepnutím kdekoli zobrazíte"; +"Common.Controls.Status.MetaEntity.Email" = "E-mailová adresa: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Zobrazit profil: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Odkaz: %@"; +"Common.Controls.Status.Poll.Closed" = "Uzavřeno"; +"Common.Controls.Status.Poll.Vote" = "Hlasovat"; +"Common.Controls.Status.SensitiveContent" = "Citlivý obsah"; +"Common.Controls.Status.ShowPost" = "Zobrazit příspěvek"; +"Common.Controls.Status.ShowUserProfile" = "Zobrazit profil uživatele"; +"Common.Controls.Status.Tag.Email" = "E-mail"; +"Common.Controls.Status.Tag.Emoji" = "Emoji"; +"Common.Controls.Status.Tag.Hashtag" = "Hashtag"; +"Common.Controls.Status.Tag.Link" = "Odkaz"; +"Common.Controls.Status.Tag.Mention" = "Zmínka"; +"Common.Controls.Status.Tag.Url" = "URL"; +"Common.Controls.Status.TapToReveal" = "Klepnutím zobrazit"; +"Common.Controls.Status.UserReblogged" = "%@ reblogged"; +"Common.Controls.Status.UserRepliedTo" = "Odpověděl %@"; +"Common.Controls.Status.Visibility.Direct" = "Pouze zmíněný uživatel může vidět tento příspěvek."; +"Common.Controls.Status.Visibility.Private" = "Pouze jejich sledující mohou vidět tento příspěvek."; +"Common.Controls.Status.Visibility.PrivateFromMe" = "Pouze moji sledující mohou vidět tento příspěvek."; +"Common.Controls.Status.Visibility.Unlisted" = "Každý může vidět tento příspěvek, ale nezobrazovat ve veřejné časové ose."; +"Common.Controls.Tabs.Home" = "Domů"; +"Common.Controls.Tabs.Notification" = "Oznamování"; +"Common.Controls.Tabs.Profile" = "Profil"; +"Common.Controls.Tabs.Search" = "Hledat"; +"Common.Controls.Timeline.Filtered" = "Filtrováno"; +"Common.Controls.Timeline.Header.BlockedWarning" = "Nemůžeš zobrazit profil tohoto uživatele, dokud tě neodblokují."; +"Common.Controls.Timeline.Header.BlockingWarning" = "Nemůžete zobrazit profil tohoto uživatele, dokud ho neodblokujete. +Váš profil pro něj vypadá takto."; +"Common.Controls.Timeline.Header.NoStatusFound" = "Nebyl nalezen žádný příspěvek"; +"Common.Controls.Timeline.Header.SuspendedWarning" = "Tento uživatel byl pozastaven."; +"Common.Controls.Timeline.Header.UserBlockedWarning" = "Nemůžete zobrazit profil %@, dokud vás neodblokuje."; +"Common.Controls.Timeline.Header.UserBlockingWarning" = "Nemůžete zobrazit profil %@, dokud ho neodblokujete. +Váš profil pro něj vypadá takto."; +"Common.Controls.Timeline.Header.UserSuspendedWarning" = "Účet %@ byl pozastaven."; +"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Načíst chybějící příspěvky"; +"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Načíst chybějící příspěvky..."; +"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Zobrazit více odovědí"; +"Common.Controls.Timeline.Timestamp.Now" = "Nyní"; +"Scene.AccountList.AddAccount" = "Přidat účet"; +"Scene.AccountList.DismissAccountSwitcher" = "Zrušit přepínač účtů"; +"Scene.AccountList.TabBarHint" = "Aktuální vybraný profil: %@. Dvojitým poklepáním zobrazíte přepínač účtů"; +"Scene.Bookmark.Title" = "Záložky"; +"Scene.Compose.Accessibility.AppendAttachment" = "Přidat přílohu"; +"Scene.Compose.Accessibility.AppendPoll" = "Přidat anketu"; +"Scene.Compose.Accessibility.CustomEmojiPicker" = "Vlastní výběr Emoji"; +"Scene.Compose.Accessibility.DisableContentWarning" = "Vypnout upozornění na obsah"; +"Scene.Compose.Accessibility.EnableContentWarning" = "Povolit upozornění na obsah"; +"Scene.Compose.Accessibility.PostOptions" = "Post Options"; +"Scene.Compose.Accessibility.PostVisibilityMenu" = "Menu viditelnosti příspěvku"; +"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; +"Scene.Compose.Accessibility.RemovePoll" = "Odstranit anketu"; +"Scene.Compose.Attachment.AttachmentBroken" = "Tento %@ je poškozený a nemůže být +nahrán do Mastodonu."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Příloha je příliš velká"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Nelze rozpoznat toto medium přílohy"; +"Scene.Compose.Attachment.CompressingState" = "Probíhá komprese..."; +"Scene.Compose.Attachment.DescriptionPhoto" = "Popište fotografii pro zrakově postižené osoby..."; +"Scene.Compose.Attachment.DescriptionVideo" = "Popište video pro zrakově postižené..."; +"Scene.Compose.Attachment.LoadFailed" = "Načtení se nezdařilo"; +"Scene.Compose.Attachment.Photo" = "fotka"; +"Scene.Compose.Attachment.ServerProcessingState" = "Zpracování serveru..."; +"Scene.Compose.Attachment.UploadFailed" = "Nahrání selhalo"; +"Scene.Compose.Attachment.Video" = "video"; +"Scene.Compose.AutoComplete.SpaceToAdd" = "Mezera k přidání"; +"Scene.Compose.ComposeAction" = "Zveřejnit"; +"Scene.Compose.ContentInputPlaceholder" = "Napište nebo vložte, co je na mysli"; +"Scene.Compose.ContentWarning.Placeholder" = "Zde napište přesné varování..."; +"Scene.Compose.Keyboard.AppendAttachmentEntry" = "Přidat přílohu - %@"; +"Scene.Compose.Keyboard.DiscardPost" = "Zahodit příspěvek"; +"Scene.Compose.Keyboard.PublishPost" = "Publikovat příspěvek"; +"Scene.Compose.Keyboard.SelectVisibilityEntry" = "Vyberte viditelnost - %@"; +"Scene.Compose.Keyboard.ToggleContentWarning" = "Přepnout varování obsahu"; +"Scene.Compose.Keyboard.TogglePoll" = "Přepnout anketu"; +"Scene.Compose.MediaSelection.Browse" = "Procházet"; +"Scene.Compose.MediaSelection.Camera" = "Vyfotit"; +"Scene.Compose.MediaSelection.PhotoLibrary" = "Knihovna fotografií"; +"Scene.Compose.Poll.DurationTime" = "Doba trvání: %@"; +"Scene.Compose.Poll.OneDay" = "1 den"; +"Scene.Compose.Poll.OneHour" = "1 hodina"; +"Scene.Compose.Poll.OptionNumber" = "Možnost %ld"; +"Scene.Compose.Poll.SevenDays" = "7 dní"; +"Scene.Compose.Poll.SixHours" = "6 hodin"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "Anketa má prázdnou možnost"; +"Scene.Compose.Poll.ThePollIsInvalid" = "Anketa je neplatná"; +"Scene.Compose.Poll.ThirtyMinutes" = "30 minut"; +"Scene.Compose.Poll.ThreeDays" = "3 dny"; +"Scene.Compose.ReplyingToUser" = "odpovídá na %@"; +"Scene.Compose.Title.NewPost" = "Nový příspěvek"; +"Scene.Compose.Title.NewReply" = "Nová odpověď"; +"Scene.Compose.Visibility.Direct" = "Pouze lidé, které zmíním"; +"Scene.Compose.Visibility.Private" = "Pouze sledující"; +"Scene.Compose.Visibility.Public" = "Veřejný"; +"Scene.Compose.Visibility.Unlisted" = "Neuvedeno"; +"Scene.ConfirmEmail.Button.OpenEmailApp" = "Otevřít e-mailovou aplikaci"; +"Scene.ConfirmEmail.Button.Resend" = "Poslat znovu"; +"Scene.ConfirmEmail.DontReceiveEmail.Description" = "Zkontrolujte, zda je vaše e-mailová adresa správná, stejně jako složka nevyžádané pošty, pokud ji máte."; +"Scene.ConfirmEmail.DontReceiveEmail.ResendEmail" = "Znovu odeslat e-mail"; +"Scene.ConfirmEmail.DontReceiveEmail.Title" = "Zkontrolujte svůj e-mail"; +"Scene.ConfirmEmail.OpenEmailApp.Description" = "Právě jsme vám poslali e-mail. Zkontrolujte složku nevyžádané zprávy, pokud ji máte."; +"Scene.ConfirmEmail.OpenEmailApp.Mail" = "Pošta"; +"Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "Otevřít e-mailového klienta"; +"Scene.ConfirmEmail.OpenEmailApp.Title" = "Zkontrolujte doručenou poštu."; +"Scene.ConfirmEmail.Subtitle" = "Klepněte na odkaz, který jsme vám poslali e-mailem, abyste ověřili Váš účet."; +"Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount" = "Klepněte na odkaz, který jsme vám poslali e-mailem, abyste ověřili Váš účet"; +"Scene.ConfirmEmail.Title" = "Ještě jedna věc."; +"Scene.Discovery.Intro" = "Toto jsou příspěvky, které získávají pozornost ve vašem koutu Mastodonu."; +"Scene.Discovery.Tabs.Community" = "Komunita"; +"Scene.Discovery.Tabs.ForYou" = "Pro vás"; +"Scene.Discovery.Tabs.Hashtags" = "Hashtagy"; +"Scene.Discovery.Tabs.News" = "Zprávy"; +"Scene.Discovery.Tabs.Posts" = "Příspěvky"; +"Scene.Familiarfollowers.FollowedByNames" = "Sledován od %@"; +"Scene.Familiarfollowers.Title" = "Sledující, které znáte"; +"Scene.Favorite.Title" = "Vaše oblíbené"; +"Scene.FavoritedBy.Title" = "Oblíben"; +"Scene.Follower.Footer" = "Sledující z jiných serverů nejsou zobrazeni."; +"Scene.Follower.Title" = "sledující"; +"Scene.Following.Footer" = "Sledování z jiných serverů není zobrazeno."; +"Scene.Following.Title" = "sledování"; +"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint" = "Klepnutím přejdete nahoru a znovu klepněte na předchozí místo"; +"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel" = "Tlačítko s logem"; +"Scene.HomeTimeline.NavigationBarState.NewPosts" = "Nové příspěvky"; +"Scene.HomeTimeline.NavigationBarState.Offline" = "Offline"; +"Scene.HomeTimeline.NavigationBarState.Published" = "Publikováno!"; +"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publikování příspěvku..."; +"Scene.HomeTimeline.Title" = "Domů"; +"Scene.Login.ServerSearchField.Placeholder" = "Zadejte URL nebo vyhledávejte váš server"; +"Scene.Login.Subtitle" = "Přihlaste se na serveru, na kterém jste si vytvořili účet."; +"Scene.Login.Title" = "Vítejte zpět"; +"Scene.Notification.FollowRequest.Accept" = "Přijmout"; +"Scene.Notification.FollowRequest.Accepted" = "Přijato"; +"Scene.Notification.FollowRequest.Reject" = "odmítnout"; +"Scene.Notification.FollowRequest.Rejected" = "Zamítnuto"; +"Scene.Notification.Keyobard.ShowEverything" = "Zobrazit vše"; +"Scene.Notification.Keyobard.ShowMentions" = "Zobrazit zmínky"; +"Scene.Notification.NotificationDescription.FavoritedYourPost" = "si oblíbil váš příspěvek"; +"Scene.Notification.NotificationDescription.FollowedYou" = "vás sleduje"; +"Scene.Notification.NotificationDescription.MentionedYou" = "vás zmínil/a"; +"Scene.Notification.NotificationDescription.PollHasEnded" = "anketa skončila"; +"Scene.Notification.NotificationDescription.RebloggedYourPost" = "boostnul váš příspěvek"; +"Scene.Notification.NotificationDescription.RequestToFollowYou" = "požádat vás o sledování"; +"Scene.Notification.Title.Everything" = "Všechno"; +"Scene.Notification.Title.Mentions" = "Zmínky"; +"Scene.Preview.Keyboard.ClosePreview" = "Zavřít náhled"; +"Scene.Preview.Keyboard.ShowNext" = "Zobrazit další"; +"Scene.Preview.Keyboard.ShowPrevious" = "Zobrazit předchozí"; +"Scene.Profile.Accessibility.DoubleTapToOpenTheList" = "Dvojitým poklepáním otevřete seznam"; +"Scene.Profile.Accessibility.EditAvatarImage" = "Upravit obrázek avataru"; +"Scene.Profile.Accessibility.ShowAvatarImage" = "Zobrazit obrázek avataru"; +"Scene.Profile.Accessibility.ShowBannerImage" = "Zobrazit obrázek banneru"; +"Scene.Profile.Dashboard.Followers" = "sledující"; +"Scene.Profile.Dashboard.Following" = "sledování"; +"Scene.Profile.Dashboard.Posts" = "příspěvky"; +"Scene.Profile.Fields.AddRow" = "Přidat řádek"; +"Scene.Profile.Fields.Placeholder.Content" = "Obsah"; +"Scene.Profile.Fields.Placeholder.Label" = "Označení"; +"Scene.Profile.Fields.Verified.Long" = "Vlastnictví tohoto odkazu bylo zkontrolováno na %@"; +"Scene.Profile.Fields.Verified.Short" = "Ověřeno na %@"; +"Scene.Profile.Header.FollowsYou" = "Sleduje vás"; +"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Potvrdit blokování %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Blokovat účet"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Potvrdit skrytí %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Skrýt účet"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Potvrďte odblokování %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Odblokovat účet"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Potvrďte zrušení ztlumení %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title" = "Zrušit skrytí účtu"; +"Scene.Profile.SegmentedControl.About" = "O uživateli"; +"Scene.Profile.SegmentedControl.Media" = "Média"; +"Scene.Profile.SegmentedControl.Posts" = "Příspěvky"; +"Scene.Profile.SegmentedControl.PostsAndReplies" = "Příspěvky a odpovědi"; +"Scene.Profile.SegmentedControl.Replies" = "Odpovědí"; +"Scene.RebloggedBy.Title" = "Reblogged By"; +"Scene.Register.Error.Item.Agreement" = "Souhlas"; +"Scene.Register.Error.Item.Email" = "E-mail"; +"Scene.Register.Error.Item.Locale" = "Jazyk"; +"Scene.Register.Error.Item.Password" = "Heslo"; +"Scene.Register.Error.Item.Reason" = "Důvod"; +"Scene.Register.Error.Item.Username" = "Uživatelské jméno"; +"Scene.Register.Error.Reason.Accepted" = "%@ musí být přijato"; +"Scene.Register.Error.Reason.Blank" = "%@ je vyžadováno"; +"Scene.Register.Error.Reason.Blocked" = "%@ používá zakázanou e-mailovou službu"; +"Scene.Register.Error.Reason.Inclusion" = "%@ není podporovaná hodnota"; +"Scene.Register.Error.Reason.Invalid" = "%@ je neplatné"; +"Scene.Register.Error.Reason.Reserved" = "%@ je rezervované klíčové slovo"; +"Scene.Register.Error.Reason.Taken" = "%@ se již používá"; +"Scene.Register.Error.Reason.TooLong" = "%@ je příliš dlouhé"; +"Scene.Register.Error.Reason.TooShort" = "%@ je příliš krátké"; +"Scene.Register.Error.Reason.Unreachable" = "%@ pravděpodobně neexistuje"; +"Scene.Register.Error.Special.EmailInvalid" = "Toto není platná e-mailová adresa"; +"Scene.Register.Error.Special.PasswordTooShort" = "Heslo je příliš krátké (musí mít alespoň 8 znaků)"; +"Scene.Register.Error.Special.UsernameInvalid" = "Uživatelské jméno musí obsahovat pouze alfanumerické znaky a podtržítka"; +"Scene.Register.Error.Special.UsernameTooLong" = "Uživatelské jméno je příliš dlouhé (nemůže být delší než 30 znaků)"; +"Scene.Register.Input.Avatar.Delete" = "Smazat"; +"Scene.Register.Input.DisplayName.Placeholder" = "zobrazované jméno"; +"Scene.Register.Input.Email.Placeholder" = "e-mail"; +"Scene.Register.Input.Invite.RegistrationUserInviteRequest" = "Proč se chcete připojit?"; +"Scene.Register.Input.Password.Accessibility.Checked" = "zaškrtnuto"; +"Scene.Register.Input.Password.Accessibility.Unchecked" = "nezaškrtnuto"; +"Scene.Register.Input.Password.CharacterLimit" = "8 znaků"; +"Scene.Register.Input.Password.Hint" = "Vaše heslo musí obsahovat alespoň 8 znaků"; +"Scene.Register.Input.Password.Placeholder" = "heslo"; +"Scene.Register.Input.Password.Require" = "Heslo musí být alespoň:"; +"Scene.Register.Input.Username.DuplicatePrompt" = "Toto uživatelské jméno je použito."; +"Scene.Register.Input.Username.Placeholder" = "uživatelské jméno"; +"Scene.Register.LetsGetYouSetUpOnDomain" = "Pojďme si nastavit %@"; +"Scene.Register.Title" = "Pojďme si nastavit %@"; +"Scene.Report.Content1" = "Existují nějaké další příspěvky, které byste chtěli přidat do zprávy?"; +"Scene.Report.Content2" = "Je o tomto hlášení něco, co by měli vědět moderátoři?"; +"Scene.Report.ReportSentTitle" = "Děkujeme za nahlášení, podíváme se na to."; +"Scene.Report.Reported" = "NAHLÁŠEN"; +"Scene.Report.Send" = "Odeslat hlášení"; +"Scene.Report.SkipToSend" = "Odeslat bez komentáře"; +"Scene.Report.Step1" = "Krok 1 ze 2"; +"Scene.Report.Step2" = "Krok 2 ze 2"; +"Scene.Report.StepFinal.BlockUser" = "Blokovat %@"; +"Scene.Report.StepFinal.DontWantToSeeThis" = "Nechcete to vidět?"; +"Scene.Report.StepFinal.MuteUser" = "Skrýt %@"; +"Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked" = "Už nebudou moci sledovat nebo vidět vaše příspěvky, ale mohou vidět, zda byly zablokovány."; +"Scene.Report.StepFinal.Unfollow" = "Přestat sledovat"; +"Scene.Report.StepFinal.UnfollowUser" = "Přestat sledovat %@"; +"Scene.Report.StepFinal.Unfollowed" = "Už nesledujete"; +"Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "Když uvidíte něco, co se vám nelíbí na Mastodonu, můžete odstranit tuto osobu ze svého zážitku."; +"Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser" = "Zatímco to posuzujeme, můžete podniknout kroky proti %@"; +"Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "Neuvidíte jejich příspěvky nebo boostnutí v domovském kanálu. Nebudou vědět, že jsou skrytí."; +"Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "Je ještě něco jiného, co bychom měli vědět?"; +"Scene.Report.StepFour.Step4Of4" = "Krok 4 ze 4"; +"Scene.Report.StepOne.IDontLikeIt" = "Nelíbí se mi"; +"Scene.Report.StepOne.ItIsNotSomethingYouWantToSee" = "Není to něco, co chcete vidět"; +"Scene.Report.StepOne.ItViolatesServerRules" = "Porušuje pravidla serveru"; +"Scene.Report.StepOne.ItsSomethingElse" = "Jde o něco jiného"; +"Scene.Report.StepOne.ItsSpam" = "Je to spam"; +"Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies" = "Škodlivé odkazy, falešné zapojení nebo opakující se odpovědi"; +"Scene.Report.StepOne.SelectTheBestMatch" = "Vyberte nejbližší možnost"; +"Scene.Report.StepOne.Step1Of4" = "Krok 1 ze 4"; +"Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories" = "Problém neodpovídá ostatním kategoriím"; +"Scene.Report.StepOne.WhatsWrongWithThisAccount" = "Co je špatně s tímto účtem?"; +"Scene.Report.StepOne.WhatsWrongWithThisPost" = "Co je na tomto příspěvku špatně?"; +"Scene.Report.StepOne.WhatsWrongWithThisUsername" = "Co je špatně na %@?"; +"Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules" = "Máte za to, že porušuje konkrétní pravidla"; +"Scene.Report.StepThree.AreThereAnyPostsThatBackUpThisReport" = "Existují příspěvky dokládající toto hlášení?"; +"Scene.Report.StepThree.SelectAllThatApply" = "Vyberte všechna relevantní"; +"Scene.Report.StepThree.Step3Of4" = "Krok 3 ze 4"; +"Scene.Report.StepTwo.IJustDon’tLikeIt" = "Jen se mi to nelíbí"; +"Scene.Report.StepTwo.SelectAllThatApply" = "Vyberte všechna relevantní"; +"Scene.Report.StepTwo.Step2Of4" = "Krok 2 ze 4"; +"Scene.Report.StepTwo.WhichRulesAreBeingViolated" = "Jaká pravidla jsou porušována?"; +"Scene.Report.TextPlaceholder" = "Napište nebo vložte další komentáře"; +"Scene.Report.Title" = "Nahlásit %@"; +"Scene.Report.TitleReport" = "Nahlásit"; +"Scene.Search.Recommend.Accounts.Description" = "Možná budete chtít sledovat tyto účty"; +"Scene.Search.Recommend.Accounts.Follow" = "Sledovat"; +"Scene.Search.Recommend.Accounts.Title" = "Účty, které by se vám mohly líbit"; +"Scene.Search.Recommend.ButtonText" = "Zobrazit vše"; +"Scene.Search.Recommend.HashTag.Description" = "Hashtagy, kterým se dostává dosti pozornosti"; +"Scene.Search.Recommend.HashTag.PeopleTalking" = "%@ lidí mluví"; +"Scene.Search.Recommend.HashTag.Title" = "Populární na Mastodonu"; +"Scene.Search.SearchBar.Cancel" = "Zrušit"; +"Scene.Search.SearchBar.Placeholder" = "Hledat hashtagy a uživatele"; +"Scene.Search.Searching.Clear" = "Vymazat"; +"Scene.Search.Searching.EmptyState.NoResults" = "Žádné výsledky"; +"Scene.Search.Searching.RecentSearch" = "Nedávná hledání"; +"Scene.Search.Searching.Segment.All" = "Vše"; +"Scene.Search.Searching.Segment.Hashtags" = "Hashtagy"; +"Scene.Search.Searching.Segment.People" = "Lidé"; +"Scene.Search.Searching.Segment.Posts" = "Příspěvky"; +"Scene.Search.Title" = "Hledat"; +"Scene.ServerPicker.Button.Category.Academia" = "akademická sféra"; +"Scene.ServerPicker.Button.Category.Activism" = "aktivismus"; +"Scene.ServerPicker.Button.Category.All" = "Vše"; +"Scene.ServerPicker.Button.Category.AllAccessiblityDescription" = "Kategorie: Vše"; +"Scene.ServerPicker.Button.Category.Art" = "umění"; +"Scene.ServerPicker.Button.Category.Food" = "jídlo"; +"Scene.ServerPicker.Button.Category.Furry" = "furry"; +"Scene.ServerPicker.Button.Category.Games" = "hry"; +"Scene.ServerPicker.Button.Category.General" = "obecné"; +"Scene.ServerPicker.Button.Category.Journalism" = "žurnalistika"; +"Scene.ServerPicker.Button.Category.Lgbt" = "lgbt"; +"Scene.ServerPicker.Button.Category.Music" = "hudba"; +"Scene.ServerPicker.Button.Category.Regional" = "regionální"; +"Scene.ServerPicker.Button.Category.Tech" = "technologie"; +"Scene.ServerPicker.Button.SeeLess" = "Zobrazit méně"; +"Scene.ServerPicker.Button.SeeMore" = "Zobrazit více"; +"Scene.ServerPicker.EmptyState.BadNetwork" = "Při načítání dat nastala chyba. Zkontrolujte připojení k internetu."; +"Scene.ServerPicker.EmptyState.FindingServers" = "Hledání dostupných serverů..."; +"Scene.ServerPicker.EmptyState.NoResults" = "Žádné výsledky"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Hledejte komunity nebo zadejte URL"; +"Scene.ServerPicker.Label.Category" = "KATEGORIE"; +"Scene.ServerPicker.Label.Language" = "JAZYK"; +"Scene.ServerPicker.Label.Users" = "UŽIVATELÉ"; +"Scene.ServerPicker.Subtitle" = "Vyberte server založený ve vašem regionu, podle zájmů nebo podle obecného účelu. Stále můžete chatovat s kýmkoli na Mastodonu bez ohledu na vaše servery."; +"Scene.ServerPicker.Title" = "Mastodon tvoří uživatelé z různých serverů."; +"Scene.ServerRules.Button.Confirm" = "Souhlasím"; +"Scene.ServerRules.PrivacyPolicy" = "zásady ochrany osobních údajů"; +"Scene.ServerRules.Prompt" = "Pokračováním budete podléhat podmínkám služby a zásad ochrany osobních údajů pro uživatele %@."; +"Scene.ServerRules.Subtitle" = "Ty nastavují a prosazují moderátoři %@."; +"Scene.ServerRules.TermsOfService" = "podmínky služby"; +"Scene.ServerRules.Title" = "Některá základní pravidla."; +"Scene.Settings.Footer.MastodonDescription" = "Mastodon je open source software. Na GitHub můžete nahlásit problémy na %@ (%@)"; +"Scene.Settings.Keyboard.CloseSettingsWindow" = "Zavřít okno nastavení"; +"Scene.Settings.Section.Appearance.Automatic" = "Automaticky"; +"Scene.Settings.Section.Appearance.Dark" = "Vždy tmavý"; +"Scene.Settings.Section.Appearance.Light" = "Vždy světlý"; +"Scene.Settings.Section.Appearance.Title" = "Vzhled"; +"Scene.Settings.Section.BoringZone.AccountSettings" = "Nastavení účtu"; +"Scene.Settings.Section.BoringZone.Privacy" = "Zásady ochrany osobních údajů"; +"Scene.Settings.Section.BoringZone.Terms" = "Podmínky služby"; +"Scene.Settings.Section.BoringZone.Title" = "Nudná část"; +"Scene.Settings.Section.LookAndFeel.Light" = "Světlý"; +"Scene.Settings.Section.LookAndFeel.ReallyDark" = "Skutečně tmavý"; +"Scene.Settings.Section.LookAndFeel.SortaDark" = "Sorta Dark"; +"Scene.Settings.Section.LookAndFeel.Title" = "Vzhled a chování"; +"Scene.Settings.Section.LookAndFeel.UseSystem" = "Použít systém"; +"Scene.Settings.Section.Notifications.Boosts" = "Boostnul můj příspěvek"; +"Scene.Settings.Section.Notifications.Favorites" = "Oblíbil si můj příspěvek"; +"Scene.Settings.Section.Notifications.Follows" = "Sleduje mě"; +"Scene.Settings.Section.Notifications.Mentions" = "Zmiňuje mě"; +"Scene.Settings.Section.Notifications.Title" = "Upozornění"; +"Scene.Settings.Section.Notifications.Trigger.Anyone" = "kdokoliv"; +"Scene.Settings.Section.Notifications.Trigger.Follow" = "kdokoli, koho sleduji"; +"Scene.Settings.Section.Notifications.Trigger.Follower" = "sledující"; +"Scene.Settings.Section.Notifications.Trigger.Noone" = "nikdo"; +"Scene.Settings.Section.Notifications.Trigger.Title" = "Upozornit, když"; +"Scene.Settings.Section.Preference.DisableAvatarAnimation" = "Zakázat animované avatary"; +"Scene.Settings.Section.Preference.DisableEmojiAnimation" = "Zakázat animované emoji"; +"Scene.Settings.Section.Preference.OpenLinksInMastodon" = "Otevřít odkazy v Mastodonu"; +"Scene.Settings.Section.Preference.Title" = "Předvolby"; +"Scene.Settings.Section.Preference.TrueBlackDarkMode" = "Skutečný černý tmavý režim"; +"Scene.Settings.Section.Preference.UsingDefaultBrowser" = "Použít výchozí prohlížeč pro otevírání odkazů"; +"Scene.Settings.Section.SpicyZone.Clear" = "Vymazat mezipaměť médií"; +"Scene.Settings.Section.SpicyZone.Signout" = "Odhlásit se"; +"Scene.Settings.Section.SpicyZone.Title" = "Ostrá část"; +"Scene.Settings.Title" = "Nastavení"; +"Scene.SuggestionAccount.FollowExplain" = "Když někoho sledujete, uvidíte jejich příspěvky ve vašem domovském kanálu."; +"Scene.SuggestionAccount.Title" = "Najít lidi pro sledování"; +"Scene.Thread.BackTitle" = "Příspěvek"; +"Scene.Thread.Title" = "Příspěvek od %@"; +"Scene.Welcome.GetStarted" = "Začínáme"; +"Scene.Welcome.LogIn" = "Přihlásit se"; +"Scene.Welcome.Slogan" = "Sociální sítě opět ve vašich rukou."; +"Scene.Wizard.AccessibilityHint" = "Dvojitým poklepáním tohoto průvodce odmítnete"; +"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Přepínání mezi více účty podržením tlačítka profilu."; +"Scene.Wizard.NewInMastodon" = "Nový v Mastodonu"; \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/cs.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/cs.lproj/Localizable.stringsdict new file mode 100644 index 000000000..6e44e9f0a --- /dev/null +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/cs.lproj/Localizable.stringsdict @@ -0,0 +1,581 @@ + + + + + a11y.plural.count.unread.notification + + NSStringLocalizedFormatKey + %#@notification_count_unread_notification@ + notification_count_unread_notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 nepřečtené oznámení + few + %ld nepřečtené oznámení + many + %ld nepřečtených oznámení + other + %ld nepřečtených oznámení + + + a11y.plural.count.input_limit_exceeds + + NSStringLocalizedFormatKey + Vstupní limit přesahuje %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 znak + few + %ld znaky + many + %ld znaků + other + %ld znaků + + + a11y.plural.count.input_limit_remains + + NSStringLocalizedFormatKey + Vstupní limit zůstává %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 znak + few + %ld znaky + many + %ld znaků + other + %ld znaků + + + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 znak + few + %ld znaky + many + %ld znaků + other + %ld znaků + + + plural.count.followed_by_and_mutual + + NSStringLocalizedFormatKey + %#@names@%#@count_mutual@ + names + + one + + few + + many + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + + + count_mutual + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + 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 + one + příspěvek + few + příspěvky + many + příspěvků + other + příspěvků + + + plural.count.media + + NSStringLocalizedFormatKey + %#@media_count@ + media_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 médium + few + %ld média + many + %ld médií + other + %ld médií + + + plural.count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 příspěvek + few + %ld příspěvky + many + %ld příspěvků + other + %ld příspěvků + + + plural.count.favorite + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 oblíbený + few + %ld favorites + many + %ld favorites + other + %ld favorites + + + plural.count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reblog + few + %ld reblogs + many + %ld reblogs + other + %ld reblogs + + + plural.count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 odpověď + few + %ld odpovědi + many + %ld odpovědí + other + %ld odpovědí + + + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 hlas + few + %ld hlasy + many + %ld hlasů + other + %ld hlasů + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 hlasující + few + %ld hlasující + many + %ld hlasujících + other + %ld hlasujících + + + plural.people_talking + + NSStringLocalizedFormatKey + %#@count_people_talking@ + count_people_talking + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + 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 + one + 1 sledující + few + %ld sledující + many + %ld sledujících + other + %ld sledujících + + + plural.count.follower + + NSStringLocalizedFormatKey + %#@count_follower@ + count_follower + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + 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 + one + Zbývá 1 rok + few + Zbývají %ld roky + many + Zbývá %ld roků + other + Zbývá %ld roků + + + date.month.left + + NSStringLocalizedFormatKey + %#@count_month_left@ + count_month_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + Zbývá 1 měsíc + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + one + 1s ago + few + %lds ago + many + %lds ago + other + %lds ago + + + + diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/sl.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/sl.lproj/Localizable.strings new file mode 100644 index 000000000..44f868442 --- /dev/null +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/sl.lproj/Localizable.strings @@ -0,0 +1,464 @@ +"Common.Alerts.BlockDomain.BlockEntireDomain" = "Blokiraj domeno"; +"Common.Alerts.BlockDomain.Title" = "Ali ste res, res prepričani, da želite blokirati celotno %@? V večini primerov je nekaj ciljnih blokiranj ali utišanj dovolj in boljše. Vsebine iz te domene ne boste videli na javnih časovnicah ali obvestilih. Vaši sledilci iz te domene bodo odstranjeni."; +"Common.Alerts.CleanCache.Message" = "Uspešno počiščem predpomnilnik %@."; +"Common.Alerts.CleanCache.Title" = "Počisti predpomnilnik"; +"Common.Alerts.Common.PleaseTryAgain" = "Poskusite znova."; +"Common.Alerts.Common.PleaseTryAgainLater" = "Poskusite znova pozneje."; +"Common.Alerts.DeletePost.Message" = "Ali ste prepričani, da želite izbrisati to objavo?"; +"Common.Alerts.DeletePost.Title" = "Izbriši objavo"; +"Common.Alerts.DiscardPostContent.Message" = "Potrdite za opustitev sestavljene vsebine objave."; +"Common.Alerts.DiscardPostContent.Title" = "Zavrzi osnutek"; +"Common.Alerts.EditProfileFailure.Message" = "Profila ni mogoče urejati. Poskusite znova."; +"Common.Alerts.EditProfileFailure.Title" = "Napaka urejanja profila"; +"Common.Alerts.PublishPostFailure.AttachmentsMessage.MoreThanOneVideo" = "Ni možno priložiti več kot enega videoposnetka."; +"Common.Alerts.PublishPostFailure.AttachmentsMessage.VideoAttachWithPhoto" = "Videoposnetka ni mogoče priložiti objavi, ki že vsebuje slike."; +"Common.Alerts.PublishPostFailure.Message" = "Objava je spodletela. +Preverite svojo internetno povezavo."; +"Common.Alerts.PublishPostFailure.Title" = "Spodletela objava"; +"Common.Alerts.SavePhotoFailure.Message" = "Za shranjevanje fotografije omogočite pravice za dostop do knjižnice fotografij."; +"Common.Alerts.SavePhotoFailure.Title" = "Neuspelo shranjevanje fotografije"; +"Common.Alerts.ServerError.Title" = "Napaka strežnika"; +"Common.Alerts.SignOut.Confirm" = "Odjava"; +"Common.Alerts.SignOut.Message" = "Ali ste prepričani, da se želite odjaviti?"; +"Common.Alerts.SignOut.Title" = "Odjava"; +"Common.Alerts.SignUpFailure.Title" = "Neuspela registracija"; +"Common.Alerts.VoteFailure.PollEnded" = "Anketa je zaključena"; +"Common.Alerts.VoteFailure.Title" = "Napaka glasovanja"; +"Common.Controls.Actions.Add" = "Dodaj"; +"Common.Controls.Actions.Back" = "Nazaj"; +"Common.Controls.Actions.BlockDomain" = "Blokiraj %@"; +"Common.Controls.Actions.Cancel" = "Prekliči"; +"Common.Controls.Actions.Compose" = "Sestavi"; +"Common.Controls.Actions.Confirm" = "Potrdi"; +"Common.Controls.Actions.Continue" = "Nadaljuj"; +"Common.Controls.Actions.CopyPhoto" = "Kopiraj fotografijo"; +"Common.Controls.Actions.Delete" = "Izbriši"; +"Common.Controls.Actions.Discard" = "Opusti"; +"Common.Controls.Actions.Done" = "Opravljeno"; +"Common.Controls.Actions.Edit" = "Uredi"; +"Common.Controls.Actions.FindPeople" = "Poiščite osebe, ki jim želite slediti"; +"Common.Controls.Actions.ManuallySearch" = "Raje išči ročno"; +"Common.Controls.Actions.Next" = "Naslednji"; +"Common.Controls.Actions.Ok" = "V redu"; +"Common.Controls.Actions.Open" = "Odpri"; +"Common.Controls.Actions.OpenInBrowser" = "Odpri v brskalniku"; +"Common.Controls.Actions.OpenInSafari" = "Odpri v Safariju"; +"Common.Controls.Actions.Preview" = "Predogled"; +"Common.Controls.Actions.Previous" = "Prejšnji"; +"Common.Controls.Actions.Remove" = "Odstrani"; +"Common.Controls.Actions.Reply" = "Odgovori"; +"Common.Controls.Actions.ReportUser" = "Prijavi %@"; +"Common.Controls.Actions.Save" = "Shrani"; +"Common.Controls.Actions.SavePhoto" = "Shrani fotografijo"; +"Common.Controls.Actions.SeeMore" = "Pokaži več"; +"Common.Controls.Actions.Settings" = "Nastavitve"; +"Common.Controls.Actions.Share" = "Deli"; +"Common.Controls.Actions.SharePost" = "Deli objavo"; +"Common.Controls.Actions.ShareUser" = "Deli %@"; +"Common.Controls.Actions.SignIn" = "Prijava"; +"Common.Controls.Actions.SignUp" = "Ustvari račun"; +"Common.Controls.Actions.Skip" = "Preskoči"; +"Common.Controls.Actions.TakePhoto" = "Posnemi fotografijo"; +"Common.Controls.Actions.TryAgain" = "Poskusi ponovno"; +"Common.Controls.Actions.UnblockDomain" = "Odblokiraj %@"; +"Common.Controls.Friendship.Block" = "Blokiraj"; +"Common.Controls.Friendship.BlockDomain" = "Blokiraj %@"; +"Common.Controls.Friendship.BlockUser" = "Blokiraj %@"; +"Common.Controls.Friendship.Blocked" = "Blokirano"; +"Common.Controls.Friendship.EditInfo" = "Uredi podatke"; +"Common.Controls.Friendship.Follow" = "Sledi"; +"Common.Controls.Friendship.Following" = "Sledi"; +"Common.Controls.Friendship.HideReblogs" = "Skrij poobjave"; +"Common.Controls.Friendship.Mute" = "Utišaj"; +"Common.Controls.Friendship.MuteUser" = "Utišaj %@"; +"Common.Controls.Friendship.Muted" = "Utišan"; +"Common.Controls.Friendship.Pending" = "Na čakanju"; +"Common.Controls.Friendship.Request" = "Zahteva"; +"Common.Controls.Friendship.ShowReblogs" = "Pokaži poobjave"; +"Common.Controls.Friendship.Unblock" = "Odblokiraj"; +"Common.Controls.Friendship.UnblockUser" = "Odblokiraj %@"; +"Common.Controls.Friendship.Unmute" = "Odtišaj"; +"Common.Controls.Friendship.UnmuteUser" = "Odtišaj %@"; +"Common.Controls.Keyboard.Common.ComposeNewPost" = "Sestavi novo objavo"; +"Common.Controls.Keyboard.Common.OpenSettings" = "Odpri nastavitve"; +"Common.Controls.Keyboard.Common.ShowFavorites" = "Pokaži priljubljene"; +"Common.Controls.Keyboard.Common.SwitchToTab" = "Preklopi na %@"; +"Common.Controls.Keyboard.SegmentedControl.NextSection" = "Naslednji odsek"; +"Common.Controls.Keyboard.SegmentedControl.PreviousSection" = "Prejšnji odsek"; +"Common.Controls.Keyboard.Timeline.NextStatus" = "Naslednja objava"; +"Common.Controls.Keyboard.Timeline.OpenAuthorProfile" = "Pokaži profil avtorja"; +"Common.Controls.Keyboard.Timeline.OpenRebloggerProfile" = "Odpri profil poobjavitelja"; +"Common.Controls.Keyboard.Timeline.OpenStatus" = "Odpri objavo"; +"Common.Controls.Keyboard.Timeline.PreviewImage" = "Predogled slike"; +"Common.Controls.Keyboard.Timeline.PreviousStatus" = "Prejšnja objava"; +"Common.Controls.Keyboard.Timeline.ReplyStatus" = "Odgovori"; +"Common.Controls.Keyboard.Timeline.ToggleContentWarning" = "Preklopi opozorilo o vsebini"; +"Common.Controls.Keyboard.Timeline.ToggleFavorite" = "Preklopi priljubljenost objave"; +"Common.Controls.Keyboard.Timeline.ToggleReblog" = "Preklopi poobjavo za objavo"; +"Common.Controls.Status.Actions.Favorite" = "Priljubljen"; +"Common.Controls.Status.Actions.Hide" = "Skrij"; +"Common.Controls.Status.Actions.Menu" = "Meni"; +"Common.Controls.Status.Actions.Reblog" = "Poobjavi"; +"Common.Controls.Status.Actions.Reply" = "Odgovori"; +"Common.Controls.Status.Actions.ShowGif" = "Pokaži GIF"; +"Common.Controls.Status.Actions.ShowImage" = "Pokaži sliko"; +"Common.Controls.Status.Actions.ShowVideoPlayer" = "Pokaži predvajalnik"; +"Common.Controls.Status.Actions.TapThenHoldToShowMenu" = "Tapnite, nato držite, da se pojavi meni"; +"Common.Controls.Status.Actions.Unfavorite" = "Odstrani iz priljubljenih"; +"Common.Controls.Status.Actions.Unreblog" = "Razveljavi poobjavo"; +"Common.Controls.Status.ContentWarning" = "Opozorilo o vsebini"; +"Common.Controls.Status.MediaContentWarning" = "Tapnite kamorkoli, da razkrijete"; +"Common.Controls.Status.MetaEntity.Email" = "E-naslov: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Ključnik: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Pokaži profil: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Povezava: %@"; +"Common.Controls.Status.Poll.Closed" = "Zaprto"; +"Common.Controls.Status.Poll.Vote" = "Glasuj"; +"Common.Controls.Status.SensitiveContent" = "Občutljiva vsebina"; +"Common.Controls.Status.ShowPost" = "Pokaži objavo"; +"Common.Controls.Status.ShowUserProfile" = "Prikaži uporabnikov profil"; +"Common.Controls.Status.Tag.Email" = "E-naslov"; +"Common.Controls.Status.Tag.Emoji" = "Emotikon"; +"Common.Controls.Status.Tag.Hashtag" = "Ključnik"; +"Common.Controls.Status.Tag.Link" = "Povezava"; +"Common.Controls.Status.Tag.Mention" = "Omeni"; +"Common.Controls.Status.Tag.Url" = "URL"; +"Common.Controls.Status.TapToReveal" = "Tapnite za razkritje"; +"Common.Controls.Status.UserReblogged" = "%@ je poobjavil_a"; +"Common.Controls.Status.UserRepliedTo" = "Odgovarja %@"; +"Common.Controls.Status.Visibility.Direct" = "Samo omenjeni uporabnik lahko vidi to objavo."; +"Common.Controls.Status.Visibility.Private" = "Samo sledilci osebe lahko vidijo to objavo."; +"Common.Controls.Status.Visibility.PrivateFromMe" = "Samo moji sledilci lahko vidijo to objavo."; +"Common.Controls.Status.Visibility.Unlisted" = "Vsak lahko vidi to objavo, ni pa prikazana na javni časovnici."; +"Common.Controls.Tabs.Home" = "Domov"; +"Common.Controls.Tabs.Notification" = "Obvestilo"; +"Common.Controls.Tabs.Profile" = "Profil"; +"Common.Controls.Tabs.Search" = "Iskanje"; +"Common.Controls.Timeline.Filtered" = "Filtrirano"; +"Common.Controls.Timeline.Header.BlockedWarning" = "Profila tega uporabnika ne morete +videti, dokler vas ne odblokirajo."; +"Common.Controls.Timeline.Header.BlockingWarning" = "Profila tega uporabnika ne morete +videti, dokler jih ne odblokirate. +Vaš profil je zanje videti tako."; +"Common.Controls.Timeline.Header.NoStatusFound" = "Ne najdem nobenih objav"; +"Common.Controls.Timeline.Header.SuspendedWarning" = "Ta oseba je bila suspendirana."; +"Common.Controls.Timeline.Header.UserBlockedWarning" = "Profila uporabnika %@ ne morete +videti, dokler vas ne odblokirajo."; +"Common.Controls.Timeline.Header.UserBlockingWarning" = "Profila uporabnika %@ ne morete +videti, dokler jih ne odblokirate. +Vaš profil je zanje videti tako."; +"Common.Controls.Timeline.Header.UserSuspendedWarning" = "Račun osebe %@ je suspendiran."; +"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Naloži manjkajoče objave"; +"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Nalaganje manjkajočih objav ..."; +"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Pokaži več odgovorov"; +"Common.Controls.Timeline.Timestamp.Now" = "Zdaj"; +"Scene.AccountList.AddAccount" = "Dodaj račun"; +"Scene.AccountList.DismissAccountSwitcher" = "Umakni preklopnik med računi"; +"Scene.AccountList.TabBarHint" = "Trenutno izbran profil: %@. Dvakrat tapnite, nato držite, da se pojavi preklopnik med računi."; +"Scene.Bookmark.Title" = "Zaznamki"; +"Scene.Compose.Accessibility.AppendAttachment" = "Dodaj priponko"; +"Scene.Compose.Accessibility.AppendPoll" = "Dodaj anketo"; +"Scene.Compose.Accessibility.CustomEmojiPicker" = "Izbirnik čustvenčkov po meri"; +"Scene.Compose.Accessibility.DisableContentWarning" = "Onemogoči opozorilo o vsebini"; +"Scene.Compose.Accessibility.EnableContentWarning" = "Omogoči opozorilo o vsebini"; +"Scene.Compose.Accessibility.PostOptions" = "Možnosti objave"; +"Scene.Compose.Accessibility.PostVisibilityMenu" = "Meni vidnosti objave"; +"Scene.Compose.Accessibility.PostingAs" = "Objavljate kot %@"; +"Scene.Compose.Accessibility.RemovePoll" = "Odstrani anketo"; +"Scene.Compose.Attachment.AttachmentBroken" = "To %@ je okvarjeno in ga ni +možno naložiti v Mastodon."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Priponka je prevelika"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Te medijske priponke ni mogoče prepoznati"; +"Scene.Compose.Attachment.CompressingState" = "Stiskanje ..."; +"Scene.Compose.Attachment.DescriptionPhoto" = "Opiši fotografijo za slabovidne in osebe z okvaro vida ..."; +"Scene.Compose.Attachment.DescriptionVideo" = "Opiši video za slabovidne in osebe z okvaro vida ..."; +"Scene.Compose.Attachment.LoadFailed" = "Nalaganje ni uspelo"; +"Scene.Compose.Attachment.Photo" = "fotografija"; +"Scene.Compose.Attachment.ServerProcessingState" = "Obdelovanje na strežniku ..."; +"Scene.Compose.Attachment.UploadFailed" = "Nalaganje na strežnik ni uspelo"; +"Scene.Compose.Attachment.Video" = "video"; +"Scene.Compose.AutoComplete.SpaceToAdd" = "Preslednica za dodajanje"; +"Scene.Compose.ComposeAction" = "Objavi"; +"Scene.Compose.ContentInputPlaceholder" = "Vnesite ali prilepite, kar vam leži na duši"; +"Scene.Compose.ContentWarning.Placeholder" = "Tukaj zapišite opozorilo ..."; +"Scene.Compose.Keyboard.AppendAttachmentEntry" = "Dodaj priponko - %@"; +"Scene.Compose.Keyboard.DiscardPost" = "Opusti objavo"; +"Scene.Compose.Keyboard.PublishPost" = "Objavi objavo"; +"Scene.Compose.Keyboard.SelectVisibilityEntry" = "Izberite vidnost - %@"; +"Scene.Compose.Keyboard.ToggleContentWarning" = "Preklopi opozorilo o vsebini"; +"Scene.Compose.Keyboard.TogglePoll" = "Preklopi anketo"; +"Scene.Compose.MediaSelection.Browse" = "Prebrskaj"; +"Scene.Compose.MediaSelection.Camera" = "Posnemi fotografijo"; +"Scene.Compose.MediaSelection.PhotoLibrary" = "Knjižnica fotografij"; +"Scene.Compose.Poll.DurationTime" = "Trajanje: %@"; +"Scene.Compose.Poll.OneDay" = "1 dan"; +"Scene.Compose.Poll.OneHour" = "1 ura"; +"Scene.Compose.Poll.OptionNumber" = "Možnost %ld"; +"Scene.Compose.Poll.SevenDays" = "7 dni"; +"Scene.Compose.Poll.SixHours" = "6 ur"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "Anketa ima prazno izbiro"; +"Scene.Compose.Poll.ThePollIsInvalid" = "Anketa je neveljavna"; +"Scene.Compose.Poll.ThirtyMinutes" = "30 minut"; +"Scene.Compose.Poll.ThreeDays" = "3 dni"; +"Scene.Compose.ReplyingToUser" = "odgovarja %@"; +"Scene.Compose.Title.NewPost" = "Nova objava"; +"Scene.Compose.Title.NewReply" = "Nov odgovor"; +"Scene.Compose.Visibility.Direct" = "Samo osebe, ki jih omenjam"; +"Scene.Compose.Visibility.Private" = "Samo sledilci"; +"Scene.Compose.Visibility.Public" = "Javno"; +"Scene.Compose.Visibility.Unlisted" = "Ni prikazano"; +"Scene.ConfirmEmail.Button.OpenEmailApp" = "Odpri aplikacijo za e-pošto"; +"Scene.ConfirmEmail.Button.Resend" = "Ponovno pošlji"; +"Scene.ConfirmEmail.DontReceiveEmail.Description" = "Preverite, ali je vaš e-naslov pravilen, pa tudi vsebino mape neželene pošte, če tega še niste storili."; +"Scene.ConfirmEmail.DontReceiveEmail.ResendEmail" = "Ponovno pošlji e-pošto"; +"Scene.ConfirmEmail.DontReceiveEmail.Title" = "Preverite svojo e-pošto"; +"Scene.ConfirmEmail.OpenEmailApp.Description" = "Ravnokar smo vam poslali e-sporočilo. Preverite neželeno pošto, če sporočila ne najdete med dohodno pošto."; +"Scene.ConfirmEmail.OpenEmailApp.Mail" = "E-pošta"; +"Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "Odpri odjemalca e-pošte"; +"Scene.ConfirmEmail.OpenEmailApp.Title" = "Preverite svojo dohodno e-pošto."; +"Scene.ConfirmEmail.Subtitle" = "Tapnite povezavo, ki smo vam jo poslali po e-pošti, da overite svoj račun."; +"Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount" = "Tapnite povezavo, ki smo vam jo poslali po e-pošti, da overite svoj račun"; +"Scene.ConfirmEmail.Title" = "Še zadnja stvar."; +"Scene.Discovery.Intro" = "To so objave, ki plenijo pozornost na vašem koncu Mastodona."; +"Scene.Discovery.Tabs.Community" = "Skupnost"; +"Scene.Discovery.Tabs.ForYou" = "Za vas"; +"Scene.Discovery.Tabs.Hashtags" = "Ključniki"; +"Scene.Discovery.Tabs.News" = "Novice"; +"Scene.Discovery.Tabs.Posts" = "Objave"; +"Scene.Familiarfollowers.FollowedByNames" = "Sledijo %@"; +"Scene.Familiarfollowers.Title" = "Znani sledilci"; +"Scene.Favorite.Title" = "Vaši priljubljeni"; +"Scene.FavoritedBy.Title" = "Med priljubljene dal_a"; +"Scene.Follower.Footer" = "Sledilci z drugih strežnikov niso prikazani."; +"Scene.Follower.Title" = "sledilec"; +"Scene.Following.Footer" = "Sledenje z drugih strežnikov ni prikazano."; +"Scene.Following.Title" = "sledi"; +"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint" = "Tapnite, da podrsate na vrh; tapnite znova, da se pomaknete na prejšnji položaj"; +"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel" = "Gumb logotipa"; +"Scene.HomeTimeline.NavigationBarState.NewPosts" = "Pokaži nove objave"; +"Scene.HomeTimeline.NavigationBarState.Offline" = "Nepovezan"; +"Scene.HomeTimeline.NavigationBarState.Published" = "Objavljeno!"; +"Scene.HomeTimeline.NavigationBarState.Publishing" = "Objavljanje objave ..."; +"Scene.HomeTimeline.Title" = "Domov"; +"Scene.Login.ServerSearchField.Placeholder" = "Vnesite URL ali poiščite svoj strežnik"; +"Scene.Login.Subtitle" = "Prijavite se na strežniku, na katerem ste ustvarili račun."; +"Scene.Login.Title" = "Dobrodošli nazaj"; +"Scene.Notification.FollowRequest.Accept" = "Sprejmi"; +"Scene.Notification.FollowRequest.Accepted" = "Sprejeto"; +"Scene.Notification.FollowRequest.Reject" = "Zavrni"; +"Scene.Notification.FollowRequest.Rejected" = "Zavrnjeno"; +"Scene.Notification.Keyobard.ShowEverything" = "Pokaži vse"; +"Scene.Notification.Keyobard.ShowMentions" = "Pokaži omembe"; +"Scene.Notification.NotificationDescription.FavoritedYourPost" = "je vzljubil/a vašo objavo"; +"Scene.Notification.NotificationDescription.FollowedYou" = "vam sledi"; +"Scene.Notification.NotificationDescription.MentionedYou" = "vas je omenil/a"; +"Scene.Notification.NotificationDescription.PollHasEnded" = "anketa je zaključena"; +"Scene.Notification.NotificationDescription.RebloggedYourPost" = "je poobjavil_a vašo objavo"; +"Scene.Notification.NotificationDescription.RequestToFollowYou" = "vas je zaprosil za sledenje"; +"Scene.Notification.Title.Everything" = "Vse"; +"Scene.Notification.Title.Mentions" = "Omembe"; +"Scene.Preview.Keyboard.ClosePreview" = "Zapri predogled"; +"Scene.Preview.Keyboard.ShowNext" = "Pokaži naslednje"; +"Scene.Preview.Keyboard.ShowPrevious" = "Pokaži prejšnje"; +"Scene.Profile.Accessibility.DoubleTapToOpenTheList" = "Dvakrat tapnite, da se odpre seznam"; +"Scene.Profile.Accessibility.EditAvatarImage" = "Uredi sliko avatarja"; +"Scene.Profile.Accessibility.ShowAvatarImage" = "Pokaži sliko avatarja"; +"Scene.Profile.Accessibility.ShowBannerImage" = "Pokaži sliko pasice"; +"Scene.Profile.Dashboard.Followers" = "sledilcev"; +"Scene.Profile.Dashboard.Following" = "sledi"; +"Scene.Profile.Dashboard.Posts" = "Objave"; +"Scene.Profile.Fields.AddRow" = "Dodaj vrstico"; +"Scene.Profile.Fields.Placeholder.Content" = "Vsebina"; +"Scene.Profile.Fields.Placeholder.Label" = "Oznaka"; +"Scene.Profile.Fields.Verified.Long" = "Lastništvo te povezave je bilo preverjeno %@"; +"Scene.Profile.Fields.Verified.Short" = "Preverjeno %@"; +"Scene.Profile.Header.FollowsYou" = "Vam sledi"; +"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Potrdite za blokado %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Blokiraj račun"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Potrdite, da poobjave ne bodo prikazane"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Skrij poobjave"; +"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Potrdite utišanje %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Utišaj račun"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Potrdite, da bodo poobjave prikazane"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Pokaži poobjave"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Potrdite za umik blokade %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Odblokiraj račun"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Potrdite umik utišanja %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title" = "Odtišaj račun"; +"Scene.Profile.SegmentedControl.About" = "O programu"; +"Scene.Profile.SegmentedControl.Media" = "Mediji"; +"Scene.Profile.SegmentedControl.Posts" = "Objave"; +"Scene.Profile.SegmentedControl.PostsAndReplies" = "Objave in odgovori"; +"Scene.Profile.SegmentedControl.Replies" = "Odgovori"; +"Scene.RebloggedBy.Title" = "Poobjavil_a"; +"Scene.Register.Error.Item.Agreement" = "Sporazum"; +"Scene.Register.Error.Item.Email" = "E-pošta"; +"Scene.Register.Error.Item.Locale" = "Krajevne nastavitve"; +"Scene.Register.Error.Item.Password" = "Geslo"; +"Scene.Register.Error.Item.Reason" = "Razlog"; +"Scene.Register.Error.Item.Username" = "Uporabniško ime"; +"Scene.Register.Error.Reason.Accepted" = "%@ mora biti sprejet"; +"Scene.Register.Error.Reason.Blank" = "%@ je zahtevan"; +"Scene.Register.Error.Reason.Blocked" = "%@ vsebuje nedovoljenega ponudnika e-poštnih storitev"; +"Scene.Register.Error.Reason.Inclusion" = "%@ ni podprta vrednost"; +"Scene.Register.Error.Reason.Invalid" = "%@ ni veljavno"; +"Scene.Register.Error.Reason.Reserved" = "%@ je rezervirana ključna beseda"; +"Scene.Register.Error.Reason.Taken" = "%@ je že v uporabi"; +"Scene.Register.Error.Reason.TooLong" = "%@ je predolgo"; +"Scene.Register.Error.Reason.TooShort" = "%@ je prekratko"; +"Scene.Register.Error.Reason.Unreachable" = "%@ kot kaže ne obstaja"; +"Scene.Register.Error.Special.EmailInvalid" = "E-naslov ni veljaven"; +"Scene.Register.Error.Special.PasswordTooShort" = "Geslo je prekratko (dolgo mora biti vsaj 8 znakov)"; +"Scene.Register.Error.Special.UsernameInvalid" = "Uporabniško ime lahko vsebuje samo alfanumerične znake ter podčrtaje."; +"Scene.Register.Error.Special.UsernameTooLong" = "Uporabniško ime je predolgo (ne more biti daljše od 30 znakov)"; +"Scene.Register.Input.Avatar.Delete" = "Izbriši"; +"Scene.Register.Input.DisplayName.Placeholder" = "pojavno ime"; +"Scene.Register.Input.Email.Placeholder" = "e-pošta"; +"Scene.Register.Input.Invite.RegistrationUserInviteRequest" = "Zakaj se želite pridružiti?"; +"Scene.Register.Input.Password.Accessibility.Checked" = "potrjeno"; +"Scene.Register.Input.Password.Accessibility.Unchecked" = "nepotrjeno"; +"Scene.Register.Input.Password.CharacterLimit" = "8 znakov"; +"Scene.Register.Input.Password.Hint" = "Geslo mora biti dolgo najmanj 8 znakov."; +"Scene.Register.Input.Password.Placeholder" = "geslo"; +"Scene.Register.Input.Password.Require" = "Vaše geslo potrebuje vsaj:"; +"Scene.Register.Input.Username.DuplicatePrompt" = "To ime je že zasedeno."; +"Scene.Register.Input.Username.Placeholder" = "uporabniško ime"; +"Scene.Register.LetsGetYouSetUpOnDomain" = "Naj vas namestimo na %@"; +"Scene.Register.Title" = "Naj vas namestimo na %@"; +"Scene.Report.Content1" = "Ali so še kakšne druge objave, ki bi jih želeli dodati k prijavi?"; +"Scene.Report.Content2" = "Je kaj, kar bi moderatorji morali vedeti o tem poročilu?"; +"Scene.Report.ReportSentTitle" = "Hvala za poročilo, bomo preverili."; +"Scene.Report.Reported" = "PRIJAVLJEN"; +"Scene.Report.Send" = "Pošlji poročilo"; +"Scene.Report.SkipToSend" = "Pošlji brez komentarja"; +"Scene.Report.Step1" = "Korak 1 od 2"; +"Scene.Report.Step2" = "Korak 2 od 2"; +"Scene.Report.StepFinal.BlockUser" = "Blokiraj %@"; +"Scene.Report.StepFinal.DontWantToSeeThis" = "Ne želite videti tega?"; +"Scene.Report.StepFinal.MuteUser" = "Utišaj %@"; +"Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked" = "Nič več ne bodo mogli slediti ali videti vaše objave, lahko pa vidijo, če so blokirani."; +"Scene.Report.StepFinal.Unfollow" = "Prenehaj slediti"; +"Scene.Report.StepFinal.UnfollowUser" = "Prenehaj slediti %@"; +"Scene.Report.StepFinal.Unfollowed" = "Ne sledi več"; +"Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "Če vidite nekaj, česar na Masodonu ne želite, lahko odstranite osebo iz svoje izkušnje."; +"Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser" = "Medtem, ko to pregledujemo, lahko proti %@ ukrepate"; +"Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "Njihovih objav ali poobjav ne boste videli v svojem domačem viru. Ne bodo vedeli, da so utišani."; +"Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "Je še kaj, za kar menite, da bi morali vedeti?"; +"Scene.Report.StepFour.Step4Of4" = "Korak 4 od 4"; +"Scene.Report.StepOne.IDontLikeIt" = "Ni mi všeč"; +"Scene.Report.StepOne.ItIsNotSomethingYouWantToSee" = "To ni tisto, kar želite videti"; +"Scene.Report.StepOne.ItViolatesServerRules" = "Krši strežniška pravila"; +"Scene.Report.StepOne.ItsSomethingElse" = "Gre za nekaj drugega"; +"Scene.Report.StepOne.ItsSpam" = "To je neželena vsebina"; +"Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies" = "Škodljive povezave, lažno prizadevanje ali ponavljajoči se odgovori"; +"Scene.Report.StepOne.SelectTheBestMatch" = "Izberite najboljši zadetek"; +"Scene.Report.StepOne.Step1Of4" = "Korak 1 od 4"; +"Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories" = "Težava ne sodi v druge kategorije"; +"Scene.Report.StepOne.WhatsWrongWithThisAccount" = "Kaj je narobe s tem računom?"; +"Scene.Report.StepOne.WhatsWrongWithThisPost" = "Kaj je narobe s to objavo?"; +"Scene.Report.StepOne.WhatsWrongWithThisUsername" = "Kaj je narobe s/z %@?"; +"Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules" = "Zavedate se, da krši določena pravila"; +"Scene.Report.StepThree.AreThereAnyPostsThatBackUpThisReport" = "Ali so kakšne objave, ki dokazujejo trditve iz tega poročila?"; +"Scene.Report.StepThree.SelectAllThatApply" = "Izberite vse, kar ustreza"; +"Scene.Report.StepThree.Step3Of4" = "Korak 3 od 4"; +"Scene.Report.StepTwo.IJustDon’tLikeIt" = "Ni mi všeč"; +"Scene.Report.StepTwo.SelectAllThatApply" = "Izberite vse, kar ustreza"; +"Scene.Report.StepTwo.Step2Of4" = "Korak 2 od 4"; +"Scene.Report.StepTwo.WhichRulesAreBeingViolated" = "Katera pravila so kršena?"; +"Scene.Report.TextPlaceholder" = "Vnesite ali prilepite dodatne komentarje"; +"Scene.Report.Title" = "Prijavi %@"; +"Scene.Report.TitleReport" = "Poročaj"; +"Scene.Search.Recommend.Accounts.Description" = "Morda želite slediti tem računom"; +"Scene.Search.Recommend.Accounts.Follow" = "Sledi"; +"Scene.Search.Recommend.Accounts.Title" = "Računi, ki vam bi bili morda všeč"; +"Scene.Search.Recommend.ButtonText" = "Prikaži vse"; +"Scene.Search.Recommend.HashTag.Description" = "Ključniki, ki imajo veliko pozornosti"; +"Scene.Search.Recommend.HashTag.PeopleTalking" = "%@ oseb se pogovarja"; +"Scene.Search.Recommend.HashTag.Title" = "V trendu na Mastodonu"; +"Scene.Search.SearchBar.Cancel" = "Prekliči"; +"Scene.Search.SearchBar.Placeholder" = "Išči ključnike in uporabnike"; +"Scene.Search.Searching.Clear" = "Počisti"; +"Scene.Search.Searching.EmptyState.NoResults" = "Ni rezultatov"; +"Scene.Search.Searching.RecentSearch" = "Nedavna iskanja"; +"Scene.Search.Searching.Segment.All" = "Vse"; +"Scene.Search.Searching.Segment.Hashtags" = "Ključniki"; +"Scene.Search.Searching.Segment.People" = "Ljudje"; +"Scene.Search.Searching.Segment.Posts" = "Objave"; +"Scene.Search.Title" = "Iskanje"; +"Scene.ServerPicker.Button.Category.Academia" = "akademsko"; +"Scene.ServerPicker.Button.Category.Activism" = "aktivizem"; +"Scene.ServerPicker.Button.Category.All" = "Vse"; +"Scene.ServerPicker.Button.Category.AllAccessiblityDescription" = "Kategorija: vse"; +"Scene.ServerPicker.Button.Category.Art" = "umetnost"; +"Scene.ServerPicker.Button.Category.Food" = "hrana"; +"Scene.ServerPicker.Button.Category.Furry" = "Kosmato"; +"Scene.ServerPicker.Button.Category.Games" = "igre"; +"Scene.ServerPicker.Button.Category.General" = "splošno"; +"Scene.ServerPicker.Button.Category.Journalism" = "novinarstvo"; +"Scene.ServerPicker.Button.Category.Lgbt" = "lgbq+"; +"Scene.ServerPicker.Button.Category.Music" = "glasba"; +"Scene.ServerPicker.Button.Category.Regional" = "regionalno"; +"Scene.ServerPicker.Button.Category.Tech" = "tehnologija"; +"Scene.ServerPicker.Button.SeeLess" = "Pokaži manj"; +"Scene.ServerPicker.Button.SeeMore" = "Pokaži več"; +"Scene.ServerPicker.EmptyState.BadNetwork" = "Med nalaganjem podatkov je prišlo do napake. Preverite svojo internetno povezavo."; +"Scene.ServerPicker.EmptyState.FindingServers" = "Iskanje razpoložljivih strežnikov ..."; +"Scene.ServerPicker.EmptyState.NoResults" = "Ni rezultatov"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Iščite po skupnostih ali vnesite URL"; +"Scene.ServerPicker.Label.Category" = "KATEGORIJA"; +"Scene.ServerPicker.Label.Language" = "JEZIK"; +"Scene.ServerPicker.Label.Users" = "UPORABNIKI"; +"Scene.ServerPicker.Subtitle" = "Strežnik izberite glede na svojo regijo, zanimanje ali pa kar splošno. Še vedno lahko klepetate s komer koli na Mastodonu, ne glede na strežnik."; +"Scene.ServerPicker.Title" = "Mastodon tvorijo uporabniki z različnih strežnikov."; +"Scene.ServerRules.Button.Confirm" = "Strinjam se"; +"Scene.ServerRules.PrivacyPolicy" = "pravilnik o zasebnosti"; +"Scene.ServerRules.Prompt" = "Če boste nadaljevali, za vas veljajo pogoji storitve in pravilnik o zasebnosti za %@."; +"Scene.ServerRules.Subtitle" = "Slednje določajo in njihovo spoštovanje zagotavljajo moderatorji %@."; +"Scene.ServerRules.TermsOfService" = "pogoji uporabe"; +"Scene.ServerRules.Title" = "Nekaj osnovnih pravil."; +"Scene.Settings.Footer.MastodonDescription" = "Mastodon je odprtokodna programska oprema. Na GitHubu na %@ (%@) lahko poročate o napakah"; +"Scene.Settings.Keyboard.CloseSettingsWindow" = "Zapri okno nastavitev"; +"Scene.Settings.Section.Appearance.Automatic" = "Samodejno"; +"Scene.Settings.Section.Appearance.Dark" = "Vedno temno"; +"Scene.Settings.Section.Appearance.Light" = "Vedno svetlo"; +"Scene.Settings.Section.Appearance.Title" = "Videz"; +"Scene.Settings.Section.BoringZone.AccountSettings" = "Nastavitve računa"; +"Scene.Settings.Section.BoringZone.Privacy" = "Pravilnik o zasebnosti"; +"Scene.Settings.Section.BoringZone.Terms" = "Pogoji uporabe"; +"Scene.Settings.Section.BoringZone.Title" = "Cona dolgočasja"; +"Scene.Settings.Section.LookAndFeel.Light" = "Svetlo"; +"Scene.Settings.Section.LookAndFeel.ReallyDark" = "Zares temno"; +"Scene.Settings.Section.LookAndFeel.SortaDark" = "Nekako temno"; +"Scene.Settings.Section.LookAndFeel.Title" = "Videz in občutek"; +"Scene.Settings.Section.LookAndFeel.UseSystem" = "Uporabi sistemsko"; +"Scene.Settings.Section.Notifications.Boosts" = "prepošlje mojo objavo"; +"Scene.Settings.Section.Notifications.Favorites" = "mojo objavo da med priljubljene"; +"Scene.Settings.Section.Notifications.Follows" = "me sledi"; +"Scene.Settings.Section.Notifications.Mentions" = "me omeni"; +"Scene.Settings.Section.Notifications.Title" = "Obvestila"; +"Scene.Settings.Section.Notifications.Trigger.Anyone" = "kdor koli"; +"Scene.Settings.Section.Notifications.Trigger.Follow" = "nekdo, ki mu sledim,"; +"Scene.Settings.Section.Notifications.Trigger.Follower" = "sledilec/ka"; +"Scene.Settings.Section.Notifications.Trigger.Noone" = "nihče"; +"Scene.Settings.Section.Notifications.Trigger.Title" = "Obvesti me, ko"; +"Scene.Settings.Section.Preference.DisableAvatarAnimation" = "Onemogoči animirane avatarje"; +"Scene.Settings.Section.Preference.DisableEmojiAnimation" = "Onemogoči animirane emotikone"; +"Scene.Settings.Section.Preference.OpenLinksInMastodon" = "Odpri povezave v Mastodonu"; +"Scene.Settings.Section.Preference.Title" = "Nastavitve"; +"Scene.Settings.Section.Preference.TrueBlackDarkMode" = "Resnično črni temni način"; +"Scene.Settings.Section.Preference.UsingDefaultBrowser" = "Uporabi privzeti brskalnik za odpiranje povezav"; +"Scene.Settings.Section.SpicyZone.Clear" = "Počisti medijski predpomnilnik"; +"Scene.Settings.Section.SpicyZone.Signout" = "Odjava"; +"Scene.Settings.Section.SpicyZone.Title" = "Pikantna cona"; +"Scene.Settings.Title" = "Nastavitve"; +"Scene.SuggestionAccount.FollowExplain" = "Ko nekomu sledite, vidite njihove objave v svojem domačem viru."; +"Scene.SuggestionAccount.Title" = "Poiščite osebe, ki jim želite slediti"; +"Scene.Thread.BackTitle" = "Objavi"; +"Scene.Thread.Title" = "Objavil/a"; +"Scene.Welcome.GetStarted" = "Začnite"; +"Scene.Welcome.LogIn" = "Prijava"; +"Scene.Welcome.Slogan" = "Družbeno mreženje +spet v vaših rokah."; +"Scene.Wizard.AccessibilityHint" = "Dvakrat tapnite, da zapustite tega čarovnika"; +"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Preklopite med več računi s pritiskom gumba profila."; +"Scene.Wizard.NewInMastodon" = "Novo v Mastodonu"; \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/sl.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/sl.lproj/Localizable.stringsdict new file mode 100644 index 000000000..87cc42142 --- /dev/null +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/sl.lproj/Localizable.stringsdict @@ -0,0 +1,581 @@ + + + + + a11y.plural.count.unread.notification + + NSStringLocalizedFormatKey + %#@notification_count_unread_notification@ + notification_count_unread_notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld neprebrano obvestilo + two + %ld neprebrani obvestili + few + %ld neprebrana obvestila + other + %ld neprebranih obvestil + + + a11y.plural.count.input_limit_exceeds + + NSStringLocalizedFormatKey + Omejitev vnosa presega %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld znak + two + %ld znaka + few + %ld znaki + other + %ld znakov + + + a11y.plural.count.input_limit_remains + + NSStringLocalizedFormatKey + Omejitev vnosa ostaja %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld znak + two + %ld znaka + few + %ld znaki + other + %ld znakov + + + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + preostaja %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld znak + two + %ld znaka + few + %ld znaki + other + %ld znakov + + + plural.count.followed_by_and_mutual + + NSStringLocalizedFormatKey + %#@names@%#@count_mutual@ + names + + one + + two + + few + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + + + count_mutual + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + Sledijo %1$@ in %ld skupni + two + Sledijo %1$@ in %ld skupna + few + Sledijo %1$@ in %ld skupni + other + Sledijo %1$@ in %ld skupnih + + + plural.count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + objava + two + objavi + few + objave + other + objav + + + plural.count.media + + NSStringLocalizedFormatKey + %#@media_count@ + media_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld medij + two + %ld medija + few + %ld mediji + other + %ld medijev + + + plural.count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld objava + two + %ld objavi + few + %ld objave + other + %ld objav + + + plural.count.favorite + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld priljubljeni + two + %ld priljubljena + few + %ld priljubljeni + other + %ld priljubljenih + + + plural.count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld poobjava + two + %ld poobjavi + few + %ld poobjave + other + %ld poobjav + + + plural.count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld odgovor + two + %ld odgovora + few + %ld odgovori + other + %ld odgovorov + + + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld glas + two + %ld glasova + few + %ld glasovi + other + %ld glasov + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld glasovalec + two + %ld glasovalca + few + %ld glasovalci + other + %ld glasovalcev + + + plural.people_talking + + NSStringLocalizedFormatKey + %#@count_people_talking@ + count_people_talking + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld oseba se pogovarja + two + %ld osebi se pogovarjata + few + %ld osebe se pogovarjajo + other + %ld oseb se pogovarja + + + plural.count.following + + NSStringLocalizedFormatKey + %#@count_following@ + count_following + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld sledi + two + %ld sledita + few + %ld sledijo + other + %ld sledijo + + + plural.count.follower + + NSStringLocalizedFormatKey + %#@count_follower@ + count_follower + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + %ld sledilec + two + %ld sledilca + few + %ld sledilci + other + %ld sledilcev + + + date.year.left + + NSStringLocalizedFormatKey + %#@count_year_left@ + count_year_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + na voljo še %ld leto + two + na voljo še %ld leti + few + na voljo še %ld leta + other + na voljo še %ld let + + + date.month.left + + NSStringLocalizedFormatKey + %#@count_month_left@ + count_month_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + na voljo še %ld mesec + two + na voljo še %ld meseca + few + na voljo še %ld mesece + other + na voljo še %ld mesecev + + + date.day.left + + NSStringLocalizedFormatKey + %#@count_day_left@ + count_day_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + še %ld dan + two + še %ld dneva + few + še %ld dnevi + other + še %ld dni + + + date.hour.left + + NSStringLocalizedFormatKey + %#@count_hour_left@ + count_hour_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + na voljo še %ld uro + two + na voljo še %ld uri + few + na voljo še %ld ure + other + na voljo še %ld ur + + + date.minute.left + + NSStringLocalizedFormatKey + %#@count_minute_left@ + count_minute_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + Še %ld min. + two + Še %ld min. + few + Še %ld min. + other + Še %ld min. + + + date.second.left + + NSStringLocalizedFormatKey + %#@count_second_left@ + count_second_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + Preostane še %ld s + two + Preostaneta še %ld s + few + Preostanejo še %ld s + other + Preostane še %ld s + + + date.year.ago.abbr + + NSStringLocalizedFormatKey + %#@count_year_ago_abbr@ + count_year_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + pred %ld letom + two + pred %ld letoma + few + pred %ld leti + other + pred %ld leti + + + date.month.ago.abbr + + NSStringLocalizedFormatKey + %#@count_month_ago_abbr@ + count_month_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + pred %ld mesecem + two + pred %ld mesecema + few + pred %ld meseci + other + pred %ld meseci + + + date.day.ago.abbr + + NSStringLocalizedFormatKey + %#@count_day_ago_abbr@ + count_day_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + pred %ld dnem + two + pred %ld dnevoma + few + pred %ld dnemi + other + pred %ld dnemi + + + date.hour.ago.abbr + + NSStringLocalizedFormatKey + %#@count_hour_ago_abbr@ + count_hour_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + pred %ld uro + two + pred %ld urama + few + pred %ld urami + other + pred %ld urami + + + date.minute.ago.abbr + + NSStringLocalizedFormatKey + %#@count_minute_ago_abbr@ + count_minute_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + pred %ld min + two + pred %ld min + few + pred %ld min + other + pred %ld min + + + date.second.ago.abbr + + NSStringLocalizedFormatKey + %#@count_second_ago_abbr@ + count_second_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + pred %ld s + two + pred %ld s + few + pred %ld s + other + pred %ld s + + + + From 3ee9035758aedfab64c7f5b646e23fc8122b62c9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 12:58:57 +0100 Subject: [PATCH 589/658] New translations app.json (Korean) --- .../StringsConvertor/input/ko.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/ko.lproj/app.json b/Localization/StringsConvertor/input/ko.lproj/app.json index 3971b18e9..070386bf9 100644 --- a/Localization/StringsConvertor/input/ko.lproj/app.json +++ b/Localization/StringsConvertor/input/ko.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "사진 촬영", "save_photo": "사진 저장", "copy_photo": "사진 복사", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "로그인", + "sign_up": "계정 생성", "see_more": "더 보기", "preview": "미리보기", "share": "공유", @@ -219,15 +219,15 @@ "log_in": "로그인" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "돌아오신 것을 환영합니다", + "subtitle": "계정을 만든 서버에 로그인.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "URL을 입력하거나 서버를 검색" } }, "server_picker": { "title": "서버를 고르세요,\n아무 서버나 좋습니다.", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "당신의 지역이나, 관심사에 따라, 혹은 그냥 일반적인 목적의 서버를 고르세요. 어떤 서버를 고르더라도 마스토돈의 다른 모두와 소통할 수 있습니다.", "button": { "category": { "all": "모두", @@ -254,7 +254,7 @@ "category": "분류" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "커뮤니티를 검색하거나 URL을 입력" }, "empty_state": { "finding_servers": "사용 가능한 서버를 찾는 중입니다...", From fe308e20d56b725e6eb8c8c73bbe6ecacb35888b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 12:58:58 +0100 Subject: [PATCH 590/658] New translations app.json (Scottish Gaelic) --- .../StringsConvertor/input/gd.lproj/app.json | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/Localization/StringsConvertor/input/gd.lproj/app.json b/Localization/StringsConvertor/input/gd.lproj/app.json index f29f0fa17..74666cb0c 100644 --- a/Localization/StringsConvertor/input/gd.lproj/app.json +++ b/Localization/StringsConvertor/input/gd.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Tog dealbh", "save_photo": "Sàbhail an dealbh", "copy_photo": "Dèan lethbhreac dhen dealbh", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "Clàraich a-steach", + "sign_up": "Cruthaich cunntas", "see_more": "Seall a bharrachd", "preview": "Ro-sheall", "share": "Co-roinn", @@ -137,10 +137,10 @@ "closed": "Dùinte" }, "meta_entity": { - "url": "Link: %s", - "hashtag": "Hashtag: %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "url": "Ceangal: %s", + "hashtag": "Taga hais: %s", + "mention": "Seall a’ phròifil: %s", + "email": "Seòladh puist-d: %s" }, "actions": { "reply": "Freagair", @@ -187,8 +187,8 @@ "unmute_user": "Dì-mhùch %s", "muted": "’Ga mhùchadh", "edit_info": "Deasaich", - "show_reblogs": "Show Reblogs", - "hide_reblogs": "Hide Reblogs" + "show_reblogs": "Seall na brosnachaidhean", + "hide_reblogs": "Falaich na brosnachaidhean" }, "timeline": { "filtered": "Criathraichte", @@ -219,15 +219,15 @@ "log_in": "Clàraich a-steach" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "Fàilte air ais", + "subtitle": "Clàraich a-steach air an fhrithealaiche far an do chruthaich thu an cunntas agad.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "Cuir a-steach URL an fhrithealaiche agad" } }, "server_picker": { "title": "Tha cleachdaichean Mhastodon air iomadh frithealaiche eadar-dhealaichte.", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "Tagh frithealaiche stèidhichte air na sgìre agad, d’ ùidhean, air far a bheil thu no fear coitcheann. ’S urrainn dhut fhathast conaltradh le duine sam bith air Mastodon ge b’ e na frithealaichean agaibh-se.", "button": { "category": { "all": "Na h-uile", @@ -254,7 +254,7 @@ "category": "ROINN-SEÒRSA" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "Lorg coimhearsnachd no cuir a-steach URL" }, "empty_state": { "finding_servers": "A’ lorg nam frithealaichean ri am faighinn…", @@ -388,12 +388,12 @@ "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…", - "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..." + "load_failed": "Dh’fhàillig leis an luchdadh", + "upload_failed": "Dh’fhàillig leis an luchdadh suas", + "can_not_recognize_this_media_attachment": "Cha do dh’aithnich sinn an ceanglachan meadhain seo", + "attachment_too_large": "Tha an ceanglachan ro mhòr", + "compressing_state": "’Ga dhùmhlachadh…", + "server_processing_state": "Tha am frithealaiche ’ga phròiseasadh…" }, "poll": { "duration_time": "Faide: %s", @@ -404,8 +404,8 @@ "three_days": "3 làithean", "seven_days": "Seachdain", "option_number": "Roghainn %ld", - "the_poll_is_invalid": "The poll is invalid", - "the_poll_has_empty_option": "The poll has empty option" + "the_poll_is_invalid": "Tha an cunntas-bheachd mì-dhligheach", + "the_poll_has_empty_option": "Tha roghainn fhalamh aig a’ chunntas-bheachd" }, "content_warning": { "placeholder": "Sgrìobh rabhadh pongail an-seo…" @@ -427,8 +427,8 @@ "enable_content_warning": "Cuir rabhadh susbainte an comas", "disable_content_warning": "Cuir rabhadh susbainte à comas", "post_visibility_menu": "Clàr-taice faicsinneachd a’ phuist", - "post_options": "Post Options", - "posting_as": "Posting as %s" + "post_options": "Roghainnean postaidh", + "posting_as": "A’ postadh mar %s" }, "keyboard": { "discard_post": "Tilg air falbh am post", @@ -455,8 +455,8 @@ "content": "Susbaint" }, "verified": { - "short": "Verified on %s", - "long": "Ownership of this link was checked on %s" + "short": "Air a dhearbhadh %s", + "long": "Chaidh dearbhadh cò leis a tha an ceangal seo %s" } }, "segmented_control": { @@ -484,12 +484,12 @@ "message": "Dearbh dì-bhacadh %s" }, "confirm_show_reblogs": { - "title": "Show Reblogs", - "message": "Confirm to show reblogs" + "title": "Seall na brosnachaidhean", + "message": "Dearbh sealladh nam brosnachaidhean" }, "confirm_hide_reblogs": { - "title": "Hide Reblogs", - "message": "Confirm to hide reblogs" + "title": "Falaich na brosnachaidhean", + "message": "Dearbh falach nam brosnachaidhean" } }, "accessibility": { @@ -721,7 +721,7 @@ "accessibility_hint": "Thoir gnogag dhùbailte a’ leigeil seachad an draoidh seo" }, "bookmark": { - "title": "Bookmarks" + "title": "Comharran-lìn" } } } From 64f397b43a84fd742378494253c6134a101fb8fc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 12:58:59 +0100 Subject: [PATCH 591/658] New translations Localizable.stringsdict (Scottish Gaelic) --- .../input/gd.lproj/Localizable.stringsdict | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Localization/StringsConvertor/input/gd.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/gd.lproj/Localizable.stringsdict index 45ba1e156..9b3e69ea7 100644 --- a/Localization/StringsConvertor/input/gd.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/gd.lproj/Localizable.stringsdict @@ -65,7 +65,7 @@ a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ left + %#@character_count@ air fhàgail character_count NSStringFormatSpecTypeKey @@ -73,13 +73,13 @@ NSStringFormatValueTypeKey ld one - 1 character + %ld charactar two - %ld characters + %ld charactar few - %ld characters + %ld caractaran other - %ld characters + %ld caractar plural.count.followed_by_and_mutual From a1596cb7eae15372ac643637d46fb11702c77660 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 12:59:01 +0100 Subject: [PATCH 592/658] New translations app.json (Chinese Simplified) --- .../input/zh-Hans.lproj/app.json | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json index 242d9aac2..c503c5186 100644 --- a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "拍照", "save_photo": "保存照片", "copy_photo": "拷贝照片", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "登录", + "sign_up": "创建账户", "see_more": "查看更多", "preview": "预览", "share": "分享", @@ -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": "已过滤", @@ -219,15 +219,15 @@ "log_in": "登录" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "欢迎回来", + "subtitle": "登入您账户所在的服务器。", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "输入网址或搜索您的服务器" } }, "server_picker": { "title": "挑选一个服务器,\n任意服务器。", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "根据你的地区、兴趣挑选一个服务器。无论你选择哪个服务器,你都可以跟其他服务器的任何人一起聊天。", "button": { "category": { "all": "全部", @@ -254,7 +254,7 @@ "category": "类别" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "搜索社区或输入 URL" }, "empty_state": { "finding_servers": "正在查找可用的服务器...", @@ -388,12 +388,12 @@ "attachment_broken": "%s已损坏\n无法上传到 Mastodon", "description_photo": "为视觉障碍人士添加照片的文字说明...", "description_video": "为视觉障碍人士添加视频的文字说明...", - "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..." + "load_failed": "加载失败", + "upload_failed": "上传失败", + "can_not_recognize_this_media_attachment": "无法识别此媒体", + "attachment_too_large": "附件太大", + "compressing_state": "压缩中...", + "server_processing_state": "服务器正在处理..." }, "poll": { "duration_time": "时长:%s", @@ -404,8 +404,8 @@ "three_days": "3 天", "seven_days": "7 天", "option_number": "选项 %ld", - "the_poll_is_invalid": "The poll is invalid", - "the_poll_has_empty_option": "The poll has empty option" + "the_poll_is_invalid": "投票无效", + "the_poll_has_empty_option": "投票含有空选项" }, "content_warning": { "placeholder": "在这里写下内容的警告消息..." @@ -427,8 +427,8 @@ "enable_content_warning": "启用内容警告", "disable_content_warning": "关闭内容警告", "post_visibility_menu": "帖子可见性", - "post_options": "Post Options", - "posting_as": "Posting as %s" + "post_options": "帖子选项", + "posting_as": "以 %s 身份发布" }, "keyboard": { "discard_post": "丢弃帖子", @@ -455,8 +455,8 @@ "content": "内容" }, "verified": { - "short": "Verified on %s", - "long": "Ownership of this link was checked on %s" + "short": "验证于 %s", + "long": "此链接的所有权已在 %s 上检查通过" } }, "segmented_control": { @@ -484,12 +484,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": { @@ -721,7 +721,7 @@ "accessibility_hint": "双击关闭此向导" }, "bookmark": { - "title": "Bookmarks" + "title": "书签" } } } From c198d2050e10fbdef9caa6d30f36e1a8968dedb0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 12:59:02 +0100 Subject: [PATCH 593/658] New translations Localizable.stringsdict (Chinese Simplified) --- .../input/zh-Hans.lproj/Localizable.stringsdict | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/zh-Hans.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/zh-Hans.lproj/Localizable.stringsdict index 915a524b5..362d55c4f 100644 --- a/Localization/StringsConvertor/input/zh-Hans.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/zh-Hans.lproj/Localizable.stringsdict @@ -47,7 +47,7 @@ a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ left + %#@character_count@ 剩余 character_count NSStringFormatSpecTypeKey @@ -55,7 +55,7 @@ NSStringFormatValueTypeKey ld other - %ld characters + %ld 个字符 plural.count.followed_by_and_mutual From 6cd6c8c9db587d7b031a6e9b187e7682be5b580d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 12:59:02 +0100 Subject: [PATCH 594/658] New translations app.json (Icelandic) --- .../StringsConvertor/input/is.lproj/app.json | 727 ++++++++++++++++++ 1 file changed, 727 insertions(+) create mode 100644 Localization/StringsConvertor/input/is.lproj/app.json diff --git a/Localization/StringsConvertor/input/is.lproj/app.json b/Localization/StringsConvertor/input/is.lproj/app.json new file mode 100644 index 000000000..3113ada74 --- /dev/null +++ b/Localization/StringsConvertor/input/is.lproj/app.json @@ -0,0 +1,727 @@ +{ + "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": "Log in", + "sign_up": "Create account", + "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" + }, + "login": { + "title": "Welcome back", + "subtitle": "Log you in on the server you created your account on.", + "server_search_field": { + "placeholder": "Enter URL or search for your server" + } + }, + "server_picker": { + "title": "Mastodon is made of users in different servers.", + "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "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": { + "search_servers_or_enter_url": "Search communities 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" + }, + "verified": { + "short": "Verified on %s", + "long": "Ownership of this link was checked on %s" + } + }, + "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" + } + } +} From 22f10bbc348808b2366c2d133504cca4ae3e3c6e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 12:59:03 +0100 Subject: [PATCH 595/658] New translations ios-infoPlist.json (Icelandic) --- .../StringsConvertor/input/is.lproj/ios-infoPlist.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Localization/StringsConvertor/input/is.lproj/ios-infoPlist.json diff --git a/Localization/StringsConvertor/input/is.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/is.lproj/ios-infoPlist.json new file mode 100644 index 000000000..c6db73de0 --- /dev/null +++ b/Localization/StringsConvertor/input/is.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" +} From beabb10bc653442057a7e133f6c23fe7d6cbd125 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 12:59:04 +0100 Subject: [PATCH 596/658] New translations Localizable.stringsdict (Icelandic) --- .../input/is.lproj/Localizable.stringsdict | 465 ++++++++++++++++++ 1 file changed, 465 insertions(+) create mode 100644 Localization/StringsConvertor/input/is.lproj/Localizable.stringsdict diff --git a/Localization/StringsConvertor/input/is.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/is.lproj/Localizable.stringsdict new file mode 100644 index 000000000..eabdc3c32 --- /dev/null +++ b/Localization/StringsConvertor/input/is.lproj/Localizable.stringsdict @@ -0,0 +1,465 @@ + + + + + a11y.plural.count.unread.notification + + NSStringLocalizedFormatKey + %#@notification_count_unread_notification@ + notification_count_unread_notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 unread notification + other + %ld unread notification + + + a11y.plural.count.input_limit_exceeds + + NSStringLocalizedFormatKey + Input limit exceeds %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + + a11y.plural.count.input_limit_remains + + NSStringLocalizedFormatKey + Input limit remains %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ left + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + + plural.count.followed_by_and_mutual + + NSStringLocalizedFormatKey + %#@names@%#@count_mutual@ + names + + one + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + + + count_mutual + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + Followed by %1$@, and another mutual + other + Followed by %1$@, and %ld mutuals + + + plural.count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + post + other + posts + + + plural.count.media + + NSStringLocalizedFormatKey + %#@media_count@ + media_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 media + other + %ld media + + + plural.count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 post + other + %ld posts + + + plural.count.favorite + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 favorite + other + %ld favorites + + + plural.count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reblog + other + %ld reblogs + + + plural.count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reply + other + %ld replies + + + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 vote + other + %ld votes + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 voter + other + %ld voters + + + plural.people_talking + + NSStringLocalizedFormatKey + %#@count_people_talking@ + count_people_talking + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 people talking + other + %ld people talking + + + plural.count.following + + NSStringLocalizedFormatKey + %#@count_following@ + count_following + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 following + other + %ld following + + + plural.count.follower + + NSStringLocalizedFormatKey + %#@count_follower@ + count_follower + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 follower + other + %ld followers + + + date.year.left + + NSStringLocalizedFormatKey + %#@count_year_left@ + count_year_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 year left + other + %ld years left + + + date.month.left + + NSStringLocalizedFormatKey + %#@count_month_left@ + count_month_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 months left + other + %ld months left + + + date.day.left + + NSStringLocalizedFormatKey + %#@count_day_left@ + count_day_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 day left + other + %ld days left + + + date.hour.left + + NSStringLocalizedFormatKey + %#@count_hour_left@ + count_hour_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 hour left + other + %ld hours left + + + date.minute.left + + NSStringLocalizedFormatKey + %#@count_minute_left@ + count_minute_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 minute left + other + %ld minutes left + + + date.second.left + + NSStringLocalizedFormatKey + %#@count_second_left@ + count_second_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 second left + other + %ld seconds left + + + date.year.ago.abbr + + NSStringLocalizedFormatKey + %#@count_year_ago_abbr@ + count_year_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1y ago + other + %ldy ago + + + date.month.ago.abbr + + NSStringLocalizedFormatKey + %#@count_month_ago_abbr@ + count_month_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1M ago + other + %ldM ago + + + date.day.ago.abbr + + NSStringLocalizedFormatKey + %#@count_day_ago_abbr@ + count_day_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1d ago + other + %ldd ago + + + date.hour.ago.abbr + + NSStringLocalizedFormatKey + %#@count_hour_ago_abbr@ + count_hour_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1h ago + other + %ldh ago + + + date.minute.ago.abbr + + NSStringLocalizedFormatKey + %#@count_minute_ago_abbr@ + count_minute_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1m ago + other + %ldm ago + + + date.second.ago.abbr + + NSStringLocalizedFormatKey + %#@count_second_ago_abbr@ + count_second_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1s ago + other + %lds ago + + + + From 4e1b2672878dbe5aa1ae4daf7b171d822c975eb9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 12:59:05 +0100 Subject: [PATCH 597/658] New translations Intents.strings (Icelandic) --- .../Intents/input/is.lproj/Intents.strings | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 Localization/StringsConvertor/Intents/input/is.lproj/Intents.strings diff --git a/Localization/StringsConvertor/Intents/input/is.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/is.lproj/Intents.strings new file mode 100644 index 000000000..6877490ba --- /dev/null +++ b/Localization/StringsConvertor/Intents/input/is.lproj/Intents.strings @@ -0,0 +1,51 @@ +"16wxgf" = "Post on Mastodon"; + +"751xkl" = "Text Content"; + +"CsR7G2" = "Post on Mastodon"; + +"HZSGTr" = "What content to post?"; + +"HdGikU" = "Posting failed"; + +"KDNTJ4" = "Failure Reason"; + +"RHxKOw" = "Send Post with text content"; + +"RxSqsb" = "Post"; + +"WCIR3D" = "Post ${content} on Mastodon"; + +"ZKJSNu" = "Post"; + +"ZS1XaK" = "${content}"; + +"ZbSjzC" = "Visibility"; + +"Zo4jgJ" = "Post Visibility"; + +"apSxMG-dYQ5NN" = "There are ${count} options matching ‘Public’."; + +"apSxMG-ehFLjY" = "There are ${count} options matching ‘Followers Only’."; + +"ayoYEb-dYQ5NN" = "${content}, Public"; + +"ayoYEb-ehFLjY" = "${content}, Followers Only"; + +"dUyuGg" = "Post on Mastodon"; + +"dYQ5NN" = "Public"; + +"ehFLjY" = "Followers Only"; + +"gfePDu" = "Posting failed. ${failureReason}"; + +"k7dbKQ" = "Post was sent successfully."; + +"oGiqmY-dYQ5NN" = "Just to confirm, you wanted ‘Public’?"; + +"oGiqmY-ehFLjY" = "Just to confirm, you wanted ‘Followers Only’?"; + +"rM6dvp" = "URL"; + +"ryJLwG" = "Post was sent successfully. "; From 67cdd1928477dfe4e39e85ca4af4d64846f90642 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 12:59:06 +0100 Subject: [PATCH 598/658] New translations Intents.stringsdict (Icelandic) --- .../input/is.lproj/Intents.stringsdict | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 Localization/StringsConvertor/Intents/input/is.lproj/Intents.stringsdict diff --git a/Localization/StringsConvertor/Intents/input/is.lproj/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/is.lproj/Intents.stringsdict new file mode 100644 index 000000000..18422c772 --- /dev/null +++ b/Localization/StringsConvertor/Intents/input/is.lproj/Intents.stringsdict @@ -0,0 +1,38 @@ + + + + + There are ${count} options matching ‘${content}’. - 2 + + NSStringLocalizedFormatKey + There are %#@count_option@ matching ‘${content}’. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + one + 1 option + other + %ld options + + + There are ${count} options matching ‘${visibility}’. + + NSStringLocalizedFormatKey + There are %#@count_option@ matching ‘${visibility}’. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + one + 1 option + other + %ld options + + + + From 1f63f68014cba20b58c6dae1ec3ccf4bb4849f29 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 16 Nov 2022 20:02:09 +0800 Subject: [PATCH 599/658] fix: not copy translation resource for info.plist issue --- Mastodon/Resources/Base.lproj/infoPlist.strings | 4 ++++ Mastodon/Resources/ar.lproj/InfoPlist.strings | 2 +- Mastodon/Resources/ckb.lproj/InfoPlist.strings | 8 ++++---- Mastodon/Resources/cs.lproj/InfoPlist.strings | 8 ++++---- Mastodon/Resources/de.lproj/infoPlist.strings | 6 +++--- Mastodon/Resources/eu.lproj/InfoPlist.strings | 8 ++++---- Mastodon/Resources/fi.lproj/InfoPlist.strings | 8 ++++---- Mastodon/Resources/gd.lproj/InfoPlist.strings | 8 ++++---- Mastodon/Resources/gl.lproj/InfoPlist.strings | 8 ++++---- Mastodon/Resources/it.lproj/InfoPlist.strings | 8 ++++---- Mastodon/Resources/kab.lproj/InfoPlist.strings | 8 ++++---- Mastodon/Resources/ku.lproj/InfoPlist.strings | 8 ++++---- Mastodon/Resources/nl.lproj/InfoPlist.strings | 2 +- Mastodon/Resources/sl.lproj/InfoPlist.strings | 8 ++++---- Mastodon/Resources/sv.lproj/InfoPlist.strings | 8 ++++---- Mastodon/Resources/tr.lproj/InfoPlist.strings | 8 ++++---- Mastodon/Resources/vi.lproj/InfoPlist.strings | 8 ++++---- Mastodon/Resources/zh-Hant.lproj/InfoPlist.strings | 8 ++++---- update_localization.sh | 1 + 19 files changed, 66 insertions(+), 61 deletions(-) create mode 100644 Mastodon/Resources/Base.lproj/infoPlist.strings diff --git a/Mastodon/Resources/Base.lproj/infoPlist.strings b/Mastodon/Resources/Base.lproj/infoPlist.strings new file mode 100644 index 000000000..710865573 --- /dev/null +++ b/Mastodon/Resources/Base.lproj/infoPlist.strings @@ -0,0 +1,4 @@ +"NSCameraUsageDescription" = "Used to take photo for post status"; +"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; +"NewPostShortcutItemTitle" = "New Post"; +"SearchShortcutItemTitle" = "Search"; \ No newline at end of file diff --git a/Mastodon/Resources/ar.lproj/InfoPlist.strings b/Mastodon/Resources/ar.lproj/InfoPlist.strings index c3b26f14a..ecb81ddd4 100644 --- a/Mastodon/Resources/ar.lproj/InfoPlist.strings +++ b/Mastodon/Resources/ar.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ "NSCameraUsageDescription" = "يُستخدم لالتقاط الصورة عِندَ نشر الحالات"; "NSPhotoLibraryAddUsageDescription" = "يُستخدم لحِفظ الصورة في مكتبة الصور"; -"NewPostShortcutItemTitle" = "منشور جديد"; +"NewPostShortcutItemTitle" = "مَنشُورٌ جَديد"; "SearchShortcutItemTitle" = "البحث"; \ No newline at end of file diff --git a/Mastodon/Resources/ckb.lproj/InfoPlist.strings b/Mastodon/Resources/ckb.lproj/InfoPlist.strings index 710865573..8c4946d2d 100644 --- a/Mastodon/Resources/ckb.lproj/InfoPlist.strings +++ b/Mastodon/Resources/ckb.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ -"NSCameraUsageDescription" = "Used to take photo for post status"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; -"NewPostShortcutItemTitle" = "New Post"; -"SearchShortcutItemTitle" = "Search"; \ No newline at end of file +"NSCameraUsageDescription" = "بەکار دێت بۆ گرتنی وێنەیەک بۆ پۆستەکە"; +"NSPhotoLibraryAddUsageDescription" = "بەکار دێت بۆ هەڵگرتنی وێنە"; +"NewPostShortcutItemTitle" = "پۆستی نوێ"; +"SearchShortcutItemTitle" = "بگەڕێ"; \ No newline at end of file diff --git a/Mastodon/Resources/cs.lproj/InfoPlist.strings b/Mastodon/Resources/cs.lproj/InfoPlist.strings index 710865573..6989cfcb1 100644 --- a/Mastodon/Resources/cs.lproj/InfoPlist.strings +++ b/Mastodon/Resources/cs.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ -"NSCameraUsageDescription" = "Used to take photo for post status"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; -"NewPostShortcutItemTitle" = "New Post"; -"SearchShortcutItemTitle" = "Search"; \ No newline at end of file +"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"; \ No newline at end of file diff --git a/Mastodon/Resources/de.lproj/infoPlist.strings b/Mastodon/Resources/de.lproj/infoPlist.strings index 9c5feae53..9c8438653 100644 --- a/Mastodon/Resources/de.lproj/infoPlist.strings +++ b/Mastodon/Resources/de.lproj/infoPlist.strings @@ -1,4 +1,4 @@ -"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"; \ No newline at end of file +"SearchShortcutItemTitle" = "Suchen"; \ No newline at end of file diff --git a/Mastodon/Resources/eu.lproj/InfoPlist.strings b/Mastodon/Resources/eu.lproj/InfoPlist.strings index 710865573..e9d36a901 100644 --- a/Mastodon/Resources/eu.lproj/InfoPlist.strings +++ b/Mastodon/Resources/eu.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ -"NSCameraUsageDescription" = "Used to take photo for post status"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; -"NewPostShortcutItemTitle" = "New Post"; -"SearchShortcutItemTitle" = "Search"; \ No newline at end of file +"NSCameraUsageDescription" = "Bidalketetarako argazkiak ateratzeko erabiltzen da"; +"NSPhotoLibraryAddUsageDescription" = "Argazkiak Argazki-liburutegian gordetzeko erabiltzen da"; +"NewPostShortcutItemTitle" = "Bidalketa berria"; +"SearchShortcutItemTitle" = "Bilatu"; \ No newline at end of file diff --git a/Mastodon/Resources/fi.lproj/InfoPlist.strings b/Mastodon/Resources/fi.lproj/InfoPlist.strings index 710865573..040b25d69 100644 --- a/Mastodon/Resources/fi.lproj/InfoPlist.strings +++ b/Mastodon/Resources/fi.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ -"NSCameraUsageDescription" = "Used to take photo for post status"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; -"NewPostShortcutItemTitle" = "New Post"; -"SearchShortcutItemTitle" = "Search"; \ No newline at end of file +"NSCameraUsageDescription" = "Käytetään kuvan ottamiseen julkaisua varten"; +"NSPhotoLibraryAddUsageDescription" = "Käytetään kuvan tallentamiseen kuvakirjastoon"; +"NewPostShortcutItemTitle" = "Uusi julkaisu"; +"SearchShortcutItemTitle" = "Haku"; \ No newline at end of file diff --git a/Mastodon/Resources/gd.lproj/InfoPlist.strings b/Mastodon/Resources/gd.lproj/InfoPlist.strings index 710865573..ccb39b44e 100644 --- a/Mastodon/Resources/gd.lproj/InfoPlist.strings +++ b/Mastodon/Resources/gd.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ -"NSCameraUsageDescription" = "Used to take photo for post status"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; -"NewPostShortcutItemTitle" = "New Post"; -"SearchShortcutItemTitle" = "Search"; \ No newline at end of file +"NSCameraUsageDescription" = "’Ga chleachdadh airson dealbh a thogail do staid puist"; +"NSPhotoLibraryAddUsageDescription" = "’Ga chleachdadh airson dealbh a shàbhaladh ann an tasg-lann nan dealbhan"; +"NewPostShortcutItemTitle" = "Post ùr"; +"SearchShortcutItemTitle" = "Lorg"; \ No newline at end of file diff --git a/Mastodon/Resources/gl.lproj/InfoPlist.strings b/Mastodon/Resources/gl.lproj/InfoPlist.strings index 710865573..3d6df3f0f 100644 --- a/Mastodon/Resources/gl.lproj/InfoPlist.strings +++ b/Mastodon/Resources/gl.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ -"NSCameraUsageDescription" = "Used to take photo for post status"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; -"NewPostShortcutItemTitle" = "New Post"; -"SearchShortcutItemTitle" = "Search"; \ No newline at end of file +"NSCameraUsageDescription" = "Utilizado para facer foto e publicar estado"; +"NSPhotoLibraryAddUsageDescription" = "Utilizado para gardar a foto na Biblioteca"; +"NewPostShortcutItemTitle" = "Nova publicación"; +"SearchShortcutItemTitle" = "Buscar"; \ No newline at end of file diff --git a/Mastodon/Resources/it.lproj/InfoPlist.strings b/Mastodon/Resources/it.lproj/InfoPlist.strings index 710865573..0da468639 100644 --- a/Mastodon/Resources/it.lproj/InfoPlist.strings +++ b/Mastodon/Resources/it.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ -"NSCameraUsageDescription" = "Used to take photo for post status"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; -"NewPostShortcutItemTitle" = "New Post"; -"SearchShortcutItemTitle" = "Search"; \ No newline at end of file +"NSCameraUsageDescription" = "Usato per scattare foto per lo stato del post"; +"NSPhotoLibraryAddUsageDescription" = "Utilizzato per salvare la foto nella galleria immagini"; +"NewPostShortcutItemTitle" = "Nuovo post"; +"SearchShortcutItemTitle" = "Cerca"; \ No newline at end of file diff --git a/Mastodon/Resources/kab.lproj/InfoPlist.strings b/Mastodon/Resources/kab.lproj/InfoPlist.strings index 710865573..42adc4cfc 100644 --- a/Mastodon/Resources/kab.lproj/InfoPlist.strings +++ b/Mastodon/Resources/kab.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ -"NSCameraUsageDescription" = "Used to take photo for post status"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; -"NewPostShortcutItemTitle" = "New Post"; -"SearchShortcutItemTitle" = "Search"; \ No newline at end of file +"NSCameraUsageDescription" = "Yettwaseqdac i tuṭṭfa n tewlafin deg usuffeɣ n waddaden"; +"NSPhotoLibraryAddUsageDescription" = "Yettwaseqdac i usekles n tewlafin deg temkarḍit n tewlafin"; +"NewPostShortcutItemTitle" = "Tasuffeɣt tamaynut"; +"SearchShortcutItemTitle" = "Nadi"; \ No newline at end of file diff --git a/Mastodon/Resources/ku.lproj/InfoPlist.strings b/Mastodon/Resources/ku.lproj/InfoPlist.strings index 710865573..669ecfacf 100644 --- a/Mastodon/Resources/ku.lproj/InfoPlist.strings +++ b/Mastodon/Resources/ku.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ -"NSCameraUsageDescription" = "Used to take photo for post status"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; -"NewPostShortcutItemTitle" = "New Post"; -"SearchShortcutItemTitle" = "Search"; \ No newline at end of file +"NSCameraUsageDescription" = "Bo kişandina wêneyê ji bo rewşa şandiyan tê bikaranîn"; +"NSPhotoLibraryAddUsageDescription" = "Ji bo tomarkirina wêneyê di pirtûkxaneya wêneyan de tê bikaranîn"; +"NewPostShortcutItemTitle" = "Şandiya nû"; +"SearchShortcutItemTitle" = "Bigere"; \ No newline at end of file diff --git a/Mastodon/Resources/nl.lproj/InfoPlist.strings b/Mastodon/Resources/nl.lproj/InfoPlist.strings index a45991068..36b6e9802 100644 --- a/Mastodon/Resources/nl.lproj/InfoPlist.strings +++ b/Mastodon/Resources/nl.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ "NSCameraUsageDescription" = "Gebruikt om foto's te nemen voor je berichten"; "NSPhotoLibraryAddUsageDescription" = "Gebruikt om foto's op te slaan in de fotobibliotheek"; "NewPostShortcutItemTitle" = "Nieuw Bericht"; -"SearchShortcutItemTitle" = "Zoeken"; \ No newline at end of file +"SearchShortcutItemTitle" = "Zoek"; \ No newline at end of file diff --git a/Mastodon/Resources/sl.lproj/InfoPlist.strings b/Mastodon/Resources/sl.lproj/InfoPlist.strings index 710865573..57c8319f5 100644 --- a/Mastodon/Resources/sl.lproj/InfoPlist.strings +++ b/Mastodon/Resources/sl.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ -"NSCameraUsageDescription" = "Used to take photo for post status"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; -"NewPostShortcutItemTitle" = "New Post"; -"SearchShortcutItemTitle" = "Search"; \ No newline at end of file +"NSCameraUsageDescription" = "Uporabljeno za zajem fotografij za stanje objave"; +"NSPhotoLibraryAddUsageDescription" = "Uporabljeno za shranjevanje fotografije v knjižnico fotografij"; +"NewPostShortcutItemTitle" = "Nova objava"; +"SearchShortcutItemTitle" = "Iskanje"; \ No newline at end of file diff --git a/Mastodon/Resources/sv.lproj/InfoPlist.strings b/Mastodon/Resources/sv.lproj/InfoPlist.strings index 710865573..e0facc1d2 100644 --- a/Mastodon/Resources/sv.lproj/InfoPlist.strings +++ b/Mastodon/Resources/sv.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ -"NSCameraUsageDescription" = "Used to take photo for post status"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; -"NewPostShortcutItemTitle" = "New Post"; -"SearchShortcutItemTitle" = "Search"; \ No newline at end of file +"NSCameraUsageDescription" = "Används för att ta foto till inlägg"; +"NSPhotoLibraryAddUsageDescription" = "Används för att spara foto till bildbiblioteket"; +"NewPostShortcutItemTitle" = "Nytt inlägg"; +"SearchShortcutItemTitle" = "Sök"; \ No newline at end of file diff --git a/Mastodon/Resources/tr.lproj/InfoPlist.strings b/Mastodon/Resources/tr.lproj/InfoPlist.strings index 710865573..1c4e05659 100644 --- a/Mastodon/Resources/tr.lproj/InfoPlist.strings +++ b/Mastodon/Resources/tr.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ -"NSCameraUsageDescription" = "Used to take photo for post status"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; -"NewPostShortcutItemTitle" = "New Post"; -"SearchShortcutItemTitle" = "Search"; \ No newline at end of file +"NSCameraUsageDescription" = "Fotoğraf çekerek durum paylaşmak için kullanılır"; +"NSPhotoLibraryAddUsageDescription" = "Fotoğraf Albümü'ne fotoğraf kaydetmek için kullanılır"; +"NewPostShortcutItemTitle" = "Yeni Gönderi"; +"SearchShortcutItemTitle" = "Arama"; \ No newline at end of file diff --git a/Mastodon/Resources/vi.lproj/InfoPlist.strings b/Mastodon/Resources/vi.lproj/InfoPlist.strings index 710865573..5dd27b7bc 100644 --- a/Mastodon/Resources/vi.lproj/InfoPlist.strings +++ b/Mastodon/Resources/vi.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ -"NSCameraUsageDescription" = "Used to take photo for post status"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; -"NewPostShortcutItemTitle" = "New Post"; -"SearchShortcutItemTitle" = "Search"; \ No newline at end of file +"NSCameraUsageDescription" = "Được sử dụng để chụp ảnh cho tút"; +"NSPhotoLibraryAddUsageDescription" = "Được sử dụng để lưu ảnh vào Thư viện ảnh"; +"NewPostShortcutItemTitle" = "Viết tút"; +"SearchShortcutItemTitle" = "Tìm kiếm"; \ No newline at end of file diff --git a/Mastodon/Resources/zh-Hant.lproj/InfoPlist.strings b/Mastodon/Resources/zh-Hant.lproj/InfoPlist.strings index 710865573..737d2f857 100644 --- a/Mastodon/Resources/zh-Hant.lproj/InfoPlist.strings +++ b/Mastodon/Resources/zh-Hant.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ -"NSCameraUsageDescription" = "Used to take photo for post status"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; -"NewPostShortcutItemTitle" = "New Post"; -"SearchShortcutItemTitle" = "Search"; \ No newline at end of file +"NSCameraUsageDescription" = "用來拍照發嘟文"; +"NSPhotoLibraryAddUsageDescription" = "用來儲存照片到圖片庫"; +"NewPostShortcutItemTitle" = "新增嘟文"; +"SearchShortcutItemTitle" = "搜尋"; \ No newline at end of file diff --git a/update_localization.sh b/update_localization.sh index 87477bdaa..1a240c893 100755 --- a/update_localization.sh +++ b/update_localization.sh @@ -25,6 +25,7 @@ cd ${SRCROOT}/Localization/StringsConvertor sh ./scripts/build.sh # Task 3 copy strings file +cp -R ${SRCROOT}/Localization/StringsConvertor/output/main/ ${SRCROOT}/Mastodon/Resources cp -R ${SRCROOT}/Localization/StringsConvertor/output/module/ ${SRCROOT}/MastodonSDK/Sources/MastodonLocalization/Resources cp -R ${SRCROOT}/Localization/StringsConvertor/Intents/output/ ${SRCROOT}/MastodonIntent From 9e1140dc7865d0421fe8faefa927cfd0c4bad2e2 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 16 Nov 2022 20:20:42 +0800 Subject: [PATCH 600/658] chore: disable Xcode Cloud and using GitHub action to make the TestFlight build --- .github/workflows/develop-build.yml | 1 + .github/workflows/main.yml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/develop-build.yml b/.github/workflows/develop-build.yml index c37f00731..4b3a4a04c 100644 --- a/.github/workflows/develop-build.yml +++ b/.github/workflows/develop-build.yml @@ -4,6 +4,7 @@ on: push: branches: - develop + - release* - ci-test jobs: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 827276208..1c40b6556 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,6 +6,9 @@ on: - master - develop - feature/* + - feature-* + - issue/* + - issue-* pull_request: branches: - develop From 634796e9dc7ba721426a245e3f3003d37b8b4710 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 16 Nov 2022 13:43:54 +0100 Subject: [PATCH 601/658] fix: Only allow double-tap account toggle if more than 1 account exists --- Mastodon/Scene/Root/MainTab/MainTabBarController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 158cd769b..5f77d9584 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -407,7 +407,7 @@ extension MainTabBarController { assert(Thread.isMainThread) let request = MastodonAuthentication.sortedFetchRequest - guard let accounts = try? context.managedObjectContext.fetch(request) else { return } + guard let accounts = try? context.managedObjectContext.fetch(request), accounts.count > 1 else { return } let nextSelectedAccountIndex: Int? = { for (index, account) in accounts.enumerated() { From 0d3c9de0026675ab7d8b4a649e69388b0862d650 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 13:57:28 +0100 Subject: [PATCH 602/658] New translations app.json (Icelandic) --- .../StringsConvertor/input/is.lproj/app.json | 502 +++++++++--------- 1 file changed, 251 insertions(+), 251 deletions(-) diff --git a/Localization/StringsConvertor/input/is.lproj/app.json b/Localization/StringsConvertor/input/is.lproj/app.json index 3113ada74..c5e73dacd 100644 --- a/Localization/StringsConvertor/input/is.lproj/app.json +++ b/Localization/StringsConvertor/input/is.lproj/app.json @@ -2,21 +2,21 @@ "common": { "alerts": { "common": { - "please_try_again": "Please try again.", - "please_try_again_later": "Please try again later." + "please_try_again": "Endilega reyndu aftur.", + "please_try_again_later": "Reyndu aftur síðar." }, "sign_up_failure": { "title": "Sign Up Failure" }, "server_error": { - "title": "Server Error" + "title": "Villa á þjóni" }, "vote_failure": { "title": "Vote Failure", "poll_ended": "The poll has ended" }, "discard_post_content": { - "title": "Discard Draft", + "title": "Henda drögum", "message": "Confirm to discard composed post content." }, "publish_post_failure": { @@ -32,9 +32,9 @@ "message": "Cannot edit profile. Please try again." }, "sign_out": { - "title": "Sign Out", - "message": "Are you sure you want to sign out?", - "confirm": "Sign Out" + "title": "Skrá út", + "message": "Ertu viss um að þú viljir skrá þig út?", + "confirm": "Skrá út" }, "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.", @@ -45,66 +45,66 @@ "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?" + "title": "Eyða færslu", + "message": "Ertu viss um að þú viljir eyða þessari færslu?" }, "clean_cache": { - "title": "Clean Cache", + "title": "Hreinsa skyndiminni", "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": "Log in", - "sign_up": "Create account", - "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" + "back": "Til baka", + "next": "Næsta", + "previous": "Fyrri", + "open": "Opna", + "add": "Bæta við", + "remove": "Fjarlægja", + "edit": "Breyta", + "save": "Vista", + "ok": "Í lagi", + "done": "Lokið", + "confirm": "Staðfesta", + "continue": "Halda áfram", + "compose": "Skrifa", + "cancel": "Hætta við", + "discard": "Henda", + "try_again": "Reyna aftur", + "take_photo": "Taka ljósmynd", + "save_photo": "Vista mynd", + "copy_photo": "Afrita mynd", + "sign_in": "Skrá inn", + "sign_up": "Stofna notandaaðgang", + "see_more": "Sjá fleira", + "preview": "Forskoða", + "share": "Deila", + "share_user": "Deila %s", + "share_post": "Deila færslu", + "open_in_safari": "Opna í Safari", + "open_in_browser": "Opna í vafra", + "find_people": "Finna fólk til að fylgjast með", + "manually_search": "Leita handvirkt í staðinn", + "skip": "Sleppa", + "reply": "Svara", + "report_user": "Kæra %s", + "block_domain": "Útiloka %s", + "unblock_domain": "Opna á %s", + "settings": "Stillingar", + "delete": "Eyða" }, "tabs": { - "home": "Home", - "search": "Search", - "notification": "Notification", + "home": "Heim", + "search": "Leita", + "notification": "Tilkynning", "profile": "Profile" }, "keyboard": { "common": { "switch_to_tab": "Switch to %s", "compose_new_post": "Compose New Post", - "show_favorites": "Show Favorites", - "open_settings": "Open Settings" + "show_favorites": "Birta eftirlæti", + "open_settings": "Opna stillingar" }, "timeline": { "previous_status": "Previous Post", @@ -127,41 +127,41 @@ "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", + "show_user_profile": "Birta notandasnið", + "content_warning": "Viðvörun vegna efnis", + "sensitive_content": "Viðkvæmt efni", + "media_content_warning": "Ýttu hvar sem er til að birta", + "tap_to_reveal": "Ýttu til að birta", "poll": { - "vote": "Vote", - "closed": "Closed" + "vote": "Greiða atkvæði", + "closed": "Lokið" }, "meta_entity": { - "url": "Link: %s", - "hashtag": "Hashtag: %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "url": "Tengill: %s", + "hashtag": "Myllumerki: %s", + "mention": "Sýna notandasnið: %s", + "email": "Tölvupóstfang: %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", + "reply": "Svara", + "reblog": "Endurbirta", + "unreblog": "Afturkalla endurbirtingu", + "favorite": "Eftirlæti", + "unfavorite": "Taka úr eftirlætum", + "menu": "Valmynd", + "hide": "Fela", + "show_image": "Sýna mynd", + "show_gif": "Birta 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" + "url": "URL-slóð", + "mention": "Minnst á", + "link": "Tengill", + "hashtag": "Myllumerki", + "email": "Tölvupóstur", + "emoji": "Tjáningartákn" }, "visibility": { "unlisted": "Everyone can see this post but not display in the public timeline.", @@ -171,29 +171,29 @@ } }, "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", + "follow": "Fylgja", + "following": "Fylgist með", + "request": "Beiðni", + "pending": "Í bið", + "block": "Útilokun", + "block_user": "Útiloka %s", + "block_domain": "Útiloka %s", + "unblock": "Aflétta útilokun", + "unblock_user": "Opna á %s", + "blocked": "Útilokað", + "mute": "Þagga niður", + "mute_user": "Þagga niður í %s", + "unmute": "Afþagga", + "unmute_user": "Afþagga %s", + "muted": "Þaggað", "edit_info": "Edit Info", "show_reblogs": "Show Reblogs", - "hide_reblogs": "Hide Reblogs" + "hide_reblogs": "Fela endurbirtingar" }, "timeline": { - "filtered": "Filtered", + "filtered": "Síað", "timestamp": { - "now": "Now" + "now": "Núna" }, "loader": { "load_missing_posts": "Load missing posts", @@ -214,12 +214,12 @@ }, "scene": { "welcome": { - "slogan": "Social networking\nback in your hands.", - "get_started": "Get Started", + "slogan": "Samfélagsmiðlar\naftur í þínar hendur.", + "get_started": "Komast í gang", "log_in": "Log In" }, "login": { - "title": "Welcome back", + "title": "Velkomin aftur", "subtitle": "Log you in on the server you created your account on.", "server_search_field": { "placeholder": "Enter URL or search for your server" @@ -230,28 +230,28 @@ "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { - "all": "All", - "all_accessiblity_description": "Category: All", - "academia": "academia", + "all": "Allt", + "all_accessiblity_description": "Flokkur: Allt", + "academia": "akademískt", "activism": "activism", - "food": "food", - "furry": "furry", - "games": "games", - "general": "general", - "journalism": "journalism", + "food": "matur", + "furry": "loðið", + "games": "leikir", + "general": "almennt", + "journalism": "blaðamennska", "lgbt": "lgbt", - "regional": "regional", - "art": "art", - "music": "music", - "tech": "tech" + "regional": "svæðisbundið", + "art": "listir", + "music": "tónlist", + "tech": "tækni" }, - "see_less": "See Less", - "see_more": "See More" + "see_less": "Sjá minna", + "see_more": "Sjá meira" }, "label": { - "language": "LANGUAGE", - "users": "USERS", - "category": "CATEGORY" + "language": "TUNGUMÁL", + "users": "NOTENDUR", + "category": "FLOKKUR" }, "input": { "search_servers_or_enter_url": "Search communities or enter URL" @@ -259,7 +259,7 @@ "empty_state": { "finding_servers": "Finding available servers...", "bad_network": "Something went wrong while loading the data. Check your internet connection.", - "no_results": "No results" + "no_results": "Engar niðurstöður" } }, "register": { @@ -267,40 +267,40 @@ "lets_get_you_set_up_on_domain": "Let’s get you set up on %s", "input": { "avatar": { - "delete": "Delete" + "delete": "Eyða" }, "username": { - "placeholder": "username", + "placeholder": "notandanafn", "duplicate_prompt": "This username is taken." }, "display_name": { - "placeholder": "display name" + "placeholder": "birtingarnafn" }, "email": { - "placeholder": "email" + "placeholder": "tölvupóstur" }, "password": { - "placeholder": "password", - "require": "Your password needs at least:", - "character_limit": "8 characters", + "placeholder": "lykilorð", + "require": "Lykilorðið þitt þarf að minnsta kosti:", + "character_limit": "8 stafi", "accessibility": { - "checked": "checked", - "unchecked": "unchecked" + "checked": "merkt", + "unchecked": "ekki merkt" }, - "hint": "Your password needs at least eight characters" + "hint": "Lykilorðið þitt verður að vera að minnsta kosti 8 stafa langt" }, "invite": { - "registration_user_invite_request": "Why do you want to join?" + "registration_user_invite_request": "Hvers vegna vilt þú taka þátt?" } }, "error": { "item": { - "username": "Username", - "email": "Email", - "password": "Password", - "agreement": "Agreement", - "locale": "Locale", - "reason": "Reason" + "username": "Notandanafn", + "email": "Tölvupóstur", + "password": "Lykilorð", + "agreement": "Notkunarskilmálar", + "locale": "Staðfærsla", + "reason": "Ástæða" }, "reason": { "blocked": "%s contains a disallowed email provider", @@ -326,10 +326,10 @@ "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", + "terms_of_service": "þjónustuskilmálar", + "privacy_policy": "persónuverndarstefna", "button": { - "confirm": "I Agree" + "confirm": "Ég samþykki" } }, "confirm_email": { @@ -338,27 +338,27 @@ "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" + "resend": "Endursenda" }, "dont_receive_email": { - "title": "Check your email", + "title": "Athugaðu tölvupóstinn þinn", "description": "Check if your email address is correct as well as your junk folder if you haven’t.", - "resend_email": "Resend Email" + "resend_email": "Endursenda tölvupóst" }, "open_email_app": { - "title": "Check your inbox.", + "title": "Athugaðu pósthólfið þitt.", "description": "We just sent you an email. Check your junk folder if you haven’t.", - "mail": "Mail", - "open_email_client": "Open Email Client" + "mail": "Tölvupóstur", + "open_email_client": "Opna tölvupóstforrit" } }, "home_timeline": { - "title": "Home", + "title": "Heim", "navigation_bar_state": { - "offline": "Offline", - "new_posts": "See new posts", - "published": "Published!", - "Publishing": "Publishing post...", + "offline": "Ónettengt", + "new_posts": "Skoða nýjar færslur", + "published": "Birt!", + "Publishing": "Birti færslu...", "accessibility": { "logo_label": "Logo Button", "logo_hint": "Tap to scroll to top and tap again to previous location" @@ -371,38 +371,38 @@ }, "compose": { "title": { - "new_post": "New Post", - "new_reply": "New Reply" + "new_post": "Ný færsla", + "new_reply": "Nýtt svar" }, "media_selection": { - "camera": "Take Photo", - "photo_library": "Photo Library", - "browse": "Browse" + "camera": "Taktu mynd", + "photo_library": "Myndasafn", + "browse": "Flakka" }, "content_input_placeholder": "Type or paste what’s on your mind", - "compose_action": "Publish", + "compose_action": "Birta", "replying_to_user": "replying to %s", "attachment": { - "photo": "photo", - "video": "video", + "photo": "ljósmynd", + "video": "myndskeið", "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", + "load_failed": "Hleðsla mistókst", + "upload_failed": "Innsending mistókst", "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..." + "compressing_state": "Þjappa...", + "server_processing_state": "Netþjónn er að vinna..." }, "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", + "duration_time": "Tímalengd: %s", + "thirty_minutes": "30 mínútur", + "one_hour": "1 klukkustund", + "six_hours": "6 klukkustundir", + "one_day": "1 dagur", + "three_days": "3 dagar", + "seven_days": "7 dagar", "option_number": "Option %ld", "the_poll_is_invalid": "The poll is invalid", "the_poll_has_empty_option": "The poll has empty option" @@ -411,8 +411,8 @@ "placeholder": "Write an accurate warning here..." }, "visibility": { - "public": "Public", - "unlisted": "Unlisted", + "public": "Opinbert", + "unlisted": "Óskráð", "private": "Followers only", "direct": "Only people I mention" }, @@ -420,10 +420,10 @@ "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", + "append_attachment": "Bæta við viðhengi", + "append_poll": "Bæta við könnun", + "remove_poll": "Fjarlægja könnun", + "custom_emoji_picker": "Sérsniðið emoji-tánmyndaval", "enable_content_warning": "Enable Content Warning", "disable_content_warning": "Disable Content Warning", "post_visibility_menu": "Post Visibility Menu", @@ -441,18 +441,18 @@ }, "profile": { "header": { - "follows_you": "Follows You" + "follows_you": "Fylgist með þér" }, "dashboard": { - "posts": "posts", - "following": "following", - "followers": "followers" + "posts": "færslur", + "following": "fylgist með", + "followers": "fylgjendur" }, "fields": { - "add_row": "Add Row", + "add_row": "Bæta við röð", "placeholder": { - "label": "Label", - "content": "Content" + "label": "Skýring", + "content": "Efni" }, "verified": { "short": "Verified on %s", @@ -460,11 +460,11 @@ } }, "segmented_control": { - "posts": "Posts", - "replies": "Replies", - "posts_and_replies": "Posts and Replies", - "media": "Media", - "about": "About" + "posts": "Færslur", + "replies": "Svör", + "posts_and_replies": "Færslur og svör", + "media": "Gagnamiðlar", + "about": "Um hugbúnaðinn" }, "relationship_action_alert": { "confirm_mute_user": { @@ -500,11 +500,11 @@ } }, "follower": { - "title": "follower", + "title": "fylgjandi", "footer": "Followers from other servers are not displayed." }, "following": { - "title": "following", + "title": "fylgist með", "footer": "Follows from other servers are not displayed." }, "familiarFollowers": { @@ -518,13 +518,13 @@ "title": "Reblogged By" }, "search": { - "title": "Search", + "title": "Leita", "search_bar": { "placeholder": "Search hashtags and users", - "cancel": "Cancel" + "cancel": "Hætta við" }, "recommend": { - "button_text": "See All", + "button_text": "Sjá allt", "hash_tag": { "title": "Trending on Mastodon", "description": "Hashtags that are getting quite a bit of attention", @@ -533,29 +533,29 @@ "accounts": { "title": "Accounts you might like", "description": "You may like to follow these accounts", - "follow": "Follow" + "follow": "Fylgjast með" } }, "searching": { "segment": { - "all": "All", - "people": "People", - "hashtags": "Hashtags", - "posts": "Posts" + "all": "Allt", + "people": "Fólk", + "hashtags": "Myllumerki", + "posts": "Færslur" }, "empty_state": { - "no_results": "No results" + "no_results": "Engar niðurstöður" }, - "recent_search": "Recent searches", - "clear": "Clear" + "recent_search": "Nýlegar leitir", + "clear": "Hreinsa" } }, "discovery": { "tabs": { - "posts": "Posts", - "hashtags": "Hashtags", - "news": "News", - "community": "Community", + "posts": "Færslur", + "hashtags": "Myllumerki", + "news": "Fréttir", + "community": "Samfélag", "for_you": "For You" }, "intro": "These are the posts gaining traction in your corner of Mastodon." @@ -565,7 +565,7 @@ }, "notification": { "title": { - "Everything": "Everything", + "Everything": "Allt", "Mentions": "Mentions" }, "notification_description": { @@ -581,87 +581,87 @@ "show_mentions": "Show Mentions" }, "follow_request": { - "accept": "Accept", - "accepted": "Accepted", - "reject": "reject", - "rejected": "Rejected" + "accept": "Samþykkja", + "accepted": "Samþykkt", + "reject": "hafna", + "rejected": "Hafnað" } }, "thread": { - "back_title": "Post", - "title": "Post from %s" + "back_title": "Færsla", + "title": "Færsla frá %s" }, "settings": { - "title": "Settings", + "title": "Stillingar", "section": { "appearance": { - "title": "Appearance", - "automatic": "Automatic", - "light": "Always Light", - "dark": "Always Dark" + "title": "Útlit", + "automatic": "Sjálfvirkt", + "light": "Alltaf ljóst", + "dark": "Alltaf dökkt" }, "look_and_feel": { - "title": "Look and Feel", - "use_system": "Use System", - "really_dark": "Really Dark", - "sorta_dark": "Sorta Dark", - "light": "Light" + "title": "Útlit og viðmót", + "use_system": "Nota stillingar kerfis", + "really_dark": "Mjög dökkt", + "sorta_dark": "Nokkuð dökkt", + "light": "Ljóst" }, "notifications": { - "title": "Notifications", + "title": "Tilkynningar", "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", + "anyone": "hver sem er", + "follower": "fylgjandi", + "follow": "hverjum sá sem ég fylgi", + "noone": "enginn", "title": "Notify me when" } }, "preference": { - "title": "Preferences", + "title": "Kjörstillingar", "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" + "open_links_in_mastodon": "Opna tengla í Mastodon" }, "boring_zone": { "title": "The Boring Zone", - "account_settings": "Account Settings", - "terms": "Terms of Service", - "privacy": "Privacy Policy" + "account_settings": "Stillingar aðgangs", + "terms": "Þjónustuskilmálar", + "privacy": "Meðferð persónuupplýsinga" }, "spicy_zone": { "title": "The Spicy Zone", - "clear": "Clear Media Cache", - "signout": "Sign Out" + "clear": "Hreinsa skyndiminni margmiðlunarefnis", + "signout": "Skrá út" } }, "footer": { "mastodon_description": "Mastodon is open source software. You can report issues on GitHub at %s (%s)" }, "keyboard": { - "close_settings_window": "Close Settings Window" + "close_settings_window": "Loka stillingaglugga" } }, "report": { - "title_report": "Report", - "title": "Report %s", - "step1": "Step 1 of 2", - "step2": "Step 2 of 2", + "title_report": "Kæra", + "title": "Kæra %s", + "step1": "Skref 1 af 2", + "step2": "Skref 2 af 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", + "send": "Senda kæru", + "skip_to_send": "Senda án athugasemdar", "text_placeholder": "Type or paste additional comments", - "reported": "REPORTED", + "reported": "TILKYNNT", "step_one": { - "step_1_of_4": "Step 1 of 4", + "step_1_of_4": "Skref 1 af 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?", @@ -676,44 +676,44 @@ "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", + "step_2_of_4": "Skref 2 af 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", + "step_3_of_4": "Skref 3 af 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", + "step_4_of_4": "Skref 4 af 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", + "unfollow": "Hætta að fylgjast með", "unfollowed": "Unfollowed", "unfollow_user": "Unfollow %s", - "mute_user": "Mute %s", + "mute_user": "Þagga niður í %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", + "block_user": "Útiloka %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" + "close_preview": "Loka forskoðun", + "show_next": "Sýna næsta", + "show_previous": "Sýna fyrri" } }, "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" + "add_account": "Bæta við notandaaðgangi" }, "wizard": { "new_in_mastodon": "New in Mastodon", @@ -721,7 +721,7 @@ "accessibility_hint": "Double tap to dismiss this wizard" }, "bookmark": { - "title": "Bookmarks" + "title": "Bókamerki" } } } From 0a2678aca41dd96d1dd745e2dce0ab770069cd6c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 13:57:29 +0100 Subject: [PATCH 603/658] New translations ios-infoPlist.json (Icelandic) --- .../StringsConvertor/input/is.lproj/ios-infoPlist.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/is.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/is.lproj/ios-infoPlist.json index c6db73de0..4d46176af 100644 --- a/Localization/StringsConvertor/input/is.lproj/ios-infoPlist.json +++ b/Localization/StringsConvertor/input/is.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" + "NewPostShortcutItemTitle": "Ný færsla", + "SearchShortcutItemTitle": "Leita" } From 5864ef1505f478b14b40973fc16b8711fae41868 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 13:57:31 +0100 Subject: [PATCH 604/658] New translations Localizable.stringsdict (Icelandic) --- .../input/is.lproj/Localizable.stringsdict | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Localization/StringsConvertor/input/is.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/is.lproj/Localizable.stringsdict index eabdc3c32..aae13ffb9 100644 --- a/Localization/StringsConvertor/input/is.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/is.lproj/Localizable.stringsdict @@ -29,7 +29,7 @@ NSStringFormatValueTypeKey ld one - 1 character + 1 stafur other %ld characters @@ -45,7 +45,7 @@ NSStringFormatValueTypeKey ld one - 1 character + 1 stafur other %ld characters @@ -61,7 +61,7 @@ NSStringFormatValueTypeKey ld one - 1 character + 1 stafur other %ld characters @@ -104,7 +104,7 @@ NSStringFormatValueTypeKey ld one - post + færsla other posts @@ -120,7 +120,7 @@ NSStringFormatValueTypeKey ld one - 1 media + 1 gagnamiðill other %ld media @@ -136,7 +136,7 @@ NSStringFormatValueTypeKey ld one - 1 post + 1 færsla other %ld posts @@ -152,7 +152,7 @@ NSStringFormatValueTypeKey ld one - 1 favorite + 1 eftirlæti other %ld favorites @@ -168,7 +168,7 @@ NSStringFormatValueTypeKey ld one - 1 reblog + 1 endurbirting other %ld reblogs @@ -184,7 +184,7 @@ NSStringFormatValueTypeKey ld one - 1 reply + 1 svar other %ld replies @@ -200,7 +200,7 @@ NSStringFormatValueTypeKey ld one - 1 vote + 1 atkvæði other %ld votes @@ -216,7 +216,7 @@ NSStringFormatValueTypeKey ld one - 1 voter + 1 kjósandi other %ld voters @@ -248,7 +248,7 @@ NSStringFormatValueTypeKey ld one - 1 following + Eftirfarandi villur komu upp: other %ld following @@ -424,7 +424,7 @@ NSStringFormatValueTypeKey ld one - 1h ago + 1klst síðan other %ldh ago @@ -440,7 +440,7 @@ NSStringFormatValueTypeKey ld one - 1m ago + 1m síðan other %ldm ago @@ -456,7 +456,7 @@ NSStringFormatValueTypeKey ld one - 1s ago + 1s síðan other %lds ago From ae642b72b27bb18874e66efc9137832388d1163f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 13:57:32 +0100 Subject: [PATCH 605/658] New translations Intents.strings (Icelandic) --- .../Intents/input/is.lproj/Intents.strings | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Localization/StringsConvertor/Intents/input/is.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/is.lproj/Intents.strings index 6877490ba..dbe022814 100644 --- a/Localization/StringsConvertor/Intents/input/is.lproj/Intents.strings +++ b/Localization/StringsConvertor/Intents/input/is.lproj/Intents.strings @@ -1,42 +1,42 @@ -"16wxgf" = "Post on Mastodon"; +"16wxgf" = "Birta á Mastodon"; -"751xkl" = "Text Content"; +"751xkl" = "Efni texta"; "CsR7G2" = "Post on Mastodon"; "HZSGTr" = "What content to post?"; -"HdGikU" = "Posting failed"; +"HdGikU" = "Birting færslu mistókst"; -"KDNTJ4" = "Failure Reason"; +"KDNTJ4" = "Ástæða bilunar"; "RHxKOw" = "Send Post with text content"; -"RxSqsb" = "Post"; +"RxSqsb" = "Færsla"; -"WCIR3D" = "Post ${content} on Mastodon"; +"WCIR3D" = "Birta ${content} á Mastodon"; -"ZKJSNu" = "Post"; +"ZKJSNu" = "Færsla"; "ZS1XaK" = "${content}"; -"ZbSjzC" = "Visibility"; +"ZbSjzC" = "Sýnileiki"; -"Zo4jgJ" = "Post Visibility"; +"Zo4jgJ" = "Sýnileiki færslu"; "apSxMG-dYQ5NN" = "There are ${count} options matching ‘Public’."; "apSxMG-ehFLjY" = "There are ${count} options matching ‘Followers Only’."; -"ayoYEb-dYQ5NN" = "${content}, Public"; +"ayoYEb-dYQ5NN" = "${content}, opinbert"; -"ayoYEb-ehFLjY" = "${content}, Followers Only"; +"ayoYEb-ehFLjY" = "${content}, einungis fylgjendur"; -"dUyuGg" = "Post on Mastodon"; +"dUyuGg" = "Birta á Mastodon"; -"dYQ5NN" = "Public"; +"dYQ5NN" = "Opinbert"; -"ehFLjY" = "Followers Only"; +"ehFLjY" = "Einungis fylgjendur"; "gfePDu" = "Posting failed. ${failureReason}"; @@ -46,6 +46,6 @@ "oGiqmY-ehFLjY" = "Just to confirm, you wanted ‘Followers Only’?"; -"rM6dvp" = "URL"; +"rM6dvp" = "URL-slóð"; "ryJLwG" = "Post was sent successfully. "; From e89677bff425e535479f7cff55be125e16edd6cf Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 13:57:33 +0100 Subject: [PATCH 606/658] New translations Intents.stringsdict (Icelandic) --- .../Intents/input/is.lproj/Intents.stringsdict | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/Intents/input/is.lproj/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/is.lproj/Intents.stringsdict index 18422c772..9212186f0 100644 --- a/Localization/StringsConvertor/Intents/input/is.lproj/Intents.stringsdict +++ b/Localization/StringsConvertor/Intents/input/is.lproj/Intents.stringsdict @@ -13,7 +13,7 @@ NSStringFormatValueTypeKey %ld one - 1 option + 1 valkostur other %ld options @@ -29,7 +29,7 @@ NSStringFormatValueTypeKey %ld one - 1 option + 1 valkostur other %ld options From ac281be9a132a18d88a9c2f994ee40ccdd95b4a2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 14:52:42 +0100 Subject: [PATCH 607/658] New translations app.json (Catalan) --- .../StringsConvertor/input/ca.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/ca.lproj/app.json b/Localization/StringsConvertor/input/ca.lproj/app.json index a417ffdbb..52bb67c77 100644 --- a/Localization/StringsConvertor/input/ca.lproj/app.json +++ b/Localization/StringsConvertor/input/ca.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Fes una foto", "save_photo": "Desa la foto", "copy_photo": "Copia la foto", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "Iniciar sessió", + "sign_up": "Crea un compte", "see_more": "Veure més", "preview": "Vista prèvia", "share": "Comparteix", @@ -219,15 +219,15 @@ "log_in": "Inicia sessió" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "Ben tornat", + "subtitle": "T'inicia sessió en el servidor on has creat el teu compte.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "Insereix la URL o cerca el teu servidor" } }, "server_picker": { "title": "Mastodon està fet d'usuaris en diferents comunitats.", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "Tria un servidor en funció de la teva regió, interessos o un de propòsit general. Seguiràs podent connectar amb tothom a Mastodon, independentment del servidor.", "button": { "category": { "all": "Totes", @@ -254,7 +254,7 @@ "category": "CATEGORIA" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "Cerca comunitats o introdueix l'URL" }, "empty_state": { "finding_servers": "Cercant els servidors disponibles...", From bd71e24a18742e13f383384aa2cc5ec49932265e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 14:52:43 +0100 Subject: [PATCH 608/658] New translations app.json (Icelandic) --- .../StringsConvertor/input/is.lproj/app.json | 144 +++++++++--------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/Localization/StringsConvertor/input/is.lproj/app.json b/Localization/StringsConvertor/input/is.lproj/app.json index c5e73dacd..5635ae4f7 100644 --- a/Localization/StringsConvertor/input/is.lproj/app.json +++ b/Localization/StringsConvertor/input/is.lproj/app.json @@ -6,30 +6,30 @@ "please_try_again_later": "Reyndu aftur síðar." }, "sign_up_failure": { - "title": "Sign Up Failure" + "title": "Innskráning mistókst" }, "server_error": { "title": "Villa á þjóni" }, "vote_failure": { - "title": "Vote Failure", - "poll_ended": "The poll has ended" + "title": "Greiðsla atkvæðis mistókst", + "poll_ended": "Könnuninni er lokið" }, "discard_post_content": { "title": "Henda drögum", "message": "Confirm to discard composed post content." }, "publish_post_failure": { - "title": "Publish Failure", - "message": "Failed to publish the post.\nPlease check your internet connection.", + "title": "Mistókst að birta", + "message": "Mistókst að birta færsluna.\nAthugaðu nettenginguna þína.", "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." + "video_attach_with_photo": "Ekki er hægt að hengja myndskeið við færslu sem þegar inniheldur myndir.", + "more_than_one_video": "Ekki er hægt að hengja við fleiri en eitt myndskeið." } }, "edit_profile_failure": { - "title": "Edit Profile Error", - "message": "Cannot edit profile. Please try again." + "title": "Villa við breytingu á notandasniði", + "message": "Mistókst að breyta notandasniði. Endilega reyndu aftur." }, "sign_out": { "title": "Skrá út", @@ -37,11 +37,11 @@ "confirm": "Skrá út" }, "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" + "title": "Ertu alveg algjörlega viss um að þú viljir loka á allt %s? Í flestum tilfellum er vænlegra að nota færri en markvissari útilokanir eða að þagga niður tiltekna aðila. Þú munt ekki sjá neitt efni frá þessu léni og fylgjendur þínir frá þessu léni verða fjarlægðir.", + "block_entire_domain": "Útiloka lén" }, "save_photo_failure": { - "title": "Save Photo Failure", + "title": "Mistókst að vista mynd", "message": "Please enable the photo library access permission to save the photo." }, "delete_post": { @@ -50,7 +50,7 @@ }, "clean_cache": { "title": "Hreinsa skyndiminni", - "message": "Successfully cleaned %s cache." + "message": "Tókst að hreinsa %s skyndiminni." } }, "controls": { @@ -97,36 +97,36 @@ "home": "Heim", "search": "Leita", "notification": "Tilkynning", - "profile": "Profile" + "profile": "Notandasnið" }, "keyboard": { "common": { - "switch_to_tab": "Switch to %s", - "compose_new_post": "Compose New Post", + "switch_to_tab": "Skipta yfir í %s", + "compose_new_post": "Semja nýja færslu", "show_favorites": "Birta eftirlæti", "open_settings": "Opna stillingar" }, "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": "Fyrri færsla", + "next_status": "Næsta færsla", + "open_status": "Opna færslu", + "open_author_profile": "Opna notandasnið höfundar", + "open_reblogger_profile": "Opna notandasnið þess sem endurbirtir", + "reply_status": "Svara færslu", + "toggle_reblog": "Víxla endurbirtingu færslu af/á", + "toggle_favorite": "Víxla eftirlæti færslu af/á", + "toggle_content_warning": "Víxla af/á viðvörun vegna efnis", + "preview_image": "Forskoða mynd" }, "segmented_control": { - "previous_section": "Previous Section", - "next_section": "Next Section" + "previous_section": "Fyrri hluti", + "next_section": "Næsti hluti" } }, "status": { - "user_reblogged": "%s reblogged", - "user_replied_to": "Replied to %s", - "show_post": "Show Post", + "user_reblogged": "%s endurbirti", + "user_replied_to": "Svaraði %s", + "show_post": "Sýna færslu", "show_user_profile": "Birta notandasnið", "content_warning": "Viðvörun vegna efnis", "sensitive_content": "Viðkvæmt efni", @@ -152,8 +152,8 @@ "hide": "Fela", "show_image": "Sýna mynd", "show_gif": "Birta GIF", - "show_video_player": "Show video player", - "tap_then_hold_to_show_menu": "Tap then hold to show menu" + "show_video_player": "Sýna myndspilara", + "tap_then_hold_to_show_menu": "Ýttu og haltu til að sýna valmynd" }, "tag": { "url": "URL-slóð", @@ -164,10 +164,10 @@ "emoji": "Tjáningartákn" }, "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." + "unlisted": "Allir geta skoðað þessa færslu, en er ekki birt á opinberum tímalínum.", + "private": "Einungis fylgjendur þeirra geta séð þessa færslu.", + "private_from_me": "Einungis fylgjendur mínir geta séð þessa færslu.", + "direct": "Einungis notendur sem minnst er á geta séð þessa færslu." } }, "friendship": { @@ -186,8 +186,8 @@ "unmute": "Afþagga", "unmute_user": "Afþagga %s", "muted": "Þaggað", - "edit_info": "Edit Info", - "show_reblogs": "Show Reblogs", + "edit_info": "Breyta upplýsingum", + "show_reblogs": "Sýna endurbirtingar", "hide_reblogs": "Fela endurbirtingar" }, "timeline": { @@ -196,18 +196,18 @@ "now": "Núna" }, "loader": { - "load_missing_posts": "Load missing posts", - "loading_missing_posts": "Loading missing posts...", - "show_more_replies": "Show more replies" + "load_missing_posts": "Hlaða inn færslum sem vantar", + "loading_missing_posts": "Hleð inn færslum sem vantar...", + "show_more_replies": "Birta fleiri svör" }, "header": { - "no_status_found": "No Post Found", + "no_status_found": "Engar færslur fundust", "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." + "suspended_warning": "Þessi notandi hefur verið settur í bið.", + "user_suspended_warning": "Notandaaðgangurinn %s hefur verið settur í bið." } } } @@ -216,24 +216,24 @@ "welcome": { "slogan": "Samfélagsmiðlar\naftur í þínar hendur.", "get_started": "Komast í gang", - "log_in": "Log In" + "log_in": "Skrá inn" }, "login": { "title": "Velkomin aftur", - "subtitle": "Log you in on the server you created your account on.", + "subtitle": "Skráðu þig inn á netþjóninum þar sem þú útbjóst aðganginn þinn.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "Settu inn slóð eða leitaðu að þjóninum þínum" } }, "server_picker": { - "title": "Mastodon is made of users in different servers.", + "title": "Mastodon isamanstendur af notendum á mismunandi netþjónum.", "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { "all": "Allt", "all_accessiblity_description": "Flokkur: Allt", "academia": "akademískt", - "activism": "activism", + "activism": "aðgerðasinnar", "food": "matur", "furry": "loðið", "games": "leikir", @@ -254,24 +254,24 @@ "category": "FLOKKUR" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "Leitaðu að samfélögum eða settu inn slóð" }, "empty_state": { - "finding_servers": "Finding available servers...", - "bad_network": "Something went wrong while loading the data. Check your internet connection.", + "finding_servers": "Finn tiltæka netþjóna...", + "bad_network": "Eitthvað fór úrskeiðis við að hlaða inn gögnunum. Athugaðu nettenginguna þína.", "no_results": "Engar niðurstöður" } }, "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": "Við skulum koma þér í gang á %s", + "lets_get_you_set_up_on_domain": "Við skulum koma þér í gang á %s", "input": { "avatar": { "delete": "Eyða" }, "username": { "placeholder": "notandanafn", - "duplicate_prompt": "This username is taken." + "duplicate_prompt": "Þetta notandanafn er þegar í notkun." }, "display_name": { "placeholder": "birtingarnafn" @@ -303,27 +303,27 @@ "reason": "Ástæða" }, "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" + "blocked": "%s notar óleyfilega tölvupóstþjónustu", + "unreachable": "%s virðist ekki vera til", + "taken": "%s er þegar í notkun", + "reserved": "%s er frátekið stikkorð", + "accepted": "%s verður að samþykkja", + "blank": "%s ier nauðsynlegt", + "invalid": "%s er ógilt", + "too_long": "%s er of langt", + "too_short": "%s er of stutt", + "inclusion": "%s er ekki stutt gildi" }, "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)" + "username_invalid": "Notendanöfn geta einungis innihaldið bókstafi og undirstrikun", + "username_too_long": "Notandanafnið er of langt (má ekki vera lengra en 30 stafir)", + "email_invalid": "Þetta lítur ekki út eins og löglegt tölvupóstfang", + "password_too_short": "Lykilorð er of stutt (verður að hafa minnst 8 stafi)" } } }, "server_rules": { - "title": "Some ground rules.", + "title": "Nokkrar grunnreglur.", "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": "þjónustuskilmálar", @@ -515,7 +515,7 @@ "title": "Favorited By" }, "reblogged_by": { - "title": "Reblogged By" + "title": "Endurbirt af" }, "search": { "title": "Leita", From ffd1734a95d7a9e8cb3c2e158ba6274e7e194272 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 14:52:44 +0100 Subject: [PATCH 609/658] New translations ios-infoPlist.json (Icelandic) --- .../StringsConvertor/input/is.lproj/ios-infoPlist.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/is.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/is.lproj/ios-infoPlist.json index 4d46176af..24af18431 100644 --- a/Localization/StringsConvertor/input/is.lproj/ios-infoPlist.json +++ b/Localization/StringsConvertor/input/is.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", + "NSCameraUsageDescription": "Notað til að taka mynd fyrir stöðufærslu", + "NSPhotoLibraryAddUsageDescription": "Notað til að vista mynd inn í ljósmyndasafnið", "NewPostShortcutItemTitle": "Ný færsla", "SearchShortcutItemTitle": "Leita" } From ccaffe031e8a84b34f42da6776c8178f43c96915 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 16 Nov 2022 15:01:49 +0100 Subject: [PATCH 610/658] fix: Local user's domain is shown for remote users when sharing a post --- Mastodon/Protocol/Provider/DataSourceFacade+Status.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 332ed75e4..3d6c49ddd 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -84,7 +84,7 @@ extension DataSourceFacade { self.url = url self.metadata = LPLinkMetadata() metadata.url = url - metadata.title = "\(status.author.displayName) (@\(status.author.username)@\(status.author.domain))" + metadata.title = "\(status.author.displayName) (@\(status.author.acctWithDomain))" metadata.iconProvider = NSItemProvider(object: IconProvider(url: status.author.avatarImageURLWithFallback(domain: status.author.domain))) } From 8b7b6c4067acd23d21b894c76572a9632de8072c Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 16 Nov 2022 22:31:21 +0800 Subject: [PATCH 611/658] fix: configure header code not dispatch on UI thread issue --- .../MastodonUI/View/Content/StatusView+Configuration.swift | 1 + .../Sources/MastodonUI/View/Content/StatusView+ViewModel.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index 353dfc097..47e4f18ff 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -130,6 +130,7 @@ extension StatusView { authorization: authenticationBox.userAuthorization ).singleOutput() } + .receive(on: DispatchQueue.main) .sink { completion in // do nothing } receiveValue: { [weak self] response in diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index 416226cbb..1c09cd3f3 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -203,6 +203,7 @@ extension StatusView.ViewModel { statusView.headerInfoLabel.configure(content: info.header) statusView.setHeaderDisplay() case .reply(let info): + assert(Thread.isMainThread) statusView.headerIconImageView.image = UIImage(systemName: "arrowshape.turn.up.left.fill") statusView.headerInfoLabel.configure(content: info.header) statusView.setHeaderDisplay() From 101eaeecc1648ee57301a72802f039097df96195 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 16:08:11 +0100 Subject: [PATCH 612/658] New translations app.json (Kurmanji (Kurdish)) --- .../StringsConvertor/input/kmr.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/kmr.lproj/app.json b/Localization/StringsConvertor/input/kmr.lproj/app.json index 4a2bc3283..eb553885c 100644 --- a/Localization/StringsConvertor/input/kmr.lproj/app.json +++ b/Localization/StringsConvertor/input/kmr.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Wêne bikişîne", "save_photo": "Wêneyê tomar bike", "copy_photo": "Wêneyê jê bigire", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "Têkeve", + "sign_up": "Ajimêr biafirîne", "see_more": "Bêtir bibîne", "preview": "Pêşdîtin", "share": "Parve bike", @@ -219,15 +219,15 @@ "log_in": "Têkeve" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "Dîsa bi xêr hatî", + "subtitle": "Têketinê bike ser rajekarê ku te ajimêrê xwe tê de çê kiriye.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "Girêdanê têxe an jî li rajekarê xwe bigere" } }, "server_picker": { "title": "Mastodon ji bikarhênerên di civakên cuda de pêk tê.", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "Li gorî herêm, berjewendî, an jî armanceke giştî rajekarekê hilbijêre. Tu hîn jî dikarî li ser Mastodon bi her kesî re biaxivî, her rajekarê te çi be.", "button": { "category": { "all": "Hemû", @@ -254,7 +254,7 @@ "category": "BEŞ" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "Li civakan bigere an jî girêdanê têxe" }, "empty_state": { "finding_servers": "Peydakirina rajekarên berdest...", From 995761aac2d52b7cd71f6871297f1db9606ec1d9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 17:26:35 +0100 Subject: [PATCH 613/658] New translations app.json (French) --- .../StringsConvertor/input/fr.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/fr.lproj/app.json b/Localization/StringsConvertor/input/fr.lproj/app.json index effeafe12..fc8fd0e7a 100644 --- a/Localization/StringsConvertor/input/fr.lproj/app.json +++ b/Localization/StringsConvertor/input/fr.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Prendre une photo", "save_photo": "Enregistrer la photo", "copy_photo": "Copier la photo", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "Se connecter", + "sign_up": "Créer un compte", "see_more": "Voir plus", "preview": "Aperçu", "share": "Partager", @@ -219,15 +219,15 @@ "log_in": "Se connecter" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "Content de vous revoir", + "subtitle": "Connectez-vous sur le serveur sur lequel vous avez créé votre compte.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "Entrez l'URL ou recherchez votre serveur" } }, "server_picker": { "title": "Choisissez un serveur,\nn'importe quel serveur.", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "Choisissez un serveur basé sur votre région, vos intérêts ou un généraliste. Vous pouvez toujours discuter avec n'importe qui sur Mastodon, indépendamment de vos serveurs.", "button": { "category": { "all": "Tout", @@ -254,7 +254,7 @@ "category": "CATÉGORIE" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "Rechercher parmi les communautés ou renseigner une URL" }, "empty_state": { "finding_servers": "Recherche des serveurs disponibles...", From 26e9ed808f381e50553d56d24dd77bf364e6bcfe Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 17:26:36 +0100 Subject: [PATCH 614/658] New translations app.json (Icelandic) --- Localization/StringsConvertor/input/is.lproj/app.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/is.lproj/app.json b/Localization/StringsConvertor/input/is.lproj/app.json index 5635ae4f7..089332935 100644 --- a/Localization/StringsConvertor/input/is.lproj/app.json +++ b/Localization/StringsConvertor/input/is.lproj/app.json @@ -324,8 +324,8 @@ }, "server_rules": { "title": "Nokkrar grunnreglur.", - "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.", + "subtitle": "Þær eru settar og séð um að þeim sé fylgt af umsjónarmönnum %s.", + "prompt": "Með því að halda áfram samþykkir þú þjónustuskilmála og persónuverndarstefnu %s.", "terms_of_service": "þjónustuskilmálar", "privacy_policy": "persónuverndarstefna", "button": { @@ -333,7 +333,7 @@ } }, "confirm_email": { - "title": "One last thing.", + "title": "Eitt að lokum.", "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": { From 2d62cf1ffda3531a2436077b7ec9ca08373a8a71 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 18:23:58 +0100 Subject: [PATCH 615/658] New translations app.json (Portuguese, Brazilian) --- .../StringsConvertor/input/pt-BR.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/app.json b/Localization/StringsConvertor/input/pt-BR.lproj/app.json index 98bb960cc..60d858235 100644 --- a/Localization/StringsConvertor/input/pt-BR.lproj/app.json +++ b/Localization/StringsConvertor/input/pt-BR.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Tirar foto", "save_photo": "Salvar foto", "copy_photo": "Copiar foto", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "Entrar", + "sign_up": "Criar conta", "see_more": "Ver mais", "preview": "Pré-visualização", "share": "Compartilhar", @@ -219,15 +219,15 @@ "log_in": "Entrar" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "Bem-vindo de volta", + "subtitle": "Logado na instância em que você criou a sua conta.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "Insira a URL ou procure pela sua instância" } }, "server_picker": { "title": "Mastodon é feito de usuários em instâncias diferentes.", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "Escolha uma instância baseada na sua região, interesses, ou uma de uso geral. Você ainda poderá conversar com qualquer um no Mastodon, independente da instância.", "button": { "category": { "all": "Todos", @@ -254,7 +254,7 @@ "category": "Categoria" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "Procurar comunidades ou inserir URL" }, "empty_state": { "finding_servers": "Procurando instâncias disponíveis...", From 180dc4cb8080a7c06fd496ce0f35652572e4b39c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 18:23:59 +0100 Subject: [PATCH 616/658] New translations app.json (Icelandic) --- .../StringsConvertor/input/is.lproj/app.json | 184 +++++++++--------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/Localization/StringsConvertor/input/is.lproj/app.json b/Localization/StringsConvertor/input/is.lproj/app.json index 089332935..58d4e3307 100644 --- a/Localization/StringsConvertor/input/is.lproj/app.json +++ b/Localization/StringsConvertor/input/is.lproj/app.json @@ -334,20 +334,20 @@ }, "confirm_email": { "title": "Eitt að lokum.", - "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", + "subtitle": "Ýttu á tengilinn sem við sendum þér til að staðfesta tölvupóstfangið þitt.", + "tap_the_link_we_emailed_to_you_to_verify_your_account": "Ýttu á tengilinn sem við sendum þér til að staðfesta tölvupóstfangið þitt", "button": { - "open_email_app": "Open Email App", + "open_email_app": "Opna tölvupóstforrit", "resend": "Endursenda" }, "dont_receive_email": { "title": "Athugaðu tölvupóstinn þinn", - "description": "Check if your email address is correct as well as your junk folder if you haven’t.", + "description": "Athugaðu hvort tölvupóstfangið þitt sé rétt auk þess að skoða í ruslpóstmöppuna þína ef þú hefur ekki gert það.", "resend_email": "Endursenda tölvupóst" }, "open_email_app": { "title": "Athugaðu pósthólfið þitt.", - "description": "We just sent you an email. Check your junk folder if you haven’t.", + "description": "Við vorum að senda þér tölvupóst. Skoðaðu í ruslpóstmöppuna þína ef þú hefur ekki gert það.", "mail": "Tölvupóstur", "open_email_client": "Opna tölvupóstforrit" } @@ -360,14 +360,14 @@ "published": "Birt!", "Publishing": "Birti færslu...", "accessibility": { - "logo_label": "Logo Button", + "logo_label": "Hnappur með táknmerki", "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." + "title": "Finndu fólk til að fylgjast með", + "follow_explain": "Þegar þú fylgist með einhverjum, muntu sjá færslur frá viðkomandi á streyminu þínu." }, "compose": { "title": { @@ -379,19 +379,19 @@ "photo_library": "Myndasafn", "browse": "Flakka" }, - "content_input_placeholder": "Type or paste what’s on your mind", + "content_input_placeholder": "Skrifaðu eða límdu það sem þér liggur á hjarta", "compose_action": "Birta", - "replying_to_user": "replying to %s", + "replying_to_user": "svarar til @%s", "attachment": { "photo": "ljósmynd", "video": "myndskeið", "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_photo": "Lýstu myndinni fyrir sjónskerta...", + "description_video": "Lýstu myndskeiðinu fyrir sjónskerta...", "load_failed": "Hleðsla mistókst", "upload_failed": "Innsending mistókst", - "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", - "attachment_too_large": "Attachment too large", + "can_not_recognize_this_media_attachment": "Þekki ekki þetta myndviðhengi", + "attachment_too_large": "Viðhengi of stórt", "compressing_state": "Þjappa...", "server_processing_state": "Netþjónn er að vinna..." }, @@ -403,40 +403,40 @@ "one_day": "1 dagur", "three_days": "3 dagar", "seven_days": "7 dagar", - "option_number": "Option %ld", - "the_poll_is_invalid": "The poll is invalid", - "the_poll_has_empty_option": "The poll has empty option" + "option_number": "Valkostur %ld", + "the_poll_is_invalid": "Könnunin er ógild", + "the_poll_has_empty_option": "Könnunin er með auðan valkost" }, "content_warning": { - "placeholder": "Write an accurate warning here..." + "placeholder": "Skrifaðu nákvæma aðvörun hér..." }, "visibility": { "public": "Opinbert", "unlisted": "Óskráð", - "private": "Followers only", - "direct": "Only people I mention" + "private": "Einungis fylgjendur", + "direct": "Einungis fólk sem ég minnist á" }, "auto_complete": { - "space_to_add": "Space to add" + "space_to_add": "Bil sem á að bæta við" }, "accessibility": { "append_attachment": "Bæta við viðhengi", "append_poll": "Bæta við könnun", "remove_poll": "Fjarlægja könnun", "custom_emoji_picker": "Sérsniðið emoji-tánmyndaval", - "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" + "enable_content_warning": "Virkja viðvörun vegna efnis", + "disable_content_warning": "Gera viðvörun vegna efnis óvirka", + "post_visibility_menu": "Sýnileikavalmynd færslu", + "post_options": "Valkostir færslu", + "posting_as": "Birti sem %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" + "discard_post": "Henda færslu", + "publish_post": "Birta færslu", + "toggle_poll": "Víxla könnun af/á", + "toggle_content_warning": "Víxla af/á viðvörun vegna efnis", + "append_attachment_entry": "Bæta við viðhengi - %s", + "select_visibility_entry": "Veldu sýnileika - %s" } }, "profile": { @@ -455,8 +455,8 @@ "content": "Efni" }, "verified": { - "short": "Verified on %s", - "long": "Ownership of this link was checked on %s" + "short": "Sannreynt þann %s", + "long": "Eignarhald á þessum tengli var athugað þann %s" } }, "segmented_control": { @@ -468,51 +468,51 @@ }, "relationship_action_alert": { "confirm_mute_user": { - "title": "Mute Account", - "message": "Confirm to mute %s" + "title": "Þagga niður í aðgangi", + "message": "Staðfestu til að þagga niður í %s" }, "confirm_unmute_user": { - "title": "Unmute Account", - "message": "Confirm to unmute %s" + "title": "Hætta að þagga niður í aðgangi", + "message": "Staðfestu til hætta að að þagga niður í %s" }, "confirm_block_user": { - "title": "Block Account", - "message": "Confirm to block %s" + "title": "Útiloka notandaaðgang", + "message": "Staðfestu til að útiloka %s" }, "confirm_unblock_user": { - "title": "Unblock Account", - "message": "Confirm to unblock %s" + "title": "Aflétta útilokun aðgangs", + "message": "Staðfestu til að hætta að útiloka %s" }, "confirm_show_reblogs": { - "title": "Show Reblogs", - "message": "Confirm to show reblogs" + "title": "Sýna endurbirtingar", + "message": "Staðfestu til að sýna endurbirtingar" }, "confirm_hide_reblogs": { - "title": "Hide Reblogs", - "message": "Confirm to hide reblogs" + "title": "Fela endurbirtingar", + "message": "Staðfestu til að fela endurbirtingar" } }, "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": "Sýna auðkennismynd", + "edit_avatar_image": "Breyta auðkennismynd", + "show_banner_image": "Sýna myndborða", + "double_tap_to_open_the_list": "Tvípikkaðu til að opna listann" } }, "follower": { "title": "fylgjandi", - "footer": "Followers from other servers are not displayed." + "footer": "Fylgjendur af öðrum netþjónum birtast ekki." }, "following": { "title": "fylgist með", - "footer": "Follows from other servers are not displayed." + "footer": "Fylgjendur af öðrum netþjónum birtast ekki." }, "familiarFollowers": { - "title": "Followers you familiar", - "followed_by_names": "Followed by %s" + "title": "Fylgjendur sem þú kannast við", + "followed_by_names": "Fylgt af %s" }, "favorited_by": { - "title": "Favorited By" + "title": "Sett í eftirlæti af" }, "reblogged_by": { "title": "Endurbirt af" @@ -520,19 +520,19 @@ "search": { "title": "Leita", "search_bar": { - "placeholder": "Search hashtags and users", + "placeholder": "Leita að myllumerkjum og notendum", "cancel": "Hætta við" }, "recommend": { "button_text": "Sjá allt", "hash_tag": { - "title": "Trending on Mastodon", + "title": "Vinsælt á Mastodon", "description": "Hashtags that are getting quite a bit of attention", - "people_talking": "%s people are talking" + "people_talking": "%s manns eru að spjalla" }, "accounts": { - "title": "Accounts you might like", - "description": "You may like to follow these accounts", + "title": "Notandaaðgangar sem þú gætir haft áhuga á", + "description": "Þú gætir viljað fylgjast með þessum aðgöngum", "follow": "Fylgjast með" } }, @@ -556,29 +556,29 @@ "hashtags": "Myllumerki", "news": "Fréttir", "community": "Samfélag", - "for_you": "For You" + "for_you": "Fyrir þig" }, "intro": "These are the posts gaining traction in your corner of Mastodon." }, "favorite": { - "title": "Your Favorites" + "title": "Eftirlætin þín" }, "notification": { "title": { "Everything": "Allt", - "Mentions": "Mentions" + "Mentions": "Minnst á" }, "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": "fylgdi þér", + "favorited_your_post": "setti færslu frá þér í eftirlæti", + "reblogged_your_post": "endurbirti færsluna þína", + "mentioned_you": "minntist á þig", + "request_to_follow_you": "bað um að fylgjast með þér", + "poll_has_ended": "könnun er lokið" }, "keyobard": { - "show_everything": "Show Everything", - "show_mentions": "Show Mentions" + "show_everything": "Sýna allt", + "show_mentions": "Sýna þegar minnst er á" }, "follow_request": { "accept": "Samþykkja", @@ -609,40 +609,40 @@ }, "notifications": { "title": "Tilkynningar", - "favorites": "Favorites my post", - "follows": "Follows me", - "boosts": "Reblogs my post", - "mentions": "Mentions me", + "favorites": "Setur færsluna mína í eftirlæti", + "follows": "Fylgist með mér", + "boosts": "Endurbirtir færsluna mína", + "mentions": "Minnist á mig", "trigger": { "anyone": "hver sem er", "follower": "fylgjandi", "follow": "hverjum sá sem ég fylgi", "noone": "enginn", - "title": "Notify me when" + "title": "Tilkynna mér þegar" } }, "preference": { "title": "Kjörstillingar", - "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", + "true_black_dark_mode": "Sannur svartur dökkur hamur", + "disable_avatar_animation": "Gera auðkennismyndir með hreyfingu óvirkar", + "disable_emoji_animation": "Gera tjáningartákn með hreyfingu óvirkar", + "using_default_browser": "Nota sjálfgefinn vafra til að opna tengla", "open_links_in_mastodon": "Opna tengla í Mastodon" }, "boring_zone": { - "title": "The Boring Zone", + "title": "Óhressa svæðið", "account_settings": "Stillingar aðgangs", "terms": "Þjónustuskilmálar", "privacy": "Meðferð persónuupplýsinga" }, "spicy_zone": { - "title": "The Spicy Zone", + "title": "Kryddaða svæðið", "clear": "Hreinsa skyndiminni margmiðlunarefnis", "signout": "Skrá út" } }, "footer": { - "mastodon_description": "Mastodon is open source software. You can report issues on GitHub at %s (%s)" + "mastodon_description": "Mastodon er frjáls hugbúnaður með opinn grunnkóða. Þú getur tilkynnt vandamál í gegnum GitHub á %s (%s)" }, "keyboard": { "close_settings_window": "Loka stillingaglugga" @@ -662,12 +662,12 @@ "reported": "TILKYNNT", "step_one": { "step_1_of_4": "Skref 1 af 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", + "whats_wrong_with_this_post": "Hvað er athugavert við þessa færslu?", + "whats_wrong_with_this_account": "Hvað er athugavert við þennan notandaaðgang?", + "whats_wrong_with_this_username": "Hvað er athugavert við %s?", + "select_the_best_match": "Velja bestu samsvörun", + "i_dont_like_it": "Mér líkar það ekki", + "it_is_not_something_you_want_to_see": "Þetta er ekki eitthvað sem þið viljið sjá", "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", @@ -694,8 +694,8 @@ "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": "Hætta að fylgjast með", - "unfollowed": "Unfollowed", - "unfollow_user": "Unfollow %s", + "unfollowed": "Hætti að fylgjast með", + "unfollow_user": "Hætta að fylgjast með %s", "mute_user": "Þagga niður í %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": "Útiloka %s", @@ -716,7 +716,7 @@ "add_account": "Bæta við notandaaðgangi" }, "wizard": { - "new_in_mastodon": "New in Mastodon", + "new_in_mastodon": "Nýtt í Mastodon", "multiple_account_switch_intro_description": "Switch between multiple accounts by holding the profile button.", "accessibility_hint": "Double tap to dismiss this wizard" }, From df9598f49dc8b36d72ca4846b6d61bca022086f6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 18:24:01 +0100 Subject: [PATCH 617/658] New translations Localizable.stringsdict (Icelandic) --- .../input/is.lproj/Localizable.stringsdict | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/Localization/StringsConvertor/input/is.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/is.lproj/Localizable.stringsdict index aae13ffb9..2a821608d 100644 --- a/Localization/StringsConvertor/input/is.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/is.lproj/Localizable.stringsdict @@ -13,9 +13,9 @@ NSStringFormatValueTypeKey ld one - 1 unread notification + 1 ólesin tilkynning other - %ld unread notification + %ld ólesnar tilkynningar a11y.plural.count.input_limit_exceeds @@ -31,7 +31,7 @@ one 1 stafur other - %ld characters + %ld stafir a11y.plural.count.input_limit_remains @@ -47,13 +47,13 @@ one 1 stafur other - %ld characters + %ld stafir a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ left + %#@character_count@ eftir character_count NSStringFormatSpecTypeKey @@ -63,7 +63,7 @@ one 1 stafur other - %ld characters + %ld stafir plural.count.followed_by_and_mutual @@ -88,9 +88,9 @@ NSStringFormatValueTypeKey ld one - Followed by %1$@, and another mutual + Fylgt af %1$@ og öðrum sameiginlegum other - Followed by %1$@, and %ld mutuals + Fylgt af %1$@ og %ld sameiginlegum plural.count.metric_formatted.post @@ -106,7 +106,7 @@ one færsla other - posts + færslur plural.count.media @@ -122,7 +122,7 @@ one 1 gagnamiðill other - %ld media + %ld gagnamiðlar plural.count.post @@ -138,7 +138,7 @@ one 1 færsla other - %ld posts + %ld færslur plural.count.favorite @@ -154,7 +154,7 @@ one 1 eftirlæti other - %ld favorites + %ld eftirlæti plural.count.reblog @@ -170,7 +170,7 @@ one 1 endurbirting other - %ld reblogs + %ld endurbirtingar plural.count.reply @@ -186,7 +186,7 @@ one 1 svar other - %ld replies + %ld svör plural.count.vote @@ -202,7 +202,7 @@ one 1 atkvæði other - %ld votes + %ld atkvæði plural.count.voter @@ -218,7 +218,7 @@ one 1 kjósandi other - %ld voters + %ld kjósendur plural.people_talking @@ -232,9 +232,9 @@ NSStringFormatValueTypeKey ld one - 1 people talking + 1 aðili að spjalla other - %ld people talking + %ld aðilar að spjalla plural.count.following @@ -248,9 +248,9 @@ NSStringFormatValueTypeKey ld one - Eftirfarandi villur komu upp: + 1 fylgist með other - %ld following + %ld fylgjast með plural.count.follower @@ -264,9 +264,9 @@ NSStringFormatValueTypeKey ld one - 1 follower + 1 fylgjandi other - %ld followers + %ld fylgjendur date.year.left @@ -280,9 +280,9 @@ NSStringFormatValueTypeKey ld one - 1 year left + 1 ár eftir other - %ld years left + %ld ár eftir date.month.left @@ -296,9 +296,9 @@ NSStringFormatValueTypeKey ld one - 1 months left + 1 mánuður eftir other - %ld months left + %ld mánuðir eftir date.day.left @@ -312,9 +312,9 @@ NSStringFormatValueTypeKey ld one - 1 day left + 1 dagur eftir other - %ld days left + %ld dagar eftir date.hour.left @@ -328,9 +328,9 @@ NSStringFormatValueTypeKey ld one - 1 hour left + 1 klukkustund eftir other - %ld hours left + %ld klukkustundir eftir date.minute.left @@ -344,9 +344,9 @@ NSStringFormatValueTypeKey ld one - 1 minute left + 1 mínúta eftir other - %ld minutes left + %ld mínútur eftir date.second.left @@ -360,9 +360,9 @@ NSStringFormatValueTypeKey ld one - 1 second left + 1 sekúnda eftir other - %ld seconds left + %ld sekúndur eftir date.year.ago.abbr @@ -376,9 +376,9 @@ NSStringFormatValueTypeKey ld one - 1y ago + Fyrir 1 ári síðan other - %ldy ago + Fyrir %ld árum síðan date.month.ago.abbr @@ -392,9 +392,9 @@ NSStringFormatValueTypeKey ld one - 1M ago + Fyrir 1mín síðan other - %ldM ago + Fyrir %ldmín síðan date.day.ago.abbr @@ -408,9 +408,9 @@ NSStringFormatValueTypeKey ld one - 1d ago + Fyrir 1 degi síðan other - %ldd ago + Fyrir %ld dögum síðan date.hour.ago.abbr @@ -426,7 +426,7 @@ one 1klst síðan other - %ldh ago + %ldklst síðan date.minute.ago.abbr @@ -442,7 +442,7 @@ one 1m síðan other - %ldm ago + %ldm síðan date.second.ago.abbr @@ -458,7 +458,7 @@ one 1s síðan other - %lds ago + %lds síðan From 170e843d861c107e1d09973cbc41b8967c07e0ba Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 18:24:03 +0100 Subject: [PATCH 618/658] New translations Intents.strings (Icelandic) --- .../Intents/input/is.lproj/Intents.strings | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Localization/StringsConvertor/Intents/input/is.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/is.lproj/Intents.strings index dbe022814..af8070895 100644 --- a/Localization/StringsConvertor/Intents/input/is.lproj/Intents.strings +++ b/Localization/StringsConvertor/Intents/input/is.lproj/Intents.strings @@ -2,9 +2,9 @@ "751xkl" = "Efni texta"; -"CsR7G2" = "Post on Mastodon"; +"CsR7G2" = "Birta á Mastodon"; -"HZSGTr" = "What content to post?"; +"HZSGTr" = "Hvaða efni á að birta?"; "HdGikU" = "Birting færslu mistókst"; @@ -42,10 +42,10 @@ "k7dbKQ" = "Post was sent successfully."; -"oGiqmY-dYQ5NN" = "Just to confirm, you wanted ‘Public’?"; +"oGiqmY-dYQ5NN" = "Bara til að staðfesta, þú vildir 'Opinbert'?"; -"oGiqmY-ehFLjY" = "Just to confirm, you wanted ‘Followers Only’?"; +"oGiqmY-ehFLjY" = "Bara til að staðfesta, þú vildir ''Einungis fylgjendur'?"; "rM6dvp" = "URL-slóð"; -"ryJLwG" = "Post was sent successfully. "; +"ryJLwG" = "Það tókst að senda færsluna. "; From f2a8b29768c3102e8076f1969db1c8dae47bd4e4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 19:34:01 +0100 Subject: [PATCH 619/658] New translations app.json (Icelandic) --- .../StringsConvertor/input/is.lproj/app.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Localization/StringsConvertor/input/is.lproj/app.json b/Localization/StringsConvertor/input/is.lproj/app.json index 58d4e3307..ea97304ac 100644 --- a/Localization/StringsConvertor/input/is.lproj/app.json +++ b/Localization/StringsConvertor/input/is.lproj/app.json @@ -17,7 +17,7 @@ }, "discard_post_content": { "title": "Henda drögum", - "message": "Confirm to discard composed post content." + "message": "Staðfestu til að henda efni úr saminni færslu." }, "publish_post_failure": { "title": "Mistókst að birta", @@ -202,10 +202,10 @@ }, "header": { "no_status_found": "Engar færslur fundust", - "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.", + "blocking_warning": "Þú getur ekki séð snið þessa notanda\nfyrr en þú hættir að útiloka hann.\nSniðið þitt lítur svona út hjá honum.", + "user_blocking_warning": "Þú getur ekki séð sniðið hjá %s\nfyrr en þú hættir að útiloka hann.\nSniðið þitt lítur svona út hjá honum.", + "blocked_warning": "Þú getur ekki séð sniðið hjá þessum notanda\nfyrr en hann hættir að útiloka þig.", + "user_blocked_warning": "Þú getur ekki séð sniðið hjá %s\nfyrr en hann hættir að útiloka þig.", "suspended_warning": "Þessi notandi hefur verið settur í bið.", "user_suspended_warning": "Notandaaðgangurinn %s hefur verið settur í bið." } @@ -226,7 +226,7 @@ } }, "server_picker": { - "title": "Mastodon isamanstendur af notendum á mismunandi netþjónum.", + "title": "Mastodon samanstendur af notendum á mismunandi netþjónum.", "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", "button": { "category": { From 638f70b06dc8dca5665a455fcb35c535140fbafc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 19:34:03 +0100 Subject: [PATCH 620/658] New translations Localizable.stringsdict (Icelandic) --- .../StringsConvertor/input/is.lproj/Localizable.stringsdict | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/is.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/is.lproj/Localizable.stringsdict index 2a821608d..03b29f09b 100644 --- a/Localization/StringsConvertor/input/is.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/is.lproj/Localizable.stringsdict @@ -21,7 +21,7 @@ a11y.plural.count.input_limit_exceeds NSStringLocalizedFormatKey - Input limit exceeds %#@character_count@ + Inntak fer fram úr takmörkunum %#@character_count@ character_count NSStringFormatSpecTypeKey @@ -37,7 +37,7 @@ a11y.plural.count.input_limit_remains NSStringLocalizedFormatKey - Input limit remains %#@character_count@ + Inntakstakmörk haldast %#@character_count@ character_count NSStringFormatSpecTypeKey From 8664a121dc88a16d075f8ac2281f4034092206b1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 19:34:04 +0100 Subject: [PATCH 621/658] New translations Intents.strings (Icelandic) --- .../Intents/input/is.lproj/Intents.strings | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Localization/StringsConvertor/Intents/input/is.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/is.lproj/Intents.strings index af8070895..196c33e70 100644 --- a/Localization/StringsConvertor/Intents/input/is.lproj/Intents.strings +++ b/Localization/StringsConvertor/Intents/input/is.lproj/Intents.strings @@ -10,7 +10,7 @@ "KDNTJ4" = "Ástæða bilunar"; -"RHxKOw" = "Send Post with text content"; +"RHxKOw" = "Senda færslu með textaefni"; "RxSqsb" = "Færsla"; @@ -24,9 +24,9 @@ "Zo4jgJ" = "Sýnileiki færslu"; -"apSxMG-dYQ5NN" = "There are ${count} options matching ‘Public’."; +"apSxMG-dYQ5NN" = "Það eru ${count} valkostir sem samsvara ‘Opinbert’."; -"apSxMG-ehFLjY" = "There are ${count} options matching ‘Followers Only’."; +"apSxMG-ehFLjY" = "Það eru ${count} valkostir sem samsvara ‘Einungis fylgjendur’."; "ayoYEb-dYQ5NN" = "${content}, opinbert"; @@ -38,9 +38,9 @@ "ehFLjY" = "Einungis fylgjendur"; -"gfePDu" = "Posting failed. ${failureReason}"; +"gfePDu" = "Birting færslu mistókst. ${failureReason}"; -"k7dbKQ" = "Post was sent successfully."; +"k7dbKQ" = "Það tókst að senda færsluna."; "oGiqmY-dYQ5NN" = "Bara til að staðfesta, þú vildir 'Opinbert'?"; From 1a1004b524edbbbe64d52bc2c83a707b6408149e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 19:34:05 +0100 Subject: [PATCH 622/658] New translations Intents.stringsdict (Icelandic) --- .../Intents/input/is.lproj/Intents.stringsdict | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/Intents/input/is.lproj/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/is.lproj/Intents.stringsdict index 9212186f0..0c4754adf 100644 --- a/Localization/StringsConvertor/Intents/input/is.lproj/Intents.stringsdict +++ b/Localization/StringsConvertor/Intents/input/is.lproj/Intents.stringsdict @@ -5,7 +5,7 @@ There are ${count} options matching ‘${content}’. - 2 NSStringLocalizedFormatKey - There are %#@count_option@ matching ‘${content}’. + Það eru %#@count_option@ sem samsvara ‘${content}’. count_option NSStringFormatSpecTypeKey @@ -21,7 +21,7 @@ There are ${count} options matching ‘${visibility}’. NSStringLocalizedFormatKey - There are %#@count_option@ matching ‘${visibility}’. + Það eru %#@count_option@ sem samsvara ‘${visibility}’. count_option NSStringFormatSpecTypeKey From b54c290a4f1c8c862958ea920f7bb55661aeb08e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 20:35:45 +0100 Subject: [PATCH 623/658] New translations app.json (Icelandic) --- .../StringsConvertor/input/is.lproj/app.json | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/Localization/StringsConvertor/input/is.lproj/app.json b/Localization/StringsConvertor/input/is.lproj/app.json index ea97304ac..1cd72f356 100644 --- a/Localization/StringsConvertor/input/is.lproj/app.json +++ b/Localization/StringsConvertor/input/is.lproj/app.json @@ -42,7 +42,7 @@ }, "save_photo_failure": { "title": "Mistókst að vista mynd", - "message": "Please enable the photo library access permission to save the photo." + "message": "Virkjaðu heimild til aðgangs að ljósmyndasafninu til að vista myndina." }, "delete_post": { "title": "Eyða færslu", @@ -151,7 +151,7 @@ "menu": "Valmynd", "hide": "Fela", "show_image": "Sýna mynd", - "show_gif": "Birta GIF", + "show_gif": "Birta GIF-myndir", "show_video_player": "Sýna myndspilara", "tap_then_hold_to_show_menu": "Ýttu og haltu til að sýna valmynd" }, @@ -227,7 +227,7 @@ }, "server_picker": { "title": "Mastodon samanstendur af notendum á mismunandi netþjónum.", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "Veldu netþjón út frá svæðinu þínu, áhugamálum, nú eða einhvern almennan. Þú getur samt spjallað við hvern sem er á Mastodon, burtséð frá á hvaða netþjóni þú ert.", "button": { "category": { "all": "Allt", @@ -385,7 +385,7 @@ "attachment": { "photo": "ljósmynd", "video": "myndskeið", - "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.", + "attachment_broken": "Þetta %s er skemmt og því ekki\nhægt að senda inn á Mastodon.", "description_photo": "Lýstu myndinni fyrir sjónskerta...", "description_video": "Lýstu myndskeiðinu fyrir sjónskerta...", "load_failed": "Hleðsla mistókst", @@ -527,7 +527,7 @@ "button_text": "Sjá allt", "hash_tag": { "title": "Vinsælt á Mastodon", - "description": "Hashtags that are getting quite a bit of attention", + "description": "Myllumerki sem eru að fá þónokkra athygli", "people_talking": "%s manns eru að spjalla" }, "accounts": { @@ -653,12 +653,12 @@ "title": "Kæra %s", "step1": "Skref 1 af 2", "step2": "Skref 2 af 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.", + "content1": "Eru einhverjar færslur sem þú myndir vilja bæta við kæruna?", + "content2": "Er eitthvað fleira sem umsjónarmenn ættu að vita varðandi þessa kæru?", + "report_sent_title": "Takk fyrir tilkynninguna, við munum skoða málið.", "send": "Senda kæru", "skip_to_send": "Senda án athugasemdar", - "text_placeholder": "Type or paste additional comments", + "text_placeholder": "Skrifaðu eða límdu aðrar athugasemdir", "reported": "TILKYNNT", "step_one": { "step_1_of_4": "Skref 1 af 4", @@ -668,30 +668,30 @@ "select_the_best_match": "Velja bestu samsvörun", "i_dont_like_it": "Mér líkar það ekki", "it_is_not_something_you_want_to_see": "Þetta er ekki eitthvað sem þið viljið sjá", - "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" + "its_spam": "Þetta er ruslpóstur", + "malicious_links_fake_engagement_or_repetetive_replies": "Slæmir tenglar, fölsk samskipti eða endurtekin svör", + "it_violates_server_rules": "Það gengur þvert á reglur fyrir netþjóninn", + "you_are_aware_that_it_breaks_specific_rules": "Þið eruð meðvituð um að þetta brýtur sértækar reglur", + "its_something_else": "Það er eitthvað annað", + "the_issue_does_not_fit_into_other_categories": "Vandamálið fellur ekki í aðra flokka" }, "step_two": { "step_2_of_4": "Skref 2 af 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" + "which_rules_are_being_violated": "Hvaða reglur eru brotnar?", + "select_all_that_apply": "Veldu allt sem á við", + "i_just_don’t_like_it": "Mér bara líkar það ekki" }, "step_three": { "step_3_of_4": "Skref 3 af 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" + "are_there_any_posts_that_back_up_this_report": "Eru einhverjar færslur sem styðja þessa kæru?", + "select_all_that_apply": "Veldu allt sem á við" }, "step_four": { "step_4_of_4": "Skref 4 af 4", - "is_there_anything_else_we_should_know": "Is there anything else we should know?" + "is_there_anything_else_we_should_know": "Er eitthvað fleira sem við ættum að vita?" }, "step_final": { - "dont_want_to_see_this": "Don’t want to see this?", + "dont_want_to_see_this": "Langar þig ekki að sjá þetta?", "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": "Hætta að fylgjast með", "unfollowed": "Hætti að fylgjast með", @@ -700,7 +700,7 @@ "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": "Útiloka %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": "Á meðan við yfirförum þetta, geturðu tekið til aðgerða gegn %s" } }, "preview": { From b906d3e6e418ae075bd60118fd2bfe55633849cc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 20:35:48 +0100 Subject: [PATCH 624/658] New translations Intents.stringsdict (Icelandic) --- .../Intents/input/is.lproj/Intents.stringsdict | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/Intents/input/is.lproj/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/is.lproj/Intents.stringsdict index 0c4754adf..fe12c972a 100644 --- a/Localization/StringsConvertor/Intents/input/is.lproj/Intents.stringsdict +++ b/Localization/StringsConvertor/Intents/input/is.lproj/Intents.stringsdict @@ -15,7 +15,7 @@ one 1 valkostur other - %ld options + %ld valkostir There are ${count} options matching ‘${visibility}’. @@ -31,7 +31,7 @@ one 1 valkostur other - %ld options + %ld valkostir From f39c8245e1f3c8ef3333ad832c26663639f51db4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 21:34:39 +0100 Subject: [PATCH 625/658] New translations app.json (Chinese Traditional) --- .../input/zh-Hant.lproj/app.json | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json index c7607ba2a..e2dfaad64 100644 --- a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json +++ b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "拍攝照片", "save_photo": "儲存照片", "copy_photo": "複製照片", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "登入", + "sign_up": "新增帳號", "see_more": "檢視更多", "preview": "預覽", "share": "分享", @@ -219,15 +219,15 @@ "log_in": "登入" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "歡迎回來", + "subtitle": "登入您新增帳號之伺服器", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "請輸入 URL 或搜尋您的伺服器" } }, "server_picker": { "title": "Mastodon 由不同伺服器的使用者組成。", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "基於您的興趣、地區、或一般用途選定一個伺服器。您仍會與任何伺服器中的每個人連結。", "button": { "category": { "all": "全部", @@ -254,7 +254,7 @@ "category": "分類" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "搜尋社群或輸入 URL 地址" }, "empty_state": { "finding_servers": "尋找可用的伺服器...", @@ -390,10 +390,10 @@ "description_video": "為視障人士提供影片說明...", "load_failed": "讀取失敗", "upload_failed": "上傳失敗", - "can_not_recognize_this_media_attachment": "Can not recognize this media attachment", + "can_not_recognize_this_media_attachment": "無法識別此媒體附加檔案", "attachment_too_large": "附加檔案大小過大", - "compressing_state": "Compressing...", - "server_processing_state": "Server Processing..." + "compressing_state": "正在壓縮...", + "server_processing_state": "伺服器處理中..." }, "poll": { "duration_time": "持續時間:%s", @@ -404,8 +404,8 @@ "three_days": "三天", "seven_days": "七天", "option_number": "選項 %ld", - "the_poll_is_invalid": "The poll is invalid", - "the_poll_has_empty_option": "The poll has empty option" + "the_poll_is_invalid": "此投票是無效的", + "the_poll_has_empty_option": "此投票有空白選項" }, "content_warning": { "placeholder": "請於此處寫下精準的警告..." @@ -455,8 +455,8 @@ "content": "內容" }, "verified": { - "short": "Verified on %s", - "long": "Ownership of this link was checked on %s" + "short": "於 %s 上已驗證", + "long": "已在 %s 檢查此連結的擁有者權限" } }, "segmented_control": { From d17b9e4a1d44d1b06510dd1defe534b81e80d675 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 21:34:40 +0100 Subject: [PATCH 626/658] New translations app.json (German) --- .../StringsConvertor/input/de.lproj/app.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Localization/StringsConvertor/input/de.lproj/app.json b/Localization/StringsConvertor/input/de.lproj/app.json index bda8161a1..894a0245e 100644 --- a/Localization/StringsConvertor/input/de.lproj/app.json +++ b/Localization/StringsConvertor/input/de.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Foto aufnehmen", "save_photo": "Foto speichern", "copy_photo": "Foto kopieren", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "Anmelden", + "sign_up": "Konto erstellen", "see_more": "Mehr anzeigen", "preview": "Vorschau", "share": "Teilen", @@ -219,15 +219,15 @@ "log_in": "Anmelden" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "Willkommen zurück", + "subtitle": "Melden Sie sich auf dem Server an, auf dem Sie Ihr Konto erstellt haben.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "URL eingeben oder nach Server suchen" } }, "server_picker": { "title": "Wähle einen Server,\nbeliebigen Server.", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "Wähle einen Server basierend auf deinen Interessen oder deiner Region – oder einfach einen allgemeinen. Du kannst trotzdem mit jedem interagieren, egal auf welchem Server.", "button": { "category": { "all": "Alle", @@ -254,7 +254,7 @@ "category": "KATEGORIE" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "Suche nach einer Community oder gib eine URL ein" }, "empty_state": { "finding_servers": "Verfügbare Server werden gesucht...", @@ -455,7 +455,7 @@ "content": "Inhalt" }, "verified": { - "short": "Verified on %s", + "short": "Überprüft am %s", "long": "Besitz des Links wurde überprüft am %s" } }, From 8d4b50018b342a8898542756762de9434eadc130 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 21:34:41 +0100 Subject: [PATCH 627/658] New translations app.json (Thai) --- .../StringsConvertor/input/th.lproj/app.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/StringsConvertor/input/th.lproj/app.json b/Localization/StringsConvertor/input/th.lproj/app.json index 50f43375e..7b1a3d08e 100644 --- a/Localization/StringsConvertor/input/th.lproj/app.json +++ b/Localization/StringsConvertor/input/th.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "ถ่ายรูป", "save_photo": "บันทึกรูปภาพ", "copy_photo": "คัดลอกรูปภาพ", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "เข้าสู่ระบบ", + "sign_up": "สร้างบัญชี", "see_more": "ดูเพิ่มเติม", "preview": "แสดงตัวอย่าง", "share": "แบ่งปัน", @@ -219,15 +219,15 @@ "log_in": "เข้าสู่ระบบ" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "ยินดีต้อนรับกลับมา", + "subtitle": "นำคุณเข้าสู่ระบบในเซิร์ฟเวอร์ที่คุณได้สร้างบัญชีของคุณไว้ใน", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "ป้อน URL หรือค้นหาสำหรับเซิร์ฟเวอร์ของคุณ" } }, "server_picker": { "title": "Mastodon ประกอบด้วยผู้ใช้ในเซิร์ฟเวอร์ต่าง ๆ", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "เลือกเซิร์ฟเวอร์ตามภูมิภาค, ความสนใจ หรือวัตถุประสงค์ทั่วไปของคุณ คุณยังคงสามารถแชทกับใครก็ตามใน Mastodon โดยไม่คำนึงถึงเซิร์ฟเวอร์ของคุณ", "button": { "category": { "all": "ทั้งหมด", @@ -254,7 +254,7 @@ "category": "หมวดหมู่" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "ค้นหาชุมชนหรือป้อน URL" }, "empty_state": { "finding_servers": "กำลังค้นหาเซิร์ฟเวอร์ที่พร้อมใช้งาน...", From e7af8667e93202b5403e68e15e65b3d1b61bd29f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 16 Nov 2022 21:34:42 +0100 Subject: [PATCH 628/658] New translations app.json (Icelandic) --- .../StringsConvertor/input/is.lproj/app.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Localization/StringsConvertor/input/is.lproj/app.json b/Localization/StringsConvertor/input/is.lproj/app.json index 1cd72f356..191a670ed 100644 --- a/Localization/StringsConvertor/input/is.lproj/app.json +++ b/Localization/StringsConvertor/input/is.lproj/app.json @@ -361,7 +361,7 @@ "Publishing": "Birti færslu...", "accessibility": { "logo_label": "Hnappur með táknmerki", - "logo_hint": "Tap to scroll to top and tap again to previous location" + "logo_hint": "Ýttu til að skruna efst og ýttu aftur til að fara aftur á fyrri staðsetningu" } } }, @@ -558,7 +558,7 @@ "community": "Samfélag", "for_you": "Fyrir þig" }, - "intro": "These are the posts gaining traction in your corner of Mastodon." + "intro": "Þetta eru færslurnar sem eru að fá aukna athygli í þínu horni á Mastodon." }, "favorite": { "title": "Eftirlætin þín" @@ -692,14 +692,14 @@ }, "step_final": { "dont_want_to_see_this": "Langar þig ekki að sjá þetta?", - "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.", + "when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "Þegar þú sér eitthvað á Mastodon sem þér líkar ekki, þá geturðu fjarlægt viðkomandi eintakling úr umhverfinu þínu.", "unfollow": "Hætta að fylgjast með", "unfollowed": "Hætti að fylgjast með", "unfollow_user": "Hætta að fylgjast með %s", "mute_user": "Þagga niður í %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.", + "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "Þú munt ekki sjá færslur eða endurbirtingar frá viðkomandi á streyminu þínu. Viðkomandi aðilar munu ekki vita að þaggað hefur verið niður í þeim.", "block_user": "Útiloka %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": "Viðkomandi mun ekki lengur geta fylgst með eða séð færslurnar þínar, en munu sjá ef viðkomandi hefur verið útilokaður.", "while_we_review_this_you_can_take_action_against_user": "Á meðan við yfirförum þetta, geturðu tekið til aðgerða gegn %s" } }, @@ -711,14 +711,14 @@ } }, "account_list": { - "tab_bar_hint": "Current selected profile: %s. Double tap then hold to show account switcher", - "dismiss_account_switcher": "Dismiss Account Switcher", + "tab_bar_hint": "Fyrirliggjandi valið notandasnið: %s. Tvípikkaðu og haltu niðri til að birta aðgangaskiptinn", + "dismiss_account_switcher": "Loka aðgangaskipti", "add_account": "Bæta við notandaaðgangi" }, "wizard": { "new_in_mastodon": "Nýtt í Mastodon", - "multiple_account_switch_intro_description": "Switch between multiple accounts by holding the profile button.", - "accessibility_hint": "Double tap to dismiss this wizard" + "multiple_account_switch_intro_description": "Skiptu milli notandaaðganga með því að halda niðri notandasniðshnappnum.", + "accessibility_hint": "Tvípikkaðu til að loka þessum leiðarvísi" }, "bookmark": { "title": "Bókamerki" From bb72150aedb692aea4ac08f41a3fb157f432266f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 17 Nov 2022 05:09:48 +0100 Subject: [PATCH 629/658] New translations app.json (Galician) --- Localization/StringsConvertor/input/gl.lproj/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index f6e01439a..f87322abb 100644 --- a/Localization/StringsConvertor/input/gl.lproj/app.json +++ b/Localization/StringsConvertor/input/gl.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Facer foto", "save_photo": "Gardar foto", "copy_photo": "Copiar foto", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "Acceder", + "sign_up": "Crear conta", "see_more": "Ver máis", "preview": "Vista previa", "share": "Compartir", From 506cd46a8d0c09aa86a387affe68e7e6730bf556 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 17 Nov 2022 06:19:30 +0100 Subject: [PATCH 630/658] New translations app.json (Galician) --- Localization/StringsConvertor/input/gl.lproj/app.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index f87322abb..15c7a612a 100644 --- a/Localization/StringsConvertor/input/gl.lproj/app.json +++ b/Localization/StringsConvertor/input/gl.lproj/app.json @@ -219,15 +219,15 @@ "log_in": "Acceder" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "Benvido outra vez", + "subtitle": "Conéctate ao servidor no que creaches a conta.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "Escribe o URL ou busca o teu servidor" } }, "server_picker": { "title": "Mastodon fórmano as persoas das diferentes comunidades.", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "Elixe un servidor en función dos teus intereses, rexión o un de propósito xeral. Poderás conversar con calquera en Mastodon, independentemente do servidor que elixas.", "button": { "category": { "all": "Todo", @@ -254,7 +254,7 @@ "category": "CATEGORÍA" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "Busca comunidades ou escribe URL" }, "empty_state": { "finding_servers": "Buscando servidores dispoñibles...", From ce1f6b3e8f480e293b4a3430891dfe6b64fe5c60 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 17 Nov 2022 06:19:31 +0100 Subject: [PATCH 631/658] New translations app.json (Indonesian) --- .../StringsConvertor/input/id.lproj/app.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Localization/StringsConvertor/input/id.lproj/app.json b/Localization/StringsConvertor/input/id.lproj/app.json index dba617b0b..59ef1e0fe 100644 --- a/Localization/StringsConvertor/input/id.lproj/app.json +++ b/Localization/StringsConvertor/input/id.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Ambil Foto", "save_photo": "Simpan Foto", "copy_photo": "Salin Foto", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "Masuk", + "sign_up": "Buat akun", "see_more": "Lihat lebih banyak", "preview": "Pratinjau", "share": "Bagikan", @@ -219,15 +219,15 @@ "log_in": "Login" }, "login": { - "title": "Welcome back", - "subtitle": "Log you in on the server you created your account on.", + "title": "Selamat datang kembali", + "subtitle": "Masuklah pada server yang Anda buat di mana akun Anda berada.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "Masukkan URL atau pencarian di server Anda" } }, "server_picker": { "title": "Pilih sebuah server,\nserver manapun.", - "subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.", + "subtitle": "Pilih server berdasarkan agamamu, minat, atau subjek umum lainnya. Kamu masih bisa berkomunikasi dengan semua orang di Mastodon, tanpa memperdulikan server Anda.", "button": { "category": { "all": "Semua", @@ -254,7 +254,7 @@ "category": "KATEGORI" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "Cari komunitas atau masukkan URL" }, "empty_state": { "finding_servers": "Mencari server yang tersedia...", @@ -315,7 +315,7 @@ "inclusion": "%s is not a supported value" }, "special": { - "username_invalid": "Username must only contain alphanumeric characters and underscores", + "username_invalid": "Nama pengguna hanya berisi angka karakter dan garis bawah", "username_too_long": "Nama pengguna terlalu panjang (tidak boleh lebih dari 30 karakter)", "email_invalid": "Ini bukan alamat surel yang valid", "password_too_short": "Kata sandi terlalu pendek (harus sekurang-kurangnya 8 karakter)" From d8164c2bc9ced34893771956f528b6aa00cdabf4 Mon Sep 17 00:00:00 2001 From: Stefan Painhapp <4242986+painhapp@users.noreply.github.com> Date: Thu, 17 Nov 2022 14:26:40 +0900 Subject: [PATCH 632/658] Fix Video Player Constraints issue --- Gemfile.lock | 2 ++ .../Video/MediaPreviewVideoViewController.swift | 7 ++++++- .../MastodonCore/Service/API/APIService+APIError.swift | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index e0ed91c5b..15d02a8ec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,7 +100,9 @@ GEM PLATFORMS arm64-darwin-21 + arm64-darwin-22 x86_64-darwin-21 + x86_64-darwin-22 DEPENDENCIES arkana diff --git a/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift b/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift index 7bdbbfed2..cc559d8bb 100644 --- a/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift +++ b/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift @@ -45,7 +45,6 @@ extension MediaPreviewVideoViewController { playerViewController.view.widthAnchor.constraint(equalTo: view.widthAnchor), playerViewController.view.heightAnchor.constraint(equalTo: view.heightAnchor), ]) - playerViewController.didMove(toParent: self) if let contentOverlayView = playerViewController.contentOverlayView { previewImageView.translatesAutoresizingMaskIntoConstraints = false @@ -90,6 +89,12 @@ extension MediaPreviewVideoViewController { } } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + playerViewController.didMove(toParent: self) + } + } // MARK: - ShareActivityProvider diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+APIError.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+APIError.swift index 6fdb973da..52b6d4678 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+APIError.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+APIError.swift @@ -43,7 +43,7 @@ extension APIService.APIError: LocalizedError { public var errorDescription: String? { switch errorReason { - case .authenticationMissing: return "Fail to Authenticatie" + case .authenticationMissing: return "Fail to Authenticate" case .badRequest: return "Bad Request" case .badResponse: return "Bad Response" case .requestThrottle: return "Request Throttled" From cb5d027ce9428b47e5e37b7d9f7b69a09b64fd8f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 17 Nov 2022 08:19:27 +0100 Subject: [PATCH 633/658] New translations app.json (Kabyle) --- Localization/StringsConvertor/input/kab.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/kab.lproj/app.json b/Localization/StringsConvertor/input/kab.lproj/app.json index fb8add87b..9c4a533bb 100644 --- a/Localization/StringsConvertor/input/kab.lproj/app.json +++ b/Localization/StringsConvertor/input/kab.lproj/app.json @@ -74,8 +74,8 @@ "take_photo": "Ṭṭef tawlaft", "save_photo": "Sekles tawlaft", "copy_photo": "Nɣel tawlaft", - "sign_in": "Log in", - "sign_up": "Create account", + "sign_in": "Qqen", + "sign_up": "Snulfu-d amiḍan", "see_more": "Wali ugar", "preview": "Taskant", "share": "Bḍu", @@ -219,7 +219,7 @@ "log_in": "Qqen" }, "login": { - "title": "Welcome back", + "title": "Ansuf yess·ek·em", "subtitle": "Log you in on the server you created your account on.", "server_search_field": { "placeholder": "Enter URL or search for your server" @@ -254,7 +254,7 @@ "category": "TAGGAYT" }, "input": { - "search_servers_or_enter_url": "Search communities or enter URL" + "search_servers_or_enter_url": "Nadi timɣiwnin neɣ sekcem URL" }, "empty_state": { "finding_servers": "Tifin n yiqeddacen yellan...", From ab094199502f3a3674a654f35c6f2641cac1552d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 17 Nov 2022 08:19:28 +0100 Subject: [PATCH 634/658] New translations Localizable.stringsdict (Kabyle) --- .../StringsConvertor/input/kab.lproj/Localizable.stringsdict | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/kab.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/kab.lproj/Localizable.stringsdict index fd7cac605..dd66c70d3 100644 --- a/Localization/StringsConvertor/input/kab.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/kab.lproj/Localizable.stringsdict @@ -61,9 +61,9 @@ NSStringFormatValueTypeKey ld one - 1 character + 1 n usekkil other - %ld characters + %ld n isekkilen plural.count.followed_by_and_mutual From a0db294abeaa3cf3fc2fe82c7c92d0149aa3fd58 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 17 Nov 2022 09:45:18 +0100 Subject: [PATCH 635/658] New translations app.json (Kabyle) --- .../StringsConvertor/input/kab.lproj/app.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Localization/StringsConvertor/input/kab.lproj/app.json b/Localization/StringsConvertor/input/kab.lproj/app.json index 9c4a533bb..62cea8780 100644 --- a/Localization/StringsConvertor/input/kab.lproj/app.json +++ b/Localization/StringsConvertor/input/kab.lproj/app.json @@ -137,10 +137,10 @@ "closed": "Ifukk" }, "meta_entity": { - "url": "Link: %s", - "hashtag": "Hashtag: %s", - "mention": "Show Profile: %s", - "email": "Email address: %s" + "url": "Asaɣ : %s", + "hashtag": "Ahacṭag : %s", + "mention": "Sken-d amaɣnu : %s", + "email": "Tansa imayl : %s" }, "actions": { "reply": "Err", @@ -222,7 +222,7 @@ "title": "Ansuf yess·ek·em", "subtitle": "Log you in on the server you created your account on.", "server_search_field": { - "placeholder": "Enter URL or search for your server" + "placeholder": "Sekcem URL neɣ nadi ɣef uqeddac-ik·im" } }, "server_picker": { @@ -357,8 +357,8 @@ "navigation_bar_state": { "offline": "Beṛṛa n tuqqna", "new_posts": "Tissufaɣ timaynutin", - "published": "Yettwasuffeɣ!", - "Publishing": "Asuffeɣ tasuffeɣt...", + "published": "Tettwasuffeɣ!", + "Publishing": "Asuffeɣ n tasuffeɣt...", "accessibility": { "logo_label": "Taqeffalt n ulugu", "logo_hint": "Sit i wakken ad tɛeddiḍ i usawen, sit tikkelt-nniḍen i wakken ad tɛeddiḍ ɣer wadig yezrin" @@ -671,7 +671,7 @@ "its_spam": "D aspam", "malicious_links_fake_engagement_or_repetetive_replies": "Yir iseɣwan, yir agman d tririyin i d-yettuɣalen", "it_violates_server_rules": "Truẓi n yilugan n uqeddac", - "you_are_aware_that_it_breaks_specific_rules": "Teẓriḍ y•tettruẓu kra n yilugan", + "you_are_aware_that_it_breaks_specific_rules": "Teẓriḍ y·tettruẓu kra n yilugan", "its_something_else": "Ɣef ssebba-nniḍen", "the_issue_does_not_fit_into_other_categories": "Ugur ur yemṣada ara akk d taggayin-nniḍen" }, From 6366d26f4b9b37ceaeaef5a83eea3641380aeb3e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 17 Nov 2022 09:45:19 +0100 Subject: [PATCH 636/658] New translations Localizable.stringsdict (Kabyle) --- .../input/kab.lproj/Localizable.stringsdict | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Localization/StringsConvertor/input/kab.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/kab.lproj/Localizable.stringsdict index dd66c70d3..f18a906c0 100644 --- a/Localization/StringsConvertor/input/kab.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/kab.lproj/Localizable.stringsdict @@ -136,7 +136,7 @@ NSStringFormatValueTypeKey ld one - 1 tsuffeɣt + 1 n tsuffeɣt other %ld n tsuffaɣ @@ -312,9 +312,9 @@ NSStringFormatValueTypeKey ld one - Yeqqim-d 1 wass + Yeqqim-d 1 n wass other - Qqimen-d %ld wussan + Qqimen-d %ld n wussan date.hour.left @@ -328,9 +328,9 @@ NSStringFormatValueTypeKey ld one - Yeqqim-d 1 usrag + Yeqqim-d 1 n wesrag other - Qqimen-d %ld yisragen + Qqimen-d %ld n yisragen date.minute.left @@ -344,9 +344,9 @@ NSStringFormatValueTypeKey ld one - 1 tesdat i d-yeqqimen + 1 n tesdat i d-yeqqimen other - %ld tesdatin i d-yeqqimen + %ld n tesdatin i d-yeqqimen date.second.left @@ -360,9 +360,9 @@ NSStringFormatValueTypeKey ld one - 1 tasint i d-yeqqimen + 1 n tasint i d-yeqqimen other - %ld tsinin i d-yeqqimen + %ld n tasinin i d-yeqqimen date.year.ago.abbr @@ -376,9 +376,9 @@ NSStringFormatValueTypeKey ld one - 1 useggas aya + %ld n useggas aya other - %ld yiseggasen aya + %ld n yiseggasen aya date.month.ago.abbr @@ -392,9 +392,9 @@ NSStringFormatValueTypeKey ld one - 1 wayyur aya + %ld n wayyur aya other - %ld wayyuren aya + %ld n wayyuren aya date.day.ago.abbr @@ -408,9 +408,9 @@ NSStringFormatValueTypeKey ld one - 1 wass aya + %ld n wass aya other - %ld wussan aya + %ld n wussan aya date.hour.ago.abbr @@ -424,9 +424,9 @@ NSStringFormatValueTypeKey ld one - 1 usrag aya + %ld n wesrag aya other - %ld yisragen aya + %ld n yisragen aya date.minute.ago.abbr @@ -440,9 +440,9 @@ NSStringFormatValueTypeKey ld one - 1 tesdat aya + %ld n tesdat aya other - %ld tesdatin aya + %ld n tesdatin aya date.second.ago.abbr @@ -456,9 +456,9 @@ NSStringFormatValueTypeKey ld one - 1 tasint aya + %ld n tasint aya other - %ld tsinin aya + %ld n tasinin aya From 936b9b9ac6f100a16b1fd552c187d805d7cd7534 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 17 Nov 2022 09:45:21 +0100 Subject: [PATCH 637/658] New translations Intents.stringsdict (Kabyle) --- .../Intents/input/kab.lproj/Intents.stringsdict | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/Intents/input/kab.lproj/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/kab.lproj/Intents.stringsdict index a8aeeaaf1..6cead6f34 100644 --- a/Localization/StringsConvertor/Intents/input/kab.lproj/Intents.stringsdict +++ b/Localization/StringsConvertor/Intents/input/kab.lproj/Intents.stringsdict @@ -29,9 +29,9 @@ NSStringFormatValueTypeKey %ld one - 1 uɣewwaṛ + %ld n uɣewwaṛ other - %ld iɣewwaṛen + %ld n iɣewwaṛen From 60d9d3537d9a98ff7ed15dc34d8761231f76539e Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 17 Nov 2022 10:22:13 +0100 Subject: [PATCH 638/658] feat: Implement double-tap account switching on iPad --- Mastodon.xcodeproj/project.pbxproj | 4 ++ .../Extension/AppContext+NextAccount.swift | 47 +++++++++++++++++++ .../Root/ContentSplitViewController.swift | 13 +++++ .../Root/MainTab/MainTabBarController.swift | 25 +--------- .../Root/Sidebar/SidebarViewController.swift | 23 +++++++++ 5 files changed, 88 insertions(+), 24 deletions(-) create mode 100644 Mastodon/Extension/AppContext+NextAccount.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 5724244b6..d8020fb3f 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; }; 164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */ = {isa = PBXBuildFile; fileRef = 164F0EBB267D4FE400249499 /* BoopSound.caf */; }; 18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; }; + 2A82294F29262EE000D2A1F7 /* AppContext+NextAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */; }; 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; }; 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; }; 2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; }; @@ -518,6 +519,7 @@ 0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = ""; }; 164F0EBB267D4FE400249499 /* BoopSound.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = BoopSound.caf; sourceTree = ""; }; 1D6D967E77A5357E2C6110D9 /* Pods-Mastodon.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk - debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk - debug.xcconfig"; sourceTree = ""; }; + 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppContext+NextAccount.swift"; sourceTree = ""; }; 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = ""; }; 2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = ""; }; 2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; @@ -2222,6 +2224,7 @@ isa = PBXGroup; children = ( 2DF123A625C3B0210020F248 /* ActiveLabel.swift */, + 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */, 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */, 2D206B8525F5FB0900143C56 /* Double.swift */, DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */, @@ -3465,6 +3468,7 @@ DBFEEC99279BDCDE004F81DD /* ProfileAboutViewModel.swift in Sources */, 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */, DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */, + 2A82294F29262EE000D2A1F7 /* AppContext+NextAccount.swift in Sources */, DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */, DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */, 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */, diff --git a/Mastodon/Extension/AppContext+NextAccount.swift b/Mastodon/Extension/AppContext+NextAccount.swift new file mode 100644 index 000000000..a8eae1e13 --- /dev/null +++ b/Mastodon/Extension/AppContext+NextAccount.swift @@ -0,0 +1,47 @@ +// +// AppContext+NextAccount.swift +// Mastodon +// +// Created by Marcus Kida on 17.11.22. +// + +import CoreData +import CoreDataStack +import MastodonCore +import MastodonSDK + +extension AppContext { + func nextAccount(in authContext: AuthContext) -> MastodonAuthentication? { + let request = MastodonAuthentication.sortedFetchRequest + guard + let accounts = try? managedObjectContext.fetch(request), + accounts.count > 1 + else { return nil } + + let nextSelectedAccountIndex: Int? = { + for (index, account) in accounts.enumerated() { + guard account == authContext.mastodonAuthenticationBox + .authenticationRecord + .object(in: managedObjectContext) + else { continue } + + let nextAccountIndex = index + 1 + + if accounts.count > nextAccountIndex { + return nextAccountIndex + } else { + return 0 + } + } + + return nil + }() + + guard + let nextSelectedAccountIndex = nextSelectedAccountIndex, + accounts.count > nextSelectedAccountIndex + else { return nil } + + return accounts[nextSelectedAccountIndex] + } +} diff --git a/Mastodon/Scene/Root/ContentSplitViewController.swift b/Mastodon/Scene/Root/ContentSplitViewController.swift index 3f4758e8e..a10f0ed9b 100644 --- a/Mastodon/Scene/Root/ContentSplitViewController.swift +++ b/Mastodon/Scene/Root/ContentSplitViewController.swift @@ -124,4 +124,17 @@ extension ContentSplitViewController: SidebarViewControllerDelegate { accountListViewController.preferredContentSize = CGSize(width: 375, height: 400) } + func sidebarViewController(_ sidebarViewController: SidebarViewController, didDoubleTapItem item: SidebarViewModel.Item, sourceView: UIView) { + guard case let .tab(tab) = item, tab == .me else { return } + guard let authContext = authContext else { return } + assert(Thread.isMainThread) + + guard let nextAccount = context.nextAccount(in: authContext) else { return } + + Task { @MainActor in + let isActive = try await context.authenticationService.activeMastodonUser(domain: nextAccount.domain, userID: nextAccount.userID) + guard isActive else { return } + self.coordinator.setup() + } + } } diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 5f77d9584..5143e00c9 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -406,30 +406,7 @@ extension MainTabBarController { guard let authContext = authContext else { return } assert(Thread.isMainThread) - let request = MastodonAuthentication.sortedFetchRequest - guard let accounts = try? context.managedObjectContext.fetch(request), accounts.count > 1 else { return } - - let nextSelectedAccountIndex: Int? = { - for (index, account) in accounts.enumerated() { - guard account == authContext.mastodonAuthenticationBox - .authenticationRecord - .object(in: context.managedObjectContext) - else { continue } - - let nextAccountIndex = index + 1 - - if accounts.count > nextAccountIndex { - return nextAccountIndex - } else { - return 0 - } - } - - return nil - }() - - guard let nextSelectedAccountIndex = nextSelectedAccountIndex, accounts.count > nextSelectedAccountIndex else { return } - let nextAccount = accounts[nextSelectedAccountIndex] + guard let nextAccount = context.nextAccount(in: authContext) else { return } Task { @MainActor in let isActive = try await context.authenticationService.activeMastodonUser(domain: nextAccount.domain, userID: nextAccount.userID) diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift index 70e1239b6..c1faafca7 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift @@ -15,6 +15,7 @@ import MastodonUI protocol SidebarViewControllerDelegate: AnyObject { func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) func sidebarViewController(_ sidebarViewController: SidebarViewController, didLongPressItem item: SidebarViewModel.Item, sourceView: UIView) + func sidebarViewController(_ sidebarViewController: SidebarViewController, didDoubleTapItem item: SidebarViewModel.Item, sourceView: UIView) } final class SidebarViewController: UIViewController, NeedsDependency { @@ -143,6 +144,14 @@ extension SidebarViewController { let sidebarLongPressGestureRecognizer = UILongPressGestureRecognizer() sidebarLongPressGestureRecognizer.addTarget(self, action: #selector(SidebarViewController.sidebarLongPressGestureRecognizerHandler(_:))) collectionView.addGestureRecognizer(sidebarLongPressGestureRecognizer) + + let sidebarDoubleTapGestureRecognizer = UITapGestureRecognizer() + sidebarDoubleTapGestureRecognizer.numberOfTapsRequired = 2 + sidebarDoubleTapGestureRecognizer.addTarget(self, action: #selector(SidebarViewController.sidebarDoubleTapGestureRecognizerHandler(_:))) + sidebarDoubleTapGestureRecognizer.delaysTouchesBegan = true + sidebarDoubleTapGestureRecognizer.cancelsTouchesInView = true + collectionView.addGestureRecognizer(sidebarDoubleTapGestureRecognizer) + } private func setupBackground(theme: Theme) { @@ -176,6 +185,20 @@ extension SidebarViewController { guard let cell = collectionView.cellForItem(at: indexPath) else { return } delegate?.sidebarViewController(self, didLongPressItem: item, sourceView: cell) } + + @objc private func sidebarDoubleTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { + guard sender.state == .ended else { return } + + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + assert(sender.view === collectionView) + + let position = sender.location(in: collectionView) + guard let indexPath = collectionView.indexPathForItem(at: position) else { return } + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + guard let cell = collectionView.cellForItem(at: indexPath) else { return } + delegate?.sidebarViewController(self, didDoubleTapItem: item, sourceView: cell) + } } From be255ff53b504dbf2efd8f667f53dd1298d99b86 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 17 Nov 2022 20:41:55 +0800 Subject: [PATCH 639/658] fix: compose content could not post media only issue --- .../ComposeContentViewModel.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 06d84566b..61ccd9e3a 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -359,7 +359,7 @@ extension ComposeContentViewModel { 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 + .delay(for: 0.3, scheduler: DispatchQueue.main) // convert to outputs with delay. Due to @Published emit before changes .map { _ in attachmentViewModels.map { $0.uploadState } } } .switchToLatest() @@ -367,18 +367,20 @@ extension ComposeContentViewModel { guard outputs.allSatisfy({ $0 == .finish }) else { return false } return true } + .prepend(true) 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 + .delay(for: 0.3, 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 } } - + .prepend(true) + let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4( isComposeContentEmpty, isComposeContentValid, @@ -394,17 +396,15 @@ extension ComposeContentViewModel { } .eraseToAnyPublisher() - let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4( - isComposeContentEmpty, - isComposeContentValid, + let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest( $isPollActive, isPollOptionsAllValid ) - .map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollOptionsAllValid -> Bool in - if isPollComposing { - return isComposeContentValid && !isComposeContentEmpty && isPollOptionsAllValid + .map { isPollActive, isPollOptionsAllValid -> Bool in + if isPollActive { + return isPollOptionsAllValid } else { - return isComposeContentValid && !isComposeContentEmpty + return true } } .eraseToAnyPublisher() From d8cf4606e2a9e53a0b23ce4fbd128696175bc750 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 17 Nov 2022 15:33:21 +0100 Subject: [PATCH 640/658] fix: TabBar has noticeable delay when selecting next tab --- Mastodon/Scene/Root/MainTab/MainTabBarController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 5f77d9584..d2596cefb 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -329,6 +329,7 @@ extension MainTabBarController { let tabBarDoubleTapGestureRecognizer = UITapGestureRecognizer() tabBarDoubleTapGestureRecognizer.numberOfTapsRequired = 2 tabBarDoubleTapGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarDoubleTapGestureRecognizerHandler(_:))) + tabBarDoubleTapGestureRecognizer.delaysTouchesEnded = false tabBar.addGestureRecognizer(tabBarDoubleTapGestureRecognizer) self.isReadyForWizardAvatarButton = authContext != nil From e8e8a2a4c962e8a9fdab36eb9caf55323370d4af Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 17 Nov 2022 15:45:25 +0100 Subject: [PATCH 641/658] fix: Don't delay touches ended --- Mastodon/Scene/Root/Sidebar/SidebarViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift index c1faafca7..8a0ed763a 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift @@ -148,7 +148,7 @@ extension SidebarViewController { let sidebarDoubleTapGestureRecognizer = UITapGestureRecognizer() sidebarDoubleTapGestureRecognizer.numberOfTapsRequired = 2 sidebarDoubleTapGestureRecognizer.addTarget(self, action: #selector(SidebarViewController.sidebarDoubleTapGestureRecognizerHandler(_:))) - sidebarDoubleTapGestureRecognizer.delaysTouchesBegan = true + sidebarDoubleTapGestureRecognizer.delaysTouchesEnded = false sidebarDoubleTapGestureRecognizer.cancelsTouchesInView = true collectionView.addGestureRecognizer(sidebarDoubleTapGestureRecognizer) From 1ca3b66e404059d7a20b3251e83adaf36aff5824 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 18 Nov 2022 00:00:17 +0800 Subject: [PATCH 642/658] chore: code cleanup --- Mastodon/Scene/Thread/ThreadViewModel.swift | 126 -------------------- 1 file changed, 126 deletions(-) diff --git a/Mastodon/Scene/Thread/ThreadViewModel.swift b/Mastodon/Scene/Thread/ThreadViewModel.swift index 735d85cd4..c845ce64c 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel.swift @@ -28,12 +28,6 @@ class ThreadViewModel { let context: AppContext let authContext: AuthContext let mastodonStatusThreadViewModel: MastodonStatusThreadViewModel - -// let cellFrameCache = NSCache() -// let existStatusFetchedResultsController: StatusFetchedResultsController - -// weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate? -// weak var tableView: UITableView? // output var diffableDataSource: UITableViewDiffableDataSource? @@ -62,12 +56,6 @@ class ThreadViewModel { self.authContext = authContext self.root = optionalRoot self.mastodonStatusThreadViewModel = MastodonStatusThreadViewModel(context: context) -// self.rootNode = CurrentValueSubject(optionalStatus.flatMap { RootNode(domain: $0.domain, statusID: $0.id, replyToID: $0.inReplyToID) }) -// self.rootItem = CurrentValueSubject(optionalStatus.flatMap { Item.root(statusObjectID: $0.objectID, attribute: Item.StatusAttribute()) }) -// self.existStatusFetchedResultsController = StatusFetchedResultsController(managedObjectContext: context.managedObjectContext, domain: nil, additionalTweetPredicate: nil) -// self.navigationBarTitle = CurrentValueSubject( -// optionalStatus.flatMap { L10n.Scene.Thread.title($0.author.displayNameWithFallback) }) -// self.navigationBarTitleEmojiMeta = CurrentValueSubject(optionalStatus.flatMap { $0.author.emojis.asDictionary } ?? [:]) // end init ManagedObjectObserver.observe(context: context.managedObjectContext) @@ -85,24 +73,6 @@ class ThreadViewModel { }) .store(in: &disposeBag) -// // bind fetcher domain -// context.authenticationService.activeMastodonAuthenticationBox -// .receive(on: RunLoop.main) -// .sink { [weak self] box in -// guard let self = self else { return } -// self.existStatusFetchedResultsController.domain.value = box?.domain -// } -// .store(in: &disposeBag) -// -// rootNode -// .receive(on: DispatchQueue.main) -// .sink { [weak self] rootNode in -// guard let self = self else { return } -// guard rootNode != nil else { return } -// self.loadThreadStateMachine.enter(LoadThreadState.Loading.self) -// } -// .store(in: &disposeBag) - $root .receive(on: DispatchQueue.main) .sink { [weak self] root in @@ -125,102 +95,6 @@ class ThreadViewModel { }() } .store(in: &disposeBag) - -// rootItem -// .receive(on: DispatchQueue.main) -// .sink { [weak self] rootItem in -// guard let self = self else { return } -// guard case let .root(objectID, _) = rootItem else { return } -// self.context.managedObjectContext.perform { -// guard let status = self.context.managedObjectContext.object(with: objectID) as? Status else { -// return -// } -// self.rootItemObserver = ManagedObjectObserver.observe(object: status) -// .receive(on: DispatchQueue.main) -// .sink(receiveCompletion: { _ in -// // do nothing -// }, receiveValue: { [weak self] change in -// guard let self = self else { return } -// switch change.changeType { -// case .delete: -// self.rootItem.value = nil -// default: -// break -// } -// }) -// } -// } -// .store(in: &disposeBag) -// -// ancestorNodes -// .receive(on: DispatchQueue.main) -// .compactMap { [weak self] nodes -> [Item]? in -// guard let self = self else { return nil } -// guard !nodes.isEmpty else { return [] } -// -// guard let diffableDataSource = self.diffableDataSource else { return nil } -// let oldSnapshot = diffableDataSource.snapshot() -// var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:] -// for item in oldSnapshot.itemIdentifiers { -// switch item { -// case .reply(let objectID, let attribute): -// oldSnapshotAttributeDict[objectID] = attribute -// default: -// break -// } -// } -// -// var items: [Item] = [] -// for node in nodes { -// let attribute = oldSnapshotAttributeDict[node.statusObjectID] ?? Item.StatusAttribute() -// items.append(Item.reply(statusObjectID: node.statusObjectID, attribute: attribute)) -// } -// -// return items.reversed() -// } -// .assign(to: \.value, on: ancestorItems) -// .store(in: &disposeBag) -// -// descendantNodes -// .receive(on: DispatchQueue.main) -// .compactMap { [weak self] nodes -> [Item]? in -// guard let self = self else { return nil } -// guard !nodes.isEmpty else { return [] } -// -// guard let diffableDataSource = self.diffableDataSource else { return nil } -// let oldSnapshot = diffableDataSource.snapshot() -// var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:] -// for item in oldSnapshot.itemIdentifiers { -// switch item { -// case .leaf(let objectID, let attribute): -// oldSnapshotAttributeDict[objectID] = attribute -// default: -// break -// } -// } -// -// var items: [Item] = [] -// -// func buildThread(node: LeafNode) { -// let attribute = oldSnapshotAttributeDict[node.objectID] ?? Item.StatusAttribute() -// items.append(Item.leaf(statusObjectID: node.objectID, attribute: attribute)) -// // only expand the first child -// if let firstChild = node.children.first { -// if !node.isChildrenExpanded { -// items.append(Item.leafBottomLoader(statusObjectID: node.objectID)) -// } else { -// buildThread(node: firstChild) -// } -// } -// } -// -// for node in nodes { -// buildThread(node: node) -// } -// return items -// } -// .assign(to: \.value, on: descendantItems) -// .store(in: &disposeBag) } deinit { From b5d883865dafa7dbe37596e2ff8c79bca87e758e Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 18 Nov 2022 00:16:21 +0800 Subject: [PATCH 643/658] fix: prefer the original mode to top-two-tier tree mode --- .../ThreadViewModel+LoadThreadState.swift | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift index 050670be7..1c6c40190 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift @@ -92,19 +92,27 @@ extension ThreadViewModel.LoadThreadState { from: response.value.ancestors ) ) + // deprecated: Tree mode replies + // viewModel.mastodonStatusThreadViewModel.appendDescendant( + // domain: threadContext.domain, + // nodes: MastodonStatusThreadViewModel.Node.children( + // of: threadContext.statusID, + // from: response.value.descendants + // ) + // ) + + // new: the same order from API viewModel.mastodonStatusThreadViewModel.appendDescendant( domain: threadContext.domain, - nodes: MastodonStatusThreadViewModel.Node.children( - of: threadContext.statusID, - from: response.value.descendants - ) + nodes: response.value.descendants.map { status in + return .init(statusID: status.id, children: []) + } ) } catch { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch status context for \(threadContext.statusID) fail: \(error.localizedDescription)") await enter(state: Fail.self) } - - } + } // end Task } } From ae420fac79f0d7d01a2bc89f91e309af9bbcbd39 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 17 Nov 2022 20:57:36 +0100 Subject: [PATCH 644/658] New translations app.json (Czech) --- Localization/StringsConvertor/input/cs.lproj/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localization/StringsConvertor/input/cs.lproj/app.json b/Localization/StringsConvertor/input/cs.lproj/app.json index f94207c89..680eb01bb 100644 --- a/Localization/StringsConvertor/input/cs.lproj/app.json +++ b/Localization/StringsConvertor/input/cs.lproj/app.json @@ -427,8 +427,8 @@ "enable_content_warning": "Povolit upozornění na obsah", "disable_content_warning": "Vypnout upozornění na obsah", "post_visibility_menu": "Menu viditelnosti příspěvku", - "post_options": "Post Options", - "posting_as": "Posting as %s" + "post_options": "Možnosti příspěvku", + "posting_as": "Odesílání jako %s" }, "keyboard": { "discard_post": "Zahodit příspěvek", From df198d81ed857dbf60e255d205d972d5e08e4b1c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 18 Nov 2022 02:19:24 +0100 Subject: [PATCH 645/658] New translations app.json (Indonesian) --- .../StringsConvertor/input/id.lproj/app.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Localization/StringsConvertor/input/id.lproj/app.json b/Localization/StringsConvertor/input/id.lproj/app.json index 59ef1e0fe..4f2050792 100644 --- a/Localization/StringsConvertor/input/id.lproj/app.json +++ b/Localization/StringsConvertor/input/id.lproj/app.json @@ -372,12 +372,12 @@ "compose": { "title": { "new_post": "Postingan Baru", - "new_reply": "New Reply" + "new_reply": "Pesan Baru" }, "media_selection": { - "camera": "Take Photo", + "camera": "Ambil Foto", "photo_library": "Photo Library", - "browse": "Browse" + "browse": "Telusuri" }, "content_input_placeholder": "Ketik atau tempel apa yang Anda pada pikiran Anda", "compose_action": "Publikasikan", @@ -388,10 +388,10 @@ "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...", - "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", + "load_failed": "Gagal Memuat", + "upload_failed": "Gagal Mengunggah", + "can_not_recognize_this_media_attachment": "Tidak dapat mengenali lampiran media ini", + "attachment_too_large": "Lampiran terlalu besar", "compressing_state": "Compressing...", "server_processing_state": "Server Processing..." }, @@ -412,7 +412,7 @@ }, "visibility": { "public": "Publik", - "unlisted": "Unlisted", + "unlisted": "Tidak terdaftar", "private": "Pengikut saja", "direct": "Hanya orang yang saya sebut" }, From 3020f28ef42d204bf184f5426e6d0f1649d67388 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 18 Nov 2022 09:40:45 +0100 Subject: [PATCH 646/658] chore: Disable double-tap-to-switch-account --- .../Scene/Root/MainTab/MainTabBarController.swift | 11 ++++++----- .../Scene/Root/Sidebar/SidebarViewController.swift | 13 +++++++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index d31cc5c84..7f7918732 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -326,11 +326,12 @@ extension MainTabBarController { tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:))) tabBar.addGestureRecognizer(tabBarLongPressGestureRecognizer) - let tabBarDoubleTapGestureRecognizer = UITapGestureRecognizer() - tabBarDoubleTapGestureRecognizer.numberOfTapsRequired = 2 - tabBarDoubleTapGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarDoubleTapGestureRecognizerHandler(_:))) - tabBarDoubleTapGestureRecognizer.delaysTouchesEnded = false - tabBar.addGestureRecognizer(tabBarDoubleTapGestureRecognizer) + // todo: reconsider the "double tap to change account" feature +// let tabBarDoubleTapGestureRecognizer = UITapGestureRecognizer() +// tabBarDoubleTapGestureRecognizer.numberOfTapsRequired = 2 +// tabBarDoubleTapGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarDoubleTapGestureRecognizerHandler(_:))) +// tabBarDoubleTapGestureRecognizer.delaysTouchesEnded = false +// tabBar.addGestureRecognizer(tabBarDoubleTapGestureRecognizer) self.isReadyForWizardAvatarButton = authContext != nil diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift index 8a0ed763a..0bb88c7c6 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift @@ -145,12 +145,13 @@ extension SidebarViewController { sidebarLongPressGestureRecognizer.addTarget(self, action: #selector(SidebarViewController.sidebarLongPressGestureRecognizerHandler(_:))) collectionView.addGestureRecognizer(sidebarLongPressGestureRecognizer) - let sidebarDoubleTapGestureRecognizer = UITapGestureRecognizer() - sidebarDoubleTapGestureRecognizer.numberOfTapsRequired = 2 - sidebarDoubleTapGestureRecognizer.addTarget(self, action: #selector(SidebarViewController.sidebarDoubleTapGestureRecognizerHandler(_:))) - sidebarDoubleTapGestureRecognizer.delaysTouchesEnded = false - sidebarDoubleTapGestureRecognizer.cancelsTouchesInView = true - collectionView.addGestureRecognizer(sidebarDoubleTapGestureRecognizer) + // todo: reconsider the "double tap to change account" feature +// let sidebarDoubleTapGestureRecognizer = UITapGestureRecognizer() +// sidebarDoubleTapGestureRecognizer.numberOfTapsRequired = 2 +// sidebarDoubleTapGestureRecognizer.addTarget(self, action: #selector(SidebarViewController.sidebarDoubleTapGestureRecognizerHandler(_:))) +// sidebarDoubleTapGestureRecognizer.delaysTouchesEnded = false +// sidebarDoubleTapGestureRecognizer.cancelsTouchesInView = true +// collectionView.addGestureRecognizer(sidebarDoubleTapGestureRecognizer) } From 4cc734fac9b3790a08d1e643a3cfd43ababd8ac7 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 18 Nov 2022 09:56:02 +0100 Subject: [PATCH 647/658] chore: Link related GitHub issue to commented-out code --- Mastodon/Scene/Root/MainTab/MainTabBarController.swift | 2 +- Mastodon/Scene/Root/Sidebar/SidebarViewController.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 7f7918732..9728c551a 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -326,7 +326,7 @@ extension MainTabBarController { tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:))) tabBar.addGestureRecognizer(tabBarLongPressGestureRecognizer) - // todo: reconsider the "double tap to change account" feature + // todo: reconsider the "double tap to change account" feature -> https://github.com/mastodon/mastodon-ios/issues/628 // let tabBarDoubleTapGestureRecognizer = UITapGestureRecognizer() // tabBarDoubleTapGestureRecognizer.numberOfTapsRequired = 2 // tabBarDoubleTapGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarDoubleTapGestureRecognizerHandler(_:))) diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift index 0bb88c7c6..65d7d5520 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift @@ -145,7 +145,7 @@ extension SidebarViewController { sidebarLongPressGestureRecognizer.addTarget(self, action: #selector(SidebarViewController.sidebarLongPressGestureRecognizerHandler(_:))) collectionView.addGestureRecognizer(sidebarLongPressGestureRecognizer) - // todo: reconsider the "double tap to change account" feature + // todo: reconsider the "double tap to change account" feature -> https://github.com/mastodon/mastodon-ios/issues/628 // let sidebarDoubleTapGestureRecognizer = UITapGestureRecognizer() // sidebarDoubleTapGestureRecognizer.numberOfTapsRequired = 2 // sidebarDoubleTapGestureRecognizer.addTarget(self, action: #selector(SidebarViewController.sidebarDoubleTapGestureRecognizerHandler(_:))) From 8fd37c83625b2c0360116d3ef67535ba7af56933 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 15 Nov 2022 11:34:03 +0100 Subject: [PATCH 648/658] feat: Implement more obvious account switcher --- .../Root/MainTab/MainTabBarController.swift | 11 ++++++++++- .../Scene/Root/Sidebar/SidebarViewModel.swift | 4 ++++ .../Sidebar/View/SidebarListContentView.swift | 17 +++++++++++++++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 9728c551a..c8fa53d2f 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -43,6 +43,7 @@ class MainTabBarController: UITabBarController { static let avatarButtonSize = CGSize(width: 25, height: 25) let avatarButton = CircleAvatarButton() + let accountSwitcherChevron = UIImageView(image: UIImage(systemName: "chevron.up.chevron.down")) @Published var currentTab: Tab = .home @@ -506,13 +507,20 @@ extension MainTabBarController { } anchorImageView.alpha = 0 + accountSwitcherChevron.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(accountSwitcherChevron) + self.avatarButton.translatesAutoresizingMaskIntoConstraints = false view.addSubview(self.avatarButton) NSLayoutConstraint.activate([ - self.avatarButton.centerXAnchor.constraint(equalTo: anchorImageView.centerXAnchor), + self.avatarButton.centerXAnchor.constraint(equalTo: anchorImageView.centerXAnchor, constant: -16), self.avatarButton.centerYAnchor.constraint(equalTo: anchorImageView.centerYAnchor), self.avatarButton.widthAnchor.constraint(equalToConstant: MainTabBarController.avatarButtonSize.width).priority(.required - 1), self.avatarButton.heightAnchor.constraint(equalToConstant: MainTabBarController.avatarButtonSize.height).priority(.required - 1), + accountSwitcherChevron.widthAnchor.constraint(equalToConstant: 10), + accountSwitcherChevron.heightAnchor.constraint(equalToConstant: 18), + accountSwitcherChevron.leadingAnchor.constraint(equalTo: avatarButton.trailingAnchor, constant: 8), + accountSwitcherChevron.centerYAnchor.constraint(equalTo: avatarButton.centerYAnchor) ]) self.avatarButton.setContentHuggingPriority(.required - 1, for: .horizontal) self.avatarButton.setContentHuggingPriority(.required - 1, for: .vertical) @@ -520,6 +528,7 @@ extension MainTabBarController { } private func updateAvatarButtonAppearance() { + accountSwitcherChevron.tintColor = currentTab == .me ? .label : .secondaryLabel avatarButton.borderColor = currentTab == .me ? .label : .systemFill avatarButton.setNeedsLayout() } diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index c3f9e3e36..607637c7c 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -80,6 +80,7 @@ extension SidebarViewModel { }() cell.item = SidebarListContentView.Item( isActive: false, + showAccountSwitcher: item == .me, title: item.title, image: item.image, activeImage: item.selectedImage, @@ -157,6 +158,7 @@ extension SidebarViewModel { case .setting: let item = SidebarListContentView.Item( isActive: false, + showAccountSwitcher: false, title: L10n.Common.Controls.Actions.settings, image: Asset.ObjectsAndTools.gear.image.withRenderingMode(.alwaysTemplate), activeImage: Asset.ObjectsAndTools.gear.image.withRenderingMode(.alwaysTemplate), @@ -166,6 +168,7 @@ extension SidebarViewModel { case .compose: let item = SidebarListContentView.Item( isActive: false, + showAccountSwitcher: self.currentTab == .me, title: L10n.Common.Controls.Actions.compose, image: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate), activeImage: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate), @@ -213,6 +216,7 @@ extension SidebarViewModel { let item = SidebarListContentView.Item( isActive: false, + showAccountSwitcher: false, title: L10n.Common.Controls.Actions.compose, image: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate), activeImage: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate), diff --git a/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift b/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift index 515988405..0ef99c0a3 100644 --- a/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift +++ b/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift @@ -23,7 +23,8 @@ final class SidebarListContentView: UIView, UIContentView { button.borderColor = UIColor.label return button }() - + let accountSwitcherChevron = UIImageView(image: UIImage(systemName: "chevron.up.chevron.down")) + private var currentConfiguration: ContentConfiguration! var configuration: UIContentConfiguration { get { @@ -60,6 +61,9 @@ extension SidebarListContentView { imageView.widthAnchor.constraint(equalToConstant: 40).priority(.required - 1), imageView.heightAnchor.constraint(equalToConstant: 40).priority(.required - 1), ]) + + accountSwitcherChevron.translatesAutoresizingMaskIntoConstraints = false + addSubview(accountSwitcherChevron) avatarButton.translatesAutoresizingMaskIntoConstraints = false addSubview(avatarButton) @@ -68,6 +72,10 @@ extension SidebarListContentView { avatarButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), avatarButton.widthAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0).priority(.required - 2), avatarButton.heightAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 1.0).priority(.required - 2), + accountSwitcherChevron.widthAnchor.constraint(equalToConstant: 12), + accountSwitcherChevron.heightAnchor.constraint(equalToConstant: 22), + accountSwitcherChevron.leadingAnchor.constraint(equalTo: avatarButton.trailingAnchor, constant: 4), + accountSwitcherChevron.centerYAnchor.constraint(equalTo: avatarButton.centerYAnchor) ]) avatarButton.setContentHuggingPriority(.defaultLow - 10, for: .vertical) avatarButton.setContentHuggingPriority(.defaultLow - 10, for: .horizontal) @@ -96,6 +104,8 @@ extension SidebarListContentView { imageView.isHidden = item.imageURL != nil avatarButton.isHidden = item.imageURL == nil imageView.image = item.isActive ? item.activeImage.withRenderingMode(.alwaysTemplate) : item.image.withRenderingMode(.alwaysTemplate) + accountSwitcherChevron.isHidden = !item.showAccountSwitcher + accountSwitcherChevron.tintColor = item.isActive ? .label : .secondaryLabel avatarButton.avatarImageView.setImage( url: item.imageURL, placeholder: avatarButton.avatarImageView.image ?? .placeholder(color: .systemFill), // reuse to avoid blink @@ -112,7 +122,8 @@ extension SidebarListContentView { var isSelected: Bool = false var isHighlighted: Bool = false var isActive: Bool - + var showAccountSwitcher: Bool + // model let title: String var image: UIImage @@ -124,6 +135,7 @@ extension SidebarListContentView { return lhs.isSelected == rhs.isSelected && lhs.isHighlighted == rhs.isHighlighted && lhs.isActive == rhs.isActive + && lhs.showAccountSwitcher == rhs.showAccountSwitcher && lhs.title == rhs.title && lhs.image == rhs.image && lhs.activeImage == rhs.activeImage @@ -134,6 +146,7 @@ extension SidebarListContentView { hasher.combine(isSelected) hasher.combine(isHighlighted) hasher.combine(isActive) + hasher.combine(showAccountSwitcher) hasher.combine(title) hasher.combine(image) hasher.combine(activeImage) From 533e62609412f01da63b24a57c9147ffe980ba1d Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 15 Nov 2022 13:54:44 +0100 Subject: [PATCH 649/658] chore: Rename accountSwitcherChevron -> accountToggleIndicator --- .../Root/MainTab/MainTabBarController.swift | 16 ++++++++-------- .../Sidebar/View/SidebarListContentView.swift | 18 +++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index c8fa53d2f..b00bc0597 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -43,7 +43,7 @@ class MainTabBarController: UITabBarController { static let avatarButtonSize = CGSize(width: 25, height: 25) let avatarButton = CircleAvatarButton() - let accountSwitcherChevron = UIImageView(image: UIImage(systemName: "chevron.up.chevron.down")) + private let accountToggleIndicator = UIImageView(image: UIImage(systemName: "chevron.up.chevron.down")) @Published var currentTab: Tab = .home @@ -507,8 +507,8 @@ extension MainTabBarController { } anchorImageView.alpha = 0 - accountSwitcherChevron.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(accountSwitcherChevron) + accountToggleIndicator.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(accountToggleIndicator) self.avatarButton.translatesAutoresizingMaskIntoConstraints = false view.addSubview(self.avatarButton) @@ -517,10 +517,10 @@ extension MainTabBarController { self.avatarButton.centerYAnchor.constraint(equalTo: anchorImageView.centerYAnchor), self.avatarButton.widthAnchor.constraint(equalToConstant: MainTabBarController.avatarButtonSize.width).priority(.required - 1), self.avatarButton.heightAnchor.constraint(equalToConstant: MainTabBarController.avatarButtonSize.height).priority(.required - 1), - accountSwitcherChevron.widthAnchor.constraint(equalToConstant: 10), - accountSwitcherChevron.heightAnchor.constraint(equalToConstant: 18), - accountSwitcherChevron.leadingAnchor.constraint(equalTo: avatarButton.trailingAnchor, constant: 8), - accountSwitcherChevron.centerYAnchor.constraint(equalTo: avatarButton.centerYAnchor) + accountToggleIndicator.widthAnchor.constraint(equalToConstant: 10), + accountToggleIndicator.heightAnchor.constraint(equalToConstant: 18), + accountToggleIndicator.leadingAnchor.constraint(equalTo: avatarButton.trailingAnchor, constant: 8), + accountToggleIndicator.centerYAnchor.constraint(equalTo: avatarButton.centerYAnchor) ]) self.avatarButton.setContentHuggingPriority(.required - 1, for: .horizontal) self.avatarButton.setContentHuggingPriority(.required - 1, for: .vertical) @@ -528,7 +528,7 @@ extension MainTabBarController { } private func updateAvatarButtonAppearance() { - accountSwitcherChevron.tintColor = currentTab == .me ? .label : .secondaryLabel + accountToggleIndicator.tintColor = currentTab == .me ? .label : .secondaryLabel avatarButton.borderColor = currentTab == .me ? .label : .systemFill avatarButton.setNeedsLayout() } diff --git a/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift b/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift index 0ef99c0a3..2e8cbc2c1 100644 --- a/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift +++ b/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift @@ -23,7 +23,7 @@ final class SidebarListContentView: UIView, UIContentView { button.borderColor = UIColor.label return button }() - let accountSwitcherChevron = UIImageView(image: UIImage(systemName: "chevron.up.chevron.down")) + private let accountToggleIndicator = UIImageView(image: UIImage(systemName: "chevron.up.chevron.down")) private var currentConfiguration: ContentConfiguration! var configuration: UIContentConfiguration { @@ -62,8 +62,8 @@ extension SidebarListContentView { imageView.heightAnchor.constraint(equalToConstant: 40).priority(.required - 1), ]) - accountSwitcherChevron.translatesAutoresizingMaskIntoConstraints = false - addSubview(accountSwitcherChevron) + accountToggleIndicator.translatesAutoresizingMaskIntoConstraints = false + addSubview(accountToggleIndicator) avatarButton.translatesAutoresizingMaskIntoConstraints = false addSubview(avatarButton) @@ -72,10 +72,10 @@ extension SidebarListContentView { avatarButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), avatarButton.widthAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0).priority(.required - 2), avatarButton.heightAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 1.0).priority(.required - 2), - accountSwitcherChevron.widthAnchor.constraint(equalToConstant: 12), - accountSwitcherChevron.heightAnchor.constraint(equalToConstant: 22), - accountSwitcherChevron.leadingAnchor.constraint(equalTo: avatarButton.trailingAnchor, constant: 4), - accountSwitcherChevron.centerYAnchor.constraint(equalTo: avatarButton.centerYAnchor) + accountToggleIndicator.widthAnchor.constraint(equalToConstant: 12), + accountToggleIndicator.heightAnchor.constraint(equalToConstant: 22), + accountToggleIndicator.leadingAnchor.constraint(equalTo: avatarButton.trailingAnchor, constant: 4), + accountToggleIndicator.centerYAnchor.constraint(equalTo: avatarButton.centerYAnchor) ]) avatarButton.setContentHuggingPriority(.defaultLow - 10, for: .vertical) avatarButton.setContentHuggingPriority(.defaultLow - 10, for: .horizontal) @@ -104,8 +104,8 @@ extension SidebarListContentView { imageView.isHidden = item.imageURL != nil avatarButton.isHidden = item.imageURL == nil imageView.image = item.isActive ? item.activeImage.withRenderingMode(.alwaysTemplate) : item.image.withRenderingMode(.alwaysTemplate) - accountSwitcherChevron.isHidden = !item.showAccountSwitcher - accountSwitcherChevron.tintColor = item.isActive ? .label : .secondaryLabel + accountToggleIndicator.isHidden = !item.showAccountSwitcher + accountToggleIndicator.tintColor = item.isActive ? .label : .secondaryLabel avatarButton.avatarImageView.setImage( url: item.imageURL, placeholder: avatarButton.avatarImageView.image ?? .placeholder(color: .systemFill), // reuse to avoid blink From 3acdbcc2cef04d2f63e93bf511afb0e1dcc7fb9c Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 15 Nov 2022 14:07:22 +0100 Subject: [PATCH 650/658] chore: Make accessoryImageView more universal --- .../Scene/Root/Sidebar/SidebarViewModel.swift | 6 ++--- .../Sidebar/View/SidebarListContentView.swift | 25 ++++++++++--------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index 607637c7c..b6a30ee60 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -80,7 +80,7 @@ extension SidebarViewModel { }() cell.item = SidebarListContentView.Item( isActive: false, - showAccountSwitcher: item == .me, + accessoryImage: item == .me ? UIImage(systemName: "chevron.up.chevron.down") : nil, title: item.title, image: item.image, activeImage: item.selectedImage, @@ -158,7 +158,6 @@ extension SidebarViewModel { case .setting: let item = SidebarListContentView.Item( isActive: false, - showAccountSwitcher: false, title: L10n.Common.Controls.Actions.settings, image: Asset.ObjectsAndTools.gear.image.withRenderingMode(.alwaysTemplate), activeImage: Asset.ObjectsAndTools.gear.image.withRenderingMode(.alwaysTemplate), @@ -168,7 +167,7 @@ extension SidebarViewModel { case .compose: let item = SidebarListContentView.Item( isActive: false, - showAccountSwitcher: self.currentTab == .me, + accessoryImage: self.currentTab == .me ? UIImage(systemName: "chevron.up.chevron.down") : nil, title: L10n.Common.Controls.Actions.compose, image: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate), activeImage: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate), @@ -216,7 +215,6 @@ extension SidebarViewModel { let item = SidebarListContentView.Item( isActive: false, - showAccountSwitcher: false, title: L10n.Common.Controls.Actions.compose, image: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate), activeImage: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate), diff --git a/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift b/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift index 2e8cbc2c1..c2aa1a4f5 100644 --- a/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift +++ b/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift @@ -23,7 +23,7 @@ final class SidebarListContentView: UIView, UIContentView { button.borderColor = UIColor.label return button }() - private let accountToggleIndicator = UIImageView(image: UIImage(systemName: "chevron.up.chevron.down")) + private let accessoryImageView = UIImageView(image: nil) private var currentConfiguration: ContentConfiguration! var configuration: UIContentConfiguration { @@ -62,8 +62,8 @@ extension SidebarListContentView { imageView.heightAnchor.constraint(equalToConstant: 40).priority(.required - 1), ]) - accountToggleIndicator.translatesAutoresizingMaskIntoConstraints = false - addSubview(accountToggleIndicator) + accessoryImageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(accessoryImageView) avatarButton.translatesAutoresizingMaskIntoConstraints = false addSubview(avatarButton) @@ -72,10 +72,10 @@ extension SidebarListContentView { avatarButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), avatarButton.widthAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0).priority(.required - 2), avatarButton.heightAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 1.0).priority(.required - 2), - accountToggleIndicator.widthAnchor.constraint(equalToConstant: 12), - accountToggleIndicator.heightAnchor.constraint(equalToConstant: 22), - accountToggleIndicator.leadingAnchor.constraint(equalTo: avatarButton.trailingAnchor, constant: 4), - accountToggleIndicator.centerYAnchor.constraint(equalTo: avatarButton.centerYAnchor) + accessoryImageView.widthAnchor.constraint(equalToConstant: 12), + accessoryImageView.heightAnchor.constraint(equalToConstant: 22), + accessoryImageView.leadingAnchor.constraint(equalTo: avatarButton.trailingAnchor, constant: 4), + accessoryImageView.centerYAnchor.constraint(equalTo: avatarButton.centerYAnchor) ]) avatarButton.setContentHuggingPriority(.defaultLow - 10, for: .vertical) avatarButton.setContentHuggingPriority(.defaultLow - 10, for: .horizontal) @@ -104,8 +104,9 @@ extension SidebarListContentView { imageView.isHidden = item.imageURL != nil avatarButton.isHidden = item.imageURL == nil imageView.image = item.isActive ? item.activeImage.withRenderingMode(.alwaysTemplate) : item.image.withRenderingMode(.alwaysTemplate) - accountToggleIndicator.isHidden = !item.showAccountSwitcher - accountToggleIndicator.tintColor = item.isActive ? .label : .secondaryLabel + accessoryImageView.image = item.accessoryImage + accessoryImageView.isHidden = item.accessoryImage == nil + accessoryImageView.tintColor = item.isActive ? .label : .secondaryLabel avatarButton.avatarImageView.setImage( url: item.imageURL, placeholder: avatarButton.avatarImageView.image ?? .placeholder(color: .systemFill), // reuse to avoid blink @@ -122,7 +123,7 @@ extension SidebarListContentView { var isSelected: Bool = false var isHighlighted: Bool = false var isActive: Bool - var showAccountSwitcher: Bool + var accessoryImage: UIImage? = nil // model let title: String @@ -135,7 +136,7 @@ extension SidebarListContentView { return lhs.isSelected == rhs.isSelected && lhs.isHighlighted == rhs.isHighlighted && lhs.isActive == rhs.isActive - && lhs.showAccountSwitcher == rhs.showAccountSwitcher + && lhs.accessoryImage == rhs.accessoryImage && lhs.title == rhs.title && lhs.image == rhs.image && lhs.activeImage == rhs.activeImage @@ -146,7 +147,7 @@ extension SidebarListContentView { hasher.combine(isSelected) hasher.combine(isHighlighted) hasher.combine(isActive) - hasher.combine(showAccountSwitcher) + hasher.combine(accessoryImage) hasher.combine(title) hasher.combine(image) hasher.combine(activeImage) From 3a2c99c75af27b8133a5dde1b6edfcc51a26ca31 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 15 Nov 2022 14:09:51 +0100 Subject: [PATCH 651/658] chore: Move accountToggleIndicator to Constants --- Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index b6a30ee60..6ceb20527 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -16,6 +16,9 @@ import MastodonCore import MastodonLocalization final class SidebarViewModel { + private enum Constants { + static let accountToggleIndicator = UIImage(systemName: "chevron.up.chevron.down") + } var disposeBag = Set() @@ -80,7 +83,7 @@ extension SidebarViewModel { }() cell.item = SidebarListContentView.Item( isActive: false, - accessoryImage: item == .me ? UIImage(systemName: "chevron.up.chevron.down") : nil, + accessoryImage: item == .me ? Constants.accountToggleIndicator : nil, title: item.title, image: item.image, activeImage: item.selectedImage, @@ -167,7 +170,7 @@ extension SidebarViewModel { case .compose: let item = SidebarListContentView.Item( isActive: false, - accessoryImage: self.currentTab == .me ? UIImage(systemName: "chevron.up.chevron.down") : nil, + accessoryImage: self.currentTab == .me ? Constants.accountToggleIndicator : nil, title: L10n.Common.Controls.Actions.compose, image: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate), activeImage: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate), From be2583094dfab6f4b3f71bb99cbce0dcbb2b2d26 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 18 Nov 2022 10:10:54 +0100 Subject: [PATCH 652/658] chore: Move UIImage(systemName: "chevron.up.chevron.down") to UIImage Extension --- Mastodon.xcodeproj/project.pbxproj | 4 ++++ Mastodon/Extension/UIImage+SFSymbols.swift | 12 ++++++++++++ .../Root/MainTab/MainTabBarController.swift | 16 ++++++++-------- .../Scene/Root/Sidebar/SidebarViewModel.swift | 8 ++------ 4 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 Mastodon/Extension/UIImage+SFSymbols.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index d8020fb3f..0a48bf4ab 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */ = {isa = PBXBuildFile; fileRef = 164F0EBB267D4FE400249499 /* BoopSound.caf */; }; 18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; }; 2A82294F29262EE000D2A1F7 /* AppContext+NextAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */; }; + 2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */; }; 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; }; 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; }; 2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; }; @@ -520,6 +521,7 @@ 164F0EBB267D4FE400249499 /* BoopSound.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = BoopSound.caf; sourceTree = ""; }; 1D6D967E77A5357E2C6110D9 /* Pods-Mastodon.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk - debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk - debug.xcconfig"; sourceTree = ""; }; 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppContext+NextAccount.swift"; sourceTree = ""; }; + 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SFSymbols.swift"; sourceTree = ""; }; 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = ""; }; 2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = ""; }; 2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; @@ -2244,6 +2246,7 @@ 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */, 2D84350425FF858100EECE90 /* UIScrollView.swift */, DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */, + 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */, DBCC3B2F261440A50045B23D /* UITabBarController.swift */, DB73BF4827140BA300781945 /* UICollectionViewDiffableDataSource.swift */, DB73BF4A27140C0800781945 /* UITableViewDiffableDataSource.swift */, @@ -3273,6 +3276,7 @@ DB73BF4927140BA300781945 /* UICollectionViewDiffableDataSource.swift in Sources */, DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */, DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */, + 2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */, DB63F7492799126300455B82 /* FollowerListViewController+DataSourceProvider.swift in Sources */, 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */, 2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */, diff --git a/Mastodon/Extension/UIImage+SFSymbols.swift b/Mastodon/Extension/UIImage+SFSymbols.swift new file mode 100644 index 000000000..cf20055ea --- /dev/null +++ b/Mastodon/Extension/UIImage+SFSymbols.swift @@ -0,0 +1,12 @@ +// +// UIImage+SFSymbols.swift +// Mastodon +// +// Created by Marcus Kida on 18.11.22. +// + +import UIKit + +extension UIImage { + static let chevronUpChevronDown = UIImage(systemName: "chevron.up.chevron.down") +} diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index b00bc0597..5aef74e3c 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -43,7 +43,7 @@ class MainTabBarController: UITabBarController { static let avatarButtonSize = CGSize(width: 25, height: 25) let avatarButton = CircleAvatarButton() - private let accountToggleIndicator = UIImageView(image: UIImage(systemName: "chevron.up.chevron.down")) + let accountSwitcherChevron = UIImageView(image: .chevronUpChevronDown) @Published var currentTab: Tab = .home @@ -507,8 +507,8 @@ extension MainTabBarController { } anchorImageView.alpha = 0 - accountToggleIndicator.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(accountToggleIndicator) + accountSwitcherChevron.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(accountSwitcherChevron) self.avatarButton.translatesAutoresizingMaskIntoConstraints = false view.addSubview(self.avatarButton) @@ -517,10 +517,10 @@ extension MainTabBarController { self.avatarButton.centerYAnchor.constraint(equalTo: anchorImageView.centerYAnchor), self.avatarButton.widthAnchor.constraint(equalToConstant: MainTabBarController.avatarButtonSize.width).priority(.required - 1), self.avatarButton.heightAnchor.constraint(equalToConstant: MainTabBarController.avatarButtonSize.height).priority(.required - 1), - accountToggleIndicator.widthAnchor.constraint(equalToConstant: 10), - accountToggleIndicator.heightAnchor.constraint(equalToConstant: 18), - accountToggleIndicator.leadingAnchor.constraint(equalTo: avatarButton.trailingAnchor, constant: 8), - accountToggleIndicator.centerYAnchor.constraint(equalTo: avatarButton.centerYAnchor) + accountSwitcherChevron.widthAnchor.constraint(equalToConstant: 10), + accountSwitcherChevron.heightAnchor.constraint(equalToConstant: 18), + accountSwitcherChevron.leadingAnchor.constraint(equalTo: avatarButton.trailingAnchor, constant: 8), + accountSwitcherChevron.centerYAnchor.constraint(equalTo: avatarButton.centerYAnchor) ]) self.avatarButton.setContentHuggingPriority(.required - 1, for: .horizontal) self.avatarButton.setContentHuggingPriority(.required - 1, for: .vertical) @@ -528,7 +528,7 @@ extension MainTabBarController { } private func updateAvatarButtonAppearance() { - accountToggleIndicator.tintColor = currentTab == .me ? .label : .secondaryLabel + accountSwitcherChevron.tintColor = currentTab == .me ? .label : .secondaryLabel avatarButton.borderColor = currentTab == .me ? .label : .systemFill avatarButton.setNeedsLayout() } diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index 6ceb20527..2e1a0f361 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -16,10 +16,6 @@ import MastodonCore import MastodonLocalization final class SidebarViewModel { - private enum Constants { - static let accountToggleIndicator = UIImage(systemName: "chevron.up.chevron.down") - } - var disposeBag = Set() // input @@ -83,7 +79,7 @@ extension SidebarViewModel { }() cell.item = SidebarListContentView.Item( isActive: false, - accessoryImage: item == .me ? Constants.accountToggleIndicator : nil, + accessoryImage: item == .me ? .chevronUpChevronDown : nil, title: item.title, image: item.image, activeImage: item.selectedImage, @@ -170,7 +166,7 @@ extension SidebarViewModel { case .compose: let item = SidebarListContentView.Item( isActive: false, - accessoryImage: self.currentTab == .me ? Constants.accountToggleIndicator : nil, + accessoryImage: self.currentTab == .me ? .chevronUpChevronDown : nil, title: L10n.Common.Controls.Actions.compose, image: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate), activeImage: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate), From ad7904e863bb0ee4a39722a721e29e2ab4747b71 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 18 Nov 2022 10:54:40 +0100 Subject: [PATCH 653/658] fix: Status notification shows "mentioned you" text --- Mastodon/Extension/String.swift | 2 ++ .../Share/View/Content/NotificationView+Configuration.swift | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Mastodon/Extension/String.swift b/Mastodon/Extension/String.swift index bf70c8937..0aa8acb3a 100644 --- a/Mastodon/Extension/String.swift +++ b/Mastodon/Extension/String.swift @@ -15,6 +15,8 @@ extension String { mutating func capitalizeFirstLetter() { self = self.capitalizingFirstLetter() } + + static let empty = "" } extension String { diff --git a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift index 98d06fd92..c7dbdb33f 100644 --- a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift @@ -154,7 +154,7 @@ extension NotificationView { ) case .status: self.viewModel.notificationIndicatorText = createMetaContent( - text: L10n.Scene.Notification.NotificationDescription.mentionedYou, + text: .empty, emojis: emojis.asDictionary ) case ._other: From 31699ea9fcf6e812c750da7532e2eef10ab85835 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 18 Nov 2022 19:04:22 +0800 Subject: [PATCH 654/658] chore: update i18n resources --- .../Resources/ca.lproj/Localizable.strings | 14 ++--- .../Resources/cs.lproj/Localizable.strings | 4 +- .../Resources/de.lproj/Localizable.strings | 16 ++--- .../Resources/fr.lproj/Localizable.strings | 14 ++--- .../Resources/gd.lproj/Localizable.strings | 60 +++++++++---------- .../gd.lproj/Localizable.stringsdict | 10 ++-- .../Resources/gl.lproj/Localizable.strings | 14 ++--- .../Resources/kab.lproj/Localizable.strings | 24 ++++---- .../kab.lproj/Localizable.stringsdict | 46 +++++++------- .../Resources/ku.lproj/Localizable.strings | 14 ++--- .../Resources/th.lproj/Localizable.strings | 14 ++--- .../zh-Hans.lproj/Localizable.strings | 60 +++++++++---------- .../zh-Hans.lproj/Localizable.stringsdict | 4 +- .../zh-Hant.lproj/Localizable.strings | 28 ++++----- 14 files changed, 161 insertions(+), 161 deletions(-) diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings index d8fa6a739..43c63deda 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings @@ -55,8 +55,8 @@ Comprova la teva connexió a Internet."; "Common.Controls.Actions.Share" = "Comparteix"; "Common.Controls.Actions.SharePost" = "Compartir Publicació"; "Common.Controls.Actions.ShareUser" = "Compartir %@"; -"Common.Controls.Actions.SignIn" = "Log in"; -"Common.Controls.Actions.SignUp" = "Create account"; +"Common.Controls.Actions.SignIn" = "Iniciar sessió"; +"Common.Controls.Actions.SignUp" = "Crea un compte"; "Common.Controls.Actions.Skip" = "Omet"; "Common.Controls.Actions.TakePhoto" = "Fes una foto"; "Common.Controls.Actions.TryAgain" = "Torna a provar"; @@ -240,9 +240,9 @@ carregat a Mastodon."; "Scene.HomeTimeline.NavigationBarState.Published" = "Publicat!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "S'està publicant..."; "Scene.HomeTimeline.Title" = "Inici"; -"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; -"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; -"Scene.Login.Title" = "Welcome back"; +"Scene.Login.ServerSearchField.Placeholder" = "Insereix la URL o cerca el teu servidor"; +"Scene.Login.Subtitle" = "T'inicia sessió en el servidor on has creat el teu compte."; +"Scene.Login.Title" = "Ben tornat"; "Scene.Notification.FollowRequest.Accept" = "Acceptar"; "Scene.Notification.FollowRequest.Accepted" = "Acceptat"; "Scene.Notification.FollowRequest.Reject" = "rebutjar"; @@ -404,11 +404,11 @@ carregat a Mastodon."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Alguna cosa no ha anat bé en carregar les dades. Comprova la teva connexió a Internet."; "Scene.ServerPicker.EmptyState.FindingServers" = "Cercant els servidors disponibles..."; "Scene.ServerPicker.EmptyState.NoResults" = "No hi ha resultats"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Cerca comunitats o introdueix l'URL"; "Scene.ServerPicker.Label.Category" = "CATEGORIA"; "Scene.ServerPicker.Label.Language" = "LLENGUATGE"; "Scene.ServerPicker.Label.Users" = "USUARIS"; -"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; +"Scene.ServerPicker.Subtitle" = "Tria un servidor en funció de la teva regió, interessos o un de propòsit general. Seguiràs podent connectar amb tothom a Mastodon, independentment del servidor."; "Scene.ServerPicker.Title" = "Mastodon està fet d'usuaris en diferents comunitats."; "Scene.ServerRules.Button.Confirm" = "Hi estic d'acord"; "Scene.ServerRules.PrivacyPolicy" = "política de privadesa"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/cs.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/cs.lproj/Localizable.strings index fbf2ff2f4..22260e380 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/cs.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/cs.lproj/Localizable.strings @@ -157,9 +157,9 @@ Váš profil pro něj vypadá takto."; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Vlastní výběr Emoji"; "Scene.Compose.Accessibility.DisableContentWarning" = "Vypnout upozornění na obsah"; "Scene.Compose.Accessibility.EnableContentWarning" = "Povolit upozornění na obsah"; -"Scene.Compose.Accessibility.PostOptions" = "Post Options"; +"Scene.Compose.Accessibility.PostOptions" = "Možnosti příspěvku"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Menu viditelnosti příspěvku"; -"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; +"Scene.Compose.Accessibility.PostingAs" = "Odesílání jako %@"; "Scene.Compose.Accessibility.RemovePoll" = "Odstranit anketu"; "Scene.Compose.Attachment.AttachmentBroken" = "Tento %@ je poškozený a nemůže být nahrán do Mastodonu."; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings index 89e826349..29f3d16f5 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings @@ -55,8 +55,8 @@ Bitte überprüfe deine Internetverbindung."; "Common.Controls.Actions.Share" = "Teilen"; "Common.Controls.Actions.SharePost" = "Beitrag teilen"; "Common.Controls.Actions.ShareUser" = "%@ teilen"; -"Common.Controls.Actions.SignIn" = "Log in"; -"Common.Controls.Actions.SignUp" = "Create account"; +"Common.Controls.Actions.SignIn" = "Anmelden"; +"Common.Controls.Actions.SignUp" = "Konto erstellen"; "Common.Controls.Actions.Skip" = "Überspringen"; "Common.Controls.Actions.TakePhoto" = "Foto aufnehmen"; "Common.Controls.Actions.TryAgain" = "Nochmals versuchen"; @@ -240,9 +240,9 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.HomeTimeline.NavigationBarState.Published" = "Veröffentlicht!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Beitrag wird veröffentlicht..."; "Scene.HomeTimeline.Title" = "Startseite"; -"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; -"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; -"Scene.Login.Title" = "Welcome back"; +"Scene.Login.ServerSearchField.Placeholder" = "URL eingeben oder nach Server suchen"; +"Scene.Login.Subtitle" = "Melden Sie sich auf dem Server an, auf dem Sie Ihr Konto erstellt haben."; +"Scene.Login.Title" = "Willkommen zurück"; "Scene.Notification.FollowRequest.Accept" = "Akzeptieren"; "Scene.Notification.FollowRequest.Accepted" = "Akzeptiert"; "Scene.Notification.FollowRequest.Reject" = "Ablehnen"; @@ -271,7 +271,7 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.Profile.Fields.Placeholder.Content" = "Inhalt"; "Scene.Profile.Fields.Placeholder.Label" = "Bezeichnung"; "Scene.Profile.Fields.Verified.Long" = "Besitz des Links wurde überprüft am %@"; -"Scene.Profile.Fields.Verified.Short" = "Verified on %@"; +"Scene.Profile.Fields.Verified.Short" = "Überprüft am %@"; "Scene.Profile.Header.FollowsYou" = "Folgt dir"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Bestätige %@ zu blockieren"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Konto blockieren"; @@ -404,11 +404,11 @@ kann nicht auf Mastodon hochgeladen werden."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Beim Laden der Daten ist etwas schief gelaufen. Überprüfe deine Internetverbindung."; "Scene.ServerPicker.EmptyState.FindingServers" = "Verfügbare Server werden gesucht..."; "Scene.ServerPicker.EmptyState.NoResults" = "Keine Ergebnisse"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Suche nach einer Community oder gib eine URL ein"; "Scene.ServerPicker.Label.Category" = "KATEGORIE"; "Scene.ServerPicker.Label.Language" = "SPRACHE"; "Scene.ServerPicker.Label.Users" = "BENUTZER"; -"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; +"Scene.ServerPicker.Subtitle" = "Wähle einen Server basierend auf deinen Interessen oder deiner Region – oder einfach einen allgemeinen. Du kannst trotzdem mit jedem interagieren, egal auf welchem Server."; "Scene.ServerPicker.Title" = "Wähle einen Server, beliebigen Server."; "Scene.ServerRules.Button.Confirm" = "Ich stimme zu"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings index 59590f612..f393adec1 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings @@ -55,8 +55,8 @@ Veuillez vérifier votre accès à Internet."; "Common.Controls.Actions.Share" = "Partager"; "Common.Controls.Actions.SharePost" = "Partager la publication"; "Common.Controls.Actions.ShareUser" = "Partager %@"; -"Common.Controls.Actions.SignIn" = "Log in"; -"Common.Controls.Actions.SignUp" = "Create account"; +"Common.Controls.Actions.SignIn" = "Se connecter"; +"Common.Controls.Actions.SignUp" = "Créer un compte"; "Common.Controls.Actions.Skip" = "Passer"; "Common.Controls.Actions.TakePhoto" = "Prendre une photo"; "Common.Controls.Actions.TryAgain" = "Réessayer"; @@ -240,9 +240,9 @@ téléversé sur Mastodon."; "Scene.HomeTimeline.NavigationBarState.Published" = "Publié!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Publication en cours ..."; "Scene.HomeTimeline.Title" = "Accueil"; -"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; -"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; -"Scene.Login.Title" = "Welcome back"; +"Scene.Login.ServerSearchField.Placeholder" = "Entrez l'URL ou recherchez votre serveur"; +"Scene.Login.Subtitle" = "Connectez-vous sur le serveur sur lequel vous avez créé votre compte."; +"Scene.Login.Title" = "Content de vous revoir"; "Scene.Notification.FollowRequest.Accept" = "Accepter"; "Scene.Notification.FollowRequest.Accepted" = "Accepté"; "Scene.Notification.FollowRequest.Reject" = "rejeter"; @@ -404,11 +404,11 @@ téléversé sur Mastodon."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Une erreur s'est produite lors du chargement des données. Vérifiez votre connexion Internet."; "Scene.ServerPicker.EmptyState.FindingServers" = "Recherche des serveurs disponibles..."; "Scene.ServerPicker.EmptyState.NoResults" = "Aucun résultat"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Rechercher parmi les communautés ou renseigner une URL"; "Scene.ServerPicker.Label.Category" = "CATÉGORIE"; "Scene.ServerPicker.Label.Language" = "LANGUE"; "Scene.ServerPicker.Label.Users" = "UTILISATEUR·RICE·S"; -"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; +"Scene.ServerPicker.Subtitle" = "Choisissez un serveur basé sur votre région, vos intérêts ou un généraliste. Vous pouvez toujours discuter avec n'importe qui sur Mastodon, indépendamment de vos serveurs."; "Scene.ServerPicker.Title" = "Choisissez un serveur, n'importe quel serveur."; "Scene.ServerRules.Button.Confirm" = "J’accepte"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings index aea2902fc..6ccf6cf15 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings @@ -55,8 +55,8 @@ Thoir sùil air a’ cheangal agad ris an eadar-lìon."; "Common.Controls.Actions.Share" = "Co-roinn"; "Common.Controls.Actions.SharePost" = "Co-roinn am post"; "Common.Controls.Actions.ShareUser" = "Co-roinn %@"; -"Common.Controls.Actions.SignIn" = "Log in"; -"Common.Controls.Actions.SignUp" = "Create account"; +"Common.Controls.Actions.SignIn" = "Clàraich a-steach"; +"Common.Controls.Actions.SignUp" = "Cruthaich cunntas"; "Common.Controls.Actions.Skip" = "Leum thairis air"; "Common.Controls.Actions.TakePhoto" = "Tog dealbh"; "Common.Controls.Actions.TryAgain" = "Feuch ris a-rithist"; @@ -68,13 +68,13 @@ Thoir sùil air a’ cheangal agad ris an eadar-lìon."; "Common.Controls.Friendship.EditInfo" = "Deasaich"; "Common.Controls.Friendship.Follow" = "Lean"; "Common.Controls.Friendship.Following" = "’Ga leantainn"; -"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs"; +"Common.Controls.Friendship.HideReblogs" = "Falaich na brosnachaidhean"; "Common.Controls.Friendship.Mute" = "Mùch"; "Common.Controls.Friendship.MuteUser" = "Mùch %@"; "Common.Controls.Friendship.Muted" = "’Ga mhùchadh"; "Common.Controls.Friendship.Pending" = "Ri dhèiligeadh"; "Common.Controls.Friendship.Request" = "Iarrtas"; -"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; +"Common.Controls.Friendship.ShowReblogs" = "Seall na brosnachaidhean"; "Common.Controls.Friendship.Unblock" = "Dì-bhac"; "Common.Controls.Friendship.UnblockUser" = "Dì-bhac %@"; "Common.Controls.Friendship.Unmute" = "Dì-mhùch"; @@ -108,10 +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.MetaEntity.Email" = "Seòladh puist-d: %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Taga hais: %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Seall a’ phròifil: %@"; +"Common.Controls.Status.MetaEntity.Url" = "Ceangal: %@"; "Common.Controls.Status.Poll.Closed" = "Dùinte"; "Common.Controls.Status.Poll.Vote" = "Cuir bhòt"; "Common.Controls.Status.SensitiveContent" = "Susbaint fhrionasach"; @@ -155,27 +155,27 @@ Seo an coltas a th’ air a’ phròifil agad dhaibh-san."; "Scene.AccountList.AddAccount" = "Cuir cunntas ris"; "Scene.AccountList.DismissAccountSwitcher" = "Leig seachad taghadh a’ chunntais"; "Scene.AccountList.TabBarHint" = "A’ phròifil air a taghadh: %@. Thoir gnogag dhùbailte is cùm sìos a ghearradh leum gu cunntas eile"; -"Scene.Bookmark.Title" = "Bookmarks"; +"Scene.Bookmark.Title" = "Comharran-lìn"; "Scene.Compose.Accessibility.AppendAttachment" = "Cuir ceanglachan ris"; "Scene.Compose.Accessibility.AppendPoll" = "Cuir cunntas-bheachd ris"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Roghnaichear nan Emoji gnàthaichte"; "Scene.Compose.Accessibility.DisableContentWarning" = "Cuir rabhadh susbainte à comas"; "Scene.Compose.Accessibility.EnableContentWarning" = "Cuir rabhadh susbainte an comas"; -"Scene.Compose.Accessibility.PostOptions" = "Post Options"; +"Scene.Compose.Accessibility.PostOptions" = "Roghainnean postaidh"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "Clàr-taice faicsinneachd a’ phuist"; -"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; +"Scene.Compose.Accessibility.PostingAs" = "A’ postadh mar %@"; "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 recognize this media attachment"; -"Scene.Compose.Attachment.CompressingState" = "Compressing..."; +"Scene.Compose.Attachment.AttachmentTooLarge" = "Tha an ceanglachan ro mhòr"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Cha do dh’aithnich sinn an ceanglachan meadhain seo"; +"Scene.Compose.Attachment.CompressingState" = "’Ga dhùmhlachadh…"; "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.LoadFailed" = "Dh’fhàillig leis an luchdadh"; "Scene.Compose.Attachment.Photo" = "dealbh"; -"Scene.Compose.Attachment.ServerProcessingState" = "Server Processing..."; -"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; +"Scene.Compose.Attachment.ServerProcessingState" = "Tha am frithealaiche ’ga phròiseasadh…"; +"Scene.Compose.Attachment.UploadFailed" = "Dh’fhàillig leis an luchdadh suas"; "Scene.Compose.Attachment.Video" = "video"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Brùth air Space gus a chur ris"; "Scene.Compose.ComposeAction" = "Foillsich"; @@ -196,8 +196,8 @@ a luchdadh suas gu Mastodon."; "Scene.Compose.Poll.OptionNumber" = "Roghainn %ld"; "Scene.Compose.Poll.SevenDays" = "Seachdain"; "Scene.Compose.Poll.SixHours" = "6 uairean a thìde"; -"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; -"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "Tha roghainn fhalamh aig a’ chunntas-bheachd"; +"Scene.Compose.Poll.ThePollIsInvalid" = "Tha an cunntas-bheachd mì-dhligheach"; "Scene.Compose.Poll.ThirtyMinutes" = "Leth-uair a thìde"; "Scene.Compose.Poll.ThreeDays" = "3 làithean"; "Scene.Compose.ReplyingToUser" = "a’ freagairt gu %@"; @@ -240,9 +240,9 @@ a luchdadh suas gu Mastodon."; "Scene.HomeTimeline.NavigationBarState.Published" = "Chaidh fhoillseachadh!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "A’ foillseachadh a’ phuist…"; "Scene.HomeTimeline.Title" = "Dachaigh"; -"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; -"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; -"Scene.Login.Title" = "Welcome back"; +"Scene.Login.ServerSearchField.Placeholder" = "Cuir a-steach URL an fhrithealaiche agad"; +"Scene.Login.Subtitle" = "Clàraich a-steach air an fhrithealaiche far an do chruthaich thu an cunntas agad."; +"Scene.Login.Title" = "Fàilte air ais"; "Scene.Notification.FollowRequest.Accept" = "Gabh ris"; "Scene.Notification.FollowRequest.Accepted" = "Air a ghabhail ris"; "Scene.Notification.FollowRequest.Reject" = "diùlt"; @@ -270,17 +270,17 @@ a luchdadh suas gu Mastodon."; "Scene.Profile.Fields.AddRow" = "Cuir ràgh ris"; "Scene.Profile.Fields.Placeholder.Content" = "Susbaint"; "Scene.Profile.Fields.Placeholder.Label" = "Leubail"; -"Scene.Profile.Fields.Verified.Long" = "Ownership of this link was checked on %@"; -"Scene.Profile.Fields.Verified.Short" = "Verified on %@"; +"Scene.Profile.Fields.Verified.Long" = "Chaidh dearbhadh cò leis a tha an ceangal seo %@"; +"Scene.Profile.Fields.Verified.Short" = "Air a dhearbhadh %@"; "Scene.Profile.Header.FollowsYou" = "’Gad leantainn"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Dearbh bacadh %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Bac an cunntas"; -"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; -"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Dearbh falach nam brosnachaidhean"; +"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Falaich na brosnachaidhean"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Dearbh mùchadh %@"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Mùch an cunntas"; -"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs"; -"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Dearbh sealladh nam brosnachaidhean"; +"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Seall na brosnachaidhean"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Dearbh dì-bhacadh %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Dì-bhac an cunntas"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Dearbh dì-mhùchadh %@"; @@ -404,11 +404,11 @@ a luchdadh suas gu Mastodon."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Chaidh rudeigin ceàrr le luchdadh an dàta. Thoir sùil air a’ cheangal agad ris an eadar-lìon."; "Scene.ServerPicker.EmptyState.FindingServers" = "A’ lorg nam frithealaichean ri am faighinn…"; "Scene.ServerPicker.EmptyState.NoResults" = "Gun toradh"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Lorg coimhearsnachd no cuir a-steach URL"; "Scene.ServerPicker.Label.Category" = "ROINN-SEÒRSA"; "Scene.ServerPicker.Label.Language" = "CÀNAN"; "Scene.ServerPicker.Label.Users" = "CLEACHDAICHEAN"; -"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; +"Scene.ServerPicker.Subtitle" = "Tagh frithealaiche stèidhichte air na sgìre agad, d’ ùidhean, air far a bheil thu no fear coitcheann. ’S urrainn dhut fhathast conaltradh le duine sam bith air Mastodon ge b’ e na frithealaichean agaibh-se."; "Scene.ServerPicker.Title" = "Tha cleachdaichean Mhastodon air iomadh frithealaiche eadar-dhealaichte."; "Scene.ServerRules.Button.Confirm" = "Gabhaidh mi ris"; "Scene.ServerRules.PrivacyPolicy" = "poileasaidh prìobhaideachd"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.stringsdict index 45ba1e156..9b3e69ea7 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.stringsdict @@ -65,7 +65,7 @@ a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ left + %#@character_count@ air fhàgail character_count NSStringFormatSpecTypeKey @@ -73,13 +73,13 @@ NSStringFormatValueTypeKey ld one - 1 character + %ld charactar two - %ld characters + %ld charactar few - %ld characters + %ld caractaran other - %ld characters + %ld caractar plural.count.followed_by_and_mutual diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings index b388ad5c3..f9e1c6589 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings @@ -55,8 +55,8 @@ Comproba a conexión a internet."; "Common.Controls.Actions.Share" = "Compartir"; "Common.Controls.Actions.SharePost" = "Compartir publicación"; "Common.Controls.Actions.ShareUser" = "Compartir %@"; -"Common.Controls.Actions.SignIn" = "Log in"; -"Common.Controls.Actions.SignUp" = "Create account"; +"Common.Controls.Actions.SignIn" = "Acceder"; +"Common.Controls.Actions.SignUp" = "Crear conta"; "Common.Controls.Actions.Skip" = "Omitir"; "Common.Controls.Actions.TakePhoto" = "Facer foto"; "Common.Controls.Actions.TryAgain" = "Intentar de novo"; @@ -240,9 +240,9 @@ ser subido a Mastodon."; "Scene.HomeTimeline.NavigationBarState.Published" = "Publicado!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Publicando..."; "Scene.HomeTimeline.Title" = "Inicio"; -"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; -"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; -"Scene.Login.Title" = "Welcome back"; +"Scene.Login.ServerSearchField.Placeholder" = "Escribe o URL ou busca o teu servidor"; +"Scene.Login.Subtitle" = "Conéctate ao servidor no que creaches a conta."; +"Scene.Login.Title" = "Benvido outra vez"; "Scene.Notification.FollowRequest.Accept" = "Aceptar"; "Scene.Notification.FollowRequest.Accepted" = "Aceptada"; "Scene.Notification.FollowRequest.Reject" = "rexeitar"; @@ -404,11 +404,11 @@ ser subido a Mastodon."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Algo fallou ao cargar os datos. Comproba a conexión a internet."; "Scene.ServerPicker.EmptyState.FindingServers" = "Buscando servidores dispoñibles..."; "Scene.ServerPicker.EmptyState.NoResults" = "Sen resultados"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Busca comunidades ou escribe URL"; "Scene.ServerPicker.Label.Category" = "CATEGORÍA"; "Scene.ServerPicker.Label.Language" = "IDIOMA"; "Scene.ServerPicker.Label.Users" = "USUARIAS"; -"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; +"Scene.ServerPicker.Subtitle" = "Elixe un servidor en función dos teus intereses, rexión o un de propósito xeral. Poderás conversar con calquera en Mastodon, independentemente do servidor que elixas."; "Scene.ServerPicker.Title" = "Mastodon fórmano as persoas das diferentes comunidades."; "Scene.ServerRules.Button.Confirm" = "Acepto"; "Scene.ServerRules.PrivacyPolicy" = "polícica de privacidade"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings index c80c8ef77..781143b71 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings @@ -55,8 +55,8 @@ Ma ulac aɣilif, senqed tuqqna-inek internet."; "Common.Controls.Actions.Share" = "Bḍu"; "Common.Controls.Actions.SharePost" = "Bḍu tasuffeɣt"; "Common.Controls.Actions.ShareUser" = "Bḍu %@"; -"Common.Controls.Actions.SignIn" = "Log in"; -"Common.Controls.Actions.SignUp" = "Create account"; +"Common.Controls.Actions.SignIn" = "Qqen"; +"Common.Controls.Actions.SignUp" = "Snulfu-d amiḍan"; "Common.Controls.Actions.Skip" = "Zgel"; "Common.Controls.Actions.TakePhoto" = "Ṭṭef tawlaft"; "Common.Controls.Actions.TryAgain" = "Ɛreḍ tikkelt-nniḍen"; @@ -108,10 +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.MetaEntity.Email" = "Tansa imayl : %@"; +"Common.Controls.Status.MetaEntity.Hashtag" = "Ahacṭag : %@"; +"Common.Controls.Status.MetaEntity.Mention" = "Sken-d amaɣnu : %@"; +"Common.Controls.Status.MetaEntity.Url" = "Asaɣ : %@"; "Common.Controls.Status.Poll.Closed" = "Ifukk"; "Common.Controls.Status.Poll.Vote" = "Dɣeṛ"; "Common.Controls.Status.SensitiveContent" = "Agbur amḥulfu"; @@ -237,12 +237,12 @@ Ad d-yettwasali ɣef Mastodon."; "Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel" = "Taqeffalt n ulugu"; "Scene.HomeTimeline.NavigationBarState.NewPosts" = "Tissufaɣ timaynutin"; "Scene.HomeTimeline.NavigationBarState.Offline" = "Beṛṛa n tuqqna"; -"Scene.HomeTimeline.NavigationBarState.Published" = "Yettwasuffeɣ!"; -"Scene.HomeTimeline.NavigationBarState.Publishing" = "Asuffeɣ tasuffeɣt..."; +"Scene.HomeTimeline.NavigationBarState.Published" = "Tettwasuffeɣ!"; +"Scene.HomeTimeline.NavigationBarState.Publishing" = "Asuffeɣ n tasuffeɣt..."; "Scene.HomeTimeline.Title" = "Agejdan"; -"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; +"Scene.Login.ServerSearchField.Placeholder" = "Sekcem URL neɣ nadi ɣef uqeddac-ik·im"; "Scene.Login.Subtitle" = "Log you in on the server you created your account on."; -"Scene.Login.Title" = "Welcome back"; +"Scene.Login.Title" = "Ansuf yess·ek·em"; "Scene.Notification.FollowRequest.Accept" = "Accept"; "Scene.Notification.FollowRequest.Accepted" = "Accepted"; "Scene.Notification.FollowRequest.Reject" = "agi"; @@ -357,7 +357,7 @@ Ad d-yettwasali ɣef Mastodon."; "Scene.Report.StepOne.WhatsWrongWithThisAccount" = "Acu n wugur yellan deg umiḍan-a?"; "Scene.Report.StepOne.WhatsWrongWithThisPost" = "Acu n wugur yellan d tsuffeɣt-a?"; "Scene.Report.StepOne.WhatsWrongWithThisUsername" = "Acu n wugur yellan d %@?"; -"Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules" = "Teẓriḍ y•tettruẓu kra n yilugan"; +"Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules" = "Teẓriḍ y·tettruẓu kra n yilugan"; "Scene.Report.StepThree.AreThereAnyPostsThatBackUpThisReport" = "Llant tsuffaɣ ara isdemren aneqqis-a?"; "Scene.Report.StepThree.SelectAllThatApply" = "Fren akk tifrat ara yettusnasen"; "Scene.Report.StepThree.Step3Of4" = "Aḥric 3 seg 4"; @@ -404,7 +404,7 @@ Ad d-yettwasali ɣef Mastodon."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Tella-d tuccḍa lawan n usali n yisefka. Senqed tuqqna-ink internet."; "Scene.ServerPicker.EmptyState.FindingServers" = "Tifin n yiqeddacen yellan..."; "Scene.ServerPicker.EmptyState.NoResults" = "Ulac igemmaḍ"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Nadi timɣiwnin neɣ sekcem URL"; "Scene.ServerPicker.Label.Category" = "TAGGAYT"; "Scene.ServerPicker.Label.Language" = "TUTLAYT"; "Scene.ServerPicker.Label.Users" = "ISEQDACEN"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.stringsdict index fd7cac605..f18a906c0 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.stringsdict @@ -61,9 +61,9 @@ NSStringFormatValueTypeKey ld one - 1 character + 1 n usekkil other - %ld characters + %ld n isekkilen plural.count.followed_by_and_mutual @@ -136,7 +136,7 @@ NSStringFormatValueTypeKey ld one - 1 tsuffeɣt + 1 n tsuffeɣt other %ld n tsuffaɣ @@ -312,9 +312,9 @@ NSStringFormatValueTypeKey ld one - Yeqqim-d 1 wass + Yeqqim-d 1 n wass other - Qqimen-d %ld wussan + Qqimen-d %ld n wussan date.hour.left @@ -328,9 +328,9 @@ NSStringFormatValueTypeKey ld one - Yeqqim-d 1 usrag + Yeqqim-d 1 n wesrag other - Qqimen-d %ld yisragen + Qqimen-d %ld n yisragen date.minute.left @@ -344,9 +344,9 @@ NSStringFormatValueTypeKey ld one - 1 tesdat i d-yeqqimen + 1 n tesdat i d-yeqqimen other - %ld tesdatin i d-yeqqimen + %ld n tesdatin i d-yeqqimen date.second.left @@ -360,9 +360,9 @@ NSStringFormatValueTypeKey ld one - 1 tasint i d-yeqqimen + 1 n tasint i d-yeqqimen other - %ld tsinin i d-yeqqimen + %ld n tasinin i d-yeqqimen date.year.ago.abbr @@ -376,9 +376,9 @@ NSStringFormatValueTypeKey ld one - 1 useggas aya + %ld n useggas aya other - %ld yiseggasen aya + %ld n yiseggasen aya date.month.ago.abbr @@ -392,9 +392,9 @@ NSStringFormatValueTypeKey ld one - 1 wayyur aya + %ld n wayyur aya other - %ld wayyuren aya + %ld n wayyuren aya date.day.ago.abbr @@ -408,9 +408,9 @@ NSStringFormatValueTypeKey ld one - 1 wass aya + %ld n wass aya other - %ld wussan aya + %ld n wussan aya date.hour.ago.abbr @@ -424,9 +424,9 @@ NSStringFormatValueTypeKey ld one - 1 usrag aya + %ld n wesrag aya other - %ld yisragen aya + %ld n yisragen aya date.minute.ago.abbr @@ -440,9 +440,9 @@ NSStringFormatValueTypeKey ld one - 1 tesdat aya + %ld n tesdat aya other - %ld tesdatin aya + %ld n tesdatin aya date.second.ago.abbr @@ -456,9 +456,9 @@ NSStringFormatValueTypeKey ld one - 1 tasint aya + %ld n tasint aya other - %ld tsinin aya + %ld n tasinin aya diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings index 758a9190b..03ac1f46d 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings @@ -55,8 +55,8 @@ Jkx girêdana înternetê xwe kontrol bike."; "Common.Controls.Actions.Share" = "Parve bike"; "Common.Controls.Actions.SharePost" = "Şandiyê parve bike"; "Common.Controls.Actions.ShareUser" = "%@ parve bike"; -"Common.Controls.Actions.SignIn" = "Log in"; -"Common.Controls.Actions.SignUp" = "Create account"; +"Common.Controls.Actions.SignIn" = "Têkeve"; +"Common.Controls.Actions.SignUp" = "Ajimêr biafirîne"; "Common.Controls.Actions.Skip" = "Derbas bike"; "Common.Controls.Actions.TakePhoto" = "Wêne bikişîne"; "Common.Controls.Actions.TryAgain" = "Dîsa biceribîne"; @@ -241,9 +241,9 @@ girêdanê bitikne da ku ajimêra xwe bidî piştrastkirin."; "Scene.HomeTimeline.NavigationBarState.Published" = "Hate weşandin!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Şandî tê weşandin..."; "Scene.HomeTimeline.Title" = "Serrûpel"; -"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; -"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; -"Scene.Login.Title" = "Welcome back"; +"Scene.Login.ServerSearchField.Placeholder" = "Girêdanê têxe an jî li rajekarê xwe bigere"; +"Scene.Login.Subtitle" = "Têketinê bike ser rajekarê ku te ajimêrê xwe tê de çê kiriye."; +"Scene.Login.Title" = "Dîsa bi xêr hatî"; "Scene.Notification.FollowRequest.Accept" = "Bipejirîne"; "Scene.Notification.FollowRequest.Accepted" = "Pejirandî"; "Scene.Notification.FollowRequest.Reject" = "nepejirîne"; @@ -405,11 +405,11 @@ girêdanê bitikne da ku ajimêra xwe bidî piştrastkirin."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Di dema barkirina daneyan da çewtî derket. Girêdana xwe ya înternetê kontrol bike."; "Scene.ServerPicker.EmptyState.FindingServers" = "Peydakirina rajekarên berdest..."; "Scene.ServerPicker.EmptyState.NoResults" = "Encam tune"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Li civakan bigere an jî girêdanê têxe"; "Scene.ServerPicker.Label.Category" = "BEŞ"; "Scene.ServerPicker.Label.Language" = "ZIMAN"; "Scene.ServerPicker.Label.Users" = "BIKARHÊNER"; -"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; +"Scene.ServerPicker.Subtitle" = "Li gorî herêm, berjewendî, an jî armanceke giştî rajekarekê hilbijêre. Tu hîn jî dikarî li ser Mastodon bi her kesî re biaxivî, her rajekarê te çi be."; "Scene.ServerPicker.Title" = "Mastodon ji bikarhênerên di civakên cuda de pêk tê."; "Scene.ServerRules.Button.Confirm" = "Ez dipejirînim"; "Scene.ServerRules.PrivacyPolicy" = "polîtikaya nihêniyê"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings index 8e63e0269..25c7e37f8 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings @@ -55,8 +55,8 @@ "Common.Controls.Actions.Share" = "แบ่งปัน"; "Common.Controls.Actions.SharePost" = "แบ่งปันโพสต์"; "Common.Controls.Actions.ShareUser" = "แบ่งปัน %@"; -"Common.Controls.Actions.SignIn" = "Log in"; -"Common.Controls.Actions.SignUp" = "Create account"; +"Common.Controls.Actions.SignIn" = "เข้าสู่ระบบ"; +"Common.Controls.Actions.SignUp" = "สร้างบัญชี"; "Common.Controls.Actions.Skip" = "ข้าม"; "Common.Controls.Actions.TakePhoto" = "ถ่ายรูป"; "Common.Controls.Actions.TryAgain" = "ลองอีกครั้ง"; @@ -240,9 +240,9 @@ "Scene.HomeTimeline.NavigationBarState.Published" = "เผยแพร่แล้ว!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "กำลังเผยแพร่โพสต์..."; "Scene.HomeTimeline.Title" = "หน้าแรก"; -"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; -"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; -"Scene.Login.Title" = "Welcome back"; +"Scene.Login.ServerSearchField.Placeholder" = "ป้อน URL หรือค้นหาสำหรับเซิร์ฟเวอร์ของคุณ"; +"Scene.Login.Subtitle" = "นำคุณเข้าสู่ระบบในเซิร์ฟเวอร์ที่คุณได้สร้างบัญชีของคุณไว้ใน"; +"Scene.Login.Title" = "ยินดีต้อนรับกลับมา"; "Scene.Notification.FollowRequest.Accept" = "ยอมรับ"; "Scene.Notification.FollowRequest.Accepted" = "ยอมรับแล้ว"; "Scene.Notification.FollowRequest.Reject" = "ปฏิเสธ"; @@ -404,11 +404,11 @@ "Scene.ServerPicker.EmptyState.BadNetwork" = "มีบางอย่างผิดพลาดขณะโหลดข้อมูล ตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณ"; "Scene.ServerPicker.EmptyState.FindingServers" = "กำลังค้นหาเซิร์ฟเวอร์ที่พร้อมใช้งาน..."; "Scene.ServerPicker.EmptyState.NoResults" = "ไม่มีผลลัพธ์"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "ค้นหาชุมชนหรือป้อน URL"; "Scene.ServerPicker.Label.Category" = "หมวดหมู่"; "Scene.ServerPicker.Label.Language" = "ภาษา"; "Scene.ServerPicker.Label.Users" = "ผู้ใช้"; -"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; +"Scene.ServerPicker.Subtitle" = "เลือกเซิร์ฟเวอร์ตามภูมิภาค, ความสนใจ หรือวัตถุประสงค์ทั่วไปของคุณ คุณยังคงสามารถแชทกับใครก็ตามใน Mastodon โดยไม่คำนึงถึงเซิร์ฟเวอร์ของคุณ"; "Scene.ServerPicker.Title" = "Mastodon ประกอบด้วยผู้ใช้ในเซิร์ฟเวอร์ต่าง ๆ"; "Scene.ServerRules.Button.Confirm" = "ฉันเห็นด้วย"; "Scene.ServerRules.PrivacyPolicy" = "นโยบายความเป็นส่วนตัว"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings index cb362ea9d..0c779d293 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings @@ -55,8 +55,8 @@ "Common.Controls.Actions.Share" = "分享"; "Common.Controls.Actions.SharePost" = "分享帖子"; "Common.Controls.Actions.ShareUser" = "分享 %@"; -"Common.Controls.Actions.SignIn" = "Log in"; -"Common.Controls.Actions.SignUp" = "Create account"; +"Common.Controls.Actions.SignIn" = "登录"; +"Common.Controls.Actions.SignUp" = "创建账户"; "Common.Controls.Actions.Skip" = "跳过"; "Common.Controls.Actions.TakePhoto" = "拍照"; "Common.Controls.Actions.TryAgain" = "再试一次"; @@ -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,10 +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.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" = "敏感内容"; @@ -155,27 +155,27 @@ "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" = "自定义表情选择器"; "Scene.Compose.Accessibility.DisableContentWarning" = "关闭内容警告"; "Scene.Compose.Accessibility.EnableContentWarning" = "启用内容警告"; -"Scene.Compose.Accessibility.PostOptions" = "Post Options"; +"Scene.Compose.Accessibility.PostOptions" = "帖子选项"; "Scene.Compose.Accessibility.PostVisibilityMenu" = "帖子可见性"; -"Scene.Compose.Accessibility.PostingAs" = "Posting as %@"; +"Scene.Compose.Accessibility.PostingAs" = "以 %@ 身份发布"; "Scene.Compose.Accessibility.RemovePoll" = "移除投票"; "Scene.Compose.Attachment.AttachmentBroken" = "%@已损坏 无法上传到 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.AttachmentTooLarge" = "附件太大"; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "无法识别此媒体"; +"Scene.Compose.Attachment.CompressingState" = "压缩中..."; "Scene.Compose.Attachment.DescriptionPhoto" = "为视觉障碍人士添加照片的文字说明..."; "Scene.Compose.Attachment.DescriptionVideo" = "为视觉障碍人士添加视频的文字说明..."; -"Scene.Compose.Attachment.LoadFailed" = "Load Failed"; +"Scene.Compose.Attachment.LoadFailed" = "加载失败"; "Scene.Compose.Attachment.Photo" = "照片"; -"Scene.Compose.Attachment.ServerProcessingState" = "Server Processing..."; -"Scene.Compose.Attachment.UploadFailed" = "Upload Failed"; +"Scene.Compose.Attachment.ServerProcessingState" = "服务器正在处理..."; +"Scene.Compose.Attachment.UploadFailed" = "上传失败"; "Scene.Compose.Attachment.Video" = "视频"; "Scene.Compose.AutoComplete.SpaceToAdd" = "输入空格键入"; "Scene.Compose.ComposeAction" = "发送"; @@ -196,8 +196,8 @@ "Scene.Compose.Poll.OptionNumber" = "选项 %ld"; "Scene.Compose.Poll.SevenDays" = "7 天"; "Scene.Compose.Poll.SixHours" = "6 小时"; -"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; -"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "投票含有空选项"; +"Scene.Compose.Poll.ThePollIsInvalid" = "投票无效"; "Scene.Compose.Poll.ThirtyMinutes" = "30 分钟"; "Scene.Compose.Poll.ThreeDays" = "3 天"; "Scene.Compose.ReplyingToUser" = "回复给 %@"; @@ -240,9 +240,9 @@ "Scene.HomeTimeline.NavigationBarState.Published" = "已发送"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "正在发送..."; "Scene.HomeTimeline.Title" = "主页"; -"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; -"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; -"Scene.Login.Title" = "Welcome back"; +"Scene.Login.ServerSearchField.Placeholder" = "输入网址或搜索您的服务器"; +"Scene.Login.Subtitle" = "登入您账户所在的服务器。"; +"Scene.Login.Title" = "欢迎回来"; "Scene.Notification.FollowRequest.Accept" = "接受"; "Scene.Notification.FollowRequest.Accepted" = "已接受"; "Scene.Notification.FollowRequest.Reject" = "拒绝"; @@ -270,17 +270,17 @@ "Scene.Profile.Fields.AddRow" = "添加"; "Scene.Profile.Fields.Placeholder.Content" = "内容"; "Scene.Profile.Fields.Placeholder.Label" = "标签"; -"Scene.Profile.Fields.Verified.Long" = "Ownership of this link was checked on %@"; -"Scene.Profile.Fields.Verified.Short" = "Verified on %@"; +"Scene.Profile.Fields.Verified.Long" = "此链接的所有权已在 %@ 上检查通过"; +"Scene.Profile.Fields.Verified.Short" = "验证于 %@"; "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" = "确认取消静音 %@"; @@ -404,11 +404,11 @@ "Scene.ServerPicker.EmptyState.BadNetwork" = "出了些问题。请检查你的互联网连接"; "Scene.ServerPicker.EmptyState.FindingServers" = "正在查找可用的服务器..."; "Scene.ServerPicker.EmptyState.NoResults" = "无结果"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "搜索社区或输入 URL"; "Scene.ServerPicker.Label.Category" = "类别"; "Scene.ServerPicker.Label.Language" = "语言"; "Scene.ServerPicker.Label.Users" = "用户"; -"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; +"Scene.ServerPicker.Subtitle" = "根据你的地区、兴趣挑选一个服务器。无论你选择哪个服务器,你都可以跟其他服务器的任何人一起聊天。"; "Scene.ServerPicker.Title" = "挑选一个服务器, 任意服务器。"; "Scene.ServerRules.Button.Confirm" = "我同意"; diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict index 915a524b5..362d55c4f 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict @@ -47,7 +47,7 @@ a11y.plural.count.characters_left NSStringLocalizedFormatKey - %#@character_count@ left + %#@character_count@ 剩余 character_count NSStringFormatSpecTypeKey @@ -55,7 +55,7 @@ NSStringFormatValueTypeKey ld other - %ld characters + %ld 个字符 plural.count.followed_by_and_mutual diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings index 60dcd2077..10dbbf8ca 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings @@ -55,8 +55,8 @@ "Common.Controls.Actions.Share" = "分享"; "Common.Controls.Actions.SharePost" = "分享嘟文"; "Common.Controls.Actions.ShareUser" = "分享 %@"; -"Common.Controls.Actions.SignIn" = "Log in"; -"Common.Controls.Actions.SignUp" = "Create account"; +"Common.Controls.Actions.SignIn" = "登入"; +"Common.Controls.Actions.SignUp" = "新增帳號"; "Common.Controls.Actions.Skip" = "跳過"; "Common.Controls.Actions.TakePhoto" = "拍攝照片"; "Common.Controls.Actions.TryAgain" = "再試一次"; @@ -163,13 +163,13 @@ "Scene.Compose.Accessibility.RemovePoll" = "移除投票"; "Scene.Compose.Attachment.AttachmentBroken" = "此 %@ 已損毀,並無法被上傳至 Mastodon。"; "Scene.Compose.Attachment.AttachmentTooLarge" = "附加檔案大小過大"; -"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not recognize this media attachment"; -"Scene.Compose.Attachment.CompressingState" = "Compressing..."; +"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "無法識別此媒體附加檔案"; +"Scene.Compose.Attachment.CompressingState" = "正在壓縮..."; "Scene.Compose.Attachment.DescriptionPhoto" = "為視障人士提供圖片說明..."; "Scene.Compose.Attachment.DescriptionVideo" = "為視障人士提供影片說明..."; "Scene.Compose.Attachment.LoadFailed" = "讀取失敗"; "Scene.Compose.Attachment.Photo" = "照片"; -"Scene.Compose.Attachment.ServerProcessingState" = "Server Processing..."; +"Scene.Compose.Attachment.ServerProcessingState" = "伺服器處理中..."; "Scene.Compose.Attachment.UploadFailed" = "上傳失敗"; "Scene.Compose.Attachment.Video" = "影片"; "Scene.Compose.AutoComplete.SpaceToAdd" = "添加的空白"; @@ -191,8 +191,8 @@ "Scene.Compose.Poll.OptionNumber" = "選項 %ld"; "Scene.Compose.Poll.SevenDays" = "七天"; "Scene.Compose.Poll.SixHours" = "六小時"; -"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; -"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; +"Scene.Compose.Poll.ThePollHasEmptyOption" = "此投票有空白選項"; +"Scene.Compose.Poll.ThePollIsInvalid" = "此投票是無效的"; "Scene.Compose.Poll.ThirtyMinutes" = "30 分鐘"; "Scene.Compose.Poll.ThreeDays" = "三天"; "Scene.Compose.ReplyingToUser" = "正在回覆 %@"; @@ -235,9 +235,9 @@ "Scene.HomeTimeline.NavigationBarState.Published" = "嘟出去!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "發表嘟文..."; "Scene.HomeTimeline.Title" = "首頁"; -"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server"; -"Scene.Login.Subtitle" = "Log you in on the server you created your account on."; -"Scene.Login.Title" = "Welcome back"; +"Scene.Login.ServerSearchField.Placeholder" = "請輸入 URL 或搜尋您的伺服器"; +"Scene.Login.Subtitle" = "登入您新增帳號之伺服器"; +"Scene.Login.Title" = "歡迎回來"; "Scene.Notification.FollowRequest.Accept" = "接受"; "Scene.Notification.FollowRequest.Accepted" = "已接受"; "Scene.Notification.FollowRequest.Reject" = "拒絕"; @@ -265,8 +265,8 @@ "Scene.Profile.Fields.AddRow" = "新增列"; "Scene.Profile.Fields.Placeholder.Content" = "內容"; "Scene.Profile.Fields.Placeholder.Label" = "標籤"; -"Scene.Profile.Fields.Verified.Long" = "Ownership of this link was checked on %@"; -"Scene.Profile.Fields.Verified.Short" = "Verified on %@"; +"Scene.Profile.Fields.Verified.Long" = "已在 %@ 檢查此連結的擁有者權限"; +"Scene.Profile.Fields.Verified.Short" = "於 %@ 上已驗證"; "Scene.Profile.Header.FollowsYou" = "跟隨了您"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "確認將 %@ 封鎖"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "封鎖"; @@ -399,11 +399,11 @@ "Scene.ServerPicker.EmptyState.BadNetwork" = "讀取資料時發生錯誤。請檢查您的網路連線。"; "Scene.ServerPicker.EmptyState.FindingServers" = "尋找可用的伺服器..."; "Scene.ServerPicker.EmptyState.NoResults" = "沒有結果"; -"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search communities or enter URL"; +"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "搜尋社群或輸入 URL 地址"; "Scene.ServerPicker.Label.Category" = "分類"; "Scene.ServerPicker.Label.Language" = "語言"; "Scene.ServerPicker.Label.Users" = "使用者"; -"Scene.ServerPicker.Subtitle" = "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers."; +"Scene.ServerPicker.Subtitle" = "基於您的興趣、地區、或一般用途選定一個伺服器。您仍會與任何伺服器中的每個人連結。"; "Scene.ServerPicker.Title" = "Mastodon 由不同伺服器的使用者組成。"; "Scene.ServerRules.Button.Confirm" = "我已閱讀並同意"; "Scene.ServerRules.PrivacyPolicy" = "隱私權政策"; From 356b8f2c008397e592c86ad875709087b1e9e408 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 18 Nov 2022 21:36:51 +0800 Subject: [PATCH 655/658] chore: try tag the build --- .github/scripts/build-release.sh | 2 ++ .github/workflows/develop-build.yml | 15 +++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/scripts/build-release.sh b/.github/scripts/build-release.sh index e3efc59df..0b88b9a98 100755 --- a/.github/scripts/build-release.sh +++ b/.github/scripts/build-release.sh @@ -36,6 +36,8 @@ BUILD_NUMBER=$(app-store-connect get-latest-testflight-build-number $ENV_APP_ID BUILD_NUMBER=$((BUILD_NUMBER+1)) CURRENT_PROJECT_VERSION=${BUILD_NUMBER:-0} +echo "GITHUB_TAG_NAME=build-$CURRENT_PROJECT_VERSION" >> $GITHUB_TAG_NAME + agvtool new-version -all $CURRENT_PROJECT_VERSION xcrun xcodebuild clean \ diff --git a/.github/workflows/develop-build.yml b/.github/workflows/develop-build.yml index 4b3a4a04c..ede061098 100644 --- a/.github/workflows/develop-build.yml +++ b/.github/workflows/develop-build.yml @@ -12,16 +12,17 @@ jobs: name: Build runs-on: macOS-12 steps: - - name: checkout + - name: Checkout uses: actions/checkout@v2 - - name: setup + - name: Setup env: NotificationEndpointDebug: ${{ secrets.NotificationEndpointDebug }} NotificationEndpointRelease: ${{ secrets.NotificationEndpointRelease }} run: exec ./.github/scripts/setup.sh - - uses: actions/setup-python@v4 + - name: Install codemagic-cli-tools + uses: actions/setup-python@v4 with: python-version: '3.11' - run: | @@ -44,7 +45,7 @@ jobs: api-key-id: ${{ secrets.APPSTORE_KEY_ID }} api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }} - - name: build + - name: Build env: ENV_APP_ID: ${{ secrets.APP_ID }} ENV_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }} @@ -61,6 +62,12 @@ jobs: api-key-id: ${{ secrets.APPSTORE_KEY_ID }} api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }} + - name: Tag commit + uses: tvdias/github-tagger@v0.0.1 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + tag: "${{ env.GITHUB_TAG_NAME }}" + - name: Clean up keychain and provisioning profile if: ${{ always() }} run: | From 5fbafe4fdb1dfa2cddbc1a672be2fe406b5a4cb5 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 18 Nov 2022 21:42:49 +0800 Subject: [PATCH 656/658] fix: the wrong command to set GitHub env --- .github/scripts/build-release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/build-release.sh b/.github/scripts/build-release.sh index 0b88b9a98..069aa673e 100755 --- a/.github/scripts/build-release.sh +++ b/.github/scripts/build-release.sh @@ -36,7 +36,7 @@ BUILD_NUMBER=$(app-store-connect get-latest-testflight-build-number $ENV_APP_ID BUILD_NUMBER=$((BUILD_NUMBER+1)) CURRENT_PROJECT_VERSION=${BUILD_NUMBER:-0} -echo "GITHUB_TAG_NAME=build-$CURRENT_PROJECT_VERSION" >> $GITHUB_TAG_NAME +echo "GITHUB_TAG_NAME=build-$CURRENT_PROJECT_VERSION" >> $GITHUB_ENV agvtool new-version -all $CURRENT_PROJECT_VERSION From f0bf9c6937ef0bb3054edf36a59d73777a27d163 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 18 Nov 2022 10:55:25 -0500 Subject: [PATCH 657/658] Bump the timeout interval for all requests to 60s --- MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index 43d5873d0..5d507bace 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -11,7 +11,7 @@ import enum NIOHTTP1.HTTPResponseStatus extension Mastodon.API { - static let timeoutInterval: TimeInterval = 10 + static let timeoutInterval: TimeInterval = 60 static let httpHeaderDateFormatter: ISO8601DateFormatter = { var formatter = ISO8601DateFormatter() From ce076b264b14efb2531f4162c0f48b6c4adb286c Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 19 Nov 2022 00:56:44 +0800 Subject: [PATCH 658/658] fix: hashtag auto complete issue --- .../ComposeContent/ComposeContentViewController.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index b84b47385..c5c2d267b 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -627,10 +627,10 @@ extension ComposeContentViewController: ComposeContentViewModelDelegate { let _replacedText: String? = { var text: String switch item { - case .hashtag(let hashtag): - text = "#" + hashtag.name - case .hashtagV1(let hashtagName): - text = "#" + hashtagName + case .hashtag, .hashtagV1: + // do no fill the hashtag + // allow user delete suffix and post they want + return nil case .account(let account): text = "@" + account.acct case .emoji(let emoji):