mirror of
https://github.com/Nutomic/ibis.git
synced 2025-02-09 07:24:41 +00:00
Proper frontend error handling (#116)
* Rename to BackendError * wip * suspense error * error handlign for read article * error handling for discussion * diff/history (with hydration error) * actions * edit * article pages done * return error from all api methods * no unwrap in api client * cleanup * article 404 page with create article link * get rid of articles list, rename instances to explore * no unwrap on instance details * more unwrap gone * use bind * more * edits page * instance details * no more unwrap!! (fixes #16) * timeout for error popup * shear
This commit is contained in:
parent
494527eccf
commit
c330061087
88 changed files with 1217 additions and 1116 deletions
|
@ -27,6 +27,7 @@ panic = "abort"
|
|||
[lints.clippy]
|
||||
dbg_macro = "deny"
|
||||
unwrap_used = "deny"
|
||||
todo = "deny"
|
||||
|
||||
# frontend and shared deps
|
||||
[dependencies]
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
|||
},
|
||||
federation::activities::{create_article::CreateArticle, submit_article_update},
|
||||
utils::{
|
||||
error::MyResult,
|
||||
error::BackendResult,
|
||||
generate_article_version,
|
||||
validate::{validate_article_title, validate_not_empty},
|
||||
},
|
||||
|
@ -52,7 +52,7 @@ pub(in crate::backend::api) async fn create_article(
|
|||
user: Extension<LocalUserView>,
|
||||
context: Data<IbisContext>,
|
||||
Form(mut params): Form<CreateArticleParams>,
|
||||
) -> MyResult<Json<DbArticleView>> {
|
||||
) -> BackendResult<Json<DbArticleView>> {
|
||||
params.title = validate_article_title(¶ms.title)?;
|
||||
validate_not_empty(¶ms.text)?;
|
||||
|
||||
|
@ -105,7 +105,7 @@ pub(in crate::backend::api) async fn edit_article(
|
|||
Extension(user): Extension<LocalUserView>,
|
||||
context: Data<IbisContext>,
|
||||
Form(mut params): Form<EditArticleParams>,
|
||||
) -> MyResult<Json<Option<ApiConflict>>> {
|
||||
) -> BackendResult<Json<Option<ApiConflict>>> {
|
||||
validate_not_empty(¶ms.new_text)?;
|
||||
// resolve conflict if any
|
||||
if let Some(resolve_conflict_id) = params.resolve_conflict_id {
|
||||
|
@ -169,7 +169,7 @@ pub(in crate::backend::api) async fn edit_article(
|
|||
pub(in crate::backend::api) async fn get_article(
|
||||
Query(query): Query<GetArticleParams>,
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<Json<DbArticleView>> {
|
||||
) -> BackendResult<Json<DbArticleView>> {
|
||||
match (query.title, query.id) {
|
||||
(Some(title), None) => Ok(Json(DbArticle::read_view_title(
|
||||
&title,
|
||||
|
@ -191,7 +191,7 @@ pub(in crate::backend::api) async fn get_article(
|
|||
pub(in crate::backend::api) async fn list_articles(
|
||||
Query(query): Query<ListArticlesParams>,
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<Json<Vec<DbArticle>>> {
|
||||
) -> BackendResult<Json<Vec<DbArticle>>> {
|
||||
Ok(Json(DbArticle::read_all(
|
||||
query.only_local,
|
||||
query.instance_id,
|
||||
|
@ -206,7 +206,7 @@ pub(in crate::backend::api) async fn fork_article(
|
|||
Extension(_user): Extension<LocalUserView>,
|
||||
context: Data<IbisContext>,
|
||||
Form(mut params): Form<ForkArticleParams>,
|
||||
) -> MyResult<Json<DbArticleView>> {
|
||||
) -> BackendResult<Json<DbArticleView>> {
|
||||
// TODO: lots of code duplicated from create_article(), can move it into helper
|
||||
let original_article = DbArticle::read_view(params.article_id, &context)?;
|
||||
params.new_title = validate_article_title(¶ms.new_title)?;
|
||||
|
@ -260,7 +260,7 @@ pub(in crate::backend::api) async fn fork_article(
|
|||
pub(super) async fn resolve_article(
|
||||
Query(query): Query<ResolveObjectParams>,
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<Json<DbArticleView>> {
|
||||
) -> BackendResult<Json<DbArticleView>> {
|
||||
let article: DbArticle = ObjectId::from(query.id).dereference(&context).await?;
|
||||
let instance = DbInstance::read(article.instance_id, &context)?;
|
||||
let comments = DbComment::read_for_article(article.id, &context)?;
|
||||
|
@ -278,7 +278,7 @@ pub(super) async fn resolve_article(
|
|||
pub(super) async fn search_article(
|
||||
Query(query): Query<SearchArticleParams>,
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<Json<Vec<DbArticle>>> {
|
||||
) -> BackendResult<Json<Vec<DbArticle>>> {
|
||||
if query.query.is_empty() {
|
||||
return Err(anyhow!("Query is empty").into());
|
||||
}
|
||||
|
@ -291,7 +291,7 @@ pub(in crate::backend::api) async fn protect_article(
|
|||
Extension(user): Extension<LocalUserView>,
|
||||
context: Data<IbisContext>,
|
||||
Form(params): Form<ProtectArticleParams>,
|
||||
) -> MyResult<Json<DbArticle>> {
|
||||
) -> BackendResult<Json<DbArticle>> {
|
||||
check_is_admin(&user)?;
|
||||
let article = DbArticle::update_protected(params.article_id, params.protected, &context)?;
|
||||
Ok(Json(article))
|
||||
|
@ -303,7 +303,7 @@ pub async fn approve_article(
|
|||
Extension(user): Extension<LocalUserView>,
|
||||
context: Data<IbisContext>,
|
||||
Form(params): Form<ApproveArticleParams>,
|
||||
) -> MyResult<Json<()>> {
|
||||
) -> BackendResult<Json<()>> {
|
||||
check_is_admin(&user)?;
|
||||
if params.approve {
|
||||
DbArticle::update_approved(params.article_id, true, &context)?;
|
||||
|
@ -319,7 +319,7 @@ pub async fn delete_conflict(
|
|||
Extension(user): Extension<LocalUserView>,
|
||||
context: Data<IbisContext>,
|
||||
Form(params): Form<DeleteConflictParams>,
|
||||
) -> MyResult<Json<()>> {
|
||||
) -> BackendResult<Json<()>> {
|
||||
DbConflict::delete(params.conflict_id, user.person.id, &context)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::{
|
|||
undo_delete_comment::UndoDeleteComment,
|
||||
},
|
||||
utils::{
|
||||
error::MyResult,
|
||||
error::BackendResult,
|
||||
validate::{validate_comment_max_depth, validate_not_empty},
|
||||
},
|
||||
},
|
||||
|
@ -31,7 +31,7 @@ pub(in crate::backend::api) async fn create_comment(
|
|||
user: Extension<LocalUserView>,
|
||||
context: Data<IbisContext>,
|
||||
Form(params): Form<CreateCommentParams>,
|
||||
) -> MyResult<Json<DbCommentView>> {
|
||||
) -> BackendResult<Json<DbCommentView>> {
|
||||
validate_not_empty(¶ms.content)?;
|
||||
let mut depth = 0;
|
||||
if let Some(parent_id) = params.parent_id {
|
||||
|
@ -78,7 +78,7 @@ pub(in crate::backend::api) async fn edit_comment(
|
|||
user: Extension<LocalUserView>,
|
||||
context: Data<IbisContext>,
|
||||
Form(params): Form<EditCommentParams>,
|
||||
) -> MyResult<Json<DbCommentView>> {
|
||||
) -> BackendResult<Json<DbCommentView>> {
|
||||
if let Some(content) = ¶ms.content {
|
||||
validate_not_empty(content)?;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
backend::{
|
||||
database::{instance::DbInstanceUpdateForm, IbisContext},
|
||||
federation::activities::follow::Follow,
|
||||
utils::error::MyResult,
|
||||
utils::error::BackendResult,
|
||||
},
|
||||
common::{
|
||||
instance::{
|
||||
|
@ -27,7 +27,7 @@ use axum_macros::debug_handler;
|
|||
pub(in crate::backend::api) async fn get_instance(
|
||||
context: Data<IbisContext>,
|
||||
Form(params): Form<GetInstanceParams>,
|
||||
) -> MyResult<Json<InstanceView>> {
|
||||
) -> BackendResult<Json<InstanceView>> {
|
||||
let local_instance = DbInstance::read_view(params.id, &context)?;
|
||||
Ok(Json(local_instance))
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ pub(in crate::backend::api) async fn get_instance(
|
|||
pub(in crate::backend::api) async fn update_instance(
|
||||
context: Data<IbisContext>,
|
||||
Form(mut params): Form<UpdateInstanceParams>,
|
||||
) -> MyResult<Json<DbInstance>> {
|
||||
) -> BackendResult<Json<DbInstance>> {
|
||||
empty_to_none(&mut params.name);
|
||||
empty_to_none(&mut params.topic);
|
||||
let form = DbInstanceUpdateForm {
|
||||
|
@ -52,7 +52,7 @@ pub(in crate::backend::api) async fn follow_instance(
|
|||
Extension(user): Extension<LocalUserView>,
|
||||
context: Data<IbisContext>,
|
||||
Form(params): Form<FollowInstanceParams>,
|
||||
) -> MyResult<Json<SuccessResponse>> {
|
||||
) -> BackendResult<Json<SuccessResponse>> {
|
||||
let target = DbInstance::read(params.id, &context)?;
|
||||
let pending = !target.local;
|
||||
DbInstance::follow(&user.person, &target, pending, &context)?;
|
||||
|
@ -67,7 +67,7 @@ pub(in crate::backend::api) async fn follow_instance(
|
|||
pub(super) async fn resolve_instance(
|
||||
Query(params): Query<ResolveObjectParams>,
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<Json<DbInstance>> {
|
||||
) -> BackendResult<Json<DbInstance>> {
|
||||
let instance: DbInstance = ObjectId::from(params.id).dereference(&context).await?;
|
||||
Ok(Json(instance))
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ pub(super) async fn resolve_instance(
|
|||
#[debug_handler]
|
||||
pub(in crate::backend::api) async fn list_instances(
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<Json<Vec<DbInstance>>> {
|
||||
) -> BackendResult<Json<Vec<DbInstance>>> {
|
||||
let instances = DbInstance::list(false, &context)?;
|
||||
Ok(Json(instances))
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ use crate::{
|
|||
user::{get_user, login_user, logout_user, register_user},
|
||||
},
|
||||
database::IbisContext,
|
||||
utils::error::MyResult,
|
||||
utils::error::BackendResult,
|
||||
},
|
||||
common::{
|
||||
article::{DbEdit, EditView, GetEditList},
|
||||
|
@ -75,7 +75,7 @@ pub fn api_routes() -> Router<()> {
|
|||
.route("/site", get(site_view))
|
||||
}
|
||||
|
||||
fn check_is_admin(user: &LocalUserView) -> MyResult<()> {
|
||||
fn check_is_admin(user: &LocalUserView) -> BackendResult<()> {
|
||||
if !user.local_user.admin {
|
||||
return Err(anyhow!("Only admin can perform this action").into());
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ fn check_is_admin(user: &LocalUserView) -> MyResult<()> {
|
|||
pub(in crate::backend::api) async fn site_view(
|
||||
context: Data<IbisContext>,
|
||||
user: Option<Extension<LocalUserView>>,
|
||||
) -> MyResult<Json<SiteView>> {
|
||||
) -> BackendResult<Json<SiteView>> {
|
||||
Ok(Json(SiteView {
|
||||
my_profile: user.map(|u| u.0),
|
||||
config: context.config.options.clone(),
|
||||
|
@ -99,7 +99,7 @@ pub async fn edit_list(
|
|||
Query(query): Query<GetEditList>,
|
||||
user: Option<Extension<LocalUserView>>,
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<Json<Vec<EditView>>> {
|
||||
) -> BackendResult<Json<Vec<EditView>>> {
|
||||
let params = if let Some(article_id) = query.article_id {
|
||||
ViewEditParams::ArticleId(article_id)
|
||||
} else if let Some(person_id) = query.person_id {
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
backend::{
|
||||
database::{conflict::DbConflict, read_jwt_secret, IbisContext},
|
||||
utils::{
|
||||
error::MyResult,
|
||||
error::BackendResult,
|
||||
validate::{validate_display_name, validate_user_name},
|
||||
},
|
||||
},
|
||||
|
@ -54,7 +54,7 @@ struct Claims {
|
|||
pub exp: u64,
|
||||
}
|
||||
|
||||
fn generate_login_token(person: &DbPerson, context: &Data<IbisContext>) -> MyResult<String> {
|
||||
fn generate_login_token(person: &DbPerson, context: &Data<IbisContext>) -> BackendResult<String> {
|
||||
let hostname = context.domain().to_string();
|
||||
let claims = Claims {
|
||||
sub: person.username.clone(),
|
||||
|
@ -69,7 +69,7 @@ fn generate_login_token(person: &DbPerson, context: &Data<IbisContext>) -> MyRes
|
|||
Ok(jwt)
|
||||
}
|
||||
|
||||
pub async fn validate(jwt: &str, context: &IbisContext) -> MyResult<LocalUserView> {
|
||||
pub async fn validate(jwt: &str, context: &IbisContext) -> BackendResult<LocalUserView> {
|
||||
let validation = Validation::default();
|
||||
let secret = read_jwt_secret(context)?;
|
||||
let key = DecodingKey::from_secret(secret.as_bytes());
|
||||
|
@ -82,7 +82,7 @@ pub(in crate::backend::api) async fn register_user(
|
|||
context: Data<IbisContext>,
|
||||
jar: CookieJar,
|
||||
Form(params): Form<RegisterUserParams>,
|
||||
) -> MyResult<(CookieJar, Json<LocalUserView>)> {
|
||||
) -> BackendResult<(CookieJar, Json<LocalUserView>)> {
|
||||
if !context.config.options.registration_open {
|
||||
return Err(anyhow!("Registration is closed").into());
|
||||
}
|
||||
|
@ -98,7 +98,7 @@ pub(in crate::backend::api) async fn login_user(
|
|||
context: Data<IbisContext>,
|
||||
jar: CookieJar,
|
||||
Form(params): Form<LoginUserParams>,
|
||||
) -> MyResult<(CookieJar, Json<LocalUserView>)> {
|
||||
) -> BackendResult<(CookieJar, Json<LocalUserView>)> {
|
||||
let user = DbPerson::read_local_from_name(¶ms.username, &context)?;
|
||||
let valid = verify(¶ms.password, &user.local_user.password_encrypted)?;
|
||||
if !valid {
|
||||
|
@ -133,7 +133,7 @@ fn create_cookie(jwt: String, context: &Data<IbisContext>) -> Cookie<'static> {
|
|||
pub(in crate::backend::api) async fn logout_user(
|
||||
context: Data<IbisContext>,
|
||||
jar: CookieJar,
|
||||
) -> MyResult<(CookieJar, Json<SuccessResponse>)> {
|
||||
) -> BackendResult<(CookieJar, Json<SuccessResponse>)> {
|
||||
let jar = jar.remove(create_cookie(String::new(), &context));
|
||||
Ok((jar, Json(SuccessResponse::default())))
|
||||
}
|
||||
|
@ -142,7 +142,7 @@ pub(in crate::backend::api) async fn logout_user(
|
|||
pub(in crate::backend::api) async fn get_user(
|
||||
params: Query<GetUserParams>,
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<Json<DbPerson>> {
|
||||
) -> BackendResult<Json<DbPerson>> {
|
||||
Ok(Json(DbPerson::read_from_name(
|
||||
¶ms.name,
|
||||
¶ms.domain,
|
||||
|
@ -154,7 +154,7 @@ pub(in crate::backend::api) async fn get_user(
|
|||
pub(in crate::backend::api) async fn update_user_profile(
|
||||
context: Data<IbisContext>,
|
||||
Form(mut params): Form<UpdateUserParams>,
|
||||
) -> MyResult<Json<SuccessResponse>> {
|
||||
) -> BackendResult<Json<SuccessResponse>> {
|
||||
empty_to_none(&mut params.display_name);
|
||||
empty_to_none(&mut params.bio);
|
||||
validate_display_name(¶ms.display_name)?;
|
||||
|
@ -166,7 +166,7 @@ pub(in crate::backend::api) async fn update_user_profile(
|
|||
pub(crate) async fn list_notifications(
|
||||
Extension(user): Extension<LocalUserView>,
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<Json<Vec<Notification>>> {
|
||||
) -> BackendResult<Json<Vec<Notification>>> {
|
||||
let conflicts = DbConflict::list(&user.person, &context)?;
|
||||
let conflicts: Vec<_> = try_join_all(conflicts.into_iter().map(|c| {
|
||||
let data = context.reset_request_count();
|
||||
|
@ -194,16 +194,20 @@ pub(crate) async fn list_notifications(
|
|||
|
||||
#[debug_handler]
|
||||
pub(crate) async fn count_notifications(
|
||||
Extension(user): Extension<LocalUserView>,
|
||||
user: Option<Extension<LocalUserView>>,
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<Json<usize>> {
|
||||
let mut count = 0;
|
||||
let conflicts = DbConflict::list(&user.person, &context)?;
|
||||
count += conflicts.len();
|
||||
if check_is_admin(&user).is_ok() {
|
||||
let articles = DbArticle::list_approval_required(&context)?;
|
||||
count += articles.len();
|
||||
}
|
||||
) -> BackendResult<Json<usize>> {
|
||||
if let Some(user) = user {
|
||||
let mut count = 0;
|
||||
let conflicts = DbConflict::list(&user.person, &context)?;
|
||||
count += conflicts.len();
|
||||
if check_is_admin(&user).is_ok() {
|
||||
let articles = DbArticle::list_approval_required(&context)?;
|
||||
count += articles.len();
|
||||
}
|
||||
|
||||
Ok(Json(count))
|
||||
Ok(Json(count))
|
||||
} else {
|
||||
Ok(Json(0))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
|||
IbisContext,
|
||||
},
|
||||
federation::objects::edits_collection::DbEditCollection,
|
||||
utils::error::MyResult,
|
||||
utils::error::BackendResult,
|
||||
},
|
||||
common::{
|
||||
article::{DbArticle, DbArticleView, EditVersion},
|
||||
|
@ -42,18 +42,18 @@ pub struct DbArticleForm {
|
|||
|
||||
// TODO: get rid of unnecessary methods
|
||||
impl DbArticle {
|
||||
pub fn edits_id(&self) -> MyResult<CollectionId<DbEditCollection>> {
|
||||
pub fn edits_id(&self) -> BackendResult<CollectionId<DbEditCollection>> {
|
||||
Ok(CollectionId::parse(&format!("{}/edits", self.ap_id))?)
|
||||
}
|
||||
|
||||
pub fn create(form: DbArticleForm, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn create(form: DbArticleForm, context: &IbisContext) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(insert_into(article::table)
|
||||
.values(form)
|
||||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn create_or_update(form: DbArticleForm, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn create_or_update(form: DbArticleForm, context: &IbisContext) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(insert_into(article::table)
|
||||
.values(&form)
|
||||
|
@ -63,40 +63,48 @@ impl DbArticle {
|
|||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn update_text(id: ArticleId, text: &str, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn update_text(id: ArticleId, text: &str, context: &IbisContext) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(diesel::update(article::dsl::article.find(id))
|
||||
.set(article::dsl::text.eq(text))
|
||||
.get_result::<Self>(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn update_protected(id: ArticleId, locked: bool, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn update_protected(
|
||||
id: ArticleId,
|
||||
locked: bool,
|
||||
context: &IbisContext,
|
||||
) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(diesel::update(article::dsl::article.find(id))
|
||||
.set(article::dsl::protected.eq(locked))
|
||||
.get_result::<Self>(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn update_approved(id: ArticleId, approved: bool, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn update_approved(
|
||||
id: ArticleId,
|
||||
approved: bool,
|
||||
context: &IbisContext,
|
||||
) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(diesel::update(article::dsl::article.find(id))
|
||||
.set(article::dsl::approved.eq(approved))
|
||||
.get_result::<Self>(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn delete(id: ArticleId, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn delete(id: ArticleId, context: &IbisContext) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(diesel::delete(article::dsl::article.find(id)).get_result::<Self>(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn read(id: ArticleId, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn read(id: ArticleId, context: &IbisContext) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(article::table
|
||||
.find(id)
|
||||
.get_result::<Self>(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn read_view(id: ArticleId, context: &IbisContext) -> MyResult<DbArticleView> {
|
||||
pub fn read_view(id: ArticleId, context: &IbisContext) -> BackendResult<DbArticleView> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
let query = article::table
|
||||
.find(id)
|
||||
|
@ -117,7 +125,7 @@ impl DbArticle {
|
|||
title: &str,
|
||||
domain: Option<String>,
|
||||
context: &IbisContext,
|
||||
) -> MyResult<DbArticleView> {
|
||||
) -> BackendResult<DbArticleView> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
let (article, instance): (DbArticle, DbInstance) = {
|
||||
let query = article::table
|
||||
|
@ -141,7 +149,10 @@ impl DbArticle {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn read_from_ap_id(ap_id: &ObjectId<DbArticle>, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn read_from_ap_id(
|
||||
ap_id: &ObjectId<DbArticle>,
|
||||
context: &IbisContext,
|
||||
) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(article::table
|
||||
.filter(article::dsl::ap_id.eq(ap_id))
|
||||
|
@ -155,7 +166,7 @@ impl DbArticle {
|
|||
only_local: Option<bool>,
|
||||
instance_id: Option<InstanceId>,
|
||||
context: &IbisContext,
|
||||
) -> MyResult<Vec<Self>> {
|
||||
) -> BackendResult<Vec<Self>> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
let mut query = article::table
|
||||
.inner_join(edit::table)
|
||||
|
@ -175,7 +186,7 @@ impl DbArticle {
|
|||
Ok(query.get_results(&mut conn)?)
|
||||
}
|
||||
|
||||
pub fn search(query: &str, context: &IbisContext) -> MyResult<Vec<Self>> {
|
||||
pub fn search(query: &str, context: &IbisContext) -> BackendResult<Vec<Self>> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
let replaced = query
|
||||
.replace('%', "\\%")
|
||||
|
@ -191,7 +202,7 @@ impl DbArticle {
|
|||
.get_results(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn latest_edit_version(&self, context: &IbisContext) -> MyResult<EditVersion> {
|
||||
pub fn latest_edit_version(&self, context: &IbisContext) -> BackendResult<EditVersion> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
let latest_version: Option<EditVersion> = edit::table
|
||||
.filter(edit::dsl::article_id.eq(self.id))
|
||||
|
@ -206,7 +217,7 @@ impl DbArticle {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn list_approval_required(context: &IbisContext) -> MyResult<Vec<Self>> {
|
||||
pub fn list_approval_required(context: &IbisContext) -> BackendResult<Vec<Self>> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
let query = article::table
|
||||
.group_by(article::dsl::id)
|
||||
|
|
|
@ -3,7 +3,7 @@ use super::{
|
|||
IbisContext,
|
||||
};
|
||||
use crate::{
|
||||
backend::utils::error::MyResult,
|
||||
backend::utils::error::BackendResult,
|
||||
common::{
|
||||
comment::{DbComment, DbCommentView},
|
||||
newtypes::{ArticleId, CommentId, PersonId},
|
||||
|
@ -48,7 +48,7 @@ pub struct DbCommentUpdateForm {
|
|||
}
|
||||
|
||||
impl DbComment {
|
||||
pub fn create(form: DbCommentInsertForm, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn create(form: DbCommentInsertForm, context: &IbisContext) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(insert_into(comment::table)
|
||||
.values(form)
|
||||
|
@ -59,7 +59,7 @@ impl DbComment {
|
|||
form: DbCommentUpdateForm,
|
||||
id: CommentId,
|
||||
context: &IbisContext,
|
||||
) -> MyResult<DbCommentView> {
|
||||
) -> BackendResult<DbCommentView> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
let comment: DbComment = update(comment::table.find(id))
|
||||
.set(form)
|
||||
|
@ -68,7 +68,10 @@ impl DbComment {
|
|||
Ok(DbCommentView { comment, creator })
|
||||
}
|
||||
|
||||
pub fn create_or_update(form: DbCommentInsertForm, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn create_or_update(
|
||||
form: DbCommentInsertForm,
|
||||
context: &IbisContext,
|
||||
) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(insert_into(comment::table)
|
||||
.values(&form)
|
||||
|
@ -78,14 +81,14 @@ impl DbComment {
|
|||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn read(id: CommentId, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn read(id: CommentId, context: &IbisContext) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(comment::table
|
||||
.find(id)
|
||||
.get_result::<Self>(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn read_view(id: CommentId, context: &IbisContext) -> MyResult<DbCommentView> {
|
||||
pub fn read_view(id: CommentId, context: &IbisContext) -> BackendResult<DbCommentView> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
let comment = comment::table
|
||||
.find(id)
|
||||
|
@ -94,7 +97,10 @@ impl DbComment {
|
|||
Ok(DbCommentView { comment, creator })
|
||||
}
|
||||
|
||||
pub fn read_from_ap_id(ap_id: &ObjectId<DbComment>, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn read_from_ap_id(
|
||||
ap_id: &ObjectId<DbComment>,
|
||||
context: &IbisContext,
|
||||
) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(comment::table
|
||||
.filter(comment::dsl::ap_id.eq(ap_id))
|
||||
|
@ -104,7 +110,7 @@ impl DbComment {
|
|||
pub fn read_for_article(
|
||||
article_id: ArticleId,
|
||||
context: &IbisContext,
|
||||
) -> MyResult<Vec<DbCommentView>> {
|
||||
) -> BackendResult<Vec<DbCommentView>> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
let comments = comment::table
|
||||
.inner_join(person::table)
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
|||
IbisContext,
|
||||
},
|
||||
federation::activities::submit_article_update,
|
||||
utils::{error::MyResult, generate_article_version},
|
||||
utils::{error::BackendResult, generate_article_version},
|
||||
},
|
||||
common::{
|
||||
article::{ApiConflict, DbArticle, DbEdit, EditVersion},
|
||||
|
@ -57,14 +57,14 @@ pub struct DbConflictForm {
|
|||
}
|
||||
|
||||
impl DbConflict {
|
||||
pub fn create(form: &DbConflictForm, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn create(form: &DbConflictForm, context: &IbisContext) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(insert_into(conflict::table)
|
||||
.values(form)
|
||||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn list(person: &DbPerson, context: &IbisContext) -> MyResult<Vec<Self>> {
|
||||
pub fn list(person: &DbPerson, context: &IbisContext) -> BackendResult<Vec<Self>> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(conflict::table
|
||||
.filter(conflict::dsl::creator_id.eq(person.id))
|
||||
|
@ -72,7 +72,11 @@ impl DbConflict {
|
|||
}
|
||||
|
||||
/// Delete merge conflict which was created by specific user
|
||||
pub fn delete(id: ConflictId, creator_id: PersonId, context: &IbisContext) -> MyResult<()> {
|
||||
pub fn delete(
|
||||
id: ConflictId,
|
||||
creator_id: PersonId,
|
||||
context: &IbisContext,
|
||||
) -> BackendResult<()> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
let conflict: Self = delete(
|
||||
conflict::table
|
||||
|
@ -92,7 +96,7 @@ impl DbConflict {
|
|||
pub async fn to_api_conflict(
|
||||
&self,
|
||||
context: &Data<IbisContext>,
|
||||
) -> MyResult<Option<ApiConflict>> {
|
||||
) -> BackendResult<Option<ApiConflict>> {
|
||||
let article = DbArticle::read_view(self.article_id, context)?;
|
||||
// Make sure to get latest version from origin so that all conflicts can be resolved
|
||||
let original_article = article.article.ap_id.dereference_forced(context).await?;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
backend::{
|
||||
database::schema::{article, edit, person},
|
||||
utils::error::MyResult,
|
||||
utils::error::BackendResult,
|
||||
IbisContext,
|
||||
},
|
||||
common::{
|
||||
|
@ -47,7 +47,7 @@ impl DbEditForm {
|
|||
summary: String,
|
||||
previous_version_id: EditVersion,
|
||||
pending: bool,
|
||||
) -> MyResult<Self> {
|
||||
) -> BackendResult<Self> {
|
||||
let diff = create_patch(&original_article.text, updated_text);
|
||||
let version = EditVersion::new(&diff.to_string());
|
||||
let ap_id = Self::generate_ap_id(original_article, &version)?;
|
||||
|
@ -67,7 +67,7 @@ impl DbEditForm {
|
|||
pub fn generate_ap_id(
|
||||
article: &DbArticle,
|
||||
version: &EditVersion,
|
||||
) -> MyResult<ObjectId<DbEdit>> {
|
||||
) -> BackendResult<ObjectId<DbEdit>> {
|
||||
Ok(ObjectId::parse(&format!(
|
||||
"{}/{}",
|
||||
article.ap_id,
|
||||
|
@ -77,7 +77,7 @@ impl DbEditForm {
|
|||
}
|
||||
|
||||
impl DbEdit {
|
||||
pub fn create(form: &DbEditForm, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn create(form: &DbEditForm, context: &IbisContext) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(insert_into(edit::table)
|
||||
.values(form)
|
||||
|
@ -87,21 +87,21 @@ impl DbEdit {
|
|||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn read(version: &EditVersion, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn read(version: &EditVersion, context: &IbisContext) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(edit::table
|
||||
.filter(edit::dsl::hash.eq(version))
|
||||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn read_from_ap_id(ap_id: &ObjectId<DbEdit>, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn read_from_ap_id(ap_id: &ObjectId<DbEdit>, context: &IbisContext) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(edit::table
|
||||
.filter(edit::dsl::ap_id.eq(ap_id))
|
||||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn list_for_article(id: ArticleId, context: &IbisContext) -> MyResult<Vec<Self>> {
|
||||
pub fn list_for_article(id: ArticleId, context: &IbisContext) -> BackendResult<Vec<Self>> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(edit::table
|
||||
.filter(edit::article_id.eq(id))
|
||||
|
@ -113,7 +113,7 @@ impl DbEdit {
|
|||
params: ViewEditParams,
|
||||
user: &Option<LocalUserView>,
|
||||
context: &IbisContext,
|
||||
) -> MyResult<Vec<EditView>> {
|
||||
) -> BackendResult<Vec<EditView>> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
let person_id = user.as_ref().map(|u| u.person.id).unwrap_or(PersonId(-1));
|
||||
let query = edit::table
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::{
|
|||
articles_collection::DbArticleCollection,
|
||||
instance_collection::DbInstanceCollection,
|
||||
},
|
||||
utils::error::MyResult,
|
||||
utils::error::BackendResult,
|
||||
},
|
||||
common::{
|
||||
instance::{DbInstance, InstanceView},
|
||||
|
@ -57,7 +57,7 @@ pub struct DbInstanceUpdateForm {
|
|||
}
|
||||
|
||||
impl DbInstance {
|
||||
pub fn create(form: &DbInstanceForm, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn create(form: &DbInstanceForm, context: &IbisContext) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(insert_into(instance::table)
|
||||
.values(form)
|
||||
|
@ -67,12 +67,12 @@ impl DbInstance {
|
|||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn read(id: InstanceId, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn read(id: InstanceId, context: &IbisContext) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(instance::table.find(id).get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn update(form: DbInstanceUpdateForm, context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn update(form: DbInstanceUpdateForm, context: &IbisContext) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(update(instance::table)
|
||||
.filter(instance::local)
|
||||
|
@ -83,14 +83,14 @@ impl DbInstance {
|
|||
pub fn read_from_ap_id(
|
||||
ap_id: &ObjectId<DbInstance>,
|
||||
context: &Data<IbisContext>,
|
||||
) -> MyResult<DbInstance> {
|
||||
) -> BackendResult<DbInstance> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(instance::table
|
||||
.filter(instance::ap_id.eq(ap_id))
|
||||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn read_local(context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn read_local(context: &IbisContext) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(instance::table
|
||||
.filter(instance::local.eq(true))
|
||||
|
@ -100,7 +100,7 @@ impl DbInstance {
|
|||
pub fn read_view(
|
||||
id: Option<InstanceId>,
|
||||
context: &Data<IbisContext>,
|
||||
) -> MyResult<InstanceView> {
|
||||
) -> BackendResult<InstanceView> {
|
||||
let instance = match id {
|
||||
Some(id) => DbInstance::read(id, context),
|
||||
None => DbInstance::read_local(context),
|
||||
|
@ -118,7 +118,7 @@ impl DbInstance {
|
|||
instance: &DbInstance,
|
||||
pending_: bool,
|
||||
context: &Data<IbisContext>,
|
||||
) -> MyResult<()> {
|
||||
) -> BackendResult<()> {
|
||||
use instance_follow::dsl::{follower_id, instance_id, pending};
|
||||
let mut conn = context.db_pool.get()?;
|
||||
let form = (
|
||||
|
@ -136,7 +136,7 @@ impl DbInstance {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_followers(id_: InstanceId, context: &IbisContext) -> MyResult<Vec<DbPerson>> {
|
||||
pub fn read_followers(id_: InstanceId, context: &IbisContext) -> BackendResult<Vec<DbPerson>> {
|
||||
use crate::backend::database::schema::person;
|
||||
use instance_follow::dsl::{follower_id, instance_id};
|
||||
let mut conn = context.db_pool.get()?;
|
||||
|
@ -147,7 +147,7 @@ impl DbInstance {
|
|||
.get_results(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn list(only_remote: bool, context: &Data<IbisContext>) -> MyResult<Vec<DbInstance>> {
|
||||
pub fn list(only_remote: bool, context: &Data<IbisContext>) -> BackendResult<Vec<DbInstance>> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
let mut query = instance::table.into_boxed();
|
||||
if only_remote {
|
||||
|
@ -161,7 +161,7 @@ impl DbInstance {
|
|||
pub fn read_for_comment(
|
||||
comment_id: CommentId,
|
||||
context: &Data<IbisContext>,
|
||||
) -> MyResult<DbInstance> {
|
||||
) -> BackendResult<DbInstance> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(instance::table
|
||||
.inner_join(article::table)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::schema::instance_stats;
|
||||
use crate::backend::{IbisContext, MyResult};
|
||||
use crate::backend::{BackendResult, IbisContext};
|
||||
use diesel::{query_dsl::methods::FindDsl, Queryable, RunQueryDsl, Selectable};
|
||||
use std::ops::DerefMut;
|
||||
|
||||
|
@ -15,7 +15,7 @@ pub struct InstanceStats {
|
|||
}
|
||||
|
||||
impl InstanceStats {
|
||||
pub fn read(context: &IbisContext) -> MyResult<Self> {
|
||||
pub fn read(context: &IbisContext) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(instance_stats::table.find(1).get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::backend::{
|
||||
database::schema::jwt_secret,
|
||||
utils::{config::IbisConfig, error::MyResult},
|
||||
utils::{config::IbisConfig, error::BackendResult},
|
||||
};
|
||||
use diesel::{
|
||||
r2d2::{ConnectionManager, Pool},
|
||||
|
@ -27,7 +27,7 @@ pub struct IbisContext {
|
|||
pub config: IbisConfig,
|
||||
}
|
||||
|
||||
pub fn read_jwt_secret(context: &IbisContext) -> MyResult<String> {
|
||||
pub fn read_jwt_secret(context: &IbisContext) -> BackendResult<String> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(jwt_secret::table
|
||||
.select(jwt_secret::dsl::secret)
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
|||
schema::{instance, instance_follow, local_user, person},
|
||||
IbisContext,
|
||||
},
|
||||
utils::{error::MyResult, generate_keypair},
|
||||
utils::{error::BackendResult, generate_keypair},
|
||||
},
|
||||
common::{
|
||||
instance::DbInstance,
|
||||
|
@ -51,7 +51,7 @@ pub struct DbPersonForm {
|
|||
}
|
||||
|
||||
impl DbPerson {
|
||||
pub fn create(person_form: &DbPersonForm, context: &Data<IbisContext>) -> MyResult<Self> {
|
||||
pub fn create(person_form: &DbPersonForm, context: &Data<IbisContext>) -> BackendResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(insert_into(person::table)
|
||||
.values(person_form)
|
||||
|
@ -61,7 +61,7 @@ impl DbPerson {
|
|||
.get_result::<DbPerson>(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn read(id: PersonId, context: &IbisContext) -> MyResult<DbPerson> {
|
||||
pub fn read(id: PersonId, context: &IbisContext) -> BackendResult<DbPerson> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(person::table.find(id).get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ impl DbPerson {
|
|||
password: String,
|
||||
admin: bool,
|
||||
context: &IbisContext,
|
||||
) -> MyResult<LocalUserView> {
|
||||
) -> BackendResult<LocalUserView> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
let domain = &context.config.federation.domain;
|
||||
let ap_id = ObjectId::parse(&format!(
|
||||
|
@ -116,7 +116,7 @@ impl DbPerson {
|
|||
pub fn read_from_ap_id(
|
||||
ap_id: &ObjectId<DbPerson>,
|
||||
context: &Data<IbisContext>,
|
||||
) -> MyResult<DbPerson> {
|
||||
) -> BackendResult<DbPerson> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(person::table
|
||||
.filter(person::dsl::ap_id.eq(ap_id))
|
||||
|
@ -127,7 +127,7 @@ impl DbPerson {
|
|||
username: &str,
|
||||
domain: &Option<String>,
|
||||
context: &Data<IbisContext>,
|
||||
) -> MyResult<DbPerson> {
|
||||
) -> BackendResult<DbPerson> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
let mut query = person::table
|
||||
.filter(person::username.eq(username))
|
||||
|
@ -144,7 +144,10 @@ impl DbPerson {
|
|||
Ok(query.get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn update_profile(params: &UpdateUserParams, context: &Data<IbisContext>) -> MyResult<()> {
|
||||
pub fn update_profile(
|
||||
params: &UpdateUserParams,
|
||||
context: &Data<IbisContext>,
|
||||
) -> BackendResult<()> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
diesel::update(person::table.find(params.person_id))
|
||||
.set((
|
||||
|
@ -155,7 +158,10 @@ impl DbPerson {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_local_from_name(username: &str, context: &IbisContext) -> MyResult<LocalUserView> {
|
||||
pub fn read_local_from_name(
|
||||
username: &str,
|
||||
context: &IbisContext,
|
||||
) -> BackendResult<LocalUserView> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
let (person, local_user) = person::table
|
||||
.inner_join(local_user::table)
|
||||
|
@ -171,7 +177,7 @@ impl DbPerson {
|
|||
})
|
||||
}
|
||||
|
||||
fn read_following(id_: PersonId, context: &IbisContext) -> MyResult<Vec<DbInstance>> {
|
||||
fn read_following(id_: PersonId, context: &IbisContext) -> BackendResult<Vec<DbInstance>> {
|
||||
use instance_follow::dsl::{follower_id, instance_id};
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(instance_follow::table
|
||||
|
@ -182,7 +188,7 @@ impl DbPerson {
|
|||
}
|
||||
|
||||
/// Ghost user serves as placeholder for deleted accounts
|
||||
pub fn ghost(context: &Data<IbisContext>) -> MyResult<DbPerson> {
|
||||
pub fn ghost(context: &Data<IbisContext>) -> BackendResult<DbPerson> {
|
||||
let username = "ghost";
|
||||
let read = DbPerson::read_from_name(username, &None, context);
|
||||
if read.is_ok() {
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
database::IbisContext,
|
||||
federation::{activities::follow::Follow, send_activity},
|
||||
utils::{
|
||||
error::{Error, MyResult},
|
||||
error::{BackendError, BackendResult},
|
||||
generate_activity_id,
|
||||
},
|
||||
},
|
||||
|
@ -33,7 +33,7 @@ impl Accept {
|
|||
local_instance: DbInstance,
|
||||
object: Follow,
|
||||
context: &Data<IbisContext>,
|
||||
) -> MyResult<()> {
|
||||
) -> BackendResult<()> {
|
||||
let id = generate_activity_id(context)?;
|
||||
let follower = object.actor.dereference(context).await?;
|
||||
let accept = Accept {
|
||||
|
@ -56,7 +56,7 @@ impl Accept {
|
|||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for Accept {
|
||||
type DataType = IbisContext;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
&self.id
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
database::IbisContext,
|
||||
federation::{routes::AnnouncableActivities, send_activity},
|
||||
utils::{
|
||||
error::{Error, MyResult},
|
||||
error::{BackendError, BackendResult},
|
||||
generate_activity_id,
|
||||
},
|
||||
},
|
||||
|
@ -32,7 +32,10 @@ pub struct AnnounceActivity {
|
|||
}
|
||||
|
||||
impl AnnounceActivity {
|
||||
pub async fn send(object: AnnouncableActivities, context: &Data<IbisContext>) -> MyResult<()> {
|
||||
pub async fn send(
|
||||
object: AnnouncableActivities,
|
||||
context: &Data<IbisContext>,
|
||||
) -> BackendResult<()> {
|
||||
let id = generate_activity_id(context)?;
|
||||
let instance = DbInstance::read_local(context)?;
|
||||
let announce = AnnounceActivity {
|
||||
|
@ -57,7 +60,7 @@ impl AnnounceActivity {
|
|||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for AnnounceActivity {
|
||||
type DataType = IbisContext;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
&self.id
|
||||
|
@ -68,12 +71,12 @@ impl ActivityHandler for AnnounceActivity {
|
|||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn verify(&self, _context: &Data<Self::DataType>) -> MyResult<()> {
|
||||
async fn verify(&self, _context: &Data<Self::DataType>) -> BackendResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn receive(self, context: &Data<Self::DataType>) -> MyResult<()> {
|
||||
async fn receive(self, context: &Data<Self::DataType>) -> BackendResult<()> {
|
||||
self.object.verify(context).await?;
|
||||
self.object.receive(context).await
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::{
|
|||
send_activity_to_instance,
|
||||
},
|
||||
generate_activity_id,
|
||||
utils::error::{Error, MyResult},
|
||||
utils::error::{BackendError, BackendResult},
|
||||
},
|
||||
common::{comment::DbComment, instance::DbInstance, user::DbPerson},
|
||||
};
|
||||
|
@ -40,7 +40,7 @@ pub struct CreateOrUpdateComment {
|
|||
}
|
||||
|
||||
impl CreateOrUpdateComment {
|
||||
pub async fn send(comment: &DbComment, context: &Data<IbisContext>) -> MyResult<()> {
|
||||
pub async fn send(comment: &DbComment, context: &Data<IbisContext>) -> BackendResult<()> {
|
||||
let instance = DbInstance::read_for_comment(comment.id, context)?;
|
||||
|
||||
let kind = if comment.updated.is_none() {
|
||||
|
@ -67,7 +67,7 @@ impl CreateOrUpdateComment {
|
|||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for CreateOrUpdateComment {
|
||||
type DataType = IbisContext;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
&self.id
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
|||
database::{comment::DbCommentUpdateForm, IbisContext},
|
||||
federation::{routes::AnnouncableActivities, send_activity_to_instance},
|
||||
utils::{
|
||||
error::{Error, MyResult},
|
||||
error::{BackendError, BackendResult},
|
||||
generate_activity_id,
|
||||
},
|
||||
},
|
||||
|
@ -39,7 +39,7 @@ impl DeleteComment {
|
|||
creator: &DbPerson,
|
||||
instance: &DbInstance,
|
||||
context: &Data<IbisContext>,
|
||||
) -> MyResult<Self> {
|
||||
) -> BackendResult<Self> {
|
||||
let id = generate_activity_id(context)?;
|
||||
Ok(DeleteComment {
|
||||
actor: creator.ap_id.clone(),
|
||||
|
@ -49,7 +49,7 @@ impl DeleteComment {
|
|||
id,
|
||||
})
|
||||
}
|
||||
pub async fn send(comment: &DbComment, context: &Data<IbisContext>) -> MyResult<()> {
|
||||
pub async fn send(comment: &DbComment, context: &Data<IbisContext>) -> BackendResult<()> {
|
||||
let instance = DbInstance::read_for_comment(comment.id, context)?;
|
||||
let creator = DbPerson::read(comment.creator_id, context)?;
|
||||
let activity = Self::new(comment, &creator, &instance, context)?;
|
||||
|
@ -62,7 +62,7 @@ impl DeleteComment {
|
|||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for DeleteComment {
|
||||
type DataType = IbisContext;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
&self.id
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{backend::utils::error::MyResult, common::instance::DbInstance};
|
||||
use crate::{backend::utils::error::BackendResult, common::instance::DbInstance};
|
||||
use activitypub_federation::kinds::public;
|
||||
use url::Url;
|
||||
|
||||
|
@ -7,7 +7,7 @@ pub mod delete_comment;
|
|||
pub mod undo_delete_comment;
|
||||
|
||||
/// Parameter is the return value from DbInstance::read_for_comment() for this comment.
|
||||
fn generate_comment_activity_to(instance: &DbInstance) -> MyResult<Vec<Url>> {
|
||||
fn generate_comment_activity_to(instance: &DbInstance) -> BackendResult<Vec<Url>> {
|
||||
let followers_url = format!("{}/followers", &instance.ap_id);
|
||||
Ok(vec![public(), followers_url.parse()?])
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
|||
database::{comment::DbCommentUpdateForm, IbisContext},
|
||||
federation::{routes::AnnouncableActivities, send_activity_to_instance},
|
||||
utils::{
|
||||
error::{Error, MyResult},
|
||||
error::{BackendError, BackendResult},
|
||||
generate_activity_id,
|
||||
},
|
||||
},
|
||||
|
@ -37,7 +37,7 @@ pub struct UndoDeleteComment {
|
|||
}
|
||||
|
||||
impl UndoDeleteComment {
|
||||
pub async fn send(comment: &DbComment, context: &Data<IbisContext>) -> MyResult<()> {
|
||||
pub async fn send(comment: &DbComment, context: &Data<IbisContext>) -> BackendResult<()> {
|
||||
let instance = DbInstance::read_for_comment(comment.id, context)?;
|
||||
let id = generate_activity_id(context)?;
|
||||
let creator = DbPerson::read(comment.creator_id, context)?;
|
||||
|
@ -58,7 +58,7 @@ impl UndoDeleteComment {
|
|||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for UndoDeleteComment {
|
||||
type DataType = IbisContext;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
&self.id
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
database::IbisContext,
|
||||
federation::objects::article::ApubArticle,
|
||||
utils::{
|
||||
error::{Error, MyResult},
|
||||
error::{BackendError, BackendResult},
|
||||
generate_activity_id,
|
||||
},
|
||||
},
|
||||
|
@ -35,7 +35,7 @@ impl CreateArticle {
|
|||
pub async fn send_to_followers(
|
||||
article: DbArticle,
|
||||
context: &Data<IbisContext>,
|
||||
) -> MyResult<()> {
|
||||
) -> BackendResult<()> {
|
||||
let local_instance = DbInstance::read_local(context)?;
|
||||
let object = article.clone().into_json(context).await?;
|
||||
let id = generate_activity_id(context)?;
|
||||
|
@ -56,7 +56,7 @@ impl CreateArticle {
|
|||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for CreateArticle {
|
||||
type DataType = IbisContext;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
&self.id
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
database::IbisContext,
|
||||
federation::{activities::accept::Accept, send_activity},
|
||||
generate_activity_id,
|
||||
utils::error::{Error, MyResult},
|
||||
utils::error::{BackendError, BackendResult},
|
||||
},
|
||||
common::{instance::DbInstance, user::DbPerson},
|
||||
};
|
||||
|
@ -32,7 +32,7 @@ impl Follow {
|
|||
actor: DbPerson,
|
||||
to: &DbInstance,
|
||||
context: &Data<IbisContext>,
|
||||
) -> MyResult<()> {
|
||||
) -> BackendResult<()> {
|
||||
let id = generate_activity_id(context)?;
|
||||
let follow = Follow {
|
||||
actor: actor.ap_id.clone(),
|
||||
|
@ -48,7 +48,7 @@ impl Follow {
|
|||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for Follow {
|
||||
type DataType = IbisContext;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
&self.id
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
|||
update_local_article::UpdateLocalArticle,
|
||||
update_remote_article::UpdateRemoteArticle,
|
||||
},
|
||||
utils::error::Error,
|
||||
utils::error::BackendError,
|
||||
},
|
||||
common::{
|
||||
article::{DbArticle, DbEdit, EditVersion},
|
||||
|
@ -31,7 +31,7 @@ pub async fn submit_article_update(
|
|||
original_article: &DbArticle,
|
||||
creator_id: PersonId,
|
||||
context: &Data<IbisContext>,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<(), BackendError> {
|
||||
let mut form = DbEditForm::new(
|
||||
original_article,
|
||||
creator_id,
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
|||
},
|
||||
federation::{objects::edit::ApubEdit, send_activity},
|
||||
utils::{
|
||||
error::{Error, MyResult},
|
||||
error::{BackendError, BackendResult},
|
||||
generate_activity_id,
|
||||
},
|
||||
},
|
||||
|
@ -39,7 +39,7 @@ impl RejectEdit {
|
|||
edit: ApubEdit,
|
||||
user_instance: DbInstance,
|
||||
context: &Data<IbisContext>,
|
||||
) -> MyResult<()> {
|
||||
) -> BackendResult<()> {
|
||||
let local_instance = DbInstance::read_local(context)?;
|
||||
let id = generate_activity_id(context)?;
|
||||
let reject = RejectEdit {
|
||||
|
@ -63,7 +63,7 @@ impl RejectEdit {
|
|||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for RejectEdit {
|
||||
type DataType = IbisContext;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
&self.id
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
database::IbisContext,
|
||||
federation::objects::article::ApubArticle,
|
||||
utils::{
|
||||
error::{Error, MyResult},
|
||||
error::{BackendError, BackendResult},
|
||||
generate_activity_id,
|
||||
},
|
||||
},
|
||||
|
@ -37,7 +37,7 @@ impl UpdateLocalArticle {
|
|||
article: DbArticle,
|
||||
extra_recipients: Vec<DbInstance>,
|
||||
context: &Data<IbisContext>,
|
||||
) -> MyResult<()> {
|
||||
) -> BackendResult<()> {
|
||||
debug_assert!(article.local);
|
||||
let local_instance = DbInstance::read_local(context)?;
|
||||
let id = generate_activity_id(context)?;
|
||||
|
@ -60,7 +60,7 @@ impl UpdateLocalArticle {
|
|||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for UpdateLocalArticle {
|
||||
type DataType = IbisContext;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
&self.id
|
||||
|
|
|
@ -7,7 +7,7 @@ use crate::{
|
|||
send_activity,
|
||||
},
|
||||
utils::{
|
||||
error::{Error, MyResult},
|
||||
error::{BackendError, BackendResult},
|
||||
generate_activity_id,
|
||||
},
|
||||
},
|
||||
|
@ -46,7 +46,7 @@ impl UpdateRemoteArticle {
|
|||
edit: DbEdit,
|
||||
article_instance: DbInstance,
|
||||
context: &Data<IbisContext>,
|
||||
) -> MyResult<()> {
|
||||
) -> BackendResult<()> {
|
||||
let local_instance = DbInstance::read_local(context)?;
|
||||
let id = generate_activity_id(context)?;
|
||||
let update = UpdateRemoteArticle {
|
||||
|
@ -70,7 +70,7 @@ impl UpdateRemoteArticle {
|
|||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for UpdateRemoteArticle {
|
||||
type DataType = IbisContext;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
&self.id
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use super::utils::error::MyResult;
|
||||
use super::utils::error::BackendResult;
|
||||
use crate::{
|
||||
backend::{database::IbisContext, utils::config::IbisConfig},
|
||||
common::{instance::DbInstance, user::DbPerson},
|
||||
|
@ -41,7 +41,7 @@ pub async fn send_activity_to_instance(
|
|||
activity: AnnouncableActivities,
|
||||
instance: &DbInstance,
|
||||
context: &Data<IbisContext>,
|
||||
) -> MyResult<()> {
|
||||
) -> BackendResult<()> {
|
||||
if instance.local {
|
||||
AnnounceActivity::send(activity, context).await?;
|
||||
} else {
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::{
|
|||
backend::{
|
||||
database::{article::DbArticleForm, IbisContext},
|
||||
federation::objects::edits_collection::DbEditCollection,
|
||||
utils::{error::Error, validate::validate_article_title},
|
||||
utils::{error::BackendError, validate::validate_article_title},
|
||||
},
|
||||
common::{
|
||||
article::{DbArticle, EditVersion},
|
||||
|
@ -42,7 +42,7 @@ pub struct ApubArticle {
|
|||
impl Object for DbArticle {
|
||||
type DataType = IbisContext;
|
||||
type Kind = ApubArticle;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
async fn read_from_id(
|
||||
object_id: Url,
|
||||
|
|
|
@ -2,7 +2,7 @@ use super::{article::ApubArticle, comment::ApubComment};
|
|||
use crate::{
|
||||
backend::{
|
||||
database::IbisContext,
|
||||
utils::error::{Error, MyResult},
|
||||
utils::error::{BackendError, BackendResult},
|
||||
},
|
||||
common::{article::DbArticle, comment::DbComment},
|
||||
};
|
||||
|
@ -28,7 +28,7 @@ pub enum ApubArticleOrComment {
|
|||
impl Object for DbArticleOrComment {
|
||||
type DataType = IbisContext;
|
||||
type Kind = ApubArticleOrComment;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
|
||||
None
|
||||
|
@ -37,7 +37,7 @@ impl Object for DbArticleOrComment {
|
|||
async fn read_from_id(
|
||||
object_id: Url,
|
||||
context: &Data<Self::DataType>,
|
||||
) -> MyResult<Option<Self>> {
|
||||
) -> BackendResult<Option<Self>> {
|
||||
let post = DbArticle::read_from_id(object_id.clone(), context).await?;
|
||||
Ok(match post {
|
||||
Some(o) => Some(Self::Article(o)),
|
||||
|
@ -47,14 +47,14 @@ impl Object for DbArticleOrComment {
|
|||
})
|
||||
}
|
||||
|
||||
async fn delete(self, context: &Data<Self::DataType>) -> MyResult<()> {
|
||||
async fn delete(self, context: &Data<Self::DataType>) -> BackendResult<()> {
|
||||
match self {
|
||||
Self::Article(p) => p.delete(context).await,
|
||||
Self::Comment(c) => c.delete(context).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn into_json(self, context: &Data<Self::DataType>) -> MyResult<Self::Kind> {
|
||||
async fn into_json(self, context: &Data<Self::DataType>) -> BackendResult<Self::Kind> {
|
||||
Ok(match self {
|
||||
Self::Article(p) => Self::Kind::Article(Box::new(p.into_json(context).await?)),
|
||||
Self::Comment(c) => Self::Kind::Comment(Box::new(c.into_json(context).await?)),
|
||||
|
@ -65,14 +65,14 @@ impl Object for DbArticleOrComment {
|
|||
apub: &Self::Kind,
|
||||
expected_domain: &Url,
|
||||
context: &Data<Self::DataType>,
|
||||
) -> MyResult<()> {
|
||||
) -> BackendResult<()> {
|
||||
match apub {
|
||||
Self::Kind::Article(a) => DbArticle::verify(a, expected_domain, context).await,
|
||||
Self::Kind::Comment(a) => DbComment::verify(a, expected_domain, context).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_json(apub: Self::Kind, context: &Data<Self::DataType>) -> MyResult<Self> {
|
||||
async fn from_json(apub: Self::Kind, context: &Data<Self::DataType>) -> BackendResult<Self> {
|
||||
Ok(match apub {
|
||||
Self::Kind::Article(p) => Self::Article(DbArticle::from_json(*p, context).await?),
|
||||
Self::Kind::Comment(n) => Self::Comment(DbComment::from_json(*n, context).await?),
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::{
|
|||
backend::{
|
||||
database::IbisContext,
|
||||
federation::objects::article::ApubArticle,
|
||||
utils::error::{Error, MyResult},
|
||||
utils::error::{BackendError, BackendResult},
|
||||
},
|
||||
common::{article::DbArticle, utils::http_protocol_str},
|
||||
};
|
||||
|
@ -30,7 +30,7 @@ pub struct ArticleCollection {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct DbArticleCollection(());
|
||||
|
||||
pub fn local_articles_url(domain: &str) -> MyResult<CollectionId<DbArticleCollection>> {
|
||||
pub fn local_articles_url(domain: &str) -> BackendResult<CollectionId<DbArticleCollection>> {
|
||||
Ok(CollectionId::parse(&format!(
|
||||
"{}://{domain}/all_articles",
|
||||
http_protocol_str()
|
||||
|
@ -42,7 +42,7 @@ impl Collection for DbArticleCollection {
|
|||
type Owner = ();
|
||||
type DataType = IbisContext;
|
||||
type Kind = ArticleCollection;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
async fn read_local(
|
||||
_owner: &Self::Owner,
|
||||
|
|
|
@ -2,7 +2,7 @@ use super::article_or_comment::DbArticleOrComment;
|
|||
use crate::{
|
||||
backend::{
|
||||
database::{comment::DbCommentInsertForm, IbisContext},
|
||||
utils::{error::Error, validate::validate_comment_max_depth},
|
||||
utils::{error::BackendError, validate::validate_comment_max_depth},
|
||||
},
|
||||
common::{article::DbArticle, comment::DbComment, user::DbPerson},
|
||||
};
|
||||
|
@ -39,7 +39,7 @@ pub struct ApubComment {
|
|||
impl Object for DbComment {
|
||||
type DataType = IbisContext;
|
||||
type Kind = ApubComment;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
async fn read_from_id(
|
||||
object_id: Url,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
backend::{
|
||||
database::{edit::DbEditForm, IbisContext},
|
||||
utils::error::Error,
|
||||
utils::error::BackendError,
|
||||
},
|
||||
common::{
|
||||
article::{DbArticle, DbEdit, EditVersion},
|
||||
|
@ -45,7 +45,7 @@ pub struct ApubEdit {
|
|||
impl Object for DbEdit {
|
||||
type DataType = IbisContext;
|
||||
type Kind = ApubEdit;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
async fn read_from_id(
|
||||
object_id: Url,
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
use crate::{
|
||||
backend::{database::IbisContext, federation::objects::edit::ApubEdit, utils::error::Error},
|
||||
backend::{
|
||||
database::IbisContext,
|
||||
federation::objects::edit::ApubEdit,
|
||||
utils::error::BackendError,
|
||||
},
|
||||
common::article::{DbArticle, DbEdit},
|
||||
};
|
||||
use activitypub_federation::{
|
||||
|
@ -30,7 +34,7 @@ impl Collection for DbEditCollection {
|
|||
type Owner = DbArticle;
|
||||
type DataType = IbisContext;
|
||||
type Kind = ApubEditCollection;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
async fn read_local(
|
||||
article: &Self::Owner,
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
backend::{
|
||||
database::{instance::DbInstanceForm, IbisContext},
|
||||
federation::{objects::articles_collection::DbArticleCollection, send_activity},
|
||||
utils::error::{Error, MyResult},
|
||||
utils::error::{BackendError, BackendResult},
|
||||
},
|
||||
common::{instance::DbInstance, utils::extract_domain},
|
||||
};
|
||||
|
@ -37,11 +37,11 @@ pub struct ApubInstance {
|
|||
}
|
||||
|
||||
impl DbInstance {
|
||||
pub fn followers_url(&self) -> MyResult<Url> {
|
||||
pub fn followers_url(&self) -> BackendResult<Url> {
|
||||
Ok(Url::parse(&format!("{}/followers", self.ap_id.inner()))?)
|
||||
}
|
||||
|
||||
pub fn follower_ids(&self, context: &Data<IbisContext>) -> MyResult<Vec<Url>> {
|
||||
pub fn follower_ids(&self, context: &Data<IbisContext>) -> BackendResult<Vec<Url>> {
|
||||
Ok(DbInstance::read_followers(self.id, context)?
|
||||
.into_iter()
|
||||
.map(|f| f.ap_id.into())
|
||||
|
@ -57,7 +57,7 @@ impl DbInstance {
|
|||
where
|
||||
Activity: ActivityHandler + Serialize + Debug + Send + Sync,
|
||||
<Activity as ActivityHandler>::Error: From<activitypub_federation::error::Error>,
|
||||
<Activity as ActivityHandler>::Error: From<Error>,
|
||||
<Activity as ActivityHandler>::Error: From<BackendError>,
|
||||
{
|
||||
let mut inboxes: Vec<_> = DbInstance::read_followers(self.id, context)?
|
||||
.iter()
|
||||
|
@ -73,7 +73,7 @@ impl DbInstance {
|
|||
impl Object for DbInstance {
|
||||
type DataType = IbisContext;
|
||||
type Kind = ApubInstance;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
|
||||
Some(self.last_refreshed_at)
|
||||
|
|
|
@ -2,7 +2,7 @@ use super::instance::ApubInstance;
|
|||
use crate::{
|
||||
backend::{
|
||||
database::IbisContext,
|
||||
utils::error::{Error, MyResult},
|
||||
utils::error::{BackendError, BackendResult},
|
||||
},
|
||||
common::{instance::DbInstance, utils::http_protocol_str},
|
||||
};
|
||||
|
@ -30,7 +30,7 @@ pub struct InstanceCollection {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct DbInstanceCollection(());
|
||||
|
||||
pub fn linked_instances_url(domain: &str) -> MyResult<CollectionId<DbInstanceCollection>> {
|
||||
pub fn linked_instances_url(domain: &str) -> BackendResult<CollectionId<DbInstanceCollection>> {
|
||||
Ok(CollectionId::parse(&format!(
|
||||
"{}://{domain}/linked_instances",
|
||||
http_protocol_str()
|
||||
|
@ -42,7 +42,7 @@ impl Collection for DbInstanceCollection {
|
|||
type Owner = ();
|
||||
type DataType = IbisContext;
|
||||
type Kind = InstanceCollection;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
async fn read_local(
|
||||
_owner: &Self::Owner,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
backend::{
|
||||
database::{user::DbPersonForm, IbisContext},
|
||||
utils::error::Error,
|
||||
utils::error::BackendError,
|
||||
},
|
||||
common::user::DbPerson,
|
||||
};
|
||||
|
@ -35,7 +35,7 @@ pub struct ApubUser {
|
|||
impl Object for DbPerson {
|
||||
type DataType = IbisContext;
|
||||
type Kind = ApubUser;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
|
||||
Some(self.last_refreshed_at)
|
||||
|
|
|
@ -30,7 +30,7 @@ use crate::{
|
|||
user::ApubUser,
|
||||
},
|
||||
},
|
||||
utils::error::{Error, MyResult},
|
||||
utils::error::{BackendError, BackendResult},
|
||||
},
|
||||
common::{
|
||||
article::DbArticle,
|
||||
|
@ -75,7 +75,7 @@ pub fn federation_routes() -> Router<()> {
|
|||
#[debug_handler]
|
||||
async fn http_get_instance(
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<FederationJson<WithContext<ApubInstance>>> {
|
||||
) -> BackendResult<FederationJson<WithContext<ApubInstance>>> {
|
||||
let local_instance = DbInstance::read_local(&context)?;
|
||||
let json_instance = local_instance.into_json(&context).await?;
|
||||
Ok(FederationJson(WithContext::new_default(json_instance)))
|
||||
|
@ -85,7 +85,7 @@ async fn http_get_instance(
|
|||
async fn http_get_person(
|
||||
Path(name): Path<String>,
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<FederationJson<WithContext<ApubUser>>> {
|
||||
) -> BackendResult<FederationJson<WithContext<ApubUser>>> {
|
||||
let person = DbPerson::read_local_from_name(&name, &context)?.person;
|
||||
let json_person = person.into_json(&context).await?;
|
||||
Ok(FederationJson(WithContext::new_default(json_person)))
|
||||
|
@ -94,7 +94,7 @@ async fn http_get_person(
|
|||
#[debug_handler]
|
||||
async fn http_get_all_articles(
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<FederationJson<WithContext<ArticleCollection>>> {
|
||||
) -> BackendResult<FederationJson<WithContext<ArticleCollection>>> {
|
||||
let collection = DbArticleCollection::read_local(&(), &context).await?;
|
||||
Ok(FederationJson(WithContext::new_default(collection)))
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ async fn http_get_all_articles(
|
|||
#[debug_handler]
|
||||
async fn http_get_linked_instances(
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<FederationJson<WithContext<InstanceCollection>>> {
|
||||
) -> BackendResult<FederationJson<WithContext<InstanceCollection>>> {
|
||||
let collection = DbInstanceCollection::read_local(&(), &context).await?;
|
||||
Ok(FederationJson(WithContext::new_default(collection)))
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ async fn http_get_linked_instances(
|
|||
async fn http_get_article(
|
||||
Path(title): Path<String>,
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<FederationJson<WithContext<ApubArticle>>> {
|
||||
) -> BackendResult<FederationJson<WithContext<ApubArticle>>> {
|
||||
let article = DbArticle::read_view_title(&title, None, &context)?;
|
||||
let json = article.article.into_json(&context).await?;
|
||||
Ok(FederationJson(WithContext::new_default(json)))
|
||||
|
@ -121,7 +121,7 @@ async fn http_get_article(
|
|||
async fn http_get_article_edits(
|
||||
Path(title): Path<String>,
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<FederationJson<WithContext<ApubEditCollection>>> {
|
||||
) -> BackendResult<FederationJson<WithContext<ApubEditCollection>>> {
|
||||
let article = DbArticle::read_view_title(&title, None, &context)?;
|
||||
let json = DbEditCollection::read_local(&article.article, &context).await?;
|
||||
Ok(FederationJson(WithContext::new_default(json)))
|
||||
|
@ -131,7 +131,7 @@ async fn http_get_article_edits(
|
|||
async fn http_get_comment(
|
||||
Path(id): Path<i32>,
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<FederationJson<WithContext<ApubComment>>> {
|
||||
) -> BackendResult<FederationJson<WithContext<ApubComment>>> {
|
||||
let comment = DbComment::read(CommentId(id), &context)?;
|
||||
let json = comment.into_json(&context).await?;
|
||||
Ok(FederationJson(WithContext::new_default(json)))
|
||||
|
@ -193,7 +193,7 @@ pub enum PersonOrInstanceType {
|
|||
impl Object for UserOrInstance {
|
||||
type DataType = IbisContext;
|
||||
type Kind = PersonOrInstance;
|
||||
type Error = Error;
|
||||
type Error = BackendError;
|
||||
|
||||
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
|
||||
Some(match self {
|
||||
|
@ -205,7 +205,7 @@ impl Object for UserOrInstance {
|
|||
async fn read_from_id(
|
||||
object_id: Url,
|
||||
data: &Data<Self::DataType>,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
) -> Result<Option<Self>, BackendError> {
|
||||
let person = DbPerson::read_from_id(object_id.clone(), data).await;
|
||||
Ok(match person {
|
||||
Ok(Some(o)) => Some(UserOrInstance::User(o)),
|
||||
|
@ -215,14 +215,14 @@ impl Object for UserOrInstance {
|
|||
})
|
||||
}
|
||||
|
||||
async fn delete(self, data: &Data<Self::DataType>) -> Result<(), Error> {
|
||||
async fn delete(self, data: &Data<Self::DataType>) -> Result<(), BackendError> {
|
||||
match self {
|
||||
UserOrInstance::User(p) => p.delete(data).await,
|
||||
UserOrInstance::Instance(p) => p.delete(data).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Error> {
|
||||
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, BackendError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
@ -230,14 +230,17 @@ impl Object for UserOrInstance {
|
|||
apub: &Self::Kind,
|
||||
expected_domain: &Url,
|
||||
data: &Data<Self::DataType>,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<(), BackendError> {
|
||||
match apub {
|
||||
PersonOrInstance::Person(a) => DbPerson::verify(a, expected_domain, data).await,
|
||||
PersonOrInstance::Instance(a) => DbInstance::verify(a, expected_domain, data).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_json(apub: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Error> {
|
||||
async fn from_json(
|
||||
apub: Self::Kind,
|
||||
data: &Data<Self::DataType>,
|
||||
) -> Result<Self, BackendError> {
|
||||
Ok(match apub {
|
||||
PersonOrInstance::Person(p) => {
|
||||
UserOrInstance::User(DbPerson::from_json(p, data).await?)
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::{
|
|||
backend::{
|
||||
database::IbisContext,
|
||||
federation::VerifyUrlData,
|
||||
utils::{config::IbisConfig, error::MyResult, generate_activity_id},
|
||||
utils::{config::IbisConfig, error::BackendResult, generate_activity_id},
|
||||
},
|
||||
common::instance::DbInstance,
|
||||
};
|
||||
|
@ -30,7 +30,7 @@ pub async fn start(
|
|||
config: IbisConfig,
|
||||
override_hostname: Option<SocketAddr>,
|
||||
notify_start: Option<oneshot::Sender<()>>,
|
||||
) -> MyResult<()> {
|
||||
) -> BackendResult<()> {
|
||||
let manager = ConnectionManager::<PgConnection>::new(&config.database.connection_url);
|
||||
let db_pool = Pool::builder()
|
||||
.max_size(config.database.pool_size)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::backend::utils::error::MyResult;
|
||||
use crate::backend::utils::error::BackendResult;
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
body::Body,
|
||||
|
@ -19,7 +19,7 @@ use tower_http::services::ServeDir;
|
|||
pub async fn file_and_error_handler(
|
||||
State(options): State<LeptosOptions>,
|
||||
request: Request<Body>,
|
||||
) -> MyResult<Response<Body>> {
|
||||
) -> BackendResult<Response<Body>> {
|
||||
if cfg!(debug_assertions) {
|
||||
// in debug mode serve assets directly from local folder
|
||||
Ok(ServeDir::new(options.site_root.as_ref())
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use super::{database::IbisContext, utils::error::MyResult};
|
||||
use super::{database::IbisContext, utils::error::BackendResult};
|
||||
use crate::{
|
||||
backend::{api::api_routes, federation::routes::federation_routes},
|
||||
common::Auth,
|
||||
|
@ -35,7 +35,7 @@ pub(super) async fn start_server(
|
|||
context: FederationConfig<IbisContext>,
|
||||
override_hostname: Option<SocketAddr>,
|
||||
notify_start: Option<oneshot::Sender<()>>,
|
||||
) -> MyResult<()> {
|
||||
) -> BackendResult<()> {
|
||||
let leptos_options = get_config_from_str(include_str!("../../../Cargo.toml"))?;
|
||||
let mut addr = leptos_options.site_addr;
|
||||
if let Some(override_hostname) = override_hostname {
|
||||
|
@ -76,8 +76,10 @@ async fn leptos_routes_handler(
|
|||
State(leptos_options): State<LeptosOptions>,
|
||||
request: Request<Body>,
|
||||
) -> Response {
|
||||
let leptos_options_ = leptos_options.clone();
|
||||
let handler = leptos_axum::render_app_async_with_context(
|
||||
move || {
|
||||
provide_context(leptos_options_.clone());
|
||||
if let Some(auth) = &auth {
|
||||
provide_context(auth.0.clone());
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
backend::{
|
||||
database::{instance_stats::InstanceStats, IbisContext},
|
||||
utils::error::MyResult,
|
||||
utils::error::BackendResult,
|
||||
},
|
||||
common::utils::http_protocol_str,
|
||||
};
|
||||
|
@ -16,7 +16,9 @@ pub fn config() -> Router<()> {
|
|||
.route("/.well-known/nodeinfo", get(node_info_well_known))
|
||||
}
|
||||
|
||||
async fn node_info_well_known(context: Data<IbisContext>) -> MyResult<Json<NodeInfoWellKnown>> {
|
||||
async fn node_info_well_known(
|
||||
context: Data<IbisContext>,
|
||||
) -> BackendResult<Json<NodeInfoWellKnown>> {
|
||||
Ok(Json(NodeInfoWellKnown {
|
||||
links: vec![NodeInfoWellKnownLinks {
|
||||
rel: Url::parse("http://nodeinfo.diaspora.software/ns/schema/2.1")?,
|
||||
|
@ -29,7 +31,7 @@ async fn node_info_well_known(context: Data<IbisContext>) -> MyResult<Json<NodeI
|
|||
}))
|
||||
}
|
||||
|
||||
async fn node_info(context: Data<IbisContext>) -> MyResult<Json<NodeInfo>> {
|
||||
async fn node_info(context: Data<IbisContext>) -> BackendResult<Json<NodeInfo>> {
|
||||
let stats = InstanceStats::read(&context)?;
|
||||
Ok(Json(NodeInfo {
|
||||
version: "2.1".to_string(),
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::{
|
|||
instance_collection::linked_instances_url,
|
||||
},
|
||||
},
|
||||
utils::{error::Error, generate_keypair},
|
||||
utils::{error::BackendError, generate_keypair},
|
||||
},
|
||||
common::{
|
||||
article::{DbArticle, EditVersion},
|
||||
|
@ -27,7 +27,7 @@ This main page can only be edited by the admin. Use it as an introduction for ne
|
|||
and to list interesting articles.
|
||||
";
|
||||
|
||||
pub async fn setup(context: &Data<IbisContext>) -> Result<(), Error> {
|
||||
pub async fn setup(context: &Data<IbisContext>) -> Result<(), BackendError> {
|
||||
let domain = &context.config.federation.domain;
|
||||
let ap_id = ObjectId::parse(&format!("{}://{domain}", http_protocol_str()))?;
|
||||
let inbox_url = format!("{}://{domain}/inbox", http_protocol_str());
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{backend::utils::error::MyResult, common::instance::Options};
|
||||
use crate::{backend::utils::error::BackendResult, common::instance::Options};
|
||||
use config::Config;
|
||||
use doku::Document;
|
||||
use serde::Deserialize;
|
||||
|
@ -17,7 +17,7 @@ pub struct IbisConfig {
|
|||
}
|
||||
|
||||
impl IbisConfig {
|
||||
pub fn read() -> MyResult<Self> {
|
||||
pub fn read() -> BackendResult<Self> {
|
||||
let config = Config::builder()
|
||||
.add_source(config::File::with_name("config.toml"))
|
||||
// Cant use _ as separator due to https://github.com/mehcode/config-rs/issues/391
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
|
||||
pub type MyResult<T> = Result<T, Error>;
|
||||
pub type BackendResult<T> = Result<T, BackendError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error(pub anyhow::Error);
|
||||
pub struct BackendError(pub anyhow::Error);
|
||||
|
||||
impl Display for Error {
|
||||
impl Display for BackendError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Error
|
||||
impl<T> From<T> for BackendError
|
||||
where
|
||||
T: Into<anyhow::Error>,
|
||||
{
|
||||
fn from(t: T) -> Self {
|
||||
Error(t.into())
|
||||
BackendError(t.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
impl axum::response::IntoResponse for Error {
|
||||
impl axum::response::IntoResponse for BackendError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
(
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
backend::{database::IbisContext, utils::error::MyResult},
|
||||
backend::{database::IbisContext, utils::error::BackendResult},
|
||||
common::{
|
||||
article::{DbEdit, EditVersion},
|
||||
utils,
|
||||
|
@ -43,7 +43,7 @@ pub(super) fn generate_activity_id(context: &Data<IbisContext>) -> Result<Url, P
|
|||
pub(super) fn generate_article_version(
|
||||
edits: &Vec<DbEdit>,
|
||||
version: &EditVersion,
|
||||
) -> MyResult<String> {
|
||||
) -> BackendResult<String> {
|
||||
let mut generated = String::new();
|
||||
if version == &EditVersion::default() {
|
||||
return Ok(generated);
|
||||
|
@ -60,7 +60,7 @@ pub(super) fn generate_article_version(
|
|||
|
||||
/// Use a single static keypair during testing which is signficantly faster than
|
||||
/// generating dozens of keys from scratch.
|
||||
pub fn generate_keypair() -> MyResult<Keypair> {
|
||||
pub fn generate_keypair() -> BackendResult<Keypair> {
|
||||
if cfg!(debug_assertions) {
|
||||
static KEYPAIR: LazyLock<Keypair> =
|
||||
LazyLock::new(|| generate_actor_keypair().expect("generate keypair"));
|
||||
|
@ -81,8 +81,8 @@ mod test {
|
|||
use chrono::Utc;
|
||||
use diffy::create_patch;
|
||||
|
||||
fn create_edits() -> MyResult<Vec<DbEdit>> {
|
||||
let generate_edit = |a, b| -> MyResult<DbEdit> {
|
||||
fn create_edits() -> BackendResult<Vec<DbEdit>> {
|
||||
let generate_edit = |a, b| -> BackendResult<DbEdit> {
|
||||
let diff = create_patch(a, b).to_string();
|
||||
Ok(DbEdit {
|
||||
id: EditId(0),
|
||||
|
@ -106,7 +106,7 @@ mod test {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_article_version() -> MyResult<()> {
|
||||
fn test_generate_article_version() -> BackendResult<()> {
|
||||
let edits = create_edits()?;
|
||||
let generated = generate_article_version(&edits, &edits[1].hash)?;
|
||||
assert_eq!("sda\n", generated);
|
||||
|
@ -114,7 +114,7 @@ mod test {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_invalid_version() -> MyResult<()> {
|
||||
fn test_generate_invalid_version() -> BackendResult<()> {
|
||||
let edits = create_edits()?;
|
||||
let generated = generate_article_version(&edits, &EditVersion::new("invalid"));
|
||||
assert!(generated.is_err());
|
||||
|
@ -122,7 +122,7 @@ mod test {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_first_version() -> MyResult<()> {
|
||||
fn test_generate_first_version() -> BackendResult<()> {
|
||||
let edits = create_edits()?;
|
||||
let generated = generate_article_version(&edits, &EditVersion::default())?;
|
||||
assert_eq!("", generated);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::backend::{database::DbPool, utils::error::MyResult};
|
||||
use crate::backend::{database::DbPool, utils::error::BackendResult};
|
||||
use clokwerk::{Scheduler, TimeUnits};
|
||||
use diesel::{sql_query, RunQueryDsl};
|
||||
use log::{error, info};
|
||||
|
@ -15,7 +15,7 @@ pub fn start(pool: DbPool) {
|
|||
let _ = scheduler.watch_thread(Duration::from_secs(60));
|
||||
}
|
||||
|
||||
fn active_counts(pool: &DbPool) -> MyResult<()> {
|
||||
fn active_counts(pool: &DbPool) -> BackendResult<()> {
|
||||
info!("Updating active user count");
|
||||
let mut conn = pool.get()?;
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use super::error::MyResult;
|
||||
use super::error::BackendResult;
|
||||
use anyhow::anyhow;
|
||||
use regex::Regex;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub fn validate_article_title(title: &str) -> MyResult<String> {
|
||||
pub fn validate_article_title(title: &str) -> BackendResult<String> {
|
||||
#[expect(clippy::expect_used)]
|
||||
static TITLE_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_]{3,100}$").expect("compile regex"));
|
||||
|
@ -14,7 +14,7 @@ pub fn validate_article_title(title: &str) -> MyResult<String> {
|
|||
Ok(title)
|
||||
}
|
||||
|
||||
pub fn validate_user_name(name: &str) -> MyResult<()> {
|
||||
pub fn validate_user_name(name: &str) -> BackendResult<()> {
|
||||
#[allow(clippy::expect_used)]
|
||||
static VALID_ACTOR_NAME_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_]{3,20}$").expect("compile regex"));
|
||||
|
@ -26,7 +26,7 @@ pub fn validate_user_name(name: &str) -> MyResult<()> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn validate_display_name(name: &Option<String>) -> MyResult<()> {
|
||||
pub fn validate_display_name(name: &Option<String>) -> BackendResult<()> {
|
||||
if let Some(name) = name {
|
||||
if name.contains('@') || name.len() < 3 || name.len() > 20 {
|
||||
return Err(anyhow!("Invalid displayname").into());
|
||||
|
@ -35,14 +35,14 @@ pub fn validate_display_name(name: &Option<String>) -> MyResult<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_comment_max_depth(depth: i32) -> MyResult<()> {
|
||||
pub fn validate_comment_max_depth(depth: i32) -> BackendResult<()> {
|
||||
if depth > 50 {
|
||||
return Err(anyhow!("Max comment depth reached").into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_not_empty(text: &str) -> MyResult<()> {
|
||||
pub fn validate_not_empty(text: &str) -> BackendResult<()> {
|
||||
if text.trim().len() < 2 {
|
||||
return Err(anyhow!("Empty text submitted").into());
|
||||
}
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
use super::{result_to_option, ApiClient};
|
||||
use crate::common::{
|
||||
article::{
|
||||
ApiConflict,
|
||||
ApproveArticleParams,
|
||||
CreateArticleParams,
|
||||
DbArticle,
|
||||
DbArticleView,
|
||||
DeleteConflictParams,
|
||||
EditArticleParams,
|
||||
EditView,
|
||||
ForkArticleParams,
|
||||
GetArticleParams,
|
||||
GetEditList,
|
||||
ListArticlesParams,
|
||||
ProtectArticleParams,
|
||||
use super::ApiClient;
|
||||
use crate::{
|
||||
common::{
|
||||
article::{
|
||||
ApiConflict,
|
||||
ApproveArticleParams,
|
||||
CreateArticleParams,
|
||||
DbArticle,
|
||||
DbArticleView,
|
||||
DeleteConflictParams,
|
||||
EditArticleParams,
|
||||
EditView,
|
||||
ForkArticleParams,
|
||||
GetArticleParams,
|
||||
GetEditList,
|
||||
ListArticlesParams,
|
||||
ProtectArticleParams,
|
||||
},
|
||||
newtypes::{ArticleId, ConflictId},
|
||||
ResolveObjectParams,
|
||||
},
|
||||
newtypes::{ArticleId, ConflictId},
|
||||
ResolveObjectParams,
|
||||
frontend::utils::errors::FrontendResult,
|
||||
};
|
||||
use http::Method;
|
||||
use leptos::prelude::ServerFnError;
|
||||
use log::error;
|
||||
use url::Url;
|
||||
|
||||
|
@ -27,67 +29,67 @@ impl ApiClient {
|
|||
pub async fn create_article(
|
||||
&self,
|
||||
data: &CreateArticleParams,
|
||||
) -> Result<DbArticleView, ServerFnError> {
|
||||
) -> FrontendResult<DbArticleView> {
|
||||
self.post("/api/v1/article", Some(&data)).await
|
||||
}
|
||||
|
||||
pub async fn get_article(&self, data: GetArticleParams) -> Option<DbArticleView> {
|
||||
self.get("/api/v1/article", Some(data)).await
|
||||
pub async fn get_article(&self, data: GetArticleParams) -> FrontendResult<DbArticleView> {
|
||||
self.send(Method::GET, "/api/v1/article", Some(data)).await
|
||||
}
|
||||
|
||||
pub async fn list_articles(&self, data: ListArticlesParams) -> Option<Vec<DbArticle>> {
|
||||
Some(self.get("/api/v1/article/list", Some(data)).await.unwrap())
|
||||
pub async fn list_articles(&self, data: ListArticlesParams) -> FrontendResult<Vec<DbArticle>> {
|
||||
self.get("/api/v1/article/list", Some(data)).await
|
||||
}
|
||||
|
||||
pub async fn edit_article(
|
||||
&self,
|
||||
params: &EditArticleParams,
|
||||
) -> Result<Option<ApiConflict>, ServerFnError> {
|
||||
) -> FrontendResult<Option<ApiConflict>> {
|
||||
self.patch("/api/v1/article", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn fork_article(
|
||||
&self,
|
||||
params: &ForkArticleParams,
|
||||
) -> Result<DbArticleView, ServerFnError> {
|
||||
pub async fn fork_article(&self, params: &ForkArticleParams) -> FrontendResult<DbArticleView> {
|
||||
self.post("/api/v1/article/fork", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn protect_article(
|
||||
&self,
|
||||
params: &ProtectArticleParams,
|
||||
) -> Result<DbArticle, ServerFnError> {
|
||||
) -> FrontendResult<DbArticle> {
|
||||
self.post("/api/v1/article/protect", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn resolve_article(&self, id: Url) -> Result<DbArticleView, ServerFnError> {
|
||||
pub async fn resolve_article(&self, id: Url) -> FrontendResult<DbArticleView> {
|
||||
let resolve_object = ResolveObjectParams { id };
|
||||
self.send(Method::GET, "/api/v1/article/resolve", Some(resolve_object))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_article_edits(&self, article_id: ArticleId) -> Option<Vec<EditView>> {
|
||||
pub async fn get_article_edits(&self, article_id: ArticleId) -> FrontendResult<Vec<EditView>> {
|
||||
let data = GetEditList {
|
||||
article_id: Some(article_id),
|
||||
..Default::default()
|
||||
};
|
||||
self.get("/api/v1/edit/list", Some(data)).await
|
||||
self.send(Method::GET, "/api/v1/edit/list", Some(data))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn approve_article(&self, article_id: ArticleId, approve: bool) -> Option<()> {
|
||||
pub async fn approve_article(
|
||||
&self,
|
||||
article_id: ArticleId,
|
||||
approve: bool,
|
||||
) -> FrontendResult<()> {
|
||||
let params = ApproveArticleParams {
|
||||
article_id,
|
||||
approve,
|
||||
};
|
||||
result_to_option(self.post("/api/v1/article/approve", Some(¶ms)).await)
|
||||
self.post("/api/v1/article/approve", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn delete_conflict(&self, conflict_id: ConflictId) -> Option<()> {
|
||||
pub async fn delete_conflict(&self, conflict_id: ConflictId) -> FrontendResult<()> {
|
||||
let params = DeleteConflictParams { conflict_id };
|
||||
result_to_option(
|
||||
self.send(Method::DELETE, "/api/v1/conflict", Some(params))
|
||||
.await,
|
||||
)
|
||||
self.send(Method::DELETE, "/api/v1/conflict", Some(params))
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -108,5 +110,6 @@ impl ApiClient {
|
|||
id: Some(params.article_id),
|
||||
})
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
use super::ApiClient;
|
||||
use crate::common::comment::{CreateCommentParams, DbCommentView, EditCommentParams};
|
||||
use leptos::prelude::ServerFnError;
|
||||
use crate::{
|
||||
common::comment::{CreateCommentParams, DbCommentView, EditCommentParams},
|
||||
frontend::utils::errors::FrontendResult,
|
||||
};
|
||||
|
||||
impl ApiClient {
|
||||
pub async fn create_comment(
|
||||
&self,
|
||||
params: &CreateCommentParams,
|
||||
) -> Result<DbCommentView, ServerFnError> {
|
||||
) -> FrontendResult<DbCommentView> {
|
||||
self.post("/api/v1/comment", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn edit_comment(
|
||||
&self,
|
||||
params: &EditCommentParams,
|
||||
) -> Result<DbCommentView, ServerFnError> {
|
||||
pub async fn edit_comment(&self, params: &EditCommentParams) -> FrontendResult<DbCommentView> {
|
||||
self.patch("/api/v1/comment", Some(¶ms)).await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,59 +1,58 @@
|
|||
use super::{result_to_option, ApiClient};
|
||||
use crate::common::{
|
||||
article::{DbArticle, SearchArticleParams},
|
||||
instance::{
|
||||
DbInstance,
|
||||
FollowInstanceParams,
|
||||
GetInstanceParams,
|
||||
InstanceView,
|
||||
SiteView,
|
||||
UpdateInstanceParams,
|
||||
use super::ApiClient;
|
||||
use crate::{
|
||||
common::{
|
||||
article::{DbArticle, SearchArticleParams},
|
||||
instance::{
|
||||
DbInstance,
|
||||
FollowInstanceParams,
|
||||
GetInstanceParams,
|
||||
InstanceView,
|
||||
SiteView,
|
||||
UpdateInstanceParams,
|
||||
},
|
||||
Notification,
|
||||
ResolveObjectParams,
|
||||
SuccessResponse,
|
||||
},
|
||||
Notification,
|
||||
ResolveObjectParams,
|
||||
SuccessResponse,
|
||||
frontend::utils::errors::FrontendResult,
|
||||
};
|
||||
use http::Method;
|
||||
use leptos::prelude::ServerFnError;
|
||||
use url::Url;
|
||||
|
||||
impl ApiClient {
|
||||
pub async fn get_local_instance(&self) -> Option<InstanceView> {
|
||||
pub async fn get_local_instance(&self) -> FrontendResult<InstanceView> {
|
||||
self.get("/api/v1/instance", None::<i32>).await
|
||||
}
|
||||
|
||||
pub async fn get_instance(&self, params: &GetInstanceParams) -> Option<InstanceView> {
|
||||
pub async fn get_instance(&self, params: &GetInstanceParams) -> FrontendResult<InstanceView> {
|
||||
self.get("/api/v1/instance", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn list_instances(&self) -> Option<Vec<DbInstance>> {
|
||||
pub async fn list_instances(&self) -> FrontendResult<Vec<DbInstance>> {
|
||||
self.get("/api/v1/instance/list", None::<i32>).await
|
||||
}
|
||||
|
||||
pub async fn update_local_instance(
|
||||
&self,
|
||||
params: &UpdateInstanceParams,
|
||||
) -> Result<DbInstance, ServerFnError> {
|
||||
) -> FrontendResult<DbInstance> {
|
||||
self.patch("/api/v1/instance", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn notifications_list(&self) -> Option<Vec<Notification>> {
|
||||
pub async fn notifications_list(&self) -> FrontendResult<Vec<Notification>> {
|
||||
self.get("/api/v1/user/notifications/list", None::<()>)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn notifications_count(&self) -> Option<usize> {
|
||||
pub async fn notifications_count(&self) -> FrontendResult<usize> {
|
||||
self.get("/api/v1/user/notifications/count", None::<()>)
|
||||
.await
|
||||
}
|
||||
pub async fn search(
|
||||
&self,
|
||||
params: &SearchArticleParams,
|
||||
) -> Result<Vec<DbArticle>, ServerFnError> {
|
||||
pub async fn search(&self, params: &SearchArticleParams) -> FrontendResult<Vec<DbArticle>> {
|
||||
self.send(Method::GET, "/api/v1/search", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn resolve_instance(&self, id: Url) -> Result<DbInstance, ServerFnError> {
|
||||
pub async fn resolve_instance(&self, id: Url) -> FrontendResult<DbInstance> {
|
||||
let resolve_object = ResolveObjectParams { id };
|
||||
self.send(
|
||||
Method::GET,
|
||||
|
@ -63,23 +62,26 @@ impl ApiClient {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn follow_instance(&self, params: FollowInstanceParams) -> Option<SuccessResponse> {
|
||||
result_to_option(self.post("/api/v1/instance/follow", Some(params)).await)
|
||||
pub async fn follow_instance(
|
||||
&self,
|
||||
params: FollowInstanceParams,
|
||||
) -> FrontendResult<SuccessResponse> {
|
||||
self.post("/api/v1/instance/follow", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn site(&self) -> Option<SiteView> {
|
||||
pub async fn site(&self) -> FrontendResult<SiteView> {
|
||||
self.get("/api/v1/site", None::<()>).await
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub async fn follow_instance_with_resolve(&self, follow_instance: &str) -> Option<DbInstance> {
|
||||
pub async fn follow_instance_with_resolve(
|
||||
&self,
|
||||
follow_instance: &str,
|
||||
) -> FrontendResult<DbInstance> {
|
||||
use crate::common::{utils::http_protocol_str, ResolveObjectParams};
|
||||
use log::error;
|
||||
use url::Url;
|
||||
let params = ResolveObjectParams {
|
||||
id: Url::parse(&format!("{}://{}", http_protocol_str(), follow_instance))
|
||||
.map_err(|e| error!("invalid url {e}"))
|
||||
.ok()?,
|
||||
id: Url::parse(&format!("{}://{}", http_protocol_str(), follow_instance))?,
|
||||
};
|
||||
let instance_resolved: DbInstance =
|
||||
self.get("/api/v1/instance/resolve", Some(params)).await?;
|
||||
|
@ -89,6 +91,6 @@ impl ApiClient {
|
|||
id: instance_resolved.id,
|
||||
};
|
||||
self.follow_instance(params).await?;
|
||||
Some(instance_resolved)
|
||||
Ok(instance_resolved)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::frontend::utils::errors::{FrontendError, FrontendResult};
|
||||
use http::{Method, StatusCode};
|
||||
use leptos::{prelude::ServerFnError, server_fn::error::NoCustomError};
|
||||
use log::{error, info};
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Debug, sync::LazyLock};
|
||||
|
||||
|
@ -9,58 +9,45 @@ pub mod comment;
|
|||
pub mod instance;
|
||||
pub mod user;
|
||||
|
||||
pub static CLIENT: LazyLock<ApiClient> = LazyLock::new(|| {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
ApiClient::new(reqwest::Client::new(), None)
|
||||
}
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
ApiClient::new()
|
||||
}
|
||||
});
|
||||
pub static CLIENT: LazyLock<ApiClient> = LazyLock::new(|| ApiClient::new(None));
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ApiClient {
|
||||
#[cfg(feature = "ssr")]
|
||||
client: reqwest::Client,
|
||||
pub hostname: String,
|
||||
ssl: bool,
|
||||
#[cfg(feature = "ssr")]
|
||||
test_hostname: Option<String>,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn new(client: reqwest::Client, hostname_: Option<String>) -> Self {
|
||||
use leptos::config::get_config_from_str;
|
||||
let leptos_options = get_config_from_str(include_str!("../../../Cargo.toml")).unwrap();
|
||||
let mut hostname = leptos_options.site_addr.to_string();
|
||||
// required for tests
|
||||
if let Some(hostname_) = hostname_ {
|
||||
hostname = hostname_;
|
||||
pub fn new(#[allow(unused)] test_hostname: Option<String>) -> Self {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
// need cookie store for auth in tests
|
||||
let client = reqwest::ClientBuilder::new()
|
||||
.cookie_store(true)
|
||||
.build()
|
||||
.expect("init reqwest");
|
||||
Self {
|
||||
client,
|
||||
test_hostname,
|
||||
}
|
||||
}
|
||||
Self {
|
||||
client,
|
||||
hostname,
|
||||
ssl: false,
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn new() -> Self {
|
||||
use leptos_use::use_document;
|
||||
let hostname = use_document().location().unwrap().host().unwrap();
|
||||
let ssl = !cfg!(debug_assertions);
|
||||
Self { hostname, ssl }
|
||||
}
|
||||
|
||||
async fn get<T, R>(&self, endpoint: &str, query: Option<R>) -> Option<T>
|
||||
async fn get<T, R>(&self, endpoint: &str, query: Option<R>) -> FrontendResult<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
R: Serialize + Debug,
|
||||
{
|
||||
result_to_option(self.send(Method::GET, endpoint, query).await)
|
||||
self.send(Method::GET, endpoint, query).await
|
||||
}
|
||||
|
||||
async fn post<T, R>(&self, endpoint: &str, query: Option<R>) -> Result<T, ServerFnError>
|
||||
async fn post<T, R>(&self, endpoint: &str, query: Option<R>) -> FrontendResult<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
R: Serialize + Debug,
|
||||
|
@ -68,7 +55,7 @@ impl ApiClient {
|
|||
self.send(Method::POST, endpoint, query).await
|
||||
}
|
||||
|
||||
async fn patch<T, R>(&self, endpoint: &str, query: Option<R>) -> Result<T, ServerFnError>
|
||||
async fn patch<T, R>(&self, endpoint: &str, query: Option<R>) -> FrontendResult<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
R: Serialize + Debug,
|
||||
|
@ -77,12 +64,7 @@ impl ApiClient {
|
|||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn send<P, T>(
|
||||
&self,
|
||||
method: Method,
|
||||
path: &str,
|
||||
params: Option<P>,
|
||||
) -> Result<T, ServerFnError>
|
||||
async fn send<P, T>(&self, method: Method, path: &str, params: Option<P>) -> FrontendResult<T>
|
||||
where
|
||||
P: Serialize + Debug,
|
||||
T: for<'de> Deserialize<'de>,
|
||||
|
@ -90,9 +72,10 @@ impl ApiClient {
|
|||
use crate::common::{Auth, AUTH_COOKIE};
|
||||
use leptos::prelude::use_context;
|
||||
use reqwest::header::HeaderName;
|
||||
|
||||
let mut req = self
|
||||
.client
|
||||
.request(method.clone(), self.request_endpoint(path));
|
||||
.request(method.clone(), self.request_endpoint(path)?);
|
||||
req = if method == Method::GET {
|
||||
req.query(¶ms)
|
||||
} else {
|
||||
|
@ -115,7 +98,7 @@ impl ApiClient {
|
|||
method: Method,
|
||||
path: &'a str,
|
||||
params: Option<P>,
|
||||
) -> impl std::future::Future<Output = Result<T, ServerFnError>> + Send + 'a
|
||||
) -> impl std::future::Future<Output = FrontendResult<T>> + Send + 'a
|
||||
where
|
||||
P: Serialize + Debug + 'a,
|
||||
T: for<'de> Deserialize<'de>,
|
||||
|
@ -136,8 +119,8 @@ impl ApiClient {
|
|||
}
|
||||
});
|
||||
|
||||
let path_with_endpoint = self.request_endpoint(path);
|
||||
let params_encoded = serde_urlencoded::to_string(¶ms).unwrap();
|
||||
let path_with_endpoint = self.request_endpoint(path)?;
|
||||
let params_encoded = serde_urlencoded::to_string(¶ms)?;
|
||||
let path = if method == Method::GET {
|
||||
// Cannot pass the form data directly but need to convert it manually
|
||||
// https://github.com/rustwasm/gloo/issues/378
|
||||
|
@ -156,8 +139,7 @@ impl ApiClient {
|
|||
.body(params_encoded)
|
||||
} else {
|
||||
builder.build()
|
||||
}
|
||||
.unwrap();
|
||||
}?;
|
||||
let res = req.send().await?;
|
||||
let status = res.status();
|
||||
let text = res.text().await?;
|
||||
|
@ -165,34 +147,53 @@ impl ApiClient {
|
|||
})
|
||||
}
|
||||
|
||||
fn response<T>(status: u16, text: String, url: &str) -> Result<T, ServerFnError>
|
||||
fn response<T>(status: u16, text: String, url: &str) -> FrontendResult<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let json = serde_json::from_str(&text).map_err(|e| {
|
||||
info!("Failed to deserialize api response: {e} from {text} on {url}");
|
||||
ServerFnError::<NoCustomError>::Deserialization(text.clone())
|
||||
FrontendError::new(&text)
|
||||
})?;
|
||||
if status == StatusCode::OK {
|
||||
Ok(json)
|
||||
} else {
|
||||
info!("API error: {text} on {url} status {status}");
|
||||
Err(ServerFnError::Response(text))
|
||||
Err(FrontendError::new(text))
|
||||
}
|
||||
}
|
||||
|
||||
fn request_endpoint(&self, path: &str) -> String {
|
||||
let protocol = if self.ssl { "https" } else { "http" };
|
||||
format!("{protocol}://{}{path}", &self.hostname)
|
||||
}
|
||||
}
|
||||
fn request_endpoint(&self, path: &str) -> FrontendResult<String> {
|
||||
let protocol = if cfg!(debug_assertions) {
|
||||
"http"
|
||||
} else {
|
||||
"https"
|
||||
};
|
||||
|
||||
fn result_to_option<T>(val: Result<T, ServerFnError>) -> Option<T> {
|
||||
match val {
|
||||
Ok(v) => Some(v),
|
||||
Err(e) => {
|
||||
error!("API error: {e}");
|
||||
None
|
||||
let hostname: String;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use leptos::{config::LeptosOptions, prelude::use_context};
|
||||
hostname = self
|
||||
.test_hostname
|
||||
.clone()
|
||||
.or_else(|| use_context::<LeptosOptions>().map(|o| o.site_addr.to_string()))
|
||||
// Needed because during tests App() gets initialized from backend
|
||||
// generate_route_list() which attempts to load some resources without providing
|
||||
// LeptosOptions. Returning an error results in hydration errors, but an invalid
|
||||
// host seems fine.
|
||||
// TODO: maybe can change this to Err after unwraps are all removed
|
||||
.unwrap_or_else(|| "localhost".to_string());
|
||||
}
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
use leptos::prelude::location;
|
||||
hostname = location()
|
||||
.host()
|
||||
.map_err(|e| FrontendError::new(format!("Failed to get hostname: {:?}", e)))?;
|
||||
}
|
||||
|
||||
Ok(format!("{protocol}://{}{path}", hostname))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +1,46 @@
|
|||
use super::{result_to_option, ApiClient};
|
||||
use crate::common::{
|
||||
article::{EditView, GetEditList},
|
||||
newtypes::PersonId,
|
||||
user::{
|
||||
DbPerson,
|
||||
GetUserParams,
|
||||
LocalUserView,
|
||||
LoginUserParams,
|
||||
RegisterUserParams,
|
||||
UpdateUserParams,
|
||||
use super::ApiClient;
|
||||
use crate::{
|
||||
common::{
|
||||
article::{EditView, GetEditList},
|
||||
newtypes::PersonId,
|
||||
user::{
|
||||
DbPerson,
|
||||
GetUserParams,
|
||||
LocalUserView,
|
||||
LoginUserParams,
|
||||
RegisterUserParams,
|
||||
UpdateUserParams,
|
||||
},
|
||||
SuccessResponse,
|
||||
},
|
||||
SuccessResponse,
|
||||
frontend::utils::errors::FrontendResult,
|
||||
};
|
||||
use leptos::prelude::ServerFnError;
|
||||
|
||||
impl ApiClient {
|
||||
pub async fn register(
|
||||
&self,
|
||||
params: RegisterUserParams,
|
||||
) -> Result<LocalUserView, ServerFnError> {
|
||||
pub async fn register(&self, params: RegisterUserParams) -> FrontendResult<LocalUserView> {
|
||||
self.post("/api/v1/account/register", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn login(&self, params: LoginUserParams) -> Result<LocalUserView, ServerFnError> {
|
||||
pub async fn login(&self, params: LoginUserParams) -> FrontendResult<LocalUserView> {
|
||||
self.post("/api/v1/account/login", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn logout(&self) -> Option<SuccessResponse> {
|
||||
result_to_option(self.post("/api/v1/account/logout", None::<()>).await)
|
||||
pub async fn logout(&self) -> FrontendResult<SuccessResponse> {
|
||||
self.post("/api/v1/account/logout", None::<()>).await
|
||||
}
|
||||
|
||||
pub async fn get_user(&self, data: GetUserParams) -> Option<DbPerson> {
|
||||
pub async fn get_user(&self, data: GetUserParams) -> FrontendResult<DbPerson> {
|
||||
self.get("/api/v1/user", Some(data)).await
|
||||
}
|
||||
|
||||
pub async fn update_user_profile(
|
||||
&self,
|
||||
data: UpdateUserParams,
|
||||
) -> Result<SuccessResponse, ServerFnError> {
|
||||
) -> FrontendResult<SuccessResponse> {
|
||||
self.post("/api/v1/account/update", Some(data)).await
|
||||
}
|
||||
|
||||
pub async fn get_person_edits(&self, person_id: PersonId) -> Option<Vec<EditView>> {
|
||||
pub async fn get_person_edits(&self, person_id: PersonId) -> FrontendResult<Vec<EditView>> {
|
||||
let data = GetEditList {
|
||||
person_id: Some(person_id),
|
||||
..Default::default()
|
||||
|
|
|
@ -9,15 +9,10 @@ use crate::frontend::{
|
|||
discussion::ArticleDiscussion,
|
||||
edit::EditArticle,
|
||||
history::ArticleHistory,
|
||||
list::ListArticles,
|
||||
read::ReadArticle,
|
||||
},
|
||||
instance::{
|
||||
details::InstanceDetails,
|
||||
list::ListInstances,
|
||||
search::Search,
|
||||
settings::InstanceSettings,
|
||||
},
|
||||
explore::Explore,
|
||||
instance::{details::InstanceDetails, search::Search, settings::InstanceSettings},
|
||||
user::{
|
||||
edit_profile::UserEditProfile,
|
||||
login::Login,
|
||||
|
@ -26,7 +21,7 @@ use crate::frontend::{
|
|||
register::Register,
|
||||
},
|
||||
},
|
||||
utils::{dark_mode::DarkMode, formatting::instance_title},
|
||||
utils::{dark_mode::DarkMode, errors::ErrorPopup, formatting::instance_title},
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::{provide_meta_context, *};
|
||||
|
@ -57,16 +52,16 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
|||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
|
||||
let site_resource = Resource::new(|| (), |_| async move { CLIENT.site().await.unwrap() });
|
||||
let site_resource = Resource::new(|| (), |_| async move { CLIENT.site().await });
|
||||
provide_context(site_resource);
|
||||
|
||||
let instance = Resource::new(|| (), |_| async move { CLIENT.get_local_instance().await });
|
||||
|
||||
let darkmode = DarkMode::init();
|
||||
provide_context(darkmode.clone());
|
||||
|
||||
let instance = Resource::new(
|
||||
|| (),
|
||||
|_| async move { CLIENT.get_local_instance().await.unwrap() },
|
||||
);
|
||||
ErrorPopup::init();
|
||||
|
||||
view! {
|
||||
<Html attr:data-theme=darkmode.theme {..} class="h-full" />
|
||||
<Body {..} class="h-full max-sm:flex max-sm:flex-col" />
|
||||
|
@ -74,37 +69,40 @@ pub fn App() -> impl IntoView {
|
|||
<Stylesheet id="ibis" href="/pkg/ibis.css" />
|
||||
<Stylesheet id="katex" href="/katex.min.css" />
|
||||
<Router>
|
||||
<Suspense>
|
||||
{move || {
|
||||
instance
|
||||
.get()
|
||||
.map(|i| {
|
||||
let formatter = move |text| {
|
||||
format!("{text} — {}", instance_title(&i.instance))
|
||||
};
|
||||
view! { <Title formatter /> }
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
<Nav />
|
||||
<main class="p-4 md:ml-64">
|
||||
<Suspense>
|
||||
{move || Suspend::new(async move {
|
||||
instance
|
||||
.await
|
||||
.map(|i| {
|
||||
let formatter = move |text| {
|
||||
format!("{text} — {}", instance_title(&i.instance))
|
||||
};
|
||||
view! { <Title formatter /> }
|
||||
})
|
||||
})}
|
||||
</Suspense>
|
||||
<Show when=move || ErrorPopup::get().is_some()>
|
||||
<div class="toast">
|
||||
<div class="alert alert-error">
|
||||
<span>{ErrorPopup::get()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Routes fallback=|| "Page not found.".into_view()>
|
||||
<Route path=path!("/") view=ReadArticle />
|
||||
<Route path=path!("/article/:title") view=ReadArticle />
|
||||
<Route path=path!("/article/:title/discussion") view=ArticleDiscussion />
|
||||
<Route path=path!("/article/:title/history") view=ArticleHistory />
|
||||
<IbisProtectedRoute
|
||||
path=path!("/article/:title/edit/:conflict_id?")
|
||||
view=EditArticle
|
||||
/>
|
||||
<IbisProtectedRoute path=path!("/article/:title/edit") view=EditArticle />
|
||||
<IbisProtectedRoute
|
||||
path=path!("/article/:title/actions")
|
||||
view=ArticleActions
|
||||
/>
|
||||
<Route path=path!("/article/:title/diff/:hash") view=EditDiff />
|
||||
<IbisProtectedRoute path=path!("/create-article") view=CreateArticle />
|
||||
<Route path=path!("/articles") view=ListArticles />
|
||||
<Route path=path!("/instances") view=ListInstances />
|
||||
<Route path=path!("/explore") view=Explore />
|
||||
<Route path=path!("/instance/:hostname") view=InstanceDetails />
|
||||
<Route path=path!("/user/:name") view=UserProfile />
|
||||
<Route path=path!("/login") view=Login />
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{
|
||||
common::{article::DbArticleView, validation::can_edit_article},
|
||||
frontend::utils::{
|
||||
errors::FrontendResult,
|
||||
formatting::{article_path, article_title},
|
||||
resources::{is_admin, is_logged_in},
|
||||
},
|
||||
|
@ -9,6 +10,7 @@ use leptos::prelude::*;
|
|||
use leptos_meta::Title;
|
||||
use leptos_router::components::A;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum ActiveTab {
|
||||
Read,
|
||||
Discussion,
|
||||
|
@ -18,21 +20,24 @@ pub enum ActiveTab {
|
|||
}
|
||||
|
||||
#[component]
|
||||
pub fn ArticleNav(article: Resource<DbArticleView>, active_tab: ActiveTab) -> impl IntoView {
|
||||
let tab_classes = tab_classes(&active_tab);
|
||||
pub fn ArticleNav(
|
||||
article: Resource<FrontendResult<DbArticleView>>,
|
||||
active_tab: ActiveTab,
|
||||
) -> impl IntoView {
|
||||
let tab_classes = tab_classes(active_tab);
|
||||
|
||||
view! {
|
||||
<Suspense>
|
||||
{move || {
|
||||
{move || Suspend::new(async move {
|
||||
article
|
||||
.get()
|
||||
.await
|
||||
.map(|article_| {
|
||||
let title = article_title(&article_.article);
|
||||
let article_link = article_path(&article_.article);
|
||||
let article_link_ = article_link.clone();
|
||||
let protected = article_.article.protected;
|
||||
view! {
|
||||
<Title text=page_title(&active_tab, &title) />
|
||||
<Title text=page_title(active_tab, &title) />
|
||||
<div role="tablist" class="tabs tabs-lifted">
|
||||
<A href=article_link.clone() {..} class=tab_classes.read>
|
||||
"Read"
|
||||
|
@ -90,13 +95,13 @@ pub fn ArticleNav(article: Resource<DbArticleView>, active_tab: ActiveTab) -> im
|
|||
</div>
|
||||
}
|
||||
})
|
||||
}}
|
||||
})}
|
||||
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
struct ActiveTabClasses {
|
||||
struct ActiveTab2Classes {
|
||||
read: &'static str,
|
||||
discussion: &'static str,
|
||||
history: &'static str,
|
||||
|
@ -104,10 +109,10 @@ struct ActiveTabClasses {
|
|||
actions: &'static str,
|
||||
}
|
||||
|
||||
fn tab_classes(active_tab: &ActiveTab) -> ActiveTabClasses {
|
||||
fn tab_classes(active_tab: ActiveTab) -> ActiveTab2Classes {
|
||||
const TAB_INACTIVE: &str = "tab";
|
||||
const TAB_ACTIVE: &str = "tab tab-active";
|
||||
let mut classes = ActiveTabClasses {
|
||||
let mut classes = ActiveTab2Classes {
|
||||
read: TAB_INACTIVE,
|
||||
discussion: TAB_INACTIVE,
|
||||
history: TAB_INACTIVE,
|
||||
|
@ -124,7 +129,7 @@ fn tab_classes(active_tab: &ActiveTab) -> ActiveTabClasses {
|
|||
classes
|
||||
}
|
||||
|
||||
fn page_title(active_tab: &ActiveTab, article_title: &str) -> String {
|
||||
fn page_title(active_tab: ActiveTab, article_title: &str) -> String {
|
||||
let active = match active_tab {
|
||||
ActiveTab::Read => return article_title.to_string(),
|
||||
ActiveTab::Discussion => "Discuss",
|
||||
|
|
|
@ -9,8 +9,9 @@ use crate::{
|
|||
components::comment_editor::{CommentEditorView, EditParams},
|
||||
markdown::render_comment_markdown,
|
||||
utils::{
|
||||
errors::{FrontendResult, FrontendResultExt},
|
||||
formatting::{time_ago, user_link},
|
||||
resources::{site, DefaultResource},
|
||||
resources::my_profile,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -18,7 +19,7 @@ use leptos::prelude::*;
|
|||
|
||||
#[component]
|
||||
pub fn CommentView(
|
||||
article: Resource<DbArticleView>,
|
||||
article: Resource<FrontendResult<DbArticleView>>,
|
||||
comment: DbCommentView,
|
||||
show_editor: (ReadSignal<CommentId>, WriteSignal<CommentId>),
|
||||
) -> impl IntoView {
|
||||
|
@ -36,6 +37,7 @@ pub fn CommentView(
|
|||
"/article/{}/discussion#{comment_id}",
|
||||
article
|
||||
.get()
|
||||
.and_then(|a| a.ok())
|
||||
.map(|a| a.article.title.clone())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
@ -46,12 +48,14 @@ pub fn CommentView(
|
|||
deleted: Some(!comment_change_signal.0.get_untracked().deleted),
|
||||
content: None,
|
||||
};
|
||||
let comment = CLIENT.edit_comment(¶ms).await.unwrap();
|
||||
comment_change_signal.1.set(comment.comment);
|
||||
CLIENT
|
||||
.edit_comment(¶ms)
|
||||
.await
|
||||
.error_popup(|comment| comment_change_signal.1.set(comment.comment));
|
||||
});
|
||||
|
||||
let is_creator = site().with_default(|site| site.my_profile.as_ref().map(|p| p.person.id))
|
||||
== Some(comment.comment.creator_id);
|
||||
let is_creator =
|
||||
my_profile().map(|my_profile| my_profile.person.id) == Some(comment.comment.creator_id);
|
||||
|
||||
let edit_params = EditParams {
|
||||
comment: comment.comment.clone(),
|
||||
|
|
|
@ -4,7 +4,10 @@ use crate::{
|
|||
comment::{CreateCommentParams, DbComment, EditCommentParams},
|
||||
newtypes::CommentId,
|
||||
},
|
||||
frontend::api::CLIENT,
|
||||
frontend::{
|
||||
api::CLIENT,
|
||||
utils::errors::{FrontendResult, FrontendResultExt},
|
||||
},
|
||||
};
|
||||
use leptos::{html::Textarea, prelude::*};
|
||||
use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn};
|
||||
|
@ -18,7 +21,7 @@ pub struct EditParams {
|
|||
|
||||
#[component]
|
||||
pub fn CommentEditorView(
|
||||
article: Resource<DbArticleView>,
|
||||
article: Resource<FrontendResult<DbArticleView>>,
|
||||
#[prop(optional)] parent_id: Option<CommentId>,
|
||||
/// Set this to CommentId(-1) to hide all editors
|
||||
#[prop(optional)]
|
||||
|
@ -47,20 +50,22 @@ pub fn CommentEditorView(
|
|||
content: Some(content.get_untracked()),
|
||||
deleted: None,
|
||||
};
|
||||
let comment = CLIENT.edit_comment(¶ms).await.unwrap();
|
||||
edit_params.set_comment.set(comment.comment);
|
||||
edit_params.set_is_editing.set(false);
|
||||
CLIENT.edit_comment(¶ms).await.error_popup(|comment| {
|
||||
edit_params.set_comment.set(comment.comment);
|
||||
edit_params.set_is_editing.set(false);
|
||||
});
|
||||
} else {
|
||||
let params = CreateCommentParams {
|
||||
content: content.get_untracked(),
|
||||
article_id: article.await.article.id,
|
||||
article_id: article.await.map(|a| a.article.id).unwrap_or_default(),
|
||||
parent_id,
|
||||
};
|
||||
CLIENT.create_comment(¶ms).await.unwrap();
|
||||
article.refetch();
|
||||
if let Some(set_show_editor) = set_show_editor {
|
||||
set_show_editor.set(CommentId(-1));
|
||||
}
|
||||
CLIENT.create_comment(¶ms).await.error_popup(|_| {
|
||||
article.refetch();
|
||||
if let Some(set_show_editor) = set_show_editor {
|
||||
set_show_editor.set(CommentId(-1));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::frontend::api::CLIENT;
|
||||
use crate::frontend::{api::CLIENT, utils::errors::FrontendResultExt};
|
||||
use codee::{Decoder, Encoder};
|
||||
use leptos::prelude::*;
|
||||
use std::fmt::Debug;
|
||||
|
@ -17,10 +17,9 @@ where
|
|||
{
|
||||
let connect_ibis_wiki = Action::new(move |_: &()| async move {
|
||||
CLIENT
|
||||
.resolve_instance(Url::parse("https://ibis.wiki").unwrap())
|
||||
.resolve_instance(Url::parse("https://ibis.wiki").expect("parse ibis.wiki url"))
|
||||
.await
|
||||
.unwrap();
|
||||
res.refetch();
|
||||
.error_popup(|_| res.refetch());
|
||||
});
|
||||
|
||||
view! {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use leptos::{ev::KeyboardEvent, prelude::*};
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn CredentialsForm(
|
||||
|
@ -33,15 +33,8 @@ pub fn CredentialsForm(
|
|||
class="input input-primary input-bordered"
|
||||
required
|
||||
placeholder="Username"
|
||||
bind:value=(username, set_username)
|
||||
prop:disabled=move || disabled.get()
|
||||
on:keyup=move |ev: KeyboardEvent| {
|
||||
let val = event_target_value(&ev);
|
||||
set_username.update(|v| *v = val);
|
||||
}
|
||||
on:change=move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
set_username.update(|v| *v = val);
|
||||
}
|
||||
/>
|
||||
<div class="h-2"></div>
|
||||
<input
|
||||
|
@ -50,21 +43,7 @@ pub fn CredentialsForm(
|
|||
required
|
||||
placeholder="Password"
|
||||
prop:disabled=move || disabled.get()
|
||||
on:keyup=move |ev: KeyboardEvent| {
|
||||
match &*ev.key() {
|
||||
"Enter" => {
|
||||
dispatch_action();
|
||||
}
|
||||
_ => {
|
||||
let val = event_target_value(&ev);
|
||||
set_password.update(|p| *p = val);
|
||||
}
|
||||
}
|
||||
}
|
||||
on:change=move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
set_password.update(|p| *p = val);
|
||||
}
|
||||
bind:value=(password, set_password)
|
||||
/>
|
||||
|
||||
<div>
|
||||
|
|
|
@ -5,7 +5,10 @@ use crate::{
|
|||
},
|
||||
frontend::{
|
||||
api::CLIENT,
|
||||
utils::resources::{site, DefaultResource},
|
||||
utils::{
|
||||
errors::FrontendResultExt,
|
||||
resources::{my_profile, site},
|
||||
},
|
||||
},
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
|
@ -16,16 +19,14 @@ pub fn InstanceFollowButton(instance: DbInstance) -> impl IntoView {
|
|||
let instance_id = *instance_id;
|
||||
async move {
|
||||
let params = FollowInstanceParams { id: instance_id };
|
||||
CLIENT.follow_instance(params).await.unwrap();
|
||||
site().refetch();
|
||||
CLIENT
|
||||
.follow_instance(params)
|
||||
.await
|
||||
.error_popup(|_| site().refetch());
|
||||
}
|
||||
});
|
||||
let is_following = site()
|
||||
.with_default(|site| {
|
||||
site.clone()
|
||||
.my_profile
|
||||
.map(|p| p.following.contains(&instance))
|
||||
})
|
||||
let is_following = my_profile()
|
||||
.map(|my_profile| my_profile.following.contains(&instance))
|
||||
.unwrap_or(false);
|
||||
let follow_text = if is_following {
|
||||
"Following instance"
|
||||
|
|
|
@ -8,3 +8,4 @@ pub mod edit_list;
|
|||
pub mod instance_follow_button;
|
||||
pub mod nav;
|
||||
pub mod protected_route;
|
||||
pub mod suspense_error;
|
||||
|
|
|
@ -2,8 +2,9 @@ use crate::frontend::{
|
|||
api::CLIENT,
|
||||
utils::{
|
||||
dark_mode::DarkMode,
|
||||
errors::FrontendResultExt,
|
||||
formatting::instance_title,
|
||||
resources::{is_admin, is_logged_in, site, DefaultResource},
|
||||
resources::{config, is_admin, is_logged_in, my_profile, site},
|
||||
},
|
||||
};
|
||||
use leptos::{component, prelude::*, view, IntoView, *};
|
||||
|
@ -12,17 +13,13 @@ use leptos_router::hooks::use_navigate;
|
|||
#[component]
|
||||
pub fn Nav() -> impl IntoView {
|
||||
let logout_action = Action::new(move |_| async move {
|
||||
CLIENT.logout().await.unwrap();
|
||||
site().refetch();
|
||||
CLIENT.logout().await.error_popup(|_| site().refetch());
|
||||
});
|
||||
let notification_count = Resource::new(
|
||||
|| (),
|
||||
move |_| async move { CLIENT.notifications_count().await.unwrap_or_default() },
|
||||
);
|
||||
let instance = Resource::new(
|
||||
|| (),
|
||||
|_| async move { CLIENT.get_local_instance().await.unwrap() },
|
||||
);
|
||||
let instance = Resource::new(|| (), |_| async move { CLIENT.get_local_instance().await });
|
||||
|
||||
let (search_query, set_search_query) = signal(String::new());
|
||||
let mut dark_mode = expect_context::<DarkMode>();
|
||||
|
@ -41,17 +38,16 @@ pub fn Nav() -> impl IntoView {
|
|||
<img src="/logo.png" class="m-auto max-sm:hidden" />
|
||||
</a>
|
||||
<h2 class="m-4 font-serif text-xl font-bold">
|
||||
{move || { instance.get().map(|i| instance_title(&i.instance)) }}
|
||||
{move || Suspend::new(async move {
|
||||
instance.await.map(|i| instance_title(&i.instance))
|
||||
})}
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">"Main Page"</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/instances">"Instances"</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/articles">"Articles"</a>
|
||||
<a href="/explore">"Explore"</a>
|
||||
</li>
|
||||
<Show when=is_logged_in>
|
||||
<li>
|
||||
|
@ -109,9 +105,7 @@ pub fn Nav() -> impl IntoView {
|
|||
<li>
|
||||
<a href="/login">"Login"</a>
|
||||
</li>
|
||||
<Show when=move || {
|
||||
site().with_default(|s| s.config.registration_open)
|
||||
}>
|
||||
<Show when=move || config().registration_open>
|
||||
<li>
|
||||
<a href="/register">"Register"</a>
|
||||
</li>
|
||||
|
@ -120,29 +114,31 @@ pub fn Nav() -> impl IntoView {
|
|||
}
|
||||
>
|
||||
|
||||
{
|
||||
let my_profile = site()
|
||||
.with_default(|site| site.clone().my_profile.unwrap());
|
||||
let profile_link = format!("/user/{}", my_profile.person.username);
|
||||
view! {
|
||||
<p class="self-center">
|
||||
"Logged in as " <a class="link" href=profile_link>
|
||||
{my_profile.person.username}
|
||||
{my_profile()
|
||||
.map(|my_profile| {
|
||||
let profile_link = format!(
|
||||
"/user/{}",
|
||||
my_profile.person.username,
|
||||
);
|
||||
view! {
|
||||
<p class="self-center">
|
||||
"Logged in as " <a class="link" href=profile_link>
|
||||
{my_profile.person.username}
|
||||
</a>
|
||||
</p>
|
||||
<a class="self-center py-2 link" href="/edit_profile">
|
||||
Edit Profile
|
||||
</a>
|
||||
</p>
|
||||
<a class="self-center py-2 link" href="/edit_profile">
|
||||
Edit Profile
|
||||
</a>
|
||||
<button
|
||||
class="self-center w-min btn btn-outline btn-xs"
|
||||
on:click=move |_| {
|
||||
logout_action.dispatch(());
|
||||
}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
}
|
||||
}
|
||||
<button
|
||||
class="self-center w-min btn btn-outline btn-xs"
|
||||
on:click=move |_| {
|
||||
logout_action.dispatch(());
|
||||
}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
}
|
||||
})}
|
||||
|
||||
</Show>
|
||||
<div class="grow min-h-2"></div>
|
||||
|
|
44
src/frontend/components/suspense_error.rs
Normal file
44
src/frontend/components/suspense_error.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use crate::frontend::{
|
||||
pages::article_title_param,
|
||||
utils::{errors::FrontendResult, resources::is_logged_in},
|
||||
};
|
||||
use leptos::{either::Either, prelude::*};
|
||||
|
||||
#[component]
|
||||
pub fn SuspenseError<T>(children: ChildrenFn, result: Resource<FrontendResult<T>>) -> impl IntoView
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
{
|
||||
view! {
|
||||
<Suspense fallback=|| {
|
||||
view! { "Loading..." }
|
||||
}>
|
||||
{move || {
|
||||
if let Some(Err(e)) = result.get() {
|
||||
let article_title = article_title_param();
|
||||
let href = format!(
|
||||
"/create-article?title={}",
|
||||
article_title.clone().unwrap_or_default(),
|
||||
);
|
||||
Either::Left(
|
||||
view! {
|
||||
<div class="grid place-items-center h-screen">
|
||||
<div>
|
||||
<div class="alert alert-error w-fit">{e.message()}</div>
|
||||
<Show when=move || article_title.is_some() && is_logged_in()>
|
||||
<a class="mt-4 btn" href=href.clone()>
|
||||
Create Article
|
||||
</a>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Either::Right(children())
|
||||
}
|
||||
}}
|
||||
|
||||
</Suspense>
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
#![deny(clippy::unwrap_used)]
|
||||
|
||||
use article_link::ArticleLinkScanner;
|
||||
use markdown_it::{
|
||||
plugins::cmark::block::{heading::ATXHeading, lheading::SetextHeader},
|
||||
|
|
|
@ -5,7 +5,10 @@ use crate::{
|
|||
},
|
||||
frontend::{
|
||||
api::CLIENT,
|
||||
components::article_nav::{ActiveTab, ArticleNav},
|
||||
components::{
|
||||
article_nav::{ActiveTab, ArticleNav},
|
||||
suspense_error::SuspenseError,
|
||||
},
|
||||
pages::article_resource,
|
||||
utils::{formatting::article_path, resources::is_admin},
|
||||
DbArticle,
|
||||
|
@ -54,12 +57,10 @@ pub fn ArticleActions() -> impl IntoView {
|
|||
});
|
||||
view! {
|
||||
<ArticleNav article=article active_tab=ActiveTab::Actions />
|
||||
<Suspense fallback=|| {
|
||||
view! { "Loading..." }
|
||||
}>
|
||||
{move || {
|
||||
<SuspenseError result=article>
|
||||
{move || Suspend::new(async move {
|
||||
article
|
||||
.get()
|
||||
.await
|
||||
.map(|article| {
|
||||
view! {
|
||||
<div>
|
||||
|
@ -108,12 +109,9 @@ pub fn ArticleActions() -> impl IntoView {
|
|||
</div>
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
||||
</Suspense>
|
||||
<Show when=move || fork_response.get().is_some()>
|
||||
<Redirect path=article_path(&fork_response.get().unwrap()) />
|
||||
</Show>
|
||||
})}
|
||||
{fork_response.get().map(|article| view! { <Redirect path=article_path(&article) /> })}
|
||||
</SuspenseError>
|
||||
<p>"TODO: add option for admin to delete article etc"</p>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,17 +3,24 @@ use crate::{
|
|||
frontend::{
|
||||
api::CLIENT,
|
||||
components::article_editor::EditorView,
|
||||
utils::resources::{is_admin, site, DefaultResource},
|
||||
utils::resources::{config, is_admin},
|
||||
},
|
||||
};
|
||||
use leptos::{html::Textarea, prelude::*};
|
||||
use leptos_meta::Title;
|
||||
use leptos_router::components::Redirect;
|
||||
use leptos_router::{components::Redirect, hooks::use_query_map};
|
||||
use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn};
|
||||
|
||||
#[component]
|
||||
pub fn CreateArticle() -> impl IntoView {
|
||||
let (title, set_title) = signal(String::new());
|
||||
let title = use_query_map()
|
||||
.get()
|
||||
.get("title")
|
||||
.unwrap_or_default()
|
||||
.replace('_', " ");
|
||||
let title = title.split_once('@').map(|(t, _)| t).unwrap_or(&title);
|
||||
let (title, set_title) = signal(title.to_string());
|
||||
|
||||
let textarea_ref = NodeRef::<Textarea>::new();
|
||||
let UseTextareaAutosizeReturn {
|
||||
content,
|
||||
|
@ -52,9 +59,7 @@ pub fn CreateArticle() -> impl IntoView {
|
|||
}
|
||||
}
|
||||
});
|
||||
let show_approval_message = Signal::derive(move || {
|
||||
site().with_default(|site| site.config.article_approval) && !is_admin()
|
||||
});
|
||||
let show_approval_message = Signal::derive(move || config().article_approval && !is_admin());
|
||||
|
||||
view! {
|
||||
<Title text="Create new Article" />
|
||||
|
@ -76,6 +81,7 @@ pub fn CreateArticle() -> impl IntoView {
|
|||
type="text"
|
||||
required
|
||||
placeholder="Title"
|
||||
value=title
|
||||
prop:disabled=move || wait_for_response.get()
|
||||
on:keyup=move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
use crate::frontend::{
|
||||
components::article_nav::{ActiveTab, ArticleNav},
|
||||
components::{
|
||||
article_nav::{ActiveTab, ArticleNav},
|
||||
suspense_error::SuspenseError,
|
||||
},
|
||||
pages::{article_edits_resource, article_resource},
|
||||
utils::formatting::{article_title, render_date_time, user_link},
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos::{either::Either, prelude::*};
|
||||
use leptos_meta::Title;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
|
||||
|
@ -11,46 +14,62 @@ use leptos_router::hooks::use_params_map;
|
|||
pub fn EditDiff() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let article = article_resource();
|
||||
let edits = article_edits_resource(article);
|
||||
|
||||
view! {
|
||||
<ArticleNav article=article active_tab=ActiveTab::History />
|
||||
<Suspense fallback=|| {
|
||||
view! { "Loading..." }
|
||||
}>
|
||||
<SuspenseError result=article>
|
||||
{move || Suspend::new(async move {
|
||||
let edits = article_edits_resource(article).await;
|
||||
let hash = params.get_untracked().get("hash").clone().unwrap();
|
||||
let edit = edits.iter().find(|e| e.edit.hash.0.to_string() == hash).unwrap();
|
||||
let label = format!(
|
||||
"{} ({})",
|
||||
edit.edit.summary,
|
||||
render_date_time(edit.edit.published),
|
||||
);
|
||||
let pending = edit.edit.pending;
|
||||
let title = format!(
|
||||
"Diff {} — {}",
|
||||
edit.edit.summary,
|
||||
article_title(&article.await.article),
|
||||
);
|
||||
view! {
|
||||
<Title text=title />
|
||||
<div class="flex w-full">
|
||||
<h2 class="my-2 font-serif text-xl font-bold grow">{label}</h2>
|
||||
<Show when=move || pending>
|
||||
<span class="p-1 w-min rounded border-2 border-rose-300 h-min">
|
||||
Pending
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
<p>"by " {user_link(&edit.creator)}</p>
|
||||
<div class="max-w-full prose prose-slate">
|
||||
<pre class="text-wrap">
|
||||
<code>{edit.edit.diff.clone()}</code>
|
||||
</pre>
|
||||
</div>
|
||||
}
|
||||
let article_title = article
|
||||
.await
|
||||
.map(|a| article_title(&a.article))
|
||||
.unwrap_or_default();
|
||||
edits
|
||||
.await
|
||||
.map(|edits| {
|
||||
let hash = params.get_untracked().get("hash").clone();
|
||||
let edit = edits.iter().find(|e| Some(e.edit.hash.0.to_string()) == hash);
|
||||
if let Some(edit) = edit {
|
||||
let label = format!(
|
||||
"{} ({})",
|
||||
edit.edit.summary,
|
||||
render_date_time(edit.edit.published),
|
||||
);
|
||||
let pending = edit.edit.pending;
|
||||
let title = format!("Diff {} — {}", edit.edit.summary, article_title);
|
||||
Either::Left(
|
||||
view! {
|
||||
<Title text=title />
|
||||
<div class="flex w-full">
|
||||
<h2 class="my-2 font-serif text-xl font-bold grow">
|
||||
{label}
|
||||
</h2>
|
||||
<Show when=move || pending>
|
||||
<span class="p-1 w-min rounded border-2 border-rose-300 h-min">
|
||||
Pending
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
<p>"by " {user_link(&edit.creator)}</p>
|
||||
<div class="max-w-full prose prose-slate">
|
||||
<pre class="text-wrap">
|
||||
<code>{edit.edit.diff.clone()}</code>
|
||||
</pre>
|
||||
</div>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Either::Right(
|
||||
view! {
|
||||
<div class="grid place-items-center h-screen">
|
||||
<div class="alert alert-error w-fit">Invalid edit</div>
|
||||
</div>
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
})}
|
||||
|
||||
</Suspense>
|
||||
</SuspenseError>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ use crate::{
|
|||
article_nav::{ActiveTab, ArticleNav},
|
||||
comment::CommentView,
|
||||
comment_editor::CommentEditorView,
|
||||
suspense_error::SuspenseError,
|
||||
},
|
||||
pages::article_resource,
|
||||
},
|
||||
|
@ -20,20 +21,28 @@ pub fn ArticleDiscussion() -> impl IntoView {
|
|||
|
||||
view! {
|
||||
<ArticleNav article=article active_tab=ActiveTab::Discussion />
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
<CommentEditorView article=article />
|
||||
<div>
|
||||
<For
|
||||
each=move || {
|
||||
article.get().map(|a| build_comments_tree(a.comments)).unwrap_or_default()
|
||||
}
|
||||
key=|comment| comment.comment.id
|
||||
children=move |comment: DbCommentView| {
|
||||
view! { <CommentView article comment show_editor /> }
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Suspense>
|
||||
<SuspenseError result=article>
|
||||
{move || Suspend::new(async move {
|
||||
let article2 = article.await;
|
||||
view! {
|
||||
<CommentEditorView article=article />
|
||||
<div>
|
||||
<For
|
||||
each=move || {
|
||||
article2
|
||||
.clone()
|
||||
.map(|a| build_comments_tree(a.comments))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
key=|comment| comment.comment.id
|
||||
children=move |comment: DbCommentView| {
|
||||
view! { <CommentView article comment show_editor /> }
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
</SuspenseError>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,19 +75,24 @@ fn build_comments_tree(comments: Vec<DbCommentView>) -> Vec<DbCommentView> {
|
|||
.iter()
|
||||
.map(|v| (v.comment.id, CommentNode::new(v.clone())))
|
||||
.collect();
|
||||
debug_assert!(comments.len() == map.len());
|
||||
|
||||
// Move top-level comments directly into tree vec. For comments having parent_id, move them
|
||||
// `children` of respective parent. This preserves existing order.
|
||||
let mut tree = Vec::<CommentNode>::new();
|
||||
for view in comments {
|
||||
let child = map.get(&view.comment.id).unwrap().clone();
|
||||
for view in &comments {
|
||||
let child = map
|
||||
.get(&view.comment.id)
|
||||
.expect("get comment by id")
|
||||
.clone();
|
||||
if let Some(parent_id) = &view.comment.parent_id {
|
||||
let parent = map.get_mut(parent_id).unwrap();
|
||||
let parent = map.get_mut(parent_id).expect("get parent comment by id");
|
||||
parent.children.push(child);
|
||||
} else {
|
||||
tree.push(child);
|
||||
}
|
||||
}
|
||||
debug_assert!(comments.len() == map.len());
|
||||
|
||||
// Now convert it back to flat array with correct order for rendering
|
||||
tree.into_iter().flat_map(|t| t.flatten()).collect()
|
||||
|
|
|
@ -10,13 +10,17 @@ use crate::{
|
|||
components::{
|
||||
article_editor::EditorView,
|
||||
article_nav::{ActiveTab, ArticleNav},
|
||||
suspense_error::SuspenseError,
|
||||
},
|
||||
pages::article_resource,
|
||||
},
|
||||
};
|
||||
use chrono::{Days, Utc};
|
||||
use leptos::{html::Textarea, prelude::*};
|
||||
use leptos_router::{components::Redirect, hooks::use_params_map};
|
||||
use leptos::{html::Textarea, prelude::*, task::spawn_local};
|
||||
use leptos_router::{
|
||||
components::Redirect,
|
||||
hooks::{use_params_map, use_query_map},
|
||||
};
|
||||
use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
|
@ -31,30 +35,30 @@ const CONFLICT_MESSAGE: &str = "There was an edit conflict. Resolve it manually
|
|||
#[component]
|
||||
pub fn EditArticle() -> impl IntoView {
|
||||
let article = article_resource();
|
||||
|
||||
let (edit_response, set_edit_response) = signal(EditResponse::None);
|
||||
let (edit_error, set_edit_error) = signal(None::<String>);
|
||||
|
||||
let conflict_id = move || use_params_map().get_untracked().get("conflict_id").clone();
|
||||
if let Some(conflict_id) = conflict_id() {
|
||||
Action::new(move |conflict_id: &String| {
|
||||
let conflict_id = ConflictId(conflict_id.parse().unwrap());
|
||||
async move {
|
||||
let conflict = CLIENT
|
||||
.notifications_list()
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter_map(|n| match n {
|
||||
Notification::EditConflict(c) => Some(c),
|
||||
_ => None,
|
||||
})
|
||||
.find(|c| c.id == conflict_id)
|
||||
.unwrap();
|
||||
set_edit_response.set(EditResponse::Conflict(conflict));
|
||||
set_edit_error.set(Some(CONFLICT_MESSAGE.to_string()));
|
||||
}
|
||||
let conflict_id = use_query_map().get_untracked().get("conflict_id").clone();
|
||||
if let Some(conflict_id) = conflict_id {
|
||||
let conflict_id = conflict_id.parse().map(ConflictId);
|
||||
spawn_local(async move {
|
||||
CLIENT
|
||||
.notifications_list()
|
||||
.await
|
||||
.ok()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|n| match n {
|
||||
Notification::EditConflict(c) => Some(c),
|
||||
_ => None,
|
||||
})
|
||||
.find(|c| Ok(c.id) == conflict_id)
|
||||
.map(|conflict| {
|
||||
set_edit_response.set(EditResponse::Conflict(conflict));
|
||||
set_edit_error.set(Some(CONFLICT_MESSAGE.to_string()));
|
||||
});
|
||||
})
|
||||
.dispatch(conflict_id);
|
||||
}
|
||||
|
||||
let textarea_ref = NodeRef::<Textarea>::new();
|
||||
|
@ -121,12 +125,10 @@ pub fn EditArticle() -> impl IntoView {
|
|||
when=move || edit_response.get() == EditResponse::Success
|
||||
fallback=move || {
|
||||
view! {
|
||||
<Suspense fallback=|| {
|
||||
view! { "Loading..." }
|
||||
}>
|
||||
{move || {
|
||||
<SuspenseError result=article>
|
||||
{move || Suspend::new(async move {
|
||||
article
|
||||
.get()
|
||||
.await
|
||||
.map(|mut article| {
|
||||
if let EditResponse::Conflict(conflict) = edit_response.get() {
|
||||
article.article.text = conflict.three_way_merge;
|
||||
|
@ -188,9 +190,8 @@ pub fn EditArticle() -> impl IntoView {
|
|||
</div>
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
||||
</Suspense>
|
||||
})}
|
||||
</SuspenseError>
|
||||
}
|
||||
}
|
||||
>
|
||||
|
|
|
@ -2,6 +2,7 @@ use crate::frontend::{
|
|||
components::{
|
||||
article_nav::{ActiveTab, ArticleNav},
|
||||
edit_list::EditList,
|
||||
suspense_error::SuspenseError,
|
||||
},
|
||||
pages::{article_edits_resource, article_resource},
|
||||
};
|
||||
|
@ -10,20 +11,22 @@ use leptos::prelude::*;
|
|||
#[component]
|
||||
pub fn ArticleHistory() -> impl IntoView {
|
||||
let article = article_resource();
|
||||
let edits = article_edits_resource(article);
|
||||
|
||||
view! {
|
||||
<ArticleNav article=article active_tab=ActiveTab::History />
|
||||
<Suspense fallback=|| {
|
||||
view! { "Loading..." }
|
||||
}>
|
||||
{move || {
|
||||
article_edits_resource(article)
|
||||
.get()
|
||||
<SuspenseError result=article>
|
||||
{move || Suspend::new(async move {
|
||||
edits
|
||||
.await
|
||||
.map(|edits| {
|
||||
view! { <EditList edits=edits for_article=true /> }
|
||||
view! {
|
||||
// TODO: move edits resource here? but leads to strange crash
|
||||
<EditList edits=edits for_article=true />
|
||||
}
|
||||
})
|
||||
}}
|
||||
})}
|
||||
|
||||
</Suspense>
|
||||
</SuspenseError>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
use crate::{
|
||||
common::article::ListArticlesParams,
|
||||
frontend::{
|
||||
api::CLIENT,
|
||||
components::connect::ConnectView,
|
||||
utils::{
|
||||
formatting::{article_path, article_title},
|
||||
resources::DefaultResource,
|
||||
},
|
||||
},
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::Title;
|
||||
|
||||
#[component]
|
||||
pub fn ListArticles() -> impl IntoView {
|
||||
let (only_local, set_only_local) = signal(false);
|
||||
let articles = Resource::new(
|
||||
move || only_local.get(),
|
||||
|only_local| async move {
|
||||
CLIENT
|
||||
.list_articles(ListArticlesParams {
|
||||
only_local: Some(only_local),
|
||||
instance_id: None,
|
||||
})
|
||||
.await
|
||||
},
|
||||
);
|
||||
let only_local_class = Resource::new(
|
||||
move || only_local.get(),
|
||||
|only_local| async move {
|
||||
if only_local {
|
||||
"btn rounded-r-none btn-primary"
|
||||
} else {
|
||||
"btn rounded-r-none"
|
||||
}
|
||||
.to_string()
|
||||
},
|
||||
);
|
||||
let all_class = Resource::new(
|
||||
move || only_local.get(),
|
||||
|only_local| async move {
|
||||
if !only_local {
|
||||
"btn rounded-l-none btn-primary"
|
||||
} else {
|
||||
"btn rounded-l-none"
|
||||
}
|
||||
.to_string()
|
||||
},
|
||||
);
|
||||
|
||||
view! {
|
||||
<Title text="Recently edited Articles" />
|
||||
<h1 class="my-4 font-serif text-4xl font-bold">"Recently edited Articles"</h1>
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
<div class="divide-x">
|
||||
<input
|
||||
type="button"
|
||||
value="Only Local"
|
||||
class=move || only_local_class.get()
|
||||
on:click=move |_| {
|
||||
set_only_local.set(true);
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="button"
|
||||
value="All"
|
||||
class=move || all_class.get()
|
||||
on:click=move |_| {
|
||||
set_only_local.set(false);
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Show
|
||||
when=move || {
|
||||
articles.get_default().unwrap_or_default().len() > 1 || only_local.get()
|
||||
}
|
||||
fallback=move || view! { <ConnectView res=articles /> }
|
||||
>
|
||||
<ul class="my-4 list-none">
|
||||
<For
|
||||
each=move || articles.get_default().unwrap_or_default()
|
||||
key=|article| article.id
|
||||
let:article
|
||||
>
|
||||
{
|
||||
view! {
|
||||
<li>
|
||||
<a class="text-lg link" href=article_path(&article)>
|
||||
{article_title(&article)}
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</For>
|
||||
|
||||
</ul>
|
||||
</Show>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
|
@ -4,5 +4,4 @@ pub mod diff;
|
|||
pub mod discussion;
|
||||
pub mod edit;
|
||||
pub mod history;
|
||||
pub mod list;
|
||||
pub mod read;
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
use crate::frontend::{
|
||||
components::article_nav::{ActiveTab, ArticleNav},
|
||||
components::{
|
||||
article_nav::{ActiveTab, ArticleNav},
|
||||
suspense_error::SuspenseError,
|
||||
},
|
||||
markdown::render_article_markdown,
|
||||
pages::article_resource,
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos::{either::Either, prelude::*};
|
||||
use leptos_router::hooks::use_query_map;
|
||||
|
||||
#[component]
|
||||
|
@ -14,26 +17,24 @@ pub fn ReadArticle() -> impl IntoView {
|
|||
|
||||
view! {
|
||||
<ArticleNav article=article active_tab=ActiveTab::Read />
|
||||
<Suspense fallback=|| {
|
||||
view! { "Loading..." }
|
||||
}>
|
||||
|
||||
{move || {
|
||||
article
|
||||
.get()
|
||||
.map(|article| {
|
||||
<SuspenseError result=article>
|
||||
{move || Suspend::new(async move {
|
||||
let article = article.await;
|
||||
let markdown = article.map(|a| render_article_markdown(&a.article.text));
|
||||
if let Ok(markdown) = markdown {
|
||||
Either::Right(
|
||||
view! {
|
||||
<div
|
||||
class="max-w-full prose prose-slate"
|
||||
inner_html=render_article_markdown(&article.article.text)
|
||||
></div>
|
||||
}
|
||||
})
|
||||
}} <Show when=move || edit_successful>
|
||||
<div class="max-w-full prose prose-slate" inner_html=markdown></div>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Either::Left(markdown)
|
||||
}
|
||||
})} <Show when=move || edit_successful>
|
||||
<div class="toast toast-center">
|
||||
<div class="alert alert-success">Edit successful</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Suspense>
|
||||
</SuspenseError>
|
||||
}
|
||||
}
|
||||
|
|
60
src/frontend/pages/explore.rs
Normal file
60
src/frontend/pages/explore.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use crate::{
|
||||
common::instance::DbInstance,
|
||||
frontend::{
|
||||
api::CLIENT,
|
||||
components::{connect::ConnectView, suspense_error::SuspenseError},
|
||||
utils::formatting::{instance_title_with_domain, instance_updated},
|
||||
},
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::Title;
|
||||
|
||||
#[component]
|
||||
pub fn Explore() -> impl IntoView {
|
||||
let instances = Resource::new(move || (), |_| async move { CLIENT.list_instances().await });
|
||||
|
||||
view! {
|
||||
<Title text="Explore" />
|
||||
<h1 class="my-4 font-serif text-4xl font-bold">Instances</h1>
|
||||
<SuspenseError result=instances>
|
||||
{move || Suspend::new(async move {
|
||||
let instances_ = instances.await;
|
||||
let is_empty = instances_.as_ref().map(|i| i.is_empty()).unwrap_or(true);
|
||||
view! {
|
||||
<Show
|
||||
when=move || !is_empty
|
||||
fallback=move || view! { <ConnectView res=instances /> }
|
||||
>
|
||||
<ul class="my-4 list-none">
|
||||
{instances_
|
||||
.clone()
|
||||
.ok()
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(instance_card)
|
||||
.collect::<Vec<_>>()}
|
||||
</ul>
|
||||
</Show>
|
||||
}
|
||||
})}
|
||||
</SuspenseError>
|
||||
}
|
||||
}
|
||||
|
||||
pub fn instance_card(i: &DbInstance) -> impl IntoView {
|
||||
view! {
|
||||
<li>
|
||||
<div class="m-4 shadow card bg-base-100">
|
||||
<div class="p-4 card-body">
|
||||
<div class="flex">
|
||||
<a class="card-title grow" href=format!("/instance/{}", i.domain)>
|
||||
{instance_title_with_domain(i)}
|
||||
</a>
|
||||
{instance_updated(i)}
|
||||
</div>
|
||||
<p>{i.topic.clone()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
}
|
|
@ -2,12 +2,15 @@ use crate::{
|
|||
common::{article::ListArticlesParams, instance::DbInstance, utils::http_protocol_str},
|
||||
frontend::{
|
||||
api::CLIENT,
|
||||
components::instance_follow_button::InstanceFollowButton,
|
||||
utils::formatting::{
|
||||
article_path,
|
||||
article_title,
|
||||
instance_title_with_domain,
|
||||
instance_updated,
|
||||
components::{instance_follow_button::InstanceFollowButton, suspense_error::SuspenseError},
|
||||
utils::{
|
||||
errors::FrontendError,
|
||||
formatting::{
|
||||
article_path,
|
||||
article_title,
|
||||
instance_title_with_domain,
|
||||
instance_updated,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -19,19 +22,18 @@ use url::Url;
|
|||
#[component]
|
||||
pub fn InstanceDetails() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let hostname = move || params.get().get("hostname").clone().unwrap();
|
||||
let hostname = move || params.get().get("hostname").clone();
|
||||
let instance_profile = Resource::new(hostname, move |hostname| async move {
|
||||
let url = Url::parse(&format!("{}://{hostname}", http_protocol_str())).unwrap();
|
||||
CLIENT.resolve_instance(url).await.unwrap()
|
||||
let hostname = hostname.ok_or(FrontendError::new("No instance given"))?;
|
||||
let url = Url::parse(&format!("{}://{hostname}", http_protocol_str()))?;
|
||||
CLIENT.resolve_instance(url).await
|
||||
});
|
||||
|
||||
view! {
|
||||
<Suspense fallback=|| {
|
||||
view! { "Loading..." }
|
||||
}>
|
||||
{move || {
|
||||
<SuspenseError result=instance_profile>
|
||||
{move || Suspend::new(async move {
|
||||
instance_profile
|
||||
.get()
|
||||
.await
|
||||
.map(|instance: DbInstance| {
|
||||
let articles = Resource::new(
|
||||
move || instance.id,
|
||||
|
@ -42,7 +44,6 @@ pub fn InstanceDetails() -> impl IntoView {
|
|||
instance_id: Some(instance_id),
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
},
|
||||
);
|
||||
let title = instance_title_with_domain(&instance);
|
||||
|
@ -60,10 +61,10 @@ pub fn InstanceDetails() -> impl IntoView {
|
|||
<div>{instance.topic}</div>
|
||||
<h2 class="font-serif text-xl font-bold">Articles</h2>
|
||||
<ul class="list-none">
|
||||
<Suspense>
|
||||
{move || {
|
||||
<SuspenseError result=articles>
|
||||
{move || Suspend::new(async move {
|
||||
articles
|
||||
.get()
|
||||
.await
|
||||
.map(|a| {
|
||||
a.into_iter()
|
||||
.map(|a| {
|
||||
|
@ -77,14 +78,14 @@ pub fn InstanceDetails() -> impl IntoView {
|
|||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
})}
|
||||
</SuspenseError>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
}}
|
||||
})}
|
||||
|
||||
</Suspense>
|
||||
</SuspenseError>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
use crate::frontend::{
|
||||
api::CLIENT,
|
||||
components::connect::ConnectView,
|
||||
utils::formatting::{instance_title_with_domain, instance_updated},
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::Title;
|
||||
|
||||
#[component]
|
||||
pub fn ListInstances() -> impl IntoView {
|
||||
let instances = Resource::new(
|
||||
move || (),
|
||||
|_| async move { CLIENT.list_instances().await.unwrap() },
|
||||
);
|
||||
|
||||
view! {
|
||||
<Title text="Instances" />
|
||||
<h1 class="my-4 font-serif text-4xl font-bold">Instances</h1>
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
<Show
|
||||
when=move || { !instances.get().unwrap_or_default().is_empty() }
|
||||
fallback=move || view! { <ConnectView res=instances /> }
|
||||
>
|
||||
<ul class="my-4 list-none">
|
||||
{move || {
|
||||
instances
|
||||
.get()
|
||||
.map(|a| {
|
||||
a.into_iter()
|
||||
.map(|ref i| {
|
||||
view! {
|
||||
<li>
|
||||
<div class="m-4 shadow card bg-base-100">
|
||||
<div class="p-4 card-body">
|
||||
<div class="flex">
|
||||
<a
|
||||
class="card-title grow"
|
||||
href=format!("/instance/{}", i.domain)
|
||||
>
|
||||
{instance_title_with_domain(i)}
|
||||
</a>
|
||||
{instance_updated(i)}
|
||||
</div>
|
||||
<p>{i.topic.clone()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}}
|
||||
|
||||
</ul>
|
||||
</Show>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
pub mod details;
|
||||
pub mod list;
|
||||
pub mod search;
|
||||
pub mod settings;
|
||||
|
|
|
@ -1,32 +1,29 @@
|
|||
use crate::{common::instance::UpdateInstanceParams, frontend::api::CLIENT};
|
||||
use crate::{
|
||||
common::instance::UpdateInstanceParams,
|
||||
frontend::{
|
||||
api::CLIENT,
|
||||
components::suspense_error::SuspenseError,
|
||||
utils::errors::FrontendResultExt,
|
||||
},
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::Title;
|
||||
|
||||
#[component]
|
||||
pub fn InstanceSettings() -> impl IntoView {
|
||||
let (saved, set_saved) = signal(false);
|
||||
let (submit_error, set_submit_error) = signal(None::<String>);
|
||||
let instance = Resource::new(
|
||||
|| (),
|
||||
|_| async move { CLIENT.get_local_instance().await.unwrap() },
|
||||
);
|
||||
let instance = Resource::new(|| (), |_| async move { CLIENT.get_local_instance().await });
|
||||
|
||||
let submit_action = Action::new(move |params: &UpdateInstanceParams| {
|
||||
let params = params.clone();
|
||||
async move {
|
||||
let result = CLIENT.update_local_instance(¶ms).await;
|
||||
match result {
|
||||
Ok(_res) => {
|
||||
CLIENT
|
||||
.update_local_instance(¶ms)
|
||||
.await
|
||||
.error_popup(|_| {
|
||||
instance.refetch();
|
||||
set_saved.set(true);
|
||||
set_submit_error.set(None);
|
||||
}
|
||||
Err(err) => {
|
||||
let msg = err.to_string();
|
||||
log::warn!("Unable to update profile: {msg}");
|
||||
set_submit_error.set(Some(msg));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -34,79 +31,65 @@ pub fn InstanceSettings() -> impl IntoView {
|
|||
// that completely breaks reactivity.
|
||||
view! {
|
||||
<Title text="Instance Settings" />
|
||||
<Suspense fallback=|| {
|
||||
view! { "Loading..." }
|
||||
}>
|
||||
<SuspenseError result=instance>
|
||||
{move || Suspend::new(async move {
|
||||
let instance = instance.await;
|
||||
let (name, set_name) = signal(instance.instance.name.unwrap_or_default());
|
||||
let (topic, set_topic) = signal(instance.instance.topic.unwrap_or_default());
|
||||
view! {
|
||||
<h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">
|
||||
"Instance Settings"
|
||||
</h1>
|
||||
{move || {
|
||||
submit_error
|
||||
.get()
|
||||
.map(|err| {
|
||||
view! { <p class="alert alert-error">{err}</p> }
|
||||
})
|
||||
}}
|
||||
<div class="flex flex-row mb-2">
|
||||
<label class="block w-20" for="name">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
class="w-80 input input-secondary input-bordered"
|
||||
prop:value=name
|
||||
value=name
|
||||
on:change=move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
set_name.set(val);
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row mb-2">
|
||||
<label class="block w-20" for="topic">
|
||||
"Topic"
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
class="w-80 input input-secondary input-bordered"
|
||||
prop:value=topic
|
||||
value=topic
|
||||
on:change=move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
set_topic.set(val);
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click=move |_| {
|
||||
let form = UpdateInstanceParams {
|
||||
name: Some(name.get()),
|
||||
topic: Some(topic.get()),
|
||||
};
|
||||
submit_action.dispatch(form);
|
||||
}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
|
||||
<Show when=move || saved.get()>
|
||||
<div class="toast">
|
||||
<div class="alert alert-info">
|
||||
<span>Saved!</span>
|
||||
instance
|
||||
.await
|
||||
.map(|instance| {
|
||||
let (name, set_name) = signal(instance.instance.name.unwrap_or_default());
|
||||
let (topic, set_topic) = signal(
|
||||
instance.instance.topic.unwrap_or_default(),
|
||||
);
|
||||
view! {
|
||||
<h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">
|
||||
"Instance Settings"
|
||||
</h1>
|
||||
<div class="flex flex-row mb-2">
|
||||
<label class="block w-20" for="name">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
class="w-80 input input-secondary input-bordered"
|
||||
bind:value=(name, set_name)
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
}
|
||||
<div class="flex flex-row mb-2">
|
||||
<label class="block w-20" for="topic">
|
||||
"Topic"
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
class="w-80 input input-secondary input-bordered"
|
||||
bind:value=(topic, set_topic)
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click=move |_| {
|
||||
let form = UpdateInstanceParams {
|
||||
name: Some(name.get()),
|
||||
topic: Some(topic.get()),
|
||||
};
|
||||
submit_action.dispatch(form);
|
||||
}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
|
||||
<Show when=move || saved.get()>
|
||||
<div class="toast">
|
||||
<div class="alert alert-info">
|
||||
<span>Saved!</span>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
}
|
||||
})
|
||||
})}
|
||||
|
||||
</Suspense>
|
||||
</SuspenseError>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use super::utils::errors::FrontendResult;
|
||||
use crate::{
|
||||
common::{
|
||||
article::{DbArticleView, EditView, GetArticleParams},
|
||||
|
@ -9,13 +10,17 @@ use leptos::prelude::*;
|
|||
use leptos_router::hooks::use_params_map;
|
||||
|
||||
pub mod article;
|
||||
pub mod explore;
|
||||
pub mod instance;
|
||||
pub mod user;
|
||||
|
||||
fn article_resource() -> Resource<DbArticleView> {
|
||||
pub fn article_title_param() -> Option<String> {
|
||||
let params = use_params_map();
|
||||
let title = move || params.get().get("title").clone();
|
||||
Resource::new(title, move |title| async move {
|
||||
params.get().get("title").clone()
|
||||
}
|
||||
|
||||
fn article_resource() -> Resource<FrontendResult<DbArticleView>> {
|
||||
Resource::new(article_title_param, move |title| async move {
|
||||
let mut title = title.unwrap_or(MAIN_PAGE_NAME.to_string());
|
||||
let mut domain = None;
|
||||
if let Some((title_, domain_)) = title.clone().split_once('@') {
|
||||
|
@ -29,17 +34,17 @@ fn article_resource() -> Resource<DbArticleView> {
|
|||
id: None,
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
fn article_edits_resource(article: Resource<DbArticleView>) -> Resource<Vec<EditView>> {
|
||||
|
||||
fn article_edits_resource(
|
||||
article: Resource<FrontendResult<DbArticleView>>,
|
||||
) -> Resource<FrontendResult<Vec<EditView>>> {
|
||||
Resource::new(
|
||||
move || article.get(),
|
||||
move |_| async move {
|
||||
CLIENT
|
||||
.get_article_edits(article.await.article.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
let id = article.await.map(|a| a.article.id)?;
|
||||
CLIENT.get_article_edits(id).await
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@ use crate::{
|
|||
common::user::UpdateUserParams,
|
||||
frontend::{
|
||||
api::CLIENT,
|
||||
utils::resources::{site, DefaultResource},
|
||||
components::suspense_error::SuspenseError,
|
||||
utils::{errors::FrontendResultExt, resources::site},
|
||||
},
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
|
@ -11,24 +12,14 @@ use leptos_meta::Title;
|
|||
#[component]
|
||||
pub fn UserEditProfile() -> impl IntoView {
|
||||
let (saved, set_saved) = signal(false);
|
||||
let (submit_error, set_submit_error) = signal(None::<String>);
|
||||
|
||||
let submit_action = Action::new(move |params: &UpdateUserParams| {
|
||||
let params = params.clone();
|
||||
async move {
|
||||
let result = CLIENT.update_user_profile(params).await;
|
||||
match result {
|
||||
Ok(_res) => {
|
||||
site().refetch();
|
||||
set_saved.set(true);
|
||||
set_submit_error.set(None);
|
||||
}
|
||||
Err(err) => {
|
||||
let msg = err.to_string();
|
||||
log::warn!("Unable to update profile: {msg}");
|
||||
set_submit_error.set(Some(msg));
|
||||
}
|
||||
}
|
||||
CLIENT.update_user_profile(params).await.error_popup(|_| {
|
||||
set_saved.set(true);
|
||||
site().refetch();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -36,78 +27,69 @@ pub fn UserEditProfile() -> impl IntoView {
|
|||
// that completely breaks reactivity.
|
||||
view! {
|
||||
<Title text="Edit Profile" />
|
||||
<Suspense fallback=|| {
|
||||
view! { "Loading..." }
|
||||
}>
|
||||
{
|
||||
let my_profile = site().with_default(|site| site.clone().my_profile.unwrap());
|
||||
let (display_name, set_display_name) = signal(
|
||||
my_profile.person.display_name.clone().unwrap_or_default(),
|
||||
);
|
||||
let (bio, set_bio) = signal(my_profile.person.bio.clone().unwrap_or_default());
|
||||
view! {
|
||||
<h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">Edit Profile</h1>
|
||||
{move || {
|
||||
submit_error
|
||||
.get()
|
||||
.map(|err| {
|
||||
view! { <p class="alert alert-error">{err}</p> }
|
||||
})
|
||||
}}
|
||||
<div class="flex flex-row mb-2">
|
||||
<label class="block w-40">Displayname</label>
|
||||
<input
|
||||
type="text"
|
||||
id="displayname"
|
||||
class="w-80 input input-secondary input-bordered"
|
||||
prop:value=display_name
|
||||
value=display_name
|
||||
on:change=move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
set_display_name.set(val);
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row mb-2">
|
||||
<label class="block w-40" for="bio">
|
||||
"Bio (Markdown supported)"
|
||||
</label>
|
||||
<textarea
|
||||
id="bio"
|
||||
class="w-80 text-base textarea textarea-secondary"
|
||||
prop:value=move || bio.get()
|
||||
on:input:target=move |evt| {
|
||||
let val = evt.target().value();
|
||||
set_bio.set(val);
|
||||
}
|
||||
>
|
||||
bio.get()
|
||||
</textarea>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click=move |_| {
|
||||
let form = UpdateUserParams {
|
||||
person_id: my_profile.person.id,
|
||||
display_name: Some(display_name.get()),
|
||||
bio: Some(bio.get()),
|
||||
};
|
||||
submit_action.dispatch(form);
|
||||
}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
|
||||
<Show when=move || saved.get()>
|
||||
<div class="toast">
|
||||
<div class="alert alert-info">
|
||||
<span>Saved!</span>
|
||||
<SuspenseError result=site()>
|
||||
{Suspend::new(async move {
|
||||
site()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|site| site.my_profile)
|
||||
.map(|my_profile| {
|
||||
let (display_name, set_display_name) = signal(
|
||||
my_profile.person.display_name.clone().unwrap_or_default(),
|
||||
);
|
||||
let (bio, set_bio) = signal(
|
||||
my_profile.person.bio.clone().unwrap_or_default(),
|
||||
);
|
||||
view! {
|
||||
<h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">
|
||||
Edit Profile
|
||||
</h1>
|
||||
<div class="flex flex-row mb-2">
|
||||
<label class="block w-40">Displayname</label>
|
||||
<input
|
||||
type="text"
|
||||
id="displayname"
|
||||
class="w-80 input input-secondary input-bordered"
|
||||
bind:value=(display_name, set_display_name)
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
<div class="flex flex-row mb-2">
|
||||
<label class="block w-40" for="bio">
|
||||
"Bio (Markdown supported)"
|
||||
</label>
|
||||
<textarea
|
||||
id="bio"
|
||||
class="w-80 text-base textarea textarea-secondary"
|
||||
bind:value=(bio, set_bio)
|
||||
>
|
||||
bio.get()
|
||||
</textarea>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click=move |_| {
|
||||
let form = UpdateUserParams {
|
||||
person_id: my_profile.person.id,
|
||||
display_name: Some(display_name.get()),
|
||||
bio: Some(bio.get()),
|
||||
};
|
||||
submit_action.dispatch(form);
|
||||
}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
|
||||
</Suspense>
|
||||
<Show when=move || saved.get()>
|
||||
<div class="toast">
|
||||
<div class="alert alert-info">
|
||||
<span>Saved!</span>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
}
|
||||
})
|
||||
})}
|
||||
|
||||
</SuspenseError>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,11 @@ use crate::{
|
|||
common::Notification,
|
||||
frontend::{
|
||||
api::CLIENT,
|
||||
utils::formatting::{article_path, article_title},
|
||||
components::suspense_error::SuspenseError,
|
||||
utils::{
|
||||
errors::FrontendResultExt,
|
||||
formatting::{article_path, article_title},
|
||||
},
|
||||
},
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
|
@ -12,17 +16,17 @@ use leptos_meta::Title;
|
|||
pub fn Notifications() -> impl IntoView {
|
||||
let notifications = Resource::new(
|
||||
move || {},
|
||||
|_| async move { CLIENT.notifications_list().await.unwrap_or_default() },
|
||||
|_| async move { CLIENT.notifications_list().await },
|
||||
);
|
||||
|
||||
view! {
|
||||
<Title text="Notifications" />
|
||||
<h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">Notifications</h1>
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
<SuspenseError result=notifications>
|
||||
<ul class="divide-y divide-solid">
|
||||
{move || {
|
||||
{move || Suspend::new(async move {
|
||||
notifications
|
||||
.get()
|
||||
.await
|
||||
.map(|n| {
|
||||
n.into_iter()
|
||||
.map(|ref notif| {
|
||||
|
@ -31,7 +35,11 @@ pub fn Notifications() -> impl IntoView {
|
|||
EditConflict(c) => {
|
||||
(
|
||||
"visibility: hidden",
|
||||
format!("{}/edit/{}", article_path(&c.article), c.id.0),
|
||||
format!(
|
||||
"{}/edit?conflict_id={}",
|
||||
article_path(&c.article),
|
||||
c.id.0,
|
||||
),
|
||||
format!(
|
||||
"Conflict: {} - {}",
|
||||
article_title(&c.article),
|
||||
|
@ -52,9 +60,11 @@ pub fn Notifications() -> impl IntoView {
|
|||
let notif_ = notif_.clone();
|
||||
async move {
|
||||
if let ArticleApprovalRequired(a) = notif_ {
|
||||
CLIENT.approve_article(a.id, true).await.unwrap();
|
||||
CLIENT
|
||||
.approve_article(a.id, true)
|
||||
.await
|
||||
.error_popup(|_| notifications.refetch());
|
||||
}
|
||||
notifications.refetch();
|
||||
}
|
||||
});
|
||||
let notif_ = notif.clone();
|
||||
|
@ -63,10 +73,13 @@ pub fn Notifications() -> impl IntoView {
|
|||
async move {
|
||||
match notif_ {
|
||||
EditConflict(c) => {
|
||||
CLIENT.delete_conflict(c.id).await.unwrap();
|
||||
CLIENT.delete_conflict(c.id).await.error_popup(|_| {});
|
||||
}
|
||||
ArticleApprovalRequired(a) => {
|
||||
CLIENT.approve_article(a.id, false).await.unwrap();
|
||||
CLIENT
|
||||
.approve_article(a.id, false)
|
||||
.await
|
||||
.error_popup(|_| {});
|
||||
}
|
||||
}
|
||||
notifications.refetch();
|
||||
|
@ -101,9 +114,9 @@ pub fn Notifications() -> impl IntoView {
|
|||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}}
|
||||
})}
|
||||
|
||||
</ul>
|
||||
</Suspense>
|
||||
</SuspenseError>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::{
|
|||
common::user::GetUserParams,
|
||||
frontend::{
|
||||
api::CLIENT,
|
||||
components::edit_list::EditList,
|
||||
components::{edit_list::EditList, suspense_error::SuspenseError},
|
||||
markdown::render_article_markdown,
|
||||
utils::formatting::user_title,
|
||||
},
|
||||
|
@ -15,59 +15,52 @@ use leptos_router::hooks::use_params_map;
|
|||
pub fn UserProfile() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let name = move || params.get().get("name").clone().unwrap_or_default();
|
||||
let (error, set_error) = signal(None::<String>);
|
||||
let user_profile = Resource::new(name, move |mut name| async move {
|
||||
set_error.set(None);
|
||||
let mut domain = None;
|
||||
if let Some((title_, domain_)) = name.clone().split_once('@') {
|
||||
name = title_.to_string();
|
||||
domain = Some(domain_.to_string());
|
||||
}
|
||||
let params = GetUserParams { name, domain };
|
||||
CLIENT.get_user(params).await.unwrap()
|
||||
CLIENT.get_user(params).await
|
||||
});
|
||||
|
||||
let edits = Resource::new(
|
||||
move || user_profile.get(),
|
||||
move |_| async move {
|
||||
CLIENT
|
||||
.get_person_edits(user_profile.await.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
},
|
||||
);
|
||||
|
||||
view! {
|
||||
{move || {
|
||||
error
|
||||
.get()
|
||||
.map(|err| {
|
||||
view! { <p style="color:red;">{err}</p> }
|
||||
})
|
||||
}}
|
||||
|
||||
<Suspense fallback=|| {
|
||||
view! { "Loading..." }
|
||||
}>
|
||||
<SuspenseError result=user_profile>
|
||||
{move || Suspend::new(async move {
|
||||
let edits = edits.await;
|
||||
let person = user_profile.await;
|
||||
view! {
|
||||
<Title text=user_title(&person) />
|
||||
<h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">
|
||||
{user_title(&person)}
|
||||
</h1>
|
||||
let edits = Resource::new(
|
||||
move || user_profile.get(),
|
||||
move |_| async move { CLIENT.get_person_edits(user_profile.await?.id).await },
|
||||
);
|
||||
user_profile
|
||||
.await
|
||||
.map(|person| {
|
||||
view! {
|
||||
<Title text=user_title(&person) />
|
||||
<h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">
|
||||
{user_title(&person)}
|
||||
</h1>
|
||||
|
||||
<div
|
||||
class="mb-2 max-w-full prose prose-slate"
|
||||
inner_html=render_article_markdown(&person.bio.unwrap_or_default())
|
||||
></div>
|
||||
<div
|
||||
class="mb-2 max-w-full prose prose-slate"
|
||||
inner_html=render_article_markdown(&person.bio.unwrap_or_default())
|
||||
></div>
|
||||
|
||||
<h2 class="font-serif text-xl font-bold">Edits</h2>
|
||||
<EditList edits=edits for_article=false />
|
||||
}
|
||||
<SuspenseError result=user_profile>
|
||||
{move || Suspend::new(async move {
|
||||
edits
|
||||
.await
|
||||
.map(|edits| {
|
||||
view! {
|
||||
<h2 class="font-serif text-xl font-bold">Edits</h2>
|
||||
<EditList edits=edits for_article=false />
|
||||
}
|
||||
})
|
||||
})}
|
||||
</SuspenseError>
|
||||
}
|
||||
})
|
||||
})}
|
||||
|
||||
</Suspense>
|
||||
</SuspenseError>
|
||||
}
|
||||
}
|
||||
|
|
97
src/frontend/utils/errors.rs
Normal file
97
src/frontend/utils/errors.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use leptos::prelude::*;
|
||||
use log::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{error::Error, fmt::Display, time::Duration};
|
||||
|
||||
pub type FrontendResult<T> = Result<T, FrontendError>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct FrontendError(String);
|
||||
|
||||
impl FrontendError {
|
||||
pub fn new(message: impl Into<String>) -> Self {
|
||||
Self(message.into())
|
||||
}
|
||||
|
||||
pub fn message(self) -> String {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FrontendResultExt<T> {
|
||||
fn error_popup<F>(self, on_success: F)
|
||||
where
|
||||
F: FnOnce(T);
|
||||
}
|
||||
|
||||
impl<T> FrontendResultExt<T> for FrontendResult<T> {
|
||||
fn error_popup<F>(self, on_success: F)
|
||||
where
|
||||
F: FnOnce(T),
|
||||
{
|
||||
match self {
|
||||
Ok(o) => on_success(o),
|
||||
Err(e) => {
|
||||
warn!("{e}");
|
||||
ErrorPopup::set(e.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ErrorPopup {
|
||||
read: ReadSignal<Option<String>>,
|
||||
write: WriteSignal<Option<String>>,
|
||||
}
|
||||
|
||||
impl ErrorPopup {
|
||||
pub fn init() {
|
||||
let (read, write) = signal(None::<String>);
|
||||
provide_context(Self { read, write });
|
||||
}
|
||||
|
||||
pub fn set(msg: String) {
|
||||
if let Some(s) = use_context::<Self>() {
|
||||
s.write.set(Some(msg));
|
||||
set_timeout(move || s.write.set(None), Duration::from_secs(15));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get() -> Option<String> {
|
||||
use_context::<Self>()?.read.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FrontendError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for FrontendError {}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
impl From<reqwest::Error> for FrontendError {
|
||||
fn from(value: reqwest::Error) -> Self {
|
||||
Self(value.to_string())
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
impl From<gloo_net::Error> for FrontendError {
|
||||
fn from(value: gloo_net::Error) -> Self {
|
||||
Self(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for FrontendError {
|
||||
fn from(value: url::ParseError) -> Self {
|
||||
Self(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_urlencoded::ser::Error> for FrontendError {
|
||||
fn from(value: serde_urlencoded::ser::Error) -> Self {
|
||||
Self(value.to_string())
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ use leptos::prelude::*;
|
|||
use leptos_use::{use_cookie_with_options, SameSite, UseCookieOptions};
|
||||
|
||||
pub mod dark_mode;
|
||||
pub mod errors;
|
||||
pub mod formatting;
|
||||
pub mod resources;
|
||||
|
||||
|
|
|
@ -1,37 +1,42 @@
|
|||
use crate::common::instance::SiteView;
|
||||
use super::errors::FrontendResult;
|
||||
use crate::{
|
||||
common::{
|
||||
instance::{Options, SiteView},
|
||||
user::LocalUserView,
|
||||
},
|
||||
frontend::api::CLIENT,
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
|
||||
pub fn site() -> Resource<SiteView> {
|
||||
use_context::<Resource<SiteView>>().unwrap()
|
||||
type SiteResource = Resource<FrontendResult<SiteView>>;
|
||||
|
||||
pub fn site() -> SiteResource {
|
||||
site_internal().unwrap_or_else(|| Resource::new(|| (), |_| async move { CLIENT.site().await }))
|
||||
}
|
||||
|
||||
fn site_internal() -> Option<SiteResource> {
|
||||
use_context::<Resource<FrontendResult<SiteView>>>()
|
||||
}
|
||||
|
||||
pub fn my_profile() -> Option<LocalUserView> {
|
||||
match site_internal() {
|
||||
Some(s) => s.map(|s| s.clone().ok().map(|s| s.my_profile))??,
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config() -> Options {
|
||||
match site_internal() {
|
||||
Some(s) => s.map(|s| s.clone().ok().map(|s| s.config)).flatten(),
|
||||
None => None,
|
||||
}
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn is_logged_in() -> bool {
|
||||
site().with_default(|site| site.my_profile.is_some())
|
||||
}
|
||||
pub fn is_admin() -> bool {
|
||||
site().with_default(|site| {
|
||||
site.my_profile
|
||||
.as_ref()
|
||||
.map(|p| p.local_user.admin)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
pub trait DefaultResource<T> {
|
||||
fn with_default<O>(&self, f: impl FnOnce(&T) -> O) -> O;
|
||||
fn get_default(&self) -> T;
|
||||
my_profile().is_some()
|
||||
}
|
||||
|
||||
impl<T: Default + Send + Sync + Clone> 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()),
|
||||
})
|
||||
}
|
||||
fn get_default(&self) -> T {
|
||||
match self.get() {
|
||||
Some(x) => x.clone(),
|
||||
None => T::default(),
|
||||
}
|
||||
}
|
||||
pub fn is_admin() -> bool {
|
||||
my_profile().map(|p| p.local_user.admin).unwrap_or(false)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#[cfg(feature = "ssr")]
|
||||
pub mod backend;
|
||||
pub mod common;
|
||||
#[expect(clippy::unwrap_used)]
|
||||
pub mod frontend;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
pub async fn main() -> ibis::backend::utils::error::MyResult<()> {
|
||||
pub async fn main() -> ibis::backend::utils::error::BackendResult<()> {
|
||||
use ibis::backend::utils::config::IbisConfig;
|
||||
use log::LevelFilter;
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ use ibis::{
|
|||
common::{instance::Options, user::RegisterUserParams},
|
||||
frontend::api::ApiClient,
|
||||
};
|
||||
use reqwest::ClientBuilder;
|
||||
use std::{
|
||||
env::current_dir,
|
||||
fs::{create_dir_all, remove_dir_all},
|
||||
|
@ -90,6 +89,7 @@ pub struct IbisInstance {
|
|||
pub api_client: ApiClient,
|
||||
db_path: String,
|
||||
db_handle: JoinHandle<()>,
|
||||
pub hostname: String,
|
||||
}
|
||||
|
||||
impl IbisInstance {
|
||||
|
@ -110,15 +110,15 @@ impl IbisInstance {
|
|||
|
||||
async fn start(db_path: String, port: i32, username: &str, article_approval: bool) -> Self {
|
||||
let connection_url = format!("postgresql://ibis:password@/ibis?host={db_path}");
|
||||
let hostname = format!("127.0.0.1:{port}");
|
||||
let domain = format!("localhost:{port}");
|
||||
|
||||
let hostname = format!("localhost:{port}");
|
||||
let config = IbisConfig {
|
||||
database: IbisConfigDatabase {
|
||||
connection_url,
|
||||
..Default::default()
|
||||
},
|
||||
federation: IbisConfigFederation {
|
||||
domain: domain.clone(),
|
||||
domain: hostname.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
options: Options {
|
||||
|
@ -127,10 +127,10 @@ impl IbisInstance {
|
|||
},
|
||||
..Default::default()
|
||||
};
|
||||
let client = ClientBuilder::new().cookie_store(true).build().unwrap();
|
||||
let api_client = ApiClient::new(client, Some(domain));
|
||||
let api_client = ApiClient::new(Some(hostname.clone()));
|
||||
let (tx, rx) = oneshot::channel::<()>();
|
||||
let handle = tokio::task::spawn(async move {
|
||||
let db_handle = tokio::task::spawn(async move {
|
||||
let hostname = format!("127.0.0.1:{port}");
|
||||
start(config, Some(hostname.parse().unwrap()), Some(tx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -145,7 +145,8 @@ impl IbisInstance {
|
|||
Self {
|
||||
api_client,
|
||||
db_path,
|
||||
db_handle: handle,
|
||||
db_handle,
|
||||
hostname,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ async fn test_create_read_and_edit_local_article() -> Result<()> {
|
|||
|
||||
// error on article which wasnt federated
|
||||
let not_found = beta.get_article(get_article_data.clone()).await;
|
||||
assert!(not_found.is_none());
|
||||
assert!(not_found.is_err());
|
||||
|
||||
// edit article
|
||||
let edit_params = EditArticleParams {
|
||||
|
@ -185,7 +185,7 @@ async fn test_synchronize_articles() -> Result<()> {
|
|||
|
||||
// try to read remote article by name, fails without domain
|
||||
let get_res = beta.get_article(get_article_data.clone()).await;
|
||||
assert!(get_res.is_none());
|
||||
assert!(get_res.is_err());
|
||||
|
||||
// get the article with instance id and compare
|
||||
let get_res = RetryFuture::new(
|
||||
|
@ -197,11 +197,11 @@ async fn test_synchronize_articles() -> Result<()> {
|
|||
};
|
||||
let res = beta.get_article(get_article_data).await;
|
||||
match res {
|
||||
None => Err(RetryPolicy::<String>::Retry(None)),
|
||||
Some(a) if a.latest_version != edit_res.latest_version => {
|
||||
Err(_) => Err(RetryPolicy::<String>::Retry(None)),
|
||||
Ok(a) if a.latest_version != edit_res.latest_version => {
|
||||
Err(RetryPolicy::Retry(None))
|
||||
}
|
||||
Some(a) => Ok(a),
|
||||
Ok(a) => Ok(a),
|
||||
}
|
||||
},
|
||||
LinearRetryStrategy::new(),
|
||||
|
@ -805,9 +805,9 @@ async fn test_synchronize_instances() -> Result<()> {
|
|||
|| async {
|
||||
let res = gamma.list_instances().await;
|
||||
match res {
|
||||
None => Err(RetryPolicy::<String>::Retry(None)),
|
||||
Some(i) if i.len() < 3 => Err(RetryPolicy::Retry(None)),
|
||||
Some(i) => Ok(i),
|
||||
Err(_) => Err(RetryPolicy::<String>::Retry(None)),
|
||||
Ok(i) if i.len() < 3 => Err(RetryPolicy::Retry(None)),
|
||||
Ok(i) => Ok(i),
|
||||
}
|
||||
},
|
||||
LinearRetryStrategy::new(),
|
||||
|
|
Loading…
Reference in a new issue