From c330061087956b26ffd28e3505a1afdb7414b284 Mon Sep 17 00:00:00 2001 From: Nutomic Date: Fri, 31 Jan 2025 11:56:10 +0000 Subject: [PATCH] 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 --- Cargo.toml | 1 + src/backend/api/article.rs | 22 +-- src/backend/api/comment.rs | 6 +- src/backend/api/instance.rs | 12 +- src/backend/api/mod.rs | 8 +- src/backend/api/user.rs | 42 ++--- src/backend/database/article.rs | 43 +++-- src/backend/database/comment.rs | 22 ++- src/backend/database/conflict.rs | 14 +- src/backend/database/edit.rs | 16 +- src/backend/database/instance.rs | 22 +-- src/backend/database/instance_stats.rs | 4 +- src/backend/database/mod.rs | 4 +- src/backend/database/user.rs | 26 +-- src/backend/federation/activities/accept.rs | 6 +- src/backend/federation/activities/announce.rs | 13 +- .../comment/create_or_update_comment.rs | 6 +- .../activities/comment/delete_comment.rs | 8 +- .../federation/activities/comment/mod.rs | 4 +- .../activities/comment/undo_delete_comment.rs | 6 +- .../federation/activities/create_article.rs | 6 +- src/backend/federation/activities/follow.rs | 6 +- src/backend/federation/activities/mod.rs | 4 +- src/backend/federation/activities/reject.rs | 6 +- .../activities/update_local_article.rs | 6 +- .../activities/update_remote_article.rs | 6 +- src/backend/federation/mod.rs | 4 +- src/backend/federation/objects/article.rs | 4 +- .../federation/objects/article_or_comment.rs | 14 +- .../federation/objects/articles_collection.rs | 6 +- src/backend/federation/objects/comment.rs | 4 +- src/backend/federation/objects/edit.rs | 4 +- .../federation/objects/edits_collection.rs | 8 +- src/backend/federation/objects/instance.rs | 10 +- .../federation/objects/instance_collection.rs | 6 +- src/backend/federation/objects/user.rs | 4 +- src/backend/federation/routes.rs | 31 ++-- src/backend/mod.rs | 4 +- src/backend/server/assets.rs | 4 +- src/backend/server/mod.rs | 6 +- src/backend/server/nodeinfo.rs | 8 +- src/backend/server/setup.rs | 4 +- src/backend/utils/config.rs | 4 +- src/backend/utils/error.rs | 12 +- src/backend/utils/mod.rs | 16 +- src/backend/utils/scheduled_tasks.rs | 4 +- src/backend/utils/validate.rs | 12 +- src/frontend/api/article.rs | 83 ++++----- src/frontend/api/comment.rs | 13 +- src/frontend/api/instance.rs | 70 ++++---- src/frontend/api/mod.rs | 127 +++++++------- src/frontend/api/user.rs | 45 +++-- src/frontend/app.rs | 60 ++++--- src/frontend/components/article_nav.rs | 25 +-- src/frontend/components/comment.rs | 16 +- src/frontend/components/comment_editor.rs | 27 +-- src/frontend/components/connect.rs | 7 +- src/frontend/components/credentials.rs | 27 +-- .../components/instance_follow_button.rs | 19 ++- src/frontend/components/mod.rs | 1 + src/frontend/components/nav.rs | 70 ++++---- src/frontend/components/suspense_error.rs | 44 +++++ src/frontend/markdown/mod.rs | 2 - src/frontend/pages/article/actions.rs | 22 ++- src/frontend/pages/article/create.rs | 18 +- src/frontend/pages/article/diff.rs | 93 ++++++----- src/frontend/pages/article/discussion.rs | 48 ++++-- src/frontend/pages/article/edit.rs | 61 +++---- src/frontend/pages/article/history.rs | 21 ++- src/frontend/pages/article/list.rs | 101 ----------- src/frontend/pages/article/mod.rs | 1 - src/frontend/pages/article/read.rs | 37 +++-- src/frontend/pages/explore.rs | 60 +++++++ src/frontend/pages/instance/details.rs | 45 ++--- src/frontend/pages/instance/list.rs | 58 ------- src/frontend/pages/instance/mod.rs | 1 - src/frontend/pages/instance/settings.rs | 157 ++++++++---------- src/frontend/pages/mod.rs | 23 ++- src/frontend/pages/user/edit_profile.rs | 154 ++++++++--------- src/frontend/pages/user/notifications.rs | 37 +++-- src/frontend/pages/user/profile.rs | 75 ++++----- src/frontend/utils/errors.rs | 97 +++++++++++ src/frontend/utils/mod.rs | 1 + src/frontend/utils/resources.rs | 63 +++---- src/lib.rs | 1 - src/main.rs | 2 +- tests/common.rs | 17 +- tests/test.rs | 16 +- 88 files changed, 1217 insertions(+), 1116 deletions(-) create mode 100644 src/frontend/components/suspense_error.rs delete mode 100644 src/frontend/pages/article/list.rs create mode 100644 src/frontend/pages/explore.rs delete mode 100644 src/frontend/pages/instance/list.rs create mode 100644 src/frontend/utils/errors.rs diff --git a/Cargo.toml b/Cargo.toml index c3e99ce..ac234c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ panic = "abort" [lints.clippy] dbg_macro = "deny" unwrap_used = "deny" +todo = "deny" # frontend and shared deps [dependencies] diff --git a/src/backend/api/article.rs b/src/backend/api/article.rs index 21e7f5d..8a45b6e 100644 --- a/src/backend/api/article.rs +++ b/src/backend/api/article.rs @@ -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, context: Data, Form(mut params): Form, -) -> MyResult> { +) -> BackendResult> { 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, context: Data, Form(mut params): Form, -) -> MyResult>> { +) -> BackendResult>> { 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, context: Data, -) -> MyResult> { +) -> BackendResult> { 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, context: Data, -) -> MyResult>> { +) -> BackendResult>> { 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, context: Data, Form(mut params): Form, -) -> MyResult> { +) -> BackendResult> { // 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, context: Data, -) -> MyResult> { +) -> BackendResult> { 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, context: Data, -) -> MyResult>> { +) -> BackendResult>> { 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, context: Data, Form(params): Form, -) -> MyResult> { +) -> BackendResult> { 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, context: Data, Form(params): Form, -) -> MyResult> { +) -> BackendResult> { 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, context: Data, Form(params): Form, -) -> MyResult> { +) -> BackendResult> { DbConflict::delete(params.conflict_id, user.person.id, &context)?; Ok(Json(())) } diff --git a/src/backend/api/comment.rs b/src/backend/api/comment.rs index d9ab110..61be3d4 100644 --- a/src/backend/api/comment.rs +++ b/src/backend/api/comment.rs @@ -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, context: Data, Form(params): Form, -) -> MyResult> { +) -> BackendResult> { 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, context: Data, Form(params): Form, -) -> MyResult> { +) -> BackendResult> { if let Some(content) = ¶ms.content { validate_not_empty(content)?; } diff --git a/src/backend/api/instance.rs b/src/backend/api/instance.rs index 35b63df..aa86fb2 100644 --- a/src/backend/api/instance.rs +++ b/src/backend/api/instance.rs @@ -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, Form(params): Form, -) -> MyResult> { +) -> BackendResult> { 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, Form(mut params): Form, -) -> MyResult> { +) -> BackendResult> { 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, context: Data, Form(params): Form, -) -> MyResult> { +) -> BackendResult> { 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, context: Data, -) -> MyResult> { +) -> BackendResult> { 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, -) -> MyResult>> { +) -> BackendResult>> { let instances = DbInstance::list(false, &context)?; Ok(Json(instances)) } diff --git a/src/backend/api/mod.rs b/src/backend/api/mod.rs index 02028a6..1b9e3f9 100644 --- a/src/backend/api/mod.rs +++ b/src/backend/api/mod.rs @@ -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, user: Option>, -) -> MyResult> { +) -> BackendResult> { 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, user: Option>, context: Data, -) -> MyResult>> { +) -> BackendResult>> { let params = if let Some(article_id) = query.article_id { ViewEditParams::ArticleId(article_id) } else if let Some(person_id) = query.person_id { diff --git a/src/backend/api/user.rs b/src/backend/api/user.rs index afec467..b8bf5dc 100644 --- a/src/backend/api/user.rs +++ b/src/backend/api/user.rs @@ -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) -> MyResult { +fn generate_login_token(person: &DbPerson, context: &Data) -> BackendResult { 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) -> MyRes Ok(jwt) } -pub async fn validate(jwt: &str, context: &IbisContext) -> MyResult { +pub async fn validate(jwt: &str, context: &IbisContext) -> BackendResult { 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, jar: CookieJar, Form(params): Form, -) -> MyResult<(CookieJar, Json)> { +) -> BackendResult<(CookieJar, Json)> { 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, jar: CookieJar, Form(params): Form, -) -> MyResult<(CookieJar, Json)> { +) -> BackendResult<(CookieJar, Json)> { 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) -> Cookie<'static> { pub(in crate::backend::api) async fn logout_user( context: Data, jar: CookieJar, -) -> MyResult<(CookieJar, Json)> { +) -> BackendResult<(CookieJar, Json)> { 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, context: Data, -) -> MyResult> { +) -> BackendResult> { 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, Form(mut params): Form, -) -> MyResult> { +) -> BackendResult> { 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, context: Data, -) -> MyResult>> { +) -> BackendResult>> { 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, + user: Option>, context: Data, -) -> MyResult> { - 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> { + 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)) + } } diff --git a/src/backend/database/article.rs b/src/backend/database/article.rs index e3861e5..f4324cf 100644 --- a/src/backend/database/article.rs +++ b/src/backend/database/article.rs @@ -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> { + pub fn edits_id(&self) -> BackendResult> { Ok(CollectionId::parse(&format!("{}/edits", self.ap_id))?) } - pub fn create(form: DbArticleForm, context: &IbisContext) -> MyResult { + pub fn create(form: DbArticleForm, context: &IbisContext) -> BackendResult { 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 { + pub fn create_or_update(form: DbArticleForm, context: &IbisContext) -> BackendResult { 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 { + pub fn update_text(id: ArticleId, text: &str, context: &IbisContext) -> BackendResult { let mut conn = context.db_pool.get()?; Ok(diesel::update(article::dsl::article.find(id)) .set(article::dsl::text.eq(text)) .get_result::(conn.deref_mut())?) } - pub fn update_protected(id: ArticleId, locked: bool, context: &IbisContext) -> MyResult { + pub fn update_protected( + id: ArticleId, + locked: bool, + context: &IbisContext, + ) -> BackendResult { let mut conn = context.db_pool.get()?; Ok(diesel::update(article::dsl::article.find(id)) .set(article::dsl::protected.eq(locked)) .get_result::(conn.deref_mut())?) } - pub fn update_approved(id: ArticleId, approved: bool, context: &IbisContext) -> MyResult { + pub fn update_approved( + id: ArticleId, + approved: bool, + context: &IbisContext, + ) -> BackendResult { let mut conn = context.db_pool.get()?; Ok(diesel::update(article::dsl::article.find(id)) .set(article::dsl::approved.eq(approved)) .get_result::(conn.deref_mut())?) } - pub fn delete(id: ArticleId, context: &IbisContext) -> MyResult { + pub fn delete(id: ArticleId, context: &IbisContext) -> BackendResult { let mut conn = context.db_pool.get()?; Ok(diesel::delete(article::dsl::article.find(id)).get_result::(conn.deref_mut())?) } - pub fn read(id: ArticleId, context: &IbisContext) -> MyResult { + pub fn read(id: ArticleId, context: &IbisContext) -> BackendResult { let mut conn = context.db_pool.get()?; Ok(article::table .find(id) .get_result::(conn.deref_mut())?) } - pub fn read_view(id: ArticleId, context: &IbisContext) -> MyResult { + pub fn read_view(id: ArticleId, context: &IbisContext) -> BackendResult { let mut conn = context.db_pool.get()?; let query = article::table .find(id) @@ -117,7 +125,7 @@ impl DbArticle { title: &str, domain: Option, context: &IbisContext, - ) -> MyResult { + ) -> BackendResult { 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, context: &IbisContext) -> MyResult { + pub fn read_from_ap_id( + ap_id: &ObjectId, + context: &IbisContext, + ) -> BackendResult { 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, instance_id: Option, context: &IbisContext, - ) -> MyResult> { + ) -> BackendResult> { 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> { + pub fn search(query: &str, context: &IbisContext) -> BackendResult> { 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 { + pub fn latest_edit_version(&self, context: &IbisContext) -> BackendResult { let mut conn = context.db_pool.get()?; let latest_version: Option = edit::table .filter(edit::dsl::article_id.eq(self.id)) @@ -206,7 +217,7 @@ impl DbArticle { } } - pub fn list_approval_required(context: &IbisContext) -> MyResult> { + pub fn list_approval_required(context: &IbisContext) -> BackendResult> { let mut conn = context.db_pool.get()?; let query = article::table .group_by(article::dsl::id) diff --git a/src/backend/database/comment.rs b/src/backend/database/comment.rs index 7e3775d..d72c919 100644 --- a/src/backend/database/comment.rs +++ b/src/backend/database/comment.rs @@ -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 { + pub fn create(form: DbCommentInsertForm, context: &IbisContext) -> BackendResult { 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 { + ) -> BackendResult { 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 { + pub fn create_or_update( + form: DbCommentInsertForm, + context: &IbisContext, + ) -> BackendResult { 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 { + pub fn read(id: CommentId, context: &IbisContext) -> BackendResult { let mut conn = context.db_pool.get()?; Ok(comment::table .find(id) .get_result::(conn.deref_mut())?) } - pub fn read_view(id: CommentId, context: &IbisContext) -> MyResult { + pub fn read_view(id: CommentId, context: &IbisContext) -> BackendResult { 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, context: &IbisContext) -> MyResult { + pub fn read_from_ap_id( + ap_id: &ObjectId, + context: &IbisContext, + ) -> BackendResult { 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> { + ) -> BackendResult> { let mut conn = context.db_pool.get()?; let comments = comment::table .inner_join(person::table) diff --git a/src/backend/database/conflict.rs b/src/backend/database/conflict.rs index 4826029..77c8170 100644 --- a/src/backend/database/conflict.rs +++ b/src/backend/database/conflict.rs @@ -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 { + pub fn create(form: &DbConflictForm, context: &IbisContext) -> BackendResult { 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> { + pub fn list(person: &DbPerson, context: &IbisContext) -> BackendResult> { 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, - ) -> MyResult> { + ) -> BackendResult> { 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?; diff --git a/src/backend/database/edit.rs b/src/backend/database/edit.rs index d706227..a6a360d 100644 --- a/src/backend/database/edit.rs +++ b/src/backend/database/edit.rs @@ -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 { + ) -> BackendResult { 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> { + ) -> BackendResult> { Ok(ObjectId::parse(&format!( "{}/{}", article.ap_id, @@ -77,7 +77,7 @@ impl DbEditForm { } impl DbEdit { - pub fn create(form: &DbEditForm, context: &IbisContext) -> MyResult { + pub fn create(form: &DbEditForm, context: &IbisContext) -> BackendResult { 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 { + pub fn read(version: &EditVersion, context: &IbisContext) -> BackendResult { 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, context: &IbisContext) -> MyResult { + pub fn read_from_ap_id(ap_id: &ObjectId, context: &IbisContext) -> BackendResult { 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> { + pub fn list_for_article(id: ArticleId, context: &IbisContext) -> BackendResult> { 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, context: &IbisContext, - ) -> MyResult> { + ) -> BackendResult> { 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 diff --git a/src/backend/database/instance.rs b/src/backend/database/instance.rs index 6bf6dbd..0dd61f9 100644 --- a/src/backend/database/instance.rs +++ b/src/backend/database/instance.rs @@ -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 { + pub fn create(form: &DbInstanceForm, context: &IbisContext) -> BackendResult { 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 { + pub fn read(id: InstanceId, context: &IbisContext) -> BackendResult { 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 { + pub fn update(form: DbInstanceUpdateForm, context: &IbisContext) -> BackendResult { 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, context: &Data, - ) -> MyResult { + ) -> BackendResult { 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 { + pub fn read_local(context: &IbisContext) -> BackendResult { 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, context: &Data, - ) -> MyResult { + ) -> BackendResult { 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, - ) -> 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> { + pub fn read_followers(id_: InstanceId, context: &IbisContext) -> BackendResult> { 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) -> MyResult> { + pub fn list(only_remote: bool, context: &Data) -> BackendResult> { 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, - ) -> MyResult { + ) -> BackendResult { let mut conn = context.db_pool.get()?; Ok(instance::table .inner_join(article::table) diff --git a/src/backend/database/instance_stats.rs b/src/backend/database/instance_stats.rs index f01ad60..903d590 100644 --- a/src/backend/database/instance_stats.rs +++ b/src/backend/database/instance_stats.rs @@ -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 { + pub fn read(context: &IbisContext) -> BackendResult { let mut conn = context.db_pool.get()?; Ok(instance_stats::table.find(1).get_result(conn.deref_mut())?) } diff --git a/src/backend/database/mod.rs b/src/backend/database/mod.rs index 4492e81..6cb2ff4 100644 --- a/src/backend/database/mod.rs +++ b/src/backend/database/mod.rs @@ -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 { +pub fn read_jwt_secret(context: &IbisContext) -> BackendResult { let mut conn = context.db_pool.get()?; Ok(jwt_secret::table .select(jwt_secret::dsl::secret) diff --git a/src/backend/database/user.rs b/src/backend/database/user.rs index 167e607..b9051e4 100644 --- a/src/backend/database/user.rs +++ b/src/backend/database/user.rs @@ -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) -> MyResult { + pub fn create(person_form: &DbPersonForm, context: &Data) -> BackendResult { let mut conn = context.db_pool.get()?; Ok(insert_into(person::table) .values(person_form) @@ -61,7 +61,7 @@ impl DbPerson { .get_result::(conn.deref_mut())?) } - pub fn read(id: PersonId, context: &IbisContext) -> MyResult { + pub fn read(id: PersonId, context: &IbisContext) -> BackendResult { 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 { + ) -> BackendResult { 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, context: &Data, - ) -> MyResult { + ) -> BackendResult { 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, context: &Data, - ) -> MyResult { + ) -> BackendResult { 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) -> MyResult<()> { + pub fn update_profile( + params: &UpdateUserParams, + context: &Data, + ) -> 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 { + pub fn read_local_from_name( + username: &str, + context: &IbisContext, + ) -> BackendResult { 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> { + fn read_following(id_: PersonId, context: &IbisContext) -> BackendResult> { 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) -> MyResult { + pub fn ghost(context: &Data) -> BackendResult { let username = "ghost"; let read = DbPerson::read_from_name(username, &None, context); if read.is_ok() { diff --git a/src/backend/federation/activities/accept.rs b/src/backend/federation/activities/accept.rs index 2c65abc..cf94057 100644 --- a/src/backend/federation/activities/accept.rs +++ b/src/backend/federation/activities/accept.rs @@ -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, - ) -> 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 diff --git a/src/backend/federation/activities/announce.rs b/src/backend/federation/activities/announce.rs index 6c96a6b..0206f12 100644 --- a/src/backend/federation/activities/announce.rs +++ b/src/backend/federation/activities/announce.rs @@ -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) -> MyResult<()> { + pub async fn send( + object: AnnouncableActivities, + context: &Data, + ) -> 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) -> MyResult<()> { + async fn verify(&self, _context: &Data) -> BackendResult<()> { Ok(()) } #[tracing::instrument(skip_all)] - async fn receive(self, context: &Data) -> MyResult<()> { + async fn receive(self, context: &Data) -> BackendResult<()> { self.object.verify(context).await?; self.object.receive(context).await } diff --git a/src/backend/federation/activities/comment/create_or_update_comment.rs b/src/backend/federation/activities/comment/create_or_update_comment.rs index 200a217..a46c93f 100644 --- a/src/backend/federation/activities/comment/create_or_update_comment.rs +++ b/src/backend/federation/activities/comment/create_or_update_comment.rs @@ -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) -> MyResult<()> { + pub async fn send(comment: &DbComment, context: &Data) -> 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 diff --git a/src/backend/federation/activities/comment/delete_comment.rs b/src/backend/federation/activities/comment/delete_comment.rs index 86dc2b9..26a749f 100644 --- a/src/backend/federation/activities/comment/delete_comment.rs +++ b/src/backend/federation/activities/comment/delete_comment.rs @@ -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, - ) -> MyResult { + ) -> BackendResult { 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) -> MyResult<()> { + pub async fn send(comment: &DbComment, context: &Data) -> 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 diff --git a/src/backend/federation/activities/comment/mod.rs b/src/backend/federation/activities/comment/mod.rs index d4fe389..f55613d 100644 --- a/src/backend/federation/activities/comment/mod.rs +++ b/src/backend/federation/activities/comment/mod.rs @@ -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> { +fn generate_comment_activity_to(instance: &DbInstance) -> BackendResult> { let followers_url = format!("{}/followers", &instance.ap_id); Ok(vec![public(), followers_url.parse()?]) } diff --git a/src/backend/federation/activities/comment/undo_delete_comment.rs b/src/backend/federation/activities/comment/undo_delete_comment.rs index 30b049d..1b2ab22 100644 --- a/src/backend/federation/activities/comment/undo_delete_comment.rs +++ b/src/backend/federation/activities/comment/undo_delete_comment.rs @@ -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) -> MyResult<()> { + pub async fn send(comment: &DbComment, context: &Data) -> 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 diff --git a/src/backend/federation/activities/create_article.rs b/src/backend/federation/activities/create_article.rs index 7a4b973..fe82d0f 100644 --- a/src/backend/federation/activities/create_article.rs +++ b/src/backend/federation/activities/create_article.rs @@ -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, - ) -> 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 diff --git a/src/backend/federation/activities/follow.rs b/src/backend/federation/activities/follow.rs index 93de0a7..e42822c 100644 --- a/src/backend/federation/activities/follow.rs +++ b/src/backend/federation/activities/follow.rs @@ -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, - ) -> 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 diff --git a/src/backend/federation/activities/mod.rs b/src/backend/federation/activities/mod.rs index fc4fa57..a33dd9d 100644 --- a/src/backend/federation/activities/mod.rs +++ b/src/backend/federation/activities/mod.rs @@ -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, -) -> Result<(), Error> { +) -> Result<(), BackendError> { let mut form = DbEditForm::new( original_article, creator_id, diff --git a/src/backend/federation/activities/reject.rs b/src/backend/federation/activities/reject.rs index 99cc767..44e4889 100644 --- a/src/backend/federation/activities/reject.rs +++ b/src/backend/federation/activities/reject.rs @@ -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, - ) -> 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 diff --git a/src/backend/federation/activities/update_local_article.rs b/src/backend/federation/activities/update_local_article.rs index 93fe240..d0ae5c7 100644 --- a/src/backend/federation/activities/update_local_article.rs +++ b/src/backend/federation/activities/update_local_article.rs @@ -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, context: &Data, - ) -> 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 diff --git a/src/backend/federation/activities/update_remote_article.rs b/src/backend/federation/activities/update_remote_article.rs index f7fc01e..ab06f20 100644 --- a/src/backend/federation/activities/update_remote_article.rs +++ b/src/backend/federation/activities/update_remote_article.rs @@ -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, - ) -> 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 diff --git a/src/backend/federation/mod.rs b/src/backend/federation/mod.rs index e39c70d..0129a19 100644 --- a/src/backend/federation/mod.rs +++ b/src/backend/federation/mod.rs @@ -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, -) -> MyResult<()> { +) -> BackendResult<()> { if instance.local { AnnounceActivity::send(activity, context).await?; } else { diff --git a/src/backend/federation/objects/article.rs b/src/backend/federation/objects/article.rs index 85702f5..800607c 100644 --- a/src/backend/federation/objects/article.rs +++ b/src/backend/federation/objects/article.rs @@ -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, diff --git a/src/backend/federation/objects/article_or_comment.rs b/src/backend/federation/objects/article_or_comment.rs index 0ff9365..4b4eb2c 100644 --- a/src/backend/federation/objects/article_or_comment.rs +++ b/src/backend/federation/objects/article_or_comment.rs @@ -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> { None @@ -37,7 +37,7 @@ impl Object for DbArticleOrComment { async fn read_from_id( object_id: Url, context: &Data, - ) -> MyResult> { + ) -> BackendResult> { 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) -> MyResult<()> { + async fn delete(self, context: &Data) -> BackendResult<()> { match self { Self::Article(p) => p.delete(context).await, Self::Comment(c) => c.delete(context).await, } } - async fn into_json(self, context: &Data) -> MyResult { + async fn into_json(self, context: &Data) -> BackendResult { 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, - ) -> 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) -> MyResult { + async fn from_json(apub: Self::Kind, context: &Data) -> BackendResult { 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?), diff --git a/src/backend/federation/objects/articles_collection.rs b/src/backend/federation/objects/articles_collection.rs index e73eff0..0cf6447 100644 --- a/src/backend/federation/objects/articles_collection.rs +++ b/src/backend/federation/objects/articles_collection.rs @@ -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> { +pub fn local_articles_url(domain: &str) -> BackendResult> { 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, diff --git a/src/backend/federation/objects/comment.rs b/src/backend/federation/objects/comment.rs index c6e6b6f..ad2dac3 100644 --- a/src/backend/federation/objects/comment.rs +++ b/src/backend/federation/objects/comment.rs @@ -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, diff --git a/src/backend/federation/objects/edit.rs b/src/backend/federation/objects/edit.rs index ae3522f..0f6456e 100644 --- a/src/backend/federation/objects/edit.rs +++ b/src/backend/federation/objects/edit.rs @@ -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, diff --git a/src/backend/federation/objects/edits_collection.rs b/src/backend/federation/objects/edits_collection.rs index f712de1..3f9bbcc 100644 --- a/src/backend/federation/objects/edits_collection.rs +++ b/src/backend/federation/objects/edits_collection.rs @@ -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, diff --git a/src/backend/federation/objects/instance.rs b/src/backend/federation/objects/instance.rs index 31d3c25..fb34588 100644 --- a/src/backend/federation/objects/instance.rs +++ b/src/backend/federation/objects/instance.rs @@ -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 { + pub fn followers_url(&self) -> BackendResult { Ok(Url::parse(&format!("{}/followers", self.ap_id.inner()))?) } - pub fn follower_ids(&self, context: &Data) -> MyResult> { + pub fn follower_ids(&self, context: &Data) -> BackendResult> { 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, ::Error: From, - ::Error: From, + ::Error: From, { 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> { Some(self.last_refreshed_at) diff --git a/src/backend/federation/objects/instance_collection.rs b/src/backend/federation/objects/instance_collection.rs index 98eb424..bcb885d 100644 --- a/src/backend/federation/objects/instance_collection.rs +++ b/src/backend/federation/objects/instance_collection.rs @@ -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> { +pub fn linked_instances_url(domain: &str) -> BackendResult> { 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, diff --git a/src/backend/federation/objects/user.rs b/src/backend/federation/objects/user.rs index cea0858..9ca135b 100644 --- a/src/backend/federation/objects/user.rs +++ b/src/backend/federation/objects/user.rs @@ -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> { Some(self.last_refreshed_at) diff --git a/src/backend/federation/routes.rs b/src/backend/federation/routes.rs index 43f6b54..0602763 100644 --- a/src/backend/federation/routes.rs +++ b/src/backend/federation/routes.rs @@ -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, -) -> MyResult>> { +) -> BackendResult>> { 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, context: Data, -) -> MyResult>> { +) -> BackendResult>> { 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, -) -> MyResult>> { +) -> BackendResult>> { 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, -) -> MyResult>> { +) -> BackendResult>> { 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, context: Data, -) -> MyResult>> { +) -> BackendResult>> { 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, context: Data, -) -> MyResult>> { +) -> BackendResult>> { 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, context: Data, -) -> MyResult>> { +) -> BackendResult>> { 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> { Some(match self { @@ -205,7 +205,7 @@ impl Object for UserOrInstance { async fn read_from_id( object_id: Url, data: &Data, - ) -> Result, Error> { + ) -> Result, 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) -> Result<(), Error> { + async fn delete(self, data: &Data) -> 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) -> Result { + async fn into_json(self, _data: &Data) -> Result { unimplemented!() } @@ -230,14 +230,17 @@ impl Object for UserOrInstance { apub: &Self::Kind, expected_domain: &Url, data: &Data, - ) -> 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) -> Result { + async fn from_json( + apub: Self::Kind, + data: &Data, + ) -> Result { Ok(match apub { PersonOrInstance::Person(p) => { UserOrInstance::User(DbPerson::from_json(p, data).await?) diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 7c13a47..f87b75e 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -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, notify_start: Option>, -) -> MyResult<()> { +) -> BackendResult<()> { let manager = ConnectionManager::::new(&config.database.connection_url); let db_pool = Pool::builder() .max_size(config.database.pool_size) diff --git a/src/backend/server/assets.rs b/src/backend/server/assets.rs index 35bc1c8..adf171a 100644 --- a/src/backend/server/assets.rs +++ b/src/backend/server/assets.rs @@ -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, request: Request, -) -> MyResult> { +) -> BackendResult> { if cfg!(debug_assertions) { // in debug mode serve assets directly from local folder Ok(ServeDir::new(options.site_root.as_ref()) diff --git a/src/backend/server/mod.rs b/src/backend/server/mod.rs index 049d95d..325ee1a 100644 --- a/src/backend/server/mod.rs +++ b/src/backend/server/mod.rs @@ -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, override_hostname: Option, notify_start: Option>, -) -> 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, request: Request, ) -> 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()); } diff --git a/src/backend/server/nodeinfo.rs b/src/backend/server/nodeinfo.rs index d2a323f..ccba787 100644 --- a/src/backend/server/nodeinfo.rs +++ b/src/backend/server/nodeinfo.rs @@ -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) -> MyResult> { +async fn node_info_well_known( + context: Data, +) -> BackendResult> { 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) -> MyResult) -> MyResult> { +async fn node_info(context: Data) -> BackendResult> { let stats = InstanceStats::read(&context)?; Ok(Json(NodeInfo { version: "2.1".to_string(), diff --git a/src/backend/server/setup.rs b/src/backend/server/setup.rs index 7afddfb..cc4ccba 100644 --- a/src/backend/server/setup.rs +++ b/src/backend/server/setup.rs @@ -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) -> Result<(), Error> { +pub async fn setup(context: &Data) -> 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()); diff --git a/src/backend/utils/config.rs b/src/backend/utils/config.rs index ed1036a..312f7b2 100644 --- a/src/backend/utils/config.rs +++ b/src/backend/utils/config.rs @@ -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 { + pub fn read() -> BackendResult { 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 diff --git a/src/backend/utils/error.rs b/src/backend/utils/error.rs index 6839058..3d1ee91 100644 --- a/src/backend/utils/error.rs +++ b/src/backend/utils/error.rs @@ -1,27 +1,27 @@ use std::fmt::{Display, Formatter}; -pub type MyResult = Result; +pub type BackendResult = Result; #[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 From for Error +impl From for BackendError where T: Into, { 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, diff --git a/src/backend/utils/mod.rs b/src/backend/utils/mod.rs index 47b5c9d..68015ba 100644 --- a/src/backend/utils/mod.rs +++ b/src/backend/utils/mod.rs @@ -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) -> Result, version: &EditVersion, -) -> MyResult { +) -> BackendResult { 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 { +pub fn generate_keypair() -> BackendResult { if cfg!(debug_assertions) { static KEYPAIR: LazyLock = 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> { - let generate_edit = |a, b| -> MyResult { + fn create_edits() -> BackendResult> { + let generate_edit = |a, b| -> BackendResult { 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); diff --git a/src/backend/utils/scheduled_tasks.rs b/src/backend/utils/scheduled_tasks.rs index a1ac749..211195e 100644 --- a/src/backend/utils/scheduled_tasks.rs +++ b/src/backend/utils/scheduled_tasks.rs @@ -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()?; diff --git a/src/backend/utils/validate.rs b/src/backend/utils/validate.rs index f5eb938..3482f04 100644 --- a/src/backend/utils/validate.rs +++ b/src/backend/utils/validate.rs @@ -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 { +pub fn validate_article_title(title: &str) -> BackendResult { #[expect(clippy::expect_used)] static TITLE_REGEX: LazyLock = 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 { 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 = 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) -> MyResult<()> { +pub fn validate_display_name(name: &Option) -> 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) -> 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()); } diff --git a/src/frontend/api/article.rs b/src/frontend/api/article.rs index 3936006..2d3d090 100644 --- a/src/frontend/api/article.rs +++ b/src/frontend/api/article.rs @@ -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 { + ) -> FrontendResult { self.post("/api/v1/article", Some(&data)).await } - pub async fn get_article(&self, data: GetArticleParams) -> Option { - self.get("/api/v1/article", Some(data)).await + pub async fn get_article(&self, data: GetArticleParams) -> FrontendResult { + self.send(Method::GET, "/api/v1/article", Some(data)).await } - pub async fn list_articles(&self, data: ListArticlesParams) -> Option> { - Some(self.get("/api/v1/article/list", Some(data)).await.unwrap()) + pub async fn list_articles(&self, data: ListArticlesParams) -> FrontendResult> { + self.get("/api/v1/article/list", Some(data)).await } pub async fn edit_article( &self, params: &EditArticleParams, - ) -> Result, ServerFnError> { + ) -> FrontendResult> { self.patch("/api/v1/article", Some(¶ms)).await } - pub async fn fork_article( - &self, - params: &ForkArticleParams, - ) -> Result { + pub async fn fork_article(&self, params: &ForkArticleParams) -> FrontendResult { self.post("/api/v1/article/fork", Some(params)).await } pub async fn protect_article( &self, params: &ProtectArticleParams, - ) -> Result { + ) -> FrontendResult { self.post("/api/v1/article/protect", Some(params)).await } - pub async fn resolve_article(&self, id: Url) -> Result { + pub async fn resolve_article(&self, id: Url) -> FrontendResult { 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> { + pub async fn get_article_edits(&self, article_id: ArticleId) -> FrontendResult> { 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() } } diff --git a/src/frontend/api/comment.rs b/src/frontend/api/comment.rs index fe61599..bc2be51 100644 --- a/src/frontend/api/comment.rs +++ b/src/frontend/api/comment.rs @@ -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 { + ) -> FrontendResult { self.post("/api/v1/comment", Some(¶ms)).await } - pub async fn edit_comment( - &self, - params: &EditCommentParams, - ) -> Result { + pub async fn edit_comment(&self, params: &EditCommentParams) -> FrontendResult { self.patch("/api/v1/comment", Some(¶ms)).await } } diff --git a/src/frontend/api/instance.rs b/src/frontend/api/instance.rs index 4894d77..902c3c2 100644 --- a/src/frontend/api/instance.rs +++ b/src/frontend/api/instance.rs @@ -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 { + pub async fn get_local_instance(&self) -> FrontendResult { self.get("/api/v1/instance", None::).await } - pub async fn get_instance(&self, params: &GetInstanceParams) -> Option { + pub async fn get_instance(&self, params: &GetInstanceParams) -> FrontendResult { self.get("/api/v1/instance", Some(¶ms)).await } - pub async fn list_instances(&self) -> Option> { + pub async fn list_instances(&self) -> FrontendResult> { self.get("/api/v1/instance/list", None::).await } pub async fn update_local_instance( &self, params: &UpdateInstanceParams, - ) -> Result { + ) -> FrontendResult { self.patch("/api/v1/instance", Some(params)).await } - pub async fn notifications_list(&self) -> Option> { + pub async fn notifications_list(&self) -> FrontendResult> { self.get("/api/v1/user/notifications/list", None::<()>) .await } - pub async fn notifications_count(&self) -> Option { + pub async fn notifications_count(&self) -> FrontendResult { self.get("/api/v1/user/notifications/count", None::<()>) .await } - pub async fn search( - &self, - params: &SearchArticleParams, - ) -> Result, ServerFnError> { + pub async fn search(&self, params: &SearchArticleParams) -> FrontendResult> { self.send(Method::GET, "/api/v1/search", Some(params)).await } - pub async fn resolve_instance(&self, id: Url) -> Result { + pub async fn resolve_instance(&self, id: Url) -> FrontendResult { 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 { - result_to_option(self.post("/api/v1/instance/follow", Some(params)).await) + pub async fn follow_instance( + &self, + params: FollowInstanceParams, + ) -> FrontendResult { + self.post("/api/v1/instance/follow", Some(params)).await } - pub async fn site(&self) -> Option { + pub async fn site(&self) -> FrontendResult { self.get("/api/v1/site", None::<()>).await } #[cfg(debug_assertions)] - pub async fn follow_instance_with_resolve(&self, follow_instance: &str) -> Option { + pub async fn follow_instance_with_resolve( + &self, + follow_instance: &str, + ) -> FrontendResult { 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) } } diff --git a/src/frontend/api/mod.rs b/src/frontend/api/mod.rs index 3b1ce5b..72cde07 100644 --- a/src/frontend/api/mod.rs +++ b/src/frontend/api/mod.rs @@ -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 = LazyLock::new(|| { - #[cfg(feature = "ssr")] - { - ApiClient::new(reqwest::Client::new(), None) - } - #[cfg(not(feature = "ssr"))] - { - ApiClient::new() - } -}); +pub static CLIENT: LazyLock = 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, } impl ApiClient { - #[cfg(feature = "ssr")] - pub fn new(client: reqwest::Client, hostname_: Option) -> 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) -> 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(&self, endpoint: &str, query: Option) -> Option + async fn get(&self, endpoint: &str, query: Option) -> FrontendResult 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(&self, endpoint: &str, query: Option) -> Result + async fn post(&self, endpoint: &str, query: Option) -> FrontendResult 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(&self, endpoint: &str, query: Option) -> Result + async fn patch(&self, endpoint: &str, query: Option) -> FrontendResult where T: for<'de> Deserialize<'de>, R: Serialize + Debug, @@ -77,12 +64,7 @@ impl ApiClient { } #[cfg(feature = "ssr")] - async fn send( - &self, - method: Method, - path: &str, - params: Option

, - ) -> Result + async fn send(&self, method: Method, path: &str, params: Option

) -> FrontendResult 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

, - ) -> impl std::future::Future> + Send + 'a + ) -> impl std::future::Future> + 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(status: u16, text: String, url: &str) -> Result + fn response(status: u16, text: String, url: &str) -> FrontendResult 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::::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 { + let protocol = if cfg!(debug_assertions) { + "http" + } else { + "https" + }; -fn result_to_option(val: Result) -> Option { - 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::().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)) } } diff --git a/src/frontend/api/user.rs b/src/frontend/api/user.rs index 9ba4ec7..ea72f2e 100644 --- a/src/frontend/api/user.rs +++ b/src/frontend/api/user.rs @@ -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 { + pub async fn register(&self, params: RegisterUserParams) -> FrontendResult { self.post("/api/v1/account/register", Some(¶ms)).await } - pub async fn login(&self, params: LoginUserParams) -> Result { + pub async fn login(&self, params: LoginUserParams) -> FrontendResult { self.post("/api/v1/account/login", Some(¶ms)).await } - pub async fn logout(&self) -> Option { - result_to_option(self.post("/api/v1/account/logout", None::<()>).await) + pub async fn logout(&self) -> FrontendResult { + self.post("/api/v1/account/logout", None::<()>).await } - pub async fn get_user(&self, data: GetUserParams) -> Option { + pub async fn get_user(&self, data: GetUserParams) -> FrontendResult { self.get("/api/v1/user", Some(data)).await } pub async fn update_user_profile( &self, data: UpdateUserParams, - ) -> Result { + ) -> FrontendResult { self.post("/api/v1/account/update", Some(data)).await } - pub async fn get_person_edits(&self, person_id: PersonId) -> Option> { + pub async fn get_person_edits(&self, person_id: PersonId) -> FrontendResult> { let data = GetEditList { person_id: Some(person_id), ..Default::default() diff --git a/src/frontend/app.rs b/src/frontend/app.rs index 2cd58a2..a1c37d6 100644 --- a/src/frontend/app.rs +++ b/src/frontend/app.rs @@ -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! { @@ -74,37 +69,40 @@ pub fn App() -> impl IntoView { - - {move || { - instance - .get() - .map(|i| { - let formatter = move |text| { - format!("{text} — {}", instance_title(&i.instance)) - }; - view! { } - }) - }} - </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 /> diff --git a/src/frontend/components/article_nav.rs b/src/frontend/components/article_nav.rs index b55846f..2da30d1 100644 --- a/src/frontend/components/article_nav.rs +++ b/src/frontend/components/article_nav.rs @@ -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", diff --git a/src/frontend/components/comment.rs b/src/frontend/components/comment.rs index 42cff1a..3cd2865 100644 --- a/src/frontend/components/comment.rs +++ b/src/frontend/components/comment.rs @@ -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(), diff --git a/src/frontend/components/comment_editor.rs b/src/frontend/components/comment_editor.rs index 5b91dde..a8ff141 100644 --- a/src/frontend/components/comment_editor.rs +++ b/src/frontend/components/comment_editor.rs @@ -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)); + } + }); } } }); diff --git a/src/frontend/components/connect.rs b/src/frontend/components/connect.rs index 8af7c66..1d85ef6 100644 --- a/src/frontend/components/connect.rs +++ b/src/frontend/components/connect.rs @@ -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! { diff --git a/src/frontend/components/credentials.rs b/src/frontend/components/credentials.rs index 1efe4ed..e0a7e1e 100644 --- a/src/frontend/components/credentials.rs +++ b/src/frontend/components/credentials.rs @@ -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> diff --git a/src/frontend/components/instance_follow_button.rs b/src/frontend/components/instance_follow_button.rs index 1499dd6..9b9ee07 100644 --- a/src/frontend/components/instance_follow_button.rs +++ b/src/frontend/components/instance_follow_button.rs @@ -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" diff --git a/src/frontend/components/mod.rs b/src/frontend/components/mod.rs index e60aebe..3215212 100644 --- a/src/frontend/components/mod.rs +++ b/src/frontend/components/mod.rs @@ -8,3 +8,4 @@ pub mod edit_list; pub mod instance_follow_button; pub mod nav; pub mod protected_route; +pub mod suspense_error; diff --git a/src/frontend/components/nav.rs b/src/frontend/components/nav.rs index c43e8d7..cef6d8f 100644 --- a/src/frontend/components/nav.rs +++ b/src/frontend/components/nav.rs @@ -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> diff --git a/src/frontend/components/suspense_error.rs b/src/frontend/components/suspense_error.rs new file mode 100644 index 0000000..8090fc2 --- /dev/null +++ b/src/frontend/components/suspense_error.rs @@ -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> + } +} diff --git a/src/frontend/markdown/mod.rs b/src/frontend/markdown/mod.rs index b683cef..ed365a4 100644 --- a/src/frontend/markdown/mod.rs +++ b/src/frontend/markdown/mod.rs @@ -1,5 +1,3 @@ -#![deny(clippy::unwrap_used)] - use article_link::ArticleLinkScanner; use markdown_it::{ plugins::cmark::block::{heading::ATXHeading, lheading::SetextHeader}, diff --git a/src/frontend/pages/article/actions.rs b/src/frontend/pages/article/actions.rs index 9832686..41b0c37 100644 --- a/src/frontend/pages/article/actions.rs +++ b/src/frontend/pages/article/actions.rs @@ -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> } } diff --git a/src/frontend/pages/article/create.rs b/src/frontend/pages/article/create.rs index e21eb67..44277c0 100644 --- a/src/frontend/pages/article/create.rs +++ b/src/frontend/pages/article/create.rs @@ -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); diff --git a/src/frontend/pages/article/diff.rs b/src/frontend/pages/article/diff.rs index f196550..3a85c47 100644 --- a/src/frontend/pages/article/diff.rs +++ b/src/frontend/pages/article/diff.rs @@ -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> } } diff --git a/src/frontend/pages/article/discussion.rs b/src/frontend/pages/article/discussion.rs index cdcf5c0..af3348a 100644 --- a/src/frontend/pages/article/discussion.rs +++ b/src/frontend/pages/article/discussion.rs @@ -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() diff --git a/src/frontend/pages/article/edit.rs b/src/frontend/pages/article/edit.rs index cf2b068..2c2abee 100644 --- a/src/frontend/pages/article/edit.rs +++ b/src/frontend/pages/article/edit.rs @@ -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> } } > diff --git a/src/frontend/pages/article/history.rs b/src/frontend/pages/article/history.rs index d604b10..419d8f7 100644 --- a/src/frontend/pages/article/history.rs +++ b/src/frontend/pages/article/history.rs @@ -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> } } diff --git a/src/frontend/pages/article/list.rs b/src/frontend/pages/article/list.rs deleted file mode 100644 index 91f4db5..0000000 --- a/src/frontend/pages/article/list.rs +++ /dev/null @@ -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> - } -} diff --git a/src/frontend/pages/article/mod.rs b/src/frontend/pages/article/mod.rs index 0654613..d41b46b 100644 --- a/src/frontend/pages/article/mod.rs +++ b/src/frontend/pages/article/mod.rs @@ -4,5 +4,4 @@ pub mod diff; pub mod discussion; pub mod edit; pub mod history; -pub mod list; pub mod read; diff --git a/src/frontend/pages/article/read.rs b/src/frontend/pages/article/read.rs index ee63d15..9ea492b 100644 --- a/src/frontend/pages/article/read.rs +++ b/src/frontend/pages/article/read.rs @@ -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> } } diff --git a/src/frontend/pages/explore.rs b/src/frontend/pages/explore.rs new file mode 100644 index 0000000..603cb96 --- /dev/null +++ b/src/frontend/pages/explore.rs @@ -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> + } +} diff --git a/src/frontend/pages/instance/details.rs b/src/frontend/pages/instance/details.rs index 09d6f9e..74cd8fd 100644 --- a/src/frontend/pages/instance/details.rs +++ b/src/frontend/pages/instance/details.rs @@ -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> } } diff --git a/src/frontend/pages/instance/list.rs b/src/frontend/pages/instance/list.rs deleted file mode 100644 index 4cb54a3..0000000 --- a/src/frontend/pages/instance/list.rs +++ /dev/null @@ -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> - } -} diff --git a/src/frontend/pages/instance/mod.rs b/src/frontend/pages/instance/mod.rs index c7d8365..4c5dc81 100644 --- a/src/frontend/pages/instance/mod.rs +++ b/src/frontend/pages/instance/mod.rs @@ -1,4 +1,3 @@ pub mod details; -pub mod list; pub mod search; pub mod settings; diff --git a/src/frontend/pages/instance/settings.rs b/src/frontend/pages/instance/settings.rs index 7fcdb14..4501f8a 100644 --- a/src/frontend/pages/instance/settings.rs +++ b/src/frontend/pages/instance/settings.rs @@ -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> } } diff --git a/src/frontend/pages/mod.rs b/src/frontend/pages/mod.rs index c648f24..f563e7e 100644 --- a/src/frontend/pages/mod.rs +++ b/src/frontend/pages/mod.rs @@ -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 }, ) } diff --git a/src/frontend/pages/user/edit_profile.rs b/src/frontend/pages/user/edit_profile.rs index f82f7e1..3872496 100644 --- a/src/frontend/pages/user/edit_profile.rs +++ b/src/frontend/pages/user/edit_profile.rs @@ -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> } } diff --git a/src/frontend/pages/user/notifications.rs b/src/frontend/pages/user/notifications.rs index 104054e..5cd3836 100644 --- a/src/frontend/pages/user/notifications.rs +++ b/src/frontend/pages/user/notifications.rs @@ -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> } } diff --git a/src/frontend/pages/user/profile.rs b/src/frontend/pages/user/profile.rs index a317607..cfd5422 100644 --- a/src/frontend/pages/user/profile.rs +++ b/src/frontend/pages/user/profile.rs @@ -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> } } diff --git a/src/frontend/utils/errors.rs b/src/frontend/utils/errors.rs new file mode 100644 index 0000000..a93d4c1 --- /dev/null +++ b/src/frontend/utils/errors.rs @@ -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()) + } +} diff --git a/src/frontend/utils/mod.rs b/src/frontend/utils/mod.rs index fd9b4da..64094a0 100644 --- a/src/frontend/utils/mod.rs +++ b/src/frontend/utils/mod.rs @@ -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; diff --git a/src/frontend/utils/resources.rs b/src/frontend/utils/resources.rs index c0ae814..4c28078 100644 --- a/src/frontend/utils/resources.rs +++ b/src/frontend/utils/resources.rs @@ -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) } diff --git a/src/lib.rs b/src/lib.rs index 19bb8c4..47c649d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ #[cfg(feature = "ssr")] pub mod backend; pub mod common; -#[expect(clippy::unwrap_used)] pub mod frontend; diff --git a/src/main.rs b/src/main.rs index 8b6ba91..38db839 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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; diff --git a/tests/common.rs b/tests/common.rs index 6d851d0..ea94103 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -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, } } diff --git a/tests/test.rs b/tests/test.rs index e5bcf88..47600c8 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -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(),