mirror of https://github.com/tooot-app/app
Fine tune compose
This commit is contained in:
parent
add331ef0e
commit
e5eaf162f4
|
@ -22,6 +22,7 @@
|
||||||
"expo-image-picker": "~9.1.1",
|
"expo-image-picker": "~9.1.1",
|
||||||
"expo-linear-gradient": "~8.3.0",
|
"expo-linear-gradient": "~8.3.0",
|
||||||
"expo-localization": "^9.0.0",
|
"expo-localization": "^9.0.0",
|
||||||
|
"expo-permissions": "~9.3.0",
|
||||||
"expo-secure-store": "~9.2.0",
|
"expo-secure-store": "~9.2.0",
|
||||||
"expo-splash-screen": "~0.6.1",
|
"expo-splash-screen": "~0.6.1",
|
||||||
"expo-status-bar": "~1.0.2",
|
"expo-status-bar": "~1.0.2",
|
||||||
|
|
|
@ -1,137 +1,3 @@
|
||||||
type AttachmentImage = {
|
|
||||||
// Base
|
|
||||||
id: string
|
|
||||||
type: 'image'
|
|
||||||
url: string
|
|
||||||
preview_url: string
|
|
||||||
|
|
||||||
// Others
|
|
||||||
remote_url?: string
|
|
||||||
text_url?: string
|
|
||||||
meta?: {
|
|
||||||
original?: { width: number; height: number; size: string; aspect: number }
|
|
||||||
small?: { width: number; height: number; size: string; aspect: number }
|
|
||||||
focus?: { x: number; y: number }
|
|
||||||
}
|
|
||||||
description?: string
|
|
||||||
blurhash?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type AttachmentVideo = {
|
|
||||||
// Base
|
|
||||||
id: string
|
|
||||||
type: 'video'
|
|
||||||
url: string
|
|
||||||
preview_url: string
|
|
||||||
|
|
||||||
// Others
|
|
||||||
remote_url?: string
|
|
||||||
text_url?: string
|
|
||||||
meta?: {
|
|
||||||
length: string
|
|
||||||
duration: number
|
|
||||||
fps: number
|
|
||||||
size: string
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
aspect: number
|
|
||||||
audio_encode: string
|
|
||||||
audio_bitrate: string
|
|
||||||
audio_channels: string
|
|
||||||
original: {
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
frame_rate: string
|
|
||||||
duration: number
|
|
||||||
bitrate: number
|
|
||||||
}
|
|
||||||
small: {
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
size: string
|
|
||||||
aspect: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
description?: string
|
|
||||||
blurhash?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type AttachmentGifv = {
|
|
||||||
// Base
|
|
||||||
id: string
|
|
||||||
type: 'gifv'
|
|
||||||
url: string
|
|
||||||
preview_url: string
|
|
||||||
|
|
||||||
// Others
|
|
||||||
remote_url?: string
|
|
||||||
text_url?: string
|
|
||||||
meta?: {
|
|
||||||
length: string
|
|
||||||
duration: number
|
|
||||||
fps: number
|
|
||||||
size: string
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
aspect: number
|
|
||||||
original: {
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
frame_rate: string
|
|
||||||
duration: number
|
|
||||||
bitrate: number
|
|
||||||
}
|
|
||||||
small: {
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
size: string
|
|
||||||
aspect: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
description?: string
|
|
||||||
blurhash?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type AttachmentAudio = {
|
|
||||||
// Base
|
|
||||||
id: string
|
|
||||||
type: 'audio'
|
|
||||||
url: string
|
|
||||||
preview_url: string
|
|
||||||
|
|
||||||
// Others
|
|
||||||
remote_url?: string
|
|
||||||
text_url?: string
|
|
||||||
meta?: {
|
|
||||||
length: string
|
|
||||||
duration: number
|
|
||||||
audio_encode: string
|
|
||||||
audio_bitrate: string
|
|
||||||
audio_channels: string
|
|
||||||
original: {
|
|
||||||
duration: number
|
|
||||||
bitrate: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
description?: string
|
|
||||||
blurhash?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type AttachmentUnknown = {
|
|
||||||
// Base
|
|
||||||
id: string
|
|
||||||
type: 'unknown'
|
|
||||||
url: string
|
|
||||||
preview_url: string
|
|
||||||
|
|
||||||
// Others
|
|
||||||
remote_url?: string
|
|
||||||
text_url?: string
|
|
||||||
meta?: any
|
|
||||||
description?: string
|
|
||||||
blurhash?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
declare namespace Mastodon {
|
declare namespace Mastodon {
|
||||||
type Account = {
|
type Account = {
|
||||||
// Base
|
// Base
|
||||||
|
@ -177,7 +43,141 @@ declare namespace Mastodon {
|
||||||
| AttachmentVideo
|
| AttachmentVideo
|
||||||
| AttachmentGifv
|
| AttachmentGifv
|
||||||
| AttachmentAudio
|
| AttachmentAudio
|
||||||
| AttachmentUnknown
|
// | AttachmentUnknown
|
||||||
|
|
||||||
|
type AttachmentImage = {
|
||||||
|
// Base
|
||||||
|
id: string
|
||||||
|
type: 'image'
|
||||||
|
url: string
|
||||||
|
preview_url: string
|
||||||
|
|
||||||
|
// Others
|
||||||
|
remote_url?: string
|
||||||
|
text_url?: string
|
||||||
|
meta?: {
|
||||||
|
original?: { width: number; height: number; size: string; aspect: number }
|
||||||
|
small?: { width: number; height: number; size: string; aspect: number }
|
||||||
|
focus?: { x: number; y: number }
|
||||||
|
}
|
||||||
|
description?: string
|
||||||
|
blurhash?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttachmentVideo = {
|
||||||
|
// Base
|
||||||
|
id: string
|
||||||
|
type: 'video'
|
||||||
|
url: string
|
||||||
|
preview_url: string
|
||||||
|
|
||||||
|
// Others
|
||||||
|
remote_url?: string
|
||||||
|
text_url?: string
|
||||||
|
meta?: {
|
||||||
|
length: string
|
||||||
|
duration: number
|
||||||
|
fps: number
|
||||||
|
size: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
aspect: number
|
||||||
|
audio_encode: string
|
||||||
|
audio_bitrate: string
|
||||||
|
audio_channels: string
|
||||||
|
original: {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
frame_rate: string
|
||||||
|
duration: number
|
||||||
|
bitrate: number
|
||||||
|
}
|
||||||
|
small: {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
size: string
|
||||||
|
aspect: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
description?: string
|
||||||
|
blurhash?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttachmentGifv = {
|
||||||
|
// Base
|
||||||
|
id: string
|
||||||
|
type: 'gifv'
|
||||||
|
url: string
|
||||||
|
preview_url: string
|
||||||
|
|
||||||
|
// Others
|
||||||
|
remote_url?: string
|
||||||
|
text_url?: string
|
||||||
|
meta?: {
|
||||||
|
length: string
|
||||||
|
duration: number
|
||||||
|
fps: number
|
||||||
|
size: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
aspect: number
|
||||||
|
original: {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
frame_rate: string
|
||||||
|
duration: number
|
||||||
|
bitrate: number
|
||||||
|
}
|
||||||
|
small: {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
size: string
|
||||||
|
aspect: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
description?: string
|
||||||
|
blurhash?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttachmentAudio = {
|
||||||
|
// Base
|
||||||
|
id: string
|
||||||
|
type: 'audio'
|
||||||
|
url: string
|
||||||
|
preview_url: string
|
||||||
|
|
||||||
|
// Others
|
||||||
|
remote_url?: string
|
||||||
|
text_url?: string
|
||||||
|
meta?: {
|
||||||
|
length: string
|
||||||
|
duration: number
|
||||||
|
audio_encode: string
|
||||||
|
audio_bitrate: string
|
||||||
|
audio_channels: string
|
||||||
|
original: {
|
||||||
|
duration: number
|
||||||
|
bitrate: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
description?: string
|
||||||
|
blurhash?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// type AttachmentUnknown = {
|
||||||
|
// // Base
|
||||||
|
// id: string
|
||||||
|
// type: 'unknown'
|
||||||
|
// url: string
|
||||||
|
// preview_url: string
|
||||||
|
|
||||||
|
// // Others
|
||||||
|
// remote_url?: string
|
||||||
|
// text_url?: string
|
||||||
|
// meta?: any
|
||||||
|
// description?: string
|
||||||
|
// blurhash?: string
|
||||||
|
// }
|
||||||
|
|
||||||
type Card = {
|
type Card = {
|
||||||
// Base
|
// Base
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||||
import { StyleConstants } from 'src/utils/styles/constants'
|
import { StyleConstants } from 'src/utils/styles/constants'
|
||||||
import Button from './Button'
|
import { ButtonRow } from './Button'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
@ -84,7 +84,7 @@ const BottomSheet: React.FC<Props> = ({ children, visible, handleDismiss }) => {
|
||||||
style={[styles.handle, { backgroundColor: theme.background }]}
|
style={[styles.handle, { backgroundColor: theme.background }]}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
<Button
|
<ButtonRow
|
||||||
onPress={() => closeModal.start(() => handleDismiss())}
|
onPress={() => closeModal.start(() => handleDismiss())}
|
||||||
text='取消'
|
text='取消'
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,82 +1,4 @@
|
||||||
import { Feather } from '@expo/vector-icons'
|
import ButtonRound from './Button/ButtonRound'
|
||||||
import React from 'react'
|
import ButtonRow from './Button/ButtonRow'
|
||||||
import { Pressable, StyleSheet, Text } from 'react-native'
|
|
||||||
import { StyleConstants } from 'src/utils/styles/constants'
|
|
||||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
|
||||||
|
|
||||||
type PropsBase = {
|
export { ButtonRound, ButtonRow }
|
||||||
onPress: () => void
|
|
||||||
disabled?: boolean
|
|
||||||
buttonSize?: 'S' | 'M'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PropsText extends PropsBase {
|
|
||||||
text: string
|
|
||||||
icon?: string
|
|
||||||
size?: 'S' | 'M' | 'L'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PropsIcon extends PropsBase {
|
|
||||||
text?: string
|
|
||||||
icon: string
|
|
||||||
size?: 'S' | 'M' | 'L'
|
|
||||||
}
|
|
||||||
|
|
||||||
const Button: React.FC<PropsText | PropsIcon> = ({
|
|
||||||
onPress,
|
|
||||||
disabled = false,
|
|
||||||
buttonSize = 'M',
|
|
||||||
text,
|
|
||||||
icon,
|
|
||||||
size = 'M'
|
|
||||||
}) => {
|
|
||||||
const { theme } = useTheme()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
{...(!disabled && { onPress })}
|
|
||||||
style={[
|
|
||||||
styles.button,
|
|
||||||
{
|
|
||||||
borderColor: disabled ? theme.secondary : theme.primary,
|
|
||||||
paddingTop: StyleConstants.Spacing[buttonSize === 'M' ? 'S' : 'XS'],
|
|
||||||
paddingBottom: StyleConstants.Spacing[buttonSize === 'M' ? 'S' : 'XS']
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{icon ? (
|
|
||||||
<Feather
|
|
||||||
name={icon}
|
|
||||||
size={StyleConstants.Font.Size[size]}
|
|
||||||
color={disabled ? theme.secondary : theme.primary}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.text,
|
|
||||||
{
|
|
||||||
color: disabled ? theme.secondary : theme.primary,
|
|
||||||
fontSize: StyleConstants.Font.Size[size]
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
button: {
|
|
||||||
paddingLeft: StyleConstants.Spacing.M,
|
|
||||||
paddingRight: StyleConstants.Spacing.M,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 100
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
textAlign: 'center'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default Button
|
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { Feather } from '@expo/vector-icons'
|
||||||
|
import React from 'react'
|
||||||
|
import { Pressable, StyleSheet } from 'react-native'
|
||||||
|
import { StyleConstants } from 'src/utils/styles/constants'
|
||||||
|
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
styles: any
|
||||||
|
onPress: () => void
|
||||||
|
icon: string
|
||||||
|
size?: 'S' | 'M' | 'L'
|
||||||
|
coordinate?: 'center' | 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtomRound: React.FC<Props> = ({
|
||||||
|
styles: extraStyles,
|
||||||
|
onPress,
|
||||||
|
icon,
|
||||||
|
size = 'M',
|
||||||
|
coordinate = 'default'
|
||||||
|
}) => {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const dimension =
|
||||||
|
StyleConstants.Spacing.S * 1.5 + StyleConstants.Font.Size[size]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
style={[
|
||||||
|
styles.base,
|
||||||
|
extraStyles,
|
||||||
|
{
|
||||||
|
backgroundColor: theme.backgroundOverlay,
|
||||||
|
...(coordinate === 'center' && {
|
||||||
|
transform: [
|
||||||
|
{ translateX: -dimension / 2 },
|
||||||
|
{ translateY: -dimension / 2 }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
onPress={onPress}
|
||||||
|
>
|
||||||
|
<Feather
|
||||||
|
name={icon}
|
||||||
|
size={StyleConstants.Font.Size[size]}
|
||||||
|
color={theme.primaryOverlay}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
base: {
|
||||||
|
position: 'absolute',
|
||||||
|
padding: StyleConstants.Spacing.S * 1.5,
|
||||||
|
borderRadius: StyleConstants.Spacing.XL
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default ButtomRound
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { Feather } from '@expo/vector-icons'
|
||||||
|
import React from 'react'
|
||||||
|
import { Pressable, StyleSheet, Text } from 'react-native'
|
||||||
|
import { StyleConstants } from 'src/utils/styles/constants'
|
||||||
|
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||||
|
|
||||||
|
type PropsBase = {
|
||||||
|
onPress: () => void
|
||||||
|
disabled?: boolean
|
||||||
|
buttonSize?: 'S' | 'M'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PropsText extends PropsBase {
|
||||||
|
text: string
|
||||||
|
icon?: string
|
||||||
|
size?: 'S' | 'M' | 'L'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PropsIcon extends PropsBase {
|
||||||
|
text?: string
|
||||||
|
icon: string
|
||||||
|
size?: 'S' | 'M' | 'L'
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonRow: React.FC<PropsText | PropsIcon> = ({
|
||||||
|
onPress,
|
||||||
|
disabled = false,
|
||||||
|
buttonSize = 'M',
|
||||||
|
text,
|
||||||
|
icon,
|
||||||
|
size = 'M'
|
||||||
|
}) => {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
{...(!disabled && { onPress })}
|
||||||
|
style={[
|
||||||
|
styles.button,
|
||||||
|
{
|
||||||
|
borderColor: disabled ? theme.secondary : theme.primary,
|
||||||
|
paddingTop: StyleConstants.Spacing[buttonSize === 'M' ? 'S' : 'XS'],
|
||||||
|
paddingBottom: StyleConstants.Spacing[buttonSize === 'M' ? 'S' : 'XS']
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{icon ? (
|
||||||
|
<Feather
|
||||||
|
name={icon}
|
||||||
|
size={StyleConstants.Font.Size[size]}
|
||||||
|
color={disabled ? theme.secondary : theme.primary}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.text,
|
||||||
|
{
|
||||||
|
color: disabled ? theme.secondary : theme.primary,
|
||||||
|
fontSize: StyleConstants.Font.Size[size]
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
button: {
|
||||||
|
paddingLeft: StyleConstants.Spacing.M,
|
||||||
|
paddingRight: StyleConstants.Spacing.M,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 100
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
textAlign: 'center'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default ButtonRow
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { useCallback, useRef, useState } from 'react'
|
import React, { useCallback, useRef, useState } from 'react'
|
||||||
import { Pressable, View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { Video } from 'expo-av'
|
import { Video } from 'expo-av'
|
||||||
import { Feather } from '@expo/vector-icons'
|
import { ButtonRound } from 'src/components/Button'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
media_attachments: Mastodon.Attachment[]
|
media_attachments: Mastodon.AttachmentVideo[]
|
||||||
width: number
|
width: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ const AttachmentVideo: React.FC<Props> = ({ media_attachments, width }) => {
|
||||||
? (width / video.meta.original.width) * video.meta.original.height
|
? (width / video.meta.original.width) * video.meta.original.height
|
||||||
: (width / 16) * 9
|
: (width / 16) * 9
|
||||||
|
|
||||||
const onPressVideo = useCallback(() => {
|
const playOnPress = useCallback(() => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
videoPlayer.current.presentFullscreenPlayer()
|
videoPlayer.current.presentFullscreenPlayer()
|
||||||
setVideoPlay(true)
|
setVideoPlay(true)
|
||||||
|
@ -46,22 +46,13 @@ const AttachmentVideo: React.FC<Props> = ({ media_attachments, width }) => {
|
||||||
shouldPlay={videoPlay}
|
shouldPlay={videoPlay}
|
||||||
/>
|
/>
|
||||||
{videoPlayer.current && !videoPlay && (
|
{videoPlayer.current && !videoPlay && (
|
||||||
<Pressable
|
<ButtonRound
|
||||||
onPress={onPressVideo}
|
icon='play'
|
||||||
style={{
|
size='L'
|
||||||
position: 'absolute',
|
onPress={playOnPress}
|
||||||
top: 0,
|
styles={{ top: videoHeight / 2, left: videoWidth / 2 }}
|
||||||
left: 0,
|
coordinate='center'
|
||||||
width: '100%',
|
/>
|
||||||
height: '100%',
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.25)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Feather name='play' size={36} color='black' />
|
|
||||||
</Pressable>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React, { useMemo, useState } from 'react'
|
||||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||||
import { useMutation, useQueryCache } from 'react-query'
|
import { useMutation, useQueryCache } from 'react-query'
|
||||||
import client from 'src/api/client'
|
import client from 'src/api/client'
|
||||||
import Button from 'src/components/Button'
|
import { ButtonRow } from 'src/components/Button'
|
||||||
import { toast } from 'src/components/toast'
|
import { toast } from 'src/components/toast'
|
||||||
import relativeTime from 'src/utils/relativeTime'
|
import relativeTime from 'src/utils/relativeTime'
|
||||||
import { StyleConstants } from 'src/utils/styles/constants'
|
import { StyleConstants } from 'src/utils/styles/constants'
|
||||||
|
@ -214,7 +214,7 @@ const TimelinePoll: React.FC<Props> = ({ queryKey, status: { poll } }) => {
|
||||||
<View style={styles.meta}>
|
<View style={styles.meta}>
|
||||||
{!poll.expired && !poll.own_votes?.length && (
|
{!poll.expired && !poll.own_votes?.length && (
|
||||||
<View style={styles.button}>
|
<View style={styles.button}>
|
||||||
<Button
|
<ButtonRow
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (poll.multiple) {
|
if (poll.multiple) {
|
||||||
mutateAction({ id: poll.id, options: multipleOptions })
|
mutateAction({ id: poll.id, options: multipleOptions })
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleConstants } from 'src/utils/styles/constants'
|
import { StyleConstants } from 'src/utils/styles/constants'
|
||||||
import Button from 'src/components/Button'
|
import { ButtonRow } from 'src/components/Button'
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
const Login: React.FC = () => {
|
||||||
const { t } = useTranslation('meRoot')
|
const { t } = useTranslation('meRoot')
|
||||||
|
@ -145,7 +145,7 @@ const Login: React.FC = () => {
|
||||||
placeholderTextColor={theme.secondary}
|
placeholderTextColor={theme.secondary}
|
||||||
returnKeyType='go'
|
returnKeyType='go'
|
||||||
/>
|
/>
|
||||||
<Button
|
<ButtonRow
|
||||||
onPress={async () => await createApplication()}
|
onPress={async () => await createApplication()}
|
||||||
text={t('content.login.button')}
|
text={t('content.login.button')}
|
||||||
disabled={!data?.uri}
|
disabled={!data?.uri}
|
||||||
|
|
|
@ -51,7 +51,7 @@ export type PostState = {
|
||||||
| string
|
| string
|
||||||
}
|
}
|
||||||
attachments: Mastodon.Attachment[]
|
attachments: Mastodon.Attachment[]
|
||||||
attachmentUploadProgress: { progress: number; aspect: number } | undefined
|
attachmentUploadProgress: { progress: number; aspect?: number } | undefined
|
||||||
visibility: 'public' | 'unlisted' | 'private' | 'direct'
|
visibility: 'public' | 'unlisted' | 'private' | 'direct'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Feather } from '@expo/vector-icons'
|
import { Feather } from '@expo/vector-icons'
|
||||||
import React, { Dispatch } from 'react'
|
import React, { Dispatch, useCallback, useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
ActionSheetIOS,
|
ActionSheetIOS,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
|
@ -8,8 +8,6 @@ import {
|
||||||
Text,
|
Text,
|
||||||
TextInput
|
TextInput
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import { getLocalToken, getLocalUrl } from 'src/utils/slices/instancesSlice'
|
|
||||||
import { StyleConstants } from 'src/utils/styles/constants'
|
import { StyleConstants } from 'src/utils/styles/constants'
|
||||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||||
import { PostAction, PostState } from '../Compose'
|
import { PostAction, PostState } from '../Compose'
|
||||||
|
@ -27,8 +25,6 @@ const ComposeActions: React.FC<Props> = ({
|
||||||
postDispatch
|
postDispatch
|
||||||
}) => {
|
}) => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const localUrl = useSelector(getLocalUrl)
|
|
||||||
const localToken = useSelector(getLocalToken)
|
|
||||||
|
|
||||||
const getVisibilityIcon = () => {
|
const getVisibilityIcon = () => {
|
||||||
switch (postState.visibility) {
|
switch (postState.visibility) {
|
||||||
|
@ -43,6 +39,63 @@ const ComposeActions: React.FC<Props> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const attachmentColor = useMemo(() => {
|
||||||
|
if (postState.poll.active) return theme.disabled
|
||||||
|
if (postState.attachmentUploadProgress) return theme.primary
|
||||||
|
|
||||||
|
if (postState.attachments.length) {
|
||||||
|
return theme.primary
|
||||||
|
} else {
|
||||||
|
return theme.secondary
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
postState.poll.active,
|
||||||
|
postState.attachments,
|
||||||
|
postState.attachmentUploadProgress
|
||||||
|
])
|
||||||
|
const attachmentOnPress = useCallback(async () => {
|
||||||
|
if (postState.poll.active) return
|
||||||
|
if (postState.attachmentUploadProgress) return
|
||||||
|
|
||||||
|
if (!postState.attachments.length) {
|
||||||
|
return await addAttachments({ postState, postDispatch })
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
postState.poll.active,
|
||||||
|
postState.attachments,
|
||||||
|
postState.attachmentUploadProgress
|
||||||
|
])
|
||||||
|
|
||||||
|
const pollColor = useMemo(() => {
|
||||||
|
if (postState.attachments.length) return theme.disabled
|
||||||
|
if (postState.attachmentUploadProgress) return theme.disabled
|
||||||
|
|
||||||
|
if (postState.poll.active) {
|
||||||
|
return theme.primary
|
||||||
|
} else {
|
||||||
|
return theme.secondary
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
postState.poll.active,
|
||||||
|
postState.attachments,
|
||||||
|
postState.attachmentUploadProgress
|
||||||
|
])
|
||||||
|
const pollOnPress = useCallback(() => {
|
||||||
|
if (!postState.attachments.length && !postState.attachmentUploadProgress) {
|
||||||
|
postDispatch({
|
||||||
|
type: 'poll',
|
||||||
|
payload: { ...postState.poll, active: !postState.poll.active }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (postState.poll.active) {
|
||||||
|
textInputRef.current?.focus()
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
postState.poll.active,
|
||||||
|
postState.attachments,
|
||||||
|
postState.attachmentUploadProgress
|
||||||
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[
|
style={[
|
||||||
|
@ -54,43 +107,14 @@ const ComposeActions: React.FC<Props> = ({
|
||||||
<Feather
|
<Feather
|
||||||
name='aperture'
|
name='aperture'
|
||||||
size={24}
|
size={24}
|
||||||
color={
|
color={attachmentColor}
|
||||||
postState.poll.active || postState.attachments.length >= 4
|
onPress={attachmentOnPress}
|
||||||
? theme.disabled
|
|
||||||
: postState.attachments.length
|
|
||||||
? theme.primary
|
|
||||||
: theme.secondary
|
|
||||||
}
|
|
||||||
onPress={async () => {
|
|
||||||
if (!postState.poll.active && postState.attachments.length < 4) {
|
|
||||||
await addAttachments({ postState, postDispatch })
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Feather
|
<Feather
|
||||||
name='bar-chart-2'
|
name='bar-chart-2'
|
||||||
size={24}
|
size={24}
|
||||||
color={
|
color={pollColor}
|
||||||
postState.attachments.length || postState.attachmentUploadProgress
|
onPress={pollOnPress}
|
||||||
? theme.disabled
|
|
||||||
: postState.poll.active
|
|
||||||
? theme.primary
|
|
||||||
: theme.secondary
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
if (
|
|
||||||
!postState.attachments.length &&
|
|
||||||
!postState.attachmentUploadProgress
|
|
||||||
) {
|
|
||||||
postDispatch({
|
|
||||||
type: 'poll',
|
|
||||||
payload: { ...postState.poll, active: !postState.poll.active }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (postState.poll.active) {
|
|
||||||
textInputRef.current?.focus()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Feather
|
<Feather
|
||||||
name={getVisibilityIcon()}
|
name={getVisibilityIcon()}
|
||||||
|
@ -124,7 +148,9 @@ const ComposeActions: React.FC<Props> = ({
|
||||||
<Feather
|
<Feather
|
||||||
name='smile'
|
name='smile'
|
||||||
size={24}
|
size={24}
|
||||||
color={postState.emoji.emojis?.length ? theme.secondary : theme.disabled}
|
color={
|
||||||
|
postState.emoji.emojis?.length ? theme.secondary : theme.disabled
|
||||||
|
}
|
||||||
{...(postState.emoji.emojis && {
|
{...(postState.emoji.emojis && {
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
if (postState.emoji.active) {
|
if (postState.emoji.active) {
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
import React, { Dispatch, useCallback } from 'react'
|
import React, { Dispatch, useCallback } from 'react'
|
||||||
import { FlatList, Image, Pressable, StyleSheet, View } from 'react-native'
|
import {
|
||||||
import { Feather } from '@expo/vector-icons'
|
FlatList,
|
||||||
|
Image,
|
||||||
|
Pressable,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View
|
||||||
|
} from 'react-native'
|
||||||
|
|
||||||
import { PostAction, PostState } from '../Compose'
|
import { PostAction, PostState } from '../Compose'
|
||||||
import { StyleConstants } from 'src/utils/styles/constants'
|
import { StyleConstants } from 'src/utils/styles/constants'
|
||||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import ShimmerPlaceholder from 'react-native-shimmer-placeholder'
|
import ShimmerPlaceholder from 'react-native-shimmer-placeholder'
|
||||||
|
import { ButtonRound } from 'src/components/Button'
|
||||||
|
import addAttachments from './addAttachments'
|
||||||
|
|
||||||
const DEFAULT_HEIGHT = 200
|
const DEFAULT_HEIGHT = 200
|
||||||
|
|
||||||
|
@ -19,34 +27,7 @@ const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
|
||||||
const imageActions = ({
|
const renderAttachment = useCallback(
|
||||||
type,
|
|
||||||
icon,
|
|
||||||
onPress
|
|
||||||
}: {
|
|
||||||
type: 'edit' | 'delete'
|
|
||||||
icon: string
|
|
||||||
onPress: () => void
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
style={[
|
|
||||||
styles.actions,
|
|
||||||
styles[type],
|
|
||||||
{ backgroundColor: theme.backgroundOverlay }
|
|
||||||
]}
|
|
||||||
onPress={onPress}
|
|
||||||
>
|
|
||||||
<Feather
|
|
||||||
name={icon}
|
|
||||||
size={StyleConstants.Font.Size.L}
|
|
||||||
color={theme.primaryOverlay}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderImage = useCallback(
|
|
||||||
({
|
({
|
||||||
item,
|
item,
|
||||||
index
|
index
|
||||||
|
@ -60,7 +41,12 @@ const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
|
||||||
style={[
|
style={[
|
||||||
styles.image,
|
styles.image,
|
||||||
{
|
{
|
||||||
width: (item.meta?.original?.aspect || 1) * DEFAULT_HEIGHT
|
width:
|
||||||
|
((item as Mastodon.AttachmentImage).meta?.original?.aspect ||
|
||||||
|
(item as Mastodon.AttachmentVideo).meta?.original.width! /
|
||||||
|
(item as Mastodon.AttachmentVideo).meta?.original
|
||||||
|
.height! ||
|
||||||
|
1) * DEFAULT_HEIGHT
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
source={{
|
source={{
|
||||||
|
@ -70,47 +56,99 @@ const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
|
||||||
: item.preview_url
|
: item.preview_url
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{imageActions({
|
{(item as Mastodon.AttachmentVideo).meta?.original?.duration && (
|
||||||
type: 'delete',
|
<Text
|
||||||
icon: 'x',
|
style={[
|
||||||
onPress: () =>
|
styles.duration,
|
||||||
|
{
|
||||||
|
color: theme.background,
|
||||||
|
backgroundColor: theme.backgroundOverlay
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{(item as Mastodon.AttachmentVideo).meta?.original.duration}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<ButtonRound
|
||||||
|
icon='x'
|
||||||
|
onPress={() =>
|
||||||
postDispatch({
|
postDispatch({
|
||||||
type: 'attachments',
|
type: 'attachments',
|
||||||
payload: postState.attachments.filter(e => e.id !== item.id)
|
payload: postState.attachments.filter(e => e.id !== item.id)
|
||||||
})
|
})
|
||||||
})}
|
}
|
||||||
{imageActions({
|
styles={styles.delete}
|
||||||
type: 'edit',
|
/>
|
||||||
icon: 'edit',
|
<ButtonRound
|
||||||
onPress: () =>
|
icon='edit'
|
||||||
|
onPress={() =>
|
||||||
navigation.navigate('Screen-Shared-Compose-EditAttachment', {
|
navigation.navigate('Screen-Shared-Compose-EditAttachment', {
|
||||||
attachment: item,
|
attachment: item,
|
||||||
postDispatch
|
postDispatch
|
||||||
})
|
})
|
||||||
})}
|
}
|
||||||
|
styles={styles.edit}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const listFooter = useCallback(() => {
|
||||||
return (
|
return (
|
||||||
<View style={styles.base}>
|
|
||||||
<FlatList
|
|
||||||
horizontal
|
|
||||||
data={postState.attachments}
|
|
||||||
renderItem={renderImage}
|
|
||||||
ListFooterComponent={
|
|
||||||
<ShimmerPlaceholder
|
<ShimmerPlaceholder
|
||||||
style={styles.progressContainer}
|
style={styles.progressContainer}
|
||||||
visible={postState.attachmentUploadProgress === undefined}
|
visible={postState.attachmentUploadProgress === undefined}
|
||||||
width={
|
width={
|
||||||
(postState.attachmentUploadProgress?.aspect || 16 / 9) *
|
(postState.attachmentUploadProgress?.aspect || 3 / 2) * DEFAULT_HEIGHT
|
||||||
DEFAULT_HEIGHT
|
|
||||||
}
|
}
|
||||||
height={200}
|
height={200}
|
||||||
/>
|
>
|
||||||
|
{postState.attachments.length > 0 &&
|
||||||
|
postState.attachments[0].type === 'image' &&
|
||||||
|
postState.attachments.length < 4 && (
|
||||||
|
<Pressable
|
||||||
|
style={{
|
||||||
|
width: DEFAULT_HEIGHT,
|
||||||
|
height: DEFAULT_HEIGHT,
|
||||||
|
backgroundColor: theme.border
|
||||||
|
}}
|
||||||
|
onPress={async () =>
|
||||||
|
await addAttachments({ postState, postDispatch })
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
<ButtonRound
|
||||||
|
icon='upload-cloud'
|
||||||
|
onPress={async () =>
|
||||||
|
await addAttachments({ postState, postDispatch })
|
||||||
|
}
|
||||||
|
styles={{
|
||||||
|
top:
|
||||||
|
(DEFAULT_HEIGHT -
|
||||||
|
StyleConstants.Spacing.Global.PagePadding) /
|
||||||
|
2,
|
||||||
|
left:
|
||||||
|
(DEFAULT_HEIGHT -
|
||||||
|
StyleConstants.Spacing.Global.PagePadding) /
|
||||||
|
2
|
||||||
|
}}
|
||||||
|
coordinate='center'
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
</ShimmerPlaceholder>
|
||||||
|
)
|
||||||
|
}, [postState.attachmentUploadProgress])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.base}>
|
||||||
|
<FlatList
|
||||||
|
horizontal
|
||||||
|
extraData={postState.attachmentUploadProgress}
|
||||||
|
data={postState.attachments}
|
||||||
|
renderItem={renderAttachment}
|
||||||
|
ListFooterComponent={listFooter}
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -130,10 +168,16 @@ const styles = StyleSheet.create({
|
||||||
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
||||||
marginBottom: StyleConstants.Spacing.Global.PagePadding
|
marginBottom: StyleConstants.Spacing.Global.PagePadding
|
||||||
},
|
},
|
||||||
actions: {
|
duration: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
padding: StyleConstants.Spacing.S * 1.5,
|
bottom:
|
||||||
borderRadius: StyleConstants.Spacing.XL
|
StyleConstants.Spacing.Global.PagePadding + StyleConstants.Spacing.S,
|
||||||
|
left: StyleConstants.Spacing.Global.PagePadding + StyleConstants.Spacing.S,
|
||||||
|
fontSize: StyleConstants.Font.Size.S,
|
||||||
|
paddingLeft: StyleConstants.Spacing.S,
|
||||||
|
paddingRight: StyleConstants.Spacing.S,
|
||||||
|
paddingTop: StyleConstants.Spacing.XS,
|
||||||
|
paddingBottom: StyleConstants.Spacing.XS
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
bottom:
|
bottom:
|
||||||
|
@ -155,4 +199,4 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default ComposeAttachments
|
export default React.memo(ComposeAttachments, () => true)
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||||
import { PanGestureHandler } from 'react-native-gesture-handler'
|
import { PanGestureHandler } from 'react-native-gesture-handler'
|
||||||
import { PostAction } from '../Compose'
|
import { PostAction } from '../Compose'
|
||||||
import client from 'src/api/client'
|
import client from 'src/api/client'
|
||||||
|
import AttachmentVideo from 'src/components/Timelines/Timeline/Shared/Attachment/AttachmentVideo'
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator()
|
const Stack = createNativeStackNavigator()
|
||||||
|
|
||||||
|
@ -45,11 +46,6 @@ const ComposeEditAttachment: React.FC<Props> = ({
|
||||||
params: { attachment, postDispatch }
|
params: { attachment, postDispatch }
|
||||||
}
|
}
|
||||||
}) => {
|
}) => {
|
||||||
const imageDimensionis = {
|
|
||||||
width: Dimensions.get('screen').width,
|
|
||||||
height: Dimensions.get('screen').width / attachment.meta?.original?.aspect!
|
|
||||||
}
|
|
||||||
|
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
@ -66,6 +62,7 @@ const ComposeEditAttachment: React.FC<Props> = ({
|
||||||
attachment.description = altText
|
attachment.description = altText
|
||||||
needUpdate = true
|
needUpdate = true
|
||||||
}
|
}
|
||||||
|
if (attachment.type === 'image') {
|
||||||
if (focus.current.x !== 0 || focus.current.y !== 0) {
|
if (focus.current.x !== 0 || focus.current.y !== 0) {
|
||||||
attachment.meta!.focus = {
|
attachment.meta!.focus = {
|
||||||
x: focus.current.x > 1 ? 1 : focus.current.x,
|
x: focus.current.x > 1 ? 1 : focus.current.x,
|
||||||
|
@ -73,6 +70,7 @@ const ComposeEditAttachment: React.FC<Props> = ({
|
||||||
}
|
}
|
||||||
needUpdate = true
|
needUpdate = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (needUpdate) {
|
if (needUpdate) {
|
||||||
postDispatch({ type: 'attachmentEdit', payload: attachment })
|
postDispatch({ type: 'attachmentEdit', payload: attachment })
|
||||||
}
|
}
|
||||||
|
@ -81,13 +79,36 @@ const ComposeEditAttachment: React.FC<Props> = ({
|
||||||
return unsubscribe
|
return unsubscribe
|
||||||
}, [navigation, altText])
|
}, [navigation, altText])
|
||||||
|
|
||||||
|
const videoPlayback = useCallback(() => {
|
||||||
|
return (
|
||||||
|
<AttachmentVideo
|
||||||
|
media_attachments={[attachment as Mastodon.AttachmentVideo]}
|
||||||
|
width={Dimensions.get('screen').width}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const imageFocus = useCallback(() => {
|
const imageFocus = useCallback(() => {
|
||||||
|
const imageDimensionis = {
|
||||||
|
width: Dimensions.get('screen').width,
|
||||||
|
height:
|
||||||
|
Dimensions.get('screen').width /
|
||||||
|
(attachment as Mastodon.AttachmentImage).meta?.original?.aspect!
|
||||||
|
}
|
||||||
|
|
||||||
const panFocus = useRef(
|
const panFocus = useRef(
|
||||||
new Animated.ValueXY(
|
new Animated.ValueXY(
|
||||||
attachment.meta.focus?.x && attachment.meta.focus?.y
|
(attachment as Mastodon.AttachmentImage).meta?.focus?.x &&
|
||||||
|
(attachment as Mastodon.AttachmentImage).meta?.focus?.y
|
||||||
? {
|
? {
|
||||||
x: (attachment.meta.focus.x * imageDimensionis.width) / 2,
|
x:
|
||||||
y: (-attachment.meta.focus.y * imageDimensionis.height) / 2
|
((attachment as Mastodon.AttachmentImage).meta!.focus!.x *
|
||||||
|
imageDimensionis.width) /
|
||||||
|
2,
|
||||||
|
y:
|
||||||
|
(-(attachment as Mastodon.AttachmentImage).meta!.focus!.y *
|
||||||
|
imageDimensionis.height) /
|
||||||
|
2
|
||||||
}
|
}
|
||||||
: { x: 0, y: 0 }
|
: { x: 0, y: 0 }
|
||||||
)
|
)
|
||||||
|
@ -283,6 +304,14 @@ const ComposeEditAttachment: React.FC<Props> = ({
|
||||||
{altTextInput()}
|
{altTextInput()}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
)
|
)
|
||||||
|
case 'video':
|
||||||
|
case 'gifv':
|
||||||
|
return (
|
||||||
|
<ScrollView style={{ flex: 1 }}>
|
||||||
|
{videoPlayback()}
|
||||||
|
{altTextInput()}
|
||||||
|
</ScrollView>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { forEach, groupBy, sortBy } from 'lodash'
|
|
||||||
import React, { Dispatch } from 'react'
|
import React, { Dispatch } from 'react'
|
||||||
import {
|
import {
|
||||||
Image,
|
Image,
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { Feather } from '@expo/vector-icons'
|
||||||
import { PostAction, PostState } from '../Compose'
|
import { PostAction, PostState } from '../Compose'
|
||||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||||
import { StyleConstants } from 'src/utils/styles/constants'
|
import { StyleConstants } from 'src/utils/styles/constants'
|
||||||
import Button from 'src/components/Button'
|
import { ButtonRow } from 'src/components/Button'
|
||||||
import { MenuContainer, MenuRow } from 'src/components/Menu'
|
import { MenuContainer, MenuRow } from 'src/components/Menu'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -73,7 +73,7 @@ const ComposePoll: React.FC<Props> = ({ postState, postDispatch }) => {
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.controlAmount}>
|
<View style={styles.controlAmount}>
|
||||||
<View style={styles.firstButton}>
|
<View style={styles.firstButton}>
|
||||||
<Button
|
<ButtonRow
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
postState.poll.total > 2 &&
|
postState.poll.total > 2 &&
|
||||||
postDispatch({
|
postDispatch({
|
||||||
|
@ -86,7 +86,7 @@ const ComposePoll: React.FC<Props> = ({ postState, postDispatch }) => {
|
||||||
buttonSize='S'
|
buttonSize='S'
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Button
|
<ButtonRow
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
postState.poll.total < 4 &&
|
postState.poll.total < 4 &&
|
||||||
postDispatch({
|
postDispatch({
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import ImagePicker from 'expo-image-picker'
|
|
||||||
import { forEach, groupBy, sortBy } from 'lodash'
|
import { forEach, groupBy, sortBy } from 'lodash'
|
||||||
import React, { Dispatch, useEffect, useMemo, useRef } from 'react'
|
import React, { Dispatch, useEffect, useMemo, useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
|
@ -25,6 +24,7 @@ import ComposeEmojis from './Emojis'
|
||||||
import ComposePoll from './Poll'
|
import ComposePoll from './Poll'
|
||||||
import ComposeTextInput from './TextInput'
|
import ComposeTextInput from './TextInput'
|
||||||
import updateText from './updateText'
|
import updateText from './updateText'
|
||||||
|
import * as Permissions from 'expo-permissions'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
postState: PostState
|
postState: PostState
|
||||||
|
@ -50,10 +50,15 @@ const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const { status } = await ImagePicker.requestCameraRollPermissionsAsync()
|
Permissions.askAsync(Permissions.CAMERA)
|
||||||
if (status !== 'granted') {
|
// const permissionGaleery = await ImagePicker.requestCameraRollPermissionsAsync()
|
||||||
alert('Sorry, we need camera roll permissions to make this work!')
|
// if (permissionGaleery.status !== 'granted') {
|
||||||
}
|
// alert('Sorry, we need camera roll permissions to make this work!')
|
||||||
|
// }
|
||||||
|
// const permissionCamera = await ImagePicker.requestCameraPermissionsAsync()
|
||||||
|
// if (permissionCamera.status !== 'granted') {
|
||||||
|
// alert('Sorry, we need camera roll permissions to make this work!')
|
||||||
|
// }
|
||||||
})()
|
})()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
@ -74,37 +79,6 @@ const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
|
||||||
|
|
||||||
const textInputRef = useRef<TextInput>(null)
|
const textInputRef = useRef<TextInput>(null)
|
||||||
|
|
||||||
const listFooter = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{postState.emoji.active && (
|
|
||||||
<View style={styles.emojis}>
|
|
||||||
<ComposeEmojis
|
|
||||||
textInputRef={textInputRef}
|
|
||||||
postState={postState}
|
|
||||||
postDispatch={postDispatch}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(postState.attachments.length > 0 ||
|
|
||||||
postState.attachmentUploadProgress) && (
|
|
||||||
<View style={styles.attachments}>
|
|
||||||
<ComposeAttachments
|
|
||||||
postState={postState}
|
|
||||||
postDispatch={postDispatch}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
{postState.poll.active && (
|
|
||||||
<View style={styles.poll}>
|
|
||||||
<ComposePoll postState={postState} postDispatch={postDispatch} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const listEmpty = useMemo(() => {
|
const listEmpty = useMemo(() => {
|
||||||
if (isFetching) {
|
if (isFetching) {
|
||||||
return <ActivityIndicator />
|
return <ActivityIndicator />
|
||||||
|
@ -125,7 +99,38 @@ const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
|
||||||
textInputRef={textInputRef}
|
textInputRef={textInputRef}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
ListFooterComponent={listFooter}
|
ListFooterComponent={
|
||||||
|
<>
|
||||||
|
{postState.emoji.active && (
|
||||||
|
<View style={styles.emojis}>
|
||||||
|
<ComposeEmojis
|
||||||
|
textInputRef={textInputRef}
|
||||||
|
postState={postState}
|
||||||
|
postDispatch={postDispatch}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(postState.attachments.length > 0 ||
|
||||||
|
postState.attachmentUploadProgress) && (
|
||||||
|
<View style={styles.attachments}>
|
||||||
|
<ComposeAttachments
|
||||||
|
postState={postState}
|
||||||
|
postDispatch={postDispatch}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{postState.poll.active && (
|
||||||
|
<View style={styles.poll}>
|
||||||
|
<ComposePoll
|
||||||
|
postState={postState}
|
||||||
|
postDispatch={postDispatch}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
ListEmptyComponent={listEmpty}
|
ListEmptyComponent={listEmpty}
|
||||||
data={postState.tag && isSuccess ? data[postState.tag.type] : []}
|
data={postState.tag && isSuccess ? data[postState.tag.type] : []}
|
||||||
renderItem={({ item, index }) => (
|
renderItem={({ item, index }) => (
|
||||||
|
|
|
@ -23,7 +23,8 @@ const ComposeTextInput: React.FC<Props> = ({
|
||||||
style={[
|
style={[
|
||||||
styles.textInput,
|
styles.textInput,
|
||||||
{
|
{
|
||||||
color: theme.primary
|
color: theme.primary,
|
||||||
|
borderBottomColor: theme.border
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
autoCapitalize='none'
|
autoCapitalize='none'
|
||||||
|
@ -58,9 +59,10 @@ const styles = StyleSheet.create({
|
||||||
textInput: {
|
textInput: {
|
||||||
fontSize: StyleConstants.Font.Size.M,
|
fontSize: StyleConstants.Font.Size.M,
|
||||||
marginTop: StyleConstants.Spacing.S,
|
marginTop: StyleConstants.Spacing.S,
|
||||||
marginBottom: StyleConstants.Spacing.M,
|
paddingBottom: StyleConstants.Spacing.M,
|
||||||
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
|
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||||
paddingRight: StyleConstants.Spacing.Global.PagePadding
|
marginRight: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
borderBottomWidth: 0.5
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -11,18 +11,18 @@ const uploadAttachment = async ({
|
||||||
postState,
|
postState,
|
||||||
postDispatch
|
postDispatch
|
||||||
}: {
|
}: {
|
||||||
result: ImageInfo
|
result: NonNullable<ImageInfo>
|
||||||
postState: PostState
|
postState: PostState
|
||||||
postDispatch: Dispatch<PostAction>
|
postDispatch: Dispatch<PostAction>
|
||||||
}) => {
|
}) => {
|
||||||
const filename = result.uri.split('/').pop()
|
|
||||||
|
|
||||||
const match = /\.(\w+)$/.exec(filename!)
|
|
||||||
const type = match ? `image/${match[1]}` : `image`
|
|
||||||
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
formData.append('file', { uri: result.uri, name: filename, type: type })
|
formData.append('file', {
|
||||||
|
// @ts-ignore
|
||||||
|
uri: result.uri,
|
||||||
|
name: result.uri.split('/').pop(),
|
||||||
|
type: 'image/jpeg/jpg'
|
||||||
|
})
|
||||||
|
|
||||||
client({
|
client({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
@ -54,7 +54,12 @@ const uploadAttachment = async ({
|
||||||
} else {
|
} else {
|
||||||
Alert.alert('上传失败', '', [
|
Alert.alert('上传失败', '', [
|
||||||
{
|
{
|
||||||
text: '返回重试'
|
text: '返回重试',
|
||||||
|
onPress: () =>
|
||||||
|
postDispatch({
|
||||||
|
type: 'attachmentUploadProgress',
|
||||||
|
payload: undefined
|
||||||
|
})
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
|
@ -63,7 +68,12 @@ const uploadAttachment = async ({
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
Alert.alert('上传失败', '', [
|
Alert.alert('上传失败', '', [
|
||||||
{
|
{
|
||||||
text: '返回重试'
|
text: '返回重试',
|
||||||
|
onPress: () =>
|
||||||
|
postDispatch({
|
||||||
|
type: 'attachmentUploadProgress',
|
||||||
|
payload: undefined
|
||||||
|
})
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
|
@ -89,10 +99,18 @@ const addAttachments = async ({
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result.cancelled) {
|
if (!result.cancelled) {
|
||||||
|
console.log(result)
|
||||||
await uploadAttachment({ result, ...params })
|
await uploadAttachment({ result, ...params })
|
||||||
}
|
}
|
||||||
} else if (buttonIndex === 1) {
|
} else if (buttonIndex === 1) {
|
||||||
// setResult(Math.floor(Math.random() * 100) + 1)
|
const result = await ImagePicker.launchCameraAsync({
|
||||||
|
mediaTypes: ImagePicker.MediaTypeOptions.All,
|
||||||
|
exif: false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.cancelled) {
|
||||||
|
await uploadAttachment({ result, ...params })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue