mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-25 17:41:08 +00:00
Add follow instance button in article nav (fixes #31)
This commit is contained in:
parent
3a8560ce37
commit
01c0175f4c
11 changed files with 92 additions and 42 deletions
|
@ -1,17 +1,18 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::{database::IbisData, error::MyResult, federation::activities::follow::Follow},
|
backend::{database::IbisData, error::MyResult, federation::activities::follow::Follow},
|
||||||
common::{DbInstance, FollowInstance, InstanceView, LocalUserView, ResolveObject},
|
common::{DbInstance, FollowInstance, GetInstance, InstanceView, LocalUserView, ResolveObject},
|
||||||
};
|
};
|
||||||
use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
|
use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
|
||||||
use axum::{extract::Query, Extension, Form, Json};
|
use axum::{extract::Query, Extension, Form, Json};
|
||||||
use axum_macros::debug_handler;
|
use axum_macros::debug_handler;
|
||||||
|
|
||||||
/// Retrieve the local instance info.
|
/// Retrieve details about an instance. If no id is provided, return local instance.
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub(in crate::backend::api) async fn get_local_instance(
|
pub(in crate::backend::api) async fn get_instance(
|
||||||
data: Data<IbisData>,
|
data: Data<IbisData>,
|
||||||
|
Form(query): Form<GetInstance>,
|
||||||
) -> MyResult<Json<InstanceView>> {
|
) -> MyResult<Json<InstanceView>> {
|
||||||
let local_instance = DbInstance::read_local_view(&data)?;
|
let local_instance = DbInstance::read_view(query.id, &data)?;
|
||||||
Ok(Json(local_instance))
|
Ok(Json(local_instance))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ use crate::{
|
||||||
resolve_article,
|
resolve_article,
|
||||||
search_article,
|
search_article,
|
||||||
},
|
},
|
||||||
instance::{follow_instance, get_local_instance, resolve_instance},
|
instance::{follow_instance, get_instance, resolve_instance},
|
||||||
user::{
|
user::{
|
||||||
get_user,
|
get_user,
|
||||||
login_user,
|
login_user,
|
||||||
|
@ -56,7 +56,7 @@ pub fn api_routes() -> Router {
|
||||||
.route("/article/resolve", get(resolve_article))
|
.route("/article/resolve", get(resolve_article))
|
||||||
.route("/article/protect", post(protect_article))
|
.route("/article/protect", post(protect_article))
|
||||||
.route("/edit_conflicts", get(edit_conflicts))
|
.route("/edit_conflicts", get(edit_conflicts))
|
||||||
.route("/instance", get(get_local_instance))
|
.route("/instance", get(get_instance))
|
||||||
.route("/instance/follow", post(follow_instance))
|
.route("/instance/follow", post(follow_instance))
|
||||||
.route("/instance/resolve", get(resolve_instance))
|
.route("/instance/resolve", get(resolve_instance))
|
||||||
.route("/search", get(search_article))
|
.route("/search", get(search_article))
|
||||||
|
|
|
@ -72,8 +72,11 @@ impl DbInstance {
|
||||||
.get_result(conn.deref_mut())?)
|
.get_result(conn.deref_mut())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_local_view(data: &Data<IbisData>) -> MyResult<InstanceView> {
|
pub fn read_view(id: Option<i32>, data: &Data<IbisData>) -> MyResult<InstanceView> {
|
||||||
let instance = DbInstance::read_local_instance(data)?;
|
let instance = match id {
|
||||||
|
Some(id) => DbInstance::read(id, data),
|
||||||
|
None => DbInstance::read_local_instance(data),
|
||||||
|
}?;
|
||||||
let followers = DbInstance::read_followers(instance.id, data)?;
|
let followers = DbInstance::read_followers(instance.id, data)?;
|
||||||
|
|
||||||
Ok(InstanceView {
|
Ok(InstanceView {
|
||||||
|
|
|
@ -210,6 +210,11 @@ pub struct ForkArticleForm {
|
||||||
pub new_title: String,
|
pub new_title: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub struct GetInstance {
|
||||||
|
pub id: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
pub struct FollowInstance {
|
pub struct FollowInstance {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
|
|
|
@ -11,6 +11,7 @@ use crate::{
|
||||||
FollowInstance,
|
FollowInstance,
|
||||||
ForkArticleForm,
|
ForkArticleForm,
|
||||||
GetArticleForm,
|
GetArticleForm,
|
||||||
|
GetInstance,
|
||||||
GetUserForm,
|
GetUserForm,
|
||||||
InstanceView,
|
InstanceView,
|
||||||
ListArticlesForm,
|
ListArticlesForm,
|
||||||
|
@ -138,6 +139,10 @@ impl ApiClient {
|
||||||
self.get_query("/api/v1/instance", None::<i32>).await
|
self.get_query("/api/v1/instance", None::<i32>).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_instance(&self, get_form: &GetInstance) -> MyResult<InstanceView> {
|
||||||
|
self.get_query("/api/v1/instance", Some(get_form)).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn follow_instance_with_resolve(
|
pub async fn follow_instance_with_resolve(
|
||||||
&self,
|
&self,
|
||||||
follow_instance: &str,
|
follow_instance: &str,
|
||||||
|
|
|
@ -81,13 +81,13 @@ impl GlobalState {
|
||||||
#[component]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
provide_meta_context();
|
provide_meta_context();
|
||||||
let backend_hostname = GlobalState {
|
let global_state = GlobalState {
|
||||||
api_client: ApiClient::new(Client::new(), None),
|
api_client: ApiClient::new(Client::new(), None),
|
||||||
my_profile: None,
|
my_profile: None,
|
||||||
};
|
};
|
||||||
// Load user profile in case we are already logged in
|
// Load user profile in case we are already logged in
|
||||||
GlobalState::update_my_profile();
|
GlobalState::update_my_profile();
|
||||||
provide_context(create_rw_signal(backend_hostname));
|
provide_context(create_rw_signal(global_state));
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,31 +1,47 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{validation::can_edit_article, ArticleView},
|
common::{validation::can_edit_article, ArticleView, GetInstance},
|
||||||
frontend::{app::GlobalState, article_link},
|
frontend::{
|
||||||
|
app::GlobalState,
|
||||||
|
article_link,
|
||||||
|
components::instance_follow_button::InstanceFollowButton,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::*;
|
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 {
|
||||||
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
|
|
||||||
view! {
|
view! {
|
||||||
<Suspense>
|
<Suspense>
|
||||||
{move || article.get().map(|article| {
|
{move || article.get().map(|article_| {
|
||||||
let article_link = article_link(&article.article);
|
let instance = create_local_resource(move || article_.article.instance_id, move |instance_id| async move {
|
||||||
|
let form = GetInstance {
|
||||||
|
id: Some(instance_id)
|
||||||
|
};
|
||||||
|
GlobalState::api_client()
|
||||||
|
.get_instance(&form)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
|
||||||
|
let article_link = article_link(&article_.article);
|
||||||
let article_link_ = article_link.clone();
|
let 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 || global_state.with(|state| {
|
<Show when=move || global_state.with(|state| {
|
||||||
let is_admin = state.my_profile.as_ref().map(|p| p.local_user.admin).unwrap_or(false);
|
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()
|
state.my_profile.is_some() && can_edit_article(&article_.article, is_admin).is_ok()
|
||||||
})>
|
})>
|
||||||
<A href={format!("{article_link}/edit")}>"Edit"</A>
|
<A href={format!("{article_link}/edit")}>"Edit"</A>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when=move || global_state.with(|state| state.my_profile.is_some())>
|
<Show when=move || global_state.with(|state| state.my_profile.is_some())>
|
||||||
<A href={format!("{article_link_}/actions")}>"Actions"</A>
|
<A href={format!("{article_link_}/actions")}>"Actions"</A>
|
||||||
|
{instance.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">"Protected"</span>
|
<span title="Article can only be edited by local admins">"Protected"</span>
|
||||||
|
|
39
src/frontend/components/instance_follow_button.rs
Normal file
39
src/frontend/components/instance_follow_button.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
use crate::{
|
||||||
|
common::{DbInstance, FollowInstance},
|
||||||
|
frontend::app::GlobalState,
|
||||||
|
};
|
||||||
|
use leptos::{component, *};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn InstanceFollowButton(instance: DbInstance) -> impl IntoView {
|
||||||
|
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
|
||||||
|
let follow_action = create_action(move |instance_id: &i32| {
|
||||||
|
let instance_id = *instance_id;
|
||||||
|
async move {
|
||||||
|
let form = FollowInstance { id: instance_id };
|
||||||
|
GlobalState::api_client()
|
||||||
|
.follow_instance(form)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
GlobalState::update_my_profile();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let is_following = global_state
|
||||||
|
.get_untracked()
|
||||||
|
.my_profile
|
||||||
|
.map(|p| p.following.contains(&instance))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let follow_text = if is_following {
|
||||||
|
"Following instance"
|
||||||
|
} else {
|
||||||
|
"Follow instance"
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<button on:click=move |_| follow_action.dispatch(instance.id)
|
||||||
|
prop:disabled=move || is_following
|
||||||
|
prop:hidden=move || instance.local>
|
||||||
|
{follow_text}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod article_nav;
|
pub mod article_nav;
|
||||||
pub(crate) mod credentials;
|
pub mod credentials;
|
||||||
|
pub mod instance_follow_button;
|
||||||
pub mod nav;
|
pub mod nav;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use crate::frontend::{
|
use crate::frontend::{
|
||||||
article_title,
|
article_title,
|
||||||
components::article_nav::ArticleNav,
|
components::article_nav::ArticleNav,
|
||||||
|
extract_domain,
|
||||||
pages::article_resource,
|
pages::article_resource,
|
||||||
user_link,
|
user_link,
|
||||||
};
|
};
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use crate::frontend::extract_domain;
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ArticleHistory() -> impl IntoView {
|
pub fn ArticleHistory() -> impl IntoView {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{utils::http_protocol_str, DbInstance, FollowInstance},
|
common::{utils::http_protocol_str, DbInstance},
|
||||||
frontend::app::GlobalState,
|
frontend::{app::GlobalState, components::instance_follow_button::InstanceFollowButton},
|
||||||
};
|
};
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::use_params_map;
|
use leptos_router::use_params_map;
|
||||||
|
@ -18,36 +18,16 @@ pub fn InstanceDetails() -> impl IntoView {
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
});
|
});
|
||||||
let follow_action = create_action(move |instance_id: &i32| {
|
|
||||||
let instance_id = *instance_id;
|
|
||||||
async move {
|
|
||||||
let form = FollowInstance { id: instance_id };
|
|
||||||
GlobalState::api_client()
|
|
||||||
.follow_instance(form)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
GlobalState::update_my_profile();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Suspense fallback=|| view! { "Loading..." }> {
|
<Suspense fallback=|| view! { "Loading..." }> {
|
||||||
move || instance_profile.get().map(|instance: DbInstance| {
|
move || instance_profile.get().map(|instance: DbInstance| {
|
||||||
let instance_ = instance.clone();
|
let instance_ = instance.clone();
|
||||||
let is_following = global_state.get().my_profile.map(|p| p.following.contains(&instance_)).unwrap_or_default();
|
|
||||||
let follow_text = if is_following {
|
|
||||||
"Following"
|
|
||||||
} else {
|
|
||||||
"Follow"
|
|
||||||
};
|
|
||||||
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 || global_state.with(|state| state.my_profile.is_some())>
|
||||||
<button on:click=move |_| follow_action.dispatch(instance.id)
|
<InstanceFollowButton instance=instance_.clone() />
|
||||||
prop:disabled=move || is_following>
|
|
||||||
{follow_text}
|
|
||||||
</button>
|
|
||||||
</Show>
|
</Show>
|
||||||
<p>Follow the instance so that new edits are federated to your instance.</p>
|
<p>Follow the instance so that new edits are federated to your instance.</p>
|
||||||
<p>"TODO: show a list of articles from the instance. For now you can use the "<a href="/article/list">Article list</a>.</p>
|
<p>"TODO: show a list of articles from the instance. For now you can use the "<a href="/article/list">Article list</a>.</p>
|
||||||
|
|
Loading…
Reference in a new issue