Merge branch 'develop' into remove_status
This commit is contained in:
commit
4561dc5495
@ -222,10 +222,11 @@
|
|||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
"block": "Block",
|
"block": "Block",
|
||||||
"block_user": "Block %s",
|
"block_user": "Block %s",
|
||||||
"block_domain": "Block %s",
|
"block_domain": "Block domain %s",
|
||||||
"unblock": "Unblock",
|
"unblock": "Unblock",
|
||||||
"unblock_user": "Unblock %s",
|
"unblock_user": "Unblock %s",
|
||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
|
"domain_blocked": "Domain Blocked",
|
||||||
"mute": "Mute",
|
"mute": "Mute",
|
||||||
"mute_user": "Mute %s",
|
"mute_user": "Mute %s",
|
||||||
"unmute": "Unmute",
|
"unmute": "Unmute",
|
||||||
@ -603,6 +604,14 @@
|
|||||||
"confirm_hide_reblogs": {
|
"confirm_hide_reblogs": {
|
||||||
"title": "Hide Reblogs",
|
"title": "Hide Reblogs",
|
||||||
"message": "Confirm to hide reblogs"
|
"message": "Confirm to hide reblogs"
|
||||||
|
},
|
||||||
|
"confirm_block_domain": {
|
||||||
|
"title": "Block domain",
|
||||||
|
"message": "Confirm to block domain %s"
|
||||||
|
},
|
||||||
|
"confirm_unblock_domain": {
|
||||||
|
"title": "Unblock domain",
|
||||||
|
"message": "Confirm to unblock domain %s"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"accessibility": {
|
"accessibility": {
|
||||||
|
@ -2310,7 +2310,7 @@
|
|||||||
path = FamiliarFollowers;
|
path = FamiliarFollowers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
DB5B54A42833BD1D00DEF8B2 /* UserLIst */ = {
|
DB5B54A42833BD1D00DEF8B2 /* UserList */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB5B54A22833BD1A00DEF8B2 /* UserListViewModel.swift */,
|
DB5B54A22833BD1A00DEF8B2 /* UserListViewModel.swift */,
|
||||||
@ -2319,7 +2319,7 @@
|
|||||||
DB5B54A92833BFAC00DEF8B2 /* FavoritedBy */,
|
DB5B54A92833BFAC00DEF8B2 /* FavoritedBy */,
|
||||||
DB5B54AC2833C12D00DEF8B2 /* RebloggedBy */,
|
DB5B54AC2833C12D00DEF8B2 /* RebloggedBy */,
|
||||||
);
|
);
|
||||||
path = UserLIst;
|
path = UserList;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
DB5B54A92833BFAC00DEF8B2 /* FavoritedBy */ = {
|
DB5B54A92833BFAC00DEF8B2 /* FavoritedBy */ = {
|
||||||
@ -2778,7 +2778,7 @@
|
|||||||
DB6B74F0272FB55400C70B6E /* Follower */,
|
DB6B74F0272FB55400C70B6E /* Follower */,
|
||||||
DB5B7296273112B400081888 /* Following */,
|
DB5B7296273112B400081888 /* Following */,
|
||||||
DB5B549B2833A60600DEF8B2 /* FamiliarFollowers */,
|
DB5B549B2833A60600DEF8B2 /* FamiliarFollowers */,
|
||||||
DB5B54A42833BD1D00DEF8B2 /* UserLIst */,
|
DB5B54A42833BD1D00DEF8B2 /* UserList */,
|
||||||
DBFEEC97279BDC6A004F81DD /* About */,
|
DBFEEC97279BDC6A004F81DD /* About */,
|
||||||
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */,
|
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */,
|
||||||
DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */,
|
DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */,
|
||||||
@ -4663,7 +4663,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2023.17;
|
MARKETING_VERSION = 2024.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -4694,7 +4694,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2023.17;
|
MARKETING_VERSION = 2024.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -4884,7 +4884,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2023.17;
|
MARKETING_VERSION = 2024.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -5180,7 +5180,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2023.17;
|
MARKETING_VERSION = 2024.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
@ -11,7 +11,6 @@ import CoreDataStack
|
|||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
enum UserItem: Hashable {
|
enum UserItem: Hashable {
|
||||||
case user(record: ManagedObjectRecord<MastodonUser>)
|
|
||||||
case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
|
case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
|
||||||
case bottomLoader
|
case bottomLoader
|
||||||
case bottomHeader(text: String)
|
case bottomHeader(text: String)
|
||||||
|
@ -48,27 +48,6 @@ extension UserSection {
|
|||||||
delegate: userTableViewCellDelegate
|
delegate: userTableViewCellDelegate
|
||||||
)
|
)
|
||||||
|
|
||||||
return cell
|
|
||||||
|
|
||||||
case .user(let record):
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
|
|
||||||
context.managedObjectContext.performAndWait {
|
|
||||||
guard let user = record.object(in: context.managedObjectContext) else { return }
|
|
||||||
configure(
|
|
||||||
context: context,
|
|
||||||
authContext: authContext,
|
|
||||||
tableView: tableView,
|
|
||||||
cell: cell,
|
|
||||||
viewModel: UserTableViewCell.ViewModel(
|
|
||||||
user: user,
|
|
||||||
followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(),
|
|
||||||
blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(),
|
|
||||||
followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()
|
|
||||||
),
|
|
||||||
userTableViewCellDelegate: userTableViewCellDelegate
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cell
|
return cell
|
||||||
case .bottomLoader:
|
case .bottomLoader:
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||||
@ -82,23 +61,3 @@ extension UserSection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UserSection {
|
|
||||||
|
|
||||||
static func configure(
|
|
||||||
context: AppContext,
|
|
||||||
authContext: AuthContext,
|
|
||||||
tableView: UITableView,
|
|
||||||
cell: UserTableViewCell,
|
|
||||||
viewModel: UserTableViewCell.ViewModel,
|
|
||||||
userTableViewCellDelegate: UserTableViewCellDelegate?
|
|
||||||
) {
|
|
||||||
cell.configure(
|
|
||||||
me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext),
|
|
||||||
tableView: tableView,
|
|
||||||
viewModel: viewModel,
|
|
||||||
delegate: userTableViewCellDelegate
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -52,4 +52,17 @@ extension DataSourceFacade {
|
|||||||
)
|
)
|
||||||
dependency.context.authenticationService.fetchFollowingAndBlockedAsync()
|
dependency.context.authenticationService.fetchFollowingAndBlockedAsync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func responseToDomainBlockAction(
|
||||||
|
dependency: NeedsDependency & AuthContextProvider,
|
||||||
|
user: ManagedObjectRecord<MastodonUser>
|
||||||
|
) async throws {
|
||||||
|
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||||
|
await selectionFeedbackGenerator.selectionChanged()
|
||||||
|
|
||||||
|
let apiService = dependency.context.apiService
|
||||||
|
let authBox = dependency.authContext.mastodonAuthenticationBox
|
||||||
|
|
||||||
|
_ = try await apiService.toggleDomainBlock(user: user, authenticationBox: authBox)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -385,8 +385,48 @@ extension DataSourceFacade {
|
|||||||
|
|
||||||
try await DataSourceFacade.responseToUserFollowAction(dependency: dependency,
|
try await DataSourceFacade.responseToUserFollowAction(dependency: dependency,
|
||||||
user: author)
|
user: author)
|
||||||
|
case .blockDomain(let context):
|
||||||
|
let title: String
|
||||||
|
let message: String
|
||||||
|
let actionTitle: String
|
||||||
|
|
||||||
|
if context.isBlocking {
|
||||||
|
title = L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.title
|
||||||
|
message = L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.message(context.domain)
|
||||||
|
actionTitle = L10n.Common.Controls.Friendship.unblockDomain(context.domain)
|
||||||
|
} else {
|
||||||
|
title = L10n.Scene.Profile.RelationshipActionAlert.ConfirmBlockDomain.title
|
||||||
|
message = L10n.Common.Alerts.BlockDomain.title(context.domain)
|
||||||
|
actionTitle = L10n.Common.Alerts.BlockDomain.blockEntireDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
let alertController = UIAlertController(
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
|
||||||
|
let confirmAction = UIAlertAction(title: actionTitle, style: .destructive ) { [weak dependency] _ in
|
||||||
|
guard let dependency = 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.responseToDomainBlockAction(
|
||||||
|
dependency: dependency,
|
||||||
|
user: user
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alertController.addAction(confirmAction)
|
||||||
|
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
|
||||||
|
alertController.addAction(cancelAction)
|
||||||
|
dependency.present(alertController, animated: true)
|
||||||
}
|
}
|
||||||
} // end func
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DataSourceFacade {
|
extension DataSourceFacade {
|
||||||
|
@ -119,7 +119,7 @@ extension ProfileHeaderView.ViewModel {
|
|||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
// blur for blocking & blockingBy
|
// blur for blocking & blockingBy
|
||||||
$relationshipActionOptionSet
|
$relationshipActionOptionSet
|
||||||
.map { $0.contains(.blocking) || $0.contains(.blockingBy) }
|
.map { $0.contains(.blocking) || $0.contains(.blockingBy) || $0.contains(.domainBlocking) }
|
||||||
.sink { needsImageOverlayBlurred in
|
.sink { needsImageOverlayBlurred in
|
||||||
UIView.animate(withDuration: 0.33) {
|
UIView.animate(withDuration: 0.33) {
|
||||||
let bannerEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.bannerImageViewOverlayBlurEffect : nil
|
let bannerEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.bannerImageViewOverlayBlurEffect : nil
|
||||||
@ -185,7 +185,7 @@ extension ProfileHeaderView.ViewModel {
|
|||||||
$relationshipActionOptionSet
|
$relationshipActionOptionSet
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { optionSet in
|
.sink { optionSet in
|
||||||
let isBlocking = optionSet.contains(.blocking)
|
let isBlocking = optionSet.contains(.blocking) || optionSet.contains(.domainBlocking)
|
||||||
let isBlockedBy = optionSet.contains(.blockingBy)
|
let isBlockedBy = optionSet.contains(.blockingBy)
|
||||||
let isSuspended = optionSet.contains(.suspended)
|
let isSuspended = optionSet.contains(.suspended)
|
||||||
let isNeedsHidden = isBlocking || isBlockedBy || isSuspended
|
let isNeedsHidden = isBlocking || isBlockedBy || isSuspended
|
||||||
|
@ -326,6 +326,7 @@ extension ProfileViewController {
|
|||||||
viewModel.relationshipViewModel.$isBlocking.assign(to: \.isBlocking, on: userTimelineViewModel).store(in: &disposeBag)
|
viewModel.relationshipViewModel.$isBlocking.assign(to: \.isBlocking, on: userTimelineViewModel).store(in: &disposeBag)
|
||||||
viewModel.relationshipViewModel.$isBlockingBy.assign(to: \.isBlockedBy, on: userTimelineViewModel).store(in: &disposeBag)
|
viewModel.relationshipViewModel.$isBlockingBy.assign(to: \.isBlockedBy, on: userTimelineViewModel).store(in: &disposeBag)
|
||||||
viewModel.relationshipViewModel.$isSuspended.assign(to: \.isSuspended, on: userTimelineViewModel).store(in: &disposeBag)
|
viewModel.relationshipViewModel.$isSuspended.assign(to: \.isSuspended, on: userTimelineViewModel).store(in: &disposeBag)
|
||||||
|
viewModel.relationshipViewModel.$isDomainBlocking.assign(to: \.isDomainBlocking, on: userTimelineViewModel).store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
// about
|
// about
|
||||||
@ -396,16 +397,16 @@ extension ProfileViewController {
|
|||||||
viewModel.relationshipViewModel.$optionSet
|
viewModel.relationshipViewModel.$optionSet
|
||||||
)
|
)
|
||||||
.asyncMap { [weak self] user, relationshipSet -> UIMenu? in
|
.asyncMap { [weak self] user, relationshipSet -> UIMenu? in
|
||||||
guard let self = self else { return nil }
|
guard let self, let user else { return nil }
|
||||||
guard let user = user else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let name = user.displayNameWithFallback
|
let name = user.displayNameWithFallback
|
||||||
|
let domain = user.domainFromAcct
|
||||||
let _ = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
|
let _ = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
|
||||||
|
|
||||||
var menuActions: [MastodonMenu.Action] = [
|
var menuActions: [MastodonMenu.Action] = [
|
||||||
.muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)),
|
.muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)),
|
||||||
.blockUser(.init(name: name, isBlocking: self.viewModel.relationshipViewModel.isBlocking)),
|
.blockUser(.init(name: name, isBlocking: self.viewModel.relationshipViewModel.isBlocking)),
|
||||||
|
.blockDomain(.init(domain: domain, isBlocking: self.viewModel.relationshipViewModel.isDomainBlocking)),
|
||||||
.reportUser(.init(name: name)),
|
.reportUser(.init(name: name)),
|
||||||
.shareUser(.init(name: name)),
|
.shareUser(.init(name: name)),
|
||||||
]
|
]
|
||||||
@ -830,6 +831,27 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
|
|||||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
|
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
|
||||||
alertController.addAction(cancelAction)
|
alertController.addAction(cancelAction)
|
||||||
present(alertController, animated: true, completion: nil)
|
present(alertController, animated: true, completion: nil)
|
||||||
|
case .domainBlocking:
|
||||||
|
guard let user = viewModel.user else { return }
|
||||||
|
let domain = user.domainFromAcct
|
||||||
|
|
||||||
|
let alertController = UIAlertController(
|
||||||
|
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.title,
|
||||||
|
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.message(domain),
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
let record = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
|
||||||
|
let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
Task {
|
||||||
|
try await DataSourceFacade.responseToDomainBlockAction(dependency: self, user: record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alertController.addAction(unblockAction)
|
||||||
|
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
|
||||||
|
alertController.addAction(cancelAction)
|
||||||
|
present(alertController, animated: true, completion: nil)
|
||||||
|
|
||||||
case .blocking:
|
case .blocking:
|
||||||
guard let user = viewModel.user else { return }
|
guard let user = viewModel.user else { return }
|
||||||
let name = user.displayNameWithFallback
|
let name = user.displayNameWithFallback
|
||||||
|
@ -41,11 +41,12 @@ extension UserTimelineViewModel {
|
|||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
let needsTimelineHidden = Publishers.CombineLatest3(
|
let needsTimelineHidden = Publishers.CombineLatest4(
|
||||||
$isBlocking,
|
$isBlocking,
|
||||||
$isBlockedBy,
|
$isBlockedBy,
|
||||||
$isSuspended
|
$isSuspended,
|
||||||
).map { $0 || $1 || $2 }
|
$isDomainBlocking
|
||||||
|
).map { $0 || $1 || $2 || $3 }
|
||||||
|
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest(
|
||||||
dataController.$records,
|
dataController.$records,
|
||||||
|
@ -27,6 +27,7 @@ final class UserTimelineViewModel {
|
|||||||
@Published var queryFilter: QueryFilter
|
@Published var queryFilter: QueryFilter
|
||||||
|
|
||||||
@Published var isBlocking = false
|
@Published var isBlocking = false
|
||||||
|
@Published var isDomainBlocking = false
|
||||||
@Published var isBlockedBy = false
|
@Published var isBlockedBy = false
|
||||||
@Published var isSuspended = false
|
@Published var isSuspended = false
|
||||||
|
|
||||||
|
@ -21,10 +21,10 @@ extension FavoritedByViewController: DataSourceProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch item {
|
switch item {
|
||||||
case .user(let record):
|
case .account(let account, let relationship):
|
||||||
return .user(record: record)
|
return .account(account: account, relationship: relationship)
|
||||||
default:
|
case .bottomHeader(_), .bottomLoader:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -22,10 +22,10 @@ extension RebloggedByViewController: DataSourceProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch item {
|
switch item {
|
||||||
case .user(let record):
|
case .account(let account, let relationship):
|
||||||
return .user(record: record)
|
return .account(account: account, relationship: relationship)
|
||||||
default:
|
case .bottomHeader(_), .bottomLoader:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import UIKit
|
|||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
import Combine
|
import Combine
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
extension UserListViewModel {
|
extension UserListViewModel {
|
||||||
@MainActor
|
@MainActor
|
||||||
@ -33,17 +34,24 @@ extension UserListViewModel {
|
|||||||
// trigger initial loading
|
// trigger initial loading
|
||||||
stateMachine.enter(UserListViewModel.State.Reloading.self)
|
stateMachine.enter(UserListViewModel.State.Reloading.self)
|
||||||
|
|
||||||
userFetchedResultsController.$records
|
$accounts
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] records in
|
.sink { [weak self] accounts in
|
||||||
guard let self = self else { return }
|
guard let self else { return }
|
||||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<UserSection, UserItem>()
|
var snapshot = NSDiffableDataSourceSnapshot<UserSection, UserItem>()
|
||||||
snapshot.appendSections([.main])
|
snapshot.appendSections([.main])
|
||||||
let items = records.map { UserItem.user(record: $0) }
|
|
||||||
|
let accountsWithRelationship: [(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)] = accounts.compactMap { account in
|
||||||
|
guard let relationship = self.relationships.first(where: {$0.id == account.id }) else { return (account: account, relationship: nil)}
|
||||||
|
|
||||||
|
return (account: account, relationship: relationship)
|
||||||
|
}
|
||||||
|
|
||||||
|
let items = accountsWithRelationship.map { UserItem.account(account: $0.account, relationship: $0.relationship) }
|
||||||
snapshot.appendItems(items, toSection: .main)
|
snapshot.appendItems(items, toSection: .main)
|
||||||
|
|
||||||
if let currentState = self.stateMachine.currentState {
|
if let currentState = self.stateMachine.currentState {
|
||||||
switch currentState {
|
switch currentState {
|
||||||
case is State.Initial, is State.Idle, is State.Reloading, is State.Loading, is State.Fail:
|
case is State.Initial, is State.Idle, is State.Reloading, is State.Loading, is State.Fail:
|
@ -52,11 +52,12 @@ extension UserListViewModel.State {
|
|||||||
|
|
||||||
override func didEnter(from previousState: GKState?) {
|
override func didEnter(from previousState: GKState?) {
|
||||||
super.didEnter(from: previousState)
|
super.didEnter(from: previousState)
|
||||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
guard let viewModel, let stateMachine else { return }
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
viewModel.userFetchedResultsController.userIDs = []
|
viewModel.accounts = []
|
||||||
|
viewModel.relationships = []
|
||||||
|
|
||||||
stateMachine.enter(Loading.self)
|
stateMachine.enter(Loading.self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,40 +124,61 @@ extension UserListViewModel.State {
|
|||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
let response: Mastodon.Response.Content<[Mastodon.Entity.Account]>
|
let accountResponse: Mastodon.Response.Content<[Mastodon.Entity.Account]>
|
||||||
switch viewModel.kind {
|
switch viewModel.kind {
|
||||||
case .favoritedBy(let status):
|
case .favoritedBy(let status):
|
||||||
response = try await viewModel.context.apiService.favoritedBy(
|
accountResponse = try await viewModel.context.apiService.favoritedBy(
|
||||||
status: status,
|
status: status,
|
||||||
query: .init(maxID: maxID, limit: nil),
|
query: .init(maxID: maxID, limit: nil),
|
||||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||||
)
|
)
|
||||||
case .rebloggedBy(let status):
|
case .rebloggedBy(let status):
|
||||||
response = try await viewModel.context.apiService.rebloggedBy(
|
accountResponse = try await viewModel.context.apiService.rebloggedBy(
|
||||||
status: status,
|
status: status,
|
||||||
query: .init(maxID: maxID, limit: nil),
|
query: .init(maxID: maxID, limit: nil),
|
||||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if accountResponse.value.isEmpty {
|
||||||
|
await enter(state: NoMore.self)
|
||||||
|
|
||||||
|
viewModel.accounts = []
|
||||||
|
viewModel.relationships = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var hasNewAppend = false
|
var hasNewAppend = false
|
||||||
var userIDs = viewModel.userFetchedResultsController.userIDs
|
|
||||||
for user in response.value {
|
let newRelationships = try await viewModel.context.apiService.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authContext.mastodonAuthenticationBox)
|
||||||
guard !userIDs.contains(user.id) else { continue }
|
|
||||||
userIDs.append(user.id)
|
var accounts = viewModel.accounts
|
||||||
|
|
||||||
|
for user in accountResponse.value {
|
||||||
|
guard accounts.contains(user) == false else { continue }
|
||||||
|
accounts.append(user)
|
||||||
hasNewAppend = true
|
hasNewAppend = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let maxID = response.link?.maxID
|
var relationships = viewModel.relationships
|
||||||
|
|
||||||
|
for relationship in newRelationships.value {
|
||||||
|
guard relationships.contains(relationship) == false else { continue }
|
||||||
|
relationships.append(relationship)
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxID = accountResponse.link?.maxID
|
||||||
|
|
||||||
if hasNewAppend, maxID != nil {
|
if hasNewAppend, maxID != nil {
|
||||||
await enter(state: Idle.self)
|
await enter(state: Idle.self)
|
||||||
} else {
|
} else {
|
||||||
await enter(state: NoMore.self)
|
await enter(state: NoMore.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.accounts = accounts
|
||||||
|
viewModel.relationships = relationships
|
||||||
self.maxID = maxID
|
self.maxID = maxID
|
||||||
viewModel.userFetchedResultsController.userIDs = userIDs
|
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
await enter(state: Fail.self)
|
await enter(state: Fail.self)
|
||||||
}
|
}
|
||||||
@ -177,9 +199,9 @@ extension UserListViewModel.State {
|
|||||||
override func didEnter(from previousState: GKState?) {
|
override func didEnter(from previousState: GKState?) {
|
||||||
super.didEnter(from: previousState)
|
super.didEnter(from: previousState)
|
||||||
|
|
||||||
guard let viewModel = viewModel else { return }
|
guard let viewModel else { return }
|
||||||
// trigger reload
|
// trigger reload
|
||||||
viewModel.userFetchedResultsController.userIDs = viewModel.userFetchedResultsController.userIDs
|
viewModel.accounts = viewModel.accounts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -19,7 +19,8 @@ final class UserListViewModel {
|
|||||||
let context: AppContext
|
let context: AppContext
|
||||||
let authContext: AuthContext
|
let authContext: AuthContext
|
||||||
let kind: Kind
|
let kind: Kind
|
||||||
let userFetchedResultsController: UserFetchedResultsController
|
@Published var accounts: [Mastodon.Entity.Account]
|
||||||
|
@Published var relationships: [Mastodon.Entity.Relationship]
|
||||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||||
|
|
||||||
// output
|
// output
|
||||||
@ -44,12 +45,8 @@ final class UserListViewModel {
|
|||||||
self.context = context
|
self.context = context
|
||||||
self.authContext = authContext
|
self.authContext = authContext
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.userFetchedResultsController = UserFetchedResultsController(
|
self.accounts = []
|
||||||
managedObjectContext: context.managedObjectContext,
|
self.relationships = []
|
||||||
domain: authContext.mastodonAuthenticationBox.domain,
|
|
||||||
additionalPredicate: nil
|
|
||||||
)
|
|
||||||
// end init
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,111 +0,0 @@
|
|||||||
//
|
|
||||||
// UserFetchedResultsController.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by MainasuK Cirno on 2021-7-7.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Combine
|
|
||||||
import CoreData
|
|
||||||
import CoreDataStack
|
|
||||||
import MastodonSDK
|
|
||||||
|
|
||||||
public final class UserFetchedResultsController: NSObject {
|
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
let fetchedResultsController: NSFetchedResultsController<MastodonUser>
|
|
||||||
|
|
||||||
// input
|
|
||||||
@Published public var domain: String? = nil
|
|
||||||
@Published public var userIDs: [Mastodon.Entity.Account.ID] = []
|
|
||||||
@Published public var additionalPredicate: NSPredicate?
|
|
||||||
|
|
||||||
// output
|
|
||||||
let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
|
||||||
@Published public private(set) var records: [ManagedObjectRecord<MastodonUser>] = []
|
|
||||||
|
|
||||||
public init(
|
|
||||||
managedObjectContext: NSManagedObjectContext,
|
|
||||||
domain: String?,
|
|
||||||
additionalPredicate: NSPredicate?
|
|
||||||
) {
|
|
||||||
self.domain = domain ?? ""
|
|
||||||
self.fetchedResultsController = {
|
|
||||||
let fetchRequest = MastodonUser.sortedFetchRequest
|
|
||||||
fetchRequest.predicate = MastodonUser.predicate(domain: domain ?? "", ids: [])
|
|
||||||
fetchRequest.returnsObjectsAsFaults = false
|
|
||||||
fetchRequest.fetchBatchSize = 20
|
|
||||||
let controller = NSFetchedResultsController(
|
|
||||||
fetchRequest: fetchRequest,
|
|
||||||
managedObjectContext: managedObjectContext,
|
|
||||||
sectionNameKeyPath: nil,
|
|
||||||
cacheName: nil
|
|
||||||
)
|
|
||||||
|
|
||||||
return controller
|
|
||||||
}()
|
|
||||||
self.additionalPredicate = additionalPredicate
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
// debounce output to prevent UI update issues
|
|
||||||
_objectIDs
|
|
||||||
.throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true)
|
|
||||||
.map { objectIDs in objectIDs.map { ManagedObjectRecord(objectID: $0) } }
|
|
||||||
.assign(to: &$records)
|
|
||||||
|
|
||||||
fetchedResultsController.delegate = self
|
|
||||||
|
|
||||||
Publishers.CombineLatest3(
|
|
||||||
self.$domain.removeDuplicates(),
|
|
||||||
self.$userIDs.removeDuplicates(),
|
|
||||||
self.$additionalPredicate.removeDuplicates()
|
|
||||||
)
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] domain, ids, additionalPredicate in
|
|
||||||
guard let self = self else { return }
|
|
||||||
var predicates = [MastodonUser.predicate(domain: domain ?? "", ids: ids)]
|
|
||||||
if let additionalPredicate = additionalPredicate {
|
|
||||||
predicates.append(additionalPredicate)
|
|
||||||
}
|
|
||||||
self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
|
|
||||||
do {
|
|
||||||
try self.fetchedResultsController.performFetch()
|
|
||||||
} catch {
|
|
||||||
assertionFailure(error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension UserFetchedResultsController {
|
|
||||||
|
|
||||||
public func append(userIDs: [Mastodon.Entity.Account.ID]) {
|
|
||||||
var result = self.userIDs
|
|
||||||
for userID in userIDs where !result.contains(userID) {
|
|
||||||
result.append(userID)
|
|
||||||
}
|
|
||||||
self.userIDs = result
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - NSFetchedResultsControllerDelegate
|
|
||||||
extension UserFetchedResultsController: NSFetchedResultsControllerDelegate {
|
|
||||||
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
|
||||||
|
|
||||||
let indexes = userIDs
|
|
||||||
let objects = fetchedResultsController.fetchedObjects ?? []
|
|
||||||
|
|
||||||
let items: [NSManagedObjectID] = objects
|
|
||||||
.compactMap { object in
|
|
||||||
indexes.firstIndex(of: object.id).map { index in (index, object) }
|
|
||||||
}
|
|
||||||
.sorted { $0.0 < $1.0 }
|
|
||||||
.map { $0.1.objectID }
|
|
||||||
self._objectIDs.value = items
|
|
||||||
}
|
|
||||||
}
|
|
@ -66,7 +66,31 @@ extension APIService {
|
|||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func toggleDomainBlock(
|
||||||
|
user: ManagedObjectRecord<MastodonUser>,
|
||||||
|
authenticationBox: MastodonAuthenticationBox
|
||||||
|
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Empty> {
|
||||||
|
guard let originalRelationship = try await relationship(records: [user], authenticationBox: authenticationBox).value.first else {
|
||||||
|
throw APIError.implicit(.badRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: Mastodon.Response.Content<Mastodon.Entity.Empty>
|
||||||
|
let domainBlocking = originalRelationship.domainBlocking ?? false
|
||||||
|
|
||||||
|
let managedObjectContext = backgroundManagedObjectContext
|
||||||
|
|
||||||
|
guard let _user = user.object(in: managedObjectContext) else { throw APIError.implicit(.badRequest) }
|
||||||
|
|
||||||
|
if domainBlocking {
|
||||||
|
response = try await unblockDomain(user: _user, authorizationBox: authenticationBox).singleOutput()
|
||||||
|
} else {
|
||||||
|
response = try await blockDomain(user: _user, authorizationBox: authenticationBox).singleOutput()
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
func blockDomain(
|
func blockDomain(
|
||||||
user: MastodonUser,
|
user: MastodonUser,
|
||||||
authorizationBox: MastodonAuthenticationBox
|
authorizationBox: MastodonAuthenticationBox
|
||||||
|
@ -216,6 +216,8 @@ public enum L10n {
|
|||||||
public static func blockUser(_ p1: Any) -> String {
|
public static func blockUser(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "Common.Controls.Friendship.BlockUser", String(describing: p1), fallback: "Block %@")
|
return L10n.tr("Localizable", "Common.Controls.Friendship.BlockUser", String(describing: p1), fallback: "Block %@")
|
||||||
}
|
}
|
||||||
|
/// Domain Blocked
|
||||||
|
public static let domainBlocked = L10n.tr("Localizable", "Common.Controls.Friendship.DomainBlocked", fallback: "Domain Blocked")
|
||||||
/// Edit Info
|
/// Edit Info
|
||||||
public static let editInfo = L10n.tr("Localizable", "Common.Controls.Friendship.EditInfo", fallback: "Edit Info")
|
public static let editInfo = L10n.tr("Localizable", "Common.Controls.Friendship.EditInfo", fallback: "Edit Info")
|
||||||
/// Follow
|
/// Follow
|
||||||
@ -241,6 +243,10 @@ public enum L10n {
|
|||||||
/// Unblock
|
/// Unblock
|
||||||
public static let unblock = L10n.tr("Localizable", "Common.Controls.Friendship.Unblock", fallback: "Unblock")
|
public static let unblock = L10n.tr("Localizable", "Common.Controls.Friendship.Unblock", fallback: "Unblock")
|
||||||
/// Unblock %@
|
/// Unblock %@
|
||||||
|
public static func unblockDomain(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "Common.Controls.Friendship.UnblockDomain", String(describing: p1), fallback: "Unblock %@")
|
||||||
|
}
|
||||||
|
/// Unblock %@
|
||||||
public static func unblockUser(_ p1: Any) -> String {
|
public static func unblockUser(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "Common.Controls.Friendship.UnblockUser", String(describing: p1), fallback: "Unblock %@")
|
return L10n.tr("Localizable", "Common.Controls.Friendship.UnblockUser", String(describing: p1), fallback: "Unblock %@")
|
||||||
}
|
}
|
||||||
@ -953,6 +959,10 @@ public enum L10n {
|
|||||||
public static let followsYou = L10n.tr("Localizable", "Scene.Profile.Header.FollowsYou", fallback: "Follows You")
|
public static let followsYou = L10n.tr("Localizable", "Scene.Profile.Header.FollowsYou", fallback: "Follows You")
|
||||||
}
|
}
|
||||||
public enum RelationshipActionAlert {
|
public enum RelationshipActionAlert {
|
||||||
|
public enum ConfirmBlockDomain {
|
||||||
|
/// Block Domain
|
||||||
|
public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmBlockDomain.Title", fallback: "Block Domain")
|
||||||
|
}
|
||||||
public enum ConfirmBlockUser {
|
public enum ConfirmBlockUser {
|
||||||
/// Confirm to block %@
|
/// Confirm to block %@
|
||||||
public static func message(_ p1: Any) -> String {
|
public static func message(_ p1: Any) -> String {
|
||||||
@ -981,6 +991,14 @@ public enum L10n {
|
|||||||
/// Show Reblogs
|
/// Show Reblogs
|
||||||
public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title", fallback: "Show Reblogs")
|
public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title", fallback: "Show Reblogs")
|
||||||
}
|
}
|
||||||
|
public enum ConfirmUnblockDomain {
|
||||||
|
/// Confirm to unblock domain %@
|
||||||
|
public static func message(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.Message", String(describing: p1), fallback: "Confirm to unblock domain %@")
|
||||||
|
}
|
||||||
|
/// Unblock Domain
|
||||||
|
public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.Title", fallback: "Unblock Domain")
|
||||||
|
}
|
||||||
public enum ConfirmUnblockUser {
|
public enum ConfirmUnblockUser {
|
||||||
/// Confirm to unblock %@
|
/// Confirm to unblock %@
|
||||||
public static func message(_ p1: Any) -> String {
|
public static func message(_ p1: Any) -> String {
|
||||||
|
@ -73,6 +73,7 @@ Please check your internet connection.";
|
|||||||
"Common.Controls.Friendship.BlockDomain" = "Block %@";
|
"Common.Controls.Friendship.BlockDomain" = "Block %@";
|
||||||
"Common.Controls.Friendship.BlockUser" = "Block %@";
|
"Common.Controls.Friendship.BlockUser" = "Block %@";
|
||||||
"Common.Controls.Friendship.Blocked" = "Blocked";
|
"Common.Controls.Friendship.Blocked" = "Blocked";
|
||||||
|
"Common.Controls.Friendship.DomainBlocked" = "Domain Blocked";
|
||||||
"Common.Controls.Friendship.EditInfo" = "Edit Info";
|
"Common.Controls.Friendship.EditInfo" = "Edit Info";
|
||||||
"Common.Controls.Friendship.Follow" = "Follow";
|
"Common.Controls.Friendship.Follow" = "Follow";
|
||||||
"Common.Controls.Friendship.Following" = "Following";
|
"Common.Controls.Friendship.Following" = "Following";
|
||||||
@ -85,6 +86,7 @@ Please check your internet connection.";
|
|||||||
"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs";
|
"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs";
|
||||||
"Common.Controls.Friendship.Unblock" = "Unblock";
|
"Common.Controls.Friendship.Unblock" = "Unblock";
|
||||||
"Common.Controls.Friendship.UnblockUser" = "Unblock %@";
|
"Common.Controls.Friendship.UnblockUser" = "Unblock %@";
|
||||||
|
"Common.Controls.Friendship.UnblockDomain" = "Unblock %@";
|
||||||
"Common.Controls.Friendship.Unmute" = "Unmute";
|
"Common.Controls.Friendship.Unmute" = "Unmute";
|
||||||
"Common.Controls.Friendship.UnmuteUser" = "Unmute %@";
|
"Common.Controls.Friendship.UnmuteUser" = "Unmute %@";
|
||||||
"Common.Controls.Keyboard.Common.ComposeNewPost" = "Compose New Post";
|
"Common.Controls.Keyboard.Common.ComposeNewPost" = "Compose New Post";
|
||||||
@ -336,6 +338,9 @@ uploaded to Mastodon.";
|
|||||||
"Scene.Profile.Header.FollowsYou" = "Follows You";
|
"Scene.Profile.Header.FollowsYou" = "Follows You";
|
||||||
"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirm to block %@";
|
"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirm to block %@";
|
||||||
"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Block Account";
|
"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Block Account";
|
||||||
|
"Scene.Profile.RelationshipActionAlert.ConfirmBlockDomain.Title" = "Block Domain";
|
||||||
|
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.Message" = "Confirm to unblock domain %@";
|
||||||
|
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.Title" = "Unblock Domain";
|
||||||
"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs";
|
"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs";
|
||||||
"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs";
|
"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs";
|
||||||
"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Confirm to mute %@";
|
"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Confirm to mute %@";
|
||||||
@ -563,4 +568,4 @@ uploaded to Mastodon.";
|
|||||||
"Widget.MultipleFollowers.ConfigurationDescription" = "Show number of followers for multiple accounts.";
|
"Widget.MultipleFollowers.ConfigurationDescription" = "Show number of followers for multiple accounts.";
|
||||||
"Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers";
|
"Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers";
|
||||||
"Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social";
|
"Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social";
|
||||||
"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower";
|
"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower";
|
||||||
|
@ -59,7 +59,8 @@ extension MastodonMenu {
|
|||||||
case deleteStatus
|
case deleteStatus
|
||||||
case editStatus
|
case editStatus
|
||||||
case followUser(FollowUserActionContext)
|
case followUser(FollowUserActionContext)
|
||||||
|
case blockDomain(BlockDomainActionContext)
|
||||||
|
|
||||||
func build(delegate: MastodonMenuDelegate) -> LabeledAction {
|
func build(delegate: MastodonMenuDelegate) -> LabeledAction {
|
||||||
switch self {
|
switch self {
|
||||||
case .hideReblogs(let context):
|
case .hideReblogs(let context):
|
||||||
@ -194,14 +195,29 @@ extension MastodonMenu {
|
|||||||
image = UIImage(systemName: "person.fill.badge.plus")
|
image = UIImage(systemName: "person.fill.badge.plus")
|
||||||
}
|
}
|
||||||
let action = LabeledAction(title: title, image: image) { [weak delegate] in
|
let action = LabeledAction(title: title, image: image) { [weak delegate] in
|
||||||
guard let delegate = delegate else { return }
|
guard let delegate else { return }
|
||||||
delegate.menuAction(self)
|
delegate.menuAction(self)
|
||||||
}
|
}
|
||||||
return action
|
return action
|
||||||
|
case .blockDomain(let context):
|
||||||
|
let title: String
|
||||||
|
let image: UIImage?
|
||||||
|
if context.isBlocking {
|
||||||
|
title = L10n.Common.Controls.Actions.unblockDomain(context.domain)
|
||||||
|
image = UIImage(systemName: "hand.raised.slash.fill")
|
||||||
|
} else {
|
||||||
|
title = L10n.Common.Controls.Actions.blockDomain(context.domain)
|
||||||
|
image = UIImage(systemName: "hand.raised.fill")
|
||||||
|
}
|
||||||
|
let action = LabeledAction(title: title, image: image) { [weak delegate] in
|
||||||
|
guard let delegate else { return }
|
||||||
|
|
||||||
} // end switch
|
delegate.menuAction(self)
|
||||||
} // end func build
|
}
|
||||||
} // end enum Action
|
return action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonMenu {
|
extension MastodonMenu {
|
||||||
@ -275,4 +291,14 @@ extension MastodonMenu {
|
|||||||
self.isFollowing = isFollowing
|
self.isFollowing = isFollowing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct BlockDomainActionContext {
|
||||||
|
public let domain: String
|
||||||
|
public let isBlocking: Bool
|
||||||
|
|
||||||
|
public init(domain: String, isBlocking: Bool) {
|
||||||
|
self.domain = domain
|
||||||
|
self.isBlocking = isBlocking
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ public enum RelationshipAction: Int, CaseIterable {
|
|||||||
case edit
|
case edit
|
||||||
case editing
|
case editing
|
||||||
case updating
|
case updating
|
||||||
|
case domainBlocking
|
||||||
|
|
||||||
public var option: RelationshipActionOptionSet {
|
public var option: RelationshipActionOptionSet {
|
||||||
return RelationshipActionOptionSet(rawValue: 1 << rawValue)
|
return RelationshipActionOptionSet(rawValue: 1 << rawValue)
|
||||||
@ -60,7 +61,8 @@ public struct RelationshipActionOptionSet: OptionSet {
|
|||||||
public static let updating = RelationshipAction.updating.option
|
public static let updating = RelationshipAction.updating.option
|
||||||
public static let showReblogs = RelationshipAction.showReblogs.option
|
public static let showReblogs = RelationshipAction.showReblogs.option
|
||||||
public static let editOptions: RelationshipActionOptionSet = [.edit, .editing, .updating]
|
public static let editOptions: RelationshipActionOptionSet = [.edit, .editing, .updating]
|
||||||
|
public static let domainBlocking = RelationshipAction.domainBlocking.option
|
||||||
|
|
||||||
public func highPriorityAction(except: RelationshipActionOptionSet) -> RelationshipAction? {
|
public func highPriorityAction(except: RelationshipActionOptionSet) -> RelationshipAction? {
|
||||||
let set = subtracting(except)
|
let set = subtracting(except)
|
||||||
for action in RelationshipAction.allCases.reversed() where set.contains(action.option) {
|
for action in RelationshipAction.allCases.reversed() where set.contains(action.option) {
|
||||||
@ -92,6 +94,7 @@ public struct RelationshipActionOptionSet: OptionSet {
|
|||||||
case .editing: return L10n.Common.Controls.Actions.done
|
case .editing: return L10n.Common.Controls.Actions.done
|
||||||
case .updating: return " "
|
case .updating: return " "
|
||||||
case .showReblogs: return " "
|
case .showReblogs: return " "
|
||||||
|
case .domainBlocking: return L10n.Common.Controls.Friendship.domainBlocked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,7 +122,8 @@ public final class RelationshipViewModel {
|
|||||||
@Published public var isBlocking = false
|
@Published public var isBlocking = false
|
||||||
@Published public var isBlockingBy = false
|
@Published public var isBlockingBy = false
|
||||||
@Published public var isSuspended = false
|
@Published public var isSuspended = false
|
||||||
|
@Published public var isDomainBlocking = false
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
Publishers.CombineLatest3(
|
Publishers.CombineLatest3(
|
||||||
$user,
|
$user,
|
||||||
@ -171,9 +175,7 @@ extension RelationshipViewModel {
|
|||||||
|
|
||||||
extension RelationshipViewModel {
|
extension RelationshipViewModel {
|
||||||
private func update(user: MastodonUser?, me: MastodonUser?) {
|
private func update(user: MastodonUser?, me: MastodonUser?) {
|
||||||
guard let user = user,
|
guard let user, let me else {
|
||||||
let me = me
|
|
||||||
else {
|
|
||||||
reset()
|
reset()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -188,6 +190,7 @@ extension RelationshipViewModel {
|
|||||||
self.isBlocking = optionSet.contains(.blocking)
|
self.isBlocking = optionSet.contains(.blocking)
|
||||||
self.isSuspended = optionSet.contains(.suspended)
|
self.isSuspended = optionSet.contains(.suspended)
|
||||||
self.showReblogs = optionSet.contains(.showReblogs)
|
self.showReblogs = optionSet.contains(.showReblogs)
|
||||||
|
self.isDomainBlocking = optionSet.contains(.domainBlocking)
|
||||||
|
|
||||||
self.optionSet = optionSet
|
self.optionSet = optionSet
|
||||||
}
|
}
|
||||||
@ -201,6 +204,7 @@ extension RelationshipViewModel {
|
|||||||
isBlocking = false
|
isBlocking = false
|
||||||
optionSet = nil
|
optionSet = nil
|
||||||
showReblogs = false
|
showReblogs = false
|
||||||
|
isDomainBlocking = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,6 +224,7 @@ extension RelationshipViewModel {
|
|||||||
let isBlockingBy = me.blockingBy.contains(user)
|
let isBlockingBy = me.blockingBy.contains(user)
|
||||||
let isBlocking = user.blockingBy.contains(me)
|
let isBlocking = user.blockingBy.contains(me)
|
||||||
let isShowingReblogs = me.showingReblogsBy.contains(user)
|
let isShowingReblogs = me.showingReblogsBy.contains(user)
|
||||||
|
let isDomainBlocking = user.domainBlockingBy.contains(me)
|
||||||
|
|
||||||
var optionSet: RelationshipActionOptionSet = [.follow]
|
var optionSet: RelationshipActionOptionSet = [.follow]
|
||||||
|
|
||||||
@ -263,6 +268,10 @@ extension RelationshipViewModel {
|
|||||||
optionSet.insert(.showReblogs)
|
optionSet.insert(.showReblogs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isDomainBlocking {
|
||||||
|
optionSet.insert(.domainBlocking)
|
||||||
|
}
|
||||||
|
|
||||||
return optionSet
|
return optionSet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import WidgetKit
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Intents
|
import Intents
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
import MastodonCore
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
|
||||||
struct FollowersCountWidgetProvider: IntentTimelineProvider {
|
struct FollowersCountWidgetProvider: IntentTimelineProvider {
|
||||||
@ -70,6 +71,9 @@ struct FollowersCountWidget: Widget {
|
|||||||
private extension FollowersCountWidgetProvider {
|
private extension FollowersCountWidgetProvider {
|
||||||
func loadCurrentEntry(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersCountEntry) -> Void) {
|
func loadCurrentEntry(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersCountEntry) -> Void) {
|
||||||
Task {
|
Task {
|
||||||
|
|
||||||
|
AuthenticationServiceProvider.shared.restore()
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let authBox = WidgetExtension.appContext
|
let authBox = WidgetExtension.appContext
|
||||||
.authenticationService
|
.authenticationService
|
||||||
|
@ -4,6 +4,7 @@ import WidgetKit
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
import MastodonCore
|
||||||
|
|
||||||
struct HashtagWidgetProvider: IntentTimelineProvider {
|
struct HashtagWidgetProvider: IntentTimelineProvider {
|
||||||
func placeholder(in context: Context) -> HashtagWidgetTimelineEntry {
|
func placeholder(in context: Context) -> HashtagWidgetTimelineEntry {
|
||||||
@ -24,6 +25,8 @@ struct HashtagWidgetProvider: IntentTimelineProvider {
|
|||||||
extension HashtagWidgetProvider {
|
extension HashtagWidgetProvider {
|
||||||
func loadMostRecentHashtag(for configuration: HashtagIntent, in context: Context, completion: @escaping (HashtagWidgetTimelineEntry) -> Void ) {
|
func loadMostRecentHashtag(for configuration: HashtagIntent, in context: Context, completion: @escaping (HashtagWidgetTimelineEntry) -> Void ) {
|
||||||
|
|
||||||
|
AuthenticationServiceProvider.shared.restore()
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let authBox = WidgetExtension.appContext
|
let authBox = WidgetExtension.appContext
|
||||||
.authenticationService
|
.authenticationService
|
||||||
|
@ -5,6 +5,7 @@ import SwiftUI
|
|||||||
import Intents
|
import Intents
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
import MastodonCore
|
||||||
|
|
||||||
struct LatestFollowersWidgetProvider: IntentTimelineProvider {
|
struct LatestFollowersWidgetProvider: IntentTimelineProvider {
|
||||||
func placeholder(in context: Context) -> LatestFollowersEntry {
|
func placeholder(in context: Context) -> LatestFollowersEntry {
|
||||||
@ -77,6 +78,9 @@ struct LatestFollowersWidget: Widget {
|
|||||||
private extension LatestFollowersWidgetProvider {
|
private extension LatestFollowersWidgetProvider {
|
||||||
func loadCurrentEntry(for configuration: LatestFollowersIntent, in context: Context, completion: @escaping (LatestFollowersEntry) -> Void) {
|
func loadCurrentEntry(for configuration: LatestFollowersIntent, in context: Context, completion: @escaping (LatestFollowersEntry) -> Void) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
|
||||||
|
AuthenticationServiceProvider.shared.restore()
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let authBox = WidgetExtension.appContext
|
let authBox = WidgetExtension.appContext
|
||||||
.authenticationService
|
.authenticationService
|
||||||
|
@ -4,6 +4,7 @@ import WidgetKit
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Intents
|
import Intents
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
import MastodonCore
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
|
||||||
struct MultiFollowersCountWidgetProvider: IntentTimelineProvider {
|
struct MultiFollowersCountWidgetProvider: IntentTimelineProvider {
|
||||||
@ -70,6 +71,9 @@ struct MultiFollowersCountWidget: Widget {
|
|||||||
private extension MultiFollowersCountWidgetProvider {
|
private extension MultiFollowersCountWidgetProvider {
|
||||||
func loadCurrentEntry(for configuration: MultiFollowersCountIntent, in context: Context, completion: @escaping (MultiFollowersCountEntry) -> Void) {
|
func loadCurrentEntry(for configuration: MultiFollowersCountIntent, in context: Context, completion: @escaping (MultiFollowersCountEntry) -> Void) {
|
||||||
Task {
|
Task {
|
||||||
|
|
||||||
|
AuthenticationServiceProvider.shared.restore()
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let authBox = WidgetExtension.appContext
|
let authBox = WidgetExtension.appContext
|
||||||
.authenticationService
|
.authenticationService
|
||||||
|
Loading…
x
Reference in New Issue
Block a user