fix(gallery): a guest's own hidden feedback is hidden from them too (#1150) (#1157)

Stable twin of #1153.

Everything in the system treats a hidden row as absent, but the per-viewer is_liked heart read the row without looking at is_hidden — so a like the photographer had hidden still showed as liked on a photo whose like_count was zero.

Making that agree exposes the second half: the duplicate check behind like/favorite toggling did not skip hidden rows either, so the now-empty heart, when clicked, found the hidden row and toggled it OFF. Skipping hidden rows there makes the click create a fresh, visible row.

Also carried from review: the per-guest caps, /my-feedback and getEventFeedbackSummary no longer count hidden rows, and unhiding collapses the guest's replacement — skipped when there is no stable identity, since that fallback was 'guest_identifier IS NULL', i.e. other visitors' rows.

Not carried: the my_color_label badge (colour labels are #1044) and the clearScope / singleValueScope visibility fix, neither of which exists on this branch.

Merged with admin privileges: the author cannot self-approve.
This commit is contained in:
Paul Nothaft
2026-08-23 22:09:41 +02:00
committed by GitHub
parent eaa8b41ba3
commit b62cd2c290
4 changed files with 304 additions and 4 deletions
+45 -2
View File
@@ -124,7 +124,12 @@ class FeedbackService {
*/
async countGuestFeedback(eventId, feedbackType, guestId, guestIdentifier) {
const query = db('photo_feedback')
.where({ event_id: eventId, feedback_type: feedbackType });
// Hidden rows do not count against the guest's cap (#1150). They are
// absent everywhere else — the heart is empty, the tallies skip them,
// and submitFeedback now treats one as room for a fresh row. Counting
// them here would meet that fresh row with limit_reached and leave the
// control dead until the guest un-likes something they can still see.
.where({ event_id: eventId, feedback_type: feedbackType, is_hidden: false });
if (guestId) {
query.where('guest_id', guestId);
} else {
@@ -152,6 +157,13 @@ class FeedbackService {
photo_id: photoId,
event_id: eventId,
feedback_type,
// A hidden row is not there (#1150). Without this the guest saw an
// empty heart — every read surface treats hidden as absent — and
// clicking it found the hidden row and TOGGLED IT OFF, so the
// click appeared to do nothing and it took two more to get back to
// a filled heart. Skipping it makes the click create a fresh,
// visible row, which is what the guest is asking for.
is_hidden: false,
});
if (guest_id) {
duplicateQuery.where('guest_id', guest_id);
@@ -297,6 +309,11 @@ class FeedbackService {
const totalStats = await db('photo_feedback')
.where('event_id', eventId)
// Hidden rows do not count, the same rule the photo counters above
// already apply — without this the two halves of THIS response
// disagreed, and a hidden row preserved beside its replacement (#1150)
// is counted twice.
.where('is_hidden', false)
.select(
db.raw('COUNT(DISTINCT CASE WHEN feedback_type = ? THEN guest_identifier END) as unique_raters', ['rating']),
db.raw('COUNT(CASE WHEN feedback_type = ? THEN 1 END) as total_ratings', ['rating']),
@@ -379,7 +396,33 @@ class FeedbackService {
await db('photo_feedback')
.where('id', feedbackId)
.update(updates);
// Unhiding can collide with a replacement (#1150). A hidden row reads as
// absent, so the guest may well have re-added the same feedback in the
// meantime; making the original visible again would leave TWO visible
// rows for one guest on one photo — double-counted in the tallies, and
// needing two toggles to clear because each one deletes a single row.
//
// Needs a stable identity to scope by. With neither id nor identifier
// the fallback degrades to `guest_identifier IS NULL`, which is every
// identifier-less row on the photo — other people's, deleted. Nothing to
// converge in that case, so leave it alone. Comments are exempt: several
// from one guest on one photo is normal.
const collapseIdentity = feedback.guest_id || feedback.guest_identifier;
if (updates.is_hidden === false && feedback.feedback_type !== 'comment' && collapseIdentity) {
const superseded = db('photo_feedback')
.where({
photo_id: feedback.photo_id,
event_id: feedback.event_id,
feedback_type: feedback.feedback_type,
is_hidden: false,
})
.whereNot('id', feedbackId);
if (feedback.guest_id) superseded.where('guest_id', feedback.guest_id);
else superseded.where('guest_identifier', feedback.guest_identifier);
await superseded.delete();
}
// Update photo stats if visibility changed
await this.updatePhotoFeedbackStats(feedback.photo_id);