1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Rewrite attachment views

This commit is contained in:
Zhiyuan Zheng
2020-12-25 18:20:09 +01:00
parent dca138eb5c
commit 6eeb86921c
17 changed files with 789 additions and 323 deletions

View File

@ -75,7 +75,7 @@ const TimelineDefault: React.FC<Props> = ({
/>
)}
{actualStatus.media_attachments.length > 0 && (
<TimelineAttachment status={actualStatus} width={contentWidth} />
<TimelineAttachment status={actualStatus} contentWidth={contentWidth} />
)}
{actualStatus.card && <TimelineCard card={actualStatus.card} />}
</View>

View File

@ -1,71 +1,23 @@
import { BlurView } from 'expo-blur'
import React, { useCallback, useEffect, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import AttachmentImage from '@components/Timelines/Timeline/Shared/Attachment/AttachmentImage'
import AttachmentVideo from '@components/Timelines/Timeline/Shared/Attachment/AttachmentVideo'
import AttachmentImage from '@root/components/Timelines/Timeline/Shared/Attachment/Image'
import AttachmentVideo from '@root/components/Timelines/Timeline/Shared/Attachment/Video'
import { IImageInfo } from 'react-native-image-zoom-viewer/built/image-viewer.type'
import { useNavigation } from '@react-navigation/native'
import AttachmentUnsupported from './Attachment/Unsupported'
export interface Props {
status: Pick<Mastodon.Status, 'media_attachments' | 'sensitive'>
width: number
contentWidth: number
}
const TimelineAttachment: React.FC<Props> = ({ status, width }) => {
const { mode, theme } = useTheme()
const allTypes = status.media_attachments.map(m => m.type)
let attachment
let attachmentHeight
const TimelineAttachment: React.FC<Props> = ({ status, contentWidth }) => {
const { theme } = useTheme()
if (allTypes.includes('image')) {
attachment = (
<AttachmentImage
media_attachments={status.media_attachments}
width={width}
/>
)
attachmentHeight = (width / 16) * 9
} else if (allTypes.includes('gifv')) {
attachment = (
<AttachmentVideo
media_attachments={status.media_attachments}
width={width}
/>
)
attachmentHeight =
status.media_attachments[0].meta?.original?.width &&
status.media_attachments[0].meta?.original?.height
? (width / status.media_attachments[0].meta.original.width) *
status.media_attachments[0].meta.original.height
: (width / 16) * 9
} else if (allTypes.includes('video')) {
attachment = (
<AttachmentVideo
media_attachments={status.media_attachments}
width={width}
/>
)
attachmentHeight =
status.media_attachments[0].meta?.original?.width &&
status.media_attachments[0].meta?.original?.height
? (width / status.media_attachments[0].meta.original.width) *
status.media_attachments[0].meta.original.height
: (width / 16) * 9
} else if (allTypes.includes('audio')) {
// attachment = (
// <AttachmentAudio
// media_attachments={media_attachments}
// sensitive={sensitive}
// width={width}
// />
// )
} else {
attachment = <Text></Text>
attachmentHeight = 25
}
const [sensitiveShown, setSensitiveShown] = useState(true)
const [sensitiveShown, setSensitiveShown] = useState(status.sensitive)
const onPressBlurView = useCallback(() => {
setSensitiveShown(false)
}, [])
@ -77,40 +29,118 @@ const TimelineAttachment: React.FC<Props> = ({ status, width }) => {
}
}, [sensitiveShown])
let imageUrls: (IImageInfo & {
preview_url: Mastodon.AttachmentImage['preview_url']
remote_url?: Mastodon.AttachmentImage['remote_url']
imageIndex: number
})[] = []
const navigation = useNavigation()
const navigateToImagesViewer = (imageIndex: number) =>
navigation.navigate('Screen-Shared-ImagesViewer', {
imageUrls,
imageIndex
})
const attachments = useMemo(
() =>
status.media_attachments.map((attachment, index) => {
switch (attachment.type) {
case 'image':
imageUrls.push({
url: attachment.url,
width: attachment.meta?.original?.width,
height: attachment.meta?.original?.height,
preview_url: attachment.preview_url,
remote_url: attachment.remote_url,
imageIndex: index
})
return (
<AttachmentImage
key={index}
sensitiveShown={sensitiveShown}
image={attachment}
imageIndex={index}
navigateToImagesViewer={navigateToImagesViewer}
/>
)
case 'video':
return (
<AttachmentVideo
key={index}
sensitiveShown={sensitiveShown}
video={attachment}
width={contentWidth}
height={(contentWidth / 16) * 9}
/>
)
case 'gifv':
return (
<AttachmentVideo
key={index}
sensitiveShown={sensitiveShown}
video={attachment}
width={contentWidth}
height={(contentWidth / 16) * 9}
/>
)
case 'audio':
return <Text key={index}></Text>
default:
return <AttachmentUnsupported key={index} attachment={attachment} />
}
}),
[sensitiveShown]
)
return (
<View
style={{
width: width + StyleConstants.Spacing.XS,
height: attachmentHeight,
marginTop: StyleConstants.Spacing.S,
marginLeft: -StyleConstants.Spacing.XS / 2
}}
style={[
styles.base,
{ width: contentWidth, height: (contentWidth / 16) * 9 }
]}
>
{attachment}
{attachments}
{status.sensitive && sensitiveShown && (
<BlurView tint={mode} intensity={100} style={styles.blurView}>
<Pressable onPress={onPressBlurView} style={styles.blurViewPressable}>
<Text style={[styles.sensitiveText, { color: theme.primary }]}>
<Pressable style={styles.sensitiveBlur}>
<Pressable
onPress={onPressBlurView}
style={[
styles.sensitiveBlurButton,
{ backgroundColor: theme.backgroundOverlay }
]}
>
<Text
style={[styles.sensitiveText, { color: theme.primaryOverlay }]}
>
</Text>
</Pressable>
</BlurView>
</Pressable>
)}
</View>
)
}
const styles = StyleSheet.create({
blurView: {
base: {
marginTop: StyleConstants.Spacing.S,
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
alignContent: 'stretch'
},
sensitiveBlur: {
position: 'absolute',
width: '100%',
height: '100%'
},
blurViewPressable: {
height: '100%',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
sensitiveBlurButton: {
padding: StyleConstants.Spacing.S,
borderRadius: 6
},
sensitiveText: {
fontSize: StyleConstants.Font.Size.M
}

View File

@ -1,80 +0,0 @@
import React, { useState } from 'react'
import { Image, Modal, StyleSheet, Pressable, View } from 'react-native'
import ImageViewer from 'react-native-image-zoom-viewer'
import { StyleConstants } from '@utils/styles/constants'
export interface Props {
media_attachments: Mastodon.Attachment[]
width: number
}
const AttachmentImage: React.FC<Props> = ({ media_attachments }) => {
const [imageModalVisible, setImageModalVisible] = useState(false)
const [imageModalIndex, setImageModalIndex] = useState(0)
let images: {
url: string
width: number | undefined
height: number | undefined
}[] = []
const imagesNode = media_attachments.map((m, i) => {
images.push({
url: m.url,
width: m.meta?.original?.width || undefined,
height: m.meta?.original?.height || undefined
})
return (
<Pressable
key={i}
style={[styles.imageContainer]}
onPress={() => {
setImageModalIndex(i)
setImageModalVisible(true)
}}
>
<Image source={{ uri: m.preview_url }} style={styles.image} />
</Pressable>
)
})
return (
<>
<View style={[styles.media]}>{imagesNode}</View>
<Modal
visible={imageModalVisible}
transparent={true}
animationType='fade'
>
<ImageViewer
imageUrls={images}
index={imageModalIndex}
onSwipeDown={() => setImageModalVisible(false)}
enableSwipeDown={true}
swipeDownThreshold={100}
useNativeDriver={true}
saveToLocalByLongPress={false}
/>
</Modal>
</>
)
}
const styles = StyleSheet.create({
media: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
alignContent: 'stretch'
},
imageContainer: {
flex: 1,
flexBasis: '50%',
padding: StyleConstants.Spacing.XS / 2
},
image: {
flex: 1
}
})
export default React.memo(AttachmentImage, () => true)

View File

@ -1,61 +0,0 @@
import React, { useCallback, useRef, useState } from 'react'
import { View } from 'react-native'
import { Video } from 'expo-av'
import { ButtonRound } from '@components/Button'
export interface Props {
media_attachments: Mastodon.Attachment[]
width: number
}
const AttachmentVideo: React.FC<Props> = ({ media_attachments, width }) => {
const videoPlayer = useRef<Video>(null)
const [videoPlay, setVideoPlay] = useState(false)
const video = media_attachments[0]
const videoWidth = width
const videoHeight =
video.meta?.original?.width && video.meta?.original?.height
? (width / video.meta.original.width) * video.meta.original.height
: (width / 16) * 9
const playOnPress = useCallback(() => {
// @ts-ignore
videoPlayer.current.presentFullscreenPlayer()
setVideoPlay(true)
}, [])
return (
<View
style={{
width: videoWidth,
height: videoHeight
}}
>
<Video
ref={videoPlayer}
source={{ uri: video.remote_url || video.url }}
style={{
width: videoWidth,
height: videoHeight
}}
resizeMode='cover'
usePoster
posterSource={{ uri: video.preview_url }}
useNativeControls
shouldPlay={videoPlay}
/>
{videoPlayer.current && !videoPlay && (
<ButtonRound
icon='play'
size='L'
onPress={playOnPress}
styles={{ top: videoHeight / 2, left: videoWidth / 2 }}
coordinate='center'
/>
)}
</View>
)
}
export default AttachmentVideo

View File

@ -0,0 +1,96 @@
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 { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@root/utils/styles/layoutAnimation'
export interface Props {
sensitiveShown: boolean
image: Mastodon.AttachmentImage
imageIndex: number
navigateToImagesViewer: (imageIndex: number) => void
}
const AttachmentImage: React.FC<Props> = ({
sensitiveShown,
image,
imageIndex,
navigateToImagesViewer
}) => {
layoutAnimation()
const [imageVisible, setImageVisible] = useState<string>()
const [imageLoadingFailed, setImageLoadingFailed] = useState(false)
useEffect(
() =>
Image.getSize(
image.preview_url,
() => setImageVisible(image.preview_url),
() => {
Image.getSize(
image.url,
() => setImageVisible(image.url),
() =>
image.remote_url
? Image.getSize(
image.remote_url,
() => setImageVisible(image.remote_url),
() => setImageLoadingFailed(true)
)
: setImageLoadingFailed(true)
)
}
),
[]
)
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(() => {
if (imageVisible && !sensitiveShown) {
navigateToImagesViewer(imageIndex)
}
}, [imageVisible, sensitiveShown])
return (
<Pressable
style={[styles.imageContainer]}
children={children}
onPress={onPress}
/>
)
}
const styles = StyleSheet.create({
imageContainer: {
flex: 1,
flexBasis: '50%',
padding: StyleConstants.Spacing.XS / 2
},
image: {
flex: 1
}
})
export default React.memo(
AttachmentImage,
(prev, next) => prev.sensitiveShown === next.sensitiveShown
)

View File

@ -0,0 +1,41 @@
import React from 'react'
import { StyleSheet, Text, View } from 'react-native'
import { ButtonRow } from '@components/Button'
import { useTheme } from '@root/utils/styles/ThemeManager'
import { StyleConstants } from '@root/utils/styles/constants'
import openLink from '@root/utils/openLink'
export interface Props {
attachment: Mastodon.Attachment
}
const AttachmentUnsupported: React.FC<Props> = ({ attachment }) => {
const { theme } = useTheme()
return (
<View style={styles.base}>
<Text style={[styles.text, { color: theme.primary }]}></Text>
{attachment.remote_url ? (
<ButtonRow
text='尝试远程链接'
size='S'
onPress={async () => await openLink(attachment.remote_url!)}
/>
) : null}
</View>
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
text: {
fontSize: StyleConstants.Font.Size.S,
textAlign: 'center',
marginBottom: StyleConstants.Spacing.S
}
})
export default AttachmentUnsupported

View File

@ -0,0 +1,88 @@
import React, { useCallback, useRef, useState } from 'react'
import { Pressable, StyleSheet } from 'react-native'
import { Video } from 'expo-av'
import { ButtonRow } from '@components/Button'
import layoutAnimation from '@root/utils/styles/layoutAnimation'
import { Surface } from 'gl-react-expo'
import { Blurhash } from 'gl-react-blurhash'
export interface Props {
sensitiveShown: boolean
video: Mastodon.AttachmentVideo | Mastodon.AttachmentGifv
width: number
height: number
}
const AttachmentVideo: React.FC<Props> = ({
sensitiveShown,
video,
width,
height
}) => {
layoutAnimation()
const videoPlayer = useRef<Video>(null)
const [videoLoaded, setVideoLoaded] = useState(false)
const [videoPosition, setVideoPosition] = useState<number>(0)
const playOnPress = useCallback(async () => {
if (!videoLoaded) {
await videoPlayer.current?.loadAsync({ uri: video.url })
}
await videoPlayer.current?.setPositionAsync(videoPosition)
await videoPlayer.current?.presentFullscreenPlayer()
videoPlayer.current?.playAsync()
videoPlayer.current?.setOnPlaybackStatusUpdate(props => {
if (props.isLoaded) {
setVideoLoaded(true)
}
// @ts-ignore
if (props.positionMillis) {
// @ts-ignore
setVideoPosition(props.positionMillis)
}
})
}, [videoLoaded, videoPosition])
return (
<>
<Video
ref={videoPlayer}
style={{
width,
height
}}
resizeMode='cover'
usePoster
posterSource={{ uri: video.preview_url }}
useNativeControls={false}
/>
<Pressable style={styles.overlay}>
{sensitiveShown ? (
<Surface
style={{
width: '100%',
height: '100%'
}}
>
<Blurhash hash={video.blurhash} />
</Surface>
) : (
<ButtonRow icon='play' size='L' onPress={playOnPress} />
)}
</Pressable>
</>
)
}
const styles = StyleSheet.create({
overlay: {
position: 'absolute',
width: '100%',
height: '100%',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
})
export default AttachmentVideo