Merge remote-tracking branch 'upstream/develop' into account-switcher-a11y

This commit is contained in:
Jed Fox 2022-11-08 13:33:08 -05:00
commit 0310508a53
No known key found for this signature in database
GPG Key ID: 0B61D18EA54B47E1
17 changed files with 488 additions and 56 deletions

View File

@ -100,6 +100,7 @@ GEM
PLATFORMS
arm64-darwin-21
x86_64-darwin-21
DEPENDENCIES
arkana

View File

@ -180,7 +180,9 @@
"unmute": "Unmute",
"unmute_user": "Unmute %s",
"muted": "Muted",
"edit_info": "Edit Info"
"edit_info": "Edit Info",
"show_reblogs": "Show Reblogs",
"hide_reblogs": "Hide Reblogs"
},
"timeline": {
"filtered": "Filtered",
@ -455,6 +457,14 @@
"confirm_unblock_user": {
"title": "Unblock Account",
"message": "Confirm to unblock %s"
},
"confirm_show_reblogs": {
"title": "Show Reblogs",
"message": "Confirm to show reblogs"
},
"confirm_hide_reblogs": {
"title": "Hide Reblogs",
"message": "Confirm to hide reblogs"
}
},
"accessibility": {

View File

@ -189,15 +189,6 @@
"version" : "0.1.4"
}
},
{
"identity" : "swiftyjson",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftyJSON/SwiftyJSON.git",
"state" : {
"revision" : "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07",
"version" : "5.0.1"
}
},
{
"identity" : "tabbarpager",
"kind" : "remoteSourceControl",

View File

@ -132,3 +132,14 @@ extension DataSourceFacade {
}
} // end func
}
extension DataSourceFacade {
static func responseToShowHideReblogAction(
dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>
) async throws {
_ = try await dependency.context.apiService.toggleShowReblogs(
for: user,
authenticationBox: dependency.authContext.mastodonAuthenticationBox)
}
}

View File

@ -205,6 +205,45 @@ extension DataSourceFacade {
menuContext: MenuContext
) async throws {
switch action {
case .hideReblogs(let actionContext):
let title = actionContext.showReblogs ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.title : L10n.Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.title
let message = actionContext.showReblogs ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.message : L10n.Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.message
let alertController = UIAlertController(
title: title,
message: message,
preferredStyle: .alert
)
let actionTitle = actionContext.showReblogs ? L10n.Common.Controls.Friendship.hideReblogs : L10n.Common.Controls.Friendship.showReblogs
let showHideReblogsAction = UIAlertAction(
title: actionTitle,
style: .destructive
) { [weak dependency] _ in
guard let dependency else { return }
Task {
let managedObjectContext = dependency.context.managedObjectContext
let _user: ManagedObjectRecord<MastodonUser>? = try? await managedObjectContext.perform {
guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil }
return ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
}
guard let user = _user else { return }
try await DataSourceFacade.responseToShowHideReblogAction(
dependency: dependency,
user: user
)
}
}
alertController.addAction(showHideReblogsAction)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
alertController.addAction(cancelAction)
dependency.present(alertController, animated: true)
case .muteUser(let actionContext):
let alertController = UIAlertController(
title: actionContext.isMuting ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title : L10n.Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.title,
@ -230,9 +269,9 @@ extension DataSourceFacade {
} // end Task
}
alertController.addAction(confirmAction)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
alertController.addAction(cancelAction)
dependency.present(alertController, animated: true, completion: nil)
dependency.present(alertController, animated: true)
case .blockUser(let actionContext):
let alertController = UIAlertController(
title: actionContext.isBlocking ? L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title : L10n.Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.title,
@ -258,9 +297,9 @@ extension DataSourceFacade {
} // end Task
}
alertController.addAction(confirmAction)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
alertController.addAction(cancelAction)
dependency.present(alertController, animated: true, completion: nil)
dependency.present(alertController, animated: true)
case .reportUser:
Task {
guard let user = menuContext.author else { return }
@ -349,9 +388,9 @@ extension DataSourceFacade {
} // end Task
}
alertController.addAction(confirmAction)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
alertController.addAction(cancelAction)
dependency.present(alertController, animated: true, completion: nil)
dependency.present(alertController, animated: true)
}
} // end func
@ -371,3 +410,4 @@ extension DataSourceFacade {
}
}

View File

@ -376,13 +376,22 @@ extension ProfileViewController {
}
let name = user.displayNameWithFallback
let _ = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
let menu = MastodonMenu.setupMenu(
actions: [
var menuActions: [MastodonMenu.Action] = [
.muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)),
.blockUser(.init(name: name, isBlocking: self.viewModel.relationshipViewModel.isBlocking)),
.reportUser(.init(name: name)),
.shareUser(.init(name: name)),
],
]
if let me = self.viewModel?.me, me.following.contains(user) {
let showReblogs = me.showingReblogsBy.contains(user)
let context = MastodonMenu.HideReblogsActionContext(showReblogs: showReblogs)
menuActions.insert(.hideReblogs(context), at: 1)
}
let menu = MastodonMenu.setupMenu(
actions: menuActions,
delegate: self
)
return menu
@ -397,8 +406,10 @@ extension ProfileViewController {
}
} receiveValue: { [weak self] menu in
guard let self = self else { return }
OperationQueue.main.addOperation {
self.moreMenuBarButtonItem.menu = menu
}
}
.store(in: &disposeBag)
}
@ -740,7 +751,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
let alertController = UIAlertController(for: error, title: L10n.Common.Alerts.EditProfileFailure.title, preferredStyle: .alert)
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
alertController.addAction(okAction)
self.coordinator.present(
_ = self.coordinator.present(
scene: .alertController(alertController: alertController),
from: nil,
transition: .alertController(animated: true, completion: nil)
@ -763,11 +774,11 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
break
case .follow, .request, .pending, .following:
guard let user = viewModel.user else { return }
let reocrd = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
let record = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
Task {
try await DataSourceFacade.responseToUserFollowAction(
dependency: self,
user: reocrd
user: record
)
}
case .muting:
@ -816,10 +827,8 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
case .blocked:
case .blocked, .showReblogs, .isMyself,.followingBy, .blockingBy, .suspended, .edit, .editing, .updating:
break
default:
assertionFailure()
}
}

View File

@ -94,6 +94,7 @@ extension SidebarViewModel {
cell.setNeedsUpdateConfiguration()
cell.isAccessibilityElement = true
cell.accessibilityLabel = item.title
cell.accessibilityTraits.insert(.button)
self.$currentTab
.receive(on: DispatchQueue.main)
@ -156,6 +157,7 @@ extension SidebarViewModel {
cell.setNeedsUpdateConfiguration()
cell.isAccessibilityElement = true
cell.accessibilityLabel = item.title
cell.accessibilityTraits.insert(.button)
}
// header

View File

@ -40,7 +40,6 @@ let package = Package(
.package(url: "https://github.com/MainasuK/CommonOSLog", from: "0.1.1"),
.package(url: "https://github.com/MainasuK/FPSIndicator.git", from: "1.0.0"),
.package(url: "https://github.com/slackhq/PanModal.git", from: "1.2.7"),
.package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"),
.package(url: "https://github.com/TimOliver/TOCropViewController.git", from: "2.6.1"),
.package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "2.2.5"),
.package(url: "https://github.com/TwidereProject/TabBarPager.git", from: "0.1.0"),
@ -103,7 +102,6 @@ let package = Package(
.target(
name: "MastodonSDK",
dependencies: [
.product(name: "SwiftyJSON", package: "SwiftyJSON"),
.product(name: "NIOHTTP1", package: "swift-nio"),
]
),

View File

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>CoreData 3.xcdatamodel</string>
<string>CoreData 4.xcdatamodel</string>
</dict>
</plist>

View File

@ -0,0 +1,254 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21279" systemVersion="21G115" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Application" representedClassName="CoreDataStack.Application" syncable="YES">
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="vapidKey" optional="YES" attributeType="String"/>
<attribute name="website" optional="YES" attributeType="String"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="application" inverseEntity="Status"/>
</entity>
<entity name="DomainBlock" representedClassName="CoreDataStack.DomainBlock" syncable="YES">
<attribute name="blockedDomain" attributeType="String"/>
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="userID" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="userID"/>
<constraint value="domain"/>
<constraint value="blockedDomain"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Emoji" representedClassName="CoreDataStack.Emoji" syncable="YES">
<attribute name="category" optional="YES" attributeType="String"/>
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="shortcode" attributeType="String"/>
<attribute name="staticURL" attributeType="String"/>
<attribute name="url" attributeType="String"/>
<attribute name="visibleInPicker" attributeType="Boolean" usesScalarValueType="YES"/>
</entity>
<entity name="Feed" representedClassName="CoreDataStack.Feed" syncable="YES">
<attribute name="acctRaw" optional="YES" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="hasMore" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isLoadingMore" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="kindRaw" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="notification" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Notification" inverseName="feeds" inverseEntity="Notification"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="feeds" inverseEntity="Status"/>
</entity>
<entity name="Instance" representedClassName="CoreDataStack.Instance" syncable="YES">
<attribute name="configurationRaw" optional="YES" attributeType="Binary"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/>
</entity>
<entity name="MastodonAuthentication" representedClassName="CoreDataStack.MastodonAuthentication" syncable="YES">
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="appAccessToken" attributeType="String"/>
<attribute name="clientID" attributeType="String"/>
<attribute name="clientSecret" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userAccessToken" attributeType="String"/>
<attribute name="userID" attributeType="String"/>
<attribute name="username" attributeType="String"/>
<relationship name="instance" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Instance" inverseName="authentications" inverseEntity="Instance"/>
<relationship name="user" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mastodonAuthentication" inverseEntity="MastodonUser"/>
</entity>
<entity name="MastodonUser" representedClassName="CoreDataStack.MastodonUser" syncable="YES">
<attribute name="acct" attributeType="String"/>
<attribute name="avatar" attributeType="String"/>
<attribute name="avatarStatic" optional="YES" attributeType="String"/>
<attribute name="bot" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="displayName" attributeType="String"/>
<attribute name="domain" attributeType="String"/>
<attribute name="emojis" optional="YES" attributeType="Binary"/>
<attribute name="fields" optional="YES" attributeType="Binary"/>
<attribute name="followersCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="followingCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="header" attributeType="String"/>
<attribute name="headerStatic" optional="YES" attributeType="String"/>
<attribute name="id" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="note" optional="YES" attributeType="String"/>
<attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="suspended" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="url" optional="YES" attributeType="String"/>
<attribute name="username" attributeType="String"/>
<relationship name="blocking" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="blockingBy" inverseEntity="MastodonUser"/>
<relationship name="blockingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="blocking" inverseEntity="MastodonUser"/>
<relationship name="bookmarked" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="bookmarkedBy" inverseEntity="Status"/>
<relationship name="domainBlocking" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="domainBlockingBy" inverseEntity="MastodonUser"/>
<relationship name="domainBlockingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="domainBlocking" inverseEntity="MastodonUser"/>
<relationship name="endorsed" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="endorsedBy" inverseEntity="MastodonUser"/>
<relationship name="endorsedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="endorsed" inverseEntity="MastodonUser"/>
<relationship name="favourite" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="favouritedBy" inverseEntity="Status"/>
<relationship name="following" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followingBy" inverseEntity="MastodonUser"/>
<relationship name="followingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="following" inverseEntity="MastodonUser"/>
<relationship name="followRequested" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followRequestedBy" inverseEntity="MastodonUser"/>
<relationship name="followRequestedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followRequested" inverseEntity="MastodonUser"/>
<relationship name="mastodonAuthentication" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="user" inverseEntity="MastodonAuthentication"/>
<relationship name="muted" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="mutedBy" inverseEntity="Status"/>
<relationship name="muting" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mutingBy" inverseEntity="MastodonUser"/>
<relationship name="mutingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muting" inverseEntity="MastodonUser"/>
<relationship name="notifications" toMany="YES" deletionRule="Nullify" destinationEntity="Notification" inverseName="account" inverseEntity="Notification"/>
<relationship name="pinnedStatus" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="pinnedBy" inverseEntity="Status"/>
<relationship name="privateNotes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="to" inverseEntity="PrivateNote"/>
<relationship name="privateNotesTo" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="from" inverseEntity="PrivateNote"/>
<relationship name="reblogged" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="rebloggedBy" inverseEntity="Status"/>
<relationship name="searchHistories" toMany="YES" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="account" inverseEntity="SearchHistory"/>
<relationship name="showingReblogs" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="showingReblogsBy" inverseEntity="MastodonUser"/>
<relationship name="showingReblogsBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="showingReblogs" inverseEntity="MastodonUser"/>
<relationship name="statuses" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="author" inverseEntity="Status"/>
<relationship name="votePollOptions" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="votedBy" inverseEntity="PollOption"/>
<relationship name="votePolls" toMany="YES" deletionRule="Nullify" destinationEntity="Poll" inverseName="votedBy" inverseEntity="Poll"/>
</entity>
<entity name="Notification" representedClassName="CoreDataStack.Notification" syncable="YES">
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="followRequestState" optional="YES" attributeType="Binary"/>
<attribute name="id" attributeType="String"/>
<attribute name="transientFollowRequestState" optional="YES" transient="YES" attributeType="Binary"/>
<attribute name="typeRaw" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userID" attributeType="String"/>
<relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="notifications" inverseEntity="MastodonUser"/>
<relationship name="feeds" toMany="YES" deletionRule="Cascade" destinationEntity="Feed" inverseName="notification" inverseEntity="Feed"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="notifications" inverseEntity="Status"/>
</entity>
<entity name="Poll" representedClassName="CoreDataStack.Poll" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String" defaultValueString=""/>
<attribute name="expired" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="expiresAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" attributeType="String"/>
<attribute name="isVoting" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="multiple" attributeType="Boolean" usesScalarValueType="YES"/>
<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="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>
<entity name="PollOption" representedClassName="CoreDataStack.PollOption" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="index" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isSelected" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="title" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="votesCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="poll" maxCount="1" deletionRule="Nullify" destinationEntity="Poll" inverseName="options" inverseEntity="Poll"/>
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePollOptions" inverseEntity="MastodonUser"/>
</entity>
<entity name="PrivateNote" representedClassName="CoreDataStack.PrivateNote" syncable="YES">
<attribute name="note" optional="YES" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="from" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotesTo" inverseEntity="MastodonUser"/>
<relationship name="to" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotes" inverseEntity="MastodonUser"/>
</entity>
<entity name="SearchHistory" representedClassName="CoreDataStack.SearchHistory" syncable="YES">
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String" defaultValueString=""/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userID" attributeType="String" defaultValueString=""/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="searchHistories" inverseEntity="MastodonUser"/>
<relationship name="hashtag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag" inverseName="searchHistories" inverseEntity="Tag"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="searchHistories" inverseEntity="Status"/>
</entity>
<entity name="Setting" representedClassName="CoreDataStack.Setting" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="preferredStaticAvatar" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="preferredStaticEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="preferredTrueBlackDarkMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="preferredUsingDefaultBrowser" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userID" attributeType="String"/>
<relationship name="subscriptions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Subscription" inverseName="setting" inverseEntity="Subscription"/>
</entity>
<entity name="Status" representedClassName="CoreDataStack.Status" syncable="YES">
<attribute name="attachments" optional="YES" attributeType="Binary"/>
<attribute name="content" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="emojis" optional="YES" attributeType="Binary"/>
<attribute name="favouritesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="inReplyToAccountID" optional="YES" attributeType="String"/>
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
<attribute name="isSensitiveToggled" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="language" optional="YES" attributeType="String"/>
<attribute name="mentions" optional="YES" attributeType="Binary"/>
<attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="repliesCount" optional="YES" attributeType="Integer 64" usesScalarValueType="NO"/>
<attribute name="revealedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="sensitive" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="spoilerText" optional="YES" attributeType="String"/>
<attribute name="text" optional="YES" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="uri" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="String"/>
<attribute name="visibilityRaw" optional="YES" attributeType="String" elementID="visibility"/>
<relationship name="application" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Application" inverseName="status" inverseEntity="Application"/>
<relationship name="author" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="statuses" inverseEntity="MastodonUser"/>
<relationship name="bookmarkedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="bookmarked" inverseEntity="MastodonUser"/>
<relationship name="favouritedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="favourite" inverseEntity="MastodonUser"/>
<relationship name="feeds" toMany="YES" deletionRule="Cascade" destinationEntity="Feed" inverseName="status" inverseEntity="Feed"/>
<relationship name="mutedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muted" inverseEntity="MastodonUser"/>
<relationship name="notifications" toMany="YES" deletionRule="Cascade" destinationEntity="Notification" inverseName="status" inverseEntity="Notification"/>
<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" toMany="YES" deletionRule="Cascade" destinationEntity="Status" inverseName="reblog" inverseEntity="Status"/>
<relationship name="rebloggedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="reblogged" inverseEntity="MastodonUser"/>
<relationship name="replyFrom" 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"/>
<relationship name="searchHistories" toMany="YES" deletionRule="Cascade" destinationEntity="SearchHistory" inverseName="status" inverseEntity="SearchHistory"/>
</entity>
<entity name="Subscription" representedClassName="CoreDataStack.Subscription" syncable="YES">
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="endpoint" optional="YES" attributeType="String"/>
<attribute name="id" optional="YES" attributeType="String"/>
<attribute name="policyRaw" attributeType="String"/>
<attribute name="serverKey" optional="YES" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userToken" optional="YES" attributeType="String"/>
<relationship name="alert" maxCount="1" deletionRule="Cascade" destinationEntity="SubscriptionAlerts" inverseName="subscription" inverseEntity="SubscriptionAlerts"/>
<relationship name="setting" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Setting" inverseName="subscriptions" inverseEntity="Setting"/>
</entity>
<entity name="SubscriptionAlerts" representedClassName="CoreDataStack.SubscriptionAlerts" syncable="YES">
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="favouriteRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="followRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="followRequestRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="mentionRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="pollRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="reblogRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="subscription" maxCount="1" deletionRule="Nullify" destinationEntity="Subscription" inverseName="alert" inverseEntity="Subscription"/>
</entity>
<entity name="Tag" representedClassName="CoreDataStack.Tag" syncable="YES">
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String" defaultValueString=""/>
<attribute name="histories" optional="YES" attributeType="Binary"/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="url" attributeType="String"/>
<relationship name="searchHistories" toMany="YES" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="hashtag" inverseEntity="SearchHistory"/>
</entity>
</model>

View File

@ -89,7 +89,8 @@ final public class MastodonUser: NSManagedObject {
@NSManaged public private(set) var endorsedBy: Set<MastodonUser>
@NSManaged public private(set) var domainBlocking: Set<MastodonUser>
@NSManaged public private(set) var domainBlockingBy: Set<MastodonUser>
@NSManaged public private(set) var showingReblogs: Set<MastodonUser>
@NSManaged public private(set) var showingReblogsBy: Set<MastodonUser>
}
extension MastodonUser {
@ -521,6 +522,7 @@ extension MastodonUser: AutoUpdatableObject {
}
}
}
public func update(isDomainBlocking: Bool, by mastodonUser: MastodonUser) {
if isDomainBlocking {
if !self.domainBlockingBy.contains(mastodonUser) {
@ -533,4 +535,15 @@ extension MastodonUser: AutoUpdatableObject {
}
}
public func update(isShowingReblogs: Bool, by mastodonUser: MastodonUser) {
if isShowingReblogs {
if !self.showingReblogsBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.showingReblogsBy)).add(mastodonUser)
}
} else {
if self.showingReblogsBy.contains(mastodonUser) {
self.mutableSetValue(forKey: #keyPath(MastodonUser.showingReblogsBy)).remove(mastodonUser)
}
}
}
}

View File

@ -157,5 +157,6 @@ extension Persistence.MastodonUser {
user.update(isBlocking: relationship.blocking, by: me)
relationship.domainBlocking.flatMap { user.update(isDomainBlocking: $0, by: me) }
relationship.blockedBy.flatMap { me.update(isBlocking: $0, by: user) }
relationship.showingReblogs.flatMap { me.update(isShowingReblogs: $0, by: user) }
}
}

View File

@ -122,4 +122,55 @@ extension APIService {
return response
}
public func toggleShowReblogs(
for user: ManagedObjectRecord<MastodonUser>,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
let managedObjectContext = backgroundManagedObjectContext
guard let user = user.object(in: managedObjectContext),
let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext)
else { throw APIError.implicit(.badRequest) }
let me = authentication.user
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
let oldShowReblogs = me.showingReblogsBy.contains(user)
let newShowReblogs = (oldShowReblogs == false)
do {
let response = try await Mastodon.API.Account.follow(
session: session,
domain: authenticationBox.domain,
accountID: user.id,
followQueryType: .follow(query: .init(reblogs: newShowReblogs)),
authorization: authenticationBox.userAuthorization
).singleOutput()
result = .success(response)
} catch {
result = .failure(error)
}
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
switch result {
case .success(let response):
Persistence.MastodonUser.update(
mastodonUser: user,
context: Persistence.MastodonUser.RelationshipContext(
entity: response.value,
me: me,
networkDate: response.networkDate
)
)
case .failure:
// rollback
user.update(isShowingReblogs: oldShowReblogs, by: me)
}
}
return try result.get()
}
}

View File

@ -198,6 +198,8 @@ public enum L10n {
public static let follow = L10n.tr("Localizable", "Common.Controls.Friendship.Follow")
/// Following
public static let following = L10n.tr("Localizable", "Common.Controls.Friendship.Following")
/// Hide Reblogs
public static let hideReblogs = L10n.tr("Localizable", "Common.Controls.Friendship.HideReblogs")
/// Mute
public static let mute = L10n.tr("Localizable", "Common.Controls.Friendship.Mute")
/// Muted
@ -210,6 +212,8 @@ public enum L10n {
public static let pending = L10n.tr("Localizable", "Common.Controls.Friendship.Pending")
/// Request
public static let request = L10n.tr("Localizable", "Common.Controls.Friendship.Request")
/// Show Reblogs
public static let showReblogs = L10n.tr("Localizable", "Common.Controls.Friendship.ShowReblogs")
/// Unblock
public static let unblock = L10n.tr("Localizable", "Common.Controls.Friendship.Unblock")
/// Unblock %@
@ -709,6 +713,12 @@ public enum L10n {
/// Block Account
public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title")
}
public enum ConfirmHideReblogs {
/// Confirm to hide reblogs
public static let message = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message")
/// Hide reblogs
public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title")
}
public enum ConfirmMuteUser {
/// Confirm to mute %@
public static func message(_ p1: Any) -> String {
@ -717,6 +727,12 @@ public enum L10n {
/// Mute Account
public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title")
}
public enum ConfirmShowReblogs {
/// Confirm to show reblogs
public static let message = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message")
/// Show Reblogs
public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title")
}
public enum ConfirmUnblockUser {
/// Confirm to unblock %@
public static func message(_ p1: Any) -> String {

View File

@ -77,6 +77,9 @@ Please check your internet connection.";
"Common.Controls.Friendship.UnblockUser" = "Unblock %@";
"Common.Controls.Friendship.Unmute" = "Unmute";
"Common.Controls.Friendship.UnmuteUser" = "Unmute %@";
"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs";
"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs";
"Common.Controls.Keyboard.Common.ComposeNewPost" = "Compose New Post";
"Common.Controls.Keyboard.Common.OpenSettings" = "Open Settings";
"Common.Controls.Keyboard.Common.ShowFavorites" = "Show Favorites";
@ -262,6 +265,10 @@ uploaded to Mastodon.";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Unblock Account";
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirm to unmute %@";
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title" = "Unmute Account";
"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs";
"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs";
"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs";
"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide reblogs";
"Scene.Profile.SegmentedControl.About" = "About";
"Scene.Profile.SegmentedControl.Media" = "Media";
"Scene.Profile.SegmentedControl.Posts" = "Posts";

View File

@ -45,11 +45,23 @@ extension MastodonMenu {
case reportUser(ReportUserActionContext)
case shareUser(ShareUserActionContext)
case bookmarkStatus(BookmarkStatusActionContext)
case hideReblogs(HideReblogsActionContext)
case shareStatus
case deleteStatus
func build(delegate: MastodonMenuDelegate) -> BuiltAction {
switch self {
case .hideReblogs(let context):
let title = context.showReblogs ? L10n.Common.Controls.Friendship.hideReblogs : L10n.Common.Controls.Friendship.showReblogs
let reblogAction = BuiltAction(
title: title,
image: UIImage(systemName: "arrow.2.squarepath")
) { [weak delegate] in
guard let delegate = delegate else { return }
delegate.menuAction(self)
}
return reblogAction
case .muteUser(let context):
let muteAction = BuiltAction(
title: context.isMuting ? L10n.Common.Controls.Friendship.unmuteUser(context.name) : L10n.Common.Controls.Friendship.muteUser(context.name),
@ -206,4 +218,11 @@ extension MastodonMenu {
}
}
public struct HideReblogsActionContext {
public let showReblogs: Bool
public init(showReblogs: Bool) {
self.showReblogs = showReblogs
}
}
}

View File

@ -12,6 +12,7 @@ import MastodonLocalization
import CoreDataStack
public enum RelationshipAction: Int, CaseIterable {
case showReblogs
case isMyself
case followingBy
case blockingBy
@ -57,7 +58,7 @@ public struct RelationshipActionOptionSet: OptionSet {
public static let edit = RelationshipAction.edit.option
public static let editing = RelationshipAction.editing.option
public static let updating = RelationshipAction.updating.option
public static let showReblogs = RelationshipAction.showReblogs.option
public static let editOptions: RelationshipActionOptionSet = [.edit, .editing, .updating]
public func highPriorityAction(except: RelationshipActionOptionSet) -> RelationshipAction? {
@ -90,9 +91,9 @@ public struct RelationshipActionOptionSet: OptionSet {
case .edit: return L10n.Common.Controls.Friendship.editInfo
case .editing: return L10n.Common.Controls.Actions.done
case .updating: return " "
case .showReblogs: return " "
}
}
}
public final class RelationshipViewModel {
@ -114,6 +115,7 @@ public final class RelationshipViewModel {
@Published public var isFollowing = false
@Published public var isFollowingBy = false
@Published public var isMuting = false
@Published public var showReblogs = false
@Published public var isBlocking = false
@Published public var isBlockingBy = false
@Published public var isSuspended = false
@ -184,6 +186,7 @@ extension RelationshipViewModel {
self.isBlockingBy = optionSet.contains(.blockingBy)
self.isBlocking = optionSet.contains(.blocking)
self.isSuspended = optionSet.contains(.suspended)
self.showReblogs = optionSet.contains(.showReblogs)
self.optionSet = optionSet
}
@ -196,6 +199,7 @@ extension RelationshipViewModel {
isBlockingBy = false
isBlocking = false
optionSet = nil
showReblogs = false
}
}
@ -214,6 +218,7 @@ extension RelationshipViewModel {
let isMuting = user.mutingBy.contains(me)
let isBlockingBy = me.blockingBy.contains(user)
let isBlocking = user.blockingBy.contains(me)
let isShowingReblogs = me.showingReblogsBy.contains(user)
var optionSet: RelationshipActionOptionSet = [.follow]
@ -253,6 +258,10 @@ extension RelationshipViewModel {
optionSet.insert(.suspended)
}
if isShowingReblogs {
optionSet.insert(.showReblogs)
}
return optionSet
}
}