From 32b5411abde60f20234895de0fe53c38583a758b Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sat, 7 Dec 2024 12:38:20 -0500 Subject: [PATCH] A few changes to profile view. - Separating the profile fetch from its combined content fetch. - Starting to separate saved_only into its own combined view. --- Cargo.lock | 1 + crates/api/src/local_user/list_saved.rs | 0 crates/api/src/local_user/mod.rs | 1 + crates/api_common/src/comment.rs | 2 - crates/api_common/src/person.rs | 62 +- crates/api_common/src/post.rs | 2 - crates/api_common/src/site.rs | 2 - crates/apub/src/api/list_comments.rs | 2 - crates/apub/src/api/list_person_content.rs | 50 ++ crates/apub/src/api/list_posts.rs | 2 - crates/apub/src/api/mod.rs | 35 +- crates/apub/src/api/read_person.rs | 69 +-- crates/apub/src/api/search.rs | 3 - .../db_schema/replaceable_schema/triggers.sql | 47 +- crates/db_schema/src/newtypes.rs | 10 +- crates/db_schema/src/schema.rs | 36 +- crates/db_schema/src/source/combined/mod.rs | 3 +- .../src/source/combined/person_content.rs | 30 + .../combined/{profile.rs => person_saved.rs} | 14 +- crates/db_views/Cargo.toml | 1 + crates/db_views/src/comment_view.rs | 61 +- crates/db_views/src/lib.rs | 13 +- .../src/person_content_combined_view.rs | 430 ++++++++++++++ .../src/person_saved_combined_view.rs | 554 ++++++++++++++++++ crates/db_views/src/post_view.rs | 43 +- crates/db_views/src/profile_combined_view.rs | 278 --------- crates/db_views/src/report_combined_view.rs | 168 +++--- crates/db_views/src/structs.rs | 16 +- .../down.sql | 3 + .../up.sql | 65 ++ .../down.sql | 2 - .../up.sql | 30 - src/api_routes_http.rs | 4 + 33 files changed, 1427 insertions(+), 612 deletions(-) create mode 100644 crates/api/src/local_user/list_saved.rs create mode 100644 crates/apub/src/api/list_person_content.rs create mode 100644 crates/db_schema/src/source/combined/person_content.rs rename crates/db_schema/src/source/combined/{profile.rs => person_saved.rs} (66%) create mode 100644 crates/db_views/src/person_content_combined_view.rs create mode 100644 crates/db_views/src/person_saved_combined_view.rs delete mode 100644 crates/db_views/src/profile_combined_view.rs create mode 100644 migrations/2024-12-05-233704_add_person_content_combined_table/down.sql create mode 100644 migrations/2024-12-05-233704_add_person_content_combined_table/up.sql delete mode 100644 migrations/2024-12-05-233704_add_profile_combined_table/down.sql delete mode 100644 migrations/2024-12-05-233704_add_profile_combined_table/up.sql diff --git a/Cargo.lock b/Cargo.lock index eebb1ce1a..5076cd3b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2677,6 +2677,7 @@ version = "0.19.6-beta.7" dependencies = [ "actix-web", "chrono", + "derive-new", "diesel", "diesel-async", "diesel_ltree", diff --git a/crates/api/src/local_user/list_saved.rs b/crates/api/src/local_user/list_saved.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/api/src/local_user/mod.rs b/crates/api/src/local_user/mod.rs index b1ee7c0b6..728cc03af 100644 --- a/crates/api/src/local_user/mod.rs +++ b/crates/api/src/local_user/mod.rs @@ -8,6 +8,7 @@ pub mod get_captcha; pub mod list_banned; pub mod list_logins; pub mod list_media; +pub mod list_saved; pub mod login; pub mod logout; pub mod notifications; diff --git a/crates/api_common/src/comment.rs b/crates/api_common/src/comment.rs index bae9c4de4..0d416e9f0 100644 --- a/crates/api_common/src/comment.rs +++ b/crates/api_common/src/comment.rs @@ -131,8 +131,6 @@ pub struct GetComments { #[cfg_attr(feature = "full", ts(optional))] pub parent_id: Option, #[cfg_attr(feature = "full", ts(optional))] - pub saved_only: Option, - #[cfg_attr(feature = "full", ts(optional))] pub liked_only: Option, #[cfg_attr(feature = "full", ts(optional))] pub disliked_only: Option, diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 0b51c7453..37f03ecaf 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -9,8 +9,8 @@ use lemmy_db_schema::{ }; use lemmy_db_views::structs::{ LocalImageView, - ProfileCombinedPaginationCursor, - ProfileCombinedView, + PersonContentCombinedPaginationCursor, + PersonContentCombinedView, }; use lemmy_db_views_actor::structs::{ CommentReplyView, @@ -226,14 +226,6 @@ pub struct GetPersonDetails { /// Example: dessalines , or dessalines@xyz.tld #[cfg_attr(feature = "full", ts(optional))] pub username: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub community_id: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub saved_only: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub page_cursor: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub page_back: Option, } #[skip_serializing_none] @@ -245,10 +237,58 @@ pub struct GetPersonDetailsResponse { pub person_view: PersonView, #[cfg_attr(feature = "full", ts(optional))] pub site: Option, - pub content: Vec, pub moderates: Vec, } +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Gets a person's content (posts and comments) +/// +/// Either person_id, or username are required. +pub struct ListPersonContent { + #[cfg_attr(feature = "full", ts(optional))] + pub person_id: Option, + /// Example: dessalines , or dessalines@xyz.tld + #[cfg_attr(feature = "full", ts(optional))] + pub username: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_cursor: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_back: Option, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A person's content response. +pub struct ListPersonContentResponse { + pub content: Vec, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Gets your saved posts and comments +pub struct ListSaved { + #[cfg_attr(feature = "full", ts(optional))] + pub page_cursor: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_back: Option, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A person's saved content response. +pub struct ListSavedResponse { + pub saved: Vec, +} + #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 8d8d3e080..81cd7363b 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -95,8 +95,6 @@ pub struct GetPosts { #[cfg_attr(feature = "full", ts(optional))] pub community_name: Option, #[cfg_attr(feature = "full", ts(optional))] - pub saved_only: Option, - #[cfg_attr(feature = "full", ts(optional))] pub liked_only: Option, #[cfg_attr(feature = "full", ts(optional))] pub disliked_only: Option, diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 9babe423c..2ec6f2979 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -94,8 +94,6 @@ pub struct Search { #[cfg_attr(feature = "full", ts(optional))] pub post_url_only: Option, #[cfg_attr(feature = "full", ts(optional))] - pub saved_only: Option, - #[cfg_attr(feature = "full", ts(optional))] pub liked_only: Option, #[cfg_attr(feature = "full", ts(optional))] pub disliked_only: Option, diff --git a/crates/apub/src/api/list_comments.rs b/crates/apub/src/api/list_comments.rs index 3e7a2f4eb..2411b874a 100644 --- a/crates/apub/src/api/list_comments.rs +++ b/crates/apub/src/api/list_comments.rs @@ -46,7 +46,6 @@ pub async fn list_comments( &site_view.local_site, )); let max_depth = data.max_depth; - let saved_only = data.saved_only; let liked_only = data.liked_only; let disliked_only = data.disliked_only; @@ -80,7 +79,6 @@ pub async fn list_comments( listing_type, sort, max_depth, - saved_only, liked_only, disliked_only, community_id, diff --git a/crates/apub/src/api/list_person_content.rs b/crates/apub/src/api/list_person_content.rs new file mode 100644 index 000000000..477e62e85 --- /dev/null +++ b/crates/apub/src/api/list_person_content.rs @@ -0,0 +1,50 @@ +use super::resolve_person_id_from_id_or_username; +use activitypub_federation::config::Data; +use actix_web::web::{Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + person::{ListPersonContent, ListPersonContentResponse}, + utils::check_private_instance, +}; +use lemmy_db_views::{ + person_content_combined_view::PersonContentCombinedQuery, + structs::{LocalUserView, SiteView}, +}; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn list_person_content( + data: Query, + context: Data, + local_user_view: Option, +) -> LemmyResult> { + let local_site = SiteView::read_local(&mut context.pool()).await?; + + check_private_instance(&local_user_view, &local_site.local_site)?; + + let person_details_id = resolve_person_id_from_id_or_username( + &data.person_id, + &data.username, + &context, + &local_user_view, + ) + .await?; + + // parse pagination token + let page_after = if let Some(pa) = &data.page_cursor { + Some(pa.read(&mut context.pool()).await?) + } else { + None + }; + let page_back = data.page_back; + + let content = PersonContentCombinedQuery { + creator_id: person_details_id, + page_after, + page_back, + } + .list(&mut context.pool(), &local_user_view) + .await?; + + Ok(Json(ListPersonContentResponse { content })) +} diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index 63e737fdd..6d043ae4f 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -41,7 +41,6 @@ pub async fn list_posts( } else { data.community_id }; - let saved_only = data.saved_only; let show_hidden = data.show_hidden; let show_read = data.show_read; let show_nsfw = data.show_nsfw; @@ -77,7 +76,6 @@ pub async fn list_posts( listing_type, sort, community_id, - saved_only, liked_only, disliked_only, page, diff --git a/crates/apub/src/api/mod.rs b/crates/apub/src/api/mod.rs index 580be3228..9359eabc4 100644 --- a/crates/apub/src/api/mod.rs +++ b/crates/apub/src/api/mod.rs @@ -1,12 +1,18 @@ +use crate::{fetcher::resolve_actor_identifier, objects::person::ApubPerson}; +use activitypub_federation::config::Data; +use lemmy_api_common::{context::LemmyContext, LemmyErrorType}; use lemmy_db_schema::{ - newtypes::CommunityId, - source::{local_site::LocalSite, local_user::LocalUser}, + newtypes::{CommunityId, PersonId}, + source::{local_site::LocalSite, local_user::LocalUser, person::Person}, CommentSortType, ListingType, PostSortType, }; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyResult; pub mod list_comments; +pub mod list_person_content; pub mod list_posts; pub mod read_community; pub mod read_person; @@ -61,3 +67,28 @@ fn comment_sort_type_with_default( .unwrap_or(local_site.default_comment_sort_type), ) } + +async fn resolve_person_id_from_id_or_username( + person_id: &Option, + username: &Option, + context: &Data, + local_user_view: &Option, +) -> LemmyResult { + // Check to make sure a person name or an id is given + if username.is_none() && person_id.is_none() { + Err(LemmyErrorType::NoIdGiven)? + } + + Ok(match person_id { + Some(id) => *id, + None => { + if let Some(username) = username { + resolve_actor_identifier::(username, context, local_user_view, true) + .await? + .id + } else { + Err(LemmyErrorType::NotFound)? + } + } + }) +} diff --git a/crates/apub/src/api/read_person.rs b/crates/apub/src/api/read_person.rs index ba3160386..fdcb6ba58 100644 --- a/crates/apub/src/api/read_person.rs +++ b/crates/apub/src/api/read_person.rs @@ -1,4 +1,4 @@ -use crate::{fetcher::resolve_actor_identifier, objects::person::ApubPerson}; +use super::resolve_person_id_from_id_or_username; use activitypub_federation::config::Data; use actix_web::web::{Json, Query}; use lemmy_api_common::{ @@ -6,13 +6,9 @@ use lemmy_api_common::{ person::{GetPersonDetails, GetPersonDetailsResponse}, utils::{check_private_instance, read_site_for_actor}, }; -use lemmy_db_schema::source::person::Person; -use lemmy_db_views::{ - profile_combined_view::ProfileCombinedQuery, - structs::{LocalUserView, SiteView}, -}; +use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views_actor::structs::{CommunityModeratorView, PersonView}; -use lemmy_utils::error::{LemmyErrorType, LemmyResult}; +use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] pub async fn read_person( @@ -20,65 +16,21 @@ pub async fn read_person( context: Data, local_user_view: Option, ) -> LemmyResult> { - // Check to make sure a person name or an id is given - if data.username.is_none() && data.person_id.is_none() { - Err(LemmyErrorType::NoIdGiven)? - } - let local_site = SiteView::read_local(&mut context.pool()).await?; check_private_instance(&local_user_view, &local_site.local_site)?; - let person_details_id = match data.person_id { - Some(id) => id, - None => { - if let Some(username) = &data.username { - resolve_actor_identifier::(username, &context, &local_user_view, true) - .await? - .id - } else { - Err(LemmyErrorType::NotFound)? - } - } - }; + let person_details_id = resolve_person_id_from_id_or_username( + &data.person_id, + &data.username, + &context, + &local_user_view, + ) + .await?; // You don't need to return settings for the user, since this comes back with GetSite // `my_user` let person_view = PersonView::read(&mut context.pool(), person_details_id).await?; - - // parse pagination token - let page_after = if let Some(pa) = &data.page_cursor { - Some(pa.read(&mut context.pool()).await?) - } else { - None - }; - let page_back = data.page_back; - let saved_only = data.saved_only; - let community_id = data.community_id; - - // If its saved only, then ignore the person details id, - // and use your local user's id - let creator_id = if !saved_only.unwrap_or_default() { - Some(person_details_id) - } else { - local_user_view.as_ref().map(|u| u.local_user.person_id) - }; - - let content = if let Some(creator_id) = creator_id { - ProfileCombinedQuery { - creator_id, - community_id, - saved_only, - page_after, - page_back, - } - .list(&mut context.pool(), &local_user_view) - .await? - } else { - // if the creator is missing (saved_only, and no local_user), then return empty content - Vec::new() - }; - let moderates = CommunityModeratorView::for_person( &mut context.pool(), person_details_id, @@ -92,6 +44,5 @@ pub async fn read_person( person_view, site, moderates, - content, })) } diff --git a/crates/apub/src/api/search.rs b/crates/apub/src/api/search.rs index cdc9bc55e..0ae7053d3 100644 --- a/crates/apub/src/api/search.rs +++ b/crates/apub/src/api/search.rs @@ -53,7 +53,6 @@ pub async fn search( limit, title_only, post_url_only, - saved_only, liked_only, disliked_only, }) = data; @@ -86,7 +85,6 @@ pub async fn search( url_only: post_url_only, liked_only, disliked_only, - saved_only, ..Default::default() }; @@ -101,7 +99,6 @@ pub async fn search( limit, liked_only, disliked_only, - saved_only, ..Default::default() }; diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index 596a86eeb..9768bb1d2 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -685,31 +685,62 @@ CALL r.create_report_combined_trigger ('comment_report'); CALL r.create_report_combined_trigger ('private_message_report'); --- Profile (comment, post) -CREATE PROCEDURE r.create_profile_combined_trigger (table_name text) +-- person_content (comment, post) +CREATE PROCEDURE r.create_person_content_combined_trigger (table_name text) LANGUAGE plpgsql AS $a$ BEGIN - EXECUTE replace($b$ CREATE FUNCTION r.profile_combined_thing_insert ( ) + EXECUTE replace($b$ CREATE FUNCTION r.person_content_combined_thing_insert ( ) RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN - INSERT INTO profile_combined (published, thing_id) + INSERT INTO person_content_combined (published, thing_id) VALUES (NEW.published, NEW.id); RETURN NEW; END $$; - CREATE TRIGGER profile_combined + CREATE TRIGGER person_content_combined AFTER INSERT ON thing FOR EACH ROW - EXECUTE FUNCTION r.profile_combined_thing_insert ( ); + EXECUTE FUNCTION r.person_content_combined_thing_insert ( ); $b$, 'thing', table_name); END; $a$; -CALL r.create_profile_combined_trigger ('post'); +CALL r.create_person_content_combined_trigger ('post'); + +CALL r.create_person_content_combined_trigger ('comment'); + +-- person_saved (comment, post) +-- TODO, not sure how to handle changes to post_actions and comment_actions.saved column. +-- False should delete this row, true should insert +-- CREATE PROCEDURE r.create_person_saved_combined_trigger (table_name text) +-- LANGUAGE plpgsql +-- AS $a$ +-- BEGIN +-- EXECUTE replace($b$ CREATE FUNCTION r.person_saved_combined_thing_insert ( ) +-- RETURNS TRIGGER +-- LANGUAGE plpgsql +-- AS $$ +-- BEGIN +-- INSERT INTO person_saved_combined (published, thing_id) +-- VALUES (NEW.saved, NEW.id); +-- RETURN NEW; +-- END $$; +-- CREATE TRIGGER person_saved_combined +-- AFTER INSERT ON thing +-- FOR EACH ROW +-- EXECUTE FUNCTION r.person_saved_combined_thing_insert ( ); +-- $b$, +-- 'thing', +-- table_name); +-- END; +-- $a$; + +-- CALL r.create_person_saved_combined_trigger ('post_actions'); + +-- CALL r.create_person_saved_combined_trigger ('comment_actions'); -CALL r.create_profile_combined_trigger ('comment'); diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 38bd4e591..5ae029071 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -188,8 +188,14 @@ pub struct ReportCombinedId(i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] -/// The profile combined id -pub struct ProfileCombinedId(i32); +/// The person content combined id +pub struct PersonContentCombinedId(i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The person saved combined id +pub struct PersonSavedCombinedId(i32); impl DbUrl { pub fn inner(&self) -> &Url { diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 468bd2b0b..dd6690849 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -729,6 +729,15 @@ diesel::table! { } } +diesel::table! { + person_content_combined (id) { + id -> Int4, + published -> Timestamptz, + post_id -> Nullable, + comment_id -> Nullable, + } +} + diesel::table! { person_mention (id) { id -> Int4, @@ -739,6 +748,15 @@ diesel::table! { } } +diesel::table! { + person_saved_combined (id) { + id -> Int4, + published -> Timestamptz, + post_id -> Nullable, + comment_id -> Nullable, + } +} + diesel::table! { post (id) { id -> Int4, @@ -856,15 +874,6 @@ diesel::table! { } } -diesel::table! { - profile_combined (id) { - id -> Int4, - published -> Timestamptz, - post_id -> Nullable, - comment_id -> Nullable, - } -} - diesel::table! { received_activity (ap_id) { ap_id -> Text, @@ -1039,8 +1048,12 @@ diesel::joinable!(password_reset_request -> local_user (local_user_id)); diesel::joinable!(person -> instance (instance_id)); diesel::joinable!(person_aggregates -> person (person_id)); diesel::joinable!(person_ban -> person (person_id)); +diesel::joinable!(person_content_combined -> comment (comment_id)); +diesel::joinable!(person_content_combined -> post (post_id)); diesel::joinable!(person_mention -> comment (comment_id)); diesel::joinable!(person_mention -> person (recipient_id)); +diesel::joinable!(person_saved_combined -> comment (comment_id)); +diesel::joinable!(person_saved_combined -> post (post_id)); diesel::joinable!(post -> community (community_id)); diesel::joinable!(post -> language (language_id)); diesel::joinable!(post -> person (creator_id)); @@ -1052,8 +1065,6 @@ diesel::joinable!(post_aggregates -> person (creator_id)); diesel::joinable!(post_aggregates -> post (post_id)); diesel::joinable!(post_report -> post (post_id)); diesel::joinable!(private_message_report -> private_message (private_message_id)); -diesel::joinable!(profile_combined -> comment (comment_id)); -diesel::joinable!(profile_combined -> post (post_id)); diesel::joinable!(registration_application -> local_user (local_user_id)); diesel::joinable!(registration_application -> person (admin_id)); diesel::joinable!(report_combined -> comment_report (comment_report_id)); @@ -1117,14 +1128,15 @@ diesel::allow_tables_to_appear_in_same_query!( person_actions, person_aggregates, person_ban, + person_content_combined, person_mention, + person_saved_combined, post, post_actions, post_aggregates, post_report, private_message, private_message_report, - profile_combined, received_activity, registration_application, remote_image, diff --git a/crates/db_schema/src/source/combined/mod.rs b/crates/db_schema/src/source/combined/mod.rs index 1d8a026d2..b2b5e7d8e 100644 --- a/crates/db_schema/src/source/combined/mod.rs +++ b/crates/db_schema/src/source/combined/mod.rs @@ -1,2 +1,3 @@ -pub mod profile; +pub mod person_content; +pub mod person_saved; pub mod report; diff --git a/crates/db_schema/src/source/combined/person_content.rs b/crates/db_schema/src/source/combined/person_content.rs new file mode 100644 index 000000000..c85b8f3b3 --- /dev/null +++ b/crates/db_schema/src/source/combined/person_content.rs @@ -0,0 +1,30 @@ +use crate::newtypes::{CommentId, PersonContentCombinedId, PostId}; +#[cfg(feature = "full")] +use crate::schema::person_content_combined; +use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use i_love_jesus::CursorKeysModule; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[skip_serializing_none] +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[cfg_attr( + feature = "full", + derive(Identifiable, Queryable, Selectable, TS, CursorKeysModule) +)] +#[cfg_attr(feature = "full", diesel(table_name = person_content_combined))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +#[cfg_attr(feature = "full", cursor_keys_module(name = person_content_combined_keys))] +/// A combined table for a persons contents (posts and comments) +pub struct PersonContentCombined { + pub id: PersonContentCombinedId, + pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] + pub post_id: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub comment_id: Option, +} diff --git a/crates/db_schema/src/source/combined/profile.rs b/crates/db_schema/src/source/combined/person_saved.rs similarity index 66% rename from crates/db_schema/src/source/combined/profile.rs rename to crates/db_schema/src/source/combined/person_saved.rs index ffb656091..08cdd0415 100644 --- a/crates/db_schema/src/source/combined/profile.rs +++ b/crates/db_schema/src/source/combined/person_saved.rs @@ -1,6 +1,6 @@ -use crate::newtypes::{CommentId, PostId, ProfileCombinedId}; +use crate::newtypes::{CommentId, PersonSavedCombinedId, PostId}; #[cfg(feature = "full")] -use crate::schema::profile_combined; +use crate::schema::person_saved_combined; use chrono::{DateTime, Utc}; #[cfg(feature = "full")] use i_love_jesus::CursorKeysModule; @@ -15,13 +15,13 @@ use ts_rs::TS; feature = "full", derive(Identifiable, Queryable, Selectable, TS, CursorKeysModule) )] -#[cfg_attr(feature = "full", diesel(table_name = profile_combined))] +#[cfg_attr(feature = "full", diesel(table_name = person_saved_combined))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] -#[cfg_attr(feature = "full", cursor_keys_module(name = profile_combined_keys))] -/// A combined profile table. -pub struct ProfileCombined { - pub id: ProfileCombinedId, +#[cfg_attr(feature = "full", cursor_keys_module(name = person_saved_combined_keys))] +/// A combined person_saved table. +pub struct PersonSavedCombined { + pub id: PersonSavedCombinedId, pub published: DateTime, #[cfg_attr(feature = "full", ts(optional))] pub post_id: Option, diff --git a/crates/db_views/Cargo.toml b/crates/db_views/Cargo.toml index df8124c8a..20dca5139 100644 --- a/crates/db_views/Cargo.toml +++ b/crates/db_views/Cargo.toml @@ -40,6 +40,7 @@ ts-rs = { workspace = true, optional = true } actix-web = { workspace = true, optional = true } i-love-jesus = { workspace = true, optional = true } chrono = { workspace = true } +derive-new.workspace = true [dev-dependencies] serial_test = { workspace = true } diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index 2cf751f9f..0067d0807 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -189,13 +189,6 @@ fn queries<'a>() -> Queries< } } - // If its saved only, then filter, and order by the saved time, not the comment creation time. - if options.saved_only.unwrap_or_default() { - query = query - .filter(comment_actions::saved.is_not_null()) - .then_order_by(comment_actions::saved.desc()); - } - if let Some(my_id) = options.local_user.person_id() { let not_creator_filter = comment::creator_id.ne(my_id); if options.liked_only.unwrap_or_default() { @@ -337,7 +330,6 @@ pub struct CommentQuery<'a> { pub creator_id: Option, pub local_user: Option<&'a LocalUser>, pub search_term: Option, - pub saved_only: Option, pub liked_only: Option, pub disliked_only: Option, pub page: Option, @@ -381,15 +373,7 @@ mod tests { newtypes::LanguageId, source::{ actor_language::LocalUserLanguage, - comment::{ - Comment, - CommentInsertForm, - CommentLike, - CommentLikeForm, - CommentSaved, - CommentSavedForm, - CommentUpdateForm, - }, + comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm}, community::{ Community, CommunityFollower, @@ -411,7 +395,7 @@ mod tests { post::{Post, PostInsertForm, PostUpdateForm}, site::{Site, SiteInsertForm}, }, - traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable}, + traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable}, utils::{build_db_pool_for_tests, RANK_DEFAULT}, CommunityVisibility, SubscribedType, @@ -897,47 +881,6 @@ mod tests { cleanup(data, pool).await } - #[tokio::test] - #[serial] - async fn test_saved_order() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - let data = init_data(pool).await?; - - // Save two comments - let save_comment_0_form = CommentSavedForm { - person_id: data.timmy_local_user_view.person.id, - comment_id: data.inserted_comment_0.id, - }; - CommentSaved::save(pool, &save_comment_0_form).await?; - - let save_comment_2_form = CommentSavedForm { - person_id: data.timmy_local_user_view.person.id, - comment_id: data.inserted_comment_2.id, - }; - CommentSaved::save(pool, &save_comment_2_form).await?; - - // Fetch the saved comments - let comments = CommentQuery { - local_user: Some(&data.timmy_local_user_view.local_user), - saved_only: Some(true), - ..Default::default() - } - .list(&data.site, pool) - .await?; - - // There should only be two comments - assert_eq!(2, comments.len()); - - // The first comment, should be the last one saved (descending order) - assert_eq!(comments[0].comment.id, data.inserted_comment_2.id); - - // The second comment, should be the first one saved - assert_eq!(comments[1].comment.id, data.inserted_comment_0.id); - - cleanup(data, pool).await - } - async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { CommentLike::remove( pool, diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs index 9a5d3cb7c..aa3b53e82 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -12,6 +12,10 @@ pub mod local_image_view; #[cfg(feature = "full")] pub mod local_user_view; #[cfg(feature = "full")] +pub mod person_content_combined_view; +#[cfg(feature = "full")] +pub mod person_saved_combined_view; +#[cfg(feature = "full")] pub mod post_report_view; #[cfg(feature = "full")] pub mod post_view; @@ -20,8 +24,6 @@ pub mod private_message_report_view; #[cfg(feature = "full")] pub mod private_message_view; #[cfg(feature = "full")] -pub mod profile_combined_view; -#[cfg(feature = "full")] pub mod registration_application_view; #[cfg(feature = "full")] pub mod report_combined_view; @@ -30,3 +32,10 @@ pub mod site_view; pub mod structs; #[cfg(feature = "full")] pub mod vote_view; + +pub trait InternalToCombinedView { + type CombinedView; + + /// Maps the combined DB row to an enum + fn map_to_enum(&self) -> Option; +} diff --git a/crates/db_views/src/person_content_combined_view.rs b/crates/db_views/src/person_content_combined_view.rs new file mode 100644 index 000000000..69be392de --- /dev/null +++ b/crates/db_views/src/person_content_combined_view.rs @@ -0,0 +1,430 @@ +use crate::{ + structs::{ + CommentView, + LocalUserView, + PersonContentCombinedPaginationCursor, + PersonContentCombinedView, + PersonContentViewInternal, + PostView, + }, + InternalToCombinedView, +}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, + SelectableHelper, +}; +use diesel_async::RunQueryDsl; +use i_love_jesus::PaginatedQueryBuilder; +use lemmy_db_schema::{ + aliases::creator_community_actions, + newtypes::PersonId, + schema::{ + comment, + comment_actions, + comment_aggregates, + community, + community_actions, + image_details, + local_user, + person, + person_actions, + person_content_combined, + post, + post_actions, + post_aggregates, + }, + source::{ + combined::person_content::{person_content_combined_keys as key, PersonContentCombined}, + community::CommunityFollower, + }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, +}; +use lemmy_utils::error::LemmyResult; + +impl PersonContentCombinedPaginationCursor { + // get cursor for page that starts immediately after the given post + pub fn after_post(view: &PersonContentCombinedView) -> PersonContentCombinedPaginationCursor { + let (prefix, id) = match view { + PersonContentCombinedView::Comment(v) => ('C', v.comment.id.0), + PersonContentCombinedView::Post(v) => ('P', v.post.id.0), + }; + // hex encoding to prevent ossification + PersonContentCombinedPaginationCursor(format!("{prefix}{id:x}")) + } + + pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { + let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); + let mut query = person_content_combined::table + .select(PersonContentCombined::as_select()) + .into_boxed(); + let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; + let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; + query = match prefix { + "C" => query.filter(person_content_combined::comment_id.eq(id)), + "P" => query.filter(person_content_combined::post_id.eq(id)), + _ => return Err(err_msg()), + }; + let token = query.first(&mut get_conn(pool).await?).await?; + + Ok(PaginationCursorData(token)) + } +} + +#[derive(Clone)] +pub struct PaginationCursorData(PersonContentCombined); + +#[derive(derive_new::new)] +pub struct PersonContentCombinedQuery { + pub creator_id: PersonId, + #[new(default)] + pub page_after: Option, + #[new(default)] + pub page_back: Option, +} + +impl PersonContentCombinedQuery { + pub async fn list( + self, + pool: &mut DbPool<'_>, + user: &Option, + ) -> LemmyResult> { + let my_person_id = user.as_ref().map(|u| u.local_user.person_id); + let item_creator = person::id; + + let conn = &mut get_conn(pool).await?; + + // Notes: since the post_id and comment_id are optional columns, + // many joins must use an OR condition. + // For example, the creator must be the person table joined to either: + // - post.creator_id + // - comment.creator_id + let query = person_content_combined::table + // The comment + .left_join(comment::table.on(person_content_combined::comment_id.eq(comment::id.nullable()))) + // The post + // It gets a bit complicated here, because since both comments and post combined have a post + // attached, you can do an inner join. + .inner_join( + post::table.on( + person_content_combined::post_id + .eq(post::id.nullable()) + .or(comment::post_id.eq(post::id)), + ), + ) + // The item creator + .inner_join( + person::table.on( + comment::creator_id + .eq(item_creator) + // Need to filter out the post rows where both the post and comment creator are the + // same. + .or( + post::creator_id + .eq(item_creator) + .and(person_content_combined::post_id.is_not_null()), + ), + ), + ) + // The community + .inner_join(community::table.on(post::community_id.eq(community::id))) + .left_join(actions_alias( + creator_community_actions, + item_creator, + post::community_id, + )) + .left_join( + local_user::table.on( + item_creator + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ) + .left_join(actions( + community_actions::table, + my_person_id, + post::community_id, + )) + .left_join(actions(post_actions::table, my_person_id, post::id)) + .left_join(actions(person_actions::table, my_person_id, item_creator)) + .inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) + .left_join( + comment_aggregates::table + .on(person_content_combined::comment_id.eq(comment_aggregates::comment_id.nullable())), + ) + .left_join(actions(comment_actions::table, my_person_id, comment::id)) + .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) + // The creator id filter + .filter(item_creator.eq(self.creator_id)) + .select(( + // Post-specific + post_aggregates::all_columns, + coalesce( + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), + post_aggregates::comments, + ), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + post_actions::like_score.nullable(), + image_details::all_columns.nullable(), + // Comment-specific + comment::all_columns.nullable(), + comment_aggregates::all_columns.nullable(), + comment_actions::saved.nullable().is_not_null(), + comment_actions::like_score.nullable(), + // Shared + post::all_columns, + community::all_columns, + person::all_columns, + CommunityFollower::select_subscribed_type(), + local_user::admin.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + person_actions::blocked.nullable().is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + )) + .into_boxed(); + + let mut query = PaginatedQueryBuilder::new(query); + + let page_after = self.page_after.map(|c| c.0); + + if self.page_back.unwrap_or_default() { + query = query.before(page_after).limit_and_offset_from_end(); + } else { + query = query.after(page_after); + } + + // Sorting by published + query = query + .then_desc(key::published) + // Tie breaker + .then_desc(key::id); + + let res = query.load::(conn).await?; + + // Map the query results to the enum + let out = res.into_iter().filter_map(|u| u.map_to_enum()).collect(); + + Ok(out) + } +} + +impl InternalToCombinedView for PersonContentViewInternal { + type CombinedView = PersonContentCombinedView; + + fn map_to_enum(&self) -> Option { + // Use for a short alias + let v = self.clone(); + + if let (Some(comment), Some(counts)) = (v.comment, v.comment_counts) { + Some(PersonContentCombinedView::Comment(CommentView { + comment, + counts, + post: v.post, + community: v.community, + creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.comment_saved, + my_vote: v.my_comment_vote, + banned_from_community: v.banned_from_community, + })) + } else { + Some(PersonContentCombinedView::Post(PostView { + post: v.post, + community: v.community, + unread_comments: v.post_unread_comments, + counts: v.post_counts, + creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.post_saved, + read: v.post_read, + hidden: v.post_hidden, + my_vote: v.my_post_vote, + image_details: v.image_details, + banned_from_community: v.banned_from_community, + })) + } + } +} + +#[cfg(test)] +#[expect(clippy::indexing_slicing)] +mod tests { + + use crate::{ + person_content_combined_view::PersonContentCombinedQuery, + structs::PersonContentCombinedView, + }; + use lemmy_db_schema::{ + source::{ + comment::{Comment, CommentInsertForm}, + community::{Community, CommunityInsertForm}, + instance::Instance, + person::{Person, PersonInsertForm}, + post::{Post, PostInsertForm}, + }, + traits::Crud, + utils::{build_db_pool_for_tests, DbPool}, + }; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + + struct Data { + instance: Instance, + timmy: Person, + sara: Person, + timmy_post: Post, + timmy_post_2: Post, + sara_post: Post, + timmy_comment: Comment, + sara_comment: Comment, + sara_comment_2: Comment, + } + + async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { + let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let timmy_form = PersonInsertForm::test_form(instance.id, "timmy_pcv"); + let timmy = Person::create(pool, &timmy_form).await?; + + let sara_form = PersonInsertForm::test_form(instance.id, "sara_pcv"); + let sara = Person::create(pool, &sara_form).await?; + + let community_form = CommunityInsertForm::new( + instance.id, + "test community pcv".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let community = Community::create(pool, &community_form).await?; + + let timmy_post_form = PostInsertForm::new("timmy post prv".into(), timmy.id, community.id); + let timmy_post = Post::create(pool, &timmy_post_form).await?; + + let timmy_post_form_2 = PostInsertForm::new("timmy post prv 2".into(), timmy.id, community.id); + let timmy_post_2 = Post::create(pool, &timmy_post_form_2).await?; + + let sara_post_form = PostInsertForm::new("sara post prv".into(), sara.id, community.id); + let sara_post = Post::create(pool, &sara_post_form).await?; + + let timmy_comment_form = + CommentInsertForm::new(timmy.id, timmy_post.id, "timmy comment prv".into()); + let timmy_comment = Comment::create(pool, &timmy_comment_form, None).await?; + + let sara_comment_form = + CommentInsertForm::new(sara.id, timmy_post.id, "sara comment prv".into()); + let sara_comment = Comment::create(pool, &sara_comment_form, None).await?; + + let sara_comment_form_2 = + CommentInsertForm::new(sara.id, timmy_post_2.id, "sara comment prv 2".into()); + let sara_comment_2 = Comment::create(pool, &sara_comment_form_2, None).await?; + + Ok(Data { + instance, + timmy, + sara, + timmy_post, + timmy_post_2, + sara_post, + timmy_comment, + sara_comment, + sara_comment_2, + }) + } + + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { + Instance::delete(pool, data.instance.id).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_combined() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Do a batch read of timmy + let timmy_content = PersonContentCombinedQuery::new(data.timmy.id) + .list(pool, &None) + .await?; + assert_eq!(3, timmy_content.len()); + + // Make sure the report types are correct + if let PersonContentCombinedView::Comment(v) = &timmy_content[0] { + assert_eq!(data.timmy_comment.id, v.comment.id); + assert_eq!(data.timmy.id, v.creator.id); + } else { + panic!("wrong type"); + } + if let PersonContentCombinedView::Post(v) = &timmy_content[1] { + assert_eq!(data.timmy_post_2.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + if let PersonContentCombinedView::Post(v) = &timmy_content[2] { + assert_eq!(data.timmy_post.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + + // Do a batch read of sara + let sara_content = PersonContentCombinedQuery::new(data.sara.id) + .list(pool, &None) + .await?; + assert_eq!(3, sara_content.len()); + + // Make sure the report types are correct + if let PersonContentCombinedView::Comment(v) = &sara_content[0] { + assert_eq!(data.sara_comment_2.id, v.comment.id); + assert_eq!(data.sara.id, v.creator.id); + // This one was to timmy_post_2 + assert_eq!(data.timmy_post_2.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + if let PersonContentCombinedView::Comment(v) = &sara_content[1] { + assert_eq!(data.sara_comment.id, v.comment.id); + assert_eq!(data.sara.id, v.creator.id); + assert_eq!(data.timmy_post.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + if let PersonContentCombinedView::Post(v) = &sara_content[2] { + assert_eq!(data.sara_post.id, v.post.id); + assert_eq!(data.sara.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + + cleanup(data, pool).await?; + + Ok(()) + } +} diff --git a/crates/db_views/src/person_saved_combined_view.rs b/crates/db_views/src/person_saved_combined_view.rs new file mode 100644 index 000000000..2e0a9f5bf --- /dev/null +++ b/crates/db_views/src/person_saved_combined_view.rs @@ -0,0 +1,554 @@ +// use crate::{ +// structs::{ +// CommentView, +// LocalUserView, +// PostView, +// ProfileCombinedPaginationCursor, +// PersonContentCombinedView, +// PersonContentViewInternal, +// }, +// InternalToCombinedView, +// }; +// use diesel::{ +// result::Error, +// BoolExpressionMethods, +// ExpressionMethods, +// JoinOnDsl, +// NullableExpressionMethods, +// QueryDsl, +// SelectableHelper, +// }; +// use diesel_async::RunQueryDsl; +// use i_love_jesus::PaginatedQueryBuilder; +// use lemmy_db_schema::{ +// aliases::creator_community_actions, +// newtypes::{CommunityId, PersonId}, +// schema::{ +// comment, +// comment_actions, +// comment_aggregates, +// community, +// community_actions, +// image_details, +// local_user, +// person, +// person_actions, +// post, +// post_actions, +// post_aggregates, +// profile_combined, +// }, +// source::{ +// combined::profile::{profile_combined_keys as key, ProfileCombined}, +// community::CommunityFollower, +// }, +// utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool, ReverseTimestampKey}, +// }; +// use lemmy_utils::error::LemmyResult; + +// impl ProfileCombinedPaginationCursor { +// // get cursor for page that starts immediately after the given post +// pub fn after_post(view: &PersonContentCombinedView) -> ProfileCombinedPaginationCursor { +// let (prefix, id) = match view { +// PersonContentCombinedView::Comment(v) => ('C', v.comment.id.0), +// PersonContentCombinedView::Post(v) => ('P', v.post.id.0), +// }; +// // hex encoding to prevent ossification +// ProfileCombinedPaginationCursor(format!("{prefix}{id:x}")) +// } + +// pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { +// let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); +// let mut query = profile_combined::table +// .select(ProfileCombined::as_select()) +// .into_boxed(); +// let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; +// let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; +// query = match prefix { +// "C" => query.filter(profile_combined::comment_id.eq(id)), +// "P" => query.filter(profile_combined::post_id.eq(id)), +// _ => return Err(err_msg()), +// }; +// let token = query.first(&mut get_conn(pool).await?).await?; + +// Ok(PaginationCursorData(token)) +// } +// } + +// #[derive(Clone)] +// pub struct PaginationCursorData(ProfileCombined); + +// #[derive(Default)] +// pub struct ProfileCombinedQuery { +// pub creator_id: PersonId, +// pub page_after: Option, +// pub page_back: Option, +// } + +// impl ProfileCombinedQuery { +// pub async fn list( +// self, +// pool: &mut DbPool<'_>, +// user: &Option, +// ) -> LemmyResult> { +// let my_person_id = user +// .as_ref() +// .map(|u| u.local_user.person_id) +// .unwrap_or(PersonId(-1)); +// let item_creator = person::id; + +// let conn = &mut get_conn(pool).await?; + +// // Notes: since the post_id and comment_id are optional columns, +// // many joins must use an OR condition. +// // For example, the creator must be the person table joined to either: +// // - post.creator_id +// // - comment.creator_id +// let mut query = profile_combined::table +// // The comment +// .left_join(comment::table.on(profile_combined::comment_id.eq(comment::id.nullable()))) +// // The post +// .inner_join( +// post::table.on( +// profile_combined::post_id +// .eq(post::id.nullable()) +// .or(comment::post_id.nullable().eq(profile_combined::post_id)), +// ), +// ) +// // The item creator +// .inner_join( +// person::table.on( +// comment::creator_id +// .eq(person::id) +// .or(post::creator_id.eq(person::id)), +// ), +// ) +// // The community +// .inner_join(community::table.on(post::community_id.eq(community::id))) +// .left_join(actions_alias( +// creator_community_actions, +// item_creator, +// post::community_id, +// )) +// .left_join( +// local_user::table.on( +// item_creator +// .eq(local_user::person_id) +// .and(local_user::admin.eq(true)), +// ), +// ) +// .left_join(actions( +// community_actions::table, +// Some(my_person_id), +// post::community_id, +// )) +// .left_join(actions(post_actions::table, Some(my_person_id), post::id)) +// .left_join(actions( +// person_actions::table, +// Some(my_person_id), +// item_creator, +// )) +// .inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) +// .left_join( +// comment_aggregates::table +// .on(profile_combined::comment_id.eq(comment_aggregates::comment_id.nullable())), +// ) +// .left_join(actions( +// comment_actions::table, +// Some(my_person_id), +// comment::id, +// )) +// .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) +// // The creator id filter +// .filter(item_creator.eq(self.creator_id)) +// .select(( +// // Post-specific +// post_aggregates::all_columns, +// coalesce( +// post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), +// post_aggregates::comments, +// ), +// post_actions::saved.nullable().is_not_null(), +// post_actions::read.nullable().is_not_null(), +// post_actions::hidden.nullable().is_not_null(), +// post_actions::like_score.nullable(), +// image_details::all_columns.nullable(), +// // Comment-specific +// comment::all_columns.nullable(), +// comment_aggregates::all_columns.nullable(), +// comment_actions::saved.nullable().is_not_null(), +// comment_actions::like_score.nullable(), +// // Shared +// post::all_columns, +// community::all_columns, +// person::all_columns, +// CommunityFollower::select_subscribed_type(), +// local_user::admin.nullable().is_not_null(), +// creator_community_actions +// .field(community_actions::became_moderator) +// .nullable() +// .is_not_null(), +// creator_community_actions +// .field(community_actions::received_ban) +// .nullable() +// .is_not_null(), +// person_actions::blocked.nullable().is_not_null(), +// community_actions::received_ban.nullable().is_not_null(), +// )) +// .into_boxed(); + +// let mut query = PaginatedQueryBuilder::new(query); + +// let page_after = self.page_after.map(|c| c.0); + +// if self.page_back.unwrap_or_default() { +// query = query.before(page_after).limit_and_offset_from_end(); +// } else { +// query = query.after(page_after); +// } + +// // Sorting by published +// query = query +// .then_desc(ReverseTimestampKey(key::published)) +// // Tie breaker +// .then_desc(key::id); + +// let res = query.load::(conn).await?; + +// // Map the query results to the enum +// let out = res.into_iter().filter_map(|u| u.map_to_enum()).collect(); + +// Ok(out) +// } +// } + +// impl InternalToCombinedView for PersonContentViewInternal { +// type CombinedView = PersonContentCombinedView; + +// fn map_to_enum(&self) -> Option { +// // Use for a short alias +// let v = self.clone(); + +// if let (Some(comment), Some(counts)) = (v.comment, v.comment_counts) { +// Some(PersonContentCombinedView::Comment(CommentView { +// comment, +// counts, +// post: v.post, +// community: v.community, +// creator: v.item_creator, +// creator_banned_from_community: v.item_creator_banned_from_community, +// creator_is_moderator: v.item_creator_is_moderator, +// creator_is_admin: v.item_creator_is_admin, +// creator_blocked: v.item_creator_blocked, +// subscribed: v.subscribed, +// saved: v.comment_saved, +// my_vote: v.my_comment_vote, +// banned_from_community: v.banned_from_community, +// })) +// } else { +// Some(PersonContentCombinedView::Post(PostView { +// post: v.post, +// community: v.community, +// unread_comments: v.post_unread_comments, +// counts: v.post_counts, +// creator: v.item_creator, +// creator_banned_from_community: v.item_creator_banned_from_community, +// creator_is_moderator: v.item_creator_is_moderator, +// creator_is_admin: v.item_creator_is_admin, +// creator_blocked: v.item_creator_blocked, +// subscribed: v.subscribed, +// saved: v.post_saved, +// read: v.post_read, +// hidden: v.post_hidden, +// my_vote: v.my_post_vote, +// image_details: v.image_details, +// banned_from_community: v.banned_from_community, +// })) +// } +// } +// } + +// #[cfg(test)] +// #[expect(clippy::indexing_slicing)] +// mod tests { + +// use crate::{ +// profile_combined_view::ProfileCombinedQuery, +// report_combined_view::ReportCombinedQuery, +// structs::{ +// CommentReportView, +// LocalUserView, +// PostReportView, +// PersonContentCombinedView, +// ReportCombinedView, +// ReportCombinedViewInternal, +// }, +// }; +// use lemmy_db_schema::{ +// aggregates::structs::{CommentAggregates, PostAggregates}, +// assert_length, +// source::{ +// comment::{Comment, CommentInsertForm, CommentSaved, CommentSavedForm}, +// comment_report::{CommentReport, CommentReportForm}, +// community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm}, +// instance::Instance, +// local_user::{LocalUser, LocalUserInsertForm}, +// local_user_vote_display_mode::LocalUserVoteDisplayMode, +// person::{Person, PersonInsertForm}, +// post::{Post, PostInsertForm}, +// post_report::{PostReport, PostReportForm}, +// private_message::{PrivateMessage, PrivateMessageInsertForm}, +// private_message_report::{PrivateMessageReport, PrivateMessageReportForm}, +// }, +// traits::{Crud, Joinable, Reportable, Saveable}, +// utils::{build_db_pool_for_tests, DbPool}, +// }; +// use lemmy_utils::error::LemmyResult; +// use pretty_assertions::assert_eq; +// use serial_test::serial; + +// struct Data { +// instance: Instance, +// timmy: Person, +// sara: Person, +// timmy_view: LocalUserView, +// community: Community, +// timmy_post: Post, +// timmy_post_2: Post, +// sara_post: Post, +// timmy_comment: Comment, +// sara_comment: Comment, +// sara_comment_2: Comment, +// } + +// async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { +// let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + +// let timmy_form = PersonInsertForm::test_form(inserted_instance.id, "timmy_pcv"); +// let inserted_timmy = Person::create(pool, &timmy_form).await?; +// let timmy_local_user_form = LocalUserInsertForm::test_form(inserted_timmy.id); +// let timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; +// let timmy_view = LocalUserView { +// local_user: timmy_local_user, +// local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), +// person: inserted_timmy.clone(), +// counts: Default::default(), +// }; + +// let sara_form = PersonInsertForm::test_form(inserted_instance.id, "sara_pcv"); +// let inserted_sara = Person::create(pool, &sara_form).await?; + +// let community_form = CommunityInsertForm::new( +// inserted_instance.id, +// "test community pcv".to_string(), +// "nada".to_owned(), +// "pubkey".to_string(), +// ); +// let inserted_community = Community::create(pool, &community_form).await?; + +// let timmy_post_form = PostInsertForm::new( +// "timmy post prv".into(), +// inserted_timmy.id, +// inserted_community.id, +// ); +// let timmy_post = Post::create(pool, &timmy_post_form).await?; + +// let timmy_post_form_2 = PostInsertForm::new( +// "timmy post prv 2".into(), +// inserted_timmy.id, +// inserted_community.id, +// ); +// let timmy_post_2 = Post::create(pool, &timmy_post_form_2).await?; + +// let sara_post_form = PostInsertForm::new( +// "sara post prv".into(), +// inserted_sara.id, +// inserted_community.id, +// ); +// let sara_post = Post::create(pool, &sara_post_form).await?; + +// let timmy_comment_form = +// CommentInsertForm::new(inserted_timmy.id, timmy_post.id, "timmy comment prv".into()); +// let timmy_comment = Comment::create(pool, &timmy_comment_form, None).await?; + +// let sara_comment_form = +// CommentInsertForm::new(inserted_sara.id, timmy_post.id, "sara comment prv".into()); +// let sara_comment = Comment::create(pool, &sara_comment_form, None).await?; + +// let sara_comment_form_2 = CommentInsertForm::new( +// inserted_sara.id, +// timmy_post_2.id, +// "sara comment prv 2".into(), +// ); +// let sara_comment_2 = Comment::create(pool, &sara_comment_form_2, None).await?; + +// Ok(Data { +// instance: inserted_instance, +// timmy: inserted_timmy, +// sara: inserted_sara, +// timmy_view, +// community: inserted_community, +// timmy_post, +// timmy_post_2, +// sara_post, +// timmy_comment, +// sara_comment, +// sara_comment_2, +// }) +// } + +// async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { +// Instance::delete(pool, data.instance.id).await?; + +// Ok(()) +// } + +// #[tokio::test] +// #[serial] +// async fn test_combined() -> LemmyResult<()> { +// let pool = &build_db_pool_for_tests(); +// let pool = &mut pool.into(); +// let data = init_data(pool).await?; + +// // Do a batch read of timmy +// let timmy_content = ProfileCombinedQuery::default().list(pool, &None).await?; +// assert_eq!(3, timmy_content.len()); + +// // Make sure the report types are correct +// if let PersonContentCombinedView::Comment(v) = &timmy_content[0] { +// assert_eq!(data.timmy_comment.id, v.comment.id); +// assert_eq!(data.timmy.id, v.creator.id); +// } else { +// panic!("wrong type"); +// } +// if let PersonContentCombinedView::Post(v) = &timmy_content[1] { +// assert_eq!(data.timmy_post_2.id, v.post.id); +// assert_eq!(data.timmy.id, v.post.creator_id); +// } else { +// panic!("wrong type"); +// } +// if let PersonContentCombinedView::Post(v) = &timmy_content[2] { +// assert_eq!(data.timmy_post.id, v.post.id); +// assert_eq!(data.timmy.id, v.post.creator_id); +// } else { +// panic!("wrong type"); +// } + +// // Do a batch read of sara +// let sara_content = ProfileCombinedQuery::default().list(pool, &None).await?; +// assert_eq!(3, sara_content.len()); + +// // Make sure the report types are correct +// if let PersonContentCombinedView::Comment(v) = &sara_content[0] { +// assert_eq!(data.sara_comment_2.id, v.comment.id); +// assert_eq!(data.sara.id, v.creator.id); +// // This one was to timmy_post_2 +// assert_eq!(data.timmy_post_2.id, v.post.id); +// assert_eq!(data.timmy.id, v.post.creator_id); +// } else { +// panic!("wrong type"); +// } +// if let PersonContentCombinedView::Comment(v) = &sara_content[1] { +// assert_eq!(data.sara_comment.id, v.comment.id); +// assert_eq!(data.sara.id, v.creator.id); +// assert_eq!(data.timmy_post.id, v.post.id); +// assert_eq!(data.timmy.id, v.post.creator_id); +// } else { +// panic!("wrong type"); +// } +// if let PersonContentCombinedView::Post(v) = &sara_content[2] { +// assert_eq!(data.timmy_post.id, v.post.id); +// assert_eq!(data.timmy.id, v.post.creator_id); +// } else { +// panic!("wrong type"); +// } + +// // Timmy saves sara's comment, and his 2nd post +// let save_comment_0_form = CommentSavedForm { +// person_id: data.timmy.id, +// comment_id: data.sara_comment.id, +// }; +// CommentSaved::save(pool, &save_comment_0_form).await?; + +// // Timmy saves sara's comment, and his 2nd post +// let save_comment_0_form = CommentSavedForm { +// person_id: data.timmy.id, +// comment_id: data.sara_comment.id, +// }; +// CommentSaved::save(pool, &save_comment_0_form).await?; + +// // Do a saved_only query +// let timmy_content_saved_only = ProfileCombinedQuery {}.list(pool, &None).await?; + +// cleanup(data, pool).await?; + +// Ok(()) +// } +// } +// #[tokio::test] +// #[serial] +// async fn test_saved_order() -> LemmyResult<()> { +// let pool = &build_db_pool_for_tests(); +// let pool = &mut pool.into(); +// let data = init_data(pool).await?; + +// // Save two comments +// let save_comment_0_form = CommentSavedForm { +// person_id: data.timmy_local_user_view.person.id, +// comment_id: data.inserted_comment_0.id, +// }; +// CommentSaved::save(pool, &save_comment_0_form).await?; + +// let save_comment_2_form = CommentSavedForm { +// person_id: data.timmy_local_user_view.person.id, +// comment_id: data.inserted_comment_2.id, +// }; +// CommentSaved::save(pool, &save_comment_2_form).await?; + +// // Fetch the saved comments +// let comments = CommentQuery { +// local_user: Some(&data.timmy_local_user_view.local_user), +// saved_only: Some(true), +// ..Default::default() +// } +// .list(&data.site, pool) +// .await?; + +// // There should only be two comments +// assert_eq!(2, comments.len()); + +// // The first comment, should be the last one saved (descending order) +// assert_eq!(comments[0].comment.id, data.inserted_comment_2.id); + +// // The second comment, should be the first one saved +// assert_eq!(comments[1].comment.id, data.inserted_comment_0.id); + +// cleanup(data, pool).await +// } +// #[tokio::test] +// #[serial] +// async fn post_listing_saved_only() -> LemmyResult<()> { +// let pool = &build_db_pool()?; +// let pool = &mut pool.into(); +// let data = init_data(pool).await?; + +// // Save only the bot post +// // The saved_only should only show the bot post +// let post_save_form = +// PostSavedForm::new(data.inserted_bot_post.id, data.local_user_view.person.id); +// PostSaved::save(pool, &post_save_form).await?; + +// // Read the saved only +// let read_saved_post_listing = PostQuery { +// community_id: Some(data.inserted_community.id), +// saved_only: Some(true), +// ..data.default_post_query() +// } +// .list(&data.site, pool) +// .await?; + +// // This should only include the bot post, not the one you created +// assert_eq!(vec![POST_BY_BOT], names(&read_saved_post_listing)); + +// cleanup(data, pool).await +// } diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index c6d1b036f..e4a65721e 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -287,15 +287,7 @@ fn queries<'a>() -> Queries< query = query.filter(post_aggregates::comments.eq(0)); }; - // If its saved only, then filter, and order by the saved time, not the comment creation time. - if options.saved_only.unwrap_or_default() { - query = query - .filter(post_actions::saved.is_not_null()) - .then_order_by(post_actions::saved.desc()); - } - // Only hide the read posts, if the saved_only is false. Otherwise ppl with the hide_read - // setting wont be able to see saved posts. - else if !options + if !options .show_read .unwrap_or(options.local_user.show_read_posts()) { @@ -488,7 +480,6 @@ pub struct PostQuery<'a> { pub local_user: Option<&'a LocalUser>, pub search_term: Option, pub url_only: Option, - pub saved_only: Option, pub liked_only: Option, pub disliked_only: Option, pub title_only: Option, @@ -646,13 +637,11 @@ mod tests { PostLikeForm, PostRead, PostReadForm, - PostSaved, - PostSavedForm, PostUpdateForm, }, site::Site, }, - traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable}, + traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable}, utils::{build_db_pool, build_db_pool_for_tests, get_conn, uplete, DbPool, RANK_DEFAULT}, CommunityVisibility, PostSortType, @@ -1090,34 +1079,6 @@ mod tests { cleanup(data, pool).await } - #[tokio::test] - #[serial] - async fn post_listing_saved_only() -> LemmyResult<()> { - let pool = &build_db_pool()?; - let pool = &mut pool.into(); - let data = init_data(pool).await?; - - // Save only the bot post - // The saved_only should only show the bot post - let post_save_form = - PostSavedForm::new(data.inserted_bot_post.id, data.local_user_view.person.id); - PostSaved::save(pool, &post_save_form).await?; - - // Read the saved only - let read_saved_post_listing = PostQuery { - community_id: Some(data.inserted_community.id), - saved_only: Some(true), - ..data.default_post_query() - } - .list(&data.site, pool) - .await?; - - // This should only include the bot post, not the one you created - assert_eq!(vec![POST_BY_BOT], names(&read_saved_post_listing)); - - cleanup(data, pool).await - } - #[tokio::test] #[serial] async fn creator_info() -> LemmyResult<()> { diff --git a/crates/db_views/src/profile_combined_view.rs b/crates/db_views/src/profile_combined_view.rs deleted file mode 100644 index d62b9315c..000000000 --- a/crates/db_views/src/profile_combined_view.rs +++ /dev/null @@ -1,278 +0,0 @@ -use crate::structs::{ - CommentView, - LocalUserView, - PostView, - ProfileCombinedPaginationCursor, - ProfileCombinedView, - ProfileCombinedViewInternal, -}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, - SelectableHelper, -}; -use diesel_async::RunQueryDsl; -use i_love_jesus::PaginatedQueryBuilder; -use lemmy_db_schema::{ - aliases::creator_community_actions, - newtypes::{CommunityId, PersonId}, - schema::{ - comment, - comment_actions, - comment_aggregates, - community, - community_actions, - image_details, - local_user, - person, - person_actions, - post, - post_actions, - post_aggregates, - profile_combined, - }, - source::{ - combined::profile::{profile_combined_keys as key, ProfileCombined}, - community::CommunityFollower, - }, - utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool, ReverseTimestampKey}, -}; -use lemmy_utils::error::LemmyResult; - -impl ProfileCombinedPaginationCursor { - // get cursor for page that starts immediately after the given post - pub fn after_post(view: &ProfileCombinedView) -> ProfileCombinedPaginationCursor { - let (prefix, id) = match view { - ProfileCombinedView::Comment(v) => ('C', v.comment.id.0), - ProfileCombinedView::Post(v) => ('P', v.post.id.0), - }; - // hex encoding to prevent ossification - ProfileCombinedPaginationCursor(format!("{prefix}{id:x}")) - } - - pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { - let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); - let mut query = profile_combined::table - .select(ProfileCombined::as_select()) - .into_boxed(); - let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; - let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; - query = match prefix { - "C" => query.filter(profile_combined::comment_id.eq(id)), - "P" => query.filter(profile_combined::post_id.eq(id)), - _ => return Err(err_msg()), - }; - let token = query.first(&mut get_conn(pool).await?).await?; - - Ok(PaginationCursorData(token)) - } -} - -#[derive(Clone)] -pub struct PaginationCursorData(ProfileCombined); - -#[derive(Default)] -pub struct ProfileCombinedQuery { - pub creator_id: PersonId, - pub community_id: Option, - pub saved_only: Option, - pub page_after: Option, - pub page_back: Option, -} - -impl ProfileCombinedQuery { - pub async fn list( - self, - pool: &mut DbPool<'_>, - user: &Option, - ) -> LemmyResult> { - let my_person_id = user - .as_ref() - .map(|u| u.local_user.person_id) - .unwrap_or(PersonId(-1)); - let item_creator = person::id; - - let conn = &mut get_conn(pool).await?; - - // Notes: since the post_id and comment_id are optional columns, - // many joins must use an OR condition. - // For example, the creator must be the person table joined to either: - // - post.creator_id - // - comment.creator_id - let mut query = profile_combined::table - // The comment - .left_join(comment::table.on(profile_combined::comment_id.eq(comment::id.nullable()))) - // The post - .inner_join( - post::table.on( - profile_combined::post_id - .eq(post::id.nullable()) - .or(comment::post_id.nullable().eq(profile_combined::post_id)), - ), - ) - // The item creator - .inner_join( - person::table.on( - comment::creator_id - .eq(person::id) - .or(post::creator_id.eq(person::id)), - ), - ) - // The community - .inner_join(community::table.on(post::community_id.eq(community::id))) - .left_join(actions_alias( - creator_community_actions, - item_creator, - post::community_id, - )) - .left_join( - local_user::table.on( - item_creator - .eq(local_user::person_id) - .and(local_user::admin.eq(true)), - ), - ) - .left_join(actions( - community_actions::table, - Some(my_person_id), - post::community_id, - )) - .left_join(actions(post_actions::table, Some(my_person_id), post::id)) - .left_join(actions( - person_actions::table, - Some(my_person_id), - item_creator, - )) - .inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) - .left_join( - comment_aggregates::table - .on(profile_combined::comment_id.eq(comment_aggregates::comment_id.nullable())), - ) - .left_join(actions( - comment_actions::table, - Some(my_person_id), - comment::id, - )) - .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) - // The creator id filter - .filter(item_creator.eq(self.creator_id)) - .select(( - // Post-specific - post_aggregates::all_columns, - coalesce( - post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), - post_aggregates::comments, - ), - post_actions::saved.nullable().is_not_null(), - post_actions::read.nullable().is_not_null(), - post_actions::hidden.nullable().is_not_null(), - post_actions::like_score.nullable(), - image_details::all_columns.nullable(), - // Comment-specific - comment::all_columns.nullable(), - comment_aggregates::all_columns.nullable(), - comment_actions::saved.nullable().is_not_null(), - comment_actions::like_score.nullable(), - // Shared - post::all_columns, - community::all_columns, - person::all_columns, - CommunityFollower::select_subscribed_type(), - local_user::admin.nullable().is_not_null(), - creator_community_actions - .field(community_actions::became_moderator) - .nullable() - .is_not_null(), - creator_community_actions - .field(community_actions::received_ban) - .nullable() - .is_not_null(), - person_actions::blocked.nullable().is_not_null(), - community_actions::received_ban.nullable().is_not_null(), - )) - .into_boxed(); - - if let Some(community_id) = self.community_id { - query = query.filter(community::id.eq(community_id)); - } - - // If its saved only, then filter - if self.saved_only.unwrap_or_default() { - query = query.filter( - comment_actions::saved - .is_not_null() - .or(post_actions::saved.is_not_null()), - ) - } - - let mut query = PaginatedQueryBuilder::new(query); - - let page_after = self.page_after.map(|c| c.0); - - if self.page_back.unwrap_or_default() { - query = query.before(page_after).limit_and_offset_from_end(); - } else { - query = query.after(page_after); - } - - // Sorting by published - query = query - .then_desc(ReverseTimestampKey(key::published)) - // Tie breaker - .then_desc(key::id); - - let res = query.load::(conn).await?; - - // Map the query results to the enum - let out = res.into_iter().filter_map(map_to_enum).collect(); - - Ok(out) - } -} - -/// Maps the combined DB row to an enum -fn map_to_enum(view: ProfileCombinedViewInternal) -> Option { - // Use for a short alias - let v = view; - - if let (Some(comment), Some(counts)) = (v.comment, v.comment_counts) { - Some(ProfileCombinedView::Comment(CommentView { - comment, - counts, - post: v.post, - community: v.community, - creator: v.item_creator, - creator_banned_from_community: v.item_creator_banned_from_community, - creator_is_moderator: v.item_creator_is_moderator, - creator_is_admin: v.item_creator_is_admin, - creator_blocked: v.item_creator_blocked, - subscribed: v.subscribed, - saved: v.comment_saved, - my_vote: v.my_comment_vote, - banned_from_community: v.banned_from_community, - })) - } else { - Some(ProfileCombinedView::Post(PostView { - post: v.post, - community: v.community, - unread_comments: v.post_unread_comments, - counts: v.post_counts, - creator: v.item_creator, - creator_banned_from_community: v.item_creator_banned_from_community, - creator_is_moderator: v.item_creator_is_moderator, - creator_is_admin: v.item_creator_is_admin, - creator_blocked: v.item_creator_blocked, - subscribed: v.subscribed, - saved: v.post_saved, - read: v.post_read, - hidden: v.post_hidden, - my_vote: v.my_post_vote, - image_details: v.image_details, - banned_from_community: v.banned_from_community, - })) - } -} diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs index 5944568e0..b38c7841f 100644 --- a/crates/db_views/src/report_combined_view.rs +++ b/crates/db_views/src/report_combined_view.rs @@ -1,11 +1,14 @@ -use crate::structs::{ - CommentReportView, - LocalUserView, - PostReportView, - PrivateMessageReportView, - ReportCombinedPaginationCursor, - ReportCombinedView, - ReportCombinedViewInternal, +use crate::{ + structs::{ + CommentReportView, + LocalUserView, + PostReportView, + PrivateMessageReportView, + ReportCombinedPaginationCursor, + ReportCombinedView, + ReportCombinedViewInternal, + }, + InternalToCombinedView, }; use diesel::{ result::Error, @@ -149,9 +152,10 @@ impl ReportCombinedQuery { user: &LocalUserView, ) -> LemmyResult> { let my_person_id = user.local_user.person_id; + let report_creator = person::id; let item_creator = aliases::person1.field(person::id); - let resolver = aliases::person2.field(person::id).nullable(); + let conn = &mut get_conn(pool).await?; // Notes: since the post_report_id and comment_report_id are optional columns, @@ -167,9 +171,9 @@ impl ReportCombinedQuery { .inner_join( person::table.on( post_report::creator_id - .eq(person::id) - .or(comment_report::creator_id.eq(person::id)) - .or(private_message_report::creator_id.eq(person::id)), + .eq(report_creator) + .or(comment_report::creator_id.eq(report_creator)) + .or(private_message_report::creator_id.eq(report_creator)), ), ) // The comment @@ -188,7 +192,8 @@ impl ReportCombinedQuery { ), ) // The item creator - // You can now use aliases::person1.field(person::id) / item_creator for all the item actions + // You can now use aliases::person1.field(person::id) / item_creator + // for all the item actions .inner_join( aliases::person1.on( post::creator_id @@ -324,81 +329,84 @@ impl ReportCombinedQuery { let res = query.load::(conn).await?; // Map the query results to the enum - let out = res.into_iter().filter_map(map_to_enum).collect(); + let out = res.into_iter().filter_map(|u| u.map_to_enum()).collect(); Ok(out) } } -/// Maps the combined DB row to an enum -fn map_to_enum(view: ReportCombinedViewInternal) -> Option { - // Use for a short alias - let v = view; +impl InternalToCombinedView for ReportCombinedViewInternal { + type CombinedView = ReportCombinedView; - if let (Some(post_report), Some(post), Some(community), Some(unread_comments), Some(counts)) = ( - v.post_report, - v.post.clone(), - v.community.clone(), - v.post_unread_comments, - v.post_counts, - ) { - Some(ReportCombinedView::Post(PostReportView { - post_report, - post, - community, - unread_comments, - counts, - creator: v.report_creator, - post_creator: v.item_creator, - creator_banned_from_community: v.item_creator_banned_from_community, - creator_is_moderator: v.item_creator_is_moderator, - creator_is_admin: v.item_creator_is_admin, - creator_blocked: v.item_creator_blocked, - subscribed: v.subscribed, - saved: v.post_saved, - read: v.post_read, - hidden: v.post_hidden, - my_vote: v.my_post_vote, - resolver: v.resolver, - })) - } else if let (Some(comment_report), Some(comment), Some(counts), Some(post), Some(community)) = ( - v.comment_report, - v.comment, - v.comment_counts, - v.post.clone(), - v.community.clone(), - ) { - Some(ReportCombinedView::Comment(CommentReportView { - comment_report, - comment, - counts, - post, - community, - creator: v.report_creator, - comment_creator: v.item_creator, - creator_banned_from_community: v.item_creator_banned_from_community, - creator_is_moderator: v.item_creator_is_moderator, - creator_is_admin: v.item_creator_is_admin, - creator_blocked: v.item_creator_blocked, - subscribed: v.subscribed, - saved: v.comment_saved, - my_vote: v.my_comment_vote, - resolver: v.resolver, - })) - } else if let (Some(private_message_report), Some(private_message)) = - (v.private_message_report, v.private_message) - { - Some(ReportCombinedView::PrivateMessage( - PrivateMessageReportView { - private_message_report, - private_message, + fn map_to_enum(&self) -> Option { + // Use for a short alias + let v = self.clone(); + + if let (Some(post_report), Some(post), Some(community), Some(unread_comments), Some(counts)) = ( + v.post_report, + v.post.clone(), + v.community.clone(), + v.post_unread_comments, + v.post_counts, + ) { + Some(ReportCombinedView::Post(PostReportView { + post_report, + post, + community, + unread_comments, + counts, creator: v.report_creator, - private_message_creator: v.item_creator, + post_creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.post_saved, + read: v.post_read, + hidden: v.post_hidden, + my_vote: v.my_post_vote, resolver: v.resolver, - }, - )) - } else { - None + })) + } else if let (Some(comment_report), Some(comment), Some(counts), Some(post), Some(community)) = ( + v.comment_report, + v.comment, + v.comment_counts, + v.post, + v.community, + ) { + Some(ReportCombinedView::Comment(CommentReportView { + comment_report, + comment, + counts, + post, + community, + creator: v.report_creator, + comment_creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.comment_saved, + my_vote: v.my_comment_vote, + resolver: v.resolver, + })) + } else if let (Some(private_message_report), Some(private_message)) = + (v.private_message_report, v.private_message) + { + Some(ReportCombinedView::PrivateMessage( + PrivateMessageReportView { + private_message_report, + private_message, + creator: v.report_creator, + private_message_creator: v.item_creator, + resolver: v.resolver, + }, + )) + } else { + None + } } } diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 3ee2fd25c..8cbbba674 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -132,11 +132,17 @@ pub struct PaginationCursor(pub String); #[cfg_attr(feature = "full", ts(export))] pub struct ReportCombinedPaginationCursor(pub String); -/// like PaginationCursor but for the profile_combined table +/// like PaginationCursor but for the person_content_combined table #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(ts_rs::TS))] #[cfg_attr(feature = "full", ts(export))] -pub struct ProfileCombinedPaginationCursor(pub String); +pub struct PersonContentCombinedPaginationCursor(pub String); + +/// like PaginationCursor but for the person_saved_combined table +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(ts_rs::TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct PersonSavedCombinedPaginationCursor(pub String); #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] @@ -299,8 +305,8 @@ pub enum ReportCombinedView { #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -/// A combined profile view -pub struct ProfileCombinedViewInternal { +/// A combined person_content view +pub struct PersonContentViewInternal { // Post-specific pub post_counts: PostAggregates, pub post_unread_comments: i64, @@ -331,7 +337,7 @@ pub struct ProfileCombinedViewInternal { #[cfg_attr(feature = "full", ts(export))] // Use serde's internal tagging, to work easier with javascript libraries #[serde(tag = "type_")] -pub enum ProfileCombinedView { +pub enum PersonContentCombinedView { Post(PostView), Comment(CommentView), } diff --git a/migrations/2024-12-05-233704_add_person_content_combined_table/down.sql b/migrations/2024-12-05-233704_add_person_content_combined_table/down.sql new file mode 100644 index 000000000..a8db9ec61 --- /dev/null +++ b/migrations/2024-12-05-233704_add_person_content_combined_table/down.sql @@ -0,0 +1,3 @@ +DROP TABLE person_content_combined; +DROP TABLE person_saved_combined; + diff --git a/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql b/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql new file mode 100644 index 000000000..40ced8238 --- /dev/null +++ b/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql @@ -0,0 +1,65 @@ +-- Creates combined tables for +-- person_content: (comment, post) +-- person_saved: (comment, post) +CREATE TABLE person_content_combined ( + id serial PRIMARY KEY, + published timestamptz NOT NULL, + post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE, + comment_id int UNIQUE REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE, + -- Make sure only one of the columns is not null + CHECK ((post_id IS NOT NULL)::integer + (comment_id IS NOT NULL)::integer = 1) +); + +CREATE INDEX idx_person_content_combined_published ON person_content_combined (published DESC, id DESC); + +CREATE INDEX idx_person_content_combined_published_asc ON person_content_combined (reverse_timestamp_sort (published) DESC, id DESC); + +-- Updating the history +INSERT INTO person_content_combined (published, post_id) +SELECT + published, + id +FROM + post; + +INSERT INTO person_content_combined (published, comment_id) +SELECT + published, + id +FROM + comment; + +-- This one is special, because you use the saved date, not the ordinary published +CREATE TABLE person_saved_combined ( + id serial PRIMARY KEY, + published timestamptz NOT NULL, + post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE, + comment_id int UNIQUE REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE, + -- Make sure only one of the columns is not null + CHECK ((post_id IS NOT NULL)::integer + (comment_id IS NOT NULL)::integer = 1) +); + +CREATE INDEX idx_person_saved_combined_published ON person_saved_combined (published DESC, id DESC); + +CREATE INDEX idx_person_saved_combined_published_asc ON person_saved_combined (reverse_timestamp_sort (published) DESC, id DESC); + +-- Updating the history +INSERT INTO person_saved_combined (published, post_id) +SELECT + saved, + post_id +FROM + post_actions +WHERE + saved IS NOT NULL; + +INSERT INTO person_saved_combined (published, comment_id) +SELECT + saved, + comment_id +FROM + comment_actions +WHERE + saved IS NOT NULL; + + diff --git a/migrations/2024-12-05-233704_add_profile_combined_table/down.sql b/migrations/2024-12-05-233704_add_profile_combined_table/down.sql deleted file mode 100644 index 9426ebe38..000000000 --- a/migrations/2024-12-05-233704_add_profile_combined_table/down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP TABLE profile_combined; - diff --git a/migrations/2024-12-05-233704_add_profile_combined_table/up.sql b/migrations/2024-12-05-233704_add_profile_combined_table/up.sql deleted file mode 100644 index 183529ee4..000000000 --- a/migrations/2024-12-05-233704_add_profile_combined_table/up.sql +++ /dev/null @@ -1,30 +0,0 @@ --- Creates combined tables for --- Profile: (comment, post) -CREATE TABLE profile_combined ( - id serial PRIMARY KEY, - published timestamptz NOT NULL, - post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE, - comment_id int UNIQUE REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE, - -- Make sure only one of the columns is not null - CHECK ((post_id IS NOT NULL)::integer + (comment_id IS NOT NULL)::integer = 1) -); - -CREATE INDEX idx_profile_combined_published ON profile_combined (published DESC, id DESC); - -CREATE INDEX idx_profile_combined_published_asc ON profile_combined (reverse_timestamp_sort (published) DESC, id DESC); - --- Updating the history -INSERT INTO profile_combined (published, post_id) -SELECT - published, - id -FROM - post; - -INSERT INTO profile_combined (published, comment_id) -SELECT - published, - id -FROM - comment; - diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 3f6153435..472728ded 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -142,6 +142,7 @@ use lemmy_api_crud::{ }; use lemmy_apub::api::{ list_comments::list_comments, + list_person_content::list_person_content, list_posts::list_posts, read_community::get_community, read_person::read_person, @@ -338,6 +339,9 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { scope("/user") .wrap(rate_limit.message()) .route("", get().to(read_person)) + .route("/content", get().to(list_person_content)) + // TODO move this to /account/saved after http routes + // .route("/saved", get().to(read_person_saved)) .route("/mention", get().to(list_mentions)) .route( "/mention/mark_as_read",