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
|
||||
* automatisch klickbar) und ein einzelnes <Text selectable> ohne nested
|
||||
* <Text> mit eigenem onPress. Nested Text mit onPress fingen die Long-Press-
|
||||
* Geste ab, damit war Markieren+Kopieren defekt.
|
||||
* - Markdown-Syntax `` und plain `https://...image.png` werden
|
||||
* erkannt — die URL bleibt im Text sichtbar (klickbar via Linkify),
|
||||
* zusaetzlich wird das Bild als <Image> 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.
|
||||
* - SVGs werden uebersprungen (React Native Image kann SVG nicht ohne Lib).
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Text, TextStyle, StyleProp } from 'react-native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text, Image, TextStyle, StyleProp } from 'react-native';
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
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 (
|
||||
<Text style={style} selectable dataDetectorType="all">
|
||||
{text}
|
||||
</Text>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user