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-02-08 23:47:20 +01:00
parent f5414412d4
commit 383ebc2775
79 changed files with 150 additions and 137 deletions

View File

@@ -0,0 +1,149 @@
import analytics from '@components/analytics'
import Button from '@components/Button'
import GracefullyImage from '@components/GracefullyImage'
import { Slider } from '@sharcoux/slider'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { Audio } from 'expo-av'
import React, { useCallback, useState } from 'react'
import { StyleSheet, View } from 'react-native'
import { Blurhash } from 'react-native-blurhash'
import attachmentAspectRatio from './aspectRatio'
export interface Props {
total: number
index: number
sensitiveShown: boolean
audio: Mastodon.AttachmentAudio
}
const AttachmentAudio: React.FC<Props> = ({
total,
index,
sensitiveShown,
audio
}) => {
const { theme } = useTheme()
const [audioPlayer, setAudioPlayer] = useState<Audio.Sound>()
const [audioPlaying, setAudioPlaying] = useState(false)
const [audioPosition, setAudioPosition] = useState(0)
const playAudio = useCallback(async () => {
analytics('timeline_shared_attachment_audio_play_press', { id: audio.id })
if (!audioPlayer) {
const { sound } = await Audio.Sound.createAsync(
{ uri: audio.url },
{},
// @ts-ignore
props => setAudioPosition(props.positionMillis)
)
setAudioPlayer(sound)
} else {
await audioPlayer.setPositionAsync(audioPosition)
audioPlayer.playAsync()
setAudioPlaying(true)
}
}, [audioPlayer, audioPosition])
const pauseAudio = useCallback(async () => {
analytics('timeline_shared_attachment_audio_pause_press', { id: audio.id })
audioPlayer!.pauseAsync()
setAudioPlaying(false)
}, [audioPlayer])
return (
<View
style={[
styles.base,
{
backgroundColor: theme.disabled,
aspectRatio: attachmentAspectRatio({ total, index })
}
]}
>
<View style={styles.overlay}>
{sensitiveShown ? (
audio.blurhash ? (
<Blurhash
blurhash={audio.blurhash}
style={{
width: '100%',
height: '100%'
}}
/>
) : null
) : (
<>
{audio.preview_url && (
<GracefullyImage
uri={{
original: audio.preview_url,
remote: audio.preview_remote_url
}}
style={styles.background}
/>
)}
<Button
type='icon'
content={audioPlaying ? 'PauseCircle' : 'PlayCircle'}
size='L'
round
overlay
{...(audioPlaying
? { onPress: pauseAudio }
: { onPress: playAudio })}
/>
</>
)}
</View>
<View
style={{
alignSelf: 'flex-end',
width: '100%',
height: StyleConstants.Spacing.M + StyleConstants.Spacing.S * 2,
backgroundColor: theme.backgroundOverlay,
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
borderRadius: 100,
opacity: sensitiveShown ? 0.35 : undefined
}}
>
<Slider
minimumValue={0}
maximumValue={audio.meta.original.duration * 1000}
value={audioPosition}
minimumTrackTintColor={theme.secondary}
maximumTrackTintColor={theme.disabled}
// onSlidingStart={() => {
// audioPlayer?.pauseAsync()
// setAudioPlaying(false)
// }}
// onSlidingComplete={value => {
// setAudioPosition(value)
// }}
enabled={false} // Bug in above sliding actions
thumbSize={StyleConstants.Spacing.M}
thumbTintColor={theme.primaryOverlay}
/>
</View>
</View>
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
flexBasis: '50%',
padding: StyleConstants.Spacing.XS / 2,
flexDirection: 'row'
},
background: { position: 'absolute', width: '100%', height: '100%' },
overlay: {
position: 'absolute',
width: '100%',
height: '100%',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
})
export default AttachmentAudio

View File

@@ -0,0 +1,58 @@
import analytics from '@components/analytics'
import GracefullyImage from '@components/GracefullyImage'
import { StyleConstants } from '@utils/styles/constants'
import React, { useCallback } from 'react'
import { StyleSheet } from 'react-native'
import attachmentAspectRatio from './aspectRatio'
export interface Props {
total: number
index: number
sensitiveShown: boolean
image: Mastodon.AttachmentImage
navigateToImagesViewer: (imageIndex: number) => void
}
const AttachmentImage: React.FC<Props> = ({
total,
index,
sensitiveShown,
image,
navigateToImagesViewer
}) => {
const onPress = useCallback(() => {
analytics('timeline_shared_attachment_image_press', { id: image.id })
navigateToImagesViewer(index)
}, [])
return (
<GracefullyImage
hidden={sensitiveShown}
uri={{
preview: image.preview_url,
original: image.url,
remote: image.remote_url
}}
sharedElement={image.url}
blurhash={image.blurhash}
onPress={onPress}
style={[
styles.base,
{ aspectRatio: attachmentAspectRatio({ total, index }) }
]}
/>
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
flexBasis: '50%',
padding: StyleConstants.Spacing.XS / 2
}
})
export default React.memo(
AttachmentImage,
(prev, next) => prev.sensitiveShown === next.sensitiveShown
)

View File

@@ -0,0 +1,88 @@
import analytics from '@components/analytics'
import Button from '@components/Button'
import openLink from '@components/openLink'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native'
import { Blurhash } from 'react-native-blurhash'
import attachmentAspectRatio from './aspectRatio'
export interface Props {
total: number
index: number
sensitiveShown: boolean
attachment: Mastodon.AttachmentUnknown
}
const AttachmentUnsupported: React.FC<Props> = ({
total,
index,
sensitiveShown,
attachment
}) => {
const { t } = useTranslation('componentTimeline')
const { theme } = useTheme()
return (
<View
style={[
styles.base,
{ aspectRatio: attachmentAspectRatio({ total, index }) }
]}
>
{attachment.blurhash ? (
<Blurhash
blurhash={attachment.blurhash}
style={{
position: 'absolute',
width: '100%',
height: '100%'
}}
/>
) : null}
{!sensitiveShown ? (
<>
<Text
style={[
styles.text,
{ color: attachment.blurhash ? theme.background : theme.primary }
]}
>
{t('shared.attachment.unsupported.text')}
</Text>
{attachment.remote_url ? (
<Button
type='text'
content={t('shared.attachment.unsupported.button')}
size='S'
overlay
onPress={async () => {
analytics('timeline_shared_attachment_unsupported_press')
attachment.remote_url && (await openLink(attachment.remote_url))
}}
/>
) : null}
</>
) : null}
</View>
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
flexBasis: '50%',
padding: StyleConstants.Spacing.XS / 2,
justifyContent: 'center',
alignItems: 'center'
},
text: {
...StyleConstants.FontStyle.S,
textAlign: 'center',
marginBottom: StyleConstants.Spacing.S
}
})
export default AttachmentUnsupported

View File

@@ -0,0 +1,129 @@
import Button from '@components/Button'
import { StyleConstants } from '@utils/styles/constants'
import { Video } from 'expo-av'
import React, { useCallback, useRef, useState } from 'react'
import { Pressable, StyleSheet, View } from 'react-native'
import { Blurhash } from 'react-native-blurhash'
import attachmentAspectRatio from './aspectRatio'
import analytics from '@components/analytics'
export interface Props {
total: number
index: number
sensitiveShown: boolean
video: Mastodon.AttachmentVideo | Mastodon.AttachmentGifv
gifv?: boolean
}
const AttachmentVideo: React.FC<Props> = ({
total,
index,
sensitiveShown,
video,
gifv = false
}) => {
const videoPlayer = useRef<Video>(null)
const [videoLoading, setVideoLoading] = useState(false)
const [videoLoaded, setVideoLoaded] = useState(false)
const [videoPosition, setVideoPosition] = useState<number>(0)
const playOnPress = useCallback(async () => {
analytics('timeline_shared_attachment_video_length', {
length: video.meta?.length
})
analytics('timeline_shared_attachment_vide_play_press', {
id: video.id,
timestamp: Date.now()
})
setVideoLoading(true)
if (!videoLoaded) {
await videoPlayer.current?.loadAsync({ uri: video.url })
}
await videoPlayer.current?.setPositionAsync(videoPosition)
await videoPlayer.current?.presentFullscreenPlayer()
videoPlayer.current?.playAsync()
setVideoLoading(false)
videoPlayer.current?.setOnPlaybackStatusUpdate(props => {
if (props.isLoaded) {
setVideoLoaded(true)
}
// @ts-ignore
if (props.positionMillis) {
// @ts-ignore
setVideoPosition(props.positionMillis)
}
})
}, [videoLoaded, videoPosition])
return (
<View
style={[
styles.base,
{ aspectRatio: attachmentAspectRatio({ total, index }) }
]}
>
<Video
ref={videoPlayer}
style={{
width: '100%',
height: '100%',
opacity: sensitiveShown ? 0 : 1
}}
resizeMode='cover'
usePoster
posterSource={{ uri: video.preview_url }}
posterStyle={{ resizeMode: 'cover' }}
useNativeControls={false}
onFullscreenUpdate={event => {
if (event.fullscreenUpdate === 3) {
analytics('timeline_shared_attachment_video_pause_press', {
id: video.id,
timestamp: Date.now()
})
videoPlayer.current?.pauseAsync()
}
}}
/>
<Pressable style={styles.overlay}>
{sensitiveShown ? (
video.blurhash ? (
<Blurhash
blurhash={video.blurhash}
style={{
width: '100%',
height: '100%'
}}
/>
) : null
) : gifv ? null : (
<Button
round
overlay
size='L'
type='icon'
content='PlayCircle'
onPress={playOnPress}
loading={videoLoading}
/>
)}
</Pressable>
</View>
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
flexBasis: '50%',
padding: StyleConstants.Spacing.XS / 2
},
overlay: {
position: 'absolute',
width: '100%',
height: '100%',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
})
export default AttachmentVideo

View File

@@ -0,0 +1,25 @@
const attachmentAspectRatio = ({
total,
index
}: {
total: number
index?: number
}) => {
switch (total) {
case 1:
case 4:
return 16 / 9
case 2:
return 8 / 9
case 3:
if (index === 2) {
return 32 / 9
} else {
return 16 / 9
}
default:
return 16 / 9
}
}
export default attachmentAspectRatio