chahinebrini 5c539f8937 feat(presence,sheets,chat): tester-build polish bundle
Online-Status (Phase 1+):
- UserAvatar mit 4 Size-Variants (sm/md/lg/xl) + integrierter Online-Dot
- OnlinePresenceProvider: Supabase-Channel + Following-Filter
- ChatHeaderStatus: "Online" neutral / "vor X min" offline
- useLastSeen + Heartbeat (60s interval + AppState-background ping)
- Privatsphäre-Toggle in profile/index

Sheets:
- FormSheet Android-keyboard-fix (Dimensions.get('screen'), kein
  useWindowDimensions-Kollaps), useKeyboardHandler statt manual
  Keyboard.addListener, state-reset on re-open
- PostCommentsSheet same Pattern + close-after-submit + drag bis under
  app-header
- ConnectMailSheet form-view refactor: scrollable, AES-Banner als
  footnote, field-order email→pw→label, fixed 0.85 über alle Steps

Chat:
- DmChatBackground iOS klecks fix (G transform statt nested Svg)
- ChatInput Lyra-1:1 (keyboardWillShow, surfaceElevated bubble,
  arrow-up send, attachment links)
- dm/room/chat headers + conversation-list nutzen UserAvatar
- Foreign-Profile "Nachricht"-Button öffnet richtige DM

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 08:06:47 +02:00

103 lines
2.8 KiB
TypeScript

import { useEffect, useState } from 'react';
import { apiFetch } from '../lib/api';
export type Plan = 'free' | 'pro' | 'legend';
/**
* Single source of truth für den eingeloggten User. /api/auth/me joint
* `auth.users` mit `rebreak.profiles` server-side — wir bekommen alles in
* einem Request: plan, avatar, nickname, streak.
*
* Live-Update-Pattern (siehe RECOVERY_LOG): nach Profile-Edit (PATCH /api/auth/me)
* MUSS `invalidateMe()` aufgerufen werden — alle useMe-Konsumenten (AppHeader,
* PostCard, ComposeCard, etc.) re-fetchen automatisch via Listener-Subscribe.
*
* WICHTIG: nicht aus `supabase.auth.getUser().user_metadata` lesen — das
* sind nur die JWT-Claims vom Signup-Zeitpunkt, NICHT der aktuelle Profile-
* Stand (Avatar/Nickname/Plan werden via Profile-Edit-API geupdated, landen
* in der DB, NICHT zurück ins JWT-Claim).
*/
export type OnboardingStep =
| 'welcome'
| 'account'
| 'plan'
| 'pre_protection'
| 'done'
// legacy (alte Builds könnten das im Profile haben — wird im neuen Flow nicht gesetzt)
| 'nickname'
| 'block';
export type Me = {
id: string;
email: string;
username: string;
nickname: string | null;
avatar: string | null;
plan: Plan;
streak: number;
lyraVoiceId: string | null;
onboardingStep: OnboardingStep;
created_at?: string;
presenceVisible?: boolean;
};
let cachedMe: Me | null = null;
const listeners = new Set<() => void>();
/**
* Lädt /api/auth/me neu und benachrichtigt ALLE useMe-Konsumenten in der App.
* Nach jedem PATCH /api/auth/me aufrufen — sonst sehen Konsumenten alten Cache.
*/
export function invalidateMe(): void {
cachedMe = null;
for (const cb of listeners) cb();
}
export function useMe(): { me: Me | null; loading: boolean; reload: () => void } {
const [me, setMe] = useState<Me | null>(cachedMe);
const [loading, setLoading] = useState(cachedMe === null);
const [version, setVersion] = useState(0);
// Auf globale Invalidierung lauschen (Avatar-/Nickname-Update aus Profile-Edit)
useEffect(() => {
const cb = () => setVersion((v) => v + 1);
listeners.add(cb);
return () => {
listeners.delete(cb);
};
}, []);
useEffect(() => {
let cancelled = false;
(async () => {
// Falls cache schon frisch ist (von anderem Konsumenten gerade geladen): nutzen
if (cachedMe !== null) {
setMe(cachedMe);
setLoading(false);
return;
}
try {
const res = await apiFetch<Me>('/api/auth/me');
if (cancelled) return;
cachedMe = res;
setMe(res);
} catch (e) {
console.warn('[useMe] fetch failed:', e);
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
}, [version]);
return {
me,
loading,
reload: () => {
invalidateMe();
},
};
}