Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/zero-rust-live-view.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'zero-rust': minor
---

Add `LiveView`, a typed live query after the Swift client's `LiveQuery` (zero-react's `useQuery`). `Zero::live(&query, &ttl)` takes a typed `Query` or a named query's `QueryRequest` and returns a `LiveView<T>` whose `value()` is the query's rows, `one()` row, or aggregate read as its Rust type and kept current, with `result_type()`, `error()` (a `LiveError`: the server's query error, or rows that didn't read, which keep the last value), `add_listener`, `update_ttl`, and `view()` for the `MaterializedView`. An update reads only the rows whose entries changed, so an optimistic update with a live view of 100,000 rows read as structs no longer reads every row again. A live view follows the client when it replaces itself, and dropping it removes its listeners and destroys its view. `QueryValue` gains `update_from_view`, which has a default, and the new `typed::TypedQuery` trait names what a query reads as.
102 changes: 42 additions & 60 deletions apps/zbugs-gpui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -729,10 +729,8 @@ impl ZbugsApp {
}
let exists = self
.labels
.snapshot()
.value
.value()
.iter()
.flatten()
.any(|l| l.name.eq_ignore_ascii_case(&name));
if exists {
self.error = Some(format!("There's already a label called {name}."));
Expand Down Expand Up @@ -784,7 +782,7 @@ impl ZbugsApp {
let login = self
.me
.as_ref()
.and_then(|me| me.snapshot().value.clone().flatten())
.and_then(|me| (*me.value()).clone())
.map(|u| u.login);
let logged_in = self.session.auth.is_some();
let app = cx.entity().downgrade();
Expand Down Expand Up @@ -835,8 +833,7 @@ impl ZbugsApp {
/// just edited) doesn't drag the list. At the top, the list stays at
/// the top.
fn keep_scroll_anchor(&mut self) {
let snapshot = self.list.snapshot();
let rows = snapshot.value.as_deref().unwrap_or_default();
let rows = self.list.value();
let handle = self.scroll.0.borrow().base_handle.clone();
let mut offset = handle.offset();
if offset.y < px(0.) {
Expand Down Expand Up @@ -992,7 +989,7 @@ impl ZbugsApp {
/// Near the end of what's loaded: ask for the next page. The rows
/// loaded so far stay where they are, so the list doesn't move.
fn grow(&mut self, cx: &mut Context<Self>) {
let loaded = self.list.snapshot().value.as_ref().map_or(0, Vec::len) as i64;
let loaded = self.list.value().len() as i64;
if loaded < self.limit {
return;
}
Expand Down Expand Up @@ -1042,12 +1039,7 @@ impl ZbugsApp {
if !self.select_first {
return;
}
let first = self
.list
.snapshot()
.value
.as_ref()
.and_then(|rows| Some(rows.first()?.id.clone()));
let first = self.list.value().first().map(|row| row.id.clone());
let Some(id) = first else {
return;
};
Expand All @@ -1060,8 +1052,7 @@ impl ZbugsApp {
/// Selects the issue `delta` rows from the selected one, or the first.
fn move_selection(&mut self, delta: isize, cx: &mut Context<Self>) {
let next = {
let snapshot = self.list.snapshot();
let rows = snapshot.value.as_deref().unwrap_or_default();
let rows = self.list.value();
if rows.is_empty() {
return;
}
Expand Down Expand Up @@ -1265,11 +1256,12 @@ impl ZbugsApp {

/// Checks or unchecks the selected issue, for space on the list.
fn toggle_selected_checked(&mut self, cx: &mut Context<Self>) {
let issue = self.list.snapshot().value.as_ref().and_then(|rows| {
rows.iter()
.find(|r| Some(&r.id) == self.selected.as_ref())
.cloned()
});
let issue = self
.list
.value()
.iter()
.find(|r| Some(&r.id) == self.selected.as_ref())
.cloned();
if let Some(issue) = issue {
self.toggle_checked(&issue, cx);
}
Expand Down Expand Up @@ -1331,8 +1323,8 @@ impl ZbugsApp {
let Some(detail) = self.detail.as_ref().filter(|d| d.is_synced()) else {
return;
};
let snapshot = detail.snapshot();
let Some(issue) = snapshot.value.as_ref().and_then(Option::as_ref) else {
let issue = detail.value();
let Some(issue) = issue.as_ref() else {
return;
};
let comments = issue.comments.as_deref().unwrap_or_default();
Expand Down Expand Up @@ -1374,10 +1366,7 @@ impl ZbugsApp {
/// "Zero Bugs → " and the shown issue's title, or the list's, as
/// zbugs' issue and list pages title themselves.
fn window_title(&self) -> String {
let issue = self
.detail
.as_ref()
.and_then(|d| d.snapshot().value.clone().flatten());
let issue = self.detail.as_ref().and_then(|d| (*d.value()).clone());
let f = &self.filters;
let shown = match issue {
Some(issue) => issue.title,
Expand Down Expand Up @@ -1407,8 +1396,8 @@ impl ZbugsApp {
.as_ref()
.and(self.detail.as_ref())
.and_then(|d| {
let snapshot = d.snapshot();
let issue = snapshot.value.as_ref()?.as_ref()?;
let issue = d.value();
let issue = issue.as_ref().as_ref()?;
is_unread(issue).then(|| issue.id.clone())
});
let Some(id) = unread else {
Expand Down Expand Up @@ -1442,7 +1431,7 @@ impl ZbugsApp {
let modified = this
.detail
.as_ref()
.and_then(|d| d.snapshot().value.clone().flatten())
.and_then(|d| (*d.value()).clone())
.filter(|issue| issue.id == id)
.map(|issue| issue.modified);
if let Some(modified) = modified {
Expand Down Expand Up @@ -1497,7 +1486,7 @@ impl ZbugsApp {
let pref = self
.skin_tone
.as_ref()
.and_then(|live| live.snapshot().value.clone().flatten());
.and_then(|live| (*live.value()).clone());
pref.and_then(|p| p.value.parse().ok())
.filter(|tone| *tone < SKIN_TONES.len() as u32)
.unwrap_or(0)
Expand Down Expand Up @@ -1618,10 +1607,8 @@ impl ZbugsApp {
fn project_id(&self) -> Option<String> {
let name = self.filters.project.to_lowercase();
self.projects
.snapshot()
.value
.value()
.iter()
.flatten()
.find(|p| p.lower_case_name == name)
.map(|p| p.id.clone())
}
Expand Down Expand Up @@ -1801,7 +1788,7 @@ impl ZbugsApp {
fn render_sidebar(&self, lights: Pixels, cx: &mut Context<Self>) -> impl IntoElement {
let f = &self.filters;
let current = f.project.to_lowercase();
let mut projects = self.projects.snapshot().value.clone().unwrap_or_default();
let mut projects = (*self.projects.value()).clone();
projects.sort_by(|a, b| a.lower_case_name.cmp(&b.lower_case_name));
let project = self.picker(
Button::new("project")
Expand Down Expand Up @@ -1889,7 +1876,7 @@ impl ZbugsApp {
let login = self
.me
.as_ref()
.and_then(|me| me.snapshot().value.clone().flatten())
.and_then(|me| (*me.value()).clone())
.map(|u| u.login);
let mine = |id: &'static str, on: bool, assigned: bool, cx: &mut Context<Self>| {
let login = login.clone();
Expand Down Expand Up @@ -1937,10 +1924,8 @@ impl ZbugsApp {
);
let mut names = self
.labels
.snapshot()
.value
.value()
.iter()
.flatten()
.map(|l| l.name.clone())
.collect::<Vec<_>>();
names.sort_by_key(|n| n.to_lowercase());
Expand Down Expand Up @@ -2035,11 +2020,13 @@ impl ZbugsApp {
/// and log in or out.
fn render_list_toolbar(&self, lights: Pixels, cx: &mut Context<Self>) -> impl IntoElement {
let count = {
let s = self.count.snapshot();
match (s.value, self.list.snapshot().value.as_ref()) {
(Some(n), _) if self.count.is_synced() || n > 0 => format!("{n}"),
(_, Some(rows)) if !rows.is_empty() => format!("{}+", rows.len()),
_ => String::new(),
let (n, rows) = (*self.count.value(), self.list.value());
if self.count.is_synced() || n > 0 {
format!("{n}")
} else if !rows.is_empty() {
format!("{}+", rows.len())
} else {
String::new()
}
};
let logged_in = self.session.auth.is_some();
Expand Down Expand Up @@ -2167,7 +2154,7 @@ impl ZbugsApp {
value: None,
checked: current.is_none(),
}];
let mut users = users.snapshot().value.clone().unwrap_or_default();
let mut users = (*users.value()).clone();
users.sort_by_key(|u| u.login.to_lowercase());
choices.extend(users.into_iter().map(|u| Choice {
avatar: Some(avatar_url(&u)),
Expand Down Expand Up @@ -2250,7 +2237,7 @@ impl ZbugsApp {
}

fn render_list(&self, cx: &mut Context<Self>) -> impl IntoElement {
let rows = self.list.snapshot().value.clone().unwrap_or_default();
let rows = (*self.list.value()).clone();
let len = rows.len();
let rows = Rc::new(rows);
if len == 0 {
Expand Down Expand Up @@ -2624,7 +2611,7 @@ impl ZbugsApp {
let Some(detail) = &self.detail else {
return empty("Select an issue", cx);
};
let issue = detail.snapshot().value.clone().flatten();
let issue = (*detail.value()).clone();
let Some(issue) = issue else {
return empty(
if detail.is_synced() {
Expand All @@ -2638,10 +2625,7 @@ impl ZbugsApp {
// The newest comments first; shown oldest first. The page takes
// over once it has at least what the issue brought.
let mut comments: Vec<Comment> = issue.comments.clone().unwrap_or_default();
if let Some(page) = self
.comment_page
.as_ref()
.and_then(|p| p.snapshot().value.clone())
if let Some(page) = self.comment_page.as_ref().map(|p| (*p.value()).clone())
&& page.len() >= comments.len()
{
comments = page;
Expand All @@ -2658,7 +2642,7 @@ impl ZbugsApp {
value: None,
checked: assignee_id.is_none(),
}];
let mut crew = self.crew.snapshot().value.clone().unwrap_or_default();
let mut crew = (*self.crew.value()).clone();
crew.sort_by_key(|u| u.login.to_lowercase());
assignee_choices.extend(crew.into_iter().map(|u| Choice {
avatar: Some(avatar_url(&u)),
Expand All @@ -2681,7 +2665,7 @@ impl ZbugsApp {
Rc::new(move |this, user_id, _, cx| this.set_assignee(id.clone(), user_id, cx)),
cx,
);
let mut all_labels = self.labels.snapshot().value.clone().unwrap_or_default();
let mut all_labels = (*self.labels.value()).clone();
all_labels.sort_by_key(|l| l.name.to_lowercase());
let mut label_choices: Vec<Choice> = all_labels
.into_iter()
Expand Down Expand Up @@ -2991,10 +2975,11 @@ impl Render for ZbugsApp {
window.push_notification(Notification::error(message), cx);
}
if self.reveal_selected {
let at = self.list.snapshot().value.as_ref().and_then(|rows| {
rows.iter()
.position(|r| Some(&r.id) == self.selected.as_ref())
});
let at = self
.list
.value()
.iter()
.position(|r| Some(&r.id) == self.selected.as_ref());
if let Some(ix) = at {
self.scroll.scroll_to_item(ix, ScrollStrategy::Center);
self.reveal_selected = false;
Expand All @@ -3013,10 +2998,7 @@ impl Render for ZbugsApp {
} else {
px(TRAFFIC_LIGHTS)
};
let issue = self
.detail
.as_ref()
.and_then(|d| d.snapshot().value.clone().flatten());
let issue = self.detail.as_ref().and_then(|d| (*d.value()).clone());
h_flex()
.size_full()
.bg(cx.theme().background)
Expand Down
24 changes: 7 additions & 17 deletions apps/zbugs-gpui/src/app/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ async fn settled(app: &Entity<ZbugsApp>, cx: &mut AsyncApp) -> Step {
}

fn rows(app: &ZbugsApp) -> Vec<Issue> {
app.list.snapshot().value.clone().unwrap_or_default()
(*app.list.value()).clone()
}

fn issue(app: &ZbugsApp) -> Option<Issue> {
app.detail.as_ref()?.snapshot().value.clone().flatten()
(*app.detail.as_ref()?.value()).clone()
}

async fn online(window: AnyWindowHandle, app: &Entity<ZbugsApp>, cx: &mut AsyncApp) -> Step {
Expand All @@ -143,7 +143,7 @@ async fn online(window: AnyWindowHandle, app: &Entity<ZbugsApp>, cx: &mut AsyncA
app.selected.is_some() && app.selected.as_ref() == rows(app).first().map(|i| &i.id)
})
.await?;
let open = app.read_with(cx, |app, _| app.count.snapshot().value.unwrap_or(0));
let open = app.read_with(cx, |app, _| *app.count.value());
shot("1-list");

// Issues created and deleted again, one from the issue and two
Expand Down Expand Up @@ -206,8 +206,7 @@ async fn online(window: AnyWindowHandle, app: &Entity<ZbugsApp>, cx: &mut AsyncA
})
.await?;
until(app, cx, "the new issue at the top of the list", |app| {
rows(app).first().is_some_and(|i| i.id == id)
&& app.count.snapshot().value == Some(open + 1)
rows(app).first().is_some_and(|i| i.id == id) && *app.count.value() == open + 1
})
.await?;

Expand Down Expand Up @@ -312,21 +311,14 @@ async fn online(window: AnyWindowHandle, app: &Entity<ZbugsApp>, cx: &mut AsyncA

// The bug label, and the issue assigned to the user.
until(app, cx, "the labels", |app| {
app.labels
.snapshot()
.value
.iter()
.flatten()
.any(|l| l.name == "bug")
app.labels.value().iter().any(|l| l.name == "bug")
})
.await?;
act(window, app, cx, |app, _, cx| {
let bug = app
.labels
.snapshot()
.value
.value()
.iter()
.flatten()
.find(|l| l.name == "bug")
.map(|l| l.id.clone());
let issue = issue(app).expect("the issue is shown");
Expand Down Expand Up @@ -397,9 +389,7 @@ async fn online(window: AnyWindowHandle, app: &Entity<ZbugsApp>, cx: &mut AsyncA
// and All shows it.
let login = app
.read_with(cx, |app, _| {
app.me
.as_ref()
.and_then(|me| me.snapshot().value.clone().flatten())
app.me.as_ref().and_then(|me| (*me.value()).clone())
})
.map(|u| u.login)
.ok_or("the user's row didn't sync")?;
Expand Down
Loading
Loading