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" "build:apk": "cd android && ./gradlew assembleRelease"
}, },
"dependencies": { "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": "18.2.0",
"react-native": "0.73.4", "react-native": "0.73.4",
"@react-navigation/native": "^6.1.9", "react-native-audio-recorder-player": "^3.6.7",
"@react-navigation/bottom-tabs": "^6.5.11", "react-native-camera-kit": "^13.0.0",
"react-native-screens": "3.27.0",
"react-native-safe-area-context": "^4.8.2",
"react-native-document-picker": "^9.1.1", "react-native-document-picker": "^9.1.1",
"react-native-sound": "^0.11.2", "react-native-fs": "^2.20.0",
"@react-native-community/geolocation": "^3.2.1",
"react-native-image-picker": "^7.1.0", "react-native-image-picker": "^7.1.0",
"react-native-permissions": "^4.1.4", "react-native-permissions": "^4.1.4",
"react-native-camera-kit": "^13.0.0", "react-native-safe-area-context": "^4.8.2",
"@react-native-async-storage/async-storage": "^1.21.0", "react-native-screens": "3.27.0",
"react-native-fs": "^2.20.0", "react-native-sound": "^0.11.2",
"react-native-audio-recorder-player": "^3.6.7" "react-native-svg": "^15.15.4"
}, },
"devDependencies": { "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": "^18.2.48",
"@types/react-native": "^0.73.0", "@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", "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 * - Markdown-Syntax `![alt](url)` und plain `https://...image.png` werden
* erkannt — die URL bleibt im Text sichtbar (klickbar via Linkify), * 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 * - Wir nutzen Androids dataDetectorType="all" (System macht Phone/URL/Email
* automatisch klickbar) und ein einzelnes <Text selectable> ohne nested * automatisch klickbar) und ein einzelnes <Text selectable> ohne nested
* <Text> mit eigenem onPress — Nested Text mit onPress fing die Long-Press- * <Text> mit eigenem onPress — Nested Text mit onPress fing die Long-Press-
* Geste ab, damit war Markieren+Kopieren defekt. * 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 React, { useEffect, useState } from 'react';
import { View, Text, Image, TextStyle, StyleProp } from 'react-native'; import { View, Text, Image, TextStyle, StyleProp } from 'react-native';
import { SvgUri } from 'react-native-svg';
interface Props { interface Props {
text: string; text: string;
style?: StyleProp<TextStyle>; style?: StyleProp<TextStyle>;
} }
// Bild-URL-Pattern: http(s)://... endend auf jpg/png/gif/webp/bmp/ico (kein // Bild-URL-Pattern: http(s)://... endend auf gaengige Bild-Endungen.
// SVG — das kann RN Image ohne externe Lib nicht). const IMG_URL_RE = /https?:\/\/[^\s)<"']+\.(?:jpe?g|png|gif|webp|bmp|ico|svg)(?:\?[^\s)<"']*)?/gi;
const IMG_URL_RE = /https?:\/\/[^\s)<"']+\.(?:jpe?g|png|gif|webp|bmp|ico)(?:\?[^\s)<"']*)?/gi;
function extractImageUrls(text: string): string[] { function extractImageUrls(text: string): string[] {
const urls = new Set<string>(); const urls = new Set<string>();
@@ -31,11 +30,16 @@ function extractImageUrls(text: string): string[] {
return Array.from(urls); 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 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); const [failed, setFailed] = useState(false);
useEffect(() => { useEffect(() => {
if (isSvg) return; // Image.getSize geht fuer SVG nicht
let cancelled = false; let cancelled = false;
Image.getSize( Image.getSize(
uri, uri,
@@ -43,8 +47,15 @@ const InlineImage: React.FC<{ uri: string }> = ({ uri }) => {
() => { if (!cancelled) setFailed(true); }, () => { if (!cancelled) setFailed(true); },
); );
return () => { cancelled = true; }; return () => { cancelled = true; };
}, [uri]); }, [uri, isSvg]);
if (failed) return null; 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 ( return (
<Image <Image
source={{ uri }} source={{ uri }}