From 14176be4ed38489a139032080cccc1ffc07102e5 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 8 Apr 2021 16:53:32 +0800 Subject: [PATCH] feat: handle suspended account in profile scene --- .../CoreData.xcdatamodel/contents | 3 +- CoreDataStack/Entity/MastodonUser.swift | 10 ++++ Localization/app.json | 4 +- Mastodon.xcodeproj/project.pbxproj | 8 --- Mastodon/Diffiable/Item/Item.swift | 14 ++++- .../CoreDataStack/MastodonUser.swift | 1 + Mastodon/Generated/Strings.swift | 8 ++- .../Resources/en.lproj/Localizable.strings | 4 +- ...meTimelineViewController+DebugAction.swift | 19 +++++++ .../Header/ProfileHeaderViewController.swift | 24 +++++++- .../ProfileRelationshipActionButton.swift | 2 +- .../Scene/Profile/ProfileViewController.swift | 45 +++++++++++---- Mastodon/Scene/Profile/ProfileViewModel.swift | 25 +++++++- .../Timeline/UserTimelineViewModel.swift | 15 ++++- .../View/Content/TimelineHeaderView.swift | 12 +++- .../APIService/APIService+Follow.swift | 57 ++++++++++++++----- .../APIService+CoreData+MastodonUser.swift | 3 + 17 files changed, 205 insertions(+), 49 deletions(-) diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index e2f059d5..9e5b7cf3 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -82,6 +82,7 @@ + @@ -199,7 +200,7 @@ - + diff --git a/CoreDataStack/Entity/MastodonUser.swift b/CoreDataStack/Entity/MastodonUser.swift index 878eb9ad..714b6d0f 100644 --- a/CoreDataStack/Entity/MastodonUser.swift +++ b/CoreDataStack/Entity/MastodonUser.swift @@ -31,6 +31,7 @@ final public class MastodonUser: NSManagedObject { @NSManaged public private(set) var locked: Bool @NSManaged public private(set) var bot: Bool + @NSManaged public private(set) var suspended: Bool @NSManaged public private(set) var createdAt: Date @NSManaged public private(set) var updatedAt: Date @@ -93,6 +94,7 @@ extension MastodonUser { user.locked = property.locked user.bot = property.bot ?? false + user.suspended = property.suspended ?? false // Mastodon do not provide relationship on the `Account` // Update relationship via attribute updating interface @@ -174,6 +176,11 @@ extension MastodonUser { self.bot = bot } } + public func update(suspended: Bool) { + if self.suspended != suspended { + self.suspended = suspended + } + } public func update(isFollowing: Bool, by mastodonUser: MastodonUser) { if isFollowing { @@ -268,6 +275,7 @@ extension MastodonUser { public let followersCount: Int public let locked: Bool public let bot: Bool? + public let suspended: Bool? public let createdAt: Date public let networkDate: Date @@ -289,6 +297,7 @@ extension MastodonUser { followersCount: Int, locked: Bool, bot: Bool?, + suspended: Bool?, createdAt: Date, networkDate: Date ) { @@ -309,6 +318,7 @@ extension MastodonUser { self.followersCount = followersCount self.locked = locked self.bot = bot + self.suspended = suspended self.createdAt = createdAt self.networkDate = networkDate } diff --git a/Localization/app.json b/Localization/app.json index ea160552..1e0f6eaa 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -69,6 +69,7 @@ "firendship": { "follow": "Follow", "following": "Following", + "request": "Request", "pending": "Pending", "block": "Block", "block_user": "Block %s", @@ -91,7 +92,8 @@ "no_status_found": "No Status Found", "blocking_warning": "You can’t view Artbot’s profile\n until you unblock them.\nYour account looks like this to them.", "blocked_warning": "You can’t view Artbot’s profile\n until they unblock you.", - "suspended_warning": "This account is suspended." + "suspended_warning": "This account has been suspended.", + "user_suspended_warning": "%s's account has been suspended." } } }, diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index c2551550..353578ba 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -757,17 +757,9 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 0F1E2D102615C39800C38565 /* View */ = { - isa = PBXGroup; - children = ( - ); - path = View; - sourceTree = ""; - }; 0F2021F5261325ED000C64BF /* HashtagTimeline */ = { isa = PBXGroup; children = ( - 0F1E2D102615C39800C38565 /* View */, 0F2021FA2613262F000C64BF /* HashtagTimelineViewController.swift */, 0F202226261411BA000C64BF /* HashtagTimelineViewController+StatusProvider.swift */, 0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */, diff --git a/Mastodon/Diffiable/Item/Item.swift b/Mastodon/Diffiable/Item/Item.swift index 0a27f187..9f82f6ca 100644 --- a/Mastodon/Diffiable/Item/Item.swift +++ b/Mastodon/Diffiable/Item/Item.swift @@ -63,11 +63,21 @@ extension Item { let id = UUID() let reason: Reason - enum Reason { + enum Reason: Equatable { case noStatusFound case blocking case blocked - case suspended + case suspended(name: String?) + + static func == (lhs: Item.EmptyStateHeaderAttribute.Reason, rhs: Item.EmptyStateHeaderAttribute.Reason) -> Bool { + switch (lhs, rhs) { + case (.noStatusFound, noStatusFound): return true + case (.blocking, blocking): return true + case (.blocked, blocked): return true + case (.suspended(let nameLeft), .suspended(let nameRight)): return nameLeft == nameRight + default: return false + } + } } init(reason: Reason) { diff --git a/Mastodon/Extension/CoreDataStack/MastodonUser.swift b/Mastodon/Extension/CoreDataStack/MastodonUser.swift index e140ab95..4e213830 100644 --- a/Mastodon/Extension/CoreDataStack/MastodonUser.swift +++ b/Mastodon/Extension/CoreDataStack/MastodonUser.swift @@ -28,6 +28,7 @@ extension MastodonUser.Property { followersCount: entity.followersCount, locked: entity.locked, bot: entity.bot, + suspended: entity.suspended, createdAt: entity.createdAt, networkDate: networkDate ) diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 8ff45af5..d20ef1b2 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -112,6 +112,8 @@ internal enum L10n { } /// Pending internal static let pending = L10n.tr("Localizable", "Common.Controls.Firendship.Pending") + /// Request + internal static let request = L10n.tr("Localizable", "Common.Controls.Firendship.Request") /// Unblock internal static let unblock = L10n.tr("Localizable", "Common.Controls.Firendship.Unblock") /// Unblock %@ @@ -179,8 +181,12 @@ internal enum L10n { internal static let blockingWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.BlockingWarning") /// No Status Found internal static let noStatusFound = L10n.tr("Localizable", "Common.Controls.Timeline.Header.NoStatusFound") - /// This account is suspended. + /// This account has been suspended. internal static let suspendedWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.SuspendedWarning") + /// %@'s account has been suspended. + internal static func userSuspendedWarning(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Timeline.Header.UserSuspendedWarning", String(describing: p1)) + } } internal enum Loader { /// Loading missing posts... diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 175d63a1..853c7577 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -38,6 +38,7 @@ Please check your internet connection."; "Common.Controls.Firendship.MuteUser" = "Mute %@"; "Common.Controls.Firendship.Muted" = "Muted"; "Common.Controls.Firendship.Pending" = "Pending"; +"Common.Controls.Firendship.Request" = "Request"; "Common.Controls.Firendship.Unblock" = "Unblock"; "Common.Controls.Firendship.UnblockUser" = "Unblock %@"; "Common.Controls.Firendship.Unmute" = "Unmute"; @@ -60,7 +61,8 @@ Please check your internet connection."; until you unblock them. Your account looks like this to them."; "Common.Controls.Timeline.Header.NoStatusFound" = "No Status Found"; -"Common.Controls.Timeline.Header.SuspendedWarning" = "This account is suspended."; +"Common.Controls.Timeline.Header.SuspendedWarning" = "This account has been suspended."; +"Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@'s account has been suspended."; "Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts"; "Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts..."; "Common.Countable.Photo.Multiple" = "photos"; diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 785d9726..70afdecf 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -25,6 +25,10 @@ extension HomeTimelineViewController { guard let self = self else { return } self.showPublicTimelineAction(action) }, + UIAction(title: "Show Profile", image: UIImage(systemName: "person.crop.circle"), attributes: []) { [weak self] action in + guard let self = self else { return } + self.showProfileAction(action) + }, UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in guard let self = self else { return } self.signOutAction(action) @@ -277,5 +281,20 @@ extension HomeTimelineViewController { coordinator.present(scene: .publicTimeline, from: self, transition: .show) } + @objc private func showProfileAction(_ sender: UIAction) { + let alertController = UIAlertController(title: "Enter User ID", message: nil, preferredStyle: .alert) + alertController.addTextField() + let showAction = UIAlertAction(title: "Show", style: .default) { [weak self, weak alertController] _ in + guard let self = self else { return } + guard let textField = alertController?.textFields?.first else { return } + let profileViewModel = RemoteProfileViewModel(context: self.context, userID: textField.text ?? "") + self.coordinator.present(scene: .profile(viewModel: profileViewModel), from: self, transition: .show) + } + alertController.addAction(showAction) + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + alertController.addAction(cancelAction) + coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) + } + } #endif diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 85558190..4412950a 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -7,6 +7,7 @@ import os.log import UIKit +import Combine protocol ProfileHeaderViewControllerDelegate: class { func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView) @@ -21,6 +22,8 @@ final class ProfileHeaderViewController: UIViewController { weak var delegate: ProfileHeaderViewControllerDelegate? + var disposeBag = Set() + let profileHeaderView = ProfileHeaderView() let pageSegmentedControl: UISegmentedControl = { let segmenetedControl = UISegmentedControl(items: ["A", "B"]) @@ -33,6 +36,8 @@ final class ProfileHeaderViewController: UIViewController { // private var isAdjustBannerImageViewForSafeAreaInset = false private var containerSafeAreaInset: UIEdgeInsets = .zero + + let needsSetupBottomShadow = CurrentValueSubject(true) deinit { os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -67,6 +72,14 @@ extension ProfileHeaderViewController { ]) pageSegmentedControl.addTarget(self, action: #selector(ProfileHeaderViewController.pageSegmentedControlValueChanged(_:)), for: .valueChanged) + + needsSetupBottomShadow + .receive(on: DispatchQueue.main) + .sink { [weak self] needsSetupBottomShadow in + guard let self = self else { return } + self.setupBottomShadow() + } + .store(in: &disposeBag) } override func viewDidAppear(_ animated: Bool) { @@ -85,7 +98,7 @@ extension ProfileHeaderViewController { super.viewDidLayoutSubviews() delegate?.profileHeaderViewController(self, viewLayoutDidUpdate: view) - view.layer.setupShadow(color: UIColor.black.withAlphaComponent(0.12), alpha: Float(bottomShadowAlpha), x: 0, y: 2, blur: 2, spread: 0, roundedRect: view.bounds, byRoundingCorners: .allCorners, cornerRadii: .zero) + setupBottomShadow() } } @@ -105,6 +118,15 @@ extension ProfileHeaderViewController { containerSafeAreaInset = inset } + func setupBottomShadow() { + guard needsSetupBottomShadow.value else { + view.layer.shadowColor = nil + view.layer.shadowRadius = 0 + return + } + view.layer.setupShadow(color: UIColor.black.withAlphaComponent(0.12), alpha: Float(bottomShadowAlpha), x: 0, y: 2, blur: 2, spread: 0, roundedRect: view.bounds, byRoundingCorners: .allCorners, cornerRadii: .zero) + } + private func updateHeaderBottomShadow(progress: CGFloat) { let alpha = min(max(0, 10 * progress - 9), 1) if bottomShadowAlpha != alpha { diff --git a/Mastodon/Scene/Profile/Header/View/ProfileRelationshipActionButton.swift b/Mastodon/Scene/Profile/Header/View/ProfileRelationshipActionButton.swift index 0f6a804b..64dc9f00 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileRelationshipActionButton.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileRelationshipActionButton.swift @@ -34,7 +34,7 @@ extension ProfileRelationshipActionButton { setTitleColor(UIColor.white.withAlphaComponent(0.5), for: .highlighted) setBackgroundImage(.placeholder(color: actionOptionSet.backgroundColor), for: .normal) setBackgroundImage(.placeholder(color: actionOptionSet.backgroundColor.withAlphaComponent(0.5)), for: .highlighted) - setBackgroundImage(.placeholder(color: actionOptionSet.backgroundColor), for: .disabled) + setBackgroundImage(.placeholder(color: actionOptionSet.backgroundColor.withAlphaComponent(0.5)), for: .disabled) if let option = actionOptionSet.highPriorityAction(except: .editOptions), option == .blocked { isEnabled = false diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index c1f11155..0eba0085 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -136,19 +136,24 @@ extension ProfileViewController { navigationItem.titleView = UIView() - Publishers.CombineLatest3( + Publishers.CombineLatest4( + viewModel.suspended.eraseToAnyPublisher(), viewModel.isMeBarButtonItemsHidden.eraseToAnyPublisher(), viewModel.isReplyBarButtonItemHidden.eraseToAnyPublisher(), viewModel.isMoreMenuBarButtonItemHidden.eraseToAnyPublisher() ) .receive(on: DispatchQueue.main) - .sink { [weak self] isMeBarButtonItemsHidden, isReplyBarButtonItemHidden, isMoreMenuBarButtonItemHidden in + .sink { [weak self] suspended, isMeBarButtonItemsHidden, isReplyBarButtonItemHidden, isMoreMenuBarButtonItemHidden in guard let self = self else { return } var items: [UIBarButtonItem] = [] defer { self.navigationItem.rightBarButtonItems = !items.isEmpty ? items : nil } + guard !suspended else { + return + } + guard isMeBarButtonItemsHidden else { items.append(self.settingBarButtonItem) items.append(self.shareBarButtonItem) @@ -345,6 +350,21 @@ extension ProfileViewController { } } .store(in: &disposeBag) + Publishers.CombineLatest3( + viewModel.isBlocking.eraseToAnyPublisher(), + viewModel.isBlockedBy.eraseToAnyPublisher(), + viewModel.suspended.eraseToAnyPublisher() + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] isBlocking, isBlockedBy, suspended in + guard let self = self else { return } + let isNeedSetHidden = isBlocking || isBlockedBy || suspended + self.profileHeaderViewController.needsSetupBottomShadow.value = !isNeedSetHidden + self.profileHeaderViewController.profileHeaderView.bioContainerView.isHidden = isNeedSetHidden + self.profileHeaderViewController.pageSegmentedControl.isHidden = isNeedSetHidden + self.viewModel.needsPagePinToTop.value = isNeedSetHidden + } + .store(in: &disposeBag) viewModel.bioDescription .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] bio in @@ -411,6 +431,8 @@ extension ProfileViewController { viewModel.userID.assign(to: \.value, on: userTimelineViewModel.userID).store(in: &disposeBag) viewModel.isBlocking.assign(to: \.value, on: userTimelineViewModel.isBlocking).store(in: &disposeBag) viewModel.isBlockedBy.assign(to: \.value, on: userTimelineViewModel.isBlockedBy).store(in: &disposeBag) + viewModel.suspended.assign(to: \.value, on: userTimelineViewModel.isSuspended).store(in: &disposeBag) + viewModel.name.assign(to: \.value, on: userTimelineViewModel.userDisplayName).store(in: &disposeBag) } } @@ -476,10 +498,15 @@ extension ProfileViewController: UIScrollViewDelegate { contentOffsets.removeAll() } else { containerScrollView.contentOffset.y = topMaxContentOffsetY - if let customScrollViewContainerController = profileSegmentedViewController.pagingViewController.currentViewController as? ScrollViewContainer { - let contentOffsetY = scrollView.contentOffset.y - containerScrollView.contentOffset.y - customScrollViewContainerController.scrollView.contentOffset.y = contentOffsetY + if viewModel.needsPagePinToTop.value { + // do nothing + } else { + if let customScrollViewContainerController = profileSegmentedViewController.pagingViewController.currentViewController as? ScrollViewContainer { + let contentOffsetY = scrollView.contentOffset.y - containerScrollView.contentOffset.y + customScrollViewContainerController.scrollView.contentOffset.y = contentOffsetY + } } + } // elastically banner image @@ -538,16 +565,14 @@ extension ProfileViewController: ProfileHeaderViewDelegate { switch relationshipAction { case .none: break - case .follow, .following: + case .follow, .reqeust, .pending, .following: UserProviderFacade.toggleUserFollowRelationship(provider: self) .sink { _ in - + // TODO: handle error } receiveValue: { _ in - + // do nothing } .store(in: &disposeBag) - case .pending: - break case .muting: guard let mastodonUser = viewModel.mastodonUser.value else { return } let name = mastodonUser.displayNameWithFallback diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 41fcdcf4..7db38d33 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -41,7 +41,7 @@ class ProfileViewModel: NSObject { let followersCount: CurrentValueSubject let protected: CurrentValueSubject - // let suspended: CurrentValueSubject + let suspended: CurrentValueSubject let relationshipActionOptionSet = CurrentValueSubject(.none) let isEditing = CurrentValueSubject(false) @@ -55,6 +55,8 @@ class ProfileViewModel: NSObject { let isMoreMenuBarButtonItemHidden = CurrentValueSubject(true) let isMeBarButtonItemsHidden = CurrentValueSubject(true) + let needsPagePinToTop = CurrentValueSubject(false) + init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) { self.context = context self.mastodonUser = CurrentValueSubject(mastodonUser) @@ -62,7 +64,6 @@ class ProfileViewModel: NSObject { self.userID = CurrentValueSubject(mastodonUser?.id) self.bannerImageURL = CurrentValueSubject(mastodonUser?.headerImageURL()) self.avatarImageURL = CurrentValueSubject(mastodonUser?.avatarImageURL()) -// self.protected = CurrentValueSubject(twitterUser?.protected) self.name = CurrentValueSubject(mastodonUser?.displayNameWithFallback) self.username = CurrentValueSubject(mastodonUser?.acctWithDomain) self.bioDescription = CurrentValueSubject(mastodonUser?.note) @@ -71,6 +72,7 @@ class ProfileViewModel: NSObject { self.followingCount = CurrentValueSubject(mastodonUser.flatMap { Int(truncating: $0.followingCount) }) self.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int(truncating: $0.followersCount) }) self.protected = CurrentValueSubject(mastodonUser?.locked) + self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false) super.init() relationshipActionOptionSet @@ -226,6 +228,7 @@ extension ProfileViewModel { self.followingCount.value = mastodonUser.flatMap { Int(truncating: $0.followingCount) } self.followersCount.value = mastodonUser.flatMap { Int(truncating: $0.followersCount) } self.protected.value = mastodonUser?.locked + self.suspended.value = mastodonUser?.suspended ?? false } private func update(mastodonUser: MastodonUser?, currentMastodonUser: MastodonUser?) { @@ -255,6 +258,14 @@ extension ProfileViewModel { // set with follow action default var relationshipActionSet = RelationshipActionOptionSet([.follow]) + if mastodonUser.locked { + relationshipActionSet.insert(.request) + } + + if mastodonUser.suspended { + relationshipActionSet.insert(.suspended) + } + let isFollowing = mastodonUser.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false if isFollowing { relationshipActionSet.insert(.following) @@ -308,11 +319,13 @@ extension ProfileViewModel { enum RelationshipAction: Int, CaseIterable { case none // set hide from UI case follow + case reqeust case pending case following case muting case blocked case blocking + case suspended case edit case editing @@ -327,11 +340,13 @@ extension ProfileViewModel { static let none = RelationshipAction.none.option static let follow = RelationshipAction.follow.option + static let request = RelationshipAction.reqeust.option static let pending = RelationshipAction.pending.option static let following = RelationshipAction.following.option static let muting = RelationshipAction.muting.option static let blocked = RelationshipAction.blocked.option static let blocking = RelationshipAction.blocking.option + static let suspended = RelationshipAction.suspended.option static let edit = RelationshipAction.edit.option static let editing = RelationshipAction.editing.option @@ -354,11 +369,13 @@ extension ProfileViewModel { switch highPriorityAction { case .none: return " " case .follow: return L10n.Common.Controls.Firendship.follow + case .reqeust: return L10n.Common.Controls.Firendship.request case .pending: return L10n.Common.Controls.Firendship.pending case .following: return L10n.Common.Controls.Firendship.following case .muting: return L10n.Common.Controls.Firendship.muted case .blocked: return L10n.Common.Controls.Firendship.follow // blocked by user case .blocking: return L10n.Common.Controls.Firendship.blocked + case .suspended: return L10n.Common.Controls.Firendship.follow case .edit: return L10n.Common.Controls.Firendship.editInfo case .editing: return L10n.Common.Controls.Actions.done } @@ -372,11 +389,13 @@ extension ProfileViewModel { switch highPriorityAction { case .none: return Asset.Colors.Button.normal.color case .follow: return Asset.Colors.Button.normal.color + case .reqeust: return Asset.Colors.Button.normal.color case .pending: return Asset.Colors.Button.normal.color case .following: return Asset.Colors.Button.normal.color case .muting: return Asset.Colors.Background.alertYellow.color - case .blocked: return Asset.Colors.Button.disabled.color + case .blocked: return Asset.Colors.Button.normal.color case .blocking: return Asset.Colors.Background.danger.color + case .suspended: return Asset.Colors.Button.normal.color case .edit: return Asset.Colors.Button.normal.color case .editing: return Asset.Colors.Button.normal.color } diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift index ac66f037..03e5e627 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift @@ -27,6 +27,8 @@ final class UserTimelineViewModel { let isBlocking = CurrentValueSubject(false) let isBlockedBy = CurrentValueSubject(false) + let isSuspended = CurrentValueSubject(false) + let userDisplayName = CurrentValueSubject(nil) // for suspended prompt label // output var diffableDataSource: UITableViewDiffableDataSource? @@ -59,14 +61,15 @@ final class UserTimelineViewModel { .assign(to: \.value, on: statusFetchedResultsController.domain) .store(in: &disposeBag) - Publishers.CombineLatest3( + Publishers.CombineLatest4( statusFetchedResultsController.objectIDs.eraseToAnyPublisher(), isBlocking.eraseToAnyPublisher(), - isBlockedBy.eraseToAnyPublisher() + isBlockedBy.eraseToAnyPublisher(), + isSuspended.eraseToAnyPublisher() ) .receive(on: DispatchQueue.main) .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) - .sink { [weak self] objectIDs, isBlocking, isBlockedBy in + .sink { [weak self] objectIDs, isBlocking, isBlockedBy, isSuspended in guard let self = self else { return } guard let diffableDataSource = self.diffableDataSource else { return } @@ -89,6 +92,12 @@ final class UserTimelineViewModel { return } + let name = self.userDisplayName.value + guard !isSuspended else { + snapshot.appendItems([Item.emptyStateHeader(attribute: Item.EmptyStateHeaderAttribute(reason: .suspended(name: name)))], toSection: .main) + return + } + var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:] let oldSnapshot = diffableDataSource.snapshot() for item in oldSnapshot.itemIdentifiers { diff --git a/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift b/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift index e253b3ca..b5e4c5bd 100644 --- a/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift +++ b/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift @@ -84,8 +84,10 @@ extension TimelineHeaderView { extension Item.EmptyStateHeaderAttribute.Reason { var iconImage: UIImage? { switch self { - case .noStatusFound, .blocking, .blocked, .suspended: + case .noStatusFound, .blocking, .blocked: return UIImage(systemName: "nosign", withConfiguration: UIImage.SymbolConfiguration(pointSize: 64, weight: .bold))! + case .suspended: + return UIImage(systemName: "person.crop.circle.badge.xmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 64, weight: .bold))! } } @@ -97,8 +99,12 @@ extension Item.EmptyStateHeaderAttribute.Reason { return L10n.Common.Controls.Timeline.Header.blockingWarning case .blocked: return L10n.Common.Controls.Timeline.Header.blockedWarning - case .suspended: - return L10n.Common.Controls.Timeline.Header.suspendedWarning + case .suspended(let name): + if let name = name { + return L10n.Common.Controls.Timeline.Header.userSuspendedWarning(name) + } else { + return L10n.Common.Controls.Timeline.Header.suspendedWarning + } } } } diff --git a/Mastodon/Service/APIService/APIService+Follow.swift b/Mastodon/Service/APIService/APIService+Follow.swift index f2c57db5..cf19878d 100644 --- a/Mastodon/Service/APIService/APIService+Follow.swift +++ b/Mastodon/Service/APIService/APIService+Follow.swift @@ -158,6 +158,7 @@ extension APIService { ) -> AnyPublisher, Error> { let domain = mastodonAuthenticationBox.domain let authorization = mastodonAuthenticationBox.userAuthorization + let requestMastodonUserID = mastodonAuthenticationBox.userID return Mastodon.API.Account.follow( session: session, @@ -166,22 +167,50 @@ extension APIService { followQueryType: followQueryType, authorization: authorization ) - .handleEvents(receiveCompletion: { [weak self] completion in - guard let _ = self else { return } - switch completion { - case .failure(let error): - // TODO: handle error - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update follow fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) - break - case .finished: - switch followQueryType { - case .follow: - break - case .unfollow: - break +// .handleEvents(receiveCompletion: { [weak self] completion in +// guard let _ = self else { return } +// switch completion { +// case .failure(let error): +// // TODO: handle error +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update follow fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) +// break +// case .finished: +// switch followQueryType { +// case .follow: +// break +// case .unfollow: +// break +// } +// } +// }) + .flatMap { response -> AnyPublisher, Error> in + let managedObjectContext = self.backgroundManagedObjectContext + return managedObjectContext.performChanges { + let requestMastodonUserRequest = MastodonUser.sortedFetchRequest + requestMastodonUserRequest.predicate = MastodonUser.predicate(domain: domain, id: requestMastodonUserID) + requestMastodonUserRequest.fetchLimit = 1 + guard let requestMastodonUser = managedObjectContext.safeFetch(requestMastodonUserRequest).first else { return } + + let lookUpMastodonUserRequest = MastodonUser.sortedFetchRequest + lookUpMastodonUserRequest.predicate = MastodonUser.predicate(domain: domain, id: mastodonUserID) + lookUpMastodonUserRequest.fetchLimit = 1 + let lookUpMastodonuser = managedObjectContext.safeFetch(lookUpMastodonUserRequest).first + + if let lookUpMastodonuser = lookUpMastodonuser { + let entity = response.value + APIService.CoreData.update(user: lookUpMastodonuser, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: response.networkDate) } } - }) + .tryMap { result -> Mastodon.Response.Content in + switch result { + case .success: + return response + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } .eraseToAnyPublisher() } diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonUser.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonUser.swift index fdac2a2a..4a123705 100644 --- a/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonUser.swift +++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonUser.swift @@ -95,6 +95,9 @@ extension APIService.CoreData { user.update(statusesCount: property.statusesCount) user.update(followingCount: property.followingCount) user.update(followersCount: property.followersCount) + user.update(locked: property.locked) + property.bot.flatMap { user.update(bot: $0) } + property.suspended.flatMap { user.update(suspended: $0) } user.didUpdate(at: networkDate) }