mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-25 19:01:09 +00:00
parent
706713be9d
commit
fdda0f584a
28 changed files with 1689 additions and 1330 deletions
|
@ -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
1537
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
67
Cargo.toml
67
Cargo.toml
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 /> }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue