spoiler veil for author too; veil history comments
Some checks failed
CI / Check / Test (push) Has been cancelled
CI / Check / Test (pull_request) Has been cancelled

This commit is contained in:
2026-10-25 19:50:11 +01:00
parent 21b6ff8f60
commit d694df4d8a
11 changed files with 18 additions and 31 deletions

View File

@@ -45,9 +45,8 @@
{% if let Some(wm) = entry.review().watch_medium() %} {% if let Some(wm) = entry.review().watch_medium() %}
<span class="watch-medium">{{ wm.label() }}</span> <span class="watch-medium">{{ wm.label() }}</span>
{% endif %} {% endif %}
{% let author = ctx.is_current_user(entry.review().user_id().value()) %}
{% if let Some(comment) = entry.review().comment() %} {% if let Some(comment) = entry.review().comment() %}
{% if !author && entry.review().spoilers() %} {% if entry.review().spoilers() %}
<details class="comment spoiler"> <details class="comment spoiler">
<summary style="cursor:pointer;opacity:.75">Contains spoilers</summary> <summary style="cursor:pointer;opacity:.75">Contains spoilers</summary>
{{ comment.value() }} {{ comment.value() }}

View File

@@ -129,9 +129,8 @@
<span class="star {% if filled %}filled{% else %}empty{% endif %}">★</span> <span class="star {% if filled %}filled{% else %}empty{% endif %}">★</span>
{% endfor %} {% endfor %}
</div> </div>
{% let author = ctx.is_current_user(entry.review().user_id().value()) %}
{% if let Some(comment) = entry.review().comment() %} {% if let Some(comment) = entry.review().comment() %}
{% if !author && entry.review().spoilers() %} {% if entry.review().spoilers() %}
<details class="comment spoiler"> <details class="comment spoiler">
<summary style="cursor:pointer;opacity:.75">Contains spoilers</summary> <summary style="cursor:pointer;opacity:.75">Contains spoilers</summary>
{{ comment.value() }} {{ comment.value() }}

View File

@@ -219,9 +219,8 @@
<span class="star {% if filled %}filled{% else %}empty{% endif %}">★</span> <span class="star {% if filled %}filled{% else %}empty{% endif %}">★</span>
{% endfor %} {% endfor %}
</div> </div>
{% let author = ctx.is_current_user(entry.review().user_id().value()) %}
{% if let Some(comment) = entry.review().comment() %} {% if let Some(comment) = entry.review().comment() %}
{% if !author && entry.review().spoilers() %} {% if entry.review().spoilers() %}
<details class="comment spoiler"> <details class="comment spoiler">
<summary style="cursor:pointer;opacity:.75">Contains spoilers</summary> <summary style="cursor:pointer;opacity:.75">Contains spoilers</summary>
{{ comment.value() }} {{ comment.value() }}

View File

@@ -10,12 +10,10 @@ import type { SocialReviewDto } from "@/features/movies"
type CommunityReviewsProps = { type CommunityReviewsProps = {
reviews: { items: SocialReviewDto[] } reviews: { items: SocialReviewDto[] }
/** Authenticated viewer id — own reviews are never veiled. */
currentUserId?: string
onShowDetail?: (review: SocialReviewDto) => void onShowDetail?: (review: SocialReviewDto) => void
} }
export function CommunityReviews({ reviews, currentUserId, onShowDetail }: CommunityReviewsProps) { export function CommunityReviews({ reviews, onShowDetail }: CommunityReviewsProps) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
@@ -26,7 +24,7 @@ export function CommunityReviews({ reviews, currentUserId, onShowDetail }: Commu
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{reviews.items.map((r, i) => { {reviews.items.map((r, i) => {
const veiled = !!r.spoilers && currentUserId !== r.user_id const veiled = !!r.spoilers
return ( return (
<Card key={i} size="sm"> <Card key={i} size="sm">
<CardHeader> <CardHeader>

View File

@@ -94,7 +94,6 @@ export function FeedTab() {
onLoadMore={loadMore} onLoadMore={loadMore}
renderItem={(entry) => { renderItem={(entry) => {
const isOwn = entry.user_id === auth?.user_id const isOwn = entry.user_id === auth?.user_id
const veil = !isOwn && !!entry.review.spoilers
const card = ( const card = (
<ReviewCard <ReviewCard
movie={entry.movie} movie={entry.movie}
@@ -103,7 +102,6 @@ export function FeedTab() {
userId={entry.user_id} userId={entry.user_id}
isFederated={entry.is_federated} isFederated={entry.is_federated}
actorUrl={entry.actor_url} actorUrl={entry.actor_url}
veil={veil}
onEdit={isOwn ? () => setEditingEntry(entry) : undefined} onEdit={isOwn ? () => setEditingEntry(entry) : undefined}
onShowDetail={entry.review.comment ? () => setDetailEntry(entry) : undefined} onShowDetail={entry.review.comment ? () => setDetailEntry(entry) : undefined}
/> />
@@ -141,7 +139,6 @@ export function FeedTab() {
movie={detailEntry.movie} movie={detailEntry.movie}
review={detailEntry.review} review={detailEntry.review}
userName={detailEntry.user_display_name} userName={detailEntry.user_display_name}
veil={detailEntry.user_id !== auth?.user_id}
/> />
)} )}
</div> </div>

View File

@@ -18,7 +18,6 @@ import { TimeAgo } from "@/components/time-ago"
import { WATCH_MEDIUMS } from "@/lib/watch-mediums" import { WATCH_MEDIUMS } from "@/lib/watch-mediums"
import { ReviewDetailSheet } from "@/components/review-detail-sheet" import { ReviewDetailSheet } from "@/components/review-detail-sheet"
import { DiaryCalendar } from "@/components/diary-calendar" import { DiaryCalendar } from "@/components/diary-calendar"
import { useAuth } from "@/components/auth-provider"
import type { DiaryEntryDto } from "@/lib/api/common" import type { DiaryEntryDto } from "@/lib/api/common"
import { tmdbProfileUrl } from "@/lib/api/client" import { tmdbProfileUrl } from "@/lib/api/client"
import type { UserProfileResponse } from "@/features/users" import type { UserProfileResponse } from "@/features/users"
@@ -151,7 +150,6 @@ function StatCell({ label, value }: { label: string; value: string | number }) {
function DiaryTab({ sortBy, userId, search }: { sortBy: string; userId?: string; search?: string }) { function DiaryTab({ sortBy, userId, search }: { sortBy: string; userId?: string; search?: string }) {
const { t } = useTranslation() const { t } = useTranslation()
const { auth } = useAuth()
const { data, isPending, hasNextPage, isFetchingNextPage, fetchNextPage } = const { data, isPending, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteDiary({ sort_by: sortBy, user_id: userId }) useInfiniteDiary({ sort_by: sortBy, user_id: userId })
const items = data?.pages.flatMap((p) => p.items) ?? [] const items = data?.pages.flatMap((p) => p.items) ?? []
@@ -181,7 +179,7 @@ function DiaryTab({ sortBy, userId, search }: { sortBy: string; userId?: string;
comment={e.review.comment} comment={e.review.comment}
subtitle={<><TimeAgo date={e.review.watched_at} /></>} subtitle={<><TimeAgo date={e.review.watched_at} /></>}
variant="compact" variant="compact"
spoilers={userId && userId === auth?.user_id ? undefined : e.review.spoilers} spoilers={e.review.spoilers}
onShowDetail={e.review.comment ? () => setDetailEntry(e) : undefined} onShowDetail={e.review.comment ? () => setDetailEntry(e) : undefined}
/> />
)} )}
@@ -192,7 +190,6 @@ function DiaryTab({ sortBy, userId, search }: { sortBy: string; userId?: string;
onOpenChange={(open) => !open && setDetailEntry(null)} onOpenChange={(open) => !open && setDetailEntry(null)}
movie={detailEntry.movie} movie={detailEntry.movie}
review={detailEntry.review} review={detailEntry.review}
veil={!!userId && userId !== auth?.user_id}
/> />
)} )}
</> </>

View File

@@ -17,13 +17,12 @@ type ReviewCardProps = {
userId?: string userId?: string
isFederated?: boolean isFederated?: boolean
actorUrl?: string actorUrl?: string
/** Blur the comment until revealed — set for every viewer except the author. */
veil?: boolean
onEdit?: () => void onEdit?: () => void
onShowDetail?: () => void onShowDetail?: () => void
} }
export function ReviewCard({ movie, review, userName, userId, isFederated, actorUrl, veil, onEdit, onShowDetail }: ReviewCardProps) { export function ReviewCard({ movie, review, userName, userId, isFederated, actorUrl, onEdit, onShowDetail }: ReviewCardProps) {
const veil = !!review.spoilers
const showDetail = veil ? undefined : onShowDetail const showDetail = veil ? undefined : onShowDetail
const card = ( const card = (
<Card size="sm"> <Card size="sm">

View File

@@ -13,11 +13,9 @@ type ReviewDetailSheetProps = {
movie: MovieDto movie: MovieDto
review: ReviewDto review: ReviewDto
userName?: string userName?: string
/** Blur the comment until revealed — set for every viewer except the author. */
veil?: boolean
} }
export function ReviewDetailSheet({ open, onOpenChange, movie, review, userName, veil }: ReviewDetailSheetProps) { export function ReviewDetailSheet({ open, onOpenChange, movie, review, userName }: ReviewDetailSheetProps) {
return ( return (
<Drawer open={open} onOpenChange={onOpenChange}> <Drawer open={open} onOpenChange={onOpenChange}>
<DrawerContent className="mx-auto max-w-lg"> <DrawerContent className="mx-auto max-w-lg">
@@ -43,7 +41,7 @@ export function ReviewDetailSheet({ open, onOpenChange, movie, review, userName,
</div> </div>
{review.comment && ( {review.comment && (
veil && review.spoilers ? ( review.spoilers ? (
<SpoilerVeil content={review.comment} className="text-sm leading-relaxed" /> <SpoilerVeil content={review.comment} className="text-sm leading-relaxed" />
) : ( ) : (
<p className="select-text whitespace-pre-wrap text-sm leading-relaxed"> <p className="select-text whitespace-pre-wrap text-sm leading-relaxed">

View File

@@ -10,8 +10,8 @@ type SpoilerVeilProps = {
} }
/** /**
* Hides review text behind a blur with a reveal button — shown to everyone * Hides review text behind a blur with a reveal button — shown to every
* except the review's author. Once revealed, the text stays visible. * viewer, the author included. Once revealed, the text stays visible.
*/ */
export function SpoilerVeil({ content, className }: SpoilerVeilProps) { export function SpoilerVeil({ content, className }: SpoilerVeilProps) {
const { t } = useTranslation() const { t } = useTranslation()

View File

@@ -1,6 +1,7 @@
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { TrendingUp } from "lucide-react" import { TrendingUp } from "lucide-react"
import { StarDisplay } from "@/components/star-display" import { StarDisplay } from "@/components/star-display"
import { SpoilerVeil } from "@/components/spoiler-veil"
import { shortDate } from "@/lib/date" import { shortDate } from "@/lib/date"
import type { ReviewHistoryResponse } from "@/features/movies" import type { ReviewHistoryResponse } from "@/features/movies"
@@ -24,7 +25,11 @@ export function ViewingHistory({ history }: { history: ReviewHistoryResponse })
<div> <div>
<p className="text-sm font-medium">{shortDate(v.watched_at)}</p> <p className="text-sm font-medium">{shortDate(v.watched_at)}</p>
{v.comment && ( {v.comment && (
<p className="mt-0.5 text-xs text-muted-foreground line-clamp-1">{v.comment}</p> v.spoilers ? (
<SpoilerVeil content={v.comment} className="mt-0.5 text-xs text-muted-foreground line-clamp-1" />
) : (
<p className="mt-0.5 text-xs text-muted-foreground line-clamp-1">{v.comment}</p>
)
)} )}
</div> </div>
<StarDisplay rating={v.rating} size="xs" /> <StarDisplay rating={v.rating} size="xs" />

View File

@@ -12,7 +12,6 @@ import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton"
import { posterUrl, tmdbProfileUrl } from "@/lib/api/client" import { posterUrl, tmdbProfileUrl } from "@/lib/api/client"
import { useAuth } from "@/components/auth-provider"
import { useMovie, useMovieHistory, useMovieProfile } from "@/features/movies" import { useMovie, useMovieHistory, useMovieProfile } from "@/features/movies"
import { useDocumentTitle } from "@/hooks/use-document-title" import { useDocumentTitle } from "@/hooks/use-document-title"
import { import {
@@ -30,7 +29,6 @@ function MovieDetailPage() {
const { t } = useTranslation() const { t } = useTranslation()
const { id } = Route.useParams() const { id } = Route.useParams()
const { data, isPending } = useMovie(id) const { data, isPending } = useMovie(id)
const { auth } = useAuth()
const { data: profile } = useMovieProfile(id) const { data: profile } = useMovieProfile(id)
const { data: history } = useMovieHistory(id) const { data: history } = useMovieHistory(id)
useDocumentTitle(data?.movie.title) useDocumentTitle(data?.movie.title)
@@ -109,7 +107,6 @@ function MovieDetailPage() {
<CommunityReviews <CommunityReviews
reviews={reviews} reviews={reviews}
currentUserId={auth?.user_id}
onShowDetail={setDetailReview} onShowDetail={setDetailReview}
/> />
@@ -122,7 +119,6 @@ function MovieDetailPage() {
movie={movie} movie={movie}
review={{ id: "", rating: detailReview.rating, comment: detailReview.comment, watched_at: detailReview.watched_at, watch_medium: detailReview.watch_medium, spoilers: detailReview.spoilers }} review={{ id: "", rating: detailReview.rating, comment: detailReview.comment, watched_at: detailReview.watched_at, watch_medium: detailReview.watch_medium, spoilers: detailReview.spoilers }}
userName={detailReview.user_display} userName={detailReview.user_display}
veil={detailReview.user_id !== auth?.user_id}
/> />
)} )}
</div> </div>