fix: hide NSFW posts entirely when user hasn't enabled NSFW - backend filtering + Flutter safety net

This commit is contained in:
Patrick Britton 2026-02-08 00:19:44 -06:00
parent b51c9ba90b
commit 25d3e213ea
4 changed files with 65 additions and 14 deletions

View file

@ -588,7 +588,15 @@ func (h *PostHandler) GetProfilePosts(c *gin.Context) {
viewerID = val.(string)
}
posts, err := h.postRepo.GetPostsByAuthor(c.Request.Context(), authorID, viewerID, limit, offset, onlyChains)
// Check viewer's NSFW preference
showNSFW := false
if viewerID != "" {
if settings, err := h.userRepo.GetUserSettings(c.Request.Context(), viewerID); err == nil && settings.NSFWEnabled != nil {
showNSFW = *settings.NSFWEnabled
}
}
posts, err := h.postRepo.GetPostsByAuthor(c.Request.Context(), authorID, viewerID, limit, offset, onlyChains, showNSFW)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch profile posts", "details": err.Error()})
return
@ -835,7 +843,15 @@ func (h *PostHandler) GetSavedPosts(c *gin.Context) {
limit := utils.GetQueryInt(c, "limit", 20)
offset := utils.GetQueryInt(c, "offset", 0)
posts, err := h.postRepo.GetSavedPosts(c.Request.Context(), userID, limit, offset)
// Check viewer's NSFW preference
showNSFW := false
if viewerID, exists := c.Get("user_id"); exists {
if settings, err := h.userRepo.GetUserSettings(c.Request.Context(), viewerID.(string)); err == nil && settings.NSFWEnabled != nil {
showNSFW = *settings.NSFWEnabled
}
}
posts, err := h.postRepo.GetSavedPosts(c.Request.Context(), userID, limit, offset, showNSFW)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch saved posts", "details": err.Error()})
return
@ -861,7 +877,15 @@ func (h *PostHandler) GetLikedPosts(c *gin.Context) {
limit := utils.GetQueryInt(c, "limit", 20)
offset := utils.GetQueryInt(c, "offset", 0)
posts, err := h.postRepo.GetLikedPosts(c.Request.Context(), userID, limit, offset)
// Check viewer's NSFW preference
showNSFW := false
if viewerID, exists := c.Get("user_id"); exists {
if settings, err := h.userRepo.GetUserSettings(c.Request.Context(), viewerID.(string)); err == nil && settings.NSFWEnabled != nil {
showNSFW = *settings.NSFWEnabled
}
}
posts, err := h.postRepo.GetLikedPosts(c.Request.Context(), userID, limit, offset, showNSFW)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch liked posts", "details": err.Error()})
return

View file

@ -255,7 +255,13 @@ func (h *UserHandler) GetSavedPosts(c *gin.Context) {
limit := utils.GetQueryInt(c, "limit", 20)
offset := utils.GetQueryInt(c, "offset", 0)
posts, err := h.postRepo.GetSavedPosts(c.Request.Context(), targetID, limit, offset)
// Check viewer's NSFW preference
showNSFW := false
if settings, err := h.repo.GetUserSettings(c.Request.Context(), currentUserID); err == nil && settings.NSFWEnabled != nil {
showNSFW = *settings.NSFWEnabled
}
posts, err := h.postRepo.GetSavedPosts(c.Request.Context(), targetID, limit, offset, showNSFW)
if err != nil {
log.Error().Err(err).Msg("Failed to fetch saved posts")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch saved posts"})
@ -284,7 +290,13 @@ func (h *UserHandler) GetLikedPosts(c *gin.Context) {
limit := 20
offset := 0
posts, err := h.postRepo.GetLikedPosts(c.Request.Context(), userID, limit, offset)
// Check viewer's NSFW preference
showNSFW := false
if settings, err := h.repo.GetUserSettings(c.Request.Context(), userID); err == nil && settings.NSFWEnabled != nil {
showNSFW = *settings.NSFWEnabled
}
posts, err := h.postRepo.GetLikedPosts(c.Request.Context(), userID, limit, offset, showNSFW)
if err != nil {
log.Error().Err(err).Msg("Failed to fetch liked posts")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch liked posts"})

View file

@ -232,7 +232,7 @@ func (r *PostRepository) GetCategories(ctx context.Context) ([]models.Category,
return categories, nil
}
func (r *PostRepository) GetPostsByAuthor(ctx context.Context, authorID string, viewerID string, limit int, offset int, onlyChains bool) ([]models.Post, error) {
func (r *PostRepository) GetPostsByAuthor(ctx context.Context, authorID string, viewerID string, limit int, offset int, onlyChains bool, showNSFW bool) ([]models.Post, error) {
query := `
SELECT
p.id, p.author_id, p.category_id, p.body,
@ -268,10 +268,14 @@ func (r *PostRepository) GetPostsByAuthor(ctx context.Context, authorID string,
)
)
AND ($5 = FALSE OR p.chain_parent_id IS NOT NULL)
AND (
COALESCE(p.is_nsfw, FALSE) = FALSE
OR $6 = TRUE
)
ORDER BY p.created_at DESC
LIMIT $2 OFFSET $3
`
rows, err := r.pool.Query(ctx, query, authorID, limit, offset, viewerID, onlyChains)
rows, err := r.pool.Query(ctx, query, authorID, limit, offset, viewerID, onlyChains, showNSFW)
if err != nil {
return nil, err
}
@ -549,7 +553,7 @@ func (r *PostRepository) GetNearbyBeacons(ctx context.Context, lat float64, long
return beacons, nil
}
func (r *PostRepository) GetSavedPosts(ctx context.Context, userID string, limit int, offset int) ([]models.Post, error) {
func (r *PostRepository) GetSavedPosts(ctx context.Context, userID string, limit int, offset int, showNSFW bool) ([]models.Post, error) {
query := `
SELECT
p.id, p.author_id, p.category_id, p.body,
@ -569,10 +573,11 @@ func (r *PostRepository) GetSavedPosts(ctx context.Context, userID string, limit
JOIN public.profiles pr ON p.author_id = pr.id
LEFT JOIN public.post_metrics m ON p.id = m.post_id
WHERE ps.user_id = $1::uuid AND p.deleted_at IS NULL
AND (COALESCE(p.is_nsfw, FALSE) = FALSE OR $4 = TRUE)
ORDER BY ps.created_at DESC
LIMIT $2 OFFSET $3
`
rows, err := r.pool.Query(ctx, query, userID, limit, offset)
rows, err := r.pool.Query(ctx, query, userID, limit, offset, showNSFW)
if err != nil {
return nil, err
}
@ -601,7 +606,7 @@ func (r *PostRepository) GetSavedPosts(ctx context.Context, userID string, limit
return posts, nil
}
func (r *PostRepository) GetLikedPosts(ctx context.Context, userID string, limit int, offset int) ([]models.Post, error) {
func (r *PostRepository) GetLikedPosts(ctx context.Context, userID string, limit int, offset int, showNSFW bool) ([]models.Post, error) {
query := `
SELECT
p.id, p.author_id, p.category_id, p.body,
@ -621,10 +626,11 @@ func (r *PostRepository) GetLikedPosts(ctx context.Context, userID string, limit
JOIN public.profiles pr ON p.author_id = pr.id
LEFT JOIN public.post_metrics m ON p.id = m.post_id
WHERE pl.user_id = $1::uuid AND p.deleted_at IS NULL
AND (COALESCE(p.is_nsfw, FALSE) = FALSE OR $4 = TRUE)
ORDER BY pl.created_at DESC
LIMIT $2 OFFSET $3
`
rows, err := r.pool.Query(ctx, query, userID, limit, offset)
rows, err := r.pool.Query(ctx, query, userID, limit, offset, showNSFW)
if err != nil {
return nil, err
}

View file

@ -91,12 +91,18 @@ class _sojornPostCardState extends ConsumerState<sojornPostCard> {
}
}
/// Whether to show NSFW blur overlay respects user's blur preference
/// Whether NSFW post should be completely hidden (not shown at all)
bool get _shouldHideNsfw {
if (!post.isNsfw) return false;
final settings = ref.read(settingsProvider);
return !(settings.user?.nsfwEnabled ?? false);
}
/// Whether to show NSFW blur overlay only applies when user HAS opted in
bool get _shouldBlurNsfw {
if (!post.isNsfw || _nsfwRevealed) return false;
if (_shouldHideNsfw) return false; // Will be hidden entirely, no blur needed
final settings = ref.read(settingsProvider);
// Always blur if user hasn't opted into NSFW content
if (!(settings.user?.nsfwEnabled ?? false)) return true;
// If opted in, respect the blur toggle
return settings.user?.nsfwBlurEnabled ?? true;
}
@ -114,6 +120,9 @@ class _sojornPostCardState extends ConsumerState<sojornPostCard> {
@override
Widget build(BuildContext context) {
// Completely hide NSFW posts when user hasn't enabled NSFW
if (_shouldHideNsfw) return const SizedBox.shrink();
return Material(
color: Colors.transparent,
child: Container(