b696b47feb
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
89 lines
3.0 KiB
TypeScript
89 lines
3.0 KiB
TypeScript
/**
|
|
* MessageText — selektierbarer Chat-Text mit Android-Auto-Linkifizierung,
|
|
* plus Inline-Image-Rendering wenn der Text Bild-URLs enthaelt.
|
|
*
|
|
* - Markdown-Syntax `` und plain `https://...image.png` werden
|
|
* erkannt — die URL bleibt im Text sichtbar (klickbar via Linkify),
|
|
* zusaetzlich wird das Bild als <Image> oder <SvgUri> drunter gerendert.
|
|
* - Wir nutzen Androids dataDetectorType="all" (System macht Phone/URL/Email
|
|
* automatisch klickbar) und ein einzelnes <Text selectable> ohne nested
|
|
* <Text> mit eigenem onPress — Nested Text mit onPress fing die Long-Press-
|
|
* Geste ab, damit war Markieren+Kopieren defekt.
|
|
*/
|
|
|
|
import React, { useEffect, useState } from 'react';
|
|
import { View, Text, Image, TextStyle, StyleProp } from 'react-native';
|
|
import { SvgUri } from 'react-native-svg';
|
|
|
|
interface Props {
|
|
text: string;
|
|
style?: StyleProp<TextStyle>;
|
|
}
|
|
|
|
// Bild-URL-Pattern: http(s)://... endend auf gaengige Bild-Endungen.
|
|
const IMG_URL_RE = /https?:\/\/[^\s)<"']+\.(?:jpe?g|png|gif|webp|bmp|ico|svg)(?:\?[^\s)<"']*)?/gi;
|
|
|
|
function extractImageUrls(text: string): string[] {
|
|
const urls = new Set<string>();
|
|
const matches = text.match(IMG_URL_RE);
|
|
if (matches) matches.forEach(u => urls.add(u));
|
|
return Array.from(urls);
|
|
}
|
|
|
|
const SVG_RE = /\.svg(?:\?|$)/i;
|
|
|
|
/** Image mit dynamischer Aspect-Ratio aus echten Bilddimensionen.
|
|
* SVGs werden ueber react-native-svg gerendert (kein Image.getSize). */
|
|
const InlineImage: React.FC<{ uri: string }> = ({ uri }) => {
|
|
const isSvg = SVG_RE.test(uri);
|
|
const [aspectRatio, setAspectRatio] = useState<number>(1);
|
|
const [failed, setFailed] = useState(false);
|
|
useEffect(() => {
|
|
if (isSvg) return; // Image.getSize geht fuer SVG nicht
|
|
let cancelled = false;
|
|
Image.getSize(
|
|
uri,
|
|
(w, h) => { if (!cancelled && w > 0 && h > 0) setAspectRatio(Math.max(0.5, Math.min(2.5, w / h))); },
|
|
() => { if (!cancelled) setFailed(true); },
|
|
);
|
|
return () => { cancelled = true; };
|
|
}, [uri, isSvg]);
|
|
if (failed) return null;
|
|
if (isSvg) {
|
|
return (
|
|
<View style={{ marginTop: 8, width: 260, height: 260, backgroundColor: '#0D0D1A', borderRadius: 8, alignItems: 'center', justifyContent: 'center' }}>
|
|
<SvgUri uri={uri} width="100%" height="100%" onError={() => setFailed(true)} />
|
|
</View>
|
|
);
|
|
}
|
|
return (
|
|
<Image
|
|
source={{ uri }}
|
|
style={{ width: 260, aspectRatio, borderRadius: 8, marginTop: 8, backgroundColor: '#0D0D1A' }}
|
|
resizeMode="cover"
|
|
onError={() => setFailed(true)}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const MessageText: React.FC<Props> = ({ text, style }) => {
|
|
const imageUrls = extractImageUrls(text || '');
|
|
if (imageUrls.length === 0) {
|
|
return (
|
|
<Text style={style} selectable dataDetectorType="all">
|
|
{text}
|
|
</Text>
|
|
);
|
|
}
|
|
return (
|
|
<View>
|
|
<Text style={style} selectable dataDetectorType="all">
|
|
{text}
|
|
</Text>
|
|
{imageUrls.map(u => <InlineImage key={u} uri={u} />)}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default MessageText;
|