diff --git a/crates/adapters/postgres/src/feed.rs b/crates/adapters/postgres/src/feed.rs index 664fca8..99d47f7 100644 --- a/crates/adapters/postgres/src/feed.rs +++ b/crates/adapters/postgres/src/feed.rs @@ -196,6 +196,36 @@ impl FeedRepository for PgFeedRepository { per_page: page.per_page, }) } + + async fn user_feed(&self, user_id: &UserId, page: &PageParams, viewer_id: Option<&UserId>) -> Result, DomainError> { + let viewer = viewer_id.map(|v| v.as_uuid()); + let uid = user_id.as_uuid(); + + let total: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM thoughts t WHERE t.user_id = $1 AND t.visibility = 'public'" + ) + .bind(uid) + .fetch_one(&self.pool) + .await + .map_err(|e| DomainError::Internal(e.to_string()))?; + + let sel = feed_select(viewer); + let sql = format!("{sel} WHERE t.user_id = $1 AND t.visibility = 'public' ORDER BY t.created_at DESC LIMIT $2 OFFSET $3"); + let rows = sqlx::query_as::<_, FeedRow>(&sql) + .bind(uid) + .bind(page.limit()) + .bind(page.offset()) + .fetch_all(&self.pool) + .await + .map_err(|e| DomainError::Internal(e.to_string()))?; + + Ok(Paginated { + items: rows.into_iter().map(row_to_entry).collect(), + total, + page: page.page, + per_page: page.per_page, + }) + } } #[cfg(test)] diff --git a/crates/adapters/postgres/src/thought.rs b/crates/adapters/postgres/src/thought.rs index a5a82d0..2ad60e9 100644 --- a/crates/adapters/postgres/src/thought.rs +++ b/crates/adapters/postgres/src/thought.rs @@ -4,12 +4,11 @@ use sqlx::PgPool; use domain::{ errors::DomainError, models::{ - feed::{FeedEntry, PageParams, Paginated}, + feed::{PageParams, Paginated}, thought::{Thought, Visibility}, - user::User, }, ports::ThoughtRepository, - value_objects::{Content, Email, PasswordHash, ThoughtId, UserId, Username}, + value_objects::{Content, ThoughtId, UserId}, }; pub struct PgThoughtRepository { pool: PgPool } @@ -119,47 +118,32 @@ impl ThoughtRepository for PgThoughtRepository { .map(|rows| rows.into_iter().map(Thought::from).collect()) } - async fn list_by_user(&self, user_id: &UserId, page: &PageParams) -> Result, DomainError> { - let total: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM thoughts WHERE user_id=$1") - .bind(user_id.as_uuid()) - .fetch_one(&self.pool) - .await - .map_err(|e| DomainError::Internal(e.to_string()))?; + async fn list_by_user(&self, user_id: &UserId, page: &PageParams) -> Result, DomainError> { + let uid = user_id.as_uuid(); + let total: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM thoughts WHERE user_id = $1" + ) + .bind(uid) + .fetch_one(&self.pool) + .await + .map_err(|e| DomainError::Internal(e.to_string()))?; let rows = sqlx::query_as::<_, ThoughtRow>( &format!("{THOUGHT_SELECT} WHERE user_id=$1 ORDER BY created_at DESC LIMIT $2 OFFSET $3") ) - .bind(user_id.as_uuid()) + .bind(uid) .bind(page.limit()) .bind(page.offset()) .fetch_all(&self.pool) .await .map_err(|e| DomainError::Internal(e.to_string()))?; - let author = sqlx::query_as::<_, crate::user::UserRow>( - "SELECT id,username,email,password_hash,display_name,bio,avatar_url,header_url,custom_css,local,ap_id,inbox_url,public_key,private_key,created_at,updated_at FROM users WHERE id=$1" - ) - .bind(user_id.as_uuid()) - .fetch_optional(&self.pool) - .await - .map_err(|e| DomainError::Internal(e.to_string()))? - .ok_or(DomainError::NotFound)?; - let author = User::from(author); - - let items = rows.into_iter().map(|r| { - let thought = Thought::from(r); - FeedEntry { - thought, - author: author.clone(), - like_count: 0, - boost_count: 0, - reply_count: 0, - liked_by_viewer: false, - boosted_by_viewer: false, - } - }).collect(); - - Ok(Paginated { items, total, page: page.page, per_page: page.per_page }) + Ok(Paginated { + items: rows.into_iter().map(Thought::from).collect(), + total, + page: page.page, + per_page: page.per_page, + }) } } diff --git a/crates/application/src/use_cases/feed.rs b/crates/application/src/use_cases/feed.rs index 24c1c46..176e346 100644 --- a/crates/application/src/use_cases/feed.rs +++ b/crates/application/src/use_cases/feed.rs @@ -4,7 +4,7 @@ use domain::{ feed::{FeedEntry, PageParams, Paginated, UserSummary}, user::User, }, - ports::{FeedRepository, FollowRepository, ThoughtRepository, UserRepository}, + ports::{FeedRepository, FollowRepository, UserRepository}, value_objects::UserId, }; @@ -17,8 +17,8 @@ pub async fn get_public_feed(feed: &dyn FeedRepository, viewer_id: Option<&UserI feed.public_feed(&page, viewer_id).await } -pub async fn get_user_feed(thoughts: &dyn ThoughtRepository, user_id: &UserId, page: PageParams) -> Result, DomainError> { - thoughts.list_by_user(user_id, &page).await +pub async fn get_user_feed(feed: &dyn FeedRepository, user_id: &UserId, page: PageParams, viewer_id: Option<&UserId>) -> Result, DomainError> { + feed.user_feed(user_id, &page, viewer_id).await } pub async fn get_followers(follows: &dyn FollowRepository, user_id: &UserId, page: PageParams) -> Result, DomainError> { diff --git a/crates/domain/src/ports.rs b/crates/domain/src/ports.rs index 63b8c31..66a76a2 100644 --- a/crates/domain/src/ports.rs +++ b/crates/domain/src/ports.rs @@ -57,7 +57,7 @@ pub trait ThoughtRepository: Send + Sync { async fn delete(&self, id: &ThoughtId, user_id: &UserId) -> Result<(), DomainError>; async fn update_content(&self, id: &ThoughtId, content: &Content) -> Result<(), DomainError>; async fn get_thread(&self, id: &ThoughtId) -> Result, DomainError>; - async fn list_by_user(&self, user_id: &UserId, page: &PageParams) -> Result, DomainError>; + async fn list_by_user(&self, user_id: &UserId, page: &PageParams) -> Result, DomainError>; } #[async_trait] @@ -139,6 +139,7 @@ pub trait FeedRepository: Send + Sync { async fn public_feed(&self, page: &PageParams, viewer_id: Option<&UserId>) -> Result, DomainError>; async fn search(&self, query: &str, page: &PageParams, viewer_id: Option<&UserId>) -> Result, DomainError>; async fn tag_feed(&self, tag_name: &str, page: &PageParams, viewer_id: Option<&UserId>) -> Result, DomainError>; + async fn user_feed(&self, user_id: &UserId, page: &PageParams, viewer_id: Option<&UserId>) -> Result, DomainError>; } #[async_trait] diff --git a/crates/domain/src/testing.rs b/crates/domain/src/testing.rs index b4e5e42..503497f 100644 --- a/crates/domain/src/testing.rs +++ b/crates/domain/src/testing.rs @@ -96,7 +96,7 @@ pub struct TestStore { .filter(|t| t.in_reply_to_id.as_ref() == Some(id) || &t.id == id) .cloned().collect()) } - async fn list_by_user(&self, _user_id: &UserId, _page: &PageParams) -> Result, DomainError> { + async fn list_by_user(&self, _user_id: &UserId, _page: &PageParams) -> Result, DomainError> { Ok(Paginated { items: vec![], total: 0, page: 1, per_page: 20 }) } } @@ -290,6 +290,9 @@ pub struct TestStore { async fn tag_feed(&self, _tag_name: &str, _page: &PageParams, _viewer_id: Option<&UserId>) -> Result, DomainError> { Ok(Paginated { items: vec![], total: 0, page: 1, per_page: 20 }) } + async fn user_feed(&self, _user_id: &UserId, _page: &PageParams, _viewer_id: Option<&UserId>) -> Result, DomainError> { + Ok(Paginated { items: vec![], total: 0, page: 1, per_page: 20 }) + } } #[async_trait] impl SearchPort for TestStore { diff --git a/crates/presentation/src/handlers/feed.rs b/crates/presentation/src/handlers/feed.rs index e942f57..297b8fa 100644 --- a/crates/presentation/src/handlers/feed.rs +++ b/crates/presentation/src/handlers/feed.rs @@ -120,11 +120,12 @@ pub async fn get_followers_handler(State(s): State, Path(username): Pa pub async fn user_thoughts_handler( State(s): State, Path(username): Path, + OptionalAuthUser(viewer): OptionalAuthUser, Query(q): Query, ) -> Result, ApiError> { let user = get_user_by_username(&*s.users, &username).await?; let page = PageParams { page: q.page(), per_page: q.per_page() }; - let result = get_user_feed(&*s.thoughts, &user.id, page).await?; + let result = get_user_feed(&*s.feed, &user.id, page, viewer.as_ref()).await?; Ok(Json(serde_json::json!({ "total": result.total, "page": result.page,