1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00
This commit is contained in:
Zhiyuan Zheng
2021-01-16 00:00:31 +01:00
parent 9f4a4e908c
commit 5ec9118fb2
31 changed files with 607 additions and 416 deletions

View File

@ -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>

View File

@ -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}
/>
)
}

View 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

View File

@ -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(
() => (

View File

@ -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>

View 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

View File

@ -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}
/>
)

View File

@ -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%',

View File

@ -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

View File

@ -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
}
})

View File

@ -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 ? (

View File

@ -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)

View File

@ -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%',