Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ enum CommentChangeEvent: Equatable, Sendable {
/// them; stales rather than inserts because a paged list cannot know the
/// reply's correct position.
case replyCreated(parentID: Int64, replyStatus: CommentListItem.Status)
/// A comment's content was edited. Carries `contentRaw` so an open detail
/// screen keeps a fresh raw value for a subsequent edit; list rows only
/// need `contentHTML` to refresh their snippet.
case contentChanged(id: Int64, contentHTML: String, contentRaw: String?)
}

extension CommentChangeEvent {
Expand All @@ -21,6 +25,7 @@ extension CommentChangeEvent {
case .statusChanged(let id, _): id
case .deleted(let id): id
case .replyCreated(let parentID, _): parentID
case .contentChanged(let id, _, _): id
}
}
}
9 changes: 8 additions & 1 deletion Modules/Sources/WordPressComments/Models/CommentDetail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ struct CommentDetail: Equatable, Sendable {
let authorIP: String? // edit context only
let postID: Int64
let parentID: Int64? // nil when the wire value is 0 (top-level)
let contentHTML: String
var contentHTML: String
var contentRaw: String? // edit context only
let link: URL?
let date: Date
var status: CommentListItem.Status
Expand All @@ -33,6 +34,7 @@ struct CommentDetail: Equatable, Sendable {
postID: comment.post,
parentID: comment.parent,
contentHTML: comment.content.rendered,
contentRaw: nil,
link: comment.link,
date: comment.dateGmt,
status: CommentListItem.Status(comment.status),
Expand All @@ -51,6 +53,7 @@ struct CommentDetail: Equatable, Sendable {
postID: comment.post,
parentID: comment.parent,
contentHTML: comment.content.rendered,
contentRaw: comment.content.raw,
link: comment.link,
date: comment.dateGmt,
status: CommentListItem.Status(comment.status),
Expand All @@ -68,6 +71,7 @@ struct CommentDetail: Equatable, Sendable {
postID: Int64,
parentID: Int64,
contentHTML: String,
contentRaw: String?,
link: String,
date: Date,
status: CommentListItem.Status,
Expand All @@ -84,6 +88,7 @@ struct CommentDetail: Equatable, Sendable {
self.postID = postID
self.parentID = parentID == 0 ? nil : parentID
self.contentHTML = contentHTML
self.contentRaw = contentRaw
self.link = link.nonEmptyString().flatMap { URL(string: $0) }
self.date = date
self.status = status
Expand All @@ -102,6 +107,7 @@ extension CommentDetail {
parentID: Int64 = 0,
contentHTML: String =
"<p>Really appreciate the detailed writeup. This is exactly the kind of review I was hoping to find before committing to the upgrade.</p>",
contentRaw: String? = "preview raw",
hasEditContext: Bool = true
) -> CommentDetail {
CommentDetail(
Expand All @@ -114,6 +120,7 @@ extension CommentDetail {
postID: 10,
parentID: parentID,
contentHTML: contentHTML,
contentRaw: contentRaw,
link: "https://example.com/?p=10#comment-\(id)",
date: Date(timeIntervalSince1970: 1_700_000_000),
status: status,
Expand Down
12 changes: 9 additions & 3 deletions Modules/Sources/WordPressComments/Models/CommentListItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ struct CommentListItem: Identifiable, Equatable, Sendable {
let authorName: String
let avatarURL: URL?
let postID: Int64
let snippet: String
var snippet: String
let date: Date
var status: Status
/// The comment's permalink; nil when the server sends none.
let link: URL?

init(
id: Int64,
Expand All @@ -29,7 +31,8 @@ struct CommentListItem: Identifiable, Equatable, Sendable {
postID: Int64,
snippet: String,
date: Date,
status: Status
status: Status,
link: URL?
) {
self.id = id
self.authorName = authorName
Expand All @@ -38,6 +41,7 @@ struct CommentListItem: Identifiable, Equatable, Sendable {
self.snippet = snippet
self.date = date
self.status = status
self.link = link
}

init(comment: CommentWithViewContext) {
Expand All @@ -48,6 +52,7 @@ struct CommentListItem: Identifiable, Equatable, Sendable {
snippet = Self.snippet(fromHTML: comment.content.rendered)
date = comment.dateGmt
status = Status(comment.status)
link = comment.link.nonEmptyString().flatMap { URL(string: $0) }
}

/// Row-shaped projection of a fetched detail (used for the parent preview
Expand All @@ -60,7 +65,8 @@ struct CommentListItem: Identifiable, Equatable, Sendable {
postID: detail.postID,
snippet: Self.snippet(fromHTML: detail.contentHTML),
date: detail.date,
status: detail.status
status: detail.status,
link: detail.link
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,32 @@ final class CommentsModerationCoordinator {
}
}

/// Replaces the comment's content. Pessimistic and reconcile-free: a
/// thrown edit may still have landed (timeout-after-commit), but a retry
/// re-sends this user's content and comment edits are last-writer-wins,
/// matching wp-admin (which locks posts but not comments). Accepted
/// limitation; see the design doc. The response also carries the
/// authoritative server status; a status correction is emitted alongside
/// the content change when it disagrees with `comment.status`.
func editContent(on comment: CommentDetail, newContent: String) async throws -> CommentDetail {
try await holdingSlot(for: comment.id, waitingForSlot: true) { [weak self] in
guard let self else { throw CancellationError() }
let updated = try await self.service.updateContent(id: comment.id, content: newContent)
self.events.send(
.contentChanged(id: comment.id, contentHTML: updated.contentHTML, contentRaw: updated.contentRaw)
)
// Editing content never changes status server-side, so this only
// fires when a concurrent moderator or plugin changed it while the
// editor was open; the correction keeps Reply gating and list-tab
// membership from going stale.
if updated.status != comment.status {
self.events.send(.statusChanged(id: comment.id, to: updated.status))
}
self.tracker?.track(.edited(commentID: comment.id, postID: comment.postID))
return updated
}
}

/// Broadcasts a status change the detail screen observed on load (its seed
/// status disagreed with the fetched truth) without running a mutation, so
/// loaded list tabs reconcile the corrected status in place.
Expand Down
11 changes: 11 additions & 0 deletions Modules/Sources/WordPressComments/Services/CommentsService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ protocol CommentsServiceProtocol: Sendable {
/// Creates a reply to `parentID` on `postID` and returns the created
/// comment's detail.
func createReply(postID: Int64, parentID: Int64, content: String) async throws -> CommentDetail

/// Updates the comment's content and returns the updated detail.
func updateContent(id: Int64, content: String) async throws -> CommentDetail
}

/// Errors raised by `CommentsService` that don't originate from wordpress-rs.
Expand Down Expand Up @@ -182,6 +185,14 @@ final class CommentsService: CommentsServiceProtocol {
)
return CommentDetail(comment: response.data)
}

func updateContent(id: Int64, content: String) async throws -> CommentDetail {
let response = try await client.api.comments.update(
commentId: id,
params: CommentUpdateParams(content: content)
)
return CommentDetail(comment: response.data)
}
}

extension WpApiError {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ public enum CommentsTrackedEvent: Equatable, Sendable {
// Permanent delete: legacy has no analytics event; deliberately untracked.
/// A reply was successfully created, matching legacy's reply-sent event.
case repliedTo(commentID: Int64, postID: Int64)
/// The content editor was opened for a comment, matching legacy's
/// edit-entry event.
case editorOpened(commentID: Int64, postID: Int64)
/// A comment's content was successfully edited, matching legacy's
/// edit-saved event.
case edited(commentID: Int64, postID: Int64)
}

public protocol CommentsTracker: Sendable {
Expand Down
30 changes: 30 additions & 0 deletions Modules/Sources/WordPressComments/Strings/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,12 @@ enum Strings {
comment: "Title of the compose screen for replying to a comment"
)

static let composerEditTitle = NSLocalizedString(
"commentComposer.title.edit",
value: "Edit Comment",
comment: "Title of the compose screen for editing a comment"
)

static let composerPlaceholder = NSLocalizedString(
"commentComposer.placeholder",
value: "Leave a reply…",
Expand All @@ -261,6 +267,12 @@ enum Strings {
comment: "Button label to send a new reply"
)

static let composerSave = NSLocalizedString(
"commentComposer.action.save",
value: "Save",
comment: "Button label to save changes to an edited comment"
)

static let composerCancel = NSLocalizedString(
"commentComposer.action.cancel",
value: "Cancel",
Expand Down Expand Up @@ -291,6 +303,12 @@ enum Strings {
comment: "Button label to continue editing instead of discarding changes"
)

static let composerDiscardChanges = NSLocalizedString(
"commentComposer.action.discardChanges",
value: "Discard Changes",
comment: "Button label to discard unsaved changes to a comment"
)

static let composerErrorClosed = NSLocalizedString(
"commentComposer.error.closed",
value: "Comments are closed for this post.",
Expand All @@ -303,6 +321,12 @@ enum Strings {
comment: "Error message shown when sending a reply fails"
)

static let composerErrorEditFailed = NSLocalizedString(
"commentComposer.error.editFailed",
value: "Failed to save changes.",
comment: "Error message shown when editing a comment fails"
)

static let noticeReplySent = NSLocalizedString(
"commentComposer.notice.replySent",
value: "Reply sent.",
Expand All @@ -326,4 +350,10 @@ enum Strings {
value: "Reply",
comment: "Button label to reply to a comment on the detail screen"
)

static let detailEdit = NSLocalizedString(
"commentDetail.action.edit",
value: "Edit",
comment: "Button label to edit a comment on the detail screen"
)
}
Loading