chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
A pull's header shows its repository owner's picture, whoever opened it
ios/Knotless/Support/Faces.swift+6−0
| @@ -17,6 +17,12 @@ actor Faces { | ||
| ||
private var loading: [URL: Task<UIImage, Error>] = [:] | ||
| ||
private nonisolated(unsafe) static let addresses = NSCache<NSString, NSString>() | ||
| ||
/// Where an account's picture is, where an answer has said. | ||
nonisolated static func address(of did: String) -> String? { addresses.object(forKey: did as NSString) as String? } | ||
nonisolated static func keep(address: String, of did: String) { addresses.setObject(address as NSString, forKey: did as NSString) } | ||
| ||
nonisolated static func kept(_ url: URL) -> UIImage? { store.object(forKey: url as NSURL) } | ||
| ||
func load(_ url: URL) async throws -> UIImage { | ||
ios/Knotless/Views/HomeView.swift+2−0
| @@ -326,11 +326,13 @@ struct MyItemsView: View { | ||
Text("Closed").tag("closed") | ||
} | ||
} label: { chip(state == "open" ? "Open" : "Closed") } | ||
.tint(.primary) | ||
Menu { | ||
Picker("Which", selection: $relation) { | ||
ForEach(relations, id: \.0) { Text($0.1).tag($0.0) } | ||
} | ||
} label: { chip(relations.first { $0.0 == relation }?.1 ?? "") } | ||
.tint(.primary) | ||
Spacer() | ||
} | ||
.padding(.horizontal, 16) | ||
ios/Knotless/Views/ThreadView.swift+41−2
| @@ -5,6 +5,7 @@ struct ThreadView: View { | ||
let uri: String | ||
| ||
@Environment(Account.self) private var account | ||
@State private var screen: CGFloat = 0 | ||
@State private var expanded = false | ||
/// Moved by a write, so the thread is read again as it now stands. | ||
@State private var edits = 0 | ||
| @@ -49,6 +50,7 @@ struct ThreadView: View { | ||
.padding(.bottom, 8) | ||
} | ||
} | ||
.onGeometryChange(for: CGFloat.self, of: \.size.width) { screen = $0 } | ||
.alert("That didn't work", isPresented: Binding(get: { failure != nil }, set: { if !$0 { failure = nil } })) { | ||
Button("OK", role: .cancel) {} | ||
} message: { | ||
| @@ -61,7 +63,8 @@ struct ThreadView: View { | ||
Text(title).font(.subheadline.weight(.semibold)).lineLimit(1) | ||
Text(repo.label).font(.caption).foregroundStyle(.secondary).lineLimit(1) | ||
} | ||
.frame(maxWidth: .infinity, alignment: .leading) | ||
// A bar doesn't keep it off the buttons beside it when it's long. | ||
.frame(width: titleWidth, alignment: .leading) | ||
} | ||
} | ||
if let url = pageURL { | ||
| @@ -103,6 +106,12 @@ struct ThreadView: View { | ||
return URL(string: "https://knotless.dev/\(repo.handle)/\(repo.name)/\(kind)/\(parts[at - 1])/\(parts[at + 1])") | ||
} | ||
| ||
/// The screen, less the back button on one side and the bar's buttons on the other. | ||
private var titleWidth: CGFloat { | ||
let buttons: CGFloat = (pageURL == nil ? 0 : 1) + (canSetState == true && state != "merged" ? 1 : 0) | ||
return max(0, screen - 76 - 16 - buttons * 52) | ||
} | ||
| ||
private func content(_ thread: PullThread) -> some View { | ||
ScrollView { | ||
VStack(spacing: 0) { | ||
| @@ -130,7 +139,7 @@ struct ThreadView: View { | ||
return VStack(alignment: .leading, spacing: 12) { | ||
NavigationLink(value: Route.repo(repo)) { | ||
HStack(spacing: 6) { | ||
Avatar(url: item.author.did == repo.did ? item.author.avatar : nil, name: repo.handle, did: repo.did, size: 24) | ||
OwnerAvatar(repo: repo, known: item.author.did == repo.did ? item.author.avatar : nil) | ||
Text(repo.handle).lineLimit(1) | ||
Text("/").foregroundStyle(.secondary) | ||
Text(repo.name).lineLimit(1) | ||
| @@ -449,6 +458,36 @@ struct CommentRow: View { | ||
} | ||
} | ||
| ||
/// A repository's owner's picture. A thread's answer doesn't name it, so unless | ||
/// its author is the owner it's asked for, once while the app runs. A plain | ||
/// ground until the answer, as the identicon would flash first. | ||
private struct OwnerAvatar: View { | ||
let repo: RepoRef | ||
let known: String? | ||
@State private var answered = false | ||
@State private var asked: String? | ||
| ||
private var url: String? { known ?? asked ?? Faces.address(of: repo.did) } | ||
| ||
var body: some View { | ||
Group { | ||
if url != nil || answered { | ||
Avatar(url: url, name: repo.handle, did: repo.did, size: 24) | ||
} else { | ||
Circle().fill(Color(.tertiarySystemFill)).frame(width: 24, height: 24) | ||
} | ||
} | ||
.task(id: repo.did) { | ||
guard url == nil else { return } | ||
let profile: Profile? = try? await Client.shared.query("getProfile", ["actor": repo.did]) | ||
guard !Task.isCancelled else { return } | ||
if let avatar = profile?.avatar { Faces.keep(address: avatar, of: repo.did) } | ||
asked = profile?.avatar | ||
answered = true | ||
} | ||
} | ||
} | ||
| ||
/// The lines of a pull's diff a comment is on, as they were when it was | ||
/// written: those it names marked, and for one alone a few before it. | ||
private struct LineQuote: View { | ||