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

@@ -1,6 +1,7 @@
use anyhow::Result;
use axum::routing::{delete, get, patch, post};
use axum::Router;
use tower_http::services::ServeDir;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod auth;
@@ -45,12 +46,27 @@ async fn main() -> Result<()> {
"/api/v1/upload/{id}",
patch(handlers::upload::edit_upload).delete(handlers::upload::delete_upload),
)
// Feed
.route("/api/v1/feed", get(handlers::feed::feed))
.route("/api/v1/feed/delta", get(handlers::feed::feed_delta))
.route("/api/v1/hashtags", get(handlers::feed::hashtags))
// Social
.route("/api/v1/upload/{id}/like", post(handlers::social::toggle_like))
.route(
"/api/v1/upload/{id}/comments",
get(handlers::social::list_comments).post(handlers::social::add_comment),
)
.route("/api/v1/comment/{id}", delete(handlers::social::delete_comment))
// SSE
.route("/api/v1/stream", get(handlers::sse::stream));
// Serve media files from disk
let media_service = ServeDir::new(&config.media_path);
let router = Router::new()
.route("/health", get(|| async { "ok" }))
.merge(api)
.nest_service("/media", media_service)
.with_state(state);
let listener = tokio::net::TcpListener::bind(("0.0.0.0", config.app_port)).await?;