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