~ 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

The iOS app's home and search follow GitHub's, and comments take reactions

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:ios/home
ios/Knotless/API/Models.swift+113−0
@@ -246,6 +246,93 @@ struct Commit: Decodable, Identifiable, Hashable {
}
}
/// What was searched for, on this phone, newest first, as one string to keep.
enum RecentSearches {
static let key = "recentSearches"
private static let most = 10
static func decode(_ text: String) -> [String] { text.split(separator: "\n").map(String.init) }
static func adding(_ query: String, to text: String) -> String {
let clean = query.replacing("\n", with: " ").trimmingCharacters(in: .whitespaces)
guard !clean.isEmpty else { return text }
return ([clean] + decode(text).filter { $0 != clean }).prefix(most).joined(separator: "\n")
}
}
/// `listActorItems`: pulls or issues, each with the repository it's on.
struct ActorItems: Decodable, Sendable {
let items: [ActorItem]
var cursor: String?
}
struct ActorItem: Decodable, Identifiable, Hashable, Sendable {
let item: Item
let repo: RepoRef
var id: String { item.uri }
}
/// `searchCode`.
struct CodeSearch: Decodable, Sendable {
var files: [File]?
var cursor: String?
struct File: Decodable, Identifiable, Hashable, Sendable {
let repo: RepoHit
let path: String
var language: String?
var chunks: [Chunk]?
var id: String { repo.did + "/" + repo.name + "/" + path }
}
struct Chunk: Decodable, Hashable, Sendable {
let startLine: Int
let content: String
var ranges: [Match]?
var highlights: String?
}
struct Match: Decodable, Hashable, Sendable {
let line: Int
let byteStart: Int
let byteEnd: Int
}
}
/// A repository in `listRepos` or `searchCode`, whose owner may not be named.
struct RepoHit: Decodable, Hashable, Sendable {
let did: String
let name: String
var description: String?
var owner: Person?
var `private`: Bool?
var starCount: Int?
var language: Language?
var ref: RepoRef { RepoRef(did: did, handle: owner?.handle ?? did, name: name) }
var repo: Repo {
Repo(
uri: "at://\(did)/dev.knotless.repo/\(name)",
owner: owner ?? Person(did: did, handle: did),
name: name, description: description, starCount: starCount, private: `private`, language: language)
}
}
struct RepoSearch: Decodable, Sendable {
let repos: [RepoHit]
var cursor: String?
}
struct OwnerSearch: Decodable, Sendable {
let owners: [Found]
struct Found: Decodable, Hashable, Sendable {
let owner: Person
let repoCount: Int
}
}
struct CommitAnswer: Decodable {
let commit: Commit
var files: [DiffFile] = []
@@ -270,6 +357,7 @@ struct Comment: Decodable, Identifiable, Hashable {
/// The comment starting the thread on those lines that this answers.
var reply: String?
var resolved: Bool?
var reactions: [ReactionCount]?
var id: String { uri ?? (author.did + createdAt) }
struct Lines: Decodable, Hashable {
@@ -456,6 +544,17 @@ struct FeedEvent: Decodable, Identifiable, Hashable {
let actor: Person
let repo: RepoRef
let action: Action
/// The pulls and issues among `events`, each once, by what last happened.
static func recent(_ events: [FeedEvent], most: Int = 8) -> [FeedEvent] {
var seen = Set<String>()
return Array(events.filter { event in
guard let subject = event.action.subject,
subject.contains("/dev.knotless.pull/") || subject.contains("/dev.knotless.issue/") else { return false }
return seen.insert(subject).inserted
}.prefix(most))
}
var id: String { createdAt + actor.did + repo.label + action.summary }
/// An open union: a kind this version doesn't know still shows as an event.
@@ -475,6 +574,20 @@ struct FeedEvent: Decodable, Identifiable, Hashable {
}
var kind: String { type.split(separator: "#").last.map(String.init) ?? type }
/// What the account whose feed this is did, in a line of its own.
var yours: String {
switch kind {
case "opened": "You opened"
case "changed": "You \(state == "open" ? "reopened" : state ?? "changed")"
case "commented":
switch verdict {
case "approve": "You approved"
case "changes": "You asked for changes"
default: "You commented"
}
default: "You updated"
}
}
var branch: String? { ref?.replacingOccurrences(of: "refs/heads/", with: "") }
var summary: String {
ios/Knotless/API/Writes.swift+6−0
@@ -15,6 +15,12 @@ enum Writes {
return Write(method: "createComment", input: input)
}
static func issue(repo: RepoRef, title: String, body: String) -> Write {
var input = ["repo": repo.uri, "title": title]
if !body.isEmpty { input["body"] = body }
return Write(method: "createIssue", input: input)
}
static func react(repo: RepoRef, subject: String, content: String, undo: Bool) -> Write {
Write(method: undo ? "removeReaction" : "addReaction", input: ["repo": repo.uri, "subject": subject, "content": content])
}
ios/Knotless/Assets.xcassets/oct-arrow-right.imageset/arrow-right.svgnew+1−0
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8.22 2.97a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042l2.97-2.97H3.75a.75.75 0 0 1 0-1.5h7.44L8.22 4.03a.75.75 0 0 1 0-1.06Z"/></svg>
\ No newline at end of file
ios/Knotless/Assets.xcassets/oct-arrow-right.imageset/Contents.jsonnew+1−0
@@ -0,0 +1 @@
{"images":[{"filename":"arrow-right.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
\ No newline at end of file
ios/Knotless/Assets.xcassets/oct-arrow-up-left.imageset/arrow-up-left.svgnew+1−0
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M3.96 4.75A.75.75 0 0 1 4.71 4h6.01a.75.75 0 0 1 0 1.5h-4.2l5.26 5.26a.75.75 0 0 1-1.06 1.061l-5.26-5.26v4.2a.75.75 0 0 1-1.5 0Z"/></svg>
\ No newline at end of file
ios/Knotless/Assets.xcassets/oct-arrow-up-left.imageset/Contents.jsonnew+1−0
@@ -0,0 +1 @@
{"images":[{"filename":"arrow-up-left.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
\ No newline at end of file
ios/Knotless/Assets.xcassets/oct-kebab-horizontal.imageset/Contents.jsonnew+1−0
@@ -0,0 +1 @@
{"images":[{"filename":"kebab-horizontal.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
\ No newline at end of file
ios/Knotless/Assets.xcassets/oct-kebab-horizontal.imageset/kebab-horizontal.svgnew+1−0
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM1.5 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm13 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"/></svg>
\ No newline at end of file
ios/Knotless/Assets.xcassets/oct-plus.imageset/Contents.jsonnew+1−0
@@ -0,0 +1 @@
{"images":[{"filename":"plus.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
\ No newline at end of file
ios/Knotless/Assets.xcassets/oct-plus.imageset/plus.svgnew+1−0
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"/></svg>
\ No newline at end of file
ios/Knotless/Assets.xcassets/oct-search.imageset/Contents.jsonnew+1−0
ios/Knotless/Assets.xcassets/oct-search.imageset/search.svgnew+1−0
ios/Knotless/Assets.xcassets/oct-x.imageset/Contents.jsonnew+1−0
ios/Knotless/Assets.xcassets/oct-x.imageset/x.svgnew+1−0
ios/Knotless/Views/HomeView.swift+405−20
425 lines changed: not loaded until asked for.
ios/Knotless/Views/SearchView.swiftnew+293−0
ios/Knotless/Views/ThreadView.swift+5−1
ios/Knotless/App.swift+21−0
ios/ModelCheck/main.swift+3−0
ios/look-answers.json+1−0
ios/look.mjs+8−0