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.
This commit is contained in:
Dessalines 2024-12-07 12:38:20 -05:00
parent 1053df1a4b
commit 32b5411abd
33 changed files with 1427 additions and 612 deletions

1
Cargo.lock generated
View file

@ -2677,6 +2677,7 @@ version = "0.19.6-beta.7"
dependencies = [
"actix-web",
"chrono",
"derive-new",
"diesel",
"diesel-async",
"diesel_ltree",

View file

View file

@ -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;

View file

@ -131,8 +131,6 @@ pub struct GetComments {
#[cfg_attr(feature = "full", ts(optional))]
pub parent_id: Option<CommentId>,
#[cfg_attr(feature = "full", ts(optional))]
pub saved_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub liked_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub disliked_only: Option<bool>,

View file

@ -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<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub community_id: Option<CommunityId>,
#[cfg_attr(feature = "full", ts(optional))]
pub saved_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub page_cursor: Option<ProfileCombinedPaginationCursor>,
#[cfg_attr(feature = "full", ts(optional))]
pub page_back: Option<bool>,
}
#[skip_serializing_none]
@ -245,10 +237,58 @@ pub struct GetPersonDetailsResponse {
pub person_view: PersonView,
#[cfg_attr(feature = "full", ts(optional))]
pub site: Option<Site>,
pub content: Vec<ProfileCombinedView>,
pub moderates: Vec<CommunityModeratorView>,
}
#[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<PersonId>,
/// Example: dessalines , or dessalines@xyz.tld
#[cfg_attr(feature = "full", ts(optional))]
pub username: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub page_cursor: Option<PersonContentCombinedPaginationCursor>,
#[cfg_attr(feature = "full", ts(optional))]
pub page_back: Option<bool>,
}
#[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<PersonContentCombinedView>,
}
#[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<PersonContentCombinedPaginationCursor>,
#[cfg_attr(feature = "full", ts(optional))]
pub page_back: Option<bool>,
}
#[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<PersonContentCombinedView>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]

View file

@ -95,8 +95,6 @@ pub struct GetPosts {
#[cfg_attr(feature = "full", ts(optional))]
pub community_name: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub saved_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub liked_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub disliked_only: Option<bool>,

View file

@ -94,8 +94,6 @@ pub struct Search {
#[cfg_attr(feature = "full", ts(optional))]
pub post_url_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub saved_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub liked_only: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub disliked_only: Option<bool>,

View file

@ -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,

View file

@ -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<ListPersonContent>,
context: Data<LemmyContext>,
local_user_view: Option<LocalUserView>,
) -> LemmyResult<Json<ListPersonContentResponse>> {
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 }))
}

View file

@ -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,

View file

@ -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<PersonId>,
username: &Option<String>,
context: &Data<LemmyContext>,
local_user_view: &Option<LocalUserView>,
) -> LemmyResult<PersonId> {
// 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::<ApubPerson, Person>(username, context, local_user_view, true)
.await?
.id
} else {
Err(LemmyErrorType::NotFound)?
}
}
})
}

View file

@ -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<LemmyContext>,
local_user_view: Option<LocalUserView>,
) -> LemmyResult<Json<GetPersonDetailsResponse>> {
// 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::<ApubPerson, Person>(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,
}))
}

View file

@ -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()
};

View file

@ -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');

View file

@ -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 {

View file

@ -729,6 +729,15 @@ diesel::table! {
}
}
diesel::table! {
person_content_combined (id) {
id -> Int4,
published -> Timestamptz,
post_id -> Nullable<Int4>,
comment_id -> Nullable<Int4>,
}
}
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<Int4>,
comment_id -> Nullable<Int4>,
}
}
diesel::table! {
post (id) {
id -> Int4,
@ -856,15 +874,6 @@ diesel::table! {
}
}
diesel::table! {
profile_combined (id) {
id -> Int4,
published -> Timestamptz,
post_id -> Nullable<Int4>,
comment_id -> Nullable<Int4>,
}
}
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,

View file

@ -1,2 +1,3 @@
pub mod profile;
pub mod person_content;
pub mod person_saved;
pub mod report;

View file

@ -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<Utc>,
#[cfg_attr(feature = "full", ts(optional))]
pub post_id: Option<PostId>,
#[cfg_attr(feature = "full", ts(optional))]
pub comment_id: Option<CommentId>,
}

View file

@ -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<Utc>,
#[cfg_attr(feature = "full", ts(optional))]
pub post_id: Option<PostId>,

View file

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

View file

@ -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<PersonId>,
pub local_user: Option<&'a LocalUser>,
pub search_term: Option<String>,
pub saved_only: Option<bool>,
pub liked_only: Option<bool>,
pub disliked_only: Option<bool>,
pub page: Option<i64>,
@ -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,

View file

@ -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<Self::CombinedView>;
}

View file

@ -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<PaginationCursorData, Error> {
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<PaginationCursorData>,
#[new(default)]
pub page_back: Option<bool>,
}
impl PersonContentCombinedQuery {
pub async fn list(
self,
pool: &mut DbPool<'_>,
user: &Option<LocalUserView>,
) -> LemmyResult<Vec<PersonContentCombinedView>> {
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::<PersonContentViewInternal>(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<Self::CombinedView> {
// 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<Data> {
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(())
}
}

View file

@ -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<PaginationCursorData, Error> {
// 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<PaginationCursorData>,
// pub page_back: Option<bool>,
// }
// impl ProfileCombinedQuery {
// pub async fn list(
// self,
// pool: &mut DbPool<'_>,
// user: &Option<LocalUserView>,
// ) -> LemmyResult<Vec<PersonContentCombinedView>> {
// 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::<PersonContentViewInternal>(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<Self::CombinedView> {
// // 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<Data> {
// 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
// }

View file

@ -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<String>,
pub url_only: Option<bool>,
pub saved_only: Option<bool>,
pub liked_only: Option<bool>,
pub disliked_only: Option<bool>,
pub title_only: Option<bool>,
@ -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<()> {

View file

@ -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<PaginationCursorData, Error> {
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<CommunityId>,
pub saved_only: Option<bool>,
pub page_after: Option<PaginationCursorData>,
pub page_back: Option<bool>,
}
impl ProfileCombinedQuery {
pub async fn list(
self,
pool: &mut DbPool<'_>,
user: &Option<LocalUserView>,
) -> LemmyResult<Vec<ProfileCombinedView>> {
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::<ProfileCombinedViewInternal>(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<ProfileCombinedView> {
// 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,
}))
}
}

View file

@ -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<Vec<ReportCombinedView>> {
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::<ReportCombinedViewInternal>(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<ReportCombinedView> {
// 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<Self::CombinedView> {
// 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
}
}
}

View file

@ -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),
}

View file

@ -0,0 +1,3 @@
DROP TABLE person_content_combined;
DROP TABLE person_saved_combined;

View file

@ -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;

View file

@ -1,2 +0,0 @@
DROP TABLE profile_combined;

View file

@ -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;

View file

@ -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",