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

120 lines
3.3 KiB
TypeScript

import { useMemo } from 'react';
import { useWindowDimensions, View } from 'react-native';
import Svg, { Circle, Path, G } from 'react-native-svg';
import { useColors } from '../../lib/theme';
const TILE = 80;
const OPACITY = 0.07;
type Symbol = 'star' | 'heart' | 'check' | 'dot' | 'cloud' | 'wave' | 'diamond';
const SEQUENCE: Symbol[] = [
'star', 'dot', 'heart', 'wave', 'check', 'dot', 'cloud',
'diamond', 'dot', 'star', 'check', 'heart', 'dot', 'wave',
];
function SymbolShape({ type, color }: { type: Symbol; color: string }) {
switch (type) {
case 'star':
return (
<Path
d="M10 2 L11.8 7.6 L18 7.6 L13 11.2 L14.8 16.8 L10 13.2 L5.2 16.8 L7 11.2 L2 7.6 L8.2 7.6 Z"
fill={color}
opacity={OPACITY}
/>
);
case 'heart':
return (
<Path
d="M10 15 C10 15 3 10 3 6 C3 3.8 4.8 2 7 2 C8.3 2 9.5 2.7 10 3.7 C10.5 2.7 11.7 2 13 2 C15.2 2 17 3.8 17 6 C17 10 10 15 10 15 Z"
fill={color}
opacity={OPACITY}
/>
);
case 'check':
return (
<Path
d="M3 9 L7 13 L17 4"
stroke={color}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
opacity={OPACITY}
/>
);
case 'dot':
return <Circle cx="10" cy="10" r="3.5" fill={color} opacity={OPACITY} />;
case 'cloud':
return (
<Path
d="M5 12 C3.3 12 2 10.7 2 9 C2 7.4 3.2 6.1 4.7 6 C5 4.3 6.5 3 8.3 3 C9.8 3 11.1 3.9 11.7 5.2 C12 5.1 12.3 5 12.7 5 C14.5 5 16 6.5 16 8.3 C16 10.3 14.3 12 12.3 12 Z"
fill={color}
opacity={OPACITY}
/>
);
case 'wave':
return (
<Path
d="M2 10 Q5 7 8 10 Q11 13 14 10 Q17 7 20 10"
stroke={color}
strokeWidth={1.5}
strokeLinecap="round"
fill="none"
opacity={OPACITY}
/>
);
case 'diamond':
return (
<Path
d="M10 2 L18 10 L10 18 L2 10 Z"
fill={color}
opacity={OPACITY}
/>
);
}
}
export function DmChatBackground() {
const { width, height } = useWindowDimensions();
const colors = useColors();
const patternColor = colors.text;
const cols = Math.ceil(width / TILE) + 1;
const rows = Math.ceil(height / TILE) + 1;
const symbols = useMemo(() => {
const items: { x: number; y: number; type: Symbol; rotate: number }[] = [];
let seq = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const offsetX = r % 2 === 0 ? 0 : TILE / 2;
items.push({
x: c * TILE + offsetX + TILE / 2,
y: r * TILE + TILE / 2,
type: SEQUENCE[seq % SEQUENCE.length],
rotate: [0, 15, -10, 30, -20, 5, -15][seq % 7],
});
seq++;
}
}
return items;
}, [cols, rows]);
return (
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }} pointerEvents="none">
<Svg width={width} height={height}>
{symbols.map((s, i) => (
<G
key={i}
transform={`translate(${s.x - 10}, ${s.y - 10}) rotate(${s.rotate}, 10, 10)`}
>
<SymbolShape type={s.type} color={patternColor} />
</G>
))}
</Svg>
</View>
);
}