Fine tune compose

This commit is contained in:
Zhiyuan Zheng 2020-12-06 21:42:19 +01:00
parent add331ef0e
commit e5eaf162f4
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
18 changed files with 586 additions and 407 deletions

View File

@ -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",

View File

@ -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

View File

@ -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='取消'
/> />

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>
) )

View File

@ -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 })

View File

@ -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}

View File

@ -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'
} }

View File

@ -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) {

View File

@ -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 (
<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 ( return (
<View style={styles.base}> <View style={styles.base}>
<FlatList <FlatList
horizontal horizontal
extraData={postState.attachmentUploadProgress}
data={postState.attachments} data={postState.attachments}
renderItem={renderImage} renderItem={renderAttachment}
ListFooterComponent={ ListFooterComponent={listFooter}
<ShimmerPlaceholder
style={styles.progressContainer}
visible={postState.attachmentUploadProgress === undefined}
width={
(postState.attachmentUploadProgress?.aspect || 16 / 9) *
DEFAULT_HEIGHT
}
height={200}
/>
}
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)

View File

@ -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,12 +62,14 @@ const ComposeEditAttachment: React.FC<Props> = ({
attachment.description = altText attachment.description = altText
needUpdate = true needUpdate = true
} }
if (focus.current.x !== 0 || focus.current.y !== 0) { if (attachment.type === 'image') {
attachment.meta!.focus = { if (focus.current.x !== 0 || focus.current.y !== 0) {
x: focus.current.x > 1 ? 1 : focus.current.x, attachment.meta!.focus = {
y: focus.current.y > 1 ? 1 : focus.current.y x: focus.current.x > 1 ? 1 : focus.current.x,
y: focus.current.y > 1 ? 1 : focus.current.y
}
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
}} }}

View File

@ -1,4 +1,3 @@
import { forEach, groupBy, sortBy } from 'lodash'
import React, { Dispatch } from 'react' import React, { Dispatch } from 'react'
import { import {
Image, Image,

View File

@ -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({

View File

@ -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 }) => (

View File

@ -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
} }
}) })

View File

@ -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 })
}
} }
} }
) )