mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 00:01:00 +00:00
Delete and redraft
This commit is contained in:
parent
96d96bd899
commit
88c3fedd93
15 changed files with 159 additions and 33 deletions
|
@ -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(
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ public extension ShareExtensionNavigationViewModel {
|
|||
allIdentitiesService: allIdentitiesService,
|
||||
identification: identification,
|
||||
environment: environment,
|
||||
inReplyTo: nil)
|
||||
inReplyTo: nil,
|
||||
redraft: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -419,13 +419,27 @@ private extension StatusView {
|
|||
}
|
||||
|
||||
if viewModel.isMine {
|
||||
menuItems.append(UIAction(
|
||||
title: viewModel.muted
|
||||
? NSLocalizedString("status.unmute", comment: "")
|
||||
: NSLocalizedString("status.mute", comment: ""),
|
||||
image: UIImage(systemName: viewModel.muted ? "speaker" : "speaker.slash")) { _ in
|
||||
viewModel.toggleMuted()
|
||||
})
|
||||
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: ""),
|
||||
|
|
Loading…
Reference in a new issue