Add issue comment editing
This commit is contained in:
201
ios/Sources/CommentEditorViewController.swift
Normal file
201
ios/Sources/CommentEditorViewController.swift
Normal file
@@ -0,0 +1,201 @@
|
||||
import Highlighter
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
final class CommentEditorViewController: UIViewController, UITextViewDelegate {
|
||||
private let context: AppContext
|
||||
private let owner: String
|
||||
private let repository: String
|
||||
private let number: Int64
|
||||
private let comment: CommentRow?
|
||||
private let saved: () -> Void
|
||||
private let scrollView = UIScrollView()
|
||||
private let modeControl = UISegmentedControl(items: ["Write", "Preview"])
|
||||
private let bodyView = UITextView()
|
||||
private let previewView = UIView()
|
||||
private let spinner = UIActivityIndicatorView(style: .medium)
|
||||
private var task: Task<Void, Never>?
|
||||
private var previewController: UIViewController?
|
||||
private var editorFont: UIFont {
|
||||
UIFontMetrics(forTextStyle: .body).scaledFont(
|
||||
for: .monospacedSystemFont(ofSize: 15, weight: .regular)
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
context: AppContext,
|
||||
owner: String,
|
||||
repository: String,
|
||||
number: Int64,
|
||||
comment: CommentRow? = nil,
|
||||
saved: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.owner = owner
|
||||
self.repository = repository
|
||||
self.number = number
|
||||
self.comment = comment
|
||||
self.saved = saved
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
title = comment == nil ? "New Comment" : "Edit Comment"
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
deinit { task?.cancel() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .systemGroupedBackground
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(
|
||||
barButtonSystemItem: .cancel,
|
||||
target: self,
|
||||
action: #selector(cancel)
|
||||
)
|
||||
restoreSaveButton()
|
||||
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
scrollView.keyboardDismissMode = .interactive
|
||||
view.addSubview(scrollView)
|
||||
let stack = UIStackView(arrangedSubviews: [modeControl, bodyView, previewView])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 12
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
scrollView.addSubview(stack)
|
||||
|
||||
modeControl.selectedSegmentIndex = 0
|
||||
modeControl.accessibilityLabel = "Comment editor mode"
|
||||
modeControl.addTarget(self, action: #selector(modeChanged), for: .valueChanged)
|
||||
bodyView.delegate = self
|
||||
bodyView.text = comment?.body ?? ""
|
||||
bodyView.font = editorFont
|
||||
bodyView.adjustsFontForContentSizeCategory = true
|
||||
bodyView.backgroundColor = .secondarySystemGroupedBackground
|
||||
bodyView.layer.cornerRadius = 10
|
||||
bodyView.textContainerInset = UIEdgeInsets(top: 12, left: 8, bottom: 12, right: 8)
|
||||
bodyView.autocapitalizationType = .sentences
|
||||
bodyView.accessibilityLabel = "Comment in Markdown"
|
||||
previewView.backgroundColor = .secondarySystemGroupedBackground
|
||||
previewView.layer.cornerRadius = 10
|
||||
previewView.clipsToBounds = true
|
||||
previewView.accessibilityLabel = "Comment preview"
|
||||
previewView.isHidden = true
|
||||
highlightBody()
|
||||
updateSaveButton()
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
scrollView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor),
|
||||
stack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 16),
|
||||
stack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -16),
|
||||
stack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 16),
|
||||
stack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -16),
|
||||
stack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32),
|
||||
bodyView.heightAnchor.constraint(greaterThanOrEqualToConstant: 280),
|
||||
previewView.heightAnchor.constraint(greaterThanOrEqualToConstant: 280),
|
||||
])
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if comment == nil { bodyView.becomeFirstResponder() }
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
highlightBody()
|
||||
updateSaveButton()
|
||||
}
|
||||
|
||||
private func highlightBody() {
|
||||
let source = bodyView.text ?? ""
|
||||
let selection = bodyView.selectedRange
|
||||
let highlighter = Highlighter()
|
||||
highlighter?.setTheme(traitCollection.userInterfaceStyle == .dark ? "atom-one-dark" : "atom-one-light")
|
||||
highlighter?.theme.setCodeFont(editorFont)
|
||||
bodyView.attributedText = highlighter?.highlight(source, as: "markdown")
|
||||
?? NSAttributedString(string: source, attributes: [
|
||||
.font: editorFont,
|
||||
.foregroundColor: UIColor.label,
|
||||
])
|
||||
bodyView.selectedRange = selection
|
||||
bodyView.typingAttributes = [
|
||||
.font: editorFont,
|
||||
.foregroundColor: UIColor.label,
|
||||
]
|
||||
}
|
||||
|
||||
@objc private func modeChanged() {
|
||||
let preview = modeControl.selectedSegmentIndex == 1
|
||||
if preview {
|
||||
previewController?.willMove(toParent: nil)
|
||||
previewController?.view.removeFromSuperview()
|
||||
previewController?.removeFromParent()
|
||||
let controller = UIHostingController(
|
||||
rootView: RepositoryMarkdownPreview(source: bodyView.text ?? "")
|
||||
)
|
||||
addChild(controller)
|
||||
controller.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
previewView.addSubview(controller.view)
|
||||
NSLayoutConstraint.activate([
|
||||
controller.view.leadingAnchor.constraint(equalTo: previewView.leadingAnchor),
|
||||
controller.view.trailingAnchor.constraint(equalTo: previewView.trailingAnchor),
|
||||
controller.view.topAnchor.constraint(equalTo: previewView.topAnchor),
|
||||
controller.view.bottomAnchor.constraint(equalTo: previewView.bottomAnchor),
|
||||
])
|
||||
controller.didMove(toParent: self)
|
||||
previewController = controller
|
||||
}
|
||||
bodyView.isHidden = preview
|
||||
previewView.isHidden = !preview
|
||||
view.endEditing(true)
|
||||
}
|
||||
|
||||
@objc private func save() {
|
||||
view.endEditing(true)
|
||||
navigationItem.leftBarButtonItem?.isEnabled = false
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: spinner)
|
||||
spinner.startAnimating()
|
||||
task?.cancel()
|
||||
task = Task {
|
||||
do {
|
||||
try await context.core.saveIssueComment(
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
number: number,
|
||||
commentId: comment?.id,
|
||||
body: bodyView.text ?? ""
|
||||
)
|
||||
guard !Task.isCancelled else { return }
|
||||
saved()
|
||||
} catch {
|
||||
if !Task.isCancelled {
|
||||
show(error: error)
|
||||
restoreSaveButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func restoreSaveButton() {
|
||||
spinner.stopAnimating()
|
||||
navigationItem.leftBarButtonItem?.isEnabled = true
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
title: "Save",
|
||||
style: .done,
|
||||
target: self,
|
||||
action: #selector(save)
|
||||
)
|
||||
updateSaveButton()
|
||||
}
|
||||
|
||||
private func updateSaveButton() {
|
||||
navigationItem.rightBarButtonItem?.isEnabled = !(bodyView.text ?? "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
|
||||
@objc private func cancel() { dismiss(animated: true) }
|
||||
}
|
||||
@@ -146,15 +146,41 @@ final class IssueViewController: MarkdownPageViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
let addComment = UIBarButtonItem(
|
||||
image: context.symbol("plus.bubble"),
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(addComment)
|
||||
)
|
||||
addComment.accessibilityLabel = "Add comment"
|
||||
let editIssue = UIBarButtonItem(
|
||||
title: "Edit",
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(editIssue)
|
||||
)
|
||||
navigationItem.rightBarButtonItems = [addComment, editIssue]
|
||||
loadContent(refreshing: false)
|
||||
}
|
||||
|
||||
@objc private func addComment() { presentCommentEditor() }
|
||||
|
||||
private func editComment(_ comment: CommentRow) { presentCommentEditor(comment) }
|
||||
|
||||
private func presentCommentEditor(_ comment: CommentRow? = nil) {
|
||||
let editor = CommentEditorViewController(
|
||||
context: context,
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
number: number,
|
||||
comment: comment
|
||||
) { [weak self] in
|
||||
guard let self else { return }
|
||||
self.dismiss(animated: true) { self.loadContent(refreshing: false) }
|
||||
}
|
||||
present(UINavigationController(rootViewController: editor), animated: true)
|
||||
}
|
||||
|
||||
@objc private func editIssue() {
|
||||
let editor = IssueEditorViewController(
|
||||
context: context,
|
||||
@@ -204,6 +230,7 @@ final class IssueViewController: MarkdownPageViewController {
|
||||
meta: page.meta,
|
||||
body: page.body,
|
||||
comments: page.comments,
|
||||
editComment: { [weak self] comment in self?.editComment(comment) },
|
||||
milestone: page.milestone
|
||||
))
|
||||
finishPagination(hasMore: page.hasMore)
|
||||
@@ -791,6 +818,7 @@ private func detailViews(
|
||||
meta: String,
|
||||
body: String,
|
||||
comments: [CommentRow],
|
||||
editComment: ((CommentRow) -> Void)? = nil,
|
||||
milestone: String = ""
|
||||
) -> [UIView] {
|
||||
let titleLabel = UILabel()
|
||||
@@ -815,7 +843,7 @@ private func detailViews(
|
||||
milestoneLabel.accessibilityLabel = "Milestone \(milestone)"
|
||||
views.append(milestoneLabel)
|
||||
}
|
||||
return views + [separator(), bodyView] + commentViews(comments)
|
||||
return views + [separator(), bodyView] + commentViews(comments, editComment: editComment)
|
||||
}
|
||||
|
||||
private func issueTitle(_ titleLabel: UILabel, state: String) -> UIView {
|
||||
@@ -843,18 +871,37 @@ private func issueTitle(_ titleLabel: UILabel, state: String) -> UIView {
|
||||
return stack
|
||||
}
|
||||
|
||||
private func commentViews(_ comments: [CommentRow]) -> [UIView] {
|
||||
private func commentViews(
|
||||
_ comments: [CommentRow],
|
||||
editComment: ((CommentRow) -> Void)? = nil
|
||||
) -> [UIView] {
|
||||
guard !comments.isEmpty else { return [] }
|
||||
var views: [UIView] = [sectionLabel("Comments")]
|
||||
for comment in comments {
|
||||
let author = UILabel()
|
||||
author.text = comment.author
|
||||
author.font = .preferredFont(forTextStyle: .headline)
|
||||
var headerViews: [UIView] = [author, UIView()]
|
||||
if comment.canEdit, let editComment {
|
||||
var configuration = UIButton.Configuration.plain()
|
||||
configuration.title = "Edit"
|
||||
configuration.image = UIImage(systemName: "pencil")
|
||||
configuration.imagePadding = 5
|
||||
configuration.contentInsets = .zero
|
||||
let button = UIButton(
|
||||
configuration: configuration,
|
||||
primaryAction: UIAction { _ in editComment(comment) }
|
||||
)
|
||||
button.accessibilityLabel = "Edit comment by \(comment.author)"
|
||||
headerViews.append(button)
|
||||
}
|
||||
let header = UIStackView(arrangedSubviews: headerViews)
|
||||
header.alignment = .center
|
||||
let date = UILabel()
|
||||
date.text = comment.meta
|
||||
date.font = .preferredFont(forTextStyle: .caption1)
|
||||
date.textColor = .tertiaryLabel
|
||||
let stack = UIStackView(arrangedSubviews: [author, markdownView(comment.body), date, separator()])
|
||||
let stack = UIStackView(arrangedSubviews: [header, markdownView(comment.body), date, separator()])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 6
|
||||
views.append(stack)
|
||||
|
||||
Reference in New Issue
Block a user