Delete and redraft

This commit is contained in:
Justin Mazzocchi 2021-01-11 14:45:30 -08:00
parent 96d96bd899
commit 88c3fedd93
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
15 changed files with 159 additions and 33 deletions

View file

@ -221,6 +221,12 @@ public extension ContentDatabase {
.eraseToAnyPublisher()
}
func delete(id: Status.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher(updates: StatusRecord.filter(StatusRecord.Columns.id == id).deleteAll)
.ignoreOutput()
.eraseToAnyPublisher()
}
func unfollow(id: Account.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
let statusIds = try Status.Id.fetchAll(

View file

@ -156,6 +156,8 @@
"share-extension-error.no-account-found" = "No account found";
"status.bookmark" = "Bookmark";
"status.content-warning-abbreviation" = "CW";
"status.delete" = "Delete";
"status.delete-and-redraft" = "Delete & re-draft";
"status.mute" = "Mute conversation";
"status.pin" = "Pin on profile";
"status.pinned-post" = "Pinned post";

View file

@ -17,7 +17,7 @@ public final class Status: Codable, Identifiable {
public let uri: String
public let createdAt: Date
public let account: Account
public let content: HTML
@DecodableDefault.EmptyHTML public private(set) var content: HTML
public let visibility: Visibility
public let sensitive: Bool
public let spoilerText: String
@ -77,7 +77,6 @@ public final class Status: Codable, Identifiable {
self.uri = uri
self.createdAt = createdAt
self.account = account
self.content = content
self.visibility = visibility
self.sensitive = sensitive
self.spoilerText = spoilerText
@ -98,6 +97,7 @@ public final class Status: Codable, Identifiable {
self.text = text
self.pinned = pinned
self.repliesCount = repliesCount
self.content = content
self.favourited = favourited
self.reblogged = reblogged
self.muted = muted

View file

@ -2,8 +2,6 @@
import Foundation
// Thank you https://www.swiftbysundell.com/tips/default-decoding-values/
public protocol DecodableDefaultSource {
associatedtype Value: Decodable
static var defaultValue: Value { get }
@ -40,6 +38,10 @@ public extension DecodableDefault {
public static var defaultValue: String { "" }
}
public enum EmptyHTML: Source {
public static var defaultValue: HTML { HTML(raw: "", attributed: NSAttributedString(string: "")) }
}
public enum EmptyList<T: List>: Source {
public static var defaultValue: T { [] }
}
@ -67,6 +69,7 @@ public extension DecodableDefault {
typealias True = Wrapper<Sources.True>
typealias False = Wrapper<Sources.False>
typealias EmptyString = Wrapper<Sources.EmptyString>
typealias EmptyHTML = Wrapper<Sources.EmptyHTML>
typealias EmptyList<T: List> = Wrapper<Sources.EmptyList<T>>
typealias EmptyMap<T: Map> = Wrapper<Sources.EmptyMap<T>>
typealias Zero = Wrapper<Sources.Zero>

View file

@ -16,6 +16,7 @@ public enum StatusEndpoint {
case unpin(id: Status.Id)
case mute(id: Status.Id)
case unmute(id: Status.Id)
case delete(id: Status.Id)
case post(Components)
}
@ -100,7 +101,7 @@ extension StatusEndpoint: Endpoint {
public var pathComponentsInContext: [String] {
switch self {
case let .status(id):
case let .status(id), let .delete(id):
return [id]
case let .reblog(id):
return [id, "reblog"]
@ -140,6 +141,8 @@ extension StatusEndpoint: Endpoint {
switch self {
case .status:
return .get
case .delete:
return .delete
default:
return .post
}

View file

@ -72,6 +72,34 @@ public extension StatusService {
.eraseToAnyPublisher()
}
func delete() -> AnyPublisher<Status, Error> {
mastodonAPIClient.request(StatusEndpoint.delete(id: status.displayStatus.id))
.flatMap { status in contentDatabase.delete(id: status.id).collect().map { _ in status } }
.eraseToAnyPublisher()
}
func deleteAndRedraft() -> AnyPublisher<(Status, Self?), Error> {
let inReplyToPublisher: AnyPublisher<Self?, Never>
if let inReplyToId = status.displayStatus.inReplyToId {
inReplyToPublisher = mastodonAPIClient.request(StatusEndpoint.status(id: inReplyToId))
.map {
Self(status: $0,
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) as Self?
}
.replaceError(with: nil)
.eraseToAnyPublisher()
} else {
inReplyToPublisher = Just(nil).eraseToAnyPublisher()
}
return mastodonAPIClient.request(StatusEndpoint.delete(id: status.displayStatus.id))
.flatMap { status in contentDatabase.delete(id: status.id).collect().map { _ in status } }
.zip(inReplyToPublisher.setFailureType(to: Error.self))
.eraseToAnyPublisher()
}
func rebloggedByService() -> AccountListService {
AccountListService(
endpoint: .rebloggedBy(id: status.id),

View file

@ -2,6 +2,7 @@
import AVKit
import Combine
import Mastodon
import SafariServices
import SwiftUI
import ViewModels
@ -248,6 +249,7 @@ private extension TableViewController {
viewModel.alertItems
.compactMap { $0 }
.handleEvents(receiveOutput: { print($0.error) })
.sink { [weak self] in self?.present(alertItem: $0) }
.store(in: &cancellables)
@ -303,8 +305,8 @@ private extension TableViewController {
handle(navigation: navigation)
case let .attachment(attachmentViewModel, statusViewModel):
present(attachmentViewModel: attachmentViewModel, statusViewModel: statusViewModel)
case let .reply(statusViewModel):
reply(statusViewModel: statusViewModel)
case let .compose(inReplyToViewModel, redraft):
compose(inReplyToViewModel: inReplyToViewModel, redraft: redraft)
case let .report(reportViewModel):
report(reportViewModel: reportViewModel)
}
@ -374,10 +376,11 @@ private extension TableViewController {
}
}
func reply(statusViewModel: StatusViewModel) {
func compose(inReplyToViewModel: StatusViewModel?, redraft: Status?) {
let newStatusViewModel = rootViewModel.newStatusViewModel(
identification: identification,
inReplyTo: statusViewModel)
inReplyTo: inReplyToViewModel,
redraft: redraft)
let newStatusViewController = UIHostingController(rootView: NewStatusView { newStatusViewModel })
let navigationController = UINavigationController(rootViewController: newStatusViewController)

View file

@ -8,16 +8,15 @@ import ServiceLayer
public final class CompositionViewModel: AttachmentsRenderingViewModel, ObservableObject, Identifiable {
public let id = Id()
public var isPosted = false
@Published public var text = ""
@Published public var contentWarning = ""
@Published public var displayContentWarning = false
@Published public var sensitive = false
@Published public var displayPoll = false
@Published public var pollMultipleChoice = false
@Published public var pollHideTotals = false
@Published public var text: String
@Published public var contentWarning: String
@Published public var displayContentWarning: Bool
@Published public var sensitive: Bool
@Published public var displayPoll: Bool
@Published public var pollMultipleChoice: Bool
@Published public var pollExpiresIn = PollExpiry.oneDay
@Published public private(set) var pollOptions = [PollOption(text: ""), PollOption(text: "")]
@Published public private(set) var attachmentViewModels = [AttachmentViewModel]()
@Published public private(set) var pollOptions: [PollOption]
@Published public private(set) var attachmentViewModels: [AttachmentViewModel]
@Published public private(set) var attachmentUpload: AttachmentUpload?
@Published public private(set) var isPostable = false
@Published public private(set) var canAddAttachment = true
@ -28,8 +27,24 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
private let eventsSubject: PassthroughSubject<Event, Never>
private var attachmentUploadCancellable: AnyCancellable?
init(eventsSubject: PassthroughSubject<Event, Never>) {
init(eventsSubject: PassthroughSubject<Event, Never>,
redraft: (status: Status, identification: Identification)? = nil) {
self.eventsSubject = eventsSubject
text = redraft?.status.text ?? ""
contentWarning = redraft?.status.spoilerText ?? ""
displayContentWarning = !(redraft?.status.spoilerText.isEmpty ?? true)
sensitive = redraft?.status.sensitive ?? false
displayPoll = redraft?.status.poll != nil
pollMultipleChoice = redraft?.status.poll?.multiple ?? false
pollOptions = redraft?.status.poll?.options.map { PollOption(text: $0.title) }
?? [PollOption(text: ""), PollOption(text: "")]
if let redraft = redraft {
attachmentViewModels = redraft.status.mediaAttachments.map {
AttachmentViewModel(attachment: $0, identification: redraft.identification)
}
} else {
attachmentViewModels = [AttachmentViewModel]()
}
$text.map { !$0.isEmpty }
.removeDuplicates()

View file

@ -1,13 +1,14 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import Mastodon
import ServiceLayer
public enum CollectionItemEvent {
case ignorableOutput
case navigation(Navigation)
case attachment(AttachmentViewModel, StatusViewModel)
case reply(StatusViewModel)
case compose(inReplyTo: StatusViewModel?, redraft: Status?)
case report(ReportViewModel)
case share(URL)
}

View file

@ -26,12 +26,24 @@ public final class NewStatusViewModel: ObservableObject {
public init(allIdentitiesService: AllIdentitiesService,
identification: Identification,
environment: AppEnvironment,
inReplyTo: StatusViewModel?) {
inReplyTo: StatusViewModel?,
redraft: Status?) {
self.allIdentitiesService = allIdentitiesService
self.identification = identification
self.environment = environment
inReplyToViewModel = inReplyTo
compositionViewModels = [CompositionViewModel(eventsSubject: compositionEventsSubject)]
let redraftAndIdentification: (status: Status, identification: Identification)?
if let redraft = redraft {
redraftAndIdentification = (status: redraft, identification: identification)
} else {
redraftAndIdentification = nil
}
compositionViewModels = [CompositionViewModel(
eventsSubject: compositionEventsSubject,
redraft: redraftAndIdentification)]
events = eventsSubject.eraseToAnyPublisher()
visibility = identification.identity.preferences.postingDefaultVisibility
allIdentitiesService.authenticatedIdentitiesPublisher()

View file

@ -2,6 +2,7 @@
import Combine
import Foundation
import Mastodon
import ServiceLayer
public final class RootViewModel: ObservableObject {
@ -58,12 +59,16 @@ public extension RootViewModel {
instanceURLService: InstanceURLService(environment: environment))
}
func newStatusViewModel(identification: Identification, inReplyTo: StatusViewModel? = nil) -> NewStatusViewModel {
func newStatusViewModel(
identification: Identification,
inReplyTo: StatusViewModel? = nil,
redraft: Status? = nil) -> NewStatusViewModel {
NewStatusViewModel(
allIdentitiesService: allIdentitiesService,
identification: identification,
environment: environment,
inReplyTo: inReplyTo)
inReplyTo: inReplyTo,
redraft: redraft)
}
}

View file

@ -37,6 +37,7 @@ public extension ShareExtensionNavigationViewModel {
allIdentitiesService: allIdentitiesService,
identification: identification,
environment: environment,
inReplyTo: nil)
inReplyTo: nil,
redraft: nil)
}
}

View file

@ -213,7 +213,10 @@ public extension StatusViewModel {
replyViewModel.configuration = configuration.reply()
eventsSubject.send(Just(.reply(replyViewModel)).setFailureType(to: Error.self).eraseToAnyPublisher())
eventsSubject.send(
Just(.compose(inReplyTo: replyViewModel, redraft: nil))
.setFailureType(to: Error.self)
.eraseToAnyPublisher())
}
func toggleReblogged() {
@ -251,6 +254,35 @@ public extension StatusViewModel {
.eraseToAnyPublisher())
}
func delete() {
eventsSubject.send(
statusService.delete()
.map { _ in .ignorableOutput }
.eraseToAnyPublisher())
}
func deleteAndRedraft() {
let identification = self.identification
eventsSubject.send(
statusService.deleteAndRedraft()
.map { redraft, inReplyToStatusService in
let inReplyToViewModel: StatusViewModel?
if let inReplyToStatusService = inReplyToStatusService {
inReplyToViewModel = Self(
statusService: inReplyToStatusService,
identification: identification)
inReplyToViewModel?.configuration = CollectionItem.StatusConfiguration.default.reply()
} else {
inReplyToViewModel = nil
}
return .compose(inReplyTo: inReplyToViewModel, redraft: redraft)
}
.eraseToAnyPublisher())
}
func attachmentSelected(viewModel: AttachmentViewModel) {
eventsSubject.send(Just(.attachment(viewModel, self)).setFailureType(to: Error.self).eraseToAnyPublisher())
}

View file

@ -49,6 +49,7 @@ private extension CompositionPollOptionView {
UIAction { [weak self] _ in
self?.option.text = textField.text ?? "" },
for: .editingChanged)
textField.text = option.text
stackView.addArrangedSubview(remainingCharactersLabel)
remainingCharactersLabel.adjustsFontForContentSizeCategory = true

View file

@ -419,13 +419,27 @@ private extension StatusView {
}
if viewModel.isMine {
menuItems.append(UIAction(
menuItems += [
UIAction(
title: viewModel.muted
? NSLocalizedString("status.unmute", comment: "")
: NSLocalizedString("status.mute", comment: ""),
image: UIImage(systemName: viewModel.muted ? "speaker" : "speaker.slash")) { _ in
viewModel.toggleMuted()
})
},
UIAction(
title: NSLocalizedString("status.delete", comment: ""),
image: UIImage(systemName: "trash"),
attributes: .destructive) { _ in
viewModel.delete()
},
UIAction(
title: NSLocalizedString("status.delete-and-redraft", comment: ""),
image: UIImage(systemName: "trash.circle"),
attributes: .destructive) { _ in
viewModel.deleteAndRedraft()
}
]
} else {
menuItems.append(UIAction(
title: NSLocalizedString("report", comment: ""),