From 4af2c0ca783fd5500f4f3c8196fa2fe44db5d073 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 24 Jun 2021 14:32:01 +0800 Subject: [PATCH 1/6] fix: profile block state i18n strings not set with name issue. resolve #171 --- Localization/app.json | 6 ++++-- .../xcschemes/xcschememanagement.plist | 4 ++-- Mastodon/Diffiable/Item/Item.swift | 8 ++++---- Mastodon/Generated/Strings.swift | 12 +++++++++-- .../Resources/ar.lproj/Localizable.strings | 9 +++++++-- .../Resources/en.lproj/Localizable.strings | 9 +++++++-- .../Timeline/UserTimelineViewModel.swift | 17 ++++++++++++---- .../View/Content/TimelineHeaderView.swift | 20 +++++++++++++------ 8 files changed, 61 insertions(+), 24 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 155061044..67694109f 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -183,8 +183,10 @@ }, "header": { "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.", + "blocking_warning": "You can’t view this profile\n until you unblock them.\nYour account looks like this to them.", + "user_blocking_warning": "You can’t view %s's profile\n until you unblock them.\nYour account looks like this to them.", + "blocked_warning": "You can’t view this’s profile\n until they unblock you.", + "user_blocked_warning": "You can’t view %s’s profile\n until they unblock you.", "suspended_warning": "This account has been suspended.", "user_suspended_warning": "%s’s account has been suspended." }, diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index f1135b12e..da1d80e90 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 20 + 21 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -37,7 +37,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 21 + 20 SuppressBuildableAutocreation diff --git a/Mastodon/Diffiable/Item/Item.swift b/Mastodon/Diffiable/Item/Item.swift index 635d0a57d..bde116b69 100644 --- a/Mastodon/Diffiable/Item/Item.swift +++ b/Mastodon/Diffiable/Item/Item.swift @@ -62,15 +62,15 @@ extension Item { enum Reason: Equatable { case noStatusFound - case blocking - case blocked + case blocking(name: String?) + case blocked(name: String?) 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 (.blocking(let nameLeft), blocking(let nameRight)): return nameLeft == nameRight + case (.blocked(let nameLeft), blocked(let nameRight)): return nameLeft == nameRight case (.suspended(let nameLeft), .suspended(let nameRight)): return nameLeft == nameRight default: return false } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 78d72d0b7..b02535cca 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -355,14 +355,22 @@ internal enum L10n { } } internal enum Header { - /// You can’t view Artbot’s profile\n until they unblock you. + /// You can’t view this’s profile\n until they unblock you. internal static let blockedWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.BlockedWarning") - /// You can’t view Artbot’s profile\n until you unblock them.\nYour account looks like this to them. + /// You can’t view this profile\n until you unblock them.\nYour account looks like this to them. 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 has been suspended. internal static let suspendedWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.SuspendedWarning") + /// You can’t view %@’s profile\n until they unblock you. + internal static func userBlockedWarning(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Timeline.Header.UserBlockedWarning", String(describing: p1)) + } + /// You can’t view %@'s profile\n until you unblock them.\nYour account looks like this to them. + internal static func userBlockingWarning(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Timeline.Header.UserBlockingWarning", String(describing: p1)) + } /// %@’s account has been suspended. internal static func userSuspendedWarning(_ p1: Any) -> String { return L10n.tr("Localizable", "Common.Controls.Timeline.Header.UserSuspendedWarning", String(describing: p1)) diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index 98d43de03..d8876a0e5 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -120,13 +120,18 @@ Please check your internet connection."; "Common.Controls.Timeline.Accessibility.CountFavorites" = "%@ favorites"; "Common.Controls.Timeline.Accessibility.CountReblogs" = "%@ reblogs"; "Common.Controls.Timeline.Accessibility.CountReplies" = "%@ replies"; -"Common.Controls.Timeline.Header.BlockedWarning" = "You can’t view Artbot’s profile +"Common.Controls.Timeline.Header.BlockedWarning" = "You can’t view this’s profile until they unblock you."; -"Common.Controls.Timeline.Header.BlockingWarning" = "You can’t view Artbot’s profile +"Common.Controls.Timeline.Header.BlockingWarning" = "You can’t view this profile 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 has been suspended."; +"Common.Controls.Timeline.Header.UserBlockedWarning" = "You can’t view %@’s profile + until they unblock you."; +"Common.Controls.Timeline.Header.UserBlockingWarning" = "You can’t view %@'s profile + until you unblock them. +Your account looks like this to them."; "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..."; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 98d43de03..d8876a0e5 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -120,13 +120,18 @@ Please check your internet connection."; "Common.Controls.Timeline.Accessibility.CountFavorites" = "%@ favorites"; "Common.Controls.Timeline.Accessibility.CountReblogs" = "%@ reblogs"; "Common.Controls.Timeline.Accessibility.CountReplies" = "%@ replies"; -"Common.Controls.Timeline.Header.BlockedWarning" = "You can’t view Artbot’s profile +"Common.Controls.Timeline.Header.BlockedWarning" = "You can’t view this’s profile until they unblock you."; -"Common.Controls.Timeline.Header.BlockingWarning" = "You can’t view Artbot’s profile +"Common.Controls.Timeline.Header.BlockingWarning" = "You can’t view this profile 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 has been suspended."; +"Common.Controls.Timeline.Header.UserBlockedWarning" = "You can’t view %@’s profile + until they unblock you."; +"Common.Controls.Timeline.Header.UserBlockingWarning" = "You can’t view %@'s profile + until you unblock them. +Your account looks like this to them."; "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..."; diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift index c282e6719..7d6a6c8fe 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift @@ -82,19 +82,28 @@ final class UserTimelineViewModel { diffableDataSource.apply(snapshot, animatingDifferences: !items.isEmpty) } + let name = self.userDisplayName.value guard !isBlocking else { - snapshot.appendItems([Item.emptyStateHeader(attribute: Item.EmptyStateHeaderAttribute(reason: .blocking))], toSection: .main) + snapshot.appendItems( + [Item.emptyStateHeader(attribute: Item.EmptyStateHeaderAttribute(reason: .blocking(name: name)))], + toSection: .main + ) return } guard !isBlockedBy else { - snapshot.appendItems([Item.emptyStateHeader(attribute: Item.EmptyStateHeaderAttribute(reason: .blocked))], toSection: .main) + snapshot.appendItems( + [Item.emptyStateHeader(attribute: Item.EmptyStateHeaderAttribute(reason: .blocked(name: name)))], + toSection: .main + ) return } - let name = self.userDisplayName.value guard !isSuspended else { - snapshot.appendItems([Item.emptyStateHeader(attribute: Item.EmptyStateHeaderAttribute(reason: .suspended(name: name)))], toSection: .main) + snapshot.appendItems( + [Item.emptyStateHeader(attribute: Item.EmptyStateHeaderAttribute(reason: .suspended(name: name)))], + toSection: .main + ) return } diff --git a/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift b/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift index f095f6f44..2948af4cf 100644 --- a/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift +++ b/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift @@ -97,10 +97,18 @@ extension Item.EmptyStateHeaderAttribute.Reason { switch self { case .noStatusFound: return L10n.Common.Controls.Timeline.Header.noStatusFound - case .blocking: - return L10n.Common.Controls.Timeline.Header.blockingWarning - case .blocked: - return L10n.Common.Controls.Timeline.Header.blockedWarning + case .blocking(let name): + if let name = name { + return L10n.Common.Controls.Timeline.Header.userBlockingWarning(name) + } else { + return L10n.Common.Controls.Timeline.Header.blockingWarning + } + case .blocked(let name): + if let name = name { + return L10n.Common.Controls.Timeline.Header.userBlockedWarning(name) + } else { + return L10n.Common.Controls.Timeline.Header.blockedWarning + } case .suspended(let name): if let name = name { return L10n.Common.Controls.Timeline.Header.userSuspendedWarning(name) @@ -119,8 +127,8 @@ struct TimelineHeaderView_Previews: PreviewProvider { Group { UIViewPreview(width: 375) { let headerView = TimelineHeaderView() - headerView.iconImageView.image = Item.EmptyStateHeaderAttribute.Reason.blocking.iconImage - headerView.messageLabel.text = Item.EmptyStateHeaderAttribute.Reason.blocking.message + headerView.iconImageView.image = Item.EmptyStateHeaderAttribute.Reason.blocking(name: nil).iconImage + headerView.messageLabel.text = Item.EmptyStateHeaderAttribute.Reason.blocking(name: nil).message return headerView } .previewLayout(.fixed(width: 375, height: 400)) From d8be41fd3343ffbcbfdbcef16b2b7cbdce98e525 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 24 Jun 2021 15:04:10 +0800 Subject: [PATCH 2/6] fix: missing update the latest relationship into database after block/unblock action issue --- .../Service/APIService/APIService+Block.swift | 33 +++++++++++++++++-- .../APIService/APIService+Follow.swift | 6 ++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/Mastodon/Service/APIService/APIService+Block.swift b/Mastodon/Service/APIService/APIService+Block.swift index 124b65155..c28db9da9 100644 --- a/Mastodon/Service/APIService/APIService+Block.swift +++ b/Mastodon/Service/APIService/APIService+Block.swift @@ -136,7 +136,8 @@ extension APIService { ) -> AnyPublisher, Error> { let domain = mastodonAuthenticationBox.domain let authorization = mastodonAuthenticationBox.userAuthorization - + let requestMastodonUserID = mastodonAuthenticationBox.userID + return Mastodon.API.Account.block( session: session, domain: domain, @@ -144,11 +145,39 @@ extension APIService { blockQueryType: blockQueryType, authorization: authorization ) + .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() + } .handleEvents(receiveCompletion: { [weak self] completion in guard let _ = self else { return } switch completion { case .failure(let error): - // TODO: handle error + // TODO: handle error in banner os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] block update fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) case .finished: diff --git a/Mastodon/Service/APIService/APIService+Follow.swift b/Mastodon/Service/APIService/APIService+Follow.swift index 53634ab4b..f527878f4 100644 --- a/Mastodon/Service/APIService/APIService+Follow.swift +++ b/Mastodon/Service/APIService/APIService+Follow.swift @@ -196,11 +196,11 @@ extension APIService { let lookUpMastodonUserRequest = MastodonUser.sortedFetchRequest lookUpMastodonUserRequest.predicate = MastodonUser.predicate(domain: domain, id: mastodonUserID) lookUpMastodonUserRequest.fetchLimit = 1 - let lookUpMastodonuser = managedObjectContext.safeFetch(lookUpMastodonUserRequest).first + let lookUpMastodonUser = managedObjectContext.safeFetch(lookUpMastodonUserRequest).first - if let lookUpMastodonuser = lookUpMastodonuser { + if let lookUpMastodonUser = lookUpMastodonUser { let entity = response.value - APIService.CoreData.update(user: lookUpMastodonuser, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: response.networkDate) + APIService.CoreData.update(user: lookUpMastodonUser, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: response.networkDate) } } .tryMap { result -> Mastodon.Response.Content in From 48c7ffe20ea7cfadbc88e2ed052b9f48bf7c4219 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 24 Jun 2021 15:14:50 +0800 Subject: [PATCH 3/6] fix: paging still enabled issue when blocking/blocked in profile scene --- Mastodon/Scene/Profile/ProfileViewController.swift | 10 +++++++++- Mastodon/Scene/Profile/ProfileViewModel.swift | 11 ++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 8ba7851cd..57b677a71 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -452,7 +452,7 @@ extension ProfileViewController { viewModel.isEditing .handleEvents(receiveOutput: { [weak self] isEditing in guard let self = self else { return } - // set firset responder for key command + // set first responder for key command if !isEditing { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.profileSegmentedViewController.pagingViewController.becomeFirstResponder() @@ -520,6 +520,14 @@ extension ProfileViewController { self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.accessibilityLabel = L10n.Scene.Profile.Dashboard.Accessibility.countFollowers(count ?? 0) } .store(in: &disposeBag) + viewModel.needsPaingEnabled + .receive(on: RunLoop.main) + .sink { [weak self] needsPaingEnabled in + guard let self = self else { return } + self.profileSegmentedViewController.pagingViewController.isScrollEnabled = needsPaingEnabled + } + .store(in: &disposeBag) + profileHeaderViewController.profileHeaderView.delegate = self } diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 619d55ba3..2d4bc429c 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -58,8 +58,9 @@ class ProfileViewModel: NSObject { let isReplyBarButtonItemHidden = CurrentValueSubject(true) let isMoreMenuBarButtonItemHidden = CurrentValueSubject(true) let isMeBarButtonItemsHidden = CurrentValueSubject(true) - + let needsPagePinToTop = CurrentValueSubject(false) + let needsPaingEnabled = CurrentValueSubject(true) init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) { self.context = context @@ -147,6 +148,14 @@ class ProfileViewModel: NSObject { } .store(in: &disposeBag) + Publishers.CombineLatest( + isBlocking, + isBlockedBy + ) + .map { !($0 || $1) } + .assign(to: \.value, on: needsPaingEnabled) + .store(in: &disposeBag) + setup() } From 36fdef15df4895f68e3816d6f0d8449c0eb89f16 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 24 Jun 2021 16:49:34 +0800 Subject: [PATCH 4/6] fix: wrong i18n string symbol issue --- Localization/app.json | 2 +- Mastodon/Generated/Strings.swift | 2 +- Mastodon/Resources/ar.lproj/Localizable.strings | 2 +- Mastodon/Resources/en.lproj/Localizable.strings | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 67694109f..269bea1ea 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -184,7 +184,7 @@ "header": { "no_status_found": "No Status Found", "blocking_warning": "You can’t view this profile\n until you unblock them.\nYour account looks like this to them.", - "user_blocking_warning": "You can’t view %s's profile\n until you unblock them.\nYour account looks like this to them.", + "user_blocking_warning": "You can’t view %s’s profile\n until you unblock them.\nYour account looks like this to them.", "blocked_warning": "You can’t view this’s profile\n until they unblock you.", "user_blocked_warning": "You can’t view %s’s profile\n until they unblock you.", "suspended_warning": "This account has been suspended.", diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index b02535cca..93ad37af0 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -367,7 +367,7 @@ internal enum L10n { internal static func userBlockedWarning(_ p1: Any) -> String { return L10n.tr("Localizable", "Common.Controls.Timeline.Header.UserBlockedWarning", String(describing: p1)) } - /// You can’t view %@'s profile\n until you unblock them.\nYour account looks like this to them. + /// You can’t view %@’s profile\n until you unblock them.\nYour account looks like this to them. internal static func userBlockingWarning(_ p1: Any) -> String { return L10n.tr("Localizable", "Common.Controls.Timeline.Header.UserBlockingWarning", String(describing: p1)) } diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index d8876a0e5..bc7a3723f 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -129,7 +129,7 @@ Your account looks like this to them."; "Common.Controls.Timeline.Header.SuspendedWarning" = "This account has been suspended."; "Common.Controls.Timeline.Header.UserBlockedWarning" = "You can’t view %@’s profile until they unblock you."; -"Common.Controls.Timeline.Header.UserBlockingWarning" = "You can’t view %@'s profile +"Common.Controls.Timeline.Header.UserBlockingWarning" = "You can’t view %@’s profile until you unblock them. Your account looks like this to them."; "Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@’s account has been suspended."; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index d8876a0e5..bc7a3723f 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -129,7 +129,7 @@ Your account looks like this to them."; "Common.Controls.Timeline.Header.SuspendedWarning" = "This account has been suspended."; "Common.Controls.Timeline.Header.UserBlockedWarning" = "You can’t view %@’s profile until they unblock you."; -"Common.Controls.Timeline.Header.UserBlockingWarning" = "You can’t view %@'s profile +"Common.Controls.Timeline.Header.UserBlockingWarning" = "You can’t view %@’s profile until you unblock them. Your account looks like this to them."; "Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@’s account has been suspended."; From ab07860e5ae00d87805bf2e57c133fbbec9e1305 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 24 Jun 2021 16:50:20 +0800 Subject: [PATCH 5/6] fix: profile image overlay not blur when blocking/blocked issue --- .../Header/ProfileHeaderViewController.swift | 4 +-- .../Header/View/ProfileHeaderView.swift | 36 +++++++++++++------ .../Scene/Profile/ProfileViewController.swift | 13 ++++++- Mastodon/Scene/Profile/ProfileViewModel.swift | 18 +++++++--- 4 files changed, 54 insertions(+), 17 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 2088df72f..ff408d54e 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -371,7 +371,7 @@ extension ProfileHeaderViewController { } else if bannerContainerBottomOffset < containerSafeAreaInset.top { // 3 // banner bottom pin to navigation bar bottom and - // the `progress` growth to 1 then segemented control pin to top + // the `progress` growth to 1 then segmented control pin to top bannerImageView.frame.origin.y = -containerSafeAreaInset.top let bannerImageHeight = bannerContainerInWindow.size.height + containerSafeAreaInset.top + (containerSafeAreaInset.top - bannerContainerBottomOffset) bannerImageView.frame.size.height = bannerImageHeight @@ -406,7 +406,7 @@ extension ProfileHeaderViewController { setProfileBannerFade(alpha: 1) } } - + private func setProfileBannerFade(alpha: CGFloat) { profileHeaderView.avatarImageViewBackgroundView.alpha = alpha profileHeaderView.avatarImageView.alpha = alpha diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index 2b938bac9..f9f2e98d4 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -50,8 +50,12 @@ final class ProfileHeaderView: UIView { imageView.accessibilityIgnoresInvertColors = true return imageView }() - let bannerImageViewOverlayView: UIView = { - let overlayView = UIView() + + // known issue: + // in iOS 14 blur maybe disappear when banner image moving and scaling + static let bannerImageViewOverlayBlurEffect = UIBlurEffect(style: .systemMaterialDark) + let bannerImageViewOverlayVisualEffectView: UIVisualEffectView = { + let overlayView = UIVisualEffectView(effect: nil) overlayView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor return overlayView }() @@ -79,6 +83,9 @@ final class ProfileHeaderView: UIView { editAvatarBackgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.6) editAvatarButton.tintColor = .white } + + static let avatarImageViewOverlayBlurEffect = UIBlurEffect(style: .systemUltraThinMaterialDark) + let avatarImageViewOverlayVisualEffectView = UIVisualEffectView(effect: nil) let editAvatarBackgroundView: UIView = { let view = UIView() @@ -226,13 +233,13 @@ extension ProfileHeaderView { bannerImageView.frame = bannerContainerView.bounds bannerContainerView.addSubview(bannerImageView) - bannerImageViewOverlayView.translatesAutoresizingMaskIntoConstraints = false - bannerImageView.addSubview(bannerImageViewOverlayView) + bannerImageViewOverlayVisualEffectView.translatesAutoresizingMaskIntoConstraints = false + bannerImageView.addSubview(bannerImageViewOverlayVisualEffectView) NSLayoutConstraint.activate([ - bannerImageViewOverlayView.topAnchor.constraint(equalTo: bannerImageView.topAnchor), - bannerImageViewOverlayView.leadingAnchor.constraint(equalTo: bannerImageView.leadingAnchor), - bannerImageViewOverlayView.trailingAnchor.constraint(equalTo: bannerImageView.trailingAnchor), - bannerImageViewOverlayView.bottomAnchor.constraint(equalTo: bannerImageView.bottomAnchor), + bannerImageViewOverlayVisualEffectView.topAnchor.constraint(equalTo: bannerImageView.topAnchor), + bannerImageViewOverlayVisualEffectView.leadingAnchor.constraint(equalTo: bannerImageView.leadingAnchor), + bannerImageViewOverlayVisualEffectView.trailingAnchor.constraint(equalTo: bannerImageView.trailingAnchor), + bannerImageViewOverlayVisualEffectView.bottomAnchor.constraint(equalTo: bannerImageView.bottomAnchor), ]) // avatar @@ -253,6 +260,15 @@ extension ProfileHeaderView { avatarImageView.widthAnchor.constraint(equalToConstant: ProfileHeaderView.avatarImageViewSize.width).priority(.required - 1), avatarImageView.heightAnchor.constraint(equalToConstant: ProfileHeaderView.avatarImageViewSize.height).priority(.required - 1), ]) + + avatarImageViewOverlayVisualEffectView.translatesAutoresizingMaskIntoConstraints = false + avatarImageViewBackgroundView.addSubview(avatarImageViewOverlayVisualEffectView) + NSLayoutConstraint.activate([ + avatarImageViewOverlayVisualEffectView.topAnchor.constraint(equalTo: avatarImageViewBackgroundView.topAnchor), + avatarImageViewOverlayVisualEffectView.leadingAnchor.constraint(equalTo: avatarImageViewBackgroundView.leadingAnchor), + avatarImageViewOverlayVisualEffectView.trailingAnchor.constraint(equalTo: avatarImageViewBackgroundView.trailingAnchor), + avatarImageViewOverlayVisualEffectView.bottomAnchor.constraint(equalTo: avatarImageViewBackgroundView.bottomAnchor), + ]) editAvatarBackgroundView.translatesAutoresizingMaskIntoConstraints = false avatarImageView.addSubview(editAvatarBackgroundView) @@ -425,7 +441,7 @@ extension ProfileHeaderView { bioTextEditorView.isHidden = true animator.addAnimations { - self.bannerImageViewOverlayView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor + self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor self.nameTextFieldBackgroundView.backgroundColor = .clear self.editAvatarBackgroundView.alpha = 0 } @@ -441,7 +457,7 @@ extension ProfileHeaderView { editAvatarBackgroundView.alpha = 0 bioTextEditorView.backgroundColor = .clear animator.addAnimations { - self.bannerImageViewOverlayView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundEditingColor + self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundEditingColor self.nameTextFieldBackgroundView.backgroundColor = Asset.Scene.Profile.Banner.nameEditBackgroundGray.color self.editAvatarBackgroundView.alpha = 1 self.bioTextEditorView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 57b677a71..fd14f9830 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -527,7 +527,18 @@ extension ProfileViewController { self.profileSegmentedViewController.pagingViewController.isScrollEnabled = needsPaingEnabled } .store(in: &disposeBag) - + viewModel.needsImageOverlayBlurred + .receive(on: RunLoop.main) + .sink { [weak self] needsImageOverlayBlurred in + guard let self = self else { return } + UIView.animate(withDuration: 0.33) { + let bannerEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.bannerImageViewOverlayBlurEffect : nil + self.profileHeaderViewController.profileHeaderView.bannerImageViewOverlayVisualEffectView.effect = bannerEffect + let avatarEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.avatarImageViewOverlayBlurEffect : nil + self.profileHeaderViewController.profileHeaderView.avatarImageViewOverlayVisualEffectView.effect = avatarEffect + } + } + .store(in: &disposeBag) profileHeaderViewController.profileHeaderView.delegate = self } diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 2d4bc429c..223a12215 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -61,6 +61,7 @@ class ProfileViewModel: NSObject { let needsPagePinToTop = CurrentValueSubject(false) let needsPaingEnabled = CurrentValueSubject(true) + let needsImageOverlayBlurred = CurrentValueSubject(false) init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) { self.context = context @@ -148,13 +149,22 @@ class ProfileViewModel: NSObject { } .store(in: &disposeBag) - Publishers.CombineLatest( + let isBlockingOrBlocked = Publishers.CombineLatest( isBlocking, isBlockedBy ) - .map { !($0 || $1) } - .assign(to: \.value, on: needsPaingEnabled) - .store(in: &disposeBag) + .map { $0 || $1 } + .share() + + isBlockingOrBlocked + .map { !$0 } + .assign(to: \.value, on: needsPaingEnabled) + .store(in: &disposeBag) + + isBlockingOrBlocked + .map { $0 } + .assign(to: \.value, on: needsImageOverlayBlurred) + .store(in: &disposeBag) setup() } From 747becda79c96528231b10bbfb3ae9e205799925 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 24 Jun 2021 19:20:41 +0800 Subject: [PATCH 6/6] fix: profile fields edit using wrong material issue --- Localization/app.json | 4 ++ .../xcschemes/xcschememanagement.plist | 4 +- .../Diffiable/Item/ProfileFieldItem.swift | 8 +-- Mastodon/Extension/ActiveLabel.swift | 6 +- Mastodon/Generated/Strings.swift | 6 ++ Mastodon/Helper/MastodonField.swift | 3 +- .../Resources/ar.lproj/Localizable.strings | 2 + .../Resources/en.lproj/Localizable.strings | 2 + .../Header/ProfileHeaderViewController.swift | 1 + .../Header/ProfileHeaderViewModel.swift | 44 ++++++++------ .../Scene/Profile/ProfileViewController.swift | 59 +++++++++++++++++-- Mastodon/Scene/Profile/ProfileViewModel.swift | 19 ++++++ Mastodon/Service/AuthenticationService.swift | 28 ++++----- 13 files changed, 135 insertions(+), 51 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 269bea1ea..114461675 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -27,6 +27,10 @@ "more_than_one_video": "Cannot attach more than one video." } }, + "edit_profile_failure": { + "title": "Edit Profile Error", + "message": "Cannot edit profile. Please try again." + }, "sign_out": { "title": "Sign out", "message": "Are you sure you want to sign out?", diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index da1d80e90..f1135b12e 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 21 + 20 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -37,7 +37,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 20 + 21 SuppressBuildableAutocreation diff --git a/Mastodon/Diffiable/Item/ProfileFieldItem.swift b/Mastodon/Diffiable/Item/ProfileFieldItem.swift index 684bd7d42..4c6b37da8 100644 --- a/Mastodon/Diffiable/Item/ProfileFieldItem.swift +++ b/Mastodon/Diffiable/Item/ProfileFieldItem.swift @@ -32,20 +32,16 @@ extension ProfileFieldItem { extension ProfileFieldItem { struct FieldValue: Equatable, Hashable { let id: UUID - + var name: CurrentValueSubject var value: CurrentValueSubject - + init(id: UUID = UUID(), name: String, value: String) { self.id = id self.name = CurrentValueSubject(name) self.value = CurrentValueSubject(value) } - func duplicate() -> FieldValue { - FieldValue(name: name.value, value: value.value) - } - static func == (lhs: ProfileFieldItem.FieldValue, rhs: ProfileFieldItem.FieldValue) -> Bool { return lhs.id == rhs.id && lhs.name.value == rhs.name.value diff --git a/Mastodon/Extension/ActiveLabel.swift b/Mastodon/Extension/ActiveLabel.swift index a9b7880f9..b4f89e71e 100644 --- a/Mastodon/Extension/ActiveLabel.swift +++ b/Mastodon/Extension/ActiveLabel.swift @@ -91,11 +91,7 @@ extension ActiveLabel { extension ActiveLabel { /// account field func configure(field: String, emojiDict: MastodonStatusContent.EmojiDict) { - activeEntities.removeAll() - let parseResult = MastodonField.parse(field: field, emojiDict: emojiDict) - text = parseResult.trimmed - activeEntities = parseResult.activeEntities - accessibilityLabel = parseResult.value + configure(content: field, emojiDict: emojiDict) } } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 93ad37af0..caa5bcce5 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -47,6 +47,12 @@ internal enum L10n { /// Discard Publish internal static let title = L10n.tr("Localizable", "Common.Alerts.DiscardPostContent.Title") } + internal enum EditProfileFailure { + /// Cannot edit profile. Please try again. + internal static let message = L10n.tr("Localizable", "Common.Alerts.EditProfileFailure.Message") + /// Edit Profile Error + internal static let title = L10n.tr("Localizable", "Common.Alerts.EditProfileFailure.Title") + } internal enum PublishPostFailure { /// Failed to publish the post.\nPlease check your internet connection. internal static let message = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Message") diff --git a/Mastodon/Helper/MastodonField.swift b/Mastodon/Helper/MastodonField.swift index 12b03c91f..437b0924b 100644 --- a/Mastodon/Helper/MastodonField.swift +++ b/Mastodon/Helper/MastodonField.swift @@ -9,7 +9,8 @@ import Foundation import ActiveLabel enum MastodonField { - + + @available(*, deprecated, message: "rely on server meta rendering") static func parse(field string: String, emojiDict: MastodonStatusContent.EmojiDict) -> ParseResult { // use content parser get emoji entities let value = string diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index bc7a3723f..71e475106 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -8,6 +8,8 @@ "Common.Alerts.DeletePost.Title" = "Are you sure you want to delete this post?"; "Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content."; "Common.Alerts.DiscardPostContent.Title" = "Discard Publish"; +"Common.Alerts.EditProfileFailure.Message" = "Cannot edit profile. Please try again."; +"Common.Alerts.EditProfileFailure.Title" = "Edit Profile Error"; "Common.Alerts.PublishPostFailure.AttchmentsMessage.MoreThanOneVideo" = "Cannot attach more than one video."; "Common.Alerts.PublishPostFailure.AttchmentsMessage.VideoAttachWithPhoto" = "Cannot attach a video to a status that already contains images."; "Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post. diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index bc7a3723f..71e475106 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -8,6 +8,8 @@ "Common.Alerts.DeletePost.Title" = "Are you sure you want to delete this post?"; "Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content."; "Common.Alerts.DiscardPostContent.Title" = "Discard Publish"; +"Common.Alerts.EditProfileFailure.Message" = "Cannot edit profile. Please try again."; +"Common.Alerts.EditProfileFailure.Title" = "Edit Profile Error"; "Common.Alerts.PublishPostFailure.AttchmentsMessage.MoreThanOneVideo" = "Cannot attach more than one video."; "Common.Alerts.PublishPostFailure.AttchmentsMessage.VideoAttachWithPhoto" = "Cannot attach a video to a status that already contains images."; "Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post. diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index ff408d54e..48470a241 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -501,6 +501,7 @@ extension ProfileHeaderViewController: UICollectionViewDelegate { // MARK: - ProfileFieldCollectionViewCellDelegate extension ProfileHeaderViewController: ProfileFieldCollectionViewCellDelegate { + // should be remove style edit button func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, editButtonDidPressed button: UIButton) { guard let diffableDataSource = viewModel.fieldDiffableDataSource else { return } guard let indexPath = profileHeaderView.fieldCollectionView.indexPath(for: cell) else { return } diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift index 0c728788d..453aa2e43 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift @@ -24,6 +24,7 @@ final class ProfileHeaderViewModel { let needsSetupBottomShadow = CurrentValueSubject(true) let isTitleViewContentOffsetSet = CurrentValueSubject(false) let emojiDict = CurrentValueSubject([:]) + let accountForEdit = CurrentValueSubject(nil) // output let displayProfileInfo = ProfileInfo() @@ -33,19 +34,24 @@ final class ProfileHeaderViewModel { init(context: AppContext) { self.context = context - - isEditing - .removeDuplicates() // only triiger when value toggle - .receive(on: DispatchQueue.main) - .sink { [weak self] isEditing in - guard let self = self else { return } - // setup editing value when toggle to editing - self.editProfileInfo.name.value = self.displayProfileInfo.name.value // set to name - self.editProfileInfo.avatarImageResource.value = .image(nil) // set to empty - self.editProfileInfo.note.value = ProfileHeaderViewModel.normalize(note: self.displayProfileInfo.note.value) - self.editProfileInfo.fields.value = self.displayProfileInfo.fields.value.map { $0.duplicate() } // set to fields - } - .store(in: &disposeBag) + + Publishers.CombineLatest( + isEditing.removeDuplicates(), // only trigger when value toggle + accountForEdit + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] isEditing, account in + guard let self = self else { return } + guard isEditing else { return } + // setup editing value when toggle to editing + self.editProfileInfo.name.value = self.displayProfileInfo.name.value // set to name + self.editProfileInfo.avatarImageResource.value = .image(nil) // set to empty + self.editProfileInfo.note.value = ProfileHeaderViewModel.normalize(note: self.displayProfileInfo.note.value) + self.editProfileInfo.fields.value = account?.source?.fields?.compactMap { field in + ProfileFieldItem.FieldValue(name: field.name, value: field.value) + } ?? [] + } + .store(in: &disposeBag) Publishers.CombineLatest4( isEditing.removeDuplicates(), @@ -152,12 +158,14 @@ extension ProfileHeaderViewModel { guard case let .image(image) = editProfileInfo.avatarImageResource.value, image == nil else { return true } guard editProfileInfo.note.value == ProfileHeaderViewModel.normalize(note: displayProfileInfo.note.value) else { return true } let isFieldsEqual: Bool = { + let originalFields = self.accountForEdit.value?.source?.fields?.compactMap { field in + ProfileFieldItem.FieldValue(name: field.name, value: field.value) + } ?? [] let editFields = editProfileInfo.fields.value - let displayFields = displayProfileInfo.fields.value - guard editFields.count == displayFields.count else { return false } - for (editField, displayField) in zip(editFields, displayFields) { - guard editField.name.value == displayField.name.value, - editField.value.value == displayField.value.value else { + guard editFields.count == originalFields.count else { return false } + for (editField, originalField) in zip(editFields, originalFields) { + guard editField.name.value == originalField.name.value, + editField.value.value == originalField.value.value else { return false } } diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index fd14f9830..476d03a9d 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -376,6 +376,9 @@ extension ProfileViewController { .receive(on: DispatchQueue.main) .assign(to: \.value, on: profileHeaderViewController.viewModel.displayProfileInfo.fields) .store(in: &disposeBag) + viewModel.accountForEdit + .assign(to: \.value, on: profileHeaderViewController.viewModel.accountForEdit) + .store(in: &disposeBag) viewModel.emojiDict .receive(on: DispatchQueue.main) .assign(to: \.value, on: profileHeaderViewController.viewModel.emojiDict) @@ -443,7 +446,7 @@ extension ProfileViewController { if relationshipActionSet.contains(.edit) { // check .edit state and set .editing when isEditing friendshipButton.configure(actionOptionSet: isUpdating ? .updating : (isEditing ? .editing : .edit)) - self.profileHeaderViewController.profileHeaderView.configure(state: isUpdating || isEditing ? .editing : .normal) + self.profileHeaderViewController.profileHeaderView.configure(state: isEditing ? .editing : .normal) } else { friendshipButton.configure(actionOptionSet: relationshipActionSet) } @@ -695,8 +698,9 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { animated: true ) } - + func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) { + // handle profile fields interaction switch entity.type { case .url(_, _, let url, _): guard let url = URL(string: url) else { return } @@ -704,6 +708,13 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { case .hashtag(let hashtag, _): let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag) coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show) + case .mention(_, let userInfo): + guard let href = userInfo?["href"] as? String else { + // currently we cannot present profile scene without userID + return + } + guard let url = URL(string: href) else { return } + coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) default: break } @@ -803,29 +814,67 @@ extension ProfileViewController: ProfileHeaderViewDelegate { func profileHeaderView(_ profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton) { let relationshipActionSet = viewModel.relationshipActionOptionSet.value + + // handle edit logic for editable profile + // handle relationship logic for non-editable profile if relationshipActionSet.contains(.edit) { + // do nothing when updating guard !viewModel.isUpdating.value else { return } - + if profileHeaderViewController.viewModel.isProfileInfoEdited() { + // update profile if changed viewModel.isUpdating.value = true profileHeaderViewController.viewModel.updateProfileInfo() .receive(on: DispatchQueue.main) .sink { [weak self] completion in guard let self = self else { return } + defer { + // finish updating + self.viewModel.isUpdating.value = false + } switch completion { case .failure(let error): os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update profile info fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) case .finished: os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update profile info success", ((#file as NSString).lastPathComponent), #line, #function) } - self.viewModel.isUpdating.value = false } receiveValue: { [weak self] _ in guard let self = self else { return } self.viewModel.isEditing.value = false } .store(in: &disposeBag) } else { - viewModel.isEditing.value.toggle() + // set `updating` then toggle `edit` state + viewModel.isUpdating.value = true + viewModel.fetchEditProfileInfo() + .receive(on: RunLoop.main) + .sink { [weak self] completion in + guard let self = self else { return } + defer { + // finish updating + self.viewModel.isUpdating.value = false + } + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch profile info for edit fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + let alertController = UIAlertController(for: error, title: L10n.Common.Alerts.EditProfileFailure.title, preferredStyle: .alert) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) + alertController.addAction(okAction) + self.coordinator.present( + scene: .alertController(alertController: alertController), + from: nil, + transition: .alertController(animated: true, completion: nil) + ) + case .finished: + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch profile info for edit success", ((#file as NSString).lastPathComponent), #line, #function) + // enter editing mode + self.viewModel.isEditing.value.toggle() + } + } receiveValue: { [weak self] response in + guard let self = self else { return } + self.viewModel.accountForEdit.value = response.value + } + .store(in: &disposeBag) } } else { guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return } diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 223a12215..ddd1ee291 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -42,6 +42,9 @@ class ProfileViewModel: NSObject { let fileds: CurrentValueSubject<[Mastodon.Entity.Field], Never> let emojiDict: CurrentValueSubject + // fulfill this before editing + let accountForEdit = CurrentValueSubject(nil) + let protected: CurrentValueSubject let suspended: CurrentValueSubject @@ -341,6 +344,22 @@ extension ProfileViewModel { } +extension ProfileViewModel { + + // fetch profile info before edit + func fetchEditProfileInfo() -> AnyPublisher, Error> { + guard let currentMastodonUser = currentMastodonUser.value, + let mastodonAuthentication = currentMastodonUser.mastodonAuthentication else { + return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher() + } + + let authorization = Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken) + return context.apiService.accountVerifyCredentials(domain: currentMastodonUser.domain, authorization: authorization) +// .erro + } + +} + extension ProfileViewModel { enum RelationshipAction: Int, CaseIterable { diff --git a/Mastodon/Service/AuthenticationService.swift b/Mastodon/Service/AuthenticationService.swift index 6b35486d6..a0bbca57a 100644 --- a/Mastodon/Service/AuthenticationService.swift +++ b/Mastodon/Service/AuthenticationService.swift @@ -103,20 +103,20 @@ extension AuthenticationService { extension AuthenticationService { func activeMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher, Never> { - var isActived = false + var isActive = false return backgroundManagedObjectContext.performChanges { let request = MastodonAuthentication.sortedFetchRequest request.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID) request.fetchLimit = 1 - guard let mastodonAutentication = try? self.backgroundManagedObjectContext.fetch(request).first else { + guard let mastodonAuthentication = try? self.backgroundManagedObjectContext.fetch(request).first else { return } - mastodonAutentication.update(activedAt: Date()) - isActived = true + mastodonAuthentication.update(activedAt: Date()) + isActive = true } .map { result in - return result.map { isActived } + return result.map { isActive } } .eraseToAnyPublisher() } @@ -124,27 +124,27 @@ extension AuthenticationService { func signOutMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher, Never> { var isSignOut = false - var _mastodonAutenticationBox: MastodonAuthenticationBox? + var _mastodonAuthenticationBox: MastodonAuthenticationBox? let managedObjectContext = backgroundManagedObjectContext return managedObjectContext.performChanges { let request = MastodonAuthentication.sortedFetchRequest request.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID) request.fetchLimit = 1 - guard let mastodonAutentication = try? managedObjectContext.fetch(request).first else { + guard let mastodonAuthentication = try? managedObjectContext.fetch(request).first else { return } - _mastodonAutenticationBox = AuthenticationService.MastodonAuthenticationBox( - domain: mastodonAutentication.domain, - userID: mastodonAutentication.userID, - appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAutentication.appAccessToken), - userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAutentication.userAccessToken) + _mastodonAuthenticationBox = AuthenticationService.MastodonAuthenticationBox( + domain: mastodonAuthentication.domain, + userID: mastodonAuthentication.userID, + appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.appAccessToken), + userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken) ) - managedObjectContext.delete(mastodonAutentication) + managedObjectContext.delete(mastodonAuthentication) isSignOut = true } .flatMap { result -> AnyPublisher, Never> in guard let apiService = self.apiService, - let mastodonAuthenticationBox = _mastodonAutenticationBox else { + let mastodonAuthenticationBox = _mastodonAuthenticationBox else { return Just(result).eraseToAnyPublisher() }