diff --git a/crates/api/src/local_user/save_settings.rs b/crates/api/src/local_user/save_settings.rs index 08820cadd..a4858d10e 100644 --- a/crates/api/src/local_user/save_settings.rs +++ b/crates/api/src/local_user/save_settings.rs @@ -142,6 +142,7 @@ pub async fn save_user_settings( enable_keyboard_navigation: data.enable_keyboard_navigation, enable_animated_images: data.enable_animated_images, collapse_bot_comments: data.collapse_bot_comments, + auto_mark_fetched_posts_as_read: data.auto_mark_fetched_posts_as_read, ..Default::default() }; diff --git a/crates/api/src/post/like.rs b/crates/api/src/post/like.rs index c81d9630a..13acaeff9 100644 --- a/crates/api/src/post/like.rs +++ b/crates/api/src/post/like.rs @@ -5,19 +5,13 @@ use lemmy_api_common::{ context::LemmyContext, post::{CreatePostLike, PostResponse}, send_activity::{ActivityChannel, SendActivityData}, - utils::{ - check_bot_account, - check_community_user_action, - check_local_vote_mode, - mark_post_as_read, - VoteItem, - }, + utils::{check_bot_account, check_community_user_action, check_local_vote_mode, VoteItem}, }; use lemmy_db_schema::{ source::{ community::Community, local_site::LocalSite, - post::{Post, PostLike, PostLikeForm}, + post::{Post, PostLike, PostLikeForm, PostRead}, }, traits::{Crud, Likeable}, }; @@ -73,7 +67,8 @@ pub async fn like_post( .with_lemmy_type(LemmyErrorType::CouldntLikePost)?; } - mark_post_as_read(person_id, post_id, &mut context.pool()).await?; + // Mark Post Read + PostRead::mark_as_read(&mut context.pool(), &[post_id], person_id).await?; let community = Community::read(&mut context.pool(), post.community_id).await?; diff --git a/crates/api/src/post/mark_read.rs b/crates/api/src/post/mark_read.rs index 3e534675a..175856136 100644 --- a/crates/api/src/post/mark_read.rs +++ b/crates/api/src/post/mark_read.rs @@ -2,8 +2,7 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{context::LemmyContext, post::MarkPostAsRead, SuccessResponse}; use lemmy_db_schema::source::post::PostRead; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS}; -use std::collections::HashSet; +use lemmy_utils::error::{LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS}; #[tracing::instrument(skip(context))] pub async fn mark_post_as_read( @@ -11,7 +10,7 @@ pub async fn mark_post_as_read( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let post_ids = HashSet::from_iter(data.post_ids.clone()); + let post_ids = &data.post_ids; if post_ids.len() > MAX_API_PARAM_ELEMENTS { Err(LemmyErrorType::TooManyItems)?; @@ -21,13 +20,9 @@ pub async fn mark_post_as_read( // Mark the post as read / unread if data.read { - PostRead::mark_as_read(&mut context.pool(), post_ids, person_id) - .await - .with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?; + PostRead::mark_as_read(&mut context.pool(), post_ids, person_id).await?; } else { - PostRead::mark_as_unread(&mut context.pool(), post_ids, person_id) - .await - .with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?; + PostRead::mark_as_unread(&mut context.pool(), post_ids, person_id).await?; } Ok(Json(SuccessResponse::default())) diff --git a/crates/api/src/post/save.rs b/crates/api/src/post/save.rs index 4549b62b1..4345d8853 100644 --- a/crates/api/src/post/save.rs +++ b/crates/api/src/post/save.rs @@ -2,10 +2,9 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, post::{PostResponse, SavePost}, - utils::mark_post_as_read, }; use lemmy_db_schema::{ - source::post::{PostSaved, PostSavedForm}, + source::post::{PostRead, PostSaved, PostSavedForm}, traits::Saveable, }; use lemmy_db_views::structs::{LocalUserView, PostView}; @@ -42,7 +41,7 @@ pub async fn save_post( ) .await?; - mark_post_as_read(person_id, post_id, &mut context.pool()).await?; + PostRead::mark_as_read(&mut context.pool(), &[post_id], person_id).await?; Ok(Json(PostResponse { post_view })) } diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 6f1ddfe43..bbddbd86b 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -138,6 +138,8 @@ pub struct SaveUserSettings { pub show_upvotes: Option, pub show_downvotes: Option, pub show_upvote_percentage: Option, + /// Whether to automatically mark fetched posts as read. + pub auto_mark_fetched_posts_as_read: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index e358d483b..412bbbb5d 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -28,7 +28,7 @@ use lemmy_db_schema::{ password_reset_request::PasswordResetRequest, person::{Person, PersonUpdateForm}, person_block::PersonBlock, - post::{Post, PostLike, PostRead}, + post::{Post, PostLike}, registration_application::RegistrationApplication, site::Site, }, @@ -64,7 +64,7 @@ use lemmy_utils::{ use moka::future::Cache; use regex::{escape, Regex, RegexSet}; use rosetta_i18n::{Language, LanguageId}; -use std::{collections::HashSet, sync::LazyLock}; +use std::sync::LazyLock; use tracing::warn; use url::{ParseError, Url}; use urlencoding::encode; @@ -140,19 +140,6 @@ pub fn is_top_mod( } } -/// Marks a post as read for a given person. -#[tracing::instrument(skip_all)] -pub async fn mark_post_as_read( - person_id: PersonId, - post_id: PostId, - pool: &mut DbPool<'_>, -) -> LemmyResult<()> { - PostRead::mark_as_read(pool, HashSet::from([post_id]), person_id) - .await - .with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?; - Ok(()) -} - /// Updates the read comment count for a post. Usually done when reading or creating a new comment. #[tracing::instrument(skip_all)] pub async fn update_read_comments( diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index 90c68bdbd..0982cf9b1 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -12,7 +12,6 @@ use lemmy_api_common::{ get_url_blocklist, honeypot_check, local_site_to_slur_regex, - mark_post_as_read, process_markdown_opt, }, }; @@ -22,7 +21,7 @@ use lemmy_db_schema::{ actor_language::CommunityLanguage, community::Community, local_site::LocalSite, - post::{Post, PostInsertForm, PostLike, PostLikeForm}, + post::{Post, PostInsertForm, PostLike, PostLikeForm, PostRead}, }, traits::{Crud, Likeable}, utils::diesel_url_create, @@ -169,7 +168,7 @@ pub async fn create_post( .await .with_lemmy_type(LemmyErrorType::CouldntLikePost)?; - mark_post_as_read(person_id, post_id, &mut context.pool()).await?; + PostRead::mark_as_read(&mut context.pool(), &[post_id], person_id).await?; build_post_response(&context, community_id, local_user_view, post_id).await } diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs index 7677d59ef..f2784e4ce 100644 --- a/crates/api_crud/src/post/read.rs +++ b/crates/api_crud/src/post/read.rs @@ -2,10 +2,13 @@ use actix_web::web::{Data, Json, Query}; use lemmy_api_common::{ context::LemmyContext, post::{GetPost, GetPostResponse}, - utils::{check_private_instance, is_mod_or_admin_opt, mark_post_as_read, update_read_comments}, + utils::{check_private_instance, is_mod_or_admin_opt, update_read_comments}, }; use lemmy_db_schema::{ - source::{comment::Comment, post::Post}, + source::{ + comment::Comment, + post::{Post, PostRead}, + }, traits::Crud, }; use lemmy_db_views::{ @@ -62,7 +65,7 @@ pub async fn get_post( let post_id = post_view.post.id; if let Some(person_id) = person_id { - mark_post_as_read(person_id, post_id, &mut context.pool()).await?; + PostRead::mark_as_read(&mut context.pool(), &[post_id], person_id).await?; update_read_comments( person_id, diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index d75a82d3b..6d569bf67 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -10,7 +10,10 @@ use lemmy_api_common::{ post::{GetPosts, GetPostsResponse}, utils::{check_conflicting_like_filters, check_private_instance}, }; -use lemmy_db_schema::source::community::Community; +use lemmy_db_schema::{ + newtypes::PostId, + source::{community::Community, post::PostRead}, +}; use lemmy_db_views::{ post_view::PostQuery, structs::{LocalUserView, PaginationCursor, SiteView}, @@ -88,6 +91,14 @@ pub async fn list_posts( .await .with_lemmy_type(LemmyErrorType::CouldntGetPosts)?; + // If in their user settings, auto-mark fetched posts as read + if let Some(local_user) = local_user { + if local_user.auto_mark_fetched_posts_as_read { + let post_ids = posts.iter().map(|p| p.post.id).collect::>(); + PostRead::mark_as_read(&mut context.pool(), &post_ids, local_user.person_id).await?; + } + } + // if this page wasn't empty, then there is a next page after the last post on this page let next_page = posts.last().map(PaginationCursor::after_post); Ok(Json(GetPostsResponse { posts, next_page })) diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index fb6245585..2631dbd03 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -39,6 +39,10 @@ use diesel::{ TextExpressionMethods, }; use diesel_async::RunQueryDsl; +use lemmy_utils::{ + error::{LemmyErrorExt, LemmyResult}, + LemmyErrorType, +}; use std::collections::HashSet; #[async_trait] @@ -322,36 +326,41 @@ impl Saveable for PostSaved { impl PostRead { pub async fn mark_as_read( pool: &mut DbPool<'_>, - post_ids: HashSet, + post_ids: &[PostId], person_id: PersonId, - ) -> Result { + ) -> LemmyResult { let conn = &mut get_conn(pool).await?; let forms = post_ids - .into_iter() - .map(|post_id| PostReadForm { post_id, person_id }) + .iter() + .map(|post_id| PostReadForm { + post_id: *post_id, + person_id, + }) .collect::>(); insert_into(post_read::table) .values(forms) .on_conflict_do_nothing() .execute(conn) .await + .with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead) } pub async fn mark_as_unread( pool: &mut DbPool<'_>, - post_id_: HashSet, + post_ids: &[PostId], person_id_: PersonId, - ) -> Result { + ) -> LemmyResult { let conn = &mut get_conn(pool).await?; diesel::delete( post_read::table - .filter(post_read::post_id.eq_any(post_id_)) + .filter(post_read::post_id.eq_any(post_ids)) .filter(post_read::person_id.eq(person_id_)), ) .execute(conn) .await + .with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead) } } @@ -417,7 +426,6 @@ mod tests { use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; - use std::collections::HashSet; use url::Url; #[tokio::test] @@ -521,7 +529,7 @@ mod tests { // Post Read let marked_as_read = PostRead::mark_as_read( pool, - HashSet::from([inserted_post.id, inserted_post2.id]), + &[inserted_post.id, inserted_post2.id], inserted_person.id, ) .await?; @@ -545,7 +553,7 @@ mod tests { assert_eq!(1, saved_removed); let read_removed = PostRead::mark_as_unread( pool, - HashSet::from([inserted_post.id, inserted_post2.id]), + &[inserted_post.id, inserted_post2.id], inserted_person.id, ) .await?; diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 9f1d00568..b212dcb9d 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -474,6 +474,7 @@ diesel::table! { enable_animated_images -> Bool, collapse_bot_comments -> Bool, default_comment_sort_type -> CommentSortTypeEnum, + auto_mark_fetched_posts_as_read -> Bool, } } diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index 37da70908..2b513153d 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -65,6 +65,8 @@ pub struct LocalUser { /// Whether to auto-collapse bot comments. pub collapse_bot_comments: bool, pub default_comment_sort_type: CommentSortType, + /// Whether to automatically mark fetched posts as read. + pub auto_mark_fetched_posts_as_read: bool, } #[derive(Clone, derive_new::new)] @@ -119,6 +121,8 @@ pub struct LocalUserInsertForm { pub collapse_bot_comments: Option, #[new(default)] pub default_comment_sort_type: Option, + #[new(default)] + pub auto_mark_fetched_posts_as_read: Option, } #[derive(Clone, Default)] @@ -149,4 +153,5 @@ pub struct LocalUserUpdateForm { pub enable_animated_images: Option, pub collapse_bot_comments: Option, pub default_comment_sort_type: Option, + pub auto_mark_fetched_posts_as_read: Option, } diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index 4fa2222ae..5a9529f1f 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -1644,7 +1644,7 @@ mod tests { // Mark a post as read PostRead::mark_as_read( pool, - HashSet::from([data.inserted_bot_post.id]), + &[data.inserted_bot_post.id], data.local_user_view.person.id, ) .await?; diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index a0a40789b..e6c985c1a 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -241,6 +241,7 @@ mod tests { enable_keyboard_navigation: inserted_sara_local_user.enable_keyboard_navigation, enable_animated_images: inserted_sara_local_user.enable_animated_images, collapse_bot_comments: inserted_sara_local_user.collapse_bot_comments, + auto_mark_fetched_posts_as_read: false, }, creator: Person { id: inserted_sara_person.id, diff --git a/migrations/2024-11-01-233231_add_mark_fetched_posts_as_read/down.sql b/migrations/2024-11-01-233231_add_mark_fetched_posts_as_read/down.sql new file mode 100644 index 000000000..0f2ff0569 --- /dev/null +++ b/migrations/2024-11-01-233231_add_mark_fetched_posts_as_read/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE local_user + DROP COLUMN auto_mark_fetched_posts_as_read; + diff --git a/migrations/2024-11-01-233231_add_mark_fetched_posts_as_read/up.sql b/migrations/2024-11-01-233231_add_mark_fetched_posts_as_read/up.sql new file mode 100644 index 000000000..fbe57276b --- /dev/null +++ b/migrations/2024-11-01-233231_add_mark_fetched_posts_as_read/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE local_user + ADD COLUMN auto_mark_fetched_posts_as_read boolean DEFAULT FALSE NOT NULL; +