mirror of
https://github.com/mastodon/mastodon-ios
synced 2025-04-11 22:58:02 +02:00
chore: [WIP] move core logic into package
This commit is contained in:
parent
28267fe6d8
commit
64f3d2fe3a
16
.arkana.yml
Normal file
16
.arkana.yml
Normal 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
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// AppShared.h
|
||||
// AppShared
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-27.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for AppShared.
|
||||
FOUNDATION_EXPORT double AppSharedVersionNumber;
|
||||
|
||||
//! Project version string for AppShared.
|
||||
FOUNDATION_EXPORT const unsigned char AppSharedVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <AppShared/PublicHeader.h>
|
||||
|
||||
|
@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.4.5</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>144</string>
|
||||
</dict>
|
||||
</plist>
|
3
Gemfile
3
Gemfile
@ -1,6 +1,5 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem 'arkana'
|
||||
gem "cocoapods"
|
||||
gem "cocoapods-clean"
|
||||
gem "cocoapods-keys"
|
||||
|
||||
|
20
Gemfile.lock
20
Gemfile.lock
@ -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
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -8,8 +8,9 @@ import UIKit
|
||||
import Combine
|
||||
import SafariServices
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import PanModal
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
|
@ -10,6 +10,7 @@ import MastodonSDK
|
||||
import MastodonMeta
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
import MastodonCore
|
||||
|
||||
enum AutoCompleteSection: Equatable, Hashable {
|
||||
case main
|
||||
|
@ -1,28 +0,0 @@
|
||||
//
|
||||
// MastodonUser.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/2/3.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension MastodonUser {
|
||||
|
||||
public var profileURL: URL {
|
||||
if let urlString = self.url,
|
||||
let url = URL(string: urlString) {
|
||||
return url
|
||||
} else {
|
||||
return URL(string: "https://\(self.domain)/@\(username)")!
|
||||
}
|
||||
}
|
||||
|
||||
public var activityItems: [Any] {
|
||||
var items: [Any] = []
|
||||
items.append(profileURL)
|
||||
return items
|
||||
}
|
||||
}
|
@ -1,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
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
//
|
||||
// MastodonEmojis.swift
|
||||
// MastodonEmojis
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-9-2.
|
||||
// Copyright © 2021 Twidere. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
|
||||
extension MastodonEmoji {
|
||||
public convenience init(emoji: Mastodon.Entity.Emoji) {
|
||||
self.init(
|
||||
code: emoji.shortcode,
|
||||
url: emoji.url,
|
||||
staticURL: emoji.staticURL,
|
||||
visibleInPicker: emoji.visibleInPicker,
|
||||
category: emoji.category
|
||||
)
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
//
|
||||
// HomeTimelinePreference.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-21.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UserDefaults {
|
||||
|
||||
@objc dynamic var preferAsyncHomeTimeline: Bool {
|
||||
get {
|
||||
register(defaults: [#function: false])
|
||||
return bool(forKey: #function)
|
||||
}
|
||||
set { self[#function] = newValue }
|
||||
}
|
||||
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
//
|
||||
// NotificationPreference.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-26.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonExtension
|
||||
|
||||
extension UserDefaults {
|
||||
|
||||
@objc dynamic var notificationBadgeCount: Int {
|
||||
get {
|
||||
register(defaults: [#function: 0])
|
||||
return integer(forKey: #function)
|
||||
}
|
||||
set { self[#function] = newValue }
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
//
|
||||
// ThemePreference.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-7-5.
|
||||
//
|
||||
|
@ -11,6 +11,8 @@ import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
|
||||
final class AccountListViewModel {
|
||||
|
||||
|
@ -12,6 +12,7 @@ import CoreDataStack
|
||||
import PanModal
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
import MastodonCore
|
||||
|
||||
final class AccountListViewController: UIViewController, NeedsDependency {
|
||||
|
||||
|
@ -10,6 +10,8 @@ import Combine
|
||||
import MetaTextKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
|
||||
final class AddAccountTableViewCell: UITableViewCell {
|
||||
|
||||
|
@ -9,6 +9,7 @@ import os.log
|
||||
import Foundation
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
|
||||
extension AutoCompleteViewModel {
|
||||
class State: GKState, NamingState {
|
||||
|
@ -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: [
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
// }
|
||||
//}
|
||||
|
@ -12,6 +12,7 @@ import GameplayKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
|
||||
final class DiscoveryCommunityViewModel {
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -9,6 +9,7 @@ import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
|
||||
final class RootSplitViewController: UISplitViewController, NeedsDependency {
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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] = [:]
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
//
|
||||
// ViewStateStore.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-1-27.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
struct ViewStateStore {
|
||||
|
||||
}
|
||||
|
||||
enum ViewState { }
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -9,6 +9,7 @@ import Foundation
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Intents
|
||||
import MastodonCore
|
||||
|
||||
extension Account {
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
241
MastodonSDK/Package.resolved
Normal file
241
MastodonSDK/Package.resolved
Normal 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
|
||||
}
|
@ -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(
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
@ -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 }
|
||||
}
|
@ -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()
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
15
MastodonSDK/Sources/MastodonCore/DocumentStore.swift
Normal file
15
MastodonSDK/Sources/MastodonCore/DocumentStore.swift
Normal 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] = [:]
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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 ?? []
|
@ -11,7 +11,6 @@ import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonUI
|
||||
|
||||
final class StatusFetchedResultsController: NSObject {
|
||||
|
@ -11,7 +11,6 @@ import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonUI
|
||||
|
||||
final class UserFetchedResultsController: NSObject {
|
||||
|
@ -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,
|
@ -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,
|
@ -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,
|
@ -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,
|
@ -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,
|
@ -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,
|
@ -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,
|
@ -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
|
@ -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,
|
@ -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(
|
@ -10,7 +10,6 @@ import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import CommonOSLog
|
||||
import DateToolsSwift
|
||||
import MastodonSDK
|
||||
|
||||
extension APIService {
|
@ -9,7 +9,6 @@ import Combine
|
||||
import CommonOSLog
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import DateToolsSwift
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
|
@ -10,7 +10,6 @@ import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import CommonOSLog
|
||||
import DateToolsSwift
|
||||
import MastodonSDK
|
||||
|
||||
extension APIService {
|
@ -10,7 +10,6 @@ import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import CommonOSLog
|
||||
import DateToolsSwift
|
||||
import MastodonSDK
|
||||
|
||||
extension APIService {
|
@ -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
Loading…
x
Reference in New Issue
Block a user