feat: thème WhatsApp + fix envoi rich/compact + nav shop + refactor
All checks were successful
Deploy XIP / deploy (push) Successful in 43s

Theming
- Thème global piloté par variables CSS (:root + [data-theme]) appliqué via un
  attribut data-theme sur la racine app. Ajout du thème "WhatsApp" (bulles +
  palette verte, bulle sortante #005c4b) sans nouveau composant message.
- useTheme: type Theme étendu + THEME_LAYOUT (whatsapp = layout bulles).
- MessageList: sélection du composant par layout avec garde de repli
  (fini le <component :is="undefined">).
- Fix du thème "compact" cassé : nouveau MessageItemCompact.vue (variante dense).
- Surfaces migrées en variables : fond app/chat, header, bouton d'envoi, bulles.

Corrections
- Bug envoi rich/fichier : le backend exigeait un content texte non vide même
  en mode HTML/CSS/JS. Validation par présence (texte OU rich OU piece jointe) ;
  le front n'envoie plus d'espace bidon. Plus besoin de faux texte.
- Shop : suppression de "Tout voir", navigation forcee par categorie
  (defaut: Publicite).

Refactor (lisibilite)
- Parite perks backend (ip-colors, audio-alert, send-skin-*) ; /api/shop/me
  renvoie myPerks precalcule ; le front consomme directement (suppression de la
  derivation dupliquee + nettoyage d'un artefact de merge dans useMessages).
- Coherence composable-singleton : myPerks lu via useMyPerks() partout.
- Extraction du composer de HomePage vers ChatComposer.vue (HomePage = layout).
- Helper type parseMeta<T>() pour les metaJson (moins de any).
- vue-tsc --noEmit : 0 erreur.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 19:51:24 +02:00
parent c0b82222bd
commit aca608e520
17 changed files with 524 additions and 303 deletions

View File

@@ -48,37 +48,19 @@ export interface Message extends Reply {
const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:3000';
/** Refresh the viewer's own perks from the server (callable from anywhere). */
/**
* Refresh the viewer's own perks from the server (callable from anywhere).
* The backend computes the perks (entitlement.kind → Perks) and returns them
* precomputed as `myPerks`, so we just adopt them — no client-side re-derivation.
*/
export async function refreshMyPerks(): Promise<void> {
try {
const res = await fetch(`${API_URL}/api/shop/me`);
if (!res.ok) return;
const { entitlements } = (await res.json()) as {
entitlements: { kind: string; metaJson?: string | null }[];
};
const p: Perks = {};
const pets: { char: string; position: 'left' | 'right' | 'both' }[] = [];
for (const e of entitlements) {
let meta: any = {};
try { meta = e.metaJson ? JSON.parse(e.metaJson) : {}; } catch { /* */ }
if (e.kind === 'noads') { p.noads = true; if (meta.plan === 'annual') p.badge = true; }
if (e.kind === 'style-dore') p.skin = 'gold';
if (e.kind === 'pet' && meta.char) pets.push({ char: meta.char, position: meta.position ?? 'left' });
if (e.kind === 'element-skin') p.elementSkin = true; if (e.kind === 'ip-colors') p.ipColors = true;
if (e.kind.startsWith('send-skin-')) {
let meta2: any = {};
try { meta2 = e.metaJson ? JSON.parse(e.metaJson) : {}; } catch {}
if (!p.sendSkins) p.sendSkins = [];
p.sendSkins.push({ id: e.kind, char: meta2.char ?? '?', label: meta2.label });
} if (e.kind === 'rich-htmlcss') p.richHtmlcss = true;
if (e.kind === 'rich-js') p.richJs = true;
if (e.kind === 'no-file-limit') p.noFileLimit = true;
if (e.kind === 'audio-alert') p.audioAlert = true;
}
if (pets.length) p.pets = pets;
myPerks.value = p;
const { myPerks: p } = (await res.json()) as { myPerks?: Perks };
myPerks.value = p ?? {};
const { ip } = useWallet();
if (ip.value) setPerks(ip.value, p);
if (ip.value) setPerks(ip.value, myPerks.value);
} catch {
/* ignore */
}
@@ -186,7 +168,7 @@ export function useMessages() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: content.trim() || ' ',
content: content.trim(),
parentId: extras.parentId,
richMode: extras.richMode,
richContent: extras.richContent,
@@ -214,6 +196,8 @@ export function useMessages() {
fetchMyPerks();
});
// Note: viewer-own perks live in the module-level `myPerks` singleton; read
// them via `useMyPerks()` rather than off this return (consistency rule).
return {
messages,
loading,
@@ -222,7 +206,6 @@ export function useMessages() {
stats,
connected,
sendTyping,
get myPerks() { return myPerks; },
myIp,
fetchMyPerks,
};