chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
iOS app edits and deletes one's own comments and opens authors' profiles
.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
| ||
| ||
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 { | ||