Add Spoiler Flag with veil, AP content-warning encoding, and full-stack plumbing

Authors can flag a review's comment as containing spoilers; the comment is
blurred behind a reveal control for every viewer except the author, omitted
from RSS, and federates as containsSpoilers plus an AP summary content
warning (ADR-0011) so Mastodon-class clients also collapse it. Flag follows
the general-edit path (ADR-0001) across domain, both DB backends, REST,
HTML templates, SPA (shadcn), TUI fixtures, and exports.
This commit is contained in:
2026-10-17 21:12:56 +02:00
parent 9dd9df1f73
commit bb2001b9cc
66 changed files with 513 additions and 167 deletions

View File

@@ -45,9 +45,17 @@
{% if let Some(wm) = entry.review().watch_medium() %}
<span class="watch-medium">{{ wm.label() }}</span>
{% endif %}
{% let author = ctx.is_current_user(entry.review().user_id().value()) %}
{% if let Some(comment) = entry.review().comment() %}
{% if !author && entry.review().spoilers() %}
<details class="comment spoiler">
<summary style="cursor:pointer;opacity:.75">Contains spoilers</summary>
{{ comment.value() }}
</details>
{% else %}
<div class="comment">{{ comment.value() }}</div>
{% endif %}
{% endif %}
<div class="feed-meta">
{% match entry.review().source() %}
{% when ReviewSource::Remote with { actor_url } %}

View File

@@ -152,8 +152,15 @@
{% endfor %}
</div>
{% if let Some(comment) = entry.review().comment() %}
{% if entry.review().spoilers() %}
<details class="embed-comment spoiler">
<summary style="cursor:pointer;opacity:.75">Contains spoilers</summary>
{{ comment.value() }}
</details>
{% else %}
<div class="embed-comment">{{ comment.value() }}</div>
{% endif %}
{% endif %}
<div class="embed-watched-at">{{ entry.review().watched_at().format("%Y-%m-%d") }}</div>
</div>
</article>

View File

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

View File

@@ -35,6 +35,9 @@
Comment<br>
<textarea name="comment"></textarea>
</label>
<label>
Contains spoilers <input type="checkbox" name="spoilers" value="true">
</label>
<label>
Watched via <span class="optional">(optional)</span><br>
<select name="watch_medium">

View File

@@ -219,9 +219,17 @@
<span class="star {% if filled %}filled{% else %}empty{% endif %}">★</span>
{% endfor %}
</div>
{% let author = ctx.is_current_user(entry.review().user_id().value()) %}
{% if let Some(comment) = entry.review().comment() %}
{% if !author && entry.review().spoilers() %}
<details class="comment spoiler">
<summary style="cursor:pointer;opacity:.75">Contains spoilers</summary>
{{ comment.value() }}
</details>
{% else %}
<div class="comment">{{ comment.value() }}</div>
{% endif %}
{% endif %}
<div class="watched-at">{{ entry.review().watched_at().format("%Y-%m-%d") }}</div>
{% if ctx.is_current_user(entry.review().user_id().value()) %}
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">