a04ecf7a19
- 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
157 lines
5.8 KiB
HTML
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>
|