diff --git a/Localization/app.json b/Localization/app.json index fbc670da6..be86eadef 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -35,6 +35,10 @@ "save_photo_failure": { "title": "Save Photo Failure", "message": "Please enable photo libaray access permission to save photo." + }, + "delete_post": { + "message": "Are you sure you want to delete this post?", + "delete": "DELETE" } }, "controls": { @@ -67,7 +71,8 @@ "report_user": "Report %s", "block_domain": "Block %s", "unblock_domain": "Unblock %s", - "settings": "Settings" + "settings": "Settings", + "delete": "Delete" }, "status": { "user_reblogged": "%s reblogged", diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index ff407b6ed..c778b8f90 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -935,7 +935,6 @@ DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.swift"; sourceTree = ""; }; DB9A485B2603010E008B817C /* PHPickerResultLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPickerResultLoader.swift; sourceTree = ""; }; DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentContainerView+EmptyStateView.swift"; sourceTree = ""; }; - DB9A488326034BD7008B817C /* APIService+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Status.swift"; sourceTree = ""; }; DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+PublishState.swift"; sourceTree = ""; }; DB9A488F26035963008B817C /* APIService+Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Media.swift"; sourceTree = ""; }; DB9A48952603685D008B817C /* MastodonAttachmentService+UploadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonAttachmentService+UploadState.swift"; sourceTree = ""; }; @@ -1735,7 +1734,6 @@ DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */, DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */, DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */, - DB9A488326034BD7008B817C /* APIService+Status.swift */, 2D61254C262547C200299647 /* APIService+Notification.swift */, DB9A488F26035963008B817C /* APIService+Media.swift */, 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */, diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3295adb41..18cf023e9 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -69,7 +69,7 @@ "repositoryURL": "https://github.com/onevcat/Kingfisher.git", "state": { "branch": null, - "revision": "bbc4bc4def7eb05a7ba8e1219f80ee9be327334e", + "revision": "15d199e84677303a7004ed2c5ecaa1a90f3863f8", "version": "6.2.1" } }, diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 3994229ee..af4b70490 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -781,7 +781,6 @@ extension StatusSection { } let author = status.authorForUserProvider let isMyself = authenticationBox.userID == author.id - let canReport = !isMyself let isInSameDomain = authenticationBox.domain == author.domainFromAcct let isMuting = (author.mutingBy ?? Set()).map(\.id).contains(authenticationBox.userID) let isBlocking = (author.blockingBy ?? Set()).map(\.id).contains(authenticationBox.userID) diff --git a/Mastodon/Extension/CoreDataStack/Status.swift b/Mastodon/Extension/CoreDataStack/Status.swift index 1a909285d..73a582937 100644 --- a/Mastodon/Extension/CoreDataStack/Status.swift +++ b/Mastodon/Extension/CoreDataStack/Status.swift @@ -16,7 +16,7 @@ extension Status.Property { id: entity.id, uri: entity.uri, createdAt: entity.createdAt, - content: entity.content, + content: entity.content!, visibility: entity.visibility?.rawValue, sensitive: entity.sensitive ?? false, spoilerText: entity.spoilerText, diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 4ec5e7037..81b8f8cc9 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -27,6 +27,12 @@ internal enum L10n { /// Please try again later. internal static let pleaseTryAgainLater = L10n.tr("Localizable", "Common.Alerts.Common.PleaseTryAgainLater") } + internal enum DeletePost { + /// DELETE + internal static let delete = L10n.tr("Localizable", "Common.Alerts.DeletePost.Delete") + /// Are you sure you want to delete this post? + internal static let message = L10n.tr("Localizable", "Common.Alerts.DeletePost.Message") + } internal enum DiscardPostContent { /// Confirm discard composed post content. internal static let message = L10n.tr("Localizable", "Common.Alerts.DiscardPostContent.Message") @@ -84,6 +90,8 @@ internal enum L10n { internal static let confirm = L10n.tr("Localizable", "Common.Controls.Actions.Confirm") /// Continue internal static let `continue` = L10n.tr("Localizable", "Common.Controls.Actions.Continue") + /// Delete + internal static let delete = L10n.tr("Localizable", "Common.Controls.Actions.Delete") /// Discard internal static let discard = L10n.tr("Localizable", "Common.Controls.Actions.Discard") /// Done diff --git a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift index 9e20e414a..089cf8ad0 100644 --- a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift +++ b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift @@ -300,6 +300,35 @@ extension UserProviderFacade { children.append(shareAction) } + if let status = shareStatus, isMyself { + let deleteAction = UIAction(title: L10n.Common.Controls.Actions.delete, image: UIImage(systemName: "delete.left"), identifier: nil, discoverabilityTitle: nil, attributes: [.destructive], state: .off) { + [weak provider] _ in + guard let provider = provider else { return } + + let alertController = UIAlertController(title: "", message: L10n.Common.Alerts.DeletePost.message, preferredStyle: .alert) + let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .default) { _ in + } + alertController.addAction(cancelAction) + let deleteAction = UIAlertAction(title: L10n.Common.Alerts.DeletePost.delete, style: .destructive) { _ in + guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return } + provider.context.apiService.deleteStatus(domain: activeMastodonAuthenticationBox.domain, + statusID: status.id, + authorizationBox: activeMastodonAuthenticationBox + ) + .sink { _ in + // do nothing + } receiveValue: { _ in + // do nothing + } + .store(in: &provider.context.disposeBag) + } + alertController.addAction(deleteAction) + provider.present(alertController, animated: true, completion: nil) + + } + children.append(deleteAction) + } + return UIMenu(title: "", options: [], children: children) } diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 59f83a5cd..7fc28ca80 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -2,6 +2,8 @@ "Common.Alerts.BlockDomain.Message" = "Are you really, really sure you want to block the entire %@? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed."; "Common.Alerts.Common.PleaseTryAgain" = "Please try again."; "Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later."; +"Common.Alerts.DeletePost.Delete" = "DELETE"; +"Common.Alerts.DeletePost.Message" = "Are you sure you want to delete this post?"; "Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content."; "Common.Alerts.DiscardPostContent.Title" = "Discard Publish"; "Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post. @@ -22,6 +24,7 @@ Please check your internet connection."; "Common.Controls.Actions.Cancel" = "Cancel"; "Common.Controls.Actions.Confirm" = "Confirm"; "Common.Controls.Actions.Continue" = "Continue"; +"Common.Controls.Actions.Delete" = "Delete"; "Common.Controls.Actions.Discard" = "Discard"; "Common.Controls.Actions.Done" = "Done"; "Common.Controls.Actions.Edit" = "Edit"; diff --git a/Mastodon/Service/APIService/APIService+Status.swift b/Mastodon/Service/APIService/APIService+Status.swift index 08806c886..2b91bbd35 100644 --- a/Mastodon/Service/APIService/APIService+Status.swift +++ b/Mastodon/Service/APIService/APIService+Status.swift @@ -88,4 +88,59 @@ extension APIService { .eraseToAnyPublisher() } + func deleteStatus( + domain: String, + statusID: Mastodon.Entity.Status.ID, + authorizationBox: AuthenticationService.MastodonAuthenticationBox + ) -> AnyPublisher, Error> { + let authorization = authorizationBox.userAuthorization + let query = Mastodon.API.Statuses.DeleteStatusQuery(id: statusID) + return Mastodon.API.Statuses.deleteStatus( + session: session, + domain: domain, + query: query, + authorization: authorization + ) + .flatMap { response -> AnyPublisher, Error> in + return self.backgroundManagedObjectContext.performChanges{ + // fetch old Status + let oldStatus: Status? = { + let request = Status.sortedFetchRequest + request.predicate = Status.predicate(domain: domain, id: response.value.id) + request.fetchLimit = 1 + request.returnsObjectsAsFaults = false + do { + return try self.backgroundManagedObjectContext.fetch(request).first + } catch { + assertionFailure(error.localizedDescription) + return nil + } + }() + if let status = oldStatus { + if let timelineIndex = status.homeTimelineIndexes?.filter({ $0.userID == status.author.id }).first { + self.backgroundManagedObjectContext.delete(timelineIndex) + } + if let poll = status.poll { + self.backgroundManagedObjectContext.delete(poll) + } + if let pollOptions = status.poll?.options { + pollOptions.forEach({ self.backgroundManagedObjectContext.delete($0) }) + } + self.backgroundManagedObjectContext.delete(status) + } + } + .setFailureType(to: Error.self) + .tryMap { result -> Mastodon.Response.Content in + switch result { + case .success: + return response + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } + } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift index bb5a4abfc..e6c8b19d3 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift @@ -10,7 +10,7 @@ import Combine extension Mastodon.API.Statuses { - static func viewStatusEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL { + static func statusEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL { let pathComponent = "statuses/" + statusID return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent) } @@ -38,7 +38,7 @@ extension Mastodon.API.Statuses { authorization: Mastodon.API.OAuth.Authorization? ) -> AnyPublisher, Error> { let request = Mastodon.API.get( - url: viewStatusEndpointURL(domain: domain, statusID: statusID), + url: statusEndpointURL(domain: domain, statusID: statusID), query: nil, authorization: authorization ) @@ -150,6 +150,54 @@ extension Mastodon.API.Statuses { } +extension Mastodon.API.Statuses { + + /// Delete status + /// + /// Delete one of your own statuses. + /// + /// - Since: 0.0.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/5/7 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/statuses/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - query: `DeleteStatusQuery` + /// - authorization: User token + /// - Returns: `AnyPublisher` contains `Status` nested in the response + public static func deleteStatus( + session: URLSession, + domain: String, + query: DeleteStatusQuery, + authorization: Mastodon.API.OAuth.Authorization? + ) -> AnyPublisher, Error> { + let request = Mastodon.API.delete( + url: statusEndpointURL(domain: domain, statusID: query.id), + query: query, + authorization: authorization + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } + + public struct DeleteStatusQuery: Codable, DeleteQuery { + public let id: Mastodon.Entity.Status.ID + + public init( + id: Mastodon.Entity.Status.ID + ) { + self.id = id + } + } +} + extension Mastodon.API.Statuses { static func statusContextEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL { diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift index 490429fce..7f8a4fd4e 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift @@ -26,7 +26,7 @@ extension Mastodon.Entity { public let uri: String public let createdAt: Date public let account: Account - public let content: String + public let content: String? // will be optional when delete status public let visibility: Visibility? public let sensitive: Bool?