feat: add visibility feature to thoughts, including new enum, database migration, and update related endpoints and tests

This commit is contained in:
2025-09-06 17:42:50 +02:00
parent 0abd275946
commit 82c6de8da8
12 changed files with 307 additions and 17 deletions

View File

@@ -76,3 +76,16 @@ pub async fn get_follower_ids(db: &DbConn, user_id: Uuid) -> Result<Vec<Uuid>, D
.await?;
Ok(followers.into_iter().map(|f| f.follower_id).collect())
}
pub async fn get_friend_ids(db: &DbConn, user_id: Uuid) -> Result<Vec<Uuid>, DbErr> {
let following = get_following_ids(db, user_id).await?;
let followers = get_follower_ids(db, user_id).await?;
let following_set: std::collections::HashSet<Uuid> = following.into_iter().collect();
let followers_set: std::collections::HashSet<Uuid> = followers.into_iter().collect();
Ok(following_set
.intersection(&followers_set)
.cloned()
.collect())
}

View File

@@ -1,6 +1,7 @@
use sea_orm::{
prelude::Uuid, ActiveModelTrait, ColumnTrait, DbConn, DbErr, EntityTrait, JoinType,
QueryFilter, QueryOrder, QuerySelect, RelationTrait, Set, TransactionTrait,
prelude::Uuid, sea_query::SimpleExpr, ActiveModelTrait, ColumnTrait, Condition, DbConn, DbErr,
EntityTrait, JoinType, QueryFilter, QueryOrder, QuerySelect, RelationTrait, Set,
TransactionTrait,
};
use models::{
@@ -11,7 +12,10 @@ use models::{
use crate::{
error::UserError,
persistence::tag::{find_or_create_tags, link_tags_to_thought, parse_hashtags},
persistence::{
follow,
tag::{find_or_create_tags, link_tags_to_thought, parse_hashtags},
},
};
pub async fn create_thought(
@@ -25,6 +29,7 @@ pub async fn create_thought(
author_id: Set(author_id),
content: Set(params.content.clone()),
reply_to_id: Set(params.reply_to_id),
visibility: Set(params.visibility.unwrap_or(thought::Visibility::Public)),
..Default::default()
}
.insert(&txn)
@@ -52,7 +57,13 @@ pub async fn delete_thought(db: &DbConn, thought_id: Uuid) -> Result<(), DbErr>
pub async fn get_thoughts_by_user(
db: &DbConn,
user_id: Uuid,
viewer_id: Option<Uuid>,
) -> Result<Vec<ThoughtWithAuthor>, DbErr> {
let mut friend_ids = vec![];
if let Some(viewer) = viewer_id {
friend_ids = follow::get_friend_ids(db, viewer).await?;
}
thought::Entity::find()
.select_only()
.column(thought::Column::Id)
@@ -60,8 +71,10 @@ pub async fn get_thoughts_by_user(
.column(thought::Column::ReplyToId)
.column(thought::Column::CreatedAt)
.column(thought::Column::AuthorId)
.column(thought::Column::Visibility)
.column_as(user::Column::Username, "author_username")
.join(JoinType::InnerJoin, thought::Relation::User.def())
.filter(apply_visibility_filter(user_id, viewer_id, &friend_ids))
.filter(thought::Column::AuthorId.eq(user_id))
.order_by_desc(thought::Column::CreatedAt)
.into_model::<ThoughtWithAuthor>()
@@ -72,20 +85,37 @@ pub async fn get_thoughts_by_user(
pub async fn get_feed_for_user(
db: &DbConn,
following_ids: Vec<Uuid>,
viewer_id: Option<Uuid>,
) -> Result<Vec<ThoughtWithAuthor>, UserError> {
if following_ids.is_empty() {
return Ok(vec![]);
}
let mut friend_ids = vec![];
if let Some(viewer) = viewer_id {
friend_ids = follow::get_friend_ids(db, viewer)
.await
.map_err(|e| UserError::Internal(e.to_string()))?;
}
thought::Entity::find()
.select_only()
.column(thought::Column::Id)
.column(thought::Column::Content)
.column(thought::Column::ReplyToId)
.column(thought::Column::CreatedAt)
.column(thought::Column::Visibility)
.column(thought::Column::AuthorId)
.column_as(user::Column::Username, "author_username")
.join(JoinType::InnerJoin, thought::Relation::User.def())
.filter(
Condition::any().add(following_ids.iter().fold(
Condition::all(),
|cond, &author_id| {
cond.add(apply_visibility_filter(author_id, viewer_id, &friend_ids))
},
)),
)
.filter(thought::Column::AuthorId.is_in(following_ids))
.order_by_desc(thought::Column::CreatedAt)
.into_model::<ThoughtWithAuthor>()
@@ -97,14 +127,21 @@ pub async fn get_feed_for_user(
pub async fn get_thoughts_by_tag_name(
db: &DbConn,
tag_name: &str,
viewer_id: Option<Uuid>,
) -> Result<Vec<ThoughtWithAuthor>, DbErr> {
thought::Entity::find()
let mut friend_ids = Vec::new();
if let Some(viewer) = viewer_id {
friend_ids = follow::get_friend_ids(db, viewer).await?;
}
let thoughts = thought::Entity::find()
.select_only()
.column(thought::Column::Id)
.column(thought::Column::Content)
.column(thought::Column::ReplyToId)
.column(thought::Column::CreatedAt)
.column(thought::Column::AuthorId)
.column(thought::Column::Visibility)
.column_as(user::Column::Username, "author_username")
.join(JoinType::InnerJoin, thought::Relation::User.def())
.join(JoinType::InnerJoin, thought::Relation::ThoughtTag.def())
@@ -113,5 +150,49 @@ pub async fn get_thoughts_by_tag_name(
.order_by_desc(thought::Column::CreatedAt)
.into_model::<ThoughtWithAuthor>()
.all(db)
.await
.await?;
let visible_thoughts = thoughts
.into_iter()
.filter(|thought| {
let mut condition = thought.visibility == thought::Visibility::Public;
if let Some(viewer) = viewer_id {
if thought.author_id == viewer {
condition = true;
}
if thought.visibility == thought::Visibility::FriendsOnly
&& friend_ids.contains(&thought.author_id)
{
condition = true;
}
}
condition
})
.collect();
Ok(visible_thoughts)
}
fn apply_visibility_filter(
user_id: Uuid,
viewer_id: Option<Uuid>,
friend_ids: &[Uuid],
) -> SimpleExpr {
let mut condition =
Condition::any().add(thought::Column::Visibility.eq(thought::Visibility::Public));
if let Some(viewer) = viewer_id {
// Viewers can see their own thoughts of any visibility
if user_id == viewer {
condition = condition
.add(thought::Column::Visibility.eq(thought::Visibility::FriendsOnly))
.add(thought::Column::Visibility.eq(thought::Visibility::Private));
}
// If the thought's author is a friend of the viewer, they can see it
else if !friend_ids.is_empty() && friend_ids.contains(&user_id) {
condition =
condition.add(thought::Column::Visibility.eq(thought::Visibility::FriendsOnly));
}
}
condition.into()
}