Skip to content

Commit

Permalink
Comment Composer: Reply Preview & Drafts (#24075)
Browse files Browse the repository at this point in the history
* Show the comment you are replying to in the comment composer

* Add support for saving drafts

* Remove unused NotificationReplyStore
  • Loading branch information
kean authored Feb 14, 2025
1 parent 117204b commit 4c03aa8
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 227 deletions.
4 changes: 2 additions & 2 deletions Modules/Sources/WordPressUI/Views/SeparatorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import UIKit
public final class SeparatorView: UIView {
public static func horizontal() -> SeparatorView {
let view = SeparatorView()
view.heightAnchor.constraint(equalToConstant: 0.5).isActive = true
view.heightAnchor.constraint(equalToConstant: 0.33).isActive = true
return view
}

public static func vertical() -> SeparatorView {
let view = SeparatorView()
view.widthAnchor.constraint(equalToConstant: 0.5).isActive = true
view.widthAnchor.constraint(equalToConstant: 0.33).isActive = true
return view
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import UIKit
import WordPressUI

final class CommentComposerReplyCommentView: UIView, UITableViewDataSource {
private let tableView = UITableView(frame: .zero, style: .plain)
private let comment: Comment
private let helper = ReaderCommentsHelper()
private lazy var heightConstraints = tableView.heightAnchor.constraint(equalToConstant: 120)

init(comment: Comment) {
self.comment = comment

super.init(frame: .zero)

addSubview(tableView)
tableView.pinEdges()

tableView.separatorStyle = .none
tableView.alwaysBounceVertical = false
tableView.register(CommentContentTableViewCell.defaultNib, forCellReuseIdentifier: CommentContentTableViewCell.defaultReuseID)

tableView.dataSource = self
heightConstraints.isActive = true
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
1
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CommentContentTableViewCell.defaultReuseID) as! CommentContentTableViewCell
cell.configure(viewModel: .init(comment: comment), helper: helper) { [weak self, weak cell] _ in
guard let self, let cell else { return }
self.didUpdateHeight(cell)
}
cell.hideActions()
return cell
}

private func didUpdateHeight(_ cell: CommentContentTableViewCell) {
UIView.performWithoutAnimation {
tableView.performBatchUpdates({})
heightConstraints.constant = min(130, cell.systemLayoutSizeFitting(bounds.size)
.height + 8) // bottom padding
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ final class CommentComposerViewController: UIViewController {
return configuration
}())

private let contentView = UIStackView(axis: .vertical, [])
private var editor: CommentEditor?
private let viewModel: CommentComposerViewModel

Expand All @@ -29,7 +30,7 @@ final class CommentComposerViewController: UIViewController {

view.backgroundColor = .systemBackground

setupEditor()
setupView()
setupNavigationBar()
setupAccessibility()

Expand All @@ -42,37 +43,53 @@ final class CommentComposerViewController: UIViewController {
WPAnalytics.track(.commentFullScreenEntered)
}

private func setupView() {
view.addSubview(contentView)
contentView.pinEdges([.top, .horizontal], to: view.safeAreaLayoutGuide)
contentView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor).isActive = true

if let comment = viewModel.comment {
let preview = CommentComposerReplyCommentView(comment: comment)
contentView.addArrangedSubview(preview)

let separator = SeparatorView.horizontal()
contentView.addArrangedSubview(separator)
}

setupEditor()
}

private func setupEditor() {
let content = viewModel.restoreDraft() ?? ""

if viewModel.isGutenbergEnabled {
setupGutenbergEditor()
setupGutenbergEditor(content: content)
} else {
setupPlainTextEditor()
setupPlainTextEditor(content: content)
}
}

private func setupPlainTextEditor() {
private func setupPlainTextEditor(content: String) {
let editorVC = CommentPlainTextEditorViewController()
editorVC.suggestionsViewModel = viewModel.suggestionsViewModel
editorVC.placeholder = viewModel.placeholder
editorVC.text = content
editorVC.delegate = self

addChild(editorVC)
view.addSubview(editorVC.view)
editorVC.view.pinEdges([.top, .horizontal], to: view.safeAreaLayoutGuide)
editorVC.view.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor).isActive = true

contentView.addArrangedSubview(editorVC.view)
editorVC.didMove(toParent: self)

self.editor = editorVC
}

private func setupGutenbergEditor() {
private func setupGutenbergEditor(content: String) {
let editorVC = CommentGutenbergEditorViewController()
editorVC.delegate = self
editorVC.initialContent = content

addChild(editorVC)
view.addSubview(editorVC.view)
editorVC.view.pinEdges()
contentView.addArrangedSubview(editorVC.view)
editorVC.didMove(toParent: self)

self.editor = editorVC
Expand Down Expand Up @@ -114,20 +131,26 @@ final class CommentComposerViewController: UIViewController {
if text.isEmpty {
presentingViewController?.dismiss(animated: true)
} else {
showCloseDraftConfirmationAlert()
showCloseDraftConfirmationAlert(content: text)
}
}

private func showCloseDraftConfirmationAlert() {
private func showCloseDraftConfirmationAlert(content: String) {
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
alert.addCancelActionWithTitle(Strings.closeConfirmationAlertCancel)
alert.addDestructiveActionWithTitle(Strings.closeConfirmationAlertDelete) { [weak self] _ in
self?.viewModel.deleteDraft()
self?.presentingViewController?.dismiss(animated: true)
}
// TODO: (kean) implement draft saving
// alert.addActionWithTitle(Strings.closeConfirmationAlertSaveDraft, style: .default) { _ in
//
// }
if viewModel.canSaveDraft {
alert.addActionWithTitle(Strings.closeConfirmationAlertSaveDraft, style: .default) { [weak self] _ in
self?.viewModel.saveDraft(content)
self?.presentingViewController?.dismiss(animated: true) {
UINotificationFeedbackGenerator().notificationOccurred(.success)
Notice(title: Strings.draftSaved).post()
}
}
}
alert.popoverPresentationController?.barButtonItem = navigationItem.leftBarButtonItem
present(alert, animated: true, completion: nil)
}
Expand Down Expand Up @@ -167,4 +190,5 @@ private enum Strings {
static let closeConfirmationAlertCancel = NSLocalizedString("commentComposer.closeConfirmationAlert.keepEditing", value: "Keep Editing", comment: "Button to keep the changes in an alert confirming discaring changes")
static let closeConfirmationAlertDelete = NSLocalizedString("commentComposer.closeConfirmationAlert.deleteDraft", value: "Delete Draft", comment: "Button in an alert confirming discaring a new draft")
static let closeConfirmationAlertSaveDraft = NSLocalizedString("commentComposer.closeConfirmationAlert.saveDraft", value: "Save Draft", comment: "Button in an alert confirming saving a new draft")
static let draftSaved = NSLocalizedString("commentComposer.draftSaved", value: "Draft Saved", comment: "Cofirmation snackbar title")
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ final class CommentComposerViewModel {
wpAssertionFailure("must be specified")
}

/// Comment you are replying it.
var comment: Comment?

private let parameters: CommentComposerParameters
private var context: NSManagedObjectContext

Expand Down Expand Up @@ -45,6 +48,7 @@ final class CommentComposerViewModel {
)

self.init(parameters: parameters, suggestionsViewModel: suggestionsViewModel)
self.comment = comment
}

init(
Expand Down Expand Up @@ -74,6 +78,34 @@ final class CommentComposerViewModel {
static var leaveCommentLocalizedPlaceholder: String {
Strings.leaveComment
}

// MARK: Drafts

func restoreDraft() -> String? {
guard let key = makeDraftKey() else { return nil }
return UserDefaults.standard.string(forKey: key)
}

var canSaveDraft: Bool {
makeDraftKey() != nil
}

func saveDraft(_ content: String) {
guard let key = makeDraftKey() else { return }
return UserDefaults.standard.set(content, forKey: key)
}

func deleteDraft() {
guard let key = makeDraftKey() else { return }
UserDefaults.standard.removeObject(forKey: key)
}

private func makeDraftKey() -> String? {
guard let userID = (try? WPAccount.lookupDefaultWordPressComAccount(in: context))?.userID else {
return nil
}
return "CommentDraft-\(userID),\(parameters.siteID),\(comment?.commentID ?? 0)"
}
}

struct CommentComposerParameters {
Expand All @@ -89,6 +121,12 @@ struct CommentComposerParameters {
}
}

private struct CommentID {
let userID: NSNumber
let siteID: NSNumber
let commentID: NSNumber?
}

private enum Strings {
static let reply = NSLocalizedString("commentComposer.navigationTitleReply", value: "Reply", comment: "Navigation bar title when leaving a reply to a comment")
static let comment = NSLocalizedString("commentComposer.navigationTitleComment", value: "Comment", comment: "Navigation bar title when leaving a reply to a comment")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ final class CommentGutenbergEditorViewController: UIViewController, CommentEdito

weak var delegate: CommentEditorDelegate?

var initialContent: String?

var text: String {
set {
wpAssertionFailure("not supported")
Expand Down Expand Up @@ -43,6 +45,7 @@ final class CommentGutenbergEditorViewController: UIViewController, CommentEdito
super.viewDidLoad()

let editorVC = GutenbergKit.EditorViewController(
content: initialContent ?? "",
service: EditorService(client: EmptyNetworkClient())
)
editorVC.delegate = self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,15 +203,18 @@ class CommentContentTableViewCell: UITableViewCell, NibReusable {
func configureForPostDetails(with comment: Comment, helper: ReaderCommentsHelper, onContentLoaded: ((CGFloat) -> Void)?) {
configure(viewModel: CommentCellViewModel(comment: comment), helper: helper, onContentLoaded: onContentLoaded)

replyButton.isHidden = true
likeButton.isHidden = true

isAccessoryButtonEnabled = false
hideActions()

containerStackLeadingConstraint.constant = 0
containerStackTrailingConstraint.constant = 0
}

func hideActions() {
replyButton.isHidden = true
likeButton.isHidden = true
isAccessoryButtonEnabled = false
}

@objc func ensureRichContentTextViewLayout() {
switch renderMethod {
case .richContent:
Expand Down
Loading

0 comments on commit 4c03aa8

Please sign in to comment.