chahinebrini 8670b45351 fix(magic): inline mobileconfig template as TS constant
serverAssets approach didn't bundle the template into the Nitro
output (no .output-staging/server/chunks/raw/ dir, no asset-storage
mount in nitro.mjs). Logs confirm: '[Magic] Profile template missing
in serverAssets'.

Drop serverAssets entirely. Inline the template (~2KB) as a TS
constant in backend/server/utils/magic-profile-template.ts. Build-
robust, no FS/storage dependency at runtime. Canonical source of
truth remains ops/mdm/rebreak-mac-dns-filter.mobileconfig — keep in
sync manually until/unless we add a codegen step.
2026-06-03 09:57:27 +02:00

94 lines
3.4 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { Text, View, Animated, Easing } from 'react-native';
import { useTranslation } from 'react-i18next';
import { useOnlineUsers } from '../../hooks/useOnlineUsers';
import { useLastSeenBatch } from '../../hooks/useLastSeenBatch';
type Props = {
userId: string;
/** Partner tippt gerade → überschreibt Online/Last-Seen mit „schreibt …". */
typing?: boolean;
};
const STATUS_COLOR = '#a3a3a3';
function formatLastSeen(ts: string, t: (key: string, opts?: Record<string, unknown>) => string): string {
const diff = Date.now() - new Date(ts).getTime();
if (diff < 60_000) return t('presence.just_now');
if (diff < 3_600_000) return t('presence.minutes_ago', { minutes: Math.floor(diff / 60_000) });
if (diff < 86_400_000) return t('presence.hours_ago', { hours: Math.floor(diff / 3_600_000) });
return t('presence.days_ago', { days: Math.floor(diff / 86_400_000) });
}
/** Drei pulsierende Punkte (WA/Insta-Style) neben dem „schreibt"-Text. */
function TypingDots() {
const dots = useRef([new Animated.Value(0.3), new Animated.Value(0.3), new Animated.Value(0.3)]).current;
useEffect(() => {
const loops = dots.map((d, i) =>
Animated.loop(
Animated.sequence([
Animated.delay(i * 160),
Animated.timing(d, { toValue: 1, duration: 320, easing: Easing.inOut(Easing.ease), useNativeDriver: true }),
Animated.timing(d, { toValue: 0.3, duration: 320, easing: Easing.inOut(Easing.ease), useNativeDriver: true }),
Animated.delay((dots.length - 1 - i) * 160),
]),
),
);
loops.forEach((l) => l.start());
return () => loops.forEach((l) => l.stop());
}, [dots]);
return (
<View style={{ flexDirection: 'row', alignItems: 'center', marginLeft: 4, gap: 2 }}>
{dots.map((d, i) => (
<Animated.View
key={i}
style={{ width: 3, height: 3, borderRadius: 1.5, backgroundColor: STATUS_COLOR, opacity: d }}
/>
))}
</View>
);
}
export function ChatHeaderStatus({ userId, typing }: Props) {
const { t } = useTranslation();
// DM-Header zeigt den ECHTEN Presence-Status des Partners (wie WhatsApp) —
// NICHT die following-gated `isOnline`-Variante aus dem Feed/Profil. Wer dir
// schreibt, sieht ohnehin via Typing-Indicator dass du da bist. `onlineUserIds`
// ist der rohe Presence-Set → updatet live über den Presence-Sync-Channel.
const { onlineUserIds } = useOnlineUsers();
const online = onlineUserIds.has(userId);
const lastSeenMap = useLastSeenBatch(online ? [] : [userId]);
if (typing) {
return (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ fontSize: 12, fontFamily: 'Nunito_400Regular', color: STATUS_COLOR }}>
{t('presence.typing')}
</Text>
<TypingDots />
</View>
);
}
if (online) {
// User-Wunsch: „Online"-Text zeigen, aber NICHT grün (Dot im Avatar reicht
// als Farb-Signal). Neutraler `textMuted`-Grau-Ton.
return (
<Text style={{ fontSize: 12, fontFamily: 'Nunito_400Regular', color: STATUS_COLOR }}>
{t('presence.online')}
</Text>
);
}
const lastSeen = lastSeenMap[userId];
if (!lastSeen) return null;
return (
<Text style={{ fontSize: 12, fontFamily: 'Nunito_400Regular', color: STATUS_COLOR }}>
{formatLastSeen(lastSeen, t)}
</Text>
);
}