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:
@@ -17,6 +17,7 @@ pub struct LogReviewCommand {
|
||||
pub comment: Option<String>,
|
||||
pub watched_at: NaiveDateTime,
|
||||
pub watch_medium: Option<WatchMedium>,
|
||||
pub spoilers: bool,
|
||||
}
|
||||
|
||||
pub struct DeleteReviewCommand {
|
||||
@@ -31,6 +32,7 @@ pub struct EditReviewCommand {
|
||||
pub comment: Option<Option<String>>,
|
||||
pub watched_at: Option<NaiveDateTime>,
|
||||
pub watch_medium: Option<Option<WatchMedium>>,
|
||||
pub spoilers: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -34,6 +34,7 @@ pub async fn execute(deps: &EditReviewDeps, cmd: EditReviewCommand) -> Result<()
|
||||
.transpose()?,
|
||||
watched_at: cmd.watched_at,
|
||||
watch_medium: cmd.watch_medium,
|
||||
spoilers: cmd.spoilers,
|
||||
});
|
||||
|
||||
deps.review.update_review(&updated).await?;
|
||||
|
||||
@@ -73,6 +73,7 @@ impl ReviewLogger for DefaultReviewLogger {
|
||||
comment,
|
||||
cmd.watched_at,
|
||||
cmd.watch_medium,
|
||||
cmd.spoilers,
|
||||
)?;
|
||||
self.review_repo.save_review(&review).await?;
|
||||
let review_event = DomainEvent::ReviewLogged {
|
||||
|
||||
@@ -33,6 +33,7 @@ fn make_review(movie_id: MovieId, user_id: UserId) -> Review {
|
||||
None,
|
||||
Utc::now().naive_utc(),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ fn make_review(user_id: UserId) -> Review {
|
||||
Some(Comment::new("original comment".into()).unwrap()),
|
||||
Utc::now().naive_utc(),
|
||||
Some(WatchMedium::Streaming),
|
||||
false,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
@@ -62,6 +63,7 @@ async fn edit_own_review_updates_rating() {
|
||||
comment: None,
|
||||
watched_at: None,
|
||||
watch_medium: None,
|
||||
spoilers: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -87,6 +89,7 @@ async fn edit_nonexistent_review_returns_not_found() {
|
||||
comment: None,
|
||||
watched_at: None,
|
||||
watch_medium: None,
|
||||
spoilers: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -108,6 +111,7 @@ async fn edit_other_users_review_returns_forbidden() {
|
||||
comment: None,
|
||||
watched_at: None,
|
||||
watch_medium: None,
|
||||
spoilers: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -129,6 +133,7 @@ async fn edit_publishes_review_updated_event() {
|
||||
comment: None,
|
||||
watched_at: None,
|
||||
watch_medium: None,
|
||||
spoilers: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -155,6 +160,7 @@ async fn edit_sets_watch_medium() {
|
||||
comment: None,
|
||||
watched_at: None,
|
||||
watch_medium: Some(Some(WatchMedium::Cinema)),
|
||||
spoilers: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -57,6 +57,7 @@ async fn user_feed_carries_author_and_entries() {
|
||||
None,
|
||||
chrono::Utc::now().naive_utc(),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
diary.set_diary_page(Paginated {
|
||||
|
||||
@@ -67,6 +67,7 @@ async fn test_log_review_creates_movie_and_review() {
|
||||
comment: None,
|
||||
watched_at: Utc::now().naive_utc(),
|
||||
watch_medium: None,
|
||||
spoilers: false,
|
||||
};
|
||||
|
||||
log_review::execute(&logger, cmd).await.unwrap();
|
||||
@@ -100,6 +101,7 @@ async fn test_log_review_reuses_existing_movie() {
|
||||
comment: None,
|
||||
watched_at: Utc::now().naive_utc(),
|
||||
watch_medium: None,
|
||||
spoilers: false,
|
||||
};
|
||||
|
||||
log_review::execute(&logger, cmd).await.unwrap();
|
||||
@@ -122,6 +124,7 @@ async fn test_log_review_with_invalid_rating_fails() {
|
||||
comment: None,
|
||||
watched_at: Utc::now().naive_utc(),
|
||||
watch_medium: None,
|
||||
spoilers: false,
|
||||
};
|
||||
let result = log_review::execute(&logger, cmd).await;
|
||||
assert!(result.is_err(), "rating > 5 should fail");
|
||||
|
||||
@@ -57,6 +57,7 @@ async fn logs_review_with_manual_movie() {
|
||||
comment: None,
|
||||
watched_at: Utc::now().naive_utc(),
|
||||
watch_medium: None,
|
||||
spoilers: false,
|
||||
};
|
||||
|
||||
logger.log_review(cmd).await.unwrap();
|
||||
@@ -106,6 +107,7 @@ async fn removes_from_watchlist_on_review() {
|
||||
comment: None,
|
||||
watched_at: Utc::now().naive_utc(),
|
||||
watch_medium: None,
|
||||
spoilers: false,
|
||||
};
|
||||
|
||||
logger.log_review(cmd).await.unwrap();
|
||||
@@ -144,6 +146,7 @@ async fn logs_review_with_existing_movie_by_id() {
|
||||
comment: None,
|
||||
watched_at: Utc::now().naive_utc(),
|
||||
watch_medium: None,
|
||||
spoilers: false,
|
||||
};
|
||||
|
||||
logger.log_review(cmd).await.unwrap();
|
||||
@@ -173,6 +176,7 @@ async fn existing_movie_not_found_returns_error() {
|
||||
comment: None,
|
||||
watched_at: Utc::now().naive_utc(),
|
||||
watch_medium: None,
|
||||
spoilers: false,
|
||||
};
|
||||
|
||||
assert!(logger.log_review(cmd).await.is_err());
|
||||
@@ -199,6 +203,7 @@ async fn invalid_rating_returns_error() {
|
||||
comment: None,
|
||||
watched_at: Utc::now().naive_utc(),
|
||||
watch_medium: None,
|
||||
spoilers: false,
|
||||
};
|
||||
|
||||
let result = logger.log_review(cmd).await;
|
||||
@@ -229,6 +234,7 @@ async fn watchlist_not_present_does_not_publish_removed() {
|
||||
comment: None,
|
||||
watched_at: Utc::now().naive_utc(),
|
||||
watch_medium: None,
|
||||
spoilers: false,
|
||||
};
|
||||
|
||||
logger.log_review(cmd).await.unwrap();
|
||||
@@ -296,6 +302,7 @@ async fn publishes_movie_discovered_for_new_movie_with_external_id() {
|
||||
comment: None,
|
||||
watched_at: Utc::now().naive_utc(),
|
||||
watch_medium: None,
|
||||
spoilers: false,
|
||||
};
|
||||
|
||||
logger.log_review(cmd).await.unwrap();
|
||||
|
||||
@@ -119,5 +119,6 @@ fn row_to_command(row: &ImportRow, user_id: Uuid) -> Result<LogReviewCommand, St
|
||||
comment: row.comment.clone(),
|
||||
watched_at,
|
||||
watch_medium: None,
|
||||
spoilers: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ pub async fn execute(
|
||||
comment: c.comment,
|
||||
watched_at: *event.watched_at(),
|
||||
watch_medium: Some(domain::value_objects::WatchMedium::MediaServer),
|
||||
spoilers: false,
|
||||
};
|
||||
|
||||
deps.review_logger.log_review(review_cmd).await?;
|
||||
|
||||
Reference in New Issue
Block a user