This commit is contained in:
Zhiyuan Zheng 2021-01-04 14:55:34 +01:00
parent e9ea0ed71e
commit 811964e10f
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
15 changed files with 136 additions and 98 deletions

View File

@ -102,7 +102,11 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
const queryNotification = useInfiniteQuery( const queryNotification = useInfiniteQuery(
['Notifications', {}], ['Notifications', {}],
timelineFetch, timelineFetch,
{ enabled: localInstance ? true : false } {
enabled: localInstance ? true : false,
refetchInterval: 1000 * 60,
refetchIntervalInBackground: true
}
) )
const prevNotification = useSelector(getLocalNotification) const prevNotification = useSelector(getLocalNotification)
useEffect(() => { useEffect(() => {

View File

@ -41,6 +41,7 @@ const client = async ({
instance === 'remote' ? instanceDomain || state.remote.url : state.local.url instance === 'remote' ? instanceDomain || state.remote.url : state.local.url
return axios({ return axios({
timeout: method === 'post' ? 1000 * 60 : 1000 * 15,
method, method,
baseURL: `https://${domain}/api/${version}/`, baseURL: `https://${domain}/api/${version}/`,
url, url,

View File

@ -1,3 +1,4 @@
import { StyleConstants } from '@root/utils/styles/constants'
import React, { createElement } from 'react' import React, { createElement } from 'react'
import { StyleProp, View, ViewStyle } from 'react-native' import { StyleProp, View, ViewStyle } from 'react-native'
import * as FeatherIcon from 'react-native-feather' import * as FeatherIcon from 'react-native-feather'
@ -8,7 +9,6 @@ export interface Props {
color: string color: string
fill?: string fill?: string
strokeWidth?: number strokeWidth?: number
inline?: boolean // When used in line of text, need to drag it down
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
} }
@ -18,7 +18,6 @@ const Icon: React.FC<Props> = ({
color, color,
fill, fill,
strokeWidth = 2, strokeWidth = 2,
inline = false,
style style
}) => { }) => {
return ( return (
@ -29,8 +28,7 @@ const Icon: React.FC<Props> = ({
width: size, width: size,
height: size, height: size,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center'
marginBottom: inline ? -size * 0.125 : undefined
} }
]} ]}
> >

View File

@ -27,8 +27,7 @@ const ParseEmojis: React.FC<Props> = ({
}, },
image: { image: {
width: StyleConstants.Font.Size[size], width: StyleConstants.Font.Size[size],
height: StyleConstants.Font.Size[size], height: StyleConstants.Font.Size[size]
marginBottom: -StyleConstants.Font.Size[size] * 0.125
} }
}) })
@ -51,7 +50,6 @@ const ParseEmojis: React.FC<Props> = ({
{/* When emoji starts a paragraph, lineHeight will break */} {/* When emoji starts a paragraph, lineHeight will break */}
{i === 0 ? <Text> </Text> : null} {i === 0 ? <Text> </Text> : null}
<Image <Image
// resizeMode='contain'
source={{ uri: emojis[emojiIndex].url }} source={{ uri: emojis[emojiIndex].url }}
style={[styles.image]} style={[styles.image]}
/> />

View File

@ -9,6 +9,8 @@ import React, { useCallback, useMemo, useState } from 'react'
import { Pressable, Text, View } from 'react-native' import { Pressable, Text, View } from 'react-native'
import HTMLView from 'react-native-htmlview' import HTMLView from 'react-native-htmlview'
import Animated, { import Animated, {
measure,
useAnimatedRef,
useAnimatedStyle, useAnimatedStyle,
useDerivedValue, useDerivedValue,
useSharedValue, useSharedValue,
@ -103,7 +105,6 @@ const renderNode = ({
> >
{!shouldBeTag ? ( {!shouldBeTag ? (
<Icon <Icon
inline
color={theme.blue} color={theme.blue}
name='ExternalLink' name='ExternalLink'
size={StyleConstants.Font.Size[size]} size={StyleConstants.Font.Size[size]}
@ -162,7 +163,13 @@ const ParseHTML: React.FC<Props> = ({
) )
const textComponent = useCallback(({ children }) => { const textComponent = useCallback(({ children }) => {
if (children) { if (children) {
return <ParseEmojis content={children.toString()} emojis={emojis} /> return (
<ParseEmojis
content={children.toString()}
emojis={emojis}
size={size}
/>
)
} else { } else {
return null return null
} }
@ -171,14 +178,15 @@ const ParseHTML: React.FC<Props> = ({
({ children }) => { ({ children }) => {
const lineHeight = StyleConstants.Font.LineHeight[size] const lineHeight = StyleConstants.Font.LineHeight[size]
const [lines, setLines] = useState<number | undefined>(undefined)
const [heightOriginal, setHeightOriginal] = useState<number>() const [heightOriginal, setHeightOriginal] = useState<number>()
const [heightTruncated, setHeightTruncated] = useState<number>() const [heightTruncated, setHeightTruncated] = useState<number>()
const [allowExpand, setAllowExpand] = useState(false) const [expandAllow, setExpandAllow] = useState(false)
const [showAllText, setShowAllText] = useState(false) const [expanded, setExpanded] = useState(false)
const viewHeight = useDerivedValue(() => { const viewHeight = useDerivedValue(() => {
if (allowExpand) { if (expandAllow) {
if (showAllText) { if (expanded) {
return heightOriginal as number return heightOriginal as number
} else { } else {
return heightTruncated as number return heightTruncated as number
@ -186,49 +194,29 @@ const ParseHTML: React.FC<Props> = ({
} else { } else {
return heightOriginal as number return heightOriginal as number
} }
}, [heightOriginal, heightTruncated, allowExpand, showAllText]) }, [heightOriginal, heightTruncated, expandAllow, expanded])
const ViewHeight = useAnimatedStyle(() => { const ViewHeight = useAnimatedStyle(() => {
return { return {
height: allowExpand height: expandAllow
? showAllText ? expanded
? withTiming(viewHeight.value) ? withTiming(viewHeight.value)
: withTiming(viewHeight.value) : withTiming(viewHeight.value)
: undefined : viewHeight.value
} }
}) })
const calNumberOfLines = useMemo(() => {
if (numberOfLines === 0) {
// For spoilers without calculation
return showAllText ? undefined : 1
} else {
if (heightOriginal) {
if (!heightTruncated) {
return numberOfLines
} else {
return undefined
}
} else {
return undefined
}
}
}, [heightOriginal, heightTruncated, allowExpand, showAllText])
const onLayout = useCallback( const onLayout = useCallback(
({ nativeEvent }) => { ({ nativeEvent }) => {
if (numberOfLines === 0) { if (!heightOriginal) {
// For spoilers without calculation setHeightOriginal(nativeEvent.layout.height)
setAllowExpand(true) setLines(numberOfLines === 0 ? 1 : numberOfLines)
} else { } else {
if (!heightOriginal) { if (!heightTruncated) {
setHeightOriginal(nativeEvent.layout.height) setHeightTruncated(nativeEvent.layout.height)
setLines(undefined)
} else { } else {
if (!heightTruncated) { if (heightOriginal > heightTruncated) {
setHeightTruncated(nativeEvent.layout.height) setExpandAllow(true)
} else {
if (heightOriginal > heightTruncated) {
setAllowExpand(true)
}
} }
} }
} }
@ -239,21 +227,21 @@ const ParseHTML: React.FC<Props> = ({
return ( return (
<View> <View>
<Animated.View style={[ViewHeight, { overflow: 'hidden' }]}> <Animated.View style={[ViewHeight, { overflow: 'hidden' }]}>
<Text <Animated.Text
children={children}
onLayout={onLayout}
numberOfLines={lines}
style={{ style={{
...StyleConstants.FontStyle[size], ...StyleConstants.FontStyle[size],
color: theme.primary, color: theme.primary,
height: allowExpand ? heightOriginal : undefined height: expandAllow ? heightOriginal : undefined
}} }}
children={children}
numberOfLines={calNumberOfLines}
onLayout={onLayout}
/> />
</Animated.View> </Animated.View>
{allowExpand ? ( {expandAllow ? (
<Pressable <Pressable
onPress={() => setShowAllText(!showAllText)} onPress={() => setExpanded(!expanded)}
style={{ marginTop: showAllText ? 0 : -lineHeight * 1.25 }} style={{ marginTop: expanded ? 0 : -lineHeight * 1.25 }}
> >
<LinearGradient <LinearGradient
colors={[ colors={[
@ -273,7 +261,7 @@ const ParseHTML: React.FC<Props> = ({
color: theme.primary color: theme.primary
}} }}
> >
{`${showAllText ? '折叠' : '展开'}${expandHint}`} {`${expanded ? '折叠' : '展开'}${expandHint}`}
</Text> </Text>
</LinearGradient> </LinearGradient>
</Pressable> </Pressable>

View File

@ -32,7 +32,7 @@ const RelationshipIncoming: React.FC<Props> = ({ id }) => {
onSuccess: ({ body }) => { onSuccess: ({ body }) => {
haptics('Success') haptics('Success')
queryClient.setQueryData(relationshipQueryKey, body) queryClient.setQueryData(relationshipQueryKey, body)
queryClient.invalidateQueries(['Notifications', {}]) queryClient.refetchQueries(['Notifications'])
}, },
onError: (err: any, { type }) => { onError: (err: any, { type }) => {
haptics('Error') haptics('Error')

View File

@ -174,7 +174,23 @@ const Compose: React.FC<Props> = ({ route: { params }, navigation }) => {
composePost(params, composeState) composePost(params, composeState)
.then(() => { .then(() => {
haptics('Success') haptics('Success')
queryClient.invalidateQueries(['Following']) queryClient.invalidateQueries(['Following', {}])
if (
params?.type &&
(params.type === 'reply' || params.type === 'conversation')
) {
queryClient.invalidateQueries(
[
'Toot',
{
toot: params.incomingStatus.reblog
? params.incomingStatus.reblog.id
: params.incomingStatus.id
}
],
{ exact: true, active: true }
)
}
navigation.goBack() navigation.goBack()
}) })
.catch(() => { .catch(() => {

View File

@ -141,19 +141,18 @@ const ComposeAttachments: React.FC = () => {
]} ]}
> >
<Chase <Chase
size={StyleConstants.Font.Size.L * 2} size={StyleConstants.Font.Size.L * 1.5}
color={theme.primaryOverlay} color={theme.primaryOverlay}
/> />
</View> </View>
) : ( ) : (
<> <View style={styles.actions}>
<Button <Button
type='icon' type='icon'
content='X' content='X'
spacing='M' spacing='M'
round round
overlay overlay
style={styles.delete}
onPress={() => { onPress={() => {
layoutAnimation() layoutAnimation()
composeDispatch({ composeDispatch({
@ -169,14 +168,13 @@ const ComposeAttachments: React.FC = () => {
spacing='M' spacing='M'
round round
overlay overlay
style={styles.edit}
onPress={() => onPress={() =>
navigation.navigate('Screen-Shared-Compose-EditAttachment', { navigation.navigate('Screen-Shared-Compose-EditAttachment', {
index index
}) })
} }
/> />
</> </View>
)} )}
</View> </View>
) )
@ -294,15 +292,12 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center' alignItems: 'center'
}, },
delete: { actions: {
position: 'absolute', ...StyleSheet.absoluteFillObject,
top: StyleConstants.Spacing.S, justifyContent: 'space-between',
right: StyleConstants.Spacing.S alignContent: 'flex-end',
}, alignItems: 'flex-end',
edit: { padding: StyleConstants.Spacing.S
position: 'absolute',
bottom: StyleConstants.Spacing.S,
right: StyleConstants.Spacing.S
} }
}) })

View File

@ -29,10 +29,16 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index, focus }) => {
const theAttachmentRemote = composeState.attachments.uploads[index].remote! const theAttachmentRemote = composeState.attachments.uploads[index].remote!
const theAttachmentLocal = composeState.attachments.uploads[index].local! const theAttachmentLocal = composeState.attachments.uploads[index].local!
const imageWidthBase =
theAttachmentRemote.meta?.original?.aspect < 1
? Dimensions.get('screen').width *
theAttachmentRemote.meta?.original?.aspect
: Dimensions.get('screen').width
const padding = (Dimensions.get('screen').width - imageWidthBase) / 2
const imageDimensionis = { const imageDimensionis = {
width: Dimensions.get('screen').width, width: imageWidthBase,
height: height:
Dimensions.get('screen').width / imageWidthBase /
(theAttachmentRemote as Mastodon.AttachmentImage).meta?.original?.aspect! (theAttachmentRemote as Mastodon.AttachmentImage).meta?.original?.aspect!
} }
@ -75,8 +81,14 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index, focus }) => {
{ {
translateX: interpolate( translateX: interpolate(
panX.value, panX.value,
[-imageDimensionis.width / 2, imageDimensionis.width / 2], [
[-imageDimensionis.width / 2, imageDimensionis.width / 2], -imageDimensionis.width / 2 + padding,
imageDimensionis.width / 2 + padding
],
[
-imageDimensionis.width / 2 + padding,
imageDimensionis.width / 2 + padding
],
Extrapolate.CLAMP Extrapolate.CLAMP
) )
}, },
@ -94,7 +106,7 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index, focus }) => {
return ( return (
<> <>
<View style={{ overflow: 'hidden' }}> <View style={{ overflow: 'hidden', flex: 1, alignItems: 'center' }}>
<Image <Image
style={{ style={{
width: imageDimensionis.width, width: imageDimensionis.width,
@ -126,13 +138,13 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index, focus }) => {
/> />
<G transform='translate(967, 967)'> <G transform='translate(967, 967)'>
<Circle <Circle
stroke={theme.background} stroke={theme.primaryOverlay}
strokeWidth='2' strokeWidth='2'
cx='33' cx='33'
cy='33' cy='33'
r='33' r='33'
/> />
<Circle fill={theme.background} cx='33' cy='33' r='2' /> <Circle fill={theme.primaryOverlay} cx='33' cy='33' r='2' />
</G> </G>
</G> </G>
</G> </G>

View File

@ -38,10 +38,19 @@ const ComposeEditAttachmentRoot: React.FC<Props> = ({
return <ComposeEditAttachmentImage index={index} focus={focus} /> return <ComposeEditAttachmentImage index={index} focus={focus} />
case 'video': case 'video':
case 'gifv': case 'gifv':
const video = composeState.attachments.uploads[index]
return ( return (
<AttachmentVideo <AttachmentVideo
sensitiveShown={false} sensitiveShown={false}
video={theAttachment as Mastodon.AttachmentVideo} video={
video.local
? ({
url: video.local.uri,
preview_url: video.local.local_thumbnail,
blurhash: video.remote?.blurhash
} as Mastodon.AttachmentVideo)
: (video.remote! as Mastodon.AttachmentVideo)
}
/> />
) )
} }
@ -56,7 +65,6 @@ const ComposeEditAttachmentRoot: React.FC<Props> = ({
</Text> </Text>
<TextInput <TextInput
autoFocus
style={[styles.altTextInput, { borderColor: theme.border }]} style={[styles.altTextInput, { borderColor: theme.border }]}
autoCapitalize='none' autoCapitalize='none'
autoCorrect={false} autoCorrect={false}

View File

@ -1,5 +1,6 @@
import client from '@api/client' import client from '@api/client'
import * as ImagePicker from 'expo-image-picker' import * as ImagePicker from 'expo-image-picker'
import * as Crypto from 'expo-crypto'
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types' import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
import * as VideoThumbnails from 'expo-video-thumbnails' import * as VideoThumbnails from 'expo-video-thumbnails'
import { Dispatch } from 'react' import { Dispatch } from 'react'
@ -11,13 +12,17 @@ export interface Props {
} }
const addAttachment = async ({ composeDispatch }: Props): Promise<any> => { const addAttachment = async ({ composeDispatch }: Props): Promise<any> => {
const uploadAttachment = (result: ImageInfo) => { const uploadAttachment = async (result: ImageInfo) => {
const hash = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
result.uri + Math.random()
)
switch (result.type) { switch (result.type) {
case 'image': case 'image':
composeDispatch({ composeDispatch({
type: 'attachment/upload/start', type: 'attachment/upload/start',
payload: { payload: {
local: { ...result, local_thumbnail: result.uri }, local: { ...result, local_thumbnail: result.uri, hash },
uploading: true uploading: true
} }
}) })
@ -28,7 +33,7 @@ const addAttachment = async ({ composeDispatch }: Props): Promise<any> => {
composeDispatch({ composeDispatch({
type: 'attachment/upload/start', type: 'attachment/upload/start',
payload: { payload: {
local: { ...result, local_thumbnail: uri }, local: { ...result, local_thumbnail: uri, hash },
uploading: true uploading: true
} }
}) })
@ -37,7 +42,7 @@ const addAttachment = async ({ composeDispatch }: Props): Promise<any> => {
composeDispatch({ composeDispatch({
type: 'attachment/upload/start', type: 'attachment/upload/start',
payload: { payload: {
local: result, local: { ...result, hash },
uploading: true uploading: true
} }
}) })
@ -47,7 +52,7 @@ const addAttachment = async ({ composeDispatch }: Props): Promise<any> => {
composeDispatch({ composeDispatch({
type: 'attachment/upload/start', type: 'attachment/upload/start',
payload: { payload: {
local: result, local: { ...result, hash },
uploading: true uploading: true
} }
}) })
@ -62,7 +67,7 @@ const addAttachment = async ({ composeDispatch }: Props): Promise<any> => {
type: 'image/jpeg/jpg' type: 'image/jpeg/jpg'
}) })
client({ return client({
method: 'post', method: 'post',
instance: 'local', instance: 'local',
url: 'media', url: 'media',
@ -74,25 +79,30 @@ const addAttachment = async ({ composeDispatch }: Props): Promise<any> => {
type: 'attachment/upload/end', type: 'attachment/upload/end',
payload: { remote: body, local: result } payload: { remote: body, local: result }
}) })
return Promise.resolve()
} else { } else {
composeDispatch({
type: 'attachment/upload/fail',
payload: hash
})
Alert.alert('上传失败', '', [ Alert.alert('上传失败', '', [
{ {
text: '返回重试', text: '返回重试',
onPress: () => {} onPress: () => {}
} }
]) ])
return Promise.reject()
} }
}) })
.catch(() => { .catch(() => {
composeDispatch({
type: 'attachment/upload/fail',
payload: hash
})
Alert.alert('上传失败', '', [ Alert.alert('上传失败', '', [
{ {
text: '返回重试', text: '返回重试',
onPress: () => {} onPress: () => {}
} }
]) ])
return Promise.reject()
}) })
} }

View File

@ -40,7 +40,7 @@ const composePost = async (
formData.append('visibility', composeState.visibility) formData.append('visibility', composeState.visibility)
const res = await client({ return client({
method: 'post', method: 'post',
instance: 'local', instance: 'local',
url: 'statuses', url: 'statuses',
@ -63,12 +63,6 @@ const composePost = async (
}, },
body: formData body: formData
}) })
if (res.body.id) {
return Promise.resolve()
} else {
return Promise.resolve()
}
} }
export default composePost export default composePost

View File

@ -40,6 +40,16 @@ const composeReducer = (
) )
} }
} }
case 'attachment/upload/fail':
return {
...state,
attachments: {
...state.attachments,
uploads: state.attachments.uploads.filter(
upload => upload.local.hash !== action.payload
)
}
}
case 'attachment/delete': case 'attachment/delete':
return { return {
...state, ...state,

View File

@ -1,6 +1,6 @@
export type ExtendedAttachment = { export type ExtendedAttachment = {
remote?: Mastodon.Attachment remote?: Mastodon.Attachment
local?: ImageInfo & { local_thumbnail?: string } local?: ImageInfo & { local_thumbnail?: string; hash?: string }
uploading?: boolean uploading?: boolean
} }
@ -94,6 +94,10 @@ export type ComposeAction =
type: 'attachment/upload/end' type: 'attachment/upload/end'
payload: { remote: Mastodon.Attachment; local: ImageInfo } payload: { remote: Mastodon.Attachment; local: ImageInfo }
} }
| {
type: 'attachment/upload/fail'
payload: ExtendedAttachment['local']['hash']
}
| { | {
type: 'attachment/delete' type: 'attachment/delete'
payload: NonNullable<ExtendedAttachment['remote']>['id'] payload: NonNullable<ExtendedAttachment['remote']>['id']

View File

@ -9,7 +9,7 @@ export const StyleConstants = {
FontStyle: { FontStyle: {
S: { fontSize: 14, lineHeight: 18 }, S: { fontSize: 14, lineHeight: 18 },
M: { fontSize: 16, lineHeight: 22 }, M: { fontSize: 16, lineHeight: 22 },
L: { fontSize: 20, lineHeight: 30 } L: { fontSize: 20, lineHeight: 26 }
}, },
Spacing: { Spacing: {