sojorn/go-backend/internal/services/feed_service.go
Patrick Britton 3c4680bdd7 Initial commit: Complete threaded conversation system with inline replies
**Major Features Added:**
- **Inline Reply System**: Replace compose screen with inline reply boxes
- **Thread Navigation**: Parent/child navigation with jump functionality
- **Chain Flow UI**: Reply counts, expand/collapse animations, visual hierarchy
- **Enhanced Animations**: Smooth transitions, hover effects, micro-interactions

 **Frontend Changes:**
- **ThreadedCommentWidget**: Complete rewrite with animations and navigation
- **ThreadNode Model**: Added parent references and descendant counting
- **ThreadedConversationScreen**: Integrated navigation handlers
- **PostDetailScreen**: Replaced with threaded conversation view
- **ComposeScreen**: Added reply indicators and context
- **PostActions**: Fixed visibility checks for chain buttons

 **Backend Changes:**
- **API Route**: Added /posts/:id/thread endpoint
- **Post Repository**: Include allow_chain and visibility fields in feed
- **Thread Handler**: Support for fetching post chains

 **UI/UX Improvements:**
- **Reply Context**: Clear indication when replying to specific posts
- **Character Counting**: 500 character limit with live counter
- **Visual Hierarchy**: Depth-based indentation and styling
- **Smooth Animations**: SizeTransition, FadeTransition, hover states
- **Chain Navigation**: Parent/child buttons with visual feedback

 **Technical Enhancements:**
- **Animation Controllers**: Proper lifecycle management
- **State Management**: Clean separation of concerns
- **Navigation Callbacks**: Reusable navigation system
- **Error Handling**: Graceful fallbacks and user feedback

This creates a Reddit-style threaded conversation experience with smooth
animations, inline replies, and intuitive navigation between posts in a chain.
2026-01-30 07:40:19 -06:00

73 lines
1.9 KiB
Go

package services
import (
"context"
"github.com/patbritton/sojorn-backend/internal/models"
"github.com/patbritton/sojorn-backend/internal/repository"
)
type FeedService struct {
postRepo *repository.PostRepository
assetService *AssetService
}
func NewFeedService(postRepo *repository.PostRepository, assetService *AssetService) *FeedService {
return &FeedService{
postRepo: postRepo,
assetService: assetService,
}
}
func (s *FeedService) GetFeed(ctx context.Context, userID string, categorySlug string, hasVideo bool, limit int, offset int) ([]models.Post, error) {
posts, err := s.postRepo.GetFeed(ctx, userID, categorySlug, hasVideo, limit, offset)
if err != nil {
return nil, err
}
// Sign URLs for initial posts
for i := range posts {
if posts[i].ImageURL != nil {
signed := s.assetService.SignImageURL(*posts[i].ImageURL)
posts[i].ImageURL = &signed
}
if posts[i].VideoURL != nil {
signed := s.assetService.SignVideoURL(*posts[i].VideoURL)
posts[i].VideoURL = &signed
}
if posts[i].ThumbnailURL != nil {
signed := s.assetService.SignImageURL(*posts[i].ThumbnailURL)
posts[i].ThumbnailURL = &signed
}
}
// Ads Injection at index 4 matching legacy Deno implementation
if len(posts) >= 4 {
ad, err := s.postRepo.GetRandomSponsoredPost(ctx, userID)
if err == nil && ad != nil {
// Sign Ad URL
if ad.ImageURL != nil {
signed := s.assetService.SignImageURL(*ad.ImageURL)
ad.ImageURL = &signed
}
if ad.VideoURL != nil {
signed := s.assetService.SignVideoURL(*ad.VideoURL)
ad.VideoURL = &signed
}
if ad.ThumbnailURL != nil {
signed := s.assetService.SignImageURL(*ad.ThumbnailURL)
ad.ThumbnailURL = &signed
}
// Insert at index 4
newPosts := make([]models.Post, 0, len(posts)+1)
newPosts = append(newPosts, posts[:4]...)
newPosts = append(newPosts, *ad)
newPosts = append(newPosts, posts[4:]...)
posts = newPosts
}
}
return posts, nil
}