feat: implement gallery feed with social features and SSE

- Cursor-based feed endpoint using v_feed view with hashtag filtering
- Like toggle (INSERT ON CONFLICT), comments CRUD
- Feed delta endpoint for SSE-driven incremental updates
- SSE client with Page Visibility API (pause/reconnect)
- Responsive photo/video grid with infinite scroll
- Hashtag filter chips, lightbox modal with comments
- Media file serving via tower-http ServeDir

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
fabi
2026-04-01 19:17:06 +02:00
parent 4e1f1d6426
commit 964598e41d
13 changed files with 1134 additions and 26 deletions

View File

@@ -0,0 +1,72 @@
use chrono::{DateTime, Utc};
use serde::Serialize;
use sqlx::PgPool;
use uuid::Uuid;
#[derive(Debug, sqlx::FromRow)]
pub struct Comment {
pub id: Uuid,
pub upload_id: Uuid,
pub user_id: Uuid,
pub body: String,
pub created_at: DateTime<Utc>,
pub deleted_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Serialize, sqlx::FromRow)]
pub struct CommentDto {
pub id: Uuid,
pub upload_id: Uuid,
pub user_id: Uuid,
pub uploader_name: String,
pub body: String,
pub created_at: DateTime<Utc>,
}
impl Comment {
pub async fn create(
pool: &PgPool,
upload_id: Uuid,
user_id: Uuid,
body: &str,
) -> Result<Self, sqlx::Error> {
sqlx::query_as::<_, Self>(
"INSERT INTO comment (upload_id, user_id, body) VALUES ($1, $2, $3) RETURNING *",
)
.bind(upload_id)
.bind(user_id)
.bind(body)
.fetch_one(pool)
.await
}
pub async fn list_for_upload(pool: &PgPool, upload_id: Uuid) -> Result<Vec<CommentDto>, sqlx::Error> {
sqlx::query_as::<_, CommentDto>(
"SELECT c.id, c.upload_id, c.user_id, u.display_name AS uploader_name, c.body, c.created_at
FROM comment c
JOIN \"user\" u ON u.id = c.user_id
WHERE c.upload_id = $1 AND c.deleted_at IS NULL
ORDER BY c.created_at ASC",
)
.bind(upload_id)
.fetch_all(pool)
.await
}
pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Self>, sqlx::Error> {
sqlx::query_as::<_, Self>(
"SELECT * FROM comment WHERE id = $1 AND deleted_at IS NULL",
)
.bind(id)
.fetch_optional(pool)
.await
}
pub async fn soft_delete(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
sqlx::query("UPDATE comment SET deleted_at = NOW() WHERE id = $1")
.bind(id)
.execute(pool)
.await?;
Ok(())
}
}