Merge branch 'develop' into patch-1

# Conflicts:
#	Documentation/Setup.md
This commit is contained in:
CMK 2022-11-02 16:46:47 +08:00
commit e8370fa834
539 changed files with 14528 additions and 8710 deletions

17
.arkana.yml Normal file
View File

@ -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 <Key>Debug and <Key>Release env vars (assuming no flavor was declared)
# Mastodon Push Notification Endpoint
- NotificationEndpoint

7
.env.example Normal file
View File

@ -0,0 +1,7 @@
# Required
# https://<your-domain>/relay-to/development
NotificationEndpointDebug=""
# https://<your-domain>/relay-to/production
NotificationEndpointRelease=""

View File

@ -7,6 +7,6 @@ set -eo pipefail
xcodebuild -workspace Mastodon.xcworkspace \ xcodebuild -workspace Mastodon.xcworkspace \
-scheme Mastodon \ -scheme Mastodon \
-destination "platform=iOS Simulator,name=iPhone SE (2nd generation)" \ -destination "platform=iOS Simulator,name=iPhone SE (2nd generation)" \
clean \ clean \
build | xcpretty build | xcpretty

View File

@ -9,8 +9,7 @@ gem install bundler:2.3.11
# Install Ruby Gems # Install Ruby Gems
bundle install bundle install
# stub keys. DO NOT use in production # Setup notification endpoint
bundle exec pod keys set notification_endpoint "<endpoint>" bundle exec arkana
bundle exec pod keys set notification_endpoint_debug "<endpoint>"
bundle exec pod install bundle exec pod install

View File

@ -15,13 +15,14 @@ on:
jobs: jobs:
build: build:
name: CI build name: CI build
runs-on: macos-11 runs-on: macos-12
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: force Xcode 13.2.1
run: sudo xcode-select -switch /Applications/Xcode_13.2.1.app
- name: setup - name: setup
env:
NotificationEndpointDebug: ${{ secrets.NotificationEndpointDebug }}
NotificationEndpointRelease: ${{ secrets.NotificationEndpointRelease }}
run: exec ./.github/scripts/setup.sh run: exec ./.github/scripts/setup.sh
- name: build - name: build
run: exec ./.github/scripts/build.sh run: exec ./.github/scripts/build.sh

5
.gitignore vendored
View File

@ -122,4 +122,7 @@ xcuserdata
# Localization/StringsConvertor/input # Localization/StringsConvertor/input
Localization/StringsConvertor/output Localization/StringsConvertor/output
.DS_Store .DS_Store
env/**/**
!env/.env

View File

@ -1,18 +0,0 @@
//
// AppShared.h
// AppShared
//
// Created by MainasuK Cirno on 2021-4-27.
//
#import <Foundation/Foundation.h>
//! 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 <AppShared/PublicHeader.h>

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.4.5</string>
<key>CFBundleVersion</key>
<string>144</string>
</dict>
</plist>

View File

@ -1,13 +1,12 @@
# Acknowledgments # Acknowledgments
- [Alamofire](https://github.com/Alamofire/Alamofire)
- [AlamofireImage](https://github.com/Alamofire/AlamofireImage) - [AlamofireImage](https://github.com/Alamofire/AlamofireImage)
- [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator) - [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator)
- [Alamofire](https://github.com/Alamofire/Alamofire) - [Arkana](https://github.com/rogerluan/arkana)
- [CommonOSLog](https://github.com/mainasuk/CommonOSLog) - [CommonOSLog](https://github.com/mainasuk/CommonOSLog)
- [CryptoSwift](https://github.com/krzyzanowskim/CryptoSwift) - [CryptoSwift](https://github.com/krzyzanowskim/CryptoSwift)
- [DateToolSwift](https://github.com/MatthewYork/DateTools) - [DateToolSwift](https://github.com/MatthewYork/DateTools)
- [DiffableDataSources](https://github.com/ra1028/DiffableDataSources)
- [DifferenceKit](https://github.com/ra1028/DifferenceKit)
- [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) - [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage)
- [FLEX](https://github.com/FLEXTool/FLEX) - [FLEX](https://github.com/FLEXTool/FLEX)
- [FPSIndicator](https://github.com/MainasuK/FPSIndicator) - [FPSIndicator](https://github.com/MainasuK/FPSIndicator)
@ -26,10 +25,10 @@
- [SwiftGen](https://github.com/SwiftGen/SwiftGen) - [SwiftGen](https://github.com/SwiftGen/SwiftGen)
- [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect) - [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect)
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON) - [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
- [Tabman](https://github.com/uias/Tabman)
- [TabBarPager](https://github.com/TwidereProject/TabBarPager) - [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) - [ThirdPartyMailer](https://github.com/vtourraine/ThirdPartyMailer)
- [TOCropViewController](https://github.com/TimOliver/TOCropViewController) - [TOCropViewController](https://github.com/TimOliver/TOCropViewController)
- [TwidereX-iOS](https://github.com/TwidereProject/TwidereX-iOS)
- [TwitterProfile](https://github.com/OfTheWolf/TwitterProfile) - [TwitterProfile](https://github.com/OfTheWolf/TwitterProfile)
- [UITextView-Placeholder](https://github.com/devxoul/UITextView-Placeholder) - [UITextView-Placeholder](https://github.com/devxoul/UITextView-Placeholder)

View File

@ -12,7 +12,7 @@ Install the latest version of Xcode from the App Store or Apple Developer Downlo
This guide may not suit your machine and actually setup procedure may change in the future. Please file the issue or Pull Request if there are any problems. 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 ## 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 #### Intel Mac
@ -52,6 +52,13 @@ bundle install
bundle install bundle install
bundle exec pod clean 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 # make install
bundle exec pod install --repo-update bundle exec pod install --repo-update
@ -59,14 +66,14 @@ bundle exec pod install --repo-update
open Mastodon.xcworkspace open Mastodon.xcworkspace
``` ```
The CocoaPods-Keys plugin will request the push notification endpoint. You can fulfill the empty string and set it later. To setup the push notification. Please check section `Push Notification` below. The 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) #### 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 CocoaPods-Keys. There are two endpoints: The app is compatible with [toot-relay](https://github.com/DagAgren/toot-relay) APNs. You can set your push notification endpoint via Arkana. There are two endpoints:
- notification_endpoint: for `RELEASE` usage - NotificationEndpointDebug: for `DEBUG` usage. e.g. `https://<your.domin>/relay-to/development`
- notification_endpoint_debug: for `DEBUG` usage - NotificationEndpointRelease: for `RELEASE` usage. e.g. `https://<your.domin>/relay-to/production`
Please check the [Establishing a Certificate-Based Connection to APNs 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. ](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.

View File

@ -1,6 +1,7 @@
source "https://rubygems.org" source "https://rubygems.org"
gem 'arkana'
gem "cocoapods" gem "cocoapods"
gem "cocoapods-clean" gem "cocoapods-clean"
gem "cocoapods-keys" gem "xcpretty"

View File

@ -3,20 +3,21 @@ GEM
specs: specs:
CFPropertyList (3.0.5) CFPropertyList (3.0.5)
rexml rexml
RubyInline (3.12.5) activesupport (6.1.7)
ZenTest (~> 4.3)
ZenTest (4.12.1)
activesupport (6.1.5.1)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
tzinfo (~> 2.0) tzinfo (~> 2.0)
zeitwerk (~> 2.3) zeitwerk (~> 2.3)
addressable (2.8.0) addressable (2.8.1)
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 6.0)
algoliasearch (1.27.5) algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3) httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1) json (>= 1.5.1)
arkana (1.2.0)
colorize (~> 0.8)
dotenv (~> 2.7)
yaml (~> 0.2)
atomos (0.1.3) atomos (0.1.3)
claide (1.1.0) claide (1.1.0)
cocoapods (1.11.3) cocoapods (1.11.3)
@ -50,9 +51,6 @@ GEM
typhoeus (~> 1.0) typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5) cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.6.3) cocoapods-downloader (1.6.3)
cocoapods-keys (2.2.1)
dotenv
osx_keychain
cocoapods-plugins (1.0.0) cocoapods-plugins (1.0.0)
nap nap
cocoapods-search (1.0.1) cocoapods-search (1.0.1)
@ -61,8 +59,9 @@ GEM
netrc (~> 0.11) netrc (~> 0.11)
cocoapods-try (1.2.0) cocoapods-try (1.2.0)
colored2 (3.1.2) colored2 (3.1.2)
colorize (0.8.1)
concurrent-ruby (1.1.10) concurrent-ruby (1.1.10)
dotenv (2.7.6) dotenv (2.8.1)
escape (0.0.4) escape (0.0.4)
ethon (0.15.0) ethon (0.15.0)
ffi (>= 1.15.0) ffi (>= 1.15.0)
@ -71,39 +70,42 @@ GEM
fuzzy_match (2.0.4) fuzzy_match (2.0.4)
gh_inspector (1.1.3) gh_inspector (1.1.3)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.10.0) i18n (1.12.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
json (2.6.1) json (2.6.2)
minitest (5.15.0) minitest (5.16.3)
molinillo (0.8.0) molinillo (0.8.0)
nanaimo (0.3.0) nanaimo (0.3.0)
nap (1.1.0) nap (1.1.0)
netrc (0.11.0) netrc (0.11.0)
osx_keychain (1.0.2)
RubyInline (~> 3)
public_suffix (4.0.7) public_suffix (4.0.7)
rexml (3.2.5) rexml (3.2.5)
rouge (2.0.7)
ruby-macho (2.5.1) ruby-macho (2.5.1)
typhoeus (1.4.0) typhoeus (1.4.0)
ethon (>= 0.9.0) ethon (>= 0.9.0)
tzinfo (2.0.4) tzinfo (2.0.5)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
xcodeproj (1.21.0) xcodeproj (1.22.0)
CFPropertyList (>= 2.3.3, < 4.0) CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3) atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1) colored2 (~> 3.1)
nanaimo (~> 0.3.0) nanaimo (~> 0.3.0)
rexml (~> 3.2.4) 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 PLATFORMS
ruby arm64-darwin-21
DEPENDENCIES DEPENDENCIES
arkana
cocoapods cocoapods
cocoapods-clean cocoapods-clean
cocoapods-keys xcpretty
BUNDLED WITH BUNDLED WITH
2.3.11 2.3.17

View File

@ -224,17 +224,17 @@
<key>NSStringFormatValueTypeKey</key> <key>NSStringFormatValueTypeKey</key>
<string>ld</string> <string>ld</string>
<key>zero</key> <key>zero</key>
<string>لا إعاد تدوين</string> <string>لَا إعادَةُ تَدوين</string>
<key>one</key> <key>one</key>
<string>إعادةُ تدوينٍ واحِدة</string> <string>إعادَةُ تَدوينٍ واحِدَة</string>
<key>two</key> <key>two</key>
<string>إعادتا تدوين</string> <string>إعادَتَا تَدوين</string>
<key>few</key> <key>few</key>
<string>%ld إعاداتِ تدوين</string> <string>%ld إعادَاتِ تَدوين</string>
<key>many</key> <key>many</key>
<string>%ld إعادةٍ للتدوين</string> <string>%ld إعادَةٍ لِلتَّدوين</string>
<key>other</key> <key>other</key>
<string>%ld إعادة تدوين</string> <string>%ld إعادَة تَدوين</string>
</dict> </dict>
</dict> </dict>
<key>plural.count.reply</key> <key>plural.count.reply</key>

View File

@ -348,7 +348,7 @@
"Publishing": "Publication en cours ...", "Publishing": "Publication en cours ...",
"accessibility": { "accessibility": {
"logo_label": "Bouton logo", "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" "show_mentions": "Afficher les mentions"
}, },
"follow_request": { "follow_request": {
"accept": "Accept", "accept": "Accepter",
"accepted": "Accepted", "accepted": "Accepté",
"reject": "reject", "reject": "rejeter",
"rejected": "Rejected" "rejected": "Reje"
} }
}, },
"thread": { "thread": {

View File

@ -546,10 +546,10 @@
"show_mentions": "Qalkirinan nîşan bike" "show_mentions": "Qalkirinan nîşan bike"
}, },
"follow_request": { "follow_request": {
"accept": "Accept", "accept": "Bipejirîne",
"accepted": "Accepted", "accepted": "Pejirandî",
"reject": "reject", "reject": "nepejirîne",
"rejected": "Rejected" "rejected": "Nepejirandî"
} }
}, },
"thread": { "thread": {

View File

@ -546,10 +546,10 @@
"show_mentions": "แสดงการกล่าวถึง" "show_mentions": "แสดงการกล่าวถึง"
}, },
"follow_request": { "follow_request": {
"accept": "Accept", "accept": "ยอมรับ",
"accepted": "Accepted", "accepted": "ยอมรับแล้ว",
"reject": "reject", "reject": "ปฏิเสธ",
"rejected": "Rejected" "rejected": "ปฏิเสธแล้ว"
} }
}, },
"thread": { "thread": {

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1330" LastUpgradeVersion = "1400"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1330" LastUpgradeVersion = "1400"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1330" LastUpgradeVersion = "1400"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -4,13 +4,6 @@
<dict> <dict>
<key>SchemeUserState</key> <key>SchemeUserState</key>
<dict> <dict>
<key>AppShared.xcscheme_^#shared#^_</key>
<dict>
<key>isShown</key>
<true/>
<key>orderHint</key>
<integer>9</integer>
</dict>
<key>CoreDataStack.xcscheme_^#shared#^_</key> <key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
@ -19,32 +12,27 @@
<key>Mastodon - Profile.xcscheme_^#shared#^_</key> <key>Mastodon - Profile.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>3</integer> <integer>1</integer>
</dict> </dict>
<key>Mastodon - RTL.xcscheme_^#shared#^_</key> <key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>12</integer> <integer>5</integer>
</dict> </dict>
<key>Mastodon - Release.xcscheme_^#shared#^_</key> <key>Mastodon - Release.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>5</integer> <integer>2</integer>
</dict> </dict>
<key>Mastodon - Snapshot.xcscheme_^#shared#^_</key> <key>Mastodon - Snapshot.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>7</integer> <integer>3</integer>
</dict>
<key>Mastodon - ar.xcscheme</key>
<dict>
<key>orderHint</key>
<integer>8</integer>
</dict> </dict>
<key>Mastodon - ar.xcscheme_^#shared#^_</key> <key>Mastodon - ar.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>11</integer> <integer>4</integer>
</dict> </dict>
<key>Mastodon - ca.xcscheme_^#shared#^_</key> <key>Mastodon - ca.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -114,7 +102,7 @@
<key>MastodonIntent.xcscheme_^#shared#^_</key> <key>MastodonIntent.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>29</integer> <integer>20</integer>
</dict> </dict>
<key>MastodonIntents.xcscheme_^#shared#^_</key> <key>MastodonIntents.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -129,12 +117,12 @@
<key>NotificationService.xcscheme_^#shared#^_</key> <key>NotificationService.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>31</integer> <integer>24</integer>
</dict> </dict>
<key>ShareActionExtension.xcscheme_^#shared#^_</key> <key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>30</integer> <integer>25</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key> <key>SuppressBuildableAutocreation</key>
@ -164,6 +152,11 @@
<key>primary</key> <key>primary</key>
<true/> <true/>
</dict> </dict>
<key>DB8FABC526AEC7B2008E5AF4</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict> </dict>
</dict> </dict>
</plist> </plist>

View File

@ -1,259 +1,257 @@
{ {
"object": { "pins" : [
"pins": [ {
{ "identity" : "alamofire",
"package": "Alamofire", "kind" : "remoteSourceControl",
"repositoryURL": "https://github.com/Alamofire/Alamofire.git", "location" : "https://github.com/Alamofire/Alamofire.git",
"state": { "state" : {
"branch": null, "revision" : "354dda32d89fc8cd4f5c46487f64957d355f53d8",
"revision": "354dda32d89fc8cd4f5c46487f64957d355f53d8", "version" : "5.6.1"
"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"
}
} }
] },
}, {
"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
} }

View File

@ -6,6 +6,7 @@
// //
import UIKit import UIKit
import MastodonCore
protocol NeedsDependency: AnyObject { protocol NeedsDependency: AnyObject {
var context: AppContext! { get set } var context: AppContext! { get set }

View File

@ -8,8 +8,9 @@ import UIKit
import Combine import Combine
import SafariServices import SafariServices
import CoreDataStack import CoreDataStack
import MastodonSDK
import PanModal import PanModal
import MastodonSDK
import MastodonCore
import MastodonAsset import MastodonAsset
import MastodonLocalization import MastodonLocalization
@ -19,7 +20,9 @@ final public class SceneCoordinator {
private weak var scene: UIScene! private weak var scene: UIScene!
private weak var sceneDelegate: SceneDelegate! 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 let id = UUID().uuidString
@ -29,7 +32,11 @@ final public class SceneCoordinator {
private(set) var secondaryStackHashValues = Set<Int>() private(set) var secondaryStackHashValues = Set<Int>()
init(scene: UIScene, sceneDelegate: SceneDelegate, appContext: AppContext) { init(
scene: UIScene,
sceneDelegate: SceneDelegate,
appContext: AppContext
) {
self.scene = scene self.scene = scene
self.sceneDelegate = sceneDelegate self.sceneDelegate = sceneDelegate
self.appContext = appContext self.appContext = appContext
@ -38,100 +45,83 @@ final public class SceneCoordinator {
appContext.notificationService.requestRevealNotificationPublisher appContext.notificationService.requestRevealNotificationPublisher
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.compactMap { [weak self] pushNotification -> AnyPublisher<MastodonPushNotification?, Never> in .sink(receiveValue: { [weak self] pushNotification 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
guard let self = self else { return } guard let self = self else { return }
guard let pushNotification = pushNotification else { return } Task {
guard let currentActiveAuthenticationBox = self.authContext?.mastodonAuthenticationBox else { return }
// redirect to notification tab let accessToken = pushNotification.accessToken // use raw accessToken value without normalize
self.switchToTabBar(tab: .notification) if currentActiveAuthenticationBox.userAuthorization.accessToken == accessToken {
// do nothing if notification for current account
return
// Delay in next run loop } else {
DispatchQueue.main.async { [weak self] in // switch to notification's account
guard let self = self else { return } let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken)
// Note: request.returnsObjectsAsFaults = false
// show (push) on phone and pad request.fetchLimit = 1
let from: UIViewController? = { do {
if let splitViewController = self.splitViewController { guard let authentication = try appContext.managedObjectContext.fetch(request).first else {
if splitViewController.compactMainTabBarViewController.topMost?.view.window != nil { return
// compact
return splitViewController.compactMainTabBarViewController.topMost
} else {
// expand
return splitViewController.contentSplitViewController.mainTabBarController.topMost
} }
} else { let domain = authentication.domain
return self.tabBarController.topMost 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) .store(in: &disposeBag)
} }
} }
@ -173,7 +163,7 @@ extension SceneCoordinator {
case hashtagTimeline(viewModel: HashtagTimelineViewModel) case hashtagTimeline(viewModel: HashtagTimelineViewModel)
// profile // profile
case accountList case accountList(viewModel: AccountListViewModel)
case profile(viewModel: ProfileViewModel) case profile(viewModel: ProfileViewModel)
case favorite(viewModel: FavoriteViewModel) case favorite(viewModel: FavoriteViewModel)
case follower(viewModel: FollowerListViewModel) case follower(viewModel: FollowerListViewModel)
@ -181,6 +171,7 @@ extension SceneCoordinator {
case familiarFollowers(viewModel: FamiliarFollowersViewModel) case familiarFollowers(viewModel: FamiliarFollowersViewModel)
case rebloggedBy(viewModel: UserListViewModel) case rebloggedBy(viewModel: UserListViewModel)
case favoritedBy(viewModel: UserListViewModel) case favoritedBy(viewModel: UserListViewModel)
case bookmark(viewModel: BookmarkViewModel)
// setting // setting
case settings(viewModel: SettingsViewModel) case settings(viewModel: SettingsViewModel)
@ -223,55 +214,61 @@ extension SceneCoordinator {
func setup() { func setup() {
let rootViewController: UIViewController 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 { do {
let request = MastodonAuthentication.sortedFetchRequest let request = MastodonAuthentication.activeSortedFetchRequest // use active order
if try appContext.managedObjectContext.count(for: request) == 0 { let _authentication = try appContext.managedObjectContext.fetch(request).first
let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
self.authContext = _authContext
switch UIDevice.current.userInterfaceIdiom {
case .phone:
let viewController = MainTabBarController(context: appContext, coordinator: self, authContext: _authContext)
self.splitViewController = nil
self.tabBarController = viewController
rootViewController = viewController
default:
let splitViewController = RootSplitViewController(context: appContext, coordinator: self, authContext: _authContext)
self.splitViewController = splitViewController
self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController
rootViewController = splitViewController
}
sceneDelegate.window?.rootViewController = rootViewController // base: main
if _authContext == nil { // entry #1: welcome
DispatchQueue.main.async { DispatchQueue.main.async {
self.present( _ = self.present(
scene: .welcome, scene: .welcome,
from: self.sceneDelegate.window?.rootViewController, 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 { } catch {
assertionFailure(error.localizedDescription) assertionFailure(error.localizedDescription)
Task {
try? await Task.sleep(nanoseconds: .second * 2)
setup() // entry #2: retry
} // end Task
} }
} }
@discardableResult
@MainActor @MainActor
@discardableResult
func present(scene: Scene, from sender: UIViewController?, transition: Transition) -> UIViewController? { func present(scene: Scene, from sender: UIViewController?, transition: Transition) -> UIViewController? {
guard let viewController = get(scene: scene) else { guard let viewController = get(scene: scene) else {
return nil return nil
@ -430,13 +427,18 @@ private extension SceneCoordinator {
let _viewController = HashtagTimelineViewController() let _viewController = HashtagTimelineViewController()
_viewController.viewModel = viewModel _viewController.viewModel = viewModel
viewController = _viewController viewController = _viewController
case .accountList: case .accountList(let viewModel):
let _viewController = AccountListViewController() let _viewController = AccountListViewController()
_viewController.viewModel = viewModel
viewController = _viewController viewController = _viewController
case .profile(let viewModel): case .profile(let viewModel):
let _viewController = ProfileViewController() let _viewController = ProfileViewController()
_viewController.viewModel = viewModel _viewController.viewModel = viewModel
viewController = _viewController viewController = _viewController
case .bookmark(let viewModel):
let _viewController = BookmarkViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .favorite(let viewModel): case .favorite(let viewModel):
let _viewController = FavoriteViewController() let _viewController = FavoriteViewController()
_viewController.viewModel = viewModel _viewController.viewModel = viewModel

View File

@ -5,11 +5,11 @@
// Created by sxiaojian on 2021/4/22. // Created by sxiaojian on 2021/4/22.
// //
import UIKit
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import Foundation import MastodonCore
import MastodonSDK import MastodonSDK
import UIKit
enum SelectedAccountSection: Equatable, Hashable { enum SelectedAccountSection: Equatable, Hashable {
case main case main

View File

@ -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<CustomEmojiPickerSection, CustomEmojiPickerItem> {
let dataSource = UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>(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
}
}

View File

@ -7,6 +7,7 @@
import os.log import os.log
import UIKit import UIKit
import MastodonCore
import MastodonUI import MastodonUI
import MastodonSDK import MastodonSDK
@ -22,13 +23,16 @@ extension DiscoverySection {
static let logger = Logger(subsystem: "DiscoverySection", category: "logic") static let logger = Logger(subsystem: "DiscoverySection", category: "logic")
class Configuration { class Configuration {
let authContext: AuthContext
weak var profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? weak var profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate?
let familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher? let familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher?
public init( public init(
authContext: AuthContext,
profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? = nil, profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? = nil,
familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher? = nil familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher? = nil
) { ) {
self.authContext = authContext
self.profileCardTableViewCellDelegate = profileCardTableViewCellDelegate self.profileCardTableViewCellDelegate = profileCardTableViewCellDelegate
self.familiarFollowers = familiarFollowers self.familiarFollowers = familiarFollowers
} }
@ -72,11 +76,9 @@ extension DiscoverySection {
} else { } else {
cell.profileCardView.viewModel.familiarFollowers = nil 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 return cell
case .bottomLoader: case .bottomLoader:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell

View File

@ -14,6 +14,8 @@ import UIKit
import MetaTextKit import MetaTextKit
import MastodonMeta import MastodonMeta
import MastodonAsset import MastodonAsset
import MastodonCore
import MastodonUI
import MastodonLocalization import MastodonLocalization
enum NotificationSection: Equatable, Hashable { enum NotificationSection: Equatable, Hashable {
@ -23,6 +25,7 @@ enum NotificationSection: Equatable, Hashable {
extension NotificationSection { extension NotificationSection {
struct Configuration { struct Configuration {
let authContext: AuthContext
weak var notificationTableViewCellDelegate: NotificationTableViewCellDelegate? weak var notificationTableViewCellDelegate: NotificationTableViewCellDelegate?
let filterContext: Mastodon.Entity.Filter.Context? let filterContext: Mastodon.Entity.Filter.Context?
let activeFilters: Published<[Mastodon.Entity.Filter]>.Publisher? let activeFilters: Published<[Mastodon.Entity.Filter]>.Publisher?
@ -73,21 +76,20 @@ extension NotificationSection {
viewModel: NotificationTableViewCell.ViewModel, viewModel: NotificationTableViewCell.ViewModel,
configuration: Configuration configuration: Configuration
) { ) {
cell.notificationView.viewModel.authContext = configuration.authContext
StatusSection.setupStatusPollDataSource( StatusSection.setupStatusPollDataSource(
context: context, context: context,
authContext: configuration.authContext,
statusView: cell.notificationView.statusView statusView: cell.notificationView.statusView
) )
StatusSection.setupStatusPollDataSource( StatusSection.setupStatusPollDataSource(
context: context, context: context,
authContext: configuration.authContext,
statusView: cell.notificationView.quoteStatusView statusView: cell.notificationView.quoteStatusView
) )
context.authenticationService.activeMastodonAuthenticationBox
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.notificationView.viewModel)
.store(in: &cell.disposeBag)
cell.configure( cell.configure(
tableView: tableView, tableView: tableView,
viewModel: viewModel, viewModel: viewModel,

View File

@ -8,6 +8,7 @@
import os import os
import UIKit import UIKit
import Combine import Combine
import MastodonCore
import MastodonMeta import MastodonMeta
import MastodonLocalization import MastodonLocalization

View File

@ -13,6 +13,7 @@ import UIKit
import MetaTextKit import MetaTextKit
import MastodonMeta import MastodonMeta
import Combine import Combine
import MastodonCore
enum RecommendAccountSection: Equatable, Hashable { enum RecommendAccountSection: Equatable, Hashable {
case main case main
@ -132,6 +133,7 @@ enum RecommendAccountSection: Equatable, Hashable {
extension RecommendAccountSection { extension RecommendAccountSection {
struct Configuration { struct Configuration {
let authContext: AuthContext
weak var suggestionAccountTableViewCellDelegate: SuggestionAccountTableViewCellDelegate? weak var suggestionAccountTableViewCellDelegate: SuggestionAccountTableViewCellDelegate?
} }
@ -149,10 +151,7 @@ extension RecommendAccountSection {
cell.configure(user: user) cell.configure(user: user)
} }
context.authenticationService.activeMastodonAuthenticationBox cell.viewModel.userIdentifier = configuration.authContext.mastodonAuthenticationBox
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.viewModel)
.store(in: &cell.disposeBag)
cell.delegate = configuration.suggestionAccountTableViewCellDelegate cell.delegate = configuration.suggestionAccountTableViewCellDelegate
} }
return cell return cell

View File

@ -13,6 +13,8 @@ import MastodonSDK
import UIKit import UIKit
import os.log import os.log
import MastodonAsset import MastodonAsset
import MastodonCore
import MastodonUI
import MastodonLocalization import MastodonLocalization
enum ReportSection: Equatable, Hashable { enum ReportSection: Equatable, Hashable {
@ -22,6 +24,7 @@ enum ReportSection: Equatable, Hashable {
extension ReportSection { extension ReportSection {
struct Configuration { struct Configuration {
let authContext: AuthContext
} }
static func diffableDataSource( static func diffableDataSource(
@ -100,13 +103,11 @@ extension ReportSection {
) { ) {
StatusSection.setupStatusPollDataSource( StatusSection.setupStatusPollDataSource(
context: context, context: context,
authContext: configuration.authContext,
statusView: cell.statusView statusView: cell.statusView
) )
context.authenticationService.activeMastodonAuthenticationBox cell.statusView.viewModel.authContext = configuration.authContext
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
.store(in: &cell.disposeBag)
cell.configure( cell.configure(
tableView: tableView, tableView: tableView,

View File

@ -7,6 +7,7 @@
import UIKit import UIKit
import CoreDataStack import CoreDataStack
import MastodonCore
enum SearchHistorySection: Hashable { enum SearchHistorySection: Hashable {
case main case main

View File

@ -12,6 +12,7 @@ import UIKit
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MastodonAsset import MastodonAsset
import MastodonCore
import MastodonLocalization import MastodonLocalization
import MastodonUI import MastodonUI
@ -24,6 +25,7 @@ extension SearchResultSection {
static let logger = Logger(subsystem: "SearchResultSection", category: "logic") static let logger = Logger(subsystem: "SearchResultSection", category: "logic")
struct Configuration { struct Configuration {
let authContext: AuthContext
weak var statusViewTableViewCellDelegate: StatusTableViewCellDelegate? weak var statusViewTableViewCellDelegate: StatusTableViewCellDelegate?
weak var userTableViewCellDelegate: UserTableViewCellDelegate? weak var userTableViewCellDelegate: UserTableViewCellDelegate?
} }
@ -98,13 +100,11 @@ extension SearchResultSection {
) { ) {
StatusSection.setupStatusPollDataSource( StatusSection.setupStatusPollDataSource(
context: context, context: context,
authContext: configuration.authContext,
statusView: cell.statusView statusView: cell.statusView
) )
context.authenticationService.activeMastodonAuthenticationBox cell.statusView.viewModel.authContext = configuration.authContext
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
.store(in: &cell.disposeBag)
cell.configure( cell.configure(
tableView: tableView, tableView: tableView,
@ -119,7 +119,7 @@ extension SearchResultSection {
cell: UserTableViewCell, cell: UserTableViewCell,
viewModel: UserTableViewCell.ViewModel, viewModel: UserTableViewCell.ViewModel,
configuration: Configuration configuration: Configuration
) { ) {
cell.configure( cell.configure(
tableView: tableView, tableView: tableView,
viewModel: viewModel, viewModel: viewModel,

View File

@ -7,6 +7,7 @@
import UIKit import UIKit
import MastodonSDK import MastodonSDK
import MastodonCore
import MastodonLocalization import MastodonLocalization
enum SearchSection: Hashable { enum SearchSection: Hashable {

View File

@ -9,6 +9,7 @@ import UIKit
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MastodonAsset import MastodonAsset
import MastodonCore
import MastodonLocalization import MastodonLocalization
enum SettingsSection: Hashable { enum SettingsSection: Hashable {
@ -124,7 +125,7 @@ extension SettingsSection {
extension SettingsSection { extension SettingsSection {
static func configureSettingToggle( public static func configureSettingToggle(
cell: SettingsToggleTableViewCell, cell: SettingsToggleTableViewCell,
item: SettingsItem, item: SettingsItem,
setting: Setting setting: Setting
@ -155,7 +156,7 @@ extension SettingsSection {
} }
} }
static func configureSettingToggle( public static func configureSettingToggle(
cell: SettingsToggleTableViewCell, cell: SettingsToggleTableViewCell,
switchMode: SettingsItem.NotificationSwitchMode, switchMode: SettingsItem.NotificationSwitchMode,
subscription: NotificationSubscription subscription: NotificationSubscription

View File

@ -15,6 +15,7 @@ import AlamofireImage
import MastodonMeta import MastodonMeta
import MastodonSDK import MastodonSDK
import NaturalLanguage import NaturalLanguage
import MastodonCore
import MastodonUI import MastodonUI
enum StatusSection: Equatable, Hashable { enum StatusSection: Equatable, Hashable {
@ -26,6 +27,7 @@ extension StatusSection {
static let logger = Logger(subsystem: "StatusSection", category: "logic") static let logger = Logger(subsystem: "StatusSection", category: "logic")
struct Configuration { struct Configuration {
let authContext: AuthContext
weak var statusTableViewCellDelegate: StatusTableViewCellDelegate? weak var statusTableViewCellDelegate: StatusTableViewCellDelegate?
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate? weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
let filterContext: Mastodon.Entity.Filter.Context? let filterContext: Mastodon.Entity.Filter.Context?
@ -158,6 +160,7 @@ extension StatusSection {
public static func setupStatusPollDataSource( public static func setupStatusPollDataSource(
context: AppContext, context: AppContext,
authContext: AuthContext,
statusView: StatusView statusView: StatusView
) { ) {
let managedObjectContext = context.managedObjectContext let managedObjectContext = context.managedObjectContext
@ -171,10 +174,7 @@ extension StatusSection {
return _cell ?? PollOptionTableViewCell() return _cell ?? PollOptionTableViewCell()
}() }()
context.authenticationService.activeMastodonAuthenticationBox cell.pollOptionView.viewModel.authContext = authContext
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.pollOptionView.viewModel)
.store(in: &cell.disposeBag)
managedObjectContext.performAndWait { managedObjectContext.performAndWait {
guard let option = record.object(in: managedObjectContext) else { guard let option = record.object(in: managedObjectContext) else {
@ -211,14 +211,13 @@ extension StatusSection {
return true return true
}() }()
if needsUpdatePoll, let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value if needsUpdatePoll {
{
let pollRecord: ManagedObjectRecord<Poll> = .init(objectID: option.poll.objectID) let pollRecord: ManagedObjectRecord<Poll> = .init(objectID: option.poll.objectID)
Task { [weak context] in Task { [weak context] in
guard let context = context else { return } guard let context = context else { return }
_ = try await context.apiService.poll( _ = try await context.apiService.poll(
poll: pollRecord, poll: pollRecord,
authenticationBox: authenticationBox authenticationBox: authContext.mastodonAuthenticationBox
) )
} }
} }
@ -247,13 +246,11 @@ extension StatusSection {
) { ) {
setupStatusPollDataSource( setupStatusPollDataSource(
context: context, context: context,
authContext: configuration.authContext,
statusView: cell.statusView statusView: cell.statusView
) )
context.authenticationService.activeMastodonAuthenticationBox cell.statusView.viewModel.authContext = configuration.authContext
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
.store(in: &cell.disposeBag)
cell.configure( cell.configure(
tableView: tableView, tableView: tableView,
@ -276,13 +273,11 @@ extension StatusSection {
) { ) {
setupStatusPollDataSource( setupStatusPollDataSource(
context: context, context: context,
authContext: configuration.authContext,
statusView: cell.statusView statusView: cell.statusView
) )
context.authenticationService.activeMastodonAuthenticationBox cell.statusView.viewModel.authContext = configuration.authContext
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
.store(in: &cell.disposeBag)
cell.configure( cell.configure(
tableView: tableView, tableView: tableView,

View File

@ -9,8 +9,10 @@ import os.log
import UIKit import UIKit
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MetaTextKit import MastodonCore
import MastodonUI
import MastodonMeta import MastodonMeta
import MetaTextKit
enum UserSection: Hashable { enum UserSection: Hashable {
case main case main

View File

@ -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
}
}

View File

@ -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
// }
//}

View File

@ -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<MastodonAuthentication>
let domain: String
let userID: MastodonUser.ID
let appAuthorization: Mastodon.API.OAuth.Authorization
let userAuthorization: Mastodon.API.OAuth.Authorization
}

View File

@ -2,19 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>onion</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
@ -30,7 +17,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.4.5</string> <string>1.4.6</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
<dict> <dict>
@ -43,7 +30,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>144</string> <string>147</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
@ -59,6 +46,19 @@
</array> </array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>onion</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
<key>NSUserActivityTypes</key> <key>NSUserActivityTypes</key>
<array> <array>
<string>SendPostIntent</string> <string>SendPostIntent</string>
@ -103,6 +103,10 @@
</array> </array>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>Main</string> <string>Main</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>

View File

@ -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
)
}
}

View File

@ -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 }
}
}

View File

@ -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 }
}
}

View File

@ -1,7 +0,0 @@
//
// ThemePreference.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-7-5.
//

View File

@ -1,12 +0,0 @@
//
// NamingState.swift
// Mastodon
//
// Created by MainasuK on 2022-1-17.
//
import Foundation
protocol NamingState {
var name: String { get }
}

View File

@ -7,19 +7,19 @@
import UIKit import UIKit
import CoreDataStack import CoreDataStack
import MastodonCore
extension DataSourceFacade { extension DataSourceFacade {
static func responseToUserBlockAction( static func responseToUserBlockAction(
dependency: NeedsDependency, dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>, user: ManagedObjectRecord<MastodonUser>
authenticationBox: MastodonAuthenticationBox
) async throws { ) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged() await selectionFeedbackGenerator.selectionChanged()
_ = try await dependency.context.apiService.toggleBlock( _ = try await dependency.context.apiService.toggleBlock(
user: user, user: user,
authenticationBox: authenticationBox authenticationBox: dependency.authContext.mastodonAuthenticationBox
) )
} // end func } // end func
} }

View File

@ -0,0 +1,26 @@
//
// DataSourceFacade+Bookmark.swift
// Mastodon
//
// Created by ProtoLimit on 2022/07/29.
//
import UIKit
import CoreData
import CoreDataStack
import MastodonCore
extension DataSourceFacade {
public static func responseToStatusBookmarkAction(
provider: UIViewController & NeedsDependency & AuthContextProvider,
status: ManagedObjectRecord<Status>
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
_ = try await provider.context.apiService.bookmark(
record: status,
authenticationBox: provider.authContext.mastodonAuthenticationBox
)
}
}

View File

@ -8,19 +8,19 @@
import UIKit import UIKit
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MastodonCore
extension DataSourceFacade { extension DataSourceFacade {
static func responseToStatusFavoriteAction( public static func responseToStatusFavoriteAction(
provider: DataSourceProvider, provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>, status: ManagedObjectRecord<Status>
authenticationBox: MastodonAuthenticationBox
) async throws { ) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged() await selectionFeedbackGenerator.selectionChanged()
_ = try await provider.context.apiService.favorite( _ = try await provider.context.apiService.favorite(
record: status, record: status,
authenticationBox: authenticationBox authenticationBox: provider.authContext.mastodonAuthenticationBox
) )
} }
} }

View File

@ -8,31 +8,30 @@
import UIKit import UIKit
import CoreDataStack import CoreDataStack
import class CoreDataStack.Notification import class CoreDataStack.Notification
import MastodonCore
import MastodonSDK import MastodonSDK
import MastodonLocalization import MastodonLocalization
extension DataSourceFacade { extension DataSourceFacade {
static func responseToUserFollowAction( static func responseToUserFollowAction(
dependency: NeedsDependency, dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>, user: ManagedObjectRecord<MastodonUser>
authenticationBox: MastodonAuthenticationBox
) async throws { ) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged() await selectionFeedbackGenerator.selectionChanged()
_ = try await dependency.context.apiService.toggleFollow( _ = try await dependency.context.apiService.toggleFollow(
user: user, user: user,
authenticationBox: authenticationBox authenticationBox: dependency.authContext.mastodonAuthenticationBox
) )
} // end func } // end func
} }
extension DataSourceFacade { extension DataSourceFacade {
static func responseToUserFollowRequestAction( static func responseToUserFollowRequestAction(
dependency: NeedsDependency, dependency: NeedsDependency & AuthContextProvider,
notification: ManagedObjectRecord<Notification>, notification: ManagedObjectRecord<Notification>,
query: Mastodon.API.Account.FollowReqeustQuery, query: Mastodon.API.Account.FollowReqeustQuery
authenticationBox: MastodonAuthenticationBox
) async throws { ) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged() await selectionFeedbackGenerator.selectionChanged()
@ -71,9 +70,10 @@ extension DataSourceFacade {
_ = try await dependency.context.apiService.followRequest( _ = try await dependency.context.apiService.followRequest(
userID: userID, userID: userID,
query: query, query: query,
authenticationBox: authenticationBox authenticationBox: dependency.authContext.mastodonAuthenticationBox
) )
} catch { } catch {
// reset state when failure
try? await managedObjectContext.performChanges { try? await managedObjectContext.performChanges {
guard let notification = notification.object(in: managedObjectContext) else { return } guard let notification = notification.object(in: managedObjectContext) else { return }
notification.transientFollowRequestState = .init(state: .none) notification.transientFollowRequestState = .init(state: .none)
@ -111,7 +111,8 @@ extension DataSourceFacade {
case .accept: case .accept:
notification.transientFollowRequestState = .init(state: .isAccept) notification.transientFollowRequestState = .init(state: .isAccept)
case .reject: case .reject:
notification.transientFollowRequestState = .init(state: .isReject) // do nothing due to will delete notification
break
} }
} }
@ -122,7 +123,11 @@ extension DataSourceFacade {
case .accept: case .accept:
notification.followRequestState = .init(state: .isAccept) notification.followRequestState = .init(state: .isAccept)
case .reject: case .reject:
notification.followRequestState = .init(state: .isReject) // delete notification
for feed in notification.feeds {
backgroundManagedObjectContext.delete(feed)
}
backgroundManagedObjectContext.delete(notification)
} }
} }
} // end func } // end func

View File

@ -7,12 +7,13 @@
import UIKit import UIKit
import CoreDataStack import CoreDataStack
import MastodonCore
import MastodonSDK import MastodonSDK
extension DataSourceFacade { extension DataSourceFacade {
@MainActor @MainActor
static func coordinateToHashtagScene( static func coordinateToHashtagScene(
provider: DataSourceProvider, provider: DataSourceProvider & AuthContextProvider,
tag: DataSourceItem.TagKind tag: DataSourceItem.TagKind
) async { ) async {
switch tag { switch tag {
@ -25,11 +26,12 @@ extension DataSourceFacade {
@MainActor @MainActor
static func coordinateToHashtagScene( static func coordinateToHashtagScene(
provider: DataSourceProvider, provider: DataSourceProvider & AuthContextProvider,
tag: Mastodon.Entity.Tag tag: Mastodon.Entity.Tag
) async { ) async {
let hashtagTimelineViewModel = HashtagTimelineViewModel( let hashtagTimelineViewModel = HashtagTimelineViewModel(
context: provider.context, context: provider.context,
authContext: provider.authContext,
hashtag: tag.name hashtag: tag.name
) )
@ -42,7 +44,7 @@ extension DataSourceFacade {
@MainActor @MainActor
static func coordinateToHashtagScene( static func coordinateToHashtagScene(
provider: DataSourceProvider, provider: DataSourceProvider & AuthContextProvider,
tag: ManagedObjectRecord<Tag> tag: ManagedObjectRecord<Tag>
) async { ) async {
let managedObjectContext = provider.context.managedObjectContext let managedObjectContext = provider.context.managedObjectContext
@ -55,6 +57,7 @@ extension DataSourceFacade {
let hashtagTimelineViewModel = HashtagTimelineViewModel( let hashtagTimelineViewModel = HashtagTimelineViewModel(
context: provider.context, context: provider.context,
authContext: provider.authContext,
hashtag: name hashtag: name
) )

View File

@ -5,6 +5,7 @@
// Created by MainasuK on 2022-1-26. // Created by MainasuK on 2022-1-26.
// //
import os.log
import UIKit import UIKit
import CoreDataStack import CoreDataStack
import MastodonUI import MastodonUI
@ -153,6 +154,8 @@ extension DataSourceFacade {
user: ManagedObjectRecord<MastodonUser>, user: ManagedObjectRecord<MastodonUser>,
previewContext: ImagePreviewContext previewContext: ImagePreviewContext
) async throws { ) async throws {
let logger = Logger(subsystem: "DataSourceFacade", category: "Media")
let managedObjectContext = dependency.context.managedObjectContext let managedObjectContext = dependency.context.managedObjectContext
var _avatarAssetURL: String? var _avatarAssetURL: String?
@ -216,13 +219,18 @@ extension DataSourceFacade {
thumbnail: thumbnail thumbnail: thumbnail
)) ))
case .profileBanner: case .profileBanner:
return .profileAvatar(.init( return .profileBanner(.init(
assetURL: _headerAssetURL, assetURL: _headerAssetURL,
thumbnail: thumbnail thumbnail: thumbnail
)) ))
} }
}() }()
guard mediaPreviewItem.isAssetURLValid else {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): discard preview due to assetURL invalid")
return
}
coordinateToMediaPreviewScene( coordinateToMediaPreviewScene(
dependency: dependency, dependency: dependency,
mediaPreviewItem: mediaPreviewItem, mediaPreviewItem: mediaPreviewItem,

View File

@ -8,11 +8,12 @@
import Foundation import Foundation
import CoreDataStack import CoreDataStack
import MetaTextKit import MetaTextKit
import MastodonCore
extension DataSourceFacade { extension DataSourceFacade {
static func responseToMetaTextAction( static func responseToMetaTextAction(
provider: DataSourceProvider, provider: DataSourceProvider & AuthContextProvider,
target: StatusTarget, target: StatusTarget,
status: ManagedObjectRecord<Status>, status: ManagedObjectRecord<Status>,
meta: Meta meta: Meta
@ -33,7 +34,7 @@ extension DataSourceFacade {
} }
static func responseToMetaTextAction( static func responseToMetaTextAction(
provider: DataSourceProvider, provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>, status: ManagedObjectRecord<Status>,
meta: Meta meta: Meta
) async { ) async {
@ -47,19 +48,20 @@ extension DataSourceFacade {
assertionFailure() assertionFailure()
return 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.count >= 4,
url.pathComponents[0] == "/", url.pathComponents[0] == "/",
url.pathComponents[1] == "web", url.pathComponents[1] == "web",
url.pathComponents[2] == "statuses" { url.pathComponents[2] == "statuses" {
let statusID = url.pathComponents[3] 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) await provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show)
} else { } else {
await provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) await provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
} }
case .hashtag(_, let hashtag, _): 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) await provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: provider, transition: .show)
case .mention(_, let mention, let userInfo): case .mention(_, let mention, let userInfo):
await coordinateToProfileScene( await coordinateToProfileScene(

View File

@ -7,19 +7,19 @@
import UIKit import UIKit
import CoreDataStack import CoreDataStack
import MastodonCore
extension DataSourceFacade { extension DataSourceFacade {
static func responseToUserMuteAction( static func responseToUserMuteAction(
dependency: NeedsDependency, dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>, user: ManagedObjectRecord<MastodonUser>
authenticationBox: MastodonAuthenticationBox
) async throws { ) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged() await selectionFeedbackGenerator.selectionChanged()
_ = try await dependency.context.apiService.toggleMute( _ = try await dependency.context.apiService.toggleMute(
user: user, user: user,
authenticationBox: authenticationBox authenticationBox: dependency.authContext.mastodonAuthenticationBox
) )
} // end func } // end func
} }

View File

@ -7,11 +7,12 @@
import UIKit import UIKit
import CoreDataStack import CoreDataStack
import MastodonCore
extension DataSourceFacade { extension DataSourceFacade {
static func coordinateToProfileScene( static func coordinateToProfileScene(
provider: DataSourceProvider, provider: DataSourceProvider & AuthContextProvider,
target: StatusTarget, target: StatusTarget,
status: ManagedObjectRecord<Status> status: ManagedObjectRecord<Status>
) async { ) async {
@ -32,7 +33,7 @@ extension DataSourceFacade {
@MainActor @MainActor
static func coordinateToProfileScene( static func coordinateToProfileScene(
provider: DataSourceProvider, provider: DataSourceProvider & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser> user: ManagedObjectRecord<MastodonUser>
) async { ) async {
guard let user = user.object(in: provider.context.managedObjectContext) else { guard let user = user.object(in: provider.context.managedObjectContext) else {
@ -42,6 +43,7 @@ extension DataSourceFacade {
let profileViewModel = CachedProfileViewModel( let profileViewModel = CachedProfileViewModel(
context: provider.context, context: provider.context,
authContext: provider.authContext,
mastodonUser: user mastodonUser: user
) )
@ -57,13 +59,12 @@ extension DataSourceFacade {
extension DataSourceFacade { extension DataSourceFacade {
static func coordinateToProfileScene( static func coordinateToProfileScene(
provider: DataSourceProvider, provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>, status: ManagedObjectRecord<Status>,
mention: String, // username, mention: String, // username,
userInfo: [AnyHashable: Any]? userInfo: [AnyHashable: Any]?
) async { ) async {
guard let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return } let domain = provider.authContext.mastodonAuthenticationBox.domain
let domain = authenticationBox.domain
let href = userInfo?["href"] as? String let href = userInfo?["href"] as? String
guard let url = href.flatMap({ URL(string: $0) }) else { return } guard let url = href.flatMap({ URL(string: $0) }) else { return }
@ -85,8 +86,8 @@ extension DataSourceFacade {
let userID = mention.id let userID = mention.id
let profileViewModel: ProfileViewModel = { let profileViewModel: ProfileViewModel = {
// check if self // check if self
guard userID != authenticationBox.userID else { guard userID != provider.authContext.mastodonAuthenticationBox.userID else {
return MeProfileViewModel(context: provider.context) return MeProfileViewModel(context: provider.context, authContext: provider.authContext)
} }
let request = MastodonUser.sortedFetchRequest let request = MastodonUser.sortedFetchRequest
@ -95,9 +96,9 @@ extension DataSourceFacade {
let _user = provider.context.managedObjectContext.safeFetch(request).first let _user = provider.context.managedObjectContext.safeFetch(request).first
if let user = _user { if let user = _user {
return CachedProfileViewModel(context: provider.context, mastodonUser: user) return CachedProfileViewModel(context: provider.context, authContext: provider.authContext, mastodonUser: user)
} else { } else {
return RemoteProfileViewModel(context: provider.context, userID: userID) return RemoteProfileViewModel(context: provider.context, authContext: provider.authContext, userID: userID)
} }
}() }()

View File

@ -7,20 +7,20 @@
import UIKit import UIKit
import CoreDataStack import CoreDataStack
import MastodonCore
import MastodonUI import MastodonUI
extension DataSourceFacade { extension DataSourceFacade {
static func responseToStatusReblogAction( static func responseToStatusReblogAction(
provider: DataSourceProvider, provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>, status: ManagedObjectRecord<Status>
authenticationBox: MastodonAuthenticationBox
) async throws { ) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged() await selectionFeedbackGenerator.selectionChanged()
_ = try await provider.context.apiService.reblog( _ = try await provider.context.apiService.reblog(
record: status, record: status,
authenticationBox: authenticationBox authenticationBox: provider.authContext.mastodonAuthenticationBox
) )
} // end func } // end func
} }

View File

@ -7,22 +7,23 @@
import Foundation import Foundation
import CoreDataStack import CoreDataStack
import MastodonCore
extension DataSourceFacade { extension DataSourceFacade {
static func responseToCreateSearchHistory( static func responseToCreateSearchHistory(
provider: DataSourceProvider, provider: DataSourceProvider & AuthContextProvider,
item: DataSourceItem item: DataSourceItem
) async { ) async {
switch item { switch item {
case .status: case .status:
break // not create search history for status break // not create search history for status
case .user(let record): case .user(let record):
let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value let authenticationBox = provider.authContext.mastodonAuthenticationBox
let managedObjectContext = provider.context.backgroundManagedObjectContext let managedObjectContext = provider.context.backgroundManagedObjectContext
try? await managedObjectContext.performChanges { 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 } guard let user = record.object(in: managedObjectContext) else { return }
_ = Persistence.SearchHistory.createOrMerge( _ = Persistence.SearchHistory.createOrMerge(
in: managedObjectContext, in: managedObjectContext,
@ -34,13 +35,12 @@ extension DataSourceFacade {
) )
} // end try? await managedObjectContext.performChanges { } } // end try? await managedObjectContext.performChanges { }
case .hashtag(let tag): case .hashtag(let tag):
let _authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value let authenticationBox = provider.authContext.mastodonAuthenticationBox
let managedObjectContext = provider.context.backgroundManagedObjectContext let managedObjectContext = provider.context.backgroundManagedObjectContext
switch tag { switch tag {
case .entity(let entity): case .entity(let entity):
try? await managedObjectContext.performChanges { try? await managedObjectContext.performChanges {
guard let authenticationBox = _authenticationBox else { return }
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
let now = Date() let now = Date()
@ -66,7 +66,7 @@ extension DataSourceFacade {
} // end try? await managedObjectContext.performChanges { } } // end try? await managedObjectContext.performChanges { }
case .record(let record): case .record(let record):
try? await managedObjectContext.performChanges { 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 me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
guard let tag = record.object(in: managedObjectContext) else { return } guard let tag = record.object(in: managedObjectContext) else { return }
@ -92,13 +92,12 @@ extension DataSourceFacade {
extension DataSourceFacade { extension DataSourceFacade {
static func responseToDeleteSearchHistory( static func responseToDeleteSearchHistory(
provider: DataSourceProvider provider: DataSourceProvider & AuthContextProvider
) async throws { ) async throws {
let _authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value let authenticationBox = provider.authContext.mastodonAuthenticationBox
let managedObjectContext = provider.context.backgroundManagedObjectContext let managedObjectContext = provider.context.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
guard let authenticationBox = _authenticationBox else { return }
guard let _ = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } guard let _ = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
let request = SearchHistory.sortedFetchRequest let request = SearchHistory.sortedFetchRequest
request.predicate = SearchHistory.predicate( request.predicate = SearchHistory.predicate(

View File

@ -7,6 +7,7 @@
import UIKit import UIKit
import CoreDataStack import CoreDataStack
import MastodonCore
import MastodonUI import MastodonUI
import MastodonLocalization import MastodonLocalization
@ -14,13 +15,12 @@ import MastodonLocalization
extension DataSourceFacade { extension DataSourceFacade {
static func responseToDeleteStatus( static func responseToDeleteStatus(
dependency: NeedsDependency, dependency: NeedsDependency & AuthContextProvider,
status: ManagedObjectRecord<Status>, status: ManagedObjectRecord<Status>
authenticationBox: MastodonAuthenticationBox
) async throws { ) async throws {
_ = try await dependency.context.apiService.deleteStatus( _ = try await dependency.context.apiService.deleteStatus(
status: status, status: status,
authenticationBox: authenticationBox authenticationBox: dependency.authContext.mastodonAuthenticationBox
) )
} }
@ -36,7 +36,7 @@ extension DataSourceFacade {
button: UIButton button: UIButton
) async throws { ) async throws {
let activityViewController = try await createActivityViewController( let activityViewController = try await createActivityViewController(
provider: provider, dependency: provider,
status: status status: status
) )
provider.coordinator.present( provider.coordinator.present(
@ -51,19 +51,19 @@ extension DataSourceFacade {
} }
private static func createActivityViewController( private static func createActivityViewController(
provider: DataSourceProvider, dependency: NeedsDependency,
status: ManagedObjectRecord<Status> status: ManagedObjectRecord<Status>
) async throws -> UIActivityViewController { ) async throws -> UIActivityViewController {
var activityItems: [Any] = try await provider.context.managedObjectContext.perform { var activityItems: [Any] = try await dependency.context.managedObjectContext.perform {
guard let status = status.object(in: provider.context.managedObjectContext) else { return [] } guard let status = status.object(in: dependency.context.managedObjectContext) else { return [] }
let url = status.url ?? status.uri let url = status.url ?? status.uri
return [URL(string: url)].compactMap { $0 } as [Any] return [URL(string: url)].compactMap { $0 } as [Any]
} }
var applicationActivities: [UIActivity] = [ 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) activityItems.append(contentsOf: provider.activities)
applicationActivities.append(contentsOf: provider.applicationActivities) applicationActivities.append(contentsOf: provider.applicationActivities)
} }
@ -80,10 +80,9 @@ extension DataSourceFacade {
extension DataSourceFacade { extension DataSourceFacade {
@MainActor @MainActor
static func responseToActionToolbar( static func responseToActionToolbar(
provider: DataSourceProvider, provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>, status: ManagedObjectRecord<Status>,
action: ActionToolbarContainer.Action, action: ActionToolbarContainer.Action,
authenticationBox: MastodonAuthenticationBox,
sender: UIButton sender: UIButton
) async throws { ) async throws {
let managedObjectContext = provider.context.managedObjectContext let managedObjectContext = provider.context.managedObjectContext
@ -99,16 +98,15 @@ extension DataSourceFacade {
switch action { switch action {
case .reply: case .reply:
guard let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
let selectionFeedbackGenerator = UISelectionFeedbackGenerator() let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
selectionFeedbackGenerator.selectionChanged() selectionFeedbackGenerator.selectionChanged()
let composeViewModel = ComposeViewModel( let composeViewModel = ComposeViewModel(
context: provider.context, context: provider.context,
composeKind: .reply(status: status), authContext: provider.authContext,
authenticationBox: authenticationBox kind: .reply(status: status)
) )
provider.coordinator.present( _ = provider.coordinator.present(
scene: .compose(viewModel: composeViewModel), scene: .compose(viewModel: composeViewModel),
from: provider, from: provider,
transition: .modal(animated: true, completion: nil) transition: .modal(animated: true, completion: nil)
@ -116,14 +114,17 @@ extension DataSourceFacade {
case .reblog: case .reblog:
try await DataSourceFacade.responseToStatusReblogAction( try await DataSourceFacade.responseToStatusReblogAction(
provider: provider, provider: provider,
status: status, status: status
authenticationBox: authenticationBox
) )
case .like: case .like:
try await DataSourceFacade.responseToStatusFavoriteAction( try await DataSourceFacade.responseToStatusFavoriteAction(
provider: provider, provider: provider,
status: status, status: status
authenticationBox: authenticationBox )
case .bookmark:
try await DataSourceFacade.responseToStatusBookmarkAction(
provider: provider,
status: status
) )
case .share: case .share:
try await DataSourceFacade.responseToStatusShareAction( try await DataSourceFacade.responseToStatusShareAction(
@ -148,10 +149,9 @@ extension DataSourceFacade {
@MainActor @MainActor
static func responseToMenuAction( static func responseToMenuAction(
dependency: NeedsDependency & UIViewController, dependency: UIViewController & NeedsDependency & AuthContextProvider,
action: MastodonMenu.Action, action: MastodonMenu.Action,
menuContext: MenuContext, menuContext: MenuContext
authenticationBox: MastodonAuthenticationBox
) async throws { ) async throws {
switch action { switch action {
case .muteUser(let actionContext): case .muteUser(let actionContext):
@ -174,8 +174,7 @@ extension DataSourceFacade {
guard let user = _user else { return } guard let user = _user else { return }
try await DataSourceFacade.responseToUserMuteAction( try await DataSourceFacade.responseToUserMuteAction(
dependency: dependency, dependency: dependency,
user: user, user: user
authenticationBox: authenticationBox
) )
} // end Task } // end Task
} }
@ -203,8 +202,7 @@ extension DataSourceFacade {
guard let user = _user else { return } guard let user = _user else { return }
try await DataSourceFacade.responseToUserBlockAction( try await DataSourceFacade.responseToUserBlockAction(
dependency: dependency, dependency: dependency,
user: user, user: user
authenticationBox: authenticationBox
) )
} // end Task } // end Task
} }
@ -218,11 +216,12 @@ extension DataSourceFacade {
let reportViewModel = ReportViewModel( let reportViewModel = ReportViewModel(
context: dependency.context, context: dependency.context,
authContext: dependency.authContext,
user: user, user: user,
status: menuContext.status status: menuContext.status
) )
dependency.coordinator.present( _ = dependency.coordinator.present(
scene: .report(viewModel: reportViewModel), scene: .report(viewModel: reportViewModel),
from: dependency, from: dependency,
transition: .modal(animated: true, completion: nil) transition: .modal(animated: true, completion: nil)
@ -239,7 +238,7 @@ extension DataSourceFacade {
user: user user: user
) )
guard let activityViewController = _activityViewController else { return } guard let activityViewController = _activityViewController else { return }
dependency.coordinator.present( _ = dependency.coordinator.present(
scene: .activityViewController( scene: .activityViewController(
activityViewController: activityViewController, activityViewController: activityViewController,
sourceView: menuContext.button, sourceView: menuContext.button,
@ -248,6 +247,37 @@ extension DataSourceFacade {
from: dependency, from: dependency,
transition: .activityViewControllerPresent(animated: true, completion: nil) 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: case .deleteStatus:
let alertController = UIAlertController( let alertController = UIAlertController(
title: "Delete Post", title: "Delete Post",
@ -263,8 +293,7 @@ extension DataSourceFacade {
Task { Task {
try await DataSourceFacade.responseToDeleteStatus( try await DataSourceFacade.responseToDeleteStatus(
dependency: dependency, dependency: dependency,
status: status, status: status
authenticationBox: authenticationBox
) )
} // end Task } // end Task
} }

View File

@ -8,10 +8,11 @@
import Foundation import Foundation
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MastodonCore
extension DataSourceFacade { extension DataSourceFacade {
static func coordinateToStatusThreadScene( static func coordinateToStatusThreadScene(
provider: DataSourceProvider, provider: DataSourceProvider & AuthContextProvider,
target: StatusTarget, target: StatusTarget,
status: ManagedObjectRecord<Status> status: ManagedObjectRecord<Status>
) async { ) async {
@ -39,14 +40,15 @@ extension DataSourceFacade {
@MainActor @MainActor
static func coordinateToStatusThreadScene( static func coordinateToStatusThreadScene(
provider: DataSourceProvider, provider: DataSourceProvider & AuthContextProvider,
root: StatusItem.Thread root: StatusItem.Thread
) async { ) async {
let threadViewModel = ThreadViewModel( let threadViewModel = ThreadViewModel(
context: provider.context, context: provider.context,
authContext: provider.authContext,
optionalRoot: root optionalRoot: root
) )
provider.coordinator.present( _ = provider.coordinator.present(
scene: .thread(viewModel: threadViewModel), scene: .thread(viewModel: threadViewModel),
from: provider, from: provider,
transition: .show transition: .show

View File

@ -7,18 +7,18 @@
import UIKit import UIKit
import MetaTextKit import MetaTextKit
import MastodonUI
import CoreDataStack import CoreDataStack
import MastodonCore
import MastodonUI
// MARK: - Notification AuthorMenuAction // MARK: - Notification AuthorMenuAction
extension NotificationTableViewCellDelegate where Self: DataSourceProvider { extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell( func tableViewCell(
_ cell: UITableViewCell, _ cell: UITableViewCell,
notificationView: NotificationView, notificationView: NotificationView,
menuButton button: UIButton, menuButton button: UIButton,
didSelectAction action: MastodonMenu.Action didSelectAction action: MastodonMenu.Action
) { ) {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
Task { Task {
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
guard let item = await item(from: source) else { guard let item = await item(from: source) else {
@ -47,15 +47,14 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
status: nil, status: nil,
button: button, button: button,
barButtonItem: nil barButtonItem: nil
), )
authenticationBox: authenticationBox
) )
} // end Task } // end Task
} }
} }
// MARK: - Notification Author Avatar // MARK: - Notification Author Avatar
extension NotificationTableViewCellDelegate where Self: DataSourceProvider { extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell( func tableViewCell(
_ cell: UITableViewCell, _ cell: UITableViewCell,
notificationView: NotificationView, notificationView: NotificationView,
@ -88,7 +87,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
} }
// MARK: - Follow Request // MARK: - Follow Request
extension NotificationTableViewCellDelegate where Self: DataSourceProvider { extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell( func tableViewCell(
_ cell: UITableViewCell, _ cell: UITableViewCell,
@ -106,15 +105,10 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
return return
} }
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
try await DataSourceFacade.responseToUserFollowRequestAction( try await DataSourceFacade.responseToUserFollowRequestAction(
dependency: self, dependency: self,
notification: notification, notification: notification,
query: .accept, query: .accept
authenticationBox: authenticationBox
) )
} // end Task } // end Task
} }
@ -135,15 +129,10 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
return return
} }
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
try await DataSourceFacade.responseToUserFollowRequestAction( try await DataSourceFacade.responseToUserFollowRequestAction(
dependency: self, dependency: self,
notification: notification, notification: notification,
query: .reject, query: .reject
authenticationBox: authenticationBox
) )
} // end Task } // end Task
} }
@ -151,7 +140,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
} }
// MARK: - Status Content // MARK: - Status Content
extension NotificationTableViewCellDelegate where Self: DataSourceProvider { extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell( func tableViewCell(
_ cell: UITableViewCell, _ cell: UITableViewCell,
notificationView: NotificationView, notificationView: NotificationView,
@ -279,7 +268,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Med
} }
// MARK: - Status Toolbar // MARK: - Status Toolbar
extension NotificationTableViewCellDelegate where Self: DataSourceProvider { extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell( func tableViewCell(
_ cell: UITableViewCell, _ cell: UITableViewCell,
notificationView: NotificationView, notificationView: NotificationView,
@ -287,7 +276,6 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
buttonDidPressed button: UIButton, buttonDidPressed button: UIButton,
action: ActionToolbarContainer.Action action: ActionToolbarContainer.Action
) { ) {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
Task { Task {
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
guard let item = await item(from: source) else { guard let item = await item(from: source) else {
@ -311,7 +299,6 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
provider: self, provider: self,
status: status, status: status,
action: action, action: action,
authenticationBox: authenticationBox,
sender: button sender: button
) )
} // end Task } // end Task
@ -319,7 +306,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
} }
// MARK: - Status Author Avatar // MARK: - Status Author Avatar
extension NotificationTableViewCellDelegate where Self: DataSourceProvider { extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell( func tableViewCell(
_ cell: UITableViewCell, _ cell: UITableViewCell,
notificationView: NotificationView, notificationView: NotificationView,
@ -354,7 +341,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
} }
// MARK: - Status Content // MARK: - Status Content
extension NotificationTableViewCellDelegate where Self: DataSourceProvider { extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell( func tableViewCell(
_ cell: UITableViewCell, _ cell: UITableViewCell,
@ -530,7 +517,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
} }
// MARK: a11y // MARK: a11y
extension NotificationTableViewCellDelegate where Self: DataSourceProvider { extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, accessibilityActivate: Void) { func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, accessibilityActivate: Void) {
Task { Task {
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)

View File

@ -8,10 +8,11 @@
import UIKit import UIKit
import CoreDataStack import CoreDataStack
import MetaTextKit import MetaTextKit
import MastodonCore
import MastodonUI import MastodonUI
// MARK: - header // MARK: - header
extension StatusTableViewCellDelegate where Self: DataSourceProvider { extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell( func tableViewCell(
_ cell: UITableViewCell, _ cell: UITableViewCell,
@ -64,7 +65,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
} }
// MARK: - avatar button // MARK: - avatar button
extension StatusTableViewCellDelegate where Self: DataSourceProvider { extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell( func tableViewCell(
_ cell: UITableViewCell, _ cell: UITableViewCell,
@ -92,7 +93,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
} }
// MARK: - content // MARK: - content
extension StatusTableViewCellDelegate where Self: DataSourceProvider { extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell( func tableViewCell(
_ cell: UITableViewCell, _ cell: UITableViewCell,
@ -169,7 +170,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & MediaPrev
// MARK: - poll // MARK: - poll
extension StatusTableViewCellDelegate where Self: DataSourceProvider { extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell( func tableViewCell(
_ cell: UITableViewCell, _ cell: UITableViewCell,
@ -177,7 +178,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
pollTableView tableView: UITableView, pollTableView tableView: UITableView,
didSelectRowAt indexPath: IndexPath didSelectRowAt indexPath: IndexPath
) { ) {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let pollTableViewDiffableDataSource = statusView.pollTableViewDiffableDataSource else { return } guard let pollTableViewDiffableDataSource = statusView.pollTableViewDiffableDataSource else { return }
guard let pollItem = pollTableViewDiffableDataSource.itemIdentifier(for: indexPath) 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( _ = try await context.apiService.vote(
poll: poll, poll: poll,
choices: [choice], 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") logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): vote poll for \(choice) success")
} catch { } catch {
@ -248,7 +248,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
statusView: StatusView, statusView: StatusView,
pollVoteButtonPressed button: UIButton pollVoteButtonPressed button: UIButton
) { ) {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let pollTableViewDiffableDataSource = statusView.pollTableViewDiffableDataSource else { return } guard let pollTableViewDiffableDataSource = statusView.pollTableViewDiffableDataSource else { return }
guard let firstPollItem = pollTableViewDiffableDataSource.snapshot().itemIdentifiers.first else { return } guard let firstPollItem = pollTableViewDiffableDataSource.snapshot().itemIdentifiers.first else { return }
guard case let .option(firstPollOption) = firstPollItem else { return } guard case let .option(firstPollOption) = firstPollItem else { return }
@ -284,7 +283,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
_ = try await context.apiService.vote( _ = try await context.apiService.vote(
poll: poll, poll: poll,
choices: choices, 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") logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): vote poll for \(choices) success")
} catch { } catch {
@ -303,7 +302,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
} }
// MARK: - toolbar // MARK: - toolbar
extension StatusTableViewCellDelegate where Self: DataSourceProvider { extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell( func tableViewCell(
_ cell: UITableViewCell, _ cell: UITableViewCell,
statusView: StatusView, statusView: StatusView,
@ -311,7 +310,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
buttonDidPressed button: UIButton, buttonDidPressed button: UIButton,
action: ActionToolbarContainer.Action action: ActionToolbarContainer.Action
) { ) {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
Task { Task {
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
guard let item = await item(from: source) else { guard let item = await item(from: source) else {
@ -327,7 +325,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
provider: self, provider: self,
status: status, status: status,
action: action, action: action,
authenticationBox: authenticationBox,
sender: button sender: button
) )
} // end Task } // end Task
@ -336,14 +333,13 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
} }
// MARK: - menu button // MARK: - menu button
extension StatusTableViewCellDelegate where Self: DataSourceProvider { extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell( func tableViewCell(
_ cell: UITableViewCell, _ cell: UITableViewCell,
statusView: StatusView, statusView: StatusView,
menuButton button: UIButton, menuButton button: UIButton,
didSelectAction action: MastodonMenu.Action didSelectAction action: MastodonMenu.Action
) { ) {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
Task { Task {
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
guard let item = await item(from: source) else { guard let item = await item(from: source) else {
@ -372,8 +368,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
status: status, status: status,
button: button, button: button,
barButtonItem: nil barButtonItem: nil
), )
authenticationBox: authenticationBox
) )
} // end Task } // end Task
} }
@ -475,7 +470,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
} }
// MARK: - StatusMetricView // MARK: - StatusMetricView
extension StatusTableViewCellDelegate where Self: DataSourceProvider { extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton) { func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton) {
Task { Task {
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
@ -489,6 +484,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
} }
let userListViewModel = UserListViewModel( let userListViewModel = UserListViewModel(
context: context, context: context,
authContext: authContext,
kind: .rebloggedBy(status: status) kind: .rebloggedBy(status: status)
) )
await coordinator.present( await coordinator.present(
@ -512,6 +508,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
} }
let userListViewModel = UserListViewModel( let userListViewModel = UserListViewModel(
context: context, context: context,
authContext: authContext,
kind: .favoritedBy(status: status) kind: .favoritedBy(status: status)
) )
await coordinator.present( await coordinator.present(
@ -524,7 +521,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
} }
// MARK: a11y // MARK: a11y
extension StatusTableViewCellDelegate where Self: DataSourceProvider { extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void) { func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void) {
Task { Task {
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)

View File

@ -8,6 +8,7 @@
import os.log import os.log
import UIKit import UIKit
import CoreDataStack import CoreDataStack
import MastodonCore
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & StatusTableViewControllerNavigateableRelay { 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) { func statusKeyCommandHandler(_ sender: UIKeyCommand) {
guard let rawValue = sender.propertyList as? String, guard let rawValue = sender.propertyList as? String,
@ -53,7 +54,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
} }
// status coordinate // status coordinate
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider { extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider {
@MainActor @MainActor
private func statusRecord() async -> ManagedObjectRecord<Status>? { private func statusRecord() async -> ManagedObjectRecord<Status>? {
@ -93,16 +94,15 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
private func replyStatus() async { private func replyStatus() async {
guard let status = await statusRecord() else { return } guard let status = await statusRecord() else { return }
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
let selectionFeedbackGenerator = UISelectionFeedbackGenerator() let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
selectionFeedbackGenerator.selectionChanged() selectionFeedbackGenerator.selectionChanged()
let composeViewModel = ComposeViewModel( let composeViewModel = ComposeViewModel(
context: self.context, context: self.context,
composeKind: .reply(status: status), authContext: authContext,
authenticationBox: authenticationBox kind: .reply(status: status)
) )
self.coordinator.present( _ = self.coordinator.present(
scene: .compose(viewModel: composeViewModel), scene: .compose(viewModel: composeViewModel),
from: self, from: self,
transition: .modal(animated: true, completion: nil) transition: .modal(animated: true, completion: nil)
@ -144,19 +144,16 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
} }
// toggle // toggle
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider { extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider {
@MainActor @MainActor
private func toggleReblog() async { private func toggleReblog() async {
guard let status = await statusRecord() else { return } guard let status = await statusRecord() else { return }
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
do { do {
try await DataSourceFacade.responseToStatusReblogAction( try await DataSourceFacade.responseToStatusReblogAction(
provider: self, provider: self,
status: status, status: status
authenticationBox: authenticationBox
) )
} catch { } catch {
assertionFailure() assertionFailure()
@ -167,13 +164,10 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
private func toggleFavorite() async { private func toggleFavorite() async {
guard let status = await statusRecord() else { return } guard let status = await statusRecord() else { return }
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
do { do {
try await DataSourceFacade.responseToStatusFavoriteAction( try await DataSourceFacade.responseToStatusFavoriteAction(
provider: self, provider: self,
status: status, status: status
authenticationBox: authenticationBox
) )
} catch { } catch {
assertionFailure() assertionFailure()

View File

@ -7,6 +7,7 @@
import os.log import os.log
import UIKit import UIKit
import MastodonCore
extension TableViewControllerNavigateableCore where Self: TableViewControllerNavigateableRelay { extension TableViewControllerNavigateableCore where Self: TableViewControllerNavigateableRelay {
var navigationKeyCommands: [UIKeyCommand] { var navigationKeyCommands: [UIKeyCommand] {
@ -124,7 +125,7 @@ extension TableViewControllerNavigateableCore {
} }
extension TableViewControllerNavigateableCore where Self: DataSourceProvider { extension TableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider {
func open() { func open() {
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return } guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
let source = DataSourceItem.Source(indexPath: indexPathForSelectedRow) let source = DataSourceItem.Source(indexPath: indexPathForSelectedRow)

View File

@ -8,9 +8,11 @@
import os.log import os.log
import UIKit import UIKit
import CoreDataStack import CoreDataStack
import MastodonCore
import MastodonUI
import MastodonLocalization import MastodonLocalization
extension UITableViewDelegate where Self: DataSourceProvider { extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider {
func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 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)") logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): indexPath: \(indexPath.debugDescription)")

View File

@ -5,50 +5,59 @@
// Created by Cirno MainasuK on 2021-9-13. // Created by Cirno MainasuK on 2021-9-13.
// //
import os.log
import UIKit import UIKit
import Combine import Combine
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MastodonSDK import MastodonSDK
import MastodonMeta import MastodonMeta
import MastodonCore
import MastodonUI
final class AccountListViewModel { final class AccountListViewModel: NSObject {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
// input // input
let context: AppContext let context: AppContext
let authContext: AuthContext
let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController<MastodonAuthentication>
// output // output
let authentications = CurrentValueSubject<[Item], Never>([]) @Published var authentications: [ManagedObjectRecord<MastodonAuthentication>] = []
let activeMastodonUserObjectID = CurrentValueSubject<NSManagedObjectID?, Never>(nil) @Published var items: [Item] = []
let dataSourceDidUpdate = PassthroughSubject<Void, Never>() let dataSourceDidUpdate = PassthroughSubject<Void, Never>()
var diffableDataSource: UITableViewDiffableDataSource<Section, Item>! var diffableDataSource: UITableViewDiffableDataSource<Section, Item>!
init(context: AppContext) { init(context: AppContext, authContext: AuthContext) {
self.context = context self.context = context
self.authContext = authContext
Publishers.CombineLatest( self.mastodonAuthenticationFetchedResultsController = {
context.authenticationService.mastodonAuthentications, let fetchRequest = MastodonAuthentication.sortedFetchRequest
context.authenticationService.activeMastodonAuthentication fetchRequest.returnsObjectsAsFaults = false
) fetchRequest.fetchBatchSize = 20
.sink { [weak self] authentications, activeAuthentication in let controller = NSFetchedResultsController(
guard let self = self else { return } fetchRequest: fetchRequest,
var items: [Item] = [] managedObjectContext: context.managedObjectContext,
var activeMastodonUserObjectID: NSManagedObjectID? sectionNameKeyPath: nil,
for authentication in authentications { cacheName: nil
let item = Item.authentication(objectID: authentication.objectID) )
items.append(item) return controller
if authentication === activeAuthentication { }()
activeMastodonUserObjectID = authentication.user.objectID super.init()
} // end init
}
self.authentications.value = items mastodonAuthenticationFetchedResultsController.delegate = self
self.activeMastodonUserObjectID.value = activeMastodonUserObjectID do {
try mastodonAuthenticationFetchedResultsController.performFetch()
authentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?.compactMap { $0.asRecrod } ?? []
} catch {
assertionFailure(error.localizedDescription)
} }
.store(in: &disposeBag)
authentications $authentications
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] authentications in .sink { [weak self] authentications in
guard let self = self else { return } guard let self = self else { return }
@ -56,7 +65,10 @@ final class AccountListViewModel {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main]) 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) snapshot.appendItems([.addAccount], toSection: .main)
diffableDataSource.apply(snapshot) { diffableDataSource.apply(snapshot) {
@ -74,7 +86,7 @@ extension AccountListViewModel {
} }
enum Item: Hashable { enum Item: Hashable {
case authentication(objectID: NSManagedObjectID) case authentication(record: ManagedObjectRecord<MastodonAuthentication>)
case addAccount case addAccount
} }
@ -84,14 +96,17 @@ extension AccountListViewModel {
) { ) {
diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in
switch item { switch item {
case .authentication(let objectID): case .authentication(let record):
let authentication = managedObjectContext.object(with: objectID) as! MastodonAuthentication
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell
AccountListViewModel.configure( if let authentication = record.object(in: managedObjectContext),
cell: cell, let activeAuthentication = self.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)
authentication: authentication, {
activeMastodonUserObjectID: self.activeMastodonUserObjectID.eraseToAnyPublisher() AccountListViewModel.configure(
) cell: cell,
authentication: authentication,
activeAuthentication: activeAuthentication
)
}
return cell return cell
case .addAccount: case .addAccount:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AddAccountTableViewCell.self), for: indexPath) as! AddAccountTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AddAccountTableViewCell.self), for: indexPath) as! AddAccountTableViewCell
@ -107,7 +122,7 @@ extension AccountListViewModel {
static func configure( static func configure(
cell: AccountListTableViewCell, cell: AccountListTableViewCell,
authentication: MastodonAuthentication, authentication: MastodonAuthentication,
activeMastodonUserObjectID: AnyPublisher<NSManagedObjectID?, Never> activeAuthentication: MastodonAuthentication
) { ) {
let user = authentication.user let user = authentication.user
@ -136,19 +151,14 @@ extension AccountListViewModel {
cell.badgeButton.setBadge(number: count) cell.badgeButton.setBadge(number: count)
// checkmark // checkmark
activeMastodonUserObjectID let isActive = activeAuthentication.userID == authentication.userID
.receive(on: DispatchQueue.main) cell.tintColor = .label
.sink { objectID in cell.checkmarkImageView.isHidden = !isActive
let isCurrentUser = user.objectID == objectID if isActive {
cell.tintColor = .label cell.accessibilityTraits.insert(.selected)
cell.checkmarkImageView.isHidden = !isCurrentUser } else {
if isCurrentUser { cell.accessibilityTraits.remove(.selected)
cell.accessibilityTraits.insert(.selected) }
} else {
cell.accessibilityTraits.remove(.selected)
}
}
.store(in: &cell.disposeBag)
cell.accessibilityLabel = [ cell.accessibilityLabel = [
cell.nameLabel.text, cell.nameLabel.text,
@ -159,3 +169,21 @@ extension AccountListViewModel {
.joined(separator: " ") .joined(separator: " ")
} }
} }
// MARK: - NSFetchedResultsControllerDelegate
extension AccountListViewModel: NSFetchedResultsControllerDelegate {
public func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard controller === mastodonAuthenticationFetchedResultsController else {
assertionFailure()
return
}
authentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?.compactMap { $0.asRecrod } ?? []
}
}

View File

@ -12,6 +12,7 @@ import CoreDataStack
import PanModal import PanModal
import MastodonAsset import MastodonAsset
import MastodonLocalization import MastodonLocalization
import MastodonCore
final class AccountListViewController: UIViewController, NeedsDependency { final class AccountListViewController: UIViewController, NeedsDependency {
@ -21,7 +22,7 @@ final class AccountListViewController: UIViewController, NeedsDependency {
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
private(set) lazy var viewModel = AccountListViewModel(context: context) var viewModel: AccountListViewModel!
private(set) lazy var addBarButtonItem: UIBarButtonItem = { private(set) lazy var addBarButtonItem: UIBarButtonItem = {
let barButtonItem = UIBarButtonItem( let barButtonItem = UIBarButtonItem(
@ -63,7 +64,10 @@ extension AccountListViewController: PanModalPresentable {
return .contentHeight(CGFloat(height)) 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) let height = calculateHeight(of: count)
return .contentHeight(height) return .contentHeight(height)
} }
@ -154,7 +158,7 @@ extension AccountListViewController {
@objc private func addBarButtonItem(_ sender: UIBarButtonItem) { @objc private func addBarButtonItem(_ sender: UIBarButtonItem) {
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") 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) { @objc private func dragIndicatorTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
@ -173,19 +177,17 @@ extension AccountListViewController: UITableViewDelegate {
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
switch item { switch item {
case .authentication(let objectID): case .authentication(let record):
assert(Thread.isMainThread) assert(Thread.isMainThread)
let authentication = context.managedObjectContext.object(with: objectID) as! MastodonAuthentication guard let authentication = record.object(in: context.managedObjectContext) else { return }
context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID) Task { @MainActor in
.receive(on: DispatchQueue.main) let isActive = try await context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID)
.sink { [weak self] result in guard isActive else { return }
guard let self = self else { return } self.coordinator.setup()
self.coordinator.setup() } // end Task
}
.store(in: &disposeBag)
case .addAccount: case .addAccount:
// TODO: add dismiss entry for welcome scene // 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))
} }
} }
} }

View File

@ -9,6 +9,7 @@ import UIKit
import Combine import Combine
import FLAnimatedImage import FLAnimatedImage
import MetaTextKit import MetaTextKit
import MastodonCore
import MastodonUI import MastodonUI
final class AccountListTableViewCell: UITableViewCell { final class AccountListTableViewCell: UITableViewCell {

View File

@ -10,6 +10,8 @@ import Combine
import MetaTextKit import MetaTextKit
import MastodonAsset import MastodonAsset
import MastodonLocalization import MastodonLocalization
import MastodonCore
import MastodonUI
final class AddAccountTableViewCell: UITableViewCell { final class AddAccountTableViewCell: UITableViewCell {

View File

@ -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<AutoCompleteSection, AutoCompleteItem>()
snapshot.appendSections([.main])
diffableDataSource?.apply(snapshot)
}
}

View File

@ -27,7 +27,7 @@ final class ComposeStatusAttachmentCollectionViewCell: UICollectionViewCell {
weak var delegate: ComposeStatusAttachmentCollectionViewCellDelegate? weak var delegate: ComposeStatusAttachmentCollectionViewCellDelegate?
let attachmentContainerView = AttachmentContainerView() // let attachmentContainerView = AttachmentContainerView()
let removeButton: UIButton = { let removeButton: UIButton = {
let button = HighlightDimmableButton() let button = HighlightDimmableButton()
button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10) button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
@ -45,11 +45,11 @@ final class ComposeStatusAttachmentCollectionViewCell: UICollectionViewCell {
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
attachmentContainerView.activityIndicatorView.startAnimating() // attachmentContainerView.activityIndicatorView.startAnimating()
attachmentContainerView.previewImageView.af.cancelImageRequest() // attachmentContainerView.previewImageView.af.cancelImageRequest()
attachmentContainerView.previewImageView.image = .placeholder(color: .systemFill) // attachmentContainerView.previewImageView.image = .placeholder(color: .systemFill)
delegate = nil // delegate = nil
disposeBag.removeAll() // disposeBag.removeAll()
} }
override init(frame: CGRect) { override init(frame: CGRect) {
@ -73,31 +73,30 @@ extension ComposeStatusAttachmentCollectionViewCell {
private func _init() { private func _init() {
// selectionStyle = .none // selectionStyle = .none
attachmentContainerView.translatesAutoresizingMaskIntoConstraints = false // attachmentContainerView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(attachmentContainerView) // contentView.addSubview(attachmentContainerView)
NSLayoutConstraint.activate([ // NSLayoutConstraint.activate([
attachmentContainerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: ComposeStatusAttachmentCollectionViewCell.verticalMarginHeight), // attachmentContainerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: ComposeStatusAttachmentCollectionViewCell.verticalMarginHeight),
attachmentContainerView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), // attachmentContainerView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
attachmentContainerView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), // attachmentContainerView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: attachmentContainerView.bottomAnchor, constant: ComposeStatusAttachmentCollectionViewCell.verticalMarginHeight), // contentView.bottomAnchor.constraint(equalTo: attachmentContainerView.bottomAnchor, constant: ComposeStatusAttachmentCollectionViewCell.verticalMarginHeight),
attachmentContainerView.heightAnchor.constraint(equalToConstant: 205).priority(.defaultHigh), // attachmentContainerView.heightAnchor.constraint(equalToConstant: 205).priority(.defaultHigh),
]) // ])
//
removeButton.translatesAutoresizingMaskIntoConstraints = false // removeButton.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(removeButton) // contentView.addSubview(removeButton)
NSLayoutConstraint.activate([ // NSLayoutConstraint.activate([
removeButton.centerXAnchor.constraint(equalTo: attachmentContainerView.trailingAnchor), // removeButton.centerXAnchor.constraint(equalTo: attachmentContainerView.trailingAnchor),
removeButton.centerYAnchor.constraint(equalTo: attachmentContainerView.topAnchor), // removeButton.centerYAnchor.constraint(equalTo: attachmentContainerView.topAnchor),
removeButton.widthAnchor.constraint(equalToConstant: ComposeStatusAttachmentCollectionViewCell.removeButtonSize.width).priority(.defaultHigh), // removeButton.widthAnchor.constraint(equalToConstant: ComposeStatusAttachmentCollectionViewCell.removeButtonSize.width).priority(.defaultHigh),
removeButton.heightAnchor.constraint(equalToConstant: ComposeStatusAttachmentCollectionViewCell.removeButtonSize.height).priority(.defaultHigh), // removeButton.heightAnchor.constraint(equalToConstant: ComposeStatusAttachmentCollectionViewCell.removeButtonSize.height).priority(.defaultHigh),
]) // ])
//
removeButton.addTarget(self, action: #selector(ComposeStatusAttachmentCollectionViewCell.removeButtonDidPressed(_:)), for: .touchUpInside) // removeButton.addTarget(self, action: #selector(ComposeStatusAttachmentCollectionViewCell.removeButtonDidPressed(_:)), for: .touchUpInside)
} }
} }
extension ComposeStatusAttachmentCollectionViewCell { extension ComposeStatusAttachmentCollectionViewCell {
@objc private func removeButtonDidPressed(_ sender: UIButton) { @objc private func removeButtonDidPressed(_ sender: UIButton) {

View File

@ -9,68 +9,70 @@ import os.log
import UIKit import UIKit
import Combine import Combine
import MastodonAsset import MastodonAsset
import MastodonCore
import MastodonUI
import MastodonLocalization import MastodonLocalization
protocol ComposeStatusPollExpiresOptionCollectionViewCellDelegate: AnyObject { //protocol ComposeStatusPollExpiresOptionCollectionViewCellDelegate: AnyObject {
func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption) // func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption)
} //}
//
final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCell { //final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCell {
//
var disposeBag = Set<AnyCancellable>() // var disposeBag = Set<AnyCancellable>()
weak var delegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate? // weak var delegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate?
//
let durationButton: UIButton = { // let durationButton: UIButton = {
let button = HighlightDimmableButton() // let button = HighlightDimmableButton()
button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12)) // button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12))
button.expandEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: -20, right: -20) // 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.setTitle(L10n.Scene.Compose.Poll.durationTime(L10n.Scene.Compose.Poll.thirtyMinutes), for: .normal)
button.setTitleColor(Asset.Colors.brand.color, for: .normal) // button.setTitleColor(Asset.Colors.brand.color, for: .normal)
return button // return button
}() // }()
//
override init(frame: CGRect) { // override init(frame: CGRect) {
super.init(frame: frame) // super.init(frame: frame)
_init() // _init()
} // }
//
required init?(coder: NSCoder) { // required init?(coder: NSCoder) {
super.init(coder: coder) // super.init(coder: coder)
_init() // _init()
} // }
//
} //}
//
extension ComposeStatusPollExpiresOptionCollectionViewCell { //extension ComposeStatusPollExpiresOptionCollectionViewCell {
//
private typealias ExpiresOption = ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption // private typealias ExpiresOption = ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption
//
private func _init() { // private func _init() {
durationButton.translatesAutoresizingMaskIntoConstraints = false // durationButton.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(durationButton) // contentView.addSubview(durationButton)
NSLayoutConstraint.activate([ // NSLayoutConstraint.activate([
durationButton.topAnchor.constraint(equalTo: contentView.topAnchor), // durationButton.topAnchor.constraint(equalTo: contentView.topAnchor),
durationButton.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor, constant: PollOptionView.checkmarkBackgroundLeadingMargin), // durationButton.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor, constant: PollOptionView.checkmarkBackgroundLeadingMargin),
durationButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), // durationButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
]) // ])
//
let children = ExpiresOption.allCases.map { expiresOption -> UIAction in // 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 // UIAction(title: expiresOption.title, image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in
guard let self = self else { return } // guard let self = self else { return }
self.expiresOptionActionHandler(action, expiresOption: expiresOption) // self.expiresOptionActionHandler(action, expiresOption: expiresOption)
} // }
} // }
durationButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children) // durationButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
durationButton.showsMenuAsPrimaryAction = true // durationButton.showsMenuAsPrimaryAction = true
} // }
//
} //}
//
extension ComposeStatusPollExpiresOptionCollectionViewCell { //extension ComposeStatusPollExpiresOptionCollectionViewCell {
//
private func expiresOptionActionHandler(_ sender: UIAction, expiresOption: ExpiresOption) { // 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) // 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) // delegate?.composeStatusPollExpiresOptionCollectionViewCell(self, didSelectExpiresOption: expiresOption)
} // }
//
} //}

View File

@ -8,6 +8,8 @@
import os.log import os.log
import UIKit import UIKit
import MastodonAsset import MastodonAsset
import MastodonCore
import MastodonUI
import MastodonLocalization import MastodonLocalization
protocol ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate: AnyObject { protocol ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate: AnyObject {

View File

@ -9,6 +9,7 @@ import os.log
import UIKit import UIKit
import Combine import Combine
import MastodonAsset import MastodonAsset
import MastodonCore
import MastodonLocalization import MastodonLocalization
import MastodonUI import MastodonUI

File diff suppressed because it is too large Load Diff

View File

@ -9,505 +9,482 @@ import os.log
import UIKit import UIKit
import Combine import Combine
import CoreDataStack import CoreDataStack
import MastodonSDK
import MastodonMeta
import MetaTextKit import MetaTextKit
import MastodonMeta
import MastodonAsset import MastodonAsset
import MastodonCore
import MastodonLocalization import MastodonLocalization
import MastodonSDK
extension ComposeViewModel { extension ComposeViewModel {
func setupDataSource( // func setupDataSource(
tableView: UITableView, // tableView: UITableView,
metaTextDelegate: MetaTextDelegate, // metaTextDelegate: MetaTextDelegate,
metaTextViewDelegate: UITextViewDelegate, // metaTextViewDelegate: UITextViewDelegate,
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel, // customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate, // composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate, // composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate, // composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate // composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
) { // ) {
// UI // // UI
bind() // bind()
//
// content // // content
bind(cell: composeStatusContentTableViewCell, tableView: tableView) // bind(cell: composeStatusContentTableViewCell, tableView: tableView)
composeStatusContentTableViewCell.metaText.delegate = metaTextDelegate // composeStatusContentTableViewCell.metaText.delegate = metaTextDelegate
composeStatusContentTableViewCell.metaText.textView.delegate = metaTextViewDelegate // composeStatusContentTableViewCell.metaText.textView.delegate = metaTextViewDelegate
//
// attachment // // attachment
bind(cell: composeStatusAttachmentTableViewCell, tableView: tableView) // bind(cell: composeStatusAttachmentTableViewCell, tableView: tableView)
composeStatusAttachmentTableViewCell.composeStatusAttachmentCollectionViewCellDelegate = composeStatusAttachmentCollectionViewCellDelegate // composeStatusAttachmentTableViewCell.composeStatusAttachmentCollectionViewCellDelegate = composeStatusAttachmentCollectionViewCellDelegate
//
// poll // // poll
bind(cell: composeStatusPollTableViewCell, tableView: tableView) // bind(cell: composeStatusPollTableViewCell, tableView: tableView)
composeStatusPollTableViewCell.delegate = self // composeStatusPollTableViewCell.delegate = self
composeStatusPollTableViewCell.customEmojiPickerInputViewModel = customEmojiPickerInputViewModel // composeStatusPollTableViewCell.customEmojiPickerInputViewModel = customEmojiPickerInputViewModel
composeStatusPollTableViewCell.composeStatusPollOptionCollectionViewCellDelegate = composeStatusPollOptionCollectionViewCellDelegate // composeStatusPollTableViewCell.composeStatusPollOptionCollectionViewCellDelegate = composeStatusPollOptionCollectionViewCellDelegate
composeStatusPollTableViewCell.composeStatusPollOptionAppendEntryCollectionViewCellDelegate = composeStatusPollOptionAppendEntryCollectionViewCellDelegate // composeStatusPollTableViewCell.composeStatusPollOptionAppendEntryCollectionViewCellDelegate = composeStatusPollOptionAppendEntryCollectionViewCellDelegate
composeStatusPollTableViewCell.composeStatusPollExpiresOptionCollectionViewCellDelegate = composeStatusPollExpiresOptionCollectionViewCellDelegate // composeStatusPollTableViewCell.composeStatusPollExpiresOptionCollectionViewCellDelegate = composeStatusPollExpiresOptionCollectionViewCellDelegate
//
// setup data source // // setup data source
tableView.dataSource = self // tableView.dataSource = self
} // }
//
func setupCustomEmojiPickerDiffableDataSource( // func setupCustomEmojiPickerDiffableDataSource(
for collectionView: UICollectionView, // for collectionView: UICollectionView,
dependency: NeedsDependency // dependency: NeedsDependency
) { // ) {
let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource( // let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource(
for: collectionView, // for: collectionView,
dependency: dependency // dependency: dependency
) // )
self.customEmojiPickerDiffableDataSource = diffableDataSource // self.customEmojiPickerDiffableDataSource = diffableDataSource
//
let _domain = customEmojiViewModel?.domain // let _domain = customEmojiViewModel?.domain
customEmojiViewModel?.emojis // customEmojiViewModel?.emojis
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [weak self, weak diffableDataSource] emojis in // .sink { [weak self, weak diffableDataSource] emojis in
guard let _ = self else { return } // guard let _ = self else { return }
guard let diffableDataSource = diffableDataSource else { return } // guard let diffableDataSource = diffableDataSource else { return }
//
var snapshot = NSDiffableDataSourceSnapshot<CustomEmojiPickerSection, CustomEmojiPickerItem>() // var snapshot = NSDiffableDataSourceSnapshot<CustomEmojiPickerSection, CustomEmojiPickerItem>()
let domain = _domain?.uppercased() ?? " " // let domain = _domain?.uppercased() ?? " "
let customEmojiSection = CustomEmojiPickerSection.emoji(name: domain) // let customEmojiSection = CustomEmojiPickerSection.emoji(name: domain)
snapshot.appendSections([customEmojiSection]) // snapshot.appendSections([customEmojiSection])
let items: [CustomEmojiPickerItem] = { // let items: [CustomEmojiPickerItem] = {
var items = [CustomEmojiPickerItem]() // var items = [CustomEmojiPickerItem]()
for emoji in emojis where emoji.visibleInPicker { // for emoji in emojis where emoji.visibleInPicker {
let attribute = CustomEmojiPickerItem.CustomEmojiAttribute(emoji: emoji) // let attribute = CustomEmojiPickerItem.CustomEmojiAttribute(emoji: emoji)
let item = CustomEmojiPickerItem.emoji(attribute: attribute) // let item = CustomEmojiPickerItem.emoji(attribute: attribute)
items.append(item) // items.append(item)
} // }
return items // return items
}() // }()
snapshot.appendItems(items, toSection: customEmojiSection) // snapshot.appendItems(items, toSection: customEmojiSection)
//
diffableDataSource.apply(snapshot) // diffableDataSource.apply(snapshot)
} // }
.store(in: &disposeBag) // .store(in: &disposeBag)
} // }
} }
// MARK: - UITableViewDataSource //// MARK: - UITableViewDataSource
extension ComposeViewModel: UITableViewDataSource { //extension ComposeViewModel: UITableViewDataSource {
enum Section: CaseIterable { // func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
case repliedTo // switch Section.allCases[indexPath.section] {
case status // case .repliedTo:
case attachment // let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentTableViewCell
case poll // 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 { //// MARK: - ComposeStatusPollTableViewCellDelegate
return Section.allCases.count //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)
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { //
switch Section.allCases[section] { // self.pollOptionAttributes = options
case .repliedTo: // }
switch composeKind { //}
case .reply: return 1 //
default: return 0 //extension ComposeViewModel {
} // private func bind() {
case .status: return 1 // $isCustomEmojiComposing
case .attachment: return 1 // .assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing)
case .poll: return 1 // .store(in: &disposeBag)
} //
} // $isContentWarningComposing
// .assign(to: \.isContentWarningComposing, on: composeStatusAttribute)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // .store(in: &disposeBag)
switch Section.allCases[indexPath.section] { //
case .repliedTo: // // bind compose toolbar UI state
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentTableViewCell // Publishers.CombineLatest(
guard case let .reply(record) = composeKind else { return cell } // $isPollComposing,
// $attachmentServices
// bind frame publisher // )
cell.framePublisher // .receive(on: DispatchQueue.main)
.receive(on: DispatchQueue.main) // .sink(receiveValue: { [weak self] isPollComposing, attachmentServices in
.assign(to: \.repliedToCellFrame, on: self) // guard let self = self else { return }
.store(in: &cell.disposeBag) // let shouldMediaDisable = isPollComposing || attachmentServices.count >= self.maxMediaAttachments
// let shouldPollDisable = attachmentServices.count > 0
// set initial width //
if cell.statusView.frame.width == .zero { // self.isMediaToolbarButtonEnabled = !shouldMediaDisable
cell.statusView.frame.size.width = tableView.frame.width // self.isPollToolbarButtonEnabled = !shouldPollDisable
} // })
// .store(in: &disposeBag)
// configure status //
context.managedObjectContext.performAndWait { // // calculate `Idempotency-Key`
guard let replyTo = record.object(in: context.managedObjectContext) else { return } // let content = Publishers.CombineLatest3(
cell.statusView.configure(status: replyTo) // composeStatusAttribute.$isContentWarningComposing,
} // composeStatusAttribute.$contentWarningContent,
// composeStatusAttribute.$composeContent
return cell // )
case .status: // .map { isContentWarningComposing, contentWarningContent, composeContent -> String in
return composeStatusContentTableViewCell // if isContentWarningComposing {
case .attachment: // return contentWarningContent + (composeContent ?? "")
return composeStatusAttachmentTableViewCell // } else {
case .poll: // return composeContent ?? ""
return composeStatusPollTableViewCell // }
} // }
} // let attachmentIDs = $attachmentServices.map { attachments -> String in
} // let attachmentIDs = attachments.compactMap { $0.attachment.value?.id }
// return attachmentIDs.joined(separator: ",")
// MARK: - ComposeStatusPollTableViewCellDelegate // }
extension ComposeViewModel: ComposeStatusPollTableViewCellDelegate { // let pollOptionsAndDuration = Publishers.CombineLatest3(
func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute]) { // $isPollComposing,
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) // $pollOptionAttributes,
// pollExpiresOptionAttribute.expiresOption
self.pollOptionAttributes = options // )
} // .map { isPollComposing, pollOptionAttributes, expiresOption -> String in
} // guard isPollComposing else {
// return ""
extension ComposeViewModel { // }
private func bind() { //
$isCustomEmojiComposing // let pollOptions = pollOptionAttributes.map { $0.option.value }.joined(separator: ",")
.assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing) // return pollOptions + expiresOption.rawValue
.store(in: &disposeBag) // }
//
$isContentWarningComposing // Publishers.CombineLatest4(
.assign(to: \.isContentWarningComposing, on: composeStatusAttribute) // content,
.store(in: &disposeBag) // attachmentIDs,
// pollOptionsAndDuration,
// bind compose toolbar UI state // $selectedStatusVisibility
Publishers.CombineLatest( // )
$isPollComposing, // .map { content, attachmentIDs, pollOptionsAndDuration, selectedStatusVisibility -> String in
$attachmentServices // var hasher = Hasher()
) // hasher.combine(content)
.receive(on: DispatchQueue.main) // hasher.combine(attachmentIDs)
.sink(receiveValue: { [weak self] isPollComposing, attachmentServices in // hasher.combine(pollOptionsAndDuration)
guard let self = self else { return } // hasher.combine(selectedStatusVisibility.visibility.rawValue)
let shouldMediaDisable = isPollComposing || attachmentServices.count >= self.maxMediaAttachments // let hashValue = hasher.finalize()
let shouldPollDisable = attachmentServices.count > 0 // return "\(hashValue)"
// }
self.isMediaToolbarButtonEnabled = !shouldMediaDisable // .assign(to: \.value, on: idempotencyKey)
self.isPollToolbarButtonEnabled = !shouldPollDisable // .store(in: &disposeBag)
}) //
.store(in: &disposeBag) // // bind modal dismiss state
// composeStatusAttribute.$composeContent
// calculate `Idempotency-Key` // .receive(on: DispatchQueue.main)
let content = Publishers.CombineLatest3( // .map { [weak self] content in
composeStatusAttribute.$isContentWarningComposing, // let content = content ?? ""
composeStatusAttribute.$contentWarningContent, // if content.isEmpty {
composeStatusAttribute.$composeContent // return true
) // }
.map { isContentWarningComposing, contentWarningContent, composeContent -> String in // // if preInsertedContent plus a space is equal to the content, simply dismiss the modal
if isContentWarningComposing { // if let preInsertedContent = self?.preInsertedContent {
return contentWarningContent + (composeContent ?? "") // return content == preInsertedContent
} else { // }
return composeContent ?? "" // return false
} // }
} // .assign(to: &$shouldDismiss)
let attachmentIDs = $attachmentServices.map { attachments -> String in //
let attachmentIDs = attachments.compactMap { $0.attachment.value?.id } // // bind compose bar button item UI state
return attachmentIDs.joined(separator: ",") // let isComposeContentEmpty = composeStatusAttribute.$composeContent
} // .map { ($0 ?? "").isEmpty }
let pollOptionsAndDuration = Publishers.CombineLatest3( // let isComposeContentValid = $characterCount
$isPollComposing, // .compactMap { [weak self] characterCount -> Bool in
$pollOptionAttributes, // guard let self = self else { return characterCount <= 500 }
pollExpiresOptionAttribute.expiresOption // return characterCount <= self.composeContentLimit
) // }
.map { isPollComposing, pollOptionAttributes, expiresOption -> String in // let isMediaEmpty = $attachmentServices
guard isPollComposing else { // .map { $0.isEmpty }
return "" // let isMediaUploadAllSuccess = $attachmentServices
} // .map { services in
// services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish }
let pollOptions = pollOptionAttributes.map { $0.option.value }.joined(separator: ",") // }
return pollOptions + expiresOption.rawValue // let isPollAttributeAllValid = $pollOptionAttributes
} // .map { pollAttributes in
// pollAttributes.allSatisfy { attribute -> Bool in
Publishers.CombineLatest4( // !attribute.option.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
content, // }
attachmentIDs, // }
pollOptionsAndDuration, //
$selectedStatusVisibility // let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4(
) // isComposeContentEmpty,
.map { content, attachmentIDs, pollOptionsAndDuration, selectedStatusVisibility -> String in // isComposeContentValid,
var hasher = Hasher() // isMediaEmpty,
hasher.combine(content) // isMediaUploadAllSuccess
hasher.combine(attachmentIDs) // )
hasher.combine(pollOptionsAndDuration) // .map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in
hasher.combine(selectedStatusVisibility.visibility.rawValue) // if isMediaEmpty {
let hashValue = hasher.finalize() // return isComposeContentValid && !isComposeContentEmpty
return "\(hashValue)" // } else {
} // return isComposeContentValid && isMediaUploadAllSuccess
.assign(to: \.value, on: idempotencyKey) // }
.store(in: &disposeBag) // }
// .eraseToAnyPublisher()
// bind modal dismiss state //
composeStatusAttribute.$composeContent // let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4(
.receive(on: DispatchQueue.main) // isComposeContentEmpty,
.map { [weak self] content in // isComposeContentValid,
let content = content ?? "" // $isPollComposing,
if content.isEmpty { // isPollAttributeAllValid
return true // )
} // .map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollAttributeAllValid -> Bool in
// if preInsertedContent plus a space is equal to the content, simply dismiss the modal // if isPollComposing {
if let preInsertedContent = self?.preInsertedContent { // return isComposeContentValid && !isComposeContentEmpty && isPollAttributeAllValid
return content == preInsertedContent // } else {
} // return isComposeContentValid && !isComposeContentEmpty
return false // }
} // }
.assign(to: &$shouldDismiss) // .eraseToAnyPublisher()
//
// bind compose bar button item UI state // Publishers.CombineLatest(
let isComposeContentEmpty = composeStatusAttribute.$composeContent // isPublishBarButtonItemEnabledPrecondition1,
.map { ($0 ?? "").isEmpty } // isPublishBarButtonItemEnabledPrecondition2
let isComposeContentValid = $characterCount // )
.compactMap { [weak self] characterCount -> Bool in // .map { $0 && $1 }
guard let self = self else { return characterCount <= 500 } // .assign(to: &$isPublishBarButtonItemEnabled)
return characterCount <= self.composeContentLimit // }
} //}
let isMediaEmpty = $attachmentServices //
.map { $0.isEmpty } //extension ComposeViewModel {
let isMediaUploadAllSuccess = $attachmentServices // private func bind(
.map { services in // cell: ComposeStatusContentTableViewCell,
services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish } // tableView: UITableView
} // ) {
let isPollAttributeAllValid = $pollOptionAttributes // // bind status content character count
.map { pollAttributes in // Publishers.CombineLatest3(
pollAttributes.allSatisfy { attribute -> Bool in // composeStatusAttribute.$composeContent,
!attribute.option.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty // composeStatusAttribute.$isContentWarningComposing,
} // composeStatusAttribute.$contentWarningContent
} // )
// .map { composeContent, isContentWarningComposing, contentWarningContent -> Int in
let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4( // let composeContent = composeContent ?? ""
isComposeContentEmpty, // var count = composeContent.count
isComposeContentValid, // if isContentWarningComposing {
isMediaEmpty, // count += contentWarningContent.count
isMediaUploadAllSuccess // }
) // return count
.map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in // }
if isMediaEmpty { // .assign(to: &$characterCount)
return isComposeContentValid && !isComposeContentEmpty //
} else { // // bind content warning
return isComposeContentValid && isMediaUploadAllSuccess // composeStatusAttribute.$isContentWarningComposing
} // .receive(on: DispatchQueue.main)
} // .sink { [weak cell, weak tableView] isContentWarningComposing in
.eraseToAnyPublisher() // guard let cell = cell else { return }
// guard let tableView = tableView else { return }
let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4( //
isComposeContentEmpty, // // self size input cell
isComposeContentValid, // cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing
$isPollComposing, // cell.statusContentWarningEditorView.alpha = 0
isPollAttributeAllValid // UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) {
) // cell.statusContentWarningEditorView.alpha = 1
.map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollAttributeAllValid -> Bool in // tableView.beginUpdates()
if isPollComposing { // tableView.endUpdates()
return isComposeContentValid && !isComposeContentEmpty && isPollAttributeAllValid // } completion: { _ in
} else { // // do nothing
return isComposeContentValid && !isComposeContentEmpty // }
} // }
} // .store(in: &disposeBag)
.eraseToAnyPublisher() //
// cell.contentWarningContent
Publishers.CombineLatest( // .removeDuplicates()
isPublishBarButtonItemEnabledPrecondition1, // .receive(on: DispatchQueue.main)
isPublishBarButtonItemEnabledPrecondition2 // .sink { [weak tableView, weak self] text in
) // guard let self = self else { return }
.map { $0 && $1 } // // bind input data
.assign(to: &$isPublishBarButtonItemEnabled) // self.composeStatusAttribute.contentWarningContent = text
} //
} // // self size input cell
// guard let tableView = tableView else { return }
extension ComposeViewModel { // UIView.performWithoutAnimation {
private func bind( // tableView.beginUpdates()
cell: ComposeStatusContentTableViewCell, // tableView.endUpdates()
tableView: UITableView // }
) { // }
// bind status content character count // .store(in: &cell.disposeBag)
Publishers.CombineLatest3( //
composeStatusAttribute.$composeContent, // // configure custom emoji picker
composeStatusAttribute.$isContentWarningComposing, // ComposeStatusSection.configureCustomEmojiPicker(
composeStatusAttribute.$contentWarningContent // viewModel: customEmojiPickerInputViewModel,
) // customEmojiReplaceableTextInput: cell.metaText.textView,
.map { composeContent, isContentWarningComposing, contentWarningContent -> Int in // disposeBag: &disposeBag
let composeContent = composeContent ?? "" // )
var count = composeContent.count // ComposeStatusSection.configureCustomEmojiPicker(
if isContentWarningComposing { // viewModel: customEmojiPickerInputViewModel,
count += contentWarningContent.count // customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView,
} // disposeBag: &disposeBag
return count // )
} // }
.assign(to: &$characterCount) //}
//
// bind content warning //extension ComposeViewModel {
composeStatusAttribute.$isContentWarningComposing // private func bind(
.receive(on: DispatchQueue.main) // cell: ComposeStatusPollTableViewCell,
.sink { [weak cell, weak tableView] isContentWarningComposing in // tableView: UITableView
guard let cell = cell else { return } // ) {
guard let tableView = tableView else { return } // Publishers.CombineLatest(
// $isPollComposing,
// self size input cell // $pollOptionAttributes
cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing // )
cell.statusContentWarningEditorView.alpha = 0 // .receive(on: DispatchQueue.main)
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) { // .sink { [weak self] isPollComposing, pollOptionAttributes in
cell.statusContentWarningEditorView.alpha = 1 // guard let self = self else { return }
tableView.beginUpdates() // guard self.isViewAppeared else { return }
tableView.endUpdates() //
} completion: { _ in // let cell = self.composeStatusPollTableViewCell
// do nothing // guard let dataSource = cell.dataSource else { return }
} //
} // var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusPollSection, ComposeStatusPollItem>()
.store(in: &disposeBag) // snapshot.appendSections([.main])
// var items: [ComposeStatusPollItem] = []
cell.contentWarningContent // if isPollComposing {
.removeDuplicates() // for attribute in pollOptionAttributes {
.receive(on: DispatchQueue.main) // items.append(.pollOption(attribute: attribute))
.sink { [weak tableView, weak self] text in // }
guard let self = self else { return } // if pollOptionAttributes.count < self.maxPollOptions {
// bind input data // items.append(.pollOptionAppendEntry)
self.composeStatusAttribute.contentWarningContent = text // }
// items.append(.pollExpiresOption(attribute: self.pollExpiresOptionAttribute))
// self size input cell // }
guard let tableView = tableView else { return } // snapshot.appendItems(items, toSection: .main)
UIView.performWithoutAnimation { //
tableView.beginUpdates() // tableView.performBatchUpdates {
tableView.endUpdates() // if #available(iOS 15.0, *) {
} // dataSource.apply(snapshot, animatingDifferences: false)
} // } else {
.store(in: &cell.disposeBag) // dataSource.apply(snapshot, animatingDifferences: true)
// }
// configure custom emoji picker // }
ComposeStatusSection.configureCustomEmojiPicker( // }
viewModel: customEmojiPickerInputViewModel, // .store(in: &disposeBag)
customEmojiReplaceableTextInput: cell.metaText.textView, //
disposeBag: &disposeBag // // bind delegate
) // $pollOptionAttributes
ComposeStatusSection.configureCustomEmojiPicker( // .sink { [weak self] pollAttributes in
viewModel: customEmojiPickerInputViewModel, // guard let self = self else { return }
customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView, // pollAttributes.forEach { $0.delegate = self }
disposeBag: &disposeBag // }
) // .store(in: &disposeBag)
} // }
} //}
//
extension ComposeViewModel { //extension ComposeViewModel {
private func bind( // private func bind(
cell: ComposeStatusPollTableViewCell, // cell: ComposeStatusAttachmentTableViewCell,
tableView: UITableView // tableView: UITableView
) { // ) {
Publishers.CombineLatest( // cell.collectionViewHeightDidUpdate
$isPollComposing, // .receive(on: DispatchQueue.main)
$pollOptionAttributes // .sink { [weak self] _ in
) // guard let _ = self else { return }
.receive(on: DispatchQueue.main) // tableView.beginUpdates()
.sink { [weak self] isPollComposing, pollOptionAttributes in // tableView.endUpdates()
guard let self = self else { return } // }
guard self.isViewAppeared else { return } // .store(in: &disposeBag)
//
let cell = self.composeStatusPollTableViewCell // $attachmentServices
guard let dataSource = cell.dataSource else { return } // .removeDuplicates()
// .receive(on: DispatchQueue.main)
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusPollSection, ComposeStatusPollItem>() // .sink { [weak self] attachmentServices in
snapshot.appendSections([.main]) // guard let self = self else { return }
var items: [ComposeStatusPollItem] = [] // guard self.isViewAppeared else { return }
if isPollComposing { //
for attribute in pollOptionAttributes { // let cell = self.composeStatusAttachmentTableViewCell
items.append(.pollOption(attribute: attribute)) // guard let dataSource = cell.dataSource else { return }
} //
if pollOptionAttributes.count < self.maxPollOptions { // var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusAttachmentSection, ComposeStatusAttachmentItem>()
items.append(.pollOptionAppendEntry) // snapshot.appendSections([.main])
} // let items = attachmentServices.map { ComposeStatusAttachmentItem.attachment(attachmentService: $0) }
items.append(.pollExpiresOption(attribute: self.pollExpiresOptionAttribute)) // snapshot.appendItems(items, toSection: .main)
} //
snapshot.appendItems(items, toSection: .main) // if #available(iOS 15.0, *) {
// dataSource.applySnapshotUsingReloadData(snapshot)
tableView.performBatchUpdates { // } else {
if #available(iOS 15.0, *) { // dataSource.apply(snapshot, animatingDifferences: false)
dataSource.apply(snapshot, animatingDifferences: false) // }
} else { // }
dataSource.apply(snapshot, animatingDifferences: true) // .store(in: &disposeBag)
} //
} // // setup attribute updater
} // $attachmentServices
.store(in: &disposeBag) // .receive(on: DispatchQueue.main)
// .debounce(for: 0.3, scheduler: DispatchQueue.main)
// bind delegate // .sink { attachmentServices in
$pollOptionAttributes // // drive service upload state
.sink { [weak self] pollAttributes in // // make image upload in the queue
guard let self = self else { return } // for attachmentService in attachmentServices {
pollAttributes.forEach { $0.delegate = self } // // skip when prefix N task when task finish OR fail OR uploading
} // guard let currentState = attachmentService.uploadStateMachine.currentState else { break }
.store(in: &disposeBag) // if currentState is MastodonAttachmentService.UploadState.Fail {
} // continue
} // }
// if currentState is MastodonAttachmentService.UploadState.Finish {
extension ComposeViewModel { // continue
private func bind( // }
cell: ComposeStatusAttachmentTableViewCell, // if currentState is MastodonAttachmentService.UploadState.Processing {
tableView: UITableView // continue
) { // }
cell.collectionViewHeightDidUpdate // if currentState is MastodonAttachmentService.UploadState.Uploading {
.receive(on: DispatchQueue.main) // break
.sink { [weak self] _ in // }
guard let _ = self else { return } // // trigger uploading one by one
tableView.beginUpdates() // if currentState is MastodonAttachmentService.UploadState.Initial {
tableView.endUpdates() // attachmentService.uploadStateMachine.enter(MastodonAttachmentService.UploadState.Uploading.self)
} // break
.store(in: &disposeBag) // }
// }
$attachmentServices // }
.removeDuplicates() // .store(in: &disposeBag)
.receive(on: DispatchQueue.main) //
.sink { [weak self] attachmentServices in // // bind delegate
guard let self = self else { return } // $attachmentServices
guard self.isViewAppeared else { return } // .sink { [weak self] attachmentServices in
// guard let self = self else { return }
let cell = self.composeStatusAttachmentTableViewCell // attachmentServices.forEach { $0.delegate = self }
guard let dataSource = cell.dataSource else { return } // }
// .store(in: &disposeBag)
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusAttachmentSection, ComposeStatusAttachmentItem>() // }
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)
}
}

View File

@ -12,153 +12,153 @@ import CoreDataStack
import GameplayKit import GameplayKit
import MastodonSDK import MastodonSDK
extension ComposeViewModel { //extension ComposeViewModel {
class PublishState: GKState { // class PublishState: GKState {
weak var viewModel: ComposeViewModel? // weak var viewModel: ComposeViewModel?
//
init(viewModel: ComposeViewModel) { // init(viewModel: ComposeViewModel) {
self.viewModel = viewModel // self.viewModel = viewModel
} // }
//
override func didEnter(from previousState: GKState?) { // 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) // 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 // viewModel?.publishStateMachinePublisher.value = self
} // }
} // }
} //}
extension ComposeViewModel.PublishState { //extension ComposeViewModel.PublishState {
class Initial: ComposeViewModel.PublishState { // class Initial: ComposeViewModel.PublishState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool { // override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return stateClass == Publishing.self // return stateClass == Publishing.self
} // }
} // }
//
class Publishing: ComposeViewModel.PublishState { // class Publishing: ComposeViewModel.PublishState {
//
var publishingSubscription: AnyCancellable? // var publishingSubscription: AnyCancellable?
//
override func isValidNextState(_ stateClass: AnyClass) -> Bool { // override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return stateClass == Fail.self || stateClass == Finish.self // return stateClass == Fail.self || stateClass == Finish.self
} // }
//
override func didEnter(from previousState: GKState?) { // override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState) // super.didEnter(from: previousState)
guard let viewModel = viewModel, let stateMachine = stateMachine else { return } // guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
//
viewModel.updatePublishDate() // viewModel.updatePublishDate()
//
let authenticationBox = viewModel.authenticationBox // let authenticationBox = viewModel.authenticationBox
let domain = authenticationBox.domain // let domain = authenticationBox.domain
let attachmentServices = viewModel.attachmentServices // let attachmentServices = viewModel.attachmentServices
let mediaIDs = attachmentServices.compactMap { attachmentService in // let mediaIDs = attachmentServices.compactMap { attachmentService in
attachmentService.attachment.value?.id // attachmentService.attachment.value?.id
} // }
let pollOptions: [String]? = { // let pollOptions: [String]? = {
guard viewModel.isPollComposing else { return nil } // guard viewModel.isPollComposing else { return nil }
return viewModel.pollOptionAttributes.map { attribute in attribute.option.value } // return viewModel.pollOptionAttributes.map { attribute in attribute.option.value }
}() // }()
let pollExpiresIn: Int? = { // let pollExpiresIn: Int? = {
guard viewModel.isPollComposing else { return nil } // guard viewModel.isPollComposing else { return nil }
return viewModel.pollExpiresOptionAttribute.expiresOption.value.seconds // return viewModel.pollExpiresOptionAttribute.expiresOption.value.seconds
}() // }()
let inReplyToID: Mastodon.Entity.Status.ID? = { // let inReplyToID: Mastodon.Entity.Status.ID? = {
guard case let .reply(status) = viewModel.composeKind else { return nil } // guard case let .reply(status) = viewModel.composeKind else { return nil }
var id: Mastodon.Entity.Status.ID? // var id: Mastodon.Entity.Status.ID?
viewModel.context.managedObjectContext.performAndWait { // viewModel.context.managedObjectContext.performAndWait {
guard let replyTo = status.object(in: viewModel.context.managedObjectContext) else { return } // guard let replyTo = status.object(in: viewModel.context.managedObjectContext) else { return }
id = replyTo.id // id = replyTo.id
} // }
return id // return id
}() // }()
let sensitive: Bool = viewModel.isContentWarningComposing // let sensitive: Bool = viewModel.isContentWarningComposing
let spoilerText: String? = { // let spoilerText: String? = {
let text = viewModel.composeStatusAttribute.contentWarningContent.trimmingCharacters(in: .whitespacesAndNewlines) // let text = viewModel.composeStatusAttribute.contentWarningContent.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty else { // guard !text.isEmpty else {
return nil // return nil
} // }
return text // return text
}() // }()
let visibility = viewModel.selectedStatusVisibility.visibility // let visibility = viewModel.selectedStatusVisibility.visibility
//
let updateMediaQuerySubscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = { // let updateMediaQuerySubscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = {
var subscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = [] // var subscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = []
for attachmentService in attachmentServices { // for attachmentService in attachmentServices {
guard let attachmentID = attachmentService.attachment.value?.id else { continue } // guard let attachmentID = attachmentService.attachment.value?.id else { continue }
let description = attachmentService.description.value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" // let description = attachmentService.description.value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !description.isEmpty else { continue } // guard !description.isEmpty else { continue }
let query = Mastodon.API.Media.UpdateMediaQuery( // let query = Mastodon.API.Media.UpdateMediaQuery(
file: nil, // file: nil,
thumbnail: nil, // thumbnail: nil,
description: description, // description: description,
focus: nil // focus: nil
) // )
let subscription = viewModel.context.apiService.updateMedia( // let subscription = viewModel.context.apiService.updateMedia(
domain: domain, // domain: domain,
attachmentID: attachmentID, // attachmentID: attachmentID,
query: query, // query: query,
mastodonAuthenticationBox: authenticationBox // mastodonAuthenticationBox: authenticationBox
) // )
subscriptions.append(subscription) // subscriptions.append(subscription)
} // }
return subscriptions // return subscriptions
}() // }()
//
let idempotencyKey = viewModel.idempotencyKey.value // let idempotencyKey = viewModel.idempotencyKey.value
//
publishingSubscription = Publishers.MergeMany(updateMediaQuerySubscriptions) // publishingSubscription = Publishers.MergeMany(updateMediaQuerySubscriptions)
.collect() // .collect()
.asyncMap { attachments -> Mastodon.Response.Content<Mastodon.Entity.Status> in // .asyncMap { attachments -> Mastodon.Response.Content<Mastodon.Entity.Status> in
let query = Mastodon.API.Statuses.PublishStatusQuery( // let query = Mastodon.API.Statuses.PublishStatusQuery(
status: viewModel.composeStatusAttribute.composeContent, // status: viewModel.composeStatusAttribute.composeContent,
mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs, // mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs,
pollOptions: pollOptions, // pollOptions: pollOptions,
pollExpiresIn: pollExpiresIn, // pollExpiresIn: pollExpiresIn,
inReplyToID: inReplyToID, // inReplyToID: inReplyToID,
sensitive: sensitive, // sensitive: sensitive,
spoilerText: spoilerText, // spoilerText: spoilerText,
visibility: visibility // visibility: visibility
) // )
return try await viewModel.context.apiService.publishStatus( // return try await viewModel.context.apiService.publishStatus(
domain: domain, // domain: domain,
idempotencyKey: idempotencyKey, // idempotencyKey: idempotencyKey,
query: query, // query: query,
authenticationBox: authenticationBox // authenticationBox: authenticationBox
) // )
} // }
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { completion in // .sink { completion in
switch completion { // switch completion {
case .failure(let error): // 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) // 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) // stateMachine.enter(Fail.self)
case .finished: // case .finished:
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status success", ((#file as NSString).lastPathComponent), #line, #function) // os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status success", ((#file as NSString).lastPathComponent), #line, #function)
stateMachine.enter(Finish.self) // stateMachine.enter(Finish.self)
} // }
} receiveValue: { response in // } 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) // 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 { // class Fail: ComposeViewModel.PublishState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool { // override func isValidNextState(_ stateClass: AnyClass) -> Bool {
// allow discard publishing // // allow discard publishing
return stateClass == Publishing.self || stateClass == Discard.self // return stateClass == Publishing.self || stateClass == Discard.self
} // }
} // }
//
class Discard: ComposeViewModel.PublishState { // class Discard: ComposeViewModel.PublishState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool { // override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return false // return false
} // }
} // }
//
class Finish: ComposeViewModel.PublishState { // class Finish: ComposeViewModel.PublishState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool { // override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return false // return false
} // }
} // }
//
} //}

View File

@ -13,6 +13,7 @@ import CoreDataStack
import GameplayKit import GameplayKit
import MastodonSDK import MastodonSDK
import MastodonAsset import MastodonAsset
import MastodonCore
import MastodonLocalization import MastodonLocalization
import MastodonMeta import MastodonMeta
import MastodonUI import MastodonUI
@ -27,159 +28,159 @@ final class ComposeViewModel: NSObject {
// input // input
let context: AppContext let context: AppContext
let composeKind: ComposeStatusSection.ComposeKind let authContext: AuthContext
let authenticationBox: MastodonAuthenticationBox let kind: ComposeContentViewModel.Kind
@Published var isPollComposing = false // var authenticationBox: MastodonAuthenticationBox {
@Published var isCustomEmojiComposing = false // authContext.mastodonAuthenticationBox
@Published var isContentWarningComposing = false // }
//
@Published var selectedStatusVisibility: ComposeToolbarView.VisibilitySelectionType // @Published var isPollComposing = false
@Published var repliedToCellFrame: CGRect = .zero // @Published var isCustomEmojiComposing = false
@Published var autoCompleteRetryLayoutTimes = 0 // @Published var isContentWarningComposing = false
@Published var autoCompleteInfo: ComposeViewController.AutoCompleteInfo? = nil //
// @Published var selectedStatusVisibility: ComposeToolbarView.VisibilitySelectionType
// @Published var repliedToCellFrame: CGRect = .zero
// @Published var autoCompleteRetryLayoutTimes = 0
// @Published var autoCompleteInfo: ComposeViewController.AutoCompleteInfo? = nil
let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make initial event emit let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make initial event emit
var isViewAppeared = false // var isViewAppeared = false
// output // output
let instanceConfiguration: Mastodon.Entity.Instance.Configuration? // let instanceConfiguration: Mastodon.Entity.Instance.Configuration?
var composeContentLimit: Int { // var composeContentLimit: Int {
guard let maxCharacters = instanceConfiguration?.statuses?.maxCharacters else { return 500 } // guard let maxCharacters = instanceConfiguration?.statuses?.maxCharacters else { return 500 }
return max(1, maxCharacters) // return max(1, maxCharacters)
} // }
var maxMediaAttachments: Int { // var maxMediaAttachments: Int {
guard let maxMediaAttachments = instanceConfiguration?.statuses?.maxMediaAttachments else { // guard let maxMediaAttachments = instanceConfiguration?.statuses?.maxMediaAttachments else {
return 4 // return 4
} // }
// FIXME: update timeline media preview UI // // FIXME: update timeline media preview UI
return min(4, max(1, maxMediaAttachments)) // return min(4, max(1, maxMediaAttachments))
// return max(1, maxMediaAttachments) // // return max(1, maxMediaAttachments)
} // }
var maxPollOptions: Int { // var maxPollOptions: Int {
guard let maxOptions = instanceConfiguration?.polls?.maxOptions else { return 4 } // guard let maxOptions = instanceConfiguration?.polls?.maxOptions else { return 4 }
return max(2, maxOptions) // return max(2, maxOptions)
} // }
//
let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute() // let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
let composeStatusContentTableViewCell = ComposeStatusContentTableViewCell() // let composeStatusContentTableViewCell = ComposeStatusContentTableViewCell()
let composeStatusAttachmentTableViewCell = ComposeStatusAttachmentTableViewCell() // let composeStatusAttachmentTableViewCell = ComposeStatusAttachmentTableViewCell()
let composeStatusPollTableViewCell = ComposeStatusPollTableViewCell() // let composeStatusPollTableViewCell = ComposeStatusPollTableViewCell()
//
// var dataSource: UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>? // // var dataSource: UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>?
var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>? // var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>?
private(set) lazy var publishStateMachine: GKStateMachine = { // private(set) lazy var publishStateMachine: GKStateMachine = {
// exclude timeline middle fetcher state // // exclude timeline middle fetcher state
let stateMachine = GKStateMachine(states: [ // let stateMachine = GKStateMachine(states: [
PublishState.Initial(viewModel: self), // PublishState.Initial(viewModel: self),
PublishState.Publishing(viewModel: self), // PublishState.Publishing(viewModel: self),
PublishState.Fail(viewModel: self), // PublishState.Fail(viewModel: self),
PublishState.Discard(viewModel: self), // PublishState.Discard(viewModel: self),
PublishState.Finish(viewModel: self), // PublishState.Finish(viewModel: self),
]) // ])
stateMachine.enter(PublishState.Initial.self) // stateMachine.enter(PublishState.Initial.self)
return stateMachine // return stateMachine
}() // }()
private(set) lazy var publishStateMachinePublisher = CurrentValueSubject<PublishState?, Never>(nil) // private(set) lazy var publishStateMachinePublisher = CurrentValueSubject<PublishState?, Never>(nil)
private(set) var publishDate = Date() // update it when enter Publishing state // private(set) var publishDate = Date() // update it when enter Publishing state
//
// TODO: group post material into Hashable class // // TODO: group post material into Hashable class
var idempotencyKey = CurrentValueSubject<String, Never>(UUID().uuidString) // var idempotencyKey = CurrentValueSubject<String, Never>(UUID().uuidString)
//
// UI & UX // // UI & UX
@Published var title: String // @Published var title: String
@Published var shouldDismiss = true // @Published var shouldDismiss = true
@Published var isPublishBarButtonItemEnabled = false // @Published var isPublishBarButtonItemEnabled = false
@Published var isMediaToolbarButtonEnabled = true // @Published var isMediaToolbarButtonEnabled = true
@Published var isPollToolbarButtonEnabled = true // @Published var isPollToolbarButtonEnabled = true
@Published var characterCount = 0 // @Published var characterCount = 0
@Published var collectionViewState: CollectionViewState = .fold // @Published var collectionViewState: CollectionViewState = .fold
//
// for hashtag: "#<hashtag> " // // for hashtag: "#<hashtag> "
// for mention: "@<mention> " // // for mention: "@<mention> "
var preInsertedContent: String? // var preInsertedContent: String?
//
// custom emojis // // custom emojis
let customEmojiViewModel: EmojiService.CustomEmojiViewModel? // let customEmojiViewModel: EmojiService.CustomEmojiViewModel?
let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel() // let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel()
@Published var isLoadingCustomEmoji = false // @Published var isLoadingCustomEmoji = false
//
// attachment // // attachment
@Published var attachmentServices: [MastodonAttachmentService] = [] // @Published var attachmentServices: [MastodonAttachmentService] = []
//
// polls // // polls
@Published var pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute] = [] // @Published var pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute] = []
let pollExpiresOptionAttribute = ComposeStatusPollItem.PollExpiresOptionAttribute() // let pollExpiresOptionAttribute = ComposeStatusPollItem.PollExpiresOptionAttribute()
init( init(
context: AppContext, context: AppContext,
composeKind: ComposeStatusSection.ComposeKind, authContext: AuthContext,
authenticationBox: MastodonAuthenticationBox kind: ComposeContentViewModel.Kind
) { ) {
self.context = context self.context = context
self.composeKind = composeKind self.authContext = authContext
self.authenticationBox = authenticationBox self.kind = kind
self.title = {
switch composeKind {
case .post, .hashtag, .mention: return L10n.Scene.Compose.Title.newPost
case .reply: return L10n.Scene.Compose.Title.newReply
}
}()
self.selectedStatusVisibility = {
// default private when user locked
var visibility: ComposeToolbarView.VisibilitySelectionType = {
guard let 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
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 { deinit {
@ -189,199 +190,192 @@ final class ComposeViewModel: NSObject {
} }
extension ComposeViewModel { extension ComposeViewModel {
enum CollectionViewState { // func createNewPollOptionIfPossible() {
case fold // snap to input // guard pollOptionAttributes.count < maxPollOptions else { return }
case expand // snap to reply //
} // let attribute = ComposeStatusPollItem.PollOptionAttribute()
// pollOptionAttributes = pollOptionAttributes + [attribute]
// }
//
// func updatePublishDate() {
// publishDate = Date()
// }
} }
extension ComposeViewModel { //extension ComposeViewModel {
func createNewPollOptionIfPossible() { //
guard pollOptionAttributes.count < maxPollOptions else { return } // enum AttachmentPrecondition: Error, LocalizedError {
// case videoAttachWithPhoto
let attribute = ComposeStatusPollItem.PollOptionAttribute() // case moreThanOneVideo
pollOptionAttributes = pollOptionAttributes + [attribute] //
} // var errorDescription: String? {
// return L10n.Common.Alerts.PublishPostFailure.title
func updatePublishDate() { // }
publishDate = Date() //
} // var failureReason: String? {
} // switch self {
// case .videoAttachWithPhoto:
extension ComposeViewModel { // return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.videoAttachWithPhoto
// case .moreThanOneVideo:
enum AttachmentPrecondition: Error, LocalizedError { // return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.moreThanOneVideo
case videoAttachWithPhoto // }
case moreThanOneVideo // }
// }
var errorDescription: String? { //
return L10n.Common.Alerts.PublishPostFailure.title // // check exclusive limit:
} // // - up to 1 video
// // - up to N photos
var failureReason: String? { // func checkAttachmentPrecondition() throws {
switch self { // let attachmentServices = self.attachmentServices
case .videoAttachWithPhoto: // guard !attachmentServices.isEmpty else { return }
return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.videoAttachWithPhoto // var photoAttachmentServices: [MastodonAttachmentService] = []
case .moreThanOneVideo: // var videoAttachmentServices: [MastodonAttachmentService] = []
return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.moreThanOneVideo // attachmentServices.forEach { service in
} // guard let file = service.file.value else {
} // assertionFailure()
} // return
// }
// check exclusive limit: // switch file {
// - up to 1 video // case .jpeg, .png, .gif:
// - up to N photos // photoAttachmentServices.append(service)
func checkAttachmentPrecondition() throws { // case .other:
let attachmentServices = self.attachmentServices // videoAttachmentServices.append(service)
guard !attachmentServices.isEmpty else { return } // }
var photoAttachmentServices: [MastodonAttachmentService] = [] // }
var videoAttachmentServices: [MastodonAttachmentService] = [] //
attachmentServices.forEach { service in // if !videoAttachmentServices.isEmpty {
guard let file = service.file.value else { // guard videoAttachmentServices.count == 1 else {
assertionFailure() // throw AttachmentPrecondition.moreThanOneVideo
return // }
} // guard photoAttachmentServices.isEmpty else {
switch file { // throw AttachmentPrecondition.videoAttachWithPhoto
case .jpeg, .png, .gif: // }
photoAttachmentServices.append(service) // }
case .other: // }
videoAttachmentServices.append(service) //
} //}
} //
//// MARK: - MastodonAttachmentServiceDelegate
if !videoAttachmentServices.isEmpty { //extension ComposeViewModel: MastodonAttachmentServiceDelegate {
guard videoAttachmentServices.count == 1 else { // func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) {
throw AttachmentPrecondition.moreThanOneVideo // // trigger new output event
} // attachmentServices = attachmentServices
guard photoAttachmentServices.isEmpty else { // }
throw AttachmentPrecondition.videoAttachWithPhoto //}
} //
} //// MARK: - ComposePollAttributeDelegate
} //extension ComposeViewModel: ComposePollAttributeDelegate {
// func composePollAttribute(_ attribute: ComposeStatusPollItem.PollOptionAttribute, pollOptionDidChange: String?) {
} // // trigger update
// pollOptionAttributes = pollOptionAttributes
// MARK: - MastodonAttachmentServiceDelegate // }
extension ComposeViewModel: MastodonAttachmentServiceDelegate { //}
func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) { //
// trigger new output event //extension ComposeViewModel {
attachmentServices = attachmentServices // private func setup(
} // cell: ComposeStatusContentTableViewCell
} // ) {
// setupStatusHeader(cell: cell)
// MARK: - ComposePollAttributeDelegate // setupStatusAuthor(cell: cell)
extension ComposeViewModel: ComposePollAttributeDelegate { // setupStatusContent(cell: cell)
func composePollAttribute(_ attribute: ComposeStatusPollItem.PollOptionAttribute, pollOptionDidChange: String?) { // }
// trigger update //
pollOptionAttributes = pollOptionAttributes // private func setupStatusHeader(
} // cell: ComposeStatusContentTableViewCell
} // ) {
// // configure header
extension ComposeViewModel { // let managedObjectContext = context.managedObjectContext
private func setup( // managedObjectContext.performAndWait {
cell: ComposeStatusContentTableViewCell // guard case let .reply(record) = self.composeKind,
) { // let replyTo = record.object(in: managedObjectContext)
setupStatusHeader(cell: cell) // else {
setupStatusAuthor(cell: cell) // cell.statusView.viewModel.header = .none
setupStatusContent(cell: cell) // return
} // }
//
private func setupStatusHeader( // let info: StatusView.ViewModel.Header.ReplyInfo
cell: ComposeStatusContentTableViewCell // do {
) { // let content = MastodonContent(
// configure header // content: replyTo.author.displayNameWithFallback,
let managedObjectContext = context.managedObjectContext // emojis: replyTo.author.emojis.asDictionary
managedObjectContext.performAndWait { // )
guard case let .reply(record) = self.composeKind, // let metaContent = try MastodonMetaContent.convert(document: content)
let replyTo = record.object(in: managedObjectContext) // info = .init(header: metaContent)
else { // } catch {
cell.statusView.viewModel.header = .none // let metaContent = PlaintextMetaContent(string: replyTo.author.displayNameWithFallback)
return // info = .init(header: metaContent)
} // }
// cell.statusView.viewModel.header = .reply(info: info)
let info: StatusView.ViewModel.Header.ReplyInfo // }
do { // }
let content = MastodonContent( //
content: replyTo.author.displayNameWithFallback, // private func setupStatusAuthor(
emojis: replyTo.author.emojis.asDictionary // cell: ComposeStatusContentTableViewCell
) // ) {
let metaContent = try MastodonMetaContent.convert(document: content) // self.context.managedObjectContext.performAndWait {
info = .init(header: metaContent) // guard let author = authenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return }
} catch { // cell.statusView.configureAuthor(author: author)
let metaContent = PlaintextMetaContent(string: replyTo.author.displayNameWithFallback) // }
info = .init(header: metaContent) // }
} //
cell.statusView.viewModel.header = .reply(info: info) // private func setupStatusContent(
} // cell: ComposeStatusContentTableViewCell
} // ) {
// switch composeKind {
private func setupStatusAuthor( // case .reply(let record):
cell: ComposeStatusContentTableViewCell // context.managedObjectContext.performAndWait {
) { // guard let status = record.object(in: context.managedObjectContext) else { return }
self.context.managedObjectContext.performAndWait { // let author = self.authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
guard let author = authenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return } //
cell.statusView.configureAuthor(author: author) // var mentionAccts: [String] = []
} // if author?.id != status.author.id {
} // mentionAccts.append("@" + status.author.acct)
// }
private func setupStatusContent( // let mentions = status.mentions
cell: ComposeStatusContentTableViewCell // .filter { author?.id != $0.id }
) { // for mention in mentions {
switch composeKind { // let acct = "@" + mention.acct
case .reply(let record): // guard !mentionAccts.contains(acct) else { continue }
context.managedObjectContext.performAndWait { // mentionAccts.append(acct)
guard let status = record.object(in: context.managedObjectContext) else { return } // }
let author = self.authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user // for acct in mentionAccts {
// UITextChecker.learnWord(acct)
var mentionAccts: [String] = [] // }
if author?.id != status.author.id { // if let spoilerText = status.spoilerText, !spoilerText.isEmpty {
mentionAccts.append("@" + status.author.acct) // self.isContentWarningComposing = true
} // self.composeStatusAttribute.contentWarningContent = spoilerText
let mentions = status.mentions // }
.filter { author?.id != $0.id } //
for mention in mentions { // let initialComposeContent = mentionAccts.joined(separator: " ")
let acct = "@" + mention.acct // let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " "
guard !mentionAccts.contains(acct) else { continue } // self.preInsertedContent = preInsertedContent
mentionAccts.append(acct) // self.composeStatusAttribute.composeContent = preInsertedContent
} // }
for acct in mentionAccts { // case .hashtag(let hashtag):
UITextChecker.learnWord(acct) // let initialComposeContent = "#" + hashtag
} // UITextChecker.learnWord(initialComposeContent)
if let spoilerText = status.spoilerText, !spoilerText.isEmpty { // let preInsertedContent = initialComposeContent + " "
self.isContentWarningComposing = true // self.preInsertedContent = preInsertedContent
self.composeStatusAttribute.contentWarningContent = spoilerText // self.composeStatusAttribute.composeContent = preInsertedContent
} // case .mention(let record):
// context.managedObjectContext.performAndWait {
let initialComposeContent = mentionAccts.joined(separator: " ") // guard let user = record.object(in: context.managedObjectContext) else { return }
let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " " // let initialComposeContent = "@" + user.acct
self.preInsertedContent = preInsertedContent // UITextChecker.learnWord(initialComposeContent)
self.composeStatusAttribute.composeContent = preInsertedContent // let preInsertedContent = initialComposeContent + " "
} // self.preInsertedContent = preInsertedContent
case .hashtag(let hashtag): // self.composeStatusAttribute.composeContent = preInsertedContent
let initialComposeContent = "#" + hashtag // }
UITextChecker.learnWord(initialComposeContent) // case .post:
let preInsertedContent = initialComposeContent + " " // self.preInsertedContent = nil
self.preInsertedContent = preInsertedContent // }
self.composeStatusAttribute.composeContent = preInsertedContent //
case .mention(let record): // // configure content warning
context.managedObjectContext.performAndWait { // if let composeContent = composeStatusAttribute.composeContent {
guard let user = record.object(in: context.managedObjectContext) else { return } // cell.metaText.textView.text = composeContent
let initialComposeContent = "@" + user.acct // }
UITextChecker.learnWord(initialComposeContent) //
let preInsertedContent = initialComposeContent + " " // // configure content warning
self.preInsertedContent = preInsertedContent // cell.statusContentWarningEditorView.textView.text = composeStatusAttribute.contentWarningContent
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
}
}

View File

@ -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<ComposeStatusAttachmentSection, ComposeStatusAttachmentItem>!
weak var composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate?
var observations = Set<NSKeyValueObservation>()
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<Void, Never>()
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
}
}
}
}

View File

@ -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<AnyCancellable>()
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<String, Never>()
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 ?? "<nil>")")
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)
}
}

View File

@ -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<ComposeStatusPollSection, ComposeStatusPollItem>!
var observations = Set<NSKeyValueObservation>()
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<Void, Never>()
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
}
}

View File

@ -6,132 +6,132 @@
// //
import UIKit import UIKit
import MastodonUI
import MastodonAsset import MastodonAsset
import MastodonCore
import MastodonUI
import MastodonLocalization import MastodonLocalization
extension AttachmentContainerView { //extension AttachmentContainerView {
final class EmptyStateView: UIView { // final class EmptyStateView: UIView {
//
static let photoFillSplitImage = Asset.Connectivity.photoFillSplit.image.withRenderingMode(.alwaysTemplate) // static let photoFillSplitImage = Asset.Connectivity.photoFillSplit.image.withRenderingMode(.alwaysTemplate)
static let videoSplashImage: UIImage = { // static let videoSplashImage: UIImage = {
let image = UIImage(systemName: "video.slash")!.withConfiguration(UIImage.SymbolConfiguration(pointSize: 64)) // let image = UIImage(systemName: "video.slash")!.withConfiguration(UIImage.SymbolConfiguration(pointSize: 64))
return image // return image
}() // }()
//
let imageView: UIImageView = { // let imageView: UIImageView = {
let imageView = UIImageView() // let imageView = UIImageView()
imageView.tintColor = Asset.Colors.Label.secondary.color // imageView.tintColor = Asset.Colors.Label.secondary.color
imageView.image = AttachmentContainerView.EmptyStateView.photoFillSplitImage // imageView.image = AttachmentContainerView.EmptyStateView.photoFillSplitImage
return imageView // return imageView
}() // }()
let label: UILabel = { // let label: UILabel = {
let label = UILabel() // let label = UILabel()
label.font = .preferredFont(forTextStyle: .body) // label.font = .preferredFont(forTextStyle: .body)
label.textColor = Asset.Colors.Label.secondary.color // label.textColor = Asset.Colors.Label.secondary.color
label.textAlignment = .center // label.textAlignment = .center
label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) // label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
label.numberOfLines = 2 // label.numberOfLines = 2
label.adjustsFontSizeToFitWidth = true // label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.3 // label.minimumScaleFactor = 0.3
return label // return label
}() // }()
//
override init(frame: CGRect) { // override init(frame: CGRect) {
super.init(frame: frame) // super.init(frame: frame)
_init() // _init()
} // }
//
required init?(coder: NSCoder) { // required init?(coder: NSCoder) {
super.init(coder: coder) // super.init(coder: coder)
_init() // _init()
} // }
//
} // }
} //}
extension AttachmentContainerView.EmptyStateView { //extension AttachmentContainerView.EmptyStateView {
private func _init() { // private func _init() {
layer.masksToBounds = true // layer.masksToBounds = true
layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius // layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius
layer.cornerCurve = .continuous // layer.cornerCurve = .continuous
backgroundColor = ThemeService.shared.currentTheme.value.systemGroupedBackgroundColor // backgroundColor = ThemeService.shared.currentTheme.value.systemGroupedBackgroundColor
//
let stackView = UIStackView() // let stackView = UIStackView()
stackView.axis = .vertical // stackView.axis = .vertical
stackView.alignment = .center // stackView.alignment = .center
stackView.translatesAutoresizingMaskIntoConstraints = false // stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView) // addSubview(stackView)
NSLayoutConstraint.activate([ // NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: topAnchor), // stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor), // stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor), // stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor), // stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
]) // ])
let topPaddingView = UIView() // let topPaddingView = UIView()
let middlePaddingView = UIView() // let middlePaddingView = UIView()
let bottomPaddingView = UIView() // let bottomPaddingView = UIView()
//
topPaddingView.translatesAutoresizingMaskIntoConstraints = false // topPaddingView.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(topPaddingView) // stackView.addArrangedSubview(topPaddingView)
imageView.translatesAutoresizingMaskIntoConstraints = false // imageView.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(imageView) // stackView.addArrangedSubview(imageView)
NSLayoutConstraint.activate([ // NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalToConstant: 92).priority(.defaultHigh), // imageView.widthAnchor.constraint(equalToConstant: 92).priority(.defaultHigh),
imageView.heightAnchor.constraint(equalToConstant: 76).priority(.defaultHigh), // imageView.heightAnchor.constraint(equalToConstant: 76).priority(.defaultHigh),
]) // ])
imageView.setContentHuggingPriority(.required - 1, for: .vertical) // imageView.setContentHuggingPriority(.required - 1, for: .vertical)
middlePaddingView.translatesAutoresizingMaskIntoConstraints = false // middlePaddingView.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(middlePaddingView) // stackView.addArrangedSubview(middlePaddingView)
stackView.addArrangedSubview(label) // stackView.addArrangedSubview(label)
bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false // bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(bottomPaddingView) // stackView.addArrangedSubview(bottomPaddingView)
NSLayoutConstraint.activate([ // NSLayoutConstraint.activate([
topPaddingView.heightAnchor.constraint(equalTo: middlePaddingView.heightAnchor, multiplier: 1.5), // topPaddingView.heightAnchor.constraint(equalTo: middlePaddingView.heightAnchor, multiplier: 1.5),
bottomPaddingView.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
//#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

View File

@ -6,160 +6,172 @@
// //
import UIKit import UIKit
import UITextView_Placeholder import SwiftUI
import MastodonAsset import MastodonUI
import MastodonLocalization
final class AttachmentContainerView: UIView { //final class AttachmentContainerView: UIView {
//
static let containerViewCornerRadius: CGFloat = 4 // static let containerViewCornerRadius: CGFloat = 4
//
var descriptionBackgroundViewFrameObservation: NSKeyValueObservation? // var descriptionBackgroundViewFrameObservation: NSKeyValueObservation?
//
let activityIndicatorView: UIActivityIndicatorView = { // let activityIndicatorView: UIActivityIndicatorView = {
let activityIndicatorView = UIActivityIndicatorView(style: .large) // let activityIndicatorView = UIActivityIndicatorView(style: .large)
activityIndicatorView.color = UIColor.white.withAlphaComponent(0.8) // activityIndicatorView.color = UIColor.white.withAlphaComponent(0.8)
return activityIndicatorView // return activityIndicatorView
}() // }()
//
let previewImageView: UIImageView = { // let previewImageView: UIImageView = {
let imageView = UIImageView() // let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill // imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius // imageView.layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius
imageView.layer.cornerCurve = .continuous // imageView.layer.cornerCurve = .continuous
imageView.layer.masksToBounds = true // imageView.layer.masksToBounds = true
return imageView // return imageView
}() // }()
//
let emptyStateView = AttachmentContainerView.EmptyStateView() // let emptyStateView = AttachmentContainerView.EmptyStateView()
let descriptionBackgroundView: UIView = { // let descriptionBackgroundView: UIView = {
let view = UIView() // let view = UIView()
view.layer.masksToBounds = true // view.layer.masksToBounds = true
view.layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius // view.layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius
view.layer.cornerCurve = .continuous // view.layer.cornerCurve = .continuous
view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] // view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
view.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 5, right: 8) // view.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 5, right: 8)
return view // return view
}() // }()
let descriptionBackgroundGradientLayer: CAGradientLayer = { // let descriptionBackgroundGradientLayer: CAGradientLayer = {
let gradientLayer = CAGradientLayer() // let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.69).cgColor] // gradientLayer.colors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.69).cgColor]
gradientLayer.locations = [0.0, 1.0] // gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0) // gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1) // gradientLayer.endPoint = CGPoint(x: 0.5, y: 1)
gradientLayer.frame = CGRect(x: 0, y: 0, width: 100, height: 100) // gradientLayer.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
return gradientLayer // return gradientLayer
}() // }()
let descriptionTextView: UITextView = { // let descriptionTextView: UITextView = {
let textView = UITextView() // let textView = UITextView()
textView.showsVerticalScrollIndicator = false // textView.showsVerticalScrollIndicator = false
textView.backgroundColor = .clear // textView.backgroundColor = .clear
textView.textColor = .white // textView.textColor = .white
textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15), maximumPointSize: 20) // textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15), maximumPointSize: 20)
textView.placeholder = L10n.Scene.Compose.Attachment.descriptionPhoto // textView.placeholder = L10n.Scene.Compose.Attachment.descriptionPhoto
textView.placeholderColor = UIColor.white.withAlphaComponent(0.6) // force white with alpha for Light/Dark mode // textView.placeholderColor = UIColor.white.withAlphaComponent(0.6) // force white with alpha for Light/Dark mode
textView.returnKeyType = .done // textView.returnKeyType = .done
return textView // return textView
}() // }()
//
override init(frame: CGRect) { // private(set) lazy var contentView = AttachmentView(viewModel: viewModel)
super.init(frame: frame) // public var viewModel: AttachmentView.ViewModel!
_init() //
} // override init(frame: CGRect) {
// super.init(frame: frame)
required init?(coder: NSCoder) { // _init()
super.init(coder: coder) // }
_init() //
} // required init?(coder: NSCoder) {
// super.init(coder: coder)
} // _init()
// }
//
//}
extension AttachmentContainerView { //extension AttachmentContainerView {
//
private func _init() { // private func _init() {
previewImageView.translatesAutoresizingMaskIntoConstraints = false // let hostingViewController = UIHostingController(rootView: contentView)
addSubview(previewImageView) // hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ // addSubview(hostingViewController.view)
previewImageView.topAnchor.constraint(equalTo: topAnchor), // NSLayoutConstraint.activate([
previewImageView.leadingAnchor.constraint(equalTo: leadingAnchor), // hostingViewController.view.topAnchor.constraint(equalTo: topAnchor),
previewImageView.trailingAnchor.constraint(equalTo: trailingAnchor), // hostingViewController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
previewImageView.bottomAnchor.constraint(equalTo: bottomAnchor), // hostingViewController.view.trailingAnchor.constraint(equalTo: trailingAnchor),
]) // hostingViewController.view.bottomAnchor.constraint(equalTo: bottomAnchor),
// ])
descriptionBackgroundView.translatesAutoresizingMaskIntoConstraints = false //
addSubview(descriptionBackgroundView) // previewImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ // addSubview(previewImageView)
descriptionBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor), // NSLayoutConstraint.activate([
descriptionBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor), // previewImageView.topAnchor.constraint(equalTo: topAnchor),
descriptionBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor), // previewImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
descriptionBackgroundView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.3), // previewImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
]) // previewImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
descriptionBackgroundView.layer.addSublayer(descriptionBackgroundGradientLayer) // ])
descriptionBackgroundViewFrameObservation = descriptionBackgroundView.observe(\.bounds, options: [.initial, .new]) { [weak self] _, _ in //
guard let self = self else { return } // descriptionBackgroundView.translatesAutoresizingMaskIntoConstraints = false
self.descriptionBackgroundGradientLayer.frame = self.descriptionBackgroundView.bounds // addSubview(descriptionBackgroundView)
} // NSLayoutConstraint.activate([
// descriptionBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
descriptionTextView.translatesAutoresizingMaskIntoConstraints = false // descriptionBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
descriptionBackgroundView.addSubview(descriptionTextView) // descriptionBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),
NSLayoutConstraint.activate([ // descriptionBackgroundView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.3),
descriptionTextView.leadingAnchor.constraint(equalTo: descriptionBackgroundView.layoutMarginsGuide.leadingAnchor), // ])
descriptionTextView.trailingAnchor.constraint(equalTo: descriptionBackgroundView.layoutMarginsGuide.trailingAnchor), // descriptionBackgroundView.layer.addSublayer(descriptionBackgroundGradientLayer)
descriptionBackgroundView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: descriptionTextView.bottomAnchor), // descriptionBackgroundViewFrameObservation = descriptionBackgroundView.observe(\.bounds, options: [.initial, .new]) { [weak self] _, _ in
descriptionTextView.heightAnchor.constraint(lessThanOrEqualToConstant: 36), // guard let self = self else { return }
]) // self.descriptionBackgroundGradientLayer.frame = self.descriptionBackgroundView.bounds
// }
emptyStateView.translatesAutoresizingMaskIntoConstraints = false //
addSubview(emptyStateView) // descriptionTextView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ // descriptionBackgroundView.addSubview(descriptionTextView)
emptyStateView.topAnchor.constraint(equalTo: topAnchor), // NSLayoutConstraint.activate([
emptyStateView.leadingAnchor.constraint(equalTo: leadingAnchor), // descriptionTextView.leadingAnchor.constraint(equalTo: descriptionBackgroundView.layoutMarginsGuide.leadingAnchor),
emptyStateView.trailingAnchor.constraint(equalTo: trailingAnchor), // descriptionTextView.trailingAnchor.constraint(equalTo: descriptionBackgroundView.layoutMarginsGuide.trailingAnchor),
emptyStateView.bottomAnchor.constraint(equalTo: bottomAnchor), // descriptionBackgroundView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: descriptionTextView.bottomAnchor),
]) // descriptionTextView.heightAnchor.constraint(lessThanOrEqualToConstant: 36),
// ])
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false //
addSubview(activityIndicatorView) // emptyStateView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ // addSubview(emptyStateView)
activityIndicatorView.centerXAnchor.constraint(equalTo: previewImageView.centerXAnchor), // NSLayoutConstraint.activate([
activityIndicatorView.centerYAnchor.constraint(equalTo: previewImageView.centerYAnchor), // 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() //// MARK: - UITextViewDelegate
//extension AttachmentContainerView: UITextViewDelegate {
emptyStateView.isHidden = true // func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
activityIndicatorView.hidesWhenStopped = true // // let keyboard dismiss when input description with "done" type return key
activityIndicatorView.startAnimating() // if textView === descriptionTextView, text == "\n" {
// textView.resignFirstResponder()
descriptionTextView.delegate = self // return false
} // }
//
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { // return true
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
}
}

View File

@ -10,6 +10,8 @@ import UIKit
import Combine import Combine
import MastodonSDK import MastodonSDK
import MastodonAsset import MastodonAsset
import MastodonCore
import MastodonUI
import MastodonLocalization import MastodonLocalization
protocol ComposeToolbarViewDelegate: AnyObject { protocol ComposeToolbarViewDelegate: AnyObject {

View File

@ -8,6 +8,8 @@
import UIKit import UIKit
import Combine import Combine
import MetaTextKit import MetaTextKit
import MastodonCore
import MastodonUI
final class CustomEmojiPickerInputViewModel { final class CustomEmojiPickerInputViewModel {

View File

@ -8,6 +8,7 @@
import UIKit import UIKit
import MastodonUI import MastodonUI
import MastodonAsset import MastodonAsset
import MastodonCore
import MastodonLocalization import MastodonLocalization
final class StatusContentWarningEditorView: UIView { final class StatusContentWarningEditorView: UIView {

View File

@ -8,6 +8,7 @@
import os.log import os.log
import UIKit import UIKit
import Combine import Combine
import MastodonCore
import MastodonUI import MastodonUI
// Local Timeline // Local Timeline
@ -115,6 +116,11 @@ extension DiscoveryCommunityViewController {
} }
// MARK: - AuthContextProvider
extension DiscoveryCommunityViewController: AuthContextProvider {
var authContext: AuthContext { viewModel.authContext }
}
// MARK: - UITableViewDelegate // MARK: - UITableViewDelegate
extension DiscoveryCommunityViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { extension DiscoveryCommunityViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
// sourcery:inline:CommunityViewController.AutoGenerateTableViewDelegate // sourcery:inline:CommunityViewController.AutoGenerateTableViewDelegate

View File

@ -18,6 +18,7 @@ extension DiscoveryCommunityViewModel {
tableView: tableView, tableView: tableView,
context: context, context: context,
configuration: StatusSection.Configuration( configuration: StatusSection.Configuration(
authContext: authContext,
statusTableViewCellDelegate: statusTableViewCellDelegate, statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil, timelineMiddleLoaderTableViewCellDelegate: nil,
filterContext: .none, filterContext: .none,

View File

@ -11,15 +11,11 @@ import GameplayKit
import MastodonSDK import MastodonSDK
extension DiscoveryCommunityViewModel { extension DiscoveryCommunityViewModel {
class State: GKState, NamingState { class State: GKState {
let logger = Logger(subsystem: "DiscoveryCommunityViewModel.State", category: "StateMachine") let logger = Logger(subsystem: "DiscoveryCommunityViewModel.State", category: "StateMachine")
let id = UUID() let id = UUID()
var name: String {
String(describing: Self.self)
}
weak var viewModel: DiscoveryCommunityViewModel? weak var viewModel: DiscoveryCommunityViewModel?
@ -29,8 +25,10 @@ extension DiscoveryCommunityViewModel {
override func didEnter(from previousState: GKState?) { override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState) 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 ?? "<nil>")") 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 @MainActor
@ -39,7 +37,7 @@ extension DiscoveryCommunityViewModel {
} }
deinit { 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 break
} }
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
stateMachine.enter(Fail.self)
return
}
let maxID = self.maxID let maxID = self.maxID
let isReloading = maxID == nil let isReloading = maxID == nil
@ -156,7 +149,7 @@ extension DiscoveryCommunityViewModel.State {
minID: nil, minID: nil,
limit: 20 limit: 20
), ),
authenticationBox: authenticationBox authenticationBox: viewModel.authContext.mastodonAuthenticationBox
) )
let newMaxID = response.link?.maxID let newMaxID = response.link?.maxID
@ -164,7 +157,7 @@ extension DiscoveryCommunityViewModel.State {
self.maxID = newMaxID self.maxID = newMaxID
var hasNewStatusesAppend = false var hasNewStatusesAppend = false
var statusIDs = isReloading ? [] : viewModel.statusFetchedResultsController.statusIDs.value var statusIDs = isReloading ? [] : viewModel.statusFetchedResultsController.statusIDs
for status in response.value { for status in response.value {
guard !statusIDs.contains(status.id) else { continue } guard !statusIDs.contains(status.id) else { continue }
statusIDs.append(status.id) statusIDs.append(status.id)
@ -177,7 +170,7 @@ extension DiscoveryCommunityViewModel.State {
} else { } else {
await enter(state: NoMore.self) await enter(state: NoMore.self)
} }
viewModel.statusFetchedResultsController.statusIDs.value = statusIDs viewModel.statusFetchedResultsController.statusIDs = statusIDs
viewModel.didLoadLatest.send() viewModel.didLoadLatest.send()
} catch { } catch {

View File

@ -12,6 +12,7 @@ import GameplayKit
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MastodonSDK import MastodonSDK
import MastodonCore
final class DiscoveryCommunityViewModel { final class DiscoveryCommunityViewModel {
@ -21,6 +22,7 @@ final class DiscoveryCommunityViewModel {
// input // input
let context: AppContext let context: AppContext
let authContext: AuthContext
let viewDidAppeared = PassthroughSubject<Void, Never>() let viewDidAppeared = PassthroughSubject<Void, Never>()
let statusFetchedResultsController: StatusFetchedResultsController let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel() let listBatchFetchViewModel = ListBatchFetchViewModel()
@ -42,20 +44,15 @@ final class DiscoveryCommunityViewModel {
let didLoadLatest = PassthroughSubject<Void, Never>() let didLoadLatest = PassthroughSubject<Void, Never>()
init(context: AppContext) { init(context: AppContext, authContext: AuthContext) {
self.context = context self.context = context
self.authContext = authContext
self.statusFetchedResultsController = StatusFetchedResultsController( self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,
domain: nil, domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil additionalTweetPredicate: nil
) )
// end init // end init
context.authenticationService.activeMastodonAuthentication
.map { $0?.domain }
.assign(to: \.value, on: statusFetchedResultsController.domain)
.store(in: &disposeBag)
} }
deinit { deinit {

View File

@ -11,6 +11,7 @@ import Combine
import Tabman import Tabman
import Pageboy import Pageboy
import MastodonAsset import MastodonAsset
import MastodonCore
import MastodonUI import MastodonUI
public class DiscoveryViewController: TabmanViewController, NeedsDependency { public class DiscoveryViewController: TabmanViewController, NeedsDependency {
@ -24,11 +25,8 @@ public class DiscoveryViewController: TabmanViewController, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
private(set) lazy var viewModel = DiscoveryViewModel( var viewModel: DiscoveryViewModel!
context: context,
coordinator: coordinator
)
private(set) lazy var buttonBar: TMBar.ButtonBar = { private(set) lazy var buttonBar: TMBar.ButtonBar = {
let buttonBar = TMBar.ButtonBar() let buttonBar = TMBar.ButtonBar()

View File

@ -9,6 +9,7 @@ import UIKit
import Combine import Combine
import Tabman import Tabman
import Pageboy import Pageboy
import MastodonCore
import MastodonLocalization import MastodonLocalization
final class DiscoveryViewModel { final class DiscoveryViewModel {
@ -17,6 +18,7 @@ final class DiscoveryViewModel {
// input // input
let context: AppContext let context: AppContext
let authContext: AuthContext
let discoveryPostsViewController: DiscoveryPostsViewController let discoveryPostsViewController: DiscoveryPostsViewController
let discoveryHashtagsViewController: DiscoveryHashtagsViewController let discoveryHashtagsViewController: DiscoveryHashtagsViewController
let discoveryNewsViewController: DiscoveryNewsViewController let discoveryNewsViewController: DiscoveryNewsViewController
@ -25,41 +27,43 @@ final class DiscoveryViewModel {
@Published var viewControllers: [ScrollViewContainer & PageViewController] @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) { func setupDependency(_ needsDependency: NeedsDependency) {
needsDependency.context = context needsDependency.context = context
needsDependency.coordinator = coordinator needsDependency.coordinator = coordinator
} }
self.context = context
discoveryPostsViewController = { discoveryPostsViewController = {
let viewController = DiscoveryPostsViewController() let viewController = DiscoveryPostsViewController()
setupDependency(viewController) setupDependency(viewController)
viewController.viewModel = DiscoveryPostsViewModel(context: context) viewController.viewModel = DiscoveryPostsViewModel(context: context, authContext: authContext)
return viewController return viewController
}() }()
discoveryHashtagsViewController = { discoveryHashtagsViewController = {
let viewController = DiscoveryHashtagsViewController() let viewController = DiscoveryHashtagsViewController()
setupDependency(viewController) setupDependency(viewController)
viewController.viewModel = DiscoveryHashtagsViewModel(context: context) viewController.viewModel = DiscoveryHashtagsViewModel(context: context, authContext: authContext)
return viewController return viewController
}() }()
discoveryNewsViewController = { discoveryNewsViewController = {
let viewController = DiscoveryNewsViewController() let viewController = DiscoveryNewsViewController()
setupDependency(viewController) setupDependency(viewController)
viewController.viewModel = DiscoveryNewsViewModel(context: context) viewController.viewModel = DiscoveryNewsViewModel(context: context, authContext: authContext)
return viewController return viewController
}() }()
discoveryCommunityViewController = { discoveryCommunityViewController = {
let viewController = DiscoveryCommunityViewController() let viewController = DiscoveryCommunityViewController()
setupDependency(viewController) setupDependency(viewController)
viewController.viewModel = DiscoveryCommunityViewModel(context: context) viewController.viewModel = DiscoveryCommunityViewModel(context: context, authContext: authContext)
return viewController return viewController
}() }()
discoveryForYouViewController = { discoveryForYouViewController = {
let viewController = DiscoveryForYouViewController() let viewController = DiscoveryForYouViewController()
setupDependency(viewController) setupDependency(viewController)
viewController.viewModel = DiscoveryForYouViewModel(context: context) viewController.viewModel = DiscoveryForYouViewModel(context: context, authContext: authContext)
return viewController return viewController
}() }()
self.viewControllers = [ self.viewControllers = [

View File

@ -9,6 +9,7 @@ import os.log
import UIKit import UIKit
import Combine import Combine
import MastodonUI import MastodonUI
import MastodonCore
final class DiscoveryForYouViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { final class DiscoveryForYouViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
@ -100,6 +101,11 @@ extension DiscoveryForYouViewController {
} }
// MARK: - AuthContextProvider
extension DiscoveryForYouViewController: AuthContextProvider {
var authContext: AuthContext { viewModel.authContext }
}
// MARK: - UITableViewDelegate // MARK: - UITableViewDelegate
extension DiscoveryForYouViewController: UITableViewDelegate { extension DiscoveryForYouViewController: UITableViewDelegate {
@ -109,9 +115,10 @@ extension DiscoveryForYouViewController: UITableViewDelegate {
guard let user = record.object(in: context.managedObjectContext) else { return } guard let user = record.object(in: context.managedObjectContext) else { return }
let profileViewModel = CachedProfileViewModel( let profileViewModel = CachedProfileViewModel(
context: context, context: context,
authContext: viewModel.authContext,
mastodonUser: user mastodonUser: user
) )
coordinator.present( _ = coordinator.present(
scene: .profile(viewModel: profileViewModel), scene: .profile(viewModel: profileViewModel),
from: self, from: self,
transition: .show transition: .show
@ -127,15 +134,13 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
profileCardView: ProfileCardView, profileCardView: ProfileCardView,
relationshipButtonDidPressed button: ProfileRelationshipActionButton relationshipButtonDidPressed button: ProfileRelationshipActionButton
) { ) {
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let indexPath = tableView.indexPath(for: cell) else { return } guard let indexPath = tableView.indexPath(for: cell) else { return }
guard case let .user(record) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return } guard case let .user(record) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return }
Task { Task {
try await DataSourceFacade.responseToUserFollowAction( try await DataSourceFacade.responseToUserFollowAction(
dependency: self, dependency: self,
user: record, user: record
authenticationBox: authenticationBox
) )
} // end Task } // end Task
} }
@ -156,9 +161,9 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
return return
} }
let familiarFollowersViewModel = FamiliarFollowersViewModel(context: context) let familiarFollowersViewModel = FamiliarFollowersViewModel(context: context, authContext: authContext)
familiarFollowersViewModel.familiarFollowers = familiarFollowers familiarFollowersViewModel.familiarFollowers = familiarFollowers
coordinator.present( _ = coordinator.present(
scene: .familiarFollowers(viewModel: familiarFollowersViewModel), scene: .familiarFollowers(viewModel: familiarFollowersViewModel),
from: self, from: self,
transition: .show transition: .show

View File

@ -19,6 +19,7 @@ extension DiscoveryForYouViewModel {
tableView: tableView, tableView: tableView,
context: context, context: context,
configuration: DiscoverySection.Configuration( configuration: DiscoverySection.Configuration(
authContext: authContext,
profileCardTableViewCellDelegate: profileCardTableViewCellDelegate, profileCardTableViewCellDelegate: profileCardTableViewCellDelegate,
familiarFollowers: $familiarFollowers familiarFollowers: $familiarFollowers
) )

View File

@ -12,6 +12,7 @@ import GameplayKit
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MastodonSDK import MastodonSDK
import MastodonCore
final class DiscoveryForYouViewModel { final class DiscoveryForYouViewModel {
@ -19,6 +20,7 @@ final class DiscoveryForYouViewModel {
// input // input
let context: AppContext let context: AppContext
let authContext: AuthContext
let userFetchedResultsController: UserFetchedResultsController let userFetchedResultsController: UserFetchedResultsController
@MainActor @MainActor
@ -29,19 +31,15 @@ final class DiscoveryForYouViewModel {
var diffableDataSource: UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem>? var diffableDataSource: UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem>?
let didLoadLatest = PassthroughSubject<Void, Never>() let didLoadLatest = PassthroughSubject<Void, Never>()
init(context: AppContext) { init(context: AppContext, authContext: AuthContext) {
self.context = context self.context = context
self.authContext = authContext
self.userFetchedResultsController = UserFetchedResultsController( self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,
domain: nil, domain: authContext.mastodonAuthenticationBox.domain,
additionalPredicate: nil additionalPredicate: nil
) )
// end init // end init
context.authenticationService.activeMastodonAuthenticationBox
.map { $0?.domain }
.assign(to: \.domain, on: userFetchedResultsController)
.store(in: &disposeBag)
} }
deinit { deinit {
@ -58,16 +56,12 @@ extension DiscoveryForYouViewModel {
isFetching = true isFetching = true
defer { isFetching = false } defer { isFetching = false }
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
throw APIService.APIError.implicit(.badRequest)
}
do { do {
let userIDs = try await fetchSuggestionAccounts() let userIDs = try await fetchSuggestionAccounts()
let _familiarFollowersResponse = try? await context.apiService.familiarFollowers( let _familiarFollowersResponse = try? await context.apiService.familiarFollowers(
query: .init(ids: userIDs), query: .init(ids: userIDs),
authenticationBox: authenticationBox authenticationBox: authContext.mastodonAuthenticationBox
) )
familiarFollowers = _familiarFollowersResponse?.value ?? [] familiarFollowers = _familiarFollowersResponse?.value ?? []
userFetchedResultsController.userIDs = userIDs userFetchedResultsController.userIDs = userIDs
@ -77,14 +71,10 @@ extension DiscoveryForYouViewModel {
} }
private func fetchSuggestionAccounts() async throws -> [Mastodon.Entity.Account.ID] { private func fetchSuggestionAccounts() async throws -> [Mastodon.Entity.Account.ID] {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
throw APIService.APIError.implicit(.badRequest)
}
do { do {
let response = try await context.apiService.suggestionAccountV2( let response = try await context.apiService.suggestionAccountV2(
query: nil, query: nil,
authenticationBox: authenticationBox authenticationBox: authContext.mastodonAuthenticationBox
) )
let userIDs = response.value.map { $0.account.id } let userIDs = response.value.map { $0.account.id }
return userIDs return userIDs
@ -92,7 +82,7 @@ extension DiscoveryForYouViewModel {
// fallback V1 // fallback V1
let response = try await context.apiService.suggestionAccount( let response = try await context.apiService.suggestionAccount(
query: nil, query: nil,
authenticationBox: authenticationBox authenticationBox: authContext.mastodonAuthenticationBox
) )
let userIDs = response.value.map { $0.id } let userIDs = response.value.map { $0.id }
return userIDs return userIDs

View File

@ -8,6 +8,7 @@
import os.log import os.log
import UIKit import UIKit
import Combine import Combine
import MastodonCore
import MastodonUI import MastodonUI
final class DiscoveryHashtagsViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { final class DiscoveryHashtagsViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
@ -106,7 +107,7 @@ extension DiscoveryHashtagsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(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 } 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( coordinator.present(
scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel),
from: self, from: self,
@ -216,7 +217,7 @@ extension DiscoveryHashtagsViewController: TableViewControllerNavigateable {
guard let item = diffableDataSource.itemIdentifier(for: indexPathForSelectedRow) else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPathForSelectedRow) else { return }
guard case let .hashtag(tag) = item 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( coordinator.present(
scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel),
from: self, from: self,

View File

@ -15,7 +15,7 @@ extension DiscoveryHashtagsViewModel {
diffableDataSource = DiscoverySection.diffableDataSource( diffableDataSource = DiscoverySection.diffableDataSource(
tableView: tableView, tableView: tableView,
context: context, context: context,
configuration: DiscoverySection.Configuration() configuration: DiscoverySection.Configuration(authContext: authContext)
) )
var snapshot = NSDiffableDataSourceSnapshot<DiscoverySection, DiscoveryItem>() var snapshot = NSDiffableDataSourceSnapshot<DiscoverySection, DiscoveryItem>()

View File

@ -11,6 +11,7 @@ import Combine
import GameplayKit import GameplayKit
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MastodonCore
import MastodonSDK import MastodonSDK
final class DiscoveryHashtagsViewModel { final class DiscoveryHashtagsViewModel {
@ -21,41 +22,37 @@ final class DiscoveryHashtagsViewModel {
// input // input
let context: AppContext let context: AppContext
let authContext: AuthContext
let viewDidAppeared = PassthroughSubject<Void, Never>() let viewDidAppeared = PassthroughSubject<Void, Never>()
// output // output
var diffableDataSource: UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem>? var diffableDataSource: UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem>?
@Published var hashtags: [Mastodon.Entity.Tag] = [] @Published var hashtags: [Mastodon.Entity.Tag] = []
init(context: AppContext) { init(context: AppContext, authContext: AuthContext) {
self.context = context self.context = context
self.authContext = authContext
// end init // end init
Publishers.CombineLatest( viewDidAppeared
context.authenticationService.activeMastodonAuthenticationBox, .throttle(for: 3, scheduler: DispatchQueue.main, latest: true)
viewDidAppeared .asyncMap { authenticationBox in
) try await context.apiService.trendHashtags(domain: authContext.mastodonAuthenticationBox.domain, query: nil)
.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<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { response } }
.catch { error in Just(Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, 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
} }
} .retry(3)
.store(in: &disposeBag) .map { response in Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { response } }
.catch { error in Just(Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, 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 { deinit {
@ -68,8 +65,7 @@ extension DiscoveryHashtagsViewModel {
@MainActor @MainActor
func fetch() async throws { func fetch() async throws {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } let response = try await context.apiService.trendHashtags(domain: authContext.mastodonAuthenticationBox.domain, query: nil)
let response = try await context.apiService.trendHashtags(domain: authenticationBox.domain, query: nil)
hashtags = response.value.filter { !$0.name.isEmpty } 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)") logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch tags: \(response.value.count)")
} }

View File

@ -8,6 +8,7 @@
import os.log import os.log
import UIKit import UIKit
import Combine import Combine
import MastodonCore
import MastodonUI import MastodonUI
final class DiscoveryNewsViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { final class DiscoveryNewsViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {

View File

@ -16,7 +16,7 @@ extension DiscoveryNewsViewModel {
diffableDataSource = DiscoverySection.diffableDataSource( diffableDataSource = DiscoverySection.diffableDataSource(
tableView: tableView, tableView: tableView,
context: context, context: context,
configuration: DiscoverySection.Configuration() configuration: DiscoverySection.Configuration(authContext: authContext)
) )
stateMachine.enter(State.Reloading.self) stateMachine.enter(State.Reloading.self)

View File

@ -11,16 +11,12 @@ import GameplayKit
import MastodonSDK import MastodonSDK
extension DiscoveryNewsViewModel { extension DiscoveryNewsViewModel {
class State: GKState, NamingState { class State: GKState {
let logger = Logger(subsystem: "DiscoveryNewsViewModel.State", category: "StateMachine") let logger = Logger(subsystem: "DiscoveryNewsViewModel.State", category: "StateMachine")
let id = UUID() let id = UUID()
var name: String {
String(describing: Self.self)
}
weak var viewModel: DiscoveryNewsViewModel? weak var viewModel: DiscoveryNewsViewModel?
init(viewModel: DiscoveryNewsViewModel) { init(viewModel: DiscoveryNewsViewModel) {
@ -29,8 +25,10 @@ extension DiscoveryNewsViewModel {
override func didEnter(from previousState: GKState?) { override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState) 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 ?? "<nil>")") 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 @MainActor
@ -39,7 +37,7 @@ extension DiscoveryNewsViewModel {
} }
deinit { 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: default:
break break
} }
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
stateMachine.enter(Fail.self)
return
}
let offset = self.offset let offset = self.offset
let isReloading = offset == nil let isReloading = offset == nil
@ -148,7 +141,7 @@ extension DiscoveryNewsViewModel.State {
Task { Task {
do { do {
let response = try await viewModel.context.apiService.trendLinks( let response = try await viewModel.context.apiService.trendLinks(
domain: authenticationBox.domain, domain: viewModel.authContext.mastodonAuthenticationBox.domain,
query: Mastodon.API.Trends.StatusQuery( query: Mastodon.API.Trends.StatusQuery(
offset: offset, offset: offset,
limit: nil limit: nil

Some files were not shown because too many files have changed in this diff Show More