Compare commits

...

2 commits

Author SHA1 Message Date
870289a5bb add: stacks mode and sort sql 2025-05-09 15:56:40 -04:00
d174b44f57 add: stacks mode and sort 2025-05-09 15:56:19 -04:00
10 changed files with 277 additions and 18 deletions

View file

@ -16,13 +16,13 @@
<div class="w-full flex flex-col gap-2" data-tab="general"> <div class="w-full flex flex-col gap-2" data-tab="general">
<div id="manage_fields" class="card tertiary flex flex-col gap-2"> <div id="manage_fields" class="card tertiary flex flex-col gap-2">
<div class="card-nest" ui_ident="privacu"> <div class="card-nest" ui_ident="privacy">
<div class="card small"> <div class="card small">
<b>Privacy</b> <b>Privacy</b>
</div> </div>
<div class="card"> <div class="card">
<select onchange="save_privacy(event, 'read')"> <select onchange="save_privacy(event)">
<option <option
value="Private" value="Private"
selected="{% if stack.privacy == 'Private' %}true{% else %}false{% endif %}" selected="{% if stack.privacy == 'Private' %}true{% else %}false{% endif %}"
@ -39,6 +39,52 @@
</div> </div>
</div> </div>
<div class="card-nest" ui_ident="mode">
<div class="card small">
<b>Mode</b>
</div>
<div class="card">
<select onchange="save_mode(event)">
<option
value="Include"
selected="{% if stack.mode == 'Include' %}true{% else %}false{% endif %}"
>
Include
</option>
<option
value="Exclude"
selected="{% if stack.mode == 'Exclude' %}true{% else %}false{% endif %}"
>
Exclude
</option>
</select>
</div>
</div>
<div class="card-nest" ui_ident="sort">
<div class="card small">
<b>Sort</b>
</div>
<div class="card">
<select onchange="save_sort(event)">
<option
value="Created"
selected="{% if stack.sort == 'Created' %}true{% else %}false{% endif %}"
>
Created
</option>
<option
value="Likes"
selected="{% if stack.sort == 'Likes' %}true{% else %}false{% endif %}"
>
Likes
</option>
</select>
</div>
</div>
<div class="card-nest" ui_ident="change_name"> <div class="card-nest" ui_ident="change_name">
<div class="card small"> <div class="card small">
<b>{{ text "stacks:label.change_name" }}</b> <b>{{ text "stacks:label.change_name" }}</b>
@ -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) => { globalThis.change_name = async (e) => {
e.preventDefault(); e.preventDefault();

View file

@ -23,7 +23,7 @@ use tetratto_core::model::{
communities_permissions::CommunityPermission, communities_permissions::CommunityPermission,
permissions::FinePermission, permissions::FinePermission,
reactions::AssetType, reactions::AssetType,
stacks::StackPrivacy, stacks::{StackMode, StackPrivacy, StackSort},
}; };
pub fn routes() -> Router { pub fn routes() -> Router {
@ -326,6 +326,8 @@ pub fn routes() -> Router {
.route("/stacks", post(stacks::create_request)) .route("/stacks", post(stacks::create_request))
.route("/stacks/{id}/name", post(stacks::update_name_request)) .route("/stacks/{id}/name", post(stacks::update_name_request))
.route("/stacks/{id}/privacy", post(stacks::update_privacy_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", post(stacks::add_user_request))
.route("/stacks/{id}/users", delete(stacks::remove_user_request)) .route("/stacks/{id}/users", delete(stacks::remove_user_request))
.route("/stacks/{id}", delete(stacks::delete_request)) .route("/stacks/{id}", delete(stacks::delete_request))
@ -531,6 +533,16 @@ pub struct UpdateStackPrivacy {
pub privacy: StackPrivacy, pub privacy: StackPrivacy,
} }
#[derive(Deserialize)]
pub struct UpdateStackMode {
pub mode: StackMode,
}
#[derive(Deserialize)]
pub struct UpdateStackSort {
pub sort: StackSort,
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct AddOrRemoveStackUser { pub struct AddOrRemoveStackUser {
pub username: String, pub username: String,

View file

@ -2,7 +2,10 @@ use crate::{State, get_user_from_token};
use axum::{Extension, Json, extract::Path, response::IntoResponse}; use axum::{Extension, Json, extract::Path, response::IntoResponse};
use axum_extra::extract::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{stacks::UserStack, ApiReturn, Error}; 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( pub async fn create_request(
jar: CookieJar, jar: CookieJar,
@ -72,6 +75,50 @@ pub async fn update_privacy_request(
} }
} }
pub async fn update_mode_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateStackMode>,
) -> 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<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateStackSort>,
) -> 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( pub async fn add_user_request(
jar: CookieJar, jar: CookieJar,
Extension(data): Extension<State>, Extension(data): Extension<State>,

View file

@ -66,15 +66,12 @@ pub async fn posts_request(
} }
let ignore_users = data.0.get_userblocks_receivers(user.id).await; 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 { let list = match data
Ok(l) => match data .0
.0 .get_stack_posts(user.id, stack.id, 12, req.page, &ignore_users)
.fill_posts_with_community(l, user.id, &ignore_users) .await
.await {
{ Ok(l) => l,
Ok(l) => l,
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
},
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
}; };

View file

@ -4,5 +4,7 @@ CREATE TABLE IF NOT EXISTS stacks (
owner BIGINT NOT NULL, owner BIGINT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
users TEXT NOT NULL, users TEXT NOT NULL,
privacy TEXT NOT NULL privacy TEXT NOT NULL,
mode TEXT NOT NULL,
sort TEXT NOT NULL
) )

View file

@ -157,6 +157,15 @@ impl DataManager {
return Err(Error::NotAllowed); 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 // create notif
self.create_notification(Notification::new( self.create_notification(Notification::new(
"You've been mentioned in a message!".to_string(), "You've been mentioned in a message!".to_string(),

View file

@ -6,6 +6,7 @@ use crate::model::auth::Notification;
use crate::model::communities::Question; use crate::model::communities::Question;
use crate::model::communities_permissions::CommunityPermission; use crate::model::communities_permissions::CommunityPermission;
use crate::model::moderation::AuditLogEntry; use crate::model::moderation::AuditLogEntry;
use crate::model::stacks::StackSort;
use crate::model::{ use crate::model::{
Error, Result, Error, Result,
auth::User, auth::User,
@ -743,6 +744,7 @@ impl DataManager {
id: usize, id: usize,
batch: usize, batch: usize,
page: usize, page: usize,
sort: StackSort,
) -> Result<Vec<Post>> { ) -> Result<Vec<Post>> {
let users = self.get_stack_by_id(id).await?.users; let users = self.get_stack_by_id(id).await?.users;
let mut users = users.iter(); let mut users = users.iter();
@ -767,8 +769,13 @@ impl DataManager {
let res = query_rows!( let res = query_rows!(
&conn, &conn,
&format!( &format!(
"SELECT * FROM posts WHERE (owner = {} {query_string}) AND replying_to = 0 ORDER BY created DESC LIMIT $1 OFFSET $2", "SELECT * FROM posts WHERE (owner = {} {query_string}) AND replying_to = 0 ORDER BY {} DESC LIMIT $1 OFFSET $2",
first first,
if sort == StackSort::Created {
"created"
} else {
"likes"
}
), ),
&[&(batch as i64), &((page * batch) as i64)], &[&(batch as i64), &((page * batch) as i64)],
|x| { Self::get_post_from_row(x) } |x| { Self::get_post_from_row(x) }

View file

@ -1,5 +1,7 @@
use super::*; use super::*;
use crate::cache::Cache; use crate::cache::Cache;
use crate::model::communities::{Community, Post, Question};
use crate::model::stacks::{StackMode, StackSort};
use crate::model::{ use crate::model::{
Error, Result, Error, Result,
auth::User, auth::User,
@ -27,11 +29,66 @@ impl DataManager {
name: get!(x->3(String)), name: get!(x->3(String)),
users: serde_json::from_str(&get!(x->4(String))).unwrap(), users: serde_json::from_str(&get!(x->4(String))).unwrap(),
privacy: serde_json::from_str(&get!(x->5(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:{}"); 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<usize>,
) -> 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. /// Get all stacks by user.
/// ///
/// # Arguments /// # Arguments
@ -90,7 +147,7 @@ impl DataManager {
let res = execute!( let res = execute!(
&conn, &conn,
"INSERT INTO stacks VALUES ($1, $2, $3, $4, $5, $6)", "INSERT INTO stacks VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
params![ params![
&(data.id as i64), &(data.id as i64),
&(data.created as i64), &(data.created as i64),
@ -98,6 +155,8 @@ impl DataManager {
&data.name, &data.name,
&serde_json::to_string(&data.users).unwrap(), &serde_json::to_string(&data.users).unwrap(),
&serde_json::to_string(&data.privacy).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_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<usize>)@get_stack_by_id:MANAGE_STACKS -> "UPDATE stacks SET users = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.stack:{}"); auto_method!(update_stack_users(Vec<usize>)@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:{}");
} }

View file

@ -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)] #[derive(Serialize, Deserialize)]
pub struct UserStack { pub struct UserStack {
pub id: usize, pub id: usize,
@ -23,6 +51,8 @@ pub struct UserStack {
pub name: String, pub name: String,
pub users: Vec<usize>, pub users: Vec<usize>,
pub privacy: StackPrivacy, pub privacy: StackPrivacy,
pub mode: StackMode,
pub sort: StackSort,
} }
impl UserStack { impl UserStack {
@ -35,6 +65,8 @@ impl UserStack {
name, name,
users, users,
privacy: StackPrivacy::default(), privacy: StackPrivacy::default(),
mode: StackMode::default(),
sort: StackSort::default(),
} }
} }
} }

View file

@ -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"';