Correctly paginate PostView when read_only is enabled (#5320)

This commit is contained in:
dullbananas 2025-01-13 13:46:46 -07:00 committed by GitHub
parent 4d17eef82b
commit c08e216ae8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 159 additions and 92 deletions

View file

@ -67,7 +67,7 @@ pub async fn list_posts(
// parse pagination token
let page_after = if let Some(pa) = &data.page_cursor {
Some(pa.read(&mut context.pool()).await?)
Some(pa.read(&mut context.pool(), local_user).await?)
} else {
None
};

View file

@ -163,7 +163,7 @@ async fn try_main() -> LemmyResult<()> {
if let Some(post_view) = post_views.into_iter().last() {
println!("👀 getting pagination cursor data for next page");
let cursor_data = PaginationCursor::after_post(&post_view)
.read(&mut conn.into())
.read(&mut conn.into(), None)
.await?;
page_after = Some(cursor_data);
} else {

View file

@ -4,6 +4,7 @@ use crate::{
schema::{community, person, post, post_actions},
source::post::{
Post,
PostActionsCursor,
PostHide,
PostHideForm,
PostInsertForm,
@ -409,6 +410,28 @@ impl PostHide {
}
}
impl PostActionsCursor {
pub async fn read(
pool: &mut DbPool<'_>,
post_id: PostId,
person_id: Option<PersonId>,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
Ok(if let Some(person_id) = person_id {
post_actions::table
.find((person_id, post_id))
.select(Self::as_select())
.first(conn)
.await
.optional()?
.unwrap_or_default()
} else {
Default::default()
})
}
}
#[cfg(test)]
mod tests {

View file

@ -4,6 +4,8 @@ use crate::schema::{post, post_actions};
use chrono::{DateTime, Utc};
#[cfg(feature = "full")]
use diesel::{dsl, expression_methods::NullableExpressionMethods};
#[cfg(feature = "full")]
use i_love_jesus::CursorKeysModule;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
@ -204,7 +206,7 @@ pub struct PostSavedForm {
pub saved: DateTime<Utc>,
}
#[derive(PartialEq, Eq, Debug)]
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(
feature = "full",
derive(Identifiable, Queryable, Selectable, Associations)
@ -257,3 +259,13 @@ pub struct PostHideForm {
#[new(value = "Utc::now()")]
pub hidden: DateTime<Utc>,
}
#[derive(PartialEq, Debug, Clone, Default)]
#[cfg_attr(feature = "full", derive(Queryable, Selectable, CursorKeysModule))]
#[cfg_attr(feature = "full", diesel(table_name = post_actions))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", cursor_keys_module(name = post_actions_keys))]
/// Sorted timestamps of actions on a post.
pub struct PostActionsCursor {
pub read: Option<DateTime<Utc>>,
}

View file

@ -37,7 +37,7 @@ use diesel_async::{
};
use diesel_bind_if_some::BindIfSome;
use futures_util::{future::BoxFuture, Future, FutureExt};
use i_love_jesus::CursorKey;
use i_love_jesus::{CursorKey, PaginatedQueryBuilder};
use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
settings::SETTINGS,
@ -734,6 +734,28 @@ impl<RF, LF> Queries<RF, LF> {
}
}
pub fn paginate<Q, C>(
query: Q,
page_after: Option<C>,
page_before_or_equal: Option<C>,
page_back: bool,
) -> PaginatedQueryBuilder<C, Q> {
let mut query = PaginatedQueryBuilder::new(query);
if page_back {
query = query
.before(page_after)
.after_or_equal(page_before_or_equal)
.limit_and_offset_from_end();
} else {
query = query
.after(page_after)
.before_or_equal(page_before_or_equal);
}
query
}
#[cfg(test)]
mod tests {

View file

@ -14,7 +14,6 @@ use diesel::{
QueryDsl,
};
use diesel_async::RunQueryDsl;
use i_love_jesus::PaginatedQueryBuilder;
use lemmy_db_schema::{
aggregates::structs::{post_aggregates_keys as key, PostAggregates},
aliases::creator_community_actions,
@ -38,6 +37,7 @@ use lemmy_db_schema::{
source::{
community::{CommunityFollower, CommunityFollowerState},
local_user::LocalUser,
post::{post_actions_keys, PostActionsCursor},
site::Site,
},
utils::{
@ -49,6 +49,7 @@ use lemmy_db_schema::{
get_conn,
limit_and_offset,
now,
paginate,
Commented,
DbConn,
DbPool,
@ -306,12 +307,6 @@ fn queries<'a>() -> Queries<
query = query.filter(post_aggregates::comments.eq(0));
};
if o.read_only.unwrap_or_default() {
query = query
.filter(post_actions::read.is_not_null())
.then_order_by(post_actions::read.desc())
}
if !o.show_read.unwrap_or(o.local_user.show_read_posts()) {
// Do not hide read posts when it is a user profile view
// Or, only hide read posts on non-profile views
@ -370,21 +365,23 @@ fn queries<'a>() -> Queries<
let (limit, offset) = limit_and_offset(o.page, o.limit)?;
query = query.limit(limit).offset(offset);
let mut query = PaginatedQueryBuilder::new(query);
let page_after = o.page_after.map(|c| c.0);
let page_before_or_equal = o.page_before_or_equal.map(|c| c.0);
if o.page_back.unwrap_or_default() {
query = query
.before(page_after)
.after_or_equal(page_before_or_equal)
.limit_and_offset_from_end();
let query = if o.read_only.unwrap_or_default() {
paginate(
query,
o.page_after.map(|c| c.post_actions),
None,
o.page_back.unwrap_or_default(),
)
.filter(post_actions::read.is_not_null())
.then_desc(post_actions_keys::read)
.as_query()
} else {
query = query
.after(page_after)
.before_or_equal(page_before_or_equal);
}
let mut query = paginate(
query,
o.page_after.map(|c| c.post_aggregates),
o.page_before_or_equal,
o.page_back.unwrap_or_default(),
);
// featured posts first
query = if o.community_id.is_none() || o.community_id_just_for_prefetch {
@ -429,8 +426,8 @@ fn queries<'a>() -> Queries<
// finally use unique post id as tie breaker
query = query.then_desc(key::post_id);
// Not done by debug_query
let query = query.as_query();
query.as_query()
};
debug!("Post View Query: {:?}", debug_query::<Pg, _>(&query));
@ -466,29 +463,36 @@ impl PaginationCursor {
// hex encoding to prevent ossification
PaginationCursor(format!("P{:x}", view.counts.post_id.0))
}
pub async fn read(&self, pool: &mut DbPool<'_>) -> Result<PaginationCursorData, Error> {
pub async fn read(
&self,
pool: &mut DbPool<'_>,
local_user: Option<&LocalUser>,
) -> Result<PaginationCursorData, Error> {
let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into());
let token = PostAggregates::read(
pool,
PostId(
let post_id = PostId(
self
.0
.get(1..)
.and_then(|e| i32::from_str_radix(e, 16).ok())
.ok_or_else(err_msg)?,
),
)
.await?;
);
let post_aggregates = PostAggregates::read(pool, post_id).await?;
let post_actions = PostActionsCursor::read(pool, post_id, local_user.person_id()).await?;
Ok(PaginationCursorData(token))
Ok(PaginationCursorData {
post_aggregates,
post_actions,
})
}
}
// currently we use a postaggregates struct as the pagination token.
// we only use some of the properties of the post aggregates, depending on which sort type we page
// by
// currently we use aggregates or actions as the pagination token.
// we only use some of the properties, depending on which sort type we page by
#[derive(Clone)]
pub struct PaginationCursorData(PostAggregates);
pub struct PaginationCursorData {
post_aggregates: PostAggregates,
post_actions: PostActionsCursor,
}
#[derive(Clone, Default)]
pub struct PostQuery<'a> {
@ -509,7 +513,7 @@ pub struct PostQuery<'a> {
pub page: Option<i64>,
pub limit: Option<i64>,
pub page_after: Option<PaginationCursorData>,
pub page_before_or_equal: Option<PaginationCursorData>,
pub page_before_or_equal: Option<PostAggregates>,
pub page_back: Option<bool>,
pub show_hidden: Option<bool>,
pub show_read: Option<bool>,
@ -589,7 +593,7 @@ impl<'a> PostQuery<'a> {
} else {
v.pop()
};
let limit_cursor = Some(PaginationCursorData(item.expect("else case").counts));
let limit_cursor = Some(item.expect("else case").counts);
Ok(Some(PostQuery {
page_before_or_equal: limit_cursor,
..self.clone()
@ -1577,7 +1581,10 @@ mod tests {
let mut page_after = None;
loop {
let post_listings = PostQuery {
page_after,
page_after: page_after.map(|p| PaginationCursorData {
post_aggregates: p,
post_actions: Default::default(),
}),
..options.clone()
}
.list(&data.site, pool)
@ -1586,7 +1593,7 @@ mod tests {
listed_post_ids.extend(post_listings.iter().map(|p| p.post.id));
if let Some(p) = post_listings.into_iter().last() {
page_after = Some(PaginationCursorData(p.counts));
page_after = Some(p.counts);
} else {
break;
}
@ -1597,7 +1604,10 @@ mod tests {
let mut page_before = None;
loop {
let post_listings = PostQuery {
page_after: page_before,
page_after: page_before.map(|p| PaginationCursorData {
post_aggregates: p,
post_actions: Default::default(),
}),
page_back: Some(true),
..options.clone()
}
@ -1614,7 +1624,7 @@ mod tests {
listed_post_ids_forward.truncate(index);
if let Some(p) = post_listings.into_iter().next() {
page_before = Some(PaginationCursorData(p.counts));
page_before = Some(p.counts);
} else {
break;
}