diff --git a/src/mail/boxes.rs b/src/mail/boxes.rs index 20bfb4c..fa780f9 100644 --- a/src/mail/boxes.rs +++ b/src/mail/boxes.rs @@ -152,7 +152,9 @@ impl RenderOnce for MailboxList { .iter() .flat_map(|(_, boxes)| boxes) .any(|mailbox| mailbox.key == *open); - assert!(listed, "no mailbox {open}"); + if !listed { + log::error!("mailbox list: no mailbox {open}; none open"); + } } let open: OnValue = { let (id, on_open) = (self.id.clone(), self.on_open.clone()); diff --git a/src/mail/labels.rs b/src/mail/labels.rs index 3fddfac..1c6a3c1 100644 --- a/src/mail/labels.rs +++ b/src/mail/labels.rs @@ -120,7 +120,12 @@ impl RenderOnce for LabelPicker { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { for key in &self.selected { let known = self.labels.iter().any(|label| label.key == *key); - assert!(known, "label picker {:?}: no label {key}", self.id); + if !known { + log::error!( + "label picker {:?}: no label {key}; it is not ticked", + self.id + ); + } } let finding = window.use_keyed_state( (self.id.clone(), "finding"), diff --git a/src/mail/list.rs b/src/mail/list.rs index 9dbd0b1..23d3e9e 100644 --- a/src/mail/list.rs +++ b/src/mail/list.rs @@ -284,7 +284,9 @@ impl RenderOnce for MailList { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { for key in &self.selected { let listed = self.mails.iter().any(|mail| mail.key == *key); - assert!(listed, "no mail {key}"); + if !listed { + log::error!("mail list: no mail {key}; it is not selected"); + } } let theme = cx.theme(); if self.mails.is_empty() { diff --git a/src/mail/tests/sort.rs b/src/mail/tests/sort.rs index 4bccac4..9af64fb 100644 --- a/src/mail/tests/sort.rs +++ b/src/mail/tests/sort.rs @@ -128,12 +128,21 @@ fn a_label_name_is_listed_once() { } #[gpui::test] -#[should_panic(expected = "no label ghost")] -fn what_is_on_is_a_label(cx: &mut TestAppContext) { +fn a_label_mail_or_box_that_left_draws_unmarked(cx: &mut TestAppContext) { let _ = mailing( |_| { - LabelPicker::new("labels", [Label::new("a", "A", 0)]) - .selected(["ghost"]) + let mail = crate::mail::Mail::new("m1", "Ada", "Plans", Timestamp::UNIX_EPOCH); + let inbox = + crate::mail::Mailbox::new("inbox", "Inbox", crate::primitives::IconName::Inbox); + div() + .w(px(480.0)) + .child(LabelPicker::new("labels", [Label::new("a", "A", 0)]).selected(["ghost"])) + .child(crate::mail::MailList::new("mails", [mail]).selected(["gone"])) + .child( + crate::mail::MailboxList::new("boxes") + .section("Mail", [inbox]) + .open("gone"), + ) .into_any_element() }, cx, diff --git a/src/mail/times.rs b/src/mail/times.rs index 42e79e9..7cb234a 100644 --- a/src/mail/times.rs +++ b/src/mail/times.rs @@ -40,12 +40,15 @@ pub(super) fn named(name: &str, at: Timestamp, zone: &TimeZone) -> (String, Time (format!("{name} · {hour}"), at) } -/// The moment `chosen` names in `zone`, once it lies after `now`. +/// The moment `chosen` names in `zone`, once it lies after `now`; none past the times jiff holds. pub(super) fn ahead(chosen: DateTime, zone: &TimeZone, now: Timestamp) -> Option { - let at = chosen - .to_zoned(zone.clone()) - .expect("the zone holds the time") - .timestamp(); + let at = match chosen.to_zoned(zone.clone()) { + Ok(at) => at.timestamp(), + Err(error) => { + log::error!("time dialog: {chosen} has no moment in its zone: {error}"); + return None; + } + }; (at > now).then_some(at) } @@ -197,5 +200,11 @@ mod tests { None, "10:00 in Tokyo is 01:00 here" ); + let west = TimeZone::fixed(offset(-9)); + assert_eq!( + ahead(jiff::civil::DateTime::MAX, &west, now), + None, + "past what jiff holds" + ); } } diff --git a/tasks/panics.md b/tasks/panics.md index ccdfc64..5920aa8 100644 --- a/tasks/panics.md +++ b/tasks/panics.md @@ -43,7 +43,7 @@ Each item reads every site in its folders, fixes what the rules name, updates ca - [x] P26 Media — `src/media` - [x] P27 Files — `src/files` - [x] P28 Messaging — `src/messaging` -- [ ] P29 Mail — `src/mail` +- [x] P29 Mail — `src/mail` - [ ] P30 Calendar — `src/calendar` - [ ] P31 Project — `src/project` - [ ] P32 Canvas & Design — `src/canvas` @@ -94,3 +94,4 @@ One line per item: what changed, what stayed and why, review rounds. - P26: a live position past its media logs and sits at the end (`media::played`); loaded time, a trim and chapters past the length peg or stay out; a gone device or track, or a refused trim, leaves none and clears the last; a dropped root path shows whole; subtitle times no clock holds read as none; a rail layout left no width gives no share (`scrubber::along`), so hover, press and drag skip it; waveform buckets count in `u64`, which a 32-bit web `usize` outgrows. Kept: non-zero lengths, one-value contracts, one list's uniqueness and order, lookups by construction. Review: 4 rounds; it also passed P25's last fix. - P27: an owner's pick gone from a grid, an explorer folder or a directory listing logs and is kept as given, so it replaces a local pick and marks none (P13's listing now does the same); the columns mark a path name only while its parent holds it as a folder; moved bytes past a total log, the bar already capped; duplicate marks on no copy, or on every copy of a group, are dropped, so Remove never takes a group's last copy. Kept: one list's uniqueness, a snapshot's path and levels, items and downloads, lookups by construction, `datetime` with a constant pattern. Review: 3 rounds. - P28: an open chat or selected member no longer listed logs and marks none. Kept: one list's uniqueness and non-emptiness, one message's receipt and replies, constant `datetime` patterns, picks from lists without disabled rows, keys from the same render. Review: 1 round. +- P29: an open mailbox, ticked label or selected mail no longer listed logs and marks none; a picked moment its zone cannot hold logs and reads as none, so the time dialog's action stays off. Kept: one list's uniqueness, day arithmetic from the owner's now, constant `datetime` patterns, the dialog's action behind its disabled button. Review: 1 round.