From bc01a04c387847379d5a530416c31b75f56f8b19 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 4 Jul 2022 04:44:31 +0200 Subject: [PATCH 01/58] 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 02/58] 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 03/58] 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 04/58] 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 05/58] 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 06/58] 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 07/58] 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 08/58] 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 09/58] 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 10/58] 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 11/58] 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 12/58] 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 13/58] 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 14/58] 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 15/58] 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 16/58] 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 17/58] 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 18/58] 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 19/58] 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 20/58] 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 21/58] 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 22/58] 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 23/58] 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 24/58] 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 25/58] 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 26/58] 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 27/58] 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 28/58] 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 29/58] 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 30/58] 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 31/58] 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 32/58] 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 33/58] 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 34/58] 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 35/58] 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 36/58] 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 37/58] 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 38/58] 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 39/58] 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 40/58] 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 41/58] 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 42/58] 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 43/58] 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 44/58] 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 45/58] 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 668a1d28e27ab0c86fac30ceeaa0b3558d27d78a Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 31 Oct 2022 14:47:13 +0800 Subject: [PATCH 46/58] 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 47/58] 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 48/58] 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 49/58] 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 87e05ecdabf9e2b9a8ccd015d8aa61076e3c91b6 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 31 Oct 2022 11:32:31 -0400 Subject: [PATCH 50/58] 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 51/58] 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 52/58] 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 53/58] 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 54/58] 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 55/58] 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 56/58] 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 57/58] 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 58/58] 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