Add follow instance button in article nav (fixes #31)

This commit is contained in:
Felix Ableitner 2024-03-15 13:59:30 +01:00
parent 3a8560ce37
commit 01c0175f4c
11 changed files with 92 additions and 42 deletions

View File

@ -1,17 +1,18 @@
use crate::{
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 axum::{extract::Query, Extension, Form, Json};
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]
pub(in crate::backend::api) async fn get_local_instance(
pub(in crate::backend::api) async fn get_instance(
data: Data<IbisData>,
Form(query): Form<GetInstance>,
) -> MyResult<Json<InstanceView>> {
let local_instance = DbInstance::read_local_view(&data)?;
let local_instance = DbInstance::read_view(query.id, &data)?;
Ok(Json(local_instance))
}

View File

@ -11,7 +11,7 @@ use crate::{
resolve_article,
search_article,
},
instance::{follow_instance, get_local_instance, resolve_instance},
instance::{follow_instance, get_instance, resolve_instance},
user::{
get_user,
login_user,
@ -56,7 +56,7 @@ pub fn api_routes() -> Router {
.route("/article/resolve", get(resolve_article))
.route("/article/protect", post(protect_article))
.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/resolve", get(resolve_instance))
.route("/search", get(search_article))

View File

@ -72,8 +72,11 @@ impl DbInstance {
.get_result(conn.deref_mut())?)
}
pub fn read_local_view(data: &Data<IbisData>) -> MyResult<InstanceView> {
let instance = DbInstance::read_local_instance(data)?;
pub fn read_view(id: Option<i32>, data: &Data<IbisData>) -> MyResult<InstanceView> {
let instance = match id {
Some(id) => DbInstance::read(id, data),
None => DbInstance::read_local_instance(data),
}?;
let followers = DbInstance::read_followers(instance.id, data)?;
Ok(InstanceView {

View File

@ -210,6 +210,11 @@ pub struct ForkArticleForm {
pub new_title: String,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct GetInstance {
pub id: Option<i32>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct FollowInstance {
pub id: i32,

View File

@ -11,6 +11,7 @@ use crate::{
FollowInstance,
ForkArticleForm,
GetArticleForm,
GetInstance,
GetUserForm,
InstanceView,
ListArticlesForm,
@ -138,6 +139,10 @@ impl ApiClient {
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(
&self,
follow_instance: &str,

View File

@ -81,13 +81,13 @@ impl GlobalState {
#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
let backend_hostname = GlobalState {
let global_state = GlobalState {
api_client: ApiClient::new(Client::new(), None),
my_profile: None,
};
// Load user profile in case we are already logged in
GlobalState::update_my_profile();
provide_context(create_rw_signal(backend_hostname));
provide_context(create_rw_signal(global_state));
view! {
<>

View File

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

View 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>
}
}

View File

@ -1,3 +1,4 @@
pub mod article_nav;
pub(crate) mod credentials;
pub mod credentials;
pub mod instance_follow_button;
pub mod nav;

View File

@ -1,11 +1,11 @@
use crate::frontend::{
article_title,
components::article_nav::ArticleNav,
extract_domain,
pages::article_resource,
user_link,
};
use leptos::*;
use crate::frontend::extract_domain;
#[component]
pub fn ArticleHistory() -> impl IntoView {

View File

@ -1,6 +1,6 @@
use crate::{
common::{utils::http_protocol_str, DbInstance, FollowInstance},
frontend::app::GlobalState,
common::{utils::http_protocol_str, DbInstance},
frontend::{app::GlobalState, components::instance_follow_button::InstanceFollowButton},
};
use leptos::*;
use leptos_router::use_params_map;
@ -18,36 +18,16 @@ pub fn InstanceDetails() -> impl IntoView {
.await
.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! {
<Suspense fallback=|| view! { "Loading..." }> {
move || instance_profile.get().map(|instance: DbInstance| {
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! {
<h1>{instance.domain}</h1>
<Show when=move || global_state.with(|state| state.my_profile.is_some())>
<button on:click=move |_| follow_action.dispatch(instance.id)
prop:disabled=move || is_following>
{follow_text}
</button>
<InstanceFollowButton instance=instance_.clone() />
</Show>
<p>Follow the instance so that new edits are federated to your instance.</p>
<p>"TODO: show a list of articles from the instance. For now you can use the "<a href="/article/list">Article list</a>.</p>