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-linear-gradient": "~8.3.0",
|
||||
"expo-localization": "^9.0.0",
|
||||
"expo-permissions": "~9.3.0",
|
||||
"expo-secure-store": "~9.2.0",
|
||||
"expo-splash-screen": "~0.6.1",
|
||||
"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 {
|
||||
type Account = {
|
||||
// Base
|
||||
|
@ -177,7 +43,141 @@ declare namespace Mastodon {
|
|||
| AttachmentVideo
|
||||
| AttachmentGifv
|
||||
| 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 = {
|
||||
// Base
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
import Button from './Button'
|
||||
import { ButtonRow } from './Button'
|
||||
|
||||
export interface Props {
|
||||
children: React.ReactNode
|
||||
|
@ -84,7 +84,7 @@ const BottomSheet: React.FC<Props> = ({ children, visible, handleDismiss }) => {
|
|||
style={[styles.handle, { backgroundColor: theme.background }]}
|
||||
/>
|
||||
{children}
|
||||
<Button
|
||||
<ButtonRow
|
||||
onPress={() => closeModal.start(() => handleDismiss())}
|
||||
text='取消'
|
||||
/>
|
||||
|
|
|
@ -1,82 +1,4 @@
|
|||
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'
|
||||
import ButtonRound from './Button/ButtonRound'
|
||||
import ButtonRow from './Button/ButtonRow'
|
||||
|
||||
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 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
|
||||
export { ButtonRound, ButtonRow }
|
||||
|
|
|
@ -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 { Pressable, View } from 'react-native'
|
||||
import { View } from 'react-native'
|
||||
import { Video } from 'expo-av'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import { ButtonRound } from 'src/components/Button'
|
||||
|
||||
export interface Props {
|
||||
media_attachments: Mastodon.Attachment[]
|
||||
media_attachments: Mastodon.AttachmentVideo[]
|
||||
width: number
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ const AttachmentVideo: React.FC<Props> = ({ media_attachments, width }) => {
|
|||
? (width / video.meta.original.width) * video.meta.original.height
|
||||
: (width / 16) * 9
|
||||
|
||||
const onPressVideo = useCallback(() => {
|
||||
const playOnPress = useCallback(() => {
|
||||
// @ts-ignore
|
||||
videoPlayer.current.presentFullscreenPlayer()
|
||||
setVideoPlay(true)
|
||||
|
@ -46,22 +46,13 @@ const AttachmentVideo: React.FC<Props> = ({ media_attachments, width }) => {
|
|||
shouldPlay={videoPlay}
|
||||
/>
|
||||
{videoPlayer.current && !videoPlay && (
|
||||
<Pressable
|
||||
onPress={onPressVideo}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
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>
|
||||
<ButtonRound
|
||||
icon='play'
|
||||
size='L'
|
||||
onPress={playOnPress}
|
||||
styles={{ top: videoHeight / 2, left: videoWidth / 2 }}
|
||||
coordinate='center'
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, { useMemo, useState } from 'react'
|
|||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { useMutation, useQueryCache } from 'react-query'
|
||||
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 relativeTime from 'src/utils/relativeTime'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
|
@ -214,7 +214,7 @@ const TimelinePoll: React.FC<Props> = ({ queryKey, status: { poll } }) => {
|
|||
<View style={styles.meta}>
|
||||
{!poll.expired && !poll.own_votes?.length && (
|
||||
<View style={styles.button}>
|
||||
<Button
|
||||
<ButtonRow
|
||||
onPress={() => {
|
||||
if (poll.multiple) {
|
||||
mutateAction({ id: poll.id, options: multipleOptions })
|
||||
|
|
|
@ -13,7 +13,7 @@ import { useTheme } from 'src/utils/styles/ThemeManager'
|
|||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
import Button from 'src/components/Button'
|
||||
import { ButtonRow } from 'src/components/Button'
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const { t } = useTranslation('meRoot')
|
||||
|
@ -145,7 +145,7 @@ const Login: React.FC = () => {
|
|||
placeholderTextColor={theme.secondary}
|
||||
returnKeyType='go'
|
||||
/>
|
||||
<Button
|
||||
<ButtonRow
|
||||
onPress={async () => await createApplication()}
|
||||
text={t('content.login.button')}
|
||||
disabled={!data?.uri}
|
||||
|
|
|
@ -51,7 +51,7 @@ export type PostState = {
|
|||
| string
|
||||
}
|
||||
attachments: Mastodon.Attachment[]
|
||||
attachmentUploadProgress: { progress: number; aspect: number } | undefined
|
||||
attachmentUploadProgress: { progress: number; aspect?: number } | undefined
|
||||
visibility: 'public' | 'unlisted' | 'private' | 'direct'
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Feather } from '@expo/vector-icons'
|
||||
import React, { Dispatch } from 'react'
|
||||
import React, { Dispatch, useCallback, useMemo } from 'react'
|
||||
import {
|
||||
ActionSheetIOS,
|
||||
Keyboard,
|
||||
|
@ -8,8 +8,6 @@ import {
|
|||
Text,
|
||||
TextInput
|
||||
} from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { getLocalToken, getLocalUrl } from 'src/utils/slices/instancesSlice'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
import { PostAction, PostState } from '../Compose'
|
||||
|
@ -27,8 +25,6 @@ const ComposeActions: React.FC<Props> = ({
|
|||
postDispatch
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const localUrl = useSelector(getLocalUrl)
|
||||
const localToken = useSelector(getLocalToken)
|
||||
|
||||
const getVisibilityIcon = () => {
|
||||
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 (
|
||||
<Pressable
|
||||
style={[
|
||||
|
@ -54,43 +107,14 @@ const ComposeActions: React.FC<Props> = ({
|
|||
<Feather
|
||||
name='aperture'
|
||||
size={24}
|
||||
color={
|
||||
postState.poll.active || postState.attachments.length >= 4
|
||||
? theme.disabled
|
||||
: postState.attachments.length
|
||||
? theme.primary
|
||||
: theme.secondary
|
||||
}
|
||||
onPress={async () => {
|
||||
if (!postState.poll.active && postState.attachments.length < 4) {
|
||||
await addAttachments({ postState, postDispatch })
|
||||
}
|
||||
}}
|
||||
color={attachmentColor}
|
||||
onPress={attachmentOnPress}
|
||||
/>
|
||||
<Feather
|
||||
name='bar-chart-2'
|
||||
size={24}
|
||||
color={
|
||||
postState.attachments.length || postState.attachmentUploadProgress
|
||||
? 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()
|
||||
}
|
||||
}}
|
||||
color={pollColor}
|
||||
onPress={pollOnPress}
|
||||
/>
|
||||
<Feather
|
||||
name={getVisibilityIcon()}
|
||||
|
@ -124,7 +148,9 @@ const ComposeActions: React.FC<Props> = ({
|
|||
<Feather
|
||||
name='smile'
|
||||
size={24}
|
||||
color={postState.emoji.emojis?.length ? theme.secondary : theme.disabled}
|
||||
color={
|
||||
postState.emoji.emojis?.length ? theme.secondary : theme.disabled
|
||||
}
|
||||
{...(postState.emoji.emojis && {
|
||||
onPress: () => {
|
||||
if (postState.emoji.active) {
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
import React, { Dispatch, useCallback } from 'react'
|
||||
import { FlatList, Image, Pressable, StyleSheet, View } from 'react-native'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import {
|
||||
FlatList,
|
||||
Image,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native'
|
||||
|
||||
import { PostAction, PostState } from '../Compose'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import ShimmerPlaceholder from 'react-native-shimmer-placeholder'
|
||||
import { ButtonRound } from 'src/components/Button'
|
||||
import addAttachments from './addAttachments'
|
||||
|
||||
const DEFAULT_HEIGHT = 200
|
||||
|
||||
|
@ -19,34 +27,7 @@ const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
|
|||
const { theme } = useTheme()
|
||||
const navigation = useNavigation()
|
||||
|
||||
const imageActions = ({
|
||||
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(
|
||||
const renderAttachment = useCallback(
|
||||
({
|
||||
item,
|
||||
index
|
||||
|
@ -60,7 +41,12 @@ const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
|
|||
style={[
|
||||
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={{
|
||||
|
@ -70,47 +56,99 @@ const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
|
|||
: item.preview_url
|
||||
}}
|
||||
/>
|
||||
{imageActions({
|
||||
type: 'delete',
|
||||
icon: 'x',
|
||||
onPress: () =>
|
||||
{(item as Mastodon.AttachmentVideo).meta?.original?.duration && (
|
||||
<Text
|
||||
style={[
|
||||
styles.duration,
|
||||
{
|
||||
color: theme.background,
|
||||
backgroundColor: theme.backgroundOverlay
|
||||
}
|
||||
]}
|
||||
>
|
||||
{(item as Mastodon.AttachmentVideo).meta?.original.duration}
|
||||
</Text>
|
||||
)}
|
||||
<ButtonRound
|
||||
icon='x'
|
||||
onPress={() =>
|
||||
postDispatch({
|
||||
type: 'attachments',
|
||||
payload: postState.attachments.filter(e => e.id !== item.id)
|
||||
})
|
||||
})}
|
||||
{imageActions({
|
||||
type: 'edit',
|
||||
icon: 'edit',
|
||||
onPress: () =>
|
||||
}
|
||||
styles={styles.delete}
|
||||
/>
|
||||
<ButtonRound
|
||||
icon='edit'
|
||||
onPress={() =>
|
||||
navigation.navigate('Screen-Shared-Compose-EditAttachment', {
|
||||
attachment: item,
|
||||
postDispatch
|
||||
})
|
||||
})}
|
||||
}
|
||||
styles={styles.edit}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const listFooter = useCallback(() => {
|
||||
return (
|
||||
<ShimmerPlaceholder
|
||||
style={styles.progressContainer}
|
||||
visible={postState.attachmentUploadProgress === undefined}
|
||||
width={
|
||||
(postState.attachmentUploadProgress?.aspect || 3 / 2) * DEFAULT_HEIGHT
|
||||
}
|
||||
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={renderImage}
|
||||
ListFooterComponent={
|
||||
<ShimmerPlaceholder
|
||||
style={styles.progressContainer}
|
||||
visible={postState.attachmentUploadProgress === undefined}
|
||||
width={
|
||||
(postState.attachmentUploadProgress?.aspect || 16 / 9) *
|
||||
DEFAULT_HEIGHT
|
||||
}
|
||||
height={200}
|
||||
/>
|
||||
}
|
||||
renderItem={renderAttachment}
|
||||
ListFooterComponent={listFooter}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
/>
|
||||
</View>
|
||||
|
@ -130,10 +168,16 @@ const styles = StyleSheet.create({
|
|||
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginBottom: StyleConstants.Spacing.Global.PagePadding
|
||||
},
|
||||
actions: {
|
||||
duration: {
|
||||
position: 'absolute',
|
||||
padding: StyleConstants.Spacing.S * 1.5,
|
||||
borderRadius: StyleConstants.Spacing.XL
|
||||
bottom:
|
||||
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: {
|
||||
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 { PostAction } from '../Compose'
|
||||
import client from 'src/api/client'
|
||||
import AttachmentVideo from 'src/components/Timelines/Timeline/Shared/Attachment/AttachmentVideo'
|
||||
|
||||
const Stack = createNativeStackNavigator()
|
||||
|
||||
|
@ -45,11 +46,6 @@ const ComposeEditAttachment: React.FC<Props> = ({
|
|||
params: { attachment, postDispatch }
|
||||
}
|
||||
}) => {
|
||||
const imageDimensionis = {
|
||||
width: Dimensions.get('screen').width,
|
||||
height: Dimensions.get('screen').width / attachment.meta?.original?.aspect!
|
||||
}
|
||||
|
||||
const navigation = useNavigation()
|
||||
|
||||
const { theme } = useTheme()
|
||||
|
@ -66,12 +62,14 @@ const ComposeEditAttachment: React.FC<Props> = ({
|
|||
attachment.description = altText
|
||||
needUpdate = true
|
||||
}
|
||||
if (focus.current.x !== 0 || focus.current.y !== 0) {
|
||||
attachment.meta!.focus = {
|
||||
x: focus.current.x > 1 ? 1 : focus.current.x,
|
||||
y: focus.current.y > 1 ? 1 : focus.current.y
|
||||
if (attachment.type === 'image') {
|
||||
if (focus.current.x !== 0 || focus.current.y !== 0) {
|
||||
attachment.meta!.focus = {
|
||||
x: focus.current.x > 1 ? 1 : focus.current.x,
|
||||
y: focus.current.y > 1 ? 1 : focus.current.y
|
||||
}
|
||||
needUpdate = true
|
||||
}
|
||||
needUpdate = true
|
||||
}
|
||||
if (needUpdate) {
|
||||
postDispatch({ type: 'attachmentEdit', payload: attachment })
|
||||
|
@ -81,13 +79,36 @@ const ComposeEditAttachment: React.FC<Props> = ({
|
|||
return unsubscribe
|
||||
}, [navigation, altText])
|
||||
|
||||
const videoPlayback = useCallback(() => {
|
||||
return (
|
||||
<AttachmentVideo
|
||||
media_attachments={[attachment as Mastodon.AttachmentVideo]}
|
||||
width={Dimensions.get('screen').width}
|
||||
/>
|
||||
)
|
||||
}, [])
|
||||
|
||||
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(
|
||||
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,
|
||||
y: (-attachment.meta.focus.y * imageDimensionis.height) / 2
|
||||
x:
|
||||
((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 }
|
||||
)
|
||||
|
@ -283,6 +304,14 @@ const ComposeEditAttachment: React.FC<Props> = ({
|
|||
{altTextInput()}
|
||||
</ScrollView>
|
||||
)
|
||||
case 'video':
|
||||
case 'gifv':
|
||||
return (
|
||||
<ScrollView style={{ flex: 1 }}>
|
||||
{videoPlayback()}
|
||||
{altTextInput()}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { forEach, groupBy, sortBy } from 'lodash'
|
||||
import React, { Dispatch } from 'react'
|
||||
import {
|
||||
Image,
|
||||
|
|
|
@ -12,7 +12,7 @@ import { Feather } from '@expo/vector-icons'
|
|||
import { PostAction, PostState } from '../Compose'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
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'
|
||||
|
||||
export interface Props {
|
||||
|
@ -73,7 +73,7 @@ const ComposePoll: React.FC<Props> = ({ postState, postDispatch }) => {
|
|||
</View>
|
||||
<View style={styles.controlAmount}>
|
||||
<View style={styles.firstButton}>
|
||||
<Button
|
||||
<ButtonRow
|
||||
onPress={() =>
|
||||
postState.poll.total > 2 &&
|
||||
postDispatch({
|
||||
|
@ -86,7 +86,7 @@ const ComposePoll: React.FC<Props> = ({ postState, postDispatch }) => {
|
|||
buttonSize='S'
|
||||
/>
|
||||
</View>
|
||||
<Button
|
||||
<ButtonRow
|
||||
onPress={() =>
|
||||
postState.poll.total < 4 &&
|
||||
postDispatch({
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import ImagePicker from 'expo-image-picker'
|
||||
import { forEach, groupBy, sortBy } from 'lodash'
|
||||
import React, { Dispatch, useEffect, useMemo, useRef } from 'react'
|
||||
import {
|
||||
|
@ -25,6 +24,7 @@ import ComposeEmojis from './Emojis'
|
|||
import ComposePoll from './Poll'
|
||||
import ComposeTextInput from './TextInput'
|
||||
import updateText from './updateText'
|
||||
import * as Permissions from 'expo-permissions'
|
||||
|
||||
export interface Props {
|
||||
postState: PostState
|
||||
|
@ -50,10 +50,15 @@ const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
|
|||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
const { status } = await ImagePicker.requestCameraRollPermissionsAsync()
|
||||
if (status !== 'granted') {
|
||||
alert('Sorry, we need camera roll permissions to make this work!')
|
||||
}
|
||||
Permissions.askAsync(Permissions.CAMERA)
|
||||
// const permissionGaleery = await ImagePicker.requestCameraRollPermissionsAsync()
|
||||
// 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 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(() => {
|
||||
if (isFetching) {
|
||||
return <ActivityIndicator />
|
||||
|
@ -125,7 +99,38 @@ const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
|
|||
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}
|
||||
data={postState.tag && isSuccess ? data[postState.tag.type] : []}
|
||||
renderItem={({ item, index }) => (
|
||||
|
|
|
@ -23,7 +23,8 @@ const ComposeTextInput: React.FC<Props> = ({
|
|||
style={[
|
||||
styles.textInput,
|
||||
{
|
||||
color: theme.primary
|
||||
color: theme.primary,
|
||||
borderBottomColor: theme.border
|
||||
}
|
||||
]}
|
||||
autoCapitalize='none'
|
||||
|
@ -58,9 +59,10 @@ const styles = StyleSheet.create({
|
|||
textInput: {
|
||||
fontSize: StyleConstants.Font.Size.M,
|
||||
marginTop: StyleConstants.Spacing.S,
|
||||
marginBottom: StyleConstants.Spacing.M,
|
||||
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||
paddingRight: StyleConstants.Spacing.Global.PagePadding
|
||||
paddingBottom: StyleConstants.Spacing.M,
|
||||
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginRight: StyleConstants.Spacing.Global.PagePadding,
|
||||
borderBottomWidth: 0.5
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -11,18 +11,18 @@ const uploadAttachment = async ({
|
|||
postState,
|
||||
postDispatch
|
||||
}: {
|
||||
result: ImageInfo
|
||||
result: NonNullable<ImageInfo>
|
||||
postState: PostState
|
||||
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()
|
||||
// @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({
|
||||
method: 'post',
|
||||
|
@ -54,7 +54,12 @@ const uploadAttachment = async ({
|
|||
} else {
|
||||
Alert.alert('上传失败', '', [
|
||||
{
|
||||
text: '返回重试'
|
||||
text: '返回重试',
|
||||
onPress: () =>
|
||||
postDispatch({
|
||||
type: 'attachmentUploadProgress',
|
||||
payload: undefined
|
||||
})
|
||||
}
|
||||
])
|
||||
return Promise.reject()
|
||||
|
@ -63,7 +68,12 @@ const uploadAttachment = async ({
|
|||
.catch(() => {
|
||||
Alert.alert('上传失败', '', [
|
||||
{
|
||||
text: '返回重试'
|
||||
text: '返回重试',
|
||||
onPress: () =>
|
||||
postDispatch({
|
||||
type: 'attachmentUploadProgress',
|
||||
payload: undefined
|
||||
})
|
||||
}
|
||||
])
|
||||
return Promise.reject()
|
||||
|
@ -89,10 +99,18 @@ const addAttachments = async ({
|
|||
})
|
||||
|
||||
if (!result.cancelled) {
|
||||
console.log(result)
|
||||
await uploadAttachment({ result, ...params })
|
||||
}
|
||||
} 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