max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
4 files changed,
+282 insertions,
-25 deletions
| @@ -65,7 +65,7 @@ | |||
| 65 | 65 | const UPSTREAM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); | |
| 66 | 66 | ||
| 67 | 67 | /// One community this reader belongs to, as the screens need it. | |
| 68 | - | pub struct MembershipView { | |
| 68 | + | pub(crate) struct MembershipView { | |
| 69 | 69 | community: String, | |
| 70 | 70 | profile_url: String, | |
| 71 | 71 | role: String, | |
| @@ -105,6 +105,30 @@ | |||
| 105 | 105 | )) | |
| 106 | 106 | } | |
| 107 | 107 | ||
| 108 | + | /// The one read both panes make, for the mounts that serve them from residuals. | |
| 109 | + | /// | |
| 110 | + | /// Answers the memberships and where they are, which is what both panes fill | |
| 111 | + | /// from. A pane that is not configured has no base and no memberships, and | |
| 112 | + | /// `library_screen` already treats those as one state. | |
| 113 | + | pub(crate) fn reading(viewer: &Viewer) -> Result<(Vec<MembershipView>, String), RouteError> { | |
| 114 | + | let Some(base) = configured_base(viewer) else { | |
| 115 | + | return Ok((Vec::new(), String::new())); | |
| 116 | + | }; | |
| 117 | + | let memberships = fetch(viewer, viewer.reader()?, &base); | |
| 118 | + | Ok((memberships, base)) | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | /// The same read for the settings pane, which refuses when nothing is | |
| 122 | + | /// configured rather than drawing an empty list. See [`settings_screen`]. | |
| 123 | + | pub(crate) fn settings_reading( | |
| 124 | + | viewer: &Viewer, | |
| 125 | + | ) -> Result<(Vec<MembershipView>, String), RouteError> { | |
| 126 | + | let base = configured_base(viewer) | |
| 127 | + | .ok_or_else(|| RouteError::not_found("forums are not configured"))?; | |
| 128 | + | let memberships = fetch(viewer, viewer.reader()?, &base); | |
| 129 | + | Ok((memberships, base)) | |
| 130 | + | } | |
| 131 | + | ||
| 108 | 132 | /// Where Multithreaded lives, if it lives anywhere. | |
| 109 | 133 | fn configured_base(viewer: &Viewer) -> Option<String> { | |
| 110 | 134 | viewer.app.config.integrations.mt_base_url.clone() | |
| @@ -173,16 +197,25 @@ | |||
| 173 | 197 | /// one word in it that goes somewhere, and splitting it into text, an act | |
| 174 | 198 | /// and more text is how a sentence stops reading as a sentence in every | |
| 175 | 199 | /// host. | |
| 200 | + | /// | |
| 201 | + | /// [`super::own_prose`]'s body, written here rather than called, because the | |
| 202 | + | /// address has to be a hole **inside** the markdown. Handed to `own_prose` | |
| 203 | + | /// the sentence is formatted first, so a staged shape gets one sentinel | |
| 204 | + | /// where the whole document should be; written here the derivation parses | |
| 205 | + | /// the link with a sentinel in its destination and the residual holds | |
| 206 | + | /// `<a href="` around a gap. `own_prose` carries the rule. | |
| 207 | + | #[staged] | |
| 176 | 208 | shape upstream_line(base: &str) -> Node; | |
| 177 | 209 | ||
| 178 | - | include super::own_prose( | |
| 179 | - | "Your memberships across [Multithreaded]({base}) forum communities." | |
| 180 | - | ); | |
| 210 | + | rich "Your memberships across [Multithreaded]({base}) forum communities." { | |
| 211 | + | trust quasi_router::Trust::Trusted; | |
| 212 | + | } | |
| 181 | 213 | } | |
| 182 | 214 | ||
| 183 | 215 | declare! { | |
| 184 | 216 | /// Everything inside the library's tab pane. | |
| 185 | - | shape library_pane(memberships: &[MembershipView], base: &str) -> Node; | |
| 217 | + | #[staged] | |
| 218 | + | pub(crate) shape library_pane(memberships: &[MembershipView], base: &str) -> Node; | |
| 186 | 219 | ||
| 187 | 220 | region LIBRARY_REGION as Pane { | |
| 188 | 221 | // The way out is offered only when there is somewhere to send them, | |
| @@ -204,16 +237,19 @@ | |||
| 204 | 237 | /// | |
| 205 | 238 | /// The heading and the line are drawn either way here, unlike the library's, | |
| 206 | 239 | /// which is the one real difference between the two screens. | |
| 207 | - | shape settings_pane(memberships: &[MembershipView], base: &str) -> Node; | |
| 240 | + | #[staged] | |
| 241 | + | pub(crate) shape settings_pane(memberships: &[MembershipView], base: &str) -> Node; | |
| 208 | 242 | ||
| 209 | 243 | region SETTINGS_REGION as Pane { | |
| 210 | 244 | section "Forum Communities"; | |
| 211 | 245 | include upstream_line(base); | |
| 212 | 246 | ||
| 213 | - | given memberships.is_empty() { | |
| 214 | - | true -> empty "You haven't joined any forum communities yet."; | |
| 215 | - | otherwise -> include table(memberships); | |
| 216 | - | } | |
| 247 | + | // Two guards rather than a dispatch, which renders the same and is | |
| 248 | + | // what the seam can derive: a dispatch's arms replace each other at one | |
| 249 | + | // position, so only one reaches the residual and the rest are lost. See | |
| 250 | + | // quasi-declare's `Emission::Given`, which refuses it. | |
| 251 | + | empty "You haven't joined any forum communities yet." when memberships.is_empty(); | |
| 252 | + | include table(memberships) unless memberships.is_empty(); | |
| 217 | 253 | } | |
| 218 | 254 | } | |
| 219 | 255 | ||
| @@ -224,6 +260,7 @@ | |||
| 224 | 260 | /// columns out of it, so every membership contributes the same four cells. | |
| 225 | 261 | /// Nothing is paged, so no `more`: this is a whole set the handler already | |
| 226 | 262 | /// counted. | |
| 263 | + | #[staged] | |
| 227 | 264 | shape table(memberships: &[MembershipView]) -> Node; | |
| 228 | 265 | ||
| 229 | 266 | table { | |
| @@ -266,6 +303,21 @@ | |||
| 266 | 303 | Webview::new().with_shell(viewer.shell()) | |
| 267 | 304 | } | |
| 268 | 305 | ||
| 306 | + | /// One membership as the tests draw it. | |
| 307 | + | /// | |
| 308 | + | /// Module-level rather than inside `mod tests` because `quasi::residuals` needs | |
| 309 | + | /// one too, and `MembershipView` is this module's own type. Test-only. | |
| 310 | + | #[cfg(test)] | |
| 311 | + | pub(crate) fn sample(community: &str, role: &str) -> MembershipView { | |
| 312 | + | MembershipView { | |
| 313 | + | community: community.into(), | |
| 314 | + | profile_url: format!("https://mt.example.com/p/{community}/u/max"), | |
| 315 | + | role: role.into(), | |
| 316 | + | posts: "12".into(), | |
| 317 | + | joined: "Aug 10, 2026".into(), | |
| 318 | + | } | |
| 319 | + | } | |
| 320 | + | ||
| 269 | 321 | #[cfg(test)] | |
| 270 | 322 | mod tests { | |
| 271 | 323 | use quasi_axum::Serves; | |
| @@ -273,15 +325,7 @@ | |||
| 273 | 325 | ||
| 274 | 326 | use super::*; | |
| 275 | 327 | ||
| 276 | - | fn membership(community: &str, role: &str) -> MembershipView { | |
| 277 | - | MembershipView { | |
| 278 | - | community: community.into(), | |
| 279 | - | profile_url: format!("https://mt.example.com/p/{community}/u/max"), | |
| 280 | - | role: role.into(), | |
| 281 | - | posts: "12".into(), | |
| 282 | - | joined: "Aug 10, 2026".into(), | |
| 283 | - | } | |
| 284 | - | } | |
| 328 | + | use super::sample as membership; | |
| 285 | 329 | ||
| 286 | 330 | fn render(node: &Node) -> String { | |
| 287 | 331 | Webview::new().fragment(node) |
| @@ -531,11 +531,19 @@ | |||
| 531 | 531 | // the same table under different chrome. | |
| 532 | 532 | ( | |
| 533 | 533 | forum_memberships::LIBRARY_PATH, | |
| 534 | - | mount( | |
| 534 | + | served_panel_mount( | |
| 535 | 535 | app, | |
| 536 | - | forum_memberships::library_screen, | |
| 536 | + | forum_memberships::LIBRARY_REGION, | |
| 537 | 537 | &[], | |
| 538 | 538 | forum_memberships::renderer, | |
| 539 | + | |viewer, _| { | |
| 540 | + | let (memberships, base) = forum_memberships::reading(viewer)?; | |
| 541 | + | Ok(forum_memberships::library_pane_serve( | |
| 542 | + | &residuals::FORUMS_LIBRARY, | |
| 543 | + | &memberships, | |
| 544 | + | &base, | |
| 545 | + | )) | |
| 546 | + | }, | |
| 539 | 547 | ), | |
| 540 | 548 | ), | |
| 541 | 549 | ( | |
| @@ -566,11 +574,19 @@ | |||
| 566 | 574 | ), | |
| 567 | 575 | ( | |
| 568 | 576 | forum_memberships::SETTINGS_PATH, | |
| 569 | - | mount( | |
| 577 | + | served_panel_mount( | |
| 570 | 578 | app, | |
| 571 | - | forum_memberships::settings_screen, | |
| 579 | + | forum_memberships::SETTINGS_REGION, | |
| 572 | 580 | &[], | |
| 573 | 581 | forum_memberships::renderer, | |
| 582 | + | |viewer, _| { | |
| 583 | + | let (memberships, base) = forum_memberships::settings_reading(viewer)?; | |
| 584 | + | Ok(forum_memberships::settings_pane_serve( | |
| 585 | + | &residuals::FORUMS_SETTINGS, | |
| 586 | + | &memberships, | |
| 587 | + | &base, | |
| 588 | + | )) | |
| 589 | + | }, | |
| 574 | 590 | ), | |
| 575 | 591 | ), | |
| 576 | 592 | ] | |
| @@ -798,6 +814,74 @@ | |||
| 798 | 814 | ] | |
| 799 | 815 | } | |
| 800 | 816 | ||
| 817 | + | /// A panel whose region is already written, mounted. | |
| 818 | + | /// | |
| 819 | + | /// The seam's mount for a screen in [`PATHS`], which is a different answer from | |
| 820 | + | /// a document's: a panel is fetched by a page that already exists, so what it | |
| 821 | + | /// returns is a fragment plus the `HX-Retarget` naming the region it changed. | |
| 822 | + | /// There is no document, no shell and no `Screen` -- which is why this is not | |
| 823 | + | /// [`served_document_mount`] with a flag. | |
| 824 | + | /// | |
| 825 | + | /// # The nest is still the adapter's, and the read is merged in front | |
| 826 | + | /// | |
| 827 | + | /// A panel screen's writes stay on the adapter: they answer a `Response` the | |
| 828 | + | /// router builds per request and there is nothing to derive about a deletion. | |
| 829 | + | /// So the nest is [`nest`] with no read registered, and the seam's `GET /` goes | |
| 830 | + | /// on the router in front of it. The adapter mounts as a fallback, deliberately | |
| 831 | + | /// (see `quasi_axum::Adapter::into_router`), so a route added here wins without | |
| 832 | + | /// anything being taken away. | |
| 833 | + | /// | |
| 834 | + | /// # The headers are the ones `quasi_http` would have set | |
| 835 | + | /// | |
| 836 | + | /// `Content-Type: text/html; charset=utf-8` and `HX-Retarget: #region`, which is | |
| 837 | + | /// exactly what `respond` emits for `Outcome::Fragment`. Written out rather than | |
| 838 | + | /// reached through, because reaching through means building the `Response` this | |
| 839 | + | /// path exists to avoid building. | |
| 840 | + | fn served_panel_mount( | |
| 841 | + | app: &AppState, | |
| 842 | + | region: &'static str, | |
| 843 | + | writes: &[(quasi_router::Method, &'static str, Screen)], | |
| 844 | + | renderer: fn(&Viewer) -> quasi_webview::Webview, | |
| 845 | + | answer: fn(&Viewer, &Carried) -> Result<String, quasi_router::RouteError>, | |
| 846 | + | ) -> axum::Router { | |
| 847 | + | let held = app.clone(); | |
| 848 | + | nest(app, None, writes, renderer).route( | |
| 849 | + | "/", | |
| 850 | + | axum::routing::get(move |mut parts: axum::http::request::Parts| { | |
| 851 | + | let app = held.clone(); | |
| 852 | + | async move { | |
| 853 | + | use axum::response::IntoResponse as _; | |
| 854 | + | ||
| 855 | + | // The gate is `Audience::Reader`, the same as the nest beside | |
| 856 | + | // it, so a signed-out request is refused before the handler | |
| 857 | + | // runs. A panel is fetched by a page that already checked, so | |
| 858 | + | // the adapter's bare refusal is the right answer here and the | |
| 859 | + | // branded 401 is the document mounts'. | |
| 860 | + | let viewer = match viewer_factory(app, Audience::Reader)(&parts).await { | |
| 861 | + | Ok(viewer) => viewer, | |
| 862 | + | Err(refusal) => { | |
| 863 | + | return axum::http::StatusCode::from_u16(refusal.class.http_status()) | |
| 864 | + | .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR) | |
| 865 | + | .into_response(); | |
| 866 | + | } | |
| 867 | + | }; | |
| 868 | + | ||
| 869 | + | let carried = Carried::of(&mut parts).await; | |
| 870 | + | match answer(&viewer, &carried) { | |
| 871 | + | Ok(markup) => ( | |
| 872 | + | [(quasi_axum::htmx::RETARGET, format!("#{region}"))], | |
| 873 | + | axum::response::Html(markup), | |
| 874 | + | ) | |
| 875 | + | .into_response(), | |
| 876 | + | Err(refusal) => axum::http::StatusCode::from_u16(refusal.class.http_status()) | |
| 877 | + | .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR) | |
| 878 | + | .into_response(), | |
| 879 | + | } | |
| 880 | + | } | |
| 881 | + | }), | |
| 882 | + | ) | |
| 883 | + | } | |
| 884 | + | ||
| 801 | 885 | /// A described document a reader NAVIGATES to with or without a session. | |
| 802 | 886 | /// | |
| 803 | 887 | /// [`document_mount`] with the two things that make it gated removed: the | |
| @@ -1321,6 +1405,11 @@ | |||
| 1321 | 1405 | // mounted unconditionally, so the thing to count is the adapter call | |
| 1322 | 1406 | // each entry makes. Not `mount(app,`: rustfmt breaks the longer entries | |
| 1323 | 1407 | // across lines and that token then finds three of the six. | |
| 1408 | + | // | |
| 1409 | + | // `served_panel_mount(` ends in the same token and is counted with | |
| 1410 | + | // them, which is what this wants: a panel that moves onto the residual | |
| 1411 | + | // seam is still a screen `PATHS` has to list. `writes_only(` does not, | |
| 1412 | + | // which is also right -- those nests answer no address of their own. | |
| 1324 | 1413 | let registered = mounted.matches("mount(").count(); | |
| 1325 | 1414 | assert_eq!( | |
| 1326 | 1415 | registered, |
| @@ -81,6 +81,14 @@ | |||
| 81 | 81 | ("GIT_REPOS", |plan| { | |
| 82 | 82 | Node::Region(super::git_repos::page_region_staged(plan)) | |
| 83 | 83 | }), | |
| 84 | + | // One module, two screens: the library tab and the settings section are | |
| 85 | + | // the same table under different chrome, so each gets its own residual. | |
| 86 | + | ("FORUMS_LIBRARY", |plan| { | |
| 87 | + | super::forum_memberships::library_pane_staged(plan) | |
| 88 | + | }), | |
| 89 | + | ("FORUMS_SETTINGS", |plan| { | |
| 90 | + | super::forum_memberships::settings_pane_staged(plan) | |
| 91 | + | }), | |
| 84 | 92 | ] | |
| 85 | 93 | } | |
| 86 | 94 | ||
| @@ -569,6 +577,48 @@ | |||
| 569 | 577 | ); | |
| 570 | 578 | } | |
| 571 | 579 | ||
| 580 | + | /// Both forum panes fill to what the renderer builds. | |
| 581 | + | /// | |
| 582 | + | /// The first panel screens on the seam, and the first module with two of | |
| 583 | + | /// them: the library tab and the settings section draw the same table under | |
| 584 | + | /// different chrome, so each has a residual and both are checked here. | |
| 585 | + | /// | |
| 586 | + | /// Empty and populated, and with the upstream configured and not. The base | |
| 587 | + | /// address is a hole inside the sentence's markdown -- the link's own | |
| 588 | + | /// destination -- so an empty one is the shape that would show a residual | |
| 589 | + | /// with the address baked into a literal. | |
| 590 | + | #[test] | |
| 591 | + | fn both_forum_residuals_fill_to_what_the_renderer_builds() { | |
| 592 | + | use crate::quasi::forum_memberships::{ | |
| 593 | + | library_pane, library_pane_serve, sample, settings_pane, settings_pane_serve, | |
| 594 | + | }; | |
| 595 | + | use quasi_axum::Serves as _; | |
| 596 | + | ||
| 597 | + | let none: Vec<_> = Vec::new(); | |
| 598 | + | let some = vec![ | |
| 599 | + | sample("rust", "moderator"), | |
| 600 | + | sample("audio", "member"), | |
| 601 | + | sample("film", "member"), | |
| 602 | + | ]; | |
| 603 | + | ||
| 604 | + | for memberships in [&none, &some] { | |
| 605 | + | for base in ["https://mt.example.com", ""] { | |
| 606 | + | assert_eq!( | |
| 607 | + | library_pane_serve(&FORUMS_LIBRARY, memberships, base), | |
| 608 | + | Webview::new().fragment(&library_pane(memberships, base)), | |
| 609 | + | "library: {} memberships, base {base:?}", | |
| 610 | + | memberships.len(), | |
| 611 | + | ); | |
| 612 | + | assert_eq!( | |
| 613 | + | settings_pane_serve(&FORUMS_SETTINGS, memberships, base), | |
| 614 | + | Webview::new().fragment(&settings_pane(memberships, base)), | |
| 615 | + | "settings: {} memberships, base {base:?}", | |
| 616 | + | memberships.len(), | |
| 617 | + | ); | |
| 618 | + | } | |
| 619 | + | } | |
| 620 | + | } | |
| 621 | + | ||
| 572 | 622 | /// Every screen the roster names is checked above. | |
| 573 | 623 | /// | |
| 574 | 624 | /// The gap this closes is the one a table opens: a screen added to | |
| @@ -583,8 +633,9 @@ | |||
| 583 | 633 | #[test] | |
| 584 | 634 | fn every_screen_on_the_seam_is_checked() { | |
| 585 | 635 | /// Screens with their own filling test: `/use-cases`, `/fan-plus`, | |
| 586 | - | /// `/c/{username}/{slug}`, `/dashboard/export`, `/git/{owner}`. | |
| 587 | - | const HOLED: usize = 5; | |
| 636 | + | /// `/c/{username}/{slug}`, `/dashboard/export`, `/git/{owner}`, and the | |
| 637 | + | /// two forum panes. | |
| 638 | + | const HOLED: usize = 7; | |
| 588 | 639 | ||
| 589 | 640 | assert_eq!( | |
| 590 | 641 | roster().len(), |
| @@ -211,4 +211,77 @@ | |||
| 211 | 211 | ])), | |
| 212 | 212 | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div class=\"anchored\" id=\"git-repos-anchored\" data-menu=\"anchored\" hidden></div></div>")), | |
| 213 | 213 | ]); | |
| 214 | + | ||
| 215 | + | pub static FORUMS_LIBRARY: ::quasi_router::stage::Residual = | |
| 216 | + | ::quasi_router::stage::Residual::compiled(&[ | |
| 217 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div id=\"library-communities\" class=\"region pane\">")), | |
| 218 | + | ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[ | |
| 219 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div class=\"placeholder\" data-state=\"empty\" role=\"status\" aria-live=\"polite\"><p class=\"placeholder-text\">You haven't joined any forum communities yet.</p>")), | |
| 220 | + | ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[ | |
| 221 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div class=\"placeholder-action\"><a class=\"button\" data-act href=\"")), | |
| 222 | + | ::quasi_router::stage::Op::Hole { scope: 0, id: 0 }, | |
| 223 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("\" target=\"_blank\" rel=\"noopener noreferrer\">Browse Communities</a></div>")), | |
| 224 | + | ||
| 225 | + | ])), | |
| 226 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div>")), | |
| 227 | + | ||
| 228 | + | ])), | |
| 229 | + | ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[ | |
| 230 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div class=\"rich\" data-disable-scripting><p>Your memberships across <a href=\"")), | |
| 231 | + | ::quasi_router::stage::Op::Hole { scope: 1, id: 0 }, | |
| 232 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("\" rel=\"noopener noreferrer\">Multithreaded</a> forum communities.</p>\n</div>")), | |
| 233 | + | ||
| 234 | + | ])), | |
| 235 | + | ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[ | |
| 236 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div role=\"table\" class=\"table\"><div role=\"row\" class=\"table-head\"><span role=\"columnheader\" class=\"table-heading col-Community cell-fill cell-keeps\">Community</span><span role=\"columnheader\" class=\"table-heading col-Role cell-content cell-drops-next\">Role</span><span role=\"columnheader\" class=\"table-heading col-Posts cell-content cell-drops-next\">Posts</span><span role=\"columnheader\" class=\"table-heading col-Joined cell-content cell-drops-first\">Joined</span></div>")), | |
| 237 | + | ::quasi_router::stage::Op::Loop(::std::borrow::Cow::Borrowed(&[ | |
| 238 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div role=\"row\" class=\"table-row\" data-row><div class=\"cell col-Community cell-fill cell-keeps\"><a class=\"cell-link\" data-act href=\"")), | |
| 239 | + | ::quasi_router::stage::Op::Hole { scope: 2, id: 1 }, | |
| 240 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("\" target=\"_blank\" rel=\"noopener noreferrer\">")), | |
| 241 | + | ::quasi_router::stage::Op::Hole { scope: 2, id: 0 }, | |
| 242 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</a></div><div class=\"cell col-Role cell-content cell-drops-next\"><span class=\"cell-tokens\"><span class=\"badge\">")), | |
| 243 | + | ::quasi_router::stage::Op::Hole { scope: 2, id: 2 }, | |
| 244 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</span></span></div><div class=\"cell col-Posts cell-content cell-drops-next cell-value\">")), | |
| 245 | + | ::quasi_router::stage::Op::Hole { scope: 2, id: 3 }, | |
| 246 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div><div class=\"cell col-Joined cell-content cell-drops-first cell-value\">")), | |
| 247 | + | ::quasi_router::stage::Op::Hole { scope: 2, id: 4 }, | |
| 248 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div></div>")), | |
| 249 | + | ||
| 250 | + | ])), | |
| 251 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div>")), | |
| 252 | + | ||
| 253 | + | ])), | |
| 254 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div class=\"anchored\" id=\"library-communities-anchored\" data-menu=\"anchored\" hidden></div></div>")), | |
| 255 | + | ]); | |
| 256 | + | ||
| 257 | + | pub static FORUMS_SETTINGS: ::quasi_router::stage::Residual = | |
| 258 | + | ::quasi_router::stage::Residual::compiled(&[ | |
| 259 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div id=\"settings-forums\" class=\"region pane\"><h2 class=\"heading\">Forum Communities</h2><div class=\"rich\" data-disable-scripting><p>Your memberships across <a href=\"")), | |
| 260 | + | ::quasi_router::stage::Op::Hole { scope: 1, id: 0 }, | |
| 261 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("\" rel=\"noopener noreferrer\">Multithreaded</a> forum communities.</p>\n</div>")), | |
| 262 | + | ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[ | |
| 263 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div class=\"placeholder\" data-state=\"empty\" role=\"status\" aria-live=\"polite\"><p class=\"placeholder-text\">You haven't joined any forum communities yet.</p></div>")), | |
| 264 | + | ||
| 265 | + | ])), | |
| 266 | + | ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[ | |
| 267 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div role=\"table\" class=\"table\"><div role=\"row\" class=\"table-head\"><span role=\"columnheader\" class=\"table-heading col-Community cell-fill cell-keeps\">Community</span><span role=\"columnheader\" class=\"table-heading col-Role cell-content cell-drops-next\">Role</span><span role=\"columnheader\" class=\"table-heading col-Posts cell-content cell-drops-next\">Posts</span><span role=\"columnheader\" class=\"table-heading col-Joined cell-content cell-drops-first\">Joined</span></div>")), | |
| 268 | + | ::quasi_router::stage::Op::Loop(::std::borrow::Cow::Borrowed(&[ | |
| 269 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div role=\"row\" class=\"table-row\" data-row><div class=\"cell col-Community cell-fill cell-keeps\"><a class=\"cell-link\" data-act href=\"")), | |
| 270 | + | ::quasi_router::stage::Op::Hole { scope: 2, id: 1 }, | |
| 271 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("\" target=\"_blank\" rel=\"noopener noreferrer\">")), | |
| 272 | + | ::quasi_router::stage::Op::Hole { scope: 2, id: 0 }, | |
| 273 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</a></div><div class=\"cell col-Role cell-content cell-drops-next\"><span class=\"cell-tokens\"><span class=\"badge\">")), | |
| 274 | + | ::quasi_router::stage::Op::Hole { scope: 2, id: 2 }, | |
| 275 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</span></span></div><div class=\"cell col-Posts cell-content cell-drops-next cell-value\">")), | |
| 276 | + | ::quasi_router::stage::Op::Hole { scope: 2, id: 3 }, | |
| 277 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div><div class=\"cell col-Joined cell-content cell-drops-first cell-value\">")), | |
| 278 | + | ::quasi_router::stage::Op::Hole { scope: 2, id: 4 }, | |
| 279 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div></div>")), | |
| 280 | + | ||
| 281 | + | ])), | |
| 282 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div>")), | |
| 283 | + | ||
| 284 | + | ])), | |
| 285 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div class=\"anchored\" id=\"settings-forums-anchored\" data-menu=\"anchored\" hidden></div></div>")), | |
| 286 | + | ]); | |
| 214 | 287 |