mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Updates
This commit is contained in:
@ -1,27 +1,25 @@
|
||||
import { ParseEmojis } from '@components/Parse'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { Image, Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import GracefullyImage from './GracefullyImage'
|
||||
|
||||
export interface Props {
|
||||
account: Mastodon.Account
|
||||
onPress: () => void
|
||||
}
|
||||
|
||||
const ComponentAccount: React.FC<Props> = ({ account }) => {
|
||||
const navigation = useNavigation()
|
||||
const ComponentAccount: React.FC<Props> = ({ account, onPress }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[styles.itemDefault, styles.itemAccount]}
|
||||
onPress={() => {
|
||||
navigation.push('Screen-Shared-Account', { account })
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: account.avatar_static }}
|
||||
<GracefullyImage
|
||||
uri={{ original: account.avatar_static }}
|
||||
style={styles.itemAccountAvatar}
|
||||
/>
|
||||
<View>
|
||||
|
@ -155,26 +155,24 @@ const Button: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Pressable
|
||||
style={[
|
||||
styles.button,
|
||||
{
|
||||
borderWidth: overlay ? 0 : 1,
|
||||
borderColor: colorBorder,
|
||||
backgroundColor: colorBackground,
|
||||
paddingVertical: StyleConstants.Spacing[spacing],
|
||||
paddingHorizontal:
|
||||
StyleConstants.Spacing[round ? spacing : spacingMapping[spacing]]
|
||||
},
|
||||
customStyle
|
||||
]}
|
||||
testID='base'
|
||||
onPress={onPress}
|
||||
children={children}
|
||||
disabled={disabled || active || loading}
|
||||
/>
|
||||
</View>
|
||||
<Pressable
|
||||
style={[
|
||||
styles.button,
|
||||
{
|
||||
borderWidth: overlay ? 0 : 1,
|
||||
borderColor: colorBorder,
|
||||
backgroundColor: colorBackground,
|
||||
paddingVertical: StyleConstants.Spacing[spacing],
|
||||
paddingHorizontal:
|
||||
StyleConstants.Spacing[round ? spacing : spacingMapping[spacing]]
|
||||
},
|
||||
customStyle
|
||||
]}
|
||||
testID='base'
|
||||
onPress={onPress}
|
||||
children={children}
|
||||
disabled={disabled || active || loading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
171
src/components/GracefullyImage.tsx
Normal file
171
src/components/GracefullyImage.tsx
Normal file
@ -0,0 +1,171 @@
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { Surface } from 'gl-react-expo'
|
||||
import { Blurhash } from 'gl-react-blurhash'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
Image,
|
||||
Pressable,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
View,
|
||||
ViewStyle
|
||||
} from 'react-native'
|
||||
import { Image as ImageCache } from 'react-native-expo-image-cache'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
|
||||
type CancelPromise = ((reason?: Error) => void) | undefined
|
||||
type ImageSize = { width: number; height: number }
|
||||
interface ImageSizeOperation {
|
||||
start: () => Promise<ImageSize>
|
||||
cancel: CancelPromise
|
||||
}
|
||||
const getImageSize = (uri: string): ImageSizeOperation => {
|
||||
let cancel: CancelPromise
|
||||
const start = (): Promise<ImageSize> =>
|
||||
new Promise<{ width: number; height: number }>((resolve, reject) => {
|
||||
cancel = reject
|
||||
Image.getSize(
|
||||
uri,
|
||||
(width, height) => {
|
||||
cancel = undefined
|
||||
resolve({ width, height })
|
||||
},
|
||||
error => {
|
||||
reject(error)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return { start, cancel }
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
hidden?: boolean
|
||||
cache?: boolean
|
||||
uri: { preview?: string; original?: string; remote?: string }
|
||||
blurhash?: string
|
||||
dimension?: { width: number; height: number }
|
||||
onPress?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
const GracefullyImage: React.FC<Props> = ({
|
||||
hidden = false,
|
||||
cache = false,
|
||||
uri,
|
||||
blurhash,
|
||||
dimension,
|
||||
onPress,
|
||||
style
|
||||
}) => {
|
||||
const { mode, theme } = useTheme()
|
||||
|
||||
const [imageVisible, setImageVisible] = useState<string>()
|
||||
const [imageLoadingFailed, setImageLoadingFailed] = useState(false)
|
||||
useEffect(() => {
|
||||
let cancel: CancelPromise
|
||||
const sideEffect = async (): Promise<void> => {
|
||||
try {
|
||||
if (uri.preview) {
|
||||
const tryPreview = getImageSize(uri.preview)
|
||||
cancel = tryPreview.cancel
|
||||
const res = await tryPreview.start()
|
||||
if (res) {
|
||||
setImageVisible(uri.preview)
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (__DEV__) console.warn('Image preview', error)
|
||||
}
|
||||
|
||||
try {
|
||||
const tryOriginal = getImageSize(uri.original!)
|
||||
cancel = tryOriginal.cancel
|
||||
const res = await tryOriginal.start()
|
||||
if (res) {
|
||||
setImageVisible(uri.original!)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
if (__DEV__) console.warn('Image original', error)
|
||||
}
|
||||
|
||||
try {
|
||||
if (uri.remote) {
|
||||
const tryRemote = getImageSize(uri.remote)
|
||||
cancel = tryRemote.cancel
|
||||
const res = await tryRemote.start()
|
||||
if (res) {
|
||||
setImageVisible(uri.remote)
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (__DEV__) console.warn('Image remote', error)
|
||||
}
|
||||
|
||||
setImageLoadingFailed(true)
|
||||
}
|
||||
|
||||
if (uri.original) {
|
||||
sideEffect()
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (cancel) {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}, [uri])
|
||||
|
||||
const children = useCallback(() => {
|
||||
if (imageVisible && !hidden) {
|
||||
if (cache) {
|
||||
return <ImageCache uri={imageVisible} style={styles.image} />
|
||||
} else {
|
||||
return <Image source={{ uri: imageVisible }} style={styles.image} />
|
||||
}
|
||||
} else if (blurhash) {
|
||||
return (
|
||||
<Surface
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
top: StyleConstants.Spacing.XS / 2,
|
||||
left: StyleConstants.Spacing.XS / 2
|
||||
}}
|
||||
>
|
||||
<Blurhash hash={blurhash} />
|
||||
</Surface>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<View
|
||||
style={[styles.image, { backgroundColor: theme.shimmerDefault }]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}, [hidden, mode, imageVisible])
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
children={children}
|
||||
style={[style, dimension && { ...dimension }]}
|
||||
{...(onPress
|
||||
? !imageVisible
|
||||
? { disabled: true }
|
||||
: { onPress }
|
||||
: { disabled: true })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
image: {
|
||||
flex: 1
|
||||
}
|
||||
})
|
||||
|
||||
export default GracefullyImage
|
@ -26,13 +26,13 @@ const HeaderRight: React.FC<Props> = ({
|
||||
const { theme } = useTheme()
|
||||
|
||||
const mounted = useRef(false)
|
||||
useEffect(() => {
|
||||
if (mounted.current) {
|
||||
layoutAnimation()
|
||||
} else {
|
||||
mounted.current = true
|
||||
}
|
||||
}, [content, loading, disabled])
|
||||
// useEffect(() => {
|
||||
// if (mounted.current) {
|
||||
// layoutAnimation()
|
||||
// } else {
|
||||
// mounted.current = true
|
||||
// }
|
||||
// }, [content, loading, disabled])
|
||||
|
||||
const loadingSpinkit = useMemo(
|
||||
() => (
|
||||
|
@ -108,7 +108,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
renderTabBar={() => null}
|
||||
onIndexChange={index => setSegment(index)}
|
||||
navigationState={{ index: segment, routes }}
|
||||
initialLayout={{ width: Dimensions.get('window').width }}
|
||||
initialLayout={{ width: Dimensions.get('screen').width }}
|
||||
/>
|
||||
)}
|
||||
</Stack.Screen>
|
||||
|
36
src/components/Timelines/Hashtag.tsx
Normal file
36
src/components/Timelines/Hashtag.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { Pressable, StyleSheet, Text } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
tag: Mastodon.Tag
|
||||
onPress: () => void
|
||||
}
|
||||
|
||||
const ComponentHashtag: React.FC<Props> = ({ tag, onPress }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[styles.itemDefault, { borderBottomColor: theme.border }]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text style={[styles.itemHashtag, { color: theme.primary }]}>
|
||||
#{tag.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
itemDefault: {
|
||||
padding: StyleConstants.Spacing.S * 1.5,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth
|
||||
},
|
||||
itemHashtag: {
|
||||
...StyleConstants.FontStyle.M
|
||||
}
|
||||
})
|
||||
|
||||
export default ComponentHashtag
|
@ -232,10 +232,10 @@ const Timeline: React.FC<Props> = ({
|
||||
{...(queryKey &&
|
||||
queryKey[1].page === 'RemotePublic' && { ListHeaderComponent })}
|
||||
{...(toot && isSuccess && { onScrollToIndexFailed })}
|
||||
// maintainVisibleContentPosition={{
|
||||
// minIndexForVisible: 0,
|
||||
// autoscrollToTopThreshold: 2
|
||||
// }}
|
||||
maintainVisibleContentPosition={{
|
||||
minIndexForVisible: 0,
|
||||
autoscrollToTopThreshold: 1
|
||||
}}
|
||||
{...customProps}
|
||||
/>
|
||||
)
|
||||
|
@ -101,8 +101,8 @@ const TimelineAttachment: React.FC<Props> = ({ status }) => {
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
{attachments}
|
||||
<View>
|
||||
<View style={styles.container}>{attachments}</View>
|
||||
|
||||
{status.sensitive &&
|
||||
(sensitiveShown ? (
|
||||
@ -123,7 +123,7 @@ const TimelineAttachment: React.FC<Props> = ({ status }) => {
|
||||
onPress={onPressShow}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: StyleConstants.Spacing.S,
|
||||
top: StyleConstants.Spacing.S * 2,
|
||||
left: StyleConstants.Spacing.S
|
||||
}}
|
||||
/>
|
||||
@ -133,7 +133,7 @@ const TimelineAttachment: React.FC<Props> = ({ status }) => {
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
container: {
|
||||
marginTop: StyleConstants.Spacing.S,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
@ -141,10 +141,6 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
alignContent: 'stretch'
|
||||
},
|
||||
container: {
|
||||
flexBasis: '50%',
|
||||
aspectRatio: 16 / 9
|
||||
},
|
||||
sensitiveBlur: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
|
@ -6,7 +6,8 @@ import { Audio } from 'expo-av'
|
||||
import { Surface } from 'gl-react-expo'
|
||||
import { Blurhash } from 'gl-react-blurhash'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { Image, StyleSheet, View } from 'react-native'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import GracefullyImage from '@components/GracefullyImage'
|
||||
|
||||
export interface Props {
|
||||
sensitiveShown: boolean
|
||||
@ -38,7 +39,7 @@ const AttachmentAudio: React.FC<Props> = ({ sensitiveShown, audio }) => {
|
||||
audioPlayer!.pauseAsync()
|
||||
setAudioPlaying(false)
|
||||
}, [audioPlayer])
|
||||
|
||||
console.log(audio)
|
||||
return (
|
||||
<View style={[styles.base, { backgroundColor: theme.disabled }]}>
|
||||
<View style={styles.overlay}>
|
||||
@ -56,9 +57,11 @@ const AttachmentAudio: React.FC<Props> = ({ sensitiveShown, audio }) => {
|
||||
) : (
|
||||
<>
|
||||
{(audio.preview_url || audio.preview_remote_url) && (
|
||||
<Image
|
||||
<GracefullyImage
|
||||
uri={{
|
||||
original: audio.preview_url || audio.preview_remote_url
|
||||
}}
|
||||
style={styles.background}
|
||||
source={{ uri: audio.preview_url || audio.preview_remote_url }}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Surface } from 'gl-react-expo'
|
||||
import { Blurhash } from 'gl-react-blurhash'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { Image, StyleSheet, Pressable } from 'react-native'
|
||||
import React, { useCallback } from 'react'
|
||||
import { StyleSheet } from 'react-native'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import GracefullyImage from '@components/GracefullyImage'
|
||||
|
||||
export interface Props {
|
||||
sensitiveShown: boolean
|
||||
@ -17,69 +16,19 @@ const AttachmentImage: React.FC<Props> = ({
|
||||
imageIndex,
|
||||
navigateToImagesViewer
|
||||
}) => {
|
||||
let isMounted = false
|
||||
useEffect(() => {
|
||||
isMounted = true
|
||||
|
||||
return () => {
|
||||
isMounted = false
|
||||
}
|
||||
})
|
||||
const [imageVisible, setImageVisible] = useState<string>()
|
||||
const [imageLoadingFailed, setImageLoadingFailed] = useState(false)
|
||||
useEffect(() => {
|
||||
const preFetch = () =>
|
||||
isMounted &&
|
||||
Image.getSize(
|
||||
image.preview_url,
|
||||
() => isMounted && setImageVisible(image.preview_url),
|
||||
() => {
|
||||
isMounted &&
|
||||
Image.getSize(
|
||||
image.url,
|
||||
() => isMounted && setImageVisible(image.url),
|
||||
() =>
|
||||
image.remote_url
|
||||
? isMounted &&
|
||||
Image.getSize(
|
||||
image.remote_url,
|
||||
() => isMounted && setImageVisible(image.remote_url),
|
||||
() => isMounted && setImageLoadingFailed(true)
|
||||
)
|
||||
: isMounted && setImageLoadingFailed(true)
|
||||
)
|
||||
}
|
||||
)
|
||||
preFetch()
|
||||
}, [isMounted])
|
||||
|
||||
const children = useCallback(() => {
|
||||
if (imageVisible && !sensitiveShown) {
|
||||
return <Image source={{ uri: imageVisible }} style={styles.image} />
|
||||
} else {
|
||||
return (
|
||||
<Surface
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
top: StyleConstants.Spacing.XS / 2,
|
||||
left: StyleConstants.Spacing.XS / 2
|
||||
}}
|
||||
>
|
||||
<Blurhash hash={image.blurhash} />
|
||||
</Surface>
|
||||
)
|
||||
}
|
||||
}, [imageVisible, sensitiveShown])
|
||||
const onPress = useCallback(() => navigateToImagesViewer(imageIndex), [])
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[styles.base]}
|
||||
children={children}
|
||||
<GracefullyImage
|
||||
hidden={sensitiveShown}
|
||||
uri={{
|
||||
preview: image.preview_url,
|
||||
original: image.url,
|
||||
remote: image.remote_url
|
||||
}}
|
||||
blurhash={image.blurhash}
|
||||
onPress={onPress}
|
||||
disabled={!imageVisible || sensitiveShown}
|
||||
style={styles.base}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -90,9 +39,6 @@ const styles = StyleSheet.create({
|
||||
flexBasis: '50%',
|
||||
aspectRatio: 16 / 9,
|
||||
padding: StyleConstants.Spacing.XS / 2
|
||||
},
|
||||
image: {
|
||||
flex: 1
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -23,6 +23,7 @@ const AttachmentVideo: React.FC<Props> = ({ sensitiveShown, video }) => {
|
||||
}
|
||||
await videoPlayer.current?.setPositionAsync(videoPosition)
|
||||
await videoPlayer.current?.presentFullscreenPlayer()
|
||||
console.log('playing!!!')
|
||||
videoPlayer.current?.playAsync()
|
||||
setVideoLoading(false)
|
||||
videoPlayer.current?.setOnPlaybackStatusUpdate(props => {
|
||||
@ -51,6 +52,11 @@ const AttachmentVideo: React.FC<Props> = ({ sensitiveShown, video }) => {
|
||||
posterSource={{ uri: video.preview_url }}
|
||||
posterStyle={{ resizeMode: 'cover' }}
|
||||
useNativeControls={false}
|
||||
onFullscreenUpdate={event => {
|
||||
if (event.fullscreenUpdate === 3) {
|
||||
videoPlayer.current?.pauseAsync()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Pressable style={styles.overlay}>
|
||||
{sensitiveShown ? (
|
||||
|
@ -1,9 +1,8 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { Pressable, StyleSheet } from 'react-native'
|
||||
import { Image } from 'react-native-expo-image-cache'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import GracefullyImage from '@components/GracefullyImage'
|
||||
|
||||
export interface Props {
|
||||
queryKey?: QueryKeyTimeline
|
||||
@ -18,23 +17,21 @@ const TimelineAvatar: React.FC<Props> = ({ queryKey, account }) => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Pressable style={styles.avatar} onPress={onPress}>
|
||||
<Image uri={account.avatar_static} style={styles.image} />
|
||||
</Pressable>
|
||||
<GracefullyImage
|
||||
cache
|
||||
onPress={onPress}
|
||||
uri={{ original: account.avatar_static }}
|
||||
dimension={{
|
||||
width: StyleConstants.Avatar.M,
|
||||
height: StyleConstants.Avatar.M
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
avatar: {
|
||||
flexBasis: StyleConstants.Avatar.M,
|
||||
height: StyleConstants.Avatar.M,
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 6
|
||||
}
|
||||
})
|
||||
|
||||
export default React.memo(TimelineAvatar, () => true)
|
||||
|
@ -1,82 +1,17 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Image, Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import GracefullyImage from '@components/GracefullyImage'
|
||||
import openLink from '@components/openLink'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { Surface } from 'gl-react-expo'
|
||||
import { Blurhash } from 'gl-react-blurhash'
|
||||
import React from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
card: Mastodon.Card
|
||||
}
|
||||
|
||||
type CancelPromise = ((reason?: Error) => void) | undefined
|
||||
type ImageSize = { width: number; height: number }
|
||||
interface ImageSizeOperation {
|
||||
start: () => Promise<ImageSize>
|
||||
cancel: CancelPromise
|
||||
}
|
||||
const getImageSize = (uri: string): ImageSizeOperation => {
|
||||
let cancel: CancelPromise
|
||||
const start = (): Promise<ImageSize> =>
|
||||
new Promise<{ width: number; height: number }>((resolve, reject) => {
|
||||
cancel = reject
|
||||
Image.getSize(
|
||||
uri,
|
||||
(width, height) => {
|
||||
cancel = undefined
|
||||
resolve({ width, height })
|
||||
},
|
||||
error => {
|
||||
reject(error)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return { start, cancel }
|
||||
}
|
||||
|
||||
const TimelineCard: React.FC<Props> = ({ card }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const [imageLoaded, setImageLoaded] = useState(false)
|
||||
useEffect(() => {
|
||||
if (card.image) {
|
||||
Image.getSize(card.image, () => setImageLoaded(true))
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
let cancel: CancelPromise
|
||||
const sideEffect = async (): Promise<void> => {
|
||||
try {
|
||||
const operation = getImageSize(card.image)
|
||||
cancel = operation.cancel
|
||||
await operation.start()
|
||||
} catch (error) {
|
||||
if (__DEV__) console.warn(error)
|
||||
}
|
||||
}
|
||||
if (card.image) {
|
||||
sideEffect()
|
||||
}
|
||||
return () => {
|
||||
if (cancel) {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
})
|
||||
const cardVisual = useMemo(() => {
|
||||
if (imageLoaded) {
|
||||
return <Image source={{ uri: card.image }} style={styles.image} />
|
||||
} else {
|
||||
return card.blurhash ? (
|
||||
<Surface style={styles.image}>
|
||||
<Blurhash hash={card.blurhash} />
|
||||
</Surface>
|
||||
) : null
|
||||
}
|
||||
}, [imageLoaded])
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[styles.card, { borderColor: theme.border }]}
|
||||
@ -84,9 +19,11 @@ const TimelineCard: React.FC<Props> = ({ card }) => {
|
||||
testID='base'
|
||||
>
|
||||
{card.image && (
|
||||
<View style={styles.left} testID='image'>
|
||||
{cardVisual}
|
||||
</View>
|
||||
<GracefullyImage
|
||||
uri={{ original: card.image }}
|
||||
blurhash={card.blurhash}
|
||||
style={styles.left}
|
||||
/>
|
||||
)}
|
||||
<View style={styles.right}>
|
||||
<Text
|
||||
@ -117,13 +54,14 @@ const styles = StyleSheet.create({
|
||||
card: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
height: StyleConstants.Avatar.L,
|
||||
height: StyleConstants.Font.LineHeight.M * 4.5,
|
||||
marginTop: StyleConstants.Spacing.M,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderRadius: 6
|
||||
},
|
||||
left: {
|
||||
width: StyleConstants.Avatar.L
|
||||
width: StyleConstants.Font.LineHeight.M * 4.5,
|
||||
height: StyleConstants.Font.LineHeight.M * 4.5
|
||||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
|
Reference in New Issue
Block a user