feat: handle suspended account in profile scene
This commit is contained in:
parent
ba48adb470
commit
14176be4ed
|
@ -82,6 +82,7 @@
|
|||
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="note" optional="YES" attributeType="String"/>
|
||||
<attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="suspended" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="url" optional="YES" attributeType="String"/>
|
||||
<attribute name="username" attributeType="String"/>
|
||||
|
@ -199,7 +200,7 @@
|
|||
<element name="History" positionX="27" positionY="126" width="128" height="119"/>
|
||||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="MastodonAuthentication" positionX="18" positionY="162" width="128" height="209"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="659"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="674"/>
|
||||
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
||||
<element name="Poll" positionX="72" positionY="162" width="128" height="194"/>
|
||||
<element name="PollOption" positionX="81" positionY="171" width="128" height="134"/>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -757,17 +757,9 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
0F1E2D102615C39800C38565 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0F2021F5261325ED000C64BF /* HashtagTimeline */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0F1E2D102615C39800C38565 /* View */,
|
||||
0F2021FA2613262F000C64BF /* HashtagTimelineViewController.swift */,
|
||||
0F202226261411BA000C64BF /* HashtagTimelineViewController+StatusProvider.swift */,
|
||||
0F202200261326E6000C64BF /* HashtagTimelineViewModel.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) {
|
||||
|
|
|
@ -28,6 +28,7 @@ extension MastodonUser.Property {
|
|||
followersCount: entity.followersCount,
|
||||
locked: entity.locked,
|
||||
bot: entity.bot,
|
||||
suspended: entity.suspended,
|
||||
createdAt: entity.createdAt,
|
||||
networkDate: networkDate
|
||||
)
|
||||
|
|
|
@ -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...
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
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<Bool, Never>(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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -41,7 +41,7 @@ class ProfileViewModel: NSObject {
|
|||
let followersCount: CurrentValueSubject<Int?, Never>
|
||||
|
||||
let protected: CurrentValueSubject<Bool?, Never>
|
||||
// let suspended: CurrentValueSubject<Bool, Never>
|
||||
let suspended: CurrentValueSubject<Bool, Never>
|
||||
|
||||
let relationshipActionOptionSet = CurrentValueSubject<RelationshipActionOptionSet, Never>(.none)
|
||||
let isEditing = CurrentValueSubject<Bool, Never>(false)
|
||||
|
@ -55,6 +55,8 @@ class ProfileViewModel: NSObject {
|
|||
let isMoreMenuBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
|
||||
let isMeBarButtonItemsHidden = CurrentValueSubject<Bool, Never>(true)
|
||||
|
||||
let needsPagePinToTop = CurrentValueSubject<Bool, Never>(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
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ final class UserTimelineViewModel {
|
|||
|
||||
let isBlocking = CurrentValueSubject<Bool, Never>(false)
|
||||
let isBlockedBy = CurrentValueSubject<Bool, Never>(false)
|
||||
let isSuspended = CurrentValueSubject<Bool, Never>(false)
|
||||
let userDisplayName = CurrentValueSubject<String?, Never>(nil) // for suspended prompt label
|
||||
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>?
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -158,6 +158,7 @@ extension APIService {
|
|||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, 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<Mastodon.Response.Content<Mastodon.Entity.Relationship>, 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<Mastodon.Entity.Relationship> in
|
||||
switch result {
|
||||
case .success:
|
||||
return response
|
||||
case .failure(let error):
|
||||
throw error
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue