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:
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
@ -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
|
@ -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
|
||||
)
|
@ -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
|
@ -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
|
Reference in New Issue
Block a user