Adding ability to hide posts. (#4480)

* Adding ability to hide posts.

- Adds an post/hide API route.
- Adds a `show_hidden` (default false) to `GetPosts`.
- Adds a `hidden` field to `PostView`.
- Removes the single `post_id` from MarkPostAsRead.
- Fixes #1403

* Add a check to make sure hidden field is true.

* Fixing test.

* Add back semicolon
This commit is contained in:
Dessalines 2024-02-29 10:42:34 -05:00 committed by GitHub
parent 6d815db375
commit 87b577467b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 266 additions and 95 deletions

View file

@ -0,0 +1,34 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{context::LemmyContext, post::HidePost, SuccessResponse};
use lemmy_db_schema::source::post::PostHide;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, MAX_API_PARAM_ELEMENTS};
use std::collections::HashSet;
#[tracing::instrument(skip(context))]
pub async fn hide_post(
data: Json<HidePost>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<SuccessResponse>, LemmyError> {
let post_ids = HashSet::from_iter(data.post_ids.clone());
if post_ids.len() > MAX_API_PARAM_ELEMENTS {
Err(LemmyErrorType::TooManyItems)?;
}
let person_id = local_user_view.person.id;
// Mark the post as hidden / unhidden
if data.hide {
PostHide::hide(&mut context.pool(), post_ids, person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntHidePost)?;
} else {
PostHide::unhide(&mut context.pool(), post_ids, person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntHidePost)?;
}
Ok(Json(SuccessResponse::default()))
}

View file

@ -11,14 +11,7 @@ pub async fn mark_post_as_read(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> Result<Json<SuccessResponse>, LemmyError> { ) -> Result<Json<SuccessResponse>, LemmyError> {
let mut post_ids = HashSet::new(); let post_ids = HashSet::from_iter(data.post_ids.clone());
if let Some(post_ids_) = &data.post_ids {
post_ids.extend(post_ids_.iter().cloned());
}
if let Some(post_id) = data.post_id {
post_ids.insert(post_id);
}
if post_ids.len() > MAX_API_PARAM_ELEMENTS { if post_ids.len() > MAX_API_PARAM_ELEMENTS {
Err(LemmyErrorType::TooManyItems)?; Err(LemmyErrorType::TooManyItems)?;

View file

@ -1,5 +1,6 @@
pub mod feature; pub mod feature;
pub mod get_link_metadata; pub mod get_link_metadata;
pub mod hide;
pub mod like; pub mod like;
pub mod list_post_likes; pub mod list_post_likes;
pub mod lock; pub mod lock;

View file

@ -79,6 +79,7 @@ pub struct GetPosts {
pub saved_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 show_hidden: Option<bool>,
pub page_cursor: Option<PaginationCursor>, pub page_cursor: Option<PaginationCursor>,
} }
@ -148,12 +149,20 @@ pub struct RemovePost {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Mark a post as read. /// Mark a post as read.
pub struct MarkPostAsRead { pub struct MarkPostAsRead {
/// TODO: deprecated, send `post_ids` instead pub post_ids: Vec<PostId>,
pub post_id: Option<PostId>,
pub post_ids: Option<Vec<PostId>>,
pub read: bool, pub read: bool,
} }
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Hide a post from list views
pub struct HidePost {
pub post_ids: Vec<PostId>,
pub hide: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]

View file

@ -36,6 +36,7 @@ pub async fn list_posts(
data.community_id data.community_id
}; };
let saved_only = data.saved_only.unwrap_or_default(); let saved_only = data.saved_only.unwrap_or_default();
let show_hidden = data.show_hidden.unwrap_or_default();
let liked_only = data.liked_only.unwrap_or_default(); let liked_only = data.liked_only.unwrap_or_default();
let disliked_only = data.disliked_only.unwrap_or_default(); let disliked_only = data.disliked_only.unwrap_or_default();
@ -75,6 +76,7 @@ pub async fn list_posts(
page, page,
page_after, page_after,
limit, limit,
show_hidden,
..Default::default() ..Default::default()
} }
.list(&local_site.site, &mut context.pool()) .list(&local_site.site, &mut context.pool())

View file

@ -1,23 +1,10 @@
use crate::{ use crate::{
newtypes::{CommunityId, DbUrl, PersonId, PostId}, newtypes::{CommunityId, DbUrl, PersonId, PostId},
schema::post::dsl::{ schema::{post, post_hide, post_like, post_read, post_saved},
ap_id,
body,
community_id,
creator_id,
deleted,
featured_community,
local,
name,
post,
published,
removed,
thumbnail_url,
updated,
url,
},
source::post::{ source::post::{
Post, Post,
PostHide,
PostHideForm,
PostInsertForm, PostInsertForm,
PostLike, PostLike,
PostLikeForm, PostLikeForm,
@ -53,9 +40,9 @@ impl Crud for Post {
async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result<Self, Error> { async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
insert_into(post) insert_into(post::table)
.values(form) .values(form)
.on_conflict(ap_id) .on_conflict(post::ap_id)
.do_update() .do_update()
.set(form) .set(form)
.get_result::<Self>(conn) .get_result::<Self>(conn)
@ -68,7 +55,7 @@ impl Crud for Post {
new_post: &Self::UpdateForm, new_post: &Self::UpdateForm,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
diesel::update(post.find(post_id)) diesel::update(post::table.find(post_id))
.set(new_post) .set(new_post)
.get_result::<Self>(conn) .get_result::<Self>(conn)
.await .await
@ -81,12 +68,12 @@ impl Post {
the_community_id: CommunityId, the_community_id: CommunityId,
) -> Result<Vec<Self>, Error> { ) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
post post::table
.filter(community_id.eq(the_community_id)) .filter(post::community_id.eq(the_community_id))
.filter(deleted.eq(false)) .filter(post::deleted.eq(false))
.filter(removed.eq(false)) .filter(post::removed.eq(false))
.then_order_by(featured_community.desc()) .then_order_by(post::featured_community.desc())
.then_order_by(published.desc()) .then_order_by(post::published.desc())
.limit(FETCH_LIMIT_MAX) .limit(FETCH_LIMIT_MAX)
.load::<Self>(conn) .load::<Self>(conn)
.await .await
@ -97,12 +84,12 @@ impl Post {
the_community_id: CommunityId, the_community_id: CommunityId,
) -> Result<Vec<Self>, Error> { ) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
post post::table
.filter(community_id.eq(the_community_id)) .filter(post::community_id.eq(the_community_id))
.filter(deleted.eq(false)) .filter(post::deleted.eq(false))
.filter(removed.eq(false)) .filter(post::removed.eq(false))
.filter(featured_community.eq(true)) .filter(post::featured_community.eq(true))
.then_order_by(published.desc()) .then_order_by(post::published.desc())
.limit(FETCH_LIMIT_MAX) .limit(FETCH_LIMIT_MAX)
.load::<Self>(conn) .load::<Self>(conn)
.await .await
@ -112,13 +99,13 @@ impl Post {
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
) -> Result<Vec<(DbUrl, chrono::DateTime<Utc>)>, Error> { ) -> Result<Vec<(DbUrl, chrono::DateTime<Utc>)>, Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
post post::table
.select((ap_id, coalesce(updated, published))) .select((post::ap_id, coalesce(post::updated, post::published)))
.filter(local.eq(true)) .filter(post::local.eq(true))
.filter(deleted.eq(false)) .filter(post::deleted.eq(false))
.filter(removed.eq(false)) .filter(post::removed.eq(false))
.filter(published.ge(Utc::now().naive_utc() - Duration::days(SITEMAP_DAYS))) .filter(post::published.ge(Utc::now().naive_utc() - Duration::days(SITEMAP_DAYS)))
.order(published.desc()) .order(post::published.desc())
.limit(SITEMAP_LIMIT) .limit(SITEMAP_LIMIT)
.load::<(DbUrl, chrono::DateTime<Utc>)>(conn) .load::<(DbUrl, chrono::DateTime<Utc>)>(conn)
.await .await
@ -130,13 +117,13 @@ impl Post {
) -> Result<Vec<Self>, Error> { ) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
diesel::update(post.filter(creator_id.eq(for_creator_id))) diesel::update(post::table.filter(post::creator_id.eq(for_creator_id)))
.set(( .set((
name.eq(DELETED_REPLACEMENT_TEXT), post::name.eq(DELETED_REPLACEMENT_TEXT),
url.eq(Option::<&str>::None), post::url.eq(Option::<&str>::None),
body.eq(DELETED_REPLACEMENT_TEXT), post::body.eq(DELETED_REPLACEMENT_TEXT),
deleted.eq(true), post::deleted.eq(true),
updated.eq(naive_now()), post::updated.eq(naive_now()),
)) ))
.get_results::<Self>(conn) .get_results::<Self>(conn)
.await .await
@ -150,15 +137,15 @@ impl Post {
) -> Result<Vec<Self>, Error> { ) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
let mut update = diesel::update(post).into_boxed(); let mut update = diesel::update(post::table).into_boxed();
update = update.filter(creator_id.eq(for_creator_id)); update = update.filter(post::creator_id.eq(for_creator_id));
if let Some(for_community_id) = for_community_id { if let Some(for_community_id) = for_community_id {
update = update.filter(community_id.eq(for_community_id)); update = update.filter(post::community_id.eq(for_community_id));
} }
update update
.set((removed.eq(new_removed), updated.eq(naive_now()))) .set((post::removed.eq(new_removed), post::updated.eq(naive_now())))
.get_results::<Self>(conn) .get_results::<Self>(conn)
.await .await
} }
@ -174,8 +161,8 @@ impl Post {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
let object_id: DbUrl = object_id.into(); let object_id: DbUrl = object_id.into();
Ok( Ok(
post post::table
.filter(ap_id.eq(object_id)) .filter(post::ap_id.eq(object_id))
.first::<Post>(conn) .first::<Post>(conn)
.await .await
.ok() .ok()
@ -190,9 +177,9 @@ impl Post {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
let pictrs_search = "%pictrs/image%"; let pictrs_search = "%pictrs/image%";
post post::table
.filter(creator_id.eq(for_creator_id)) .filter(post::creator_id.eq(for_creator_id))
.filter(url.like(pictrs_search)) .filter(post::url.like(pictrs_search))
.load::<Self>(conn) .load::<Self>(conn)
.await .await
} }
@ -206,13 +193,13 @@ impl Post {
let pictrs_search = "%pictrs/image%"; let pictrs_search = "%pictrs/image%";
diesel::update( diesel::update(
post post::table
.filter(creator_id.eq(for_creator_id)) .filter(post::creator_id.eq(for_creator_id))
.filter(url.like(pictrs_search)), .filter(post::url.like(pictrs_search)),
) )
.set(( .set((
url.eq::<Option<String>>(None), post::url.eq::<Option<String>>(None),
thumbnail_url.eq::<Option<String>>(None), post::thumbnail_url.eq::<Option<String>>(None),
)) ))
.get_results::<Self>(conn) .get_results::<Self>(conn)
.await .await
@ -224,9 +211,9 @@ impl Post {
) -> Result<Vec<Self>, Error> { ) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
let pictrs_search = "%pictrs/image%"; let pictrs_search = "%pictrs/image%";
post post::table
.filter(community_id.eq(for_community_id)) .filter(post::community_id.eq(for_community_id))
.filter(url.like(pictrs_search)) .filter(post::url.like(pictrs_search))
.load::<Self>(conn) .load::<Self>(conn)
.await .await
} }
@ -240,13 +227,13 @@ impl Post {
let pictrs_search = "%pictrs/image%"; let pictrs_search = "%pictrs/image%";
diesel::update( diesel::update(
post post::table
.filter(community_id.eq(for_community_id)) .filter(post::community_id.eq(for_community_id))
.filter(url.like(pictrs_search)), .filter(post::url.like(pictrs_search)),
) )
.set(( .set((
url.eq::<Option<String>>(None), post::url.eq::<Option<String>>(None),
thumbnail_url.eq::<Option<String>>(None), post::thumbnail_url.eq::<Option<String>>(None),
)) ))
.get_results::<Self>(conn) .get_results::<Self>(conn)
.await .await
@ -258,11 +245,10 @@ impl Likeable for PostLike {
type Form = PostLikeForm; type Form = PostLikeForm;
type IdType = PostId; type IdType = PostId;
async fn like(pool: &mut DbPool<'_>, post_like_form: &PostLikeForm) -> Result<Self, Error> { async fn like(pool: &mut DbPool<'_>, post_like_form: &PostLikeForm) -> Result<Self, Error> {
use crate::schema::post_like::dsl::{person_id, post_id, post_like};
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
insert_into(post_like) insert_into(post_like::table)
.values(post_like_form) .values(post_like_form)
.on_conflict((post_id, person_id)) .on_conflict((post_like::post_id, post_like::person_id))
.do_update() .do_update()
.set(post_like_form) .set(post_like_form)
.get_result::<Self>(conn) .get_result::<Self>(conn)
@ -273,9 +259,8 @@ impl Likeable for PostLike {
person_id: PersonId, person_id: PersonId,
post_id: PostId, post_id: PostId,
) -> Result<usize, Error> { ) -> Result<usize, Error> {
use crate::schema::post_like::dsl;
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
diesel::delete(dsl::post_like.find((person_id, post_id))) diesel::delete(post_like::table.find((person_id, post_id)))
.execute(conn) .execute(conn)
.await .await
} }
@ -285,20 +270,18 @@ impl Likeable for PostLike {
impl Saveable for PostSaved { impl Saveable for PostSaved {
type Form = PostSavedForm; type Form = PostSavedForm;
async fn save(pool: &mut DbPool<'_>, post_saved_form: &PostSavedForm) -> Result<Self, Error> { async fn save(pool: &mut DbPool<'_>, post_saved_form: &PostSavedForm) -> Result<Self, Error> {
use crate::schema::post_saved::dsl::{person_id, post_id, post_saved};
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
insert_into(post_saved) insert_into(post_saved::table)
.values(post_saved_form) .values(post_saved_form)
.on_conflict((post_id, person_id)) .on_conflict((post_saved::post_id, post_saved::person_id))
.do_update() .do_update()
.set(post_saved_form) .set(post_saved_form)
.get_result::<Self>(conn) .get_result::<Self>(conn)
.await .await
} }
async fn unsave(pool: &mut DbPool<'_>, post_saved_form: &PostSavedForm) -> Result<usize, Error> { async fn unsave(pool: &mut DbPool<'_>, post_saved_form: &PostSavedForm) -> Result<usize, Error> {
use crate::schema::post_saved::dsl::post_saved;
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
diesel::delete(post_saved.find((post_saved_form.person_id, post_saved_form.post_id))) diesel::delete(post_saved::table.find((post_saved_form.person_id, post_saved_form.post_id)))
.execute(conn) .execute(conn)
.await .await
} }
@ -310,14 +293,13 @@ impl PostRead {
post_ids: HashSet<PostId>, post_ids: HashSet<PostId>,
person_id: PersonId, person_id: PersonId,
) -> Result<usize, Error> { ) -> Result<usize, Error> {
use crate::schema::post_read::dsl::post_read;
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
let forms = post_ids let forms = post_ids
.into_iter() .into_iter()
.map(|post_id| PostReadForm { post_id, person_id }) .map(|post_id| PostReadForm { post_id, person_id })
.collect::<Vec<PostReadForm>>(); .collect::<Vec<PostReadForm>>();
insert_into(post_read) insert_into(post_read::table)
.values(forms) .values(forms)
.on_conflict_do_nothing() .on_conflict_do_nothing()
.execute(conn) .execute(conn)
@ -329,13 +311,48 @@ impl PostRead {
post_id_: HashSet<PostId>, post_id_: HashSet<PostId>,
person_id_: PersonId, person_id_: PersonId,
) -> Result<usize, Error> { ) -> Result<usize, Error> {
use crate::schema::post_read::dsl::{person_id, post_id, post_read};
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
diesel::delete( diesel::delete(
post_read post_read::table
.filter(post_id.eq_any(post_id_)) .filter(post_read::post_id.eq_any(post_id_))
.filter(person_id.eq(person_id_)), .filter(post_read::person_id.eq(person_id_)),
)
.execute(conn)
.await
}
}
impl PostHide {
pub async fn hide(
pool: &mut DbPool<'_>,
post_ids: HashSet<PostId>,
person_id: PersonId,
) -> Result<usize, Error> {
let conn = &mut get_conn(pool).await?;
let forms = post_ids
.into_iter()
.map(|post_id| PostHideForm { post_id, person_id })
.collect::<Vec<PostHideForm>>();
insert_into(post_hide::table)
.values(forms)
.on_conflict_do_nothing()
.execute(conn)
.await
}
pub async fn unhide(
pool: &mut DbPool<'_>,
post_id_: HashSet<PostId>,
person_id_: PersonId,
) -> Result<usize, Error> {
let conn = &mut get_conn(pool).await?;
diesel::delete(
post_hide::table
.filter(post_hide::post_id.eq_any(post_id_))
.filter(post_hide::person_id.eq(person_id_)),
) )
.execute(conn) .execute(conn)
.await .await

View file

@ -730,6 +730,14 @@ diesel::table! {
} }
} }
diesel::table! {
post_hide (person_id, post_id) {
post_id -> Int4,
person_id -> Int4,
published -> Timestamptz,
}
}
diesel::table! { diesel::table! {
post_like (person_id, post_id) { post_like (person_id, post_id) {
post_id -> Int4, post_id -> Int4,
@ -983,6 +991,8 @@ diesel::joinable!(post_aggregates -> community (community_id));
diesel::joinable!(post_aggregates -> instance (instance_id)); diesel::joinable!(post_aggregates -> instance (instance_id));
diesel::joinable!(post_aggregates -> person (creator_id)); diesel::joinable!(post_aggregates -> person (creator_id));
diesel::joinable!(post_aggregates -> post (post_id)); diesel::joinable!(post_aggregates -> post (post_id));
diesel::joinable!(post_hide -> person (person_id));
diesel::joinable!(post_hide -> post (post_id));
diesel::joinable!(post_like -> person (person_id)); diesel::joinable!(post_like -> person (person_id));
diesel::joinable!(post_like -> post (post_id)); diesel::joinable!(post_like -> post (post_id));
diesel::joinable!(post_read -> person (person_id)); diesel::joinable!(post_read -> person (person_id));
@ -1054,6 +1064,7 @@ diesel::allow_tables_to_appear_in_same_query!(
person_post_aggregates, person_post_aggregates,
post, post,
post_aggregates, post_aggregates,
post_hide,
post_like, post_like,
post_read, post_read,
post_report, post_report,

View file

@ -1,6 +1,6 @@
use crate::newtypes::{CommunityId, DbUrl, LanguageId, PersonId, PostId}; use crate::newtypes::{CommunityId, DbUrl, LanguageId, PersonId, PostId};
#[cfg(feature = "full")] #[cfg(feature = "full")]
use crate::schema::{post, post_like, post_read, post_saved}; use crate::schema::{post, post_hide, post_like, post_read, post_saved};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
@ -182,3 +182,25 @@ pub(crate) struct PostReadForm {
pub post_id: PostId, pub post_id: PostId,
pub person_id: PersonId, pub person_id: PersonId,
} }
#[derive(PartialEq, Eq, Debug)]
#[cfg_attr(
feature = "full",
derive(Identifiable, Queryable, Selectable, Associations)
)]
#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))]
#[cfg_attr(feature = "full", diesel(table_name = post_hide))]
#[cfg_attr(feature = "full", diesel(primary_key(post_id, person_id)))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
pub struct PostHide {
pub post_id: PostId,
pub person_id: PersonId,
pub published: DateTime<Utc>,
}
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = post_hide))]
pub(crate) struct PostHideForm {
pub post_id: PostId,
pub person_id: PersonId,
}

View file

@ -35,6 +35,7 @@ use lemmy_db_schema::{
person_post_aggregates, person_post_aggregates,
post, post,
post_aggregates, post_aggregates,
post_hide,
post_like, post_like,
post_read, post_read,
post_saved, post_saved,
@ -107,6 +108,16 @@ fn queries<'a>() -> Queries<
) )
}; };
let is_hidden = |person_id| {
exists(
post_hide::table.filter(
post_aggregates::post_id
.eq(post_hide::post_id)
.and(post_hide::person_id.eq(person_id)),
),
)
};
let is_creator_blocked = |person_id| { let is_creator_blocked = |person_id| {
exists( exists(
person_block::table.filter( person_block::table.filter(
@ -147,6 +158,13 @@ fn queries<'a>() -> Queries<
Box::new(false.into_sql::<sql_types::Bool>()) Box::new(false.into_sql::<sql_types::Bool>())
}; };
let is_hidden_selection: Box<dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>> =
if let Some(person_id) = my_person_id {
Box::new(is_hidden(person_id))
} else {
Box::new(false.into_sql::<sql_types::Bool>())
};
let is_creator_blocked_selection: Box<dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>> = let is_creator_blocked_selection: Box<dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>> =
if let Some(person_id) = my_person_id { if let Some(person_id) = my_person_id {
Box::new(is_creator_blocked(person_id)) Box::new(is_creator_blocked(person_id))
@ -211,6 +229,7 @@ fn queries<'a>() -> Queries<
subscribed_type_selection, subscribed_type_selection,
is_saved_selection, is_saved_selection,
is_read_selection, is_read_selection,
is_hidden_selection,
is_creator_blocked_selection, is_creator_blocked_selection,
score_selection, score_selection,
coalesce( coalesce(
@ -406,6 +425,13 @@ fn queries<'a>() -> Queries<
} }
} }
if !options.show_hidden {
// If a creator id isn't given (IE its on home or community pages), hide the hidden posts
if let (None, Some(person_id)) = (options.creator_id, my_person_id) {
query = query.filter(not(is_hidden(person_id)));
}
}
if let Some(person_id) = my_person_id { if let Some(person_id) = my_person_id {
if options.liked_only { if options.liked_only {
query = query.filter(score(person_id).eq(1)); query = query.filter(score(person_id).eq(1));
@ -593,6 +619,7 @@ pub struct PostQuery<'a> {
pub page_after: Option<PaginationCursorData>, pub page_after: Option<PaginationCursorData>,
pub page_before_or_equal: Option<PaginationCursorData>, pub page_before_or_equal: Option<PaginationCursorData>,
pub page_back: bool, pub page_back: bool,
pub show_hidden: bool,
} }
impl<'a> PostQuery<'a> { impl<'a> PostQuery<'a> {
@ -726,7 +753,7 @@ mod tests {
local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
person::{Person, PersonInsertForm}, person::{Person, PersonInsertForm},
person_block::{PersonBlock, PersonBlockForm}, person_block::{PersonBlock, PersonBlockForm},
post::{Post, PostInsertForm, PostLike, PostLikeForm, PostRead, PostUpdateForm}, post::{Post, PostHide, PostInsertForm, PostLike, PostLikeForm, PostRead, PostUpdateForm},
site::Site, site::Site,
}, },
traits::{Blockable, Crud, Joinable, Likeable}, traits::{Blockable, Crud, Joinable, Likeable},
@ -1463,6 +1490,47 @@ mod tests {
cleanup(data, pool).await cleanup(data, pool).await
} }
#[tokio::test]
#[serial]
async fn post_listings_hide_hidden() -> LemmyResult<()> {
let pool = &build_db_pool().await?;
let pool = &mut pool.into();
let data = init_data(pool).await?;
// Mark a post as hidden
PostHide::hide(
pool,
HashSet::from([data.inserted_bot_post.id]),
data.local_user_view.person.id,
)
.await?;
// Make sure you don't see the hidden post in the results
let post_listings_hide_hidden = data.default_post_query().list(&data.site, pool).await?;
assert_eq!(vec![POST], names(&post_listings_hide_hidden));
// Make sure it does come back with the show_hidden option
let post_listings_show_hidden = PostQuery {
sort: Some(SortType::New),
local_user: Some(&data.local_user_view),
show_hidden: true,
..Default::default()
}
.list(&data.site, pool)
.await?;
assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_show_hidden));
// Make sure that hidden field is true.
assert!(
&post_listings_show_hidden
.first()
.expect("first post should exist")
.hidden
);
cleanup(data, pool).await
}
async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> {
let num_deleted = Post::delete(pool, data.inserted_post.id).await?; let num_deleted = Post::delete(pool, data.inserted_post.id).await?;
Community::delete(pool, data.inserted_community.id).await?; Community::delete(pool, data.inserted_community.id).await?;
@ -1584,6 +1652,7 @@ mod tests {
}, },
subscribed: SubscribedType::NotSubscribed, subscribed: SubscribedType::NotSubscribed,
read: false, read: false,
hidden: false,
saved: false, saved: false,
creator_blocked: false, creator_blocked: false,
}) })

View file

@ -120,6 +120,7 @@ pub struct PostView {
pub subscribed: SubscribedType, pub subscribed: SubscribedType,
pub saved: bool, pub saved: bool,
pub read: bool, pub read: bool,
pub hidden: bool,
pub creator_blocked: bool, pub creator_blocked: bool,
pub my_vote: Option<i16>, pub my_vote: Option<i16>,
pub unread_comments: i64, pub unread_comments: i64,

View file

@ -122,6 +122,7 @@ pub enum LemmyErrorType {
CouldntLikePost, CouldntLikePost,
CouldntSavePost, CouldntSavePost,
CouldntMarkPostAsRead, CouldntMarkPostAsRead,
CouldntHidePost,
CouldntUpdateCommunity, CouldntUpdateCommunity,
CouldntUpdateReplies, CouldntUpdateReplies,
CouldntUpdatePersonMentions, CouldntUpdatePersonMentions,

View file

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

View file

@ -0,0 +1,7 @@
CREATE TABLE post_hide (
post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
published timestamp with time zone NOT NULL DEFAULT now(),
PRIMARY KEY (person_id, post_id)
);

View file

@ -49,6 +49,7 @@ use lemmy_api::{
post::{ post::{
feature::feature_post, feature::feature_post,
get_link_metadata::get_link_metadata, get_link_metadata::get_link_metadata,
hide::hide_post,
like::like_post, like::like_post,
list_post_likes::list_post_likes, list_post_likes::list_post_likes,
lock::lock_post, lock::lock_post,
@ -206,6 +207,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
.route("/delete", web::post().to(delete_post)) .route("/delete", web::post().to(delete_post))
.route("/remove", web::post().to(remove_post)) .route("/remove", web::post().to(remove_post))
.route("/mark_as_read", web::post().to(mark_post_as_read)) .route("/mark_as_read", web::post().to(mark_post_as_read))
.route("/hide", web::post().to(hide_post))
.route("/lock", web::post().to(lock_post)) .route("/lock", web::post().to(lock_post))
.route("/feature", web::post().to(feature_post)) .route("/feature", web::post().to(feature_post))
.route("/list", web::get().to(list_posts)) .route("/list", web::get().to(list_posts))