feat(app): Inline-Bilder in Chat-Nachrichten anzeigen (wie in Diagnostic)
MessageText erkennt http(s)-URLs auf Bilder (jpg/png/gif/webp/bmp/ico) und rendert sie als <Image> unter dem Text. Markdown-Syntax  wird durch dasselbe Regex erfasst weil die URL drin ist. SVGs ausgespart — React Native Image kann SVG nicht ohne Extra-Lib. Aspect-Ratio wird via Image.getSize ermittelt, gecapped auf 0.5..2.5 damit Panorama-/Streifen-Bilder die Bubble nicht sprengen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,25 +1,76 @@
|
|||||||
/**
|
/**
|
||||||
* MessageText — selektierbarer Chat-Text mit Android-Auto-Linkifizierung.
|
* MessageText — selektierbarer Chat-Text mit Android-Auto-Linkifizierung,
|
||||||
|
* plus Inline-Image-Rendering wenn der Text Bild-URLs enthaelt.
|
||||||
*
|
*
|
||||||
* Wir nutzen Androids dataDetectorType="all" (System macht Phone/URL/Email
|
* - Markdown-Syntax `` und plain `https://...image.png` werden
|
||||||
* automatisch klickbar) und ein einzelnes <Text selectable> ohne nested
|
* erkannt — die URL bleibt im Text sichtbar (klickbar via Linkify),
|
||||||
* <Text> mit eigenem onPress. Nested Text mit onPress fingen die Long-Press-
|
* zusaetzlich wird das Bild als <Image> drunter gerendert.
|
||||||
* Geste ab, damit war Markieren+Kopieren defekt.
|
* - 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.
|
||||||
|
* - SVGs werden uebersprungen (React Native Image kann SVG nicht ohne Lib).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Text, TextStyle, StyleProp } from 'react-native';
|
import { View, Text, Image, TextStyle, StyleProp } from 'react-native';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
text: string;
|
text: string;
|
||||||
style?: StyleProp<TextStyle>;
|
style?: StyleProp<TextStyle>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageText: React.FC<Props> = ({ text, style }) => {
|
// Bild-URL-Pattern: http(s)://... endend auf jpg/png/gif/webp/bmp/ico (kein
|
||||||
|
// SVG — das kann RN Image ohne externe Lib nicht).
|
||||||
|
const IMG_URL_RE = /https?:\/\/[^\s)<"']+\.(?:jpe?g|png|gif|webp|bmp|ico)(?:\?[^\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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Image mit dynamischer Aspect-Ratio aus echten Bilddimensionen. */
|
||||||
|
const InlineImage: React.FC<{ uri: string }> = ({ uri }) => {
|
||||||
|
const [aspectRatio, setAspectRatio] = useState<number>(16 / 9);
|
||||||
|
const [failed, setFailed] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
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]);
|
||||||
|
if (failed) return null;
|
||||||
return (
|
return (
|
||||||
<Text style={style} selectable dataDetectorType="all">
|
<Image
|
||||||
{text}
|
source={{ uri }}
|
||||||
</Text>
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user