~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

iOS app edits and deletes one's own comments and opens authors' profiles

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:ios/comment-actions
.changeset/owner-watches-private.mdnew+7−0
@@ -0,0 +1,7 @@
---
default: minor
---
# An agent watching an owner finds their private repositories shared with it
`knotless repo share` leaves the account shared with an invite at the appview, and `knotless-agent --owner <handle>` watches, besides that account's public repositories, every private one it shared with the agent's account, so a reviewer need not be told each private repository by name.
ios/Knotless/API/Writes.swift+5−0
@@ -99,6 +99,11 @@ enum Writes {
}
/// The runners run a pull's head, or a ref, again: all its workflows, or `workflow`.
/// Takes back one's own comment or review.
static func deleteComment(repo: RepoRef, subject: String) -> Write {
Write(method: "deleteComment", input: ["repo": repo.uri, "subject": subject])
}
ios/Knotless/API/Writes.swiftline 105
review.knotless.dev

rerun's doc comment ("The runners run a pull's head…") sits above deleteComment's own, which leaves rerun without a description. Put deleteComment above that comment.

static func rerun(repo: RepoRef, pull: String?, ref: String?, workflow: String?) -> Write {
var input = ["repo": repo.uri]
if let pull { input["pull"] = pull } else if let ref { input["ref"] = ref }
ios/Knotless/Views/ThreadView.swift+42−4
@@ -482,22 +482,37 @@ struct CommentRow: View {
@Environment(Account.self) private var account
@State private var reporting: Reporting?
@State private var editing = false
@State private var deleting = false
@State private var failure: String?
private var mine: Bool { account.signedIn && account.person?.did == comment.author.did }
var body: some View {
VStack(alignment: .leading, spacing: Space.s2) {
HStack(spacing: Space.s2) {
Avatar(url: comment.author.avatar, name: comment.author.handle, did: comment.author.did, size: Size.Avatar.small)
Text(comment.author.handle).font(.subheadline.weight(.medium)).lineLimit(1)
NavigationLink(value: Route.profile(comment.author.handle)) {
HStack(spacing: Space.s2) {
Avatar(url: comment.author.avatar, name: comment.author.handle, did: comment.author.did, size: Size.Avatar.small)
Text(comment.author.handle).font(.subheadline.weight(.medium)).lineLimit(1)
}
}
.buttonStyle(.plain)
Text(When.short(comment.createdAt)).font(.subheadline).foregroundStyle(.secondary)
if comment.verdict == "approve" {
Oct(name: "check-circle-fill").foregroundStyle(Color.success).accessibilityLabel("Approved")
} else if comment.verdict == "changes" {
Oct(name: "alert-fill").foregroundStyle(Color.danger).accessibilityLabel("Asked for changes")
}
if account.signedIn, account.person?.did != comment.author.did, let uri = comment.uri {
if account.signedIn, let uri = comment.uri {
Spacer(minLength: 0)
Menu {
Button("Report this comment") { reporting = Reporting(subject: uri, what: "this comment") }
if mine {
Button("Edit") { editing = true }
Button("Delete", role: .destructive) { deleting = true }
} else {
Button("Report this comment") { reporting = Reporting(subject: uri, what: "this comment") }
}
} label: {
Oct(name: "kebab-horizontal")
.foregroundStyle(.secondary)
@@ -527,6 +542,29 @@ struct CommentRow: View {
.padding(.leading, comment.reply == nil ? 16 : 40)
.frame(maxWidth: .infinity, alignment: .leading)
.sheet(item: $reporting) { ReportSheet(reporting: $0) }
.sheet(isPresented: $editing) {
if let uri = comment.uri {
Composer(repo: repo, uri: uri, editing: comment.body ?? "", sent: changed)
}
}
.confirmationDialog("Delete this comment for good?", isPresented: $deleting, titleVisibility: .visible) {
Button("Delete", role: .destructive) { Task { await delete() } }
}
.alert("That didn't work", isPresented: Binding(get: { failure != nil }, set: { if !$0 { failure = nil } })) {
Button("OK", role: .cancel) {}
} message: {
Text(failure ?? "")
}
}
private func delete() async {
guard let uri = comment.uri else { return }
do {
try await Client.shared.send(Writes.deleteComment(repo: repo, subject: uri))
changed()
} catch {
failure = error.localizedDescription
}
}
}
ios/Knotless/Views/Writing.swift+14−6
@@ -1,9 +1,10 @@
import SwiftUI
/// Writes a comment.
/// Writes a comment, or with `editing`, changes one's body: `uri` is then the comment's.
struct Composer: View {
let repo: RepoRef
let uri: String
var editing: String? = nil
let sent: () -> Void
@Environment(\.dismiss) private var dismiss
@@ -35,19 +36,22 @@ struct Composer: View {
}
}
.padding()
.navigationTitle("Comment")
.navigationTitle(editing == nil ? "Comment" : "Edit comment")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("Send") { Task { await send() } }
Button(editing == nil ? "Send" : "Save") { Task { await send() } }
.disabled(sending || empty)
}
}
.onAppear { focused = true }
.interactiveDismissDisabled(!empty)
.onAppear {
if let editing, text.isEmpty { text = editing }
focused = true
}
.interactiveDismissDisabled(editing == nil ? !empty : text != editing)
}
}
@@ -56,7 +60,11 @@ struct Composer: View {
failure = nil
defer { sending = false }
do {
try await Client.shared.send(Writes.comment(repo: repo, subject: uri, body: text, verdict: nil))
if editing != nil {
try await Client.shared.send(Writes.edit(repo: repo, subject: uri, body: text))
} else {
try await Client.shared.send(Writes.comment(repo: repo, subject: uri, body: text, verdict: nil))
}
sent()
dismiss()
} catch {