Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions WordPress/Classes/Services/CommentService.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 12 additions & 4 deletions WordPress/Classes/Services/CommentService.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -993,22 +993,24 @@ private extension CommentDetailViewController {

@objc func buttonAddCommentTapped() {
let viewModel = CommentCreateViewModel(replyingTo: comment) { [weak self] in
try await self?.createReply(content: $0)
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: The object ID of the newly created reply.
@MainActor
func createReply(content: String) async throws {
isNotificationComment ? WPAppAnalytics.track(.notificationsCommentRepliedTo) :
CommentAnalytics.trackCommentRepliedTo(comment: comment)
func createReply(content: String) async throws -> TaggedManagedObjectID<Comment> {
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: UnsafeContinuation<Void, Error>) in
Expand All @@ -1020,7 +1022,7 @@ private extension CommentDetailViewController {
}
self.commentService.uploadComment(reply, success: { [weak self] in
self?.refreshCommentReplyIfNeeded()
continuation.resume()
continuation.resume(returning: TaggedManagedObjectID(reply))
}, failure: { error in
DDLogError("Failed uploading comment reply: \(String(describing: error))")
continuation.resume(throwing: error ?? URLError(.unknown))
Expand All @@ -1029,22 +1031,30 @@ private extension CommentDetailViewController {
}
}

/// - returns: The object ID of the newly created reply.
@MainActor
func createPostCommentReply(content: String) async throws {
func createPostCommentReply(content: String) async throws -> TaggedManagedObjectID<Comment> {
guard let post = comment.post as? ReaderPost else {
return
throw URLError(.unknown)
}
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()
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))")
continuation.resume(throwing: error ?? URLError(.unknown))
}
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import UIKit
import WordPressData
import WordPressUI

final class CommentCreateViewController: UIViewController {
Expand Down Expand Up @@ -64,10 +65,13 @@ final class CommentCreateViewController: UIViewController {
Task { @MainActor in
do {
let text = await editorVC.text
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)
presentingViewController?
.dismiss(animated: true) { [weak self] in
self?.showModerationNoticeIfNeeded(for: commentID)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This notice does not apply to all sites, right? Some site you can just publish without being reviewed. Like, if you are member of another site, your comments on that site should go straight to approved?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok will update according to that 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated Code, now toast will show only when comment is unapproved.

The core problem: the composer had no way to know whether a comment was actually held for moderation. I traced the real status through the whole call chain and made it flow back to the UI.

} catch {
setLoading(false)
UINotificationFeedbackGenerator().notificationOccurred(.error)
Expand All @@ -82,6 +86,23 @@ final class CommentCreateViewController: UIViewController {
editorVC.isEnabled = !isLoading
}

private func showModerationNoticeIfNeeded(for commentID: TaggedManagedObjectID<Comment>) {
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
Expand All @@ -105,9 +126,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
Expand Down Expand Up @@ -147,6 +169,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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ 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
private var _save: (String) async throws -> TaggedManagedObjectID<Comment> = { _ in
wpAssertionFailure("Not implemented")
throw URLError(.unknown)
}

var isGutenbergEnabled: Bool {
Expand All @@ -49,12 +50,13 @@ final class CommentCreateViewModel {
}

self._save = { [weak self] in
try await self?.sendComment($0, post: post, replyingTo: comment)
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 -> Void) {
init(replyingTo comment: Comment, save: @escaping (String) async throws -> TaggedManagedObjectID<Comment>) {
let siteID = comment.associatedSiteID ?? 0

self.siteID = siteID
Expand All @@ -72,27 +74,46 @@ final class CommentCreateViewModel {
Strings.leaveComment
}

func save(content: String) async throws {
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<Comment> {
let commentID = try await _save(content)
deleteDraft()
return commentID
}

// 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 -> TaggedManagedObjectID<Comment> {
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()
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) {
service.reply(to: post, content: content) { newComment in
self?.trackReply(isReplyingToComment: true, post: post)
continuation.resume()
guard let newComment else {
continuation.resume(throwing: URLError(.unknown))
return
}
continuation.resume(returning: TaggedManagedObjectID(newComment))
} failure: {
continuation.resume(throwing: $0 ?? URLError(.unknown))
}
Expand Down
45 changes: 41 additions & 4 deletions WordPress/Classes/ViewRelated/System/Notices/NoticeStyle.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import UIKit
import WordPressShared
import DesignSystem

public enum NoticeAnimationStyle {
case moveIn
Expand Down Expand Up @@ -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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason for adding this new initializer? Does the default Notice work?

@Vivek09Chahal Vivek09Chahal Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

init already existed for this struct it just got updated, to reuse the existing notification item, to reuse it I have to add 2 new value in here

  1. Image: UIImage
  2. Image Color: UIColor

And before we were only accepting NSAttributedString changed to String it will accept string now instead of NSAttributedString and it was already optional.

Before it didn't have support of the image. Now it do, Both Optional, as if we want to use old style, we no need to pass down those value and it will set back to previous version

@Vivek09Chahal Vivek09Chahal Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just on clarification, before this Notice was not dismissible, it stays on the screen until user removed it, its value was set to false, now it can get auto-dismiss after few seconds.

I can revert this just one line change, if there is no need.

Please clarify on this one.

@crazytonyli crazytonyli Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reusing the existing style should be fine, like Notice(title:...). The notice auto dismisses after a few seconds, and users can tap it to dismiss it, which should all be the default UX.

@Vivek09Chahal Vivek09Chahal Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, that is how it's working, either it will dismiss after sometime, or user can dismiss it before that time.

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
Expand All @@ -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
Expand Down