tangled
alpha
login
or
join now
nonbinary.computer
/
weaver
atproto blogging
24
fork
atom
overview
issues
2
pulls
pipelines
ssr options for stuff
Orual
2 months ago
23946661
6b7587e4
+288
-134
9 changed files
expand all
collapse all
unified
split
crates
weaver-app
.env-example
.env-prod
src
components
css.rs
identity.rs
data.rs
env.rs
main.rs
views
home.rs
notebook.rs
-10
crates/weaver-app/.env-example
···
1
1
-
WEAVER_APP_ENV="dev"
2
2
-
WEAVER_APP_HOST="http://localhost"
3
3
-
WEAVER_APP_DOMAIN=""
4
4
-
WEAVER_PORT=8080
5
5
-
WEAVER_APP_SCOPES="atproto transition:generic"
6
6
-
WEAVER_CLIENT_NAME="Weaver"
7
7
-
8
8
-
WEAVER_LOGO_URI=""
9
9
-
WEAVER_TOS_URI=""
10
10
-
WEAVER_PRIVACY_POLICY_URI=""
+10
crates/weaver-app/.env-prod
···
1
1
+
WEAVER_APP_ENV="prod"
2
2
+
WEAVER_APP_HOST="https://alpha.weaver.sh"
3
3
+
WEAVER_APP_DOMAIN="https://alpha.weaver.sh"
4
4
+
WEAVER_PORT=8080
5
5
+
WEAVER_APP_SCOPES="atproto transition:generic"
6
6
+
WEAVER_CLIENT_NAME="Weaver"
7
7
+
8
8
+
WEAVER_LOGO_URI=""
9
9
+
WEAVER_TOS_URI=""
10
10
+
WEAVER_PRIVACY_POLICY_URI=""
+5
-5
crates/weaver-app/src/components/css.rs
···
1
1
#[allow(unused_imports)]
2
2
use crate::fetch;
3
3
#[allow(unused_imports)]
4
4
-
use dioxus::{prelude::*, CapturedError};
4
4
+
use dioxus::{CapturedError, prelude::*};
5
5
6
6
#[cfg(feature = "fullstack-server")]
7
7
use dioxus::fullstack::response::Response;
···
20
20
use dioxus::fullstack::get_server_url;
21
21
rsx! {
22
22
document::Stylesheet {
23
23
-
href: "{get_server_url()}/{ident}/{notebook}/css"
23
23
+
href: "{get_server_url()}/css/{ident}/{notebook}"
24
24
}
25
25
}
26
26
}
···
30
30
pub fn NotebookCss(ident: SmolStr, notebook: SmolStr) -> Element {
31
31
use jacquard::client::AgentSessionExt;
32
32
use jacquard::types::ident::AtIdentifier;
33
33
-
use jacquard::{from_data, CowStr};
33
33
+
use jacquard::{CowStr, from_data};
34
34
use weaver_api::sh_weaver::notebook::book::Book;
35
35
use weaver_renderer::css::{generate_base_css, generate_syntax_css};
36
36
use weaver_renderer::theme::{default_resolved_theme, resolve_theme};
···
87
87
}
88
88
89
89
#[cfg(feature = "fullstack-server")]
90
90
-
#[get("/{ident}/{notebook}/css", fetcher: Extension<Arc<fetch::CachedFetcher>>)]
90
90
+
#[get("/css/{ident}/{notebook}", fetcher: Extension<Arc<fetch::CachedFetcher>>)]
91
91
pub async fn css(ident: SmolStr, notebook: SmolStr) -> Result<Response> {
92
92
use dioxus::fullstack::http::header::CONTENT_TYPE;
93
93
use jacquard::client::AgentSessionExt;
94
94
use jacquard::types::ident::AtIdentifier;
95
95
-
use jacquard::{from_data, CowStr};
95
95
+
use jacquard::{CowStr, from_data};
96
96
97
97
use weaver_api::sh_weaver::notebook::book::Book;
98
98
use weaver_renderer::css::{generate_base_css, generate_syntax_css};
+22
-26
crates/weaver-app/src/components/identity.rs
···
1
1
-
use crate::{Route, fetch};
1
1
+
use crate::{Route, data, fetch};
2
2
use dioxus::prelude::*;
3
3
use jacquard::{smol_str::SmolStr, types::ident::AtIdentifier};
4
4
use weaver_api::com_atproto::repo::strong_ref::StrongRef;
···
20
20
pub fn RepositoryIndex(ident: ReadSignal<AtIdentifier<'static>>) -> Element {
21
21
use crate::components::ProfileDisplay;
22
22
23
23
-
let fetcher = use_context::<fetch::CachedFetcher>();
24
24
-
25
25
-
// Fetch notebooks for this specific DID
26
26
-
let notebooks = use_resource(move || {
27
27
-
let fetcher = fetcher.clone();
28
28
-
async move { fetcher.fetch_notebooks_for_did(&ident()).await }
29
29
-
});
23
23
+
// Fetch notebooks for this specific DID with SSR support
24
24
+
let notebooks = data::use_notebooks_for_did(ident()).ok();
30
25
31
26
rsx! {
32
27
document::Stylesheet { href: NOTEBOOK_CARD_CSS }
···
40
35
// Main content area
41
36
main { class: "repository-main",
42
37
div { class: "notebooks-list",
43
43
-
match notebooks() {
44
44
-
Some(Ok(notebook_list)) => rsx! {
45
45
-
for notebook in notebook_list.iter() {
46
46
-
{
47
47
-
let view = ¬ebook.0;
48
48
-
let entries = ¬ebook.1;
49
49
-
rsx! {
50
50
-
div {
51
51
-
key: "{view.cid}",
52
52
-
NotebookCard {
53
53
-
notebook: view.clone(),
54
54
-
entry_refs: entries.clone()
38
38
+
if let Some(notebooks_memo) = ¬ebooks {
39
39
+
match &*notebooks_memo.read_unchecked() {
40
40
+
Some(notebook_list) => rsx! {
41
41
+
for notebook in notebook_list.iter() {
42
42
+
{
43
43
+
let view = ¬ebook.0;
44
44
+
let entries = ¬ebook.1;
45
45
+
rsx! {
46
46
+
div {
47
47
+
key: "{view.cid}",
48
48
+
NotebookCard {
49
49
+
notebook: view.clone(),
50
50
+
entry_refs: entries.clone()
51
51
+
}
55
52
}
56
53
}
57
54
}
58
55
}
56
56
+
},
57
57
+
None => rsx! {
58
58
+
div { "Loading notebooks..." }
59
59
}
60
60
-
},
61
61
-
Some(Err(_)) => rsx! {
62
62
-
div { "Error loading notebooks" }
63
63
-
},
64
64
-
None => rsx! {
65
65
-
div { "Loading notebooks..." }
66
60
}
61
61
+
} else {
62
62
+
div { "Loading notebooks..." }
67
63
}
68
64
}
69
65
}
+187
-1
crates/weaver-app/src/data.rs
···
18
18
};
19
19
#[allow(unused_imports)]
20
20
use std::sync::Arc;
21
21
-
use weaver_api::sh_weaver::notebook::{BookEntryView, entry::Entry};
21
21
+
use weaver_api::sh_weaver::notebook::{BookEntryView, NotebookView, entry::Entry};
22
22
// ============================================================================
23
23
// Wrapper Hooks (feature-gated)
24
24
// ============================================================================
···
358
358
.await
359
359
.ok()
360
360
.map(|notebooks| notebooks.iter().map(|arc| (*arc).clone()).collect())
361
361
+
}
362
362
+
}));
363
363
+
Ok(use_memo(move || {
364
364
+
r.read_unchecked().as_ref().and_then(|v| v.clone())
365
365
+
}))
366
366
+
}
367
367
+
368
368
+
/// Fetches notebooks from UFOS with SSR support in fullstack mode
369
369
+
#[cfg(feature = "fullstack-server")]
370
370
+
pub fn use_notebooks_from_ufos() -> Result<
371
371
+
Memo<Option<Vec<(NotebookView<'static>, Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef<'static>>)>>>,
372
372
+
RenderError,
373
373
+
> {
374
374
+
let fetcher = use_context::<crate::fetch::CachedFetcher>();
375
375
+
let res = use_server_future(move || {
376
376
+
let fetcher = fetcher.clone();
377
377
+
async move {
378
378
+
fetcher
379
379
+
.fetch_notebooks_from_ufos()
380
380
+
.await
381
381
+
.ok()
382
382
+
.map(|notebooks| {
383
383
+
notebooks
384
384
+
.iter()
385
385
+
.map(|arc| serde_json::to_value(arc.as_ref()).ok())
386
386
+
.collect::<Option<Vec<_>>>()
387
387
+
})
388
388
+
.flatten()
389
389
+
}
390
390
+
})?;
391
391
+
Ok(use_memo(move || {
392
392
+
if let Some(Some(values)) = &*res.read_unchecked() {
393
393
+
values
394
394
+
.iter()
395
395
+
.map(|v| {
396
396
+
jacquard::from_json_value::<(
397
397
+
NotebookView,
398
398
+
Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef>,
399
399
+
)>(v.clone())
400
400
+
.ok()
401
401
+
})
402
402
+
.collect::<Option<Vec<_>>>()
403
403
+
} else {
404
404
+
None
405
405
+
}
406
406
+
}))
407
407
+
}
408
408
+
409
409
+
/// Fetches notebooks from UFOS client-side only (no SSR)
410
410
+
#[cfg(not(feature = "fullstack-server"))]
411
411
+
pub fn use_notebooks_from_ufos() -> Result<
412
412
+
Memo<Option<Vec<(NotebookView<'static>, Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef<'static>>)>>>,
413
413
+
RenderError,
414
414
+
> {
415
415
+
let fetcher = use_context::<crate::fetch::CachedFetcher>();
416
416
+
let r = use_resource(move || {
417
417
+
let fetcher = fetcher.clone();
418
418
+
async move {
419
419
+
fetcher
420
420
+
.fetch_notebooks_from_ufos()
421
421
+
.await
422
422
+
.ok()
423
423
+
.map(|notebooks| notebooks.iter().map(|arc| (*arc).clone()).collect())
424
424
+
}
425
425
+
});
426
426
+
Ok(use_memo(move || {
427
427
+
r.read_unchecked().as_ref().and_then(|v| v.clone())
428
428
+
}))
429
429
+
}
430
430
+
431
431
+
/// Fetches notebook metadata with SSR support in fullstack mode
432
432
+
#[cfg(feature = "fullstack-server")]
433
433
+
pub fn use_notebook(
434
434
+
ident: AtIdentifier<'static>,
435
435
+
book_title: SmolStr,
436
436
+
) -> Result<
437
437
+
Memo<Option<(NotebookView<'static>, Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef<'static>>)>>,
438
438
+
RenderError,
439
439
+
> {
440
440
+
let fetcher = use_context::<crate::fetch::CachedFetcher>();
441
441
+
let ident = use_signal(|| ident);
442
442
+
let book_title = use_signal(|| book_title);
443
443
+
let res = use_server_future(move || {
444
444
+
let fetcher = fetcher.clone();
445
445
+
async move {
446
446
+
fetcher
447
447
+
.get_notebook(ident(), book_title())
448
448
+
.await
449
449
+
.ok()
450
450
+
.flatten()
451
451
+
.map(|arc| serde_json::to_value(arc.as_ref()).ok())
452
452
+
.flatten()
453
453
+
}
454
454
+
})?;
455
455
+
Ok(use_memo(move || {
456
456
+
if let Some(Some(value)) = &*res.read_unchecked() {
457
457
+
jacquard::from_json_value::<(
458
458
+
NotebookView,
459
459
+
Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef>,
460
460
+
)>(value.clone())
461
461
+
.ok()
462
462
+
} else {
463
463
+
None
464
464
+
}
465
465
+
}))
466
466
+
}
467
467
+
468
468
+
/// Fetches notebook metadata client-side only (no SSR)
469
469
+
#[cfg(not(feature = "fullstack-server"))]
470
470
+
pub fn use_notebook(
471
471
+
ident: AtIdentifier<'static>,
472
472
+
book_title: SmolStr,
473
473
+
) -> Result<
474
474
+
Memo<Option<(NotebookView<'static>, Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef<'static>>)>>,
475
475
+
RenderError,
476
476
+
> {
477
477
+
let fetcher = use_context::<crate::fetch::CachedFetcher>();
478
478
+
let r = use_resource(use_reactive!(|(ident, book_title)| {
479
479
+
let fetcher = fetcher.clone();
480
480
+
async move {
481
481
+
fetcher
482
482
+
.get_notebook(ident, book_title)
483
483
+
.await
484
484
+
.ok()
485
485
+
.flatten()
486
486
+
.map(|arc| (*arc).clone())
487
487
+
}
488
488
+
}));
489
489
+
Ok(use_memo(move || {
490
490
+
r.read_unchecked().as_ref().and_then(|v| v.clone())
491
491
+
}))
492
492
+
}
493
493
+
494
494
+
/// Fetches notebook entries with SSR support in fullstack mode
495
495
+
#[cfg(feature = "fullstack-server")]
496
496
+
pub fn use_notebook_entries(
497
497
+
ident: AtIdentifier<'static>,
498
498
+
book_title: SmolStr,
499
499
+
) -> Result<Memo<Option<Vec<BookEntryView<'static>>>>, RenderError> {
500
500
+
let fetcher = use_context::<crate::fetch::CachedFetcher>();
501
501
+
let ident = use_signal(|| ident);
502
502
+
let book_title = use_signal(|| book_title);
503
503
+
let res = use_server_future(move || {
504
504
+
let fetcher = fetcher.clone();
505
505
+
async move {
506
506
+
fetcher
507
507
+
.list_notebook_entries(ident(), book_title())
508
508
+
.await
509
509
+
.ok()
510
510
+
.flatten()
511
511
+
.map(|entries| {
512
512
+
entries
513
513
+
.iter()
514
514
+
.map(|e| serde_json::to_value(e).ok())
515
515
+
.collect::<Option<Vec<_>>>()
516
516
+
})
517
517
+
.flatten()
518
518
+
}
519
519
+
})?;
520
520
+
Ok(use_memo(move || {
521
521
+
if let Some(Some(values)) = &*res.read_unchecked() {
522
522
+
values
523
523
+
.iter()
524
524
+
.map(|v| jacquard::from_json_value::<BookEntryView>(v.clone()).ok())
525
525
+
.collect::<Option<Vec<_>>>()
526
526
+
} else {
527
527
+
None
528
528
+
}
529
529
+
}))
530
530
+
}
531
531
+
532
532
+
/// Fetches notebook entries client-side only (no SSR)
533
533
+
#[cfg(not(feature = "fullstack-server"))]
534
534
+
pub fn use_notebook_entries(
535
535
+
ident: AtIdentifier<'static>,
536
536
+
book_title: SmolStr,
537
537
+
) -> Result<Memo<Option<Vec<BookEntryView<'static>>>>, RenderError> {
538
538
+
let fetcher = use_context::<crate::fetch::CachedFetcher>();
539
539
+
let r = use_resource(use_reactive!(|(ident, book_title)| {
540
540
+
let fetcher = fetcher.clone();
541
541
+
async move {
542
542
+
fetcher
543
543
+
.list_notebook_entries(ident, book_title)
544
544
+
.await
545
545
+
.ok()
546
546
+
.flatten()
361
547
}
362
548
}));
363
549
Ok(use_memo(move || {
+3
-3
crates/weaver-app/src/env.rs
···
1
1
// This file is automatically generated by build.rs
2
2
3
3
#[allow(unused)]
4
4
-
pub const WEAVER_APP_ENV: &'static str = "prod";
4
4
+
pub const WEAVER_APP_ENV: &'static str = "dev";
5
5
#[allow(unused)]
6
6
-
pub const WEAVER_APP_HOST: &'static str = "https://alpha.weaver.sh";
6
6
+
pub const WEAVER_APP_HOST: &'static str = "http://localhost";
7
7
#[allow(unused)]
8
8
-
pub const WEAVER_APP_DOMAIN: &'static str = "https://alpha.weaver.sh";
8
8
+
pub const WEAVER_APP_DOMAIN: &'static str = "";
9
9
#[allow(unused)]
10
10
pub const WEAVER_PORT: &'static str = "8080";
11
11
#[allow(unused)]
+9
-15
crates/weaver-app/src/main.rs
···
136
136
// Enable incremental rendering
137
137
.incremental(
138
138
dioxus::server::IncrementalRendererConfig::new()
139
139
-
.static_dir(
140
140
-
std::env::current_exe()
141
141
-
.unwrap()
142
142
-
.parent()
143
143
-
.unwrap()
144
144
-
.join("public"),
145
145
-
)
139
139
+
.pre_render(true)
146
140
.clear_cache(false),
147
141
)
148
142
.enable_out_of_order_streaming(),
···
260
254
}
261
255
}
262
256
263
263
-
#[server(endpoint = "static_routes", output = server_fn::codec::Json)]
264
264
-
async fn static_routes() -> Result<Vec<String>, ServerFnError> {
265
265
-
// The `Routable` trait has a `static_routes` method that returns all static routes in the enum
266
266
-
Ok(Route::static_routes()
267
267
-
.iter()
268
268
-
.map(ToString::to_string)
269
269
-
.collect())
270
270
-
}
257
257
+
// #[server(endpoint = "static_routes", output = server_fn::codec::Json)]
258
258
+
// async fn static_routes() -> Result<Vec<String>, ServerFnError> {
259
259
+
// // The `Routable` trait has a `static_routes` method that returns all static routes in the enum
260
260
+
// Ok(Route::static_routes()
261
261
+
// .iter()
262
262
+
// .map(ToString::to_string)
263
263
+
// .collect())
264
264
+
// }
+22
-26
crates/weaver-app/src/views/home.rs
···
1
1
-
use crate::{Route, components::identity::NotebookCard, fetch};
1
1
+
use crate::{Route, components::identity::NotebookCard, data};
2
2
use dioxus::prelude::*;
3
3
use jacquard::types::aturi::AtUri;
4
4
···
7
7
/// The Home page component that will be rendered when the current route is `[Route::Home]`
8
8
#[component]
9
9
pub fn Home() -> Element {
10
10
-
let fetcher = use_context::<fetch::CachedFetcher>();
11
11
-
12
12
-
// Fetch notebooks from UFOS
13
13
-
let notebooks = use_resource(move || {
14
14
-
let fetcher = fetcher.clone();
15
15
-
async move { fetcher.fetch_notebooks_from_ufos().await }
16
16
-
});
10
10
+
// Fetch notebooks from UFOS with SSR support
11
11
+
let notebooks = data::use_notebooks_from_ufos().ok();
17
12
let navigator = use_navigator();
18
13
let mut uri_input = use_signal(|| String::new());
19
14
···
49
44
}
50
45
}
51
46
div { class: "notebooks-list",
52
52
-
match notebooks() {
53
53
-
Some(Ok(notebook_list)) => rsx! {
54
54
-
for notebook in notebook_list.iter() {
55
55
-
{
56
56
-
let view = ¬ebook.0;
57
57
-
let entries = ¬ebook.1;
58
58
-
rsx! {
59
59
-
div {
60
60
-
key: "{view.cid}",
61
61
-
NotebookCard {
62
62
-
notebook: view.clone(),
63
63
-
entry_refs: entries.clone()
47
47
+
if let Some(notebooks_memo) = ¬ebooks {
48
48
+
match &*notebooks_memo.read_unchecked() {
49
49
+
Some(notebook_list) => rsx! {
50
50
+
for notebook in notebook_list.iter() {
51
51
+
{
52
52
+
let view = ¬ebook.0;
53
53
+
let entries = ¬ebook.1;
54
54
+
rsx! {
55
55
+
div {
56
56
+
key: "{view.cid}",
57
57
+
NotebookCard {
58
58
+
notebook: view.clone(),
59
59
+
entry_refs: entries.clone()
60
60
+
}
64
61
}
65
62
}
66
63
}
67
64
}
65
65
+
},
66
66
+
None => rsx! {
67
67
+
div { "Loading notebooks..." }
68
68
}
69
69
-
},
70
70
-
Some(Err(_)) => rsx! {
71
71
-
div { "Error loading notebooks" }
72
72
-
},
73
73
-
None => rsx! {
74
74
-
div { "Loading notebooks..." }
75
69
}
70
70
+
} else {
71
71
+
div { "Loading notebooks..." }
76
72
}
77
73
}
78
74
}
+30
-48
crates/weaver-app/src/views/notebook.rs
···
1
1
use crate::{
2
2
Route,
3
3
components::{EntryCard, NotebookCover, NotebookCss},
4
4
-
fetch,
4
4
+
data,
5
5
};
6
6
use dioxus::prelude::*;
7
7
use jacquard::{
···
28
28
ident: ReadSignal<AtIdentifier<'static>>,
29
29
book_title: ReadSignal<SmolStr>,
30
30
) -> Element {
31
31
-
let fetcher = use_context::<fetch::CachedFetcher>();
32
32
-
// Fetch full notebook to get author count
33
33
-
let data_fetcher = fetcher.clone();
34
34
-
let notebook_data = use_resource(move || {
35
35
-
let fetcher = data_fetcher.clone();
36
36
-
async move {
37
37
-
fetcher
38
38
-
.get_notebook(ident(), book_title())
39
39
-
.await
40
40
-
.ok()
41
41
-
.flatten()
42
42
-
}
43
43
-
});
31
31
+
// Fetch full notebook metadata with SSR support
32
32
+
let notebook_data = data::use_notebook(ident(), book_title()).ok();
44
33
45
45
-
// Also fetch entries
46
46
-
let entry_fetcher = fetcher.clone();
47
47
-
let entries_resource = use_resource(move || {
48
48
-
let fetcher = entry_fetcher.clone();
49
49
-
async move {
50
50
-
fetcher
51
51
-
.list_notebook_entries(ident(), book_title())
52
52
-
.await
53
53
-
.ok()
54
54
-
.flatten()
55
55
-
}
56
56
-
});
34
34
+
// Fetch entries with SSR support
35
35
+
let entries_resource = data::use_notebook_entries(ident(), book_title()).ok();
57
36
58
37
rsx! {
59
38
document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS }
60
39
61
61
-
match (&*notebook_data.read_unchecked(), &*entries_resource.read_unchecked()) {
62
62
-
(Some(Some(data)), Some(Some(entries))) => {
63
63
-
let (notebook_view, _) = data.as_ref();
64
64
-
let author_count = notebook_view.authors.len();
40
40
+
if let (Some(notebook_memo), Some(entries_memo)) = (¬ebook_data, &entries_resource) {
41
41
+
match (&*notebook_memo.read_unchecked(), &*entries_memo.read_unchecked()) {
42
42
+
(Some(data), Some(entries)) => {
43
43
+
let (notebook_view, _) = data;
44
44
+
let author_count = notebook_view.authors.len();
65
45
66
66
-
rsx! {
67
67
-
div { class: "notebook-layout",
68
68
-
aside { class: "notebook-sidebar",
69
69
-
NotebookCover {
70
70
-
notebook: notebook_view.clone(),
71
71
-
title: book_title().to_string()
46
46
+
rsx! {
47
47
+
div { class: "notebook-layout",
48
48
+
aside { class: "notebook-sidebar",
49
49
+
NotebookCover {
50
50
+
notebook: notebook_view.clone(),
51
51
+
title: book_title().to_string()
52
52
+
}
72
53
}
73
73
-
}
74
54
75
75
-
main { class: "notebook-main",
76
76
-
div { class: "entries-list",
77
77
-
for entry in entries {
78
78
-
EntryCard {
79
79
-
entry: entry.clone(),
80
80
-
book_title: book_title(),
81
81
-
author_count
55
55
+
main { class: "notebook-main",
56
56
+
div { class: "entries-list",
57
57
+
for entry in entries {
58
58
+
EntryCard {
59
59
+
entry: entry.clone(),
60
60
+
book_title: book_title(),
61
61
+
author_count
62
62
+
}
82
63
}
83
64
}
84
65
}
85
66
}
86
67
}
87
87
-
}
88
88
-
},
89
89
-
(Some(None), _) | (_, Some(None)) => rsx! { div { class: "error", "Notebook or entries not found" } },
90
90
-
_ => rsx! { div { class: "loading", "Loading..." } }
68
68
+
},
69
69
+
_ => rsx! { div { class: "loading", "Loading..." } }
70
70
+
}
71
71
+
} else {
72
72
+
div { class: "loading", "Loading..." }
91
73
}
92
74
}
93
75
}