feat: implement status publish API

This commit is contained in:
CMK 2021-03-18 17:33:07 +08:00
parent 1b3ba1ccfb
commit 296d29f3e0
15 changed files with 361 additions and 9 deletions

View File

@ -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 whats happening for low vision people..."
}
}
}

View File

@ -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 */

View File

@ -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"
}
}
]
},

View File

@ -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
}
}

View File

@ -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 whats 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

View File

@ -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"

View File

@ -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 whats happening for low vision people...";
"Scene.Compose.Attachment.Photo" = "photo";
"Scene.Compose.Attachment.Video" = "video";
"Scene.Compose.ComposeAction" = "Publish";

View File

@ -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

View 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
}
}
}

View File

@ -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

View File

@ -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()

View 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
)
}
}

View File

@ -17,6 +17,7 @@ final class MastodonAttachmentService {
// input
let pickerResult: PHPickerResult
let description = CurrentValueSubject<String?, Never>(nil)
// output
let imageData = CurrentValueSubject<Data?, Never>(nil)

View File

@ -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
}
}
}

View File

@ -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