diff --git a/__tests__/view/com/composer/SelectedPhoto.test.tsx b/__tests__/view/com/composer/SelectedPhoto.test.tsx index 26059ae303..18a14f1108 100644 --- a/__tests__/view/com/composer/SelectedPhoto.test.tsx +++ b/__tests__/view/com/composer/SelectedPhoto.test.tsx @@ -36,16 +36,14 @@ describe('SelectedPhoto', () => { const selectedPhotoImage = await findByTestId('selectedPhotoImage') expect(selectedPhotoImage).toBeTruthy() - // @ts-expect-error - expect(selectedPhotoImage).toHaveStyle({width: 250}) }) it('has 2 photos to render', async () => { const {findAllByTestId} = render() const selectedPhotoImage = await findAllByTestId('selectedPhotoImage') expect(selectedPhotoImage[0]).toBeTruthy() - // @ts-expect-error - expect(selectedPhotoImage[0]).toHaveStyle({width: 175}) + expect(selectedPhotoImage[1]).toBeTruthy() + expect(selectedPhotoImage[2]).toBeFalsy() }) it('has 3 photos to render', async () => { @@ -57,8 +55,10 @@ describe('SelectedPhoto', () => { ) const selectedPhotoImage = await findAllByTestId('selectedPhotoImage') expect(selectedPhotoImage[0]).toBeTruthy() - // @ts-expect-error - expect(selectedPhotoImage[0]).toHaveStyle({width: 85}) + expect(selectedPhotoImage[0]).toBeTruthy() + expect(selectedPhotoImage[1]).toBeTruthy() + expect(selectedPhotoImage[2]).toBeTruthy() + expect(selectedPhotoImage[3]).toBeFalsy() }) it('removes a photo', async () => { diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0c9bf86eb6..1df1a99cf5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -17,6 +17,15 @@ PODS: - hermes-engine/Pre-built (= 0.71.0) - hermes-engine/Pre-built (0.71.0) - libevent (2.1.12) + - libwebp (1.2.4): + - libwebp/demux (= 1.2.4) + - libwebp/mux (= 1.2.4) + - libwebp/webp (= 1.2.4) + - libwebp/demux (1.2.4): + - libwebp/webp + - libwebp/mux (1.2.4): + - libwebp/demux + - libwebp/webp (1.2.4) - RCT-Folly (2021.07.22.00): - boost - DoubleConversion @@ -346,6 +355,10 @@ PODS: - React-Core - RNCClipboard (1.11.1): - React-Core + - RNFastImage (8.6.3): + - React-Core + - SDWebImage (~> 5.11.1) + - SDWebImageWebPCoder (~> 0.8.4) - RNFS (2.20.0): - React-Core - RNGestureHandler (2.8.0): @@ -400,6 +413,12 @@ PODS: - React-RCTImage - RNSVG (12.5.0): - React-Core + - SDWebImage (5.11.1): + - SDWebImage/Core (= 5.11.1) + - SDWebImage/Core (5.11.1) + - SDWebImageWebPCoder (0.8.5): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.10) - segment-analytics-react-native (2.10.1): - React-Core - sovran-react-native @@ -458,6 +477,7 @@ DEPENDENCIES: - RNBackgroundFetch (from `../node_modules/react-native-background-fetch`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" + - RNFastImage (from `../node_modules/react-native-fast-image`) - RNFS (from `../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) @@ -475,6 +495,9 @@ SPEC REPOS: trunk: - fmt - libevent + - libwebp + - SDWebImage + - SDWebImageWebPCoder - Swime - TOCropViewController @@ -571,6 +594,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-async-storage/async-storage" RNCClipboard: :path: "../node_modules/@react-native-clipboard/clipboard" + RNFastImage: + :path: "../node_modules/react-native-fast-image" RNFS: :path: "../node_modules/react-native-fs" RNGestureHandler: @@ -606,6 +631,7 @@ SPEC CHECKSUMS: glog: 476ee3e89abb49e07f822b48323c51c57124b572 hermes-engine: f6e715aa6c8bd38de6c13bc85e07b0a337edaa89 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 + libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCTRequired: fd4d923b964658aa0c4091a32c8b2004c6d9e3a6 RCTTypeSafety: c276d85975bde3d8448907235c70bf0da257adfd @@ -645,6 +671,7 @@ SPEC CHECKSUMS: RNBackgroundFetch: 8e16176ff415daac743a6eb57afc8e9e14dbe623 RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60 RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd + RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3 RNImageCropPicker: 648356d68fbf9911a1016b3e3723885d28373eda @@ -654,6 +681,8 @@ SPEC CHECKSUMS: RNReanimated: d8d9d3d3801bda5e35e85cdffc871577d044dc2e RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d RNSVG: 6adc5c52d2488a476248413064b7f2832e639057 + SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d + SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d segment-analytics-react-native: cb097e393c3560a0d4cfd877044293e37b0050d9 sovran-react-native: fd3dc8f1a4b14acdc4ad25fc6b4ac4f52a2a2a15 Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b diff --git a/package.json b/package.json index a180286316..f6710d8679 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "react-native": "0.71.1", "react-native-appstate-hook": "^1.0.6", "react-native-background-fetch": "^4.1.8", + "react-native-fast-image": "^8.6.3", "react-native-fs": "^2.20.0", "react-native-gesture-handler": "^2.5.0", "react-native-haptic-feedback": "^1.14.0", diff --git a/public/img/loading.png b/public/img/loading.png new file mode 100644 index 0000000000..763d1cf674 Binary files /dev/null and b/public/img/loading.png differ diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index 7eaec5f04a..b7c10e4c95 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -8,7 +8,7 @@ import { import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {BlurView} from '@react-native-community/blur' import LinearGradient from 'react-native-linear-gradient' -import {AutoSizedImage} from '../util/images/AutoSizedImage' +import {Image} from '../util/images/Image' import {Text} from '../util/text/Text' import {s, gradients} from '../../lib/styles' import {usePalette} from '../../lib/hooks/usePalette' @@ -38,10 +38,7 @@ export const ExternalEmbed = ({ ) : link.localThumb ? ( - + ) : ( - { const firstImageToShow = uris[index] - Image.prefetch(firstImageToShow) + RNImage.prefetch(firstImageToShow) uris.forEach(uri => { if (firstImageToShow !== uri) { // First image already prefeched above - Image.prefetch(uri) + RNImage.prefetch(uri) } }) } @@ -85,12 +91,12 @@ export function PostEmbeds({ } else { return ( - openLightbox(0)} onLongPress={() => onLongPress(0)} onPressIn={() => onPressIn(0)} - containerStyle={styles.singleImage} + style={styles.singleImage} /> ) @@ -105,7 +111,7 @@ export function PostEmbeds({ href={link.uri} noFeedback> {link.thumb ? ( - + ) : ( {avatar ? ( - ) : ( renderSvg(size, initials) @@ -122,10 +126,10 @@ export function UserAvatar({ ) : avatar ? ( - ) : ( renderSvg(size, initials) diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index 01f0a2a6d0..d10b540232 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -1,8 +1,9 @@ import React from 'react' -import {StyleSheet, View, Image} from 'react-native' +import {StyleSheet, View} from 'react-native' import Svg, {Rect, Defs, LinearGradient, Stop} from 'react-native-svg' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Image as PickedImage} from 'react-native-image-crop-picker' +import FastImage from 'react-native-fast-image' import {colors, gradients} from '../../lib/styles' import { openCamera, @@ -95,7 +96,7 @@ export function UserBanner({ bottomOffset={-10} menuWidth={170}> {banner ? ( - + ) : ( renderSvg() )} @@ -108,7 +109,7 @@ export function UserBanner({ ) : banner ? ( - void - style?: StyleProp - containerStyle?: StyleProp -}) { - const theme = useTheme() - const errPal = usePalette('error') - const [error, setError] = useState('') - const [imgInfo, setImgInfo] = useState() - const [containerInfo, setContainerInfo] = useState() - - useEffect(() => { - let aborted = false - if (!imgInfo) { - Image.getSize( - uri, - (width: number, height: number) => { - if (!aborted) { - setImgInfo({width, height}) - } - }, - (err: any) => { - if (!aborted) { - setError(String(err)) - } - }, - ) - } - return () => { - aborted = true - } - }, [uri, imgInfo]) - - const onLayout = (evt: LayoutChangeEvent) => { - setContainerInfo({ - width: evt.nativeEvent.layout.width, - height: evt.nativeEvent.layout.height, - }) - } - - let calculatedStyle: StyleProp | undefined - if (imgInfo && containerInfo) { - // imgInfo.height / imgInfo.width = x / containerInfo.width - // x = imgInfo.height / imgInfo.width * containerInfo.width - calculatedStyle = { - height: Math.min( - MAX_HEIGHT, - (imgInfo.height / imgInfo.width) * containerInfo.width, - ), - } - } - - return ( - - - {error ? ( - - {error} - - ) : calculatedStyle ? ( - - - - ) : ( - - )} - - - ) -} - -const styles = StyleSheet.create({ - placeholder: { - width: '100%', - aspectRatio: 1, - }, - errorContainer: { - paddingHorizontal: 12, - paddingVertical: 8, - }, - container: { - overflow: 'hidden', - }, -}) diff --git a/src/view/com/util/images/Image.tsx b/src/view/com/util/images/Image.tsx new file mode 100644 index 0000000000..dc8df6dd01 --- /dev/null +++ b/src/view/com/util/images/Image.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import {StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native' +import FastImage from 'react-native-fast-image' +import {DELAY_PRESS_IN} from './constants' +import {LOADING} from '../../../lib/assets' + +export function Image({ + uri, + onPress, + onLongPress, + onPressIn, + style, +}: { + uri: string + onPress?: () => void + onLongPress?: () => void + onPressIn?: () => void + style?: StyleProp +}) { + return ( + + + + ) +} + +const styles = StyleSheet.create({ + container: { + overflow: 'hidden', + }, + image: { + width: '100%', + aspectRatio: 1, + }, +}) diff --git a/src/view/com/util/images/ImageHorzList.tsx b/src/view/com/util/images/ImageHorzList.tsx index 3664243086..ff5e43f910 100644 --- a/src/view/com/util/images/ImageHorzList.tsx +++ b/src/view/com/util/images/ImageHorzList.tsx @@ -1,12 +1,12 @@ import React from 'react' import { - Image, StyleProp, StyleSheet, TouchableWithoutFeedback, View, ViewStyle, } from 'react-native' +import FastImage from 'react-native-fast-image' export function ImageHorzList({ uris, @@ -21,7 +21,7 @@ export function ImageHorzList({ {uris.map((uri, i) => ( onPress?.(i)}> - + ))} diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx index 8e1faa66bd..5271913538 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/view/com/util/images/ImageLayoutGrid.tsx @@ -1,7 +1,5 @@ import React from 'react' import { - Image, - ImageStyle, LayoutChangeEvent, StyleProp, StyleSheet, @@ -9,6 +7,8 @@ import { View, ViewStyle, } from 'react-native' +import FastImage, {ImageStyle as FastImageStyle} from 'react-native-fast-image' +import {LOADING} from '../../../lib/assets' import {DELAY_PRESS_IN} from './constants' interface Dim { @@ -73,7 +73,7 @@ function ImageLayoutGridInner({ onPressIn?: (index: number) => void containerInfo: Dim }) { - const size1 = React.useMemo(() => { + const size1 = React.useMemo>(() => { if (type === 'three') { const size = (containerInfo.width - 10) / 3 return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} @@ -82,7 +82,7 @@ function ImageLayoutGridInner({ return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} } }, [type, containerInfo]) - const size2 = React.useMemo(() => { + const size2 = React.useMemo>(() => { if (type === 'three') { const size = ((containerInfo.width - 10) / 3) * 2 + 5 return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} @@ -100,7 +100,11 @@ function ImageLayoutGridInner({ onPress={() => onPress?.(0)} onPressIn={() => onPressIn?.(0)} onLongPress={() => onLongPress(0)}> - + onPress?.(1)} onPressIn={() => onPressIn?.(1)} onLongPress={() => onLongPress(1)}> - + ) @@ -121,7 +129,11 @@ function ImageLayoutGridInner({ onPress={() => onPress?.(0)} onPressIn={() => onPressIn?.(0)} onLongPress={() => onLongPress(0)}> - + @@ -130,7 +142,11 @@ function ImageLayoutGridInner({ onPress={() => onPress?.(1)} onPressIn={() => onPressIn?.(1)} onLongPress={() => onLongPress(1)}> - + onPress?.(2)} onPressIn={() => onPressIn?.(2)} onLongPress={() => onLongPress(2)}> - + @@ -153,7 +173,11 @@ function ImageLayoutGridInner({ onPress={() => onPress?.(0)} onPressIn={() => onPressIn?.(0)} onLongPress={() => onLongPress(0)}> - + onPress?.(2)} onPressIn={() => onPressIn?.(2)} onLongPress={() => onLongPress(2)}> - + @@ -171,7 +199,11 @@ function ImageLayoutGridInner({ onPress={() => onPress?.(1)} onPressIn={() => onPressIn?.(1)} onLongPress={() => onLongPress(1)}> - + onPress?.(3)} onPressIn={() => onPressIn?.(3)} onLongPress={() => onLongPress(3)}> - + diff --git a/src/view/lib/assets.native.ts b/src/view/lib/assets.native.ts index a2db553e74..4414273240 100644 --- a/src/view/lib/assets.native.ts +++ b/src/view/lib/assets.native.ts @@ -1,5 +1,6 @@ -import {ImageSourcePropType} from 'react-native' +import {ImageRequireSource} from 'react-native' -export const DEF_AVATAR: ImageSourcePropType = require('../../../public/img/default-avatar.jpg') -export const TABS_EXPLAINER: ImageSourcePropType = require('../../../public/img/tabs-explainer.jpg') -export const CLOUD_SPLASH: ImageSourcePropType = require('../../../public/img/cloud-splash.png') +export const DEF_AVATAR: ImageRequireSource = require('../../../public/img/default-avatar.jpg') +export const TABS_EXPLAINER: ImageRequireSource = require('../../../public/img/tabs-explainer.jpg') +export const CLOUD_SPLASH: ImageRequireSource = require('../../../public/img/cloud-splash.png') +export const LOADING: ImageRequireSource = require('../../../public/img/loading.png') diff --git a/src/view/lib/assets.ts b/src/view/lib/assets.ts index 948f3a22f3..ba7e7d93b8 100644 --- a/src/view/lib/assets.ts +++ b/src/view/lib/assets.ts @@ -1,7 +1,12 @@ -import {ImageSourcePropType} from 'react-native' +import {ImageRequireSource} from 'react-native' -export const DEF_AVATAR: ImageSourcePropType = {uri: '/img/default-avatar.jpg'} -export const TABS_EXPLAINER: ImageSourcePropType = { +// @ts-ignore we need to pretend -prf +export const DEF_AVATAR: ImageRequireSource = {uri: '/img/default-avatar.jpg'} +// @ts-ignore we need to pretend -prf +export const TABS_EXPLAINER: ImageRequireSource = { uri: '/img/tabs-explainer.jpg', } -export const CLOUD_SPLASH: ImageSourcePropType = {uri: '/img/cloud-splash.png'} +// @ts-ignore we need to pretend -prf +export const CLOUD_SPLASH: ImageRequireSource = {uri: '/img/cloud-splash.png'} +// @ts-ignore we need to pretend -prf +export const LOADING: ImageRequireSource = {uri: '/img/loading.png'} diff --git a/src/view/screens/Login.tsx b/src/view/screens/Login.tsx index 81a2c9e6b2..136ffb967f 100644 --- a/src/view/screens/Login.tsx +++ b/src/view/screens/Login.tsx @@ -1,11 +1,6 @@ import React, {useState} from 'react' -import { - Image, - SafeAreaView, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' +import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native' +import FastImage, {Source as FISource} from 'react-native-fast-image' import {observer} from 'mobx-react-lite' import {Signin} from '../com/login/Signin' import {CreateAccount} from '../com/login/CreateAccount' @@ -64,7 +59,7 @@ export const Login = observer(() => { if (screenState === ScreenState.S_SigninOrCreateAccount) { return ( - +