mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Fixed #17
This commit is contained in:
149
src/components/Timeline/Shared/Attachment/Audio.tsx
Normal file
149
src/components/Timeline/Shared/Attachment/Audio.tsx
Normal 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
|
58
src/components/Timeline/Shared/Attachment/Image.tsx
Normal file
58
src/components/Timeline/Shared/Attachment/Image.tsx
Normal 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
|
||||
)
|
88
src/components/Timeline/Shared/Attachment/Unsupported.tsx
Normal file
88
src/components/Timeline/Shared/Attachment/Unsupported.tsx
Normal 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
|
129
src/components/Timeline/Shared/Attachment/Video.tsx
Normal file
129
src/components/Timeline/Shared/Attachment/Video.tsx
Normal 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
|
25
src/components/Timeline/Shared/Attachment/aspectRatio.ts
Normal file
25
src/components/Timeline/Shared/Attachment/aspectRatio.ts
Normal 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
|
Reference in New Issue
Block a user