chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
A feed event says where its pull or issue stands now
crates/knotless-serve/src/web/feed.rs+26−5
| @@ -86,23 +86,35 @@ pub enum Action { | ||
refname: String, | ||
}, | ||
/// A pull or an issue opened. | ||
#[serde(rename = "dev.knotless.getFeed#opened")] | ||
Opened { subject: String, title: String }, | ||
#[serde(rename = "dev.knotless.getFeed#opened", rename_all = "camelCase")] | ||
Opened { | ||
subject: String, | ||
title: String, | ||
/// Where the subject stands now, whoever moved it there. | ||
#[serde(skip_serializing_if = "Option::is_none")] | ||
subject_state: Option<String>, | ||
}, | ||
/// A comment, or a review with a verdict. | ||
#[serde(rename = "dev.knotless.getFeed#commented")] | ||
#[serde(rename = "dev.knotless.getFeed#commented", rename_all = "camelCase")] | ||
Commented { | ||
subject: String, | ||
title: String, | ||
text: String, | ||
#[serde(skip_serializing_if = "Option::is_none")] | ||
verdict: Option<String>, | ||
/// Where the subject stands now, whoever moved it there. | ||
#[serde(skip_serializing_if = "Option::is_none")] | ||
subject_state: Option<String>, | ||
}, | ||
/// A pull or an issue moved to another state. | ||
#[serde(rename = "dev.knotless.getFeed#changed")] | ||
#[serde(rename = "dev.knotless.getFeed#changed", rename_all = "camelCase")] | ||
Changed { | ||
subject: String, | ||
title: String, | ||
state: String, | ||
/// Where the subject stands now, whoever moved it there. | ||
#[serde(skip_serializing_if = "Option::is_none")] | ||
subject_state: Option<String>, | ||
}, | ||
/// The event's repository starred, by its record. | ||
#[serde(rename = "dev.knotless.getFeed#starred")] | ||
| @@ -273,6 +285,7 @@ async fn events( | ||
// Pulls and issues, as their repositories' owners let them stand, by | ||
// title, and the repositories they are on. | ||
let mut titles: HashMap<String, String> = HashMap::new(); | ||
let mut states: HashMap<String, String> = HashMap::new(); | ||
let on: HashSet<(&str, &str)> = said | ||
.iter() | ||
.map(|(_, did, key)| (did.as_str(), key.as_str())) | ||
| @@ -302,6 +315,7 @@ async fn events( | ||
titles.insert(e.uri.clone(), title); | ||
} | ||
} | ||
states.extend(made.items.iter().map(|i| (i.uri.clone(), i.state.clone()))); | ||
on_repos.insert((did, key), (name, record, made)); | ||
} | ||
// Who each is shown as said or done by. | ||
| @@ -488,20 +502,27 @@ async fn events( | ||
}; | ||
let shown = titles.contains_key(subject); | ||
let title = titles.get(subject).cloned().unwrap_or_default(); | ||
let subject_state = states.get(subject).cloned(); | ||
let text = |field: &str| e.value[field].as_str().unwrap_or_default().to_string(); | ||
let subject = subject.to_string(); | ||
let action = match forge::parse_uri(&e.uri).unwrap_or_default().1 { | ||
nsid::PULL | nsid::ISSUE => Action::Opened { subject, title }, | ||
nsid::PULL | nsid::ISSUE => Action::Opened { | ||
subject, | ||
title, | ||
subject_state, | ||
}, | ||
nsid::COMMENT => Action::Commented { | ||
subject, | ||
title, | ||
text: text("body"), | ||
verdict: e.value["verdict"].as_str().map(str::to_string), | ||
subject_state, | ||
}, | ||
_ => Action::Changed { | ||
subject, | ||
title, | ||
state: text("state"), | ||
subject_state, | ||
}, | ||
}; | ||
let event = Event { | ||
crates/knotless-serve/tests/web.rs+12−4
| @@ -2475,10 +2475,15 @@ fn the_feed() { | ||
o, | ||
("changed", json!({"subject": pull, "state": "merged"})), | ||
&[ | ||
("opened", json!({"subject": pull, "title": "Add b"})), | ||
// Each says where its subject stands now, not then: the pull | ||
// merged, and the issue, closed and reopened above, open. | ||
( | ||
"opened", | ||
json!({"subject": pull, "title": "Add b", "subjectState": "merged"}), | ||
), | ||
( | ||
"commented", | ||
json!({"subject": pull, "verdict": "changes", "text": "Not *yet*"}), | ||
json!({"subject": pull, "verdict": "changes", "text": "Not *yet*", "subjectState": "merged"}), | ||
), | ||
( | ||
"commented", | ||
| @@ -2486,13 +2491,16 @@ fn the_feed() { | ||
), | ||
( | ||
"opened", | ||
json!({"subject": issue, "title": "Broken <thing>"}), | ||
json!({"subject": issue, "title": "Broken <thing>", "subjectState": "open"}), | ||
), | ||
( | ||
"commented", | ||
json!({"subject": issue, "text": "Seen it", "title": "Broken <thing>"}), | ||
), | ||
("changed", json!({"subject": issue, "state": "closed"})), | ||
( | ||
"changed", | ||
json!({"subject": issue, "state": "closed", "subjectState": "open"}), | ||
), | ||
], | ||
); | ||
// Opened, said and decided about, by title. | ||
docs/reference/api.md+3−3
| @@ -246,9 +246,9 @@ Answers `events` (`createdAt`, `actor` `{did, handle, avatar?}`, `repo` | ||
| `dev.knotless.getFeed#created` | `description?`, `forkOf?` | | ||
| `dev.knotless.getFeed#pushed` | `ref`, `created`, `to?`, `forced?`, `count`, `new`, `commits` (`sha`, `message`, `merge?`) | | ||
| `dev.knotless.getFeed#deleted` | `ref` | | ||
| `dev.knotless.getFeed#opened` | `subject`, `title` | | ||
| `dev.knotless.getFeed#commented` | `subject`, `title`, `text`, `verdict?` | | ||
| `dev.knotless.getFeed#changed` | `subject`, `title`, `state` | | ||
| `dev.knotless.getFeed#opened` | `subject`, `title`, `subjectState?` (where it stands now, whoever moved it there) | | ||
| `dev.knotless.getFeed#commented` | `subject`, `title`, `text`, `verdict?`, `subjectState?` | | ||
| `dev.knotless.getFeed#changed` | `subject`, `title`, `state`, `subjectState?` | | ||
| `dev.knotless.getFeed#starred` | `subject`, the repository record | | ||
| ||
A push's `actor` is the account that pushed: a ref's claim or record that | ||
ios/Knotless/API/Models.swift+11−2
| @@ -569,6 +569,7 @@ struct FeedEvent: Decodable, Identifiable, Hashable { | ||
var subject: String? | ||
var title: String? | ||
var state: String? | ||
var subjectState: String? | ||
var text: String? | ||
var verdict: String? | ||
var ref: String? | ||
| @@ -576,14 +577,22 @@ struct FeedEvent: Decodable, Identifiable, Hashable { | ||
var forkOf: RepoRef? | ||
| ||
enum CodingKeys: String, CodingKey { | ||
case type = "$type", subject, title, state, text, verdict, ref, count, forkOf | ||
case type = "$type", subject, title, state, subjectState, text, verdict, ref, count, forkOf | ||
} | ||
| ||
var kind: String { type.split(separator: "#").last.map(String.init) ?? type } | ||
/// Where the subject stands now, or as this event left it on an older server. | ||
var standing: String? { subjectState ?? state } | ||
/// What the account whose feed this is did, in a line of its own. | ||
var yours: String { | ||
switch kind { | ||
case "opened": "You opened" | ||
// A queue's runner merges what you opened, so no event of yours says so. | ||
case "opened": | ||
switch subjectState { | ||
case "merged": "Merged" | ||
case "closed": "Closed" | ||
default: "You opened" | ||
} | ||
case "changed": "You \(state == "open" ? "reopened" : state ?? "changed")" | ||
case "commented": | ||
switch verdict { | ||
ios/Knotless/Views/HomeView.swift+2−2
| @@ -184,10 +184,10 @@ private struct RecentRow: View { | ||
let event: FeedEvent | ||
| ||
var body: some View { | ||
let style = StateStyle(kind: kind(ofURI: event.action.subject), state: event.action.state) | ||
let style = StateStyle(kind: kind(ofURI: event.action.subject), state: event.action.standing) | ||
HStack(alignment: .top, spacing: Space.s3) { | ||
Oct(name: String(style.icon.dropFirst(4)), size: Icon.xl) | ||
.foregroundStyle(event.action.state == nil ? Color.neutral : style.color) | ||
.foregroundStyle(event.action.standing == nil ? Color.neutral : style.color) | ||
.frame(width: 30) | ||
.padding(.top, Space.s0_5) | ||
VStack(alignment: .leading, spacing: Space.s1) { | ||
lexicons/dev/knotless/getFeed.json+6−3
| @@ -100,7 +100,8 @@ | ||
"required": ["subject", "title"], | ||
"properties": { | ||
"subject": { "type": "string", "format": "at-uri", "description": "The dev.knotless.pull or dev.knotless.issue record." }, | ||
"title": { "type": "string" } | ||
"title": { "type": "string" }, | ||
"subjectState": { "type": "string", "knownValues": ["open", "closed", "merged"], "description": "Where the subject stands now, whoever moved it there." } | ||
} | ||
}, | ||
"commented": { | ||
| @@ -111,7 +112,8 @@ | ||
"subject": { "type": "string", "format": "at-uri" }, | ||
"title": { "type": "string", "description": "The subject's title." }, | ||
"text": { "type": "string", "description": "The comment, as Markdown." }, | ||
"verdict": { "type": "string", "knownValues": ["approve", "changes"] } | ||
"verdict": { "type": "string", "knownValues": ["approve", "changes"] }, | ||
"subjectState": { "type": "string", "knownValues": ["open", "closed", "merged"], "description": "Where the subject stands now, whoever moved it there." } | ||
} | ||
}, | ||
"changed": { | ||
| @@ -121,7 +123,8 @@ | ||
"properties": { | ||
"subject": { "type": "string", "format": "at-uri" }, | ||
"title": { "type": "string", "description": "The subject's title." }, | ||
"state": { "type": "string", "knownValues": ["open", "closed", "merged"] } | ||
"state": { "type": "string", "knownValues": ["open", "closed", "merged"] }, | ||
"subjectState": { "type": "string", "knownValues": ["open", "closed", "merged"], "description": "Where the subject stands now, whoever moved it there." } | ||
} | ||
}, | ||
"starred": { | ||