Files
ChatApp/design_handoff_chatapp/ChatApp Redesign.html
T
byGalax a04ecf7a19 feat: voice messages, offline queue, delivery ticks, volume slider, admin + scaling
- Voice messages: MediaRecorder → encrypted attachment, custom waveform
  player via OfflineAudioContext, 60s limit + live mic-level meter
- Offline message queue: localStorage outbox, exponential backoff retries,
  optimistic pending bubble with retry/discard
- Delivery indicator: message_deliveries table + RLS (reciprocal receipts),
  ✓ / ✓✓ / ✓✓-blue tick states, group-aware (all members must ack)
- Per-participant volume slider in calls via right-click tile menu,
  persisted to localStorage, applied to attached audio elements
- Group call scaling: grid up to 12 tiles with pagination,
  active-speaker auto-promotion in fullscreen
- Push notifications scaffolding: service worker, VAPID subscription
  registration, notify-push edge function skeleton
- Backup recovery code: 24-char base32 code (~120 bits entropy) as
  alternative decrypt path, restore UI with mode toggle
- Admin panel: conversations list, audit log (admin_audit_log table +
  admin_log_action RPC), audit entry on user flag toggle
- Search v2: sender filter, attachment-only toggle, date range
- Reactions pop animation (scale 0.4→1.15→1 on count change)
- Message list windowing (150 default, expand via IntersectionObserver)
- Stub cleanup: removed dead ScreenshareStub from CallParticipantTile

Fixes:
- Focus-triggered flicker: dropped window.focus listeners in three spots,
  throttled visibilitychange/online wake-refreshes to 30s, keep existing
  data visible during background re-syncs (no more spinner on every click)
- Voice attachment audio element collapsed to 0px on peer side — now
  forces 280px min-width on bubble

Migrations (push required):
  20260421000001_message_deliveries.sql
  20260421000002_admin_audit_log.sql

Server TODO:
  VAPID keys + notify-push edge function deploy
2026-04-21 01:14:16 +02:00

157 lines
5.8 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>ChatApp Redesign — 6 Varianten</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Geist:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@500;600;700;800&family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:wght@600;700;800&family=Archivo+Black&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles/base.css">
<link rel="stylesheet" href="styles/variants.css">
<link rel="stylesheet" href="styles/rail.css">
<link rel="stylesheet" href="styles/call.css">
<link rel="stylesheet" href="styles/pages.css">
<style>
/* Cache-busting inline override for rail page-mode grid */
.rail-layout.page-mode { grid-template-columns: 64px 1fr !important; }
</style>
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
</head>
<body>
<!-- TWEAKS state -->
<script>
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"showCall": false,
"density": "normal",
"font": "default"
}/*EDITMODE-END*/;
</script>
<!-- Variant switcher -->
<div class="variant-switcher" id="variant-switcher"></div>
<div class="variant-container" id="root"></div>
<!-- Tweaks panel -->
<div class="tweaks-panel" id="tweaks-panel">
<h3>Tweaks</h3>
<div class="tweaks-row">
<label>Incoming call overlay</label>
<select id="tweak-call">
<option value="false">Off</option>
<option value="true">Show</option>
</select>
</div>
<div class="tweaks-row">
<label>Content density</label>
<select id="tweak-density">
<option value="normal">Normal</option>
<option value="compact">Compact</option>
<option value="spacious">Spacious</option>
</select>
</div>
<div class="tweaks-row" style="font-size: 11px; opacity: 0.6; margin-top: 16px;">
Tipp: Klicke oben auf die Varianten um zu wechseln.
</div>
</div>
<script src="components/icons.jsx" type="text/babel"></script>
<script src="components/data.jsx" type="text/babel"></script>
<script src="components/call.jsx" type="text/babel"></script>
<script src="components/pages.jsx" type="text/babel"></script>
<script src="components/app.jsx" type="text/babel"></script>
<script type="text/babel">
const VARIANTS = [
{ key: 'clean', label: 'Clean Minimal', layout: 'topnav' },
{ key: 'clean-rail', label: 'Clean Rail', baseKey: 'clean', layout: 'rail' },
{ key: 'playful', label: 'Playful', layout: 'topnav' },
{ key: 'y2k', label: 'Y2K Glass', layout: 'topnav' },
{ key: 'cyber', label: 'Cyberpunk', layout: 'topnav' },
{ key: 'warm', label: 'Warm Soft', layout: 'topnav' },
{ key: 'brutal', label: 'Brutal Gaming', layout: 'topnav' },
];
const App = () => {
const [activeVariant, setActiveVariant] = React.useState(() => {
return localStorage.getItem('chatapp-variant') || 'clean';
});
const [isDark, setIsDark] = React.useState(() => {
return localStorage.getItem('chatapp-dark') === 'true';
});
const [tweaks, setTweaks] = React.useState(TWEAK_DEFAULTS);
React.useEffect(() => {
localStorage.setItem('chatapp-variant', activeVariant);
}, [activeVariant]);
React.useEffect(() => {
localStorage.setItem('chatapp-dark', String(isDark));
}, [isDark]);
// Tweaks wiring
React.useEffect(() => {
const handler = (e) => {
if (e.data?.type === '__activate_edit_mode') {
document.getElementById('tweaks-panel').classList.add('open');
} else if (e.data?.type === '__deactivate_edit_mode') {
document.getElementById('tweaks-panel').classList.remove('open');
}
};
window.addEventListener('message', handler);
window.parent.postMessage({type: '__edit_mode_available'}, '*');
const callSel = document.getElementById('tweak-call');
const densSel = document.getElementById('tweak-density');
callSel.value = String(tweaks.showCall);
densSel.value = tweaks.density;
callSel.onchange = () => {
const v = callSel.value === 'true';
setTweaks(t => ({...t, showCall: v}));
window.parent.postMessage({type: '__edit_mode_set_keys', edits: {showCall: v}}, '*');
};
densSel.onchange = () => {
setTweaks(t => ({...t, density: densSel.value}));
window.parent.postMessage({type: '__edit_mode_set_keys', edits: {density: densSel.value}}, '*');
};
return () => window.removeEventListener('message', handler);
}, []);
// Render variant switcher buttons
React.useEffect(() => {
const sw = document.getElementById('variant-switcher');
sw.innerHTML = '';
VARIANTS.forEach((v, i) => {
const b = document.createElement('button');
b.textContent = `${String(i+1).padStart(2,'0')} ${v.label}`;
if (v.key === activeVariant) b.classList.add('active');
b.onclick = () => setActiveVariant(v.key);
sw.appendChild(b);
});
}, [activeVariant]);
const idx = VARIANTS.findIndex(v => v.key === activeVariant);
const variant = { ...VARIANTS[idx], idx };
return (
<ChatApp
key={activeVariant}
variant={variant}
showCall={tweaks.showCall}
showProfile={false}
isDark={isDark}
onToggleDark={() => setIsDark(d => !d)}
/>
);
};
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>