| 24 |
24 |
|
//! `?shown=`.
|
| 25 |
25 |
|
//! - `GET /emails/list` — the list alone, which is what a filter swaps.
|
| 26 |
26 |
|
//! - `POST /emails/read-all` — every message read, whatever the view.
|
|
27 |
+ |
//! - `POST /emails/list/read` — every ticked thread read.
|
|
28 |
+ |
//! - `POST /emails/list/archive` — every ticked thread archived.
|
|
29 |
+ |
//! - `POST /emails/list/snooze` — every ticked thread snoozed, under `until`.
|
|
30 |
+ |
//! - `POST /emails/list/delete` — every ticked thread deleted.
|
| 27 |
31 |
|
//! - `GET /emails/{id}` — the thread, read.
|
| 28 |
32 |
|
//! - `POST /emails/{id}/read` — read or unread, under `read`.
|
| 29 |
33 |
|
//! - `POST /emails/{id}/archive` — archive or unarchive, under `on`.
|
| 85 |
89 |
|
/// The region one thread is drawn in.
|
| 86 |
90 |
|
const THREAD: &str = "emails-thread";
|
| 87 |
91 |
|
|
|
92 |
+ |
/// The name of the set the list's ticks go into.
|
|
93 |
+ |
///
|
|
94 |
+ |
/// The same word the task list uses for its own set. A selection is
|
|
95 |
+ |
/// screen-scoped — [`Screen::selection`](quasi_router::Screen::selection) holds
|
|
96 |
+ |
/// one name and [`Act::over`] names it back — so two screens sharing a spelling
|
|
97 |
+ |
/// is a reader's convenience and not a shared set.
|
|
98 |
+ |
const SELECTION: &str = "chosen";
|
|
99 |
+ |
|
| 88 |
100 |
|
/// The list as it was being looked at.
|
| 89 |
101 |
|
///
|
| 90 |
102 |
|
/// `emails.js` holds this across four places — `emailPaging.baseFilters`,
|
| 103 |
115 |
|
label: Option<String>,
|
| 104 |
116 |
|
/// Whether archived mail is included.
|
| 105 |
117 |
|
archived: bool,
|
|
118 |
+ |
/// Whether the rows arrive ticked.
|
|
119 |
+ |
///
|
|
120 |
+ |
/// Select-all, and it is an address, for the reason
|
|
121 |
+ |
/// [`super::task_list`]'s own `ticked` gives at length: a renderer could
|
|
122 |
+ |
/// tick every box it drew, but a webview one would need a script this
|
|
123 |
+ |
/// crate does not ship and a terminal a key it invents, and neither
|
|
124 |
+ |
/// survives the fragment swap that replaces the boxes. Answering it from
|
|
125 |
+ |
/// the server is one query against a local SQLite file and every host gets
|
|
126 |
+ |
/// it.
|
|
127 |
+ |
///
|
|
128 |
+ |
/// Only the arriving state. What the user ticks or unticks afterwards is
|
|
129 |
+ |
/// the renderer's, which is the whole point of `5f2b8753`: this screen
|
|
130 |
+ |
/// never holds which rows are ticked.
|
|
131 |
+ |
ticked: bool,
|
| 106 |
132 |
|
/// How many threads are on screen.
|
| 107 |
133 |
|
///
|
| 108 |
134 |
|
/// The JS appends: it holds what it has fetched and asks for the next 200
|
| 123 |
149 |
|
folder: text(&request.carried, "folder"),
|
| 124 |
150 |
|
label: text(&request.carried, "label"),
|
| 125 |
151 |
|
archived: matches!(request.carried.get("archived"), Some("1" | "true")),
|
|
152 |
+ |
// `carried`, and the ticks themselves arrive under `ticked` in
|
|
153 |
+ |
// `payload` ([`Node::TICKED`]). Two bags, so the select-all address
|
|
154 |
+ |
// and the set it produces cannot be read as each other. Same
|
|
155 |
+ |
// arrangement `View::carry` records for `folder` and `archived`.
|
|
156 |
+ |
ticked: matches!(request.carried.get("ticked"), Some("all")),
|
| 126 |
157 |
|
// A hand-typed `shown` is clamped rather than refused: this is an
|
| 127 |
158 |
|
// address, and landing on the first page is a more useful answer
|
| 128 |
159 |
|
// than an error page. The ceiling is the one the JS's own paging
|
| 184 |
215 |
|
} else {
|
| 185 |
216 |
|
action
|
| 186 |
217 |
|
};
|
|
218 |
+ |
let action = if self.ticked {
|
|
219 |
+ |
action.carrying("ticked", "all")
|
|
220 |
+ |
} else {
|
|
221 |
+ |
action
|
|
222 |
+ |
};
|
| 187 |
223 |
|
if self.shown == PAGE {
|
| 188 |
224 |
|
action
|
| 189 |
225 |
|
} else {
|
| 246 |
282 |
|
|
| 247 |
283 |
|
/// One thread as a row.
|
| 248 |
284 |
|
///
|
| 249 |
|
- |
/// # The first finding
|
|
285 |
+ |
/// # The first finding, closed
|
| 250 |
286 |
|
///
|
| 251 |
|
- |
/// **Nothing describes a selection, or an action taken over one.**
|
|
287 |
+ |
/// **Nothing described a selection, or an action taken over one.**
|
| 252 |
288 |
|
///
|
| 253 |
|
- |
/// [`Row::selectable`] says a row is ticked, and that is the whole of it. The
|
| 254 |
|
- |
/// shipped screen has a `SelectionManager` with shift-range select, a bulk bar
|
| 255 |
|
- |
/// that appears at one selected and counts upward, and five actions that run
|
| 256 |
|
- |
/// over the set — mark read, archive, snooze, delete, select-all. A description
|
| 257 |
|
- |
/// can say each row is ticked and cannot say what the ticks are *for*, so the
|
| 258 |
|
- |
/// bar has nowhere to go and the five actions have no address to call.
|
|
289 |
+ |
/// [`Row::selectable`] said a row was ticked and that was the whole of it, so
|
|
290 |
+ |
/// the port described no ticks at all: the shipped screen's `SelectionManager`,
|
|
291 |
+ |
/// its counting bulk bar and its five verbs had nothing to say themselves in.
|
| 259 |
292 |
|
///
|
| 260 |
|
- |
/// Not worked around. The workaround is a route per bulk action taking a list of
|
| 261 |
|
- |
/// ids in a param, which is a real design and not one this port should invent on
|
| 262 |
|
- |
/// its own from one consumer's shape. Filed against quasicoherent.
|
|
293 |
+ |
/// Closed by quasi 0.37.0. A screen names the set with
|
|
294 |
+ |
/// [`Screen::selecting`](quasi_router::Screen::selecting), a row says what its
|
|
295 |
+ |
/// tick contributes with [`Row::ticking`], and a control says it runs over the
|
|
296 |
+ |
/// whole of it with [`Act::over`], which sends every ticked value under
|
|
297 |
+ |
/// [`Node::TICKED`]. So the row ticks under its own id and [`bulk`] is the bar.
|
|
298 |
+ |
///
|
|
299 |
+ |
/// The tick state itself stays where it was put. A renderer holds which rows
|
|
300 |
+ |
/// are ticked, the running count and the clearing; this screen holds only
|
|
301 |
+ |
/// whether the rows *arrive* ticked, which is [`View::ticked`] and is select-all.
|
| 263 |
302 |
|
///
|
| 264 |
303 |
|
/// One part of it the port gets for free and is worth writing down: `emails.js`
|
| 265 |
304 |
|
/// carries a charter rule that selection clears on a filter change, "so bulk
|
| 266 |
305 |
|
/// actions can't target rows the user can no longer see", and it is enforced by
|
| 267 |
306 |
|
/// a `clearSelectionIfAny` call in each of the four filter handlers. A filter
|
| 268 |
307 |
|
/// here is an address. A different view is a different page, and there is no
|
| 269 |
|
- |
/// selection to carry into it, so the rule holds without anyone maintaining it.
|
|
308 |
+ |
/// selection to carry into it, so the rule holds for the ticks a user made
|
|
309 |
+ |
/// without anyone maintaining it. The one half that does need saying is
|
|
310 |
+ |
/// `ticked=all`, which rides on the address: [`filters`] drops it, so "all" can
|
|
311 |
+ |
/// never quietly come to mean a different all.
|
| 270 |
312 |
|
///
|
| 271 |
313 |
|
/// # What the row does have
|
| 272 |
314 |
|
///
|
| 308 |
350 |
|
}
|
| 309 |
351 |
|
|
| 310 |
352 |
|
row.current = open == Some(email.id);
|
|
353 |
+ |
// The tick joins the screen's set under the message's own id, which is what
|
|
354 |
+ |
// the bar acts on. `emails.js` gathers the same ids from the checkboxes by
|
|
355 |
+ |
// hand (`SelectionManager.setItems`, over `mostRecentEmail.id`).
|
|
356 |
+ |
row = row.ticking(email.id.to_string(), view.ticked);
|
| 311 |
357 |
|
row.activate = Some(view.carry(Action::get(format!("/emails/{}", email.id))));
|
| 312 |
358 |
|
for act in row_acts(email, view) {
|
| 313 |
359 |
|
row = row.act(act);
|
| 466 |
512 |
|
|
| 467 |
513 |
|
// A filter change is a new page of results, so `shown` goes back to one
|
| 468 |
514 |
|
// page. Carrying it would ask for 400 rows of a folder holding nine.
|
|
515 |
+ |
//
|
|
516 |
+ |
// `ticked` goes with it, and for the sharper reason `row_for` records:
|
|
517 |
+ |
// carrying select-all through a filter change is exactly what `emails.js`'s
|
|
518 |
+ |
// charter rule forbids, since "everything" would silently come to mean a
|
|
519 |
+ |
// different everything.
|
| 469 |
520 |
|
let base = View {
|
| 470 |
521 |
|
shown: PAGE,
|
|
522 |
+ |
ticked: false,
|
| 471 |
523 |
|
..view.clone()
|
| 472 |
524 |
|
};
|
| 473 |
525 |
|
|
| 520 |
572 |
|
Ok(out)
|
| 521 |
573 |
|
}
|
| 522 |
574 |
|
|
|
575 |
+ |
/// The controls over the selection.
|
|
576 |
+ |
///
|
|
577 |
+ |
/// The shipped bar's five buttons, in its order: Mark Selected Read, Archive,
|
|
578 |
+ |
/// Snooze, Delete, Select All. Four of them run over the set and the fifth is
|
|
579 |
+ |
/// an address, which is the split [`View::ticked`] describes.
|
|
580 |
+ |
///
|
|
581 |
+ |
/// Snooze is the one that needs a value before it can go, and it asks for it
|
|
582 |
+ |
/// with [`Act::asking`] rather than through a form. `bulk-actions.js` opens a
|
|
583 |
+ |
/// modal for the same moment (`openBulkSnoozeModal`), which is a whole screen
|
|
584 |
+ |
/// raised to collect one time; described, it is the verb carrying the question
|
|
585 |
+ |
/// it has to ask, and whether that becomes a popover, a line under the button
|
|
586 |
+ |
/// or a prompt is the renderer's.
|
|
587 |
+ |
///
|
|
588 |
+ |
/// The options are [`get_snooze_options`]'s, the same ones the open thread
|
|
589 |
+ |
/// offers, computed from the local clock per render so "Later Today" stops
|
|
590 |
+ |
/// being offered once it means nothing. No blank leading option, unlike the
|
|
591 |
+ |
/// task list's snooze picker: that one writes on change and needed a resting
|
|
592 |
+ |
/// state, and this one is answered by the press.
|
|
593 |
+ |
///
|
|
594 |
+ |
/// # What is not here
|
|
595 |
+ |
///
|
|
596 |
+ |
/// The count. The bar says "3 selected" and hides itself at zero, and neither
|
|
597 |
+ |
/// is sayable here, because the ticks belong to the renderer until a press
|
|
598 |
+ |
/// sends them. That is the right place for it — a renderer knows exactly how
|
|
599 |
+ |
/// many boxes it drew ticked — and every renderer has drawn a control over an
|
|
600 |
+ |
/// empty selection as disabled since 2026-08-16, which is the hiding half
|
|
601 |
+ |
/// arrived at from the other side. The bar is always on screen, which is the
|
|
602 |
+ |
/// honest version of not knowing.
|
|
603 |
+ |
fn bulk(view: &View) -> Vec<Node> {
|
|
604 |
+ |
let over = |suffix: &str| view.carry(Action::post(format!("/emails/list/{suffix}")));
|
|
605 |
+ |
|
|
606 |
+ |
let mut out = vec![
|
|
607 |
+ |
Node::Act(Act::new("Mark read", over("read")).over(SELECTION)),
|
|
608 |
+ |
Node::Act(Act::new("Archive", over("archive")).over(SELECTION)),
|
|
609 |
+ |
Node::Act(
|
|
610 |
+ |
Act::new("Snooze", over("snooze"))
|
|
611 |
+ |
.over(SELECTION)
|
|
612 |
+ |
.asking(Field::select(
|
|
613 |
+ |
"until",
|
|
614 |
+ |
"Snooze until",
|
|
615 |
+ |
get_snooze_options()
|
|
616 |
+ |
.options
|
|
617 |
+ |
.into_iter()
|
|
618 |
+ |
.map(|option| Choice::new(option.time.to_rfc3339(), option.label))
|
|
619 |
+ |
.collect(),
|
|
620 |
+ |
)),
|
|
621 |
+ |
),
|
|
622 |
+ |
Node::Act(
|
|
623 |
+ |
Act::new("Delete", over("delete"))
|
|
624 |
+ |
.tone(makeover_layout::Tone::Danger)
|
|
625 |
+ |
.over(SELECTION)
|
|
626 |
+ |
.confirm("Delete every selected email? This cannot be undone."),
|
|
627 |
+ |
),
|
|
628 |
+ |
];
|
|
629 |
+ |
|
|
630 |
+ |
// Select-all is an address, so its opposite is the same address without it,
|
|
631 |
+ |
// and it is only offered when there is something to clear.
|
|
632 |
+ |
out.push(Node::act(
|
|
633 |
+ |
"Select all",
|
|
634 |
+ |
View {
|
|
635 |
+ |
ticked: true,
|
|
636 |
+ |
..view.clone()
|
|
637 |
+ |
}
|
|
638 |
+ |
.list(),
|
|
639 |
+ |
));
|
|
640 |
+ |
if view.ticked {
|
|
641 |
+ |
out.push(Node::act(
|
|
642 |
+ |
"Clear selection",
|
|
643 |
+ |
View {
|
|
644 |
+ |
ticked: false,
|
|
645 |
+ |
..view.clone()
|
|
646 |
+ |
}
|
|
647 |
+ |
.list(),
|
|
648 |
+ |
));
|
|
649 |
+ |
}
|
|
650 |
+ |
|
|
651 |
+ |
out
|
|
652 |
+ |
}
|
|
653 |
+ |
|
| 523 |
654 |
|
/// The whole screen, with one thread open or none.
|
| 524 |
655 |
|
///
|
| 525 |
656 |
|
/// Built here rather than inside each route for the reason the projects screen
|
| 547 |
678 |
|
"Mark all read",
|
| 548 |
679 |
|
view.carry(Action::post("/emails/read-all")),
|
| 549 |
680 |
|
));
|
|
681 |
+ |
band = band.extend(bulk(view));
|
| 550 |
682 |
|
|
| 551 |
683 |
|
let thread_pane = match open {
|
| 552 |
684 |
|
Some(id) => thread_slot(state, id, view)?,
|
| 554 |
686 |
|
};
|
| 555 |
687 |
|
|
| 556 |
688 |
|
Ok(Screen::list_detail("Emails", false)
|
|
689 |
+ |
.selecting(SELECTION)
|
| 557 |
690 |
|
.with(band)
|
| 558 |
691 |
|
.with(Slot::new(LIST, RegionKind::Pane).with(list(state, view, open)?))
|
| 559 |
692 |
|
.with(thread_pane))
|
| 923 |
1056 |
|
))
|
| 924 |
1057 |
|
}
|
| 925 |
1058 |
|
|
|
1059 |
+ |
/// Every message the user ticked, in the order they arrived.
|
|
1060 |
+ |
///
|
|
1061 |
+ |
/// The ticks come back under one repeated name, [`Node::TICKED`], which is what
|
|
1062 |
+ |
/// [`quasi_router::Params::get_all`] is for and why no delimiter had to be one
|
|
1063 |
+ |
/// no id can contain.
|
|
1064 |
+ |
///
|
|
1065 |
+ |
/// An id that does not parse is dropped rather than refused, on the task list's
|
|
1066 |
+ |
/// reasoning: a bulk write is answered by the list it happened in, and failing
|
|
1067 |
+ |
/// the whole press over one malformed value would lose the other thirty-nine.
|
|
1068 |
+ |
/// The count in the toast is what the user actually gets, so a drop shows up as
|
|
1069 |
+ |
/// a smaller number.
|
|
1070 |
+ |
///
|
|
1071 |
+ |
/// An empty set is not an error either. Every renderer has drawn a control over
|
|
1072 |
+ |
/// an empty selection as disabled since 2026-08-16, so the ordinary way to
|
|
1073 |
+ |
/// arrive here with nothing is gone, and the ways that are left — a hand-typed
|
|
1074 |
+ |
/// request, a webview host serving no selection script — deserve the unchanged
|
|
1075 |
+ |
/// list rather than a 404. `bulk-actions.js` reaches the same place with its
|
|
1076 |
+ |
/// `if (selectedIds.size === 0) return;`.
|
|
1077 |
+ |
fn chosen(request: &quasi_router::Request) -> Vec<EmailId> {
|
|
1078 |
+ |
request
|
|
1079 |
+ |
.payload
|
|
1080 |
+ |
.get_all(quasi_router::Node::TICKED)
|
|
1081 |
+ |
.filter_map(|raw| uuid::Uuid::parse_str(raw.trim()).ok())
|
|
1082 |
+ |
.map(EmailId::from)
|
|
1083 |
+ |
.collect()
|
|
1084 |
+ |
}
|
|
1085 |
+ |
|
|
1086 |
+ |
/// `N emails` or `1 email`, for a toast that counts.
|
|
1087 |
+ |
fn counted(n: usize) -> String {
|
|
1088 |
+ |
if n == 1 {
|
|
1089 |
+ |
"1 email".to_owned()
|
|
1090 |
+ |
} else {
|
|
1091 |
+ |
format!("{n} emails")
|
|
1092 |
+ |
}
|
|
1093 |
+ |
}
|
|
1094 |
+ |
|
|
1095 |
+ |
/// The list after a bulk write, with nothing ticked.
|
|
1096 |
+ |
///
|
|
1097 |
+ |
/// The ticks are cleared by answering a view that has none, which is
|
|
1098 |
+ |
/// `bulk-actions.js`'s `clearSelection()` in each of its five paths arrived at
|
|
1099 |
+ |
/// from the other side. Nothing to clear renderer-side either: the rows are
|
|
1100 |
+ |
/// redrawn, and a row that comes back unticked is unticked.
|
|
1101 |
+ |
fn bulk_wrote(
|
|
1102 |
+ |
state: &AppState,
|
|
1103 |
+ |
request: &quasi_router::Request,
|
|
1104 |
+ |
message: String,
|
|
1105 |
+ |
) -> Result<Response, RouteError> {
|
|
1106 |
+ |
let view = View {
|
|
1107 |
+ |
ticked: false,
|
|
1108 |
+ |
..View::of(request)
|
|
1109 |
+ |
};
|
|
1110 |
+ |
Ok(wrote(state, &view, None)?.toast(makeover_layout::Tone::Success, message))
|
|
1111 |
+ |
}
|
|
1112 |
+ |
|
|
1113 |
+ |
/// Mark every ticked message read.
|
|
1114 |
+ |
///
|
|
1115 |
+ |
/// One at a time through the same repository call a row's own Mark read takes,
|
|
1116 |
+ |
/// which is what `bulk-actions.js` does with its `Promise.allSettled` over the
|
|
1117 |
+ |
/// per-message API. A message that has gone since the list was drawn is skipped
|
|
1118 |
+ |
/// rather than failing the press.
|
|
1119 |
+ |
fn read_chosen(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
|
|
1120 |
+ |
let mut done = 0;
|
|
1121 |
+ |
for id in chosen(&request) {
|
|
1122 |
+ |
if state
|
|
1123 |
+ |
.emails
|
|
1124 |
+ |
.mark_read(id, DESKTOP_USER_ID)
|
|
1125 |
+ |
.map_err(|error| RouteError::internal(error.to_string()))?
|
|
1126 |
+ |
{
|
|
1127 |
+ |
done += 1;
|
|
1128 |
+ |
}
|
|
1129 |
+ |
}
|
|
1130 |
+ |
bulk_wrote(state, &request, format!("{} marked read.", counted(done)))
|
|
1131 |
+ |
}
|
|
1132 |
+ |
|
|
1133 |
+ |
/// Archive every ticked message.
|
|
1134 |
+ |
///
|
|
1135 |
+ |
/// The local half only; see [`set_archived`] for why the IMAP half cannot be
|
|
1136 |
+ |
/// described from here.
|
|
1137 |
+ |
fn archive_chosen(
|
|
1138 |
+ |
state: &AppState,
|
|
1139 |
+ |
request: quasi_router::Request,
|
|
1140 |
+ |
) -> Result<Response, RouteError> {
|
|
1141 |
+ |
let mut done = 0;
|
|
1142 |
+ |
for id in chosen(&request) {
|
|
1143 |
+ |
if state
|
|
1144 |
+ |
.emails
|
|
1145 |
+ |
.archive(id, DESKTOP_USER_ID)
|
|
1146 |
+ |
.map_err(|error| RouteError::internal(error.to_string()))?
|
|
1147 |
+ |
{
|
|
1148 |
+ |
done += 1;
|
|
1149 |
+ |
}
|
|
1150 |
+ |
}
|
|
1151 |
+ |
bulk_wrote(state, &request, format!("{} archived.", counted(done)))
|
|
1152 |
+ |
}
|
|
1153 |
+ |
|
|
1154 |
+ |
/// Snooze every ticked message until the time the verb asked for.
|
|
1155 |
+ |
///
|
|
1156 |
+ |
/// The time arrives under its own [`Field::name`] because that is how
|
|
1157 |
+ |
/// [`Act::asks`] sends it, so this reads `until` exactly as [`set_snooze`]
|
|
1158 |
+ |
/// does, and refuses a past one for the same reason: the repository would take
|
|
1159 |
+ |
/// it, `is_snoozed` would read false the moment it landed, and the user would
|
|
1160 |
+ |
/// be told forty messages were hidden when none of them were.
|
|
1161 |
+ |
fn snooze_chosen(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
|
|
1162 |
+ |
let until = request
|
|
1163 |
+ |
.payload
|
|
1164 |
+ |
.get("until")
|
|
1165 |
+ |
.and_then(|raw| DateTime::parse_from_rfc3339(raw).ok())
|
|
1166 |
+ |
.map(|when| when.with_timezone(&Utc))
|
|
1167 |
+ |
.filter(|when| *when > Utc::now())
|
|
1168 |
+ |
.ok_or_else(|| RouteError::not_found("not a time to snooze until"))?;
|
|
1169 |
+ |
|
|
1170 |
+ |
let mut done = 0;
|
|
1171 |
+ |
for id in chosen(&request) {
|
|
1172 |
+ |
if state
|
|
1173 |
+ |
.emails
|
|
1174 |
+ |
.snooze(id, DESKTOP_USER_ID, until)
|
|
1175 |
+ |
.map_err(|error| RouteError::internal(error.to_string()))?
|
|
1176 |
+ |
.is_some()
|
|
1177 |
+ |
{
|
|
1178 |
+ |
done += 1;
|
|
1179 |
+ |
}
|
|
1180 |
+ |
}
|
|
1181 |
+ |
bulk_wrote(
|
|
1182 |
+ |
state,
|
|
1183 |
+ |
&request,
|
|
1184 |
+ |
format!(
|
|
1185 |
+ |
"{} snoozed until {}.",
|
|
1186 |
+ |
counted(done),
|
|
1187 |
+ |
date_utils::format_relative_future(until, Utc::now())
|
|
1188 |
+ |
),
|
|
1189 |
+ |
)
|
|
1190 |
+ |
}
|
|
1191 |
+ |
|
|
1192 |
+ |
/// Delete every ticked message.
|
|
1193 |
+ |
fn delete_chosen(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
|
|
1194 |
+ |
let mut done = 0;
|
|
1195 |
+ |
for id in chosen(&request) {
|
|
1196 |
+ |
if state
|
|
1197 |
+ |
.emails
|
|
1198 |
+ |
.delete(id, DESKTOP_USER_ID)
|
|
1199 |
+ |
.map_err(|error| RouteError::internal(error.to_string()))?
|
|
1200 |
+ |
{
|
|
1201 |
+ |
done += 1;
|
|
1202 |
+ |
}
|
|
1203 |
+ |
}
|
|
1204 |
+ |
bulk_wrote(state, &request, format!("{} deleted.", counted(done)))
|
|
1205 |
+ |
}
|
|
1206 |
+ |
|
| 926 |
1207 |
|
/// Archive, or bring it back.
|
| 927 |
1208 |
|
///
|
| 928 |
1209 |
|
/// # The fifth finding
|
| 1196 |
1477 |
|
.get("/emails/list", list_only)
|
| 1197 |
1478 |
|
.get("/emails", index)
|
| 1198 |
1479 |
|
.post("/emails/read-all", read_all)
|
|
1480 |
+ |
// Ahead of the `{id}` writes below for the same reason `/emails/list`
|
|
1481 |
+ |
// is ahead of `/emails`: a literal segment outranks a capture, and a
|
|
1482 |
+ |
// reader should not have to know that to be sure `list` never arrives
|
|
1483 |
+ |
// as an id.
|
|
1484 |
+ |
.post("/emails/list/read", read_chosen)
|
|
1485 |
+ |
.post("/emails/list/archive", archive_chosen)
|
|
1486 |
+ |
.post("/emails/list/snooze", snooze_chosen)
|
|
1487 |
+ |
.post("/emails/list/delete", delete_chosen)
|
| 1199 |
1488 |
|
.get("/emails/{id}", thread)
|
| 1200 |
1489 |
|
.post("/emails/{id}/read", set_read)
|
| 1201 |
1490 |
|
.post("/emails/{id}/archive", set_archived)
|