chore: [WIP] move core logic into package

This commit is contained in:
CMK 2022-09-30 19:28:09 +08:00
parent 28267fe6d8
commit 64f3d2fe3a
173 changed files with 1529 additions and 2471 deletions

16
.arkana.yml Normal file
View File

@ -0,0 +1,16 @@
import_name: 'ArkanaKeys'
namespace: 'Keys'
result_path: 'Dependencies'
flavors:
- AppStore
swift_declaration_strategy: let
should_generate_unit_tests: true
package_manager: spm
environments:
- Debug
- Release
global_secrets:
# nothing
environment_secrets:
# Will lookup for <Key>Debug and <Key>Release env vars (assuming no flavor was declared)
- NotificationEndpoint

View File

@ -1,18 +0,0 @@
//
// AppShared.h
// AppShared
//
// Created by MainasuK Cirno on 2021-4-27.
//
#import <Foundation/Foundation.h>
//! Project version number for AppShared.
FOUNDATION_EXPORT double AppSharedVersionNumber;
//! Project version string for AppShared.
FOUNDATION_EXPORT const unsigned char AppSharedVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <AppShared/PublicHeader.h>

View File

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

View File

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

View File

@ -3,9 +3,6 @@ GEM
specs: specs:
CFPropertyList (3.0.5) CFPropertyList (3.0.5)
rexml rexml
RubyInline (3.12.5)
ZenTest (~> 4.3)
ZenTest (4.12.1)
activesupport (6.1.5.1) activesupport (6.1.5.1)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
@ -17,6 +14,10 @@ GEM
algoliasearch (1.27.5) algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3) httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1) json (>= 1.5.1)
arkana (1.2.0)
colorize (~> 0.8)
dotenv (~> 2.7)
yaml (~> 0.2)
atomos (0.1.3) atomos (0.1.3)
claide (1.1.0) claide (1.1.0)
cocoapods (1.11.3) cocoapods (1.11.3)
@ -50,9 +51,6 @@ GEM
typhoeus (~> 1.0) typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5) cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.6.3) cocoapods-downloader (1.6.3)
cocoapods-keys (2.2.1)
dotenv
osx_keychain
cocoapods-plugins (1.0.0) cocoapods-plugins (1.0.0)
nap nap
cocoapods-search (1.0.1) cocoapods-search (1.0.1)
@ -61,8 +59,9 @@ GEM
netrc (~> 0.11) netrc (~> 0.11)
cocoapods-try (1.2.0) cocoapods-try (1.2.0)
colored2 (3.1.2) colored2 (3.1.2)
colorize (0.8.1)
concurrent-ruby (1.1.10) concurrent-ruby (1.1.10)
dotenv (2.7.6) dotenv (2.8.1)
escape (0.0.4) escape (0.0.4)
ethon (0.15.0) ethon (0.15.0)
ffi (>= 1.15.0) ffi (>= 1.15.0)
@ -79,8 +78,6 @@ GEM
nanaimo (0.3.0) nanaimo (0.3.0)
nap (1.1.0) nap (1.1.0)
netrc (0.11.0) netrc (0.11.0)
osx_keychain (1.0.2)
RubyInline (~> 3)
public_suffix (4.0.7) public_suffix (4.0.7)
rexml (3.2.5) rexml (3.2.5)
ruby-macho (2.5.1) ruby-macho (2.5.1)
@ -95,15 +92,16 @@ GEM
colored2 (~> 3.1) colored2 (~> 3.1)
nanaimo (~> 0.3.0) nanaimo (~> 0.3.0)
rexml (~> 3.2.4) rexml (~> 3.2.4)
yaml (0.2.0)
zeitwerk (2.5.4) zeitwerk (2.5.4)
PLATFORMS PLATFORMS
ruby ruby
DEPENDENCIES DEPENDENCIES
arkana
cocoapods cocoapods
cocoapods-clean cocoapods-clean
cocoapods-keys
BUNDLED WITH BUNDLED WITH
2.3.11 2.3.17

File diff suppressed because it is too large Load Diff

View File

@ -4,13 +4,6 @@
<dict> <dict>
<key>SchemeUserState</key> <key>SchemeUserState</key>
<dict> <dict>
<key>AppShared.xcscheme_^#shared#^_</key>
<dict>
<key>isShown</key>
<true/>
<key>orderHint</key>
<integer>6</integer>
</dict>
<key>CoreDataStack.xcscheme_^#shared#^_</key> <key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
@ -19,32 +12,27 @@
<key>Mastodon - Profile.xcscheme_^#shared#^_</key> <key>Mastodon - Profile.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>2</integer> <integer>1</integer>
</dict> </dict>
<key>Mastodon - RTL.xcscheme_^#shared#^_</key> <key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>7</integer> <integer>5</integer>
</dict> </dict>
<key>Mastodon - Release.xcscheme_^#shared#^_</key> <key>Mastodon - Release.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>3</integer> <integer>2</integer>
</dict> </dict>
<key>Mastodon - Snapshot.xcscheme_^#shared#^_</key> <key>Mastodon - Snapshot.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>4</integer> <integer>3</integer>
</dict>
<key>Mastodon - ar.xcscheme</key>
<dict>
<key>orderHint</key>
<integer>5</integer>
</dict> </dict>
<key>Mastodon - ar.xcscheme_^#shared#^_</key> <key>Mastodon - ar.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>11</integer> <integer>4</integer>
</dict> </dict>
<key>Mastodon - ca.xcscheme_^#shared#^_</key> <key>Mastodon - ca.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -111,11 +99,6 @@
<key>orderHint</key> <key>orderHint</key>
<integer>0</integer> <integer>0</integer>
</dict> </dict>
<key>MastodonIntent.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>22</integer>
</dict>
<key>MastodonIntents.xcscheme_^#shared#^_</key> <key>MastodonIntents.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
@ -129,12 +112,12 @@
<key>NotificationService.xcscheme_^#shared#^_</key> <key>NotificationService.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>23</integer> <integer>7</integer>
</dict> </dict>
<key>ShareActionExtension.xcscheme_^#shared#^_</key> <key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>24</integer> <integer>6</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key> <key>SuppressBuildableAutocreation</key>
@ -164,6 +147,11 @@
<key>primary</key> <key>primary</key>
<true/> <true/>
</dict> </dict>
<key>DB8FABC526AEC7B2008E5AF4</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict> </dict>
</dict> </dict>
</plist> </plist>

View File

@ -19,15 +19,6 @@
"version": "4.2.0" "version": "4.2.0"
} }
}, },
{
"package": "AlamofireNetworkActivityIndicator",
"repositoryURL": "https://github.com/Alamofire/AlamofireNetworkActivityIndicator",
"state": {
"branch": null,
"revision": "392bed083e8d193aca16bfa684ee24e4bcff0510",
"version": "3.1.0"
}
},
{ {
"package": "CommonOSLog", "package": "CommonOSLog",
"repositoryURL": "https://github.com/MainasuK/CommonOSLog", "repositoryURL": "https://github.com/MainasuK/CommonOSLog",
@ -37,24 +28,6 @@
"version": "0.1.1" "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", "package": "FaviconFinder",
"repositoryURL": "https://github.com/will-lumley/FaviconFinder.git", "repositoryURL": "https://github.com/will-lumley/FaviconFinder.git",
@ -159,8 +132,8 @@
"repositoryURL": "https://github.com/apple/swift-collections.git", "repositoryURL": "https://github.com/apple/swift-collections.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "9d8719c8bebdc79740b6969c912ac706eb721d7a", "revision": "f504716c27d2e5d4144fa4794b12129301d17729",
"version": "0.0.7" "version": "1.0.3"
} }
}, },
{ {
@ -222,8 +195,8 @@
"repositoryURL": "https://github.com/uias/Tabman", "repositoryURL": "https://github.com/uias/Tabman",
"state": { "state": {
"branch": null, "branch": null,
"revision": "a9f10cb862a32e6a22549836af013abd6b0692d3", "revision": "4a4f7c755b875ffd4f9ef10d67a67883669d2465",
"version": "2.12.0" "version": "2.13.0"
} }
}, },
{ {
@ -231,8 +204,8 @@
"repositoryURL": "https://github.com/vtourraine/ThirdPartyMailer.git", "repositoryURL": "https://github.com/vtourraine/ThirdPartyMailer.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "779da6ce0793b461ccbbac2804755c1e29b6fa63", "revision": "44c1cfaa6969963f22691aa67f88a69e3b6d651f",
"version": "1.8.0" "version": "2.1.0"
} }
}, },
{ {
@ -244,6 +217,15 @@
"version": "2.6.1" "version": "2.6.1"
} }
}, },
{
"package": "UIHostingConfigurationBackport",
"repositoryURL": "https://github.com/woxtu/UIHostingConfigurationBackport.git",
"state": {
"branch": null,
"revision": "6091f2d38faa4b24fc2ca0389c651e2f666624a3",
"version": "0.1.0"
}
},
{ {
"package": "UITextView+Placeholder", "package": "UITextView+Placeholder",
"repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git", "repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git",

View File

@ -8,8 +8,9 @@ import UIKit
import Combine import Combine
import SafariServices import SafariServices
import CoreDataStack import CoreDataStack
import MastodonSDK
import PanModal import PanModal
import MastodonSDK
import MastodonCore
import MastodonAsset import MastodonAsset
import MastodonLocalization import MastodonLocalization

View File

@ -10,6 +10,7 @@ import MastodonSDK
import MastodonMeta import MastodonMeta
import MastodonAsset import MastodonAsset
import MastodonLocalization import MastodonLocalization
import MastodonCore
enum AutoCompleteSection: Equatable, Hashable { enum AutoCompleteSection: Equatable, Hashable {
case main case main

View File

@ -1,28 +0,0 @@
//
// MastodonUser.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021/2/3.
//
import Foundation
import CoreDataStack
import MastodonSDK
extension MastodonUser {
public var profileURL: URL {
if let urlString = self.url,
let url = URL(string: urlString) {
return url
} else {
return URL(string: "https://\(self.domain)/@\(username)")!
}
}
public var activityItems: [Any] {
var items: [Any] = []
items.append(profileURL)
return items
}
}

View File

@ -1,19 +0,0 @@
//
// MastodonAuthenticationBox.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-7-20.
//
import Foundation
import CoreDataStack
import MastodonSDK
import MastodonUI
struct MastodonAuthenticationBox: UserIdentifier {
let authenticationRecord: ManagedObjectRecord<MastodonAuthentication>
let domain: String
let userID: MastodonUser.ID
let appAuthorization: Mastodon.API.OAuth.Authorization
let userAuthorization: Mastodon.API.OAuth.Authorization
}

View File

@ -1,24 +0,0 @@
//
// MastodonEmojis.swift
// MastodonEmojis
//
// Created by Cirno MainasuK on 2021-9-2.
// Copyright © 2021 Twidere. All rights reserved.
//
import Foundation
import CoreDataStack
import MastodonSDK
import MastodonMeta
extension MastodonEmoji {
public convenience init(emoji: Mastodon.Entity.Emoji) {
self.init(
code: emoji.shortcode,
url: emoji.url,
staticURL: emoji.staticURL,
visibleInPicker: emoji.visibleInPicker,
category: emoji.category
)
}
}

View File

@ -1,20 +0,0 @@
//
// HomeTimelinePreference.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-6-21.
//
import UIKit
extension UserDefaults {
@objc dynamic var preferAsyncHomeTimeline: Bool {
get {
register(defaults: [#function: false])
return bool(forKey: #function)
}
set { self[#function] = newValue }
}
}

View File

@ -1,21 +0,0 @@
//
// NotificationPreference.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-4-26.
//
import UIKit
import MastodonExtension
extension UserDefaults {
@objc dynamic var notificationBadgeCount: Int {
get {
register(defaults: [#function: 0])
return integer(forKey: #function)
}
set { self[#function] = newValue }
}
}

View File

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

View File

@ -11,6 +11,8 @@ import CoreData
import CoreDataStack import CoreDataStack
import MastodonSDK import MastodonSDK
import MastodonMeta import MastodonMeta
import MastodonCore
import MastodonUI
final class AccountListViewModel { final class AccountListViewModel {

View File

@ -12,6 +12,7 @@ import CoreDataStack
import PanModal import PanModal
import MastodonAsset import MastodonAsset
import MastodonLocalization import MastodonLocalization
import MastodonCore
final class AccountListViewController: UIViewController, NeedsDependency { final class AccountListViewController: UIViewController, NeedsDependency {

View File

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

View File

@ -9,6 +9,7 @@ import os.log
import Foundation import Foundation
import GameplayKit import GameplayKit
import MastodonSDK import MastodonSDK
import MastodonCore
extension AutoCompleteViewModel { extension AutoCompleteViewModel {
class State: GKState, NamingState { class State: GKState, NamingState {

View File

@ -9,6 +9,7 @@ import UIKit
import Combine import Combine
import GameplayKit import GameplayKit
import MastodonSDK import MastodonSDK
import MastodonCore
final class AutoCompleteViewModel { final class AutoCompleteViewModel {
@ -16,13 +17,13 @@ final class AutoCompleteViewModel {
// input // input
let context: AppContext let context: AppContext
let inputText = CurrentValueSubject<String, Never>("") // contains "@" or "#" prefix public let inputText = CurrentValueSubject<String, Never>("") // contains "@" or "#" prefix
let symbolBoundingRect = CurrentValueSubject<CGRect, Never>(.zero) public let symbolBoundingRect = CurrentValueSubject<CGRect, Never>(.zero)
let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil) public let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil)
// output // output
var autoCompleteItems = CurrentValueSubject<[AutoCompleteItem], Never>([]) public var autoCompleteItems = CurrentValueSubject<[AutoCompleteItem], Never>([])
var diffableDataSource: UITableViewDiffableDataSource<AutoCompleteSection, AutoCompleteItem>! public var diffableDataSource: UITableViewDiffableDataSource<AutoCompleteSection, AutoCompleteItem>!
private(set) lazy var stateMachine: GKStateMachine = { private(set) lazy var stateMachine: GKStateMachine = {
// exclude timeline middle fetcher state // exclude timeline middle fetcher state
let stateMachine = GKStateMachine(states: [ let stateMachine = GKStateMachine(states: [

View File

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

View File

@ -74,17 +74,23 @@ final class ComposeViewController: UIViewController, NeedsDependency {
publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal) publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
} }
let tableView: ComposeTableView = { let scrollView: UIScrollView = {
let tableView = ComposeTableView() let scrollView = UIScrollView()
tableView.register(ComposeRepliedToStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self)) scrollView.alwaysBounceVertical = true
tableView.register(ComposeStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusContentTableViewCell.self)) return scrollView
tableView.register(ComposeStatusAttachmentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusAttachmentTableViewCell.self))
tableView.alwaysBounceVertical = true
tableView.separatorStyle = .none
tableView.tableFooterView = UIView()
return tableView
}() }()
// let tableView: ComposeTableView = {
// let tableView = ComposeTableView()
// tableView.register(ComposeRepliedToStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self))
// tableView.register(ComposeStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusContentTableViewCell.self))
// tableView.register(ComposeStatusAttachmentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusAttachmentTableViewCell.self))
// tableView.alwaysBounceVertical = true
// tableView.separatorStyle = .none
// tableView.tableFooterView = UIView()
// return tableView
// }()
var systemKeyboardHeight: CGFloat = .zero { var systemKeyboardHeight: CGFloat = .zero {
didSet { didSet {
// note: some system AutoLayout warning here // note: some system AutoLayout warning here
@ -202,13 +208,13 @@ extension ComposeViewController {
publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
tableView.translatesAutoresizingMaskIntoConstraints = false scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView) view.addSubview(scrollView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor), scrollView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]) ])
composeToolbarView.translatesAutoresizingMaskIntoConstraints = false composeToolbarView.translatesAutoresizingMaskIntoConstraints = false
@ -232,318 +238,320 @@ extension ComposeViewController {
view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor), view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor),
]) ])
tableView.delegate = self // tableView.delegate = self
viewModel.setupDataSource( // viewModel.setupDataSource(
tableView: tableView, // tableView: tableView,
metaTextDelegate: self, // metaTextDelegate: self,
metaTextViewDelegate: self, // metaTextViewDelegate: self,
customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel, // customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel,
composeStatusAttachmentCollectionViewCellDelegate: self, // composeStatusAttachmentCollectionViewCellDelegate: self,
composeStatusPollOptionCollectionViewCellDelegate: self, // composeStatusPollOptionCollectionViewCellDelegate: self,
composeStatusPollOptionAppendEntryCollectionViewCellDelegate: self, // composeStatusPollOptionAppendEntryCollectionViewCellDelegate: self,
composeStatusPollExpiresOptionCollectionViewCellDelegate: self // composeStatusPollExpiresOptionCollectionViewCellDelegate: self
) // )
viewModel.composeStatusAttribute.$composeContent // viewModel.composeStatusAttribute.$composeContent
.removeDuplicates() // .removeDuplicates()
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [weak self] _ in // .sink { [weak self] _ in
guard let self = self else { return } // guard let self = self else { return }
guard self.view.window != nil else { return } // guard self.view.window != nil else { return }
UIView.performWithoutAnimation { // UIView.performWithoutAnimation {
self.tableView.beginUpdates() // self.tableView.beginUpdates()
self.tableView.endUpdates() // self.tableView.setNeedsLayout()
} // self.tableView.layoutIfNeeded()
} // self.tableView.endUpdates()
.store(in: &disposeBag) // }
// }
// .store(in: &disposeBag)
customEmojiPickerInputView.collectionView.delegate = self // customEmojiPickerInputView.collectionView.delegate = self
viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView // viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView
viewModel.setupCustomEmojiPickerDiffableDataSource( // viewModel.setupCustomEmojiPickerDiffableDataSource(
for: customEmojiPickerInputView.collectionView, // for: customEmojiPickerInputView.collectionView,
dependency: self // dependency: self
) // )
viewModel.composeStatusContentTableViewCell.delegate = self // viewModel.composeStatusContentTableViewCell.delegate = self
//
// update layout when keyboard show/dismiss // // update layout when keyboard show/dismiss
view.layoutIfNeeded() // view.layoutIfNeeded()
//
let keyboardHasShortcutBar = CurrentValueSubject<Bool, Never>(traitCollection.userInterfaceIdiom == .pad) // update default value later // let keyboardHasShortcutBar = CurrentValueSubject<Bool, Never>(traitCollection.userInterfaceIdiom == .pad) // update default value later
let keyboardEventPublishers = Publishers.CombineLatest3( // let keyboardEventPublishers = Publishers.CombineLatest3(
KeyboardResponderService.shared.isShow, // KeyboardResponderService.shared.isShow,
KeyboardResponderService.shared.state, // KeyboardResponderService.shared.state,
KeyboardResponderService.shared.endFrame // KeyboardResponderService.shared.endFrame
) // )
Publishers.CombineLatest3( // Publishers.CombineLatest3(
keyboardEventPublishers, // keyboardEventPublishers,
viewModel.$isCustomEmojiComposing, // viewModel.$isCustomEmojiComposing,
viewModel.$autoCompleteInfo // viewModel.$autoCompleteInfo
) // )
.sink(receiveValue: { [weak self] keyboardEvents, isCustomEmojiComposing, autoCompleteInfo in // .sink(receiveValue: { [weak self] keyboardEvents, isCustomEmojiComposing, autoCompleteInfo in
guard let self = self else { return } // guard let self = self else { return }
//
let (isShow, state, endFrame) = keyboardEvents // let (isShow, state, endFrame) = keyboardEvents
//
switch self.traitCollection.userInterfaceIdiom { // switch self.traitCollection.userInterfaceIdiom {
case .pad: // case .pad:
keyboardHasShortcutBar.value = state != .floating // keyboardHasShortcutBar.value = state != .floating
default: // default:
keyboardHasShortcutBar.value = false // keyboardHasShortcutBar.value = false
} // }
//
let extraMargin: CGFloat = { // let extraMargin: CGFloat = {
var margin = self.composeToolbarView.frame.height // var margin = self.composeToolbarView.frame.height
if autoCompleteInfo != nil { // if autoCompleteInfo != nil {
margin += ComposeViewController.minAutoCompleteVisibleHeight // margin += ComposeViewController.minAutoCompleteVisibleHeight
} // }
return margin // return margin
}() // }()
//
guard isShow, state == .dock else { // guard isShow, state == .dock else {
self.tableView.contentInset.bottom = extraMargin // self.tableView.contentInset.bottom = extraMargin
self.tableView.verticalScrollIndicatorInsets.bottom = extraMargin // self.tableView.verticalScrollIndicatorInsets.bottom = extraMargin
//
if let superView = self.autoCompleteViewController.tableView.superview { // if let superView = self.autoCompleteViewController.tableView.superview {
let autoCompleteTableViewBottomInset: CGFloat = { // let autoCompleteTableViewBottomInset: CGFloat = {
let tableViewFrameInWindow = superView.convert(self.autoCompleteViewController.tableView.frame, to: nil) // let tableViewFrameInWindow = superView.convert(self.autoCompleteViewController.tableView.frame, to: nil)
let padding = tableViewFrameInWindow.maxY + self.composeToolbarView.frame.height + AutoCompleteViewController.chevronViewHeight - self.view.frame.maxY // let padding = tableViewFrameInWindow.maxY + self.composeToolbarView.frame.height + AutoCompleteViewController.chevronViewHeight - self.view.frame.maxY
return max(0, padding) // return max(0, padding)
}() // }()
self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset // self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset
self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset // self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset
} // }
//
UIView.animate(withDuration: 0.3) { // UIView.animate(withDuration: 0.3) {
self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom // self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom
if self.view.window != nil { // if self.view.window != nil {
self.view.layoutIfNeeded() // self.view.layoutIfNeeded()
} // }
} // }
return // return
} // }
// isShow AND dock state // // isShow AND dock state
self.systemKeyboardHeight = endFrame.height // self.systemKeyboardHeight = endFrame.height
//
// adjust inset for auto-complete // // adjust inset for auto-complete
let autoCompleteTableViewBottomInset: CGFloat = { // let autoCompleteTableViewBottomInset: CGFloat = {
guard let superview = self.autoCompleteViewController.tableView.superview else { return .zero } // guard let superview = self.autoCompleteViewController.tableView.superview else { return .zero }
let tableViewFrameInWindow = superview.convert(self.autoCompleteViewController.tableView.frame, to: nil) // let tableViewFrameInWindow = superview.convert(self.autoCompleteViewController.tableView.frame, to: nil)
let padding = tableViewFrameInWindow.maxY + self.composeToolbarView.frame.height + AutoCompleteViewController.chevronViewHeight - endFrame.minY // let padding = tableViewFrameInWindow.maxY + self.composeToolbarView.frame.height + AutoCompleteViewController.chevronViewHeight - endFrame.minY
return max(0, padding) // return max(0, padding)
}() // }()
self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset // self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset
self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset // self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset
//
// adjust inset for tableView // // adjust inset for tableView
let contentFrame = self.view.convert(self.tableView.frame, to: nil) // let contentFrame = self.view.convert(self.tableView.frame, to: nil)
let padding = contentFrame.maxY + extraMargin - endFrame.minY // let padding = contentFrame.maxY + extraMargin - endFrame.minY
guard padding > 0 else { // guard padding > 0 else {
self.tableView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin // self.tableView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin
self.tableView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin // self.tableView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin
return // return
} // }
//
self.tableView.contentInset.bottom = padding - self.view.safeAreaInsets.bottom // self.tableView.contentInset.bottom = padding - self.view.safeAreaInsets.bottom
self.tableView.verticalScrollIndicatorInsets.bottom = padding - self.view.safeAreaInsets.bottom // self.tableView.verticalScrollIndicatorInsets.bottom = padding - self.view.safeAreaInsets.bottom
UIView.animate(withDuration: 0.3) { // UIView.animate(withDuration: 0.3) {
self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height // self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height
self.view.layoutIfNeeded() // self.view.layoutIfNeeded()
} // }
}) // })
.store(in: &disposeBag) // .store(in: &disposeBag)
//
// bind auto-complete // // bind auto-complete
viewModel.$autoCompleteInfo // viewModel.$autoCompleteInfo
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [weak self] info in // .sink { [weak self] info in
guard let self = self else { return } // guard let self = self else { return }
let textEditorView = self.textEditorView // let textEditorView = self.textEditorView
if self.autoCompleteViewController.view.superview == nil { // if self.autoCompleteViewController.view.superview == nil {
self.autoCompleteViewController.view.frame = self.view.bounds // self.autoCompleteViewController.view.frame = self.view.bounds
// add to container view. seealso: `viewDidLayoutSubviews()` // // add to container view. seealso: `viewDidLayoutSubviews()`
self.viewModel.composeStatusContentTableViewCell.textEditorViewContainerView.addSubview(self.autoCompleteViewController.view) // self.viewModel.composeStatusContentTableViewCell.textEditorViewContainerView.addSubview(self.autoCompleteViewController.view)
self.addChild(self.autoCompleteViewController) // self.addChild(self.autoCompleteViewController)
self.autoCompleteViewController.didMove(toParent: self) // self.autoCompleteViewController.didMove(toParent: self)
self.autoCompleteViewController.view.isHidden = true // self.autoCompleteViewController.view.isHidden = true
self.tableView.autoCompleteViewController = self.autoCompleteViewController // self.tableView.autoCompleteViewController = self.autoCompleteViewController
} // }
self.updateAutoCompleteViewControllerLayout() // self.updateAutoCompleteViewControllerLayout()
self.autoCompleteViewController.view.isHidden = info == nil // self.autoCompleteViewController.view.isHidden = info == nil
guard let info = info else { return } // guard let info = info else { return }
let symbolBoundingRectInContainer = textEditorView.textView.convert(info.symbolBoundingRect, to: self.autoCompleteViewController.chevronView) // let symbolBoundingRectInContainer = textEditorView.textView.convert(info.symbolBoundingRect, to: self.autoCompleteViewController.chevronView)
self.autoCompleteViewController.view.frame.origin.y = info.textBoundingRect.maxY // self.autoCompleteViewController.view.frame.origin.y = info.textBoundingRect.maxY
self.autoCompleteViewController.viewModel.symbolBoundingRect.value = symbolBoundingRectInContainer // self.autoCompleteViewController.viewModel.symbolBoundingRect.value = symbolBoundingRectInContainer
self.autoCompleteViewController.viewModel.inputText.value = String(info.inputText) // self.autoCompleteViewController.viewModel.inputText.value = String(info.inputText)
} // }
.store(in: &disposeBag) // .store(in: &disposeBag)
//
// bind publish bar button state // // bind publish bar button state
viewModel.$isPublishBarButtonItemEnabled // viewModel.$isPublishBarButtonItemEnabled
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.assign(to: \.isEnabled, on: publishButton) // .assign(to: \.isEnabled, on: publishButton)
.store(in: &disposeBag) // .store(in: &disposeBag)
//
// bind media button toolbar state // // bind media button toolbar state
viewModel.$isMediaToolbarButtonEnabled // viewModel.$isMediaToolbarButtonEnabled
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [weak self] isMediaToolbarButtonEnabled in // .sink { [weak self] isMediaToolbarButtonEnabled in
guard let self = self else { return } // guard let self = self else { return }
self.composeToolbarView.mediaBarButtonItem.isEnabled = isMediaToolbarButtonEnabled // self.composeToolbarView.mediaBarButtonItem.isEnabled = isMediaToolbarButtonEnabled
self.composeToolbarView.mediaButton.isEnabled = isMediaToolbarButtonEnabled // self.composeToolbarView.mediaButton.isEnabled = isMediaToolbarButtonEnabled
} // }
.store(in: &disposeBag) // .store(in: &disposeBag)
//
// bind poll button toolbar state // // bind poll button toolbar state
viewModel.$isPollToolbarButtonEnabled // viewModel.$isPollToolbarButtonEnabled
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [weak self] isPollToolbarButtonEnabled in // .sink { [weak self] isPollToolbarButtonEnabled in
guard let self = self else { return } // guard let self = self else { return }
self.composeToolbarView.pollBarButtonItem.isEnabled = isPollToolbarButtonEnabled // self.composeToolbarView.pollBarButtonItem.isEnabled = isPollToolbarButtonEnabled
self.composeToolbarView.pollButton.isEnabled = isPollToolbarButtonEnabled // self.composeToolbarView.pollButton.isEnabled = isPollToolbarButtonEnabled
} // }
.store(in: &disposeBag) // .store(in: &disposeBag)
//
Publishers.CombineLatest( // Publishers.CombineLatest(
viewModel.$isPollComposing, // viewModel.$isPollComposing,
viewModel.$isPollToolbarButtonEnabled // viewModel.$isPollToolbarButtonEnabled
) // )
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [weak self] isPollComposing, isPollToolbarButtonEnabled in // .sink { [weak self] isPollComposing, isPollToolbarButtonEnabled in
guard let self = self else { return } // guard let self = self else { return }
guard isPollToolbarButtonEnabled else { // guard isPollToolbarButtonEnabled else {
let accessibilityLabel = L10n.Scene.Compose.Accessibility.appendPoll // let accessibilityLabel = L10n.Scene.Compose.Accessibility.appendPoll
self.composeToolbarView.pollBarButtonItem.accessibilityLabel = accessibilityLabel // self.composeToolbarView.pollBarButtonItem.accessibilityLabel = accessibilityLabel
self.composeToolbarView.pollButton.accessibilityLabel = accessibilityLabel // self.composeToolbarView.pollButton.accessibilityLabel = accessibilityLabel
return // return
} // }
let accessibilityLabel = isPollComposing ? L10n.Scene.Compose.Accessibility.removePoll : L10n.Scene.Compose.Accessibility.appendPoll // let accessibilityLabel = isPollComposing ? L10n.Scene.Compose.Accessibility.removePoll : L10n.Scene.Compose.Accessibility.appendPoll
self.composeToolbarView.pollBarButtonItem.accessibilityLabel = accessibilityLabel // self.composeToolbarView.pollBarButtonItem.accessibilityLabel = accessibilityLabel
self.composeToolbarView.pollButton.accessibilityLabel = accessibilityLabel // self.composeToolbarView.pollButton.accessibilityLabel = accessibilityLabel
} // }
.store(in: &disposeBag) // .store(in: &disposeBag)
//
// bind image picker toolbar state // // bind image picker toolbar state
viewModel.$attachmentServices // viewModel.$attachmentServices
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [weak self] attachmentServices in // .sink { [weak self] attachmentServices in
guard let self = self else { return } // guard let self = self else { return }
let isEnabled = attachmentServices.count < self.viewModel.maxMediaAttachments // let isEnabled = attachmentServices.count < self.viewModel.maxMediaAttachments
self.composeToolbarView.mediaBarButtonItem.isEnabled = isEnabled // self.composeToolbarView.mediaBarButtonItem.isEnabled = isEnabled
self.composeToolbarView.mediaButton.isEnabled = isEnabled // self.composeToolbarView.mediaButton.isEnabled = isEnabled
self.resetImagePicker() // self.resetImagePicker()
} // }
.store(in: &disposeBag) // .store(in: &disposeBag)
//
// bind content warning button state // // bind content warning button state
viewModel.$isContentWarningComposing // viewModel.$isContentWarningComposing
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [weak self] isContentWarningComposing in // .sink { [weak self] isContentWarningComposing in
guard let self = self else { return } // guard let self = self else { return }
let accessibilityLabel = isContentWarningComposing ? L10n.Scene.Compose.Accessibility.disableContentWarning : L10n.Scene.Compose.Accessibility.enableContentWarning // let accessibilityLabel = isContentWarningComposing ? L10n.Scene.Compose.Accessibility.disableContentWarning : L10n.Scene.Compose.Accessibility.enableContentWarning
self.composeToolbarView.contentWarningBarButtonItem.accessibilityLabel = accessibilityLabel // self.composeToolbarView.contentWarningBarButtonItem.accessibilityLabel = accessibilityLabel
self.composeToolbarView.contentWarningButton.accessibilityLabel = accessibilityLabel // self.composeToolbarView.contentWarningButton.accessibilityLabel = accessibilityLabel
} // }
.store(in: &disposeBag) // .store(in: &disposeBag)
//
// bind visibility toolbar UI // // bind visibility toolbar UI
Publishers.CombineLatest( // Publishers.CombineLatest(
viewModel.$selectedStatusVisibility, // viewModel.$selectedStatusVisibility,
viewModel.traitCollectionDidChangePublisher // viewModel.traitCollectionDidChangePublisher
) // )
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [weak self] type, _ in // .sink { [weak self] type, _ in
guard let self = self else { return } // guard let self = self else { return }
let image = type.image(interfaceStyle: self.traitCollection.userInterfaceStyle) // let image = type.image(interfaceStyle: self.traitCollection.userInterfaceStyle)
self.composeToolbarView.visibilityBarButtonItem.image = image // self.composeToolbarView.visibilityBarButtonItem.image = image
self.composeToolbarView.visibilityButton.setImage(image, for: .normal) // self.composeToolbarView.visibilityButton.setImage(image, for: .normal)
self.composeToolbarView.activeVisibilityType.value = type // self.composeToolbarView.activeVisibilityType.value = type
} // }
.store(in: &disposeBag) // .store(in: &disposeBag)
//
viewModel.$characterCount // viewModel.$characterCount
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [weak self] characterCount in // .sink { [weak self] characterCount in
guard let self = self else { return } // guard let self = self else { return }
let count = self.viewModel.composeContentLimit - characterCount // let count = self.viewModel.composeContentLimit - characterCount
self.composeToolbarView.characterCountLabel.text = "\(count)" // self.composeToolbarView.characterCountLabel.text = "\(count)"
self.characterCountLabel.text = "\(count)" // self.characterCountLabel.text = "\(count)"
let font: UIFont // let font: UIFont
let textColor: UIColor // let textColor: UIColor
let accessibilityLabel: String // let accessibilityLabel: String
switch count { // switch count {
case _ where count < 0: // case _ where count < 0:
font = .monospacedDigitSystemFont(ofSize: 24, weight: .bold) // font = .monospacedDigitSystemFont(ofSize: 24, weight: .bold)
textColor = Asset.Colors.danger.color // textColor = Asset.Colors.danger.color
accessibilityLabel = L10n.A11y.Plural.Count.inputLimitExceeds(abs(count)) // accessibilityLabel = L10n.A11y.Plural.Count.inputLimitExceeds(abs(count))
default: // default:
font = .monospacedDigitSystemFont(ofSize: 15, weight: .regular) // font = .monospacedDigitSystemFont(ofSize: 15, weight: .regular)
textColor = Asset.Colors.Label.secondary.color // textColor = Asset.Colors.Label.secondary.color
accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(count) // accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(count)
} // }
self.composeToolbarView.characterCountLabel.font = font // self.composeToolbarView.characterCountLabel.font = font
self.composeToolbarView.characterCountLabel.textColor = textColor // self.composeToolbarView.characterCountLabel.textColor = textColor
self.composeToolbarView.characterCountLabel.accessibilityLabel = accessibilityLabel // self.composeToolbarView.characterCountLabel.accessibilityLabel = accessibilityLabel
self.characterCountLabel.font = font // self.characterCountLabel.font = font
self.characterCountLabel.textColor = textColor // self.characterCountLabel.textColor = textColor
self.characterCountLabel.accessibilityLabel = accessibilityLabel // self.characterCountLabel.accessibilityLabel = accessibilityLabel
self.characterCountLabel.sizeToFit() // self.characterCountLabel.sizeToFit()
} // }
.store(in: &disposeBag) // .store(in: &disposeBag)
//
// bind custom emoji picker UI // // bind custom emoji picker UI
viewModel.customEmojiViewModel?.emojis // viewModel.customEmojiViewModel?.emojis
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] emojis in // .sink(receiveValue: { [weak self] emojis in
guard let self = self else { return } // guard let self = self else { return }
if emojis.isEmpty { // if emojis.isEmpty {
self.customEmojiPickerInputView.activityIndicatorView.startAnimating() // self.customEmojiPickerInputView.activityIndicatorView.startAnimating()
} else { // } else {
self.customEmojiPickerInputView.activityIndicatorView.stopAnimating() // self.customEmojiPickerInputView.activityIndicatorView.stopAnimating()
} // }
}) // })
.store(in: &disposeBag) // .store(in: &disposeBag)
//
// setup snap behavior // // setup snap behavior
Publishers.CombineLatest( // Publishers.CombineLatest(
viewModel.$repliedToCellFrame, // viewModel.$repliedToCellFrame,
viewModel.$collectionViewState // viewModel.$collectionViewState
) // )
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [weak self] repliedToCellFrame, collectionViewState in // .sink { [weak self] repliedToCellFrame, collectionViewState in
guard let self = self else { return } // guard let self = self else { return }
guard repliedToCellFrame != .zero else { return } // guard repliedToCellFrame != .zero else { return }
switch collectionViewState { // switch collectionViewState {
case .fold: // case .fold:
self.tableView.contentInset.top = -repliedToCellFrame.height // self.tableView.contentInset.top = -repliedToCellFrame.height
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: set contentInset.top: -%s", ((#file as NSString).lastPathComponent), #line, #function, repliedToCellFrame.height.description) // os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: set contentInset.top: -%s", ((#file as NSString).lastPathComponent), #line, #function, repliedToCellFrame.height.description)
//
case .expand: // case .expand:
self.tableView.contentInset.top = 0 // self.tableView.contentInset.top = 0
} // }
} // }
.store(in: &disposeBag) // .store(in: &disposeBag)
//
configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar.value) // configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar.value)
Publishers.CombineLatest( // Publishers.CombineLatest(
keyboardHasShortcutBar, // keyboardHasShortcutBar,
viewModel.traitCollectionDidChangePublisher // viewModel.traitCollectionDidChangePublisher
) // )
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [weak self] keyboardHasShortcutBar, _ in // .sink { [weak self] keyboardHasShortcutBar, _ in
guard let self = self else { return } // guard let self = self else { return }
self.configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar) // self.configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar)
} // }
.store(in: &disposeBag) // .store(in: &disposeBag)
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
// update MetaText without trigger call underlaying `UITextStorage.processEditing` // // update MetaText without trigger call underlaying `UITextStorage.processEditing`
_ = textEditorView.processEditing(textEditorView.textStorage) // _ = textEditorView.processEditing(textEditorView.textStorage)
markTextEditorViewBecomeFirstResponser() // markTextEditorViewBecomeFirstResponser()
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
@ -678,8 +686,8 @@ extension ComposeViewController {
} }
}) })
view.backgroundColor = backgroundColor view.backgroundColor = backgroundColor
tableView.backgroundColor = backgroundColor // tableView.backgroundColor = backgroundColor
composeToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor // composeToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor
} }
// keyboard shortcutBar // keyboard shortcutBar
@ -991,53 +999,53 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
// MARK: - UIScrollViewDelegate // MARK: - UIScrollViewDelegate
extension ComposeViewController { extension ComposeViewController {
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { // func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard scrollView === tableView else { return } // guard scrollView === tableView else { return }
//
let repliedToCellFrame = viewModel.repliedToCellFrame // let repliedToCellFrame = viewModel.repliedToCellFrame
guard repliedToCellFrame != .zero else { return } // guard repliedToCellFrame != .zero else { return }
//
// try to find some patterns: // // try to find some patterns:
// print(""" // // print("""
// repliedToCellFrame: \(viewModel.repliedToCellFrame.value.height) // // repliedToCellFrame: \(viewModel.repliedToCellFrame.value.height)
// scrollView.contentOffset.y: \(scrollView.contentOffset.y) // // scrollView.contentOffset.y: \(scrollView.contentOffset.y)
// scrollView.contentSize.height: \(scrollView.contentSize.height) // // scrollView.contentSize.height: \(scrollView.contentSize.height)
// scrollView.frame: \(scrollView.frame) // // scrollView.frame: \(scrollView.frame)
// scrollView.adjustedContentInset.top: \(scrollView.adjustedContentInset.top) // // scrollView.adjustedContentInset.top: \(scrollView.adjustedContentInset.top)
// scrollView.adjustedContentInset.bottom: \(scrollView.adjustedContentInset.bottom) // // scrollView.adjustedContentInset.bottom: \(scrollView.adjustedContentInset.bottom)
// """) // // """)
//
switch viewModel.collectionViewState { // switch viewModel.collectionViewState {
case .fold: // case .fold:
os_log("%{public}s[%{public}ld], %{public}s: fold", ((#file as NSString).lastPathComponent), #line, #function) // os_log("%{public}s[%{public}ld], %{public}s: fold", ((#file as NSString).lastPathComponent), #line, #function)
guard velocity.y < 0 else { return } // guard velocity.y < 0 else { return }
let offsetY = scrollView.contentOffset.y + scrollView.adjustedContentInset.top // let offsetY = scrollView.contentOffset.y + scrollView.adjustedContentInset.top
if offsetY < -44 { // if offsetY < -44 {
tableView.contentInset.top = 0 // tableView.contentInset.top = 0
targetContentOffset.pointee = CGPoint(x: 0, y: -scrollView.adjustedContentInset.top) // targetContentOffset.pointee = CGPoint(x: 0, y: -scrollView.adjustedContentInset.top)
viewModel.collectionViewState = .expand // viewModel.collectionViewState = .expand
} // }
//
case .expand: // case .expand:
os_log("%{public}s[%{public}ld], %{public}s: expand", ((#file as NSString).lastPathComponent), #line, #function) // os_log("%{public}s[%{public}ld], %{public}s: expand", ((#file as NSString).lastPathComponent), #line, #function)
guard velocity.y > 0 else { return } // guard velocity.y > 0 else { return }
// check if top across // // check if top across
let topOffset = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) - repliedToCellFrame.height // let topOffset = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) - repliedToCellFrame.height
//
// check if bottom bounce // // check if bottom bounce
let bottomOffsetY = scrollView.contentOffset.y + (scrollView.frame.height - scrollView.adjustedContentInset.bottom) // let bottomOffsetY = scrollView.contentOffset.y + (scrollView.frame.height - scrollView.adjustedContentInset.bottom)
let bottomOffset = bottomOffsetY - scrollView.contentSize.height // let bottomOffset = bottomOffsetY - scrollView.contentSize.height
//
if topOffset > 44 { // if topOffset > 44 {
// do not interrupt user scrolling // // do not interrupt user scrolling
viewModel.collectionViewState = .fold // viewModel.collectionViewState = .fold
} else if bottomOffset > 44 { // } else if bottomOffset > 44 {
tableView.contentInset.top = -repliedToCellFrame.height // tableView.contentInset.top = -repliedToCellFrame.height
targetContentOffset.pointee = CGPoint(x: 0, y: -repliedToCellFrame.height) // targetContentOffset.pointee = CGPoint(x: 0, y: -repliedToCellFrame.height)
viewModel.collectionViewState = .fold // viewModel.collectionViewState = .fold
} // }
} // }
} // }
} }
// MARK: - UITableViewDelegate // MARK: - UITableViewDelegate

View File

@ -6,10 +6,12 @@
// //
import UIKit import UIKit
import SwiftUI
import Combine import Combine
import AlamofireImage import AlamofireImage
import MastodonAsset import MastodonAsset
import MastodonLocalization import MastodonLocalization
import UIHostingConfigurationBackport
final class ComposeStatusAttachmentTableViewCell: UITableViewCell { final class ComposeStatusAttachmentTableViewCell: UITableViewCell {
@ -75,85 +77,91 @@ extension ComposeStatusAttachmentTableViewCell {
} }
.store(in: &observations) .store(in: &observations)
self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [ self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
weak self [weak self] collectionView, indexPath, item -> UICollectionViewCell? in
] collectionView, indexPath, item -> UICollectionViewCell? in
guard let self = self else { return UICollectionViewCell() } guard let self = self else { return UICollectionViewCell() }
switch item { switch item {
case .attachment(let attachmentService): case .attachment(let attachmentService):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell
cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value cell.contentConfiguration = UIHostingConfigurationBackport {
cell.delegate = self.composeStatusAttachmentCollectionViewCellDelegate HStack {
attachmentService.thumbnailImage Image(systemName: "star")
.receive(on: DispatchQueue.main) Text("Favorites")
.sink { [weak cell] thumbnailImage in Spacer()
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) // cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value
NotificationCenter.default.publisher( // cell.delegate = self.composeStatusAttachmentCollectionViewCellDelegate
for: UITextView.textDidChangeNotification, // attachmentService.thumbnailImage
object: cell.attachmentContainerView.descriptionTextView // .receive(on: DispatchQueue.main)
) // .sink { [weak cell] thumbnailImage in
.receive(on: DispatchQueue.main) // guard let cell = cell else { return }
.sink { notification in // let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1)
guard let textField = notification.object as? UITextView else { return } // guard let image = thumbnailImage else {
let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) // let placeholder = UIImage.placeholder(
attachmentService.description.value = text // size: size,
} // color: ThemeService.shared.currentTheme.value.systemGroupedBackgroundColor
.store(in: &cell.disposeBag) // )
// .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 return cell
} }
} }

View File

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

View File

@ -12,6 +12,7 @@ import GameplayKit
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MastodonSDK import MastodonSDK
import MastodonCore
final class DiscoveryCommunityViewModel { final class DiscoveryCommunityViewModel {

View File

@ -77,31 +77,8 @@ final class NotificationTimelineViewModel {
} }
extension NotificationTimelineViewModel { extension NotificationTimelineViewModel {
enum Scope: Hashable, CaseIterable {
case everything typealias Scope = APIService.NotificationScope
case mentions
var includeTypes: [MastodonNotificationType]? {
switch self {
case .everything: return nil
case .mentions: return [.mention, .status]
}
}
var excludeTypes: [MastodonNotificationType]? {
switch self {
case .everything: return nil
case .mentions: return [.follow, .followRequest, .reblog, .favourite, .poll]
}
}
var _excludeTypes: [Mastodon.Entity.Notification.NotificationType]? {
switch self {
case .everything: return nil
case .mentions: return [.follow, .followRequest, .reblog, .favourite, .poll]
}
}
}
static func feedPredicate( static func feedPredicate(
authenticationBox: MastodonAuthenticationBox, authenticationBox: MastodonAuthenticationBox,

View File

@ -12,9 +12,6 @@ import AuthenticationServices
final class MastodonAuthenticationController { final class MastodonAuthenticationController {
static let callbackURLScheme = "mastodon"
static let callbackURL = "mastodon://joinmastodon.org/oauth"
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
// input // input
@ -43,7 +40,7 @@ extension MastodonAuthenticationController {
private func authentication() { private func authentication() {
authenticationSession = ASWebAuthenticationSession( authenticationSession = ASWebAuthenticationSession(
url: authenticateURL, url: authenticateURL,
callbackURLScheme: MastodonAuthenticationController.callbackURLScheme callbackURLScheme: APIService.callbackURLScheme
) { [weak self] callback, error in ) { [weak self] callback, error in
guard let self = self else { return } guard let self = self else { return }
os_log("%{public}s[%{public}ld], %{public}s: callback: %s, error: %s", ((#file as NSString).lastPathComponent), #line, #function, callback?.debugDescription ?? "<nil>", error.debugDescription) os_log("%{public}s[%{public}ld], %{public}s: callback: %s, error: %s", ((#file as NSString).lastPathComponent), #line, #function, callback?.debugDescription ?? "<nil>", error.debugDescription)

View File

@ -9,6 +9,7 @@ import os.log
import UIKit import UIKit
import Combine import Combine
import CoreDataStack import CoreDataStack
import MastodonCore
final class RootSplitViewController: UISplitViewController, NeedsDependency { final class RootSplitViewController: UISplitViewController, NeedsDependency {

View File

@ -1,79 +0,0 @@
//
// StatusPublishService.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-26.
//
import os.log
import Foundation
import Intents
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
import UIKit
final class StatusPublishService {
var disposeBag = Set<AnyCancellable>()
let workingQueue = DispatchQueue(label: "org.joinmastodon.app.StatusPublishService.working-queue")
// input
var viewModels = CurrentValueSubject<[ComposeViewModel], Never>([]) // use strong reference to retain the view models
// output
let composeViewModelDidUpdatePublisher = PassthroughSubject<Void, Never>()
let latestPublishingComposeViewModel = CurrentValueSubject<ComposeViewModel?, Never>(nil)
init() {
Publishers.CombineLatest(
viewModels.eraseToAnyPublisher(),
composeViewModelDidUpdatePublisher.eraseToAnyPublisher()
)
.map { viewModels, _ in viewModels.last }
.assign(to: \.value, on: latestPublishingComposeViewModel)
.store(in: &disposeBag)
}
}
extension StatusPublishService {
func publish(composeViewModel: ComposeViewModel) {
workingQueue.sync {
guard !self.viewModels.value.contains(where: { $0 === composeViewModel }) else { return }
self.viewModels.value = self.viewModels.value + [composeViewModel]
composeViewModel.publishStateMachinePublisher
.receive(on: DispatchQueue.main)
.sink { [weak self, weak composeViewModel] state in
guard let self = self else { return }
guard let composeViewModel = composeViewModel else { return }
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: composeViewModelDidUpdate", ((#file as NSString).lastPathComponent), #line, #function)
self.composeViewModelDidUpdatePublisher.send()
switch state {
case is ComposeViewModel.PublishState.Finish:
self.remove(composeViewModel: composeViewModel)
default:
break
}
}
.store(in: &composeViewModel.disposeBag) // cancel subscription when viewModel dealloc
}
}
func remove(composeViewModel: ComposeViewModel) {
workingQueue.async {
var viewModels = self.viewModels.value
viewModels.removeAll(where: { $0 === composeViewModel })
self.viewModels.value = viewModels
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: composeViewModel removed", ((#file as NSString).lastPathComponent), #line, #function)
}
}
}

View File

@ -1,15 +0,0 @@
//
// DocumentStore.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021-1-27.
//
import UIKit
import Combine
import MastodonSDK
class DocumentStore: ObservableObject {
let appStartUpTimestamp = Date()
var defaultRevealStatusDict: [Mastodon.Entity.Status.ID: Bool] = [:]
}

View File

@ -1,14 +0,0 @@
//
// ViewStateStore.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021-1-27.
//
import Combine
struct ViewStateStore {
}
enum ViewState { }

View File

@ -8,9 +8,9 @@
import os.log import os.log
import UIKit import UIKit
import UserNotifications import UserNotifications
import AppShared
import AVFoundation import AVFoundation
@_exported import MastodonUI import MastodonCore
import MastodonUI
@main @main
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {

View File

@ -11,6 +11,7 @@ import Combine
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MastodonSDK import MastodonSDK
import MastodonCore
final class SendPostIntentHandler: NSObject { final class SendPostIntentHandler: NSObject {
@ -18,8 +19,12 @@ final class SendPostIntentHandler: NSObject {
let coreDataStack = CoreDataStack() let coreDataStack = CoreDataStack()
lazy var managedObjectContext = coreDataStack.persistentContainer.viewContext lazy var managedObjectContext = coreDataStack.persistentContainer.viewContext
lazy var api = APIService.shared lazy var api: APIService = {
let backgroundManagedObjectContext = coreDataStack.newTaskContext()
return APIService(
backgroundManagedObjectContext: backgroundManagedObjectContext
)
}()
} }
// MARK: - SendPostIntentHandling // MARK: - SendPostIntentHandling

View File

@ -9,6 +9,7 @@ import Foundation
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import Intents import Intents
import MastodonCore
extension Account { extension Account {

View File

@ -1,33 +0,0 @@
//
// APIService.swift
// MastodonIntent
//
// Created by Cirno MainasuK on 2021-7-26.
//
import os.log
import Foundation
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
// Replica APIService for share extension
final class APIService {
var disposeBag = Set<AnyCancellable>()
static let shared = APIService()
// internal
let session: URLSession
// output
let error = PassthroughSubject<APIError, Never>()
private init() {
self.session = URLSession(configuration: .default)
}
}

View File

@ -0,0 +1,241 @@
{
"object": {
"pins": [
{
"package": "Alamofire",
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
"state": {
"branch": null,
"revision": "8dd85aee02e39dd280c75eef88ffdb86eed4b07b",
"version": "5.6.2"
}
},
{
"package": "AlamofireImage",
"repositoryURL": "https://github.com/Alamofire/AlamofireImage.git",
"state": {
"branch": null,
"revision": "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10",
"version": "4.2.0"
}
},
{
"package": "CommonOSLog",
"repositoryURL": "https://github.com/MainasuK/CommonOSLog",
"state": {
"branch": null,
"revision": "c121624a30698e9886efe38aebb36ff51c01b6c2",
"version": "0.1.1"
}
},
{
"package": "FaviconFinder",
"repositoryURL": "https://github.com/will-lumley/FaviconFinder.git",
"state": {
"branch": null,
"revision": "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a",
"version": "3.3.0"
}
},
{
"package": "FLAnimatedImage",
"repositoryURL": "https://github.com/Flipboard/FLAnimatedImage.git",
"state": {
"branch": null,
"revision": "d4f07b6f164d53c1212c3e54d6460738b1981e9f",
"version": "1.0.17"
}
},
{
"package": "FPSIndicator",
"repositoryURL": "https://github.com/MainasuK/FPSIndicator.git",
"state": {
"branch": null,
"revision": "e4a5067ccd5293b024c767f09e51056afd4a4796",
"version": "1.1.0"
}
},
{
"package": "Fuzi",
"repositoryURL": "https://github.com/cezheng/Fuzi.git",
"state": {
"branch": null,
"revision": "f08c8323da21e985f3772610753bcfc652c2103f",
"version": "3.1.3"
}
},
{
"package": "KeychainAccess",
"repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git",
"state": {
"branch": null,
"revision": "84e546727d66f1adc5439debad16270d0fdd04e7",
"version": "4.2.2"
}
},
{
"package": "MetaTextKit",
"repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git",
"state": {
"branch": null,
"revision": "dcd5255d6930c2fab408dc8562c577547e477624",
"version": "2.2.5"
}
},
{
"package": "Nuke",
"repositoryURL": "https://github.com/kean/Nuke.git",
"state": {
"branch": null,
"revision": "a002b7fd786f2df2ed4333fe73a9727499fd9d97",
"version": "10.11.2"
}
},
{
"package": "NukeFLAnimatedImagePlugin",
"repositoryURL": "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git",
"state": {
"branch": null,
"revision": "b59c346a7d536336db3b0f12c72c6e53ee709e16",
"version": "8.0.0"
}
},
{
"package": "Pageboy",
"repositoryURL": "https://github.com/uias/Pageboy",
"state": {
"branch": null,
"revision": "af8fa81788b893205e1ff42ddd88c5b0b315d7c5",
"version": "3.7.0"
}
},
{
"package": "PanModal",
"repositoryURL": "https://github.com/slackhq/PanModal.git",
"state": {
"branch": null,
"revision": "b012aecb6b67a8e46369227f893c12544846613f",
"version": "1.2.7"
}
},
{
"package": "SDWebImage",
"repositoryURL": "https://github.com/SDWebImage/SDWebImage.git",
"state": {
"branch": null,
"revision": "9248fe561a2a153916fb9597e3af4434784c6d32",
"version": "5.13.4"
}
},
{
"package": "swift-collections",
"repositoryURL": "https://github.com/apple/swift-collections.git",
"state": {
"branch": null,
"revision": "f504716c27d2e5d4144fa4794b12129301d17729",
"version": "1.0.3"
}
},
{
"package": "swift-nio",
"repositoryURL": "https://github.com/apple/swift-nio.git",
"state": {
"branch": null,
"revision": "546610d52b19be3e19935e0880bb06b9c03f5cef",
"version": "1.14.4"
}
},
{
"package": "swift-nio-zlib-support",
"repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git",
"state": {
"branch": null,
"revision": "37760e9a52030bb9011972c5213c3350fa9d41fd",
"version": "1.0.0"
}
},
{
"package": "SwiftSoup",
"repositoryURL": "https://github.com/scinfu/SwiftSoup.git",
"state": {
"branch": null,
"revision": "6778575285177365cbad3e5b8a72f2a20583cfec",
"version": "2.4.3"
}
},
{
"package": "Introspect",
"repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git",
"state": {
"branch": null,
"revision": "f2616860a41f9d9932da412a8978fec79c06fe24",
"version": "0.1.4"
}
},
{
"package": "SwiftyJSON",
"repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git",
"state": {
"branch": null,
"revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07",
"version": "5.0.1"
}
},
{
"package": "TabBarPager",
"repositoryURL": "https://github.com/TwidereProject/TabBarPager.git",
"state": {
"branch": null,
"revision": "488aa66d157a648901b61721212c0dec23d27ee5",
"version": "0.1.0"
}
},
{
"package": "Tabman",
"repositoryURL": "https://github.com/uias/Tabman",
"state": {
"branch": null,
"revision": "4a4f7c755b875ffd4f9ef10d67a67883669d2465",
"version": "2.13.0"
}
},
{
"package": "ThirdPartyMailer",
"repositoryURL": "https://github.com/vtourraine/ThirdPartyMailer.git",
"state": {
"branch": null,
"revision": "44c1cfaa6969963f22691aa67f88a69e3b6d651f",
"version": "2.1.0"
}
},
{
"package": "TOCropViewController",
"repositoryURL": "https://github.com/TimOliver/TOCropViewController.git",
"state": {
"branch": null,
"revision": "d0470491f56e734731bbf77991944c0dfdee3e0e",
"version": "2.6.1"
}
},
{
"package": "UIHostingConfigurationBackport",
"repositoryURL": "https://github.com/woxtu/UIHostingConfigurationBackport.git",
"state": {
"branch": null,
"revision": "6091f2d38faa4b24fc2ca0389c651e2f666624a3",
"version": "0.1.0"
}
},
{
"package": "UITextView+Placeholder",
"repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git",
"state": {
"branch": null,
"revision": "20f513ded04a040cdf5467f0891849b1763ede3b",
"version": "1.4.1"
}
}
]
},
"version": 1
}

View File

@ -16,6 +16,7 @@ let package = Package(
"CoreDataStack", "CoreDataStack",
"MastodonAsset", "MastodonAsset",
"MastodonCommon", "MastodonCommon",
"MastodonCore",
"MastodonExtension", "MastodonExtension",
"MastodonLocalization", "MastodonLocalization",
"MastodonSDK", "MastodonSDK",
@ -23,17 +24,30 @@ let package = Package(
]) ])
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"),
.package(url: "https://github.com/apple/swift-nio.git", from: "1.0.0"), .package(name: "FaviconFinder", url: "https://github.com/will-lumley/FaviconFinder.git", from: "3.2.2"),
.package(url: "https://github.com/kean/Nuke.git", from: "10.3.1"), .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.3"),
.package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"), .package(name: "UITextView+Placeholder", url: "https://github.com/MainasuK/UITextView-Placeholder.git", from: "1.4.1"),
.package(url: "https://github.com/TwidereProject/MetaTextKit.git", .exact("2.2.5")),
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.4.0"), .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.4.0"),
.package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"), .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"),
.package(name: "NukeFLAnimatedImagePlugin", url: "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", from: "8.0.0"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.3"),
.package(name: "UITextView+Placeholder", url: "https://github.com/MainasuK/UITextView-Placeholder.git", from: "1.4.1"), .package(url: "https://github.com/apple/swift-nio.git", from: "1.0.0"),
.package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.3"), .package(url: "https://github.com/cezheng/Fuzi.git", from: "3.1.3"),
.package(name: "FaviconFinder", url: "https://github.com/will-lumley/FaviconFinder.git", from: "3.2.2"), .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"),
.package(url: "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", from: "8.0.0"),
.package(url: "https://github.com/kean/Nuke.git", from: "10.3.1"),
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"),
.package(url: "https://github.com/MainasuK/CommonOSLog", from: "0.1.1"),
.package(url: "https://github.com/MainasuK/FPSIndicator.git", from: "1.0.0"),
.package(url: "https://github.com/slackhq/PanModal.git", from: "1.2.7"),
.package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"),
.package(url: "https://github.com/TimOliver/TOCropViewController.git", from: "2.6.1"),
.package(url: "https://github.com/TwidereProject/MetaTextKit.git", .exact("2.2.5")),
.package(url: "https://github.com/TwidereProject/TabBarPager.git", from: "0.1.0"),
.package(url: "https://github.com/uias/Tabman", from: "2.13.0"),
.package(url: "https://github.com/vtourraine/ThirdPartyMailer.git", from: "2.1.0"),
.package(url: "https://github.com/woxtu/UIHostingConfigurationBackport.git", from: "0.1.0"),
.package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.12.0"),
], ],
targets: [ targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets are the basic building blocks of a package. A target can define a module or a test suite.
@ -60,6 +74,22 @@ let package = Package(
"MastodonExtension" "MastodonExtension"
] ]
), ),
.target(
name: "MastodonCore",
dependencies: [
"CoreDataStack",
"MastodonAsset",
"MastodonCommon",
"MastodonLocalization",
"MastodonSDK",
.product(name: "Alamofire", package: "Alamofire"),
.product(name: "AlamofireImage", package: "AlamofireImage"),
.product(name: "CommonOSLog", package: "CommonOSLog"),
.product(name: "ArkanaKeys", package: "ArkanaKeys"),
.product(name: "KeychainAccess", package: "KeychainAccess"),
.product(name: "MetaTextKit", package: "MetaTextKit")
]
),
.target( .target(
name: "MastodonExtension", name: "MastodonExtension",
dependencies: [] dependencies: []
@ -78,20 +108,20 @@ let package = Package(
.target( .target(
name: "MastodonUI", name: "MastodonUI",
dependencies: [ dependencies: [
"CoreDataStack", "MastodonCore",
"MastodonSDK",
"MastodonExtension",
"MastodonAsset",
"MastodonLocalization",
.product(name: "Alamofire", package: "Alamofire"),
.product(name: "AlamofireImage", package: "AlamofireImage"),
.product(name: "FLAnimatedImage", package: "FLAnimatedImage"), .product(name: "FLAnimatedImage", package: "FLAnimatedImage"),
.product(name: "FaviconFinder", package: "FaviconFinder"), .product(name: "FaviconFinder", package: "FaviconFinder"),
.product(name: "MetaTextKit", package: "MetaTextKit"),
.product(name: "Nuke", package: "Nuke"), .product(name: "Nuke", package: "Nuke"),
.product(name: "NukeFLAnimatedImagePlugin", package: "NukeFLAnimatedImagePlugin"),
.product(name: "Introspect", package: "Introspect"), .product(name: "Introspect", package: "Introspect"),
.product(name: "UITextView+Placeholder", package: "UITextView+Placeholder"), .product(name: "UITextView+Placeholder", package: "UITextView+Placeholder"),
.product(name: "UIHostingConfigurationBackport", package: "UIHostingConfigurationBackport"),
.product(name: "TabBarPager", package: "TabBarPager"),
.product(name: "ThirdPartyMailer", package: "ThirdPartyMailer"),
.product(name: "OrderedCollections", package: "swift-collections"),
.product(name: "Tabman", package: "Tabman"),
.product(name: "MetaTextKit", package: "MetaTextKit"),
.product(name: "CropViewController", package: "TOCropViewController"),
.product(name: "PanModal", package: "PanModal"),
] ]
), ),
.testTarget( .testTarget(

View File

@ -113,6 +113,15 @@ public final class CoreDataStack {
} }
extension CoreDataStack {
public func newTaskContext() -> NSManagedObjectContext {
let taskContext = persistentContainer.newBackgroundContext()
taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
taskContext.undoManager = nil
return taskContext
}
}
extension CoreDataStack { extension CoreDataStack {
public func rebuild() { public func rebuild() {

View File

@ -9,7 +9,7 @@ import UIKit
extension UserDefaults { extension UserDefaults {
@objc dynamic var preferredUsingDefaultBrowser: Bool { @objc public dynamic var preferredUsingDefaultBrowser: Bool {
get { get {
register(defaults: [#function: false]) register(defaults: [#function: false])
return bool(forKey: #function) return bool(forKey: #function)

View File

@ -7,6 +7,7 @@
import UIKit import UIKit
import CryptoKit import CryptoKit
import MastodonExtension
extension UserDefaults { extension UserDefaults {
// always use hash value (SHA256) from accessToken as key // always use hash value (SHA256) from accessToken as key
@ -38,3 +39,15 @@ extension UserDefaults {
} }
} }
extension UserDefaults {
@objc public dynamic var notificationBadgeCount: Int {
get {
register(defaults: [#function: 0])
return integer(forKey: #function)
}
set { self[#function] = newValue }
}
}

View File

@ -9,14 +9,14 @@ import Foundation
extension UserDefaults { extension UserDefaults {
@objc dynamic var processCompletedCount: Int { @objc public dynamic var processCompletedCount: Int {
get { get {
return integer(forKey: #function) return integer(forKey: #function)
} }
set { self[#function] = newValue } set { self[#function] = newValue }
} }
@objc dynamic var lastVersionPromptedForReview: String? { @objc public dynamic var lastVersionPromptedForReview: String? {
get { get {
return string(forKey: #function) return string(forKey: #function)
} }

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
extension UserDefaults { extension UserDefaults {
@objc dynamic var didShowMultipleAccountSwitchWizard: Bool { @objc public dynamic var didShowMultipleAccountSwitchWizard: Bool {
get { return bool(forKey: #function) } get { return bool(forKey: #function) }
set { self[#function] = newValue } set { self[#function] = newValue }
} }

View File

@ -1,44 +1,42 @@
// //
// AppContext.swift // AppContext.swift
// Mastodon //
// //
// Created by Cirno MainasuK on 2021-1-27. // Created by MainasuK on 22/9/30.
// //
import os.log import os.log
import UIKit import UIKit
import SwiftUI
import Combine import Combine
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import AlamofireImage import AlamofireImage
import MastodonUI
class AppContext: ObservableObject { public class AppContext: ObservableObject {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
@Published var viewStateStore = ViewStateStore() public let coreDataStack: CoreDataStack
public let managedObjectContext: NSManagedObjectContext
let coreDataStack: CoreDataStack public let backgroundManagedObjectContext: NSManagedObjectContext
let managedObjectContext: NSManagedObjectContext
let backgroundManagedObjectContext: NSManagedObjectContext
let apiService: APIService public let apiService: APIService
let authenticationService: AuthenticationService public let authenticationService: AuthenticationService
let emojiService: EmojiService public let emojiService: EmojiService
let statusPublishService = StatusPublishService() public let statusPublishService = StatusPublishService()
let notificationService: NotificationService public let notificationService: NotificationService
let settingService: SettingService public let settingService: SettingService
let instanceService: InstanceService public let instanceService: InstanceService
let blockDomainService: BlockDomainService public let blockDomainService: BlockDomainService
let statusFilterService: StatusFilterService public let statusFilterService: StatusFilterService
let photoLibraryService = PhotoLibraryService() public let photoLibraryService = PhotoLibraryService()
let placeholderImageCacheService = PlaceholderImageCacheService() public let placeholderImageCacheService = PlaceholderImageCacheService()
let blurhashImageCacheService = BlurhashImageCacheService.shared public let blurhashImageCacheService = BlurhashImageCacheService.shared
let documentStore: DocumentStore public let documentStore: DocumentStore
private var documentStoreSubscription: AnyCancellable! private var documentStoreSubscription: AnyCancellable!
let overrideTraitCollection = CurrentValueSubject<UITraitCollection?, Never>(nil) let overrideTraitCollection = CurrentValueSubject<UITraitCollection?, Never>(nil)
@ -46,8 +44,8 @@ class AppContext: ObservableObject {
.autoconnect() .autoconnect()
.share() .share()
.eraseToAnyPublisher() .eraseToAnyPublisher()
init() { public init() {
let _coreDataStack = CoreDataStack() let _coreDataStack = CoreDataStack()
let _managedObjectContext = _coreDataStack.persistentContainer.viewContext let _managedObjectContext = _coreDataStack.persistentContainer.viewContext
let _backgroundManagedObjectContext = _coreDataStack.persistentContainer.newBackgroundContext() let _backgroundManagedObjectContext = _coreDataStack.persistentContainer.newBackgroundContext()

View File

@ -9,8 +9,8 @@
import Foundation import Foundation
import CryptoKit import CryptoKit
import KeychainAccess import KeychainAccess
import Keys
import MastodonCommon import MastodonCommon
import ArkanaKeys
public final class AppSecret { public final class AppSecret {
@ -36,12 +36,10 @@ public final class AppSecret {
}() }()
init() { init() {
let keys = MastodonKeys()
#if DEBUG #if DEBUG
self.notificationEndpoint = keys.notification_endpoint_debug self.notificationEndpoint = Keys.Debug().notificationEndpoint
#else #else
self.notificationEndpoint = keys.notification_endpoint self.notificationEndpoint = Keys.Release().notificationEndpoint
#endif #endif
} }

View File

@ -0,0 +1,32 @@
//
// MastodonAuthenticationBox.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-7-20.
//
import Foundation
import CoreDataStack
import MastodonSDK
public struct MastodonAuthenticationBox: UserIdentifier {
public let authenticationRecord: ManagedObjectRecord<MastodonAuthentication>
public let domain: String
public let userID: MastodonUser.ID
public let appAuthorization: Mastodon.API.OAuth.Authorization
public let userAuthorization: Mastodon.API.OAuth.Authorization
public init(
authenticationRecord: ManagedObjectRecord<MastodonAuthentication>,
domain: String,
userID: MastodonUser.ID,
appAuthorization: Mastodon.API.OAuth.Authorization,
userAuthorization: Mastodon.API.OAuth.Authorization
) {
self.authenticationRecord = authenticationRecord
self.domain = domain
self.userID = userID
self.appAuthorization = appAuthorization
self.userAuthorization = userAuthorization
}
}

View File

@ -0,0 +1,15 @@
//
// DocumentStore.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021-1-27.
//
import UIKit
import Combine
import MastodonSDK
public class DocumentStore: ObservableObject {
public let appStartUpTimestamp = Date()
public var defaultRevealStatusDict: [Mastodon.Entity.Status.ID: Bool] = [:]
}

View File

@ -29,3 +29,15 @@ extension Collection where Element == Mastodon.Entity.Emoji {
return dictionary return dictionary
} }
} }
extension MastodonEmoji {
public convenience init(emoji: Mastodon.Entity.Emoji) {
self.init(
code: emoji.shortcode,
url: emoji.url,
staticURL: emoji.staticURL,
visibleInPicker: emoji.visibleInPicker,
category: emoji.category
)
}
}

View File

@ -1,13 +1,13 @@
// //
// MastodonUser.swift // MastodonUser.swift
// // Mastodon
// //
// Created by MainasuK on 2022-4-14. // Created by MainasuK Cirno on 2021/2/3.
// //
import Foundation import Foundation
import CoreDataStack import CoreDataStack
import MastodonCommon import MastodonSDK
extension MastodonUser { extension MastodonUser {
@ -55,3 +55,21 @@ extension MastodonUser {
} }
} }
extension MastodonUser {
public var profileURL: URL {
if let urlString = self.url,
let url = URL(string: urlString) {
return url
} else {
return URL(string: "https://\(self.domain)/@\(username)")!
}
}
public var activityItems: [Any] {
var items: [Any] = []
items.append(profileURL)
return items
}
}

View File

@ -12,7 +12,7 @@ import CoreData
import CoreDataStack import CoreDataStack
import MastodonSDK import MastodonSDK
final class SettingFetchedResultController: NSObject { public final class SettingFetchedResultController: NSObject {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
@ -21,9 +21,9 @@ final class SettingFetchedResultController: NSObject {
// input // input
// output // output
let settings = CurrentValueSubject<[Setting], Never>([]) public let settings = CurrentValueSubject<[Setting], Never>([])
init(managedObjectContext: NSManagedObjectContext, additionalPredicate: NSPredicate?) { public init(managedObjectContext: NSManagedObjectContext, additionalPredicate: NSPredicate?) {
self.fetchedResultsController = { self.fetchedResultsController = {
let fetchRequest = Setting.sortedFetchRequest let fetchRequest = Setting.sortedFetchRequest
fetchRequest.returnsObjectsAsFaults = false fetchRequest.returnsObjectsAsFaults = false
@ -55,7 +55,7 @@ final class SettingFetchedResultController: NSObject {
// MARK: - NSFetchedResultsControllerDelegate // MARK: - NSFetchedResultsControllerDelegate
extension SettingFetchedResultController: NSFetchedResultsControllerDelegate { extension SettingFetchedResultController: NSFetchedResultsControllerDelegate {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
let objects = fetchedResultsController.fetchedObjects ?? [] let objects = fetchedResultsController.fetchedObjects ?? []

View File

@ -11,7 +11,6 @@ import Combine
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MastodonSDK import MastodonSDK
import MastodonUI
final class StatusFetchedResultsController: NSObject { final class StatusFetchedResultsController: NSObject {

View File

@ -11,7 +11,6 @@ import Combine
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MastodonSDK import MastodonSDK
import MastodonUI
final class UserFetchedResultsController: NSObject { final class UserFetchedResultsController: NSObject {

View File

@ -19,8 +19,8 @@ extension Persistence.MastodonUser {
public let entity: Mastodon.Entity.Account public let entity: Mastodon.Entity.Account
public let cache: Persistence.PersistCache<MastodonUser>? public let cache: Persistence.PersistCache<MastodonUser>?
public let networkDate: Date public let networkDate: Date
public let log = OSLog.api public let log = Logger(subsystem: "MastodonUser", category: "Persistence")
public init( public init(
domain: String, domain: String,
entity: Mastodon.Entity.Account, entity: Mastodon.Entity.Account,
@ -127,8 +127,8 @@ extension Persistence.MastodonUser {
public let entity: Mastodon.Entity.Relationship public let entity: Mastodon.Entity.Relationship
public let me: MastodonUser public let me: MastodonUser
public let networkDate: Date public let networkDate: Date
public let log = OSLog.api public let log = Logger(subsystem: "MastodonUser", category: "Persistence")
public init( public init(
entity: Mastodon.Entity.Relationship, entity: Mastodon.Entity.Relationship,
me: MastodonUser, me: MastodonUser,

View File

@ -19,8 +19,8 @@ extension Persistence.Notification {
public let entity: Mastodon.Entity.Notification public let entity: Mastodon.Entity.Notification
public let me: MastodonUser public let me: MastodonUser
public let networkDate: Date public let networkDate: Date
public let log = OSLog.api public let log = Logger(subsystem: "Notification", category: "Persistence")
public init( public init(
domain: String, domain: String,
entity: Mastodon.Entity.Notification, entity: Mastodon.Entity.Notification,

View File

@ -18,8 +18,7 @@ extension Persistence.Poll {
public let entity: Mastodon.Entity.Poll public let entity: Mastodon.Entity.Poll
public let me: MastodonUser? public let me: MastodonUser?
public let networkDate: Date public let networkDate: Date
public let log = OSLog.api public let log = Logger(subsystem: "Poll", category: "Persistence")
public init( public init(
domain: String, domain: String,
entity: Mastodon.Entity.Poll, entity: Mastodon.Entity.Poll,

View File

@ -18,7 +18,7 @@ extension Persistence.PollOption {
public let entity: Mastodon.Entity.Poll.Option public let entity: Mastodon.Entity.Poll.Option
public let me: MastodonUser? public let me: MastodonUser?
public let networkDate: Date public let networkDate: Date
public let log = OSLog.api public let log = Logger(subsystem: "PollOption", category: "Persistence")
public init( public init(
index: Int, index: Int,

View File

@ -17,8 +17,7 @@ extension Persistence.SearchHistory {
public let entity: Entity public let entity: Entity
public let me: MastodonUser public let me: MastodonUser
public let now: Date public let now: Date
public let log = OSLog.api public let log = Logger(subsystem: "SearchHistory", category: "Persistence")
public init( public init(
entity: Entity, entity: Entity,
me: MastodonUser, me: MastodonUser,

View File

@ -21,8 +21,8 @@ extension Persistence.Status {
public let statusCache: Persistence.PersistCache<Status>? public let statusCache: Persistence.PersistCache<Status>?
public let userCache: Persistence.PersistCache<MastodonUser>? public let userCache: Persistence.PersistCache<MastodonUser>?
public let networkDate: Date public let networkDate: Date
public let log = OSLog.api public let log = Logger(subsystem: "Status", category: "Persistence")
public init( public init(
domain: String, domain: String,
entity: Mastodon.Entity.Status, entity: Mastodon.Entity.Status,

View File

@ -18,7 +18,7 @@ extension Persistence.Tag {
public let entity: Mastodon.Entity.Tag public let entity: Mastodon.Entity.Tag
public let me: MastodonUser? public let me: MastodonUser?
public let networkDate: Date public let networkDate: Date
public let log = OSLog.api public let log = Logger(subsystem: "Tag", category: "Persistence")
public init( public init(
domain: String, domain: String,

View File

@ -10,12 +10,12 @@ import MastodonSDK
import MastodonLocalization import MastodonLocalization
extension APIService { extension APIService {
enum APIError: Error { public enum APIError: Error {
case implicit(ErrorReason) case implicit(ErrorReason)
case explicit(ErrorReason) case explicit(ErrorReason)
enum ErrorReason { public enum ErrorReason {
// application internal error // application internal error
case authenticationMissing case authenticationMissing
case badRequest case badRequest
@ -60,7 +60,7 @@ extension APIService.APIError: LocalizedError {
} }
} }
var failureReason: String? { public var failureReason: String? {
switch errorReason { switch errorReason {
case .authenticationMissing: return "Account credential not found." case .authenticationMissing: return "Account credential not found."
case .badRequest: return "Request invalid." case .badRequest: return "Request invalid."
@ -75,7 +75,7 @@ extension APIService.APIError: LocalizedError {
} }
} }
var helpAnchor: String? { public var helpAnchor: String? {
switch errorReason { switch errorReason {
case .authenticationMissing: return "Please request after authenticated." case .authenticationMissing: return "Please request after authenticated."
case .badRequest: return L10n.Common.Alerts.Common.pleaseTryAgain case .badRequest: return L10n.Common.Alerts.Common.pleaseTryAgain

View File

@ -9,6 +9,7 @@ import os.log
import Foundation import Foundation
import Combine import Combine
import CommonOSLog import CommonOSLog
import MastodonCommon
import MastodonSDK import MastodonSDK
extension APIService { extension APIService {
@ -59,7 +60,7 @@ extension APIService {
authorization: authorization authorization: authorization
) )
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in .flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
let log = OSLog.api let logger = Logger(subsystem: "Account", category: "API")
let account = response.value let account = response.value
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
@ -74,7 +75,7 @@ extension APIService {
) )
) )
let flag = result.isNewInsertion ? "+" : "-" let flag = result.isNewInsertion ? "+" : "-"
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: mastodon user [%s](%s)%s verifed", ((#file as NSString).lastPathComponent), #line, #function, flag, result.user.id, result.user.username) logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): mastodon user [\(flag)](\(result.user.id))\(result.user.username) verifed")
} }
.setFailureType(to: Error.self) .setFailureType(to: Error.self)
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Account> in .tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Account> in
@ -95,7 +96,7 @@ extension APIService {
query: Mastodon.API.Account.UpdateCredentialQuery, query: Mastodon.API.Account.UpdateCredentialQuery,
authorization: Mastodon.API.OAuth.Authorization authorization: Mastodon.API.OAuth.Authorization
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Account> { ) async throws -> Mastodon.Response.Content<Mastodon.Entity.Account> {
let logger = Logger(subsystem: "APIService", category: "Account") let logger = Logger(subsystem: "Account", category: "API")
let response = try await Mastodon.API.Account.updateCredentials( let response = try await Mastodon.API.Account.updateCredentials(
session: session, session: session,

View File

@ -24,7 +24,7 @@ extension APIService {
func createApplication(domain: String) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Application>, Error> { func createApplication(domain: String) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Application>, Error> {
let query = Mastodon.API.App.CreateQuery( let query = Mastodon.API.App.CreateQuery(
clientName: APIService.clientName, clientName: APIService.clientName,
redirectURIs: MastodonAuthenticationController.callbackURL, redirectURIs: APIService.oauthCallbackURL,
website: APIService.appWebsite website: APIService.appWebsite
) )
return Mastodon.API.App.create( return Mastodon.API.App.create(

View File

@ -10,7 +10,6 @@ import Combine
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import CommonOSLog import CommonOSLog
import DateToolsSwift
import MastodonSDK import MastodonSDK
extension APIService { extension APIService {

View File

@ -9,7 +9,6 @@ import Combine
import CommonOSLog import CommonOSLog
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import DateToolsSwift
import Foundation import Foundation
import MastodonSDK import MastodonSDK

View File

@ -10,7 +10,6 @@ import Combine
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import CommonOSLog import CommonOSLog
import DateToolsSwift
import MastodonSDK import MastodonSDK
extension APIService { extension APIService {

View File

@ -10,7 +10,6 @@ import Combine
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import CommonOSLog import CommonOSLog
import DateToolsSwift
import MastodonSDK import MastodonSDK
extension APIService { extension APIService {

View File

@ -10,7 +10,6 @@ import Combine
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import CommonOSLog import CommonOSLog
import DateToolsSwift
import MastodonSDK import MastodonSDK
extension APIService { extension APIService {

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