diff --git a/.arkana.yml b/.arkana.yml new file mode 100644 index 000000000..7865ceacf --- /dev/null +++ b/.arkana.yml @@ -0,0 +1,17 @@ +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) + # Mastodon Push Notification Endpoint + - NotificationEndpoint 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/build.sh b/.github/scripts/build.sh index f5894901a..d4fc4acc7 100755 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -7,6 +7,6 @@ set -eo pipefail xcodebuild -workspace Mastodon.xcworkspace \ -scheme Mastodon \ - -destination "platform=iOS Simulator,name=iPhone SE (2nd generation)" \ + -destination "platform=iOS Simulator,name=iPhone SE (2nd generation)" \ clean \ build | xcpretty 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..827276208 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,13 +15,14 @@ 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 }} + NotificationEndpointRelease: ${{ secrets.NotificationEndpointRelease }} run: exec ./.github/scripts/setup.sh - name: build run: exec ./.github/scripts/build.sh 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/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/Documentation/Acknowledgments.md b/Documentation/Acknowledgments.md index ff6dbc081..3e9a2d7b4 100644 --- a/Documentation/Acknowledgments.md +++ b/Documentation/Acknowledgments.md @@ -1,13 +1,12 @@ # 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) -- [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) @@ -26,10 +25,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..bdf2a2c77 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,13 @@ bundle install bundle install bundle exec pod clean +# setup arkana +# please check the `.env.example` to create your's or use the empty example directly +bundle exec arkana -e ./env/.env + +# clean pods +bundle exec pod clean + # make install bundle exec pod install --repo-update @@ -59,14 +66,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. +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 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. @@ -82,4 +89,4 @@ Please check and set the `notification.Topic` to the app BundleID in [toot-relay ## What's next -We welcome contributions! And if you have an interest to contribute codes. Here is a document that describes the app architecture and what's tech stack it uses. \ No newline at end of file +We welcome contributions! And if you have an interest to contribute codes. Here is a document that describes the app architecture and what's tech stack it uses. 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..873f725e5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,20 +3,21 @@ GEM specs: CFPropertyList (3.0.5) rexml - RubyInline (3.12.5) - ZenTest (~> 4.3) - ZenTest (4.12.1) - activesupport (6.1.5.1) + activesupport (6.1.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) 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) @@ -71,39 +70,42 @@ GEM fuzzy_match (2.0.4) gh_inspector (1.1.3) httpclient (2.8.3) - i18n (1.10.0) + i18n (1.12.0) concurrent-ruby (~> 1.0) - json (2.6.1) - minitest (5.15.0) + json (2.6.2) + minitest (5.16.3) molinillo (0.8.0) 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) - tzinfo (2.0.4) + tzinfo (2.0.5) concurrent-ruby (~> 1.0) - xcodeproj (1.21.0) + xcodeproj (1.22.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) rexml (~> 3.2.4) - zeitwerk (2.5.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) + yaml (0.2.0) + zeitwerk (2.6.3) PLATFORMS - ruby + arm64-darwin-21 DEPENDENCIES + arkana cocoapods cocoapods-clean - cocoapods-keys + xcpretty BUNDLED WITH - 2.3.11 + 2.3.17 diff --git a/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict index 197897e8e..862d98184 100644 --- a/Localization/StringsConvertor/input/ar.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/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/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": { 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": { 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": { diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 9f0a8f44c..e40958be5 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 */ @@ -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 */; }; @@ -30,9 +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 */; }; - 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,16 +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 */; }; - 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 */; }; 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76319E25C1521200929FB9 /* StatusSection.swift */; }; @@ -68,46 +57,29 @@ 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 */; }; 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 */; }; - 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 */; }; 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 */; }; - 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 */; }; @@ -124,18 +96,11 @@ 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 */; }; @@ -146,9 +111,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 */; }; @@ -156,10 +119,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 */; }; 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 */; }; @@ -173,7 +132,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 */; }; @@ -195,37 +153,19 @@ 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 */; }; - 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 */; }; 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 */; }; @@ -236,22 +176,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 */; }; @@ -261,30 +187,15 @@ 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 */; }; 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 */; }; @@ -293,12 +204,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 */; }; @@ -337,45 +244,26 @@ 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 */; }; @@ -389,40 +277,23 @@ 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 */; }; 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 */; }; - 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 */; }; @@ -438,9 +309,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 */; }; @@ -448,20 +316,14 @@ 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 */; }; 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 */; }; @@ -477,40 +339,25 @@ 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 */; }; 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 */; }; - 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 */; }; @@ -519,7 +366,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 */; }; @@ -529,44 +375,25 @@ 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 */; }; - 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 App Extensions */ = {isa = PBXBuildFile; fileRef = DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - DBC6462326A1712000B0E31B /* ShareViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6462226A1712000B0E31B /* ShareViewModel.swift */; }; + DBC6461C26A170AB00B0E31B /* ShareActionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DBC6462326A1712000B0E31B /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6462226A1712000B0E31B /* ComposeViewModel.swift */; }; 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 */; }; @@ -576,17 +403,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 */; }; @@ -604,9 +426,8 @@ 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, ); }; }; + 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 */; }; @@ -614,20 +435,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 */; }; - 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 */ @@ -645,20 +452,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 */; @@ -666,13 +459,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 */; @@ -680,13 +466,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 */; @@ -703,22 +482,21 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */, ); 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 */ @@ -730,9 +508,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,9 +525,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 = ""; }; - 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 = ""; }; @@ -767,14 +540,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 = ""; }; - 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 = ""; }; 2D76319E25C1521200929FB9 /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = ""; }; @@ -785,24 +554,15 @@ 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 = ""; }; 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 = ""; }; - 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; }; @@ -812,14 +572,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 = ""; }; @@ -827,12 +584,8 @@ 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 = ""; }; - 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 = ""; }; @@ -870,17 +623,11 @@ 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 = ""; }; 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 = ""; }; - 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 = ""; }; @@ -893,9 +640,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 = ""; }; @@ -904,10 +649,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 = ""; }; 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 = ""; }; @@ -921,7 +662,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 = ""; }; @@ -944,37 +684,16 @@ 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 = ""; }; - 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 = ""; }; 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 = ""; }; @@ -985,7 +704,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; }; @@ -1003,29 +721,16 @@ 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 = ""; }; 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 = ""; }; @@ -1048,7 +753,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 = ""; }; @@ -1066,8 +770,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 = ""; }; @@ -1106,40 +808,25 @@ 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 = ""; }; 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 = ""; }; - 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 = ""; }; @@ -1154,40 +841,23 @@ 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 = ""; }; 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 = ""; }; - 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 = ""; }; @@ -1206,9 +876,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 = ""; }; @@ -1232,13 +899,7 @@ 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 = ""; }; @@ -1254,20 +915,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 = ""; }; @@ -1284,18 +939,13 @@ 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 = ""; }; 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 = ""; }; - 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 = ""; }; @@ -1312,26 +962,16 @@ 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 = ""; }; - 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 = ""; }; - 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 = ""; }; @@ -1343,19 +983,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 = ""; }; @@ -1371,7 +1005,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 = ""; }; @@ -1420,8 +1053,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 = ""; }; @@ -1440,22 +1071,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; }; @@ -1476,26 +1094,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; }; @@ -1503,10 +1107,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; }; @@ -1514,10 +1115,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; }; @@ -1637,8 +1235,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 */, @@ -1723,7 +1319,6 @@ 2D5A3D0125CF8640002347D6 /* Vender */ = { isa = PBXGroup; children = ( - 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */, DB6180EC26391C6C0018D199 /* TransitioningMath.swift */, DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */, DBF156E32702DB3F00EC00B7 /* HandleTapAction.swift */, @@ -1733,31 +1328,10 @@ 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 = ( DB697DD7278F4C34004EF2F7 /* Provider */, - DB0FCB7127952986006C02E2 /* NamingState.swift */, 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */, DB4AA6B227BA34B6009EC082 /* CellFrameCacheContainer.swift */, 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */, @@ -1780,12 +1354,10 @@ DB0FCB892796BE1E006C02E2 /* RecommandAccount */, DB4F097926A039C400D62E92 /* Status */, DB65C63527A2AF52008BAC2E /* Report */, - DB4F097626A0398000D62E92 /* Compose */, DB0617F727855B010030EE79 /* Notification */, DB4F097726A039A200D62E92 /* Search */, DB3E6FE52806A5BA00B035AE /* Discovery */, DB0617FA27855B660030EE79 /* Settings */, - DBCBED2226132E1D00B49291 /* FetchedResultsController */, ); path = Diffiable; sourceTree = ""; @@ -1805,7 +1377,6 @@ 2D7631A525C1532D00929FB9 /* View */ = { isa = PBXGroup; children = ( - 2DA504672601ADBA008F4E6C /* Decoration */, 2D42FF8325C82245004A627A /* Button */, DBA9B90325F1D4420012E7B6 /* Control */, 2D152A8A25C295B8009AA50C /* Content */, @@ -1825,11 +1396,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 */, @@ -1837,14 +1403,6 @@ path = TableviewCell; sourceTree = ""; }; - 2DA504672601ADBA008F4E6C /* Decoration */ = { - isa = PBXGroup; - children = ( - 2DA504682601ADE7008F4E6C /* SawToothView.swift */, - ); - path = Decoration; - sourceTree = ""; - }; 2DAC9E36262FC20B0062E1A6 /* SuggestionAccount */ = { isa = PBXGroup; children = ( @@ -1976,50 +1534,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 = ( - DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */, - DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */, - DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */, - DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */, - ); - path = TableViewCell; - sourceTree = ""; - }; DB0617F727855B010030EE79 /* Notification */ = { isa = PBXGroup; children = ( @@ -2083,19 +1597,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 = ( @@ -2149,16 +1650,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 = ( @@ -2202,14 +1693,6 @@ path = Discovery; sourceTree = ""; }; - DB3E6FEA2806BD2500B035AE /* MastodonUI */ = { - isa = PBXGroup; - children = ( - DB3E6FE82806BD2200B035AE /* ThemeService.swift */, - ); - path = MastodonUI; - sourceTree = ""; - }; DB3E6FED2806D7FC00B035AE /* News */ = { isa = PBXGroup; children = ( @@ -2253,7 +1736,6 @@ DB427DD425BAA00100D1B89D /* Mastodon */, DB427DEB25BAA00100D1B89D /* MastodonTests */, DB427DF625BAA00100D1B89D /* MastodonUITests */, - DB6804802637CD4C00430867 /* AppShared */, DBF8AE14263293E400C9C23C /* NotificationService */, DBC6461326A170AB00B0E31B /* ShareActionExtension */, DB8FABC826AEC7B2008E5AF4 /* MastodonIntent */, @@ -2271,7 +1753,6 @@ DB427DE825BAA00100D1B89D /* MastodonTests.xctest */, DB427DF325BAA00100D1B89D /* MastodonUITests.xctest */, DBF8AE13263293E400C9C23C /* NotificationService.appex */, - DB68047F2637CD4C00430867 /* AppShared.framework */, DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */, DB8FABC626AEC7B2008E5AF4 /* MastodonIntent.appex */, ); @@ -2284,15 +1765,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 */, @@ -2321,72 +1799,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 = ( @@ -2400,23 +1812,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 = ( @@ -2474,13 +1869,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 = ""; @@ -2488,7 +1877,6 @@ DB55D32225FB4D320002F825 /* View */ = { isa = PBXGroup; children = ( - DB03F7F42689B782007B274C /* ComposeTableView.swift */, DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */, DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */, DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */, @@ -2672,17 +2060,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 = ( @@ -2744,34 +2121,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 = ( @@ -2796,10 +2145,8 @@ DB789A1025F9F29B0071ACA0 /* Compose */ = { isa = PBXGroup; children = ( - DB6F5E36264E78EA009108F4 /* AutoComplete */, DB55D32225FB4D320002F825 /* View */, DB789A2125F9F76D0071ACA0 /* CollectionViewCell */, - DB03F7F1268990A2007B274C /* TableViewCell */, DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */, DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */, DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */, @@ -2843,16 +2190,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 = ( @@ -2898,16 +2235,11 @@ 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 */, @@ -2939,7 +2271,6 @@ DB8FABC926AEC7B2008E5AF4 /* IntentHandler.swift */, DB64BA462851F23300ADF1B7 /* Model */, DB64BA492851F65F00ADF1B7 /* Handler */, - DBB8AB4B26AED0B800F6D281 /* Service */, DB8FABCB26AEC7B2008E5AF4 /* Info.plist */, ); path = MastodonIntent; @@ -3022,15 +2353,6 @@ path = ReportResult; sourceTree = ""; }; - DB9A489B26036E19008B817C /* MastodonAttachmentService */ = { - isa = PBXGroup; - children = ( - DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */, - DB9A48952603685D008B817C /* MastodonAttachmentService+UploadState.swift */, - ); - path = MastodonAttachmentService; - sourceTree = ""; - }; DB9D6BEE25E4F5370051B173 /* Search */ = { isa = PBXGroup; children = ( @@ -3198,40 +2520,15 @@ 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; 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 = ( @@ -3239,23 +2536,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 = ( @@ -3379,7 +2663,6 @@ DB68053E2638011000430867 /* NotificationService.entitlements */, DBF8AE15263293E400C9C23C /* NotificationService.swift */, DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */, - DB68045A2636DC6A00430867 /* MastodonPushNotification.swift */, DBCBCBF3267CB070000F5B51 /* Decode85.swift */, DBF8AE17263293E400C9C23C /* Info.plist */, ); @@ -3427,33 +2710,14 @@ isa = PBXGroup; children = ( DBFEF05426A576EE006D7ED1 /* View */, - DBC6462226A1712000B0E31B /* ShareViewModel.swift */, - DBC6461426A170AB00B0E31B /* ShareViewController.swift */, + DBC6462226A1712000B0E31B /* ComposeViewModel.swift */, + DBC6461426A170AB00B0E31B /* ComposeViewController.swift */, ); 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; @@ -3465,7 +2729,7 @@ DB427DCE25BAA00100D1B89D /* Sources */, DB427DCF25BAA00100D1B89D /* Frameworks */, DB89BA0825C10FD0008580ED /* Embed Frameworks */, - DBF8AE1B263293E400C9C23C /* Embed App Extensions */, + DBF8AE1B263293E400C9C23C /* Embed Foundation Extensions */, DB3D100425BAA71500EAA174 /* ShellScript */, DB025B8E278D6448002F581E /* ShellScript */, DB697DD2278F48D5004EF2F7 /* ShellScript */, @@ -3474,25 +2738,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 */; @@ -3537,33 +2788,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" */; @@ -3575,9 +2799,11 @@ buildRules = ( ); dependencies = ( - DB8FABDA26AEC873008E5AF4 /* PBXTargetDependency */, ); name = MastodonIntent; + packageProductDependencies = ( + DB22C92728E700B70082A9E9 /* MastodonSDK */, + ); productName = MastodonIntent; productReference = DB8FABC626AEC7B2008E5AF4 /* MastodonIntent.appex */; productType = "com.apple.product-type.app-extension"; @@ -3593,13 +2819,10 @@ buildRules = ( ); dependencies = ( - DBC6463626A195DB00B0E31B /* PBXTargetDependency */, ); name = ShareActionExtension; packageProductDependencies = ( - DB3EA90B281BBE9600598866 /* AlamofireImage */, - DB3EA90D281BBE9600598866 /* AlamofireNetworkActivityIndicator */, - DB3EA90F281BBE9600598866 /* Alamofire */, + DB22C92528E700AF0082A9E9 /* MastodonSDK */, ); productName = ShareActionExtension; productReference = DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */; @@ -3616,13 +2839,10 @@ buildRules = ( ); dependencies = ( - DB6804A82637CDCC00430867 /* PBXTargetDependency */, ); name = NotificationService; packageProductDependencies = ( - DB3EA905281BBE8200598866 /* AlamofireImage */, - DB3EA907281BBE8200598866 /* AlamofireNetworkActivityIndicator */, - DB3EA909281BBE8200598866 /* Alamofire */, + DB22C92128E700A10082A9E9 /* MastodonSDK */, ); productName = NotificationService; productReference = DBF8AE13263293E400C9C23C /* NotificationService.appex */; @@ -3635,7 +2855,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1250; - LastUpgradeCheck = 1250; + LastUpgradeCheck = 1400; TargetAttributes = { DB427DD125BAA00100D1B89D = { CreatedOnToolsVersion = 12.4; @@ -3649,10 +2869,6 @@ CreatedOnToolsVersion = 12.4; TestTargetID = DB427DD125BAA00100D1B89D; }; - DB68047E2637CD4C00430867 = { - CreatedOnToolsVersion = 12.4; - LastSwiftMigration = 1240; - }; DB8FABC526AEC7B2008E5AF4 = { CreatedOnToolsVersion = 12.5.1; }; @@ -3697,23 +2913,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 = ""; @@ -3722,7 +2921,6 @@ DB427DD125BAA00100D1B89D /* Mastodon */, DB427DE725BAA00100D1B89D /* MastodonTests */, DB427DF225BAA00100D1B89D /* MastodonUITests */, - DB68047E2637CD4C00430867 /* AppShared */, DBF8AE12263293E400C9C23C /* NotificationService */, DBC6461126A170AB00B0E31B /* ShareActionExtension */, DB8FABC526AEC7B2008E5AF4 /* MastodonIntent */, @@ -3761,13 +2959,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DB68047D2637CD4C00430867 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; DB8FABC426AEC7B2008E5AF4 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -3857,31 +3048,10 @@ 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; + alwaysOutOfDate = 1; + buildActionMask = 12; files = ( ); inputFileListPaths = ( @@ -3898,7 +3068,8 @@ }; DB3D100425BAA71500EAA174 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; + alwaysOutOfDate = 1; + buildActionMask = 12; files = ( ); inputFileListPaths = ( @@ -3915,7 +3086,8 @@ }; DB697DD2278F48D5004EF2F7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; + alwaysOutOfDate = 1; + buildActionMask = 12; files = ( ); inputFileListPaths = ( @@ -3981,41 +3153,25 @@ 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 */, 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 */, 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 */, @@ -4031,9 +3187,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 */, @@ -4049,27 +3203,20 @@ DB697DD9278F4CED004EF2F7 /* HomeTimelineViewController+DataSourceProvider.swift in Sources */, 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 */, 2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */, 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 */, @@ -4097,37 +3244,24 @@ 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 */, 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 */, @@ -4140,31 +3274,21 @@ 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 */, - 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 */, @@ -4172,16 +3296,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 */, - 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 */, @@ -4190,20 +3310,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 */, CD91FB31290EDA6F00BB9463 /* MetaTextView+PasteExtensions.swift in Sources */, @@ -4214,48 +3328,33 @@ 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 */, 5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */, 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 */, 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 */, - 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 */, @@ -4263,10 +3362,8 @@ 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 */, - DB336F28278D6EC70031E64B /* MastodonFieldContainer.swift in Sources */, DBCA0EBC282BB38A0029E2B0 /* PageboyNavigateable.swift in Sources */, DBF156E42702DB3F00EC00B7 /* HandleTapAction.swift in Sources */, DB98EB4727B0DFAA0082E365 /* ReportStatusViewModel+State.swift in Sources */, @@ -4274,8 +3371,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 */, @@ -4285,16 +3380,11 @@ 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 */, - 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 */, @@ -4305,8 +3395,6 @@ DB3E6FDD2806A40F00B035AE /* DiscoveryHashtagsViewController.swift in Sources */, 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 */, @@ -4320,15 +3408,12 @@ 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 */, - 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 */, @@ -4339,7 +3424,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 */, @@ -4348,79 +3432,48 @@ 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 */, 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 */, - 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 */, 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 */, @@ -4428,7 +3481,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,14 +3489,11 @@ 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 */, - 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 */, @@ -4455,15 +3504,10 @@ 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,36 +3517,24 @@ 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 */, - 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 */, - DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */, DB6180E326391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift in Sources */, 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; }; @@ -4523,28 +3555,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; @@ -4553,25 +3571,9 @@ isa = PBXSourcesBuildPhase; 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 */, + DBC6462326A1712000B0E31B /* ComposeViewModel.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 */, - DBC6461526A170AB00B0E31B /* ShareViewController.swift in Sources */, + DBC6461526A170AB00B0E31B /* ComposeViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4579,8 +3581,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 */, @@ -4601,36 +3601,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 */; @@ -4890,7 +3870,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4898,7 +3878,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 = ""; @@ -4920,7 +3900,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4928,7 +3908,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 = ""; @@ -5021,67 +4001,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 = { @@ -5154,7 +4073,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -5162,7 +4081,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 = ""; @@ -5215,44 +4134,13 @@ }; 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 = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5275,7 +4163,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5299,7 +4187,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5323,7 +4211,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5347,7 +4235,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5371,7 +4259,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5395,7 +4283,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5482,7 +4370,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -5490,7 +4378,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 = ""; @@ -5549,11 +4437,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 144; + DYLIB_CURRENT_VERSION = 147; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5578,7 +4466,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5601,7 +4489,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5625,7 +4513,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5650,7 +4538,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5674,7 +4562,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5739,17 +4627,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 = ( @@ -5785,269 +4662,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/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 @@ 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#^_ @@ -114,7 +102,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 22 + 20 MastodonIntents.xcscheme_^#shared#^_ @@ -129,12 +117,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 23 + 24 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 24 + 25 SuppressBuildableAutocreation @@ -164,6 +152,11 @@ primary + DB8FABC526AEC7B2008E5AF4 + + primary + + diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 29c81554a..34ffd227b 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,259 +1,257 @@ { - "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": "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": "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": "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": "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" : "commonoslog", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MainasuK/CommonOSLog", + "state" : { + "revision" : "c121624a30698e9886efe38aebb36ff51c01b6c2", + "version" : "0.1.1" + } + }, + { + "identity" : "faviconfinder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/will-lumley/FaviconFinder.git", + "state" : { + "revision" : "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a", + "version" : "3.3.0" + } + }, + { + "identity" : "flanimatedimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Flipboard/FLAnimatedImage.git", + "state" : { + "revision" : "e7f9fd4681ae41bf6f3056db08af4f401d61da52", + "version" : "1.0.16" + } + }, + { + "identity" : "fpsindicator", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MainasuK/FPSIndicator.git", + "state" : { + "revision" : "e4a5067ccd5293b024c767f09e51056afd4a4796", + "version" : "1.1.0" + } + }, + { + "identity" : "fuzi", + "kind" : "remoteSourceControl", + "location" : "https://github.com/cezheng/Fuzi.git", + "state" : { + "revision" : "f08c8323da21e985f3772610753bcfc652c2103f", + "version" : "3.1.3" + } + }, + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", + "state" : { + "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", + "version" : "4.2.2" + } + }, + { + "identity" : "kingfisher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Kingfisher.git", + "state" : { + "revision" : "44e891bdb61426a95e31492a67c7c0dfad1f87c5", + "version" : "7.4.1" + } + }, + { + "identity" : "metatextkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TwidereProject/MetaTextKit.git", + "state" : { + "revision" : "dcd5255d6930c2fab408dc8562c577547e477624", + "version" : "2.2.5" + } + }, + { + "identity" : "nuke", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kean/Nuke.git", + "state" : { + "revision" : "0ea7545b5c918285aacc044dc75048625c8257cc", + "version" : "10.8.0" + } + }, + { + "identity" : "nuke-flanimatedimage-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", + "state" : { + "revision" : "b59c346a7d536336db3b0f12c72c6e53ee709e16", + "version" : "8.0.0" + } + }, + { + "identity" : "pageboy", + "kind" : "remoteSourceControl", + "location" : "https://github.com/uias/Pageboy", + "state" : { + "revision" : "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6", + "version" : "3.6.2" + } + }, + { + "identity" : "panmodal", + "kind" : "remoteSourceControl", + "location" : "https://github.com/slackhq/PanModal.git", + "state" : { + "revision" : "b012aecb6b67a8e46369227f893c12544846613f", + "version" : "1.2.7" + } + }, + { + "identity" : "sdwebimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SDWebImage/SDWebImage.git", + "state" : { + "revision" : "2e63d0061da449ad0ed130768d05dceb1496de44", + "version" : "5.12.5" + } + }, + { + "identity" : "stripes", + "kind" : "remoteSourceControl", + "location" : "https://github.com/eneko/Stripes.git", + "state" : { + "revision" : "d533fd44b8043a3abbf523e733599173d6f98c11", + "version" : "0.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "f504716c27d2e5d4144fa4794b12129301d17729", + "version" : "1.0.3" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "546610d52b19be3e19935e0880bb06b9c03f5cef", + "version" : "1.14.4" + } + }, + { + "identity" : "swift-nio-zlib-support", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-zlib-support.git", + "state" : { + "revision" : "37760e9a52030bb9011972c5213c3350fa9d41fd", + "version" : "1.0.0" + } + }, + { + "identity" : "swiftsoup", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scinfu/SwiftSoup.git", + "state" : { + "revision" : "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886", + "version" : "2.4.2" + } + }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", + "state" : { + "revision" : "f2616860a41f9d9932da412a8978fec79c06fe24", + "version" : "0.1.4" + } + }, + { + "identity" : "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/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/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 82c58e1f6..8a0825969 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 @@ -19,7 +20,9 @@ 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! + + private(set) var authContext: AuthContext? let id = UUID().uuidString @@ -29,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 @@ -38,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) } } @@ -173,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) @@ -224,55 +214,61 @@ 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 request = MastodonAuthentication.activeSortedFetchRequest // use active order + 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) ) } + } 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 { 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 @@ -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/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/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/Discovery/DiscoverySection.swift b/Mastodon/Diffiable/Discovery/DiscoverySection.swift index 94e07c71b..225b6f46a 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 @@ -22,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 } @@ -72,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 97cf8ada0..387affbc7 100644 --- a/Mastodon/Diffiable/Notification/NotificationSection.swift +++ b/Mastodon/Diffiable/Notification/NotificationSection.swift @@ -14,6 +14,8 @@ import UIKit import MetaTextKit import MastodonMeta import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization enum NotificationSection: Equatable, Hashable { @@ -23,6 +25,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? @@ -73,21 +76,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/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..e5aa0a605 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 @@ -132,6 +133,7 @@ enum RecommendAccountSection: Equatable, Hashable { extension RecommendAccountSection { struct Configuration { + let authContext: AuthContext weak var suggestionAccountTableViewCellDelegate: SuggestionAccountTableViewCellDelegate? } @@ -149,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 69b9da234..ba3c5525a 100644 --- a/Mastodon/Diffiable/Report/ReportSection.swift +++ b/Mastodon/Diffiable/Report/ReportSection.swift @@ -13,6 +13,8 @@ import MastodonSDK import UIKit import os.log import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization enum ReportSection: Equatable, Hashable { @@ -22,6 +24,7 @@ enum ReportSection: Equatable, Hashable { extension ReportSection { struct Configuration { + let authContext: AuthContext } static func diffableDataSource( @@ -100,13 +103,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/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..8a5d7e75f 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 @@ -24,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? } @@ -98,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, @@ -119,7 +119,7 @@ extension SearchResultSection { cell: UserTableViewCell, viewModel: UserTableViewCell.ViewModel, configuration: Configuration - ) { + ) { cell.configure( tableView: tableView, viewModel: viewModel, 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..38b8e641f 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 { @@ -26,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? @@ -158,6 +160,7 @@ extension StatusSection { public static func setupStatusPollDataSource( context: AppContext, + authContext: AuthContext, statusView: StatusView ) { let managedObjectContext = context.managedObjectContext @@ -171,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 { @@ -211,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 ) } } @@ -247,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, @@ -276,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/Diffiable/User/UserSection.swift b/Mastodon/Diffiable/User/UserSection.swift index cb806c4e9..20812b7e8 100644 --- a/Mastodon/Diffiable/User/UserSection.swift +++ b/Mastodon/Diffiable/User/UserSection.swift @@ -9,8 +9,10 @@ import os.log import UIKit import CoreData import CoreDataStack -import MetaTextKit +import MastodonCore +import MastodonUI import MastodonMeta +import MetaTextKit enum UserSection: 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/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/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/Info.plist b/Mastodon/Info.plist index 31b425322..cfdbb923d 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -2,19 +2,6 @@ - NSAppTransportSecurity - - NSExceptionDomains - - onion - - NSExceptionAllowsInsecureHTTPLoads - - NSIncludesSubdomains - - - - CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion @@ -30,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleURLTypes @@ -43,7 +30,7 @@ CFBundleVersion - 144 + 147 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes @@ -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/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/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+Block.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift index e9a0b02c0..2747f4735 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift @@ -7,19 +7,19 @@ import UIKit import CoreDataStack +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 0c467778d..2c54653ba 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift @@ -8,19 +8,19 @@ import UIKit import CoreData import CoreDataStack +import MastodonCore extension DataSourceFacade { - static func responseToStatusBookmarkAction( - provider: DataSourceProvider, - status: ManagedObjectRecord, - authenticationBox: MastodonAuthenticationBox + public static func responseToStatusBookmarkAction( + provider: UIViewController & NeedsDependency & 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 fba4f697b..92945b9ee 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift @@ -8,19 +8,19 @@ import UIKit import CoreData import CoreDataStack +import MastodonCore extension DataSourceFacade { - static func responseToStatusFavoriteAction( - provider: DataSourceProvider, - status: ManagedObjectRecord, - authenticationBox: MastodonAuthenticationBox + public static func responseToStatusFavoriteAction( + 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 f0bc379ae..c6e40e7d9 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -8,31 +8,30 @@ import UIKit import CoreDataStack import class CoreDataStack.Notification +import MastodonCore import MastodonSDK 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() @@ -71,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 b5b2dec97..1db94bd4f 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift @@ -7,19 +7,19 @@ import UIKit import CoreDataStack +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 7eda84599..ff3e95820 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift @@ -7,20 +7,20 @@ import UIKit import CoreDataStack +import MastodonCore 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 8beaabbae..18d238c02 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift @@ -7,22 +7,23 @@ import Foundation import CoreDataStack +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, @@ -34,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() @@ -66,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 } @@ -92,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 4c948c716..23e2022ea 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 @@ -14,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 ) } @@ -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) } @@ -80,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 @@ -99,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, + kind: .reply(status: status) ) - provider.coordinator.present( + _ = provider.coordinator.present( scene: .compose(viewModel: composeViewModel), from: provider, transition: .modal(animated: true, completion: nil) @@ -116,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( @@ -154,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): @@ -180,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 } @@ -209,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 } @@ -224,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) @@ -245,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, @@ -254,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", @@ -269,8 +293,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..e7b55f91c 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,16 +94,15 @@ 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, + kind: .reply(status: status) ) - self.coordinator.present( + _ = self.coordinator.present( scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil) @@ -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 c00c36971..3a71e5346 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -8,9 +8,11 @@ import os.log import UIKit import CoreDataStack +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 83d0240f8..e0aaf97fc 100644 --- a/Mastodon/Scene/Account/AccountListViewModel.swift +++ b/Mastodon/Scene/Account/AccountListViewModel.swift @@ -5,50 +5,59 @@ // Created by Cirno MainasuK on 2021-9-13. // +import os.log import UIKit import Combine import CoreData import CoreDataStack import MastodonSDK 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 - - 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 + 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 + do { + try mastodonAuthenticationFetchedResultsController.performFetch() + authentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?.compactMap { $0.asRecrod } ?? [] + } catch { + assertionFailure(error.localizedDescription) } - .store(in: &disposeBag) - authentications + $authentications .receive(on: DispatchQueue.main) .sink { [weak self] authentications in guard let self = self else { return } @@ -56,7 +65,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) { @@ -74,7 +86,7 @@ extension AccountListViewModel { } enum Item: Hashable { - case authentication(objectID: NSManagedObjectID) + case authentication(record: ManagedObjectRecord) case addAccount } @@ -84,14 +96,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 @@ -107,7 +122,7 @@ extension AccountListViewModel { static func configure( cell: AccountListTableViewCell, authentication: MastodonAuthentication, - activeMastodonUserObjectID: AnyPublisher + activeAuthentication: MastodonAuthentication ) { let user = authentication.user @@ -136,19 +151,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, @@ -159,3 +169,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 20d7b26a1..e25c75b01 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 { @@ -21,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( @@ -63,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) } @@ -154,7 +158,7 @@ extension AccountListViewController { @objc private func addBarButtonItem(_ sender: UIBarButtonItem) { logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil)) + _ = coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil)) } @objc private func dragIndicatorTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { @@ -173,19 +177,17 @@ 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)) + _ = coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil)) } } } 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/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+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/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/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift index 8a00fccde..e5b043adb 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift @@ -9,68 +9,70 @@ import os.log import UIKit import Combine import MastodonAsset +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/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 6ca09eba0..bf9145d6c 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 @@ -29,6 +30,19 @@ final class ComposeViewController: UIViewController, NeedsDependency { let logger = Logger(subsystem: "ComposeViewController", category: "logic") + lazy var composeContentViewModel: ComposeContentViewModel = { + return ComposeContentViewModel( + context: context, + authContext: viewModel.authContext, + kind: viewModel.kind + ) + }() + private(set) lazy var composeContentViewController: ComposeContentViewController = { + let composeContentViewController = ComposeContentViewController() + composeContentViewController.viewModel = composeContentViewModel + 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() @@ -42,7 +56,7 @@ final class ComposeViewController: UIViewController, NeedsDependency { let barButtonItem = UIBarButtonItem(customView: characterCountLabel) return barButtonItem }() - + let publishButton: UIButton = { let button = RoundedEdgesButton(type: .custom) button.cornerRadius = 10 @@ -65,7 +79,6 @@ final class ComposeViewController: UIViewController, NeedsDependency { let barButtonItem = UIBarButtonItem(customView: shadowBackgroundContainer) return barButtonItem }() - private func configurePublishButtonApperance() { publishButton.adjustsImageWhenHighlighted = false publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color), for: .normal) @@ -73,68 +86,33 @@ 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 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 - 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) - 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() +// +// +// 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) @@ -160,31 +138,7 @@ 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 @@ -200,363 +154,281 @@ extension ComposeViewController { } .store(in: &disposeBag) publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) - - - tableView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(tableView) + + addChild(composeContentViewController) + composeContentViewController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(composeContentViewController.view) 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), - ]) - - 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), + 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) - tableView.delegate = self - viewModel.setupDataSource( - tableView: tableView, - metaTextDelegate: self, - metaTextViewDelegate: self, - customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel, - composeStatusAttachmentCollectionViewCellDelegate: self, - composeStatusPollOptionCollectionViewCellDelegate: self, - composeStatusPollOptionAppendEntryCollectionViewCellDelegate: self, - composeStatusPollExpiresOptionCollectionViewCellDelegate: self - ) +// configureNavigationBarTitleStyle() +// viewModel.traitCollectionDidChangePublisher +// .receive(on: DispatchQueue.main) +// .sink { [weak self] _ in +// guard let self = self else { return } +// self.configureNavigationBarTitleStyle() +// } +// .store(in: &disposeBag) +// +// viewModel.$title +// .receive(on: DispatchQueue.main) +// .sink { [weak self] title in +// guard let self = self else { return } +// self.title = title +// } +// .store(in: &disposeBag) +// +// composeToolbarView.translatesAutoresizingMaskIntoConstraints = false +// view.addSubview(composeToolbarView) +// composeToolbarViewBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: composeToolbarView.bottomAnchor) +// NSLayoutConstraint.activate([ +// composeToolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), +// composeToolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor), +// composeToolbarViewBottomLayoutConstraint, +// composeToolbarView.heightAnchor.constraint(equalToConstant: ComposeToolbarView.toolbarHeight), +// ]) +// composeToolbarView.preservesSuperviewLayoutMargins = true +// composeToolbarView.delegate = self +// +// composeToolbarBackgroundView.translatesAutoresizingMaskIntoConstraints = false +// view.insertSubview(composeToolbarBackgroundView, belowSubview: composeToolbarView) +// NSLayoutConstraint.activate([ +// composeToolbarBackgroundView.topAnchor.constraint(equalTo: composeToolbarView.topAnchor), +// composeToolbarBackgroundView.leadingAnchor.constraint(equalTo: composeToolbarView.leadingAnchor), +// composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor), +// view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor), +// ]) - 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) - - customEmojiPickerInputView.collectionView.delegate = self - viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView - viewModel.setupCustomEmojiPickerDiffableDataSource( - for: customEmojiPickerInputView.collectionView, - dependency: self - ) - - viewModel.composeStatusContentTableViewCell.delegate = self +// tableView.delegate = self +// viewModel.setupDataSource( +// tableView: tableView, +// metaTextDelegate: self, +// metaTextViewDelegate: self, +// customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel, +// composeStatusAttachmentCollectionViewCellDelegate: self, +// composeStatusPollOptionCollectionViewCellDelegate: self, +// composeStatusPollOptionAppendEntryCollectionViewCellDelegate: self, +// composeStatusPollExpiresOptionCollectionViewCellDelegate: self +// ) - // update layout when keyboard show/dismiss - view.layoutIfNeeded() +// 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) - 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 - }() +// 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() +// - 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) +// +// // 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) +// +// 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) { 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() { @@ -567,505 +439,450 @@ 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 - } +// 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 { +// +// 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 - } + 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) } @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 - } +// 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 +// guard viewModel.publishStateMachine.enter(ComposeViewModel.PublishState.Publishing.self) else { +// // TODO: handle error +// return +// } + + // 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 } - context.statusPublishService.publish(composeViewModel: viewModel) + 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: - 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 { -// MARK: - UITextViewDelegate -extension ComposeViewController: UITextViewDelegate { - - func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { - setupInputAssistantItem(item: textView.inputAssistantItem) - return true - } +// 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 +// } +// +//} - 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: - 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 { @@ -1079,15 +896,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) @@ -1095,386 +911,304 @@ 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 - } - } - -} - -extension ComposeViewController { - public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - - // Enable pasting images - if (action == #selector(UIResponderStandardEditActions.paste(_:))) { - return UIPasteboard.general.hasStrings || UIPasteboard.general.hasImages; - } - - return super.canPerformAction(action, withSender: sender); - } - - override func paste(_ sender: Any?) { - logger.debug("Paste event received") - - // Look for images on the clipboard - if (UIPasteboard.general.hasImages) { - if let images = UIPasteboard.general.images { - viewModel.attachmentServices = viewModel.attachmentServices + images.map({ image in - MastodonAttachmentService( - context: context, - image: image, - initialAuthenticationBox: viewModel.authenticationBox - ) - }) - } - } - } -} +//// 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 c638eb769..b3d8f52dc 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift @@ -9,505 +9,482 @@ 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 { - 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 162043064..45c9f1e93 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 @@ -27,159 +28,159 @@ final class ComposeViewModel: NSObject { // input let context: AppContext - let composeKind: ComposeStatusSection.ComposeKind - let authenticationBox: MastodonAuthenticationBox - + let authContext: AuthContext + let kind: ComposeContentViewModel.Kind - @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 +// 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, - authenticationBox: MastodonAuthenticationBox + authContext: AuthContext, + kind: ComposeContentViewModel.Kind ) { self.context = context - self.composeKind = composeKind - self.authenticationBox = authenticationBox - 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 authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value, - let author = authenticationBox.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 = authenticationBox.authenticationRecord.object(in: context.managedObjectContext) - else { - return - } - configuration = authentication.instance?.configuration - } - return configuration - }() - self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authenticationBox.domain) - super.init() - // end init + self.authContext = authContext + self.kind = kind - 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 { @@ -189,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 85c36fae0..000000000 --- a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// ComposeStatusAttachmentTableViewCell.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-29. -// - -import UIKit -import Combine -import AlamofireImage -import MastodonAsset -import MastodonLocalization - -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 = 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 - } - } - } - .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/AttachmentContainerView+EmptyStateView.swift b/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift index 1d32931af..d976cbff7 100644 --- a/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift +++ b/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift @@ -6,132 +6,132 @@ // import UIKit -import MastodonUI import MastodonAsset +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 4743c9527..4e8fe4e7c 100644 --- a/Mastodon/Scene/Compose/View/AttachmentContainerView.swift +++ b/Mastodon/Scene/Compose/View/AttachmentContainerView.swift @@ -6,160 +6,172 @@ // 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? - - 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 - }() - - override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} +//final class AttachmentContainerView: UIView { +// +// static let containerViewCornerRadius: CGFloat = 4 +// +// 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 +// }() +// +// 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() { - 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), - ]) +//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([ +// 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() +// } +// +//} +// +//extension AttachmentContainerView { +// +// private func setupBroader() { +// emptyStateView.layer.borderWidth = 1 +// emptyStateView.layer.borderColor = traitCollection.userInterfaceStyle == .dark ? ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor.cgColor : nil +// } +// +//} - setupBroader() - - emptyStateView.isHidden = true - activityIndicatorView.hidesWhenStopped = true - activityIndicatorView.startAnimating() - - descriptionTextView.delegate = self - } - - 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 - } - -} - -// 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/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/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/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..d592c3033 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 @@ -115,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 a3947e6ab..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))") } } } @@ -136,11 +134,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 +149,7 @@ extension DiscoveryCommunityViewModel.State { minID: nil, limit: 20 ), - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) let newMaxID = response.link?.maxID @@ -164,7 +157,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 +170,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 8911a506e..eaf8646c4 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 { @@ -21,6 +22,7 @@ final class DiscoveryCommunityViewModel { // input let context: AppContext + let authContext: AuthContext let viewDidAppeared = PassthroughSubject() let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -42,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: \.value, on: statusFetchedResultsController.domain) - .store(in: &disposeBag) - } deinit { diff --git a/Mastodon/Scene/Discovery/DiscoveryViewController.swift b/Mastodon/Scene/Discovery/DiscoveryViewController.swift index d94e6e592..33bbefaef 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 { @@ -24,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 dfeb16e2b..244a2e8d4 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 { @@ -17,6 +18,7 @@ final class DiscoveryViewModel { // input let context: AppContext + let authContext: AuthContext let discoveryPostsViewController: DiscoveryPostsViewController let discoveryHashtagsViewController: DiscoveryHashtagsViewController let discoveryNewsViewController: DiscoveryNewsViewController @@ -25,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 9f6368e63..ce1aadbb4 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 { @@ -100,6 +101,11 @@ extension DiscoveryForYouViewController { } +// MARK: - AuthContextProvider +extension DiscoveryForYouViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension DiscoveryForYouViewController: UITableViewDelegate { @@ -109,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 @@ -127,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 } @@ -156,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 a31022a7c..89122c06e 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 { @@ -19,6 +20,7 @@ final class DiscoveryForYouViewModel { // input let context: AppContext + let authContext: AuthContext let userFetchedResultsController: UserFetchedResultsController @MainActor @@ -29,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 { @@ -58,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 @@ -77,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 @@ -92,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 20ad408a2..e315f04ee 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 { @@ -106,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, @@ -216,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 1b119f3d7..7727a2358 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 { @@ -21,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 { @@ -68,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/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+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..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))") } } } @@ -136,11 +134,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 +141,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 2c4d89dc8..d440e9528 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 { @@ -19,6 +20,7 @@ final class DiscoveryNewsViewModel { // input let context: AppContext + let authContext: AuthContext let listBatchFetchViewModel = ListBatchFetchViewModel() // output @@ -40,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 { @@ -58,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 537ca1c58..ee3538885 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 { @@ -127,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 0ff6cbb14..3ed245e99 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift @@ -9,18 +9,15 @@ import os.log import Foundation import GameplayKit 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) { @@ -29,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 @@ -39,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))") } } } @@ -135,11 +134,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 @@ -147,7 +141,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 @@ -166,7 +160,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 +172,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..7a1b044fd 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 { @@ -19,6 +20,7 @@ final class DiscoveryPostsViewModel { // input let context: AppContext + let authContext: AuthContext let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -40,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: \.value, on: statusFetchedResultsController.domain) - .store(in: &disposeBag) - Task { await checkServerEndpoint() } // end Task @@ -67,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/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..42079a91e 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift @@ -12,6 +12,8 @@ import Combine import GameplayKit import CoreData import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization final class HashtagTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { @@ -165,17 +167,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, + 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)) } } +// 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 6b75d3875..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 @@ -135,12 +136,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 +143,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? = { @@ -167,7 +162,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..af4d2a01a 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") @@ -25,6 +26,7 @@ final class HashtagTimelineViewModel { // input let context: AppContext + let authContext: AuthContext let fetchedResultsController: StatusFetchedResultsController let isFetchingLatestTimeline = CurrentValueSubject(false) let timelinePredicate = CurrentValueSubject(nil) @@ -49,22 +51,17 @@ final class HashtagTimelineViewModel { stateMachine.enter(State.Initial.self) return stateMachine }() - 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: \.value, on: fetchedResultsController.domain) - .store(in: &disposeBag) } deinit { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 4fae66d33..d057c0376 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 @@ -80,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) @@ -149,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() }, @@ -332,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) { @@ -341,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) @@ -356,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) @@ -366,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() @@ -379,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, @@ -392,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 @@ -419,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 24b96f265..3efcd5cbe 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 { @@ -27,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() @@ -164,6 +165,7 @@ extension HomeTimelineViewController { tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) + // // layout publish progress publishProgressView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(publishProgressView) NSLayoutConstraint.activate([ @@ -203,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 { @@ -372,9 +376,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) @@ -383,14 +387,14 @@ 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)) } @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)) } @@ -402,14 +406,9 @@ 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() - self.coordinator.setupOnboardingIfNeeds(animated: true) } } @@ -492,6 +491,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..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 { @@ -21,6 +22,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 3e46c2af4..41cea6b58 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 { @@ -62,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 @@ -84,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..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))") } } } @@ -64,11 +60,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 +83,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 be7de3a5a..edd431e52 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -15,6 +15,8 @@ import CoreDataStack import GameplayKit import AlamofireImage import DateToolsSwift +import MastodonCore +import MastodonUI final class HomeTimelineViewModel: NSObject { @@ -25,6 +27,7 @@ final class HomeTimelineViewModel: NSObject { // input let context: AppContext + let authContext: AuthContext let fetchedResultsController: FeedFetchedResultsController let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -75,25 +78,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 @@ -130,7 +125,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() @@ -168,7 +162,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/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift b/Mastodon/Scene/HomeTimeline/View/HomeTimelineNavigationBarTitleViewModel.swift index 71b4dda8b..79f568edf 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,44 @@ 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 + 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 - 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) + } + .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(), @@ -81,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) } } @@ -149,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/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..00e2a9fc8 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 { @@ -146,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 @@ -296,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 @@ -306,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..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))") } } } @@ -63,11 +61,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 +86,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 c48ed1199..f4ce7c2d7 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 { @@ -20,6 +21,7 @@ final class NotificationTimelineViewModel { // input let context: AppContext + let authContext: AuthContext let scope: Scope let feedFetchedResultsController: FeedFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -46,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 { @@ -77,31 +70,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.MastodonNotificationScope static func feedPredicate( authenticationBox: MastodonAuthenticationBox, @@ -144,8 +114,6 @@ extension NotificationTimelineViewModel { // load lastest func loadLatest() async { - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } - isLoadingLatest = true defer { isLoadingLatest = false } @@ -153,7 +121,7 @@ extension NotificationTimelineViewModel { _ = try await context.apiService.notifications( maxID: nil, scope: scope, - authenticationBox: authenticationBox + authenticationBox: authContext.mastodonAuthenticationBox ) } catch { didLoadLatest.send() @@ -164,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)" @@ -185,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 0935c9967..665dffefa 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 { @@ -23,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() @@ -153,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 60967c436..2f2be1805 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 { @@ -18,6 +19,7 @@ final class NotificationViewModel { // input let context: AppContext + let authContext: AuthContext let viewDidLoad = PassthroughSubject() // output @@ -26,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/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..eb26f75be 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 @@ -181,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 @@ -281,7 +286,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..b8dbf994e 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift @@ -8,9 +8,11 @@ import UIKit import Combine import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization -final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell { +public final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell { let containerView: UIView = { let view = UIView() @@ -28,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/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/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/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 c97fc1489..e56c7f126 100644 --- a/Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift +++ b/Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift @@ -9,16 +9,14 @@ import os.log import UIKit import Combine import AuthenticationServices +import MastodonCore final class MastodonAuthenticationController { - static let callbackURLScheme = "mastodon" - static let callbackURL = "mastodon://joinmastodon.org/oauth" - var disposeBag = Set() // input - var context: AppContext! + var context: AppContext let authenticateURL: URL var authenticationSession: ASWebAuthenticationSession? @@ -43,7 +41,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/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..a2b8df83a 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 { @@ -143,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 74b13b1a8..e835027f3 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 { @@ -16,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/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..5edec2618 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift @@ -11,6 +11,8 @@ import AVKit import Combine import GameplayKit import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization final class BookmarkViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { @@ -133,6 +135,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 eda823b50..e86ee92cc 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift @@ -9,18 +9,15 @@ import os.log import Foundation import GameplayKit 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) { @@ -29,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 @@ -39,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))") } } } @@ -47,10 +46,10 @@ 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 viewModel.activeMastodonAuthenticationBox.value != nil + return true default: return false } @@ -72,7 +71,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) } @@ -131,26 +130,21 @@ 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 } - 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 - 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 +163,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..f56e65526 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 { @@ -17,7 +18,8 @@ final class BookmarkViewModel { // input let context: AppContext - let activeMastodonAuthenticationBox: CurrentValueSubject + let authContext: AuthContext + let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -36,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: \.value, on: statusFetchedResultsController.domain) - .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Profile/CachedProfileViewModel.swift b/Mastodon/Scene/Profile/CachedProfileViewModel.swift index c33a905a7..cdd572fc4 100644 --- a/Mastodon/Scene/Profile/CachedProfileViewModel.swift +++ b/Mastodon/Scene/Profile/CachedProfileViewModel.swift @@ -7,11 +7,12 @@ import Foundation import CoreDataStack +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 1e60f7a90..b6d6f8313 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 { @@ -74,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 544f8a062..40d4eb14f 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 @@ -16,6 +17,7 @@ final class FamiliarFollowersViewModel { // input let context: AppContext + let authContext: AuthContext let userFetchedResultsController: UserFetchedResultsController @Published var familiarFollowers: Mastodon.Entity.FamiliarFollowers? @@ -23,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 2ac1e2065..c15adcf83 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift @@ -14,6 +14,8 @@ import AVKit import Combine import GameplayKit import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization final class FavoriteViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { @@ -103,6 +105,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 6c539450c..803a9d45e 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift @@ -8,18 +8,15 @@ import os.log import Foundation import GameplayKit +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? @@ -29,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 @@ -39,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))") } } } @@ -50,7 +49,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 } @@ -72,7 +71,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) } @@ -133,10 +132,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 } @@ -146,11 +141,11 @@ extension FavoriteViewModel.State { do { let response = try await viewModel.context.apiService.favoritedStatuses( maxID: maxID, - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) 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 +164,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..0dd3c7203 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 { @@ -17,7 +18,7 @@ final class FavoriteViewModel { // input let context: AppContext - let activeMastodonAuthenticationBox: CurrentValueSubject + let authContext: AuthContext let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -36,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: \.value, on: statusFetchedResultsController.domain) - .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift index decc1ee97..190fa27e5 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift @@ -9,6 +9,8 @@ import os.log import UIKit import GameplayKit import Combine +import MastodonCore +import MastodonUI import MastodonLocalization final class FollowerListViewController: UIViewController, NeedsDependency { @@ -81,15 +83,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) { @@ -100,6 +102,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 a2958de3c..045def7b7 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift @@ -9,9 +9,10 @@ import os.log import Foundation import GameplayKit import MastodonSDK +import MastodonCore extension FollowerListViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "FollowerListViewModel.State", category: "StateMachine") @@ -29,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 @@ -39,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))") } } } @@ -50,7 +53,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 } @@ -138,12 +141,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 } @@ -153,7 +151,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 80f26e608..1b0d505b5 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 { @@ -19,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 = { @@ -39,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 c125b0214..e16b600c2 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift @@ -10,6 +10,8 @@ import UIKit import GameplayKit import Combine import MastodonLocalization +import MastodonCore +import MastodonUI final class FollowingListViewController: UIViewController, NeedsDependency { @@ -81,8 +83,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 @@ -100,6 +102,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..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))") } } } @@ -50,7 +48,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 +136,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 +146,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 f1e07f9d8..12b294b8b 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 { @@ -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 f35ac6aa4..c5dbbecd4 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 @@ -330,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 @@ -344,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 e28b250cf..65f15efa7 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 { @@ -23,6 +24,8 @@ final class ProfileHeaderViewModel { // input let context: AppContext + let authContext: AuthContext + @Published var user: MastodonUser? @Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none @@ -40,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/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..a2194758b 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 @@ -78,7 +79,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() diff --git a/Mastodon/Scene/Profile/MeProfileViewModel.swift b/Mastodon/Scene/Profile/MeProfileViewModel.swift index cee6d5e47..995e32002 100644 --- a/Mastodon/Scene/Profile/MeProfileViewModel.swift +++ b/Mastodon/Scene/Profile/MeProfileViewModel.swift @@ -10,14 +10,17 @@ import UIKit import Combine import CoreData import CoreDataStack +import MastodonCore 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/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..9dd06b22c 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 @@ -110,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 }() @@ -459,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 } @@ -484,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)) } @@ -498,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, @@ -512,26 +513,25 @@ 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) - coordinator.present(scene: .favorite(viewModel: favoriteViewModel), from: self, transition: .show) + 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) - coordinator.present(scene: .bookmark(viewModel: bookmarkViewModel), from: self, transition: .show) + 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, + 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) { @@ -670,6 +670,11 @@ extension ProfileViewController: TabBarPagerDataSource { // //} +// MARK: - AuthContextProvider +extension ProfileViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - ProfileHeaderViewControllerDelegate extension ProfileViewController: ProfileHeaderViewControllerDelegate { func profileHeaderViewController( @@ -759,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 @@ -783,8 +785,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { Task { try await DataSourceFacade.responseToUserMuteAction( dependency: self, - user: record, - authenticationBox: authenticationBox + user: record ) } } @@ -793,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 @@ -808,8 +808,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { Task { try await DataSourceFacade.responseToUserBlockAction( dependency: self, - user: record, - authenticationBox: authenticationBox + user: record ) } } @@ -851,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) @@ -865,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 9d64df6ee..e23b465d4 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 @@ -34,6 +35,7 @@ class ProfileViewModel: NSObject { // input let context: AppContext + let authContext: AuthContext @Published var me: MastodonUser? @Published var user: MastodonUser? @@ -57,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) ) @@ -79,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) @@ -131,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 } @@ -215,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 bb565c3e0..1e1388c95 100644 --- a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift +++ b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift @@ -10,17 +10,15 @@ import Foundation import Combine import CoreDataStack import MastodonSDK +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( @@ -53,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 } @@ -78,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 fb42b81b8..8a983da33 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 { @@ -102,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 ca798fa0b..4ed266c2e 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift @@ -8,19 +8,16 @@ import os.log import Foundation import GameplayKit +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) { @@ -29,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 @@ -39,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))") } } } @@ -72,7 +71,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,17 +129,13 @@ 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) return } - guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - stateMachine.enter(Fail.self) - return - } let queryFilter = viewModel.queryFilter Task { @@ -153,11 +148,11 @@ extension UserTimelineViewModel.State { excludeReplies: queryFilter.excludeReplies, excludeReblogs: queryFilter.excludeReblogs, onlyMedia: queryFilter.onlyMedia, - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) 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 +164,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..0d85b6807 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 { @@ -19,6 +20,7 @@ final class UserTimelineViewModel { // input let context: AppContext + let authContext: AuthContext let title: String let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -49,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: \.value, on: statusFetchedResultsController.domain) - .store(in: &disposeBag) } deinit { diff --git a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController.swift b/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController.swift index 0e2bc64ed..ebce374e7 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 { @@ -92,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 78988bb41..0688bcccb 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 { @@ -92,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 90b928235..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))") } } } @@ -137,10 +135,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 +146,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 ) } @@ -205,7 +199,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..2a0ed0271 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 { @@ -18,6 +19,7 @@ final class UserListViewModel { // input let context: AppContext + let authContext: AuthContext let kind: Kind let userFetchedResultsController: UserFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -36,23 +38,20 @@ final class UserListViewModel { return stateMachine }() - init( + 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 854ea96b6..f1418c5a1 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 { @@ -93,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 @@ -143,7 +145,7 @@ extension ReportViewController: ReportStatusViewControllerDelegate { } private func coordinateToReportSupplementary() { - coordinator.present( + _ = coordinator.present( scene: .reportSupplementary(viewModel: viewModel.reportSupplementaryViewModel), from: self, transition: .show @@ -169,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 @@ -183,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 81ba20125..c368ce42c 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 { @@ -27,6 +28,7 @@ class ReportViewModel { // input let context: AppContext + let authContext: AuthContext let user: ManagedObjectRecord let status: ManagedObjectRecord? @@ -36,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 @@ -73,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 ?? [] @@ -94,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/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..75021934b 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 @@ -154,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 957760f38..10dcdf373 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 { @@ -92,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 @@ -116,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 @@ -140,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 @@ -175,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 67d7475dd..8123a8773 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 @@ -22,6 +23,7 @@ class ReportResultViewModel: ObservableObject { // input let context: AppContext + let authContext: AuthContext let user: ManagedObjectRecord let isReported: Bool @@ -46,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/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..d45c196cd 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift @@ -10,6 +10,8 @@ import UIKit import Combine import CoreDataStack import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization protocol ReportStatusViewControllerDelegate: AnyObject { 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 c653fc4ad..01e8715d1 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift @@ -76,12 +76,8 @@ 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.value.last + let maxID = viewModel.statusFetchedResultsController.statusIDs.last Task { let managedObjectContext = viewModel.context.managedObjectContext @@ -102,11 +98,11 @@ extension ReportStatusViewModel.State { excludeReplies: true, excludeReblogs: true, onlyMedia: false, - authenticationBox: authenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) 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 +114,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..5b80a9f3a 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 { @@ -23,6 +24,7 @@ class ReportStatusViewModel { // input let context: AppContext + let authContext: AuthContext let user: ManagedObjectRecord let status: ManagedObjectRecord? let statusFetchedResultsController: StatusFetchedResultsController @@ -49,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 @@ -65,12 +69,7 @@ class ReportStatusViewModel { if let status = status { selectStatuses.append(status) } - - context.authenticationService.activeMastodonAuthenticationBox - .map { $0?.domain } - .assign(to: \.value, on: statusFetchedResultsController.domain) - .store(in: &disposeBag) - + $selectStatuses .map { statuses -> Bool in return status == nil ? !statuses.isEmpty : statuses.count > 1 diff --git a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift index fd7783170..e644c29ea 100644 --- a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift +++ b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift @@ -9,6 +9,8 @@ import os.log import UIKit import Combine import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization protocol ReportSupplementaryViewControllerDelegate: AnyObject { 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 c07ee1f54..a4239bbc4 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 { @@ -15,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() @@ -28,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 0058f5f6e..3f4758e8e 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) @@ -23,20 +24,22 @@ 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 = { 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 }() @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 } @@ -108,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 8970e2f29..c49dcc1a1 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,11 +18,13 @@ 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! + var authContext: AuthContext? + let composeButttonShadowBackgroundContainer = ShadowBackgroundContainer() let composeButton: UIButton = { let button = UIButton() @@ -33,6 +36,7 @@ class MainTabBarController: UITabBarController { button.layer.masksToBounds = true button.layer.cornerCurve = .continuous button.layer.cornerRadius = 8 + button.isAccessibilityElement = false return button }() @@ -102,18 +106,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() @@ -121,12 +131,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 @@ -142,9 +153,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) } @@ -177,7 +193,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() @@ -185,14 +201,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 @@ -220,45 +228,46 @@ 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 - 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 @@ -288,43 +297,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) @@ -363,15 +360,15 @@ 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 } + guard let authContext = self.authContext else { return } let composeViewModel = ComposeViewModel( context: context, - composeKind: .post, - authenticationBox: authenticationBox + 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)) } @objc private func tabBarLongPressGestureRecognizerHandler(_ sender: UILongPressGestureRecognizer) { @@ -393,7 +390,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 } @@ -504,6 +503,14 @@ 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 { @@ -717,26 +724,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, + 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)) } } diff --git a/Mastodon/Scene/Root/RootSplitViewController.swift b/Mastodon/Scene/Root/RootSplitViewController.swift index f19282936..d138f6006 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 { @@ -19,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 }() @@ -33,16 +37,21 @@ final class RootSplitViewController: UISplitViewController, NeedsDependency { let searchViewController = SearchViewController() searchViewController.context = context searchViewController.coordinator = coordinator + searchViewController.viewModel = .init( + context: context, + authContext: authContext + ) 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/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..70e1239b6 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 { @@ -190,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() } @@ -200,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, + kind: .post ) - 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 a6698d6c2..9f0eb1899 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 { @@ -20,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 @@ -29,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, @@ -41,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 } } @@ -80,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 } @@ -108,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 { @@ -134,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/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..32f3dbe3d 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 { @@ -29,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) @@ -48,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 }() @@ -92,6 +99,8 @@ extension SearchViewController { // collectionView: collectionView // ) + guard let discoveryViewController = self.discoveryViewController else { return } + addChild(discoveryViewController) discoveryViewController.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(discoveryViewController.view) @@ -142,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 b47bc2e88..51d614280 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 @@ -19,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/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..6ffc90182 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 { @@ -82,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 }() } @@ -130,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 @@ -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/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/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..52d0ffb9c 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 { @@ -108,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 c7a135964..1ec06ebe7 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 { @@ -16,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/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..67de62bf3 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 { @@ -152,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 b763547bf..9b12e1af0 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift @@ -9,17 +9,15 @@ import os.log import Foundation import GameplayKit 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) { @@ -28,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 @@ -38,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))") } } } @@ -72,11 +72,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 @@ -132,7 +127,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 @@ -156,7 +151,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..546920749 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 { @@ -19,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] = [] @@ -47,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: \.value, on: statusFetchedResultsController.domain) - .store(in: &disposeBag) - // Publishers.CombineLatest( // items, // statusFetchedResultsController.objectIDs.removeDuplicates() diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index 8455ac7d9..53a856fd0 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 { @@ -279,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) @@ -287,17 +289,14 @@ 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() - self.coordinator.setupOnboardingIfNeeds(animated: true) } } @@ -371,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) @@ -384,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 1eb9a4094..8d737b93b 100644 --- a/Mastodon/Scene/Settings/SettingsViewModel.swift +++ b/Mastodon/Scene/Settings/SettingsViewModel.swift @@ -13,15 +13,17 @@ import MastodonSDK import UIKit import os.log import AuthenticationServices +import MastodonCore 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() @@ -41,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 @@ -59,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/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..98d06fd92 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 @@ -161,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 99b0f3b6b..334c9ce15 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 { @@ -56,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/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/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/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..13c8311c7 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift @@ -12,6 +12,8 @@ import Foundation import OSLog import UIKit import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization class SuggestionAccountViewController: UIViewController, NeedsDependency { @@ -158,7 +160,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, @@ -168,6 +170,12 @@ extension SuggestionAccountViewController: UITableViewDelegate { } } +// MARK: - AuthContextProvider +extension SuggestionAccountViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + +// MARK: - SuggestionAccountTableViewCellDelegate extension SuggestionAccountViewController: SuggestionAccountTableViewCellDelegate { func suggestionAccountTableViewCell( _ cell: SuggestionAccountTableViewCell, @@ -176,7 +184,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): @@ -185,8 +192,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 0263b61ec..b8af80bb4 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 @@ -24,6 +25,7 @@ final class SuggestionAccountViewModel: NSObject { // input let context: AppContext + let authContext: AuthContext let userFetchedResultsController: UserFetchedResultsController let selectedUserFetchedResultsController: UserFetchedResultsController @@ -34,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, @@ -49,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 @@ -65,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 { @@ -89,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/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..00c29e157 100644 --- a/Mastodon/Scene/Thread/CachedThreadViewModel.swift +++ b/Mastodon/Scene/Thread/CachedThreadViewModel.swift @@ -7,12 +7,14 @@ import Foundation 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/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..e22b11961 100644 --- a/Mastodon/Scene/Thread/RemoteThreadViewModel.swift +++ b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift @@ -8,28 +8,27 @@ import os.log import UIKit import CoreDataStack +import MastodonCore import MastodonSDK 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 @@ -48,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 bd90fb370..6599f449e 100644 --- a/Mastodon/Scene/Thread/ThreadViewController.swift +++ b/Mastodon/Scene/Thread/ThreadViewController.swift @@ -12,6 +12,8 @@ import CoreData import AVKit import MastodonMeta import MastodonAsset +import MastodonCore +import MastodonUI import MastodonLocalization final class ThreadViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { @@ -111,13 +113,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, + kind: .reply(status: threadContext.status) ) - coordinator.present( + _ = coordinator.present( scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil) @@ -125,8 +126,10 @@ extension ThreadViewController { } } -//// MARK: - StatusTableViewControllerAspect -//extension ThreadViewController: StatusTableViewControllerAspect { } +// MARK: - AuthContextProvider +extension ThreadViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} // MARK: - UITableViewDelegate extension ThreadViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { @@ -177,7 +180,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 ededcb044..834d478e6 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift @@ -9,6 +9,8 @@ import UIKit import Combine import CoreData import CoreDataStack +import MastodonCore +import MastodonUI import MastodonSDK extension ThreadViewModel { @@ -22,6 +24,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..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))") } } } @@ -69,11 +67,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 +77,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 5a3127e66..735d85cd4 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 { @@ -25,6 +26,7 @@ class ThreadViewModel { // input let context: AppContext + let authContext: AuthContext let mastodonStatusThreadViewModel: MastodonStatusThreadViewModel // let cellFrameCache = NSCache() @@ -53,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/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..3bf38f6da 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 { @@ -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 54b1fd57e..d813f8ffd 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -9,6 +9,8 @@ import os.log import UIKit import Combine import CoreDataStack +import MastodonCore +import MastodonExtension #if PROFILE import FPSIndicator @@ -57,7 +59,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.coordinator = sceneCoordinator sceneCoordinator.setup() - sceneCoordinator.setupOnboardingIfNeeds(animated: false) window.makeKeyAndVisible() #if SNAPSHOT @@ -110,7 +111,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,30 +137,60 @@ 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: + 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 + ) + + 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…") } 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, + 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") } } + 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") @@ -166,6 +199,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 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/Info.plist b/MastodonIntent/Info.plist index 05a3cc313..3816b660b 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 + 147 NSExtension NSExtensionAttributes 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..852817c20 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 @@ -16,6 +16,7 @@ let package = Package( "CoreDataStack", "MastodonAsset", "MastodonCommon", + "MastodonCore", "MastodonExtension", "MastodonLocalization", "MastodonSDK", @@ -23,17 +24,32 @@ 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"), + .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. @@ -57,7 +73,23 @@ let package = Package( .target( name: "MastodonCommon", dependencies: [ - "MastodonExtension" + "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( @@ -78,20 +110,22 @@ 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"), + .product(name: "Stripes", package: "Stripes"), + .product(name: "Kingfisher", package: "Kingfisher"), ] ), .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/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 { 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/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/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/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.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/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.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/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/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/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/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 edbf78a0b..5cd0059d8 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift @@ -129,6 +129,22 @@ public enum Asset { public static let star = ImageAsset(name: "ObjectsAndTools/star") } public enum Scene { + public enum Compose { + public static let earth = ImageAsset(name: "Scene/Compose/Earth") + public static let mention = ImageAsset(name: "Scene/Compose/Mention") + public static let more = ImageAsset(name: "Scene/Compose/More") + public static let people = ImageAsset(name: "Scene/Compose/People") + 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 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 peopleAdd = ImageAsset(name: "Scene/Compose/people.add") + 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") } @@ -194,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") @@ -214,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/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 81% rename from AppShared/UserDefaults+Notification.swift rename to MastodonSDK/Sources/MastodonCommon/Preference/Preference+Notification.swift index e743e70a0..38ed3aa5e 100644 --- a/AppShared/UserDefaults+Notification.swift +++ b/MastodonSDK/Sources/MastodonCommon/Preference/Preference+Notification.swift @@ -1,12 +1,13 @@ // // UserDefaults+Notification.swift -// AppShared +// MastodonCommon // // Created by Cirno MainasuK on 2021-10-9. // 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 82% rename from Mastodon/State/AppContext.swift rename to MastodonSDK/Sources/MastodonCore/AppContext.swift index 683c81f33..d44c1ea5a 100644 --- a/Mastodon/State/AppContext.swift +++ b/MastodonSDK/Sources/MastodonCore/AppContext.swift @@ -1,44 +1,43 @@ // // 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() + public 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 publisherService: PublisherService + 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 +45,8 @@ class AppContext: ObservableObject { .autoconnect() .share() .eraseToAnyPublisher() - - init() { + + public init() { let _coreDataStack = CoreDataStack() let _managedObjectContext = _coreDataStack.persistentContainer.viewContext let _backgroundManagedObjectContext = _coreDataStack.persistentContainer.newBackgroundContext() @@ -69,6 +68,8 @@ class AppContext: ObservableObject { apiService: apiService ) + publisherService = .init(apiService: _apiService) + let _notificationService = NotificationService( apiService: _apiService, authenticationService: _authenticationService @@ -118,16 +119,16 @@ 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/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/AppShared/AppSecret.swift b/MastodonSDK/Sources/MastodonCore/AppSecret.swift similarity index 92% rename from AppShared/AppSecret.swift rename to MastodonSDK/Sources/MastodonCore/AppSecret.swift index 9110f2490..c409a2761 100644 --- a/AppShared/AppSecret.swift +++ b/MastodonSDK/Sources/MastodonCore/AppSecret.swift @@ -1,6 +1,6 @@ // // AppSecret.swift -// AppShared +// MastodonCore // // Created by MainasuK Cirno on 2021-4-27. // @@ -9,8 +9,8 @@ import Foundation import CryptoKit import KeychainAccess -import Keys import MastodonCommon +import ArkanaKeys 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/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 new file mode 100644 index 000000000..ec6cb0bfb --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift @@ -0,0 +1,46 @@ +// +// 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 + } +} + +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/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 89% rename from Mastodon/Extension/CoreDataStack/Instance.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Instance.swift index 6cacd9db9..4192b68a2 100644 --- a/Mastodon/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/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 50% rename from MastodonSDK/Sources/MastodonUI/Extension/CoreDataStack/MastodonUser.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser.swift index 9b61ab5f6..6d952726c 100644 --- a/MastodonSDK/Sources/MastodonUI/Extension/CoreDataStack/MastodonUser.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser.swift @@ -1,13 +1,14 @@ // // 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 +import MastodonMeta extension MastodonUser { @@ -55,3 +56,47 @@ 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 + } + +} + +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/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 89% rename from Mastodon/Extension/CoreDataStack/Setting.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Setting.swift index 4d1fc0ca5..bb76b69fb 100644 --- a/Mastodon/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/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 88% rename from Mastodon/Extension/CoreDataStack/Status.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status.swift index 2e0cf516a..797abac7f 100644 --- a/Mastodon/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/Mastodon/Extension/CoreDataStack/Subscription.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Subscription.swift similarity index 70% rename from Mastodon/Extension/CoreDataStack/Subscription.swift rename to MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Subscription.swift index 8253264a0..67f3709d1 100644 --- a/Mastodon/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/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/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/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/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift similarity index 84% rename from Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift rename to MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift index 09bbb3d8a..059f09420 100644 --- a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift @@ -1,11 +1,11 @@ // // Mastodon+Entity+Account.swift -// Mastodon +// // -// Created by xiaojian sun on 2021/4/2. +// Created by MainasuK on 2022-5-16. // -import UIKit +import Foundation import MastodonSDK import MastodonMeta @@ -19,27 +19,25 @@ extension Mastodon.Entity.Account: Hashable { } } -extension Mastodon.Entity.Account { - - var displayNameWithFallback: String { - return !displayName.isEmpty ? displayName : username - } - -} - -extension Mastodon.Entity.Account { +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 { + 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() 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/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/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/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/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/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 78% rename from Mastodon/Diffiable/FetchedResultsController/SearchHistoryFetchedResultController.swift rename to MastodonSDK/Sources/MastodonCore/FetchedResultsController/SearchHistoryFetchedResultController.swift index c3521c6fe..196c9b8f5 100644 --- a/Mastodon/Diffiable/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/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 79% rename from Mastodon/Diffiable/FetchedResultsController/StatusFetchedResultsController.swift rename to MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift index 24d8a6790..c08673acb 100644 --- a/Mastodon/Diffiable/FetchedResultsController/StatusFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift @@ -11,24 +11,23 @@ import Combine import CoreData import CoreDataStack import MastodonSDK -import MastodonUI -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: []) @@ -54,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 @@ -79,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/Mastodon/Diffiable/FetchedResultsController/UserFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift similarity index 87% rename from Mastodon/Diffiable/FetchedResultsController/UserFetchedResultsController.swift rename to MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift index c0922afcb..d95a62bbb 100644 --- a/Mastodon/Diffiable/FetchedResultsController/UserFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift @@ -11,24 +11,23 @@ import Combine import CoreData import CoreDataStack import MastodonSDK -import MastodonUI -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? @@ -97,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/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 100% rename from Mastodon/Diffiable/Compose/ComposeStatusAttachmentItem.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusAttachmentItem.swift diff --git a/Mastodon/Diffiable/Compose/ComposeStatusAttachmentSection.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusAttachmentSection.swift similarity index 99% rename from Mastodon/Diffiable/Compose/ComposeStatusAttachmentSection.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusAttachmentSection.swift index 4de7653a5..2e2a94206 100644 --- a/Mastodon/Diffiable/Compose/ComposeStatusAttachmentSection.swift +++ b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusAttachmentSection.swift @@ -10,4 +10,3 @@ import Foundation enum ComposeStatusAttachmentSection: Hashable { case main } - diff --git a/Mastodon/Diffiable/Compose/ComposeStatusItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusItem.swift similarity index 100% rename from Mastodon/Diffiable/Compose/ComposeStatusItem.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusItem.swift 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 45ed86783..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 { - 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/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/Model/Poll/PollComposeItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeItem.swift new file mode 100644 index 000000000..384cb49d2 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Model/Poll/PollComposeItem.swift @@ -0,0 +1,114 @@ +// +// 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? + + // 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) + } + } +} + +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: 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 + } + + 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/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/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 90% rename from Mastodon/Service/APIService/APIService+Account.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index 11da2f4ee..68649d24c 100644 --- a/Mastodon/Service/APIService/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -9,11 +9,12 @@ import os.log import Foundation import Combine import CommonOSLog +import MastodonCommon import MastodonSDK extension APIService { - func accountInfo( + public func accountInfo( domain: String, userID: Mastodon.Entity.Account.ID, authorization: Mastodon.API.OAuth.Authorization @@ -49,7 +50,7 @@ extension APIService { extension APIService { - func accountVerifyCredentials( + public func accountVerifyCredentials( domain: String, authorization: Mastodon.API.OAuth.Authorization ) -> AnyPublisher, Error> { @@ -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 @@ -90,12 +91,12 @@ extension APIService { .eraseToAnyPublisher() } - func accountUpdateCredentials( + public func accountUpdateCredentials( domain: String, 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, @@ -124,7 +125,7 @@ extension APIService { return response } - func accountRegister( + public func accountRegister( domain: String, query: Mastodon.API.Account.RegisterQuery, authorization: Mastodon.API.OAuth.Authorization @@ -137,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/Mastodon/Service/APIService/APIService+App.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+App.swift similarity index 78% rename from Mastodon/Service/APIService/APIService+App.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+App.swift index 7153bc2af..82d814294 100644 --- a/Mastodon/Service/APIService/APIService+App.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+App.swift @@ -21,10 +21,10 @@ 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: 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 95% rename from Mastodon/Service/APIService/APIService+Authentication.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Authentication.swift index ffd9afd77..d4d096f49 100644 --- a/Mastodon/Service/APIService/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/Mastodon/Service/APIService/APIService+Block.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift similarity index 99% rename from Mastodon/Service/APIService/APIService+Block.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift index 428401703..7c78a65f7 100644 --- a/Mastodon/Service/APIService/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/Mastodon/Service/APIService/APIService+Bookmark.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift similarity index 98% rename from Mastodon/Service/APIService/APIService+Bookmark.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift index 8100b3b58..0d8c243f8 100644 --- a/Mastodon/Service/APIService/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/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 98% rename from Mastodon/Service/APIService/APIService+Favorite.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift index 6ae2254e3..4abe9ba5f 100644 --- a/Mastodon/Service/APIService/APIService+Favorite.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift @@ -21,7 +21,7 @@ extension APIService { let favoritedCount: Int64 } - func favorite( + public func favorite( record: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { @@ -108,7 +108,7 @@ extension APIService { } extension APIService { - func favoritedStatuses( + public func favoritedStatuses( limit: Int = onceRequestStatusMaxCount, maxID: String? = nil, authenticationBox: MastodonAuthenticationBox @@ -152,7 +152,7 @@ extension APIService { } extension APIService { - func favoritedBy( + public func favoritedBy( status: ManagedObjectRecord, query: Mastodon.API.Statuses.FavoriteByQuery, authenticationBox: MastodonAuthenticationBox 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 99% rename from Mastodon/Service/APIService/APIService+Follow.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index 1e908a2e4..cfb5b8ee2 100644 --- a/Mastodon/Service/APIService/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/Mastodon/Service/APIService/APIService+FollowRequest.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift similarity index 97% rename from Mastodon/Service/APIService/APIService+FollowRequest.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift index 5c91d282c..f0f0bfb7b 100644 --- a/Mastodon/Service/APIService/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/Mastodon/Service/APIService/APIService+Follower.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift similarity index 98% rename from Mastodon/Service/APIService/APIService+Follower.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift index f0350013f..0f0ea1a59 100644 --- a/Mastodon/Service/APIService/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/Mastodon/Service/APIService/APIService+Following.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift similarity index 98% rename from Mastodon/Service/APIService/APIService+Following.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift index d0cdc233f..313d715ed 100644 --- a/Mastodon/Service/APIService/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/Mastodon/Service/APIService/APIService+HashtagTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift similarity index 97% rename from Mastodon/Service/APIService/APIService+HashtagTimeline.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift index ce8783895..d2fbe844a 100644 --- a/Mastodon/Service/APIService/APIService+HashtagTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift @@ -10,12 +10,11 @@ import Combine import CoreData import CoreDataStack import CommonOSLog -import DateToolsSwift 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/Mastodon/Service/APIService/APIService+HomeTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift similarity index 98% rename from Mastodon/Service/APIService/APIService+HomeTimeline.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift index 863510af4..4e65aeadc 100644 --- a/Mastodon/Service/APIService/APIService+HomeTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift @@ -10,12 +10,11 @@ import Combine import CoreData import CoreDataStack import CommonOSLog -import DateToolsSwift 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/Mastodon/Service/APIService/APIService+Instance.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift similarity index 91% rename from Mastodon/Service/APIService/APIService+Instance.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift index 4bc613b55..93bfcf09a 100644 --- a/Mastodon/Service/APIService/APIService+Instance.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift @@ -10,12 +10,11 @@ import Combine import CoreData import CoreDataStack import CommonOSLog -import DateToolsSwift 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/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 99% rename from Mastodon/Service/APIService/APIService+Mute.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift index c93dbcf6f..ee43ddce8 100644 --- a/Mastodon/Service/APIService/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/Mastodon/Service/APIService/APIService+Notification.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift similarity index 85% rename from Mastodon/Service/APIService/APIService+Notification.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift index 88c69847b..31b6509de 100644 --- a/Mastodon/Service/APIService/APIService+Notification.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift @@ -14,9 +14,36 @@ import OSLog import class CoreDataStack.Notification extension APIService { - func notifications( + + public enum MastodonNotificationScope: Hashable, CaseIterable { + case everything + case mentions + + public var includeTypes: [MastodonNotificationType]? { + switch self { + case .everything: return nil + case .mentions: return [.mention, .status] + } + } + + public var excludeTypes: [MastodonNotificationType]? { + switch self { + case .everything: return nil + case .mentions: return [.follow, .followRequest, .reblog, .favourite, .poll] + } + } + + public var _excludeTypes: [Mastodon.Entity.Notification.NotificationType]? { + switch self { + case .everything: return nil + case .mentions: return [.follow, .followRequest, .reblog, .favourite, .poll] + } + } + } + + public 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 @@ -132,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/Mastodon/Service/APIService/APIService+Onboarding.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Onboarding.swift similarity index 78% rename from Mastodon/Service/APIService/APIService+Onboarding.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Onboarding.swift index 5cbf455a0..383763359 100644 --- a/Mastodon/Service/APIService/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/Mastodon/Service/APIService/APIService+Poll.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift similarity index 98% rename from Mastodon/Service/APIService/APIService+Poll.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift index 15a6847c7..5a4b38b29 100644 --- a/Mastodon/Service/APIService/APIService+Poll.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift @@ -10,12 +10,11 @@ import Combine import CoreData import CoreDataStack import CommonOSLog -import DateToolsSwift import MastodonSDK extension APIService { - func poll( + public func poll( poll: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { @@ -56,7 +55,7 @@ extension APIService { extension APIService { - func vote( + public func vote( poll: ManagedObjectRecord, choices: [Int], authenticationBox: MastodonAuthenticationBox diff --git a/Mastodon/Service/APIService/APIService+PublicTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift similarity index 98% rename from Mastodon/Service/APIService/APIService+PublicTimeline.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift index fd0c2f0cd..21f198299 100644 --- a/Mastodon/Service/APIService/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/Mastodon/Service/APIService/APIService+Reblog.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift similarity index 99% rename from Mastodon/Service/APIService/APIService+Reblog.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift index 2542636c4..0dc1a40e5 100644 --- a/Mastodon/Service/APIService/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/Mastodon/Service/APIService/APIService+Recommend.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Recommend.swift similarity index 97% rename from Mastodon/Service/APIService/APIService+Recommend.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Recommend.swift index f88d82305..14255fc82 100644 --- a/Mastodon/Service/APIService/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/Mastodon/Service/APIService/APIService+Relationship.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift similarity index 98% rename from Mastodon/Service/APIService/APIService+Relationship.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift index a852eaf67..f5c108725 100644 --- a/Mastodon/Service/APIService/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/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 97% rename from Mastodon/Service/APIService/APIService+Status.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift index cf6974fbd..52b169ee7 100644 --- a/Mastodon/Service/APIService/APIService+Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift @@ -10,12 +10,11 @@ import Combine import CoreData import CoreDataStack import CommonOSLog -import DateToolsSwift import MastodonSDK extension APIService { - func status( + public func status( statusID: Mastodon.Entity.Status.ID, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { @@ -48,7 +47,7 @@ extension APIService { return response } - func deleteStatus( + public func deleteStatus( status: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { 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 98% rename from Mastodon/Service/APIService/APIService+Thread.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift index f6c36e5b6..ddd782856 100644 --- a/Mastodon/Service/APIService/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/Mastodon/Service/APIService/APIService+Trend.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Trend.swift similarity index 95% rename from Mastodon/Service/APIService/APIService+Trend.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+Trend.swift index 47dda6bd2..5432b02a0 100644 --- a/Mastodon/Service/APIService/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/Mastodon/Service/APIService/APIService+UserTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift similarity index 98% rename from Mastodon/Service/APIService/APIService+UserTimeline.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift index 85b8d6153..fdf90a2aa 100644 --- a/Mastodon/Service/APIService/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/Mastodon/Service/APIService/APIService+WebFinger.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+WebFinger.swift similarity index 95% rename from Mastodon/Service/APIService/APIService+WebFinger.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService+WebFinger.swift index b49ad9e31..b542cbe8d 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 { @@ -22,7 +21,7 @@ extension APIService { .appendingPathComponent("webfinger") } - func webFinger( + public func webFinger( domain: String ) -> AnyPublisher { let url = APIService.webFingerEndpointURL(domain: domain) diff --git a/Mastodon/Service/APIService/APIService.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService.swift similarity index 64% rename from Mastodon/Service/APIService/APIService.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/APIService.swift index dabdadfea..44eb9938e 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() @@ -22,12 +25,12 @@ final class APIService { let session: URLSession // input - let backgroundManagedObjectContext: NSManagedObjectContext + public let backgroundManagedObjectContext: NSManagedObjectContext // output - let error = PassthroughSubject() + public 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 97% rename from Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonAuthentication.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+MastodonAuthentication.swift index 15624f71d..1acb52a77 100644 --- a/Mastodon/Service/APIService/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/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 96% rename from Mastodon/Service/APIService/CoreData/APIService+CoreData+Subscriptions.swift rename to MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+Subscriptions.swift index d5958cf8f..ec8eec8b2 100644 --- a/Mastodon/Service/APIService/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/Mastodon/Service/AuthenticationService.swift b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift similarity index 51% rename from Mastodon/Service/AuthenticationService.swift rename to MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift index 97afde932..48da254c6 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,8 @@ 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) + @Published public var mastodonAuthentications: [ManagedObjectRecord] = [] + @Published public var mastodonAuthenticationBoxes: [MastodonAuthenticationBox] = [] init( managedObjectContext: NSManagedObjectContext, @@ -53,38 +51,23 @@ 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,26 @@ final class AuthenticationService: NSObject { extension AuthenticationService { - 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 } - func signOutMastodonUser( - authenticationBox: MastodonAuthenticationBox - ) async throws { + public func signOutMastodonUser(authenticationBox: MastodonAuthenticationBox) async throws { let managedObjectContext = backgroundManagedObjectContext try await managedObjectContext.performChanges { // remove Feed @@ -176,19 +133,22 @@ 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) { - if controller === mastodonAuthenticationFetchedResultsController { - mastodonAuthentications.value = mastodonAuthenticationFetchedResultsController.fetchedObjects ?? [] + public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + guard controller === mastodonAuthenticationFetchedResultsController else { + assertionFailure() + return } + + mastodonAuthentications = mastodonAuthenticationFetchedResultsController.fetchedObjects? + .sorted(by: { $0.activedAt > $1.activedAt }) + .compactMap { $0.asRecrod } ?? [] } } - diff --git a/Mastodon/Service/BlockDomainService.swift b/MastodonSDK/Sources/MastodonCore/Service/BlockDomainService.swift similarity index 83% rename from Mastodon/Service/BlockDomainService.swift rename to MastodonSDK/Sources/MastodonCore/Service/BlockDomainService.swift index 90d860143..02b8bdccd 100644 --- a/Mastodon/Service/BlockDomainService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/BlockDomainService.swift @@ -13,7 +13,8 @@ import MastodonSDK import OSLog import UIKit -final class BlockDomainService { +public final class BlockDomainService { + // input weak var backgroundManagedObjectContext: NSManagedObjectContext? weak var authenticationService: AuthenticationService? @@ -27,21 +28,21 @@ 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/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 84% rename from Mastodon/Service/EmojiService/EmojiService+CustomEmojiViewModel.swift rename to MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService+CustomEmojiViewModel.swift index b0ee6cb80..38e1e1f7a 100644 --- a/Mastodon/Service/EmojiService/EmojiService+CustomEmojiViewModel.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService+CustomEmojiViewModel.swift @@ -9,15 +9,16 @@ import Foundation import Combine import GameplayKit import MastodonSDK +import MastodonCommon extension EmojiService { - final class CustomEmojiViewModel { + public final class CustomEmojiViewModel { 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 = { @@ -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 89% rename from Mastodon/Service/EmojiService/EmojiService.swift rename to MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService.swift index a11ebb8bc..f912217d4 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? @@ -26,7 +26,7 @@ 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/Mastodon/Service/EmojiService/Trie.swift b/MastodonSDK/Sources/MastodonCore/Service/Emoji/Trie.swift similarity index 75% rename from Mastodon/Service/EmojiService/Trie.swift rename to MastodonSDK/Sources/MastodonCore/Service/Emoji/Trie.swift index 3bf9eeaf0..85f948599 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,26 +40,20 @@ extension Trie { } } -//extension Array { -// var slice: ArraySlice { -// return ArraySlice(self) -// } -//} - 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) @@ -67,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 @@ -89,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] } @@ -102,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/Mastodon/Service/InstanceService.swift b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift similarity index 94% rename from Mastodon/Service/InstanceService.swift rename to MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift index 03b8dfd4e..c63e965bd 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() @@ -33,9 +33,9 @@ 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 } @@ -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 87% rename from Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift rename to MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService+UploadState.swift index 8ff076dc1..c4a403fc9 100644 --- a/Mastodon/Service/MastodonAttachmentService/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/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift b/MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService.swift similarity index 89% rename from Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift rename to MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService.swift index e42c9bf2e..1af18efbe 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/MastodonAttachment/MastodonAttachmentService.swift @@ -12,31 +12,30 @@ import PhotosUI import GameplayKit import MobileCoreServices import MastodonSDK -import MastodonUI -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), @@ -48,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? @@ -88,7 +87,7 @@ final class MastodonAttachmentService { .store(in: &disposeBag) } - init( + public init( context: AppContext, image: UIImage, initialAuthenticationBox: MastodonAuthenticationBox? @@ -103,7 +102,7 @@ final class MastodonAttachmentService { uploadStateMachine.enter(UploadState.Initial.self) } - init( + public init( context: AppContext, documentURL: URL, initialAuthenticationBox: MastodonAuthenticationBox? @@ -184,7 +183,7 @@ final class MastodonAttachmentService { } extension MastodonAttachmentService { - enum AttachmentError: Error { + public enum AttachmentError: Error { case invalidAttachmentType case attachmentTooLarge } @@ -200,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/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 75% rename from Mastodon/Service/NotificationService.swift rename to MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift index e4e7508a3..65d92fc29 100644 --- a/Mastodon/Service/NotificationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift @@ -11,9 +11,12 @@ import Combine import CoreData import CoreDataStack import MastodonSDK -import AppShared +import MastodonCommon +import MastodonLocalization -final class NotificationService { +public final class NotificationService { + + public static let unreadShortcutItemIdentifier = "org.joinmastodon.app.NotificationService.unread-shortcut" var disposeBag = Set() @@ -22,15 +25,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, @@ -39,7 +42,7 @@ 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 } @@ -55,25 +58,29 @@ 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) 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 UIApplication.shared.applicationIconBadgeNumber = count + Task { @MainActor in + UIApplication.shared.shortcutItems = try? await self.unreadApplicationShortcutItems() + } self.unreadNotificationCountDidUpdate.send() } @@ -100,6 +107,37 @@ 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 { + 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( @@ -121,7 +159,7 @@ extension NotificationService { return _notificationSubscription } - func handle( + public func handle( pushNotification: MastodonPushNotification ) { defer { @@ -140,9 +178,9 @@ extension NotificationService { } extension NotificationService { - func clearNotificationCountForActiveUser() { + 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) } @@ -239,7 +277,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/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/Mastodon/Service/SettingService.swift b/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift similarity index 82% rename from Mastodon/Service/SettingService.swift rename to MastodonSDK/Sources/MastodonCore/Service/SettingService.swift index bd571d8f4..48c66baef 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() @@ -27,7 +27,7 @@ final class SettingService { // output let settingFetchedResultController: SettingFetchedResultController - let currentSetting = CurrentValueSubject(nil) + public let currentSetting = CurrentValueSubject(nil) init( apiService: APIService, @@ -43,7 +43,7 @@ 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 @@ 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 } @@ -108,17 +108,19 @@ final class SettingService { }) } .store(in: &disposeBag) - + + let logger = Logger(subsystem: "Notification", category: "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 } @@ -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) }) @@ -174,7 +176,7 @@ 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/Mastodon/Service/StatusFilterService.swift b/MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift similarity index 86% rename from Mastodon/Service/StatusFilterService.swift rename to MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift index b5afd08ab..e752a022e 100644 --- a/Mastodon/Service/StatusFilterService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift @@ -13,17 +13,17 @@ import CoreDataStack import MastodonSDK import MastodonMeta -final class StatusFilterService { +public final class StatusFilterService { var disposeBag = Set() // 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, @@ -44,13 +44,12 @@ 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/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 95% rename from MastodonSDK/Sources/MastodonUI/Service/ThemeService/MastodonTheme.swift rename to MastodonSDK/Sources/MastodonCore/Service/Theme/MastodonTheme.swift index 76173590e..563c3d48a 100644 --- a/MastodonSDK/Sources/MastodonUI/Service/ThemeService/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/MastodonUI/Service/ThemeService/SystemTheme.swift b/MastodonSDK/Sources/MastodonCore/Service/Theme/SystemTheme.swift similarity index 95% rename from MastodonSDK/Sources/MastodonUI/Service/ThemeService/SystemTheme.swift rename to MastodonSDK/Sources/MastodonCore/Service/Theme/SystemTheme.swift index cea10a281..ab297780b 100644 --- a/MastodonSDK/Sources/MastodonUI/Service/ThemeService/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/MastodonUI/Service/ThemeService/Theme.swift b/MastodonSDK/Sources/MastodonCore/Service/Theme/Theme.swift similarity index 96% rename from MastodonSDK/Sources/MastodonUI/Service/ThemeService/Theme.swift rename to MastodonSDK/Sources/MastodonCore/Service/Theme/Theme.swift index ae555da00..bfe8e9c6e 100644 --- a/MastodonSDK/Sources/MastodonUI/Service/ThemeService/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/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/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" = "ได้ชื่นชอบโพสต์ของคุณ"; 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/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 + } } } 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/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/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 1a2bf45f0..3022899f1 100644 --- a/Mastodon/Diffiable/Compose/AutoCompleteSection.swift +++ b/MastodonSDK/Sources/MastodonUI/DataSource/AutoCompleteSection+Diffable.swift @@ -1,24 +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 MastodonCore import MastodonSDK -import MastodonMeta -import MastodonAsset import MastodonLocalization - -enum AutoCompleteSection: Equatable, Hashable { - case main -} +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/MastodonSDK/Sources/MastodonUI/Extension/MastodonSDK/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonUI/Extension/MastodonSDK/Mastodon+Entity+Account.swift deleted file mode 100644 index 2441ebfd0..000000000 --- a/MastodonSDK/Sources/MastodonUI/Extension/MastodonSDK/Mastodon+Entity+Account.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// Mastodon+Entity+Account.swift -// -// -// Created by MainasuK on 2022-5-16. -// - -import Foundation -import MastodonSDK - -extension Mastodon.Entity.Account { - public var displayNameWithFallback: String { - if displayName.isEmpty { - return username - } else { - return displayName - } - } -} - -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")! - } -} 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/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/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/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 6b71c5dd2..aa21057d1 100644 --- a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/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) @@ -87,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 91% rename from Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+State.swift index 632b57b66..b1f5f3187 100644 --- a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+State.swift @@ -9,18 +9,15 @@ import os.log import Foundation import GameplayKit 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) { @@ -29,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 @@ -39,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))") } } } @@ -132,11 +131,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 @@ -153,7 +147,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/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift similarity index 83% rename from Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift index 3110f93e3..61715cd63 100644 --- a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift @@ -9,6 +9,7 @@ import UIKit import Combine import GameplayKit import MastodonSDK +import MastodonCore final class AutoCompleteViewModel { @@ -16,13 +17,14 @@ final class AutoCompleteViewModel { // input let context: AppContext - let inputText = CurrentValueSubject("") // contains "@" or "#" prefix - let symbolBoundingRect = CurrentValueSubject(.zero) - let customEmojiViewModel = CurrentValueSubject(nil) + let authContext: AuthContext + 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: [ @@ -35,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/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 99% rename from Mastodon/Scene/Compose/AutoComplete/View/AutoCompleteTopChevronView.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/View/AutoCompleteTopChevronView.swift index 9c3c81c3a..ccc36b1df 100644 --- a/Mastodon/Scene/Compose/AutoComplete/View/AutoCompleteTopChevronView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/View/AutoCompleteTopChevronView.swift @@ -7,6 +7,8 @@ import UIKit import Combine +import MastodonCore +import MastodonUI final class AutoCompleteTopChevronView: UIView { diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift new file mode 100644 index 000000000..3417ed935 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -0,0 +1,430 @@ +// +// ComposeContentViewController.swift +// +// +// Created by MainasuK on 22/9/30. +// + +import os.log +import UIKit +import SwiftUI +import Combine +import PhotosUI +import MastodonCore + +public final class ComposeContentViewController: UIViewController { + + let logger = Logger(subsystem: "ComposeContentViewController", category: "ViewController") + + var disposeBag = Set() + public var viewModel: ComposeContentViewModel! + private(set) lazy var composeContentToolbarViewModel = ComposeContentToolbarView.ViewModel(delegate: self) + + let tableView: ComposeTableView = { + let tableView = ComposeTableView() + tableView.estimatedRowHeight = UITableView.automaticDimension + tableView.alwaysBounceVertical = true + tableView.separatorStyle = .none + tableView.tableFooterView = UIView() + return tableView + }() + + 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) + } + +} + +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([ + 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) + + 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( + 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) + + // bind toolbar + bindToolbarViewModel() + } + + 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) + } + } +} + +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 + } + + private func bindToolbarViewModel() { + 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) + } +} + +// 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 + } + } + } +} + +// 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() + 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() + } + } + + 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+DataSource.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift new file mode 100644 index 000000000..3f6028b56 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift @@ -0,0 +1,101 @@ +// +// ComposeContentViewModel+DataSource.swift +// +// +// Created by MainasuK on 22/10/10. +// + +import UIKit +import MastodonCore +import CoreDataStack +import UIHostingConfigurationBackport + +extension ComposeContentViewModel { + + func setupDataSource( + tableView: UITableView + ) { + tableView.dataSource = self + + setupTableViewCell(tableView: tableView) + } + +} + +extension ComposeContentViewModel { + enum Section: CaseIterable { + case replyTo + case status + } + + 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: \.replyToCellFrame, 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 + } + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch Section.allCases[indexPath.section] { + case .replyTo: + return composeReplyToTableViewCell + case .status: + return composeContentTableViewCell + } + } +} 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 new file mode 100644 index 000000000..73272b419 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -0,0 +1,394 @@ +// +// ComposeContentViewModel.swift +// +// +// Created by MainasuK on 22/9/30. +// + +import os.log +import UIKit +import Combine +import CoreDataStack +import Meta +import MetaTextKit +import MastodonMeta +import MastodonCore +import MastodonSDK + +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() + + // author (me) + @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 = "" + + // 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] = { + // initial with 2 options + var options: [PollComposeItem.Option] = [] + options.append(PollComposeItem.Option()) + options.append(PollComposeItem.Option()) + 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 + @Published var scrollViewState: ScrollViewState = .fold + + + public init( + context: AppContext, + authContext: AuthContext, + kind: Kind + ) { + 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 + + // 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) + + // 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 { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension ComposeContentViewModel { + public enum Kind { + case post + case hashtag(hashtag: String) + case mention(user: ManagedObjectRecord) + case reply(status: ManagedObjectRecord) + } + + public enum ScrollViewState { + case fold // snap to input + case expand // snap to reply + } +} + +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) + } +} + +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) { + 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 { + + 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/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 new file mode 100644 index 000000000..0cf451d8d --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionRow.swift @@ -0,0 +1,51 @@ +// +// PollOptionRow.swift +// +// +// Created by MainasuK on 2022-5-31. +// + +import SwiftUI +import MastodonAsset +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 { + 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 new file mode 100644 index 000000000..5143bea35 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift @@ -0,0 +1,104 @@ +// +// 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 = .none + textField.backgroundColor = .clear + textField.returnKeyType = .next + textField.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 16, weight: .regular)) + textField.adjustsFontForContentSizeCategory = true + 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/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/Scene/ComposeContent/TableViewCell/ComposeContentTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeContentTableViewCell.swift new file mode 100644 index 000000000..3a646f1fc --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeContentTableViewCell.swift @@ -0,0 +1,171 @@ +// +// ComposeContentTableViewCell.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 UIHostingConfigurationBackport + +//protocol ComposeStatusContentTableViewCellDelegate: AnyObject { +// func composeStatusContentTableViewCell(_ cell: ComposeStatusContentTableViewCell, textViewShouldBeginEditing textView: UITextView) -> Bool +//} + +final class ComposeContentTableViewCell: UITableViewCell { + + let logger = Logger(subsystem: "ComposeContentTableViewCell", category: "View") + +// var disposeBag = Set() +// weak var delegate: ComposeStatusContentTableViewCellDelegate? +// +// let statusView = StatusView() +// +// let statusContentWarningEditorView = StatusContentWarningEditorView() +// +// let textEditorViewContainerView = UIView() +// +// static let metaTextViewTag: Int = 333 +// let metaText: MetaText = { +// let metaText = MetaText() +// metaText.textView.backgroundColor = .clear +// metaText.textView.isScrollEnabled = false +// metaText.textView.keyboardType = .twitter +// metaText.textView.textDragInteraction?.isEnabled = false // disable drag for link and attachment +// metaText.textView.textContainer.lineFragmentPadding = 10 // leading inset +// metaText.textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)) +// metaText.textView.attributedPlaceholder = { +// var attributes = metaText.textAttributes +// attributes[.foregroundColor] = Asset.Colors.Label.secondary.color +// return NSAttributedString( +// string: L10n.Scene.Compose.contentInputPlaceholder, +// attributes: attributes +// ) +// }() +// metaText.paragraphStyle = { +// let style = NSMutableParagraphStyle() +// style.lineSpacing = 5 +// style.paragraphSpacing = 0 +// return style +// }() +// metaText.textAttributes = [ +// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)), +// .foregroundColor: Asset.Colors.Label.primary.color, +// ] +// metaText.linkAttributes = [ +// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)), +// .foregroundColor: Asset.Colors.brand.color, +// ] +// return metaText +// }() +// +// // output +// let contentWarningContent = PassthroughSubject() +// +// override func prepareForReuse() { +// super.prepareForReuse() +// +// metaText.delegate = nil +// metaText.textView.delegate = nil +// } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension ComposeContentTableViewCell { + + private func _init() { + selectionStyle = .none + layer.zPosition = 999 + backgroundColor = .clear + +// let containerStackView = UIStackView() +// containerStackView.axis = .vertical +// containerStackView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(containerStackView) +// NSLayoutConstraint.activate([ +// containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), +// containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), +// containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), +// containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// ]) +// containerStackView.preservesSuperviewLayoutMargins = true +// +// containerStackView.addArrangedSubview(statusContentWarningEditorView) +// statusContentWarningEditorView.setContentHuggingPriority(.required - 1, for: .vertical) +// +// let statusContainerView = UIView() +// statusContainerView.preservesSuperviewLayoutMargins = true +// containerStackView.addArrangedSubview(statusContainerView) +// statusView.translatesAutoresizingMaskIntoConstraints = false +// statusContainerView.addSubview(statusView) +// NSLayoutConstraint.activate([ +// statusView.topAnchor.constraint(equalTo: statusContainerView.topAnchor, constant: 20), +// statusView.leadingAnchor.constraint(equalTo: statusContainerView.leadingAnchor), +// statusView.trailingAnchor.constraint(equalTo: statusContainerView.trailingAnchor), +// statusView.bottomAnchor.constraint(equalTo: statusContainerView.bottomAnchor), +// ]) +// statusView.setup(style: .composeStatusAuthor) +// +// containerStackView.addArrangedSubview(textEditorViewContainerView) +// metaText.textView.translatesAutoresizingMaskIntoConstraints = false +// textEditorViewContainerView.addSubview(metaText.textView) +// NSLayoutConstraint.activate([ +// metaText.textView.topAnchor.constraint(equalTo: textEditorViewContainerView.topAnchor), +// metaText.textView.leadingAnchor.constraint(equalTo: textEditorViewContainerView.layoutMarginsGuide.leadingAnchor), +// metaText.textView.trailingAnchor.constraint(equalTo: textEditorViewContainerView.layoutMarginsGuide.trailingAnchor), +// metaText.textView.bottomAnchor.constraint(equalTo: textEditorViewContainerView.bottomAnchor), +// metaText.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 64).priority(.defaultHigh), +// ]) +// statusContentWarningEditorView.textView.delegate = self + } + +} + +// MARK: - UITextViewDelegate +//extension ComposeStatusContentTableViewCell: UITextViewDelegate { +// +// func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { +// return delegate?.composeStatusContentTableViewCell(self, textViewShouldBeginEditing: textView) ?? true +// } +// +// func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { +// switch textView { +// case statusContentWarningEditorView.textView: +// // disable input line break +// guard text != "\n" else { return false } +// return true +// default: +// assertionFailure() +// return true +// } +// } +// +// func textViewDidChange(_ textView: UITextView) { +// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): text: \(textView.text ?? "")") +// guard textView === statusContentWarningEditorView.textView else { return } +// // replace line break with space +// // needs check input state to prevent break the IME +// if textView.markedTextRange == nil { +// textView.text = textView.text.replacingOccurrences(of: "\n", with: " ") +// } +// contentWarningContent.send(textView.text) +// } +// +//} + diff --git a/Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeReplyToTableViewCell.swift similarity index 84% rename from Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeReplyToTableViewCell.swift index f15675b24..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. @@ -8,13 +8,13 @@ import UIKit import Combine -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() @@ -35,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/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/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..4a34c77d4 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift @@ -0,0 +1,123 @@ +// +// ComposeContentToolbarView.swift +// +// +// Created by MainasuK on 22/10/18. +// + +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 + var allVisibilities: [Mastodon.Entity.Status.Visibility] { + return [.public, .private, .direct] + } + + @Published var isPollActive = false + @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) { + self.delegate = delegate + // end 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 activeImage: UIImage { + switch self { + case .attachment: + return Asset.Scene.Compose.media.image.withRenderingMode(.alwaysTemplate) + case .poll: + return Asset.Scene.Compose.pollFill.image.withRenderingMode(.alwaysTemplate) + case .emoji: + return Asset.Scene.Compose.emojiFill.image.withRenderingMode(.alwaysTemplate) + case .contentWarning: + return Asset.Scene.Compose.chatWarningFill.image.withRenderingMode(.alwaysTemplate) + case .visibility: + return Asset.Scene.Compose.earth.image.withRenderingMode(.alwaysTemplate) + } + } + + 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 new file mode 100644 index 000000000..52026c636 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift @@ -0,0 +1,132 @@ +// +// ComposeContentToolbarView.swift +// +// +// Created by MainasuK on 22/10/18. +// + +import os.log +import SwiftUI +import MastodonAsset +import MastodonLocalization +import MastodonSDK + +protocol ComposeContentToolbarViewDelegate: AnyObject { + func composeContentToolbarView(_ viewModel: ComposeContentToolbarView.ViewModel, toolbarItemDidPressed action: ComposeContentToolbarView.ViewModel.Action) + func composeContentToolbarView(_ viewModel: ComposeContentToolbarView.ViewModel, attachmentMenuDidPressed action: ComposeContentToolbarView.ViewModel.AttachmentAction) +} + +struct ComposeContentToolbarView: View { + + let logger = Logger(subsystem: "ComposeContentToolbarView", category: "View") + + static var toolbarHeight: CGFloat { 48 } + + @ObservedObject var viewModel: ViewModel + + var body: some View { + HStack(spacing: .zero) { + ForEach(ComposeContentToolbarView.ViewModel.Action.allCases, id: \.self) { action in + switch action { + case .attachment: + Menu { + ForEach(ComposeContentToolbarView.ViewModel.AttachmentAction.allCases, id: \.self) { attachmentAction in + Button { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public), \(attachmentAction.title)") + viewModel.delegate?.composeContentToolbarView(viewModel, attachmentMenuDidPressed: attachmentAction) + } label: { + Label { + Text(attachmentAction.title) + } icon: { + Image(uiImage: attachmentAction.image) + } + } + } + } label: { + label(for: action) + } + .frame(width: 48, height: 48) + case .visibility: + Menu { + Picker(selection: $viewModel.visibility) { + ForEach(viewModel.allVisibilities, id: \.self) { visibility in + Label { + Text(visibility.title) + } icon: { + Image(uiImage: visibility.image) + } + } + } label: { + Text(viewModel.visibility.title) + } + } label: { + label(for: viewModel.visibility.image) + } + .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) + } + .frame(width: 48, height: 48) + } + } + Spacer() + let count: Int = { + if viewModel.isContentWarningActive { + return viewModel.contentWeightedLength + viewModel.contentWarningWeightedLength + } else { + return viewModel.contentWeightedLength + } + }() + let remains = viewModel.maxTextInputLimit - count + let isOverflow = remains < 0 + Text("\(remains)") + .foregroundColor(Color(isOverflow ? UIColor.systemRed : UIColor.secondaryLabel)) + .font(.system(size: isOverflow ? 18 : 16, weight: isOverflow ? .medium : .regular)) + } + .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: 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 new file mode 100644 index 000000000..25584848a --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift @@ -0,0 +1,231 @@ +// +// ComposeContentView.swift +// +// +// Created by MainasuK on 22/9/30. +// + +import os.log +import SwiftUI +import MastodonAsset +import MastodonCore +import MastodonLocalization +import Stripes + +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 { + // 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 + 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) + .padding(.horizontal, ComposeContentView.margin) + // poll + pollView + .padding(.horizontal, ComposeContentView.margin) + } + .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)") + let rect = frame.standardized + viewModel.contentCellFrame = CGRect( + origin: frame.origin, + size: CGSize(width: floor(rect.width), height: floor(rect.height)) + ) + } + ) + Spacer() + } // end VStack + } // 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(.system(size: 15, weight: .regular)) + .foregroundColor(.secondary) + Spacer() + } + Spacer() + } + } +} + +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) + } + } + if viewModel.maxPollOptionLimit != viewModel.pollOptions.count { + PollAddOptionRow() + .onTapGesture { + viewModel.createNewPollOptionIfCould() + } + } + Menu { + 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 { + 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) + } + } + } // end VStack + } +} + +//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) { } +} + +// 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/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/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/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/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/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/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/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/MediaView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift index cfe9e73ce..438baff7e 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift @@ -9,8 +9,10 @@ import UIKit import Combine import CoreData +import CoreDataStack import Photos import AlamofireImage +import MastodonCore extension MediaView { public class Configuration: Hashable { @@ -177,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/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift index 0e8c394b7..d7ba51e17 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 @@ -23,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? @@ -54,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) } @@ -143,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/NotificationView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift index 5848844a2..2db731971 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 @@ -163,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 7a48ddc3a..a91f57dc2 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 { @@ -29,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/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/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/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift similarity index 79% rename from Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift rename to MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index 1bdff4d80..353dfc097 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -7,9 +7,9 @@ import UIKit import Combine -import MastodonUI import CoreDataStack import MastodonSDK +import MastodonCore import MastodonLocalization import MastodonMeta import Meta @@ -120,14 +120,15 @@ 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( + return try await Mastodon.API.Account.accountInfo( + session: .shared, domain: authenticationBox.domain, userID: userID, authorization: authenticationBox.userAuthorization - ) + ).singleOutput() } .sink { completion in // do nothing @@ -183,41 +184,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) @@ -315,15 +311,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 } @@ -370,44 +366,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/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index bd80afe86..202d70d91 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -9,13 +9,14 @@ import os.log import UIKit import Combine import CoreData -import Meta -import MastodonSDK -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 { @@ -25,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 @@ -126,6 +127,8 @@ extension StatusView { } public func prepareForReuse() { + authContext = nil + authorAvatarImageURL = nil isContentSensitive = false @@ -511,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) { @@ -574,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 @@ -590,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 4c983df34..4c4318bad 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 { @@ -704,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 { @@ -721,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/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 { 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/Mastodon/Scene/Share/View/Decoration/SawToothView.swift b/MastodonSDK/Sources/MastodonUI/View/Decoration/SawToothView.swift similarity index 93% rename from Mastodon/Scene/Share/View/Decoration/SawToothView.swift rename to MastodonSDK/Sources/MastodonUI/View/Decoration/SawToothView.swift index e344b62ef..1a7a977e6 100644 --- a/Mastodon/Scene/Share/View/Decoration/SawToothView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Decoration/SawToothView.swift @@ -8,8 +8,9 @@ import Foundation import UIKit import Combine +import MastodonCore -final class SawToothView: UIView { +public final class SawToothView: UIView { static let widthUint = 8 var disposeBag = Set() @@ -40,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/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 diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineBottomLoaderTableViewCell.swift similarity index 79% rename from Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift rename to MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineBottomLoaderTableViewCell.swift index 70f366bce..cc16e520d 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineBottomLoaderTableViewCell.swift @@ -7,17 +7,18 @@ import UIKit import Combine +import MastodonCore -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 29344eb28..6e396dc6d 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineLoaderTableViewCell.swift @@ -8,22 +8,23 @@ import UIKit import Combine import MastodonAsset +import MastodonCore 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) @@ -32,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 a12920c59..7cb5f2b44 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell.swift @@ -10,11 +10,11 @@ import CoreData import os.log import UIKit -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? @@ -27,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 81% rename from Mastodon/Scene/Share/View/TableviewCell/TimelineTopLoaderTableViewCell.swift rename to MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineTopLoaderTableViewCell.swift index 4accee1de..742614854 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineTopLoaderTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineTopLoaderTableViewCell.swift @@ -7,9 +7,10 @@ import UIKit import Combine +import MastodonCore -final class TimelineTopLoaderTableViewCell: TimelineLoaderTableViewCell { - override func _init() { +public final class TimelineTopLoaderTableViewCell: TimelineLoaderTableViewCell { + public override func _init() { super._init() activityIndicatorView.isHidden = false 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) + } + } 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)") + + } +} 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/MastodonTests/Info.plist b/MastodonTests/Info.plist index 21baf4a3e..9f09d489c 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 + 147 diff --git a/MastodonUITests/Info.plist b/MastodonUITests/Info.plist index 21baf4a3e..9f09d489c 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 + 147 diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 1361dc875..854a8a529 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 + 147 NSExtension NSExtensionPointIdentifier 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..3c482446a 100644 --- a/Podfile +++ b/Podfile @@ -30,19 +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 - -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..0cec6626a 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: 50ec5b2c4aa189024cc5ab41039f983dc5609040 COCOAPODS: 1.11.3 diff --git a/ShareActionExtension/Info.plist b/ShareActionExtension/Info.plist index 18b7be8a4..4372c5048 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 + 147 NSExtension NSExtensionAttributes 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 542fce6d5..000000000 --- a/ShareActionExtension/Scene/ShareViewController.swift +++ /dev/null @@ -1,324 +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 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 c56f8ecfd..000000000 --- a/ShareActionExtension/Scene/ShareViewModel.swift +++ /dev/null @@ -1,402 +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 - -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 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.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 - } - - if let movieProvider = _movieProvider { - composeViewModel.setupAttachmentViewModels([ - StatusAttachmentViewModel(itemProvider: movieProvider) - ]) - } else if !imageProviders.isEmpty { - let viewModels = imageProviders.map { provider in - StatusAttachmentViewModel(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 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 = APIService.shared.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 APIService.shared.publishStatus( - domain: domain, - idempotencyKey: nil, // FIXME: - query: query, - authenticationBox: authenticationBox - ) - } - .eraseToAnyPublisher() - } -} 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) - } - -} diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh new file mode 100755 index 000000000..75605f7b7 --- /dev/null +++ b/ci_scripts/ci_post_clone.sh @@ -0,0 +1,36 @@ +#!/bin/zsh + +# Xcode Cloud scripts + +set -xeu +set -o pipefail + +# list hardware +system_profiler SPSoftwareDataType SPHardwareDataType + +echo $PWD +cd $CI_WORKSPACE +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 +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 --install-dir $GEM_HOME + +# setup gems +bundle install + +bundle exec arkana +bundle exec pod install 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