mirror of
https://github.com/mastodon/mastodon-ios.git
synced 2025-01-31 01:27:11 +01:00
feat: implement status publish API
This commit is contained in:
parent
1b3ba1ccfb
commit
296d29f3e0
@ -198,7 +198,9 @@
|
||||
"attachment": {
|
||||
"photo": "photo",
|
||||
"video": "video",
|
||||
"attachment_broken": "This %s is broken and can't be\nuploaded to Mastodon."
|
||||
"attachment_broken": "This %s is broken and can't be\nuploaded to Mastodon.",
|
||||
"description_photo": "Describe photo for low vision people...",
|
||||
"description_video": "Describe what’s happening for low vision people..."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -214,6 +214,9 @@
|
||||
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; };
|
||||
DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A485B2603010E008B817C /* PHPickerResultLoader.swift */; };
|
||||
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; };
|
||||
DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* UITextView+Placeholder */; };
|
||||
DB9A488426034BD7008B817C /* APIService+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488326034BD7008B817C /* APIService+Status.swift */; };
|
||||
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */; };
|
||||
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; };
|
||||
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; };
|
||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; };
|
||||
@ -500,6 +503,8 @@
|
||||
DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.swift"; sourceTree = "<group>"; };
|
||||
DB9A485B2603010E008B817C /* PHPickerResultLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPickerResultLoader.swift; sourceTree = "<group>"; };
|
||||
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentContainerView+EmptyStateView.swift"; sourceTree = "<group>"; };
|
||||
DB9A488326034BD7008B817C /* APIService+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Status.swift"; sourceTree = "<group>"; };
|
||||
DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+PublishState.swift"; sourceTree = "<group>"; };
|
||||
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
|
||||
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = "<group>"; };
|
||||
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
||||
@ -530,6 +535,7 @@
|
||||
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
|
||||
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
|
||||
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
|
||||
DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */,
|
||||
2D939AC825EE14620076FA61 /* CropViewController in Frameworks */,
|
||||
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */,
|
||||
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */,
|
||||
@ -1029,6 +1035,7 @@
|
||||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
|
||||
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
|
||||
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */,
|
||||
DB9A488326034BD7008B817C /* APIService+Status.swift */,
|
||||
);
|
||||
path = APIService;
|
||||
sourceTree = "<group>";
|
||||
@ -1113,6 +1120,7 @@
|
||||
DB789A2125F9F76D0071ACA0 /* TableViewCell */,
|
||||
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
|
||||
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */,
|
||||
DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */,
|
||||
DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */,
|
||||
);
|
||||
path = Compose;
|
||||
@ -1407,6 +1415,7 @@
|
||||
2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */,
|
||||
2D939AC725EE14620076FA61 /* CropViewController */,
|
||||
DB6672A225F9FDE500D60309 /* TwitterTextEditor */,
|
||||
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
|
||||
);
|
||||
productName = Mastodon;
|
||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||
@ -1537,6 +1546,7 @@
|
||||
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */,
|
||||
2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */,
|
||||
DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
||||
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */,
|
||||
);
|
||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||
projectDirPath = "";
|
||||
@ -1748,6 +1758,7 @@
|
||||
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
||||
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift in Sources */,
|
||||
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */,
|
||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */,
|
||||
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */,
|
||||
@ -1818,6 +1829,7 @@
|
||||
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
||||
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentTableViewCell.swift in Sources */,
|
||||
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
||||
DB9A488426034BD7008B817C /* APIService+Status.swift in Sources */,
|
||||
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
||||
@ -2489,6 +2501,14 @@
|
||||
minimumVersion = 1.0.0;
|
||||
};
|
||||
};
|
||||
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/MainasuK/UITextView-Placeholder";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.4.1;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@ -2536,6 +2556,11 @@
|
||||
package = DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */;
|
||||
productName = TwitterTextEditor;
|
||||
};
|
||||
DB9A487D2603456B008B817C /* UITextView+Placeholder */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
|
||||
productName = "UITextView+Placeholder";
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
|
@ -108,6 +108,15 @@
|
||||
"revision": "8aa914134c5b6aa46e862de63f239ec0e3b52a91",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "UITextView+Placeholder",
|
||||
"repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "20f513ded04a040cdf5467f0891849b1763ede3b",
|
||||
"version": "1.4.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -42,6 +42,7 @@ extension ComposeStatusSection {
|
||||
return cell
|
||||
case .input(let replyToTootObjectID, let attribute):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeStatusContentTableViewCell.self), for: indexPath) as! ComposeStatusContentTableViewCell
|
||||
cell.textEditorView.text = attribute.composeContent.value ?? ""
|
||||
managedObjectContext.perform {
|
||||
guard let replyToTootObjectID = replyToTootObjectID,
|
||||
let replyTo = managedObjectContext.object(with: replyToTootObjectID) as? Toot else {
|
||||
@ -55,15 +56,19 @@ extension ComposeStatusSection {
|
||||
cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
|
||||
// self size input cell
|
||||
cell.composeContent
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { text in
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
// bind input data
|
||||
attribute.composeContent.value = text
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
return cell
|
||||
case .attachment(let attachmentService):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeStatusAttachmentTableViewCell.self), for: indexPath) as! ComposeStatusAttachmentTableViewCell
|
||||
cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value
|
||||
cell.delegate = composeStatusAttachmentTableViewCellDelegate
|
||||
attachmentService.imageData
|
||||
.receive(on: DispatchQueue.main)
|
||||
@ -93,6 +98,17 @@ extension ComposeStatusSection {
|
||||
cell.attachmentContainerView.emptyStateView.isHidden = error == nil
|
||||
}
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
@ -149,6 +149,10 @@ internal enum L10n {
|
||||
internal static func attachmentBroken(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Compose.Attachment.AttachmentBroken", String(describing: p1))
|
||||
}
|
||||
/// Describe photo for low vision people...
|
||||
internal static let descriptionPhoto = L10n.tr("Localizable", "Scene.Compose.Attachment.DescriptionPhoto")
|
||||
/// Describe what’s happening for low vision people...
|
||||
internal static let descriptionVideo = L10n.tr("Localizable", "Scene.Compose.Attachment.DescriptionVideo")
|
||||
/// photo
|
||||
internal static let photo = L10n.tr("Localizable", "Scene.Compose.Attachment.Photo")
|
||||
/// video
|
||||
|
@ -5,9 +5,9 @@
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.353",
|
||||
"green" : "0.251",
|
||||
"red" : "0.875"
|
||||
"blue" : "66",
|
||||
"green" : "46",
|
||||
"red" : "163"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
@ -40,6 +40,8 @@
|
||||
"Common.Countable.Photo.Single" = "photo";
|
||||
"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can't be
|
||||
uploaded to Mastodon.";
|
||||
"Scene.Compose.Attachment.DescriptionPhoto" = "Describe photo for low vision people...";
|
||||
"Scene.Compose.Attachment.DescriptionVideo" = "Describe what’s happening for low vision people...";
|
||||
"Scene.Compose.Attachment.Photo" = "photo";
|
||||
"Scene.Compose.Attachment.Video" = "video";
|
||||
"Scene.Compose.ComposeAction" = "Publish";
|
||||
|
@ -22,7 +22,7 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||
|
||||
private var suffixedAttachmentViews: [UIView] = []
|
||||
|
||||
let composeTootBarButtonItem: UIBarButtonItem = {
|
||||
let publishButton: UIButton = {
|
||||
let button = RoundedEdgesButton(type: .custom)
|
||||
button.setTitle(L10n.Scene.Compose.composeAction, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold)
|
||||
@ -32,7 +32,10 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||
button.setTitleColor(.white, for: .normal)
|
||||
button.contentEdgeInsets = UIEdgeInsets(top: 3, left: 16, bottom: 3, right: 16)
|
||||
button.adjustsImageWhenHighlighted = false
|
||||
let barButtonItem = UIBarButtonItem(customView: button)
|
||||
return button
|
||||
}()
|
||||
private(set) lazy var publishBarButtonItem: UIBarButtonItem = {
|
||||
let barButtonItem = UIBarButtonItem(customView: publishButton)
|
||||
return barButtonItem
|
||||
}()
|
||||
|
||||
@ -85,7 +88,8 @@ extension ComposeViewController {
|
||||
.store(in: &disposeBag)
|
||||
view.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
|
||||
navigationItem.rightBarButtonItem = composeTootBarButtonItem
|
||||
navigationItem.rightBarButtonItem = publishBarButtonItem
|
||||
publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
|
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(tableView)
|
||||
@ -171,7 +175,7 @@ extension ComposeViewController {
|
||||
|
||||
viewModel.isComposeTootBarButtonItemEnabled
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.isEnabled, on: composeTootBarButtonItem)
|
||||
.assign(to: \.isEnabled, on: publishBarButtonItem)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind custom emojis
|
||||
@ -187,6 +191,16 @@ extension ComposeViewController {
|
||||
self.textEditorView()?.setNeedsUpdateTextAttributes()
|
||||
})
|
||||
.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 }
|
||||
self.composeToolbarView.mediaButton.isEnabled = attachmentServices.count < 4
|
||||
self.resetImagePicker()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
@ -241,6 +255,22 @@ extension ComposeViewController {
|
||||
alertController.addAction(cancelAction)
|
||||
present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func resetImagePicker() {
|
||||
var configuration = PHPickerConfiguration()
|
||||
configuration.filter = .images
|
||||
let selectionLimit = max(1, 4 - viewModel.attachmentServices.value.count)
|
||||
configuration.selectionLimit = selectionLimit
|
||||
|
||||
imagePicker = createImagePicker(configuration: configuration)
|
||||
}
|
||||
|
||||
private func createImagePicker(configuration: PHPickerConfiguration) -> PHPickerViewController {
|
||||
let imagePicker = PHPickerViewController(configuration: configuration)
|
||||
imagePicker.delegate = self
|
||||
return imagePicker
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeViewController {
|
||||
@ -254,6 +284,16 @@ extension ComposeViewController {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
guard viewModel.publishStateMachine.enter(ComposeViewModel.PublishState.Publishing.self) else {
|
||||
// TODO: handle error
|
||||
return
|
||||
}
|
||||
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - TextEditorViewTextAttributesDelegate
|
||||
|
88
Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift
Normal file
88
Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift
Normal file
@ -0,0 +1,88 @@
|
||||
//
|
||||
// ComposeViewModel+PublishState.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-18.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
|
||||
extension ComposeViewModel {
|
||||
class PublishState: GKState {
|
||||
weak var viewModel: ComposeViewModel?
|
||||
|
||||
init(viewModel: ComposeViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeViewModel.PublishState {
|
||||
class Initial: ComposeViewModel.PublishState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
return stateClass == Publishing.self
|
||||
}
|
||||
}
|
||||
|
||||
class Publishing: ComposeViewModel.PublishState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
return stateClass == Fail.self || stateClass == Finish.self
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||
guard let mastodonAuthenticationBox = viewModel.activeAuthenticationBox.value else {
|
||||
stateMachine.enter(Fail.self)
|
||||
return
|
||||
}
|
||||
|
||||
let query = Mastodon.API.Statuses.PublishStatusQuery(
|
||||
status: viewModel.composeStatusAttribute.composeContent.value,
|
||||
mediaIDs: nil
|
||||
)
|
||||
viewModel.context.apiService.publishStatus(
|
||||
domain: mastodonAuthenticationBox.domain,
|
||||
query: query,
|
||||
mastodonAuthenticationBox: mastodonAuthenticationBox
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
stateMachine.enter(Fail.self)
|
||||
case .finished:
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
stateMachine.enter(Finish.self)
|
||||
}
|
||||
} receiveValue: { status in
|
||||
|
||||
}
|
||||
.store(in: &viewModel.disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
class Fail: ComposeViewModel.PublishState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
// allow discard publishing
|
||||
return stateClass == Publishing.self || stateClass == Finish.self
|
||||
}
|
||||
}
|
||||
|
||||
class Finish: ComposeViewModel.PublishState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -9,6 +9,7 @@ import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import GameplayKit
|
||||
|
||||
final class ComposeViewModel {
|
||||
|
||||
@ -18,11 +19,22 @@ final class ComposeViewModel {
|
||||
let context: AppContext
|
||||
let composeKind: ComposeStatusSection.ComposeKind
|
||||
let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
|
||||
let composeContent = CurrentValueSubject<String, Never>("")
|
||||
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
|
||||
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
||||
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
||||
private(set) lazy var publishStateMachine: GKStateMachine = {
|
||||
// exclude timeline middle fetcher state
|
||||
let stateMachine = GKStateMachine(states: [
|
||||
PublishState.Initial(viewModel: self),
|
||||
PublishState.Publishing(viewModel: self),
|
||||
PublishState.Fail(viewModel: self),
|
||||
PublishState.Finish(viewModel: self),
|
||||
])
|
||||
stateMachine.enter(PublishState.Initial.self)
|
||||
return stateMachine
|
||||
}()
|
||||
|
||||
// UI & UX
|
||||
let title: CurrentValueSubject<String, Never>
|
||||
@ -47,12 +59,16 @@ final class ComposeViewModel {
|
||||
case .reply: self.title = CurrentValueSubject(L10n.Scene.Compose.Title.newReply)
|
||||
}
|
||||
self.activeAuthentication = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value)
|
||||
self.activeAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
|
||||
// end init
|
||||
|
||||
// bind active authentication
|
||||
context.authenticationService.activeMastodonAuthentication
|
||||
.assign(to: \.value, on: activeAuthentication)
|
||||
.store(in: &disposeBag)
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.assign(to: \.value, on: activeAuthenticationBox)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind avatar and names
|
||||
activeAuthentication
|
||||
|
@ -6,11 +6,14 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import UITextView_Placeholder
|
||||
|
||||
final class AttachmentContainerView: UIView {
|
||||
|
||||
static let containerViewCornerRadius: CGFloat = 4
|
||||
|
||||
var descriptionBackgroundViewFrameObservation: NSKeyValueObservation?
|
||||
|
||||
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
|
||||
|
||||
let previewImageView: UIImageView = {
|
||||
@ -21,6 +24,34 @@ final class AttachmentContainerView: UIView {
|
||||
}()
|
||||
|
||||
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))
|
||||
textView.placeholder = L10n.Scene.Compose.Attachment.descriptionPhoto
|
||||
textView.placeholderColor = Asset.Colors.Label.secondary.color
|
||||
return textView
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
@ -46,6 +77,29 @@ extension AttachmentContainerView {
|
||||
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([
|
||||
@ -62,6 +116,8 @@ extension AttachmentContainerView {
|
||||
activityIndicatorView.centerYAnchor.constraint(equalTo: previewImageView.centerYAnchor),
|
||||
])
|
||||
|
||||
descriptionBackgroundView.overrideUserInterfaceStyle = .dark
|
||||
|
||||
emptyStateView.isHidden = true
|
||||
activityIndicatorView.hidesWhenStopped = true
|
||||
activityIndicatorView.startAnimating()
|
||||
|
33
Mastodon/Service/APIService/APIService+Status.swift
Normal file
33
Mastodon/Service/APIService/APIService+Status.swift
Normal file
@ -0,0 +1,33 @@
|
||||
//
|
||||
// APIService+Status.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import CommonOSLog
|
||||
import DateToolsSwift
|
||||
import MastodonSDK
|
||||
|
||||
extension APIService {
|
||||
|
||||
func publishStatus(
|
||||
domain: String,
|
||||
query: Mastodon.API.Statuses.PublishStatusQuery,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
||||
return Mastodon.API.Statuses.publishStatus(
|
||||
session: session,
|
||||
domain: domain,
|
||||
query: query,
|
||||
authorization: authorization
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -17,6 +17,7 @@ final class MastodonAttachmentService {
|
||||
|
||||
// input
|
||||
let pickerResult: PHPickerResult
|
||||
let description = CurrentValueSubject<String?, Never>(nil)
|
||||
|
||||
// output
|
||||
let imageData = CurrentValueSubject<Data?, Never>(nil)
|
||||
|
@ -6,3 +6,62 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
extension Mastodon.API.Statuses {
|
||||
|
||||
static func publishNewStatusEndpointURL(domain: String) -> URL {
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("statuses")
|
||||
}
|
||||
|
||||
/// Publish new status
|
||||
///
|
||||
/// Post a new status.
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/3/18
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/statuses/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - query: `PublishStatusQuery`
|
||||
/// - authorization: User token
|
||||
/// - Returns: `AnyPublisher` contains `Status` nested in the response
|
||||
public static func publishStatus(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
query: PublishStatusQuery,
|
||||
authorization: Mastodon.API.OAuth.Authorization?
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||
let request = Mastodon.API.post(
|
||||
url: publishNewStatusEndpointURL(domain: domain),
|
||||
query: query,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public struct PublishStatusQuery: Codable, PostQuery {
|
||||
public let status: String?
|
||||
public let mediaIDs: [String]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case status
|
||||
case mediaIDs = "media_ids"
|
||||
}
|
||||
|
||||
public init(status: String?, mediaIDs: [String]?) {
|
||||
self.status = status
|
||||
self.mediaIDs = mediaIDs
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -54,5 +54,6 @@ arch -x86_64 pod install
|
||||
- [SwiftGen](https://github.com/SwiftGen/SwiftGen)
|
||||
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
|
||||
- [TwitterTextEditor](https://github.com/twitter/TwitterTextEditor)
|
||||
- [UITextView-Placeholder](https://github.com/devxoul/UITextView-Placeholder)
|
||||
|
||||
## License
|
||||
|
Loading…
x
Reference in New Issue
Block a user