mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-22 14:41:09 +00:00
Rework how site data is fetched
This commit is contained in:
parent
66f89d3bfd
commit
61806e12a6
19 changed files with 180 additions and 192 deletions
|
@ -68,7 +68,7 @@ pub(in crate::backend::api) async fn create_article(
|
||||||
instance_id: local_instance.id,
|
instance_id: local_instance.id,
|
||||||
local: true,
|
local: true,
|
||||||
protected: false,
|
protected: false,
|
||||||
approved: !data.config.article_approval,
|
approved: !data.config.config.article_approval,
|
||||||
};
|
};
|
||||||
let article = DbArticle::create(form, &data)?;
|
let article = DbArticle::create(form, &data)?;
|
||||||
|
|
||||||
|
@ -214,7 +214,7 @@ pub(in crate::backend::api) async fn fork_article(
|
||||||
instance_id: local_instance.id,
|
instance_id: local_instance.id,
|
||||||
local: true,
|
local: true,
|
||||||
protected: false,
|
protected: false,
|
||||||
approved: !data.config.article_approval,
|
approved: !data.config.config.article_approval,
|
||||||
};
|
};
|
||||||
let article = DbArticle::create(form, &data)?;
|
let article = DbArticle::create(form, &data)?;
|
||||||
|
|
||||||
|
|
|
@ -12,12 +12,12 @@ use crate::{
|
||||||
search_article,
|
search_article,
|
||||||
},
|
},
|
||||||
instance::{follow_instance, get_instance, resolve_instance},
|
instance::{follow_instance, get_instance, resolve_instance},
|
||||||
user::{get_user, login_user, logout_user, my_profile, register_user, validate},
|
user::{get_user, login_user, logout_user, register_user, validate},
|
||||||
},
|
},
|
||||||
database::IbisData,
|
database::IbisData,
|
||||||
error::MyResult,
|
error::MyResult,
|
||||||
},
|
},
|
||||||
common::{LocalUserView, AUTH_COOKIE},
|
common::{LocalUserView, SiteView, AUTH_COOKIE},
|
||||||
};
|
};
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
@ -28,9 +28,12 @@ use axum::{
|
||||||
middleware::{self, Next},
|
middleware::{self, Next},
|
||||||
response::Response,
|
response::Response,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
|
Extension,
|
||||||
|
Json,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
|
use axum_macros::debug_handler;
|
||||||
use instance::list_remote_instances;
|
use instance::list_remote_instances;
|
||||||
use user::{count_notifications, list_notifications};
|
use user::{count_notifications, list_notifications};
|
||||||
|
|
||||||
|
@ -59,8 +62,8 @@ pub fn api_routes() -> Router<()> {
|
||||||
.route("/user/notifications/count", get(count_notifications))
|
.route("/user/notifications/count", get(count_notifications))
|
||||||
.route("/account/register", post(register_user))
|
.route("/account/register", post(register_user))
|
||||||
.route("/account/login", post(login_user))
|
.route("/account/login", post(login_user))
|
||||||
.route("/account/my_profile", get(my_profile))
|
|
||||||
.route("/account/logout", get(logout_user))
|
.route("/account/logout", get(logout_user))
|
||||||
|
.route("/site", get(site_view))
|
||||||
.route_layer(middleware::from_fn(auth))
|
.route_layer(middleware::from_fn(auth))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,8 +76,7 @@ async fn auth(
|
||||||
let auth = request
|
let auth = request
|
||||||
.headers()
|
.headers()
|
||||||
.get(AUTH_COOKIE)
|
.get(AUTH_COOKIE)
|
||||||
.map(|h| h.to_str().ok())
|
.and_then(|h| h.to_str().ok())
|
||||||
.flatten()
|
|
||||||
.or(jar.get(AUTH_COOKIE).map(|c| c.value()));
|
.or(jar.get(AUTH_COOKIE).map(|c| c.value()));
|
||||||
|
|
||||||
if let Some(auth) = auth {
|
if let Some(auth) = auth {
|
||||||
|
@ -92,3 +94,14 @@ fn check_is_admin(user: &LocalUserView) -> MyResult<()> {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
pub(in crate::backend::api) async fn site_view(
|
||||||
|
data: Data<IbisData>,
|
||||||
|
user: Option<Extension<LocalUserView>>,
|
||||||
|
) -> MyResult<Json<SiteView>> {
|
||||||
|
Ok(Json(SiteView {
|
||||||
|
my_profile: user.map(|u| u.0),
|
||||||
|
config: data.config.config.clone(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
|
@ -76,7 +76,7 @@ pub(in crate::backend::api) async fn register_user(
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
Form(form): Form<RegisterUserForm>,
|
Form(form): Form<RegisterUserForm>,
|
||||||
) -> MyResult<(CookieJar, Json<LocalUserView>)> {
|
) -> MyResult<(CookieJar, Json<LocalUserView>)> {
|
||||||
if !data.config.registration_open {
|
if !data.config.config.registration_open {
|
||||||
return Err(anyhow!("Registration is closed").into());
|
return Err(anyhow!("Registration is closed").into());
|
||||||
}
|
}
|
||||||
let user = DbPerson::create_local(form.username, form.password, false, &data)?;
|
let user = DbPerson::create_local(form.username, form.password, false, &data)?;
|
||||||
|
@ -121,19 +121,6 @@ fn create_cookie(jwt: String, data: &Data<IbisData>) -> Cookie<'static> {
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
pub(in crate::backend::api) async fn my_profile(
|
|
||||||
data: Data<IbisData>,
|
|
||||||
jar: CookieJar,
|
|
||||||
) -> MyResult<Json<LocalUserView>> {
|
|
||||||
let jwt = jar.get(AUTH_COOKIE).map(|c| c.value());
|
|
||||||
if let Some(jwt) = jwt {
|
|
||||||
Ok(Json(validate(jwt, &data).await?))
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("invalid/missing auth").into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub(in crate::backend::api) async fn logout_user(
|
pub(in crate::backend::api) async fn logout_user(
|
||||||
data: Data<IbisData>,
|
data: Data<IbisData>,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::backend::error::MyResult;
|
use crate::{backend::error::MyResult, common::SharedConfig};
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use doku::Document;
|
use doku::Document;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -10,17 +10,10 @@ use smart_default::SmartDefault;
|
||||||
pub struct IbisConfig {
|
pub struct IbisConfig {
|
||||||
/// Details about the PostgreSQL database connection
|
/// Details about the PostgreSQL database connection
|
||||||
pub database: IbisConfigDatabase,
|
pub database: IbisConfigDatabase,
|
||||||
/// Whether users can create new accounts
|
|
||||||
#[default = true]
|
|
||||||
#[doku(example = "true")]
|
|
||||||
pub registration_open: bool,
|
|
||||||
/// Whether admins need to approve new articles
|
|
||||||
#[default = false]
|
|
||||||
#[doku(example = "false")]
|
|
||||||
pub article_approval: bool,
|
|
||||||
/// Details of the initial admin account
|
/// Details of the initial admin account
|
||||||
pub setup: IbisConfigSetup,
|
pub setup: IbisConfigSetup,
|
||||||
pub federation: IbisConfigFederation,
|
pub federation: IbisConfigFederation,
|
||||||
|
pub config: SharedConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IbisConfig {
|
impl IbisConfig {
|
||||||
|
|
|
@ -86,7 +86,6 @@ impl DbInstance {
|
||||||
Ok(InstanceView {
|
Ok(InstanceView {
|
||||||
instance,
|
instance,
|
||||||
followers,
|
followers,
|
||||||
registration_open: data.config.registration_open,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -136,7 +136,7 @@ async fn leptos_routes_handler(
|
||||||
let cookie = jar.get(AUTH_COOKIE).map(|c| c.value().to_string());
|
let cookie = jar.get(AUTH_COOKIE).map(|c| c.value().to_string());
|
||||||
provide_context(Auth(cookie));
|
provide_context(Auth(cookie));
|
||||||
},
|
},
|
||||||
move || view! { <App/> },
|
move || view! { <App /> },
|
||||||
);
|
);
|
||||||
|
|
||||||
handler(req).await.into_response()
|
handler(req).await.into_response()
|
||||||
|
|
|
@ -34,7 +34,7 @@ async fn node_info(data: Data<IbisData>) -> MyResult<Json<NodeInfo>> {
|
||||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||||
},
|
},
|
||||||
protocols: vec!["activitypub".to_string()],
|
protocols: vec!["activitypub".to_string()],
|
||||||
open_registrations: data.config.registration_open,
|
open_registrations: data.config.config.registration_open,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ use chrono::{DateTime, Utc};
|
||||||
use newtypes::{ArticleId, ConflictId, EditId, InstanceId, PersonId};
|
use newtypes::{ArticleId, ConflictId, EditId, InstanceId, PersonId};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
use smart_default::SmartDefault;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
|
@ -17,6 +18,7 @@ use {
|
||||||
},
|
},
|
||||||
activitypub_federation::fetch::{collection_id::CollectionId, object_id::ObjectId},
|
activitypub_federation::fetch::{collection_id::CollectionId, object_id::ObjectId},
|
||||||
diesel::{Identifiable, Queryable, Selectable},
|
diesel::{Identifiable, Queryable, Selectable},
|
||||||
|
doku::Document,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const MAIN_PAGE_NAME: &str = "Main_Page";
|
pub const MAIN_PAGE_NAME: &str = "Main_Page";
|
||||||
|
@ -311,7 +313,6 @@ impl DbInstance {
|
||||||
pub struct InstanceView {
|
pub struct InstanceView {
|
||||||
pub instance: DbInstance,
|
pub instance: DbInstance,
|
||||||
pub followers: Vec<DbPerson>,
|
pub followers: Vec<DbPerson>,
|
||||||
pub registration_open: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
|
@ -320,6 +321,30 @@ pub struct GetUserForm {
|
||||||
pub domain: Option<String>,
|
pub domain: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, SmartDefault)]
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
#[cfg_attr(feature = "ssr", derive(Queryable, Document))]
|
||||||
|
#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||||
|
pub struct SharedConfig {
|
||||||
|
/// Whether users can create new accounts
|
||||||
|
#[default = true]
|
||||||
|
#[cfg_attr(feature = "ssr", doku(example = "true"))]
|
||||||
|
pub registration_open: bool,
|
||||||
|
/// Whether admins need to approve new articles
|
||||||
|
#[default = false]
|
||||||
|
#[cfg_attr(feature = "ssr", doku(example = "false"))]
|
||||||
|
pub article_approval: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
|
||||||
|
#[cfg_attr(feature = "ssr", derive(Queryable))]
|
||||||
|
#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||||
|
pub struct SiteView {
|
||||||
|
pub my_profile: Option<LocalUserView>,
|
||||||
|
pub config: SharedConfig,
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_edit_versions() {
|
fn test_edit_versions() {
|
||||||
let default = EditVersion::default();
|
let default = EditVersion::default();
|
||||||
|
|
|
@ -24,6 +24,7 @@ use crate::{
|
||||||
RegisterUserForm,
|
RegisterUserForm,
|
||||||
ResolveObject,
|
ResolveObject,
|
||||||
SearchArticleForm,
|
SearchArticleForm,
|
||||||
|
SiteView,
|
||||||
},
|
},
|
||||||
frontend::error::MyResult,
|
frontend::error::MyResult,
|
||||||
};
|
};
|
||||||
|
@ -210,10 +211,8 @@ impl ApiClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn my_profile(&self) -> MyResult<LocalUserView> {
|
pub async fn site(&self) -> MyResult<SiteView> {
|
||||||
let req = self
|
let req = self.client.get(self.request_endpoint("/api/v1/site"));
|
||||||
.client
|
|
||||||
.get(self.request_endpoint("/api/v1/account/my_profile"));
|
|
||||||
handle_json_res(req).await
|
handle_json_res(req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
common::LocalUserView,
|
common::SiteView,
|
||||||
frontend::{
|
frontend::{
|
||||||
api::CLIENT,
|
api::CLIENT,
|
||||||
components::nav::Nav,
|
components::nav::Nav,
|
||||||
|
@ -23,60 +23,51 @@ use crate::{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use leptos::{
|
use leptos::*;
|
||||||
component,
|
|
||||||
create_local_resource,
|
|
||||||
create_rw_signal,
|
|
||||||
expect_context,
|
|
||||||
provide_context,
|
|
||||||
use_context,
|
|
||||||
view,
|
|
||||||
DynAttrs,
|
|
||||||
IntoView,
|
|
||||||
RwSignal,
|
|
||||||
SignalGet,
|
|
||||||
SignalUpdate,
|
|
||||||
};
|
|
||||||
use leptos_meta::{provide_meta_context, *};
|
use leptos_meta::{provide_meta_context, *};
|
||||||
use leptos_router::{Route, Router, Routes};
|
use leptos_router::{Route, Router, Routes};
|
||||||
|
|
||||||
// https://book.leptos.dev/15_global_state.html
|
pub fn site() -> Resource<(), SiteView> {
|
||||||
#[derive(Clone)]
|
use_context::<Resource<(), SiteView>>().unwrap()
|
||||||
pub struct GlobalState {
|
|
||||||
pub(crate) my_profile: Option<LocalUserView>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GlobalState {
|
pub fn is_logged_in() -> bool {
|
||||||
pub fn update_my_profile() {
|
site().with_default(|site| site.my_profile.is_some())
|
||||||
create_local_resource(
|
}
|
||||||
move || (),
|
pub fn is_admin() -> bool {
|
||||||
|_| async move {
|
site().with_default(|site| {
|
||||||
let my_profile = CLIENT.my_profile().await.ok();
|
site.my_profile
|
||||||
expect_context::<RwSignal<GlobalState>>()
|
.as_ref()
|
||||||
.update(|state| state.my_profile = my_profile.clone());
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_admin() -> fn() -> bool {
|
|
||||||
move || {
|
|
||||||
use_context::<RwSignal<GlobalState>>()
|
|
||||||
.expect("global state is provided")
|
|
||||||
.get()
|
|
||||||
.my_profile
|
|
||||||
.map(|p| p.local_user.admin)
|
.map(|p| p.local_user.admin)
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait DefaultResource<T> {
|
||||||
|
fn with_default<O>(&self, f: impl FnOnce(&T) -> O) -> O;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Default> DefaultResource<T> for Resource<(), T> {
|
||||||
|
fn with_default<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||||
|
self.with(|x| match x {
|
||||||
|
Some(x) => f(x),
|
||||||
|
None => f(&T::default()),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
|
// TODO: should create_resource() but then things break
|
||||||
|
let site_resource = create_local_resource(
|
||||||
|
move || (),
|
||||||
|
|_| async move {
|
||||||
|
let site = CLIENT.site().await.unwrap();
|
||||||
|
site
|
||||||
|
},
|
||||||
|
);
|
||||||
|
provide_context(site_resource);
|
||||||
provide_meta_context();
|
provide_meta_context();
|
||||||
let global_state = GlobalState { my_profile: None };
|
|
||||||
// Load user profile in case we are already logged in
|
|
||||||
GlobalState::update_my_profile();
|
|
||||||
provide_context(create_rw_signal(global_state));
|
|
||||||
|
|
||||||
let darkmode = DarkMode::init();
|
let darkmode = DarkMode::init();
|
||||||
provide_context(darkmode.clone());
|
provide_context(darkmode.clone());
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::{
|
||||||
common::{validation::can_edit_article, ArticleView, GetInstance},
|
common::{validation::can_edit_article, ArticleView, GetInstance},
|
||||||
frontend::{
|
frontend::{
|
||||||
api::CLIENT,
|
api::CLIENT,
|
||||||
app::GlobalState,
|
app::{is_admin, is_logged_in},
|
||||||
article_link,
|
article_link,
|
||||||
article_title,
|
article_title,
|
||||||
components::instance_follow_button::InstanceFollowButton,
|
components::instance_follow_button::InstanceFollowButton,
|
||||||
|
@ -32,7 +32,7 @@ pub fn ArticleNav(
|
||||||
.get()
|
.get()
|
||||||
.map(|article_| {
|
.map(|article_| {
|
||||||
let title = article_title(&article_.article);
|
let title = article_title(&article_.article);
|
||||||
let instance = create_local_resource(
|
let instance = create_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 {
|
||||||
|
@ -41,7 +41,6 @@ pub fn ArticleNav(
|
||||||
CLIENT.get_instance(&form).await.unwrap()
|
CLIENT.get_instance(&form).await.unwrap()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
|
|
||||||
let article_link = article_link(&article_.article);
|
let article_link = article_link(&article_.article);
|
||||||
let article_link_ = article_link.clone();
|
let article_link_ = article_link.clone();
|
||||||
let protected = article_.article.protected;
|
let protected = article_.article.protected;
|
||||||
|
@ -54,24 +53,14 @@ pub fn ArticleNav(
|
||||||
"History"
|
"History"
|
||||||
</A>
|
</A>
|
||||||
<Show when=move || {
|
<Show when=move || {
|
||||||
global_state
|
is_logged_in()
|
||||||
.with(|state| {
|
&& can_edit_article(&article_.article, is_admin()).is_ok()
|
||||||
let is_admin = state
|
|
||||||
.my_profile
|
|
||||||
.as_ref()
|
|
||||||
.map(|p| p.local_user.admin)
|
|
||||||
.unwrap_or(false);
|
|
||||||
state.my_profile.is_some()
|
|
||||||
&& can_edit_article(&article_.article, is_admin).is_ok()
|
|
||||||
})
|
|
||||||
}>
|
}>
|
||||||
<A class=tab_classes.edit href=format!("{article_link}/edit")>
|
<A class=tab_classes.edit href=format!("{article_link}/edit")>
|
||||||
"Edit"
|
"Edit"
|
||||||
</A>
|
</A>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when=move || {
|
<Show when=is_logged_in>
|
||||||
global_state.with(|state| state.my_profile.is_some())
|
|
||||||
}>
|
|
||||||
<A
|
<A
|
||||||
class=tab_classes.actions
|
class=tab_classes.actions
|
||||||
href=format!("{article_link_}/actions")
|
href=format!("{article_link_}/actions")
|
||||||
|
|
|
@ -1,24 +1,28 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{newtypes::InstanceId, DbInstance, FollowInstance},
|
common::{newtypes::InstanceId, DbInstance, FollowInstance},
|
||||||
frontend::{api::CLIENT, app::GlobalState},
|
frontend::{
|
||||||
|
api::CLIENT,
|
||||||
|
app::{site, DefaultResource},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use leptos::{component, *};
|
use leptos::{component, *};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn InstanceFollowButton(instance: DbInstance) -> impl IntoView {
|
pub fn InstanceFollowButton(instance: DbInstance) -> impl IntoView {
|
||||||
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
|
|
||||||
let follow_action = create_action(move |instance_id: &InstanceId| {
|
let follow_action = create_action(move |instance_id: &InstanceId| {
|
||||||
let instance_id = *instance_id;
|
let instance_id = *instance_id;
|
||||||
async move {
|
async move {
|
||||||
let form = FollowInstance { id: instance_id };
|
let form = FollowInstance { id: instance_id };
|
||||||
CLIENT.follow_instance(form).await.unwrap();
|
CLIENT.follow_instance(form).await.unwrap();
|
||||||
GlobalState::update_my_profile();
|
site().refetch();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let is_following = global_state
|
let is_following = site()
|
||||||
.get_untracked()
|
.with_default(|site| {
|
||||||
|
site.clone()
|
||||||
.my_profile
|
.my_profile
|
||||||
.map(|p| p.following.contains(&instance))
|
.map(|p| p.following.contains(&instance))
|
||||||
|
})
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let follow_text = if is_following {
|
let follow_text = if is_following {
|
||||||
"Following instance"
|
"Following instance"
|
||||||
|
|
|
@ -1,24 +1,17 @@
|
||||||
use crate::frontend::{api::CLIENT, app::GlobalState, dark_mode::DarkMode};
|
use crate::frontend::{
|
||||||
use leptos::{component, use_context, view, IntoView, RwSignal, SignalWith, *};
|
api::CLIENT,
|
||||||
|
app::{is_logged_in, site, DefaultResource},
|
||||||
|
dark_mode::DarkMode,
|
||||||
|
};
|
||||||
|
use leptos::{component, view, IntoView, *};
|
||||||
use leptos_router::*;
|
use leptos_router::*;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Nav() -> impl IntoView {
|
pub fn Nav() -> impl IntoView {
|
||||||
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
|
|
||||||
let logout_action = create_action(move |_| async move {
|
let logout_action = create_action(move |_| async move {
|
||||||
CLIENT.logout().await.unwrap();
|
CLIENT.logout().await.unwrap();
|
||||||
GlobalState::update_my_profile();
|
site().refetch();
|
||||||
});
|
});
|
||||||
let registration_open = create_local_resource(
|
|
||||||
|| (),
|
|
||||||
move |_| async move {
|
|
||||||
CLIENT
|
|
||||||
.get_local_instance()
|
|
||||||
.await
|
|
||||||
.map(|i| i.registration_open)
|
|
||||||
.unwrap_or_default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let notification_count = create_resource(
|
let notification_count = create_resource(
|
||||||
|| (),
|
|| (),
|
||||||
move |_| async move { CLIENT.notifications_count().await.unwrap_or_default() },
|
move |_| async move { CLIENT.notifications_count().await.unwrap_or_default() },
|
||||||
|
@ -56,7 +49,8 @@ pub fn Nav() -> impl IntoView {
|
||||||
<li>
|
<li>
|
||||||
<A href="/article/list">"Articles"</A>
|
<A href="/article/list">"Articles"</A>
|
||||||
</li>
|
</li>
|
||||||
<Show when=move || global_state.with(|state| state.my_profile.is_some())>
|
<Transition>
|
||||||
|
<Show when=is_logged_in>
|
||||||
<li>
|
<li>
|
||||||
<A href="/article/create">"Create Article"</A>
|
<A href="/article/create">"Create Article"</A>
|
||||||
</li>
|
</li>
|
||||||
|
@ -64,11 +58,12 @@ pub fn Nav() -> impl IntoView {
|
||||||
<A href="/notifications">
|
<A href="/notifications">
|
||||||
"Notifications "
|
"Notifications "
|
||||||
<span class="indicator-item indicator-end badge badge-neutral">
|
<span class="indicator-item indicator-end badge badge-neutral">
|
||||||
{notification_count}
|
{move || notification_count.get()}
|
||||||
</span>
|
</span>
|
||||||
</A>
|
</A>
|
||||||
</li>
|
</li>
|
||||||
</Show>
|
</Show>
|
||||||
|
</Transition>
|
||||||
<li>
|
<li>
|
||||||
<form
|
<form
|
||||||
class="form-control m-0 p-1"
|
class="form-control m-0 p-1"
|
||||||
|
@ -96,14 +91,17 @@ pub fn Nav() -> impl IntoView {
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
<Transition>
|
||||||
<Show
|
<Show
|
||||||
when=move || global_state.with(|state| state.my_profile.is_some())
|
when=is_logged_in
|
||||||
fallback=move || {
|
fallback=move || {
|
||||||
view! {
|
view! {
|
||||||
<li>
|
<li>
|
||||||
<A href="/login">"Login"</A>
|
<A href="/login">"Login"</A>
|
||||||
</li>
|
</li>
|
||||||
<Show when=move || registration_open.get().unwrap_or_default()>
|
<Show when=move || {
|
||||||
|
site().with_default(|s| s.config.registration_open)
|
||||||
|
}>
|
||||||
<li>
|
<li>
|
||||||
<A href="/register">"Register"</A>
|
<A href="/register">"Register"</A>
|
||||||
</li>
|
</li>
|
||||||
|
@ -113,8 +111,8 @@ pub fn Nav() -> impl IntoView {
|
||||||
>
|
>
|
||||||
|
|
||||||
{
|
{
|
||||||
let my_profile = global_state
|
let my_profile = site()
|
||||||
.with(|state| state.my_profile.clone().unwrap());
|
.with_default(|site| site.clone().my_profile.unwrap());
|
||||||
let profile_link = format!("/user/{}", my_profile.person.username);
|
let profile_link = format!("/user/{}", my_profile.person.username);
|
||||||
view! {
|
view! {
|
||||||
<p class="self-center pb-2">
|
<p class="self-center pb-2">
|
||||||
|
@ -132,6 +130,7 @@ pub fn Nav() -> impl IntoView {
|
||||||
}
|
}
|
||||||
|
|
||||||
</Show>
|
</Show>
|
||||||
|
</Transition>
|
||||||
<div class="flex-grow min-h-2"></div>
|
<div class="flex-grow min-h-2"></div>
|
||||||
<div class="m-1 grid gap-2">
|
<div class="m-1 grid gap-2">
|
||||||
<label class="flex cursor-pointer gap-2">
|
<label class="flex cursor-pointer gap-2">
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::{
|
||||||
common::{newtypes::ArticleId, ForkArticleForm, ProtectArticleForm},
|
common::{newtypes::ArticleId, ForkArticleForm, ProtectArticleForm},
|
||||||
frontend::{
|
frontend::{
|
||||||
api::CLIENT,
|
api::CLIENT,
|
||||||
app::GlobalState,
|
app::is_admin,
|
||||||
article_link,
|
article_link,
|
||||||
components::article_nav::{ActiveTab, ArticleNav},
|
components::article_nav::{ActiveTab, ArticleNav},
|
||||||
pages::article_resource,
|
pages::article_resource,
|
||||||
|
@ -14,7 +14,6 @@ use leptos_router::Redirect;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ArticleActions() -> impl IntoView {
|
pub fn ArticleActions() -> impl IntoView {
|
||||||
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
|
|
||||||
let article = article_resource();
|
let article = article_resource();
|
||||||
let (new_title, set_new_title) = create_signal(String::new());
|
let (new_title, set_new_title) = create_signal(String::new());
|
||||||
let (fork_response, set_fork_response) = create_signal(Option::<DbArticle>::None);
|
let (fork_response, set_fork_response) = create_signal(Option::<DbArticle>::None);
|
||||||
|
@ -68,17 +67,7 @@ pub fn ArticleActions() -> impl IntoView {
|
||||||
.map(|err| {
|
.map(|err| {
|
||||||
view! { <p class="alert">{err}</p> }
|
view! { <p class="alert">{err}</p> }
|
||||||
})
|
})
|
||||||
}}
|
}} <Show when=move || { is_admin() && article.article.local }>
|
||||||
<Show when=move || {
|
|
||||||
global_state
|
|
||||||
.with(|state| {
|
|
||||||
state
|
|
||||||
.my_profile
|
|
||||||
.as_ref()
|
|
||||||
.map(|p| p.local_user.admin)
|
|
||||||
.unwrap_or_default() && article.article.local
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
on:click=move |_| {
|
on:click=move |_| {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
common::LoginUserForm,
|
common::LoginUserForm,
|
||||||
frontend::{api::CLIENT, app::GlobalState, components::credentials::*},
|
frontend::{api::CLIENT, app::site, components::credentials::*},
|
||||||
};
|
};
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::Redirect;
|
use leptos_router::Redirect;
|
||||||
|
@ -20,9 +20,8 @@ pub fn Login() -> impl IntoView {
|
||||||
let result = CLIENT.login(credentials).await;
|
let result = CLIENT.login(credentials).await;
|
||||||
set_wait_for_response.update(|w| *w = false);
|
set_wait_for_response.update(|w| *w = false);
|
||||||
match result {
|
match result {
|
||||||
Ok(res) => {
|
Ok(_res) => {
|
||||||
expect_context::<RwSignal<GlobalState>>()
|
site().refetch();
|
||||||
.update(|state| state.my_profile = Some(res));
|
|
||||||
set_login_response.update(|v| *v = Some(()));
|
set_login_response.update(|v| *v = Some(()));
|
||||||
set_login_error.update(|e| *e = None);
|
set_login_error.update(|e| *e = None);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use leptos::*;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Notifications() -> impl IntoView {
|
pub fn Notifications() -> impl IntoView {
|
||||||
let notifications = create_local_resource(
|
let notifications = create_resource(
|
||||||
move || {},
|
move || {},
|
||||||
|_| async move { CLIENT.notifications_list().await.unwrap() },
|
|_| async move { CLIENT.notifications_list().await.unwrap() },
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{LocalUserView, RegisterUserForm},
|
common::{LocalUserView, RegisterUserForm},
|
||||||
frontend::{api::CLIENT, app::GlobalState, components::credentials::*, error::MyResult},
|
frontend::{api::CLIENT, app::site, components::credentials::*, error::MyResult},
|
||||||
};
|
};
|
||||||
use leptos::{logging::log, *};
|
use leptos::{logging::log, *};
|
||||||
|
|
||||||
|
@ -20,9 +20,8 @@ pub fn Register() -> impl IntoView {
|
||||||
let result: MyResult<LocalUserView> = CLIENT.register(credentials).await;
|
let result: MyResult<LocalUserView> = CLIENT.register(credentials).await;
|
||||||
set_wait_for_response.update(|w| *w = false);
|
set_wait_for_response.update(|w| *w = false);
|
||||||
match result {
|
match result {
|
||||||
Ok(res) => {
|
Ok(_res) => {
|
||||||
expect_context::<RwSignal<GlobalState>>()
|
site().refetch();
|
||||||
.update(|state| state.my_profile = Some(res));
|
|
||||||
set_register_response.update(|v| *v = Some(()));
|
set_register_response.update(|v| *v = Some(()));
|
||||||
set_register_error.update(|e| *e = None);
|
set_register_error.update(|e| *e = None);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use ibis::{
|
||||||
config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation},
|
config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation},
|
||||||
start,
|
start,
|
||||||
},
|
},
|
||||||
common::RegisterUserForm,
|
common::{RegisterUserForm, SharedConfig},
|
||||||
frontend::{api::ApiClient, error::MyResult},
|
frontend::{api::ApiClient, error::MyResult},
|
||||||
};
|
};
|
||||||
use reqwest::ClientBuilder;
|
use reqwest::ClientBuilder;
|
||||||
|
@ -124,12 +124,14 @@ impl IbisInstance {
|
||||||
connection_url,
|
connection_url,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
registration_open: true,
|
|
||||||
federation: IbisConfigFederation {
|
federation: IbisConfigFederation {
|
||||||
domain: domain.clone(),
|
domain: domain.clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
config: SharedConfig {
|
||||||
|
registration_open: true,
|
||||||
article_approval,
|
article_approval,
|
||||||
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let client = ClientBuilder::new().cookie_store(true).build().unwrap();
|
let client = ClientBuilder::new().cookie_store(true).build().unwrap();
|
||||||
|
|
|
@ -112,7 +112,7 @@ async fn test_follow_instance() -> MyResult<()> {
|
||||||
let data = TestData::start(false).await;
|
let data = TestData::start(false).await;
|
||||||
|
|
||||||
// check initial state
|
// check initial state
|
||||||
let alpha_user = data.alpha.my_profile().await?;
|
let alpha_user = data.alpha.site().await?.my_profile.unwrap();
|
||||||
assert_eq!(0, alpha_user.following.len());
|
assert_eq!(0, alpha_user.following.len());
|
||||||
let beta_instance = data.beta.get_local_instance().await?;
|
let beta_instance = data.beta.get_local_instance().await?;
|
||||||
assert_eq!(0, beta_instance.followers.len());
|
assert_eq!(0, beta_instance.followers.len());
|
||||||
|
@ -122,7 +122,7 @@ async fn test_follow_instance() -> MyResult<()> {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// check that follow was federated
|
// check that follow was federated
|
||||||
let alpha_user = data.alpha.my_profile().await?;
|
let alpha_user = data.alpha.site().await?.my_profile.unwrap();
|
||||||
assert_eq!(1, alpha_user.following.len());
|
assert_eq!(1, alpha_user.following.len());
|
||||||
assert_eq!(beta_instance.instance.ap_id, alpha_user.following[0].ap_id);
|
assert_eq!(beta_instance.instance.ap_id, alpha_user.following[0].ap_id);
|
||||||
|
|
||||||
|
@ -610,13 +610,13 @@ async fn test_user_registration_login() -> MyResult<()> {
|
||||||
};
|
};
|
||||||
data.alpha.login(login_data).await?;
|
data.alpha.login(login_data).await?;
|
||||||
|
|
||||||
let my_profile = data.alpha.my_profile().await?;
|
let my_profile = data.alpha.site().await?.my_profile.unwrap();
|
||||||
assert_eq!(username, my_profile.person.username);
|
assert_eq!(username, my_profile.person.username);
|
||||||
|
|
||||||
data.alpha.logout().await?;
|
data.alpha.logout().await?;
|
||||||
|
|
||||||
let my_profile_after_logout = data.alpha.my_profile().await;
|
let my_profile_after_logout = data.alpha.site().await?.my_profile;
|
||||||
assert!(my_profile_after_logout.is_err());
|
assert!(my_profile_after_logout.is_none());
|
||||||
|
|
||||||
data.stop()
|
data.stop()
|
||||||
}
|
}
|
||||||
|
@ -635,7 +635,7 @@ async fn test_user_profile() -> MyResult<()> {
|
||||||
data.beta
|
data.beta
|
||||||
.resolve_article(create_res.article.ap_id.into_inner())
|
.resolve_article(create_res.article.ap_id.into_inner())
|
||||||
.await?;
|
.await?;
|
||||||
let domain = extract_domain(&data.alpha.my_profile().await?.person.ap_id);
|
let domain = extract_domain(&data.alpha.site().await?.my_profile.unwrap().person.ap_id);
|
||||||
|
|
||||||
// Now we can fetch the remote user from local api
|
// Now we can fetch the remote user from local api
|
||||||
let params = GetUserForm {
|
let params = GetUserForm {
|
||||||
|
|
Loading…
Reference in a new issue