From 5278002c1562cfc5b3596e7b4112c22ebbce2c0a Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 7 May 2021 16:08:07 +0800 Subject: [PATCH 1/6] feat: Add post delete action entry for user posts --- Localization/app.json | 7 ++- Mastodon.xcodeproj/project.pbxproj | 2 - .../xcshareddata/swiftpm/Package.resolved | 2 +- .../Diffiable/Section/StatusSection.swift | 1 - Mastodon/Extension/CoreDataStack/Status.swift | 2 +- Mastodon/Generated/Strings.swift | 8 +++ .../UserProvider/UserProviderFacade.swift | 29 ++++++++++ .../Resources/en.lproj/Localizable.strings | 3 + .../APIService/APIService+Status.swift | 55 +++++++++++++++++++ .../API/Mastodon+API+Statuses.swift | 52 +++++++++++++++++- .../Entity/Mastodon+Entity+Status.swift | 2 +- 11 files changed, 154 insertions(+), 9 deletions(-) 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? From faeb8d99efd9bf4b4139c03a3605b65b3e9d6822 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 7 May 2021 18:25:57 +0800 Subject: [PATCH 2/6] feat: display custom emoji for timeline post --- .../CoreData.xcdatamodel/contents | 10 +-- CoreDataStack/Entity/MastodonUser.swift | 13 ++++ CoreDataStack/Entity/Status.swift | 19 +++-- Mastodon.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Section/ComposeStatusSection.swift | 3 +- .../Diffiable/Section/StatusSection.swift | 14 +++- Mastodon/Extension/ActiveLabel.swift | 20 ++++- Mastodon/Extension/CoreDataStack/Emojis.swift | 36 +++++++++ .../CoreDataStack/MastodonUser.swift | 3 + Mastodon/Extension/CoreDataStack/Status.swift | 3 + Mastodon/Helper/MastodonStatusContent.swift | 74 ++++++++++++------- .../Header/ProfileHeaderViewController.swift | 2 +- .../Header/View/ProfileFieldView.swift | 2 +- .../Settings/SettingsViewController.swift | 2 +- .../Scene/Share/View/Content/StatusView.swift | 14 ++-- .../CoreData/APIService+CoreData+Status.swift | 4 - 17 files changed, 166 insertions(+), 65 deletions(-) create mode 100644 Mastodon/Extension/CoreDataStack/Emojis.swift diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index 93d6e4731..6c2d177f8 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -45,7 +45,6 @@ - @@ -102,6 +101,7 @@ + @@ -197,6 +197,7 @@ + @@ -216,7 +217,6 @@ - @@ -267,12 +267,12 @@ - + - + diff --git a/CoreDataStack/Entity/MastodonUser.swift b/CoreDataStack/Entity/MastodonUser.swift index 714b6d0f6..e93d923c0 100644 --- a/CoreDataStack/Entity/MastodonUser.swift +++ b/CoreDataStack/Entity/MastodonUser.swift @@ -25,6 +25,9 @@ final public class MastodonUser: NSManagedObject { @NSManaged public private(set) var headerStatic: String? @NSManaged public private(set) var note: String? @NSManaged public private(set) var url: String? + + @NSManaged public private(set) var emojisData: Data? + @NSManaged public private(set) var statusesCount: NSNumber @NSManaged public private(set) var followingCount: NSNumber @NSManaged public private(set) var followersCount: NSNumber @@ -88,6 +91,8 @@ extension MastodonUser { user.headerStatic = property.headerStatic user.note = property.note user.url = property.url + user.emojisData = property.emojisData + user.statusesCount = NSNumber(value: property.statusesCount) user.followingCount = NSNumber(value: property.followingCount) user.followersCount = NSNumber(value: property.followersCount) @@ -151,6 +156,11 @@ extension MastodonUser { self.url = url } } + public func update(emojisData: Data?) { + if self.emojisData != emojisData { + self.emojisData = emojisData + } + } public func update(statusesCount: Int) { if self.statusesCount.intValue != statusesCount { self.statusesCount = NSNumber(value: statusesCount) @@ -270,6 +280,7 @@ extension MastodonUser { public let headerStatic: String? public let note: String? public let url: String? + public let emojisData: Data? public let statusesCount: Int public let followingCount: Int public let followersCount: Int @@ -292,6 +303,7 @@ extension MastodonUser { headerStatic: String?, note: String?, url: String?, + emojisData: Data?, statusesCount: Int, followingCount: Int, followersCount: Int, @@ -313,6 +325,7 @@ extension MastodonUser { self.headerStatic = headerStatic self.note = note self.url = url + self.emojisData = emojisData self.statusesCount = statusesCount self.followingCount = followingCount self.followersCount = followersCount diff --git a/CoreDataStack/Entity/Status.swift b/CoreDataStack/Entity/Status.swift index 1bb71a1db..79214ea42 100644 --- a/CoreDataStack/Entity/Status.swift +++ b/CoreDataStack/Entity/Status.swift @@ -24,6 +24,8 @@ public final class Status: NSManagedObject { @NSManaged public private(set) var spoilerText: String? @NSManaged public private(set) var application: Application? + @NSManaged public private(set) var emojisData: Data? + // Informational @NSManaged public private(set) var reblogsCount: NSNumber @NSManaged public private(set) var favouritesCount: NSNumber @@ -54,7 +56,6 @@ public final class Status: NSManagedObject { // one-to-many relationship @NSManaged public private(set) var reblogFrom: Set? @NSManaged public private(set) var mentions: Set? - @NSManaged public private(set) var emojis: Set? @NSManaged public private(set) var tags: Set? @NSManaged public private(set) var homeTimelineIndexes: Set? @NSManaged public private(set) var mediaAttachments: Set? @@ -77,7 +78,6 @@ extension Status { replyTo: Status?, poll: Poll?, mentions: [Mention]?, - emojis: [Emoji]?, tags: [Tag]?, mediaAttachments: [Attachment]?, favouritedBy: MastodonUser?, @@ -100,6 +100,8 @@ extension Status { status.sensitive = property.sensitive status.spoilerText = property.spoilerText status.application = application + + status.emojisData = property.emojisData status.reblogsCount = property.reblogsCount status.favouritesCount = property.favouritesCount @@ -121,9 +123,6 @@ extension Status { if let mentions = mentions { status.mutableSetValue(forKey: #keyPath(Status.mentions)).addObjects(from: mentions) } - if let emojis = emojis { - status.mutableSetValue(forKey: #keyPath(Status.emojis)).addObjects(from: emojis) - } if let tags = tags { status.mutableSetValue(forKey: #keyPath(Status.tags)).addObjects(from: tags) } @@ -148,6 +147,12 @@ extension Status { return status } + public func update(emojisData: Data?) { + if self.emojisData != emojisData { + self.emojisData = emojisData + } + } + public func update(reblogsCount: NSNumber) { if self.reblogsCount.intValue != reblogsCount.intValue { self.reblogsCount = reblogsCount @@ -248,6 +253,8 @@ extension Status { public let sensitive: Bool public let spoilerText: String? + public let emojisData: Data? + public let reblogsCount: NSNumber public let favouritesCount: NSNumber public let repliesCount: NSNumber? @@ -269,6 +276,7 @@ extension Status { visibility: String?, sensitive: Bool, spoilerText: String?, + emojisData: Data?, reblogsCount: NSNumber, favouritesCount: NSNumber, repliesCount: NSNumber?, @@ -288,6 +296,7 @@ extension Status { self.visibility = visibility self.sensitive = sensitive self.spoilerText = spoilerText + self.emojisData = emojisData self.reblogsCount = reblogsCount self.favouritesCount = favouritesCount self.repliesCount = repliesCount diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index ff407b6ed..2f37b291a 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -400,6 +400,7 @@ DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; }; DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */; }; DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; + DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; }; DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; }; DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; }; DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; }; @@ -962,6 +963,7 @@ DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = ""; }; DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = ""; }; DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = ""; }; + DBAFB7342645463500371D5F /* Emojis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emojis.swift; sourceTree = ""; }; DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = ""; }; DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = ""; }; DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = ""; }; @@ -1594,6 +1596,7 @@ DB6D9F6E2635807F008423CD /* Setting.swift */, DB6D9F4826353FD6008423CD /* Subscription.swift */, DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */, + DBAFB7342645463500371D5F /* Emojis.swift */, ); path = CoreDataStack; sourceTree = ""; @@ -3199,6 +3202,7 @@ DB6180ED26391C6C0018D199 /* TransitioningMath.swift in Sources */, 2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */, DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */, + DBAFB7352645463500371D5F /* Emojis.swift in Sources */, DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */, DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */, DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */, @@ -3913,8 +3917,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 4.0.0; + kind = exactVersion; + version = 5.0.1; }; }; 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = { diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3295adb41..c32d3a6b2 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift", "state": { "branch": null, - "revision": "d6cf96e0ca4f2269021bcf8f11381ab57897f84a", - "version": "4.0.0" + "revision": "40e104063d825d1125ef4b8eeb6460eba8a57483", + "version": "5.0.1" } }, { diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift index 91363ef09..2b7aecae0 100644 --- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift @@ -68,7 +68,8 @@ extension ComposeStatusSection { }() cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct // set text - cell.statusView.activeTextLabel.configure(content: status.content) + //status.emoji + cell.statusView.activeTextLabel.configure(content: status.content, emojiDict: [:]) // set date cell.statusView.dateLabel.text = status.createdAt.shortTimeAgoSinceNow diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 3994229ee..d069266f8 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -158,10 +158,11 @@ extension StatusSection { .store(in: &cell.disposeBag) // set name username - cell.statusView.nameLabel.text = { + let nameText: String = { let author = (status.reblog ?? status).author return author.displayName.isEmpty ? author.username : author.displayName }() + cell.statusView.nameLabel.configure(content: nameText, emojiDict: (status.reblog ?? status).author.emojiDict) cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct // set avatar if let reblog = status.reblog { @@ -176,7 +177,10 @@ extension StatusSection { } // set text - cell.statusView.activeTextLabel.configure(content: (status.reblog ?? status).content) + cell.statusView.activeTextLabel.configure( + content: (status.reblog ?? status).content, + emojiDict: (status.reblog ?? status).emojiDict + ) // prepare media attachments let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending } @@ -569,15 +573,16 @@ extension StatusSection { if status.reblog != nil { cell.statusView.headerContainerView.isHidden = false cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.reblogIconImage) - cell.statusView.headerInfoLabel.text = { + let headerText: String = { let author = status.author let name = author.displayName.isEmpty ? author.username : author.displayName return L10n.Common.Controls.Status.userReblogged(name) }() + cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.author.emojiDict) } else if status.inReplyToID != nil { cell.statusView.headerContainerView.isHidden = false cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage) - cell.statusView.headerInfoLabel.text = { + let headerText: String = { guard let replyTo = status.replyTo else { return L10n.Common.Controls.Status.userRepliedTo("-") } @@ -585,6 +590,7 @@ extension StatusSection { let name = author.displayName.isEmpty ? author.username : author.displayName return L10n.Common.Controls.Status.userRepliedTo(name) }() + cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.replyTo?.author.emojiDict ?? [:]) } else { cell.statusView.headerContainerView.isHidden = true } diff --git a/Mastodon/Extension/ActiveLabel.swift b/Mastodon/Extension/ActiveLabel.swift index 66452e23e..d929cb571 100644 --- a/Mastodon/Extension/ActiveLabel.swift +++ b/Mastodon/Extension/ActiveLabel.swift @@ -14,6 +14,8 @@ extension ActiveLabel { enum Style { case `default` + case statusHeader + case statusName case profileField } @@ -25,6 +27,7 @@ extension ActiveLabel { mentionColor = Asset.Colors.Label.highlight.color hashtagColor = Asset.Colors.Label.highlight.color URLColor = Asset.Colors.Label.highlight.color + emojiPlaceholderColor = .systemFill #if DEBUG text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." #endif @@ -33,6 +36,14 @@ extension ActiveLabel { case .default: font = .preferredFont(forTextStyle: .body) textColor = Asset.Colors.Label.primary.color + case .statusHeader: + font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium)) + textColor = Asset.Colors.Label.secondary.color + numberOfLines = 1 + case .statusName: + font = .systemFont(ofSize: 17, weight: .semibold) + textColor = Asset.Colors.Label.primary.color + numberOfLines = 1 case .profileField: font = .preferredFont(forTextStyle: .body) textColor = Asset.Colors.Label.primary.color @@ -44,9 +55,10 @@ extension ActiveLabel { extension ActiveLabel { /// status content - func configure(content: String) { + func configure(content: String, emojiDict: MastodonStatusContent.EmojiDict) { activeEntities.removeAll() - if let parseResult = try? MastodonStatusContent.parse(status: content) { + + if let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict) { text = parseResult.trimmed activeEntities = parseResult.activeEntities } else { @@ -55,8 +67,8 @@ extension ActiveLabel { } /// account note - func configure(note: String) { - configure(content: note) + func configure(note: String, emojiDict: MastodonStatusContent.EmojiDict) { + configure(content: note, emojiDict: emojiDict) } } diff --git a/Mastodon/Extension/CoreDataStack/Emojis.swift b/Mastodon/Extension/CoreDataStack/Emojis.swift new file mode 100644 index 000000000..87ae50171 --- /dev/null +++ b/Mastodon/Extension/CoreDataStack/Emojis.swift @@ -0,0 +1,36 @@ +// +// Emojis.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-5-7. +// + +import Foundation +import MastodonSDK + +protocol EmojiContinaer { + var emojisData: Data? { get } +} + +extension EmojiContinaer { + + static func encode(emojis: [Mastodon.Entity.Emoji]) -> Data? { + return try? JSONEncoder().encode(emojis) + } + + var emojis: [Mastodon.Entity.Emoji]? { + let decoder = JSONDecoder() + return emojisData.flatMap { try? decoder.decode([Mastodon.Entity.Emoji].self, from: $0) } + } + + var emojiDict: MastodonStatusContent.EmojiDict { + var dict = MastodonStatusContent.EmojiDict() + for emoji in emojis ?? [] { + guard let url = URL(string: emoji.url) else { continue } + dict[emoji.shortcode] = url + } + return dict + } + +} + diff --git a/Mastodon/Extension/CoreDataStack/MastodonUser.swift b/Mastodon/Extension/CoreDataStack/MastodonUser.swift index b780f5916..8180b0255 100644 --- a/Mastodon/Extension/CoreDataStack/MastodonUser.swift +++ b/Mastodon/Extension/CoreDataStack/MastodonUser.swift @@ -23,6 +23,7 @@ extension MastodonUser.Property { headerStatic: entity.headerStatic, note: entity.note, url: entity.url, + emojisData: entity.emojis.flatMap { Status.encode(emojis: $0) }, statusesCount: entity.statusesCount, followingCount: entity.followingCount, followersCount: entity.followersCount, @@ -98,3 +99,5 @@ extension MastodonUser { return items } } + +extension MastodonUser: EmojiContinaer { } diff --git a/Mastodon/Extension/CoreDataStack/Status.swift b/Mastodon/Extension/CoreDataStack/Status.swift index 1a909285d..701f9243e 100644 --- a/Mastodon/Extension/CoreDataStack/Status.swift +++ b/Mastodon/Extension/CoreDataStack/Status.swift @@ -20,6 +20,7 @@ extension Status.Property { visibility: entity.visibility?.rawValue, sensitive: entity.sensitive ?? false, spoilerText: entity.spoilerText, + emojisData: entity.emojis.flatMap { Status.encode(emojis: $0) }, reblogsCount: NSNumber(value: entity.reblogsCount), favouritesCount: NSNumber(value: entity.favouritesCount), repliesCount: entity.repliesCount.flatMap { NSNumber(value: $0) }, @@ -86,3 +87,5 @@ extension Status { return items } } + +extension Status: EmojiContinaer { } diff --git a/Mastodon/Helper/MastodonStatusContent.swift b/Mastodon/Helper/MastodonStatusContent.swift index 5b535b806..284b726a6 100755 --- a/Mastodon/Helper/MastodonStatusContent.swift +++ b/Mastodon/Helper/MastodonStatusContent.swift @@ -11,9 +11,21 @@ import ActiveLabel enum MastodonStatusContent { - static func parse(status: String) throws -> MastodonStatusContent.ParseResult { - let status = status.replacingOccurrences(of: "
", with: "\n") - let rootNode = try Node.parse(document: status) + typealias EmojiShortcode = String + typealias EmojiDict = [EmojiShortcode: URL] + + static func parse(content: String, emojiDict: EmojiDict) throws -> MastodonStatusContent.ParseResult { + let document: String = { + var content = content + content = content.replacingOccurrences(of: "
", with: "\n") + for (shortcode, url) in emojiDict { + let emojiNode = "\(shortcode)" + let pattern = ":\(shortcode):" + content = content.replacingOccurrences(of: pattern, with: emojiNode) + } + return content + }() + let rootNode = try Node.parse(document: document) let text = String(rootNode.text) var activeEntities: [ActiveEntity] = [] @@ -25,7 +37,7 @@ enum MastodonStatusContent { case .url: guard let href = entity.href else { continue } let text = String(entity.text) - activeEntities.append(ActiveEntity(range: range, type: .url(text, trimmed: entity.hrefEllipsis ?? text, url: href))) + activeEntities.append(ActiveEntity(range: range, type: .url(text, trimmed: entity.hrefEllipsis ?? text, url: href, userInfo: nil))) case .hashtag: var userInfo: [AnyHashable: Any] = [:] entity.href.flatMap { href in @@ -40,30 +52,47 @@ enum MastodonStatusContent { } let mention = String(entity.text).deletingPrefix("@") activeEntities.append(ActiveEntity(range: range, type: .mention(mention, userInfo: userInfo))) - default: + case .emoji: + var userInfo: [AnyHashable: Any] = [:] + guard let href = entity.href else { continue } + userInfo["href"] = href + let emoji = String(entity.text) + activeEntities.append(ActiveEntity(range: range, type: .emoji(emoji, url: href, userInfo: userInfo))) + case .none: continue } } var trimmed = text for activeEntity in activeEntities { - guard case .url = activeEntity.type else { continue } - MastodonStatusContent.trimEntity(status: &trimmed, activeEntity: activeEntity, activeEntities: activeEntities) + MastodonStatusContent.trimEntity(toot: &trimmed, activeEntity: activeEntity, activeEntities: activeEntities) } return ParseResult( - document: status, + document: document, original: text, trimmed: trimmed, - activeEntities: validate(text: trimmed, activeEntities: activeEntities) ? activeEntities : [] + activeEntities: activeEntities ) } - static func trimEntity(status: inout String, activeEntity: ActiveEntity, activeEntities: [ActiveEntity]) { - guard case let .url(text, trimmed, _, _) = activeEntity.type else { return } + static func trimEntity(toot: inout String, activeEntity: ActiveEntity, activeEntities: [ActiveEntity]) { + let text: String + let trimmed: String + switch activeEntity.type { + case .url(let _text, let _trimmed, _, _): + text = _text + trimmed = _trimmed + case .emoji(let _text, _, _): + text = _text + trimmed = " " + default: + return + } + guard let index = activeEntities.firstIndex(where: { $0.range == activeEntity.range }) else { return } - guard let range = Range(activeEntity.range, in: status) else { return } - status.replaceSubrange(range, with: trimmed) + guard let range = Range(activeEntity.range, in: toot) else { return } + toot.replaceSubrange(range, with: trimmed) let offset = trimmed.count - text.count activeEntity.range.length += offset @@ -73,19 +102,6 @@ enum MastodonStatusContent { moveActiveEntity.range.location += offset } } - - private static func validate(text: String, activeEntities: [ActiveEntity]) -> Bool { - for activeEntity in activeEntities { - let count = text.utf16.count - let endIndex = activeEntity.range.location + activeEntity.range.length - guard endIndex <= count else { - assertionFailure("Please file issue") - return false - } - } - - return true - } } @@ -106,6 +122,7 @@ extension MastodonStatusContent { } } + extension MastodonStatusContent { class Node { @@ -154,6 +171,10 @@ extension MastodonStatusContent { } } + if _classNames.contains("emoji") { + return .emoji + } + return nil }() self.level = level @@ -257,6 +278,7 @@ extension MastodonStatusContent.Node { case url case mention case hashtag + case emoji } static func entities(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] { diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 3949c3281..0610ae52f 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -175,7 +175,7 @@ extension ProfileHeaderViewController { .receive(on: DispatchQueue.main) .sink { [weak self] isEditing, note, editingNote in guard let self = self else { return } - self.profileHeaderView.bioActiveLabel.configure(note: note ?? "") + self.profileHeaderView.bioActiveLabel.configure(note: note ?? "", emojiDict: [:]) // FIXME: custom emoji self.profileHeaderView.bioTextEditorView.text = editingNote ?? "" } .store(in: &disposeBag) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift index e95697e5c..320a495eb 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift @@ -20,7 +20,7 @@ final class ProfileFieldView: UIView { let valueActiveLabel: ActiveLabel = { let label = ActiveLabel(style: .profileField) - label.configure(content: "value") + label.configure(content: "value", emojiDict: [:]) return label }() diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index 3c4a101a0..e3b186026 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -108,7 +108,7 @@ class SettingsViewController: UIViewController, NeedsDependency { let label = ActiveLabel(style: .default) label.textAlignment = .center - label.configure(content: "Mastodon is open source software. You can contribute or report issues on GitHub at tootsuite/mastodon (v3.3.0).") + label.configure(content: "Mastodon is open source software. You can contribute or report issues on GitHub at tootsuite/mastodon (v3.3.0).", emojiDict: [:]) label.delegate = self view.addArrangedSubview(label) diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 20437a8d2..9655940c4 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -11,7 +11,7 @@ import AVKit import ActiveLabel import AlamofireImage -protocol StatusViewDelegate: class { +protocol StatusViewDelegate: AnyObject { func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel) func statusView(_ statusView: StatusView, avatarButtonDidPressed button: UIButton) func statusView(_ statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) @@ -69,10 +69,8 @@ final class StatusView: UIView { return label }() - let headerInfoLabel: UILabel = { - let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium)) - label.textColor = Asset.Colors.Label.secondary.color + let headerInfoLabel: ActiveLabel = { + let label = ActiveLabel(style: .statusHeader) label.text = "Bob reblogged" return label }() @@ -87,10 +85,8 @@ final class StatusView: UIView { }() let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton() - let nameLabel: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: 17, weight: .semibold) - label.textColor = Asset.Colors.Label.primary.color + let nameLabel: ActiveLabel = { + let label = ActiveLabel(style: .statusName) label.text = "Alice" return label }() diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Status.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Status.swift index 328fa2305..32628a203 100644 --- a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Status.swift +++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Status.swift @@ -89,9 +89,6 @@ extension APIService.CoreData { let metions = entity.mentions?.enumerated().compactMap { index, mention -> Mention in Mention.insert(into: managedObjectContext, property: Mention.Property(id: mention.id, username: mention.username, acct: mention.acct, url: mention.url), index: index) } - let emojis = entity.emojis?.compactMap { emoji -> Emoji in - Emoji.insert(into: managedObjectContext, property: Emoji.Property(shortcode: emoji.shortcode, url: emoji.url, staticURL: emoji.staticURL, visibleInPicker: emoji.visibleInPicker, category: emoji.category)) - } let tags = entity.tags?.compactMap { tag -> Tag in let histories = tag.history?.compactMap { history -> History in History.insert(into: managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts)) @@ -121,7 +118,6 @@ extension APIService.CoreData { replyTo: replyTo, poll: poll, mentions: metions, - emojis: emojis, tags: tags, mediaAttachments: mediaAttachments, favouritedBy: (entity.favourited ?? false) ? requestMastodonUser : nil, From 26187d98b7993d9bc0ad8be5d830367c8365f319 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 7 May 2021 18:42:49 +0800 Subject: [PATCH 3/6] chore: renaming interface --- Mastodon/Helper/MastodonStatusContent.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Mastodon/Helper/MastodonStatusContent.swift b/Mastodon/Helper/MastodonStatusContent.swift index 284b726a6..0f9dbc6c0 100755 --- a/Mastodon/Helper/MastodonStatusContent.swift +++ b/Mastodon/Helper/MastodonStatusContent.swift @@ -65,7 +65,7 @@ enum MastodonStatusContent { var trimmed = text for activeEntity in activeEntities { - MastodonStatusContent.trimEntity(toot: &trimmed, activeEntity: activeEntity, activeEntities: activeEntities) + MastodonStatusContent.trimEntity(status: &trimmed, activeEntity: activeEntity, activeEntities: activeEntities) } return ParseResult( @@ -76,7 +76,7 @@ enum MastodonStatusContent { ) } - static func trimEntity(toot: inout String, activeEntity: ActiveEntity, activeEntities: [ActiveEntity]) { + static func trimEntity(status: inout String, activeEntity: ActiveEntity, activeEntities: [ActiveEntity]) { let text: String let trimmed: String switch activeEntity.type { @@ -91,8 +91,8 @@ enum MastodonStatusContent { } guard let index = activeEntities.firstIndex(where: { $0.range == activeEntity.range }) else { return } - guard let range = Range(activeEntity.range, in: toot) else { return } - toot.replaceSubrange(range, with: trimmed) + guard let range = Range(activeEntity.range, in: status) else { return } + status.replaceSubrange(range, with: trimmed) let offset = trimmed.count - text.count activeEntity.range.length += offset From d3256f31713f29584393e1ec103500232a249363 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 7 May 2021 20:36:31 +0800 Subject: [PATCH 4/6] fix: delete reblogFrom inNotifications poll pollOption when delete status --- .../CoreData.xcdatamodel/contents | 11 ++++++----- CoreDataStack/Entity/Status.swift | 2 ++ Localization/app.json | 2 +- Mastodon/Generated/Strings.swift | 2 +- Mastodon/Resources/en.lproj/Localizable.strings | 2 +- Mastodon/Service/APIService/APIService+Status.swift | 9 --------- 6 files changed, 11 insertions(+), 17 deletions(-) diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index 93d6e4731..9073b3fbf 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -87,7 +87,7 @@ - + @@ -158,7 +158,7 @@ - +
@@ -218,14 +218,15 @@ - + + - + @@ -279,7 +280,7 @@ - + diff --git a/CoreDataStack/Entity/Status.swift b/CoreDataStack/Entity/Status.swift index 1bb71a1db..cd1ebf3f5 100644 --- a/CoreDataStack/Entity/Status.swift +++ b/CoreDataStack/Entity/Status.swift @@ -60,6 +60,8 @@ public final class Status: NSManagedObject { @NSManaged public private(set) var mediaAttachments: Set? @NSManaged public private(set) var replyFrom: Set? + @NSManaged public private(set) var inNotifications: Set? + @NSManaged public private(set) var updatedAt: Date @NSManaged public private(set) var deletedAt: Date? @NSManaged public private(set) var revealedAt: Date? diff --git a/Localization/app.json b/Localization/app.json index be86eadef..1776c2c0c 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -38,7 +38,7 @@ }, "delete_post": { "message": "Are you sure you want to delete this post?", - "delete": "DELETE" + "delete": "Delete" } }, "controls": { diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 81b8f8cc9..092aa5c45 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -28,7 +28,7 @@ internal enum L10n { internal static let pleaseTryAgainLater = L10n.tr("Localizable", "Common.Alerts.Common.PleaseTryAgainLater") } internal enum DeletePost { - /// DELETE + /// 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") diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 7fc28ca80..d88b00e99 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -2,7 +2,7 @@ "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.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"; diff --git a/Mastodon/Service/APIService/APIService+Status.swift b/Mastodon/Service/APIService/APIService+Status.swift index 2b91bbd35..c927b05a8 100644 --- a/Mastodon/Service/APIService/APIService+Status.swift +++ b/Mastodon/Service/APIService/APIService+Status.swift @@ -117,15 +117,6 @@ extension APIService { } }() 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) } } From 04eeb0100e6ec4f61b07b2a7e95969cfefc09729 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Sat, 8 May 2021 10:38:55 +0800 Subject: [PATCH 5/6] fix: safely cancel the listenser for status --- Mastodon/Diffiable/Section/StatusSection.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index af4b70490..76bc91592 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -143,6 +143,21 @@ extension StatusSection { requestUserID: String, statusItemAttribute: Item.StatusAttribute ) { + // safely cancel the listenser when deleted + ManagedObjectObserver.observe(object: status.reblog ?? status) + .receive(on: DispatchQueue.main) + .sink { _ in + // do nothing + } receiveValue: { [weak cell] change in + guard let cell = cell else { return } + guard let changeType = change.changeType else { return } + if case .delete = changeType { + cell.disposeBag.removeAll() + } + } + .store(in: &cell.disposeBag) + + // set header StatusSection.configureHeader(cell: cell, status: status) ManagedObjectObserver.observe(object: status) From 505c251b37284a8c5b8af9ef7c2a9a1207f34126 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 10 May 2021 15:40:46 +0800 Subject: [PATCH 6/6] fix: move message to title --- Localization/app.json | 4 ++-- Mastodon/Generated/Strings.swift | 6 +++--- Mastodon/Protocol/UserProvider/UserProviderFacade.swift | 4 ++-- Mastodon/Resources/en.lproj/Localizable.strings | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 1776c2c0c..ab888f8f8 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -29,7 +29,7 @@ "confirm": "Sign Out" }, "block_domain": { - "message": "Are you really, really sure you want to block the entire %s? 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.", + "title": "Are you really, really sure you want to block the entire %s? 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.", "block_entire_domain": "Block entire domain" }, "save_photo_failure": { @@ -37,7 +37,7 @@ "message": "Please enable photo libaray access permission to save photo." }, "delete_post": { - "message": "Are you sure you want to delete this post?", + "title": "Are you sure you want to delete this post?", "delete": "Delete" } }, diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 092aa5c45..6e8c8e39d 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -17,8 +17,8 @@ internal enum L10n { /// Block entire domain internal static let blockEntireDomain = L10n.tr("Localizable", "Common.Alerts.BlockDomain.BlockEntireDomain") /// 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. - internal static func message(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.BlockDomain.Message", String(describing: p1)) + internal static func title(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Alerts.BlockDomain.Title", String(describing: p1)) } } internal enum Common { @@ -31,7 +31,7 @@ internal enum L10n { /// 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 static let title = L10n.tr("Localizable", "Common.Alerts.DeletePost.Title") } internal enum DiscardPostContent { /// Confirm discard composed post content. diff --git a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift index 089cf8ad0..1f9215a76 100644 --- a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift +++ b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift @@ -252,7 +252,7 @@ extension UserProviderFacade { } else { let blockDomainAction = UIAction(title: L10n.Common.Controls.Actions.blockDomain(mastodonUser.domainFromAcct), image: UIImage(systemName: "nosign"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak provider] _ in guard let provider = provider else { return } - let alertController = UIAlertController(title: "", message: L10n.Common.Alerts.BlockDomain.message(mastodonUser.domainFromAcct), preferredStyle: .alert) + let alertController = UIAlertController(title: L10n.Common.Alerts.BlockDomain.title(mastodonUser.domainFromAcct), message: nil, preferredStyle: .alert) let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .default) { _ in } alertController.addAction(cancelAction) @@ -305,7 +305,7 @@ extension UserProviderFacade { [weak provider] _ in guard let provider = provider else { return } - let alertController = UIAlertController(title: "", message: L10n.Common.Alerts.DeletePost.message, preferredStyle: .alert) + let alertController = UIAlertController(title: L10n.Common.Alerts.DeletePost.title, message: nil, preferredStyle: .alert) let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .default) { _ in } alertController.addAction(cancelAction) diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index d88b00e99..7b11194a3 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -1,9 +1,9 @@ "Common.Alerts.BlockDomain.BlockEntireDomain" = "Block entire domain"; -"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.BlockDomain.Title" = "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.DeletePost.Title" = "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.