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"
gem 'arkana'
gem "cocoapods"
gem "cocoapods-clean"
gem "cocoapods-keys"

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import MastodonSDK
import MastodonMeta
import MastodonAsset
import MastodonLocalization
import MastodonCore
enum AutoCompleteSection: Equatable, Hashable {
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 MastodonSDK
import MastodonMeta
import MastodonCore
import MastodonUI
final class AccountListViewModel {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,10 +6,12 @@
//
import UIKit
import SwiftUI
import Combine
import AlamofireImage
import MastodonAsset
import MastodonLocalization
import UIHostingConfigurationBackport
final class ComposeStatusAttachmentTableViewCell: UITableViewCell {
@ -75,85 +77,91 @@ extension ComposeStatusAttachmentTableViewCell {
}
.store(in: &observations)
self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [
weak self
] collectionView, indexPath, item -> UICollectionViewCell? in
self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
[weak self] collectionView, indexPath, item -> UICollectionViewCell? in
guard let self = self else { return UICollectionViewCell() }
switch item {
case .attachment(let attachmentService):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell
cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value
cell.delegate = self.composeStatusAttachmentCollectionViewCellDelegate
attachmentService.thumbnailImage
.receive(on: DispatchQueue.main)
.sink { [weak cell] thumbnailImage in
guard let cell = cell else { return }
let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1)
guard let image = thumbnailImage else {
let placeholder = UIImage.placeholder(
size: size,
color: ThemeService.shared.currentTheme.value.systemGroupedBackgroundColor
)
.af.imageRounded(
withCornerRadius: AttachmentContainerView.containerViewCornerRadius
)
cell.attachmentContainerView.previewImageView.image = placeholder
return
}
// cannot get correct size. set corner radius on layer
cell.attachmentContainerView.previewImageView.image = image
}
.store(in: &cell.disposeBag)
Publishers.CombineLatest(
attachmentService.uploadStateMachineSubject.eraseToAnyPublisher(),
attachmentService.error.eraseToAnyPublisher()
)
.receive(on: DispatchQueue.main)
.sink { [weak cell, weak attachmentService] uploadState, error in
guard let cell = cell else { return }
guard let attachmentService = attachmentService else { return }
cell.attachmentContainerView.emptyStateView.isHidden = error == nil
cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil
if let error = error {
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
cell.attachmentContainerView.emptyStateView.label.text = error.localizedDescription
} else {
guard let uploadState = uploadState else { return }
switch uploadState {
case is MastodonAttachmentService.UploadState.Finish:
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
case is MastodonAttachmentService.UploadState.Fail:
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
// FIXME: not display
cell.attachmentContainerView.emptyStateView.label.text = {
if let file = attachmentService.file.value {
switch file {
case .jpeg, .png, .gif:
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
case .other:
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video)
}
} else {
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
}
}()
default:
break
}
cell.contentConfiguration = UIHostingConfigurationBackport {
HStack {
Image(systemName: "star")
Text("Favorites")
Spacer()
}
}
.store(in: &cell.disposeBag)
NotificationCenter.default.publisher(
for: UITextView.textDidChangeNotification,
object: cell.attachmentContainerView.descriptionTextView
)
.receive(on: DispatchQueue.main)
.sink { notification in
guard let textField = notification.object as? UITextView else { return }
let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines)
attachmentService.description.value = text
}
.store(in: &cell.disposeBag)
// cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value
// cell.delegate = self.composeStatusAttachmentCollectionViewCellDelegate
// attachmentService.thumbnailImage
// .receive(on: DispatchQueue.main)
// .sink { [weak cell] thumbnailImage in
// guard let cell = cell else { return }
// let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1)
// guard let image = thumbnailImage else {
// let placeholder = UIImage.placeholder(
// size: size,
// color: ThemeService.shared.currentTheme.value.systemGroupedBackgroundColor
// )
// .af.imageRounded(
// withCornerRadius: AttachmentContainerView.containerViewCornerRadius
// )
// cell.attachmentContainerView.previewImageView.image = placeholder
// return
// }
// // cannot get correct size. set corner radius on layer
// cell.attachmentContainerView.previewImageView.image = image
// }
// .store(in: &cell.disposeBag)
// Publishers.CombineLatest(
// attachmentService.uploadStateMachineSubject.eraseToAnyPublisher(),
// attachmentService.error.eraseToAnyPublisher()
// )
// .receive(on: DispatchQueue.main)
// .sink { [weak cell, weak attachmentService] uploadState, error in
// guard let cell = cell else { return }
// guard let attachmentService = attachmentService else { return }
// cell.attachmentContainerView.emptyStateView.isHidden = error == nil
// cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil
// if let error = error {
// cell.attachmentContainerView.activityIndicatorView.stopAnimating()
// cell.attachmentContainerView.emptyStateView.label.text = error.localizedDescription
// } else {
// guard let uploadState = uploadState else { return }
// switch uploadState {
// case is MastodonAttachmentService.UploadState.Finish:
// cell.attachmentContainerView.activityIndicatorView.stopAnimating()
// case is MastodonAttachmentService.UploadState.Fail:
// cell.attachmentContainerView.activityIndicatorView.stopAnimating()
// // FIXME: not display
// cell.attachmentContainerView.emptyStateView.label.text = {
// if let file = attachmentService.file.value {
// switch file {
// case .jpeg, .png, .gif:
// return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
// case .other:
// return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video)
// }
// } else {
// return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
// }
// }()
// default:
// break
// }
// }
// }
// .store(in: &cell.disposeBag)
// NotificationCenter.default.publisher(
// for: UITextView.textDidChangeNotification,
// object: cell.attachmentContainerView.descriptionTextView
// )
// .receive(on: DispatchQueue.main)
// .sink { notification in
// guard let textField = notification.object as? UITextView else { return }
// let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines)
// attachmentService.description.value = text
// }
// .store(in: &cell.disposeBag)
return cell
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import os.log
import UIKit
import Combine
import CoreDataStack
import MastodonCore
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 UIKit
import UserNotifications
import AppShared
import AVFoundation
@_exported import MastodonUI
import MastodonCore
import MastodonUI
@main
class AppDelegate: UIResponder, UIApplicationDelegate {

View File

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

View File

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

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 {
public func rebuild() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -12,7 +12,7 @@ import CoreData
import CoreDataStack
import MastodonSDK
final class SettingFetchedResultController: NSObject {
public final class SettingFetchedResultController: NSObject {
var disposeBag = Set<AnyCancellable>()
@ -21,9 +21,9 @@ final class SettingFetchedResultController: NSObject {
// input
// output
let settings = CurrentValueSubject<[Setting], Never>([])
public let settings = CurrentValueSubject<[Setting], Never>([])
init(managedObjectContext: NSManagedObjectContext, additionalPredicate: NSPredicate?) {
public init(managedObjectContext: NSManagedObjectContext, additionalPredicate: NSPredicate?) {
self.fetchedResultsController = {
let fetchRequest = Setting.sortedFetchRequest
fetchRequest.returnsObjectsAsFaults = false
@ -55,7 +55,7 @@ final class SettingFetchedResultController: NSObject {
// MARK: - NSFetchedResultsControllerDelegate
extension SettingFetchedResultController: NSFetchedResultsControllerDelegate {
func controller(_ controller: NSFetchedResultsController<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)
let objects = fetchedResultsController.fetchedObjects ?? []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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