Refactor compose intialization

- split ComposeContentViewModel.Kind into Destination (top level/reply) and an initial content string
- replies get the mentions prepended to the initial content string
This commit is contained in:
Jed Fox 2022-12-03 13:25:07 -05:00
parent ebf3835403
commit 3661b5ce90
No known key found for this signature in database
GPG Key ID: 0B61D18EA54B47E1
12 changed files with 49 additions and 64 deletions

View File

@ -117,7 +117,7 @@ extension DataSourceFacade {
let composeViewModel = ComposeViewModel( let composeViewModel = ComposeViewModel(
context: provider.context, context: provider.context,
authContext: provider.authContext, authContext: provider.authContext,
kind: .reply(status: status) destination: .reply(parent: status)
) )
_ = provider.coordinator.present( _ = provider.coordinator.present(
scene: .compose(viewModel: composeViewModel), scene: .compose(viewModel: composeViewModel),

View File

@ -100,7 +100,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
let composeViewModel = ComposeViewModel( let composeViewModel = ComposeViewModel(
context: self.context, context: self.context,
authContext: authContext, authContext: authContext,
kind: .reply(status: status) destination: .reply(parent: status)
) )
_ = self.coordinator.present( _ = self.coordinator.present(
scene: .compose(viewModel: composeViewModel), scene: .compose(viewModel: composeViewModel),

View File

@ -34,7 +34,8 @@ final class ComposeViewController: UIViewController, NeedsDependency {
return ComposeContentViewModel( return ComposeContentViewModel(
context: context, context: context,
authContext: viewModel.authContext, authContext: viewModel.authContext,
kind: viewModel.kind destination: viewModel.destination,
initialContent: viewModel.initialContent
) )
}() }()
private(set) lazy var composeContentViewController: ComposeContentViewController = { private(set) lazy var composeContentViewController: ComposeContentViewController = {

View File

@ -29,7 +29,8 @@ final class ComposeViewModel {
// input // input
let context: AppContext let context: AppContext
let authContext: AuthContext let authContext: AuthContext
let kind: ComposeContentViewModel.Kind let destination: ComposeContentViewModel.Destination
let initialContent: String
let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make initial event emit let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make initial event emit
@ -41,17 +42,19 @@ final class ComposeViewModel {
init( init(
context: AppContext, context: AppContext,
authContext: AuthContext, authContext: AuthContext,
kind: ComposeContentViewModel.Kind destination: ComposeContentViewModel.Destination,
initialContent: String = ""
) { ) {
self.context = context self.context = context
self.authContext = authContext self.authContext = authContext
self.kind = kind self.destination = destination
self.initialContent = initialContent
// end init // end init
self.title = { self.title = {
switch kind { switch destination {
case .post, .hashtag, .mention: return L10n.Scene.Compose.Title.newPost case .topLevel: return L10n.Scene.Compose.Title.newPost
case .reply: return L10n.Scene.Compose.Title.newReply case .reply: return L10n.Scene.Compose.Title.newReply
} }
}() }()
} }

View File

@ -162,10 +162,13 @@ extension HashtagTimelineViewController {
@objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) { @objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
let hashtag = "#" + viewModel.hashtag
UITextChecker.learnWord(hashtag)
let composeViewModel = ComposeViewModel( let composeViewModel = ComposeViewModel(
context: context, context: context,
authContext: viewModel.authContext, authContext: viewModel.authContext,
kind: .hashtag(hashtag: viewModel.hashtag) destination: .topLevel,
initialContent: hashtag
) )
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
} }

View File

@ -538,10 +538,13 @@ extension ProfileViewController {
@objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) { @objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard let mastodonUser = viewModel.user else { return } guard let mastodonUser = viewModel.user else { return }
let mention = "@" + mastodonUser.acct
UITextChecker.learnWord(mention)
let composeViewModel = ComposeViewModel( let composeViewModel = ComposeViewModel(
context: context, context: context,
authContext: viewModel.authContext, authContext: viewModel.authContext,
kind: .mention(user: mastodonUser.asRecord) destination: .topLevel,
initialContent: mention
) )
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
} }

View File

@ -379,7 +379,7 @@ extension MainTabBarController {
let composeViewModel = ComposeViewModel( let composeViewModel = ComposeViewModel(
context: context, context: context,
authContext: authContext, authContext: authContext,
kind: .post destination: .topLevel
) )
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil))
} }
@ -804,7 +804,7 @@ extension MainTabBarController {
let composeViewModel = ComposeViewModel( let composeViewModel = ComposeViewModel(
context: context, context: context,
authContext: authContext, authContext: authContext,
kind: .post destination: .topLevel
) )
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil))
} }

View File

@ -227,7 +227,7 @@ extension SidebarViewController: UICollectionViewDelegate {
let composeViewModel = ComposeViewModel( let composeViewModel = ComposeViewModel(
context: context, context: context,
authContext: authContext, authContext: authContext,
kind: .post destination: .topLevel
) )
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
default: default:

View File

@ -117,7 +117,7 @@ extension ThreadViewController {
let composeViewModel = ComposeViewModel( let composeViewModel = ComposeViewModel(
context: context, context: context,
authContext: viewModel.authContext, authContext: viewModel.authContext,
kind: .reply(status: threadContext.status) destination: .reply(parent: threadContext.status)
) )
_ = coordinator.present( _ = coordinator.present(
scene: .compose(viewModel: composeViewModel), scene: .compose(viewModel: composeViewModel),

View File

@ -185,7 +185,7 @@ extension SceneDelegate {
let composeViewModel = ComposeViewModel( let composeViewModel = ComposeViewModel(
context: AppContext.shared, context: AppContext.shared,
authContext: authContext, authContext: authContext,
kind: .post destination: .topLevel
) )
_ = coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) _ = coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil))
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): present compose scene") logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): present compose scene")

View File

@ -47,10 +47,7 @@ extension ComposeContentViewModel {
} }
.store(in: &disposeBag) .store(in: &disposeBag)
switch kind { if case .reply(let status) = destination {
case .post:
break
case .reply(let status):
let cell = composeReplyToTableViewCell let cell = composeReplyToTableViewCell
// bind frame publisher // bind frame publisher
cell.$framePublisher cell.$framePublisher
@ -66,10 +63,6 @@ extension ComposeContentViewModel {
guard let replyTo = status.object(in: context.managedObjectContext) else { return } guard let replyTo = status.object(in: context.managedObjectContext) else { return }
cell.statusView.configure(status: replyTo) cell.statusView.configure(status: replyTo)
} }
case .hashtag:
break
case .mention:
break
} }
} }
} }
@ -83,7 +76,7 @@ extension ComposeContentViewModel: UITableViewDataSource {
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch Section.allCases[section] { switch Section.allCases[section] {
case .replyTo: case .replyTo:
switch kind { switch destination {
case .reply: return 1 case .reply: return 1
default: return 0 default: return 0
} }

View File

@ -32,7 +32,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// input // input
let context: AppContext let context: AppContext
let kind: Kind let destination: Destination
weak var delegate: ComposeContentViewModelDelegate? weak var delegate: ComposeContentViewModelDelegate?
@Published var viewLayoutFrame = ViewLayoutFrame() @Published var viewLayoutFrame = ViewLayoutFrame()
@ -59,8 +59,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
customEmojiPickerInputViewModel.configure(textInput: textView) customEmojiPickerInputViewModel.configure(textInput: textView)
} }
} }
// for hashtag: "#<hashtag> " // allow dismissing the compose view without confirmation if content == intialContent
// for mention: "@<mention> "
@Published public var initialContent = "" @Published public var initialContent = ""
@Published public var content = "" @Published public var content = ""
@Published public var contentWeightedLength = 0 @Published public var contentWeightedLength = 0
@ -138,11 +137,12 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
public init( public init(
context: AppContext, context: AppContext,
authContext: AuthContext, authContext: AuthContext,
kind: Kind destination: Destination,
initialContent: String
) { ) {
self.context = context self.context = context
self.authContext = authContext self.authContext = authContext
self.kind = kind self.destination = destination
self.visibility = { self.visibility = {
// default private when user locked // default private when user locked
var visibility: Mastodon.Entity.Status.Visibility = { var visibility: Mastodon.Entity.Status.Visibility = {
@ -152,8 +152,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
return author.locked ? .private : .public return author.locked ? .private : .public
}() }()
// set visibility for reply post // set visibility for reply post
switch kind { if case .reply(let record) = destination {
case .reply(let record):
context.managedObjectContext.performAndWait { context.managedObjectContext.performAndWait {
guard let status = record.object(in: context.managedObjectContext) else { guard let status = record.object(in: context.managedObjectContext) else {
assertionFailure() assertionFailure()
@ -173,8 +172,6 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
break break
} }
} }
default:
break
} }
return visibility return visibility
}() }()
@ -185,7 +182,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// end init // end init
// setup initial value // setup initial value
switch kind { let initialContentWithSpace = initialContent.isEmpty ? "" : initialContent + " "
switch destination {
case .reply(let record): case .reply(let record):
context.managedObjectContext.performAndWait { context.managedObjectContext.performAndWait {
guard let status = record.object(in: context.managedObjectContext) else { guard let status = record.object(in: context.managedObjectContext) else {
@ -214,29 +212,15 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
} }
let initialComposeContent = mentionAccts.joined(separator: " ") let initialComposeContent = mentionAccts.joined(separator: " ")
let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " " let preInsertedContent = initialComposeContent.isEmpty ? "" : initialComposeContent + " "
self.initialContent = preInsertedContent ?? "" self.initialContent = preInsertedContent + initialContentWithSpace
self.content = preInsertedContent ?? "" self.content = preInsertedContent + initialContentWithSpace
} }
case .hashtag(let hashtag): case .topLevel:
let initialComposeContent = "#" + hashtag self.initialContent = initialContentWithSpace
UITextChecker.learnWord(initialComposeContent) self.content = initialContentWithSpace
let preInsertedContent = initialComposeContent + " "
self.initialContent = preInsertedContent
self.content = preInsertedContent
case .mention(let record):
context.managedObjectContext.performAndWait {
guard let user = record.object(in: context.managedObjectContext) else { return }
let initialComposeContent = "@" + user.acct
UITextChecker.learnWord(initialComposeContent)
let preInsertedContent = initialComposeContent + " "
self.initialContent = preInsertedContent
self.content = preInsertedContent
}
case .post:
break
} }
// set limit // set limit
let _configuration: Mastodon.Entity.Instance.Configuration? = { let _configuration: Mastodon.Entity.Instance.Configuration? = {
var configuration: Mastodon.Entity.Instance.Configuration? = nil var configuration: Mastodon.Entity.Instance.Configuration? = nil
@ -443,11 +427,9 @@ extension ComposeContentViewModel {
} }
extension ComposeContentViewModel { extension ComposeContentViewModel {
public enum Kind { public enum Destination {
case post case topLevel
case hashtag(hashtag: String) case reply(parent: ManagedObjectRecord<Status>)
case mention(user: ManagedObjectRecord<MastodonUser>)
case reply(status: ManagedObjectRecord<Status>)
} }
public enum ScrollViewState { public enum ScrollViewState {
@ -530,10 +512,10 @@ extension ComposeContentViewModel {
return MastodonStatusPublisher( return MastodonStatusPublisher(
author: author, author: author,
replyTo: { replyTo: {
switch self.kind { if case .reply(let status) = destination {
case .reply(let status): return status return status
default: return nil
} }
return nil
}(), }(),
isContentWarningComposing: isContentWarningActive, isContentWarningComposing: isContentWarningActive,
contentWarning: contentWarning, contentWarning: contentWarning,