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)
}