Upgrade dependencies

This commit is contained in:
Felix Ableitner 2024-10-01 15:17:14 +02:00
parent 816bcc1f97
commit fdc8b97f12
30 changed files with 1702 additions and 1144 deletions

2
.leptosfmt.toml Normal file
View File

@ -0,0 +1,2 @@
max_width = 100
attr_value_brace_style = "WhenRequired" # "Always", "AlwaysUnlessLit", "WhenRequired" or "Preserve"

View File

@ -1,5 +1,6 @@
variables:
- &rust_image "rust:1.75"
- &install_binstall "wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz && tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz && cp cargo-binstall /usr/local/cargo/bin"
steps:
cargo_fmt:
@ -10,6 +11,20 @@ steps:
commands:
- rustup component add rustfmt
- cargo +nightly fmt -- --check
leptos_fmt:
image: *rust_image
commands:
- *install_binstall
- cargo binstall -y leptosfmt
- leptosfmt -c .leptosfmt.toml --check src
when:
- event: pull_request
toml_fmt:
image: tamasfe/taplo:0.8.1
commands:
- taplo format --check
when:
- event: pull_request

1538
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -26,60 +26,60 @@ dbg_macro = "deny"
unwrap_used = "deny"
[dependencies]
activitypub_federation = { version = "0.5.2", features = [
activitypub_federation = { version = "0.6.0-alpha2", features = [
"axum",
"diesel",
], default-features = false, optional = true }
anyhow = "1.0.75"
async-trait = "0.1.74"
axum = { version = "0.6.20", optional = true }
axum-macros = { version = "0.3.8", optional = true }
axum-extra = { version = "0.7.7", features = ["cookie"], optional = true }
leptos = "0.5.4"
leptos_meta = "0.5.4"
leptos_router = "0.5.4"
leptos_axum = { version = "0.5.4", optional = true }
bcrypt = "0.15.0"
chrono = { version = "0.4.31", features = ["serde"] }
diesel = { version = "2.1.4", features = [
anyhow = "1.0.89"
async-trait = "0.1.83"
axum = { version = "0.7.7", optional = true }
axum-macros = { version = "0.4.2", optional = true }
axum-extra = { version = "0.9.4", features = ["cookie"], optional = true }
leptos = "0.6.15"
leptos_meta = "0.6.15"
leptos_router = "0.6.15"
leptos_axum = { version = "0.6.15", optional = true }
bcrypt = "0.15.1"
chrono = { version = "0.4.38", features = ["serde"] }
diesel = { version = "2.2.4", features = [
"postgres",
"chrono",
"uuid",
"r2d2"
"r2d2",
], optional = true }
diesel-derive-newtype = { version = "2.1.0", optional = true }
diesel_migrations = { version = "2.1.0", optional = true }
diffy = "0.3.0"
diesel-derive-newtype = { version = "2.1.2", optional = true }
diesel_migrations = { version = "2.2.0", optional = true }
diffy = "0.4.0"
enum_delegate = "0.2.0"
env_logger = { version = "0.10.1", default-features = false }
futures = "0.3.29"
env_logger = { version = "0.11.5", default-features = false }
futures = "0.3.30"
hex = "0.4.3"
jsonwebtoken = { version = "9.2.0", optional = true }
jsonwebtoken = { version = "9.3.0", optional = true }
rand = "0.8.5"
serde_json = "1.0.108"
serde_json = "1.0.128"
sha2 = "0.10.8"
tokio = { version = "1.34.0", features = ["full"], optional = true }
uuid = { version = "1.6.1", features = ["serde"] }
tower-http = { version = "0.4.0", features = ["cors", "fs"], optional = true }
serde = { version = "1.0.192", features = ["derive"] }
url = { version = "2.4.1", features = ["serde"] }
reqwest = { version = "0.11.22", features = ["json", "cookies"] }
tokio = { version = "1.40.0", features = ["full"], optional = true }
uuid = { version = "1.10.0", features = ["serde"] }
tower-http = { version = "0.6.1", features = ["cors", "fs"], optional = true }
serde = { version = "1.0.210", features = ["derive"] }
url = { version = "2.5.2", features = ["serde"] }
reqwest = { version = "0.12.8", features = ["json", "cookies"] }
log = "0.4"
tracing = "0.1.40"
once_cell = "1.18.0"
wasm-bindgen = "0.2.89"
once_cell = "1.20.1"
wasm-bindgen = "0.2.93"
console_error_panic_hook = "0.1.7"
console_log = "1.0.0"
time = "0.3.31"
tower = "0.4.13"
markdown-it = "0.6.0"
web-sys = "0.3.68"
time = "0.3.36"
tower = "0.5.1"
markdown-it = "0.6.1"
web-sys = "0.3.70"
config = { version = "0.14.0", features = ["toml"] }
doku = "0.21.1"
smart-default = "0.7.1"
[dev-dependencies]
pretty_assertions = "1.4.0"
pretty_assertions = "1.4.1"
[package.metadata.leptos]
output-name = "ibis"

View File

@ -9,5 +9,5 @@ IBIS__BIND="${IBIS_BIND:-"127.0.0.1:8081"}"
# start frontend
CARGO_TARGET_DIR=target/frontend trunk serve -w src/frontend/ --proxy-backend http://$IBIS__BIND &
# start backend, with separate target folder to avoid rebuilds from arch change
cargo watch -x run
bacon -j run
)

View File

@ -29,6 +29,7 @@ use crate::{
};
use activitypub_federation::config::Data;
use axum::{
body::Body,
http::{Request, StatusCode},
middleware::{self, Next},
response::Response,
@ -45,7 +46,7 @@ pub mod article;
pub mod instance;
pub mod user;
pub fn api_routes() -> Router {
pub fn api_routes() -> Router<()> {
Router::new()
.route(
"/article",
@ -68,11 +69,11 @@ pub fn api_routes() -> Router {
.route_layer(middleware::from_fn(auth))
}
async fn auth<B>(
async fn auth(
data: Data<IbisData>,
jar: CookieJar,
mut request: Request<B>,
next: Next<B>,
mut request: Request<Body>,
next: Next,
) -> Result<Response, StatusCode> {
if let Some(auth) = jar.get(AUTH_COOKIE) {
if let Ok(user) = validate(auth.value(), &data).await {

View File

@ -98,7 +98,7 @@ fn create_cookie(jwt: String, data: &Data<IbisData>) -> Cookie<'static> {
if domain.contains(':') {
domain = domain.split(':').collect::<Vec<_>>()[0].to_string();
}
Cookie::build(AUTH_COOKIE, jwt)
Cookie::build((AUTH_COOKIE, jwt))
.domain(domain)
.same_site(SameSite::Strict)
.path("/")
@ -107,7 +107,7 @@ fn create_cookie(jwt: String, data: &Data<IbisData>) -> Cookie<'static> {
.expires(Expiration::DateTime(
OffsetDateTime::now_utc() + Duration::weeks(52),
))
.finish()
.build()
}
#[debug_handler]

View File

@ -42,7 +42,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use url::Url;
pub fn federation_routes() -> Router {
pub fn federation_routes() -> Router<()> {
Router::new()
.route("/", get(http_get_instance))
.route("/user/:name", get(http_get_person))

View File

@ -23,26 +23,25 @@ use activitypub_federation::{
};
use api::api_routes;
use axum::{
debug_handler,
headers::HeaderMap,
body::Body,
http::{HeaderValue, Request},
middleware::Next,
response::{IntoResponse, Response},
routing::get,
Router,
Server,
ServiceExt,
};
use axum_macros::{debug_handler, debug_middleware};
use chrono::Local;
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use leptos::{leptos_config::get_config_from_str, *};
use leptos::leptos_config::get_config_from_str;
use leptos_axum::{generate_route_list, LeptosRoutes};
use log::info;
use tower::Layer;
use reqwest::header::HeaderMap;
use tokio::net::TcpListener;
use tower_http::cors::CorsLayer;
pub mod api;
@ -81,36 +80,34 @@ pub async fn start(config: IbisConfig) -> MyResult<()> {
setup(&data.to_request_data()).await?;
}
let conf = get_config_from_str(include_str!("../../Cargo.toml"))?;
let mut leptos_options = conf.leptos_options;
leptos_options.site_addr = data.config.bind;
let mut conf = get_config_from_str(include_str!("../../Cargo.toml"))?;
conf.site_addr = data.config.bind;
let routes = generate_route_list(App);
// Rewrite federation routes
// https://docs.rs/axum/0.7.4/axum/middleware/index.html#rewriting-request-uri-in-middleware
let middleware = axum::middleware::from_fn(federation_routes_middleware);
let config = data.clone();
let app = Router::new()
.leptos_routes(&leptos_options, routes, || view! { <App/> })
.with_state(leptos_options)
.leptos_routes(&conf, routes, App)
.with_state(conf)
.nest("", asset_routes()?)
.nest(FEDERATION_ROUTES_PREFIX, federation_routes())
.nest("/api/v1", api_routes())
.nest("", nodeinfo::config())
.layer(FederationMiddleware::new(config))
.layer(CorsLayer::permissive());
// Rewrite federation routes
// https://docs.rs/axum/0.7.4/axum/middleware/index.html#rewriting-request-uri-in-middleware
let middleware = axum::middleware::from_fn(federation_routes_middleware);
let app_with_middleware = middleware.layer(app);
.layer(CorsLayer::permissive())
.layer(middleware);
info!("Listening on {}", &data.config.bind);
Server::bind(&data.config.bind)
.serve(app_with_middleware.into_make_service())
.await?;
let listener = TcpListener::bind(&data.config.bind).await?;
axum::serve(listener, app.into_make_service()).await?;
Ok(())
}
pub fn asset_routes() -> MyResult<Router> {
pub fn asset_routes() -> MyResult<Router<()>> {
let mut css_headers = HeaderMap::new();
css_headers.insert("Content-Type", "text/css".parse()?);
Ok(Router::new()
@ -209,7 +206,8 @@ async fn setup(data: &Data<IbisData>) -> Result<(), Error> {
/// with frontend routes. If a request is an Activitypub fetch as indicated by
/// `Accept: application/activity+json` header, use the federation routes. Otherwise
/// leave the path unchanged so it can go to frontend.
async fn federation_routes_middleware<B>(request: Request<B>, next: Next<B>) -> Response {
#[debug_middleware]
async fn federation_routes_middleware(request: Request<Body>, next: Next) -> Response {
let (mut parts, body) = request.into_parts();
// rewrite uri based on accept header
let mut uri = parts.uri.to_string();

View File

@ -7,7 +7,7 @@ use axum::{routing::get, Json, Router};
use serde::{Deserialize, Serialize};
use url::Url;
pub fn config() -> Router {
pub fn config() -> Router<()> {
Router::new()
.route("/nodeinfo/2.0.json", get(node_info))
.route("/.well-known/nodeinfo", get(node_info_well_known))

View File

@ -94,7 +94,7 @@ pub fn App() -> impl IntoView {
<Stylesheet id="simple" href="/assets/simple.css"/>
<Stylesheet id="ibis" href="/assets/ibis.css"/>
<Router>
<Nav />
<Nav/>
<main>
<Routes>
<Route path="/" view=ReadArticle/>

View File

@ -13,41 +13,60 @@ use leptos_router::*;
pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoView {
view! {
<Suspense>
{move || article.get().map(|article_| {
let instance = create_local_resource(move || article_.article.instance_id, move |instance_id| async move {
{move || {
article
.get()
.map(|article_| {
let instance = create_local_resource(
move || article_.article.instance_id,
move |instance_id| async move {
let form = GetInstance {
id: Some(instance_id)
id: Some(instance_id),
};
GlobalState::api_client()
.get_instance(&form)
.await
.unwrap()
});
GlobalState::api_client().get_instance(&form).await.unwrap()
},
);
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
let article_link = article_link(&article_.article);
let article_link_ = article_link.clone();
let protected = article_.article.protected;
view!{
view! {
<nav class="inner">
<A href=article_link.clone()>"Read"</A>
<A href={format!("{article_link}/history")}>"History"</A>
<Show when=move || global_state.with(|state| {
let is_admin = state.my_profile.as_ref().map(|p| p.local_user.admin).unwrap_or(false);
state.my_profile.is_some() && can_edit_article(&article_.article, is_admin).is_ok()
})>
<A href={format!("{article_link}/edit")}>"Edit"</A>
<A href=format!("{article_link}/history")>"History"</A>
<Show when=move || {
global_state
.with(|state| {
let is_admin = state
.my_profile
.as_ref()
.map(|p| p.local_user.admin)
.unwrap_or(false);
state.my_profile.is_some()
&& can_edit_article(&article_.article, is_admin).is_ok()
})
}>
<A href=format!("{article_link}/edit")>"Edit"</A>
</Show>
<Show when=move || global_state.with(|state| state.my_profile.is_some())>
<A href={format!("{article_link_}/actions")}>"Actions"</A>
{instance.get().map(|i|
view!{ <InstanceFollowButton instance=i.instance.clone() /> }
)}
<A href=format!("{article_link_}/actions")>"Actions"</A>
{instance
.get()
.map(|i| {
view! { <InstanceFollowButton instance=i.instance.clone()/> }
})}
</Show>
<Show when=move || protected>
<span title="Article can only be edited by local admins">"Protected"</span>
<span title="Article can only be edited by local admins">
"Protected"
</span>
</Show>
</nav>
}})}
}
})
}}
</Suspense>
}
}

View File

@ -27,6 +27,7 @@ pub fn CredentialsForm(
view! { <p style="color:red;">{err}</p> }
})
}}
<input
type="text"
required
@ -36,11 +37,13 @@ pub fn CredentialsForm(
let val = event_target_value(&ev);
set_username.update(|v| *v = val);
}
on:change=move |ev| {
let val = event_target_value(&ev);
set_username.update(|v| *v = val);
}
/>
<input
type="password"
required
@ -57,15 +60,18 @@ pub fn CredentialsForm(
}
}
}
on:change=move |ev| {
let val = event_target_value(&ev);
set_password.update(|p| *p = val);
}
/>
<div>
<button
prop:disabled=move || button_is_disabled.get()
on:click=move |_| dispatch_action()>
on:click=move |_| dispatch_action()
>
{action_label}
</button>
</div>

View File

@ -30,9 +30,11 @@ pub fn InstanceFollowButton(instance: DbInstance) -> impl IntoView {
};
view! {
<button on:click=move |_| follow_action.dispatch(instance.id)
<button
on:click=move |_| follow_action.dispatch(instance.id)
prop:disabled=move || is_following
prop:hidden=move || instance.local>
prop:hidden=move || instance.local
>
{follow_text}
</button>
}

View File

@ -29,8 +29,7 @@ pub fn Nav() -> impl IntoView {
<li>
<A href="/article/list">"List Articles"</A>
</li>
<Show
when=move || global_state.with(|state| state.my_profile.is_some())>
<Show when=move || global_state.with(|state| state.my_profile.is_some())>
<li>
<A href="/article/create">"Create Article"</A>
</li>
@ -47,12 +46,16 @@ pub fn Nav() -> impl IntoView {
navigate(&format!("/search?query={query}"), Default::default());
}
}>
<input type="text" placeholder="Search"
<input
type="text"
placeholder="Search"
prop:value=search_query
on:keyup=move |ev: ev::KeyboardEvent| {
let val = event_target_value(&ev);
set_search_query.update(|v| *v = val);
} />
}
/>
<button>Go</button>
</form>
</li>
@ -71,20 +74,24 @@ pub fn Nav() -> impl IntoView {
}
}
>
{
let my_profile = global_state.with(|state| state.my_profile.clone().unwrap());
let profile_link = format!("/user/{}", my_profile.person.username);
view ! {
<p>"Logged in as "
<a href=profile_link style="border: none; padding: 0; color: var(--accent) !important;">
view! {
<p>
"Logged in as "
<a
href=profile_link
style="border: none; padding: 0; color: var(--accent) !important;"
>
{my_profile.person.username}
</a>
</p>
<button on:click=move |_| logout_action.dispatch(())>
Logout
</button>
<button on:click=move |_| logout_action.dispatch(())>Logout</button>
}
}
</Show>
</nav>
}

View File

@ -43,7 +43,5 @@ fn user_title(person: &DbPerson) -> String {
fn user_link(person: &DbPerson) -> impl IntoView {
let creator_path = format!("/user/{}", person.username);
view! {
<a href={creator_path}>{user_title(person)}</a>
}
view! { <a href=creator_path>{user_title(person)}</a> }
}

View File

@ -53,8 +53,13 @@ pub fn ArticleActions() -> impl IntoView {
});
view! {
<ArticleNav article=article/>
<Suspense fallback=|| view! { "Loading..." }> {
move || article.get().map(|article|
<Suspense fallback=|| {
view! { "Loading..." }
}>
{move || {
article
.get()
.map(|article| {
view! {
<div class="item-view">
<h1>{article_title(&article.article)}</h1>
@ -65,13 +70,21 @@ pub fn ArticleActions() -> impl IntoView {
view! { <p style="color:red;">{err}</p> }
})
}}
<Show
when=move || global_state.with(|state| {
state.my_profile.as_ref().map(|p| p.local_user.admin).unwrap_or_default()
&& article.article.local
})>
<button
on:click=move |_| protect_action.dispatch((article.article.id, article.article.protected))>Toggle Article Protection</button>
<Show when=move || {
global_state
.with(|state| {
state
.my_profile
.as_ref()
.map(|p| p.local_user.admin)
.unwrap_or_default() && article.article.local
})
}>
<button on:click=move |_| {
protect_action
.dispatch((article.article.id, article.article.protected))
}>Toggle Article Protection</button>
<p>"Protect a local article so that only admins can edit it"</p>
</Show>
<Show when=move || !article.article.local>
@ -80,21 +93,31 @@ pub fn ArticleActions() -> impl IntoView {
on:keyup=move |ev: ev::KeyboardEvent| {
let val = event_target_value(&ev);
set_new_title.update(|v| *v = val);
} />
}
/>
<button
disabled=move || new_title.get().is_empty()
on:click=move |_| fork_action.dispatch((article.article.id, new_title.get()))>Fork Article</button>
on:click=move |_| {
fork_action.dispatch((article.article.id, new_title.get()))
}
>
Fork Article
</button>
<p>
"You can fork a remote article to the local instance. This is useful if the original
instance is dead, or if there are disagreements how the article should be written."
</p>
</Show>
</div>
})
}
})
}}
</Suspense>
<Show when=move || fork_response.get().is_some()>
<Redirect path={article_link(&fork_response.get().unwrap())}/>
<Redirect path=article_link(&fork_response.get().unwrap())/>
</Show>
<p>"TODO: add option for admin to delete article etc"</p>
}

View File

@ -56,12 +56,21 @@ pub fn CreateArticle() -> impl IntoView {
set_title.update(|v| *v = val);
}
/>
<textarea placeholder="Article text..." on:keyup=move |ev| {
<textarea
placeholder="Article text..."
on:keyup=move |ev| {
let val = event_target_value(&ev);
set_text.update(|p| *p = val);
} >
}
>
</textarea>
<div><a href="https://commonmark.org/help/" target="blank_">Markdown</a>" formatting is supported"</div>
<div>
<a href="https://commonmark.org/help/" target="blank_">
Markdown
</a>
" formatting is supported"
</div>
{move || {
create_error
.get()
@ -69,21 +78,28 @@ pub fn CreateArticle() -> impl IntoView {
view! { <p style="color:red;">{err}</p> }
})
}}
<input type="text"
<input
type="text"
placeholder="Edit summary"
on:keyup=move |ev| {
let val = event_target_value(&ev);
set_summary.update(|p| *p = val);
}/>
}
/>
<button
prop:disabled=move || button_is_disabled.get()
on:click=move |_| submit_action.dispatch((title.get(), text.get(), summary.get()))>
on:click=move |_| submit_action.dispatch((title.get(), text.get(), summary.get()))
>
Submit
</button>
</div>
}
}>
<Redirect path={format!("/article/{}", title.get().replace(' ', "_"))} />
}
>
<Redirect path=format!("/article/{}", title.get().replace(' ', "_"))/>
</Show>
}
}

View File

@ -105,16 +105,21 @@ pub fn EditArticle() -> impl IntoView {
when=move || edit_response.get() == EditResponse::Success
fallback=move || {
view! {
<Suspense fallback=|| view! { "Loading..." }> {
move || article.get().map(|mut article| {
<Suspense fallback=|| {
view! { "Loading..." }
}>
{move || {
article
.get()
.map(|mut article| {
if let EditResponse::Conflict(conflict) = edit_response.get() {
article.article.text = conflict.three_way_merge;
set_summary.set(conflict.summary);
}
// set initial text, otherwise submit with no changes results in empty text
set_text.set(article.article.text.clone());
let article_ = article.clone();
view! {
// set initial text, otherwise submit with no changes results in empty text
<div class="item-view">
<h1>{article_title(&article.article)}</h1>
{move || {
@ -124,31 +129,52 @@ pub fn EditArticle() -> impl IntoView {
view! { <p style="color:red;">{err}</p> }
})
}}
<textarea on:keyup=move |ev| {
let val = event_target_value(&ev);
set_text.update(|p| *p = val);
}>
{article.article.text.clone()}
</textarea>
<div><a href="https://commonmark.org/help/" target="blank_">Markdown</a>" formatting is supported"</div>
<input type="text"
}>{article.article.text.clone()}</textarea>
<div>
<a href="https://commonmark.org/help/" target="blank_">
Markdown
</a>
" formatting is supported"
</div>
<input
type="text"
placeholder="Edit summary"
value={summary.get_untracked()}
value=summary.get_untracked()
on:keyup=move |ev| {
let val = event_target_value(&ev);
set_summary.update(|p| *p = val);
}/>
}
/>
<button
prop:disabled=move || button_is_disabled.get()
on:click=move |_| submit_action.dispatch((text.get(), summary.get(), article_.clone(), edit_response.get()))>
on:click=move |_| {
submit_action
.dispatch((
text.get(),
summary.get(),
article_.clone(),
edit_response.get(),
))
}
>
Submit
</button>
</div>
}
})
}
}}
</Suspense>
}}>
}
}
>
Edit successful!
</Show>
}

View File

@ -13,22 +13,48 @@ pub fn ArticleHistory() -> impl IntoView {
view! {
<ArticleNav article=article/>
<Suspense fallback=|| view! { "Loading..." }> {
move || article.get().map(|article| {
<Suspense fallback=|| {
view! { "Loading..." }
}>
{move || {
article
.get()
.map(|article| {
view! {
<div class="item-view">
<h1>{article_title(&article.article)}</h1>
{
article.edits.into_iter().rev().map(|edit| {
let path = format!("/article/{}@{}/diff/{}", article.article.title, extract_domain(&article.article.ap_id), edit.edit.hash.0);
let label = format!("{} ({})", edit.edit.summary, edit.edit.created.to_rfc2822());
view! {<li><a href={path}>{label}</a>" by "{user_link(&edit.creator)}</li> }
}).collect::<Vec<_>>()
{article
.edits
.into_iter()
.rev()
.map(|edit| {
let path = format!(
"/article/{}@{}/diff/{}",
article.article.title,
extract_domain(&article.article.ap_id),
edit.edit.hash.0,
);
let label = format!(
"{} ({})",
edit.edit.summary,
edit.edit.created.to_rfc2822(),
);
view! {
<li>
<a href=path>{label}</a>
" by "
{user_link(&edit.creator)}
</li>
}
})
.collect::<Vec<_>>()}
</div>
}
})
}
}}
</Suspense>
}
}

View File

@ -24,25 +24,33 @@ pub fn ListArticles() -> impl IntoView {
<h1>Most recently edited Articles</h1>
<Suspense fallback=|| view! { "Loading..." }>
<fieldset on:input=move |ev| {
let val = ev
.target()
.unwrap()
.unchecked_into::<web_sys::HtmlInputElement>()
.id();
let val = ev.target().unwrap().unchecked_into::<web_sys::HtmlInputElement>().id();
let is_local_only = val == "only-local";
set_only_local.update(|p| *p = is_local_only);
}>
<input type="radio" name="listing-type" id="only-local" />
<input type="radio" name="listing-type" id="only-local"/>
<label for="only-local">Only Local</label>
<input type="radio" name="listing-type" id="all" checked/>
<label for="all">All</label>
</fieldset>
<ul> {
move || articles.get().map(|a|
a.into_iter().map(|a| view! {
<li><a href=article_link(&a)>{article_title(&a)}</a></li>
}).collect::<Vec<_>>())
} </ul>
<ul>
{move || {
articles
.get()
.map(|a| {
a.into_iter()
.map(|a| {
view! {
<li>
<a href=article_link(&a)>{article_title(&a)}</a>
</li>
}
})
.collect::<Vec<_>>()
})
}}
</ul>
</Suspense>
}
}

View File

@ -12,16 +12,26 @@ pub fn ReadArticle() -> impl IntoView {
view! {
<ArticleNav article=article/>
<Suspense fallback=|| view! { "Loading..." }> {
<Suspense fallback=|| {
view! { "Loading..." }
}>
{
let parser = markdown_parser();
move || article.get().map(|article|
move || {
article
.get()
.map(|article| {
view! {
<div class="item-view">
<h1>{article_title(&article.article)}</h1>
<div inner_html={parser.parse(&article.article.text).render()}/>
<div inner_html=parser.parse(&article.article.text).render()></div>
</div>
}
})
}
}
</Suspense>
}
}

View File

@ -11,14 +11,25 @@ pub fn Conflicts() -> impl IntoView {
view! {
<h1>Your unresolved edit conflicts</h1>
<Suspense fallback=|| view! { "Loading..." }>
<ul> {
move || conflicts.get().map(|c|
c.into_iter().map(|c| {
<ul>
{move || {
conflicts
.get()
.map(|c| {
c.into_iter()
.map(|c| {
let link = format!("{}/edit/{}", article_link(&c.article), c.id);
view! {
<li><a href=link>{article_title(&c.article)}" - "{c.summary}</a></li>
}}).collect::<Vec<_>>())
} </ul>
<li>
<a href=link>{article_title(&c.article)} " - " {c.summary}</a>
</li>
}
})
.collect::<Vec<_>>()
})
}}
</ul>
</Suspense>
}
}

View File

@ -9,25 +9,35 @@ pub fn EditDiff() -> impl IntoView {
view! {
<ArticleNav article=article/>
<Suspense fallback=|| view! { "Loading..." }> {
move || article.get().map(|article| {
let hash = params
.get_untracked()
.get("hash")
.cloned().unwrap();
let edit = article.edits.iter().find(|e| e.edit.hash.0.to_string() == hash).unwrap();
let label = format!("{} ({})", edit.edit.summary, edit.edit.created.to_rfc2822());
<Suspense fallback=|| {
view! { "Loading..." }
}>
{move || {
article
.get()
.map(|article| {
let hash = params.get_untracked().get("hash").cloned().unwrap();
let edit = article
.edits
.iter()
.find(|e| e.edit.hash.0.to_string() == hash)
.unwrap();
let label = format!(
"{} ({})",
edit.edit.summary,
edit.edit.created.to_rfc2822(),
);
view! {
<div class="item-view">
<h1>{article.article.title.replace('_', " ")}</h1>
<h2>{label}</h2>
<p>"by "{user_link(&edit.creator)}</p>
<p>"by " {user_link(&edit.creator)}</p>
<pre>{edit.edit.diff.clone()}</pre>
</div>
}
})
}
}}
</Suspense>
}
}

View File

@ -20,22 +20,32 @@ pub fn InstanceDetails() -> impl IntoView {
});
view! {
<Suspense fallback=|| view! { "Loading..." }> {
move || instance_profile.get().map(|instance: DbInstance| {
<Suspense fallback=|| {
view! { "Loading..." }
}>
{move || {
instance_profile
.get()
.map(|instance: DbInstance| {
let instance_ = instance.clone();
view! {
<h1>{instance.domain}</h1>
<Show when=move || global_state.with(|state| state.my_profile.is_some())>
<InstanceFollowButton instance=instance_.clone() />
<InstanceFollowButton instance=instance_.clone()/>
</Show>
<p>Follow the instance so that new edits are federated to your instance.</p>
<p>"TODO: show a list of articles from the instance. For now you can use the "<a href="/article/list">Article list</a>.</p>
<p>
"TODO: show a list of articles from the instance. For now you can use the "
<a href="/article/list">Article list</a> .
</p>
<hr/>
<h2>"Description:"</h2>
<div>{instance.description}</div>
}
})
}</Suspense>
}}
</Suspense>
}
}

View File

@ -52,6 +52,7 @@ pub fn Login() -> impl IntoView {
}
}
>
<Redirect path="/"/>
</Show>
}

View File

@ -53,6 +53,7 @@ pub fn Register() -> impl IntoView {
}
}
>
<p>"You have successfully registered."</p>
</Show>
}

View File

@ -52,45 +52,67 @@ pub fn Search() -> impl IntoView {
});
view! {
<h1>"Search results for "{query}</h1>
<Suspense fallback=|| view! { "Loading..." }> {
move || search_results.get().map(move |search_results| {
<h1>"Search results for " {query}</h1>
<Suspense fallback=|| {
view! { "Loading..." }
}>
{move || {
search_results
.get()
.map(move |search_results| {
let is_empty = search_results.is_empty();
view! {
<Show when=move || !is_empty
<Show
when=move || !is_empty
fallback=move || {
let error_view = move || {
error.get().map(|err| {
error
.get()
.map(|err| {
view! { <p style="color:red;">{err}</p> }
})
};
view! {
{error_view}
<p>No results found</p>
}}>
}
}
>
<ul>
{
// render resolved instance
if let Some(instance) = &search_results.instance {
{if let Some(instance) = &search_results.instance {
let domain = &instance.domain;
vec![view! { <li>
<a href={format!("/instance/{domain}")}>{domain}</a>
</li>}]
} else { vec![] }
}
{
vec![
view! {
<li>
<a href=format!("/instance/{domain}")>{domain}</a>
</li>
},
]
} else {
vec![]
}}
// render articles from resolve/search
search_results.articles
{search_results
.articles
.iter()
.map(|a| view! { <li>
<a href={article_link(a)}>{article_title(a)}</a>
</li>})
.collect::<Vec<_>>()
.map(|a| {
view! {
<li>
<a href=article_link(a)>{article_title(a)}</a>
</li>
}
})
.collect::<Vec<_>>()}
</ul>
</Show>
}})
}
})
}}
</Suspense>
}
}

View File

@ -29,13 +29,21 @@ pub fn UserProfile() -> impl IntoView {
view! { <p style="color:red;">{err}</p> }
})
}}
<Suspense fallback=|| view! { "Loading..." }> {
move || user_profile.get().map(|person: DbPerson| {
<Suspense fallback=|| {
view! { "Loading..." }
}>
{move || {
user_profile
.get()
.map(|person: DbPerson| {
view! {
<h1>{user_title(&person)}</h1>
<p>TODO: create actual user profile</p>
}
})
}</Suspense>
}}
</Suspense>
}
}