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.
73 lines
2.6 KiB
TypeScript
73 lines
2.6 KiB
TypeScript
import { useEffect, useRef, useState, useCallback } from 'react';
|
||
import { supabase } from '../lib/supabase';
|
||
import type { RealtimeChannel } from '@supabase/supabase-js';
|
||
|
||
/**
|
||
* Typing-Indicator für eine DM-Konversation via Supabase-Broadcast (ephemer,
|
||
* KEIN DB-Write — Tipp-Status muss nicht persistiert werden).
|
||
*
|
||
* Beide Peers joinen denselben deterministischen Channel (sortiertes ID-Paar),
|
||
* damit `send()` von A bei B ankommt. `self:false` filtert die eigenen Events.
|
||
*
|
||
* - `sendTyping()` → throttled-Broadcast „ich tippe" (max 1×/1.5s)
|
||
* - `sendStopTyping()` → sofortiger „Stop" (beim Senden / Leeren des Inputs)
|
||
* - `partnerTyping` → true solange Partner-Events reinkommen (Auto-Clear 4s)
|
||
*/
|
||
export function useDmTyping(myUserId: string | undefined, partnerId: string | undefined) {
|
||
const [partnerTyping, setPartnerTyping] = useState(false);
|
||
const channelRef = useRef<RealtimeChannel | null>(null);
|
||
const clearTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||
const lastSent = useRef(0);
|
||
|
||
useEffect(() => {
|
||
if (!myUserId || !partnerId) return;
|
||
const pair = [myUserId, partnerId].sort().join('_');
|
||
const channel = supabase.channel(`dm-typing:${pair}`, {
|
||
config: { broadcast: { self: false } },
|
||
});
|
||
channel
|
||
.on('broadcast', { event: 'typing' }, (msg: any) => {
|
||
if (msg?.payload?.userId !== partnerId) return;
|
||
setPartnerTyping(true);
|
||
if (clearTimer.current) clearTimeout(clearTimer.current);
|
||
clearTimer.current = setTimeout(() => setPartnerTyping(false), 4000);
|
||
})
|
||
.on('broadcast', { event: 'stop_typing' }, (msg: any) => {
|
||
if (msg?.payload?.userId !== partnerId) return;
|
||
if (clearTimer.current) clearTimeout(clearTimer.current);
|
||
setPartnerTyping(false);
|
||
})
|
||
.subscribe();
|
||
channelRef.current = channel;
|
||
|
||
return () => {
|
||
if (clearTimer.current) clearTimeout(clearTimer.current);
|
||
supabase.removeChannel(channel);
|
||
channelRef.current = null;
|
||
setPartnerTyping(false);
|
||
};
|
||
}, [myUserId, partnerId]);
|
||
|
||
const sendTyping = useCallback(() => {
|
||
const now = Date.now();
|
||
if (now - lastSent.current < 1500) return; // Throttle
|
||
lastSent.current = now;
|
||
channelRef.current?.send({
|
||
type: 'broadcast',
|
||
event: 'typing',
|
||
payload: { userId: myUserId },
|
||
});
|
||
}, [myUserId]);
|
||
|
||
const sendStopTyping = useCallback(() => {
|
||
lastSent.current = 0;
|
||
channelRef.current?.send({
|
||
type: 'broadcast',
|
||
event: 'stop_typing',
|
||
payload: { userId: myUserId },
|
||
});
|
||
}, [myUserId]);
|
||
|
||
return { partnerTyping, sendTyping, sendStopTyping };
|
||
}
|