Merge branch 'develop' into patch-1
# Conflicts: # Documentation/Setup.md
This commit is contained in:
commit
e8370fa834
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
# Required
|
||||
|
||||
# https://<your-domain>/relay-to/development
|
||||
NotificationEndpointDebug=""
|
||||
|
||||
# https://<your-domain>/relay-to/production
|
||||
NotificationEndpointRelease=""
|
|
@ -7,6 +7,6 @@ set -eo pipefail
|
|||
|
||||
xcodebuild -workspace Mastodon.xcworkspace \
|
||||
-scheme Mastodon \
|
||||
-destination "platform=iOS Simulator,name=iPhone SE (2nd generation)" \
|
||||
-destination "platform=iOS Simulator,name=iPhone SE (2nd generation)" \
|
||||
clean \
|
||||
build | xcpretty
|
||||
|
|
|
@ -9,8 +9,7 @@ gem install bundler:2.3.11
|
|||
# Install Ruby Gems
|
||||
bundle install
|
||||
|
||||
# stub keys. DO NOT use in production
|
||||
bundle exec pod keys set notification_endpoint "<endpoint>"
|
||||
bundle exec pod keys set notification_endpoint_debug "<endpoint>"
|
||||
# Setup notification endpoint
|
||||
bundle exec arkana
|
||||
|
||||
bundle exec pod install
|
||||
|
|
|
@ -15,13 +15,14 @@ on:
|
|||
jobs:
|
||||
build:
|
||||
name: CI build
|
||||
runs-on: macos-11
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: force Xcode 13.2.1
|
||||
run: sudo xcode-select -switch /Applications/Xcode_13.2.1.app
|
||||
- name: setup
|
||||
env:
|
||||
NotificationEndpointDebug: ${{ secrets.NotificationEndpointDebug }}
|
||||
NotificationEndpointRelease: ${{ secrets.NotificationEndpointRelease }}
|
||||
run: exec ./.github/scripts/setup.sh
|
||||
- name: build
|
||||
run: exec ./.github/scripts/build.sh
|
||||
|
|
|
@ -123,3 +123,6 @@ xcuserdata
|
|||
# Localization/StringsConvertor/input
|
||||
Localization/StringsConvertor/output
|
||||
.DS_Store
|
||||
|
||||
env/**/**
|
||||
!env/.env
|
|
@ -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>
|
||||
|
||||
|
|
@ -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>
|
|
@ -1,13 +1,12 @@
|
|||
# Acknowledgments
|
||||
|
||||
- [Alamofire](https://github.com/Alamofire/Alamofire)
|
||||
- [AlamofireImage](https://github.com/Alamofire/AlamofireImage)
|
||||
- [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator)
|
||||
- [Alamofire](https://github.com/Alamofire/Alamofire)
|
||||
- [Arkana](https://github.com/rogerluan/arkana)
|
||||
- [CommonOSLog](https://github.com/mainasuk/CommonOSLog)
|
||||
- [CryptoSwift](https://github.com/krzyzanowskim/CryptoSwift)
|
||||
- [DateToolSwift](https://github.com/MatthewYork/DateTools)
|
||||
- [DiffableDataSources](https://github.com/ra1028/DiffableDataSources)
|
||||
- [DifferenceKit](https://github.com/ra1028/DifferenceKit)
|
||||
- [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage)
|
||||
- [FLEX](https://github.com/FLEXTool/FLEX)
|
||||
- [FPSIndicator](https://github.com/MainasuK/FPSIndicator)
|
||||
|
@ -26,10 +25,10 @@
|
|||
- [SwiftGen](https://github.com/SwiftGen/SwiftGen)
|
||||
- [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect)
|
||||
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
|
||||
- [Tabman](https://github.com/uias/Tabman)
|
||||
- [TabBarPager](https://github.com/TwidereProject/TabBarPager)
|
||||
- [TwidereX-iOS](https://github.com/TwidereProject/TwidereX-iOS)
|
||||
- [Tabman](https://github.com/uias/Tabman)
|
||||
- [ThirdPartyMailer](https://github.com/vtourraine/ThirdPartyMailer)
|
||||
- [TOCropViewController](https://github.com/TimOliver/TOCropViewController)
|
||||
- [TwidereX-iOS](https://github.com/TwidereProject/TwidereX-iOS)
|
||||
- [TwitterProfile](https://github.com/OfTheWolf/TwitterProfile)
|
||||
- [UITextView-Placeholder](https://github.com/devxoul/UITextView-Placeholder)
|
|
@ -12,7 +12,7 @@ Install the latest version of Xcode from the App Store or Apple Developer Downlo
|
|||
This guide may not suit your machine and actually setup procedure may change in the future. Please file the issue or Pull Request if there are any problems.
|
||||
|
||||
## CocoaPods
|
||||
The app use [CocoaPods]() and [CocoaPods-Keys](https://github.com/orta/cocoapods-keys). Ruby Gems are managed through Bundler. The M1 Mac needs virtual ruby env to workaround compatibility issues.
|
||||
The app use [CocoaPods]() and [Arkana](https://github.com/rogerluan/arkana). Ruby Gems are managed through Bundler. The M1 Mac needs virtual ruby env to workaround compatibility issues.
|
||||
|
||||
#### Intel Mac
|
||||
|
||||
|
@ -52,6 +52,13 @@ bundle install
|
|||
bundle install
|
||||
bundle exec pod clean
|
||||
|
||||
# setup arkana
|
||||
# please check the `.env.example` to create your's or use the empty example directly
|
||||
bundle exec arkana -e ./env/.env
|
||||
|
||||
# clean pods
|
||||
bundle exec pod clean
|
||||
|
||||
# make install
|
||||
bundle exec pod install --repo-update
|
||||
|
||||
|
@ -59,14 +66,14 @@ bundle exec pod install --repo-update
|
|||
open Mastodon.xcworkspace
|
||||
```
|
||||
|
||||
The CocoaPods-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)
|
||||
The app is compatible with [toot-relay](https://github.com/DagAgren/toot-relay) APNs. You can set your push notification endpoint via CocoaPods-Keys. There are two endpoints:
|
||||
- notification_endpoint: for `RELEASE` usage
|
||||
- notification_endpoint_debug: for `DEBUG` usage
|
||||
The app is compatible with [toot-relay](https://github.com/DagAgren/toot-relay) APNs. You can set your push notification endpoint via Arkana. There are two endpoints:
|
||||
- NotificationEndpointDebug: for `DEBUG` usage. e.g. `https://<your.domin>/relay-to/development`
|
||||
- NotificationEndpointRelease: for `RELEASE` usage. e.g. `https://<your.domin>/relay-to/production`
|
||||
|
||||
Please check the [Establishing a Certificate-Based Connection to APNs
|
||||
](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/establishing_a_certificate-based_connection_to_apns) document to generate the certificate and exports the p12 file.
|
||||
|
|
3
Gemfile
3
Gemfile
|
@ -1,6 +1,7 @@
|
|||
source "https://rubygems.org"
|
||||
|
||||
gem 'arkana'
|
||||
gem "cocoapods"
|
||||
gem "cocoapods-clean"
|
||||
gem "cocoapods-keys"
|
||||
gem "xcpretty"
|
||||
|
||||
|
|
44
Gemfile.lock
44
Gemfile.lock
|
@ -3,20 +3,21 @@ GEM
|
|||
specs:
|
||||
CFPropertyList (3.0.5)
|
||||
rexml
|
||||
RubyInline (3.12.5)
|
||||
ZenTest (~> 4.3)
|
||||
ZenTest (4.12.1)
|
||||
activesupport (6.1.5.1)
|
||||
activesupport (6.1.7)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
zeitwerk (~> 2.3)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
addressable (2.8.1)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
algoliasearch (1.27.5)
|
||||
httpclient (~> 2.8, >= 2.8.3)
|
||||
json (>= 1.5.1)
|
||||
arkana (1.2.0)
|
||||
colorize (~> 0.8)
|
||||
dotenv (~> 2.7)
|
||||
yaml (~> 0.2)
|
||||
atomos (0.1.3)
|
||||
claide (1.1.0)
|
||||
cocoapods (1.11.3)
|
||||
|
@ -50,9 +51,6 @@ GEM
|
|||
typhoeus (~> 1.0)
|
||||
cocoapods-deintegrate (1.0.5)
|
||||
cocoapods-downloader (1.6.3)
|
||||
cocoapods-keys (2.2.1)
|
||||
dotenv
|
||||
osx_keychain
|
||||
cocoapods-plugins (1.0.0)
|
||||
nap
|
||||
cocoapods-search (1.0.1)
|
||||
|
@ -61,8 +59,9 @@ GEM
|
|||
netrc (~> 0.11)
|
||||
cocoapods-try (1.2.0)
|
||||
colored2 (3.1.2)
|
||||
colorize (0.8.1)
|
||||
concurrent-ruby (1.1.10)
|
||||
dotenv (2.7.6)
|
||||
dotenv (2.8.1)
|
||||
escape (0.0.4)
|
||||
ethon (0.15.0)
|
||||
ffi (>= 1.15.0)
|
||||
|
@ -71,39 +70,42 @@ GEM
|
|||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.10.0)
|
||||
i18n (1.12.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.6.1)
|
||||
minitest (5.15.0)
|
||||
json (2.6.2)
|
||||
minitest (5.16.3)
|
||||
molinillo (0.8.0)
|
||||
nanaimo (0.3.0)
|
||||
nap (1.1.0)
|
||||
netrc (0.11.0)
|
||||
osx_keychain (1.0.2)
|
||||
RubyInline (~> 3)
|
||||
public_suffix (4.0.7)
|
||||
rexml (3.2.5)
|
||||
rouge (2.0.7)
|
||||
ruby-macho (2.5.1)
|
||||
typhoeus (1.4.0)
|
||||
ethon (>= 0.9.0)
|
||||
tzinfo (2.0.4)
|
||||
tzinfo (2.0.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
xcodeproj (1.21.0)
|
||||
xcodeproj (1.22.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
zeitwerk (2.5.4)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
yaml (0.2.0)
|
||||
zeitwerk (2.6.3)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
arm64-darwin-21
|
||||
|
||||
DEPENDENCIES
|
||||
arkana
|
||||
cocoapods
|
||||
cocoapods-clean
|
||||
cocoapods-keys
|
||||
xcpretty
|
||||
|
||||
BUNDLED WITH
|
||||
2.3.11
|
||||
2.3.17
|
||||
|
|
|
@ -224,17 +224,17 @@
|
|||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>zero</key>
|
||||
<string>لا إعاد تدوين</string>
|
||||
<string>لَا إعادَةُ تَدوين</string>
|
||||
<key>one</key>
|
||||
<string>إعادةُ تدوينٍ واحِدة</string>
|
||||
<string>إعادَةُ تَدوينٍ واحِدَة</string>
|
||||
<key>two</key>
|
||||
<string>إعادتا تدوين</string>
|
||||
<string>إعادَتَا تَدوين</string>
|
||||
<key>few</key>
|
||||
<string>%ld إعاداتِ تدوين</string>
|
||||
<string>%ld إعادَاتِ تَدوين</string>
|
||||
<key>many</key>
|
||||
<string>%ld إعادةٍ للتدوين</string>
|
||||
<string>%ld إعادَةٍ لِلتَّدوين</string>
|
||||
<key>other</key>
|
||||
<string>%ld إعادة تدوين</string>
|
||||
<string>%ld إعادَة تَدوين</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.reply</key>
|
||||
|
|
|
@ -348,7 +348,7 @@
|
|||
"Publishing": "Publication en cours ...",
|
||||
"accessibility": {
|
||||
"logo_label": "Bouton logo",
|
||||
"logo_hint": "Tap to scroll to top and tap again to previous location"
|
||||
"logo_hint": "Appuyez pour faire défiler vers le haut et appuyez à nouveau vers l'emplacement précédent"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -546,10 +546,10 @@
|
|||
"show_mentions": "Afficher les mentions"
|
||||
},
|
||||
"follow_request": {
|
||||
"accept": "Accept",
|
||||
"accepted": "Accepted",
|
||||
"reject": "reject",
|
||||
"rejected": "Rejected"
|
||||
"accept": "Accepter",
|
||||
"accepted": "Accepté",
|
||||
"reject": "rejeter",
|
||||
"rejected": "Rejeté"
|
||||
}
|
||||
},
|
||||
"thread": {
|
||||
|
|
|
@ -546,10 +546,10 @@
|
|||
"show_mentions": "Qalkirinan nîşan bike"
|
||||
},
|
||||
"follow_request": {
|
||||
"accept": "Accept",
|
||||
"accepted": "Accepted",
|
||||
"reject": "reject",
|
||||
"rejected": "Rejected"
|
||||
"accept": "Bipejirîne",
|
||||
"accepted": "Pejirandî",
|
||||
"reject": "nepejirîne",
|
||||
"rejected": "Nepejirandî"
|
||||
}
|
||||
},
|
||||
"thread": {
|
||||
|
|
|
@ -546,10 +546,10 @@
|
|||
"show_mentions": "แสดงการกล่าวถึง"
|
||||
},
|
||||
"follow_request": {
|
||||
"accept": "Accept",
|
||||
"accepted": "Accepted",
|
||||
"reject": "reject",
|
||||
"rejected": "Rejected"
|
||||
"accept": "ยอมรับ",
|
||||
"accepted": "ยอมรับแล้ว",
|
||||
"reject": "ปฏิเสธ",
|
||||
"rejected": "ปฏิเสธแล้ว"
|
||||
}
|
||||
},
|
||||
"thread": {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1330"
|
||||
LastUpgradeVersion = "1400"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1330"
|
||||
LastUpgradeVersion = "1400"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1330"
|
||||
LastUpgradeVersion = "1400"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -4,13 +4,6 @@
|
|||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>AppShared.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<true/>
|
||||
<key>orderHint</key>
|
||||
<integer>9</integer>
|
||||
</dict>
|
||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
|
@ -19,32 +12,27 @@
|
|||
<key>Mastodon - Profile.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>12</integer>
|
||||
<integer>5</integer>
|
||||
</dict>
|
||||
<key>Mastodon - Release.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>5</integer>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>Mastodon - Snapshot.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>7</integer>
|
||||
</dict>
|
||||
<key>Mastodon - ar.xcscheme</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>8</integer>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
<key>Mastodon - ar.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>11</integer>
|
||||
<integer>4</integer>
|
||||
</dict>
|
||||
<key>Mastodon - ca.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -114,7 +102,7 @@
|
|||
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>29</integer>
|
||||
<integer>20</integer>
|
||||
</dict>
|
||||
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -129,12 +117,12 @@
|
|||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>31</integer>
|
||||
<integer>24</integer>
|
||||
</dict>
|
||||
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>30</integer>
|
||||
<integer>25</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
@ -164,6 +152,11 @@
|
|||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>DB8FABC526AEC7B2008E5AF4</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -1,259 +1,257 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "Alamofire",
|
||||
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "354dda32d89fc8cd4f5c46487f64957d355f53d8",
|
||||
"version": "5.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "AlamofireImage",
|
||||
"repositoryURL": "https://github.com/Alamofire/AlamofireImage.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10",
|
||||
"version": "4.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "AlamofireNetworkActivityIndicator",
|
||||
"repositoryURL": "https://github.com/Alamofire/AlamofireNetworkActivityIndicator",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "392bed083e8d193aca16bfa684ee24e4bcff0510",
|
||||
"version": "3.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "CommonOSLog",
|
||||
"repositoryURL": "https://github.com/MainasuK/CommonOSLog",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "c121624a30698e9886efe38aebb36ff51c01b6c2",
|
||||
"version": "0.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "DiffableDataSources",
|
||||
"repositoryURL": "https://github.com/MainasuK/DiffableDataSources.git",
|
||||
"state": {
|
||||
"branch": "feature/async-display-table",
|
||||
"revision": "73393a97690959d24387c95594c045c62d9c47cf",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "DifferenceKit",
|
||||
"repositoryURL": "https://github.com/ra1028/DifferenceKit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "62745d7780deef4a023a792a1f8f763ec7bf9705",
|
||||
"version": "1.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "FaviconFinder",
|
||||
"repositoryURL": "https://github.com/will-lumley/FaviconFinder.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a",
|
||||
"version": "3.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "FLAnimatedImage",
|
||||
"repositoryURL": "https://github.com/Flipboard/FLAnimatedImage.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "e7f9fd4681ae41bf6f3056db08af4f401d61da52",
|
||||
"version": "1.0.16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "FPSIndicator",
|
||||
"repositoryURL": "https://github.com/MainasuK/FPSIndicator.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "e4a5067ccd5293b024c767f09e51056afd4a4796",
|
||||
"version": "1.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Fuzi",
|
||||
"repositoryURL": "https://github.com/cezheng/Fuzi.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f08c8323da21e985f3772610753bcfc652c2103f",
|
||||
"version": "3.1.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "KeychainAccess",
|
||||
"repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "84e546727d66f1adc5439debad16270d0fdd04e7",
|
||||
"version": "4.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "MetaTextKit",
|
||||
"repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "dcd5255d6930c2fab408dc8562c577547e477624",
|
||||
"version": "2.2.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Nuke",
|
||||
"repositoryURL": "https://github.com/kean/Nuke.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "0ea7545b5c918285aacc044dc75048625c8257cc",
|
||||
"version": "10.8.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "NukeFLAnimatedImagePlugin",
|
||||
"repositoryURL": "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b59c346a7d536336db3b0f12c72c6e53ee709e16",
|
||||
"version": "8.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Pageboy",
|
||||
"repositoryURL": "https://github.com/uias/Pageboy",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6",
|
||||
"version": "3.6.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "PanModal",
|
||||
"repositoryURL": "https://github.com/slackhq/PanModal.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b012aecb6b67a8e46369227f893c12544846613f",
|
||||
"version": "1.2.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SDWebImage",
|
||||
"repositoryURL": "https://github.com/SDWebImage/SDWebImage.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "2e63d0061da449ad0ed130768d05dceb1496de44",
|
||||
"version": "5.12.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-collections",
|
||||
"repositoryURL": "https://github.com/apple/swift-collections.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "9d8719c8bebdc79740b6969c912ac706eb721d7a",
|
||||
"version": "0.0.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-nio",
|
||||
"repositoryURL": "https://github.com/apple/swift-nio.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "546610d52b19be3e19935e0880bb06b9c03f5cef",
|
||||
"version": "1.14.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-nio-zlib-support",
|
||||
"repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "37760e9a52030bb9011972c5213c3350fa9d41fd",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SwiftSoup",
|
||||
"repositoryURL": "https://github.com/scinfu/SwiftSoup.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886",
|
||||
"version": "2.4.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Introspect",
|
||||
"repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f2616860a41f9d9932da412a8978fec79c06fe24",
|
||||
"version": "0.1.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SwiftyJSON",
|
||||
"repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07",
|
||||
"version": "5.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "TabBarPager",
|
||||
"repositoryURL": "https://github.com/TwidereProject/TabBarPager.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "488aa66d157a648901b61721212c0dec23d27ee5",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Tabman",
|
||||
"repositoryURL": "https://github.com/uias/Tabman",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "a9f10cb862a32e6a22549836af013abd6b0692d3",
|
||||
"version": "2.12.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "ThirdPartyMailer",
|
||||
"repositoryURL": "https://github.com/vtourraine/ThirdPartyMailer.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "779da6ce0793b461ccbbac2804755c1e29b6fa63",
|
||||
"version": "1.8.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "TOCropViewController",
|
||||
"repositoryURL": "https://github.com/TimOliver/TOCropViewController.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "d0470491f56e734731bbf77991944c0dfdee3e0e",
|
||||
"version": "2.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "UITextView+Placeholder",
|
||||
"repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "20f513ded04a040cdf5467f0891849b1763ede3b",
|
||||
"version": "1.4.1"
|
||||
}
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "alamofire",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Alamofire/Alamofire.git",
|
||||
"state" : {
|
||||
"revision" : "354dda32d89fc8cd4f5c46487f64957d355f53d8",
|
||||
"version" : "5.6.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"identity" : "alamofireimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Alamofire/AlamofireImage.git",
|
||||
"state" : {
|
||||
"revision" : "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10",
|
||||
"version" : "4.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "commonoslog",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/MainasuK/CommonOSLog",
|
||||
"state" : {
|
||||
"revision" : "c121624a30698e9886efe38aebb36ff51c01b6c2",
|
||||
"version" : "0.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "faviconfinder",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/will-lumley/FaviconFinder.git",
|
||||
"state" : {
|
||||
"revision" : "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a",
|
||||
"version" : "3.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "flanimatedimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Flipboard/FLAnimatedImage.git",
|
||||
"state" : {
|
||||
"revision" : "e7f9fd4681ae41bf6f3056db08af4f401d61da52",
|
||||
"version" : "1.0.16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "fpsindicator",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/MainasuK/FPSIndicator.git",
|
||||
"state" : {
|
||||
"revision" : "e4a5067ccd5293b024c767f09e51056afd4a4796",
|
||||
"version" : "1.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "fuzi",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/cezheng/Fuzi.git",
|
||||
"state" : {
|
||||
"revision" : "f08c8323da21e985f3772610753bcfc652c2103f",
|
||||
"version" : "3.1.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "keychainaccess",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kishikawakatsumi/KeychainAccess.git",
|
||||
"state" : {
|
||||
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
|
||||
"version" : "4.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "kingfisher",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/onevcat/Kingfisher.git",
|
||||
"state" : {
|
||||
"revision" : "44e891bdb61426a95e31492a67c7c0dfad1f87c5",
|
||||
"version" : "7.4.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "metatextkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TwidereProject/MetaTextKit.git",
|
||||
"state" : {
|
||||
"revision" : "dcd5255d6930c2fab408dc8562c577547e477624",
|
||||
"version" : "2.2.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nuke",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kean/Nuke.git",
|
||||
"state" : {
|
||||
"revision" : "0ea7545b5c918285aacc044dc75048625c8257cc",
|
||||
"version" : "10.8.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nuke-flanimatedimage-plugin",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git",
|
||||
"state" : {
|
||||
"revision" : "b59c346a7d536336db3b0f12c72c6e53ee709e16",
|
||||
"version" : "8.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "pageboy",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/uias/Pageboy",
|
||||
"state" : {
|
||||
"revision" : "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6",
|
||||
"version" : "3.6.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "panmodal",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/slackhq/PanModal.git",
|
||||
"state" : {
|
||||
"revision" : "b012aecb6b67a8e46369227f893c12544846613f",
|
||||
"version" : "1.2.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sdwebimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage.git",
|
||||
"state" : {
|
||||
"revision" : "2e63d0061da449ad0ed130768d05dceb1496de44",
|
||||
"version" : "5.12.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "stripes",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/eneko/Stripes.git",
|
||||
"state" : {
|
||||
"revision" : "d533fd44b8043a3abbf523e733599173d6f98c11",
|
||||
"version" : "0.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-collections",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-collections.git",
|
||||
"state" : {
|
||||
"revision" : "f504716c27d2e5d4144fa4794b12129301d17729",
|
||||
"version" : "1.0.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-nio",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-nio.git",
|
||||
"state" : {
|
||||
"revision" : "546610d52b19be3e19935e0880bb06b9c03f5cef",
|
||||
"version" : "1.14.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-nio-zlib-support",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-nio-zlib-support.git",
|
||||
"state" : {
|
||||
"revision" : "37760e9a52030bb9011972c5213c3350fa9d41fd",
|
||||
"version" : "1.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftsoup",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/scinfu/SwiftSoup.git",
|
||||
"state" : {
|
||||
"revision" : "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886",
|
||||
"version" : "2.4.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftui-introspect",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
|
||||
"state" : {
|
||||
"revision" : "f2616860a41f9d9932da412a8978fec79c06fe24",
|
||||
"version" : "0.1.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftyjson",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SwiftyJSON/SwiftyJSON.git",
|
||||
"state" : {
|
||||
"revision" : "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07",
|
||||
"version" : "5.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tabbarpager",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TwidereProject/TabBarPager.git",
|
||||
"state" : {
|
||||
"revision" : "488aa66d157a648901b61721212c0dec23d27ee5",
|
||||
"version" : "0.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tabman",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/uias/Tabman",
|
||||
"state" : {
|
||||
"revision" : "4a4f7c755b875ffd4f9ef10d67a67883669d2465",
|
||||
"version" : "2.13.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "thirdpartymailer",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/vtourraine/ThirdPartyMailer.git",
|
||||
"state" : {
|
||||
"revision" : "44c1cfaa6969963f22691aa67f88a69e3b6d651f",
|
||||
"version" : "2.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tocropviewcontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TimOliver/TOCropViewController.git",
|
||||
"state" : {
|
||||
"revision" : "d0470491f56e734731bbf77991944c0dfdee3e0e",
|
||||
"version" : "2.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "uihostingconfigurationbackport",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/woxtu/UIHostingConfigurationBackport.git",
|
||||
"state" : {
|
||||
"revision" : "6091f2d38faa4b24fc2ca0389c651e2f666624a3",
|
||||
"version" : "0.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "uitextview-placeholder",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/MainasuK/UITextView-Placeholder.git",
|
||||
"state" : {
|
||||
"revision" : "20f513ded04a040cdf5467f0891849b1763ede3b",
|
||||
"version" : "1.4.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonCore
|
||||
|
||||
protocol NeedsDependency: AnyObject {
|
||||
var context: AppContext! { get set }
|
||||
|
|
|
@ -8,8 +8,9 @@ import UIKit
|
|||
import Combine
|
||||
import SafariServices
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import PanModal
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
|
@ -19,7 +20,9 @@ final public class SceneCoordinator {
|
|||
|
||||
private weak var scene: UIScene!
|
||||
private weak var sceneDelegate: SceneDelegate!
|
||||
private weak var appContext: AppContext!
|
||||
private(set) weak var appContext: AppContext!
|
||||
|
||||
private(set) var authContext: AuthContext?
|
||||
|
||||
let id = UUID().uuidString
|
||||
|
||||
|
@ -29,7 +32,11 @@ final public class SceneCoordinator {
|
|||
|
||||
private(set) var secondaryStackHashValues = Set<Int>()
|
||||
|
||||
init(scene: UIScene, sceneDelegate: SceneDelegate, appContext: AppContext) {
|
||||
init(
|
||||
scene: UIScene,
|
||||
sceneDelegate: SceneDelegate,
|
||||
appContext: AppContext
|
||||
) {
|
||||
self.scene = scene
|
||||
self.sceneDelegate = sceneDelegate
|
||||
self.appContext = appContext
|
||||
|
@ -38,100 +45,83 @@ final public class SceneCoordinator {
|
|||
|
||||
appContext.notificationService.requestRevealNotificationPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.compactMap { [weak self] pushNotification -> AnyPublisher<MastodonPushNotification?, Never> in
|
||||
guard let self = self else { return Just(nil).eraseToAnyPublisher() }
|
||||
// skip if no available account
|
||||
guard let currentActiveAuthenticationBox = appContext.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
return Just(nil).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let accessToken = pushNotification.accessToken // use raw accessToken value without normalize
|
||||
if currentActiveAuthenticationBox.userAuthorization.accessToken == accessToken {
|
||||
// do nothing if notification for current account
|
||||
return Just(pushNotification).eraseToAnyPublisher()
|
||||
} else {
|
||||
// switch to notification's account
|
||||
let request = MastodonAuthentication.sortedFetchRequest
|
||||
request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken)
|
||||
request.returnsObjectsAsFaults = false
|
||||
request.fetchLimit = 1
|
||||
do {
|
||||
guard let authentication = try appContext.managedObjectContext.fetch(request).first else {
|
||||
return Just(nil).eraseToAnyPublisher()
|
||||
}
|
||||
let domain = authentication.domain
|
||||
let userID = authentication.userID
|
||||
return appContext.authenticationService.activeMastodonUser(domain: domain, userID: userID)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.map { [weak self] result -> MastodonPushNotification? in
|
||||
guard let self = self else { return nil }
|
||||
switch result {
|
||||
case .success:
|
||||
// reset view hierarchy
|
||||
self.setup()
|
||||
return pushNotification
|
||||
case .failure:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
.delay(for: 1, scheduler: DispatchQueue.main) // set delay to slow transition (not must)
|
||||
.eraseToAnyPublisher()
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
return Just(nil).eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
}
|
||||
.switchToLatest()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] pushNotification in
|
||||
.sink(receiveValue: { [weak self] pushNotification in
|
||||
guard let self = self else { return }
|
||||
guard let pushNotification = pushNotification else { return }
|
||||
|
||||
// redirect to notification tab
|
||||
self.switchToTabBar(tab: .notification)
|
||||
|
||||
|
||||
// Delay in next run loop
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
// Note:
|
||||
// show (push) on phone and pad
|
||||
let from: UIViewController? = {
|
||||
if let splitViewController = self.splitViewController {
|
||||
if splitViewController.compactMainTabBarViewController.topMost?.view.window != nil {
|
||||
// compact
|
||||
return splitViewController.compactMainTabBarViewController.topMost
|
||||
} else {
|
||||
// expand
|
||||
return splitViewController.contentSplitViewController.mainTabBarController.topMost
|
||||
Task {
|
||||
guard let currentActiveAuthenticationBox = self.authContext?.mastodonAuthenticationBox else { return }
|
||||
let accessToken = pushNotification.accessToken // use raw accessToken value without normalize
|
||||
if currentActiveAuthenticationBox.userAuthorization.accessToken == accessToken {
|
||||
// do nothing if notification for current account
|
||||
return
|
||||
} else {
|
||||
// switch to notification's account
|
||||
let request = MastodonAuthentication.sortedFetchRequest
|
||||
request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken)
|
||||
request.returnsObjectsAsFaults = false
|
||||
request.fetchLimit = 1
|
||||
do {
|
||||
guard let authentication = try appContext.managedObjectContext.fetch(request).first else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return self.tabBarController.topMost
|
||||
let domain = authentication.domain
|
||||
let userID = authentication.userID
|
||||
let isSuccess = try await appContext.authenticationService.activeMastodonUser(domain: domain, userID: userID)
|
||||
guard isSuccess else { return }
|
||||
|
||||
self.setup()
|
||||
try await Task.sleep(nanoseconds: .second * 1)
|
||||
|
||||
// redirect to notification tab
|
||||
self.switchToTabBar(tab: .notification)
|
||||
|
||||
// Delay in next run loop
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
// Note:
|
||||
// show (push) on phone and pad
|
||||
let from: UIViewController? = {
|
||||
if let splitViewController = self.splitViewController {
|
||||
if splitViewController.compactMainTabBarViewController.topMost?.view.window != nil {
|
||||
// compact
|
||||
return splitViewController.compactMainTabBarViewController.topMost
|
||||
} else {
|
||||
// expand
|
||||
return splitViewController.contentSplitViewController.mainTabBarController.topMost
|
||||
}
|
||||
} else {
|
||||
return self.tabBarController.topMost
|
||||
}
|
||||
}()
|
||||
|
||||
// show notification related content
|
||||
guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return }
|
||||
guard let authContext = self.authContext else { return }
|
||||
let notificationID = String(pushNotification.notificationID)
|
||||
|
||||
switch type {
|
||||
case .follow:
|
||||
let profileViewModel = RemoteProfileViewModel(context: appContext, authContext: authContext, notificationID: notificationID)
|
||||
_ = self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
|
||||
case .followRequest:
|
||||
// do nothing
|
||||
break
|
||||
case .mention, .reblog, .favourite, .poll, .status:
|
||||
let threadViewModel = RemoteThreadViewModel(context: appContext, authContext: authContext, notificationID: notificationID)
|
||||
_ = self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show)
|
||||
case ._other:
|
||||
assertionFailure()
|
||||
break
|
||||
}
|
||||
} // end DispatchQueue.main.async
|
||||
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// show notification related content
|
||||
guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return }
|
||||
let notificationID = String(pushNotification.notificationID)
|
||||
|
||||
switch type {
|
||||
case .follow:
|
||||
let profileViewModel = RemoteProfileViewModel(context: appContext, notificationID: notificationID)
|
||||
self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
|
||||
case .followRequest:
|
||||
// do nothing
|
||||
break
|
||||
case .mention, .reblog, .favourite, .poll, .status:
|
||||
let threadViewModel = RemoteThreadViewModel(context: appContext, notificationID: notificationID)
|
||||
self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show)
|
||||
case ._other:
|
||||
assertionFailure()
|
||||
break
|
||||
}
|
||||
} // end DispatchQueue.main.async
|
||||
}
|
||||
} // end Task
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
||||
|
@ -173,7 +163,7 @@ extension SceneCoordinator {
|
|||
case hashtagTimeline(viewModel: HashtagTimelineViewModel)
|
||||
|
||||
// profile
|
||||
case accountList
|
||||
case accountList(viewModel: AccountListViewModel)
|
||||
case profile(viewModel: ProfileViewModel)
|
||||
case favorite(viewModel: FavoriteViewModel)
|
||||
case follower(viewModel: FollowerListViewModel)
|
||||
|
@ -181,6 +171,7 @@ extension SceneCoordinator {
|
|||
case familiarFollowers(viewModel: FamiliarFollowersViewModel)
|
||||
case rebloggedBy(viewModel: UserListViewModel)
|
||||
case favoritedBy(viewModel: UserListViewModel)
|
||||
case bookmark(viewModel: BookmarkViewModel)
|
||||
|
||||
// setting
|
||||
case settings(viewModel: SettingsViewModel)
|
||||
|
@ -223,55 +214,61 @@ extension SceneCoordinator {
|
|||
|
||||
func setup() {
|
||||
let rootViewController: UIViewController
|
||||
switch UIDevice.current.userInterfaceIdiom {
|
||||
case .phone:
|
||||
let viewController = MainTabBarController(context: appContext, coordinator: self)
|
||||
self.splitViewController = nil
|
||||
self.tabBarController = viewController
|
||||
rootViewController = viewController
|
||||
default:
|
||||
let splitViewController = RootSplitViewController(context: appContext, coordinator: self)
|
||||
self.splitViewController = splitViewController
|
||||
self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController
|
||||
rootViewController = splitViewController
|
||||
}
|
||||
|
||||
let wizardViewController = WizardViewController()
|
||||
if !wizardViewController.items.isEmpty,
|
||||
let delegate = rootViewController as? WizardViewControllerDelegate
|
||||
{
|
||||
// do not add as child view controller.
|
||||
// otherwise, the tab bar controller will add as a new tab
|
||||
wizardViewController.delegate = delegate
|
||||
wizardViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
wizardViewController.view.frame = rootViewController.view.bounds
|
||||
rootViewController.view.addSubview(wizardViewController.view)
|
||||
self.wizardViewController = wizardViewController
|
||||
}
|
||||
|
||||
sceneDelegate.window?.rootViewController = rootViewController
|
||||
}
|
||||
|
||||
func setupOnboardingIfNeeds(animated: Bool) {
|
||||
// Check user authentication status and show onboarding if needs
|
||||
do {
|
||||
let request = MastodonAuthentication.sortedFetchRequest
|
||||
if try appContext.managedObjectContext.count(for: request) == 0 {
|
||||
let request = MastodonAuthentication.activeSortedFetchRequest // use active order
|
||||
let _authentication = try appContext.managedObjectContext.fetch(request).first
|
||||
let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
|
||||
self.authContext = _authContext
|
||||
|
||||
switch UIDevice.current.userInterfaceIdiom {
|
||||
case .phone:
|
||||
let viewController = MainTabBarController(context: appContext, coordinator: self, authContext: _authContext)
|
||||
self.splitViewController = nil
|
||||
self.tabBarController = viewController
|
||||
rootViewController = viewController
|
||||
default:
|
||||
let splitViewController = RootSplitViewController(context: appContext, coordinator: self, authContext: _authContext)
|
||||
self.splitViewController = splitViewController
|
||||
self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController
|
||||
rootViewController = splitViewController
|
||||
}
|
||||
sceneDelegate.window?.rootViewController = rootViewController // base: main
|
||||
|
||||
if _authContext == nil { // entry #1: welcome
|
||||
DispatchQueue.main.async {
|
||||
self.present(
|
||||
_ = self.present(
|
||||
scene: .welcome,
|
||||
from: self.sceneDelegate.window?.rootViewController,
|
||||
transition: .modal(animated: animated, completion: nil)
|
||||
transition: .modal(animated: true, completion: nil)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
let wizardViewController = WizardViewController()
|
||||
if !wizardViewController.items.isEmpty,
|
||||
let delegate = rootViewController as? WizardViewControllerDelegate
|
||||
{
|
||||
// do not add as child view controller.
|
||||
// otherwise, the tab bar controller will add as a new tab
|
||||
wizardViewController.delegate = delegate
|
||||
wizardViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
wizardViewController.view.frame = rootViewController.view.bounds
|
||||
rootViewController.view.addSubview(wizardViewController.view)
|
||||
self.wizardViewController = wizardViewController
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: .second * 2)
|
||||
setup() // entry #2: retry
|
||||
} // end Task
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@MainActor
|
||||
@discardableResult
|
||||
func present(scene: Scene, from sender: UIViewController?, transition: Transition) -> UIViewController? {
|
||||
guard let viewController = get(scene: scene) else {
|
||||
return nil
|
||||
|
@ -430,13 +427,18 @@ private extension SceneCoordinator {
|
|||
let _viewController = HashtagTimelineViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .accountList:
|
||||
case .accountList(let viewModel):
|
||||
let _viewController = AccountListViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .profile(let viewModel):
|
||||
let _viewController = ProfileViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .bookmark(let viewModel):
|
||||
let _viewController = BookmarkViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .favorite(let viewModel):
|
||||
let _viewController = FavoriteViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
// Created by sxiaojian on 2021/4/22.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
|
||||
enum SelectedAccountSection: Equatable, Hashable {
|
||||
case main
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import os.log
|
||||
import UIKit
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonSDK
|
||||
|
||||
|
@ -22,13 +23,16 @@ extension DiscoverySection {
|
|||
static let logger = Logger(subsystem: "DiscoverySection", category: "logic")
|
||||
|
||||
class Configuration {
|
||||
let authContext: AuthContext
|
||||
weak var profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate?
|
||||
let familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher?
|
||||
|
||||
public init(
|
||||
authContext: AuthContext,
|
||||
profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? = nil,
|
||||
familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher? = nil
|
||||
) {
|
||||
self.authContext = authContext
|
||||
self.profileCardTableViewCellDelegate = profileCardTableViewCellDelegate
|
||||
self.familiarFollowers = familiarFollowers
|
||||
}
|
||||
|
@ -72,11 +76,9 @@ extension DiscoverySection {
|
|||
} else {
|
||||
cell.profileCardView.viewModel.familiarFollowers = nil
|
||||
}
|
||||
// bind me
|
||||
cell.profileCardView.viewModel.relationshipViewModel.me = configuration.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
|
||||
}
|
||||
context.authenticationService.activeMastodonAuthentication
|
||||
.map { $0?.user }
|
||||
.assign(to: \.me, on: cell.profileCardView.viewModel.relationshipViewModel)
|
||||
.store(in: &cell.disposeBag)
|
||||
return cell
|
||||
case .bottomLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||
|
|
|
@ -14,6 +14,8 @@ import UIKit
|
|||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
|
||||
enum NotificationSection: Equatable, Hashable {
|
||||
|
@ -23,6 +25,7 @@ enum NotificationSection: Equatable, Hashable {
|
|||
extension NotificationSection {
|
||||
|
||||
struct Configuration {
|
||||
let authContext: AuthContext
|
||||
weak var notificationTableViewCellDelegate: NotificationTableViewCellDelegate?
|
||||
let filterContext: Mastodon.Entity.Filter.Context?
|
||||
let activeFilters: Published<[Mastodon.Entity.Filter]>.Publisher?
|
||||
|
@ -73,21 +76,20 @@ extension NotificationSection {
|
|||
viewModel: NotificationTableViewCell.ViewModel,
|
||||
configuration: Configuration
|
||||
) {
|
||||
cell.notificationView.viewModel.authContext = configuration.authContext
|
||||
|
||||
StatusSection.setupStatusPollDataSource(
|
||||
context: context,
|
||||
authContext: configuration.authContext,
|
||||
statusView: cell.notificationView.statusView
|
||||
)
|
||||
|
||||
StatusSection.setupStatusPollDataSource(
|
||||
context: context,
|
||||
authContext: configuration.authContext,
|
||||
statusView: cell.notificationView.quoteStatusView
|
||||
)
|
||||
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.map { $0 as UserIdentifier? }
|
||||
.assign(to: \.userIdentifier, on: cell.notificationView.viewModel)
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
cell.configure(
|
||||
tableView: tableView,
|
||||
viewModel: viewModel,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import os
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonCore
|
||||
import MastodonMeta
|
||||
import MastodonLocalization
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import UIKit
|
|||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
import Combine
|
||||
import MastodonCore
|
||||
|
||||
enum RecommendAccountSection: Equatable, Hashable {
|
||||
case main
|
||||
|
@ -132,6 +133,7 @@ enum RecommendAccountSection: Equatable, Hashable {
|
|||
extension RecommendAccountSection {
|
||||
|
||||
struct Configuration {
|
||||
let authContext: AuthContext
|
||||
weak var suggestionAccountTableViewCellDelegate: SuggestionAccountTableViewCellDelegate?
|
||||
}
|
||||
|
||||
|
@ -149,10 +151,7 @@ extension RecommendAccountSection {
|
|||
cell.configure(user: user)
|
||||
}
|
||||
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.map { $0 as UserIdentifier? }
|
||||
.assign(to: \.userIdentifier, on: cell.viewModel)
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.viewModel.userIdentifier = configuration.authContext.mastodonAuthenticationBox
|
||||
cell.delegate = configuration.suggestionAccountTableViewCellDelegate
|
||||
}
|
||||
return cell
|
||||
|
|
|
@ -13,6 +13,8 @@ import MastodonSDK
|
|||
import UIKit
|
||||
import os.log
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
|
||||
enum ReportSection: Equatable, Hashable {
|
||||
|
@ -22,6 +24,7 @@ enum ReportSection: Equatable, Hashable {
|
|||
extension ReportSection {
|
||||
|
||||
struct Configuration {
|
||||
let authContext: AuthContext
|
||||
}
|
||||
|
||||
static func diffableDataSource(
|
||||
|
@ -100,13 +103,11 @@ extension ReportSection {
|
|||
) {
|
||||
StatusSection.setupStatusPollDataSource(
|
||||
context: context,
|
||||
authContext: configuration.authContext,
|
||||
statusView: cell.statusView
|
||||
)
|
||||
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.map { $0 as UserIdentifier? }
|
||||
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.statusView.viewModel.authContext = configuration.authContext
|
||||
|
||||
cell.configure(
|
||||
tableView: tableView,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
|
||||
enum SearchHistorySection: Hashable {
|
||||
case main
|
||||
|
|
|
@ -12,6 +12,7 @@ import UIKit
|
|||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import MastodonUI
|
||||
|
||||
|
@ -24,6 +25,7 @@ extension SearchResultSection {
|
|||
static let logger = Logger(subsystem: "SearchResultSection", category: "logic")
|
||||
|
||||
struct Configuration {
|
||||
let authContext: AuthContext
|
||||
weak var statusViewTableViewCellDelegate: StatusTableViewCellDelegate?
|
||||
weak var userTableViewCellDelegate: UserTableViewCellDelegate?
|
||||
}
|
||||
|
@ -98,13 +100,11 @@ extension SearchResultSection {
|
|||
) {
|
||||
StatusSection.setupStatusPollDataSource(
|
||||
context: context,
|
||||
authContext: configuration.authContext,
|
||||
statusView: cell.statusView
|
||||
)
|
||||
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.map { $0 as UserIdentifier? }
|
||||
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.statusView.viewModel.authContext = configuration.authContext
|
||||
|
||||
cell.configure(
|
||||
tableView: tableView,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
|
||||
enum SearchSection: Hashable {
|
||||
|
|
|
@ -9,6 +9,7 @@ import UIKit
|
|||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
|
||||
enum SettingsSection: Hashable {
|
||||
|
@ -124,7 +125,7 @@ extension SettingsSection {
|
|||
|
||||
extension SettingsSection {
|
||||
|
||||
static func configureSettingToggle(
|
||||
public static func configureSettingToggle(
|
||||
cell: SettingsToggleTableViewCell,
|
||||
item: SettingsItem,
|
||||
setting: Setting
|
||||
|
@ -155,7 +156,7 @@ extension SettingsSection {
|
|||
}
|
||||
}
|
||||
|
||||
static func configureSettingToggle(
|
||||
public static func configureSettingToggle(
|
||||
cell: SettingsToggleTableViewCell,
|
||||
switchMode: SettingsItem.NotificationSwitchMode,
|
||||
subscription: NotificationSubscription
|
||||
|
|
|
@ -15,6 +15,7 @@ import AlamofireImage
|
|||
import MastodonMeta
|
||||
import MastodonSDK
|
||||
import NaturalLanguage
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
|
||||
enum StatusSection: Equatable, Hashable {
|
||||
|
@ -26,6 +27,7 @@ extension StatusSection {
|
|||
static let logger = Logger(subsystem: "StatusSection", category: "logic")
|
||||
|
||||
struct Configuration {
|
||||
let authContext: AuthContext
|
||||
weak var statusTableViewCellDelegate: StatusTableViewCellDelegate?
|
||||
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
|
||||
let filterContext: Mastodon.Entity.Filter.Context?
|
||||
|
@ -158,6 +160,7 @@ extension StatusSection {
|
|||
|
||||
public static func setupStatusPollDataSource(
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
statusView: StatusView
|
||||
) {
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
|
@ -171,10 +174,7 @@ extension StatusSection {
|
|||
return _cell ?? PollOptionTableViewCell()
|
||||
}()
|
||||
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.map { $0 as UserIdentifier? }
|
||||
.assign(to: \.userIdentifier, on: cell.pollOptionView.viewModel)
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.pollOptionView.viewModel.authContext = authContext
|
||||
|
||||
managedObjectContext.performAndWait {
|
||||
guard let option = record.object(in: managedObjectContext) else {
|
||||
|
@ -211,14 +211,13 @@ extension StatusSection {
|
|||
return true
|
||||
}()
|
||||
|
||||
if needsUpdatePoll, let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value
|
||||
{
|
||||
if needsUpdatePoll {
|
||||
let pollRecord: ManagedObjectRecord<Poll> = .init(objectID: option.poll.objectID)
|
||||
Task { [weak context] in
|
||||
guard let context = context else { return }
|
||||
_ = try await context.apiService.poll(
|
||||
poll: pollRecord,
|
||||
authenticationBox: authenticationBox
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -247,13 +246,11 @@ extension StatusSection {
|
|||
) {
|
||||
setupStatusPollDataSource(
|
||||
context: context,
|
||||
authContext: configuration.authContext,
|
||||
statusView: cell.statusView
|
||||
)
|
||||
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.map { $0 as UserIdentifier? }
|
||||
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.statusView.viewModel.authContext = configuration.authContext
|
||||
|
||||
cell.configure(
|
||||
tableView: tableView,
|
||||
|
@ -276,13 +273,11 @@ extension StatusSection {
|
|||
) {
|
||||
setupStatusPollDataSource(
|
||||
context: context,
|
||||
authContext: configuration.authContext,
|
||||
statusView: cell.statusView
|
||||
)
|
||||
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.map { $0 as UserIdentifier? }
|
||||
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.statusView.viewModel.authContext = configuration.authContext
|
||||
|
||||
cell.configure(
|
||||
tableView: tableView,
|
||||
|
|
|
@ -9,8 +9,10 @@ import os.log
|
|||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MetaTextKit
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonMeta
|
||||
import MetaTextKit
|
||||
|
||||
enum UserSection: Hashable {
|
||||
case main
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
// }
|
||||
//}
|
|
@ -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
|
||||
}
|
|
@ -2,19 +2,6 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<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>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
|
@ -30,7 +17,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.4.5</string>
|
||||
<string>1.4.6</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
@ -43,7 +30,7 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>144</string>
|
||||
<string>147</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
|
@ -59,6 +46,19 @@
|
|||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<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>
|
||||
<array>
|
||||
<string>SendPostIntent</string>
|
||||
|
@ -103,6 +103,10 @@
|
|||
</array>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>Main</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
//
|
||||
// ThemePreference.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-7-5.
|
||||
//
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
//
|
||||
// NamingState.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-17.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol NamingState {
|
||||
var name: String { get }
|
||||
}
|
|
@ -7,19 +7,19 @@
|
|||
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
|
||||
extension DataSourceFacade {
|
||||
static func responseToUserBlockAction(
|
||||
dependency: NeedsDependency,
|
||||
user: ManagedObjectRecord<MastodonUser>,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
user: ManagedObjectRecord<MastodonUser>
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
_ = try await dependency.context.apiService.toggleBlock(
|
||||
user: user,
|
||||
authenticationBox: authenticationBox
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
} // end func
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -8,19 +8,19 @@
|
|||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
|
||||
extension DataSourceFacade {
|
||||
static func responseToStatusFavoriteAction(
|
||||
provider: DataSourceProvider,
|
||||
status: ManagedObjectRecord<Status>,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
public static func responseToStatusFavoriteAction(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
status: ManagedObjectRecord<Status>
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
_ = try await provider.context.apiService.favorite(
|
||||
record: status,
|
||||
authenticationBox: authenticationBox
|
||||
authenticationBox: provider.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,31 +8,30 @@
|
|||
import UIKit
|
||||
import CoreDataStack
|
||||
import class CoreDataStack.Notification
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
import MastodonLocalization
|
||||
|
||||
extension DataSourceFacade {
|
||||
static func responseToUserFollowAction(
|
||||
dependency: NeedsDependency,
|
||||
user: ManagedObjectRecord<MastodonUser>,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
user: ManagedObjectRecord<MastodonUser>
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
_ = try await dependency.context.apiService.toggleFollow(
|
||||
user: user,
|
||||
authenticationBox: authenticationBox
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
} // end func
|
||||
}
|
||||
|
||||
extension DataSourceFacade {
|
||||
static func responseToUserFollowRequestAction(
|
||||
dependency: NeedsDependency,
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
notification: ManagedObjectRecord<Notification>,
|
||||
query: Mastodon.API.Account.FollowReqeustQuery,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
query: Mastodon.API.Account.FollowReqeustQuery
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
@ -71,9 +70,10 @@ extension DataSourceFacade {
|
|||
_ = try await dependency.context.apiService.followRequest(
|
||||
userID: userID,
|
||||
query: query,
|
||||
authenticationBox: authenticationBox
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
} catch {
|
||||
// reset state when failure
|
||||
try? await managedObjectContext.performChanges {
|
||||
guard let notification = notification.object(in: managedObjectContext) else { return }
|
||||
notification.transientFollowRequestState = .init(state: .none)
|
||||
|
@ -111,7 +111,8 @@ extension DataSourceFacade {
|
|||
case .accept:
|
||||
notification.transientFollowRequestState = .init(state: .isAccept)
|
||||
case .reject:
|
||||
notification.transientFollowRequestState = .init(state: .isReject)
|
||||
// do nothing due to will delete notification
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,7 +123,11 @@ extension DataSourceFacade {
|
|||
case .accept:
|
||||
notification.followRequestState = .init(state: .isAccept)
|
||||
case .reject:
|
||||
notification.followRequestState = .init(state: .isReject)
|
||||
// delete notification
|
||||
for feed in notification.feeds {
|
||||
backgroundManagedObjectContext.delete(feed)
|
||||
}
|
||||
backgroundManagedObjectContext.delete(notification)
|
||||
}
|
||||
}
|
||||
} // end func
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
extension DataSourceFacade {
|
||||
@MainActor
|
||||
static func coordinateToHashtagScene(
|
||||
provider: DataSourceProvider,
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
tag: DataSourceItem.TagKind
|
||||
) async {
|
||||
switch tag {
|
||||
|
@ -25,11 +26,12 @@ extension DataSourceFacade {
|
|||
|
||||
@MainActor
|
||||
static func coordinateToHashtagScene(
|
||||
provider: DataSourceProvider,
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
tag: Mastodon.Entity.Tag
|
||||
) async {
|
||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(
|
||||
context: provider.context,
|
||||
authContext: provider.authContext,
|
||||
hashtag: tag.name
|
||||
)
|
||||
|
||||
|
@ -42,7 +44,7 @@ extension DataSourceFacade {
|
|||
|
||||
@MainActor
|
||||
static func coordinateToHashtagScene(
|
||||
provider: DataSourceProvider,
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
tag: ManagedObjectRecord<Tag>
|
||||
) async {
|
||||
let managedObjectContext = provider.context.managedObjectContext
|
||||
|
@ -55,6 +57,7 @@ extension DataSourceFacade {
|
|||
|
||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(
|
||||
context: provider.context,
|
||||
authContext: provider.authContext,
|
||||
hashtag: name
|
||||
)
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
// Created by MainasuK on 2022-1-26.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonUI
|
||||
|
@ -153,6 +154,8 @@ extension DataSourceFacade {
|
|||
user: ManagedObjectRecord<MastodonUser>,
|
||||
previewContext: ImagePreviewContext
|
||||
) async throws {
|
||||
let logger = Logger(subsystem: "DataSourceFacade", category: "Media")
|
||||
|
||||
let managedObjectContext = dependency.context.managedObjectContext
|
||||
|
||||
var _avatarAssetURL: String?
|
||||
|
@ -216,13 +219,18 @@ extension DataSourceFacade {
|
|||
thumbnail: thumbnail
|
||||
))
|
||||
case .profileBanner:
|
||||
return .profileAvatar(.init(
|
||||
return .profileBanner(.init(
|
||||
assetURL: _headerAssetURL,
|
||||
thumbnail: thumbnail
|
||||
))
|
||||
}
|
||||
}()
|
||||
|
||||
guard mediaPreviewItem.isAssetURLValid else {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): discard preview due to assetURL invalid")
|
||||
return
|
||||
}
|
||||
|
||||
coordinateToMediaPreviewScene(
|
||||
dependency: dependency,
|
||||
mediaPreviewItem: mediaPreviewItem,
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
import Foundation
|
||||
import CoreDataStack
|
||||
import MetaTextKit
|
||||
import MastodonCore
|
||||
|
||||
extension DataSourceFacade {
|
||||
|
||||
static func responseToMetaTextAction(
|
||||
provider: DataSourceProvider,
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
target: StatusTarget,
|
||||
status: ManagedObjectRecord<Status>,
|
||||
meta: Meta
|
||||
|
@ -33,7 +34,7 @@ extension DataSourceFacade {
|
|||
}
|
||||
|
||||
static func responseToMetaTextAction(
|
||||
provider: DataSourceProvider,
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
status: ManagedObjectRecord<Status>,
|
||||
meta: Meta
|
||||
) async {
|
||||
|
@ -47,19 +48,20 @@ extension DataSourceFacade {
|
|||
assertionFailure()
|
||||
return
|
||||
}
|
||||
if let domain = provider.context.authenticationService.activeMastodonAuthenticationBox.value?.domain, url.host == domain,
|
||||
let domain = provider.authContext.mastodonAuthenticationBox.domain
|
||||
if url.host == domain,
|
||||
url.pathComponents.count >= 4,
|
||||
url.pathComponents[0] == "/",
|
||||
url.pathComponents[1] == "web",
|
||||
url.pathComponents[2] == "statuses" {
|
||||
let statusID = url.pathComponents[3]
|
||||
let threadViewModel = RemoteThreadViewModel(context: provider.context, statusID: statusID)
|
||||
let threadViewModel = RemoteThreadViewModel(context: provider.context, authContext: provider.authContext, statusID: statusID)
|
||||
await provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show)
|
||||
} else {
|
||||
await provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
||||
}
|
||||
case .hashtag(_, let hashtag, _):
|
||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: hashtag)
|
||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, authContext: provider.authContext, hashtag: hashtag)
|
||||
await provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: provider, transition: .show)
|
||||
case .mention(_, let mention, let userInfo):
|
||||
await coordinateToProfileScene(
|
||||
|
|
|
@ -7,19 +7,19 @@
|
|||
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
|
||||
extension DataSourceFacade {
|
||||
static func responseToUserMuteAction(
|
||||
dependency: NeedsDependency,
|
||||
user: ManagedObjectRecord<MastodonUser>,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
user: ManagedObjectRecord<MastodonUser>
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
_ = try await dependency.context.apiService.toggleMute(
|
||||
user: user,
|
||||
authenticationBox: authenticationBox
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
} // end func
|
||||
}
|
||||
|
|
|
@ -7,11 +7,12 @@
|
|||
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
|
||||
extension DataSourceFacade {
|
||||
|
||||
static func coordinateToProfileScene(
|
||||
provider: DataSourceProvider,
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
target: StatusTarget,
|
||||
status: ManagedObjectRecord<Status>
|
||||
) async {
|
||||
|
@ -32,7 +33,7 @@ extension DataSourceFacade {
|
|||
|
||||
@MainActor
|
||||
static func coordinateToProfileScene(
|
||||
provider: DataSourceProvider,
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
user: ManagedObjectRecord<MastodonUser>
|
||||
) async {
|
||||
guard let user = user.object(in: provider.context.managedObjectContext) else {
|
||||
|
@ -42,6 +43,7 @@ extension DataSourceFacade {
|
|||
|
||||
let profileViewModel = CachedProfileViewModel(
|
||||
context: provider.context,
|
||||
authContext: provider.authContext,
|
||||
mastodonUser: user
|
||||
)
|
||||
|
||||
|
@ -57,13 +59,12 @@ extension DataSourceFacade {
|
|||
extension DataSourceFacade {
|
||||
|
||||
static func coordinateToProfileScene(
|
||||
provider: DataSourceProvider,
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
status: ManagedObjectRecord<Status>,
|
||||
mention: String, // username,
|
||||
userInfo: [AnyHashable: Any]?
|
||||
) async {
|
||||
guard let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
let domain = authenticationBox.domain
|
||||
let domain = provider.authContext.mastodonAuthenticationBox.domain
|
||||
|
||||
let href = userInfo?["href"] as? String
|
||||
guard let url = href.flatMap({ URL(string: $0) }) else { return }
|
||||
|
@ -85,8 +86,8 @@ extension DataSourceFacade {
|
|||
let userID = mention.id
|
||||
let profileViewModel: ProfileViewModel = {
|
||||
// check if self
|
||||
guard userID != authenticationBox.userID else {
|
||||
return MeProfileViewModel(context: provider.context)
|
||||
guard userID != provider.authContext.mastodonAuthenticationBox.userID else {
|
||||
return MeProfileViewModel(context: provider.context, authContext: provider.authContext)
|
||||
}
|
||||
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
|
@ -95,9 +96,9 @@ extension DataSourceFacade {
|
|||
let _user = provider.context.managedObjectContext.safeFetch(request).first
|
||||
|
||||
if let user = _user {
|
||||
return CachedProfileViewModel(context: provider.context, mastodonUser: user)
|
||||
return CachedProfileViewModel(context: provider.context, authContext: provider.authContext, mastodonUser: user)
|
||||
} else {
|
||||
return RemoteProfileViewModel(context: provider.context, userID: userID)
|
||||
return RemoteProfileViewModel(context: provider.context, authContext: provider.authContext, userID: userID)
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
|
@ -7,20 +7,20 @@
|
|||
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
|
||||
extension DataSourceFacade {
|
||||
static func responseToStatusReblogAction(
|
||||
provider: DataSourceProvider,
|
||||
status: ManagedObjectRecord<Status>,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
status: ManagedObjectRecord<Status>
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
_ = try await provider.context.apiService.reblog(
|
||||
record: status,
|
||||
authenticationBox: authenticationBox
|
||||
authenticationBox: provider.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
} // end func
|
||||
}
|
||||
|
|
|
@ -7,22 +7,23 @@
|
|||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
|
||||
extension DataSourceFacade {
|
||||
|
||||
static func responseToCreateSearchHistory(
|
||||
provider: DataSourceProvider,
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
item: DataSourceItem
|
||||
) async {
|
||||
switch item {
|
||||
case .status:
|
||||
break // not create search history for status
|
||||
case .user(let record):
|
||||
let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value
|
||||
let authenticationBox = provider.authContext.mastodonAuthenticationBox
|
||||
let managedObjectContext = provider.context.backgroundManagedObjectContext
|
||||
|
||||
try? await managedObjectContext.performChanges {
|
||||
guard let me = authenticationBox?.authenticationRecord.object(in: managedObjectContext)?.user else { return }
|
||||
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
|
||||
guard let user = record.object(in: managedObjectContext) else { return }
|
||||
_ = Persistence.SearchHistory.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
|
@ -34,13 +35,12 @@ extension DataSourceFacade {
|
|||
)
|
||||
} // end try? await managedObjectContext.performChanges { … }
|
||||
case .hashtag(let tag):
|
||||
let _authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value
|
||||
let authenticationBox = provider.authContext.mastodonAuthenticationBox
|
||||
let managedObjectContext = provider.context.backgroundManagedObjectContext
|
||||
|
||||
switch tag {
|
||||
case .entity(let entity):
|
||||
try? await managedObjectContext.performChanges {
|
||||
guard let authenticationBox = _authenticationBox else { return }
|
||||
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
|
||||
|
||||
let now = Date()
|
||||
|
@ -66,7 +66,7 @@ extension DataSourceFacade {
|
|||
} // end try? await managedObjectContext.performChanges { … }
|
||||
case .record(let record):
|
||||
try? await managedObjectContext.performChanges {
|
||||
guard let authenticationBox = _authenticationBox else { return }
|
||||
let authenticationBox = provider.authContext.mastodonAuthenticationBox
|
||||
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
|
||||
guard let tag = record.object(in: managedObjectContext) else { return }
|
||||
|
||||
|
@ -92,13 +92,12 @@ extension DataSourceFacade {
|
|||
extension DataSourceFacade {
|
||||
|
||||
static func responseToDeleteSearchHistory(
|
||||
provider: DataSourceProvider
|
||||
provider: DataSourceProvider & AuthContextProvider
|
||||
) async throws {
|
||||
let _authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value
|
||||
let authenticationBox = provider.authContext.mastodonAuthenticationBox
|
||||
let managedObjectContext = provider.context.backgroundManagedObjectContext
|
||||
|
||||
try await managedObjectContext.performChanges {
|
||||
guard let authenticationBox = _authenticationBox else { return }
|
||||
guard let _ = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
|
||||
let request = SearchHistory.sortedFetchRequest
|
||||
request.predicate = SearchHistory.predicate(
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
|
||||
|
@ -14,13 +15,12 @@ import MastodonLocalization
|
|||
extension DataSourceFacade {
|
||||
|
||||
static func responseToDeleteStatus(
|
||||
dependency: NeedsDependency,
|
||||
status: ManagedObjectRecord<Status>,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
status: ManagedObjectRecord<Status>
|
||||
) async throws {
|
||||
_ = try await dependency.context.apiService.deleteStatus(
|
||||
status: status,
|
||||
authenticationBox: authenticationBox
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ extension DataSourceFacade {
|
|||
button: UIButton
|
||||
) async throws {
|
||||
let activityViewController = try await createActivityViewController(
|
||||
provider: provider,
|
||||
dependency: provider,
|
||||
status: status
|
||||
)
|
||||
provider.coordinator.present(
|
||||
|
@ -51,19 +51,19 @@ extension DataSourceFacade {
|
|||
}
|
||||
|
||||
private static func createActivityViewController(
|
||||
provider: DataSourceProvider,
|
||||
dependency: NeedsDependency,
|
||||
status: ManagedObjectRecord<Status>
|
||||
) async throws -> UIActivityViewController {
|
||||
var activityItems: [Any] = try await provider.context.managedObjectContext.perform {
|
||||
guard let status = status.object(in: provider.context.managedObjectContext) else { return [] }
|
||||
var activityItems: [Any] = try await dependency.context.managedObjectContext.perform {
|
||||
guard let status = status.object(in: dependency.context.managedObjectContext) else { return [] }
|
||||
let url = status.url ?? status.uri
|
||||
return [URL(string: url)].compactMap { $0 } as [Any]
|
||||
}
|
||||
var applicationActivities: [UIActivity] = [
|
||||
SafariActivity(sceneCoordinator: provider.coordinator), // open URL
|
||||
SafariActivity(sceneCoordinator: dependency.coordinator), // open URL
|
||||
]
|
||||
|
||||
if let provider = provider as? ShareActivityProvider {
|
||||
if let provider = dependency as? ShareActivityProvider {
|
||||
activityItems.append(contentsOf: provider.activities)
|
||||
applicationActivities.append(contentsOf: provider.applicationActivities)
|
||||
}
|
||||
|
@ -80,10 +80,9 @@ extension DataSourceFacade {
|
|||
extension DataSourceFacade {
|
||||
@MainActor
|
||||
static func responseToActionToolbar(
|
||||
provider: DataSourceProvider,
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
status: ManagedObjectRecord<Status>,
|
||||
action: ActionToolbarContainer.Action,
|
||||
authenticationBox: MastodonAuthenticationBox,
|
||||
sender: UIButton
|
||||
) async throws {
|
||||
let managedObjectContext = provider.context.managedObjectContext
|
||||
|
@ -99,16 +98,15 @@ extension DataSourceFacade {
|
|||
|
||||
switch action {
|
||||
case .reply:
|
||||
guard let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
|
||||
selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
let composeViewModel = ComposeViewModel(
|
||||
context: provider.context,
|
||||
composeKind: .reply(status: status),
|
||||
authenticationBox: authenticationBox
|
||||
authContext: provider.authContext,
|
||||
kind: .reply(status: status)
|
||||
)
|
||||
provider.coordinator.present(
|
||||
_ = provider.coordinator.present(
|
||||
scene: .compose(viewModel: composeViewModel),
|
||||
from: provider,
|
||||
transition: .modal(animated: true, completion: nil)
|
||||
|
@ -116,14 +114,17 @@ extension DataSourceFacade {
|
|||
case .reblog:
|
||||
try await DataSourceFacade.responseToStatusReblogAction(
|
||||
provider: provider,
|
||||
status: status,
|
||||
authenticationBox: authenticationBox
|
||||
status: status
|
||||
)
|
||||
case .like:
|
||||
try await DataSourceFacade.responseToStatusFavoriteAction(
|
||||
provider: provider,
|
||||
status: status,
|
||||
authenticationBox: authenticationBox
|
||||
status: status
|
||||
)
|
||||
case .bookmark:
|
||||
try await DataSourceFacade.responseToStatusBookmarkAction(
|
||||
provider: provider,
|
||||
status: status
|
||||
)
|
||||
case .share:
|
||||
try await DataSourceFacade.responseToStatusShareAction(
|
||||
|
@ -148,10 +149,9 @@ extension DataSourceFacade {
|
|||
|
||||
@MainActor
|
||||
static func responseToMenuAction(
|
||||
dependency: NeedsDependency & UIViewController,
|
||||
dependency: UIViewController & NeedsDependency & AuthContextProvider,
|
||||
action: MastodonMenu.Action,
|
||||
menuContext: MenuContext,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
menuContext: MenuContext
|
||||
) async throws {
|
||||
switch action {
|
||||
case .muteUser(let actionContext):
|
||||
|
@ -174,8 +174,7 @@ extension DataSourceFacade {
|
|||
guard let user = _user else { return }
|
||||
try await DataSourceFacade.responseToUserMuteAction(
|
||||
dependency: dependency,
|
||||
user: user,
|
||||
authenticationBox: authenticationBox
|
||||
user: user
|
||||
)
|
||||
} // end Task
|
||||
}
|
||||
|
@ -203,8 +202,7 @@ extension DataSourceFacade {
|
|||
guard let user = _user else { return }
|
||||
try await DataSourceFacade.responseToUserBlockAction(
|
||||
dependency: dependency,
|
||||
user: user,
|
||||
authenticationBox: authenticationBox
|
||||
user: user
|
||||
)
|
||||
} // end Task
|
||||
}
|
||||
|
@ -218,11 +216,12 @@ extension DataSourceFacade {
|
|||
|
||||
let reportViewModel = ReportViewModel(
|
||||
context: dependency.context,
|
||||
authContext: dependency.authContext,
|
||||
user: user,
|
||||
status: menuContext.status
|
||||
)
|
||||
|
||||
dependency.coordinator.present(
|
||||
_ = dependency.coordinator.present(
|
||||
scene: .report(viewModel: reportViewModel),
|
||||
from: dependency,
|
||||
transition: .modal(animated: true, completion: nil)
|
||||
|
@ -239,7 +238,7 @@ extension DataSourceFacade {
|
|||
user: user
|
||||
)
|
||||
guard let activityViewController = _activityViewController else { return }
|
||||
dependency.coordinator.present(
|
||||
_ = dependency.coordinator.present(
|
||||
scene: .activityViewController(
|
||||
activityViewController: activityViewController,
|
||||
sourceView: menuContext.button,
|
||||
|
@ -248,6 +247,37 @@ extension DataSourceFacade {
|
|||
from: dependency,
|
||||
transition: .activityViewControllerPresent(animated: true, completion: nil)
|
||||
)
|
||||
case .bookmarkStatus:
|
||||
Task {
|
||||
guard let status = menuContext.status else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
try await DataSourceFacade.responseToStatusBookmarkAction(
|
||||
provider: dependency,
|
||||
status: status
|
||||
)
|
||||
} // end Task
|
||||
case .shareStatus:
|
||||
Task {
|
||||
guard let status = menuContext.status else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
let activityViewController = try await DataSourceFacade.createActivityViewController(
|
||||
dependency: dependency,
|
||||
status: status
|
||||
)
|
||||
await dependency.coordinator.present(
|
||||
scene: .activityViewController(
|
||||
activityViewController: activityViewController,
|
||||
sourceView: menuContext.button,
|
||||
barButtonItem: menuContext.barButtonItem
|
||||
),
|
||||
from: dependency,
|
||||
transition: .activityViewControllerPresent(animated: true, completion: nil)
|
||||
)
|
||||
} // end Task
|
||||
case .deleteStatus:
|
||||
let alertController = UIAlertController(
|
||||
title: "Delete Post",
|
||||
|
@ -263,8 +293,7 @@ extension DataSourceFacade {
|
|||
Task {
|
||||
try await DataSourceFacade.responseToDeleteStatus(
|
||||
dependency: dependency,
|
||||
status: status,
|
||||
authenticationBox: authenticationBox
|
||||
status: status
|
||||
)
|
||||
} // end Task
|
||||
}
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
import Foundation
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
|
||||
extension DataSourceFacade {
|
||||
static func coordinateToStatusThreadScene(
|
||||
provider: DataSourceProvider,
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
target: StatusTarget,
|
||||
status: ManagedObjectRecord<Status>
|
||||
) async {
|
||||
|
@ -39,14 +40,15 @@ extension DataSourceFacade {
|
|||
|
||||
@MainActor
|
||||
static func coordinateToStatusThreadScene(
|
||||
provider: DataSourceProvider,
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
root: StatusItem.Thread
|
||||
) async {
|
||||
let threadViewModel = ThreadViewModel(
|
||||
context: provider.context,
|
||||
authContext: provider.authContext,
|
||||
optionalRoot: root
|
||||
)
|
||||
provider.coordinator.present(
|
||||
_ = provider.coordinator.present(
|
||||
scene: .thread(viewModel: threadViewModel),
|
||||
from: provider,
|
||||
transition: .show
|
||||
|
|
|
@ -7,18 +7,18 @@
|
|||
|
||||
import UIKit
|
||||
import MetaTextKit
|
||||
import MastodonUI
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
|
||||
// MARK: - Notification AuthorMenuAction
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
notificationView: NotificationView,
|
||||
menuButton button: UIButton,
|
||||
didSelectAction action: MastodonMenu.Action
|
||||
) {
|
||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
guard let item = await item(from: source) else {
|
||||
|
@ -47,15 +47,14 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
|||
status: nil,
|
||||
button: button,
|
||||
barButtonItem: nil
|
||||
),
|
||||
authenticationBox: authenticationBox
|
||||
)
|
||||
)
|
||||
} // end Task
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notification Author Avatar
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
notificationView: NotificationView,
|
||||
|
@ -88,7 +87,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
|||
}
|
||||
|
||||
// MARK: - Follow Request
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
|
@ -106,15 +105,10 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
|||
return
|
||||
}
|
||||
|
||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
return
|
||||
}
|
||||
|
||||
try await DataSourceFacade.responseToUserFollowRequestAction(
|
||||
dependency: self,
|
||||
notification: notification,
|
||||
query: .accept,
|
||||
authenticationBox: authenticationBox
|
||||
query: .accept
|
||||
)
|
||||
} // end Task
|
||||
}
|
||||
|
@ -135,15 +129,10 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
|||
return
|
||||
}
|
||||
|
||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
return
|
||||
}
|
||||
|
||||
try await DataSourceFacade.responseToUserFollowRequestAction(
|
||||
dependency: self,
|
||||
notification: notification,
|
||||
query: .reject,
|
||||
authenticationBox: authenticationBox
|
||||
query: .reject
|
||||
)
|
||||
} // end Task
|
||||
}
|
||||
|
@ -151,7 +140,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
|||
}
|
||||
|
||||
// MARK: - Status Content
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
notificationView: NotificationView,
|
||||
|
@ -279,7 +268,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Med
|
|||
}
|
||||
|
||||
// MARK: - Status Toolbar
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
notificationView: NotificationView,
|
||||
|
@ -287,7 +276,6 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
|||
buttonDidPressed button: UIButton,
|
||||
action: ActionToolbarContainer.Action
|
||||
) {
|
||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
guard let item = await item(from: source) else {
|
||||
|
@ -311,7 +299,6 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
|||
provider: self,
|
||||
status: status,
|
||||
action: action,
|
||||
authenticationBox: authenticationBox,
|
||||
sender: button
|
||||
)
|
||||
} // end Task
|
||||
|
@ -319,7 +306,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
|||
}
|
||||
|
||||
// MARK: - Status Author Avatar
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
notificationView: NotificationView,
|
||||
|
@ -354,7 +341,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
|||
}
|
||||
|
||||
// MARK: - Status Content
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
|
@ -530,7 +517,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
|||
}
|
||||
|
||||
// MARK: a11y
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, accessibilityActivate: Void) {
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
import UIKit
|
||||
import CoreDataStack
|
||||
import MetaTextKit
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
|
||||
// MARK: - header
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
|
@ -64,7 +65,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
|||
}
|
||||
|
||||
// MARK: - avatar button
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
|
@ -92,7 +93,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
|||
}
|
||||
|
||||
// MARK: - content
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
|
@ -169,7 +170,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & MediaPrev
|
|||
|
||||
|
||||
// MARK: - poll
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
|
@ -177,7 +178,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
|||
pollTableView tableView: UITableView,
|
||||
didSelectRowAt indexPath: IndexPath
|
||||
) {
|
||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
guard let pollTableViewDiffableDataSource = statusView.pollTableViewDiffableDataSource else { return }
|
||||
guard let pollItem = pollTableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
|
||||
|
@ -226,7 +226,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
|||
_ = try await context.apiService.vote(
|
||||
poll: poll,
|
||||
choices: [choice],
|
||||
authenticationBox: authenticationBox
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): vote poll for \(choice) success")
|
||||
} catch {
|
||||
|
@ -248,7 +248,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
|||
statusView: StatusView,
|
||||
pollVoteButtonPressed button: UIButton
|
||||
) {
|
||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
guard let pollTableViewDiffableDataSource = statusView.pollTableViewDiffableDataSource else { return }
|
||||
guard let firstPollItem = pollTableViewDiffableDataSource.snapshot().itemIdentifiers.first else { return }
|
||||
guard case let .option(firstPollOption) = firstPollItem else { return }
|
||||
|
@ -284,7 +283,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
|||
_ = try await context.apiService.vote(
|
||||
poll: poll,
|
||||
choices: choices,
|
||||
authenticationBox: authenticationBox
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): vote poll for \(choices) success")
|
||||
} catch {
|
||||
|
@ -303,7 +302,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
|||
}
|
||||
|
||||
// MARK: - toolbar
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
statusView: StatusView,
|
||||
|
@ -311,7 +310,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
|||
buttonDidPressed button: UIButton,
|
||||
action: ActionToolbarContainer.Action
|
||||
) {
|
||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
guard let item = await item(from: source) else {
|
||||
|
@ -327,7 +325,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
|||
provider: self,
|
||||
status: status,
|
||||
action: action,
|
||||
authenticationBox: authenticationBox,
|
||||
sender: button
|
||||
)
|
||||
} // end Task
|
||||
|
@ -336,14 +333,13 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
|||
}
|
||||
|
||||
// MARK: - menu button
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
statusView: StatusView,
|
||||
menuButton button: UIButton,
|
||||
didSelectAction action: MastodonMenu.Action
|
||||
) {
|
||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
guard let item = await item(from: source) else {
|
||||
|
@ -372,8 +368,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
|||
status: status,
|
||||
button: button,
|
||||
barButtonItem: nil
|
||||
),
|
||||
authenticationBox: authenticationBox
|
||||
)
|
||||
)
|
||||
} // end Task
|
||||
}
|
||||
|
@ -475,7 +470,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
|||
}
|
||||
|
||||
// MARK: - StatusMetricView
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton) {
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
|
@ -489,6 +484,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
|||
}
|
||||
let userListViewModel = UserListViewModel(
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
kind: .rebloggedBy(status: status)
|
||||
)
|
||||
await coordinator.present(
|
||||
|
@ -512,6 +508,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
|||
}
|
||||
let userListViewModel = UserListViewModel(
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
kind: .favoritedBy(status: status)
|
||||
)
|
||||
await coordinator.present(
|
||||
|
@ -524,7 +521,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
|||
}
|
||||
|
||||
// MARK: a11y
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void) {
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
|
||||
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & StatusTableViewControllerNavigateableRelay {
|
||||
|
||||
|
@ -30,7 +31,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
|
|||
|
||||
}
|
||||
|
||||
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider {
|
||||
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
func statusKeyCommandHandler(_ sender: UIKeyCommand) {
|
||||
guard let rawValue = sender.propertyList as? String,
|
||||
|
@ -53,7 +54,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
|
|||
}
|
||||
|
||||
// status coordinate
|
||||
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider {
|
||||
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
@MainActor
|
||||
private func statusRecord() async -> ManagedObjectRecord<Status>? {
|
||||
|
@ -93,16 +94,15 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
|
|||
private func replyStatus() async {
|
||||
guard let status = await statusRecord() else { return }
|
||||
|
||||
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
|
||||
selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
let composeViewModel = ComposeViewModel(
|
||||
context: self.context,
|
||||
composeKind: .reply(status: status),
|
||||
authenticationBox: authenticationBox
|
||||
authContext: authContext,
|
||||
kind: .reply(status: status)
|
||||
)
|
||||
self.coordinator.present(
|
||||
_ = self.coordinator.present(
|
||||
scene: .compose(viewModel: composeViewModel),
|
||||
from: self,
|
||||
transition: .modal(animated: true, completion: nil)
|
||||
|
@ -144,19 +144,16 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
|
|||
}
|
||||
|
||||
// toggle
|
||||
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider {
|
||||
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
@MainActor
|
||||
private func toggleReblog() async {
|
||||
guard let status = await statusRecord() else { return }
|
||||
|
||||
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
|
||||
do {
|
||||
try await DataSourceFacade.responseToStatusReblogAction(
|
||||
provider: self,
|
||||
status: status,
|
||||
authenticationBox: authenticationBox
|
||||
status: status
|
||||
)
|
||||
} catch {
|
||||
assertionFailure()
|
||||
|
@ -167,13 +164,10 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
|
|||
private func toggleFavorite() async {
|
||||
guard let status = await statusRecord() else { return }
|
||||
|
||||
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
|
||||
do {
|
||||
try await DataSourceFacade.responseToStatusFavoriteAction(
|
||||
provider: self,
|
||||
status: status,
|
||||
authenticationBox: authenticationBox
|
||||
status: status
|
||||
)
|
||||
} catch {
|
||||
assertionFailure()
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import os.log
|
||||
import UIKit
|
||||
import MastodonCore
|
||||
|
||||
extension TableViewControllerNavigateableCore where Self: TableViewControllerNavigateableRelay {
|
||||
var navigationKeyCommands: [UIKeyCommand] {
|
||||
|
@ -124,7 +125,7 @@ extension TableViewControllerNavigateableCore {
|
|||
|
||||
}
|
||||
|
||||
extension TableViewControllerNavigateableCore where Self: DataSourceProvider {
|
||||
extension TableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider {
|
||||
func open() {
|
||||
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
||||
let source = DataSourceItem.Source(indexPath: indexPathForSelectedRow)
|
||||
|
|
|
@ -8,9 +8,11 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
|
||||
extension UITableViewDelegate where Self: DataSourceProvider {
|
||||
extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): indexPath: \(indexPath.debugDescription)")
|
||||
|
|
|
@ -5,50 +5,59 @@
|
|||
// Created by Cirno MainasuK on 2021-9-13.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
|
||||
final class AccountListViewModel {
|
||||
final class AccountListViewModel: NSObject {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController<MastodonAuthentication>
|
||||
|
||||
// output
|
||||
let authentications = CurrentValueSubject<[Item], Never>([])
|
||||
let activeMastodonUserObjectID = CurrentValueSubject<NSManagedObjectID?, Never>(nil)
|
||||
@Published var authentications: [ManagedObjectRecord<MastodonAuthentication>] = []
|
||||
@Published var items: [Item] = []
|
||||
|
||||
let dataSourceDidUpdate = PassthroughSubject<Void, Never>()
|
||||
var diffableDataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||
|
||||
init(context: AppContext) {
|
||||
init(context: AppContext, authContext: AuthContext) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.mastodonAuthenticationFetchedResultsController = {
|
||||
let fetchRequest = MastodonAuthentication.sortedFetchRequest
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
fetchRequest.fetchBatchSize = 20
|
||||
let controller = NSFetchedResultsController(
|
||||
fetchRequest: fetchRequest,
|
||||
managedObjectContext: context.managedObjectContext,
|
||||
sectionNameKeyPath: nil,
|
||||
cacheName: nil
|
||||
)
|
||||
return controller
|
||||
}()
|
||||
super.init()
|
||||
// end init
|
||||
|
||||
Publishers.CombineLatest(
|
||||
context.authenticationService.mastodonAuthentications,
|
||||
context.authenticationService.activeMastodonAuthentication
|
||||
)
|
||||
.sink { [weak self] authentications, activeAuthentication in
|
||||
guard let self = self else { return }
|
||||
var items: [Item] = []
|
||||
var activeMastodonUserObjectID: NSManagedObjectID?
|
||||
for authentication in authentications {
|
||||
let item = Item.authentication(objectID: authentication.objectID)
|
||||
items.append(item)
|
||||
if authentication === activeAuthentication {
|
||||
activeMastodonUserObjectID = authentication.user.objectID
|
||||
}
|
||||
}
|
||||
self.authentications.value = items
|
||||
self.activeMastodonUserObjectID.value = activeMastodonUserObjectID
|
||||
mastodonAuthenticationFetchedResultsController.delegate = self
|
||||
do {
|
||||
try mastodonAuthenticationFetchedResultsController.performFetch()
|
||||
authentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?.compactMap { $0.asRecrod } ?? []
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
authentications
|
||||
$authentications
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] authentications in
|
||||
guard let self = self else { return }
|
||||
|
@ -56,7 +65,10 @@ final class AccountListViewModel {
|
|||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(authentications, toSection: .main)
|
||||
let authenticationItems: [Item] = authentications.map {
|
||||
Item.authentication(record: $0)
|
||||
}
|
||||
snapshot.appendItems(authenticationItems, toSection: .main)
|
||||
snapshot.appendItems([.addAccount], toSection: .main)
|
||||
|
||||
diffableDataSource.apply(snapshot) {
|
||||
|
@ -74,7 +86,7 @@ extension AccountListViewModel {
|
|||
}
|
||||
|
||||
enum Item: Hashable {
|
||||
case authentication(objectID: NSManagedObjectID)
|
||||
case authentication(record: ManagedObjectRecord<MastodonAuthentication>)
|
||||
case addAccount
|
||||
}
|
||||
|
||||
|
@ -84,14 +96,17 @@ extension AccountListViewModel {
|
|||
) {
|
||||
diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in
|
||||
switch item {
|
||||
case .authentication(let objectID):
|
||||
let authentication = managedObjectContext.object(with: objectID) as! MastodonAuthentication
|
||||
case .authentication(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell
|
||||
AccountListViewModel.configure(
|
||||
cell: cell,
|
||||
authentication: authentication,
|
||||
activeMastodonUserObjectID: self.activeMastodonUserObjectID.eraseToAnyPublisher()
|
||||
)
|
||||
if let authentication = record.object(in: managedObjectContext),
|
||||
let activeAuthentication = self.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)
|
||||
{
|
||||
AccountListViewModel.configure(
|
||||
cell: cell,
|
||||
authentication: authentication,
|
||||
activeAuthentication: activeAuthentication
|
||||
)
|
||||
}
|
||||
return cell
|
||||
case .addAccount:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AddAccountTableViewCell.self), for: indexPath) as! AddAccountTableViewCell
|
||||
|
@ -107,7 +122,7 @@ extension AccountListViewModel {
|
|||
static func configure(
|
||||
cell: AccountListTableViewCell,
|
||||
authentication: MastodonAuthentication,
|
||||
activeMastodonUserObjectID: AnyPublisher<NSManagedObjectID?, Never>
|
||||
activeAuthentication: MastodonAuthentication
|
||||
) {
|
||||
let user = authentication.user
|
||||
|
||||
|
@ -136,19 +151,14 @@ extension AccountListViewModel {
|
|||
cell.badgeButton.setBadge(number: count)
|
||||
|
||||
// checkmark
|
||||
activeMastodonUserObjectID
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { objectID in
|
||||
let isCurrentUser = user.objectID == objectID
|
||||
cell.tintColor = .label
|
||||
cell.checkmarkImageView.isHidden = !isCurrentUser
|
||||
if isCurrentUser {
|
||||
cell.accessibilityTraits.insert(.selected)
|
||||
} else {
|
||||
cell.accessibilityTraits.remove(.selected)
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
let isActive = activeAuthentication.userID == authentication.userID
|
||||
cell.tintColor = .label
|
||||
cell.checkmarkImageView.isHidden = !isActive
|
||||
if isActive {
|
||||
cell.accessibilityTraits.insert(.selected)
|
||||
} else {
|
||||
cell.accessibilityTraits.remove(.selected)
|
||||
}
|
||||
|
||||
cell.accessibilityLabel = [
|
||||
cell.nameLabel.text,
|
||||
|
@ -159,3 +169,21 @@ extension AccountListViewModel {
|
|||
.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSFetchedResultsControllerDelegate
|
||||
extension AccountListViewModel: NSFetchedResultsControllerDelegate {
|
||||
|
||||
public func controllerWillChangeContent(_ controller: NSFetchedResultsController<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 } ?? []
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import CoreDataStack
|
|||
import PanModal
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
import MastodonCore
|
||||
|
||||
final class AccountListViewController: UIViewController, NeedsDependency {
|
||||
|
||||
|
@ -21,7 +22,7 @@ final class AccountListViewController: UIViewController, NeedsDependency {
|
|||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
private(set) lazy var viewModel = AccountListViewModel(context: context)
|
||||
var viewModel: AccountListViewModel!
|
||||
|
||||
private(set) lazy var addBarButtonItem: UIBarButtonItem = {
|
||||
let barButtonItem = UIBarButtonItem(
|
||||
|
@ -63,7 +64,10 @@ extension AccountListViewController: PanModalPresentable {
|
|||
return .contentHeight(CGFloat(height))
|
||||
}
|
||||
|
||||
let count = viewModel.context.authenticationService.mastodonAuthentications.value.count + 1
|
||||
let request = MastodonAuthentication.sortedFetchRequest
|
||||
let authenticationCount = (try? context.managedObjectContext.count(for: request)) ?? 0
|
||||
|
||||
let count = authenticationCount + 1
|
||||
let height = calculateHeight(of: count)
|
||||
return .contentHeight(height)
|
||||
}
|
||||
|
@ -154,7 +158,7 @@ extension AccountListViewController {
|
|||
|
||||
@objc private func addBarButtonItem(_ sender: UIBarButtonItem) {
|
||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil))
|
||||
_ = coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil))
|
||||
}
|
||||
|
||||
@objc private func dragIndicatorTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
||||
|
@ -173,19 +177,17 @@ extension AccountListViewController: UITableViewDelegate {
|
|||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
|
||||
switch item {
|
||||
case .authentication(let objectID):
|
||||
case .authentication(let record):
|
||||
assert(Thread.isMainThread)
|
||||
let authentication = context.managedObjectContext.object(with: objectID) as! MastodonAuthentication
|
||||
context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
self.coordinator.setup()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
guard let authentication = record.object(in: context.managedObjectContext) else { return }
|
||||
Task { @MainActor in
|
||||
let isActive = try await context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID)
|
||||
guard isActive else { return }
|
||||
self.coordinator.setup()
|
||||
} // end Task
|
||||
case .addAccount:
|
||||
// TODO: add dismiss entry for welcome scene
|
||||
coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil))
|
||||
_ = coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import UIKit
|
|||
import Combine
|
||||
import FLAnimatedImage
|
||||
import MetaTextKit
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
|
||||
final class AccountListTableViewCell: UITableViewCell {
|
||||
|
|
|
@ -10,6 +10,8 @@ import Combine
|
|||
import MetaTextKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
|
||||
final class AddAccountTableViewCell: UITableViewCell {
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -27,7 +27,7 @@ final class ComposeStatusAttachmentCollectionViewCell: UICollectionViewCell {
|
|||
|
||||
weak var delegate: ComposeStatusAttachmentCollectionViewCellDelegate?
|
||||
|
||||
let attachmentContainerView = AttachmentContainerView()
|
||||
// let attachmentContainerView = AttachmentContainerView()
|
||||
let removeButton: UIButton = {
|
||||
let button = HighlightDimmableButton()
|
||||
button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
|
||||
|
@ -45,11 +45,11 @@ final class ComposeStatusAttachmentCollectionViewCell: UICollectionViewCell {
|
|||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
attachmentContainerView.activityIndicatorView.startAnimating()
|
||||
attachmentContainerView.previewImageView.af.cancelImageRequest()
|
||||
attachmentContainerView.previewImageView.image = .placeholder(color: .systemFill)
|
||||
delegate = nil
|
||||
disposeBag.removeAll()
|
||||
// attachmentContainerView.activityIndicatorView.startAnimating()
|
||||
// attachmentContainerView.previewImageView.af.cancelImageRequest()
|
||||
// attachmentContainerView.previewImageView.image = .placeholder(color: .systemFill)
|
||||
// delegate = nil
|
||||
// disposeBag.removeAll()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
@ -73,31 +73,30 @@ extension ComposeStatusAttachmentCollectionViewCell {
|
|||
private func _init() {
|
||||
// selectionStyle = .none
|
||||
|
||||
attachmentContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(attachmentContainerView)
|
||||
NSLayoutConstraint.activate([
|
||||
attachmentContainerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: ComposeStatusAttachmentCollectionViewCell.verticalMarginHeight),
|
||||
attachmentContainerView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
attachmentContainerView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: attachmentContainerView.bottomAnchor, constant: ComposeStatusAttachmentCollectionViewCell.verticalMarginHeight),
|
||||
attachmentContainerView.heightAnchor.constraint(equalToConstant: 205).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
removeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(removeButton)
|
||||
NSLayoutConstraint.activate([
|
||||
removeButton.centerXAnchor.constraint(equalTo: attachmentContainerView.trailingAnchor),
|
||||
removeButton.centerYAnchor.constraint(equalTo: attachmentContainerView.topAnchor),
|
||||
removeButton.widthAnchor.constraint(equalToConstant: ComposeStatusAttachmentCollectionViewCell.removeButtonSize.width).priority(.defaultHigh),
|
||||
removeButton.heightAnchor.constraint(equalToConstant: ComposeStatusAttachmentCollectionViewCell.removeButtonSize.height).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
removeButton.addTarget(self, action: #selector(ComposeStatusAttachmentCollectionViewCell.removeButtonDidPressed(_:)), for: .touchUpInside)
|
||||
// attachmentContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// contentView.addSubview(attachmentContainerView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// attachmentContainerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: ComposeStatusAttachmentCollectionViewCell.verticalMarginHeight),
|
||||
// attachmentContainerView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
// attachmentContainerView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
// contentView.bottomAnchor.constraint(equalTo: attachmentContainerView.bottomAnchor, constant: ComposeStatusAttachmentCollectionViewCell.verticalMarginHeight),
|
||||
// attachmentContainerView.heightAnchor.constraint(equalToConstant: 205).priority(.defaultHigh),
|
||||
// ])
|
||||
//
|
||||
// removeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
// contentView.addSubview(removeButton)
|
||||
// NSLayoutConstraint.activate([
|
||||
// removeButton.centerXAnchor.constraint(equalTo: attachmentContainerView.trailingAnchor),
|
||||
// removeButton.centerYAnchor.constraint(equalTo: attachmentContainerView.topAnchor),
|
||||
// removeButton.widthAnchor.constraint(equalToConstant: ComposeStatusAttachmentCollectionViewCell.removeButtonSize.width).priority(.defaultHigh),
|
||||
// removeButton.heightAnchor.constraint(equalToConstant: ComposeStatusAttachmentCollectionViewCell.removeButtonSize.height).priority(.defaultHigh),
|
||||
// ])
|
||||
//
|
||||
// removeButton.addTarget(self, action: #selector(ComposeStatusAttachmentCollectionViewCell.removeButtonDidPressed(_:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
extension ComposeStatusAttachmentCollectionViewCell {
|
||||
|
||||
@objc private func removeButtonDidPressed(_ sender: UIButton) {
|
||||
|
|
|
@ -9,68 +9,70 @@ import os.log
|
|||
import UIKit
|
||||
import Combine
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
|
||||
protocol ComposeStatusPollExpiresOptionCollectionViewCellDelegate: AnyObject {
|
||||
func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption)
|
||||
}
|
||||
|
||||
final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
weak var delegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate?
|
||||
|
||||
let durationButton: UIButton = {
|
||||
let button = HighlightDimmableButton()
|
||||
button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12))
|
||||
button.expandEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: -20, right: -20)
|
||||
button.setTitle(L10n.Scene.Compose.Poll.durationTime(L10n.Scene.Compose.Poll.thirtyMinutes), for: .normal)
|
||||
button.setTitleColor(Asset.Colors.brand.color, for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeStatusPollExpiresOptionCollectionViewCell {
|
||||
|
||||
private typealias ExpiresOption = ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption
|
||||
|
||||
private func _init() {
|
||||
durationButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(durationButton)
|
||||
NSLayoutConstraint.activate([
|
||||
durationButton.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
durationButton.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor, constant: PollOptionView.checkmarkBackgroundLeadingMargin),
|
||||
durationButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
])
|
||||
|
||||
let children = ExpiresOption.allCases.map { expiresOption -> UIAction in
|
||||
UIAction(title: expiresOption.title, image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.expiresOptionActionHandler(action, expiresOption: expiresOption)
|
||||
}
|
||||
}
|
||||
durationButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
|
||||
durationButton.showsMenuAsPrimaryAction = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeStatusPollExpiresOptionCollectionViewCell {
|
||||
|
||||
private func expiresOptionActionHandler(_ sender: UIAction, expiresOption: ExpiresOption) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, expiresOption.title)
|
||||
delegate?.composeStatusPollExpiresOptionCollectionViewCell(self, didSelectExpiresOption: expiresOption)
|
||||
}
|
||||
|
||||
}
|
||||
//protocol ComposeStatusPollExpiresOptionCollectionViewCellDelegate: AnyObject {
|
||||
// func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption)
|
||||
//}
|
||||
//
|
||||
//final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCell {
|
||||
//
|
||||
// var disposeBag = Set<AnyCancellable>()
|
||||
// weak var delegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate?
|
||||
//
|
||||
// let durationButton: UIButton = {
|
||||
// let button = HighlightDimmableButton()
|
||||
// button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12))
|
||||
// button.expandEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: -20, right: -20)
|
||||
// button.setTitle(L10n.Scene.Compose.Poll.durationTime(L10n.Scene.Compose.Poll.thirtyMinutes), for: .normal)
|
||||
// button.setTitleColor(Asset.Colors.brand.color, for: .normal)
|
||||
// return button
|
||||
// }()
|
||||
//
|
||||
// override init(frame: CGRect) {
|
||||
// super.init(frame: frame)
|
||||
// _init()
|
||||
// }
|
||||
//
|
||||
// required init?(coder: NSCoder) {
|
||||
// super.init(coder: coder)
|
||||
// _init()
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
//extension ComposeStatusPollExpiresOptionCollectionViewCell {
|
||||
//
|
||||
// private typealias ExpiresOption = ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption
|
||||
//
|
||||
// private func _init() {
|
||||
// durationButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
// contentView.addSubview(durationButton)
|
||||
// NSLayoutConstraint.activate([
|
||||
// durationButton.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
// durationButton.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor, constant: PollOptionView.checkmarkBackgroundLeadingMargin),
|
||||
// durationButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
// ])
|
||||
//
|
||||
// let children = ExpiresOption.allCases.map { expiresOption -> UIAction in
|
||||
// UIAction(title: expiresOption.title, image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in
|
||||
// guard let self = self else { return }
|
||||
// self.expiresOptionActionHandler(action, expiresOption: expiresOption)
|
||||
// }
|
||||
// }
|
||||
// durationButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
|
||||
// durationButton.showsMenuAsPrimaryAction = true
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
//extension ComposeStatusPollExpiresOptionCollectionViewCell {
|
||||
//
|
||||
// private func expiresOptionActionHandler(_ sender: UIAction, expiresOption: ExpiresOption) {
|
||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, expiresOption.title)
|
||||
// delegate?.composeStatusPollExpiresOptionCollectionViewCell(self, didSelectExpiresOption: expiresOption)
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
|
||||
protocol ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate: AnyObject {
|
||||
|
|
|
@ -9,6 +9,7 @@ import os.log
|
|||
import UIKit
|
||||
import Combine
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import MastodonUI
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,505 +9,482 @@ import os.log
|
|||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import MastodonSDK
|
||||
|
||||
extension ComposeViewModel {
|
||||
|
||||
func setupDataSource(
|
||||
tableView: UITableView,
|
||||
metaTextDelegate: MetaTextDelegate,
|
||||
metaTextViewDelegate: UITextViewDelegate,
|
||||
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
||||
composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
||||
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
||||
composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
||||
composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
) {
|
||||
// UI
|
||||
bind()
|
||||
|
||||
// content
|
||||
bind(cell: composeStatusContentTableViewCell, tableView: tableView)
|
||||
composeStatusContentTableViewCell.metaText.delegate = metaTextDelegate
|
||||
composeStatusContentTableViewCell.metaText.textView.delegate = metaTextViewDelegate
|
||||
|
||||
// attachment
|
||||
bind(cell: composeStatusAttachmentTableViewCell, tableView: tableView)
|
||||
composeStatusAttachmentTableViewCell.composeStatusAttachmentCollectionViewCellDelegate = composeStatusAttachmentCollectionViewCellDelegate
|
||||
|
||||
// poll
|
||||
bind(cell: composeStatusPollTableViewCell, tableView: tableView)
|
||||
composeStatusPollTableViewCell.delegate = self
|
||||
composeStatusPollTableViewCell.customEmojiPickerInputViewModel = customEmojiPickerInputViewModel
|
||||
composeStatusPollTableViewCell.composeStatusPollOptionCollectionViewCellDelegate = composeStatusPollOptionCollectionViewCellDelegate
|
||||
composeStatusPollTableViewCell.composeStatusPollOptionAppendEntryCollectionViewCellDelegate = composeStatusPollOptionAppendEntryCollectionViewCellDelegate
|
||||
composeStatusPollTableViewCell.composeStatusPollExpiresOptionCollectionViewCellDelegate = composeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
|
||||
// setup data source
|
||||
tableView.dataSource = self
|
||||
}
|
||||
|
||||
func setupCustomEmojiPickerDiffableDataSource(
|
||||
for collectionView: UICollectionView,
|
||||
dependency: NeedsDependency
|
||||
) {
|
||||
let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource(
|
||||
for: collectionView,
|
||||
dependency: dependency
|
||||
)
|
||||
self.customEmojiPickerDiffableDataSource = diffableDataSource
|
||||
|
||||
let _domain = customEmojiViewModel?.domain
|
||||
customEmojiViewModel?.emojis
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self, weak diffableDataSource] emojis in
|
||||
guard let _ = self else { return }
|
||||
guard let diffableDataSource = diffableDataSource else { return }
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<CustomEmojiPickerSection, CustomEmojiPickerItem>()
|
||||
let domain = _domain?.uppercased() ?? " "
|
||||
let customEmojiSection = CustomEmojiPickerSection.emoji(name: domain)
|
||||
snapshot.appendSections([customEmojiSection])
|
||||
let items: [CustomEmojiPickerItem] = {
|
||||
var items = [CustomEmojiPickerItem]()
|
||||
for emoji in emojis where emoji.visibleInPicker {
|
||||
let attribute = CustomEmojiPickerItem.CustomEmojiAttribute(emoji: emoji)
|
||||
let item = CustomEmojiPickerItem.emoji(attribute: attribute)
|
||||
items.append(item)
|
||||
}
|
||||
return items
|
||||
}()
|
||||
snapshot.appendItems(items, toSection: customEmojiSection)
|
||||
|
||||
diffableDataSource.apply(snapshot)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
// func setupDataSource(
|
||||
// tableView: UITableView,
|
||||
// metaTextDelegate: MetaTextDelegate,
|
||||
// metaTextViewDelegate: UITextViewDelegate,
|
||||
// customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
||||
// composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
||||
// composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
||||
// composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
||||
// composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
// ) {
|
||||
// // UI
|
||||
// bind()
|
||||
//
|
||||
// // content
|
||||
// bind(cell: composeStatusContentTableViewCell, tableView: tableView)
|
||||
// composeStatusContentTableViewCell.metaText.delegate = metaTextDelegate
|
||||
// composeStatusContentTableViewCell.metaText.textView.delegate = metaTextViewDelegate
|
||||
//
|
||||
// // attachment
|
||||
// bind(cell: composeStatusAttachmentTableViewCell, tableView: tableView)
|
||||
// composeStatusAttachmentTableViewCell.composeStatusAttachmentCollectionViewCellDelegate = composeStatusAttachmentCollectionViewCellDelegate
|
||||
//
|
||||
// // poll
|
||||
// bind(cell: composeStatusPollTableViewCell, tableView: tableView)
|
||||
// composeStatusPollTableViewCell.delegate = self
|
||||
// composeStatusPollTableViewCell.customEmojiPickerInputViewModel = customEmojiPickerInputViewModel
|
||||
// composeStatusPollTableViewCell.composeStatusPollOptionCollectionViewCellDelegate = composeStatusPollOptionCollectionViewCellDelegate
|
||||
// composeStatusPollTableViewCell.composeStatusPollOptionAppendEntryCollectionViewCellDelegate = composeStatusPollOptionAppendEntryCollectionViewCellDelegate
|
||||
// composeStatusPollTableViewCell.composeStatusPollExpiresOptionCollectionViewCellDelegate = composeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
//
|
||||
// // setup data source
|
||||
// tableView.dataSource = self
|
||||
// }
|
||||
//
|
||||
// func setupCustomEmojiPickerDiffableDataSource(
|
||||
// for collectionView: UICollectionView,
|
||||
// dependency: NeedsDependency
|
||||
// ) {
|
||||
// let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource(
|
||||
// for: collectionView,
|
||||
// dependency: dependency
|
||||
// )
|
||||
// self.customEmojiPickerDiffableDataSource = diffableDataSource
|
||||
//
|
||||
// let _domain = customEmojiViewModel?.domain
|
||||
// customEmojiViewModel?.emojis
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self, weak diffableDataSource] emojis in
|
||||
// guard let _ = self else { return }
|
||||
// guard let diffableDataSource = diffableDataSource else { return }
|
||||
//
|
||||
// var snapshot = NSDiffableDataSourceSnapshot<CustomEmojiPickerSection, CustomEmojiPickerItem>()
|
||||
// let domain = _domain?.uppercased() ?? " "
|
||||
// let customEmojiSection = CustomEmojiPickerSection.emoji(name: domain)
|
||||
// snapshot.appendSections([customEmojiSection])
|
||||
// let items: [CustomEmojiPickerItem] = {
|
||||
// var items = [CustomEmojiPickerItem]()
|
||||
// for emoji in emojis where emoji.visibleInPicker {
|
||||
// let attribute = CustomEmojiPickerItem.CustomEmojiAttribute(emoji: emoji)
|
||||
// let item = CustomEmojiPickerItem.emoji(attribute: attribute)
|
||||
// items.append(item)
|
||||
// }
|
||||
// return items
|
||||
// }()
|
||||
// snapshot.appendItems(items, toSection: customEmojiSection)
|
||||
//
|
||||
// diffableDataSource.apply(snapshot)
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
extension ComposeViewModel: UITableViewDataSource {
|
||||
//// MARK: - UITableViewDataSource
|
||||
//extension ComposeViewModel: UITableViewDataSource {
|
||||
|
||||
enum Section: CaseIterable {
|
||||
case repliedTo
|
||||
case status
|
||||
case attachment
|
||||
case poll
|
||||
}
|
||||
// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
// switch Section.allCases[indexPath.section] {
|
||||
// case .repliedTo:
|
||||
// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentTableViewCell
|
||||
// guard case let .reply(record) = composeKind else { return cell }
|
||||
//
|
||||
// // bind frame publisher
|
||||
// cell.framePublisher
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .assign(to: \.repliedToCellFrame, on: self)
|
||||
// .store(in: &cell.disposeBag)
|
||||
//
|
||||
// // set initial width
|
||||
// if cell.statusView.frame.width == .zero {
|
||||
// cell.statusView.frame.size.width = tableView.frame.width
|
||||
// }
|
||||
//
|
||||
// // configure status
|
||||
// context.managedObjectContext.performAndWait {
|
||||
// guard let replyTo = record.object(in: context.managedObjectContext) else { return }
|
||||
// cell.statusView.configure(status: replyTo)
|
||||
// }
|
||||
//
|
||||
// return cell
|
||||
// case .status:
|
||||
// return composeStatusContentTableViewCell
|
||||
// case .attachment:
|
||||
// return composeStatusAttachmentTableViewCell
|
||||
// case .poll:
|
||||
// return composeStatusPollTableViewCell
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return Section.allCases.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch Section.allCases[section] {
|
||||
case .repliedTo:
|
||||
switch composeKind {
|
||||
case .reply: return 1
|
||||
default: return 0
|
||||
}
|
||||
case .status: return 1
|
||||
case .attachment: return 1
|
||||
case .poll: return 1
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
switch Section.allCases[indexPath.section] {
|
||||
case .repliedTo:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentTableViewCell
|
||||
guard case let .reply(record) = composeKind else { return cell }
|
||||
|
||||
// bind frame publisher
|
||||
cell.framePublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.repliedToCellFrame, on: self)
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
// set initial width
|
||||
if cell.statusView.frame.width == .zero {
|
||||
cell.statusView.frame.size.width = tableView.frame.width
|
||||
}
|
||||
|
||||
// configure status
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let replyTo = record.object(in: context.managedObjectContext) else { return }
|
||||
cell.statusView.configure(status: replyTo)
|
||||
}
|
||||
|
||||
return cell
|
||||
case .status:
|
||||
return composeStatusContentTableViewCell
|
||||
case .attachment:
|
||||
return composeStatusAttachmentTableViewCell
|
||||
case .poll:
|
||||
return composeStatusPollTableViewCell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ComposeStatusPollTableViewCellDelegate
|
||||
extension ComposeViewModel: ComposeStatusPollTableViewCellDelegate {
|
||||
func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute]) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
self.pollOptionAttributes = options
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeViewModel {
|
||||
private func bind() {
|
||||
$isCustomEmojiComposing
|
||||
.assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
$isContentWarningComposing
|
||||
.assign(to: \.isContentWarningComposing, on: composeStatusAttribute)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind compose toolbar UI state
|
||||
Publishers.CombineLatest(
|
||||
$isPollComposing,
|
||||
$attachmentServices
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] isPollComposing, attachmentServices in
|
||||
guard let self = self else { return }
|
||||
let shouldMediaDisable = isPollComposing || attachmentServices.count >= self.maxMediaAttachments
|
||||
let shouldPollDisable = attachmentServices.count > 0
|
||||
|
||||
self.isMediaToolbarButtonEnabled = !shouldMediaDisable
|
||||
self.isPollToolbarButtonEnabled = !shouldPollDisable
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// calculate `Idempotency-Key`
|
||||
let content = Publishers.CombineLatest3(
|
||||
composeStatusAttribute.$isContentWarningComposing,
|
||||
composeStatusAttribute.$contentWarningContent,
|
||||
composeStatusAttribute.$composeContent
|
||||
)
|
||||
.map { isContentWarningComposing, contentWarningContent, composeContent -> String in
|
||||
if isContentWarningComposing {
|
||||
return contentWarningContent + (composeContent ?? "")
|
||||
} else {
|
||||
return composeContent ?? ""
|
||||
}
|
||||
}
|
||||
let attachmentIDs = $attachmentServices.map { attachments -> String in
|
||||
let attachmentIDs = attachments.compactMap { $0.attachment.value?.id }
|
||||
return attachmentIDs.joined(separator: ",")
|
||||
}
|
||||
let pollOptionsAndDuration = Publishers.CombineLatest3(
|
||||
$isPollComposing,
|
||||
$pollOptionAttributes,
|
||||
pollExpiresOptionAttribute.expiresOption
|
||||
)
|
||||
.map { isPollComposing, pollOptionAttributes, expiresOption -> String in
|
||||
guard isPollComposing else {
|
||||
return ""
|
||||
}
|
||||
|
||||
let pollOptions = pollOptionAttributes.map { $0.option.value }.joined(separator: ",")
|
||||
return pollOptions + expiresOption.rawValue
|
||||
}
|
||||
|
||||
Publishers.CombineLatest4(
|
||||
content,
|
||||
attachmentIDs,
|
||||
pollOptionsAndDuration,
|
||||
$selectedStatusVisibility
|
||||
)
|
||||
.map { content, attachmentIDs, pollOptionsAndDuration, selectedStatusVisibility -> String in
|
||||
var hasher = Hasher()
|
||||
hasher.combine(content)
|
||||
hasher.combine(attachmentIDs)
|
||||
hasher.combine(pollOptionsAndDuration)
|
||||
hasher.combine(selectedStatusVisibility.visibility.rawValue)
|
||||
let hashValue = hasher.finalize()
|
||||
return "\(hashValue)"
|
||||
}
|
||||
.assign(to: \.value, on: idempotencyKey)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind modal dismiss state
|
||||
composeStatusAttribute.$composeContent
|
||||
.receive(on: DispatchQueue.main)
|
||||
.map { [weak self] content in
|
||||
let content = content ?? ""
|
||||
if content.isEmpty {
|
||||
return true
|
||||
}
|
||||
// if preInsertedContent plus a space is equal to the content, simply dismiss the modal
|
||||
if let preInsertedContent = self?.preInsertedContent {
|
||||
return content == preInsertedContent
|
||||
}
|
||||
return false
|
||||
}
|
||||
.assign(to: &$shouldDismiss)
|
||||
|
||||
// bind compose bar button item UI state
|
||||
let isComposeContentEmpty = composeStatusAttribute.$composeContent
|
||||
.map { ($0 ?? "").isEmpty }
|
||||
let isComposeContentValid = $characterCount
|
||||
.compactMap { [weak self] characterCount -> Bool in
|
||||
guard let self = self else { return characterCount <= 500 }
|
||||
return characterCount <= self.composeContentLimit
|
||||
}
|
||||
let isMediaEmpty = $attachmentServices
|
||||
.map { $0.isEmpty }
|
||||
let isMediaUploadAllSuccess = $attachmentServices
|
||||
.map { services in
|
||||
services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish }
|
||||
}
|
||||
let isPollAttributeAllValid = $pollOptionAttributes
|
||||
.map { pollAttributes in
|
||||
pollAttributes.allSatisfy { attribute -> Bool in
|
||||
!attribute.option.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4(
|
||||
isComposeContentEmpty,
|
||||
isComposeContentValid,
|
||||
isMediaEmpty,
|
||||
isMediaUploadAllSuccess
|
||||
)
|
||||
.map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in
|
||||
if isMediaEmpty {
|
||||
return isComposeContentValid && !isComposeContentEmpty
|
||||
} else {
|
||||
return isComposeContentValid && isMediaUploadAllSuccess
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4(
|
||||
isComposeContentEmpty,
|
||||
isComposeContentValid,
|
||||
$isPollComposing,
|
||||
isPollAttributeAllValid
|
||||
)
|
||||
.map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollAttributeAllValid -> Bool in
|
||||
if isPollComposing {
|
||||
return isComposeContentValid && !isComposeContentEmpty && isPollAttributeAllValid
|
||||
} else {
|
||||
return isComposeContentValid && !isComposeContentEmpty
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
Publishers.CombineLatest(
|
||||
isPublishBarButtonItemEnabledPrecondition1,
|
||||
isPublishBarButtonItemEnabledPrecondition2
|
||||
)
|
||||
.map { $0 && $1 }
|
||||
.assign(to: &$isPublishBarButtonItemEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeViewModel {
|
||||
private func bind(
|
||||
cell: ComposeStatusContentTableViewCell,
|
||||
tableView: UITableView
|
||||
) {
|
||||
// bind status content character count
|
||||
Publishers.CombineLatest3(
|
||||
composeStatusAttribute.$composeContent,
|
||||
composeStatusAttribute.$isContentWarningComposing,
|
||||
composeStatusAttribute.$contentWarningContent
|
||||
)
|
||||
.map { composeContent, isContentWarningComposing, contentWarningContent -> Int in
|
||||
let composeContent = composeContent ?? ""
|
||||
var count = composeContent.count
|
||||
if isContentWarningComposing {
|
||||
count += contentWarningContent.count
|
||||
}
|
||||
return count
|
||||
}
|
||||
.assign(to: &$characterCount)
|
||||
|
||||
// bind content warning
|
||||
composeStatusAttribute.$isContentWarningComposing
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell, weak tableView] isContentWarningComposing in
|
||||
guard let cell = cell else { return }
|
||||
guard let tableView = tableView else { return }
|
||||
|
||||
// self size input cell
|
||||
cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing
|
||||
cell.statusContentWarningEditorView.alpha = 0
|
||||
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) {
|
||||
cell.statusContentWarningEditorView.alpha = 1
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
} completion: { _ in
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
cell.contentWarningContent
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak tableView, weak self] text in
|
||||
guard let self = self else { return }
|
||||
// bind input data
|
||||
self.composeStatusAttribute.contentWarningContent = text
|
||||
|
||||
// self size input cell
|
||||
guard let tableView = tableView else { return }
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
// configure custom emoji picker
|
||||
ComposeStatusSection.configureCustomEmojiPicker(
|
||||
viewModel: customEmojiPickerInputViewModel,
|
||||
customEmojiReplaceableTextInput: cell.metaText.textView,
|
||||
disposeBag: &disposeBag
|
||||
)
|
||||
ComposeStatusSection.configureCustomEmojiPicker(
|
||||
viewModel: customEmojiPickerInputViewModel,
|
||||
customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView,
|
||||
disposeBag: &disposeBag
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeViewModel {
|
||||
private func bind(
|
||||
cell: ComposeStatusPollTableViewCell,
|
||||
tableView: UITableView
|
||||
) {
|
||||
Publishers.CombineLatest(
|
||||
$isPollComposing,
|
||||
$pollOptionAttributes
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isPollComposing, pollOptionAttributes in
|
||||
guard let self = self else { return }
|
||||
guard self.isViewAppeared else { return }
|
||||
|
||||
let cell = self.composeStatusPollTableViewCell
|
||||
guard let dataSource = cell.dataSource else { return }
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusPollSection, ComposeStatusPollItem>()
|
||||
snapshot.appendSections([.main])
|
||||
var items: [ComposeStatusPollItem] = []
|
||||
if isPollComposing {
|
||||
for attribute in pollOptionAttributes {
|
||||
items.append(.pollOption(attribute: attribute))
|
||||
}
|
||||
if pollOptionAttributes.count < self.maxPollOptions {
|
||||
items.append(.pollOptionAppendEntry)
|
||||
}
|
||||
items.append(.pollExpiresOption(attribute: self.pollExpiresOptionAttribute))
|
||||
}
|
||||
snapshot.appendItems(items, toSection: .main)
|
||||
|
||||
tableView.performBatchUpdates {
|
||||
if #available(iOS 15.0, *) {
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
} else {
|
||||
dataSource.apply(snapshot, animatingDifferences: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind delegate
|
||||
$pollOptionAttributes
|
||||
.sink { [weak self] pollAttributes in
|
||||
guard let self = self else { return }
|
||||
pollAttributes.forEach { $0.delegate = self }
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeViewModel {
|
||||
private func bind(
|
||||
cell: ComposeStatusAttachmentTableViewCell,
|
||||
tableView: UITableView
|
||||
) {
|
||||
cell.collectionViewHeightDidUpdate
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let _ = self else { return }
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
$attachmentServices
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] attachmentServices in
|
||||
guard let self = self else { return }
|
||||
guard self.isViewAppeared else { return }
|
||||
|
||||
let cell = self.composeStatusAttachmentTableViewCell
|
||||
guard let dataSource = cell.dataSource else { return }
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<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)
|
||||
}
|
||||
}
|
||||
//// MARK: - ComposeStatusPollTableViewCellDelegate
|
||||
//extension ComposeViewModel: ComposeStatusPollTableViewCellDelegate {
|
||||
// func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute]) {
|
||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
//
|
||||
// self.pollOptionAttributes = options
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension ComposeViewModel {
|
||||
// private func bind() {
|
||||
// $isCustomEmojiComposing
|
||||
// .assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing)
|
||||
// .store(in: &disposeBag)
|
||||
//
|
||||
// $isContentWarningComposing
|
||||
// .assign(to: \.isContentWarningComposing, on: composeStatusAttribute)
|
||||
// .store(in: &disposeBag)
|
||||
//
|
||||
// // bind compose toolbar UI state
|
||||
// Publishers.CombineLatest(
|
||||
// $isPollComposing,
|
||||
// $attachmentServices
|
||||
// )
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink(receiveValue: { [weak self] isPollComposing, attachmentServices in
|
||||
// guard let self = self else { return }
|
||||
// let shouldMediaDisable = isPollComposing || attachmentServices.count >= self.maxMediaAttachments
|
||||
// let shouldPollDisable = attachmentServices.count > 0
|
||||
//
|
||||
// self.isMediaToolbarButtonEnabled = !shouldMediaDisable
|
||||
// self.isPollToolbarButtonEnabled = !shouldPollDisable
|
||||
// })
|
||||
// .store(in: &disposeBag)
|
||||
//
|
||||
// // calculate `Idempotency-Key`
|
||||
// let content = Publishers.CombineLatest3(
|
||||
// composeStatusAttribute.$isContentWarningComposing,
|
||||
// composeStatusAttribute.$contentWarningContent,
|
||||
// composeStatusAttribute.$composeContent
|
||||
// )
|
||||
// .map { isContentWarningComposing, contentWarningContent, composeContent -> String in
|
||||
// if isContentWarningComposing {
|
||||
// return contentWarningContent + (composeContent ?? "")
|
||||
// } else {
|
||||
// return composeContent ?? ""
|
||||
// }
|
||||
// }
|
||||
// let attachmentIDs = $attachmentServices.map { attachments -> String in
|
||||
// let attachmentIDs = attachments.compactMap { $0.attachment.value?.id }
|
||||
// return attachmentIDs.joined(separator: ",")
|
||||
// }
|
||||
// let pollOptionsAndDuration = Publishers.CombineLatest3(
|
||||
// $isPollComposing,
|
||||
// $pollOptionAttributes,
|
||||
// pollExpiresOptionAttribute.expiresOption
|
||||
// )
|
||||
// .map { isPollComposing, pollOptionAttributes, expiresOption -> String in
|
||||
// guard isPollComposing else {
|
||||
// return ""
|
||||
// }
|
||||
//
|
||||
// let pollOptions = pollOptionAttributes.map { $0.option.value }.joined(separator: ",")
|
||||
// return pollOptions + expiresOption.rawValue
|
||||
// }
|
||||
//
|
||||
// Publishers.CombineLatest4(
|
||||
// content,
|
||||
// attachmentIDs,
|
||||
// pollOptionsAndDuration,
|
||||
// $selectedStatusVisibility
|
||||
// )
|
||||
// .map { content, attachmentIDs, pollOptionsAndDuration, selectedStatusVisibility -> String in
|
||||
// var hasher = Hasher()
|
||||
// hasher.combine(content)
|
||||
// hasher.combine(attachmentIDs)
|
||||
// hasher.combine(pollOptionsAndDuration)
|
||||
// hasher.combine(selectedStatusVisibility.visibility.rawValue)
|
||||
// let hashValue = hasher.finalize()
|
||||
// return "\(hashValue)"
|
||||
// }
|
||||
// .assign(to: \.value, on: idempotencyKey)
|
||||
// .store(in: &disposeBag)
|
||||
//
|
||||
// // bind modal dismiss state
|
||||
// composeStatusAttribute.$composeContent
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .map { [weak self] content in
|
||||
// let content = content ?? ""
|
||||
// if content.isEmpty {
|
||||
// return true
|
||||
// }
|
||||
// // if preInsertedContent plus a space is equal to the content, simply dismiss the modal
|
||||
// if let preInsertedContent = self?.preInsertedContent {
|
||||
// return content == preInsertedContent
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
// .assign(to: &$shouldDismiss)
|
||||
//
|
||||
// // bind compose bar button item UI state
|
||||
// let isComposeContentEmpty = composeStatusAttribute.$composeContent
|
||||
// .map { ($0 ?? "").isEmpty }
|
||||
// let isComposeContentValid = $characterCount
|
||||
// .compactMap { [weak self] characterCount -> Bool in
|
||||
// guard let self = self else { return characterCount <= 500 }
|
||||
// return characterCount <= self.composeContentLimit
|
||||
// }
|
||||
// let isMediaEmpty = $attachmentServices
|
||||
// .map { $0.isEmpty }
|
||||
// let isMediaUploadAllSuccess = $attachmentServices
|
||||
// .map { services in
|
||||
// services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish }
|
||||
// }
|
||||
// let isPollAttributeAllValid = $pollOptionAttributes
|
||||
// .map { pollAttributes in
|
||||
// pollAttributes.allSatisfy { attribute -> Bool in
|
||||
// !attribute.option.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4(
|
||||
// isComposeContentEmpty,
|
||||
// isComposeContentValid,
|
||||
// isMediaEmpty,
|
||||
// isMediaUploadAllSuccess
|
||||
// )
|
||||
// .map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in
|
||||
// if isMediaEmpty {
|
||||
// return isComposeContentValid && !isComposeContentEmpty
|
||||
// } else {
|
||||
// return isComposeContentValid && isMediaUploadAllSuccess
|
||||
// }
|
||||
// }
|
||||
// .eraseToAnyPublisher()
|
||||
//
|
||||
// let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4(
|
||||
// isComposeContentEmpty,
|
||||
// isComposeContentValid,
|
||||
// $isPollComposing,
|
||||
// isPollAttributeAllValid
|
||||
// )
|
||||
// .map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollAttributeAllValid -> Bool in
|
||||
// if isPollComposing {
|
||||
// return isComposeContentValid && !isComposeContentEmpty && isPollAttributeAllValid
|
||||
// } else {
|
||||
// return isComposeContentValid && !isComposeContentEmpty
|
||||
// }
|
||||
// }
|
||||
// .eraseToAnyPublisher()
|
||||
//
|
||||
// Publishers.CombineLatest(
|
||||
// isPublishBarButtonItemEnabledPrecondition1,
|
||||
// isPublishBarButtonItemEnabledPrecondition2
|
||||
// )
|
||||
// .map { $0 && $1 }
|
||||
// .assign(to: &$isPublishBarButtonItemEnabled)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension ComposeViewModel {
|
||||
// private func bind(
|
||||
// cell: ComposeStatusContentTableViewCell,
|
||||
// tableView: UITableView
|
||||
// ) {
|
||||
// // bind status content character count
|
||||
// Publishers.CombineLatest3(
|
||||
// composeStatusAttribute.$composeContent,
|
||||
// composeStatusAttribute.$isContentWarningComposing,
|
||||
// composeStatusAttribute.$contentWarningContent
|
||||
// )
|
||||
// .map { composeContent, isContentWarningComposing, contentWarningContent -> Int in
|
||||
// let composeContent = composeContent ?? ""
|
||||
// var count = composeContent.count
|
||||
// if isContentWarningComposing {
|
||||
// count += contentWarningContent.count
|
||||
// }
|
||||
// return count
|
||||
// }
|
||||
// .assign(to: &$characterCount)
|
||||
//
|
||||
// // bind content warning
|
||||
// composeStatusAttribute.$isContentWarningComposing
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak cell, weak tableView] isContentWarningComposing in
|
||||
// guard let cell = cell else { return }
|
||||
// guard let tableView = tableView else { return }
|
||||
//
|
||||
// // self size input cell
|
||||
// cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing
|
||||
// cell.statusContentWarningEditorView.alpha = 0
|
||||
// UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) {
|
||||
// cell.statusContentWarningEditorView.alpha = 1
|
||||
// tableView.beginUpdates()
|
||||
// tableView.endUpdates()
|
||||
// } completion: { _ in
|
||||
// // do nothing
|
||||
// }
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
//
|
||||
// cell.contentWarningContent
|
||||
// .removeDuplicates()
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak tableView, weak self] text in
|
||||
// guard let self = self else { return }
|
||||
// // bind input data
|
||||
// self.composeStatusAttribute.contentWarningContent = text
|
||||
//
|
||||
// // self size input cell
|
||||
// guard let tableView = tableView else { return }
|
||||
// UIView.performWithoutAnimation {
|
||||
// tableView.beginUpdates()
|
||||
// tableView.endUpdates()
|
||||
// }
|
||||
// }
|
||||
// .store(in: &cell.disposeBag)
|
||||
//
|
||||
// // configure custom emoji picker
|
||||
// ComposeStatusSection.configureCustomEmojiPicker(
|
||||
// viewModel: customEmojiPickerInputViewModel,
|
||||
// customEmojiReplaceableTextInput: cell.metaText.textView,
|
||||
// disposeBag: &disposeBag
|
||||
// )
|
||||
// ComposeStatusSection.configureCustomEmojiPicker(
|
||||
// viewModel: customEmojiPickerInputViewModel,
|
||||
// customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView,
|
||||
// disposeBag: &disposeBag
|
||||
// )
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension ComposeViewModel {
|
||||
// private func bind(
|
||||
// cell: ComposeStatusPollTableViewCell,
|
||||
// tableView: UITableView
|
||||
// ) {
|
||||
// Publishers.CombineLatest(
|
||||
// $isPollComposing,
|
||||
// $pollOptionAttributes
|
||||
// )
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] isPollComposing, pollOptionAttributes in
|
||||
// guard let self = self else { return }
|
||||
// guard self.isViewAppeared else { return }
|
||||
//
|
||||
// let cell = self.composeStatusPollTableViewCell
|
||||
// guard let dataSource = cell.dataSource else { return }
|
||||
//
|
||||
// var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusPollSection, ComposeStatusPollItem>()
|
||||
// snapshot.appendSections([.main])
|
||||
// var items: [ComposeStatusPollItem] = []
|
||||
// if isPollComposing {
|
||||
// for attribute in pollOptionAttributes {
|
||||
// items.append(.pollOption(attribute: attribute))
|
||||
// }
|
||||
// if pollOptionAttributes.count < self.maxPollOptions {
|
||||
// items.append(.pollOptionAppendEntry)
|
||||
// }
|
||||
// items.append(.pollExpiresOption(attribute: self.pollExpiresOptionAttribute))
|
||||
// }
|
||||
// snapshot.appendItems(items, toSection: .main)
|
||||
//
|
||||
// tableView.performBatchUpdates {
|
||||
// if #available(iOS 15.0, *) {
|
||||
// dataSource.apply(snapshot, animatingDifferences: false)
|
||||
// } else {
|
||||
// dataSource.apply(snapshot, animatingDifferences: true)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
//
|
||||
// // bind delegate
|
||||
// $pollOptionAttributes
|
||||
// .sink { [weak self] pollAttributes in
|
||||
// guard let self = self else { return }
|
||||
// pollAttributes.forEach { $0.delegate = self }
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension ComposeViewModel {
|
||||
// private func bind(
|
||||
// cell: ComposeStatusAttachmentTableViewCell,
|
||||
// tableView: UITableView
|
||||
// ) {
|
||||
// cell.collectionViewHeightDidUpdate
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] _ in
|
||||
// guard let _ = self else { return }
|
||||
// tableView.beginUpdates()
|
||||
// tableView.endUpdates()
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
//
|
||||
// $attachmentServices
|
||||
// .removeDuplicates()
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] attachmentServices in
|
||||
// guard let self = self else { return }
|
||||
// guard self.isViewAppeared else { return }
|
||||
//
|
||||
// let cell = self.composeStatusAttachmentTableViewCell
|
||||
// guard let dataSource = cell.dataSource else { return }
|
||||
//
|
||||
// var snapshot = NSDiffableDataSourceSnapshot<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)
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -12,153 +12,153 @@ import CoreDataStack
|
|||
import GameplayKit
|
||||
import MastodonSDK
|
||||
|
||||
extension ComposeViewModel {
|
||||
class PublishState: GKState {
|
||||
weak var viewModel: ComposeViewModel?
|
||||
//extension ComposeViewModel {
|
||||
// class PublishState: GKState {
|
||||
// weak var viewModel: ComposeViewModel?
|
||||
//
|
||||
// init(viewModel: ComposeViewModel) {
|
||||
// self.viewModel = viewModel
|
||||
// }
|
||||
//
|
||||
// override func didEnter(from previousState: GKState?) {
|
||||
// os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
|
||||
// viewModel?.publishStateMachinePublisher.value = self
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
init(viewModel: ComposeViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
|
||||
viewModel?.publishStateMachinePublisher.value = self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeViewModel.PublishState {
|
||||
class Initial: ComposeViewModel.PublishState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
return stateClass == Publishing.self
|
||||
}
|
||||
}
|
||||
|
||||
class Publishing: ComposeViewModel.PublishState {
|
||||
|
||||
var publishingSubscription: AnyCancellable?
|
||||
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
return stateClass == Fail.self || stateClass == Finish.self
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||
|
||||
viewModel.updatePublishDate()
|
||||
|
||||
let authenticationBox = viewModel.authenticationBox
|
||||
let domain = authenticationBox.domain
|
||||
let attachmentServices = viewModel.attachmentServices
|
||||
let mediaIDs = attachmentServices.compactMap { attachmentService in
|
||||
attachmentService.attachment.value?.id
|
||||
}
|
||||
let pollOptions: [String]? = {
|
||||
guard viewModel.isPollComposing else { return nil }
|
||||
return viewModel.pollOptionAttributes.map { attribute in attribute.option.value }
|
||||
}()
|
||||
let pollExpiresIn: Int? = {
|
||||
guard viewModel.isPollComposing else { return nil }
|
||||
return viewModel.pollExpiresOptionAttribute.expiresOption.value.seconds
|
||||
}()
|
||||
let inReplyToID: Mastodon.Entity.Status.ID? = {
|
||||
guard case let .reply(status) = viewModel.composeKind else { return nil }
|
||||
var id: Mastodon.Entity.Status.ID?
|
||||
viewModel.context.managedObjectContext.performAndWait {
|
||||
guard let replyTo = status.object(in: viewModel.context.managedObjectContext) else { return }
|
||||
id = replyTo.id
|
||||
}
|
||||
return id
|
||||
}()
|
||||
let sensitive: Bool = viewModel.isContentWarningComposing
|
||||
let spoilerText: String? = {
|
||||
let text = viewModel.composeStatusAttribute.contentWarningContent.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !text.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return text
|
||||
}()
|
||||
let visibility = viewModel.selectedStatusVisibility.visibility
|
||||
|
||||
let updateMediaQuerySubscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = {
|
||||
var subscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = []
|
||||
for attachmentService in attachmentServices {
|
||||
guard let attachmentID = attachmentService.attachment.value?.id else { continue }
|
||||
let description = attachmentService.description.value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard !description.isEmpty else { continue }
|
||||
let query = Mastodon.API.Media.UpdateMediaQuery(
|
||||
file: nil,
|
||||
thumbnail: nil,
|
||||
description: description,
|
||||
focus: nil
|
||||
)
|
||||
let subscription = viewModel.context.apiService.updateMedia(
|
||||
domain: domain,
|
||||
attachmentID: attachmentID,
|
||||
query: query,
|
||||
mastodonAuthenticationBox: authenticationBox
|
||||
)
|
||||
subscriptions.append(subscription)
|
||||
}
|
||||
return subscriptions
|
||||
}()
|
||||
|
||||
let idempotencyKey = viewModel.idempotencyKey.value
|
||||
|
||||
publishingSubscription = Publishers.MergeMany(updateMediaQuerySubscriptions)
|
||||
.collect()
|
||||
.asyncMap { attachments -> Mastodon.Response.Content<Mastodon.Entity.Status> in
|
||||
let query = Mastodon.API.Statuses.PublishStatusQuery(
|
||||
status: viewModel.composeStatusAttribute.composeContent,
|
||||
mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs,
|
||||
pollOptions: pollOptions,
|
||||
pollExpiresIn: pollExpiresIn,
|
||||
inReplyToID: inReplyToID,
|
||||
sensitive: sensitive,
|
||||
spoilerText: spoilerText,
|
||||
visibility: visibility
|
||||
)
|
||||
return try await viewModel.context.apiService.publishStatus(
|
||||
domain: domain,
|
||||
idempotencyKey: idempotencyKey,
|
||||
query: query,
|
||||
authenticationBox: authenticationBox
|
||||
)
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
stateMachine.enter(Fail.self)
|
||||
case .finished:
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
stateMachine.enter(Finish.self)
|
||||
}
|
||||
} receiveValue: { response in
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: status %s published: %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.id, response.value.uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Fail: ComposeViewModel.PublishState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
// allow discard publishing
|
||||
return stateClass == Publishing.self || stateClass == Discard.self
|
||||
}
|
||||
}
|
||||
|
||||
class Discard: ComposeViewModel.PublishState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class Finish: ComposeViewModel.PublishState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
//extension ComposeViewModel.PublishState {
|
||||
// class Initial: ComposeViewModel.PublishState {
|
||||
// override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
// return stateClass == Publishing.self
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// class Publishing: ComposeViewModel.PublishState {
|
||||
//
|
||||
// var publishingSubscription: AnyCancellable?
|
||||
//
|
||||
// override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
// return stateClass == Fail.self || stateClass == Finish.self
|
||||
// }
|
||||
//
|
||||
// override func didEnter(from previousState: GKState?) {
|
||||
// super.didEnter(from: previousState)
|
||||
// guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||
//
|
||||
// viewModel.updatePublishDate()
|
||||
//
|
||||
// let authenticationBox = viewModel.authenticationBox
|
||||
// let domain = authenticationBox.domain
|
||||
// let attachmentServices = viewModel.attachmentServices
|
||||
// let mediaIDs = attachmentServices.compactMap { attachmentService in
|
||||
// attachmentService.attachment.value?.id
|
||||
// }
|
||||
// let pollOptions: [String]? = {
|
||||
// guard viewModel.isPollComposing else { return nil }
|
||||
// return viewModel.pollOptionAttributes.map { attribute in attribute.option.value }
|
||||
// }()
|
||||
// let pollExpiresIn: Int? = {
|
||||
// guard viewModel.isPollComposing else { return nil }
|
||||
// return viewModel.pollExpiresOptionAttribute.expiresOption.value.seconds
|
||||
// }()
|
||||
// let inReplyToID: Mastodon.Entity.Status.ID? = {
|
||||
// guard case let .reply(status) = viewModel.composeKind else { return nil }
|
||||
// var id: Mastodon.Entity.Status.ID?
|
||||
// viewModel.context.managedObjectContext.performAndWait {
|
||||
// guard let replyTo = status.object(in: viewModel.context.managedObjectContext) else { return }
|
||||
// id = replyTo.id
|
||||
// }
|
||||
// return id
|
||||
// }()
|
||||
// let sensitive: Bool = viewModel.isContentWarningComposing
|
||||
// let spoilerText: String? = {
|
||||
// let text = viewModel.composeStatusAttribute.contentWarningContent.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
// guard !text.isEmpty else {
|
||||
// return nil
|
||||
// }
|
||||
// return text
|
||||
// }()
|
||||
// let visibility = viewModel.selectedStatusVisibility.visibility
|
||||
//
|
||||
// let updateMediaQuerySubscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = {
|
||||
// var subscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = []
|
||||
// for attachmentService in attachmentServices {
|
||||
// guard let attachmentID = attachmentService.attachment.value?.id else { continue }
|
||||
// let description = attachmentService.description.value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
// guard !description.isEmpty else { continue }
|
||||
// let query = Mastodon.API.Media.UpdateMediaQuery(
|
||||
// file: nil,
|
||||
// thumbnail: nil,
|
||||
// description: description,
|
||||
// focus: nil
|
||||
// )
|
||||
// let subscription = viewModel.context.apiService.updateMedia(
|
||||
// domain: domain,
|
||||
// attachmentID: attachmentID,
|
||||
// query: query,
|
||||
// mastodonAuthenticationBox: authenticationBox
|
||||
// )
|
||||
// subscriptions.append(subscription)
|
||||
// }
|
||||
// return subscriptions
|
||||
// }()
|
||||
//
|
||||
// let idempotencyKey = viewModel.idempotencyKey.value
|
||||
//
|
||||
// publishingSubscription = Publishers.MergeMany(updateMediaQuerySubscriptions)
|
||||
// .collect()
|
||||
// .asyncMap { attachments -> Mastodon.Response.Content<Mastodon.Entity.Status> in
|
||||
// let query = Mastodon.API.Statuses.PublishStatusQuery(
|
||||
// status: viewModel.composeStatusAttribute.composeContent,
|
||||
// mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs,
|
||||
// pollOptions: pollOptions,
|
||||
// pollExpiresIn: pollExpiresIn,
|
||||
// inReplyToID: inReplyToID,
|
||||
// sensitive: sensitive,
|
||||
// spoilerText: spoilerText,
|
||||
// visibility: visibility
|
||||
// )
|
||||
// return try await viewModel.context.apiService.publishStatus(
|
||||
// domain: domain,
|
||||
// idempotencyKey: idempotencyKey,
|
||||
// query: query,
|
||||
// authenticationBox: authenticationBox
|
||||
// )
|
||||
// }
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { completion in
|
||||
// switch completion {
|
||||
// case .failure(let error):
|
||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
// stateMachine.enter(Fail.self)
|
||||
// case .finished:
|
||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
// stateMachine.enter(Finish.self)
|
||||
// }
|
||||
// } receiveValue: { response in
|
||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: status %s published: %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.id, response.value.uri)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// class Fail: ComposeViewModel.PublishState {
|
||||
// override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
// // allow discard publishing
|
||||
// return stateClass == Publishing.self || stateClass == Discard.self
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// class Discard: ComposeViewModel.PublishState {
|
||||
// override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// class Finish: ComposeViewModel.PublishState {
|
||||
// override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
|
|
@ -13,6 +13,7 @@ import CoreDataStack
|
|||
import GameplayKit
|
||||
import MastodonSDK
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import MastodonMeta
|
||||
import MastodonUI
|
||||
|
@ -27,159 +28,159 @@ final class ComposeViewModel: NSObject {
|
|||
|
||||
// input
|
||||
let context: AppContext
|
||||
let composeKind: ComposeStatusSection.ComposeKind
|
||||
let authenticationBox: MastodonAuthenticationBox
|
||||
let authContext: AuthContext
|
||||
let kind: ComposeContentViewModel.Kind
|
||||
|
||||
|
||||
@Published var isPollComposing = false
|
||||
@Published var isCustomEmojiComposing = false
|
||||
@Published var isContentWarningComposing = false
|
||||
|
||||
@Published var selectedStatusVisibility: ComposeToolbarView.VisibilitySelectionType
|
||||
@Published var repliedToCellFrame: CGRect = .zero
|
||||
@Published var autoCompleteRetryLayoutTimes = 0
|
||||
@Published var autoCompleteInfo: ComposeViewController.AutoCompleteInfo? = nil
|
||||
// var authenticationBox: MastodonAuthenticationBox {
|
||||
// authContext.mastodonAuthenticationBox
|
||||
// }
|
||||
//
|
||||
// @Published var isPollComposing = false
|
||||
// @Published var isCustomEmojiComposing = false
|
||||
// @Published var isContentWarningComposing = false
|
||||
//
|
||||
// @Published var selectedStatusVisibility: ComposeToolbarView.VisibilitySelectionType
|
||||
// @Published var repliedToCellFrame: CGRect = .zero
|
||||
// @Published var autoCompleteRetryLayoutTimes = 0
|
||||
// @Published var autoCompleteInfo: ComposeViewController.AutoCompleteInfo? = nil
|
||||
|
||||
let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make initial event emit
|
||||
var isViewAppeared = false
|
||||
// var isViewAppeared = false
|
||||
|
||||
// output
|
||||
let instanceConfiguration: Mastodon.Entity.Instance.Configuration?
|
||||
var composeContentLimit: Int {
|
||||
guard let maxCharacters = instanceConfiguration?.statuses?.maxCharacters else { return 500 }
|
||||
return max(1, maxCharacters)
|
||||
}
|
||||
var maxMediaAttachments: Int {
|
||||
guard let maxMediaAttachments = instanceConfiguration?.statuses?.maxMediaAttachments else {
|
||||
return 4
|
||||
}
|
||||
// FIXME: update timeline media preview UI
|
||||
return min(4, max(1, maxMediaAttachments))
|
||||
// return max(1, maxMediaAttachments)
|
||||
}
|
||||
var maxPollOptions: Int {
|
||||
guard let maxOptions = instanceConfiguration?.polls?.maxOptions else { return 4 }
|
||||
return max(2, maxOptions)
|
||||
}
|
||||
|
||||
let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
|
||||
let composeStatusContentTableViewCell = ComposeStatusContentTableViewCell()
|
||||
let composeStatusAttachmentTableViewCell = ComposeStatusAttachmentTableViewCell()
|
||||
let composeStatusPollTableViewCell = ComposeStatusPollTableViewCell()
|
||||
|
||||
// var dataSource: UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>?
|
||||
var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>?
|
||||
private(set) lazy var publishStateMachine: GKStateMachine = {
|
||||
// exclude timeline middle fetcher state
|
||||
let stateMachine = GKStateMachine(states: [
|
||||
PublishState.Initial(viewModel: self),
|
||||
PublishState.Publishing(viewModel: self),
|
||||
PublishState.Fail(viewModel: self),
|
||||
PublishState.Discard(viewModel: self),
|
||||
PublishState.Finish(viewModel: self),
|
||||
])
|
||||
stateMachine.enter(PublishState.Initial.self)
|
||||
return stateMachine
|
||||
}()
|
||||
private(set) lazy var publishStateMachinePublisher = CurrentValueSubject<PublishState?, Never>(nil)
|
||||
private(set) var publishDate = Date() // update it when enter Publishing state
|
||||
|
||||
// TODO: group post material into Hashable class
|
||||
var idempotencyKey = CurrentValueSubject<String, Never>(UUID().uuidString)
|
||||
|
||||
// UI & UX
|
||||
@Published var title: String
|
||||
@Published var shouldDismiss = true
|
||||
@Published var isPublishBarButtonItemEnabled = false
|
||||
@Published var isMediaToolbarButtonEnabled = true
|
||||
@Published var isPollToolbarButtonEnabled = true
|
||||
@Published var characterCount = 0
|
||||
@Published var collectionViewState: CollectionViewState = .fold
|
||||
|
||||
// for hashtag: "#<hashtag> "
|
||||
// for mention: "@<mention> "
|
||||
var preInsertedContent: String?
|
||||
|
||||
// custom emojis
|
||||
let customEmojiViewModel: EmojiService.CustomEmojiViewModel?
|
||||
let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel()
|
||||
@Published var isLoadingCustomEmoji = false
|
||||
|
||||
// attachment
|
||||
@Published var attachmentServices: [MastodonAttachmentService] = []
|
||||
|
||||
// polls
|
||||
@Published var pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute] = []
|
||||
let pollExpiresOptionAttribute = ComposeStatusPollItem.PollExpiresOptionAttribute()
|
||||
// let instanceConfiguration: Mastodon.Entity.Instance.Configuration?
|
||||
// var composeContentLimit: Int {
|
||||
// guard let maxCharacters = instanceConfiguration?.statuses?.maxCharacters else { return 500 }
|
||||
// return max(1, maxCharacters)
|
||||
// }
|
||||
// var maxMediaAttachments: Int {
|
||||
// guard let maxMediaAttachments = instanceConfiguration?.statuses?.maxMediaAttachments else {
|
||||
// return 4
|
||||
// }
|
||||
// // FIXME: update timeline media preview UI
|
||||
// return min(4, max(1, maxMediaAttachments))
|
||||
// // return max(1, maxMediaAttachments)
|
||||
// }
|
||||
// var maxPollOptions: Int {
|
||||
// guard let maxOptions = instanceConfiguration?.polls?.maxOptions else { return 4 }
|
||||
// return max(2, maxOptions)
|
||||
// }
|
||||
//
|
||||
// let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
|
||||
// let composeStatusContentTableViewCell = ComposeStatusContentTableViewCell()
|
||||
// let composeStatusAttachmentTableViewCell = ComposeStatusAttachmentTableViewCell()
|
||||
// let composeStatusPollTableViewCell = ComposeStatusPollTableViewCell()
|
||||
//
|
||||
// // var dataSource: UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>?
|
||||
// var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>?
|
||||
// private(set) lazy var publishStateMachine: GKStateMachine = {
|
||||
// // exclude timeline middle fetcher state
|
||||
// let stateMachine = GKStateMachine(states: [
|
||||
// PublishState.Initial(viewModel: self),
|
||||
// PublishState.Publishing(viewModel: self),
|
||||
// PublishState.Fail(viewModel: self),
|
||||
// PublishState.Discard(viewModel: self),
|
||||
// PublishState.Finish(viewModel: self),
|
||||
// ])
|
||||
// stateMachine.enter(PublishState.Initial.self)
|
||||
// return stateMachine
|
||||
// }()
|
||||
// private(set) lazy var publishStateMachinePublisher = CurrentValueSubject<PublishState?, Never>(nil)
|
||||
// private(set) var publishDate = Date() // update it when enter Publishing state
|
||||
//
|
||||
// // TODO: group post material into Hashable class
|
||||
// var idempotencyKey = CurrentValueSubject<String, Never>(UUID().uuidString)
|
||||
//
|
||||
// // UI & UX
|
||||
// @Published var title: String
|
||||
// @Published var shouldDismiss = true
|
||||
// @Published var isPublishBarButtonItemEnabled = false
|
||||
// @Published var isMediaToolbarButtonEnabled = true
|
||||
// @Published var isPollToolbarButtonEnabled = true
|
||||
// @Published var characterCount = 0
|
||||
// @Published var collectionViewState: CollectionViewState = .fold
|
||||
//
|
||||
// // for hashtag: "#<hashtag> "
|
||||
// // for mention: "@<mention> "
|
||||
// var preInsertedContent: String?
|
||||
//
|
||||
// // custom emojis
|
||||
// let customEmojiViewModel: EmojiService.CustomEmojiViewModel?
|
||||
// let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel()
|
||||
// @Published var isLoadingCustomEmoji = false
|
||||
//
|
||||
// // attachment
|
||||
// @Published var attachmentServices: [MastodonAttachmentService] = []
|
||||
//
|
||||
// // polls
|
||||
// @Published var pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute] = []
|
||||
// let pollExpiresOptionAttribute = ComposeStatusPollItem.PollExpiresOptionAttribute()
|
||||
|
||||
init(
|
||||
context: AppContext,
|
||||
composeKind: ComposeStatusSection.ComposeKind,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
authContext: AuthContext,
|
||||
kind: ComposeContentViewModel.Kind
|
||||
) {
|
||||
self.context = context
|
||||
self.composeKind = composeKind
|
||||
self.authenticationBox = authenticationBox
|
||||
self.title = {
|
||||
switch composeKind {
|
||||
case .post, .hashtag, .mention: return L10n.Scene.Compose.Title.newPost
|
||||
case .reply: return L10n.Scene.Compose.Title.newReply
|
||||
}
|
||||
}()
|
||||
self.selectedStatusVisibility = {
|
||||
// default private when user locked
|
||||
var visibility: ComposeToolbarView.VisibilitySelectionType = {
|
||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value,
|
||||
let author = authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
|
||||
else {
|
||||
return .public
|
||||
}
|
||||
return author.locked ? .private : .public
|
||||
}()
|
||||
// set visibility for reply post
|
||||
switch composeKind {
|
||||
case .reply(let record):
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let status = record.object(in: context.managedObjectContext) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
let repliedStatusVisibility = status.visibility
|
||||
switch repliedStatusVisibility {
|
||||
case .public, .unlisted:
|
||||
// keep default
|
||||
break
|
||||
case .private:
|
||||
visibility = .private
|
||||
case .direct:
|
||||
visibility = .direct
|
||||
case ._other:
|
||||
assertionFailure()
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return visibility
|
||||
}()
|
||||
// set limit
|
||||
self.instanceConfiguration = {
|
||||
var configuration: Mastodon.Entity.Instance.Configuration? = nil
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let authentication = authenticationBox.authenticationRecord.object(in: context.managedObjectContext)
|
||||
else {
|
||||
return
|
||||
}
|
||||
configuration = authentication.instance?.configuration
|
||||
}
|
||||
return configuration
|
||||
}()
|
||||
self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authenticationBox.domain)
|
||||
super.init()
|
||||
// end init
|
||||
self.authContext = authContext
|
||||
self.kind = kind
|
||||
|
||||
setup(cell: composeStatusContentTableViewCell)
|
||||
// self.title = {
|
||||
// switch composeKind {
|
||||
// case .post, .hashtag, .mention: return L10n.Scene.Compose.Title.newPost
|
||||
// case .reply: return L10n.Scene.Compose.Title.newReply
|
||||
// }
|
||||
// }()
|
||||
// self.selectedStatusVisibility = {
|
||||
// // default private when user locked
|
||||
// var visibility: ComposeToolbarView.VisibilitySelectionType = {
|
||||
// guard let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
|
||||
// else {
|
||||
// return .public
|
||||
// }
|
||||
// return author.locked ? .private : .public
|
||||
// }()
|
||||
// // set visibility for reply post
|
||||
// switch composeKind {
|
||||
// case .reply(let record):
|
||||
// context.managedObjectContext.performAndWait {
|
||||
// guard let status = record.object(in: context.managedObjectContext) else {
|
||||
// assertionFailure()
|
||||
// return
|
||||
// }
|
||||
// let repliedStatusVisibility = status.visibility
|
||||
// switch repliedStatusVisibility {
|
||||
// case .public, .unlisted:
|
||||
// // keep default
|
||||
// break
|
||||
// case .private:
|
||||
// visibility = .private
|
||||
// case .direct:
|
||||
// visibility = .direct
|
||||
// case ._other:
|
||||
// assertionFailure()
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// default:
|
||||
// break
|
||||
// }
|
||||
// return visibility
|
||||
// }()
|
||||
// // set limit
|
||||
// self.instanceConfiguration = {
|
||||
// var configuration: Mastodon.Entity.Instance.Configuration? = nil
|
||||
// context.managedObjectContext.performAndWait {
|
||||
// guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) else { return }
|
||||
// configuration = authentication.instance?.configuration
|
||||
// }
|
||||
// return configuration
|
||||
// }()
|
||||
// self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authContext.mastodonAuthenticationBox.domain)
|
||||
// super.init()
|
||||
// // end init
|
||||
//
|
||||
// setup(cell: composeStatusContentTableViewCell)
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
@ -189,199 +190,192 @@ final class ComposeViewModel: NSObject {
|
|||
}
|
||||
|
||||
extension ComposeViewModel {
|
||||
enum CollectionViewState {
|
||||
case fold // snap to input
|
||||
case expand // snap to reply
|
||||
}
|
||||
// func createNewPollOptionIfPossible() {
|
||||
// guard pollOptionAttributes.count < maxPollOptions else { return }
|
||||
//
|
||||
// let attribute = ComposeStatusPollItem.PollOptionAttribute()
|
||||
// pollOptionAttributes = pollOptionAttributes + [attribute]
|
||||
// }
|
||||
//
|
||||
// func updatePublishDate() {
|
||||
// publishDate = Date()
|
||||
// }
|
||||
}
|
||||
|
||||
extension ComposeViewModel {
|
||||
func createNewPollOptionIfPossible() {
|
||||
guard pollOptionAttributes.count < maxPollOptions else { return }
|
||||
|
||||
let attribute = ComposeStatusPollItem.PollOptionAttribute()
|
||||
pollOptionAttributes = pollOptionAttributes + [attribute]
|
||||
}
|
||||
|
||||
func updatePublishDate() {
|
||||
publishDate = Date()
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeViewModel {
|
||||
|
||||
enum AttachmentPrecondition: Error, LocalizedError {
|
||||
case videoAttachWithPhoto
|
||||
case moreThanOneVideo
|
||||
|
||||
var errorDescription: String? {
|
||||
return L10n.Common.Alerts.PublishPostFailure.title
|
||||
}
|
||||
|
||||
var failureReason: String? {
|
||||
switch self {
|
||||
case .videoAttachWithPhoto:
|
||||
return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.videoAttachWithPhoto
|
||||
case .moreThanOneVideo:
|
||||
return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.moreThanOneVideo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check exclusive limit:
|
||||
// - up to 1 video
|
||||
// - up to N photos
|
||||
func checkAttachmentPrecondition() throws {
|
||||
let attachmentServices = self.attachmentServices
|
||||
guard !attachmentServices.isEmpty else { return }
|
||||
var photoAttachmentServices: [MastodonAttachmentService] = []
|
||||
var videoAttachmentServices: [MastodonAttachmentService] = []
|
||||
attachmentServices.forEach { service in
|
||||
guard let file = service.file.value else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
switch file {
|
||||
case .jpeg, .png, .gif:
|
||||
photoAttachmentServices.append(service)
|
||||
case .other:
|
||||
videoAttachmentServices.append(service)
|
||||
}
|
||||
}
|
||||
|
||||
if !videoAttachmentServices.isEmpty {
|
||||
guard videoAttachmentServices.count == 1 else {
|
||||
throw AttachmentPrecondition.moreThanOneVideo
|
||||
}
|
||||
guard photoAttachmentServices.isEmpty else {
|
||||
throw AttachmentPrecondition.videoAttachWithPhoto
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MastodonAttachmentServiceDelegate
|
||||
extension ComposeViewModel: MastodonAttachmentServiceDelegate {
|
||||
func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) {
|
||||
// trigger new output event
|
||||
attachmentServices = attachmentServices
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ComposePollAttributeDelegate
|
||||
extension ComposeViewModel: ComposePollAttributeDelegate {
|
||||
func composePollAttribute(_ attribute: ComposeStatusPollItem.PollOptionAttribute, pollOptionDidChange: String?) {
|
||||
// trigger update
|
||||
pollOptionAttributes = pollOptionAttributes
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeViewModel {
|
||||
private func setup(
|
||||
cell: ComposeStatusContentTableViewCell
|
||||
) {
|
||||
setupStatusHeader(cell: cell)
|
||||
setupStatusAuthor(cell: cell)
|
||||
setupStatusContent(cell: cell)
|
||||
}
|
||||
|
||||
private func setupStatusHeader(
|
||||
cell: ComposeStatusContentTableViewCell
|
||||
) {
|
||||
// configure header
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
managedObjectContext.performAndWait {
|
||||
guard case let .reply(record) = self.composeKind,
|
||||
let replyTo = record.object(in: managedObjectContext)
|
||||
else {
|
||||
cell.statusView.viewModel.header = .none
|
||||
return
|
||||
}
|
||||
|
||||
let info: StatusView.ViewModel.Header.ReplyInfo
|
||||
do {
|
||||
let content = MastodonContent(
|
||||
content: replyTo.author.displayNameWithFallback,
|
||||
emojis: replyTo.author.emojis.asDictionary
|
||||
)
|
||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||
info = .init(header: metaContent)
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: replyTo.author.displayNameWithFallback)
|
||||
info = .init(header: metaContent)
|
||||
}
|
||||
cell.statusView.viewModel.header = .reply(info: info)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupStatusAuthor(
|
||||
cell: ComposeStatusContentTableViewCell
|
||||
) {
|
||||
self.context.managedObjectContext.performAndWait {
|
||||
guard let author = authenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return }
|
||||
cell.statusView.configureAuthor(author: author)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupStatusContent(
|
||||
cell: ComposeStatusContentTableViewCell
|
||||
) {
|
||||
switch composeKind {
|
||||
case .reply(let record):
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let status = record.object(in: context.managedObjectContext) else { return }
|
||||
let author = self.authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
|
||||
|
||||
var mentionAccts: [String] = []
|
||||
if author?.id != status.author.id {
|
||||
mentionAccts.append("@" + status.author.acct)
|
||||
}
|
||||
let mentions = status.mentions
|
||||
.filter { author?.id != $0.id }
|
||||
for mention in mentions {
|
||||
let acct = "@" + mention.acct
|
||||
guard !mentionAccts.contains(acct) else { continue }
|
||||
mentionAccts.append(acct)
|
||||
}
|
||||
for acct in mentionAccts {
|
||||
UITextChecker.learnWord(acct)
|
||||
}
|
||||
if let spoilerText = status.spoilerText, !spoilerText.isEmpty {
|
||||
self.isContentWarningComposing = true
|
||||
self.composeStatusAttribute.contentWarningContent = spoilerText
|
||||
}
|
||||
|
||||
let initialComposeContent = mentionAccts.joined(separator: " ")
|
||||
let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " "
|
||||
self.preInsertedContent = preInsertedContent
|
||||
self.composeStatusAttribute.composeContent = preInsertedContent
|
||||
}
|
||||
case .hashtag(let hashtag):
|
||||
let initialComposeContent = "#" + hashtag
|
||||
UITextChecker.learnWord(initialComposeContent)
|
||||
let preInsertedContent = initialComposeContent + " "
|
||||
self.preInsertedContent = preInsertedContent
|
||||
self.composeStatusAttribute.composeContent = preInsertedContent
|
||||
case .mention(let record):
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||
let initialComposeContent = "@" + user.acct
|
||||
UITextChecker.learnWord(initialComposeContent)
|
||||
let preInsertedContent = initialComposeContent + " "
|
||||
self.preInsertedContent = preInsertedContent
|
||||
self.composeStatusAttribute.composeContent = preInsertedContent
|
||||
}
|
||||
case .post:
|
||||
self.preInsertedContent = nil
|
||||
}
|
||||
|
||||
// configure content warning
|
||||
if let composeContent = composeStatusAttribute.composeContent {
|
||||
cell.metaText.textView.text = composeContent
|
||||
}
|
||||
|
||||
// configure content warning
|
||||
cell.statusContentWarningEditorView.textView.text = composeStatusAttribute.contentWarningContent
|
||||
}
|
||||
}
|
||||
//extension ComposeViewModel {
|
||||
//
|
||||
// enum AttachmentPrecondition: Error, LocalizedError {
|
||||
// case videoAttachWithPhoto
|
||||
// case moreThanOneVideo
|
||||
//
|
||||
// var errorDescription: String? {
|
||||
// return L10n.Common.Alerts.PublishPostFailure.title
|
||||
// }
|
||||
//
|
||||
// var failureReason: String? {
|
||||
// switch self {
|
||||
// case .videoAttachWithPhoto:
|
||||
// return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.videoAttachWithPhoto
|
||||
// case .moreThanOneVideo:
|
||||
// return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.moreThanOneVideo
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // check exclusive limit:
|
||||
// // - up to 1 video
|
||||
// // - up to N photos
|
||||
// func checkAttachmentPrecondition() throws {
|
||||
// let attachmentServices = self.attachmentServices
|
||||
// guard !attachmentServices.isEmpty else { return }
|
||||
// var photoAttachmentServices: [MastodonAttachmentService] = []
|
||||
// var videoAttachmentServices: [MastodonAttachmentService] = []
|
||||
// attachmentServices.forEach { service in
|
||||
// guard let file = service.file.value else {
|
||||
// assertionFailure()
|
||||
// return
|
||||
// }
|
||||
// switch file {
|
||||
// case .jpeg, .png, .gif:
|
||||
// photoAttachmentServices.append(service)
|
||||
// case .other:
|
||||
// videoAttachmentServices.append(service)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if !videoAttachmentServices.isEmpty {
|
||||
// guard videoAttachmentServices.count == 1 else {
|
||||
// throw AttachmentPrecondition.moreThanOneVideo
|
||||
// }
|
||||
// guard photoAttachmentServices.isEmpty else {
|
||||
// throw AttachmentPrecondition.videoAttachWithPhoto
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
//// MARK: - MastodonAttachmentServiceDelegate
|
||||
//extension ComposeViewModel: MastodonAttachmentServiceDelegate {
|
||||
// func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) {
|
||||
// // trigger new output event
|
||||
// attachmentServices = attachmentServices
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// MARK: - ComposePollAttributeDelegate
|
||||
//extension ComposeViewModel: ComposePollAttributeDelegate {
|
||||
// func composePollAttribute(_ attribute: ComposeStatusPollItem.PollOptionAttribute, pollOptionDidChange: String?) {
|
||||
// // trigger update
|
||||
// pollOptionAttributes = pollOptionAttributes
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension ComposeViewModel {
|
||||
// private func setup(
|
||||
// cell: ComposeStatusContentTableViewCell
|
||||
// ) {
|
||||
// setupStatusHeader(cell: cell)
|
||||
// setupStatusAuthor(cell: cell)
|
||||
// setupStatusContent(cell: cell)
|
||||
// }
|
||||
//
|
||||
// private func setupStatusHeader(
|
||||
// cell: ComposeStatusContentTableViewCell
|
||||
// ) {
|
||||
// // configure header
|
||||
// let managedObjectContext = context.managedObjectContext
|
||||
// managedObjectContext.performAndWait {
|
||||
// guard case let .reply(record) = self.composeKind,
|
||||
// let replyTo = record.object(in: managedObjectContext)
|
||||
// else {
|
||||
// cell.statusView.viewModel.header = .none
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let info: StatusView.ViewModel.Header.ReplyInfo
|
||||
// do {
|
||||
// let content = MastodonContent(
|
||||
// content: replyTo.author.displayNameWithFallback,
|
||||
// emojis: replyTo.author.emojis.asDictionary
|
||||
// )
|
||||
// let metaContent = try MastodonMetaContent.convert(document: content)
|
||||
// info = .init(header: metaContent)
|
||||
// } catch {
|
||||
// let metaContent = PlaintextMetaContent(string: replyTo.author.displayNameWithFallback)
|
||||
// info = .init(header: metaContent)
|
||||
// }
|
||||
// cell.statusView.viewModel.header = .reply(info: info)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func setupStatusAuthor(
|
||||
// cell: ComposeStatusContentTableViewCell
|
||||
// ) {
|
||||
// self.context.managedObjectContext.performAndWait {
|
||||
// guard let author = authenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return }
|
||||
// cell.statusView.configureAuthor(author: author)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func setupStatusContent(
|
||||
// cell: ComposeStatusContentTableViewCell
|
||||
// ) {
|
||||
// switch composeKind {
|
||||
// case .reply(let record):
|
||||
// context.managedObjectContext.performAndWait {
|
||||
// guard let status = record.object(in: context.managedObjectContext) else { return }
|
||||
// let author = self.authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
|
||||
//
|
||||
// var mentionAccts: [String] = []
|
||||
// if author?.id != status.author.id {
|
||||
// mentionAccts.append("@" + status.author.acct)
|
||||
// }
|
||||
// let mentions = status.mentions
|
||||
// .filter { author?.id != $0.id }
|
||||
// for mention in mentions {
|
||||
// let acct = "@" + mention.acct
|
||||
// guard !mentionAccts.contains(acct) else { continue }
|
||||
// mentionAccts.append(acct)
|
||||
// }
|
||||
// for acct in mentionAccts {
|
||||
// UITextChecker.learnWord(acct)
|
||||
// }
|
||||
// if let spoilerText = status.spoilerText, !spoilerText.isEmpty {
|
||||
// self.isContentWarningComposing = true
|
||||
// self.composeStatusAttribute.contentWarningContent = spoilerText
|
||||
// }
|
||||
//
|
||||
// let initialComposeContent = mentionAccts.joined(separator: " ")
|
||||
// let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " "
|
||||
// self.preInsertedContent = preInsertedContent
|
||||
// self.composeStatusAttribute.composeContent = preInsertedContent
|
||||
// }
|
||||
// case .hashtag(let hashtag):
|
||||
// let initialComposeContent = "#" + hashtag
|
||||
// UITextChecker.learnWord(initialComposeContent)
|
||||
// let preInsertedContent = initialComposeContent + " "
|
||||
// self.preInsertedContent = preInsertedContent
|
||||
// self.composeStatusAttribute.composeContent = preInsertedContent
|
||||
// case .mention(let record):
|
||||
// context.managedObjectContext.performAndWait {
|
||||
// guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||
// let initialComposeContent = "@" + user.acct
|
||||
// UITextChecker.learnWord(initialComposeContent)
|
||||
// let preInsertedContent = initialComposeContent + " "
|
||||
// self.preInsertedContent = preInsertedContent
|
||||
// self.composeStatusAttribute.composeContent = preInsertedContent
|
||||
// }
|
||||
// case .post:
|
||||
// self.preInsertedContent = nil
|
||||
// }
|
||||
//
|
||||
// // configure content warning
|
||||
// if let composeContent = composeStatusAttribute.composeContent {
|
||||
// cell.metaText.textView.text = composeContent
|
||||
// }
|
||||
//
|
||||
// // configure content warning
|
||||
// cell.statusContentWarningEditorView.textView.text = composeStatusAttribute.contentWarningContent
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -6,132 +6,132 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonUI
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
|
||||
extension AttachmentContainerView {
|
||||
final class EmptyStateView: UIView {
|
||||
//extension AttachmentContainerView {
|
||||
// final class EmptyStateView: UIView {
|
||||
//
|
||||
// static let photoFillSplitImage = Asset.Connectivity.photoFillSplit.image.withRenderingMode(.alwaysTemplate)
|
||||
// static let videoSplashImage: UIImage = {
|
||||
// let image = UIImage(systemName: "video.slash")!.withConfiguration(UIImage.SymbolConfiguration(pointSize: 64))
|
||||
// return image
|
||||
// }()
|
||||
//
|
||||
// let imageView: UIImageView = {
|
||||
// let imageView = UIImageView()
|
||||
// imageView.tintColor = Asset.Colors.Label.secondary.color
|
||||
// imageView.image = AttachmentContainerView.EmptyStateView.photoFillSplitImage
|
||||
// return imageView
|
||||
// }()
|
||||
// let label: UILabel = {
|
||||
// let label = UILabel()
|
||||
// label.font = .preferredFont(forTextStyle: .body)
|
||||
// label.textColor = Asset.Colors.Label.secondary.color
|
||||
// label.textAlignment = .center
|
||||
// label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
|
||||
// label.numberOfLines = 2
|
||||
// label.adjustsFontSizeToFitWidth = true
|
||||
// label.minimumScaleFactor = 0.3
|
||||
// return label
|
||||
// }()
|
||||
//
|
||||
// override init(frame: CGRect) {
|
||||
// super.init(frame: frame)
|
||||
// _init()
|
||||
// }
|
||||
//
|
||||
// required init?(coder: NSCoder) {
|
||||
// super.init(coder: coder)
|
||||
// _init()
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//}
|
||||
|
||||
static let photoFillSplitImage = Asset.Connectivity.photoFillSplit.image.withRenderingMode(.alwaysTemplate)
|
||||
static let videoSplashImage: UIImage = {
|
||||
let image = UIImage(systemName: "video.slash")!.withConfiguration(UIImage.SymbolConfiguration(pointSize: 64))
|
||||
return image
|
||||
}()
|
||||
|
||||
let imageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.tintColor = Asset.Colors.Label.secondary.color
|
||||
imageView.image = AttachmentContainerView.EmptyStateView.photoFillSplitImage
|
||||
return imageView
|
||||
}()
|
||||
let label: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .body)
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.textAlignment = .center
|
||||
label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
|
||||
label.numberOfLines = 2
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.minimumScaleFactor = 0.3
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension AttachmentContainerView.EmptyStateView {
|
||||
private func _init() {
|
||||
layer.masksToBounds = true
|
||||
layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius
|
||||
layer.cornerCurve = .continuous
|
||||
backgroundColor = ThemeService.shared.currentTheme.value.systemGroupedBackgroundColor
|
||||
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .center
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(stackView)
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.topAnchor.constraint(equalTo: topAnchor),
|
||||
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
let topPaddingView = UIView()
|
||||
let middlePaddingView = UIView()
|
||||
let bottomPaddingView = UIView()
|
||||
|
||||
topPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.addArrangedSubview(topPaddingView)
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.addArrangedSubview(imageView)
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.widthAnchor.constraint(equalToConstant: 92).priority(.defaultHigh),
|
||||
imageView.heightAnchor.constraint(equalToConstant: 76).priority(.defaultHigh),
|
||||
])
|
||||
imageView.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||
middlePaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.addArrangedSubview(middlePaddingView)
|
||||
stackView.addArrangedSubview(label)
|
||||
bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.addArrangedSubview(bottomPaddingView)
|
||||
NSLayoutConstraint.activate([
|
||||
topPaddingView.heightAnchor.constraint(equalTo: middlePaddingView.heightAnchor, multiplier: 1.5),
|
||||
bottomPaddingView.heightAnchor.constraint(equalTo: middlePaddingView.heightAnchor, multiplier: 1.5),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
import SwiftUI
|
||||
|
||||
struct AttachmentContainerView_EmptyStateView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
UIViewPreview(width: 375) {
|
||||
let emptyStateView = AttachmentContainerView.EmptyStateView()
|
||||
NSLayoutConstraint.activate([
|
||||
emptyStateView.heightAnchor.constraint(equalToConstant: 205)
|
||||
])
|
||||
return emptyStateView
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 205))
|
||||
UIViewPreview(width: 375) {
|
||||
let emptyStateView = AttachmentContainerView.EmptyStateView()
|
||||
NSLayoutConstraint.activate([
|
||||
emptyStateView.heightAnchor.constraint(equalToConstant: 205)
|
||||
])
|
||||
return emptyStateView
|
||||
}
|
||||
.preferredColorScheme(.dark)
|
||||
.previewLayout(.fixed(width: 375, height: 205))
|
||||
UIViewPreview(width: 375) {
|
||||
let emptyStateView = AttachmentContainerView.EmptyStateView()
|
||||
emptyStateView.imageView.image = AttachmentContainerView.EmptyStateView.videoSplashImage
|
||||
emptyStateView.label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
emptyStateView.heightAnchor.constraint(equalToConstant: 205)
|
||||
])
|
||||
return emptyStateView
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 205))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
//extension AttachmentContainerView.EmptyStateView {
|
||||
// private func _init() {
|
||||
// layer.masksToBounds = true
|
||||
// layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius
|
||||
// layer.cornerCurve = .continuous
|
||||
// backgroundColor = ThemeService.shared.currentTheme.value.systemGroupedBackgroundColor
|
||||
//
|
||||
// let stackView = UIStackView()
|
||||
// stackView.axis = .vertical
|
||||
// stackView.alignment = .center
|
||||
// stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// addSubview(stackView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// stackView.topAnchor.constraint(equalTo: topAnchor),
|
||||
// stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
// stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
// stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
// ])
|
||||
// let topPaddingView = UIView()
|
||||
// let middlePaddingView = UIView()
|
||||
// let bottomPaddingView = UIView()
|
||||
//
|
||||
// topPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// stackView.addArrangedSubview(topPaddingView)
|
||||
// imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// stackView.addArrangedSubview(imageView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// imageView.widthAnchor.constraint(equalToConstant: 92).priority(.defaultHigh),
|
||||
// imageView.heightAnchor.constraint(equalToConstant: 76).priority(.defaultHigh),
|
||||
// ])
|
||||
// imageView.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||
// middlePaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// stackView.addArrangedSubview(middlePaddingView)
|
||||
// stackView.addArrangedSubview(label)
|
||||
// bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// stackView.addArrangedSubview(bottomPaddingView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// topPaddingView.heightAnchor.constraint(equalTo: middlePaddingView.heightAnchor, multiplier: 1.5),
|
||||
// bottomPaddingView.heightAnchor.constraint(equalTo: middlePaddingView.heightAnchor, multiplier: 1.5),
|
||||
// ])
|
||||
// }
|
||||
//}
|
||||
|
||||
//#if canImport(SwiftUI) && DEBUG
|
||||
//import SwiftUI
|
||||
//
|
||||
//struct AttachmentContainerView_EmptyStateView_Previews: PreviewProvider {
|
||||
//
|
||||
// static var previews: some View {
|
||||
// Group {
|
||||
// UIViewPreview(width: 375) {
|
||||
// let emptyStateView = AttachmentContainerView.EmptyStateView()
|
||||
// NSLayoutConstraint.activate([
|
||||
// emptyStateView.heightAnchor.constraint(equalToConstant: 205)
|
||||
// ])
|
||||
// return emptyStateView
|
||||
// }
|
||||
// .previewLayout(.fixed(width: 375, height: 205))
|
||||
// UIViewPreview(width: 375) {
|
||||
// let emptyStateView = AttachmentContainerView.EmptyStateView()
|
||||
// NSLayoutConstraint.activate([
|
||||
// emptyStateView.heightAnchor.constraint(equalToConstant: 205)
|
||||
// ])
|
||||
// return emptyStateView
|
||||
// }
|
||||
// .preferredColorScheme(.dark)
|
||||
// .previewLayout(.fixed(width: 375, height: 205))
|
||||
// UIViewPreview(width: 375) {
|
||||
// let emptyStateView = AttachmentContainerView.EmptyStateView()
|
||||
// emptyStateView.imageView.image = AttachmentContainerView.EmptyStateView.videoSplashImage
|
||||
// emptyStateView.label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video)
|
||||
//
|
||||
// NSLayoutConstraint.activate([
|
||||
// emptyStateView.heightAnchor.constraint(equalToConstant: 205)
|
||||
// ])
|
||||
// return emptyStateView
|
||||
// }
|
||||
// .previewLayout(.fixed(width: 375, height: 205))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
//#endif
|
||||
|
|
|
@ -6,160 +6,172 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import UITextView_Placeholder
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
import SwiftUI
|
||||
import MastodonUI
|
||||
|
||||
final class AttachmentContainerView: UIView {
|
||||
//final class AttachmentContainerView: UIView {
|
||||
//
|
||||
// static let containerViewCornerRadius: CGFloat = 4
|
||||
//
|
||||
// var descriptionBackgroundViewFrameObservation: NSKeyValueObservation?
|
||||
//
|
||||
// let activityIndicatorView: UIActivityIndicatorView = {
|
||||
// let activityIndicatorView = UIActivityIndicatorView(style: .large)
|
||||
// activityIndicatorView.color = UIColor.white.withAlphaComponent(0.8)
|
||||
// return activityIndicatorView
|
||||
// }()
|
||||
//
|
||||
// let previewImageView: UIImageView = {
|
||||
// let imageView = UIImageView()
|
||||
// imageView.contentMode = .scaleAspectFill
|
||||
// imageView.layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius
|
||||
// imageView.layer.cornerCurve = .continuous
|
||||
// imageView.layer.masksToBounds = true
|
||||
// return imageView
|
||||
// }()
|
||||
//
|
||||
// let emptyStateView = AttachmentContainerView.EmptyStateView()
|
||||
// let descriptionBackgroundView: UIView = {
|
||||
// let view = UIView()
|
||||
// view.layer.masksToBounds = true
|
||||
// view.layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius
|
||||
// view.layer.cornerCurve = .continuous
|
||||
// view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
// view.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 5, right: 8)
|
||||
// return view
|
||||
// }()
|
||||
// let descriptionBackgroundGradientLayer: CAGradientLayer = {
|
||||
// let gradientLayer = CAGradientLayer()
|
||||
// gradientLayer.colors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.69).cgColor]
|
||||
// gradientLayer.locations = [0.0, 1.0]
|
||||
// gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
|
||||
// gradientLayer.endPoint = CGPoint(x: 0.5, y: 1)
|
||||
// gradientLayer.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
|
||||
// return gradientLayer
|
||||
// }()
|
||||
// let descriptionTextView: UITextView = {
|
||||
// let textView = UITextView()
|
||||
// textView.showsVerticalScrollIndicator = false
|
||||
// textView.backgroundColor = .clear
|
||||
// textView.textColor = .white
|
||||
// textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15), maximumPointSize: 20)
|
||||
// textView.placeholder = L10n.Scene.Compose.Attachment.descriptionPhoto
|
||||
// textView.placeholderColor = UIColor.white.withAlphaComponent(0.6) // force white with alpha for Light/Dark mode
|
||||
// textView.returnKeyType = .done
|
||||
// return textView
|
||||
// }()
|
||||
//
|
||||
// private(set) lazy var contentView = AttachmentView(viewModel: viewModel)
|
||||
// public var viewModel: AttachmentView.ViewModel!
|
||||
//
|
||||
// override init(frame: CGRect) {
|
||||
// super.init(frame: frame)
|
||||
// _init()
|
||||
// }
|
||||
//
|
||||
// required init?(coder: NSCoder) {
|
||||
// super.init(coder: coder)
|
||||
// _init()
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
static let containerViewCornerRadius: CGFloat = 4
|
||||
//extension AttachmentContainerView {
|
||||
//
|
||||
// private func _init() {
|
||||
// let hostingViewController = UIHostingController(rootView: contentView)
|
||||
// hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
// addSubview(hostingViewController.view)
|
||||
// NSLayoutConstraint.activate([
|
||||
// hostingViewController.view.topAnchor.constraint(equalTo: topAnchor),
|
||||
// hostingViewController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
// hostingViewController.view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
// hostingViewController.view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
// ])
|
||||
//
|
||||
// previewImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// addSubview(previewImageView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// previewImageView.topAnchor.constraint(equalTo: topAnchor),
|
||||
// previewImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
// previewImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
// previewImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
// ])
|
||||
//
|
||||
// descriptionBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// addSubview(descriptionBackgroundView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// descriptionBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
// descriptionBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
// descriptionBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
// descriptionBackgroundView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.3),
|
||||
// ])
|
||||
// descriptionBackgroundView.layer.addSublayer(descriptionBackgroundGradientLayer)
|
||||
// descriptionBackgroundViewFrameObservation = descriptionBackgroundView.observe(\.bounds, options: [.initial, .new]) { [weak self] _, _ in
|
||||
// guard let self = self else { return }
|
||||
// self.descriptionBackgroundGradientLayer.frame = self.descriptionBackgroundView.bounds
|
||||
// }
|
||||
//
|
||||
// descriptionTextView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// descriptionBackgroundView.addSubview(descriptionTextView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// descriptionTextView.leadingAnchor.constraint(equalTo: descriptionBackgroundView.layoutMarginsGuide.leadingAnchor),
|
||||
// descriptionTextView.trailingAnchor.constraint(equalTo: descriptionBackgroundView.layoutMarginsGuide.trailingAnchor),
|
||||
// descriptionBackgroundView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: descriptionTextView.bottomAnchor),
|
||||
// descriptionTextView.heightAnchor.constraint(lessThanOrEqualToConstant: 36),
|
||||
// ])
|
||||
//
|
||||
// emptyStateView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// addSubview(emptyStateView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// emptyStateView.topAnchor.constraint(equalTo: topAnchor),
|
||||
// emptyStateView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
// emptyStateView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
// emptyStateView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
// ])
|
||||
//
|
||||
// activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// addSubview(activityIndicatorView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// activityIndicatorView.centerXAnchor.constraint(equalTo: previewImageView.centerXAnchor),
|
||||
// activityIndicatorView.centerYAnchor.constraint(equalTo: previewImageView.centerYAnchor),
|
||||
// ])
|
||||
//
|
||||
// setupBroader()
|
||||
//
|
||||
// emptyStateView.isHidden = true
|
||||
// activityIndicatorView.hidesWhenStopped = true
|
||||
// activityIndicatorView.startAnimating()
|
||||
//
|
||||
// descriptionTextView.delegate = self
|
||||
// }
|
||||
//
|
||||
//// override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
// super.traitCollectionDidChange(previousTraitCollection)
|
||||
//
|
||||
// setupBroader()
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
//extension AttachmentContainerView {
|
||||
//
|
||||
// private func setupBroader() {
|
||||
// emptyStateView.layer.borderWidth = 1
|
||||
// emptyStateView.layer.borderColor = traitCollection.userInterfaceStyle == .dark ? ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor.cgColor : nil
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
var descriptionBackgroundViewFrameObservation: NSKeyValueObservation?
|
||||
|
||||
let activityIndicatorView: UIActivityIndicatorView = {
|
||||
let activityIndicatorView = UIActivityIndicatorView(style: .large)
|
||||
activityIndicatorView.color = UIColor.white.withAlphaComponent(0.8)
|
||||
return activityIndicatorView
|
||||
}()
|
||||
|
||||
let previewImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius
|
||||
imageView.layer.cornerCurve = .continuous
|
||||
imageView.layer.masksToBounds = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let emptyStateView = AttachmentContainerView.EmptyStateView()
|
||||
let descriptionBackgroundView: UIView = {
|
||||
let view = UIView()
|
||||
view.layer.masksToBounds = true
|
||||
view.layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius
|
||||
view.layer.cornerCurve = .continuous
|
||||
view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
view.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 5, right: 8)
|
||||
return view
|
||||
}()
|
||||
let descriptionBackgroundGradientLayer: CAGradientLayer = {
|
||||
let gradientLayer = CAGradientLayer()
|
||||
gradientLayer.colors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.69).cgColor]
|
||||
gradientLayer.locations = [0.0, 1.0]
|
||||
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
|
||||
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1)
|
||||
gradientLayer.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
|
||||
return gradientLayer
|
||||
}()
|
||||
let descriptionTextView: UITextView = {
|
||||
let textView = UITextView()
|
||||
textView.showsVerticalScrollIndicator = false
|
||||
textView.backgroundColor = .clear
|
||||
textView.textColor = .white
|
||||
textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15), maximumPointSize: 20)
|
||||
textView.placeholder = L10n.Scene.Compose.Attachment.descriptionPhoto
|
||||
textView.placeholderColor = UIColor.white.withAlphaComponent(0.6) // force white with alpha for Light/Dark mode
|
||||
textView.returnKeyType = .done
|
||||
return textView
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AttachmentContainerView {
|
||||
|
||||
private func _init() {
|
||||
previewImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(previewImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
previewImageView.topAnchor.constraint(equalTo: topAnchor),
|
||||
previewImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
previewImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
previewImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
|
||||
descriptionBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(descriptionBackgroundView)
|
||||
NSLayoutConstraint.activate([
|
||||
descriptionBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
descriptionBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
descriptionBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
descriptionBackgroundView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.3),
|
||||
])
|
||||
descriptionBackgroundView.layer.addSublayer(descriptionBackgroundGradientLayer)
|
||||
descriptionBackgroundViewFrameObservation = descriptionBackgroundView.observe(\.bounds, options: [.initial, .new]) { [weak self] _, _ in
|
||||
guard let self = self else { return }
|
||||
self.descriptionBackgroundGradientLayer.frame = self.descriptionBackgroundView.bounds
|
||||
}
|
||||
|
||||
descriptionTextView.translatesAutoresizingMaskIntoConstraints = false
|
||||
descriptionBackgroundView.addSubview(descriptionTextView)
|
||||
NSLayoutConstraint.activate([
|
||||
descriptionTextView.leadingAnchor.constraint(equalTo: descriptionBackgroundView.layoutMarginsGuide.leadingAnchor),
|
||||
descriptionTextView.trailingAnchor.constraint(equalTo: descriptionBackgroundView.layoutMarginsGuide.trailingAnchor),
|
||||
descriptionBackgroundView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: descriptionTextView.bottomAnchor),
|
||||
descriptionTextView.heightAnchor.constraint(lessThanOrEqualToConstant: 36),
|
||||
])
|
||||
|
||||
emptyStateView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(emptyStateView)
|
||||
NSLayoutConstraint.activate([
|
||||
emptyStateView.topAnchor.constraint(equalTo: topAnchor),
|
||||
emptyStateView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
emptyStateView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
emptyStateView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
|
||||
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(activityIndicatorView)
|
||||
NSLayoutConstraint.activate([
|
||||
activityIndicatorView.centerXAnchor.constraint(equalTo: previewImageView.centerXAnchor),
|
||||
activityIndicatorView.centerYAnchor.constraint(equalTo: previewImageView.centerYAnchor),
|
||||
])
|
||||
|
||||
setupBroader()
|
||||
|
||||
emptyStateView.isHidden = true
|
||||
activityIndicatorView.hidesWhenStopped = true
|
||||
activityIndicatorView.startAnimating()
|
||||
|
||||
descriptionTextView.delegate = self
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
setupBroader()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AttachmentContainerView {
|
||||
|
||||
private func setupBroader() {
|
||||
emptyStateView.layer.borderWidth = 1
|
||||
emptyStateView.layer.borderColor = traitCollection.userInterfaceStyle == .dark ? ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor.cgColor : nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITextViewDelegate
|
||||
extension AttachmentContainerView: UITextViewDelegate {
|
||||
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
// let keyboard dismiss when input description with "done" type return key
|
||||
if textView === descriptionTextView, text == "\n" {
|
||||
textView.resignFirstResponder()
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
//// MARK: - UITextViewDelegate
|
||||
//extension AttachmentContainerView: UITextViewDelegate {
|
||||
// func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
// // let keyboard dismiss when input description with "done" type return key
|
||||
// if textView === descriptionTextView, text == "\n" {
|
||||
// textView.resignFirstResponder()
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// return true
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -10,6 +10,8 @@ import UIKit
|
|||
import Combine
|
||||
import MastodonSDK
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
|
||||
protocol ComposeToolbarViewDelegate: AnyObject {
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import UIKit
|
||||
import Combine
|
||||
import MetaTextKit
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
|
||||
final class CustomEmojiPickerInputViewModel {
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import UIKit
|
||||
import MastodonUI
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
|
||||
final class StatusContentWarningEditorView: UIView {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
|
||||
// Local Timeline
|
||||
|
@ -115,6 +116,11 @@ extension DiscoveryCommunityViewController {
|
|||
|
||||
}
|
||||
|
||||
// MARK: - AuthContextProvider
|
||||
extension DiscoveryCommunityViewController: AuthContextProvider {
|
||||
var authContext: AuthContext { viewModel.authContext }
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension DiscoveryCommunityViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
|
||||
// sourcery:inline:CommunityViewController.AutoGenerateTableViewDelegate
|
||||
|
|
|
@ -18,6 +18,7 @@ extension DiscoveryCommunityViewModel {
|
|||
tableView: tableView,
|
||||
context: context,
|
||||
configuration: StatusSection.Configuration(
|
||||
authContext: authContext,
|
||||
statusTableViewCellDelegate: statusTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: nil,
|
||||
filterContext: .none,
|
||||
|
|
|
@ -11,16 +11,12 @@ import GameplayKit
|
|||
import MastodonSDK
|
||||
|
||||
extension DiscoveryCommunityViewModel {
|
||||
class State: GKState, NamingState {
|
||||
class State: GKState {
|
||||
|
||||
let logger = Logger(subsystem: "DiscoveryCommunityViewModel.State", category: "StateMachine")
|
||||
|
||||
let id = UUID()
|
||||
|
||||
var name: String {
|
||||
String(describing: Self.self)
|
||||
}
|
||||
|
||||
weak var viewModel: DiscoveryCommunityViewModel?
|
||||
|
||||
init(viewModel: DiscoveryCommunityViewModel) {
|
||||
|
@ -29,8 +25,10 @@ extension DiscoveryCommunityViewModel {
|
|||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
let previousState = previousState as? DiscoveryCommunityViewModel.State
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "<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
|
||||
|
@ -39,7 +37,7 @@ extension DiscoveryCommunityViewModel {
|
|||
}
|
||||
|
||||
deinit {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)")
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -136,11 +134,6 @@ extension DiscoveryCommunityViewModel.State {
|
|||
break
|
||||
}
|
||||
|
||||
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
stateMachine.enter(Fail.self)
|
||||
return
|
||||
}
|
||||
|
||||
let maxID = self.maxID
|
||||
let isReloading = maxID == nil
|
||||
|
||||
|
@ -156,7 +149,7 @@ extension DiscoveryCommunityViewModel.State {
|
|||
minID: nil,
|
||||
limit: 20
|
||||
),
|
||||
authenticationBox: authenticationBox
|
||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
|
||||
let newMaxID = response.link?.maxID
|
||||
|
@ -164,7 +157,7 @@ extension DiscoveryCommunityViewModel.State {
|
|||
self.maxID = newMaxID
|
||||
|
||||
var hasNewStatusesAppend = false
|
||||
var statusIDs = isReloading ? [] : viewModel.statusFetchedResultsController.statusIDs.value
|
||||
var statusIDs = isReloading ? [] : viewModel.statusFetchedResultsController.statusIDs
|
||||
for status in response.value {
|
||||
guard !statusIDs.contains(status.id) else { continue }
|
||||
statusIDs.append(status.id)
|
||||
|
@ -177,7 +170,7 @@ extension DiscoveryCommunityViewModel.State {
|
|||
} else {
|
||||
await enter(state: NoMore.self)
|
||||
}
|
||||
viewModel.statusFetchedResultsController.statusIDs.value = statusIDs
|
||||
viewModel.statusFetchedResultsController.statusIDs = statusIDs
|
||||
viewModel.didLoadLatest.send()
|
||||
|
||||
} catch {
|
||||
|
|
|
@ -12,6 +12,7 @@ import GameplayKit
|
|||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
|
||||
final class DiscoveryCommunityViewModel {
|
||||
|
||||
|
@ -21,6 +22,7 @@ final class DiscoveryCommunityViewModel {
|
|||
|
||||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let viewDidAppeared = PassthroughSubject<Void, Never>()
|
||||
let statusFetchedResultsController: StatusFetchedResultsController
|
||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||
|
@ -42,20 +44,15 @@ final class DiscoveryCommunityViewModel {
|
|||
|
||||
let didLoadLatest = PassthroughSubject<Void, Never>()
|
||||
|
||||
init(context: AppContext) {
|
||||
init(context: AppContext, authContext: AuthContext) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.statusFetchedResultsController = StatusFetchedResultsController(
|
||||
managedObjectContext: context.managedObjectContext,
|
||||
domain: nil,
|
||||
domain: authContext.mastodonAuthenticationBox.domain,
|
||||
additionalTweetPredicate: nil
|
||||
)
|
||||
// end init
|
||||
|
||||
context.authenticationService.activeMastodonAuthentication
|
||||
.map { $0?.domain }
|
||||
.assign(to: \.value, on: statusFetchedResultsController.domain)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
|
|
@ -11,6 +11,7 @@ import Combine
|
|||
import Tabman
|
||||
import Pageboy
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
|
||||
public class DiscoveryViewController: TabmanViewController, NeedsDependency {
|
||||
|
@ -25,10 +26,7 @@ public class DiscoveryViewController: TabmanViewController, NeedsDependency {
|
|||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
private(set) lazy var viewModel = DiscoveryViewModel(
|
||||
context: context,
|
||||
coordinator: coordinator
|
||||
)
|
||||
var viewModel: DiscoveryViewModel!
|
||||
|
||||
private(set) lazy var buttonBar: TMBar.ButtonBar = {
|
||||
let buttonBar = TMBar.ButtonBar()
|
||||
|
|
|
@ -9,6 +9,7 @@ import UIKit
|
|||
import Combine
|
||||
import Tabman
|
||||
import Pageboy
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
|
||||
final class DiscoveryViewModel {
|
||||
|
@ -17,6 +18,7 @@ final class DiscoveryViewModel {
|
|||
|
||||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let discoveryPostsViewController: DiscoveryPostsViewController
|
||||
let discoveryHashtagsViewController: DiscoveryHashtagsViewController
|
||||
let discoveryNewsViewController: DiscoveryNewsViewController
|
||||
|
@ -25,41 +27,43 @@ final class DiscoveryViewModel {
|
|||
|
||||
@Published var viewControllers: [ScrollViewContainer & PageViewController]
|
||||
|
||||
init(context: AppContext, coordinator: SceneCoordinator) {
|
||||
init(context: AppContext, coordinator: SceneCoordinator, authContext: AuthContext) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
|
||||
func setupDependency(_ needsDependency: NeedsDependency) {
|
||||
needsDependency.context = context
|
||||
needsDependency.coordinator = coordinator
|
||||
}
|
||||
|
||||
self.context = context
|
||||
discoveryPostsViewController = {
|
||||
let viewController = DiscoveryPostsViewController()
|
||||
setupDependency(viewController)
|
||||
viewController.viewModel = DiscoveryPostsViewModel(context: context)
|
||||
viewController.viewModel = DiscoveryPostsViewModel(context: context, authContext: authContext)
|
||||
return viewController
|
||||
}()
|
||||
discoveryHashtagsViewController = {
|
||||
let viewController = DiscoveryHashtagsViewController()
|
||||
setupDependency(viewController)
|
||||
viewController.viewModel = DiscoveryHashtagsViewModel(context: context)
|
||||
viewController.viewModel = DiscoveryHashtagsViewModel(context: context, authContext: authContext)
|
||||
return viewController
|
||||
}()
|
||||
discoveryNewsViewController = {
|
||||
let viewController = DiscoveryNewsViewController()
|
||||
setupDependency(viewController)
|
||||
viewController.viewModel = DiscoveryNewsViewModel(context: context)
|
||||
viewController.viewModel = DiscoveryNewsViewModel(context: context, authContext: authContext)
|
||||
return viewController
|
||||
}()
|
||||
discoveryCommunityViewController = {
|
||||
let viewController = DiscoveryCommunityViewController()
|
||||
setupDependency(viewController)
|
||||
viewController.viewModel = DiscoveryCommunityViewModel(context: context)
|
||||
viewController.viewModel = DiscoveryCommunityViewModel(context: context, authContext: authContext)
|
||||
return viewController
|
||||
}()
|
||||
discoveryForYouViewController = {
|
||||
let viewController = DiscoveryForYouViewController()
|
||||
setupDependency(viewController)
|
||||
viewController.viewModel = DiscoveryForYouViewModel(context: context)
|
||||
viewController.viewModel = DiscoveryForYouViewModel(context: context, authContext: authContext)
|
||||
return viewController
|
||||
}()
|
||||
self.viewControllers = [
|
||||
|
|
|
@ -9,6 +9,7 @@ import os.log
|
|||
import UIKit
|
||||
import Combine
|
||||
import MastodonUI
|
||||
import MastodonCore
|
||||
|
||||
final class DiscoveryForYouViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
|
@ -100,6 +101,11 @@ extension DiscoveryForYouViewController {
|
|||
|
||||
}
|
||||
|
||||
// MARK: - AuthContextProvider
|
||||
extension DiscoveryForYouViewController: AuthContextProvider {
|
||||
var authContext: AuthContext { viewModel.authContext }
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension DiscoveryForYouViewController: UITableViewDelegate {
|
||||
|
||||
|
@ -109,9 +115,10 @@ extension DiscoveryForYouViewController: UITableViewDelegate {
|
|||
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||
let profileViewModel = CachedProfileViewModel(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
mastodonUser: user
|
||||
)
|
||||
coordinator.present(
|
||||
_ = coordinator.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
from: self,
|
||||
transition: .show
|
||||
|
@ -127,15 +134,13 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
|
|||
profileCardView: ProfileCardView,
|
||||
relationshipButtonDidPressed button: ProfileRelationshipActionButton
|
||||
) {
|
||||
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
guard let indexPath = tableView.indexPath(for: cell) else { return }
|
||||
guard case let .user(record) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return }
|
||||
|
||||
Task {
|
||||
try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: self,
|
||||
user: record,
|
||||
authenticationBox: authenticationBox
|
||||
user: record
|
||||
)
|
||||
} // end Task
|
||||
}
|
||||
|
@ -156,9 +161,9 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
|
|||
return
|
||||
}
|
||||
|
||||
let familiarFollowersViewModel = FamiliarFollowersViewModel(context: context)
|
||||
let familiarFollowersViewModel = FamiliarFollowersViewModel(context: context, authContext: authContext)
|
||||
familiarFollowersViewModel.familiarFollowers = familiarFollowers
|
||||
coordinator.present(
|
||||
_ = coordinator.present(
|
||||
scene: .familiarFollowers(viewModel: familiarFollowersViewModel),
|
||||
from: self,
|
||||
transition: .show
|
||||
|
|
|
@ -19,6 +19,7 @@ extension DiscoveryForYouViewModel {
|
|||
tableView: tableView,
|
||||
context: context,
|
||||
configuration: DiscoverySection.Configuration(
|
||||
authContext: authContext,
|
||||
profileCardTableViewCellDelegate: profileCardTableViewCellDelegate,
|
||||
familiarFollowers: $familiarFollowers
|
||||
)
|
||||
|
|
|
@ -12,6 +12,7 @@ import GameplayKit
|
|||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
|
||||
final class DiscoveryForYouViewModel {
|
||||
|
||||
|
@ -19,6 +20,7 @@ final class DiscoveryForYouViewModel {
|
|||
|
||||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let userFetchedResultsController: UserFetchedResultsController
|
||||
|
||||
@MainActor
|
||||
|
@ -29,19 +31,15 @@ final class DiscoveryForYouViewModel {
|
|||
var diffableDataSource: UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem>?
|
||||
let didLoadLatest = PassthroughSubject<Void, Never>()
|
||||
|
||||
init(context: AppContext) {
|
||||
init(context: AppContext, authContext: AuthContext) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.userFetchedResultsController = UserFetchedResultsController(
|
||||
managedObjectContext: context.managedObjectContext,
|
||||
domain: nil,
|
||||
domain: authContext.mastodonAuthenticationBox.domain,
|
||||
additionalPredicate: nil
|
||||
)
|
||||
// end init
|
||||
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.map { $0?.domain }
|
||||
.assign(to: \.domain, on: userFetchedResultsController)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
@ -58,16 +56,12 @@ extension DiscoveryForYouViewModel {
|
|||
isFetching = true
|
||||
defer { isFetching = false }
|
||||
|
||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
throw APIService.APIError.implicit(.badRequest)
|
||||
}
|
||||
|
||||
do {
|
||||
let userIDs = try await fetchSuggestionAccounts()
|
||||
|
||||
let _familiarFollowersResponse = try? await context.apiService.familiarFollowers(
|
||||
query: .init(ids: userIDs),
|
||||
authenticationBox: authenticationBox
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
familiarFollowers = _familiarFollowersResponse?.value ?? []
|
||||
userFetchedResultsController.userIDs = userIDs
|
||||
|
@ -77,14 +71,10 @@ extension DiscoveryForYouViewModel {
|
|||
}
|
||||
|
||||
private func fetchSuggestionAccounts() async throws -> [Mastodon.Entity.Account.ID] {
|
||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
throw APIService.APIError.implicit(.badRequest)
|
||||
}
|
||||
|
||||
do {
|
||||
let response = try await context.apiService.suggestionAccountV2(
|
||||
query: nil,
|
||||
authenticationBox: authenticationBox
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
let userIDs = response.value.map { $0.account.id }
|
||||
return userIDs
|
||||
|
@ -92,7 +82,7 @@ extension DiscoveryForYouViewModel {
|
|||
// fallback V1
|
||||
let response = try await context.apiService.suggestionAccount(
|
||||
query: nil,
|
||||
authenticationBox: authenticationBox
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
let userIDs = response.value.map { $0.id }
|
||||
return userIDs
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
|
||||
final class DiscoveryHashtagsViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
|
@ -106,7 +107,7 @@ extension DiscoveryHashtagsViewController: UITableViewDelegate {
|
|||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(indexPath)")
|
||||
guard case let .hashtag(tag) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return }
|
||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: tag.name)
|
||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, authContext: viewModel.authContext, hashtag: tag.name)
|
||||
coordinator.present(
|
||||
scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel),
|
||||
from: self,
|
||||
|
@ -216,7 +217,7 @@ extension DiscoveryHashtagsViewController: TableViewControllerNavigateable {
|
|||
guard let item = diffableDataSource.itemIdentifier(for: indexPathForSelectedRow) else { return }
|
||||
|
||||
guard case let .hashtag(tag) = item else { return }
|
||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: tag.name)
|
||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, authContext: viewModel.authContext, hashtag: tag.name)
|
||||
coordinator.present(
|
||||
scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel),
|
||||
from: self,
|
||||
|
|
|
@ -15,7 +15,7 @@ extension DiscoveryHashtagsViewModel {
|
|||
diffableDataSource = DiscoverySection.diffableDataSource(
|
||||
tableView: tableView,
|
||||
context: context,
|
||||
configuration: DiscoverySection.Configuration()
|
||||
configuration: DiscoverySection.Configuration(authContext: authContext)
|
||||
)
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<DiscoverySection, DiscoveryItem>()
|
||||
|
|
|
@ -11,6 +11,7 @@ import Combine
|
|||
import GameplayKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
final class DiscoveryHashtagsViewModel {
|
||||
|
@ -21,41 +22,37 @@ final class DiscoveryHashtagsViewModel {
|
|||
|
||||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let viewDidAppeared = PassthroughSubject<Void, Never>()
|
||||
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem>?
|
||||
@Published var hashtags: [Mastodon.Entity.Tag] = []
|
||||
|
||||
init(context: AppContext) {
|
||||
init(context: AppContext, authContext: AuthContext) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
// end init
|
||||
|
||||
Publishers.CombineLatest(
|
||||
context.authenticationService.activeMastodonAuthenticationBox,
|
||||
viewDidAppeared
|
||||
)
|
||||
.compactMap { authenticationBox, _ -> MastodonAuthenticationBox? in
|
||||
return authenticationBox
|
||||
}
|
||||
.throttle(for: 3, scheduler: DispatchQueue.main, latest: true)
|
||||
.asyncMap { authenticationBox in
|
||||
try await context.apiService.trendHashtags(domain: authenticationBox.domain, query: nil)
|
||||
}
|
||||
.retry(3)
|
||||
.map { response in Result<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
|
||||
viewDidAppeared
|
||||
.throttle(for: 3, scheduler: DispatchQueue.main, latest: true)
|
||||
.asyncMap { authenticationBox in
|
||||
try await context.apiService.trendHashtags(domain: authContext.mastodonAuthenticationBox.domain, query: nil)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
.retry(3)
|
||||
.map { response in Result<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 {
|
||||
|
@ -68,8 +65,7 @@ extension DiscoveryHashtagsViewModel {
|
|||
|
||||
@MainActor
|
||||
func fetch() async throws {
|
||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
let response = try await context.apiService.trendHashtags(domain: authenticationBox.domain, query: nil)
|
||||
let response = try await context.apiService.trendHashtags(domain: authContext.mastodonAuthenticationBox.domain, query: nil)
|
||||
hashtags = response.value.filter { !$0.name.isEmpty }
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch tags: \(response.value.count)")
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
|
||||
final class DiscoveryNewsViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
|
|
|
@ -16,7 +16,7 @@ extension DiscoveryNewsViewModel {
|
|||
diffableDataSource = DiscoverySection.diffableDataSource(
|
||||
tableView: tableView,
|
||||
context: context,
|
||||
configuration: DiscoverySection.Configuration()
|
||||
configuration: DiscoverySection.Configuration(authContext: authContext)
|
||||
)
|
||||
|
||||
stateMachine.enter(State.Reloading.self)
|
||||
|
|
|
@ -11,16 +11,12 @@ import GameplayKit
|
|||
import MastodonSDK
|
||||
|
||||
extension DiscoveryNewsViewModel {
|
||||
class State: GKState, NamingState {
|
||||
class State: GKState {
|
||||
|
||||
let logger = Logger(subsystem: "DiscoveryNewsViewModel.State", category: "StateMachine")
|
||||
|
||||
let id = UUID()
|
||||
|
||||
var name: String {
|
||||
String(describing: Self.self)
|
||||
}
|
||||
|
||||
weak var viewModel: DiscoveryNewsViewModel?
|
||||
|
||||
init(viewModel: DiscoveryNewsViewModel) {
|
||||
|
@ -29,8 +25,10 @@ extension DiscoveryNewsViewModel {
|
|||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
let previousState = previousState as? DiscoveryNewsViewModel.State
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "<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
|
||||
|
@ -39,7 +37,7 @@ extension DiscoveryNewsViewModel {
|
|||
}
|
||||
|
||||
deinit {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)")
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -137,18 +135,13 @@ extension DiscoveryNewsViewModel.State {
|
|||
break
|
||||
}
|
||||
|
||||
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
stateMachine.enter(Fail.self)
|
||||
return
|
||||
}
|
||||
|
||||
let offset = self.offset
|
||||
let isReloading = offset == nil
|
||||
|
||||
Task {
|
||||
do {
|
||||
let response = try await viewModel.context.apiService.trendLinks(
|
||||
domain: authenticationBox.domain,
|
||||
domain: viewModel.authContext.mastodonAuthenticationBox.domain,
|
||||
query: Mastodon.API.Trends.StatusQuery(
|
||||
offset: offset,
|
||||
limit: nil
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue