perf(images): migrate react-native Image → expo-image (memory+disk cache)
Avatare (Dicebear-URLs), Chat-Attachments und Feed-Bilder wurden bei jedem App-Reload neu vom Netzwerk geladen — RN Image hat nur flüchtigen Memory-Cache. expo-image (~3.0.11) bringt persistenten Disk-Cache (cachePolicy 'memory-disk' default). 14 Files migriert: UserAvatar, ChatBubble, RoomCard, ChatInput, PostCard, ComposeCard, NotificationsDropdown, AppHeader, ProfileHeader, AddDomainSheet, DomainGrid, room, profile/edit, signup. API-Mapping: resizeMode→contentFit. PostCard onLoad las e.nativeEvent. source — expo-image liefert e.source direkt (sonst wäre der Post-Bild- Aspect-Ratio-Fix still gebrochen). PostCard: nur Image-Zeilen angefasst, Like/Count/Memo-Logik unberührt (memory/feedback_minimal_post_changes.md). Kommt mit v0.3.3 (expo-image ist Native-Modul, braucht neuen Build).
This commit is contained in:
parent
a9015d1951
commit
b8e4b02b88
@ -4,10 +4,10 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
Image,
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import Svg, { Path } from 'react-native-svg';
|
||||
|
||||
@ -5,10 +5,10 @@ import {
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Image,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
Keyboard,
|
||||
KeyboardAvoidingView,
|
||||
} from 'react-native';
|
||||
import { Image } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
@ -322,7 +322,7 @@ export default function RoomScreen() {
|
||||
<View style={styles.headerCenter}>
|
||||
<View style={styles.headerAvatar}>
|
||||
{room?.avatarUrl ? (
|
||||
<Image source={{ uri: room.avatarUrl }} style={styles.headerAvatarImg} resizeMode="cover" />
|
||||
<Image source={{ uri: room.avatarUrl }} style={styles.headerAvatarImg} contentFit="cover" />
|
||||
) : (
|
||||
<Text style={styles.headerAvatarInitials}>{initials}</Text>
|
||||
)}
|
||||
@ -556,7 +556,7 @@ function RoomSettingsModal({
|
||||
style={modal.avatarWrap}
|
||||
>
|
||||
{room.avatarUrl ? (
|
||||
<Image source={{ uri: room.avatarUrl }} style={modal.avatar} resizeMode="cover" />
|
||||
<Image source={{ uri: room.avatarUrl }} style={modal.avatar} contentFit="cover" />
|
||||
) : (
|
||||
<View style={[modal.avatar, modal.avatarPlaceholder]}>
|
||||
<Ionicons name="people" size={32} color="#737373" />
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, Image } from 'react-native';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter, type RelativePathString } from 'expo-router';
|
||||
|
||||
@ -5,10 +5,10 @@ import {
|
||||
TextInput,
|
||||
Pressable,
|
||||
TouchableOpacity,
|
||||
Image,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -126,7 +126,7 @@ export function ComposeCard({ onPosted }: Props) {
|
||||
source={{ uri: imageUri }}
|
||||
className="w-full rounded-xl"
|
||||
style={{ height: 160 }}
|
||||
resizeMode="cover"
|
||||
contentFit="cover"
|
||||
/>
|
||||
<Pressable
|
||||
onPress={() => setImageUri(null)}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { View, Text, Pressable, TouchableOpacity, Modal, FlatList, Animated, Image } from 'react-native';
|
||||
import { View, Text, Pressable, TouchableOpacity, Modal, FlatList, Animated } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter, type RelativePathString } from 'expo-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { memo, useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { View, Text, Image, Pressable, Animated, TouchableOpacity } from 'react-native';
|
||||
import { View, Text, Pressable, Animated, TouchableOpacity } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -308,7 +309,7 @@ function PostCardImpl({ post, onCommentPress }: Props) {
|
||||
<Image
|
||||
source={{ uri: displayImage }}
|
||||
onLoad={(e) => {
|
||||
const { width, height } = e.nativeEvent.source;
|
||||
const { width, height } = e.source;
|
||||
if (width && height) {
|
||||
const ratio = width / height;
|
||||
setImageAspectRatio(Math.max(0.6, Math.min(1.78, ratio)));
|
||||
@ -316,7 +317,7 @@ function PostCardImpl({ post, onCommentPress }: Props) {
|
||||
}}
|
||||
className="w-full rounded-xl mt-3"
|
||||
style={{ aspectRatio: imageAspectRatio ?? 1.78 }}
|
||||
resizeMode="cover"
|
||||
contentFit="cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -439,7 +440,7 @@ function DomainFavicon({ domain, size }: DomainFaviconProps) {
|
||||
<Image
|
||||
source={{ uri }}
|
||||
style={{ width: size, height: size, borderRadius: 6 }}
|
||||
resizeMode="cover"
|
||||
contentFit="cover"
|
||||
onError={() => setFailed(true)}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { View, Text, Image } from 'react-native';
|
||||
import { View, Text } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { useOnlineUsers } from '../hooks/useOnlineUsers';
|
||||
import { resolveAvatar } from '../lib/resolveAvatar';
|
||||
import { useColors } from '../lib/theme';
|
||||
@ -89,7 +90,7 @@ export function UserAvatar({
|
||||
borderRadius: radius,
|
||||
backgroundColor: colors.surfaceElevated,
|
||||
}}
|
||||
resizeMode="cover"
|
||||
contentFit="cover"
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@ -98,6 +98,14 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
||||
setError(t('blocker.error_web_limit_reached'));
|
||||
} else if (raw.includes('mail_limit_reached')) {
|
||||
setError(t('blocker.error_mail_limit_reached'));
|
||||
} else if (raw === 'limit_reached' || raw.includes('limit_reached')) {
|
||||
// Client-side tier.atLimit Reject (combined web+mail). Bucket-spezifisch
|
||||
// wäre genauer aber der Generic-Limit-Hinweis reicht für jetzt.
|
||||
setError(
|
||||
kind === 'mail'
|
||||
? t('blocker.error_mail_limit_reached')
|
||||
: t('blocker.error_web_limit_reached'),
|
||||
);
|
||||
} else if (raw.includes('invalid_mail_domain') || raw.includes('display_name_not_supported')) {
|
||||
setError(t('blocker.error_invalid_mail'));
|
||||
} else if (raw.includes('invalid_domain') || raw.includes('invalid_pattern')) {
|
||||
|
||||
@ -3,9 +3,9 @@ import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
Image,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SuccessAlert } from '../SuccessAlert';
|
||||
|
||||
@ -2,12 +2,12 @@ import { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Image,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Modal,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import * as Clipboard from 'expo-clipboard';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -198,7 +198,7 @@ export function ChatBubble({
|
||||
<Image
|
||||
source={{ uri: msg.attachmentUrl }}
|
||||
style={styles.image}
|
||||
resizeMode="cover"
|
||||
contentFit="cover"
|
||||
/>
|
||||
{isImageOnly && (
|
||||
<View style={styles.imageTimeOverlay}>
|
||||
|
||||
@ -2,7 +2,6 @@ import { useState, useRef } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Image,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
@ -10,6 +9,7 @@ import {
|
||||
Platform,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
// TODO(sdk54): migrate to new expo-file-system class-based API (File/Directory/Paths) — see Task #14
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
@ -165,7 +165,7 @@ export function ChatInput({
|
||||
{attachment && (
|
||||
<View style={styles.attachBar}>
|
||||
{attachment.isImage ? (
|
||||
<Image source={{ uri: attachment.uri }} style={styles.attachImg} resizeMode="cover" />
|
||||
<Image source={{ uri: attachment.uri }} style={styles.attachImg} contentFit="cover" />
|
||||
) : (
|
||||
<View style={styles.attachFileIcon}>
|
||||
<Ionicons name="document" size={18} color="#737373" />
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useColors } from '../../lib/theme';
|
||||
@ -43,7 +44,7 @@ export function RoomCard({ room, onPress }: Props) {
|
||||
<View style={styles.row}>
|
||||
<View style={[styles.avatar, { backgroundColor: room.isPublic ? '#EFF6FF' : colors.surfaceElevated }]}>
|
||||
{room.avatarUrl ? (
|
||||
<Image source={{ uri: room.avatarUrl }} style={styles.avatarImg} resizeMode="cover" />
|
||||
<Image source={{ uri: room.avatarUrl }} style={styles.avatarImg} contentFit="cover" />
|
||||
) : !room.isPublic ? (
|
||||
<Text style={styles.avatarInitials}>{initials}</Text>
|
||||
) : (
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, Image } from 'react-native';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import Svg, { Path } from 'react-native-svg';
|
||||
import { useColors } from '../../lib/theme';
|
||||
|
||||
@ -36,7 +36,8 @@
|
||||
"expo-file-system": "~19.0.22",
|
||||
"expo-font": "~14.0.11",
|
||||
"expo-haptics": "^15.0.8",
|
||||
"expo-image-manipulator": "~14.0.7",
|
||||
"expo-image": "~3.0.11",
|
||||
"expo-image-manipulator": "~14.0.7",
|
||||
"expo-image-picker": "~17.0.11",
|
||||
"expo-linking": "~8.0.12",
|
||||
"expo-local-authentication": "~17.0.8",
|
||||
|
||||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@ -192,6 +192,9 @@ importers:
|
||||
expo-haptics:
|
||||
specifier: ^15.0.8
|
||||
version: 15.0.8(expo@54.0.34)
|
||||
expo-image:
|
||||
specifier: ~3.0.11
|
||||
version: 3.0.11(expo@54.0.34)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0)
|
||||
expo-image-manipulator:
|
||||
specifier: ~14.0.7
|
||||
version: 14.0.8(expo@54.0.34)
|
||||
@ -5567,6 +5570,17 @@ packages:
|
||||
peerDependencies:
|
||||
expo: '*'
|
||||
|
||||
expo-image@3.0.11:
|
||||
resolution: {integrity: sha512-4TudfUCLgYgENv+f48omnU8tjS2S0Pd9EaON5/s1ZUBRwZ7K8acEr4NfvLPSaeXvxW24iLAiyQ7sV7BXQH3RoA==}
|
||||
peerDependencies:
|
||||
expo: '*'
|
||||
react: '*'
|
||||
react-native: '*'
|
||||
react-native-web: '*'
|
||||
peerDependenciesMeta:
|
||||
react-native-web:
|
||||
optional: true
|
||||
|
||||
expo-json-utils@0.15.0:
|
||||
resolution: {integrity: sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==}
|
||||
|
||||
@ -15566,6 +15580,12 @@ snapshots:
|
||||
expo: 54.0.34(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
|
||||
expo-image-loader: 6.0.0(expo@54.0.34)
|
||||
|
||||
expo-image@3.0.11(expo@54.0.34)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
expo: 54.0.34(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
|
||||
react: 19.1.0
|
||||
react-native: 0.81.5(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.1.0)
|
||||
|
||||
expo-json-utils@0.15.0: {}
|
||||
|
||||
expo-keep-awake@15.0.8(expo@54.0.34)(react@19.1.0):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user