Upgrade deps (#60)

* Upgrade dependencies

* fix tests

* ci config
This commit is contained in:
Nutomic 2024-10-02 14:47:44 +02:00 committed by GitHub
parent 706713be9d
commit fdda0f584a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1689 additions and 1330 deletions

View File

@ -1,5 +1,5 @@
variables: variables:
- &rust_image "rust:1.75" - &rust_image "rust:1.81"
- &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" - &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: steps:
@ -11,6 +11,8 @@ steps:
commands: commands:
- rustup component add rustfmt - rustup component add rustfmt
- cargo +nightly fmt -- --check - cargo +nightly fmt -- --check
when:
- event: pull_request
leptos_fmt: leptos_fmt:
image: *rust_image image: *rust_image

1537
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -9,5 +9,5 @@ IBIS__BIND="${IBIS_BIND:-"127.0.0.1:8081"}"
# start frontend # start frontend
CARGO_TARGET_DIR=target/frontend trunk serve -w src/frontend/ --proxy-backend http://$IBIS__BIND & 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 # 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 activitypub_federation::config::Data;
use axum::{ use axum::{
body::Body,
http::{Request, StatusCode}, http::{Request, StatusCode},
middleware::{self, Next}, middleware::{self, Next},
response::Response, response::Response,
@ -45,7 +46,7 @@ pub mod article;
pub mod instance; pub mod instance;
pub mod user; pub mod user;
pub fn api_routes() -> Router { pub fn api_routes() -> Router<()> {
Router::new() Router::new()
.route( .route(
"/article", "/article",
@ -68,11 +69,11 @@ pub fn api_routes() -> Router {
.route_layer(middleware::from_fn(auth)) .route_layer(middleware::from_fn(auth))
} }
async fn auth<B>( async fn auth(
data: Data<IbisData>, data: Data<IbisData>,
jar: CookieJar, jar: CookieJar,
mut request: Request<B>, mut request: Request<Body>,
next: Next<B>, next: Next,
) -> Result<Response, StatusCode> { ) -> Result<Response, StatusCode> {
if let Some(auth) = jar.get(AUTH_COOKIE) { if let Some(auth) = jar.get(AUTH_COOKIE) {
if let Ok(user) = validate(auth.value(), &data).await { 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(':') { if domain.contains(':') {
domain = domain.split(':').collect::<Vec<_>>()[0].to_string(); domain = domain.split(':').collect::<Vec<_>>()[0].to_string();
} }
Cookie::build(AUTH_COOKIE, jwt) Cookie::build((AUTH_COOKIE, jwt))
.domain(domain) .domain(domain)
.same_site(SameSite::Strict) .same_site(SameSite::Strict)
.path("/") .path("/")
@ -107,7 +107,7 @@ fn create_cookie(jwt: String, data: &Data<IbisData>) -> Cookie<'static> {
.expires(Expiration::DateTime( .expires(Expiration::DateTime(
OffsetDateTime::now_utc() + Duration::weeks(52), OffsetDateTime::now_utc() + Duration::weeks(52),
)) ))
.finish() .build()
} }
#[debug_handler] #[debug_handler]

View File

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

View File

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

View File

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

View File

@ -90,30 +90,30 @@ pub fn App() -> impl IntoView {
provide_context(create_rw_signal(global_state)); provide_context(create_rw_signal(global_state));
view! { view! {
<> <>
<Stylesheet id="simple" href="/assets/simple.css"/> <Stylesheet id="simple" href="/assets/simple.css" />
<Stylesheet id="ibis" href="/assets/ibis.css"/> <Stylesheet id="ibis" href="/assets/ibis.css" />
<Router> <Router>
<Nav/> <Nav />
<main> <main>
<Routes> <Routes>
<Route path="/" view=ReadArticle/> <Route path="/" view=ReadArticle />
<Route path="/article/:title" view=ReadArticle/> <Route path="/article/:title" view=ReadArticle />
<Route path="/article/:title/history" view=ArticleHistory/> <Route path="/article/:title/history" view=ArticleHistory />
<Route path="/article/:title/edit/:conflict_id?" view=EditArticle/> <Route path="/article/:title/edit/:conflict_id?" view=EditArticle />
<Route path="/article/:title/actions" view=ArticleActions/> <Route path="/article/:title/actions" view=ArticleActions />
<Route path="/article/:title/diff/:hash" view=EditDiff/> <Route path="/article/:title/diff/:hash" view=EditDiff />
<Route path="/article/create" view=CreateArticle/> <Route path="/article/create" view=CreateArticle />
<Route path="/article/list" view=ListArticles/> <Route path="/article/list" view=ListArticles />
<Route path="/instance/:hostname" view=InstanceDetails/> <Route path="/instance/:hostname" view=InstanceDetails />
<Route path="/user/:name" view=UserProfile/> <Route path="/user/:name" view=UserProfile />
<Route path="/login" view=Login/> <Route path="/login" view=Login />
<Route path="/register" view=Register/> <Route path="/register" view=Register />
<Route path="/search" view=Search/> <Route path="/search" view=Search />
<Route path="/conflicts" view=Conflicts/> <Route path="/conflicts" view=Conflicts />
</Routes> </Routes>
</main> </main>
</Router> </Router>
</> </>
} }
} }

View File

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

View File

@ -18,63 +18,63 @@ pub fn CredentialsForm(
}); });
view! { view! {
<form on:submit=|ev| ev.prevent_default()> <form on:submit=|ev| ev.prevent_default()>
<p>{title}</p> <p>{title}</p>
{move || { {move || {
error error
.get() .get()
.map(|err| { .map(|err| {
view! { <p style="color:red;">{err}</p> } view! { <p style="color:red;">{err}</p> }
}) })
}} }}
<input <input
type="text" type="text"
required required
placeholder="Username" placeholder="Username"
prop:disabled=move || disabled.get() prop:disabled=move || disabled.get()
on:keyup=move |ev: ev::KeyboardEvent| { on:keyup=move |ev: ev::KeyboardEvent| {
let val = event_target_value(&ev); let val = event_target_value(&ev);
set_username.update(|v| *v = val); set_username.update(|v| *v = val);
} }
on:change=move |ev| { on:change=move |ev| {
let val = event_target_value(&ev); let val = event_target_value(&ev);
set_username.update(|v| *v = val); set_username.update(|v| *v = val);
} }
/> />
<input <input
type="password" type="password"
required required
placeholder="Password" placeholder="Password"
prop:disabled=move || disabled.get() prop:disabled=move || disabled.get()
on:keyup=move |ev: ev::KeyboardEvent| { on:keyup=move |ev: ev::KeyboardEvent| {
match &*ev.key() { match &*ev.key() {
"Enter" => { "Enter" => {
dispatch_action(); dispatch_action();
} }
_ => { _ => {
let val = event_target_value(&ev); let val = event_target_value(&ev);
set_password.update(|p| *p = val); set_password.update(|p| *p = val);
} }
} }
} }
on:change=move |ev| { on:change=move |ev| {
let val = event_target_value(&ev); let val = event_target_value(&ev);
set_password.update(|p| *p = val); set_password.update(|p| *p = val);
} }
/> />
<div> <div>
<button <button
prop:disabled=move || button_is_disabled.get() prop:disabled=move || button_is_disabled.get()
on:click=move |_| dispatch_action() on:click=move |_| dispatch_action()
> >
{action_label} {action_label}
</button> </button>
</div> </div>
</form> </form>
} }
} }

View File

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

View File

@ -22,77 +22,77 @@ pub fn Nav() -> impl IntoView {
let (search_query, set_search_query) = create_signal(String::new()); let (search_query, set_search_query) = create_signal(String::new());
view! { view! {
<nav class="inner" style="min-width: 250px;"> <nav class="inner" style="min-width: 250px;">
<li> <li>
<A href="/">"Main Page"</A> <A href="/">"Main Page"</A>
</li> </li>
<li> <li>
<A href="/article/list">"List Articles"</A> <A href="/article/list">"List Articles"</A>
</li> </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>
<li>
<A href="/conflicts">"Edit Conflicts"</A>
</li>
</Show>
<li>
<form on:submit=move |ev| {
ev.prevent_default();
let navigate = leptos_router::use_navigate();
let query = search_query.get();
if !query.is_empty() {
navigate(&format!("/search?query={query}"), Default::default());
}
}>
<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>
<Show
when=move || global_state.with(|state| state.my_profile.is_some())
fallback=move || {
view! {
<li> <li>
<A href="/login">"Login"</A> <A href="/article/create">"Create Article"</A>
</li> </li>
<Show when=move || registration_open.get().unwrap_or_default()> <li>
<li> <A href="/conflicts">"Edit Conflicts"</A>
<A href="/register">"Register"</A> </li>
</li> </Show>
</Show> <li>
} <form on:submit=move |ev| {
} ev.prevent_default();
> let navigate = leptos_router::use_navigate();
let query = search_query.get();
if !query.is_empty() {
navigate(&format!("/search?query={query}"), Default::default());
}
}>
<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>
let my_profile = global_state.with(|state| state.my_profile.clone().unwrap()); </form>
let profile_link = format!("/user/{}", my_profile.person.username); </li>
view! { <Show
<p> when=move || global_state.with(|state| state.my_profile.is_some())
"Logged in as " fallback=move || {
<a view! {
href=profile_link <li>
style="border: none; padding: 0; color: var(--accent) !important;" <A href="/login">"Login"</A>
> </li>
{my_profile.person.username} <Show when=move || registration_open.get().unwrap_or_default()>
</a> <li>
</p> <A href="/register">"Register"</A>
<button on:click=move |_| logout_action.dispatch(())>Logout</button> </li>
} </Show>
} }
}
>
</Show> {
</nav> 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;"
>
{my_profile.person.username}
</a>
</p>
<button on:click=move |_| logout_action.dispatch(())>Logout</button>
}
}
</Show>
</nav>
} }
} }

View File

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

View File

@ -40,66 +40,67 @@ pub fn CreateArticle() -> impl IntoView {
}); });
view! { view! {
<h1>Create new Article</h1> <h1>Create new Article</h1>
<Show <Show
when=move || create_response.get().is_some() when=move || create_response.get().is_some()
fallback=move || { fallback=move || {
view! { view! {
<div class="item-view"> <div class="item-view">
<input <input
type="text" type="text"
required required
placeholder="Title" placeholder="Title"
prop:disabled=move || wait_for_response.get() prop:disabled=move || wait_for_response.get()
on:keyup=move |ev| { on:keyup=move |ev| {
let val = event_target_value(&ev); let val = event_target_value(&ev);
set_title.update(|v| *v = val); set_title.update(|v| *v = val);
} }
/> />
<textarea <textarea
placeholder="Article text..." placeholder="Article text..."
on:keyup=move |ev| { on:keyup=move |ev| {
let val = event_target_value(&ev); let val = event_target_value(&ev);
set_text.update(|p| *p = val); set_text.update(|p| *p = val);
} }
> ></textarea>
</textarea> <div>
<div> <a href="https://commonmark.org/help/" target="blank_">
<a href="https://commonmark.org/help/" target="blank_"> Markdown
Markdown </a>
</a> " formatting is supported"
" formatting is supported" </div>
</div> {move || {
{move || { create_error
create_error .get()
.get() .map(|err| {
.map(|err| { view! { <p style="color:red;">{err}</p> }
view! { <p style="color:red;">{err}</p> } })
}) }}
}}
<input <input
type="text" type="text"
placeholder="Edit summary" placeholder="Edit summary"
on:keyup=move |ev| { on:keyup=move |ev| {
let val = event_target_value(&ev); let val = event_target_value(&ev);
set_summary.update(|p| *p = val); set_summary.update(|p| *p = val);
} }
/> />
<button <button
prop:disabled=move || button_is_disabled.get() 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> Submit
</button>
</div>
}
} }
} >
>
<Redirect path=format!("/article/{}", title.get().replace(' ', "_"))/> <Redirect path=format!("/article/{}", title.get().replace(' ', "_")) />
</Show> </Show>
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,22 +38,22 @@ pub fn Login() -> impl IntoView {
let disabled = Signal::derive(move || wait_for_response.get()); let disabled = Signal::derive(move || wait_for_response.get());
view! { view! {
<Show <Show
when=move || login_response.get().is_some() when=move || login_response.get().is_some()
fallback=move || { fallback=move || {
view! { view! {
<CredentialsForm <CredentialsForm
title="Please enter the desired credentials" title="Please enter the desired credentials"
action_label="Login" action_label="Login"
action=login_action action=login_action
error=login_error.into() error=login_error.into()
disabled disabled
/> />
}
} }
} >
>
<Redirect path="/"/> <Redirect path="/" />
</Show> </Show>
} }
} }

View File

@ -39,22 +39,22 @@ pub fn Register() -> impl IntoView {
let disabled = Signal::derive(move || wait_for_response.get()); let disabled = Signal::derive(move || wait_for_response.get());
view! { view! {
<Show <Show
when=move || register_response.get().is_some() when=move || register_response.get().is_some()
fallback=move || { fallback=move || {
view! { view! {
<CredentialsForm <CredentialsForm
title="Please enter the desired credentials" title="Please enter the desired credentials"
action_label="Register" action_label="Register"
action=register_action action=register_action
error=register_error.into() error=register_error.into()
disabled disabled
/> />
}
} }
} >
>
<p>"You have successfully registered."</p> <p>"You have successfully registered."</p>
</Show> </Show>
} }
} }

View File

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

View File

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

View File

@ -28,6 +28,6 @@ fn main() {
_ = console_log::init_with_level(log::Level::Debug); _ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
mount_to_body(|| { mount_to_body(|| {
view! { <App/> } view! { <App /> }
}); });
} }