mirror of
https://github.com/LemmyNet/lemmy.git
synced 2025-01-18 16:05:56 +00:00
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:
parent
1053df1a4b
commit
32b5411abd
33 changed files with 1427 additions and 612 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2677,6 +2677,7 @@ version = "0.19.6-beta.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"derive-new",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel-async",
|
"diesel-async",
|
||||||
"diesel_ltree",
|
"diesel_ltree",
|
||||||
|
|
0
crates/api/src/local_user/list_saved.rs
Normal file
0
crates/api/src/local_user/list_saved.rs
Normal file
|
@ -8,6 +8,7 @@ pub mod get_captcha;
|
||||||
pub mod list_banned;
|
pub mod list_banned;
|
||||||
pub mod list_logins;
|
pub mod list_logins;
|
||||||
pub mod list_media;
|
pub mod list_media;
|
||||||
|
pub mod list_saved;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod logout;
|
pub mod logout;
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
|
|
|
@ -131,8 +131,6 @@ pub struct GetComments {
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub parent_id: Option<CommentId>,
|
pub parent_id: Option<CommentId>,
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub saved_only: Option<bool>,
|
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
|
||||||
pub liked_only: Option<bool>,
|
pub liked_only: Option<bool>,
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub disliked_only: Option<bool>,
|
pub disliked_only: Option<bool>,
|
||||||
|
|
|
@ -9,8 +9,8 @@ use lemmy_db_schema::{
|
||||||
};
|
};
|
||||||
use lemmy_db_views::structs::{
|
use lemmy_db_views::structs::{
|
||||||
LocalImageView,
|
LocalImageView,
|
||||||
ProfileCombinedPaginationCursor,
|
PersonContentCombinedPaginationCursor,
|
||||||
ProfileCombinedView,
|
PersonContentCombinedView,
|
||||||
};
|
};
|
||||||
use lemmy_db_views_actor::structs::{
|
use lemmy_db_views_actor::structs::{
|
||||||
CommentReplyView,
|
CommentReplyView,
|
||||||
|
@ -226,14 +226,6 @@ pub struct GetPersonDetails {
|
||||||
/// Example: dessalines , or dessalines@xyz.tld
|
/// Example: dessalines , or dessalines@xyz.tld
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub username: Option<String>,
|
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]
|
#[skip_serializing_none]
|
||||||
|
@ -245,10 +237,58 @@ pub struct GetPersonDetailsResponse {
|
||||||
pub person_view: PersonView,
|
pub person_view: PersonView,
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub site: Option<Site>,
|
pub site: Option<Site>,
|
||||||
pub content: Vec<ProfileCombinedView>,
|
|
||||||
pub moderates: Vec<CommunityModeratorView>,
|
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)]
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "full", derive(TS))]
|
#[cfg_attr(feature = "full", derive(TS))]
|
||||||
#[cfg_attr(feature = "full", ts(export))]
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
|
|
@ -95,8 +95,6 @@ pub struct GetPosts {
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub community_name: Option<String>,
|
pub community_name: Option<String>,
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub saved_only: Option<bool>,
|
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
|
||||||
pub liked_only: Option<bool>,
|
pub liked_only: Option<bool>,
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub disliked_only: Option<bool>,
|
pub disliked_only: Option<bool>,
|
||||||
|
|
|
@ -94,8 +94,6 @@ pub struct Search {
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub post_url_only: Option<bool>,
|
pub post_url_only: Option<bool>,
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub saved_only: Option<bool>,
|
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
|
||||||
pub liked_only: Option<bool>,
|
pub liked_only: Option<bool>,
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub disliked_only: Option<bool>,
|
pub disliked_only: Option<bool>,
|
||||||
|
|
|
@ -46,7 +46,6 @@ pub async fn list_comments(
|
||||||
&site_view.local_site,
|
&site_view.local_site,
|
||||||
));
|
));
|
||||||
let max_depth = data.max_depth;
|
let max_depth = data.max_depth;
|
||||||
let saved_only = data.saved_only;
|
|
||||||
|
|
||||||
let liked_only = data.liked_only;
|
let liked_only = data.liked_only;
|
||||||
let disliked_only = data.disliked_only;
|
let disliked_only = data.disliked_only;
|
||||||
|
@ -80,7 +79,6 @@ pub async fn list_comments(
|
||||||
listing_type,
|
listing_type,
|
||||||
sort,
|
sort,
|
||||||
max_depth,
|
max_depth,
|
||||||
saved_only,
|
|
||||||
liked_only,
|
liked_only,
|
||||||
disliked_only,
|
disliked_only,
|
||||||
community_id,
|
community_id,
|
||||||
|
|
50
crates/apub/src/api/list_person_content.rs
Normal file
50
crates/apub/src/api/list_person_content.rs
Normal 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 }))
|
||||||
|
}
|
|
@ -41,7 +41,6 @@ pub async fn list_posts(
|
||||||
} else {
|
} else {
|
||||||
data.community_id
|
data.community_id
|
||||||
};
|
};
|
||||||
let saved_only = data.saved_only;
|
|
||||||
let show_hidden = data.show_hidden;
|
let show_hidden = data.show_hidden;
|
||||||
let show_read = data.show_read;
|
let show_read = data.show_read;
|
||||||
let show_nsfw = data.show_nsfw;
|
let show_nsfw = data.show_nsfw;
|
||||||
|
@ -77,7 +76,6 @@ pub async fn list_posts(
|
||||||
listing_type,
|
listing_type,
|
||||||
sort,
|
sort,
|
||||||
community_id,
|
community_id,
|
||||||
saved_only,
|
|
||||||
liked_only,
|
liked_only,
|
||||||
disliked_only,
|
disliked_only,
|
||||||
page,
|
page,
|
||||||
|
|
|
@ -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::{
|
use lemmy_db_schema::{
|
||||||
newtypes::CommunityId,
|
newtypes::{CommunityId, PersonId},
|
||||||
source::{local_site::LocalSite, local_user::LocalUser},
|
source::{local_site::LocalSite, local_user::LocalUser, person::Person},
|
||||||
CommentSortType,
|
CommentSortType,
|
||||||
ListingType,
|
ListingType,
|
||||||
PostSortType,
|
PostSortType,
|
||||||
};
|
};
|
||||||
|
use lemmy_db_views::structs::LocalUserView;
|
||||||
|
use lemmy_utils::error::LemmyResult;
|
||||||
|
|
||||||
pub mod list_comments;
|
pub mod list_comments;
|
||||||
|
pub mod list_person_content;
|
||||||
pub mod list_posts;
|
pub mod list_posts;
|
||||||
pub mod read_community;
|
pub mod read_community;
|
||||||
pub mod read_person;
|
pub mod read_person;
|
||||||
|
@ -61,3 +67,28 @@ fn comment_sort_type_with_default(
|
||||||
.unwrap_or(local_site.default_comment_sort_type),
|
.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)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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 activitypub_federation::config::Data;
|
||||||
use actix_web::web::{Json, Query};
|
use actix_web::web::{Json, Query};
|
||||||
use lemmy_api_common::{
|
use lemmy_api_common::{
|
||||||
|
@ -6,13 +6,9 @@ use lemmy_api_common::{
|
||||||
person::{GetPersonDetails, GetPersonDetailsResponse},
|
person::{GetPersonDetails, GetPersonDetailsResponse},
|
||||||
utils::{check_private_instance, read_site_for_actor},
|
utils::{check_private_instance, read_site_for_actor},
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::source::person::Person;
|
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||||
use lemmy_db_views::{
|
|
||||||
profile_combined_view::ProfileCombinedQuery,
|
|
||||||
structs::{LocalUserView, SiteView},
|
|
||||||
};
|
|
||||||
use lemmy_db_views_actor::structs::{CommunityModeratorView, PersonView};
|
use lemmy_db_views_actor::structs::{CommunityModeratorView, PersonView};
|
||||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
use lemmy_utils::error::LemmyResult;
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
#[tracing::instrument(skip(context))]
|
||||||
pub async fn read_person(
|
pub async fn read_person(
|
||||||
|
@ -20,65 +16,21 @@ pub async fn read_person(
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
local_user_view: Option<LocalUserView>,
|
local_user_view: Option<LocalUserView>,
|
||||||
) -> LemmyResult<Json<GetPersonDetailsResponse>> {
|
) -> 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?;
|
let local_site = SiteView::read_local(&mut context.pool()).await?;
|
||||||
|
|
||||||
check_private_instance(&local_user_view, &local_site.local_site)?;
|
check_private_instance(&local_user_view, &local_site.local_site)?;
|
||||||
|
|
||||||
let person_details_id = match data.person_id {
|
let person_details_id = resolve_person_id_from_id_or_username(
|
||||||
Some(id) => id,
|
&data.person_id,
|
||||||
None => {
|
&data.username,
|
||||||
if let Some(username) = &data.username {
|
&context,
|
||||||
resolve_actor_identifier::<ApubPerson, Person>(username, &context, &local_user_view, true)
|
&local_user_view,
|
||||||
.await?
|
)
|
||||||
.id
|
.await?;
|
||||||
} else {
|
|
||||||
Err(LemmyErrorType::NotFound)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// You don't need to return settings for the user, since this comes back with GetSite
|
// You don't need to return settings for the user, since this comes back with GetSite
|
||||||
// `my_user`
|
// `my_user`
|
||||||
let person_view = PersonView::read(&mut context.pool(), person_details_id).await?;
|
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(
|
let moderates = CommunityModeratorView::for_person(
|
||||||
&mut context.pool(),
|
&mut context.pool(),
|
||||||
person_details_id,
|
person_details_id,
|
||||||
|
@ -92,6 +44,5 @@ pub async fn read_person(
|
||||||
person_view,
|
person_view,
|
||||||
site,
|
site,
|
||||||
moderates,
|
moderates,
|
||||||
content,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,6 @@ pub async fn search(
|
||||||
limit,
|
limit,
|
||||||
title_only,
|
title_only,
|
||||||
post_url_only,
|
post_url_only,
|
||||||
saved_only,
|
|
||||||
liked_only,
|
liked_only,
|
||||||
disliked_only,
|
disliked_only,
|
||||||
}) = data;
|
}) = data;
|
||||||
|
@ -86,7 +85,6 @@ pub async fn search(
|
||||||
url_only: post_url_only,
|
url_only: post_url_only,
|
||||||
liked_only,
|
liked_only,
|
||||||
disliked_only,
|
disliked_only,
|
||||||
saved_only,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -101,7 +99,6 @@ pub async fn search(
|
||||||
limit,
|
limit,
|
||||||
liked_only,
|
liked_only,
|
||||||
disliked_only,
|
disliked_only,
|
||||||
saved_only,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -685,31 +685,62 @@ CALL r.create_report_combined_trigger ('comment_report');
|
||||||
|
|
||||||
CALL r.create_report_combined_trigger ('private_message_report');
|
CALL r.create_report_combined_trigger ('private_message_report');
|
||||||
|
|
||||||
-- Profile (comment, post)
|
-- person_content (comment, post)
|
||||||
CREATE PROCEDURE r.create_profile_combined_trigger (table_name text)
|
CREATE PROCEDURE r.create_person_content_combined_trigger (table_name text)
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
AS $a$
|
AS $a$
|
||||||
BEGIN
|
BEGIN
|
||||||
EXECUTE replace($b$ CREATE FUNCTION r.profile_combined_thing_insert ( )
|
EXECUTE replace($b$ CREATE FUNCTION r.person_content_combined_thing_insert ( )
|
||||||
RETURNS TRIGGER
|
RETURNS TRIGGER
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
AS $$
|
AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
INSERT INTO profile_combined (published, thing_id)
|
INSERT INTO person_content_combined (published, thing_id)
|
||||||
VALUES (NEW.published, NEW.id);
|
VALUES (NEW.published, NEW.id);
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END $$;
|
END $$;
|
||||||
CREATE TRIGGER profile_combined
|
CREATE TRIGGER person_content_combined
|
||||||
AFTER INSERT ON thing
|
AFTER INSERT ON thing
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE FUNCTION r.profile_combined_thing_insert ( );
|
EXECUTE FUNCTION r.person_content_combined_thing_insert ( );
|
||||||
$b$,
|
$b$,
|
||||||
'thing',
|
'thing',
|
||||||
table_name);
|
table_name);
|
||||||
END;
|
END;
|
||||||
$a$;
|
$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');
|
|
||||||
|
|
||||||
|
|
|
@ -188,8 +188,14 @@ pub struct ReportCombinedId(i32);
|
||||||
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
|
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
|
||||||
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]
|
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]
|
||||||
#[cfg_attr(feature = "full", ts(export))]
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
/// The profile combined id
|
/// The person content combined id
|
||||||
pub struct ProfileCombinedId(i32);
|
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 {
|
impl DbUrl {
|
||||||
pub fn inner(&self) -> &Url {
|
pub fn inner(&self) -> &Url {
|
||||||
|
|
|
@ -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! {
|
diesel::table! {
|
||||||
person_mention (id) {
|
person_mention (id) {
|
||||||
id -> Int4,
|
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! {
|
diesel::table! {
|
||||||
post (id) {
|
post (id) {
|
||||||
id -> Int4,
|
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! {
|
diesel::table! {
|
||||||
received_activity (ap_id) {
|
received_activity (ap_id) {
|
||||||
ap_id -> Text,
|
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 -> instance (instance_id));
|
||||||
diesel::joinable!(person_aggregates -> person (person_id));
|
diesel::joinable!(person_aggregates -> person (person_id));
|
||||||
diesel::joinable!(person_ban -> 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 -> comment (comment_id));
|
||||||
diesel::joinable!(person_mention -> person (recipient_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 -> community (community_id));
|
||||||
diesel::joinable!(post -> language (language_id));
|
diesel::joinable!(post -> language (language_id));
|
||||||
diesel::joinable!(post -> person (creator_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_aggregates -> post (post_id));
|
||||||
diesel::joinable!(post_report -> post (post_id));
|
diesel::joinable!(post_report -> post (post_id));
|
||||||
diesel::joinable!(private_message_report -> private_message (private_message_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 -> local_user (local_user_id));
|
||||||
diesel::joinable!(registration_application -> person (admin_id));
|
diesel::joinable!(registration_application -> person (admin_id));
|
||||||
diesel::joinable!(report_combined -> comment_report (comment_report_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_actions,
|
||||||
person_aggregates,
|
person_aggregates,
|
||||||
person_ban,
|
person_ban,
|
||||||
|
person_content_combined,
|
||||||
person_mention,
|
person_mention,
|
||||||
|
person_saved_combined,
|
||||||
post,
|
post,
|
||||||
post_actions,
|
post_actions,
|
||||||
post_aggregates,
|
post_aggregates,
|
||||||
post_report,
|
post_report,
|
||||||
private_message,
|
private_message,
|
||||||
private_message_report,
|
private_message_report,
|
||||||
profile_combined,
|
|
||||||
received_activity,
|
received_activity,
|
||||||
registration_application,
|
registration_application,
|
||||||
remote_image,
|
remote_image,
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod profile;
|
pub mod person_content;
|
||||||
|
pub mod person_saved;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
|
|
30
crates/db_schema/src/source/combined/person_content.rs
Normal file
30
crates/db_schema/src/source/combined/person_content.rs
Normal 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>,
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::newtypes::{CommentId, PostId, ProfileCombinedId};
|
use crate::newtypes::{CommentId, PersonSavedCombinedId, PostId};
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
use crate::schema::profile_combined;
|
use crate::schema::person_saved_combined;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
use i_love_jesus::CursorKeysModule;
|
use i_love_jesus::CursorKeysModule;
|
||||||
|
@ -15,13 +15,13 @@ use ts_rs::TS;
|
||||||
feature = "full",
|
feature = "full",
|
||||||
derive(Identifiable, Queryable, Selectable, TS, CursorKeysModule)
|
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", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||||
#[cfg_attr(feature = "full", ts(export))]
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
#[cfg_attr(feature = "full", cursor_keys_module(name = profile_combined_keys))]
|
#[cfg_attr(feature = "full", cursor_keys_module(name = person_saved_combined_keys))]
|
||||||
/// A combined profile table.
|
/// A combined person_saved table.
|
||||||
pub struct ProfileCombined {
|
pub struct PersonSavedCombined {
|
||||||
pub id: ProfileCombinedId,
|
pub id: PersonSavedCombinedId,
|
||||||
pub published: DateTime<Utc>,
|
pub published: DateTime<Utc>,
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub post_id: Option<PostId>,
|
pub post_id: Option<PostId>,
|
|
@ -40,6 +40,7 @@ ts-rs = { workspace = true, optional = true }
|
||||||
actix-web = { workspace = true, optional = true }
|
actix-web = { workspace = true, optional = true }
|
||||||
i-love-jesus = { workspace = true, optional = true }
|
i-love-jesus = { workspace = true, optional = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
|
derive-new.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = { workspace = true }
|
serial_test = { workspace = true }
|
||||||
|
|
|
@ -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() {
|
if let Some(my_id) = options.local_user.person_id() {
|
||||||
let not_creator_filter = comment::creator_id.ne(my_id);
|
let not_creator_filter = comment::creator_id.ne(my_id);
|
||||||
if options.liked_only.unwrap_or_default() {
|
if options.liked_only.unwrap_or_default() {
|
||||||
|
@ -337,7 +330,6 @@ pub struct CommentQuery<'a> {
|
||||||
pub creator_id: Option<PersonId>,
|
pub creator_id: Option<PersonId>,
|
||||||
pub local_user: Option<&'a LocalUser>,
|
pub local_user: Option<&'a LocalUser>,
|
||||||
pub search_term: Option<String>,
|
pub search_term: Option<String>,
|
||||||
pub saved_only: Option<bool>,
|
|
||||||
pub liked_only: Option<bool>,
|
pub liked_only: Option<bool>,
|
||||||
pub disliked_only: Option<bool>,
|
pub disliked_only: Option<bool>,
|
||||||
pub page: Option<i64>,
|
pub page: Option<i64>,
|
||||||
|
@ -381,15 +373,7 @@ mod tests {
|
||||||
newtypes::LanguageId,
|
newtypes::LanguageId,
|
||||||
source::{
|
source::{
|
||||||
actor_language::LocalUserLanguage,
|
actor_language::LocalUserLanguage,
|
||||||
comment::{
|
comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm},
|
||||||
Comment,
|
|
||||||
CommentInsertForm,
|
|
||||||
CommentLike,
|
|
||||||
CommentLikeForm,
|
|
||||||
CommentSaved,
|
|
||||||
CommentSavedForm,
|
|
||||||
CommentUpdateForm,
|
|
||||||
},
|
|
||||||
community::{
|
community::{
|
||||||
Community,
|
Community,
|
||||||
CommunityFollower,
|
CommunityFollower,
|
||||||
|
@ -411,7 +395,7 @@ mod tests {
|
||||||
post::{Post, PostInsertForm, PostUpdateForm},
|
post::{Post, PostInsertForm, PostUpdateForm},
|
||||||
site::{Site, SiteInsertForm},
|
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},
|
utils::{build_db_pool_for_tests, RANK_DEFAULT},
|
||||||
CommunityVisibility,
|
CommunityVisibility,
|
||||||
SubscribedType,
|
SubscribedType,
|
||||||
|
@ -897,47 +881,6 @@ mod tests {
|
||||||
cleanup(data, pool).await
|
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<()> {
|
async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> {
|
||||||
CommentLike::remove(
|
CommentLike::remove(
|
||||||
pool,
|
pool,
|
||||||
|
|
|
@ -12,6 +12,10 @@ pub mod local_image_view;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod local_user_view;
|
pub mod local_user_view;
|
||||||
#[cfg(feature = "full")]
|
#[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;
|
pub mod post_report_view;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod post_view;
|
pub mod post_view;
|
||||||
|
@ -20,8 +24,6 @@ pub mod private_message_report_view;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod private_message_view;
|
pub mod private_message_view;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod profile_combined_view;
|
|
||||||
#[cfg(feature = "full")]
|
|
||||||
pub mod registration_application_view;
|
pub mod registration_application_view;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod report_combined_view;
|
pub mod report_combined_view;
|
||||||
|
@ -30,3 +32,10 @@ pub mod site_view;
|
||||||
pub mod structs;
|
pub mod structs;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod vote_view;
|
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>;
|
||||||
|
}
|
||||||
|
|
430
crates/db_views/src/person_content_combined_view.rs
Normal file
430
crates/db_views/src/person_content_combined_view.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
554
crates/db_views/src/person_saved_combined_view.rs
Normal file
554
crates/db_views/src/person_saved_combined_view.rs
Normal 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
|
||||||
|
// }
|
|
@ -287,15 +287,7 @@ fn queries<'a>() -> Queries<
|
||||||
query = query.filter(post_aggregates::comments.eq(0));
|
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
|
||||||
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
|
|
||||||
.show_read
|
.show_read
|
||||||
.unwrap_or(options.local_user.show_read_posts())
|
.unwrap_or(options.local_user.show_read_posts())
|
||||||
{
|
{
|
||||||
|
@ -488,7 +480,6 @@ pub struct PostQuery<'a> {
|
||||||
pub local_user: Option<&'a LocalUser>,
|
pub local_user: Option<&'a LocalUser>,
|
||||||
pub search_term: Option<String>,
|
pub search_term: Option<String>,
|
||||||
pub url_only: Option<bool>,
|
pub url_only: Option<bool>,
|
||||||
pub saved_only: Option<bool>,
|
|
||||||
pub liked_only: Option<bool>,
|
pub liked_only: Option<bool>,
|
||||||
pub disliked_only: Option<bool>,
|
pub disliked_only: Option<bool>,
|
||||||
pub title_only: Option<bool>,
|
pub title_only: Option<bool>,
|
||||||
|
@ -646,13 +637,11 @@ mod tests {
|
||||||
PostLikeForm,
|
PostLikeForm,
|
||||||
PostRead,
|
PostRead,
|
||||||
PostReadForm,
|
PostReadForm,
|
||||||
PostSaved,
|
|
||||||
PostSavedForm,
|
|
||||||
PostUpdateForm,
|
PostUpdateForm,
|
||||||
},
|
},
|
||||||
site::Site,
|
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},
|
utils::{build_db_pool, build_db_pool_for_tests, get_conn, uplete, DbPool, RANK_DEFAULT},
|
||||||
CommunityVisibility,
|
CommunityVisibility,
|
||||||
PostSortType,
|
PostSortType,
|
||||||
|
@ -1090,34 +1079,6 @@ mod tests {
|
||||||
cleanup(data, pool).await
|
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]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn creator_info() -> LemmyResult<()> {
|
async fn creator_info() -> LemmyResult<()> {
|
||||||
|
|
|
@ -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,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +1,14 @@
|
||||||
use crate::structs::{
|
use crate::{
|
||||||
CommentReportView,
|
structs::{
|
||||||
LocalUserView,
|
CommentReportView,
|
||||||
PostReportView,
|
LocalUserView,
|
||||||
PrivateMessageReportView,
|
PostReportView,
|
||||||
ReportCombinedPaginationCursor,
|
PrivateMessageReportView,
|
||||||
ReportCombinedView,
|
ReportCombinedPaginationCursor,
|
||||||
ReportCombinedViewInternal,
|
ReportCombinedView,
|
||||||
|
ReportCombinedViewInternal,
|
||||||
|
},
|
||||||
|
InternalToCombinedView,
|
||||||
};
|
};
|
||||||
use diesel::{
|
use diesel::{
|
||||||
result::Error,
|
result::Error,
|
||||||
|
@ -149,9 +152,10 @@ impl ReportCombinedQuery {
|
||||||
user: &LocalUserView,
|
user: &LocalUserView,
|
||||||
) -> LemmyResult<Vec<ReportCombinedView>> {
|
) -> LemmyResult<Vec<ReportCombinedView>> {
|
||||||
let my_person_id = user.local_user.person_id;
|
let my_person_id = user.local_user.person_id;
|
||||||
|
let report_creator = person::id;
|
||||||
let item_creator = aliases::person1.field(person::id);
|
let item_creator = aliases::person1.field(person::id);
|
||||||
|
|
||||||
let resolver = aliases::person2.field(person::id).nullable();
|
let resolver = aliases::person2.field(person::id).nullable();
|
||||||
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
|
|
||||||
// Notes: since the post_report_id and comment_report_id are optional columns,
|
// Notes: since the post_report_id and comment_report_id are optional columns,
|
||||||
|
@ -167,9 +171,9 @@ impl ReportCombinedQuery {
|
||||||
.inner_join(
|
.inner_join(
|
||||||
person::table.on(
|
person::table.on(
|
||||||
post_report::creator_id
|
post_report::creator_id
|
||||||
.eq(person::id)
|
.eq(report_creator)
|
||||||
.or(comment_report::creator_id.eq(person::id))
|
.or(comment_report::creator_id.eq(report_creator))
|
||||||
.or(private_message_report::creator_id.eq(person::id)),
|
.or(private_message_report::creator_id.eq(report_creator)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
// The comment
|
// The comment
|
||||||
|
@ -188,7 +192,8 @@ impl ReportCombinedQuery {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
// The item creator
|
// 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(
|
.inner_join(
|
||||||
aliases::person1.on(
|
aliases::person1.on(
|
||||||
post::creator_id
|
post::creator_id
|
||||||
|
@ -324,81 +329,84 @@ impl ReportCombinedQuery {
|
||||||
let res = query.load::<ReportCombinedViewInternal>(conn).await?;
|
let res = query.load::<ReportCombinedViewInternal>(conn).await?;
|
||||||
|
|
||||||
// Map the query results to the enum
|
// 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)
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maps the combined DB row to an enum
|
impl InternalToCombinedView for ReportCombinedViewInternal {
|
||||||
fn map_to_enum(view: ReportCombinedViewInternal) -> Option<ReportCombinedView> {
|
type CombinedView = ReportCombinedView;
|
||||||
// Use for a short alias
|
|
||||||
let v = view;
|
|
||||||
|
|
||||||
if let (Some(post_report), Some(post), Some(community), Some(unread_comments), Some(counts)) = (
|
fn map_to_enum(&self) -> Option<Self::CombinedView> {
|
||||||
v.post_report,
|
// Use for a short alias
|
||||||
v.post.clone(),
|
let v = self.clone();
|
||||||
v.community.clone(),
|
|
||||||
v.post_unread_comments,
|
if let (Some(post_report), Some(post), Some(community), Some(unread_comments), Some(counts)) = (
|
||||||
v.post_counts,
|
v.post_report,
|
||||||
) {
|
v.post.clone(),
|
||||||
Some(ReportCombinedView::Post(PostReportView {
|
v.community.clone(),
|
||||||
post_report,
|
v.post_unread_comments,
|
||||||
post,
|
v.post_counts,
|
||||||
community,
|
) {
|
||||||
unread_comments,
|
Some(ReportCombinedView::Post(PostReportView {
|
||||||
counts,
|
post_report,
|
||||||
creator: v.report_creator,
|
post,
|
||||||
post_creator: v.item_creator,
|
community,
|
||||||
creator_banned_from_community: v.item_creator_banned_from_community,
|
unread_comments,
|
||||||
creator_is_moderator: v.item_creator_is_moderator,
|
counts,
|
||||||
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,
|
|
||||||
creator: v.report_creator,
|
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,
|
resolver: v.resolver,
|
||||||
},
|
}))
|
||||||
))
|
} else if let (Some(comment_report), Some(comment), Some(counts), Some(post), Some(community)) = (
|
||||||
} else {
|
v.comment_report,
|
||||||
None
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -132,11 +132,17 @@ pub struct PaginationCursor(pub String);
|
||||||
#[cfg_attr(feature = "full", ts(export))]
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
pub struct ReportCombinedPaginationCursor(pub String);
|
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)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
#[cfg_attr(feature = "full", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "full", derive(ts_rs::TS))]
|
||||||
#[cfg_attr(feature = "full", ts(export))]
|
#[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]
|
#[skip_serializing_none]
|
||||||
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
|
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
|
||||||
|
@ -299,8 +305,8 @@ pub enum ReportCombinedView {
|
||||||
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
|
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
|
||||||
#[cfg_attr(feature = "full", derive(Queryable))]
|
#[cfg_attr(feature = "full", derive(Queryable))]
|
||||||
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||||
/// A combined profile view
|
/// A combined person_content view
|
||||||
pub struct ProfileCombinedViewInternal {
|
pub struct PersonContentViewInternal {
|
||||||
// Post-specific
|
// Post-specific
|
||||||
pub post_counts: PostAggregates,
|
pub post_counts: PostAggregates,
|
||||||
pub post_unread_comments: i64,
|
pub post_unread_comments: i64,
|
||||||
|
@ -331,7 +337,7 @@ pub struct ProfileCombinedViewInternal {
|
||||||
#[cfg_attr(feature = "full", ts(export))]
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
// Use serde's internal tagging, to work easier with javascript libraries
|
// Use serde's internal tagging, to work easier with javascript libraries
|
||||||
#[serde(tag = "type_")]
|
#[serde(tag = "type_")]
|
||||||
pub enum ProfileCombinedView {
|
pub enum PersonContentCombinedView {
|
||||||
Post(PostView),
|
Post(PostView),
|
||||||
Comment(CommentView),
|
Comment(CommentView),
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
DROP TABLE person_content_combined;
|
||||||
|
DROP TABLE person_saved_combined;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
DROP TABLE profile_combined;
|
|
||||||
|
|
|
@ -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;
|
|
||||||
|
|
|
@ -142,6 +142,7 @@ use lemmy_api_crud::{
|
||||||
};
|
};
|
||||||
use lemmy_apub::api::{
|
use lemmy_apub::api::{
|
||||||
list_comments::list_comments,
|
list_comments::list_comments,
|
||||||
|
list_person_content::list_person_content,
|
||||||
list_posts::list_posts,
|
list_posts::list_posts,
|
||||||
read_community::get_community,
|
read_community::get_community,
|
||||||
read_person::read_person,
|
read_person::read_person,
|
||||||
|
@ -338,6 +339,9 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) {
|
||||||
scope("/user")
|
scope("/user")
|
||||||
.wrap(rate_limit.message())
|
.wrap(rate_limit.message())
|
||||||
.route("", get().to(read_person))
|
.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", get().to(list_mentions))
|
||||||
.route(
|
.route(
|
||||||
"/mention/mark_as_read",
|
"/mention/mark_as_read",
|
||||||
|
|
Loading…
Reference in a new issue