Permet de consulter les jours précédents du Mur de la Honte, pour tout le monde
Build and deploy / deploy (push) Successful in 36s

wall_notes_today() devient wall_notes_for_date(p_date default null) — même garantie d'anonymat qu'avant, juste paramétrée par jour plutôt que figée sur aujourd'hui. Nouvelle RPC wall_note_days() (dates ayant au moins une note, sans contenu ni auteur) peuple un sélecteur de jour au-dessus de la grille, visible par tous — pas seulement les Archontes via l'historique complet, qui lui révèle l'auteur. wall_note_reaction_counts() gagne le même paramètre p_date. Le compteur "X/12 aujourd'hui" et l'éligibilité à publier restent toujours basés sur aujourd'hui, peu importe le jour consulté.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Valentin ROBIN
2026-08-25 18:15:34 +02:00
parent 203afafec2
commit 0eaebb7a05
4 changed files with 134 additions and 32 deletions
+12 -9
View File
@@ -6,7 +6,7 @@ Application web pour un groupe d'amis (~12 personnes) qui anime un jeu et des ol
## 1. État actuel
V1 à V14 sont livrées :
V1 à V15 sont livrées :
- **V1** : comptes, profils, leaderboard statique.
- **V2** : authentification par email réel (plus de pseudo+email interne), rôles `public`/`judge`, verrou de pseudo (modifiable une fois), section Admin.
- **V3** : attribution de points par les juges (RPC sécurisée, +/- avec confirmation explicite), journal public « le crieur », podium top 3, flèches de progression, recadrage/compression photo côté client.
@@ -25,6 +25,8 @@ V1 à V14 sont livrées :
- **V14** : Le Mur de la Honte (V9) s'ouvre aux Archontes — ils peuvent désormais publier une note anonyme comme n'importe quel Citoyen (`post_wall_note` n'a plus de vérification de rôle), et la limite quotidienne passe de 10 à 12 notes pour couvrir tout le groupe plutôt que les seuls Citoyens. La policy Storage d'upload sur `wall-images` perd elle aussi son filtre par rôle (sinon un Archonte aurait pu poster une note mais pas y joindre d'image). Contraste assumé avec V10 (l'Urne, où les Archontes ne votent ni ne reçoivent de jetons) : ici rien ne les distingue plus des Citoyens côté publication — ils gardent seulement, comme avant, leur visibilité privilégiée sur l'auteur de chaque note via « Historique complet ».
- **V15** : Le Mur de la Honte s'ouvre à la consultation des jours précédents, pour tout le monde (pas seulement les Archontes via « Historique complet », qui révèle lui l'auteur). `wall_notes_today()` devient `wall_notes_for_date(p_date default null)` — même garantie d'anonymat qu'avant (jamais de `user_id`), juste paramétrée par jour plutôt que hardcodée sur aujourd'hui. Nouvelle RPC `wall_note_days()` (dates ayant au moins une note, sans contenu ni auteur) peuple un sélecteur de jour au-dessus de la grille, visible par tous. Vue « aujourd'hui » toujours branchée sur le Realtime existant (`notes`/`reactionCounts`) ; un jour passé se charge à la demande dans un état séparé (`pastNotes`/`pastReactionCounts`, pas de Realtime dessus — un jour passé n'évolue plus) pour ne jamais mélanger les deux, notamment le compteur « X/12 aujourd'hui » et l'éligibilité à publier, qui restent toujours basés sur aujourd'hui quel que soit le jour affiché. `wall_note_reaction_counts()` gagne le même paramètre `p_date` (ancienne surcharge à 0 argument explicitement supprimée, même remarque que `post_wall_note`).
Hors périmètre pour l'instant (voir roadmap en fin de doc) : éditions/saisons, mode grand écran.
---
@@ -140,20 +142,21 @@ Deuxième volet de l'audit (revue complète de tout le code applicatif, pas seul
- `submit_chariot_question(p_text)` — authenticated, rejette les juges (seuls les Citoyens proposent), valide un texte non vide (≤ 300 caractères), et applique le même gel que `submit_icarus_score` via `settings.tribunal_date` (no-op silencieux une fois l'Agora commencée). `insert ... on conflict (user_id) do update` : une ligne par personne dans `chariot_submissions`, plus une ligne append-only dans `chariot_submission_history` à chaque écriture réelle (pas lors du no-op figé).
- `chariot_revealed_question()` — authenticated, `language sql`, seul moyen pour un non-juge de connaître la question actuellement révélée sans jamais lire `chariot_questions` (RLS juges uniquement, voir §3bis) — renvoie zéro ligne tant qu'aucune question n'est révélée, et calcule `question_number` (position dans l'ordre complet) côté serveur pour afficher « Question N » sur `/char` sans exposer les autres lignes.
- `admin_list_members()` — authenticated, vérifie `role = 'judge'`, seule façon d'exposer `auth.users.email` (jamais stocké dans `profiles`) sur `/admin` sans passer par la `service_role key` côté client.
- `wall_notes_today()` — authenticated, `language sql`, renvoie les notes du jour (heure de Paris), y compris `image_url`, **sans `user_id`** — seul moyen de laisser un Citoyen voir les notes des autres sans jamais exposer l'auteur côté client (la RLS ne peut pas masquer une colonne pour certaines lignes).
- `wall_notes_for_date(p_date default null)` — authenticated, `language sql`, renvoie les notes d'un jour donné (heure de Paris, `p_date` à `null` = aujourd'hui), y compris `image_url`, **sans `user_id`** — seul moyen de voir les notes des autres sans jamais exposer l'auteur côté client (la RLS ne peut pas masquer une colonne pour certaines lignes), ouvert à tout le monde y compris pour les jours précédents depuis V15.
- `wall_note_days()` — authenticated, `language sql`, renvoie les dates ayant au moins une note (jamais le contenu ni l'auteur) — peuple le sélecteur de jour de `wall_notes_for_date` sur `/mur`.
- `post_wall_note(p_text, p_image_url default null)` — authenticated, ouvert à tout le monde y compris les juges depuis V14 (plus de vérification de rôle), valide un texte non vide (≤ 200 caractères), puis vérifie que l'appelant n'a pas déjà publié aujourd'hui et que le mur du jour a moins de 12 notes (sinon `raise exception` dans les deux cas — pas de no-op silencieux ici, l'UI doit remonter l'erreur). `p_image_url` est facultatif et doit venir du bucket `wall-images` (vérifié par un `like`).
- `wall_note_reaction_counts()` — authenticated, `language sql`, renvoie `(note_id, emoji, count)` agrégé sur les notes du jour — même principe que `urn_vote_counts` : jamais qui a réagi, seulement le total.
- `wall_note_reaction_counts(p_date default null)` — authenticated, `language sql`, renvoie `(note_id, emoji, count)` agrégé sur les notes d'un jour donné (même paramètre `p_date` que `wall_notes_for_date`, depuis V15) — même principe que `urn_vote_counts` : jamais qui a réagi, seulement le total.
- `toggle_wall_note_reaction(p_note_id, p_emoji)` — authenticated, bascule une réaction (ajoute si absente, retire si déjà posée en un `delete` puis, si rien n'a été supprimé, un `insert`). `p_emoji` limité à une liste fermée (contrainte `check` sur la colonne, revérifiée dans la RPC pour un message d'erreur clair).
- `urn_vote_counts()` — authenticated, `language sql`, renvoie `(target_id, votes)` agrégé sur tout l'historique (pas de filtre de date, contrairement à `wall_notes_today`) — seul moyen de calculer un total public sans jamais exposer une ligne individuelle (qui a voté pour qui).
- `urn_vote_reasons(p_target_id)` — authenticated, `language sql`, renvoie les justifications (`reason`, `created_at`) laissées pour une personne, **sans `voter_id`** — même principe que `wall_notes_today` : le contenu est public depuis le classement, l'auteur ne l'est jamais.
- `urn_vote_counts()` — authenticated, `language sql`, renvoie `(target_id, votes)` agrégé sur tout l'historique (pas de filtre de date, contrairement à `wall_notes_for_date`) — seul moyen de calculer un total public sans jamais exposer une ligne individuelle (qui a voté pour qui).
- `urn_vote_reasons(p_target_id)` — authenticated, `language sql`, renvoie les justifications (`reason`, `created_at`) laissées pour une personne, **sans `voter_id`** — même principe que `wall_notes_for_date` : le contenu est public depuis le classement, l'auteur ne l'est jamais.
- `cast_urn_vote(p_target_id, p_reason default null)` — authenticated, rejette les juges (ni votants ni cibles), rejette le vote pour soi-même, vérifie que l'appelant n'a pas déjà voté aujourd'hui (heure de Paris) — vote définitif, aucune RPC de modification/suppression. `p_reason` est une justification facultative (≤ 200 caractères, `nullif(trim(...), '')` pour normaliser une chaîne vide en `null`).
### `public.wall_notes` (Le Mur de la Honte, V9)
- `id, user_id, text, image_url, created_at` — une ligne par note, jamais éditée/supprimée (comme `points_log`) ; le « reset quotidien » n'est qu'un filtrage par date dans `wall_notes_today()`, pas une suppression. `image_url` est facultative, pointe vers le bucket `wall-images`.
- RLS `select` : le propriétaire voit sa propre note (sert à afficher « tu as déjà publié aujourd'hui » et à réafficher son propre texte), les juges voient tout avec l'auteur (embed `profiles(pseudo, avatar_url)`, section « Historique complet » sur `/mur`) — pas de visibilité entre Citoyens, c'est tout l'intérêt de `wall_notes_today()`.
- `id, user_id, text, image_url, created_at` — une ligne par note, jamais éditée/supprimée (comme `points_log`) ; le « reset quotidien » du compteur de publication n'est qu'un filtrage par date dans `wall_notes_for_date()`, pas une suppression — les notes restent consultables (anonymement) les jours suivants depuis V15. `image_url` est facultative, pointe vers le bucket `wall-images`.
- RLS `select` : le propriétaire voit sa propre note (sert à afficher « tu as déjà publié aujourd'hui » et à réafficher son propre texte), les juges voient tout avec l'auteur (embed `profiles(pseudo, avatar_url)`, section « Historique complet » sur `/mur`) — pas de visibilité entre Citoyens, c'est tout l'intérêt de `wall_notes_for_date()`.
- Pas de gel via `settings.tribunal_date` : contrairement au Char, c'est une mécanique de toute la semaine.
- Bucket Storage `wall-images` (public, `file_size_limit` 3 Mo, `allowed_mime_types` image/webp+jpeg+png — même limite que le bucket `avatars`, voir §3bis) : contrairement à `avatars`, le chemin d'objet est un UUID **sans le `user_id`** — sinon l'auteur fuiterait via l'URL publique de l'image, ce que `wall_notes_today()` prend justement soin de ne jamais exposer. Compression côté client avant upload (`resizeImageToBlob`, `src/lib/image.ts`, contrairement à `cropImageToBlob` qui force un carré pour les avatars) ; la limite du bucket n'est qu'un filet de sécurité, pas la seule garantie.
- Bucket Storage `wall-images` (public, `file_size_limit` 3 Mo, `allowed_mime_types` image/webp+jpeg+png — même limite que le bucket `avatars`, voir §3bis) : contrairement à `avatars`, le chemin d'objet est un UUID **sans le `user_id`** — sinon l'auteur fuiterait via l'URL publique de l'image, ce que `wall_notes_for_date()` prend justement soin de ne jamais exposer. Compression côté client avant upload (`resizeImageToBlob`, `src/lib/image.ts`, contrairement à `cropImageToBlob` qui force un carré pour les avatars) ; la limite du bucket n'est qu'un filet de sécurité, pas la seule garantie.
- `public.wall_note_reactions` (`id, note_id, user_id, emoji, created_at`, `unique (note_id, user_id, emoji)`) : réactions emoji sur une note, même principe d'anonymat que les notes elles-mêmes — RLS `select` limitée à `user_id = auth.uid()` (pour savoir lesquelles sont déjà activées), le total public passe uniquement par `wall_note_reaction_counts()`.
### `public.urn_votes` (L'Urne de l'Agora, V10)
@@ -181,7 +184,7 @@ Deuxième volet de l'audit (revue complète de tout le code applicatif, pas seul
| `/corne` | « La Corne d'Abondance » — jeu de fusion façon Watermelon Game (avatars aléatoires, physique `matter-js`), même mécanique de gloires que Le Vol d'Icare |
| `/char` | « Le Jeu de l'Agora » — format paysage (projeté) : Le Char (2 colonnes de 3, boutons de duel), Le Gardien du Silence (tirage aléatoire, minuteur discret) et La Roulette (tirage au sort animé) |
| `/char/questions` | Banque de questions du Char (ajout/édition/réordonnancement/afficher-masquer) + modération des suggestions des Citoyens (ajouter à la banque/rejeter) — juges uniquement, pilotée depuis un téléphone pendant que `/char` est projetée |
| `/mur` | « Le Mur de la Honte » — une note anonyme par personne par jour, Archontes compris depuis V14 (douze places, remis à zéro chaque jour) ; les Archontes y voient en plus l'historique complet avec l'auteur de chaque note |
| `/mur` | « Le Mur de la Honte » — une note anonyme par personne par jour, Archontes compris depuis V14 (douze places, remis à zéro chaque jour) ; les jours précédents restent consultables anonymement par tous depuis V15 (sélecteur de jour) ; les Archontes y voient en plus l'historique complet avec l'auteur de chaque note |
| `/urne` | « L'Urne de l'Agora » — un jeton par Citoyen par jour sur une autre personne, classement cumulatif public identique pour tous, Archontes compris (aucune vision privilégiée ici) |
| `/calendrier` | « Le Calendrier des Dieux » — agenda de la semaine, panthéon, date du Tribunal, édition réservée aux juges. Retirée du menu de navigation (la date du Tribunal, seule info largement utile au quotidien, est reprise sur `/leaderboard`) mais la route reste fonctionnelle — les juges y accèdent directement par l'URL pour gérer le calendrier et changer la date |
| `/journal` | « Le Crieur » — fil live des décrets (attributions de points), lecture pour tous |
+76 -10
View File
@@ -149,6 +149,7 @@ export function MurView({
initialHistory,
initialReactionCounts,
initialMyReactions,
availableDays,
}: {
isJudge: boolean;
currentUserId: string;
@@ -157,6 +158,7 @@ export function MurView({
initialHistory: WallHistoryRow[];
initialReactionCounts: ReactionCountRow[];
initialMyReactions: MyReactionRow[];
availableDays: string[];
}) {
const [notes, setNotes] = useState(initialNotes);
const [myNoteToday, setMyNoteToday] = useState(initialMyNoteToday);
@@ -171,6 +173,15 @@ export function MurView({
const [error, setError] = useState<string | null>(null);
const [historyDateFilter, setHistoryDateFilter] = useState("all");
const [zoomedImage, setZoomedImage] = useState<string | null>(null);
// Jour affiché dans la grille du haut (ouverte à tout le monde,
// contrairement à l'historique complet réservé aux Archontes plus bas) :
// null = aujourd'hui, réactif au Realtime comme avant. Un jour passé se
// charge à la demande (pas de Realtime dessus, un jour passé n'évolue
// plus) dans un état séparé pour ne jamais mélanger les deux.
const [viewedDate, setViewedDate] = useState<string | null>(null);
const [pastNotes, setPastNotes] = useState<WallNoteRow[]>([]);
const [pastReactionCounts, setPastReactionCounts] = useState<Record<string, Record<string, number>>>({});
const [pastLoading, setPastLoading] = useState(false);
const refetch = useCallback(async () => {
const supabase = createClient();
@@ -228,6 +239,27 @@ export function MurView({
};
}, [refetch]);
const fetchDay = useCallback(async (dateKey: string) => {
const supabase = createClient();
const [{ data: dayNotes }, { data: dayCounts }] = await Promise.all([
supabase.rpc("wall_notes_for_date", { p_date: dateKey }),
supabase.rpc("wall_note_reaction_counts", { p_date: dateKey }),
]);
setPastNotes(dayNotes ?? []);
setPastReactionCounts(buildReactionCounts(dayCounts ?? []));
}, []);
async function handleSelectDate(value: string) {
if (value === "today") {
setViewedDate(null);
return;
}
setViewedDate(value);
setPastLoading(true);
await fetchDay(value);
setPastLoading(false);
}
function handleImageChange(event: ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
event.target.value = "";
@@ -318,7 +350,11 @@ export function MurView({
}
async function handleToggleReaction(noteId: string, emoji: string) {
// Optimiste : le canal Realtime confirmera (ou corrigera) juste après.
// Optimiste : le canal Realtime confirmera (ou corrigera) juste après
// sauf en dehors d'aujourd'hui, où il n'y a pas de Realtime (un jour
// passé n'évolue plus) : on corrige alors en rechargeant ce jour-là.
const setCounts = viewedDate === null ? setReactionCounts : setPastReactionCounts;
setMyReactions((current) => {
const next = new Set(current[noteId] ?? []);
const wasActive = next.has(emoji);
@@ -326,7 +362,7 @@ export function MurView({
else next.add(emoji);
return { ...current, [noteId]: next };
});
setReactionCounts((current) => {
setCounts((current) => {
const wasActive = myReactions[noteId]?.has(emoji) ?? false;
const noteCounts = { ...(current[noteId] ?? {}) };
noteCounts[emoji] = Math.max(0, (noteCounts[emoji] ?? 0) + (wasActive ? -1 : 1));
@@ -338,11 +374,22 @@ export function MurView({
p_note_id: noteId,
p_emoji: emoji,
});
if (rpcError) refetch();
if (rpcError) {
if (viewedDate === null) refetch();
else fetchDay(viewedDate);
}
}
// wallFull/le compteur restent toujours basés sur aujourd'hui (notes),
// jamais sur le jour affiché (displayedNotes) : consulter un jour passé
// ne doit jamais changer l'éligibilité à publier aujourd'hui.
const wallFull = notes.length >= MAX_NOTES_PER_DAY;
const todayKey = new Date().toLocaleDateString("sv-SE", { timeZone: "Europe/Paris" });
const pastDays = availableDays.filter((day) => day !== todayKey);
const displayedNotes = viewedDate === null ? notes : pastNotes;
const displayedReactionCounts = viewedDate === null ? reactionCounts : pastReactionCounts;
const historyDates = Array.from(new Set(history.map((n) => localDateKey(n.created_at)))).sort((a, b) =>
b.localeCompare(a),
);
@@ -352,19 +399,38 @@ export function MurView({
return (
<div className="flex flex-col gap-6">
<div>
<p className="mb-3 text-center text-xs font-medium tracking-wide text-marble/60 uppercase">
{notes.length} / {MAX_NOTES_PER_DAY}{" "}
aujourd&apos;hui
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
<p className="text-xs font-medium tracking-wide text-marble/60 uppercase">
{viewedDate === null ? `${notes.length} / ${MAX_NOTES_PER_DAY} aujourd'hui` : formatDateLabel(viewedDate)}
</p>
{pastDays.length > 0 && (
<select
value={viewedDate ?? "today"}
onChange={(event) => handleSelectDate(event.target.value)}
className="rounded-md border border-gold/30 bg-white/50 px-2 py-1 text-xs text-text-marble outline-none focus:border-gold"
>
<option value="today">Aujourd&apos;hui</option>
{pastDays.map((day) => (
<option key={day} value={day}>
{formatDateLabel(day)}
</option>
))}
</select>
)}
</div>
{pastLoading ? (
<p className="text-center text-sm text-marble/60">Chargement</p>
) : displayedNotes.length === 0 ? (
<p className="text-center text-sm text-marble/60">
{viewedDate === null ? "Le mur est encore vide aujourd'hui." : "Le mur était vide ce jour-là."}
</p>
{notes.length === 0 ? (
<p className="text-center text-sm text-marble/60">Le mur est encore vide aujourd&apos;hui.</p>
) : (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
{notes.map((note) => (
{displayedNotes.map((note) => (
<NoteCard
key={note.id}
note={note}
reactionCounts={reactionCounts[note.id] ?? {}}
reactionCounts={displayedReactionCounts[note.id] ?? {}}
myReactions={myReactions[note.id] ?? new Set()}
onToggleReaction={(emoji) => handleToggleReaction(note.id, emoji)}
onImageClick={setZoomedImage}
+7 -3
View File
@@ -44,11 +44,13 @@ export default async function MurPage() {
.single();
const isJudge = callerProfile?.role === "judge";
const [{ data: todayNotes }, { data: reactionCounts }, { data: myReactions }] = await Promise.all([
supabase.rpc("wall_notes_today"),
const [{ data: todayNotes }, { data: reactionCounts }, { data: myReactions }, { data: days }] = await Promise.all([
supabase.rpc("wall_notes_for_date"),
supabase.rpc("wall_note_reaction_counts"),
supabase.from("wall_note_reactions").select("note_id, emoji").eq("user_id", user.id),
supabase.rpc("wall_note_days"),
]);
const availableDays = (days ?? []).map((row: { note_date: string }) => row.note_date);
// Les Archontes peuvent désormais publier aussi (comme les Citoyens) :
// besoin de savoir s'ils ont déjà posté aujourd'hui, peu importe le rôle.
@@ -74,7 +76,8 @@ export default async function MurPage() {
<div className="mb-6 text-center">
<h1 className="font-heading text-2xl tracking-[0.1em] text-gold-bright uppercase">Le Mur de la Honte</h1>
<p className="font-serif text-sm text-marble/60 italic">
Une note anonyme par jour, douze places sur le mur la journée passée, tout s&apos;efface.
Une note anonyme par jour, douze places sur le mur les jours précédents restent consultables, toujours
anonymement.
</p>
</div>
<MurView
@@ -85,6 +88,7 @@ export default async function MurPage() {
initialHistory={history}
initialReactionCounts={reactionCounts ?? []}
initialMyReactions={myReactions ?? []}
availableDays={availableDays}
/>
</div>
);
+38 -9
View File
@@ -1369,12 +1369,16 @@ end $$;
-- Seul moyen pour un Citoyen de voir les notes des autres sans jamais
-- exposer qui les a écrites côté client : la RLS ne peut pas masquer une
-- colonne pour certaines lignes, donc cette RPC (SECURITY DEFINER) fait le
-- filtre elle-même et ne renvoie jamais user_id. Le type de retour change
-- (ajout d'image_url) : drop explicite requis, create or replace ne peut
-- pas changer le type de retour d'une fonction existante.
-- filtre elle-même et ne renvoie jamais user_id. Paramétrée par jour
-- (p_date, Europe/Paris) plutôt que hardcodée sur aujourd'hui — remplace
-- wall_notes_today(), pour que n'importe qui (pas seulement les Archontes
-- via « Historique complet » plus bas, qui révèle lui l'auteur) puisse
-- consulter les jours précédents, toujours sans jamais exposer qui a
-- écrit quoi. p_date à null (par défaut) = aujourd'hui, pour que l'appel
-- de chargement initial de la page reste un simple appel sans argument.
drop function if exists public.wall_notes_today();
create or replace function public.wall_notes_today()
create or replace function public.wall_notes_for_date(p_date date default null)
returns table (id uuid, text text, image_url text, created_at timestamptz)
language sql
security definer
@@ -1382,11 +1386,28 @@ set search_path = public
as $$
select w.id, w.text, w.image_url, w.created_at
from public.wall_notes w
where (w.created_at at time zone 'Europe/Paris')::date = (now() at time zone 'Europe/Paris')::date
where (w.created_at at time zone 'Europe/Paris')::date = coalesce(p_date, (now() at time zone 'Europe/Paris')::date)
order by w.created_at asc;
$$;
grant execute on function public.wall_notes_today() to authenticated;
grant execute on function public.wall_notes_for_date(date) to authenticated;
-- Liste des jours ayant au moins une note (jamais le contenu ni l'auteur,
-- juste la date) : peuple le sélecteur de jour de wall_notes_for_date()
-- côté client, pour tout le monde — contrairement à l'historique complet
-- avec auteur plus bas, réservé aux Archontes.
create or replace function public.wall_note_days()
returns table (note_date date)
language sql
security definer
set search_path = public
as $$
select distinct (w.created_at at time zone 'Europe/Paris')::date as note_date
from public.wall_notes w
order by note_date desc;
$$;
grant execute on function public.wall_note_days() to authenticated;
-- Seul point d'entrée pour publier une note. Pas de gel via tribunal_date
-- (contrairement au Char) : contrairement à la soirée de clôture, c'est une
@@ -1523,7 +1544,15 @@ exception
when duplicate_object then null;
end $$;
create or replace function public.wall_note_reaction_counts()
-- Paramétrée par jour (comme wall_notes_for_date ci-dessus, même p_date à
-- null = aujourd'hui, donc les anciens appels sans argument continuent de
-- fonctionner à l'identique). L'ancienne surcharge à 0 argument est
-- explicitement supprimée : create or replace ne remplace que la même
-- signature, sinon les deux versions coexisteraient (même remarque que
-- pour post_wall_note plus haut).
drop function if exists public.wall_note_reaction_counts();
create or replace function public.wall_note_reaction_counts(p_date date default null)
returns table (note_id uuid, emoji text, count bigint)
language sql
security definer
@@ -1532,11 +1561,11 @@ as $$
select r.note_id, r.emoji, count(*) as count
from public.wall_note_reactions r
join public.wall_notes w on w.id = r.note_id
where (w.created_at at time zone 'Europe/Paris')::date = (now() at time zone 'Europe/Paris')::date
where (w.created_at at time zone 'Europe/Paris')::date = coalesce(p_date, (now() at time zone 'Europe/Paris')::date)
group by r.note_id, r.emoji;
$$;
grant execute on function public.wall_note_reaction_counts() to authenticated;
grant execute on function public.wall_note_reaction_counts(date) to authenticated;
-- Bascule une réaction (ajoute si absente, retire si déjà posée) en un seul
-- aller-retour : un delete puis, si rien n'a été supprimé, un insert —