chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
The iOS app's home and search follow GitHub's, and comments take reactions
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.