feat(app): SVG-Inline-Rendering via react-native-svg

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 18:33:30 +02:00
parent 6aae565541
commit b696b47feb
2 changed files with 36 additions and 24 deletions
+17 -16
View File
@@ -10,31 +10,32 @@
"build:apk": "cd android && ./gradlew assembleRelease"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^1.21.0",
"@react-native-community/geolocation": "^3.2.1",
"@react-navigation/bottom-tabs": "^6.5.11",
"@react-navigation/native": "^6.1.9",
"react": "18.2.0",
"react-native": "0.73.4",
"@react-navigation/native": "^6.1.9",
"@react-navigation/bottom-tabs": "^6.5.11",
"react-native-screens": "3.27.0",
"react-native-safe-area-context": "^4.8.2",
"react-native-audio-recorder-player": "^3.6.7",
"react-native-camera-kit": "^13.0.0",
"react-native-document-picker": "^9.1.1",
"react-native-sound": "^0.11.2",
"@react-native-community/geolocation": "^3.2.1",
"react-native-fs": "^2.20.0",
"react-native-image-picker": "^7.1.0",
"react-native-permissions": "^4.1.4",
"react-native-camera-kit": "^13.0.0",
"@react-native-async-storage/async-storage": "^1.21.0",
"react-native-fs": "^2.20.0",
"react-native-audio-recorder-player": "^3.6.7"
"react-native-safe-area-context": "^4.8.2",
"react-native-screens": "3.27.0",
"react-native-sound": "^0.11.2",
"react-native-svg": "^15.15.4"
},
"devDependencies": {
"typescript": "^5.3.3",
"@react-native/eslint-config": "^0.73.2",
"@react-native/metro-config": "^0.73.5",
"@react-native/typescript-config": "^0.73.1",
"@types/jest": "^29.5.11",
"@types/react": "^18.2.48",
"@types/react-native": "^0.73.0",
"@react-native/eslint-config": "^0.73.2",
"@react-native/typescript-config": "^0.73.1",
"@react-native/metro-config": "^0.73.5",
"metro-react-native-babel-preset": "^0.77.0",
"jest": "^29.7.0",
"@types/jest": "^29.5.11"
"metro-react-native-babel-preset": "^0.77.0",
"typescript": "^5.3.3"
}
}
+19 -8
View File
@@ -4,25 +4,24 @@
*
* - Markdown-Syntax `![alt](url)` und plain `https://...image.png` werden
* erkannt — die URL bleibt im Text sichtbar (klickbar via Linkify),
* zusaetzlich wird das Bild als <Image> drunter gerendert.
* 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.
* - SVGs werden uebersprungen (React Native Image kann SVG nicht ohne Lib).
*/
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 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;
// 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>();
@@ -31,11 +30,16 @@ function extractImageUrls(text: string): string[] {
return Array.from(urls);
}
/** Image mit dynamischer Aspect-Ratio aus echten Bilddimensionen. */
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 [aspectRatio, setAspectRatio] = useState<number>(16 / 9);
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,
@@ -43,8 +47,15 @@ const InlineImage: React.FC<{ uri: string }> = ({ uri }) => {
() => { if (!cancelled) setFailed(true); },
);
return () => { cancelled = true; };
}, [uri]);
}, [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 }}