Merge branch 'develop' into feature/post-emoji

This commit is contained in:
CMK 2021-05-10 18:48:47 +08:00
commit 6a54beca1e
13 changed files with 173 additions and 19 deletions

View File

@ -86,7 +86,7 @@
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userID" attributeType="String"/>
<relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="inNotifications" inverseEntity="Status"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
@ -158,7 +158,7 @@
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="votersCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="votesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="options" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="poll" inverseEntity="PollOption"/>
<relationship name="options" toMany="YES" deletionRule="Cascade" destinationEntity="PollOption" inverseName="poll" inverseEntity="PollOption"/>
<relationship name="status" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="poll" inverseEntity="Status"/>
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePolls" inverseEntity="MastodonUser"/>
</entity>
@ -218,14 +218,15 @@
<relationship name="author" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="statuses" inverseEntity="MastodonUser"/>
<relationship name="bookmarkedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="bookmarked" inverseEntity="MastodonUser"/>
<relationship name="favouritedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="favourite" inverseEntity="MastodonUser"/>
<relationship name="homeTimelineIndexes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="HomeTimelineIndex" inverseName="status" inverseEntity="HomeTimelineIndex"/>
<relationship name="homeTimelineIndexes" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="HomeTimelineIndex" inverseName="status" inverseEntity="HomeTimelineIndex"/>
<relationship name="inNotifications" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="MastodonNotification" inverseName="status" inverseEntity="MastodonNotification"/>
<relationship name="mediaAttachments" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Attachment" inverseName="status" inverseEntity="Attachment"/>
<relationship name="mentions" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Mention" inverseName="status" inverseEntity="Mention"/>
<relationship name="mutedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muted" inverseEntity="MastodonUser"/>
<relationship name="pinnedBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="pinnedStatus" inverseEntity="MastodonUser"/>
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Poll" inverseName="status" inverseEntity="Poll"/>
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="reblogFrom" inverseEntity="Status"/>
<relationship name="reblogFrom" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="reblog" inverseEntity="Status"/>
<relationship name="reblogFrom" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Status" inverseName="reblog" inverseEntity="Status"/>
<relationship name="rebloggedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="reblogged" inverseEntity="MastodonUser"/>
<relationship name="replyFrom" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="replyTo" inverseEntity="Status"/>
<relationship name="replyTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="replyFrom" inverseEntity="Status"/>
@ -279,7 +280,7 @@
<element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
<element name="SearchHistory" positionX="0" positionY="0" width="128" height="104"/>
<element name="Setting" positionX="72" positionY="162" width="128" height="119"/>
<element name="Status" positionX="0" positionY="0" width="128" height="584"/>
<element name="Status" positionX="0" positionY="0" width="128" height="599"/>
<element name="Subscription" positionX="81" positionY="171" width="128" height="179"/>
<element name="SubscriptionAlerts" positionX="72" positionY="162" width="128" height="14"/>
<element name="Tag" positionX="0" positionY="0" width="128" height="134"/>

View File

@ -61,6 +61,8 @@ public final class Status: NSManagedObject {
@NSManaged public private(set) var mediaAttachments: Set<Attachment>?
@NSManaged public private(set) var replyFrom: Set<Status>?
@NSManaged public private(set) var inNotifications: Set<MastodonNotification>?
@NSManaged public private(set) var updatedAt: Date
@NSManaged public private(set) var deletedAt: Date?
@NSManaged public private(set) var revealedAt: Date?

View File

@ -29,12 +29,16 @@
"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": {
"title": "Save Photo Failure",
"message": "Please enable photo libaray access permission to save photo."
},
"delete_post": {
"title": "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",

View File

@ -936,7 +936,6 @@
DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.swift"; sourceTree = "<group>"; };
DB9A485B2603010E008B817C /* PHPickerResultLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPickerResultLoader.swift; sourceTree = "<group>"; };
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentContainerView+EmptyStateView.swift"; sourceTree = "<group>"; };
DB9A488326034BD7008B817C /* APIService+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Status.swift"; sourceTree = "<group>"; };
DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+PublishState.swift"; sourceTree = "<group>"; };
DB9A488F26035963008B817C /* APIService+Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Media.swift"; sourceTree = "<group>"; };
DB9A48952603685D008B817C /* MastodonAttachmentService+UploadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonAttachmentService+UploadState.swift"; sourceTree = "<group>"; };
@ -1738,7 +1737,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 */,

View File

@ -69,7 +69,7 @@
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
"state": {
"branch": null,
"revision": "bbc4bc4def7eb05a7ba8e1219f80ee9be327334e",
"revision": "15d199e84677303a7004ed2c5ecaa1a90f3863f8",
"version": "6.2.1"
}
},

View File

@ -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)
@ -787,7 +802,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)

View File

@ -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,

View File

@ -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 {
@ -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 title = L10n.tr("Localizable", "Common.Alerts.DeletePost.Title")
}
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

View File

@ -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)
@ -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: L10n.Common.Alerts.DeletePost.title, message: nil, 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)
}

View File

@ -1,7 +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.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.
@ -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";

View File

@ -88,4 +88,50 @@ extension APIService {
.eraseToAnyPublisher()
}
func deleteStatus(
domain: String,
statusID: Mastodon.Entity.Status.ID,
authorizationBox: AuthenticationService.MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, 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<Mastodon.Response.Content<Mastodon.Entity.Status>, 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 {
self.backgroundManagedObjectContext.delete(status)
}
}
.setFailureType(to: Error.self)
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Status> in
switch result {
case .success:
return response
case .failure(let error):
throw error
}
}
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}

View File

@ -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<Mastodon.Response.Content<Mastodon.Entity.Status>, 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<Mastodon.Response.Content<Mastodon.Entity.Status>, 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 {

View File

@ -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?