From d174b44f575db48b6a5fc27c9af7037d3c9b286e Mon Sep 17 00:00:00 2001 From: trisua Date: Fri, 9 May 2025 15:56:19 -0400 Subject: [PATCH 1/2] add: stacks mode and sort --- crates/app/src/public/html/stacks/manage.html | 90 ++++++++++++++++++- crates/app/src/routes/api/v1/mod.rs | 14 ++- crates/app/src/routes/api/v1/stacks.rs | 49 +++++++++- crates/app/src/routes/pages/stacks.rs | 15 ++-- .../database/drivers/sql/create_stacks.sql | 4 +- crates/core/src/database/messages.rs | 9 ++ crates/core/src/database/posts.rs | 11 ++- crates/core/src/database/stacks.rs | 66 +++++++++++++- crates/core/src/model/stacks.rs | 32 +++++++ 9 files changed, 272 insertions(+), 18 deletions(-) diff --git a/crates/app/src/public/html/stacks/manage.html b/crates/app/src/public/html/stacks/manage.html index e7093b9..5df7ff2 100644 --- a/crates/app/src/public/html/stacks/manage.html +++ b/crates/app/src/public/html/stacks/manage.html @@ -16,13 +16,13 @@
-
+
Privacy
-
+
+
+ Mode +
+ +
+ +
+
+ +
+
+ Sort +
+ +
+ +
+
+
{{ text "stacks:label.change_name" }} @@ -186,6 +232,46 @@ }); }; + globalThis.save_mode = (event, mode) => { + const selected = event.target.selectedOptions[0]; + fetch(`/api/v1/stacks/{{ stack.id }}/mode`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + mode: selected.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger("atto::toast", [ + res.ok ? "success" : "error", + res.message, + ]); + }); + }; + + globalThis.save_sort = (event, mode) => { + const selected = event.target.selectedOptions[0]; + fetch(`/api/v1/stacks/{{ stack.id }}/sort`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + sort: selected.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger("atto::toast", [ + res.ok ? "success" : "error", + res.message, + ]); + }); + }; + globalThis.change_name = async (e) => { e.preventDefault(); diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index b8d2f71..743c8f5 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -23,7 +23,7 @@ use tetratto_core::model::{ communities_permissions::CommunityPermission, permissions::FinePermission, reactions::AssetType, - stacks::StackPrivacy, + stacks::{StackMode, StackPrivacy, StackSort}, }; pub fn routes() -> Router { @@ -326,6 +326,8 @@ pub fn routes() -> Router { .route("/stacks", post(stacks::create_request)) .route("/stacks/{id}/name", post(stacks::update_name_request)) .route("/stacks/{id}/privacy", post(stacks::update_privacy_request)) + .route("/stacks/{id}/mode", post(stacks::update_mode_request)) + .route("/stacks/{id}/sort", post(stacks::update_sort_request)) .route("/stacks/{id}/users", post(stacks::add_user_request)) .route("/stacks/{id}/users", delete(stacks::remove_user_request)) .route("/stacks/{id}", delete(stacks::delete_request)) @@ -531,6 +533,16 @@ pub struct UpdateStackPrivacy { pub privacy: StackPrivacy, } +#[derive(Deserialize)] +pub struct UpdateStackMode { + pub mode: StackMode, +} + +#[derive(Deserialize)] +pub struct UpdateStackSort { + pub sort: StackSort, +} + #[derive(Deserialize)] pub struct AddOrRemoveStackUser { pub username: String, diff --git a/crates/app/src/routes/api/v1/stacks.rs b/crates/app/src/routes/api/v1/stacks.rs index b3437e0..a33df1e 100644 --- a/crates/app/src/routes/api/v1/stacks.rs +++ b/crates/app/src/routes/api/v1/stacks.rs @@ -2,7 +2,10 @@ use crate::{State, get_user_from_token}; use axum::{Extension, Json, extract::Path, response::IntoResponse}; use axum_extra::extract::CookieJar; use tetratto_core::model::{stacks::UserStack, ApiReturn, Error}; -use super::{AddOrRemoveStackUser, CreateStack, UpdateStackName, UpdateStackPrivacy}; +use super::{ + AddOrRemoveStackUser, CreateStack, UpdateStackMode, UpdateStackName, UpdateStackPrivacy, + UpdateStackSort, +}; pub async fn create_request( jar: CookieJar, @@ -72,6 +75,50 @@ pub async fn update_privacy_request( } } +pub async fn update_mode_request( + jar: CookieJar, + Extension(data): Extension, + Path(id): Path, + Json(req): Json, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.update_stack_mode(id, user, req.mode).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Stack updated".to_string(), + payload: (), + }), + Err(e) => Json(e.into()), + } +} + +pub async fn update_sort_request( + jar: CookieJar, + Extension(data): Extension, + Path(id): Path, + Json(req): Json, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.update_stack_sort(id, user, req.sort).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Stack updated".to_string(), + payload: (), + }), + Err(e) => Json(e.into()), + } +} + pub async fn add_user_request( jar: CookieJar, Extension(data): Extension, diff --git a/crates/app/src/routes/pages/stacks.rs b/crates/app/src/routes/pages/stacks.rs index 75eb39d..bb19b00 100644 --- a/crates/app/src/routes/pages/stacks.rs +++ b/crates/app/src/routes/pages/stacks.rs @@ -66,15 +66,12 @@ pub async fn posts_request( } let ignore_users = data.0.get_userblocks_receivers(user.id).await; - let list = match data.0.get_posts_from_stack(stack.id, 12, req.page).await { - Ok(l) => match data - .0 - .fill_posts_with_community(l, user.id, &ignore_users) - .await - { - Ok(l) => l, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - }, + let list = match data + .0 + .get_stack_posts(user.id, stack.id, 12, req.page, &ignore_users) + .await + { + Ok(l) => l, Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), }; diff --git a/crates/core/src/database/drivers/sql/create_stacks.sql b/crates/core/src/database/drivers/sql/create_stacks.sql index 6ec6e28..e9d0def 100644 --- a/crates/core/src/database/drivers/sql/create_stacks.sql +++ b/crates/core/src/database/drivers/sql/create_stacks.sql @@ -4,5 +4,7 @@ CREATE TABLE IF NOT EXISTS stacks ( owner BIGINT NOT NULL, name TEXT NOT NULL, users TEXT NOT NULL, - privacy TEXT NOT NULL + privacy TEXT NOT NULL, + mode TEXT NOT NULL, + sort TEXT NOT NULL ) diff --git a/crates/core/src/database/messages.rs b/crates/core/src/database/messages.rs index e1b013d..8d34b1e 100644 --- a/crates/core/src/database/messages.rs +++ b/crates/core/src/database/messages.rs @@ -157,6 +157,15 @@ impl DataManager { return Err(Error::NotAllowed); } + // check if the user can read the channel + let membership = self + .get_membership_by_owner_community(user.id, channel.community) + .await?; + + if !channel.check_read(user.id, Some(membership.role)) { + continue; + } + // create notif self.create_notification(Notification::new( "You've been mentioned in a message!".to_string(), diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index 0c2dc00..8cd8c23 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -6,6 +6,7 @@ use crate::model::auth::Notification; use crate::model::communities::Question; use crate::model::communities_permissions::CommunityPermission; use crate::model::moderation::AuditLogEntry; +use crate::model::stacks::StackSort; use crate::model::{ Error, Result, auth::User, @@ -743,6 +744,7 @@ impl DataManager { id: usize, batch: usize, page: usize, + sort: StackSort, ) -> Result> { let users = self.get_stack_by_id(id).await?.users; let mut users = users.iter(); @@ -767,8 +769,13 @@ impl DataManager { let res = query_rows!( &conn, &format!( - "SELECT * FROM posts WHERE (owner = {} {query_string}) AND replying_to = 0 ORDER BY created DESC LIMIT $1 OFFSET $2", - first + "SELECT * FROM posts WHERE (owner = {} {query_string}) AND replying_to = 0 ORDER BY {} DESC LIMIT $1 OFFSET $2", + first, + if sort == StackSort::Created { + "created" + } else { + "likes" + } ), &[&(batch as i64), &((page * batch) as i64)], |x| { Self::get_post_from_row(x) } diff --git a/crates/core/src/database/stacks.rs b/crates/core/src/database/stacks.rs index c69b35d..9bb32d0 100644 --- a/crates/core/src/database/stacks.rs +++ b/crates/core/src/database/stacks.rs @@ -1,5 +1,7 @@ use super::*; use crate::cache::Cache; +use crate::model::communities::{Community, Post, Question}; +use crate::model::stacks::{StackMode, StackSort}; use crate::model::{ Error, Result, auth::User, @@ -27,11 +29,66 @@ impl DataManager { name: get!(x->3(String)), users: serde_json::from_str(&get!(x->4(String))).unwrap(), privacy: serde_json::from_str(&get!(x->5(String))).unwrap(), + mode: serde_json::from_str(&get!(x->6(String))).unwrap(), + sort: serde_json::from_str(&get!(x->7(String))).unwrap(), } } auto_method!(get_stack_by_id(usize as i64)@get_stack_from_row -> "SELECT * FROM stacks WHERE id = $1" --name="stack" --returns=UserStack --cache-key-tmpl="atto.stack:{}"); + pub async fn get_stack_posts( + &self, + as_user_id: usize, + id: usize, + batch: usize, + page: usize, + ignore_users: &Vec, + ) -> Result< + Vec<( + Post, + User, + Community, + Option<(User, Post)>, + Option<(Question, User)>, + )>, + > { + let stack = self.get_stack_by_id(id).await?; + + Ok(match stack.mode { + StackMode::Include => { + self.fill_posts_with_community( + self.get_posts_from_stack(id, batch, page, stack.sort) + .await?, + as_user_id, + ignore_users, + ) + .await? + } + StackMode::Exclude => { + let ignore_users = [ignore_users.to_owned(), stack.users].concat(); + + match stack.sort { + StackSort::Created => { + self.fill_posts_with_community( + self.get_latest_posts(batch, page).await?, + as_user_id, + &ignore_users, + ) + .await? + } + StackSort::Likes => { + self.fill_posts_with_community( + self.get_popular_posts(batch, page, 604_800_000).await?, + as_user_id, + &ignore_users, + ) + .await? + } + } + } + }) + } + /// Get all stacks by user. /// /// # Arguments @@ -90,7 +147,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO stacks VALUES ($1, $2, $3, $4, $5, $6)", + "INSERT INTO stacks VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", params![ &(data.id as i64), &(data.created as i64), @@ -98,6 +155,8 @@ impl DataManager { &data.name, &serde_json::to_string(&data.users).unwrap(), &serde_json::to_string(&data.privacy).unwrap(), + &serde_json::to_string(&data.mode).unwrap(), + &serde_json::to_string(&data.sort).unwrap(), ] ); @@ -135,6 +194,9 @@ impl DataManager { } auto_method!(update_stack_name(&str)@get_stack_by_id:MANAGE_STACKS -> "UPDATE stacks SET name = $1 WHERE id = $2" --cache-key-tmpl="atto.stack:{}"); - auto_method!(update_stack_privacy(StackPrivacy)@get_stack_by_id:MANAGE_STACKS -> "UPDATE stacks SET privacy = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.stack:{}"); auto_method!(update_stack_users(Vec)@get_stack_by_id:MANAGE_STACKS -> "UPDATE stacks SET users = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.stack:{}"); + + auto_method!(update_stack_privacy(StackPrivacy)@get_stack_by_id:MANAGE_STACKS -> "UPDATE stacks SET privacy = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.stack:{}"); + auto_method!(update_stack_mode(StackMode)@get_stack_by_id:MANAGE_STACKS -> "UPDATE stacks SET mode = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.stack:{}"); + auto_method!(update_stack_sort(StackSort)@get_stack_by_id:MANAGE_STACKS -> "UPDATE stacks SET sort = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.stack:{}"); } diff --git a/crates/core/src/model/stacks.rs b/crates/core/src/model/stacks.rs index afe7e3c..88dd855 100644 --- a/crates/core/src/model/stacks.rs +++ b/crates/core/src/model/stacks.rs @@ -15,6 +15,34 @@ impl Default for StackPrivacy { } } +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum StackMode { + /// `users` vec contains ID of users to INCLUDE into the timeline; + /// every other user is excluded + Include, + /// `users` vec contains ID of users to EXCLUDE from the timeline; + /// every other user is included + Exclude, +} + +impl Default for StackMode { + fn default() -> Self { + Self::Include + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum StackSort { + Created, + Likes, +} + +impl Default for StackSort { + fn default() -> Self { + Self::Created + } +} + #[derive(Serialize, Deserialize)] pub struct UserStack { pub id: usize, @@ -23,6 +51,8 @@ pub struct UserStack { pub name: String, pub users: Vec, pub privacy: StackPrivacy, + pub mode: StackMode, + pub sort: StackSort, } impl UserStack { @@ -35,6 +65,8 @@ impl UserStack { name, users, privacy: StackPrivacy::default(), + mode: StackMode::default(), + sort: StackSort::default(), } } } From 870289a5bb981cac85cae182c2884db9da0fef33 Mon Sep 17 00:00:00 2001 From: trisua Date: Fri, 9 May 2025 15:56:40 -0400 Subject: [PATCH 2/2] add: stacks mode and sort sql --- sql_changes/stacks_mode_sort.sql | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 sql_changes/stacks_mode_sort.sql diff --git a/sql_changes/stacks_mode_sort.sql b/sql_changes/stacks_mode_sort.sql new file mode 100644 index 0000000..35d7aa6 --- /dev/null +++ b/sql_changes/stacks_mode_sort.sql @@ -0,0 +1,5 @@ +ALTER TABLE stacks +ADD COLUMN mode TEXT NOT NULL DEFAULT '"Include"'; + +ALTER TABLE stacks +ADD COLUMN sort TEXT NOT NULL DEFAULT '"Created"';