metatext/ViewModels/Sources/ViewModels/View Models/NewStatusViewModel.swift

248 lines
9.2 KiB
Swift
Raw Normal View History

2020-12-06 03:10:27 +00:00
// Copyright © 2020 Metabolist. All rights reserved.
import Combine
import Foundation
import Mastodon
import ServiceLayer
public final class NewStatusViewModel: ObservableObject {
2021-01-01 00:49:59 +00:00
@Published public var visibility: Status.Visibility
2021-01-17 07:14:17 +00:00
@Published public private(set) var compositionViewModels = [CompositionViewModel]()
2021-01-26 00:06:35 +00:00
@Published public private(set) var identityContext: IdentityContext
2020-12-16 01:39:38 +00:00
@Published public var canPost = false
2020-12-10 02:44:06 +00:00
@Published public var alertItem: AlertItem?
2021-01-01 20:18:10 +00:00
@Published public private(set) var postingState = PostingState.composing
2021-01-27 01:12:03 +00:00
public let canChangeIdentity: Bool
2021-01-10 05:56:15 +00:00
public let inReplyToViewModel: StatusViewModel?
2021-01-01 00:49:59 +00:00
public let events: AnyPublisher<Event, Never>
2020-12-06 03:10:27 +00:00
2020-12-10 02:44:06 +00:00
private let allIdentitiesService: AllIdentitiesService
private let environment: AppEnvironment
2021-01-01 00:49:59 +00:00
private let eventsSubject = PassthroughSubject<Event, Never>()
2021-01-10 01:26:51 +00:00
private let compositionEventsSubject = PassthroughSubject<CompositionViewModel.Event, Never>()
2020-12-10 02:44:06 +00:00
private var cancellables = Set<AnyCancellable>()
2021-01-27 04:38:32 +00:00
// swiftlint:disable:next function_body_length
2020-12-10 02:44:06 +00:00
public init(allIdentitiesService: AllIdentitiesService,
2021-01-26 00:06:35 +00:00
identityContext: IdentityContext,
2021-01-10 05:56:15 +00:00
environment: AppEnvironment,
2021-03-21 23:23:41 +00:00
identity: Identity?,
2021-01-11 22:45:30 +00:00
inReplyTo: StatusViewModel?,
2021-01-17 07:14:17 +00:00
redraft: Status?,
2021-03-02 00:53:36 +00:00
directMessageTo: AccountViewModel?,
2021-01-17 07:14:17 +00:00
extensionContext: NSExtensionContext?) {
2020-12-10 02:44:06 +00:00
self.allIdentitiesService = allIdentitiesService
2021-01-26 00:06:35 +00:00
self.identityContext = identityContext
2020-12-10 02:44:06 +00:00
self.environment = environment
2021-01-10 05:56:15 +00:00
inReplyToViewModel = inReplyTo
2021-01-17 07:14:17 +00:00
events = eventsSubject.eraseToAnyPublisher()
2021-03-01 22:38:05 +00:00
visibility = redraft?.visibility
?? inReplyTo?.visibility
2021-03-21 23:23:41 +00:00
?? (identity ?? identityContext.identity).preferences.postingDefaultVisibility
2021-01-11 22:45:30 +00:00
2021-01-27 01:12:03 +00:00
if let inReplyTo = inReplyTo {
switch inReplyTo.visibility {
case .public, .unlisted:
canChangeIdentity = true
default:
canChangeIdentity = false
}
} else {
canChangeIdentity = true
}
2021-01-17 07:14:17 +00:00
let compositionViewModel: CompositionViewModel
2021-01-11 22:45:30 +00:00
if let redraft = redraft {
2021-01-17 07:14:17 +00:00
compositionViewModel = CompositionViewModel(
eventsSubject: compositionEventsSubject,
redraft: redraft,
2021-01-26 00:06:35 +00:00
identityContext: identityContext)
2021-01-17 07:14:17 +00:00
} else if let extensionContext = extensionContext {
compositionViewModel = CompositionViewModel(
eventsSubject: compositionEventsSubject,
extensionContext: extensionContext,
parentViewModel: self)
2021-01-11 22:45:30 +00:00
} else {
2021-01-17 07:14:17 +00:00
compositionViewModel = CompositionViewModel(eventsSubject: compositionEventsSubject)
2021-01-11 22:45:30 +00:00
}
2021-02-01 22:23:07 +00:00
if let inReplyTo = inReplyTo, redraft == nil {
2021-02-06 21:26:42 +00:00
var mentions = Set<String>()
if !inReplyTo.isMine {
mentions.insert(inReplyTo.accountName)
}
mentions.formUnion(inReplyTo.mentions.map(\.acct)
2021-03-21 23:23:41 +00:00
.filter { $0 != (identity ?? identityContext.identity).account?.username }
2021-02-06 21:26:42 +00:00
.map("@".appending))
compositionViewModel.text = mentions.joined(separator: " ").appending(" ")
2021-03-09 05:08:16 +00:00
compositionViewModel.contentWarning = inReplyTo.spoilerText
compositionViewModel.displayContentWarning = !inReplyTo.spoilerText.isEmpty
2021-03-02 00:53:36 +00:00
} else if let directMessageTo = directMessageTo {
compositionViewModel.text = directMessageTo.accountName.appending(" ")
visibility = .direct
2021-01-31 19:57:37 +00:00
}
2021-01-17 07:14:17 +00:00
compositionViewModels = [compositionViewModel]
2020-12-16 01:39:38 +00:00
$compositionViewModels.flatMap { Publishers.MergeMany($0.map(\.$isPostable)) }
.receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring
.compactMap { [weak self] _ in self?.compositionViewModels.allSatisfy(\.isPostable) }
2021-01-01 20:18:10 +00:00
.combineLatest($postingState)
.map { $0 && $1 == .composing }
2020-12-16 01:39:38 +00:00
.assign(to: &$canPost)
2021-01-10 01:26:51 +00:00
compositionEventsSubject
.sink { [weak self] in self?.handle(event: $0) }
.store(in: &cancellables)
2021-03-21 23:23:41 +00:00
if let identity = identity {
setIdentity(identity)
}
2020-12-10 02:44:06 +00:00
}
}
public extension NewStatusViewModel {
2021-01-01 00:49:59 +00:00
enum Event {
case presentMediaPicker(CompositionViewModel)
2021-01-01 23:31:39 +00:00
case presentCamera(CompositionViewModel)
2021-01-10 07:08:45 +00:00
case presentDocumentPicker(CompositionViewModel)
2021-01-14 17:49:53 +00:00
case presentEmojiPicker(Int)
2021-01-10 01:26:51 +00:00
case editAttachment(AttachmentViewModel, CompositionViewModel)
2021-01-27 01:42:32 +00:00
case changeIdentity(Identity)
2020-12-10 02:44:06 +00:00
}
2021-01-01 20:18:10 +00:00
enum PostingState {
case composing
case posting
case done
}
2020-12-10 02:44:06 +00:00
func setIdentity(_ identity: Identity) {
let identityService: IdentityService
do {
identityService = try allIdentitiesService.identityService(id: identity.id)
} catch {
alertItem = AlertItem(error: error)
return
}
2021-01-26 00:06:35 +00:00
identityContext = IdentityContext(
2020-12-10 02:44:06 +00:00
identity: identity,
publisher: identityService.identityPublisher(immediate: false)
.assignErrorsToAlertItem(to: \.alertItem, on: self),
service: identityService,
environment: environment)
2020-12-06 03:10:27 +00:00
}
2020-12-16 01:39:38 +00:00
2021-01-01 00:49:59 +00:00
func presentMediaPicker(viewModel: CompositionViewModel) {
eventsSubject.send(.presentMediaPicker(viewModel))
2020-12-16 01:39:38 +00:00
}
2021-01-01 23:31:39 +00:00
func presentCamera(viewModel: CompositionViewModel) {
eventsSubject.send(.presentCamera(viewModel))
}
2021-01-10 07:08:45 +00:00
func presentDocumentPicker(viewModel: CompositionViewModel) {
eventsSubject.send(.presentDocumentPicker(viewModel))
}
2021-01-14 17:49:53 +00:00
func presentEmojiPicker(tag: Int) {
eventsSubject.send(.presentEmojiPicker(tag))
}
2021-01-10 06:32:41 +00:00
func remove(viewModel: CompositionViewModel) {
compositionViewModels.removeAll { $0 === viewModel }
}
2021-01-01 00:49:59 +00:00
func insert(after: CompositionViewModel) {
guard let index = compositionViewModels.firstIndex(where: { $0 === after })
else { return }
2020-12-16 01:39:38 +00:00
2021-01-10 01:26:51 +00:00
let newViewModel = CompositionViewModel(eventsSubject: compositionEventsSubject)
2020-12-16 01:39:38 +00:00
2021-01-01 00:49:59 +00:00
newViewModel.contentWarning = after.contentWarning
newViewModel.displayContentWarning = after.displayContentWarning
2020-12-16 01:39:38 +00:00
2021-02-17 07:07:53 +00:00
let mentions = Self.mentionsRegularExpression.matches(
in: after.text,
range: NSRange(location: 0, length: after.text.count))
.compactMap { result -> String? in
guard let range = Range(result.range, in: after.text) else { return nil }
return String(after.text[range])
}
if !mentions.isEmpty {
newViewModel.text = mentions.joined(separator: " ").appending(" ")
}
2021-01-01 00:49:59 +00:00
if index >= compositionViewModels.count - 1 {
compositionViewModels.append(newViewModel)
} else {
compositionViewModels.insert(newViewModel, at: index + 1)
2020-12-16 01:39:38 +00:00
}
}
2020-12-19 06:30:19 +00:00
func attach(itemProviders: [NSItemProvider], to compositionViewModel: CompositionViewModel) {
compositionViewModel.attach(itemProviders: itemProviders, parentViewModel: self)
2021-01-01 00:49:59 +00:00
}
func post() {
guard let unposted = compositionViewModels.first(where: { !$0.isPosted }) else { return }
2021-01-10 05:56:15 +00:00
post(viewModel: unposted, inReplyToId: inReplyToViewModel?.id)
2021-01-01 00:49:59 +00:00
}
2021-01-27 01:42:32 +00:00
func changeIdentity(_ identity: Identity) {
eventsSubject.send(.changeIdentity(identity))
}
2021-01-01 00:49:59 +00:00
}
private extension NewStatusViewModel {
2021-02-17 07:07:53 +00:00
// swiftlint:disable:next force_try
2021-02-17 07:35:02 +00:00
static let mentionsRegularExpression = try! NSRegularExpression(pattern: #"@\S+"#)
2021-02-17 07:07:53 +00:00
2021-01-10 01:26:51 +00:00
func handle(event: CompositionViewModel.Event) {
switch event {
case let .editAttachment(attachmentViewModel, compositionViewModel):
eventsSubject.send(.editAttachment(attachmentViewModel, compositionViewModel))
case let .updateAttachment(publisher):
publisher.assignErrorsToAlertItem(to: \.alertItem, on: self).sink { _ in }.store(in: &cancellables)
}
}
2021-02-17 07:07:53 +00:00
2020-12-19 06:30:19 +00:00
func post(viewModel: CompositionViewModel, inReplyToId: Status.Id?) {
2021-01-01 20:18:10 +00:00
postingState = .posting
2021-01-26 00:06:35 +00:00
identityContext.service.post(statusComponents: viewModel.components(
2021-01-01 00:49:59 +00:00
inReplyToId: inReplyToId,
visibility: visibility))
2020-12-19 06:30:19 +00:00
.receive(on: DispatchQueue.main)
.sink { [weak self] in
guard let self = self else { return }
switch $0 {
case .finished:
2021-01-01 20:18:10 +00:00
if self.compositionViewModels.allSatisfy(\.isPosted) {
self.postingState = .done
}
2020-12-19 06:30:19 +00:00
case let .failure(error):
self.alertItem = AlertItem(error: error)
2021-01-01 20:18:10 +00:00
self.postingState = .composing
2020-12-19 06:30:19 +00:00
}
} receiveValue: { [weak self] in
guard let self = self else { return }
viewModel.isPosted = true
if let unposted = self.compositionViewModels.first(where: { !$0.isPosted }) {
self.post(viewModel: unposted, inReplyToId: $0)
}
}
.store(in: &cancellables)
}
2020-12-06 03:10:27 +00:00
}