mirror of
https://github.com/mastodon/mastodon-ios.git
synced 2025-01-27 07:46:15 +01:00
Merge pull request #181 from tootsuite/fix/profile
Fix UI/UX issue for profile scene
This commit is contained in:
commit
6da2d87cf6
@ -27,6 +27,10 @@
|
|||||||
"more_than_one_video": "Cannot attach more than one video."
|
"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": {
|
"sign_out": {
|
||||||
"title": "Sign out",
|
"title": "Sign out",
|
||||||
"message": "Are you sure you want to sign out?",
|
"message": "Are you sure you want to sign out?",
|
||||||
@ -183,8 +187,10 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"no_status_found": "No Status Found",
|
"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.",
|
"blocking_warning": "You can’t view this 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.",
|
"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.",
|
"suspended_warning": "This account has been suspended.",
|
||||||
"user_suspended_warning": "%s’s account has been suspended."
|
"user_suspended_warning": "%s’s account has been suspended."
|
||||||
},
|
},
|
||||||
|
@ -62,15 +62,15 @@ extension Item {
|
|||||||
|
|
||||||
enum Reason: Equatable {
|
enum Reason: Equatable {
|
||||||
case noStatusFound
|
case noStatusFound
|
||||||
case blocking
|
case blocking(name: String?)
|
||||||
case blocked
|
case blocked(name: String?)
|
||||||
case suspended(name: String?)
|
case suspended(name: String?)
|
||||||
|
|
||||||
static func == (lhs: Item.EmptyStateHeaderAttribute.Reason, rhs: Item.EmptyStateHeaderAttribute.Reason) -> Bool {
|
static func == (lhs: Item.EmptyStateHeaderAttribute.Reason, rhs: Item.EmptyStateHeaderAttribute.Reason) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case (.noStatusFound, noStatusFound): return true
|
case (.noStatusFound, noStatusFound): return true
|
||||||
case (.blocking, blocking): return true
|
case (.blocking(let nameLeft), blocking(let nameRight)): return nameLeft == nameRight
|
||||||
case (.blocked, blocked): return true
|
case (.blocked(let nameLeft), blocked(let nameRight)): return nameLeft == nameRight
|
||||||
case (.suspended(let nameLeft), .suspended(let nameRight)): return nameLeft == nameRight
|
case (.suspended(let nameLeft), .suspended(let nameRight)): return nameLeft == nameRight
|
||||||
default: return false
|
default: return false
|
||||||
}
|
}
|
||||||
|
@ -32,20 +32,16 @@ extension ProfileFieldItem {
|
|||||||
extension ProfileFieldItem {
|
extension ProfileFieldItem {
|
||||||
struct FieldValue: Equatable, Hashable {
|
struct FieldValue: Equatable, Hashable {
|
||||||
let id: UUID
|
let id: UUID
|
||||||
|
|
||||||
var name: CurrentValueSubject<String, Never>
|
var name: CurrentValueSubject<String, Never>
|
||||||
var value: CurrentValueSubject<String, Never>
|
var value: CurrentValueSubject<String, Never>
|
||||||
|
|
||||||
init(id: UUID = UUID(), name: String, value: String) {
|
init(id: UUID = UUID(), name: String, value: String) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.name = CurrentValueSubject(name)
|
self.name = CurrentValueSubject(name)
|
||||||
self.value = CurrentValueSubject(value)
|
self.value = CurrentValueSubject(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func duplicate() -> FieldValue {
|
|
||||||
FieldValue(name: name.value, value: value.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func == (lhs: ProfileFieldItem.FieldValue, rhs: ProfileFieldItem.FieldValue) -> Bool {
|
static func == (lhs: ProfileFieldItem.FieldValue, rhs: ProfileFieldItem.FieldValue) -> Bool {
|
||||||
return lhs.id == rhs.id
|
return lhs.id == rhs.id
|
||||||
&& lhs.name.value == rhs.name.value
|
&& lhs.name.value == rhs.name.value
|
||||||
|
@ -91,11 +91,7 @@ extension ActiveLabel {
|
|||||||
extension ActiveLabel {
|
extension ActiveLabel {
|
||||||
/// account field
|
/// account field
|
||||||
func configure(field: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
func configure(field: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||||
activeEntities.removeAll()
|
configure(content: field, emojiDict: emojiDict)
|
||||||
let parseResult = MastodonField.parse(field: field, emojiDict: emojiDict)
|
|
||||||
text = parseResult.trimmed
|
|
||||||
activeEntities = parseResult.activeEntities
|
|
||||||
accessibilityLabel = parseResult.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +47,12 @@ internal enum L10n {
|
|||||||
/// Discard Publish
|
/// Discard Publish
|
||||||
internal static let title = L10n.tr("Localizable", "Common.Alerts.DiscardPostContent.Title")
|
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 {
|
internal enum PublishPostFailure {
|
||||||
/// Failed to publish the post.\nPlease check your internet connection.
|
/// Failed to publish the post.\nPlease check your internet connection.
|
||||||
internal static let message = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Message")
|
internal static let message = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Message")
|
||||||
@ -355,14 +361,22 @@ internal enum L10n {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
internal enum Header {
|
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")
|
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")
|
internal static let blockingWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.BlockingWarning")
|
||||||
/// No Status Found
|
/// No Status Found
|
||||||
internal static let noStatusFound = L10n.tr("Localizable", "Common.Controls.Timeline.Header.NoStatusFound")
|
internal static let noStatusFound = L10n.tr("Localizable", "Common.Controls.Timeline.Header.NoStatusFound")
|
||||||
/// This account has been suspended.
|
/// This account has been suspended.
|
||||||
internal static let suspendedWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.SuspendedWarning")
|
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.
|
/// %@’s account has been suspended.
|
||||||
internal static func userSuspendedWarning(_ p1: Any) -> String {
|
internal static func userSuspendedWarning(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "Common.Controls.Timeline.Header.UserSuspendedWarning", String(describing: p1))
|
return L10n.tr("Localizable", "Common.Controls.Timeline.Header.UserSuspendedWarning", String(describing: p1))
|
||||||
|
@ -9,7 +9,8 @@ import Foundation
|
|||||||
import ActiveLabel
|
import ActiveLabel
|
||||||
|
|
||||||
enum MastodonField {
|
enum MastodonField {
|
||||||
|
|
||||||
|
@available(*, deprecated, message: "rely on server meta rendering")
|
||||||
static func parse(field string: String, emojiDict: MastodonStatusContent.EmojiDict) -> ParseResult {
|
static func parse(field string: String, emojiDict: MastodonStatusContent.EmojiDict) -> ParseResult {
|
||||||
// use content parser get emoji entities
|
// use content parser get emoji entities
|
||||||
let value = string
|
let value = string
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
"Common.Alerts.DeletePost.Title" = "Are you sure you want to delete this post?";
|
"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.Message" = "Confirm discard composed post content.";
|
||||||
"Common.Alerts.DiscardPostContent.Title" = "Discard Publish";
|
"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.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.AttchmentsMessage.VideoAttachWithPhoto" = "Cannot attach a video to a status that already contains images.";
|
||||||
"Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post.
|
"Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post.
|
||||||
@ -120,13 +122,18 @@ Please check your internet connection.";
|
|||||||
"Common.Controls.Timeline.Accessibility.CountFavorites" = "%@ favorites";
|
"Common.Controls.Timeline.Accessibility.CountFavorites" = "%@ favorites";
|
||||||
"Common.Controls.Timeline.Accessibility.CountReblogs" = "%@ reblogs";
|
"Common.Controls.Timeline.Accessibility.CountReblogs" = "%@ reblogs";
|
||||||
"Common.Controls.Timeline.Accessibility.CountReplies" = "%@ replies";
|
"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.";
|
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.
|
until you unblock them.
|
||||||
Your account looks like this to them.";
|
Your account looks like this to them.";
|
||||||
"Common.Controls.Timeline.Header.NoStatusFound" = "No Status Found";
|
"Common.Controls.Timeline.Header.NoStatusFound" = "No Status Found";
|
||||||
"Common.Controls.Timeline.Header.SuspendedWarning" = "This account has been suspended.";
|
"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.Header.UserSuspendedWarning" = "%@’s account has been suspended.";
|
||||||
"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts";
|
"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts";
|
||||||
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
|
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
"Common.Alerts.DeletePost.Title" = "Are you sure you want to delete this post?";
|
"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.Message" = "Confirm discard composed post content.";
|
||||||
"Common.Alerts.DiscardPostContent.Title" = "Discard Publish";
|
"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.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.AttchmentsMessage.VideoAttachWithPhoto" = "Cannot attach a video to a status that already contains images.";
|
||||||
"Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post.
|
"Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post.
|
||||||
@ -120,13 +122,18 @@ Please check your internet connection.";
|
|||||||
"Common.Controls.Timeline.Accessibility.CountFavorites" = "%@ favorites";
|
"Common.Controls.Timeline.Accessibility.CountFavorites" = "%@ favorites";
|
||||||
"Common.Controls.Timeline.Accessibility.CountReblogs" = "%@ reblogs";
|
"Common.Controls.Timeline.Accessibility.CountReblogs" = "%@ reblogs";
|
||||||
"Common.Controls.Timeline.Accessibility.CountReplies" = "%@ replies";
|
"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.";
|
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.
|
until you unblock them.
|
||||||
Your account looks like this to them.";
|
Your account looks like this to them.";
|
||||||
"Common.Controls.Timeline.Header.NoStatusFound" = "No Status Found";
|
"Common.Controls.Timeline.Header.NoStatusFound" = "No Status Found";
|
||||||
"Common.Controls.Timeline.Header.SuspendedWarning" = "This account has been suspended.";
|
"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.Header.UserSuspendedWarning" = "%@’s account has been suspended.";
|
||||||
"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts";
|
"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts";
|
||||||
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
|
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
|
||||||
|
@ -371,7 +371,7 @@ extension ProfileHeaderViewController {
|
|||||||
} else if bannerContainerBottomOffset < containerSafeAreaInset.top {
|
} else if bannerContainerBottomOffset < containerSafeAreaInset.top {
|
||||||
// 3
|
// 3
|
||||||
// banner bottom pin to navigation bar bottom and
|
// 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
|
bannerImageView.frame.origin.y = -containerSafeAreaInset.top
|
||||||
let bannerImageHeight = bannerContainerInWindow.size.height + containerSafeAreaInset.top + (containerSafeAreaInset.top - bannerContainerBottomOffset)
|
let bannerImageHeight = bannerContainerInWindow.size.height + containerSafeAreaInset.top + (containerSafeAreaInset.top - bannerContainerBottomOffset)
|
||||||
bannerImageView.frame.size.height = bannerImageHeight
|
bannerImageView.frame.size.height = bannerImageHeight
|
||||||
@ -406,7 +406,7 @@ extension ProfileHeaderViewController {
|
|||||||
setProfileBannerFade(alpha: 1)
|
setProfileBannerFade(alpha: 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setProfileBannerFade(alpha: CGFloat) {
|
private func setProfileBannerFade(alpha: CGFloat) {
|
||||||
profileHeaderView.avatarImageViewBackgroundView.alpha = alpha
|
profileHeaderView.avatarImageViewBackgroundView.alpha = alpha
|
||||||
profileHeaderView.avatarImageView.alpha = alpha
|
profileHeaderView.avatarImageView.alpha = alpha
|
||||||
@ -501,6 +501,7 @@ extension ProfileHeaderViewController: UICollectionViewDelegate {
|
|||||||
|
|
||||||
// MARK: - ProfileFieldCollectionViewCellDelegate
|
// MARK: - ProfileFieldCollectionViewCellDelegate
|
||||||
extension ProfileHeaderViewController: ProfileFieldCollectionViewCellDelegate {
|
extension ProfileHeaderViewController: ProfileFieldCollectionViewCellDelegate {
|
||||||
|
// should be remove style edit button
|
||||||
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, editButtonDidPressed button: UIButton) {
|
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, editButtonDidPressed button: UIButton) {
|
||||||
guard let diffableDataSource = viewModel.fieldDiffableDataSource else { return }
|
guard let diffableDataSource = viewModel.fieldDiffableDataSource else { return }
|
||||||
guard let indexPath = profileHeaderView.fieldCollectionView.indexPath(for: cell) else { return }
|
guard let indexPath = profileHeaderView.fieldCollectionView.indexPath(for: cell) else { return }
|
||||||
|
@ -24,6 +24,7 @@ final class ProfileHeaderViewModel {
|
|||||||
let needsSetupBottomShadow = CurrentValueSubject<Bool, Never>(true)
|
let needsSetupBottomShadow = CurrentValueSubject<Bool, Never>(true)
|
||||||
let isTitleViewContentOffsetSet = CurrentValueSubject<Bool, Never>(false)
|
let isTitleViewContentOffsetSet = CurrentValueSubject<Bool, Never>(false)
|
||||||
let emojiDict = CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>([:])
|
let emojiDict = CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>([:])
|
||||||
|
let accountForEdit = CurrentValueSubject<Mastodon.Entity.Account?, Never>(nil)
|
||||||
|
|
||||||
// output
|
// output
|
||||||
let displayProfileInfo = ProfileInfo()
|
let displayProfileInfo = ProfileInfo()
|
||||||
@ -33,19 +34,24 @@ final class ProfileHeaderViewModel {
|
|||||||
|
|
||||||
init(context: AppContext) {
|
init(context: AppContext) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
isEditing
|
Publishers.CombineLatest(
|
||||||
.removeDuplicates() // only triiger when value toggle
|
isEditing.removeDuplicates(), // only trigger when value toggle
|
||||||
.receive(on: DispatchQueue.main)
|
accountForEdit
|
||||||
.sink { [weak self] isEditing in
|
)
|
||||||
guard let self = self else { return }
|
.receive(on: DispatchQueue.main)
|
||||||
// setup editing value when toggle to editing
|
.sink { [weak self] isEditing, account in
|
||||||
self.editProfileInfo.name.value = self.displayProfileInfo.name.value // set to name
|
guard let self = self else { return }
|
||||||
self.editProfileInfo.avatarImageResource.value = .image(nil) // set to empty
|
guard isEditing else { return }
|
||||||
self.editProfileInfo.note.value = ProfileHeaderViewModel.normalize(note: self.displayProfileInfo.note.value)
|
// setup editing value when toggle to editing
|
||||||
self.editProfileInfo.fields.value = self.displayProfileInfo.fields.value.map { $0.duplicate() } // set to fields
|
self.editProfileInfo.name.value = self.displayProfileInfo.name.value // set to name
|
||||||
}
|
self.editProfileInfo.avatarImageResource.value = .image(nil) // set to empty
|
||||||
.store(in: &disposeBag)
|
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(
|
Publishers.CombineLatest4(
|
||||||
isEditing.removeDuplicates(),
|
isEditing.removeDuplicates(),
|
||||||
@ -152,12 +158,14 @@ extension ProfileHeaderViewModel {
|
|||||||
guard case let .image(image) = editProfileInfo.avatarImageResource.value, image == nil else { return true }
|
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 }
|
guard editProfileInfo.note.value == ProfileHeaderViewModel.normalize(note: displayProfileInfo.note.value) else { return true }
|
||||||
let isFieldsEqual: Bool = {
|
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 editFields = editProfileInfo.fields.value
|
||||||
let displayFields = displayProfileInfo.fields.value
|
guard editFields.count == originalFields.count else { return false }
|
||||||
guard editFields.count == displayFields.count else { return false }
|
for (editField, originalField) in zip(editFields, originalFields) {
|
||||||
for (editField, displayField) in zip(editFields, displayFields) {
|
guard editField.name.value == originalField.name.value,
|
||||||
guard editField.name.value == displayField.name.value,
|
editField.value.value == originalField.value.value else {
|
||||||
editField.value.value == displayField.value.value else {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,8 +50,12 @@ final class ProfileHeaderView: UIView {
|
|||||||
imageView.accessibilityIgnoresInvertColors = true
|
imageView.accessibilityIgnoresInvertColors = true
|
||||||
return imageView
|
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
|
overlayView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor
|
||||||
return overlayView
|
return overlayView
|
||||||
}()
|
}()
|
||||||
@ -79,6 +83,9 @@ final class ProfileHeaderView: UIView {
|
|||||||
editAvatarBackgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.6)
|
editAvatarBackgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.6)
|
||||||
editAvatarButton.tintColor = .white
|
editAvatarButton.tintColor = .white
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static let avatarImageViewOverlayBlurEffect = UIBlurEffect(style: .systemUltraThinMaterialDark)
|
||||||
|
let avatarImageViewOverlayVisualEffectView = UIVisualEffectView(effect: nil)
|
||||||
|
|
||||||
let editAvatarBackgroundView: UIView = {
|
let editAvatarBackgroundView: UIView = {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
@ -226,13 +233,13 @@ extension ProfileHeaderView {
|
|||||||
bannerImageView.frame = bannerContainerView.bounds
|
bannerImageView.frame = bannerContainerView.bounds
|
||||||
bannerContainerView.addSubview(bannerImageView)
|
bannerContainerView.addSubview(bannerImageView)
|
||||||
|
|
||||||
bannerImageViewOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
bannerImageViewOverlayVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
bannerImageView.addSubview(bannerImageViewOverlayView)
|
bannerImageView.addSubview(bannerImageViewOverlayVisualEffectView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
bannerImageViewOverlayView.topAnchor.constraint(equalTo: bannerImageView.topAnchor),
|
bannerImageViewOverlayVisualEffectView.topAnchor.constraint(equalTo: bannerImageView.topAnchor),
|
||||||
bannerImageViewOverlayView.leadingAnchor.constraint(equalTo: bannerImageView.leadingAnchor),
|
bannerImageViewOverlayVisualEffectView.leadingAnchor.constraint(equalTo: bannerImageView.leadingAnchor),
|
||||||
bannerImageViewOverlayView.trailingAnchor.constraint(equalTo: bannerImageView.trailingAnchor),
|
bannerImageViewOverlayVisualEffectView.trailingAnchor.constraint(equalTo: bannerImageView.trailingAnchor),
|
||||||
bannerImageViewOverlayView.bottomAnchor.constraint(equalTo: bannerImageView.bottomAnchor),
|
bannerImageViewOverlayVisualEffectView.bottomAnchor.constraint(equalTo: bannerImageView.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
// avatar
|
// avatar
|
||||||
@ -253,6 +260,15 @@ extension ProfileHeaderView {
|
|||||||
avatarImageView.widthAnchor.constraint(equalToConstant: ProfileHeaderView.avatarImageViewSize.width).priority(.required - 1),
|
avatarImageView.widthAnchor.constraint(equalToConstant: ProfileHeaderView.avatarImageViewSize.width).priority(.required - 1),
|
||||||
avatarImageView.heightAnchor.constraint(equalToConstant: ProfileHeaderView.avatarImageViewSize.height).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
|
editAvatarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
avatarImageView.addSubview(editAvatarBackgroundView)
|
avatarImageView.addSubview(editAvatarBackgroundView)
|
||||||
@ -425,7 +441,7 @@ extension ProfileHeaderView {
|
|||||||
bioTextEditorView.isHidden = true
|
bioTextEditorView.isHidden = true
|
||||||
|
|
||||||
animator.addAnimations {
|
animator.addAnimations {
|
||||||
self.bannerImageViewOverlayView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor
|
self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor
|
||||||
self.nameTextFieldBackgroundView.backgroundColor = .clear
|
self.nameTextFieldBackgroundView.backgroundColor = .clear
|
||||||
self.editAvatarBackgroundView.alpha = 0
|
self.editAvatarBackgroundView.alpha = 0
|
||||||
}
|
}
|
||||||
@ -441,7 +457,7 @@ extension ProfileHeaderView {
|
|||||||
editAvatarBackgroundView.alpha = 0
|
editAvatarBackgroundView.alpha = 0
|
||||||
bioTextEditorView.backgroundColor = .clear
|
bioTextEditorView.backgroundColor = .clear
|
||||||
animator.addAnimations {
|
animator.addAnimations {
|
||||||
self.bannerImageViewOverlayView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundEditingColor
|
self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundEditingColor
|
||||||
self.nameTextFieldBackgroundView.backgroundColor = Asset.Scene.Profile.Banner.nameEditBackgroundGray.color
|
self.nameTextFieldBackgroundView.backgroundColor = Asset.Scene.Profile.Banner.nameEditBackgroundGray.color
|
||||||
self.editAvatarBackgroundView.alpha = 1
|
self.editAvatarBackgroundView.alpha = 1
|
||||||
self.bioTextEditorView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
|
self.bioTextEditorView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
|
||||||
|
@ -376,6 +376,9 @@ extension ProfileViewController {
|
|||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.assign(to: \.value, on: profileHeaderViewController.viewModel.displayProfileInfo.fields)
|
.assign(to: \.value, on: profileHeaderViewController.viewModel.displayProfileInfo.fields)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
viewModel.accountForEdit
|
||||||
|
.assign(to: \.value, on: profileHeaderViewController.viewModel.accountForEdit)
|
||||||
|
.store(in: &disposeBag)
|
||||||
viewModel.emojiDict
|
viewModel.emojiDict
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.assign(to: \.value, on: profileHeaderViewController.viewModel.emojiDict)
|
.assign(to: \.value, on: profileHeaderViewController.viewModel.emojiDict)
|
||||||
@ -443,7 +446,7 @@ extension ProfileViewController {
|
|||||||
if relationshipActionSet.contains(.edit) {
|
if relationshipActionSet.contains(.edit) {
|
||||||
// check .edit state and set .editing when isEditing
|
// check .edit state and set .editing when isEditing
|
||||||
friendshipButton.configure(actionOptionSet: isUpdating ? .updating : (isEditing ? .editing : .edit))
|
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 {
|
} else {
|
||||||
friendshipButton.configure(actionOptionSet: relationshipActionSet)
|
friendshipButton.configure(actionOptionSet: relationshipActionSet)
|
||||||
}
|
}
|
||||||
@ -452,7 +455,7 @@ extension ProfileViewController {
|
|||||||
viewModel.isEditing
|
viewModel.isEditing
|
||||||
.handleEvents(receiveOutput: { [weak self] isEditing in
|
.handleEvents(receiveOutput: { [weak self] isEditing in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
// set firset responder for key command
|
// set first responder for key command
|
||||||
if !isEditing {
|
if !isEditing {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
self.profileSegmentedViewController.pagingViewController.becomeFirstResponder()
|
self.profileSegmentedViewController.pagingViewController.becomeFirstResponder()
|
||||||
@ -520,6 +523,25 @@ extension ProfileViewController {
|
|||||||
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.accessibilityLabel = L10n.Scene.Profile.Dashboard.Accessibility.countFollowers(count ?? 0)
|
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.accessibilityLabel = L10n.Scene.Profile.Dashboard.Accessibility.countFollowers(count ?? 0)
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.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)
|
||||||
|
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
|
profileHeaderViewController.profileHeaderView.delegate = self
|
||||||
}
|
}
|
||||||
@ -676,8 +698,9 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
|
|||||||
animated: true
|
animated: true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||||
|
// handle profile fields interaction
|
||||||
switch entity.type {
|
switch entity.type {
|
||||||
case .url(_, _, let url, _):
|
case .url(_, _, let url, _):
|
||||||
guard let url = URL(string: url) else { return }
|
guard let url = URL(string: url) else { return }
|
||||||
@ -685,6 +708,13 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
|
|||||||
case .hashtag(let hashtag, _):
|
case .hashtag(let hashtag, _):
|
||||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag)
|
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag)
|
||||||
coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show)
|
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:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -784,29 +814,67 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
|
|||||||
|
|
||||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton) {
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton) {
|
||||||
let relationshipActionSet = viewModel.relationshipActionOptionSet.value
|
let relationshipActionSet = viewModel.relationshipActionOptionSet.value
|
||||||
|
|
||||||
|
// handle edit logic for editable profile
|
||||||
|
// handle relationship logic for non-editable profile
|
||||||
if relationshipActionSet.contains(.edit) {
|
if relationshipActionSet.contains(.edit) {
|
||||||
|
// do nothing when updating
|
||||||
guard !viewModel.isUpdating.value else { return }
|
guard !viewModel.isUpdating.value else { return }
|
||||||
|
|
||||||
if profileHeaderViewController.viewModel.isProfileInfoEdited() {
|
if profileHeaderViewController.viewModel.isProfileInfoEdited() {
|
||||||
|
// update profile if changed
|
||||||
viewModel.isUpdating.value = true
|
viewModel.isUpdating.value = true
|
||||||
profileHeaderViewController.viewModel.updateProfileInfo()
|
profileHeaderViewController.viewModel.updateProfileInfo()
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] completion in
|
.sink { [weak self] completion in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
defer {
|
||||||
|
// finish updating
|
||||||
|
self.viewModel.isUpdating.value = false
|
||||||
|
}
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
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)
|
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:
|
case .finished:
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update profile info success", ((#file as NSString).lastPathComponent), #line, #function)
|
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
|
} receiveValue: { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.viewModel.isEditing.value = false
|
self.viewModel.isEditing.value = false
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return }
|
guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return }
|
||||||
|
@ -42,6 +42,9 @@ class ProfileViewModel: NSObject {
|
|||||||
let fileds: CurrentValueSubject<[Mastodon.Entity.Field], Never>
|
let fileds: CurrentValueSubject<[Mastodon.Entity.Field], Never>
|
||||||
let emojiDict: CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>
|
let emojiDict: CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>
|
||||||
|
|
||||||
|
// fulfill this before editing
|
||||||
|
let accountForEdit = CurrentValueSubject<Mastodon.Entity.Account?, Never>(nil)
|
||||||
|
|
||||||
let protected: CurrentValueSubject<Bool?, Never>
|
let protected: CurrentValueSubject<Bool?, Never>
|
||||||
let suspended: CurrentValueSubject<Bool, Never>
|
let suspended: CurrentValueSubject<Bool, Never>
|
||||||
|
|
||||||
@ -58,8 +61,10 @@ class ProfileViewModel: NSObject {
|
|||||||
let isReplyBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
|
let isReplyBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
|
||||||
let isMoreMenuBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
|
let isMoreMenuBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
|
||||||
let isMeBarButtonItemsHidden = CurrentValueSubject<Bool, Never>(true)
|
let isMeBarButtonItemsHidden = CurrentValueSubject<Bool, Never>(true)
|
||||||
|
|
||||||
let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
|
let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
let needsPaingEnabled = CurrentValueSubject<Bool, Never>(true)
|
||||||
|
let needsImageOverlayBlurred = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
|
||||||
init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) {
|
init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) {
|
||||||
self.context = context
|
self.context = context
|
||||||
@ -147,6 +152,23 @@ class ProfileViewModel: NSObject {
|
|||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
let isBlockingOrBlocked = Publishers.CombineLatest(
|
||||||
|
isBlocking,
|
||||||
|
isBlockedBy
|
||||||
|
)
|
||||||
|
.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()
|
setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,6 +344,22 @@ extension ProfileViewModel {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ProfileViewModel {
|
||||||
|
|
||||||
|
// fetch profile info before edit
|
||||||
|
func fetchEditProfileInfo() -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, 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 {
|
extension ProfileViewModel {
|
||||||
|
|
||||||
enum RelationshipAction: Int, CaseIterable {
|
enum RelationshipAction: Int, CaseIterable {
|
||||||
|
@ -82,19 +82,28 @@ final class UserTimelineViewModel {
|
|||||||
diffableDataSource.apply(snapshot, animatingDifferences: !items.isEmpty)
|
diffableDataSource.apply(snapshot, animatingDifferences: !items.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let name = self.userDisplayName.value
|
||||||
guard !isBlocking else {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard !isBlockedBy else {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = self.userDisplayName.value
|
|
||||||
guard !isSuspended else {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,10 +97,18 @@ extension Item.EmptyStateHeaderAttribute.Reason {
|
|||||||
switch self {
|
switch self {
|
||||||
case .noStatusFound:
|
case .noStatusFound:
|
||||||
return L10n.Common.Controls.Timeline.Header.noStatusFound
|
return L10n.Common.Controls.Timeline.Header.noStatusFound
|
||||||
case .blocking:
|
case .blocking(let name):
|
||||||
return L10n.Common.Controls.Timeline.Header.blockingWarning
|
if let name = name {
|
||||||
case .blocked:
|
return L10n.Common.Controls.Timeline.Header.userBlockingWarning(name)
|
||||||
return L10n.Common.Controls.Timeline.Header.blockedWarning
|
} 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):
|
case .suspended(let name):
|
||||||
if let name = name {
|
if let name = name {
|
||||||
return L10n.Common.Controls.Timeline.Header.userSuspendedWarning(name)
|
return L10n.Common.Controls.Timeline.Header.userSuspendedWarning(name)
|
||||||
@ -119,8 +127,8 @@ struct TimelineHeaderView_Previews: PreviewProvider {
|
|||||||
Group {
|
Group {
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let headerView = TimelineHeaderView()
|
let headerView = TimelineHeaderView()
|
||||||
headerView.iconImageView.image = Item.EmptyStateHeaderAttribute.Reason.blocking.iconImage
|
headerView.iconImageView.image = Item.EmptyStateHeaderAttribute.Reason.blocking(name: nil).iconImage
|
||||||
headerView.messageLabel.text = Item.EmptyStateHeaderAttribute.Reason.blocking.message
|
headerView.messageLabel.text = Item.EmptyStateHeaderAttribute.Reason.blocking(name: nil).message
|
||||||
return headerView
|
return headerView
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 400))
|
.previewLayout(.fixed(width: 375, height: 400))
|
||||||
|
@ -136,7 +136,8 @@ extension APIService {
|
|||||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||||
let domain = mastodonAuthenticationBox.domain
|
let domain = mastodonAuthenticationBox.domain
|
||||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||||
|
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||||
|
|
||||||
return Mastodon.API.Account.block(
|
return Mastodon.API.Account.block(
|
||||||
session: session,
|
session: session,
|
||||||
domain: domain,
|
domain: domain,
|
||||||
@ -144,11 +145,39 @@ extension APIService {
|
|||||||
blockQueryType: blockQueryType,
|
blockQueryType: blockQueryType,
|
||||||
authorization: authorization
|
authorization: authorization
|
||||||
)
|
)
|
||||||
|
.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()
|
||||||
|
}
|
||||||
.handleEvents(receiveCompletion: { [weak self] completion in
|
.handleEvents(receiveCompletion: { [weak self] completion in
|
||||||
guard let _ = self else { return }
|
guard let _ = self else { return }
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
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)
|
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:
|
case .finished:
|
||||||
|
@ -196,11 +196,11 @@ extension APIService {
|
|||||||
let lookUpMastodonUserRequest = MastodonUser.sortedFetchRequest
|
let lookUpMastodonUserRequest = MastodonUser.sortedFetchRequest
|
||||||
lookUpMastodonUserRequest.predicate = MastodonUser.predicate(domain: domain, id: mastodonUserID)
|
lookUpMastodonUserRequest.predicate = MastodonUser.predicate(domain: domain, id: mastodonUserID)
|
||||||
lookUpMastodonUserRequest.fetchLimit = 1
|
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
|
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<Mastodon.Entity.Relationship> in
|
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Relationship> in
|
||||||
|
@ -103,20 +103,20 @@ extension AuthenticationService {
|
|||||||
extension AuthenticationService {
|
extension AuthenticationService {
|
||||||
|
|
||||||
func activeMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher<Result<Bool, Error>, Never> {
|
func activeMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher<Result<Bool, Error>, Never> {
|
||||||
var isActived = false
|
var isActive = false
|
||||||
|
|
||||||
return backgroundManagedObjectContext.performChanges {
|
return backgroundManagedObjectContext.performChanges {
|
||||||
let request = MastodonAuthentication.sortedFetchRequest
|
let request = MastodonAuthentication.sortedFetchRequest
|
||||||
request.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID)
|
request.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID)
|
||||||
request.fetchLimit = 1
|
request.fetchLimit = 1
|
||||||
guard let mastodonAutentication = try? self.backgroundManagedObjectContext.fetch(request).first else {
|
guard let mastodonAuthentication = try? self.backgroundManagedObjectContext.fetch(request).first else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mastodonAutentication.update(activedAt: Date())
|
mastodonAuthentication.update(activedAt: Date())
|
||||||
isActived = true
|
isActive = true
|
||||||
}
|
}
|
||||||
.map { result in
|
.map { result in
|
||||||
return result.map { isActived }
|
return result.map { isActive }
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
@ -124,27 +124,27 @@ extension AuthenticationService {
|
|||||||
func signOutMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher<Result<Bool, Error>, Never> {
|
func signOutMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher<Result<Bool, Error>, Never> {
|
||||||
var isSignOut = false
|
var isSignOut = false
|
||||||
|
|
||||||
var _mastodonAutenticationBox: MastodonAuthenticationBox?
|
var _mastodonAuthenticationBox: MastodonAuthenticationBox?
|
||||||
let managedObjectContext = backgroundManagedObjectContext
|
let managedObjectContext = backgroundManagedObjectContext
|
||||||
return managedObjectContext.performChanges {
|
return managedObjectContext.performChanges {
|
||||||
let request = MastodonAuthentication.sortedFetchRequest
|
let request = MastodonAuthentication.sortedFetchRequest
|
||||||
request.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID)
|
request.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID)
|
||||||
request.fetchLimit = 1
|
request.fetchLimit = 1
|
||||||
guard let mastodonAutentication = try? managedObjectContext.fetch(request).first else {
|
guard let mastodonAuthentication = try? managedObjectContext.fetch(request).first else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_mastodonAutenticationBox = AuthenticationService.MastodonAuthenticationBox(
|
_mastodonAuthenticationBox = AuthenticationService.MastodonAuthenticationBox(
|
||||||
domain: mastodonAutentication.domain,
|
domain: mastodonAuthentication.domain,
|
||||||
userID: mastodonAutentication.userID,
|
userID: mastodonAuthentication.userID,
|
||||||
appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAutentication.appAccessToken),
|
appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.appAccessToken),
|
||||||
userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAutentication.userAccessToken)
|
userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken)
|
||||||
)
|
)
|
||||||
managedObjectContext.delete(mastodonAutentication)
|
managedObjectContext.delete(mastodonAuthentication)
|
||||||
isSignOut = true
|
isSignOut = true
|
||||||
}
|
}
|
||||||
.flatMap { result -> AnyPublisher<Result<Void, Error>, Never> in
|
.flatMap { result -> AnyPublisher<Result<Void, Error>, Never> in
|
||||||
guard let apiService = self.apiService,
|
guard let apiService = self.apiService,
|
||||||
let mastodonAuthenticationBox = _mastodonAutenticationBox else {
|
let mastodonAuthenticationBox = _mastodonAuthenticationBox else {
|
||||||
return Just(result).eraseToAnyPublisher()
|
return Just(result).eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user