Image performance improvements (#126)
* Switch out most images for FastImage * Add image loading placeholders * Fix tests
This commit is contained in:
@@ -36,16 +36,14 @@ describe('SelectedPhoto', () => {
|
|||||||
|
|
||||||
const selectedPhotoImage = await findByTestId('selectedPhotoImage')
|
const selectedPhotoImage = await findByTestId('selectedPhotoImage')
|
||||||
expect(selectedPhotoImage).toBeTruthy()
|
expect(selectedPhotoImage).toBeTruthy()
|
||||||
// @ts-expect-error
|
|
||||||
expect(selectedPhotoImage).toHaveStyle({width: 250})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has 2 photos to render', async () => {
|
it('has 2 photos to render', async () => {
|
||||||
const {findAllByTestId} = render(<SelectedPhoto {...mockedProps} />)
|
const {findAllByTestId} = render(<SelectedPhoto {...mockedProps} />)
|
||||||
const selectedPhotoImage = await findAllByTestId('selectedPhotoImage')
|
const selectedPhotoImage = await findAllByTestId('selectedPhotoImage')
|
||||||
expect(selectedPhotoImage[0]).toBeTruthy()
|
expect(selectedPhotoImage[0]).toBeTruthy()
|
||||||
// @ts-expect-error
|
expect(selectedPhotoImage[1]).toBeTruthy()
|
||||||
expect(selectedPhotoImage[0]).toHaveStyle({width: 175})
|
expect(selectedPhotoImage[2]).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has 3 photos to render', async () => {
|
it('has 3 photos to render', async () => {
|
||||||
@@ -57,8 +55,10 @@ describe('SelectedPhoto', () => {
|
|||||||
)
|
)
|
||||||
const selectedPhotoImage = await findAllByTestId('selectedPhotoImage')
|
const selectedPhotoImage = await findAllByTestId('selectedPhotoImage')
|
||||||
expect(selectedPhotoImage[0]).toBeTruthy()
|
expect(selectedPhotoImage[0]).toBeTruthy()
|
||||||
// @ts-expect-error
|
expect(selectedPhotoImage[0]).toBeTruthy()
|
||||||
expect(selectedPhotoImage[0]).toHaveStyle({width: 85})
|
expect(selectedPhotoImage[1]).toBeTruthy()
|
||||||
|
expect(selectedPhotoImage[2]).toBeTruthy()
|
||||||
|
expect(selectedPhotoImage[3]).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('removes a photo', async () => {
|
it('removes a photo', async () => {
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ PODS:
|
|||||||
- hermes-engine/Pre-built (= 0.71.0)
|
- hermes-engine/Pre-built (= 0.71.0)
|
||||||
- hermes-engine/Pre-built (0.71.0)
|
- hermes-engine/Pre-built (0.71.0)
|
||||||
- libevent (2.1.12)
|
- 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):
|
- RCT-Folly (2021.07.22.00):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
@@ -346,6 +355,10 @@ PODS:
|
|||||||
- React-Core
|
- React-Core
|
||||||
- RNCClipboard (1.11.1):
|
- RNCClipboard (1.11.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
- RNFastImage (8.6.3):
|
||||||
|
- React-Core
|
||||||
|
- SDWebImage (~> 5.11.1)
|
||||||
|
- SDWebImageWebPCoder (~> 0.8.4)
|
||||||
- RNFS (2.20.0):
|
- RNFS (2.20.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNGestureHandler (2.8.0):
|
- RNGestureHandler (2.8.0):
|
||||||
@@ -400,6 +413,12 @@ PODS:
|
|||||||
- React-RCTImage
|
- React-RCTImage
|
||||||
- RNSVG (12.5.0):
|
- RNSVG (12.5.0):
|
||||||
- React-Core
|
- 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):
|
- segment-analytics-react-native (2.10.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- sovran-react-native
|
- sovran-react-native
|
||||||
@@ -458,6 +477,7 @@ DEPENDENCIES:
|
|||||||
- RNBackgroundFetch (from `../node_modules/react-native-background-fetch`)
|
- RNBackgroundFetch (from `../node_modules/react-native-background-fetch`)
|
||||||
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||||
- "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
|
- "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
|
||||||
|
- RNFastImage (from `../node_modules/react-native-fast-image`)
|
||||||
- RNFS (from `../node_modules/react-native-fs`)
|
- RNFS (from `../node_modules/react-native-fs`)
|
||||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||||
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
|
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
|
||||||
@@ -475,6 +495,9 @@ SPEC REPOS:
|
|||||||
trunk:
|
trunk:
|
||||||
- fmt
|
- fmt
|
||||||
- libevent
|
- libevent
|
||||||
|
- libwebp
|
||||||
|
- SDWebImage
|
||||||
|
- SDWebImageWebPCoder
|
||||||
- Swime
|
- Swime
|
||||||
- TOCropViewController
|
- TOCropViewController
|
||||||
|
|
||||||
@@ -571,6 +594,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/@react-native-async-storage/async-storage"
|
:path: "../node_modules/@react-native-async-storage/async-storage"
|
||||||
RNCClipboard:
|
RNCClipboard:
|
||||||
:path: "../node_modules/@react-native-clipboard/clipboard"
|
:path: "../node_modules/@react-native-clipboard/clipboard"
|
||||||
|
RNFastImage:
|
||||||
|
:path: "../node_modules/react-native-fast-image"
|
||||||
RNFS:
|
RNFS:
|
||||||
:path: "../node_modules/react-native-fs"
|
:path: "../node_modules/react-native-fs"
|
||||||
RNGestureHandler:
|
RNGestureHandler:
|
||||||
@@ -606,6 +631,7 @@ SPEC CHECKSUMS:
|
|||||||
glog: 476ee3e89abb49e07f822b48323c51c57124b572
|
glog: 476ee3e89abb49e07f822b48323c51c57124b572
|
||||||
hermes-engine: f6e715aa6c8bd38de6c13bc85e07b0a337edaa89
|
hermes-engine: f6e715aa6c8bd38de6c13bc85e07b0a337edaa89
|
||||||
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
||||||
|
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
|
||||||
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
|
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
|
||||||
RCTRequired: fd4d923b964658aa0c4091a32c8b2004c6d9e3a6
|
RCTRequired: fd4d923b964658aa0c4091a32c8b2004c6d9e3a6
|
||||||
RCTTypeSafety: c276d85975bde3d8448907235c70bf0da257adfd
|
RCTTypeSafety: c276d85975bde3d8448907235c70bf0da257adfd
|
||||||
@@ -645,6 +671,7 @@ SPEC CHECKSUMS:
|
|||||||
RNBackgroundFetch: 8e16176ff415daac743a6eb57afc8e9e14dbe623
|
RNBackgroundFetch: 8e16176ff415daac743a6eb57afc8e9e14dbe623
|
||||||
RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60
|
RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60
|
||||||
RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd
|
RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd
|
||||||
|
RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8
|
||||||
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
||||||
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3
|
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3
|
||||||
RNImageCropPicker: 648356d68fbf9911a1016b3e3723885d28373eda
|
RNImageCropPicker: 648356d68fbf9911a1016b3e3723885d28373eda
|
||||||
@@ -654,6 +681,8 @@ SPEC CHECKSUMS:
|
|||||||
RNReanimated: d8d9d3d3801bda5e35e85cdffc871577d044dc2e
|
RNReanimated: d8d9d3d3801bda5e35e85cdffc871577d044dc2e
|
||||||
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
|
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
|
||||||
RNSVG: 6adc5c52d2488a476248413064b7f2832e639057
|
RNSVG: 6adc5c52d2488a476248413064b7f2832e639057
|
||||||
|
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
|
||||||
|
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
|
||||||
segment-analytics-react-native: cb097e393c3560a0d4cfd877044293e37b0050d9
|
segment-analytics-react-native: cb097e393c3560a0d4cfd877044293e37b0050d9
|
||||||
sovran-react-native: fd3dc8f1a4b14acdc4ad25fc6b4ac4f52a2a2a15
|
sovran-react-native: fd3dc8f1a4b14acdc4ad25fc6b4ac4f52a2a2a15
|
||||||
Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b
|
Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"react-native": "0.71.1",
|
"react-native": "0.71.1",
|
||||||
"react-native-appstate-hook": "^1.0.6",
|
"react-native-appstate-hook": "^1.0.6",
|
||||||
"react-native-background-fetch": "^4.1.8",
|
"react-native-background-fetch": "^4.1.8",
|
||||||
|
"react-native-fast-image": "^8.6.3",
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-fs": "^2.20.0",
|
||||||
"react-native-gesture-handler": "^2.5.0",
|
"react-native-gesture-handler": "^2.5.0",
|
||||||
"react-native-haptic-feedback": "^1.14.0",
|
"react-native-haptic-feedback": "^1.14.0",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -8,7 +8,7 @@ import {
|
|||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {BlurView} from '@react-native-community/blur'
|
import {BlurView} from '@react-native-community/blur'
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
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 {Text} from '../util/text/Text'
|
||||||
import {s, gradients} from '../../lib/styles'
|
import {s, gradients} from '../../lib/styles'
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
import {usePalette} from '../../lib/hooks/usePalette'
|
||||||
@@ -38,10 +38,7 @@ export const ExternalEmbed = ({
|
|||||||
<ActivityIndicator size="large" style={styles.spinner} />
|
<ActivityIndicator size="large" style={styles.spinner} />
|
||||||
</View>
|
</View>
|
||||||
) : link.localThumb ? (
|
) : link.localThumb ? (
|
||||||
<AutoSizedImage
|
<Image uri={link.localThumb.path} style={styles.image} />
|
||||||
uri={link.localThumb.path}
|
|
||||||
containerStyle={styles.image}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueDark.start, gradients.blueDark.end]}
|
colors={[gradients.blueDark.start, gradients.blueDark.end]}
|
||||||
@@ -98,7 +95,7 @@ const styles = StyleSheet.create({
|
|||||||
borderTopLeftRadius: 6,
|
borderTopLeftRadius: 6,
|
||||||
borderTopRightRadius: 6,
|
borderTopRightRadius: 6,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 200,
|
maxHeight: 200,
|
||||||
},
|
},
|
||||||
imageFallback: {
|
imageFallback: {
|
||||||
height: 160,
|
height: 160,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, {useCallback} from 'react'
|
import React, {useCallback} from 'react'
|
||||||
import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
|
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import FastImage from 'react-native-fast-image'
|
||||||
import {colors} from '../../lib/styles'
|
import {colors} from '../../lib/styles'
|
||||||
|
|
||||||
export const SelectedPhoto = ({
|
export const SelectedPhoto = ({
|
||||||
@@ -42,7 +43,7 @@ export const SelectedPhoto = ({
|
|||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<Image
|
<FastImage
|
||||||
testID="selectedPhotoImage"
|
testID="selectedPhotoImage"
|
||||||
style={[styles.image, imageStyle]}
|
style={[styles.image, imageStyle]}
|
||||||
source={{uri: item}}
|
source={{uri: item}}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleSheet, StyleProp, View, ViewStyle, Image} from 'react-native'
|
import {
|
||||||
|
StyleSheet,
|
||||||
|
StyleProp,
|
||||||
|
View,
|
||||||
|
ViewStyle,
|
||||||
|
Image as RNImage,
|
||||||
|
} from 'react-native'
|
||||||
import {AppBskyEmbedImages, AppBskyEmbedExternal} from '@atproto/api'
|
import {AppBskyEmbedImages, AppBskyEmbedExternal} from '@atproto/api'
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
import {Text} from './text/Text'
|
import {Text} from './text/Text'
|
||||||
import {AutoSizedImage} from './images/AutoSizedImage'
|
import {Image} from './images/Image'
|
||||||
import {ImageLayoutGrid} from './images/ImageLayoutGrid'
|
import {ImageLayoutGrid} from './images/ImageLayoutGrid'
|
||||||
import {ImagesLightbox} from '../../../state/models/shell-ui'
|
import {ImagesLightbox} from '../../../state/models/shell-ui'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
@@ -37,11 +43,11 @@ export function PostEmbeds({
|
|||||||
}
|
}
|
||||||
const onPressIn = (index: number) => {
|
const onPressIn = (index: number) => {
|
||||||
const firstImageToShow = uris[index]
|
const firstImageToShow = uris[index]
|
||||||
Image.prefetch(firstImageToShow)
|
RNImage.prefetch(firstImageToShow)
|
||||||
uris.forEach(uri => {
|
uris.forEach(uri => {
|
||||||
if (firstImageToShow !== uri) {
|
if (firstImageToShow !== uri) {
|
||||||
// First image already prefeched above
|
// First image already prefeched above
|
||||||
Image.prefetch(uri)
|
RNImage.prefetch(uri)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -85,12 +91,12 @@ export function PostEmbeds({
|
|||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.imagesContainer, style]}>
|
<View style={[styles.imagesContainer, style]}>
|
||||||
<AutoSizedImage
|
<Image
|
||||||
uri={embed.images[0].thumb}
|
uri={embed.images[0].thumb}
|
||||||
onPress={() => openLightbox(0)}
|
onPress={() => openLightbox(0)}
|
||||||
onLongPress={() => onLongPress(0)}
|
onLongPress={() => onLongPress(0)}
|
||||||
onPressIn={() => onPressIn(0)}
|
onPressIn={() => onPressIn(0)}
|
||||||
containerStyle={styles.singleImage}
|
style={styles.singleImage}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@@ -105,7 +111,7 @@ export function PostEmbeds({
|
|||||||
href={link.uri}
|
href={link.uri}
|
||||||
noFeedback>
|
noFeedback>
|
||||||
{link.thumb ? (
|
{link.thumb ? (
|
||||||
<AutoSizedImage uri={link.thumb} containerStyle={styles.extImage} />
|
<Image uri={link.thumb} style={styles.extImage} />
|
||||||
) : (
|
) : (
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueDark.start, gradients.blueDark.end]}
|
colors={[gradients.blueDark.start, gradients.blueDark.end]}
|
||||||
@@ -145,6 +151,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
singleImage: {
|
singleImage: {
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
|
maxHeight: 500,
|
||||||
},
|
},
|
||||||
extOuter: {
|
extOuter: {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Image, StyleSheet, View} from 'react-native'
|
import {StyleSheet, View} from 'react-native'
|
||||||
import Svg, {Circle, Text, Defs, LinearGradient, Stop} from 'react-native-svg'
|
import Svg, {Circle, Text, Defs, LinearGradient, Stop} from 'react-native-svg'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import FastImage from 'react-native-fast-image'
|
||||||
import {
|
import {
|
||||||
openCamera,
|
openCamera,
|
||||||
openCropper,
|
openCropper,
|
||||||
@@ -102,13 +103,16 @@ export function UserAvatar({
|
|||||||
bottomOffset={-10}
|
bottomOffset={-10}
|
||||||
menuWidth={170}>
|
menuWidth={170}>
|
||||||
{avatar ? (
|
{avatar ? (
|
||||||
<Image
|
<FastImage
|
||||||
style={{
|
style={{
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
borderRadius: Math.floor(size / 2),
|
borderRadius: Math.floor(size / 2),
|
||||||
}}
|
}}
|
||||||
source={{uri: avatar}}
|
source={{
|
||||||
|
uri: avatar,
|
||||||
|
priority: FastImage.priority.high,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
renderSvg(size, initials)
|
renderSvg(size, initials)
|
||||||
@@ -122,10 +126,10 @@ export function UserAvatar({
|
|||||||
</View>
|
</View>
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
) : avatar ? (
|
) : avatar ? (
|
||||||
<Image
|
<FastImage
|
||||||
style={{width: size, height: size, borderRadius: Math.floor(size / 2)}}
|
style={{width: size, height: size, borderRadius: Math.floor(size / 2)}}
|
||||||
resizeMode="stretch"
|
resizeMode="stretch"
|
||||||
source={{uri: avatar}}
|
source={{uri: avatar, priority: FastImage.priority.high}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
renderSvg(size, initials)
|
renderSvg(size, initials)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React from 'react'
|
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 Svg, {Rect, Defs, LinearGradient, Stop} from 'react-native-svg'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {Image as PickedImage} from 'react-native-image-crop-picker'
|
import {Image as PickedImage} from 'react-native-image-crop-picker'
|
||||||
|
import FastImage from 'react-native-fast-image'
|
||||||
import {colors, gradients} from '../../lib/styles'
|
import {colors, gradients} from '../../lib/styles'
|
||||||
import {
|
import {
|
||||||
openCamera,
|
openCamera,
|
||||||
@@ -95,7 +96,7 @@ export function UserBanner({
|
|||||||
bottomOffset={-10}
|
bottomOffset={-10}
|
||||||
menuWidth={170}>
|
menuWidth={170}>
|
||||||
{banner ? (
|
{banner ? (
|
||||||
<Image style={styles.bannerImage} source={{uri: banner}} />
|
<FastImage style={styles.bannerImage} source={{uri: banner}} />
|
||||||
) : (
|
) : (
|
||||||
renderSvg()
|
renderSvg()
|
||||||
)}
|
)}
|
||||||
@@ -108,7 +109,7 @@ export function UserBanner({
|
|||||||
</View>
|
</View>
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
) : banner ? (
|
) : banner ? (
|
||||||
<Image
|
<FastImage
|
||||||
style={styles.bannerImage}
|
style={styles.bannerImage}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
source={{uri: banner}}
|
source={{uri: banner}}
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
import React, {useState, useEffect} from 'react'
|
|
||||||
import {
|
|
||||||
Image,
|
|
||||||
ImageStyle,
|
|
||||||
LayoutChangeEvent,
|
|
||||||
StyleProp,
|
|
||||||
StyleSheet,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
ViewStyle,
|
|
||||||
} from 'react-native'
|
|
||||||
import {Text} from '../text/Text'
|
|
||||||
import {useTheme} from '../../../lib/ThemeContext'
|
|
||||||
import {usePalette} from '../../../lib/hooks/usePalette'
|
|
||||||
import {DELAY_PRESS_IN} from './constants'
|
|
||||||
|
|
||||||
const MAX_HEIGHT = 300
|
|
||||||
|
|
||||||
interface Dim {
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AutoSizedImage({
|
|
||||||
uri,
|
|
||||||
onPress,
|
|
||||||
onLongPress,
|
|
||||||
onPressIn,
|
|
||||||
style,
|
|
||||||
containerStyle,
|
|
||||||
}: {
|
|
||||||
uri: string
|
|
||||||
onPress?: () => void
|
|
||||||
style?: StyleProp<ImageStyle>
|
|
||||||
containerStyle?: StyleProp<ViewStyle>
|
|
||||||
}) {
|
|
||||||
const theme = useTheme()
|
|
||||||
const errPal = usePalette('error')
|
|
||||||
const [error, setError] = useState<string | undefined>('')
|
|
||||||
const [imgInfo, setImgInfo] = useState<Dim | undefined>()
|
|
||||||
const [containerInfo, setContainerInfo] = useState<Dim | undefined>()
|
|
||||||
|
|
||||||
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<ViewStyle> | 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 (
|
|
||||||
<View style={style}>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={onPress}
|
|
||||||
onLongPress={onLongPress}
|
|
||||||
onPressIn={onPressIn}
|
|
||||||
delayPressIn={DELAY_PRESS_IN}>
|
|
||||||
{error ? (
|
|
||||||
<View style={[styles.errorContainer, errPal.view, containerStyle]}>
|
|
||||||
<Text style={errPal.text}>{error}</Text>
|
|
||||||
</View>
|
|
||||||
) : calculatedStyle ? (
|
|
||||||
<View style={[styles.container, containerStyle]}>
|
|
||||||
<Image style={calculatedStyle} source={{uri}} />
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
style,
|
|
||||||
styles.placeholder,
|
|
||||||
{backgroundColor: theme.palette.default.backgroundLight},
|
|
||||||
]}
|
|
||||||
onLayout={onLayout}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
placeholder: {
|
|
||||||
width: '100%',
|
|
||||||
aspectRatio: 1,
|
|
||||||
},
|
|
||||||
errorContainer: {
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingVertical: 8,
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -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<ViewStyle>
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onPress}
|
||||||
|
onLongPress={onLongPress}
|
||||||
|
onPressIn={onPressIn}
|
||||||
|
delayPressIn={DELAY_PRESS_IN}
|
||||||
|
style={[styles.container, style]}>
|
||||||
|
<FastImage style={styles.image} source={{uri}} defaultSource={LOADING} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
width: '100%',
|
||||||
|
aspectRatio: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
Image,
|
|
||||||
StyleProp,
|
StyleProp,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
View,
|
View,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
import FastImage from 'react-native-fast-image'
|
||||||
|
|
||||||
export function ImageHorzList({
|
export function ImageHorzList({
|
||||||
uris,
|
uris,
|
||||||
@@ -21,7 +21,7 @@ export function ImageHorzList({
|
|||||||
<View style={[styles.flexRow, style]}>
|
<View style={[styles.flexRow, style]}>
|
||||||
{uris.map((uri, i) => (
|
{uris.map((uri, i) => (
|
||||||
<TouchableWithoutFeedback key={i} onPress={() => onPress?.(i)}>
|
<TouchableWithoutFeedback key={i} onPress={() => onPress?.(i)}>
|
||||||
<Image source={{uri}} style={styles.image} />
|
<FastImage source={{uri}} style={styles.image} />
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
Image,
|
|
||||||
ImageStyle,
|
|
||||||
LayoutChangeEvent,
|
LayoutChangeEvent,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -9,6 +7,8 @@ import {
|
|||||||
View,
|
View,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
import FastImage, {ImageStyle as FastImageStyle} from 'react-native-fast-image'
|
||||||
|
import {LOADING} from '../../../lib/assets'
|
||||||
import {DELAY_PRESS_IN} from './constants'
|
import {DELAY_PRESS_IN} from './constants'
|
||||||
|
|
||||||
interface Dim {
|
interface Dim {
|
||||||
@@ -73,7 +73,7 @@ function ImageLayoutGridInner({
|
|||||||
onPressIn?: (index: number) => void
|
onPressIn?: (index: number) => void
|
||||||
containerInfo: Dim
|
containerInfo: Dim
|
||||||
}) {
|
}) {
|
||||||
const size1 = React.useMemo<ImageStyle>(() => {
|
const size1 = React.useMemo<StyleProp<FastImageStyle>>(() => {
|
||||||
if (type === 'three') {
|
if (type === 'three') {
|
||||||
const size = (containerInfo.width - 10) / 3
|
const size = (containerInfo.width - 10) / 3
|
||||||
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
|
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
|
||||||
@@ -82,7 +82,7 @@ function ImageLayoutGridInner({
|
|||||||
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
|
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
|
||||||
}
|
}
|
||||||
}, [type, containerInfo])
|
}, [type, containerInfo])
|
||||||
const size2 = React.useMemo<ImageStyle>(() => {
|
const size2 = React.useMemo<StyleProp<FastImageStyle>>(() => {
|
||||||
if (type === 'three') {
|
if (type === 'three') {
|
||||||
const size = ((containerInfo.width - 10) / 3) * 2 + 5
|
const size = ((containerInfo.width - 10) / 3) * 2 + 5
|
||||||
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
|
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
|
||||||
@@ -100,7 +100,11 @@ function ImageLayoutGridInner({
|
|||||||
onPress={() => onPress?.(0)}
|
onPress={() => onPress?.(0)}
|
||||||
onPressIn={() => onPressIn?.(0)}
|
onPressIn={() => onPressIn?.(0)}
|
||||||
onLongPress={() => onLongPress(0)}>
|
onLongPress={() => onLongPress(0)}>
|
||||||
<Image source={{uri: uris[0]}} style={size1} />
|
<FastImage
|
||||||
|
source={{uri: uris[0]}}
|
||||||
|
style={size1}
|
||||||
|
defaultSource={LOADING}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={styles.wSpace} />
|
<View style={styles.wSpace} />
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -108,7 +112,11 @@ function ImageLayoutGridInner({
|
|||||||
onPress={() => onPress?.(1)}
|
onPress={() => onPress?.(1)}
|
||||||
onPressIn={() => onPressIn?.(1)}
|
onPressIn={() => onPressIn?.(1)}
|
||||||
onLongPress={() => onLongPress(1)}>
|
onLongPress={() => onLongPress(1)}>
|
||||||
<Image source={{uri: uris[1]}} style={size1} />
|
<FastImage
|
||||||
|
source={{uri: uris[1]}}
|
||||||
|
style={size1}
|
||||||
|
defaultSource={LOADING}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@@ -121,7 +129,11 @@ function ImageLayoutGridInner({
|
|||||||
onPress={() => onPress?.(0)}
|
onPress={() => onPress?.(0)}
|
||||||
onPressIn={() => onPressIn?.(0)}
|
onPressIn={() => onPressIn?.(0)}
|
||||||
onLongPress={() => onLongPress(0)}>
|
onLongPress={() => onLongPress(0)}>
|
||||||
<Image source={{uri: uris[0]}} style={size2} />
|
<FastImage
|
||||||
|
source={{uri: uris[0]}}
|
||||||
|
style={size2}
|
||||||
|
defaultSource={LOADING}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={styles.wSpace} />
|
<View style={styles.wSpace} />
|
||||||
<View>
|
<View>
|
||||||
@@ -130,7 +142,11 @@ function ImageLayoutGridInner({
|
|||||||
onPress={() => onPress?.(1)}
|
onPress={() => onPress?.(1)}
|
||||||
onPressIn={() => onPressIn?.(1)}
|
onPressIn={() => onPressIn?.(1)}
|
||||||
onLongPress={() => onLongPress(1)}>
|
onLongPress={() => onLongPress(1)}>
|
||||||
<Image source={{uri: uris[1]}} style={size1} />
|
<FastImage
|
||||||
|
source={{uri: uris[1]}}
|
||||||
|
style={size1}
|
||||||
|
defaultSource={LOADING}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={styles.hSpace} />
|
<View style={styles.hSpace} />
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -138,7 +154,11 @@ function ImageLayoutGridInner({
|
|||||||
onPress={() => onPress?.(2)}
|
onPress={() => onPress?.(2)}
|
||||||
onPressIn={() => onPressIn?.(2)}
|
onPressIn={() => onPressIn?.(2)}
|
||||||
onLongPress={() => onLongPress(2)}>
|
onLongPress={() => onLongPress(2)}>
|
||||||
<Image source={{uri: uris[2]}} style={size1} />
|
<FastImage
|
||||||
|
source={{uri: uris[2]}}
|
||||||
|
style={size1}
|
||||||
|
defaultSource={LOADING}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -153,7 +173,11 @@ function ImageLayoutGridInner({
|
|||||||
onPress={() => onPress?.(0)}
|
onPress={() => onPress?.(0)}
|
||||||
onPressIn={() => onPressIn?.(0)}
|
onPressIn={() => onPressIn?.(0)}
|
||||||
onLongPress={() => onLongPress(0)}>
|
onLongPress={() => onLongPress(0)}>
|
||||||
<Image source={{uri: uris[0]}} style={size1} />
|
<FastImage
|
||||||
|
source={{uri: uris[0]}}
|
||||||
|
style={size1}
|
||||||
|
defaultSource={LOADING}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={styles.hSpace} />
|
<View style={styles.hSpace} />
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -161,7 +185,11 @@ function ImageLayoutGridInner({
|
|||||||
onPress={() => onPress?.(2)}
|
onPress={() => onPress?.(2)}
|
||||||
onPressIn={() => onPressIn?.(2)}
|
onPressIn={() => onPressIn?.(2)}
|
||||||
onLongPress={() => onLongPress(2)}>
|
onLongPress={() => onLongPress(2)}>
|
||||||
<Image source={{uri: uris[2]}} style={size1} />
|
<FastImage
|
||||||
|
source={{uri: uris[2]}}
|
||||||
|
style={size1}
|
||||||
|
defaultSource={LOADING}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.wSpace} />
|
<View style={styles.wSpace} />
|
||||||
@@ -171,7 +199,11 @@ function ImageLayoutGridInner({
|
|||||||
onPress={() => onPress?.(1)}
|
onPress={() => onPress?.(1)}
|
||||||
onPressIn={() => onPressIn?.(1)}
|
onPressIn={() => onPressIn?.(1)}
|
||||||
onLongPress={() => onLongPress(1)}>
|
onLongPress={() => onLongPress(1)}>
|
||||||
<Image source={{uri: uris[1]}} style={size1} />
|
<FastImage
|
||||||
|
source={{uri: uris[1]}}
|
||||||
|
style={size1}
|
||||||
|
defaultSource={LOADING}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={styles.hSpace} />
|
<View style={styles.hSpace} />
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -179,7 +211,11 @@ function ImageLayoutGridInner({
|
|||||||
onPress={() => onPress?.(3)}
|
onPress={() => onPress?.(3)}
|
||||||
onPressIn={() => onPressIn?.(3)}
|
onPressIn={() => onPressIn?.(3)}
|
||||||
onLongPress={() => onLongPress(3)}>
|
onLongPress={() => onLongPress(3)}>
|
||||||
<Image source={{uri: uris[3]}} style={size1} />
|
<FastImage
|
||||||
|
source={{uri: uris[3]}}
|
||||||
|
style={size1}
|
||||||
|
defaultSource={LOADING}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -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 DEF_AVATAR: ImageRequireSource = require('../../../public/img/default-avatar.jpg')
|
||||||
export const TABS_EXPLAINER: ImageSourcePropType = require('../../../public/img/tabs-explainer.jpg')
|
export const TABS_EXPLAINER: ImageRequireSource = require('../../../public/img/tabs-explainer.jpg')
|
||||||
export const CLOUD_SPLASH: ImageSourcePropType = require('../../../public/img/cloud-splash.png')
|
export const CLOUD_SPLASH: ImageRequireSource = require('../../../public/img/cloud-splash.png')
|
||||||
|
export const LOADING: ImageRequireSource = require('../../../public/img/loading.png')
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import {ImageSourcePropType} from 'react-native'
|
import {ImageRequireSource} from 'react-native'
|
||||||
|
|
||||||
export const DEF_AVATAR: ImageSourcePropType = {uri: '/img/default-avatar.jpg'}
|
// @ts-ignore we need to pretend -prf
|
||||||
export const TABS_EXPLAINER: ImageSourcePropType = {
|
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',
|
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'}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import React, {useState} from 'react'
|
import React, {useState} from 'react'
|
||||||
import {
|
import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||||
Image,
|
import FastImage, {Source as FISource} from 'react-native-fast-image'
|
||||||
SafeAreaView,
|
|
||||||
StyleSheet,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {Signin} from '../com/login/Signin'
|
import {Signin} from '../com/login/Signin'
|
||||||
import {CreateAccount} from '../com/login/CreateAccount'
|
import {CreateAccount} from '../com/login/CreateAccount'
|
||||||
@@ -64,7 +59,7 @@ export const Login = observer(() => {
|
|||||||
if (screenState === ScreenState.S_SigninOrCreateAccount) {
|
if (screenState === ScreenState.S_SigninOrCreateAccount) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Image source={CLOUD_SPLASH} style={styles.bgImg} />
|
<FastImage source={CLOUD_SPLASH as FISource} style={styles.bgImg} />
|
||||||
<SafeAreaView testID="noSessionView" style={styles.container}>
|
<SafeAreaView testID="noSessionView" style={styles.container}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<SigninOrCreateAccount
|
<SigninOrCreateAccount
|
||||||
|
|||||||
@@ -11076,6 +11076,11 @@ react-native-dotenv@^3.3.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
dotenv "^16.0.3"
|
dotenv "^16.0.3"
|
||||||
|
|
||||||
|
react-native-fast-image@^8.6.3:
|
||||||
|
version "8.6.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz#6edc3f9190092a909d636d93eecbcc54a8822255"
|
||||||
|
integrity sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==
|
||||||
|
|
||||||
react-native-fs@^2.20.0:
|
react-native-fs@^2.20.0:
|
||||||
version "2.20.0"
|
version "2.20.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6"
|
resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6"
|
||||||
|
|||||||
Reference in New Issue
Block a user