Skip to content

Commit

Permalink
feat: implement status publish API
Browse files Browse the repository at this point in the history
  • Loading branch information
MainasuK committed Mar 18, 2021
1 parent 1b3ba1c commit 296d29f
Show file tree
Hide file tree
Showing 15 changed files with 361 additions and 9 deletions.
4 changes: 3 additions & 1 deletion Localization/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
}
}
}
Expand Down
25 changes: 25 additions & 0 deletions Mastodon.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -1029,6 +1035,7 @@
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */,
DB9A488326034BD7008B817C /* APIService+Status.swift */,
);
path = APIService;
sourceTree = "<group>";
Expand Down Expand Up @@ -1113,6 +1120,7 @@
DB789A2125F9F76D0071ACA0 /* TableViewCell */,
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */,
DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */,
DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */,
);
path = Compose;
Expand Down Expand Up @@ -1407,6 +1415,7 @@
2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */,
2D939AC725EE14620076FA61 /* CropViewController */,
DB6672A225F9FDE500D60309 /* TwitterTextEditor */,
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
);
productName = Mastodon;
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
Expand Down Expand Up @@ -1537,6 +1546,7 @@
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */,
2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */,
DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */,
);
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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 */
Expand Down
9 changes: 9 additions & 0 deletions Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
]
},
Expand Down
16 changes: 16 additions & 0 deletions Mastodon/Diffiable/Section/ComposeStatusSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
}
Expand Down
4 changes: 4 additions & 0 deletions Mastodon/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions Mastodon/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
48 changes: 44 additions & 4 deletions Mastodon/Scene/Compose/ComposeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}()

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
88 changes: 88 additions & 0 deletions Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift
Original file line number Diff line number Diff line change
@@ -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
}
}

}
Loading

0 comments on commit 296d29f

Please sign in to comment.