From 42e89987a115ba5bed3f89fa401f96329a08bac8 Mon Sep 17 00:00:00 2001 From: Vivek Date: Wed, 26 Aug 2026 18:21:18 +0530 Subject: [PATCH 1/5] implmented the toast shows up after a user successfully submit comments on the post. --- .../Create/CommentCreateViewController.swift | 10 ++++- .../System/Notices/NoticeStyle.swift | 45 +++++++++++++++++-- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewController.swift b/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewController.swift index 527cf50a53ed..5271ed304793 100644 --- a/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewController.swift +++ b/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewController.swift @@ -67,7 +67,14 @@ final class CommentCreateViewController: UIViewController { try await viewModel.save(content: text) UINotificationFeedbackGenerator().notificationOccurred(.success) NotificationCenter.default.post(name: .ReaderCommentModifiedNotification, object: nil) - presentingViewController?.dismiss(animated: true) + presentingViewController?.dismiss(animated: true) { + Notice(title: Strings.commentHeldForModeration, + style: InAppUpdateNoticeStyle( + icon: UIImage(systemName: "checkmark.seal.fill"), + iconColor: UIAppColor.success, title: Strings.commentHeldForModeration + )) + .post() + } } catch { setLoading(false) UINotificationFeedbackGenerator().notificationOccurred(.error) @@ -147,6 +154,7 @@ extension CommentCreateViewController: CommentEditorViewControllerDelegate { private enum Strings { static let send = NSLocalizedString("commentCreate.send", value: "Send", comment: "Navigation bar button title") static let failedToSend = NSLocalizedString("commentCreate.failedToSentComment", value: "Failed to send comment", comment: "Error title") + static let commentHeldForModeration = NSLocalizedString("commentCreate.commentHeldForModeration", value: "Comment is awaiting review", comment: "Toast title shown after successfully submitting a comment") static let closeConfirmationAlertCancel = NSLocalizedString("commentCreate.closeConfirmationAlert.keepEditing", value: "Keep Editing", comment: "Button to keep the changes in an alert confirming discaring changes") static let closeConfirmationAlertDelete = NSLocalizedString("commentCreate.closeConfirmationAlert.deleteDraft", value: "Delete Draft", comment: "Button in an alert confirming discaring a new draft") static let closeConfirmationAlertSaveDraft = NSLocalizedString("commentCreate.closeConfirmationAlert.saveDraft", value: "Save Draft", comment: "Button in an alert confirming saving a new draft") diff --git a/WordPress/Classes/ViewRelated/System/Notices/NoticeStyle.swift b/WordPress/Classes/ViewRelated/System/Notices/NoticeStyle.swift index c632b786c586..a50e69f24ae5 100644 --- a/WordPress/Classes/ViewRelated/System/Notices/NoticeStyle.swift +++ b/WordPress/Classes/ViewRelated/System/Notices/NoticeStyle.swift @@ -1,5 +1,6 @@ import UIKit import WordPressShared +import DesignSystem public enum NoticeAnimationStyle { case moveIn @@ -73,9 +74,46 @@ public struct NormalNoticeStyle: NoticeStyle { public struct InAppUpdateNoticeStyle: NoticeStyle { public let attributedMessage: NSAttributedString? - - init(attributedMessage: NSAttributedString? = nil) { - self.attributedMessage = attributedMessage + public let isDismissable: Bool + + /// - Parameters: + /// - icon: An optional SF Symbol rendered inline before `title`, e.g. a checkmark seal to indicate success. + /// - iconColor: The tint color applied to `icon`. + /// - title: When provided (with or without `icon`), builds `attributedMessage` from it and makes the + /// Notice auto-dismiss after a few seconds. When `nil`, the Notice falls back to its own `title`/`message` + /// and stays on screen until the user dismisses it, matching the original in-app-update banner behavior. + init(icon: UIImage? = nil, iconColor: UIColor = .invertedLabel, title: String? = nil) { + guard let title else { + self.attributedMessage = nil + self.isDismissable = false + return + } + + self.isDismissable = true + + let font = UIFont.boldSystemFont(ofSize: 14.0) + let message = NSMutableAttributedString() + + if let icon = icon?.withTintColor(iconColor, renderingMode: .alwaysOriginal) { + let attachment = NSTextAttachment(image: icon) + attachment.accessibilityLabel = "" // Decorative; the title text conveys the meaning. + let iconHeight = font.lineHeight + let ratio = icon.size.width / icon.size.height + attachment.bounds = CGRect( + x: 0, + y: (font.capHeight - iconHeight) / 2, + width: iconHeight * ratio, + height: iconHeight + ) + message.append(NSAttributedString(attachment: attachment)) + message.append(NSAttributedString(string: " ")) + } + + message.append( + NSAttributedString(string: title, attributes: [.font: font, .foregroundColor: UIColor.invertedLabel]) + ) + + self.attributedMessage = message } // Return new UIFont instance everytime in order to be responsive to accessibility font size changes @@ -85,7 +123,6 @@ public struct InAppUpdateNoticeStyle: NoticeStyle { public let directionalLayoutMargins = NSDirectionalEdgeInsets(top: 13.0, leading: 16.0, bottom: 13.0, trailing: 16.0) - public var isDismissable = false public let showNextArrow = false public let animationStyle = NoticeAnimationStyle.moveIn From 3dd5d5a5730c1e26b988e2d37e69f37134667856 Mon Sep 17 00:00:00 2001 From: Vivek Date: Thu, 27 Aug 2026 17:41:29 +0530 Subject: [PATCH 2/5] now toast mesage will be showup on unapproved comment. --- WordPress/Classes/Services/CommentService.h | 4 +- WordPress/Classes/Services/CommentService.m | 16 +++-- .../CommentDetailViewController.swift | 63 +++++++++++-------- .../Create/CommentCreateViewController.swift | 24 ++++--- .../Create/CommentCreateViewModel.swift | 32 +++++++--- 5 files changed, 87 insertions(+), 52 deletions(-) diff --git a/WordPress/Classes/Services/CommentService.h b/WordPress/Classes/Services/CommentService.h index ff84f9ce9717..05a3c81c9d37 100644 --- a/WordPress/Classes/Services/CommentService.h +++ b/WordPress/Classes/Services/CommentService.h @@ -138,13 +138,13 @@ extern NSUInteger const WPTopLevelHierarchicalCommentsPerPage; // Replies - (void)replyToPost:(ReaderPost *)post content:(NSString *)content - success:(void (^ _Nullable)(void))success + success:(void (^ _Nullable)(Comment * _Nullable comment))success failure:(void (^ _Nullable)(NSError * _Nullable error))failure; - (void)replyToHierarchicalCommentWithID:(NSNumber *)commentID post:(ReaderPost *)post content:(NSString *)content - success:(void (^ _Nullable)(void))success + success:(void (^ _Nullable)(Comment * _Nullable comment))success failure:(void (^ _Nullable)(NSError * _Nullable error))failure; - (void)replyToCommentWithID:(NSNumber *)commentID diff --git a/WordPress/Classes/Services/CommentService.m b/WordPress/Classes/Services/CommentService.m index dca4f0f8e765..0c4e527a2e80 100644 --- a/WordPress/Classes/Services/CommentService.m +++ b/WordPress/Classes/Services/CommentService.m @@ -712,7 +712,7 @@ - (void)updateCommentWithID:(NSNumber *)commentID // Replies - (void)replyToPost:(ReaderPost *)post content:(NSString *)content - success:(void (^)(void))success + success:(void (^)(Comment * _Nullable comment))success failure:(void (^)(NSError *error))failure { // Create and optimistically save a comment, based on the current wpcom acct @@ -737,7 +737,11 @@ - (void)replyToPost:(ReaderPost *)post remoteComment.content = [self sanitizeCommentContent:remoteComment.content isPrivateSite:isPrivateSite]; [self updateHierarchicalComment:comment withRemoteComment:remoteComment]; - } completion:success onQueue:dispatch_get_main_queue()]; + } completion:^{ + if (success) { + success([self.coreDataStack.mainContext existingObjectWithID:commentID error:nil]); + } + } onQueue:dispatch_get_main_queue()]; }; void (^failureBlock)(NSError *error) = ^void(NSError *error) { @@ -765,7 +769,7 @@ - (void)replyToPost:(ReaderPost *)post - (void)replyToHierarchicalCommentWithID:(NSNumber *)commentID post:(ReaderPost *)post content:(NSString *)content - success:(void (^)(void))success + success:(void (^)(Comment * _Nullable comment))success failure:(void (^)(NSError *error))failure { // Create and optimistically save a comment, based on the current wpcom acct @@ -791,7 +795,11 @@ - (void)replyToHierarchicalCommentWithID:(NSNumber *)commentID remoteComment.content = [self sanitizeCommentContent:remoteComment.content isPrivateSite:isPrivateSite]; [self updateHierarchicalComment:comment withRemoteComment:remoteComment]; - } completion:success onQueue:dispatch_get_main_queue()]; + } completion:^{ + if (success) { + success([self.coreDataStack.mainContext existingObjectWithID:commentObjectID error:nil]); + } + } onQueue:dispatch_get_main_queue()]; }; void (^failureBlock)(NSError *error) = ^void(NSError *error) { diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift b/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift index e6b0e0eef0bc..1d0fbb2309e3 100644 --- a/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift +++ b/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift @@ -993,53 +993,62 @@ private extension CommentDetailViewController { @objc func buttonAddCommentTapped() { let viewModel = CommentCreateViewModel(replyingTo: comment) { [weak self] in - try await self?.createReply(content: $0) + try await self?.createReply(content: $0) ?? false } let composerVC = CommentCreateViewController(viewModel: viewModel) let navigationVC = UINavigationController(rootViewController: composerVC) present(navigationVC, animated: true) } + /// - returns: `true` if the comment is pending moderation (not immediately approved). @MainActor - func createReply(content: String) async throws { - isNotificationComment ? WPAppAnalytics.track(.notificationsCommentRepliedTo) : - CommentAnalytics.trackCommentRepliedTo(comment: comment) + func createReply(content: String) async throws -> Bool { + isNotificationComment + ? WPAppAnalytics.track(.notificationsCommentRepliedTo) + : CommentAnalytics.trackCommentRepliedTo(comment: comment) // If there is no Blog, try with the Post. guard comment.blog != nil else { - try await createPostCommentReply(content: content) - return + return try await createPostCommentReply(content: content) } - try await withUnsafeThrowingContinuation { continuation in + return try await withUnsafeThrowingContinuation { continuation in commentService.createReply(for: comment, content: content) { reply in - self.commentService.uploadComment(reply, success: { [weak self] in - self?.refreshCommentReplyIfNeeded() - continuation.resume() - }, failure: { error in - DDLogError("Failed uploading comment reply: \(String(describing: error))") - continuation.resume(throwing: error ?? URLError(.unknown)) - }) + self.commentService.uploadComment( + reply, + success: { [weak self] in + self?.refreshCommentReplyIfNeeded() + continuation.resume(returning: reply.isApproved() == false) + }, + failure: { error in + DDLogError("Failed uploading comment reply: \(String(describing: error))") + continuation.resume(throwing: error ?? URLError(.unknown)) + } + ) } } } + /// - returns: `true` if the comment is pending moderation (not immediately approved). @MainActor - func createPostCommentReply(content: String) async throws { + func createPostCommentReply(content: String) async throws -> Bool { guard let post = comment.post as? ReaderPost else { - return + return false } - try await withUnsafeThrowingContinuation { continuation in - commentService.replyToHierarchicalComment(withID: NSNumber(value: comment.commentID), - post: post, - content: content, - success: { [weak self] in - self?.refreshCommentReplyIfNeeded() - continuation.resume() - }, failure: { error in - DDLogError("Failed creating post comment reply: \(String(describing: error))") - continuation.resume(throwing: error ?? URLError(.unknown)) - }) + return try await withUnsafeThrowingContinuation { continuation in + commentService.replyToHierarchicalComment( + withID: NSNumber(value: comment.commentID), + post: post, + content: content, + success: { [weak self] newComment in + self?.refreshCommentReplyIfNeeded() + continuation.resume(returning: newComment?.isApproved() == false) + }, + failure: { error in + DDLogError("Failed creating post comment reply: \(String(describing: error))") + continuation.resume(throwing: error ?? URLError(.unknown)) + } + ) } } } diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewController.swift b/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewController.swift index 5271ed304793..c7b3de6e2783 100644 --- a/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewController.swift +++ b/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewController.swift @@ -64,15 +64,20 @@ final class CommentCreateViewController: UIViewController { Task { @MainActor in do { let text = await editorVC.text - try await viewModel.save(content: text) + let isPendingModeration = try await viewModel.save(content: text) UINotificationFeedbackGenerator().notificationOccurred(.success) NotificationCenter.default.post(name: .ReaderCommentModifiedNotification, object: nil) - presentingViewController?.dismiss(animated: true) { - Notice(title: Strings.commentHeldForModeration, - style: InAppUpdateNoticeStyle( + presentingViewController? + .dismiss(animated: true) { + guard isPendingModeration else { return } + Notice( + title: Strings.commentHeldForModeration, + style: InAppUpdateNoticeStyle( icon: UIImage(systemName: "checkmark.seal.fill"), - iconColor: UIAppColor.success, title: Strings.commentHeldForModeration - )) + iconColor: UIAppColor.success, + title: Strings.commentHeldForModeration + ) + ) .post() } } catch { @@ -112,9 +117,10 @@ final class CommentCreateViewController: UIViewController { if viewModel.canSaveDraft { alert.addActionWithTitle(Strings.closeConfirmationAlertSaveDraft, style: .default) { [weak self] _ in self?.viewModel.saveDraft(content) - self?.presentingViewController?.dismiss(animated: true) { - UINotificationFeedbackGenerator().notificationOccurred(.success) - } + self?.presentingViewController? + .dismiss(animated: true) { + UINotificationFeedbackGenerator().notificationOccurred(.success) + } } } alert.popoverPresentationController?.barButtonItem = navigationItem.leftBarButtonItem diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewModel.swift b/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewModel.swift index 5c835fb0651e..aadd0c327198 100644 --- a/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewModel.swift +++ b/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewModel.swift @@ -23,8 +23,10 @@ final class CommentCreateViewModel { /// - note: It's a temporary solution until the respective save logic /// can be moved from the view controllers. - private var _save: (String) async throws -> Void = { _ in + /// - returns: `true` if the comment is pending moderation (not immediately approved). + private var _save: (String) async throws -> Bool = { _ in wpAssertionFailure("Not implemented") + return false } var isGutenbergEnabled: Bool { @@ -49,12 +51,12 @@ final class CommentCreateViewModel { } self._save = { [weak self] in - try await self?.sendComment($0, post: post, replyingTo: comment) + try await self?.sendComment($0, post: post, replyingTo: comment) ?? false } } /// Create a reply to the given comment (from notifications) - init(replyingTo comment: Comment, save: @escaping (String) async throws -> Void) { + init(replyingTo comment: Comment, save: @escaping (String) async throws -> Bool) { let siteID = comment.associatedSiteID ?? 0 self.siteID = siteID @@ -72,27 +74,37 @@ final class CommentCreateViewModel { Strings.leaveComment } - func save(content: String) async throws { - try await _save(content) + /// - returns: `true` if the comment is pending moderation (not immediately approved). + func save(content: String) async throws -> Bool { + let isPendingModeration = try await _save(content) deleteDraft() + return isPendingModeration } // MARK: Reader - private func sendComment(_ content: String, post: ReaderPost, replyingTo comment: Comment? = nil) async throws { + private func sendComment( + _ content: String, + post: ReaderPost, + replyingTo comment: Comment? = nil + ) async throws -> Bool { try await withUnsafeThrowingContinuation { [weak self] continuation in let service = CommentService(coreDataStack: ContextManager.shared) if let comment { - service.replyToHierarchicalComment(withID: comment.commentID as NSNumber, post: post, content: content) { + service.replyToHierarchicalComment( + withID: comment.commentID as NSNumber, + post: post, + content: content + ) { newComment in self?.trackReply(isReplyingToComment: true, post: post) - continuation.resume() + continuation.resume(returning: newComment?.isApproved() == false) } failure: { continuation.resume(throwing: $0 ?? URLError(.unknown)) } } else { - service.reply(to: post, content: content) { + service.reply(to: post, content: content) { newComment in self?.trackReply(isReplyingToComment: true, post: post) - continuation.resume() + continuation.resume(returning: newComment?.isApproved() == false) } failure: { continuation.resume(throwing: $0 ?? URLError(.unknown)) } From 2579231bdf5a28d672401561fb7e1d3cfcf31c71 Mon Sep 17 00:00:00 2001 From: Vivek Date: Mon, 31 Aug 2026 20:12:11 +0530 Subject: [PATCH 3/5] Passing down TaggedManagedObjectID instead of Bool when comment is saved --- .../CommentDetailViewController.swift | 21 +++++++----- .../Create/CommentCreateViewController.swift | 33 ++++++++++++------- .../Create/CommentCreateViewModel.swift | 33 ++++++++++++------- 3 files changed, 55 insertions(+), 32 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift b/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift index 1d0fbb2309e3..32a9323b8fc9 100644 --- a/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift +++ b/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift @@ -993,16 +993,17 @@ private extension CommentDetailViewController { @objc func buttonAddCommentTapped() { let viewModel = CommentCreateViewModel(replyingTo: comment) { [weak self] in - try await self?.createReply(content: $0) ?? false + guard let self else { throw URLError(.unknown) } + return try await self.createReply(content: $0) } let composerVC = CommentCreateViewController(viewModel: viewModel) let navigationVC = UINavigationController(rootViewController: composerVC) present(navigationVC, animated: true) } - /// - returns: `true` if the comment is pending moderation (not immediately approved). + /// - returns: The object ID of the newly created reply. @MainActor - func createReply(content: String) async throws -> Bool { + func createReply(content: String) async throws -> TaggedManagedObjectID { isNotificationComment ? WPAppAnalytics.track(.notificationsCommentRepliedTo) : CommentAnalytics.trackCommentRepliedTo(comment: comment) @@ -1018,7 +1019,7 @@ private extension CommentDetailViewController { reply, success: { [weak self] in self?.refreshCommentReplyIfNeeded() - continuation.resume(returning: reply.isApproved() == false) + continuation.resume(returning: TaggedManagedObjectID(reply)) }, failure: { error in DDLogError("Failed uploading comment reply: \(String(describing: error))") @@ -1029,11 +1030,11 @@ private extension CommentDetailViewController { } } - /// - returns: `true` if the comment is pending moderation (not immediately approved). + /// - returns: The object ID of the newly created reply. @MainActor - func createPostCommentReply(content: String) async throws -> Bool { + func createPostCommentReply(content: String) async throws -> TaggedManagedObjectID { guard let post = comment.post as? ReaderPost else { - return false + throw URLError(.unknown) } return try await withUnsafeThrowingContinuation { continuation in commentService.replyToHierarchicalComment( @@ -1042,7 +1043,11 @@ private extension CommentDetailViewController { content: content, success: { [weak self] newComment in self?.refreshCommentReplyIfNeeded() - continuation.resume(returning: newComment?.isApproved() == false) + guard let newComment else { + continuation.resume(throwing: URLError(.unknown)) + return + } + continuation.resume(returning: TaggedManagedObjectID(newComment)) }, failure: { error in DDLogError("Failed creating post comment reply: \(String(describing: error))") diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewController.swift b/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewController.swift index c7b3de6e2783..cf3507afed2e 100644 --- a/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewController.swift +++ b/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewController.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressData import WordPressUI final class CommentCreateViewController: UIViewController { @@ -64,21 +65,12 @@ final class CommentCreateViewController: UIViewController { Task { @MainActor in do { let text = await editorVC.text - let isPendingModeration = try await viewModel.save(content: text) + let commentID = try await viewModel.save(content: text) UINotificationFeedbackGenerator().notificationOccurred(.success) NotificationCenter.default.post(name: .ReaderCommentModifiedNotification, object: nil) presentingViewController? - .dismiss(animated: true) { - guard isPendingModeration else { return } - Notice( - title: Strings.commentHeldForModeration, - style: InAppUpdateNoticeStyle( - icon: UIImage(systemName: "checkmark.seal.fill"), - iconColor: UIAppColor.success, - title: Strings.commentHeldForModeration - ) - ) - .post() + .dismiss(animated: true) { [weak self] in + self?.showModerationNoticeIfNeeded(for: commentID) } } catch { setLoading(false) @@ -94,6 +86,23 @@ final class CommentCreateViewController: UIViewController { editorVC.isEnabled = !isLoading } + private func showModerationNoticeIfNeeded(for commentID: TaggedManagedObjectID) { + guard let comment = try? ContextManager.shared.mainContext.existingObject(with: commentID), + !comment.isApproved() + else { + return + } + Notice( + title: Strings.commentHeldForModeration, + style: InAppUpdateNoticeStyle( + icon: UIImage(systemName: "checkmark.seal.fill"), + iconColor: UIAppColor.success, + title: Strings.commentHeldForModeration + ) + ) + .post() + } + @objc private func buttonCancelTapped() { navigationItem.leftBarButtonItem?.isEnabled = false Task { @MainActor in diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewModel.swift b/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewModel.swift index aadd0c327198..1bffd13bcb93 100644 --- a/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewModel.swift +++ b/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewModel.swift @@ -23,10 +23,9 @@ final class CommentCreateViewModel { /// - note: It's a temporary solution until the respective save logic /// can be moved from the view controllers. - /// - returns: `true` if the comment is pending moderation (not immediately approved). - private var _save: (String) async throws -> Bool = { _ in + private var _save: (String) async throws -> TaggedManagedObjectID = { _ in wpAssertionFailure("Not implemented") - return false + throw URLError(.unknown) } var isGutenbergEnabled: Bool { @@ -51,12 +50,13 @@ final class CommentCreateViewModel { } self._save = { [weak self] in - try await self?.sendComment($0, post: post, replyingTo: comment) ?? false + guard let self else { throw URLError(.unknown) } + return try await self.sendComment($0, post: post, replyingTo: comment) } } /// Create a reply to the given comment (from notifications) - init(replyingTo comment: Comment, save: @escaping (String) async throws -> Bool) { + init(replyingTo comment: Comment, save: @escaping (String) async throws -> TaggedManagedObjectID) { let siteID = comment.associatedSiteID ?? 0 self.siteID = siteID @@ -74,11 +74,12 @@ final class CommentCreateViewModel { Strings.leaveComment } - /// - returns: `true` if the comment is pending moderation (not immediately approved). - func save(content: String) async throws -> Bool { - let isPendingModeration = try await _save(content) + /// - returns: The object ID of the newly created comment. Callers can resolve it against + /// a context to inspect the comment's current state (e.g. its moderation status). + func save(content: String) async throws -> TaggedManagedObjectID { + let commentID = try await _save(content) deleteDraft() - return isPendingModeration + return commentID } // MARK: Reader @@ -87,7 +88,7 @@ final class CommentCreateViewModel { _ content: String, post: ReaderPost, replyingTo comment: Comment? = nil - ) async throws -> Bool { + ) async throws -> TaggedManagedObjectID { try await withUnsafeThrowingContinuation { [weak self] continuation in let service = CommentService(coreDataStack: ContextManager.shared) if let comment { @@ -97,14 +98,22 @@ final class CommentCreateViewModel { content: content ) { newComment in self?.trackReply(isReplyingToComment: true, post: post) - continuation.resume(returning: newComment?.isApproved() == false) + guard let newComment else { + continuation.resume(throwing: URLError(.unknown)) + return + } + continuation.resume(returning: TaggedManagedObjectID(newComment)) } failure: { continuation.resume(throwing: $0 ?? URLError(.unknown)) } } else { service.reply(to: post, content: content) { newComment in self?.trackReply(isReplyingToComment: true, post: post) - continuation.resume(returning: newComment?.isApproved() == false) + guard let newComment else { + continuation.resume(throwing: URLError(.unknown)) + return + } + continuation.resume(returning: TaggedManagedObjectID(newComment)) } failure: { continuation.resume(throwing: $0 ?? URLError(.unknown)) } From 0b54d702c15dabdd1d79feb1ffbb571f9ed74582 Mon Sep 17 00:00:00 2001 From: Vivek Date: Fri, 4 Sep 2026 08:16:17 +0530 Subject: [PATCH 4/5] restore the Notice title, remove the icon implementation --- .../CommentDetailViewController.swift | 20 +++++---- .../Create/CommentCreateViewController.swift | 10 +---- .../System/Notices/NoticeStyle.swift | 44 ++----------------- 3 files changed, 17 insertions(+), 57 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift b/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift index b192073ad818..8c7abe7e7790 100644 --- a/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift +++ b/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift @@ -1013,20 +1013,24 @@ private extension CommentDetailViewController { return try await createPostCommentReply(content: content) } - try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in + return try await withUnsafeThrowingContinuation { continuation in commentService.createReply(for: comment, content: content) { reply in guard let reply else { DDLogError("Failed creating comment reply: reply was nil after save") continuation.resume(throwing: URLError(.unknown)) return } - self.commentService.uploadComment(reply, success: { [weak self] in - self?.refreshCommentReplyIfNeeded() - continuation.resume(returning: TaggedManagedObjectID(reply)) - }, failure: { error in - DDLogError("Failed uploading comment reply: \(String(describing: error))") - continuation.resume(throwing: error ?? URLError(.unknown)) - }) + self.commentService.uploadComment( + reply, + success: { [weak self] in + self?.refreshCommentReplyIfNeeded() + continuation.resume(returning: TaggedManagedObjectID(reply)) + }, + failure: { error in + DDLogError("Failed uploading comment reply: \(String(describing: error))") + continuation.resume(throwing: error ?? URLError(.unknown)) + } + ) } } } diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewController.swift b/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewController.swift index cf3507afed2e..42ee07bb7a3c 100644 --- a/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewController.swift +++ b/WordPress/Classes/ViewRelated/Comments/Controllers/Create/CommentCreateViewController.swift @@ -92,15 +92,7 @@ final class CommentCreateViewController: UIViewController { else { return } - Notice( - title: Strings.commentHeldForModeration, - style: InAppUpdateNoticeStyle( - icon: UIImage(systemName: "checkmark.seal.fill"), - iconColor: UIAppColor.success, - title: Strings.commentHeldForModeration - ) - ) - .post() + Notice(title: Strings.commentHeldForModeration).post() } @objc private func buttonCancelTapped() { diff --git a/WordPress/Classes/ViewRelated/System/Notices/NoticeStyle.swift b/WordPress/Classes/ViewRelated/System/Notices/NoticeStyle.swift index a50e69f24ae5..2ec05b88d930 100644 --- a/WordPress/Classes/ViewRelated/System/Notices/NoticeStyle.swift +++ b/WordPress/Classes/ViewRelated/System/Notices/NoticeStyle.swift @@ -74,46 +74,9 @@ public struct NormalNoticeStyle: NoticeStyle { public struct InAppUpdateNoticeStyle: NoticeStyle { public let attributedMessage: NSAttributedString? - public let isDismissable: Bool - - /// - Parameters: - /// - icon: An optional SF Symbol rendered inline before `title`, e.g. a checkmark seal to indicate success. - /// - iconColor: The tint color applied to `icon`. - /// - title: When provided (with or without `icon`), builds `attributedMessage` from it and makes the - /// Notice auto-dismiss after a few seconds. When `nil`, the Notice falls back to its own `title`/`message` - /// and stays on screen until the user dismisses it, matching the original in-app-update banner behavior. - init(icon: UIImage? = nil, iconColor: UIColor = .invertedLabel, title: String? = nil) { - guard let title else { - self.attributedMessage = nil - self.isDismissable = false - return - } - - self.isDismissable = true - - let font = UIFont.boldSystemFont(ofSize: 14.0) - let message = NSMutableAttributedString() - - if let icon = icon?.withTintColor(iconColor, renderingMode: .alwaysOriginal) { - let attachment = NSTextAttachment(image: icon) - attachment.accessibilityLabel = "" // Decorative; the title text conveys the meaning. - let iconHeight = font.lineHeight - let ratio = icon.size.width / icon.size.height - attachment.bounds = CGRect( - x: 0, - y: (font.capHeight - iconHeight) / 2, - width: iconHeight * ratio, - height: iconHeight - ) - message.append(NSAttributedString(attachment: attachment)) - message.append(NSAttributedString(string: " ")) - } - - message.append( - NSAttributedString(string: title, attributes: [.font: font, .foregroundColor: UIColor.invertedLabel]) - ) - - self.attributedMessage = message + + init(attributedMessage: NSAttributedString? = nil) { + self.attributedMessage = attributedMessage } // Return new UIFont instance everytime in order to be responsive to accessibility font size changes @@ -123,6 +86,7 @@ public struct InAppUpdateNoticeStyle: NoticeStyle { public let directionalLayoutMargins = NSDirectionalEdgeInsets(top: 13.0, leading: 16.0, bottom: 13.0, trailing: 16.0) + public var isDismissable = false public let showNextArrow = false public let animationStyle = NoticeAnimationStyle.moveIn From c6b81202a7feb555045b7c7b6a9a47133c93d38b Mon Sep 17 00:00:00 2001 From: Vivek Date: Fri, 4 Sep 2026 12:27:05 +0530 Subject: [PATCH 5/5] fized Unit test issue --- .../Tests/Services/CommentService+RepliesTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/KeystoneTests/Tests/Services/CommentService+RepliesTests.swift b/Tests/KeystoneTests/Tests/Services/CommentService+RepliesTests.swift index 5c3c7b303d41..f27a85730dcd 100644 --- a/Tests/KeystoneTests/Tests/Services/CommentService+RepliesTests.swift +++ b/Tests/KeystoneTests/Tests/Services/CommentService+RepliesTests.swift @@ -134,7 +134,7 @@ final class CommentService_RepliesTests: CoreDataTestCase { // Call the reply function and wait for the HTTP API to complete let exp = expectation(description: "Reply should complete") - self.commentService.reply(to: post, content: "test comment") { + self.commentService.reply(to: post, content: "test comment") { _ in exp.fulfill() } failure: { error in XCTFail("Unexpected error: \(String(describing: error))") @@ -165,7 +165,7 @@ final class CommentService_RepliesTests: CoreDataTestCase { // Call the reply function and wait for the HTTP API to complete let exp = expectation(description: "Reply should fail") - self.commentService.reply(to: post, content: "test comment") { + self.commentService.reply(to: post, content: "test comment") { _ in XCTFail("The failure should be called instead") exp.fulfill() } failure: { _ in @@ -205,7 +205,7 @@ final class CommentService_RepliesTests: CoreDataTestCase { // Call the reply function and wait for the HTTP API to complete let exp = expectation(description: "Reply to comment should complete") - self.commentService.replyToHierarchicalComment(withID: 3, post: post, content: "test comment") { + self.commentService.replyToHierarchicalComment(withID: 3, post: post, content: "test comment") { _ in exp.fulfill() } failure: { error in XCTFail("Unexpected error: \(String(describing: error))") @@ -239,7 +239,7 @@ final class CommentService_RepliesTests: CoreDataTestCase { // Call the reply function and wait for the HTTP API to complete let exp = expectation(description: "Reply to comment should fail") - self.commentService.replyToHierarchicalComment(withID: 3, post: post, content: "test comment") { + self.commentService.replyToHierarchicalComment(withID: 3, post: post, content: "test comment") { _ in XCTFail("The failure should be called instead") exp.fulfill() } failure: { _ in