Add support for Featured Posts (#2585)

* Add support for Featured Posts

* Fix rebase

* More fixes
This commit is contained in:
Anon 2022-12-12 05:17:10 -06:00 committed by GitHub
parent 0ecf256ce3
commit 9dfd819691
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 319 additions and 156 deletions

View file

@ -20,7 +20,7 @@
"eslint": "^8.25.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "^27.0.6",
"lemmy-js-client": "0.17.0-rc.48",
"lemmy-js-client": "0.17.0-rc.56",
"node-fetch": "^2.6.1",
"prettier": "^2.7.1",
"reflect-metadata": "^0.1.13",

View file

@ -11,7 +11,7 @@ import {
setupLogins,
createPost,
editPost,
stickyPost,
featurePost,
lockPost,
resolvePost,
likePost,
@ -157,8 +157,8 @@ test("Sticky a post", async () => {
let betaPost1 = (
await resolvePost(beta, postRes.post_view.post)
).post.unwrap();
let stickiedPostRes = await stickyPost(beta, true, betaPost1.post);
expect(stickiedPostRes.post_view.post.stickied).toBe(true);
let stickiedPostRes = await featurePost(beta, true, betaPost1.post);
expect(stickiedPostRes.post_view.post.featured_community).toBe(true);
// Make sure that post is stickied on beta
let betaPost = (
@ -166,11 +166,11 @@ test("Sticky a post", async () => {
).post.unwrap();
expect(betaPost.community.local).toBe(true);
expect(betaPost.creator.local).toBe(false);
expect(betaPost.post.stickied).toBe(true);
expect(betaPost.post.featured_community).toBe(true);
// Unsticky a post
let unstickiedPost = await stickyPost(beta, false, betaPost1.post);
expect(unstickiedPost.post_view.post.stickied).toBe(false);
let unstickiedPost = await featurePost(beta, false, betaPost1.post);
expect(unstickiedPost.post_view.post.featured_community).toBe(false);
// Make sure that post is unstickied on beta
let betaPost2 = (
@ -178,18 +178,18 @@ test("Sticky a post", async () => {
).post.unwrap();
expect(betaPost2.community.local).toBe(true);
expect(betaPost2.creator.local).toBe(false);
expect(betaPost2.post.stickied).toBe(false);
expect(betaPost2.post.featured_community).toBe(false);
// Make sure that gamma cannot sticky the post on beta
let gammaPost = (
await resolvePost(gamma, postRes.post_view.post)
).post.unwrap();
let gammaTrySticky = await stickyPost(gamma, true, gammaPost.post);
let gammaTrySticky = await featurePost(gamma, true, gammaPost.post);
let betaPost3 = (
await resolvePost(beta, postRes.post_view.post)
).post.unwrap();
expect(gammaTrySticky.post_view.post.stickied).toBe(true);
expect(betaPost3.post.stickied).toBe(false);
expect(gammaTrySticky.post_view.post.featured_community).toBe(true);
expect(betaPost3.post.featured_community).toBe(false);
});
test("Lock a post", async () => {

View file

@ -7,7 +7,6 @@ import {
CreateComment,
DeletePost,
RemovePost,
StickyPost,
LockPost,
PostResponse,
SearchResponse,
@ -64,6 +63,8 @@ import {
CommentSortType,
GetComments,
GetCommentsResponse,
FeaturePost,
PostFeatureType,
} from "lemmy-js-client";
export interface API {
@ -180,14 +181,13 @@ export async function setupLogins() {
rate_limit_search: Some(999),
rate_limit_search_per_second: None,
federation_enabled: None,
federation_strict_allowlist: None,
federation_http_fetch_retry_limit: None,
federation_worker_count: None,
captcha_enabled: None,
captcha_difficulty: None,
allowed_instances: None,
blocked_instances: None,
auth: "",
taglines: None,
});
// Set the blocks and auths for each
@ -293,17 +293,18 @@ export async function removePost(
return api.client.removePost(form);
}
export async function stickyPost(
export async function featurePost(
api: API,
stickied: boolean,
featured: boolean,
post: Post
): Promise<PostResponse> {
let form = new StickyPost({
let form = new FeaturePost({
post_id: post.id,
stickied,
featured,
feature_type: PostFeatureType.Community,
auth: api.auth.unwrap(),
});
return api.client.stickyPost(form);
return api.client.featurePost(form);
}
export async function lockPost(

View file

@ -2373,10 +2373,15 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
lemmy-js-client@0.17.0-rc.48:
version "0.17.0-rc.48"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.0-rc.48.tgz#6085812d4901b7d12b3fca237d8aced7f5210eac"
integrity sha512-Lz8Nzq/kczQtDj6STlbhxoEarFHtTCoWcWBabyPs6X6em/pfK/cnZqx1mMn7EaBSDUVQ+WL8UNFjQiqjhR4kww==
lemmy-js-client@0.17.0-rc.56:
version "0.17.0-rc.56"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.0-rc.56.tgz#2c7abba9b8195826eb36401e7c5c2cb75609bcf2"
integrity sha512-7MM5xV8H9fIr1TbM/4e9PFKJpwlD2t135pSiH92TFgdkTzOMf0mtLO2BWLAQ7Rq+XVoVgj/WSBR4BofJka8XRQ==
dependencies:
"@sniptt/monads" "^0.5.10"
class-transformer "^0.5.1"
node-fetch "2.6.6"
reflect-metadata "^0.1.13"
leven@^3.1.0:
version "3.1.0"
@ -2511,6 +2516,13 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
node-fetch@2.6.6:
version "2.6.6"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==
dependencies:
whatwg-url "^5.0.0"
node-fetch@^2.6.1:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"

View file

@ -2,26 +2,28 @@ use crate::Perform;
use actix_web::web::Data;
use lemmy_api_common::{
context::LemmyContext,
post::{PostResponse, StickyPost},
post::{FeaturePost, PostResponse},
utils::{
check_community_ban,
check_community_deleted_or_removed,
get_local_user_view_from_jwt,
is_admin,
is_mod_or_admin,
},
websocket::{send::send_post_ws_message, UserOperation},
};
use lemmy_db_schema::{
source::{
moderator::{ModStickyPost, ModStickyPostForm},
moderator::{ModFeaturePost, ModFeaturePostForm},
post::{Post, PostUpdateForm},
},
traits::Crud,
PostFeatureType,
};
use lemmy_utils::{error::LemmyError, ConnectionId};
#[async_trait::async_trait(?Send)]
impl Perform for StickyPost {
impl Perform for FeaturePost {
type Response = PostResponse;
#[tracing::instrument(skip(context, websocket_id))]
@ -30,7 +32,7 @@ impl Perform for StickyPost {
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &StickyPost = self;
let data: &FeaturePost = self;
let local_user_view =
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
@ -45,36 +47,44 @@ impl Perform for StickyPost {
.await?;
check_community_deleted_or_removed(orig_post.community_id, context.pool()).await?;
// Verify that only the mods can sticky
is_mod_or_admin(
context.pool(),
local_user_view.person.id,
orig_post.community_id,
)
.await?;
if data.feature_type == PostFeatureType::Community {
// Verify that only the mods can feature in community
is_mod_or_admin(
context.pool(),
local_user_view.person.id,
orig_post.community_id,
)
.await?;
} else {
is_admin(&local_user_view)?;
}
// Update the post
let post_id = data.post_id;
let stickied = data.stickied;
Post::update(
context.pool(),
post_id,
&PostUpdateForm::builder().stickied(Some(stickied)).build(),
)
.await?;
let new_post: PostUpdateForm = if data.feature_type == PostFeatureType::Community {
PostUpdateForm::builder()
.featured_community(Some(data.featured))
.build()
} else {
PostUpdateForm::builder()
.featured_local(Some(data.featured))
.build()
};
Post::update(context.pool(), post_id, &new_post).await?;
// Mod tables
let form = ModStickyPostForm {
let form = ModFeaturePostForm {
mod_person_id: local_user_view.person.id,
post_id: data.post_id,
stickied: Some(stickied),
featured: data.featured,
is_featured_community: data.feature_type == PostFeatureType::Community,
};
ModStickyPost::create(context.pool(), &form).await?;
ModFeaturePost::create(context.pool(), &form).await?;
send_post_ws_message(
data.post_id,
UserOperation::StickyPost,
UserOperation::FeaturePost,
websocket_id,
Some(local_user_view.person.id),
context,

View file

@ -1,6 +1,6 @@
mod feature;
mod get_link_metadata;
mod like;
mod lock;
mod mark_read;
mod save;
mod sticky;

View file

@ -19,12 +19,12 @@ use lemmy_db_views_moderator::structs::{
ModAddView,
ModBanFromCommunityView,
ModBanView,
ModFeaturePostView,
ModHideCommunityView,
ModLockPostView,
ModRemoveCommentView,
ModRemoveCommunityView,
ModRemovePostView,
ModStickyPostView,
ModTransferCommunityView,
ModlogListParams,
};
@ -91,8 +91,8 @@ impl Perform for GetModlog {
_ => Default::default(),
};
let stickied_posts = match type_ {
All | ModStickyPost => ModStickyPostView::list(context.pool(), params).await?,
let featured_posts = match type_ {
All | ModFeaturePost => ModFeaturePostView::list(context.pool(), params).await?,
_ => Default::default(),
};
@ -181,7 +181,7 @@ impl Perform for GetModlog {
Ok(GetModlogResponse {
removed_posts,
locked_posts,
stickied_posts,
featured_posts,
removed_comments,
removed_communities,
banned_from_community,

View file

@ -2,6 +2,7 @@ use crate::sensitive::Sensitive;
use lemmy_db_schema::{
newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, PostReportId},
ListingType,
PostFeatureType,
SortType,
};
use lemmy_db_views::structs::{PostReportView, PostView};
@ -106,9 +107,10 @@ pub struct LockPost {
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct StickyPost {
pub struct FeaturePost {
pub post_id: PostId,
pub stickied: bool,
pub featured: bool,
pub feature_type: PostFeatureType,
pub auth: Sensitive<String>,
}

View file

@ -31,12 +31,12 @@ use lemmy_db_views_moderator::structs::{
ModAddView,
ModBanFromCommunityView,
ModBanView,
ModFeaturePostView,
ModHideCommunityView,
ModLockPostView,
ModRemoveCommentView,
ModRemoveCommunityView,
ModRemovePostView,
ModStickyPostView,
ModTransferCommunityView,
};
use serde::{Deserialize, Serialize};
@ -93,7 +93,7 @@ pub struct GetModlog {
pub struct GetModlogResponse {
pub removed_posts: Vec<ModRemovePostView>,
pub locked_posts: Vec<ModLockPostView>,
pub stickied_posts: Vec<ModStickyPostView>,
pub featured_posts: Vec<ModFeaturePostView>,
pub removed_comments: Vec<ModRemoveCommentView>,
pub removed_communities: Vec<ModRemoveCommunityView>,
pub banned_from_community: Vec<ModBanFromCommunityView>,

View file

@ -38,7 +38,7 @@ pub enum UserOperation {
ListCommentReports,
CreatePostLike,
LockPost,
StickyPost,
FeaturePost,
MarkPostAsRead,
SavePost,
CreatePostReport,

View file

@ -25,7 +25,7 @@ use activitypub_federation::{
use activitystreams_kinds::public;
use lemmy_api_common::{
context::LemmyContext,
post::{CreatePost, EditPost, LockPost, PostResponse, StickyPost},
post::{CreatePost, EditPost, FeaturePost, LockPost, PostResponse},
utils::get_local_user_view_from_jwt,
websocket::{send::send_post_ws_message, UserOperationCrud},
};
@ -101,7 +101,7 @@ impl SendActivity for LockPost {
}
#[async_trait::async_trait(?Send)]
impl SendActivity for StickyPost {
impl SendActivity for FeaturePost {
type Response = PostResponse;
async fn send_activity(
@ -205,9 +205,9 @@ impl ActivityHandler for CreateOrUpdatePage {
// However, when fetching a remote post we generate a new create activity with the current
// locked/stickied value, so this check may fail. So only check if its a local community,
// because then we will definitely receive all create and update activities separately.
let is_stickied_or_locked =
let is_featured_or_locked =
self.object.stickied == Some(true) || self.object.comments_enabled == Some(false);
if community.local && is_stickied_or_locked {
if community.local && is_featured_or_locked {
return Err(LemmyError::from_message(
"New post cannot be stickied or locked",
));

View file

@ -32,7 +32,7 @@ use lemmy_db_schema::{
source::{
community::Community,
local_site::LocalSite,
moderator::{ModLockPost, ModLockPostForm, ModStickyPost, ModStickyPostForm},
moderator::{ModFeaturePost, ModFeaturePostForm, ModLockPost, ModLockPostForm},
person::Person,
post::{Post, PostInsertForm, PostUpdateForm},
},
@ -116,7 +116,7 @@ impl ApubObject for ApubPost {
image: self.thumbnail_url.clone().map(ImageObject::new),
comments_enabled: Some(!self.locked),
sensitive: Some(self.nsfw),
stickied: Some(self.stickied),
stickied: Some(self.featured_community),
language,
published: Some(convert_datetime(self.published)),
updated: self.updated.map(convert_datetime),
@ -208,7 +208,6 @@ impl ApubObject for ApubPost {
updated: page.updated.map(|u| u.naive_local()),
deleted: Some(false),
nsfw: page.sensitive,
stickied: page.stickied,
embed_title,
embed_description,
embed_video_url,
@ -216,6 +215,8 @@ impl ApubObject for ApubPost {
ap_id: Some(page.id.clone().into()),
local: Some(false),
language_id,
featured_community: page.stickied,
featured_local: None,
}
} else {
// if is mod action, only update locked/stickied fields, nothing else
@ -225,7 +226,7 @@ impl ApubObject for ApubPost {
.community_id(community.id)
.ap_id(Some(page.id.clone().into()))
.locked(page.comments_enabled.map(|e| !e))
.stickied(page.stickied)
.featured_community(page.stickied)
.updated(page.updated.map(|u| u.naive_local()))
.build()
};
@ -236,14 +237,15 @@ impl ApubObject for ApubPost {
let post = Post::create(context.pool(), &form).await?;
// write mod log entries for sticky/lock
if Page::is_stickied_changed(&old_post, &page.stickied) {
let form = ModStickyPostForm {
// write mod log entries for feature/lock
if Page::is_featured_changed(&old_post, &page.stickied) {
let form = ModFeaturePostForm {
mod_person_id: creator.id,
post_id: post.id,
stickied: Some(post.stickied),
featured: post.featured_community,
is_featured_community: true,
};
ModStickyPost::create(context.pool(), &form).await?;
ModFeaturePost::create(context.pool(), &form).await?;
}
if Page::is_locked_changed(&old_post, &page.comments_enabled) {
let form = ModLockPostForm {
@ -295,7 +297,7 @@ mod tests {
assert!(post.body.is_some());
assert_eq!(post.body.as_ref().unwrap().len(), 45);
assert!(!post.locked);
assert!(post.stickied);
assert!(post.featured_community);
assert_eq!(request_counter, 0);
Post::delete(context.pool(), post.id).await.unwrap();

View file

@ -137,18 +137,18 @@ impl Page {
.dereference_local(context)
.await;
let stickied_changed = Page::is_stickied_changed(&old_post, &self.stickied);
let featured_changed = Page::is_featured_changed(&old_post, &self.stickied);
let locked_changed = Page::is_locked_changed(&old_post, &self.comments_enabled);
Ok(stickied_changed || locked_changed)
Ok(featured_changed || locked_changed)
}
pub(crate) fn is_stickied_changed<E>(
pub(crate) fn is_featured_changed<E>(
old_post: &Result<ApubPost, E>,
new_stickied: &Option<bool>,
new_featured_community: &Option<bool>,
) -> bool {
if let Some(new_stickied) = new_stickied {
if let Some(new_featured_community) = new_featured_community {
if let Ok(old_post) = old_post {
return new_stickied != &old_post.stickied;
return new_featured_community != &old_post.featured_community;
}
}

View file

@ -68,10 +68,11 @@ pub struct PostAggregates {
pub score: i64,
pub upvotes: i64,
pub downvotes: i64,
pub stickied: bool,
pub published: chrono::NaiveDateTime,
pub newest_comment_time_necro: chrono::NaiveDateTime, // A newest comment time, limited to 2 days, to prevent necrobumping
pub newest_comment_time: chrono::NaiveDateTime,
pub featured_community: bool,
pub featured_local: bool,
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]

View file

@ -16,6 +16,8 @@ use crate::{
ModBanForm,
ModBanFromCommunity,
ModBanFromCommunityForm,
ModFeaturePost,
ModFeaturePostForm,
ModHideCommunity,
ModHideCommunityForm,
ModLockPost,
@ -26,8 +28,6 @@ use crate::{
ModRemoveCommunityForm,
ModRemovePost,
ModRemovePostForm,
ModStickyPost,
ModStickyPostForm,
ModTransferCommunity,
ModTransferCommunityForm,
},
@ -98,29 +98,29 @@ impl Crud for ModLockPost {
}
#[async_trait]
impl Crud for ModStickyPost {
type InsertForm = ModStickyPostForm;
type UpdateForm = ModStickyPostForm;
impl Crud for ModFeaturePost {
type InsertForm = ModFeaturePostForm;
type UpdateForm = ModFeaturePostForm;
type IdType = i32;
async fn read(pool: &DbPool, from_id: i32) -> Result<Self, Error> {
use crate::schema::mod_sticky_post::dsl::mod_sticky_post;
use crate::schema::mod_feature_post::dsl::mod_feature_post;
let conn = &mut get_conn(pool).await?;
mod_sticky_post.find(from_id).first::<Self>(conn).await
mod_feature_post.find(from_id).first::<Self>(conn).await
}
async fn create(pool: &DbPool, form: &ModStickyPostForm) -> Result<Self, Error> {
use crate::schema::mod_sticky_post::dsl::mod_sticky_post;
async fn create(pool: &DbPool, form: &ModFeaturePostForm) -> Result<Self, Error> {
use crate::schema::mod_feature_post::dsl::mod_feature_post;
let conn = &mut get_conn(pool).await?;
insert_into(mod_sticky_post)
insert_into(mod_feature_post)
.values(form)
.get_result::<Self>(conn)
.await
}
async fn update(pool: &DbPool, from_id: i32, form: &ModStickyPostForm) -> Result<Self, Error> {
use crate::schema::mod_sticky_post::dsl::mod_sticky_post;
async fn update(pool: &DbPool, from_id: i32, form: &ModFeaturePostForm) -> Result<Self, Error> {
use crate::schema::mod_feature_post::dsl::mod_feature_post;
let conn = &mut get_conn(pool).await?;
diesel::update(mod_sticky_post.find(from_id))
diesel::update(mod_feature_post.find(from_id))
.set(form)
.get_result::<Self>(conn)
.await
@ -525,6 +525,8 @@ mod tests {
ModBanForm,
ModBanFromCommunity,
ModBanFromCommunityForm,
ModFeaturePost,
ModFeaturePostForm,
ModLockPost,
ModLockPostForm,
ModRemoveComment,
@ -533,8 +535,6 @@ mod tests {
ModRemoveCommunityForm,
ModRemovePost,
ModRemovePostForm,
ModStickyPost,
ModStickyPostForm,
},
person::{Person, PersonInsertForm},
post::{Post, PostInsertForm},
@ -637,25 +637,27 @@ mod tests {
when_: inserted_mod_lock_post.when_,
};
// sticky post
// feature post
let mod_sticky_post_form = ModStickyPostForm {
let mod_feature_post_form = ModFeaturePostForm {
mod_person_id: inserted_mod.id,
post_id: inserted_post.id,
stickied: None,
featured: false,
is_featured_community: true,
};
let inserted_mod_sticky_post = ModStickyPost::create(pool, &mod_sticky_post_form)
let inserted_mod_feature_post = ModFeaturePost::create(pool, &mod_feature_post_form)
.await
.unwrap();
let read_mod_sticky_post = ModStickyPost::read(pool, inserted_mod_sticky_post.id)
let read_mod_feature_post = ModFeaturePost::read(pool, inserted_mod_feature_post.id)
.await
.unwrap();
let expected_mod_sticky_post = ModStickyPost {
id: inserted_mod_sticky_post.id,
let expected_mod_feature_post = ModFeaturePost {
id: inserted_mod_feature_post.id,
post_id: inserted_post.id,
mod_person_id: inserted_mod.id,
stickied: Some(true),
when_: inserted_mod_sticky_post.when_,
featured: false,
is_featured_community: true,
when_: inserted_mod_feature_post.when_,
};
// comment
@ -809,7 +811,7 @@ mod tests {
assert_eq!(expected_mod_remove_post, read_mod_remove_post);
assert_eq!(expected_mod_lock_post, read_mod_lock_post);
assert_eq!(expected_mod_sticky_post, read_mod_sticky_post);
assert_eq!(expected_mod_feature_post, read_mod_feature_post);
assert_eq!(expected_mod_remove_comment, read_mod_remove_comment);
assert_eq!(expected_mod_remove_community, read_mod_remove_community);
assert_eq!(expected_mod_ban_from_community, read_mod_ban_from_community);

View file

@ -6,11 +6,11 @@ use crate::{
community_id,
creator_id,
deleted,
featured_community,
name,
post,
published,
removed,
stickied,
thumbnail_url,
updated,
url,
@ -83,7 +83,7 @@ impl Post {
.filter(deleted.eq(false))
.filter(removed.eq(false))
.then_order_by(published.desc())
.then_order_by(stickied.desc())
.then_order_by(featured_community.desc())
.limit(FETCH_LIMIT_MAX)
.load::<Self>(conn)
.await
@ -381,7 +381,6 @@ mod tests {
published: inserted_post.published,
removed: false,
locked: false,
stickied: false,
nsfw: false,
deleted: false,
updated: None,
@ -392,6 +391,8 @@ mod tests {
ap_id: inserted_post.ap_id.clone(),
local: true,
language_id: Default::default(),
featured_community: false,
featured_local: false,
};
// Post Like

View file

@ -82,7 +82,7 @@ pub enum ModlogActionType {
All,
ModRemovePost,
ModLockPost,
ModStickyPost,
ModFeaturePost,
ModRemoveComment,
ModRemoveCommunity,
ModBanFromCommunity,
@ -96,3 +96,12 @@ pub enum ModlogActionType {
AdminPurgePost,
AdminPurgeComment,
}
#[derive(
EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq,
)]
pub enum PostFeatureType {
#[default]
Local,
Community,
}

View file

@ -273,12 +273,13 @@ table! {
}
table! {
mod_sticky_post (id) {
mod_feature_post (id) {
id -> Int4,
mod_person_id -> Int4,
post_id -> Int4,
stickied -> Nullable<Bool>,
featured -> Bool,
when_ -> Timestamp,
is_featured_community -> Bool,
}
}
@ -371,7 +372,6 @@ table! {
updated -> Nullable<Timestamp>,
deleted -> Bool,
nsfw -> Bool,
stickied -> Bool,
embed_title -> Nullable<Text>,
embed_description -> Nullable<Text>,
embed_video_url -> Nullable<Text>,
@ -379,6 +379,8 @@ table! {
ap_id -> Varchar,
local -> Bool,
language_id -> Int4,
featured_community -> Bool,
featured_local -> Bool,
}
}
@ -400,10 +402,11 @@ table! {
score -> Int8,
upvotes -> Int8,
downvotes -> Int8,
stickied -> Bool,
published -> Timestamp,
newest_comment_time_necro -> Timestamp,
newest_comment_time -> Timestamp,
featured_community -> Bool,
featured_local -> Bool,
}
}
@ -771,8 +774,8 @@ joinable!(mod_remove_community -> community (community_id));
joinable!(mod_remove_community -> person (mod_person_id));
joinable!(mod_remove_post -> person (mod_person_id));
joinable!(mod_remove_post -> post (post_id));
joinable!(mod_sticky_post -> person (mod_person_id));
joinable!(mod_sticky_post -> post (post_id));
joinable!(mod_feature_post -> person (mod_person_id));
joinable!(mod_feature_post -> post (post_id));
joinable!(password_reset_request -> local_user (local_user_id));
joinable!(person_aggregates -> person (person_id));
joinable!(person_ban -> person (person_id));
@ -848,7 +851,7 @@ allow_tables_to_appear_in_same_query!(
mod_remove_comment,
mod_remove_community,
mod_remove_post,
mod_sticky_post,
mod_feature_post,
mod_hide_community,
password_reset_request,
person,

View file

@ -9,12 +9,12 @@ use crate::schema::{
mod_add_community,
mod_ban,
mod_ban_from_community,
mod_feature_post,
mod_hide_community,
mod_lock_post,
mod_remove_comment,
mod_remove_community,
mod_remove_post,
mod_sticky_post,
mod_transfer_community,
};
use serde::{Deserialize, Serialize};
@ -61,21 +61,23 @@ pub struct ModLockPostForm {
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "full", derive(Queryable, Identifiable))]
#[cfg_attr(feature = "full", diesel(table_name = mod_sticky_post))]
pub struct ModStickyPost {
#[cfg_attr(feature = "full", diesel(table_name = mod_feature_post))]
pub struct ModFeaturePost {
pub id: i32,
pub mod_person_id: PersonId,
pub post_id: PostId,
pub stickied: Option<bool>,
pub featured: bool,
pub when_: chrono::NaiveDateTime,
pub is_featured_community: bool,
}
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = mod_sticky_post))]
pub struct ModStickyPostForm {
#[cfg_attr(feature = "full", diesel(table_name = mod_feature_post))]
pub struct ModFeaturePostForm {
pub mod_person_id: PersonId,
pub post_id: PostId,
pub stickied: Option<bool>,
pub featured: bool,
pub is_featured_community: bool,
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]

View file

@ -20,7 +20,6 @@ pub struct Post {
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub nsfw: bool,
pub stickied: bool,
pub embed_title: Option<String>,
pub embed_description: Option<String>,
pub embed_video_url: Option<DbUrl>,
@ -28,6 +27,8 @@ pub struct Post {
pub ap_id: DbUrl,
pub local: bool,
pub language_id: LanguageId,
pub featured_community: bool,
pub featured_local: bool,
}
#[derive(Debug, Clone, TypedBuilder)]
@ -49,7 +50,6 @@ pub struct PostInsertForm {
pub updated: Option<chrono::NaiveDateTime>,
pub published: Option<chrono::NaiveDateTime>,
pub deleted: Option<bool>,
pub stickied: Option<bool>,
pub embed_title: Option<String>,
pub embed_description: Option<String>,
pub embed_video_url: Option<DbUrl>,
@ -57,6 +57,8 @@ pub struct PostInsertForm {
pub ap_id: Option<DbUrl>,
pub local: Option<bool>,
pub language_id: Option<LanguageId>,
pub featured_community: Option<bool>,
pub featured_local: Option<bool>,
}
#[derive(Debug, Clone, TypedBuilder)]
@ -73,7 +75,6 @@ pub struct PostUpdateForm {
pub published: Option<chrono::NaiveDateTime>,
pub updated: Option<Option<chrono::NaiveDateTime>>,
pub deleted: Option<bool>,
pub stickied: Option<bool>,
pub embed_title: Option<Option<String>>,
pub embed_description: Option<Option<String>>,
pub embed_video_url: Option<Option<DbUrl>>,
@ -81,6 +82,8 @@ pub struct PostUpdateForm {
pub ap_id: Option<DbUrl>,
pub local: Option<bool>,
pub language_id: Option<LanguageId>,
pub featured_community: Option<bool>,
pub featured_local: Option<bool>,
}
#[derive(PartialEq, Eq, Debug)]

View file

@ -863,7 +863,6 @@ mod tests {
removed: false,
deleted: false,
locked: false,
stickied: false,
nsfw: false,
embed_title: None,
embed_description: None,
@ -872,6 +871,8 @@ mod tests {
ap_id: data.inserted_post.ap_id.clone(),
local: true,
language_id: Default::default(),
featured_community: false,
featured_local: false,
},
community: CommunitySafe {
id: data.inserted_community.id,

View file

@ -469,10 +469,11 @@ mod tests {
score: 0,
upvotes: 0,
downvotes: 0,
stickied: false,
published: agg.published,
newest_comment_time_necro: inserted_post.published,
newest_comment_time: inserted_post.published,
featured_community: false,
featured_local: false,
},
resolver: None,
};

View file

@ -324,17 +324,16 @@ impl<'a> PostQuery<'a> {
}
}
}
if let Some(community_id) = self.community_id {
if self.community_id.is_none() && self.community_actor_id.is_none() {
query = query.then_order_by(post_aggregates::featured_local.desc());
} else if let Some(community_id) = self.community_id {
query = query
.filter(post::community_id.eq(community_id))
.then_order_by(post_aggregates::stickied.desc());
}
if let Some(community_actor_id) = self.community_actor_id {
.then_order_by(post_aggregates::featured_community.desc());
} else if let Some(community_actor_id) = self.community_actor_id {
query = query
.filter(community::actor_id.eq(community_actor_id))
.then_order_by(post_aggregates::stickied.desc());
.then_order_by(post_aggregates::featured_community.desc());
}
if let Some(url_search) = self.url_search {
@ -860,7 +859,6 @@ mod tests {
removed: false,
deleted: false,
locked: false,
stickied: false,
nsfw: false,
embed_title: None,
embed_description: None,
@ -869,6 +867,8 @@ mod tests {
ap_id: inserted_post.ap_id.clone(),
local: true,
language_id: LanguageId(47),
featured_community: false,
featured_local: false,
},
my_vote: None,
unread_comments: 0,
@ -919,10 +919,11 @@ mod tests {
score: 0,
upvotes: 0,
downvotes: 0,
stickied: false,
published: agg.published,
newest_comment_time_necro: inserted_post.published,
newest_comment_time: inserted_post.published,
featured_community: false,
featured_local: false,
},
subscribed: SubscribedType::NotSubscribed,
read: false,

View file

@ -15,6 +15,8 @@ pub mod mod_ban_from_community_view;
#[cfg(feature = "full")]
pub mod mod_ban_view;
#[cfg(feature = "full")]
pub mod mod_feature_post_view;
#[cfg(feature = "full")]
pub mod mod_hide_community_view;
#[cfg(feature = "full")]
pub mod mod_lock_post_view;
@ -25,7 +27,5 @@ pub mod mod_remove_community_view;
#[cfg(feature = "full")]
pub mod mod_remove_post_view;
#[cfg(feature = "full")]
pub mod mod_sticky_post_view;
#[cfg(feature = "full")]
pub mod mod_transfer_community_view;
pub mod structs;

View file

@ -1,4 +1,4 @@
use crate::structs::{ModStickyPostView, ModlogListParams};
use crate::structs::{ModFeaturePostView, ModlogListParams};
use diesel::{
result::Error,
BoolExpressionMethods,
@ -11,10 +11,10 @@ use diesel::{
use diesel_async::RunQueryDsl;
use lemmy_db_schema::{
newtypes::PersonId,
schema::{community, mod_sticky_post, person, post},
schema::{community, mod_feature_post, person, post},
source::{
community::{Community, CommunitySafe},
moderator::ModStickyPost,
moderator::ModFeaturePost,
person::{Person, PersonSafe},
post::Post,
},
@ -22,9 +22,9 @@ use lemmy_db_schema::{
utils::{get_conn, limit_and_offset, DbPool},
};
type ModStickyPostViewTuple = (ModStickyPost, Option<PersonSafe>, Post, CommunitySafe);
type ModFeaturePostViewTuple = (ModFeaturePost, Option<PersonSafe>, Post, CommunitySafe);
impl ModStickyPostView {
impl ModFeaturePostView {
pub async fn list(pool: &DbPool, params: ModlogListParams) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?;
let person_alias_1 = diesel::alias!(person as person1);
@ -32,16 +32,16 @@ impl ModStickyPostView {
let show_mod_names = !params.hide_modlog_names;
let show_mod_names_expr = show_mod_names.as_sql::<diesel::sql_types::Bool>();
let admin_names_join = mod_sticky_post::mod_person_id
let admin_names_join = mod_feature_post::mod_person_id
.eq(person::id)
.and(show_mod_names_expr.or(person::id.eq(admin_person_id_join)));
let mut query = mod_sticky_post::table
let mut query = mod_feature_post::table
.left_join(person::table.on(admin_names_join))
.inner_join(post::table)
.inner_join(person_alias_1.on(post::creator_id.eq(person_alias_1.field(person::id))))
.inner_join(community::table.on(post::community_id.eq(community::id)))
.select((
mod_sticky_post::all_columns,
mod_feature_post::all_columns,
Person::safe_columns_tuple().nullable(),
post::all_columns,
Community::safe_columns_tuple(),
@ -53,7 +53,7 @@ impl ModStickyPostView {
};
if let Some(mod_person_id) = params.mod_person_id {
query = query.filter(mod_sticky_post::mod_person_id.eq(mod_person_id));
query = query.filter(mod_feature_post::mod_person_id.eq(mod_person_id));
};
if let Some(other_person_id) = params.other_person_id {
@ -65,8 +65,8 @@ impl ModStickyPostView {
let res = query
.limit(limit)
.offset(offset)
.order_by(mod_sticky_post::when_.desc())
.load::<ModStickyPostViewTuple>(conn)
.order_by(mod_feature_post::when_.desc())
.load::<ModFeaturePostViewTuple>(conn)
.await?;
let results = Self::from_tuple_to_vec(res);
@ -74,13 +74,13 @@ impl ModStickyPostView {
}
}
impl ViewToVec for ModStickyPostView {
type DbTuple = ModStickyPostViewTuple;
impl ViewToVec for ModFeaturePostView {
type DbTuple = ModFeaturePostViewTuple;
fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
items
.into_iter()
.map(|a| Self {
mod_sticky_post: a.0,
mod_feature_post: a.0,
moderator: a.1,
post: a.2,
community: a.3,

View file

@ -12,12 +12,12 @@ use lemmy_db_schema::{
ModAddCommunity,
ModBan,
ModBanFromCommunity,
ModFeaturePost,
ModHideCommunity,
ModLockPost,
ModRemoveComment,
ModRemoveCommunity,
ModRemovePost,
ModStickyPost,
ModTransferCommunity,
},
person::PersonSafe,
@ -97,8 +97,8 @@ pub struct ModRemovePostView {
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ModStickyPostView {
pub mod_sticky_post: ModStickyPost,
pub struct ModFeaturePostView {
pub mod_feature_post: ModFeaturePost,
pub moderator: Option<PersonSafe>,
pub post: Post,
pub community: CommunitySafe,

View file

@ -0,0 +1,47 @@
DROP TRIGGER IF EXISTS post_aggregates_featured_local ON post;
DROP TRIGGER IF EXISTS post_aggregates_featured_community ON post;
drop function post_aggregates_featured_community;
drop function post_aggregates_featured_local;
alter table post ADD stickied boolean NOT NULL DEFAULT false;
Update post
set stickied = featured_community;
alter table post DROP COLUMN featured_community;
alter table post DROP COLUMN featured_local;
alter table post_aggregates ADD stickied boolean NOT NULL DEFAULT false;
Update post_aggregates
set stickied = featured_community;
alter table post_aggregates DROP COLUMN featured_community;
alter table post_aggregates DROP COLUMN featured_local;
alter table mod_feature_post
rename column featured TO stickied;
alter table mod_feature_post
DROP COLUMN is_featured_community;
alter table mod_feature_post
alter column stickied DROP NOT NULL;
alter table mod_feature_post
Rename To mod_sticky_post;
create function post_aggregates_stickied()
returns trigger language plpgsql
as $$
begin
update post_aggregates pa
set stickied = NEW.stickied
where pa.post_id = NEW.id;
return null;
end $$;
create trigger post_aggregates_stickied
after update on post
for each row
when (OLD.stickied is distinct from NEW.stickied)
execute procedure post_aggregates_stickied();

View file

@ -0,0 +1,63 @@
DROP TRIGGER IF EXISTS post_aggregates_stickied ON post;
drop function
post_aggregates_stickied;
alter table post ADD featured_community boolean NOT NULL DEFAULT false;
alter table post ADD featured_local boolean NOT NULL DEFAULT false;
update post
set featured_community = stickied;
alter table post DROP COLUMN stickied;
alter table post_aggregates ADD featured_community boolean NOT NULL DEFAULT false;
alter table post_aggregates ADD featured_local boolean NOT NULL DEFAULT false;
update post_aggregates
set featured_community = stickied;
alter table post_aggregates DROP COLUMN stickied;
alter table mod_sticky_post
rename column stickied TO featured;
alter table mod_sticky_post
alter column featured SET NOT NULL;
alter table mod_sticky_post
ADD is_featured_community boolean NOT NULL DEFAULT true;
alter table mod_sticky_post
Rename To mod_feature_post;
create function post_aggregates_featured_community()
returns trigger language plpgsql
as $$
begin
update post_aggregates pa
set featured_community = NEW.featured_community
where pa.post_id = NEW.id;
return null;
end $$;
create function post_aggregates_featured_local()
returns trigger language plpgsql
as $$
begin
update post_aggregates pa
set featured_local = NEW.featured_local
where pa.post_id = NEW.id;
return null;
end $$;
CREATE TRIGGER post_aggregates_featured_community
AFTER UPDATE
ON public.post
FOR EACH ROW
WHEN (old.featured_community IS DISTINCT FROM new.featured_community)
EXECUTE FUNCTION public.post_aggregates_featured_community();
CREATE TRIGGER post_aggregates_featured_local
AFTER UPDATE
ON public.post
FOR EACH ROW
WHEN (old.featured_local IS DISTINCT FROM new.featured_local)
EXECUTE FUNCTION public.post_aggregates_featured_local();

View file

@ -59,6 +59,7 @@ use lemmy_api_common::{
CreatePostReport,
DeletePost,
EditPost,
FeaturePost,
GetPost,
GetPosts,
GetSiteMetadata,
@ -68,7 +69,6 @@ use lemmy_api_common::{
RemovePost,
ResolvePostReport,
SavePost,
StickyPost,
},
private_message::{
CreatePrivateMessage,
@ -183,7 +183,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
web::post().to(route_post::<MarkPostAsRead>),
)
.route("/lock", web::post().to(route_post::<LockPost>))
.route("/sticky", web::post().to(route_post::<StickyPost>))
.route("/feature", web::post().to(route_post::<FeaturePost>))
.route("/list", web::get().to(route_get_apub::<GetPosts>))
.route("/like", web::post().to(route_post::<CreatePostLike>))
.route("/save", web::put().to(route_post::<SavePost>))

View file

@ -60,6 +60,7 @@ use lemmy_api_common::{
CreatePostReport,
DeletePost,
EditPost,
FeaturePost,
GetPost,
GetPosts,
GetSiteMetadata,
@ -69,7 +70,6 @@ use lemmy_api_common::{
RemovePost,
ResolvePostReport,
SavePost,
StickyPost,
},
private_message::{
CreatePrivateMessage,
@ -560,7 +560,9 @@ pub async fn match_websocket_operation(
// Post ops
UserOperation::LockPost => do_websocket_operation::<LockPost>(context, id, op, data).await,
UserOperation::StickyPost => do_websocket_operation::<StickyPost>(context, id, op, data).await,
UserOperation::FeaturePost => {
do_websocket_operation::<FeaturePost>(context, id, op, data).await
}
UserOperation::CreatePostLike => {
do_websocket_operation::<CreatePostLike>(context, id, op, data).await
}