mirror of
https://github.com/mastodon/mastodon-ios.git
synced 2024-12-18 11:49:00 +01:00
fix: profile fields edit using wrong material issue
This commit is contained in:
parent
ab07860e5a
commit
747becda79
Localization
Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes
Mastodon
Diffiable/Item
Extension
Generated
Helper
Resources
Scene/Profile
Service
@ -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?",
|
||||
|
@ -12,7 +12,7 @@
|
||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>21</integer>
|
||||
<integer>20</integer>
|
||||
</dict>
|
||||
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
@ -37,7 +37,7 @@
|
||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>20</integer>
|
||||
<integer>21</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
@ -32,20 +32,16 @@ extension ProfileFieldItem {
|
||||
extension ProfileFieldItem {
|
||||
struct FieldValue: Equatable, Hashable {
|
||||
let id: UUID
|
||||
|
||||
|
||||
var name: CurrentValueSubject<String, Never>
|
||||
var value: CurrentValueSubject<String, Never>
|
||||
|
||||
|
||||
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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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 }
|
||||
|
@ -24,6 +24,7 @@ final class ProfileHeaderViewModel {
|
||||
let needsSetupBottomShadow = CurrentValueSubject<Bool, Never>(true)
|
||||
let isTitleViewContentOffsetSet = CurrentValueSubject<Bool, Never>(false)
|
||||
let emojiDict = CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>([:])
|
||||
let accountForEdit = CurrentValueSubject<Mastodon.Entity.Account?, Never>(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
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -42,6 +42,9 @@ class ProfileViewModel: NSObject {
|
||||
let fileds: CurrentValueSubject<[Mastodon.Entity.Field], 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 suspended: CurrentValueSubject<Bool, Never>
|
||||
|
||||
@ -341,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 {
|
||||
|
||||
enum RelationshipAction: Int, CaseIterable {
|
||||
|
@ -103,20 +103,20 @@ extension AuthenticationService {
|
||||
extension AuthenticationService {
|
||||
|
||||
func activeMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher<Result<Bool, Error>, 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<Result<Bool, Error>, 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<Result<Void, Error>, Never> in
|
||||
guard let apiService = self.apiService,
|
||||
let mastodonAuthenticationBox = _mastodonAutenticationBox else {
|
||||
let mastodonAuthenticationBox = _mastodonAuthenticationBox else {
|
||||
return Just(result).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user