This commit is contained in:
Zhiyuan Zheng 2021-01-16 00:00:31 +01:00
parent 9f4a4e908c
commit 5ec9118fb2
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
31 changed files with 607 additions and 416 deletions

2
src/@types/app.d.ts vendored
View File

@ -10,7 +10,7 @@ declare namespace App {
| 'Toot'
| 'Account_Default'
| 'Account_All'
| 'Account_Media'
| 'Account_Attachments'
| 'Conversations'
| 'Bookmarks'
| 'Favourites'

View File

@ -12,6 +12,7 @@ declare namespace Nav {
account: Pick<Mastodon.Account, 'id' | 'username' | 'acct' | 'url'>
}
'Screen-Shared-Announcements': { showAll?: boolean }
'Screen-Shared-Attachments': { account: Mastodon.Account }
'Screen-Shared-Compose':
| {
type: 'reply' | 'conversation' | 'edit'

View File

@ -1,27 +1,25 @@
import { ParseEmojis } from '@components/Parse'
import { useNavigation } from '@react-navigation/native'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { Image, Pressable, StyleSheet, Text, View } from 'react-native'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import GracefullyImage from './GracefullyImage'
export interface Props {
account: Mastodon.Account
onPress: () => void
}
const ComponentAccount: React.FC<Props> = ({ account }) => {
const navigation = useNavigation()
const ComponentAccount: React.FC<Props> = ({ account, onPress }) => {
const { theme } = useTheme()
return (
<Pressable
style={[styles.itemDefault, styles.itemAccount]}
onPress={() => {
navigation.push('Screen-Shared-Account', { account })
}}
onPress={onPress}
>
<Image
source={{ uri: account.avatar_static }}
<GracefullyImage
uri={{ original: account.avatar_static }}
style={styles.itemAccountAvatar}
/>
<View>

View File

@ -155,26 +155,24 @@ const Button: React.FC<Props> = ({
}
return (
<View>
<Pressable
style={[
styles.button,
{
borderWidth: overlay ? 0 : 1,
borderColor: colorBorder,
backgroundColor: colorBackground,
paddingVertical: StyleConstants.Spacing[spacing],
paddingHorizontal:
StyleConstants.Spacing[round ? spacing : spacingMapping[spacing]]
},
customStyle
]}
testID='base'
onPress={onPress}
children={children}
disabled={disabled || active || loading}
/>
</View>
<Pressable
style={[
styles.button,
{
borderWidth: overlay ? 0 : 1,
borderColor: colorBorder,
backgroundColor: colorBackground,
paddingVertical: StyleConstants.Spacing[spacing],
paddingHorizontal:
StyleConstants.Spacing[round ? spacing : spacingMapping[spacing]]
},
customStyle
]}
testID='base'
onPress={onPress}
children={children}
disabled={disabled || active || loading}
/>
)
}

View File

@ -0,0 +1,171 @@
import { StyleConstants } from '@utils/styles/constants'
import { Surface } from 'gl-react-expo'
import { Blurhash } from 'gl-react-blurhash'
import React, { useCallback, useEffect, useState } from 'react'
import {
Image,
Pressable,
StyleProp,
StyleSheet,
View,
ViewStyle
} from 'react-native'
import { Image as ImageCache } from 'react-native-expo-image-cache'
import { useTheme } from '@utils/styles/ThemeManager'
type CancelPromise = ((reason?: Error) => void) | undefined
type ImageSize = { width: number; height: number }
interface ImageSizeOperation {
start: () => Promise<ImageSize>
cancel: CancelPromise
}
const getImageSize = (uri: string): ImageSizeOperation => {
let cancel: CancelPromise
const start = (): Promise<ImageSize> =>
new Promise<{ width: number; height: number }>((resolve, reject) => {
cancel = reject
Image.getSize(
uri,
(width, height) => {
cancel = undefined
resolve({ width, height })
},
error => {
reject(error)
}
)
})
return { start, cancel }
}
export interface Props {
hidden?: boolean
cache?: boolean
uri: { preview?: string; original?: string; remote?: string }
blurhash?: string
dimension?: { width: number; height: number }
onPress?: () => void
style?: StyleProp<ViewStyle>
}
const GracefullyImage: React.FC<Props> = ({
hidden = false,
cache = false,
uri,
blurhash,
dimension,
onPress,
style
}) => {
const { mode, theme } = useTheme()
const [imageVisible, setImageVisible] = useState<string>()
const [imageLoadingFailed, setImageLoadingFailed] = useState(false)
useEffect(() => {
let cancel: CancelPromise
const sideEffect = async (): Promise<void> => {
try {
if (uri.preview) {
const tryPreview = getImageSize(uri.preview)
cancel = tryPreview.cancel
const res = await tryPreview.start()
if (res) {
setImageVisible(uri.preview)
return
}
}
} catch (error) {
if (__DEV__) console.warn('Image preview', error)
}
try {
const tryOriginal = getImageSize(uri.original!)
cancel = tryOriginal.cancel
const res = await tryOriginal.start()
if (res) {
setImageVisible(uri.original!)
return
}
} catch (error) {
if (__DEV__) console.warn('Image original', error)
}
try {
if (uri.remote) {
const tryRemote = getImageSize(uri.remote)
cancel = tryRemote.cancel
const res = await tryRemote.start()
if (res) {
setImageVisible(uri.remote)
return
}
}
} catch (error) {
if (__DEV__) console.warn('Image remote', error)
}
setImageLoadingFailed(true)
}
if (uri.original) {
sideEffect()
}
return () => {
if (cancel) {
cancel()
}
}
}, [uri])
const children = useCallback(() => {
if (imageVisible && !hidden) {
if (cache) {
return <ImageCache uri={imageVisible} style={styles.image} />
} else {
return <Image source={{ uri: imageVisible }} style={styles.image} />
}
} else if (blurhash) {
return (
<Surface
style={{
width: '100%',
height: '100%',
position: 'absolute',
top: StyleConstants.Spacing.XS / 2,
left: StyleConstants.Spacing.XS / 2
}}
>
<Blurhash hash={blurhash} />
</Surface>
)
} else {
return (
<View
style={[styles.image, { backgroundColor: theme.shimmerDefault }]}
/>
)
}
}, [hidden, mode, imageVisible])
return (
<Pressable
children={children}
style={[style, dimension && { ...dimension }]}
{...(onPress
? !imageVisible
? { disabled: true }
: { onPress }
: { disabled: true })}
/>
)
}
const styles = StyleSheet.create({
image: {
flex: 1
}
})
export default GracefullyImage

View File

@ -26,13 +26,13 @@ const HeaderRight: React.FC<Props> = ({
const { theme } = useTheme()
const mounted = useRef(false)
useEffect(() => {
if (mounted.current) {
layoutAnimation()
} else {
mounted.current = true
}
}, [content, loading, disabled])
// useEffect(() => {
// if (mounted.current) {
// layoutAnimation()
// } else {
// mounted.current = true
// }
// }, [content, loading, disabled])
const loadingSpinkit = useMemo(
() => (

View File

@ -108,7 +108,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
renderTabBar={() => null}
onIndexChange={index => setSegment(index)}
navigationState={{ index: segment, routes }}
initialLayout={{ width: Dimensions.get('window').width }}
initialLayout={{ width: Dimensions.get('screen').width }}
/>
)}
</Stack.Screen>

View File

@ -0,0 +1,36 @@
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { Pressable, StyleSheet, Text } from 'react-native'
export interface Props {
tag: Mastodon.Tag
onPress: () => void
}
const ComponentHashtag: React.FC<Props> = ({ tag, onPress }) => {
const { theme } = useTheme()
return (
<Pressable
style={[styles.itemDefault, { borderBottomColor: theme.border }]}
onPress={onPress}
>
<Text style={[styles.itemHashtag, { color: theme.primary }]}>
#{tag.name}
</Text>
</Pressable>
)
}
const styles = StyleSheet.create({
itemDefault: {
padding: StyleConstants.Spacing.S * 1.5,
borderBottomWidth: StyleSheet.hairlineWidth
},
itemHashtag: {
...StyleConstants.FontStyle.M
}
})
export default ComponentHashtag

View File

@ -232,10 +232,10 @@ const Timeline: React.FC<Props> = ({
{...(queryKey &&
queryKey[1].page === 'RemotePublic' && { ListHeaderComponent })}
{...(toot && isSuccess && { onScrollToIndexFailed })}
// maintainVisibleContentPosition={{
// minIndexForVisible: 0,
// autoscrollToTopThreshold: 2
// }}
maintainVisibleContentPosition={{
minIndexForVisible: 0,
autoscrollToTopThreshold: 1
}}
{...customProps}
/>
)

View File

@ -101,8 +101,8 @@ const TimelineAttachment: React.FC<Props> = ({ status }) => {
)
return (
<View style={styles.base}>
{attachments}
<View>
<View style={styles.container}>{attachments}</View>
{status.sensitive &&
(sensitiveShown ? (
@ -123,7 +123,7 @@ const TimelineAttachment: React.FC<Props> = ({ status }) => {
onPress={onPressShow}
style={{
position: 'absolute',
top: StyleConstants.Spacing.S,
top: StyleConstants.Spacing.S * 2,
left: StyleConstants.Spacing.S
}}
/>
@ -133,7 +133,7 @@ const TimelineAttachment: React.FC<Props> = ({ status }) => {
}
const styles = StyleSheet.create({
base: {
container: {
marginTop: StyleConstants.Spacing.S,
flex: 1,
flexDirection: 'row',
@ -141,10 +141,6 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignContent: 'stretch'
},
container: {
flexBasis: '50%',
aspectRatio: 16 / 9
},
sensitiveBlur: {
position: 'absolute',
width: '100%',

View File

@ -6,7 +6,8 @@ import { Audio } from 'expo-av'
import { Surface } from 'gl-react-expo'
import { Blurhash } from 'gl-react-blurhash'
import React, { useCallback, useState } from 'react'
import { Image, StyleSheet, View } from 'react-native'
import { StyleSheet, View } from 'react-native'
import GracefullyImage from '@components/GracefullyImage'
export interface Props {
sensitiveShown: boolean
@ -38,7 +39,7 @@ const AttachmentAudio: React.FC<Props> = ({ sensitiveShown, audio }) => {
audioPlayer!.pauseAsync()
setAudioPlaying(false)
}, [audioPlayer])
console.log(audio)
return (
<View style={[styles.base, { backgroundColor: theme.disabled }]}>
<View style={styles.overlay}>
@ -56,9 +57,11 @@ const AttachmentAudio: React.FC<Props> = ({ sensitiveShown, audio }) => {
) : (
<>
{(audio.preview_url || audio.preview_remote_url) && (
<Image
<GracefullyImage
uri={{
original: audio.preview_url || audio.preview_remote_url
}}
style={styles.background}
source={{ uri: audio.preview_url || audio.preview_remote_url }}
/>
)}
<Button

View File

@ -1,8 +1,7 @@
import { Surface } from 'gl-react-expo'
import { Blurhash } from 'gl-react-blurhash'
import React, { useCallback, useEffect, useState } from 'react'
import { Image, StyleSheet, Pressable } from 'react-native'
import React, { useCallback } from 'react'
import { StyleSheet } from 'react-native'
import { StyleConstants } from '@utils/styles/constants'
import GracefullyImage from '@components/GracefullyImage'
export interface Props {
sensitiveShown: boolean
@ -17,69 +16,19 @@ const AttachmentImage: React.FC<Props> = ({
imageIndex,
navigateToImagesViewer
}) => {
let isMounted = false
useEffect(() => {
isMounted = true
return () => {
isMounted = false
}
})
const [imageVisible, setImageVisible] = useState<string>()
const [imageLoadingFailed, setImageLoadingFailed] = useState(false)
useEffect(() => {
const preFetch = () =>
isMounted &&
Image.getSize(
image.preview_url,
() => isMounted && setImageVisible(image.preview_url),
() => {
isMounted &&
Image.getSize(
image.url,
() => isMounted && setImageVisible(image.url),
() =>
image.remote_url
? isMounted &&
Image.getSize(
image.remote_url,
() => isMounted && setImageVisible(image.remote_url),
() => isMounted && setImageLoadingFailed(true)
)
: isMounted && setImageLoadingFailed(true)
)
}
)
preFetch()
}, [isMounted])
const children = useCallback(() => {
if (imageVisible && !sensitiveShown) {
return <Image source={{ uri: imageVisible }} style={styles.image} />
} else {
return (
<Surface
style={{
width: '100%',
height: '100%',
position: 'absolute',
top: StyleConstants.Spacing.XS / 2,
left: StyleConstants.Spacing.XS / 2
}}
>
<Blurhash hash={image.blurhash} />
</Surface>
)
}
}, [imageVisible, sensitiveShown])
const onPress = useCallback(() => navigateToImagesViewer(imageIndex), [])
return (
<Pressable
style={[styles.base]}
children={children}
<GracefullyImage
hidden={sensitiveShown}
uri={{
preview: image.preview_url,
original: image.url,
remote: image.remote_url
}}
blurhash={image.blurhash}
onPress={onPress}
disabled={!imageVisible || sensitiveShown}
style={styles.base}
/>
)
}
@ -90,9 +39,6 @@ const styles = StyleSheet.create({
flexBasis: '50%',
aspectRatio: 16 / 9,
padding: StyleConstants.Spacing.XS / 2
},
image: {
flex: 1
}
})

View File

@ -23,6 +23,7 @@ const AttachmentVideo: React.FC<Props> = ({ sensitiveShown, video }) => {
}
await videoPlayer.current?.setPositionAsync(videoPosition)
await videoPlayer.current?.presentFullscreenPlayer()
console.log('playing!!!')
videoPlayer.current?.playAsync()
setVideoLoading(false)
videoPlayer.current?.setOnPlaybackStatusUpdate(props => {
@ -51,6 +52,11 @@ const AttachmentVideo: React.FC<Props> = ({ sensitiveShown, video }) => {
posterSource={{ uri: video.preview_url }}
posterStyle={{ resizeMode: 'cover' }}
useNativeControls={false}
onFullscreenUpdate={event => {
if (event.fullscreenUpdate === 3) {
videoPlayer.current?.pauseAsync()
}
}}
/>
<Pressable style={styles.overlay}>
{sensitiveShown ? (

View File

@ -1,9 +1,8 @@
import React, { useCallback } from 'react'
import { Pressable, StyleSheet } from 'react-native'
import { Image } from 'react-native-expo-image-cache'
import { StyleConstants } from '@utils/styles/constants'
import { useNavigation } from '@react-navigation/native'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import GracefullyImage from '@components/GracefullyImage'
export interface Props {
queryKey?: QueryKeyTimeline
@ -18,23 +17,21 @@ const TimelineAvatar: React.FC<Props> = ({ queryKey, account }) => {
}, [])
return (
<Pressable style={styles.avatar} onPress={onPress}>
<Image uri={account.avatar_static} style={styles.image} />
</Pressable>
<GracefullyImage
cache
onPress={onPress}
uri={{ original: account.avatar_static }}
dimension={{
width: StyleConstants.Avatar.M,
height: StyleConstants.Avatar.M
}}
style={{
borderRadius: 4,
overflow: 'hidden',
marginRight: StyleConstants.Spacing.S
}}
/>
)
}
const styles = StyleSheet.create({
avatar: {
flexBasis: StyleConstants.Avatar.M,
height: StyleConstants.Avatar.M,
marginRight: StyleConstants.Spacing.S
},
image: {
width: '100%',
height: '100%',
borderRadius: 6
}
})
export default React.memo(TimelineAvatar, () => true)

View File

@ -1,82 +1,17 @@
import React, { useEffect, useMemo, useState } from 'react'
import { Image, Pressable, StyleSheet, Text, View } from 'react-native'
import GracefullyImage from '@components/GracefullyImage'
import openLink from '@components/openLink'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { Surface } from 'gl-react-expo'
import { Blurhash } from 'gl-react-blurhash'
import React from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native'
export interface Props {
card: Mastodon.Card
}
type CancelPromise = ((reason?: Error) => void) | undefined
type ImageSize = { width: number; height: number }
interface ImageSizeOperation {
start: () => Promise<ImageSize>
cancel: CancelPromise
}
const getImageSize = (uri: string): ImageSizeOperation => {
let cancel: CancelPromise
const start = (): Promise<ImageSize> =>
new Promise<{ width: number; height: number }>((resolve, reject) => {
cancel = reject
Image.getSize(
uri,
(width, height) => {
cancel = undefined
resolve({ width, height })
},
error => {
reject(error)
}
)
})
return { start, cancel }
}
const TimelineCard: React.FC<Props> = ({ card }) => {
const { theme } = useTheme()
const [imageLoaded, setImageLoaded] = useState(false)
useEffect(() => {
if (card.image) {
Image.getSize(card.image, () => setImageLoaded(true))
}
}, [])
useEffect(() => {
let cancel: CancelPromise
const sideEffect = async (): Promise<void> => {
try {
const operation = getImageSize(card.image)
cancel = operation.cancel
await operation.start()
} catch (error) {
if (__DEV__) console.warn(error)
}
}
if (card.image) {
sideEffect()
}
return () => {
if (cancel) {
cancel()
}
}
})
const cardVisual = useMemo(() => {
if (imageLoaded) {
return <Image source={{ uri: card.image }} style={styles.image} />
} else {
return card.blurhash ? (
<Surface style={styles.image}>
<Blurhash hash={card.blurhash} />
</Surface>
) : null
}
}, [imageLoaded])
return (
<Pressable
style={[styles.card, { borderColor: theme.border }]}
@ -84,9 +19,11 @@ const TimelineCard: React.FC<Props> = ({ card }) => {
testID='base'
>
{card.image && (
<View style={styles.left} testID='image'>
{cardVisual}
</View>
<GracefullyImage
uri={{ original: card.image }}
blurhash={card.blurhash}
style={styles.left}
/>
)}
<View style={styles.right}>
<Text
@ -117,13 +54,14 @@ const styles = StyleSheet.create({
card: {
flex: 1,
flexDirection: 'row',
height: StyleConstants.Avatar.L,
height: StyleConstants.Font.LineHeight.M * 4.5,
marginTop: StyleConstants.Spacing.M,
borderWidth: StyleSheet.hairlineWidth,
borderRadius: 6
},
left: {
width: StyleConstants.Avatar.L
width: StyleConstants.Font.LineHeight.M * 4.5,
height: StyleConstants.Font.LineHeight.M * 4.5
},
image: {
width: '100%',

View File

@ -4,13 +4,13 @@ const strings = {
suffixAgo: '之前',
suffixFromNow: '之后',
seconds: '%d秒',
minute: '大约1分钟',
minute: '1分钟',
minutes: '%d分钟',
hour: '大约1小时',
hours: '大约%d小时',
hour: '1小时',
hours: '%d小时',
day: '1天',
days: '%d天',
month: '大约1个月',
month: '1个月',
months: '%d月',
year: '大约1年',
years: '%d年',

View File

@ -4,9 +4,18 @@ import Timeline from '@components/Timelines/Timeline'
import HeaderActionsAccount from '@components/Timelines/Timeline/Shared/HeaderActions/ActionsAccount'
import { useAccountQuery } from '@utils/queryHooks/account'
import { getLocalAccount } from '@utils/slices/instancesSlice'
import React, { useCallback, useEffect, useReducer, useState } from 'react'
import { useTheme } from '@utils/styles/ThemeManager'
import React, {
useCallback,
useEffect,
useMemo,
useReducer,
useState
} from 'react'
import { StyleSheet, View } from 'react-native'
import { useSharedValue } from 'react-native-reanimated'
import { useSelector } from 'react-redux'
import AccountAttachments from './Account/Attachments'
import AccountHeader from './Account/Header'
import AccountInformation from './Account/Information'
import AccountNav from './Account/Nav'
@ -23,6 +32,8 @@ const ScreenSharedAccount: React.FC<SharedAccountProp> = ({
},
navigation
}) => {
const { theme } = useTheme()
const localAccount = useSelector(getLocalAccount)
const { data } = useAccountQuery({ id: account.id })
@ -50,6 +61,16 @@ const ScreenSharedAccount: React.FC<SharedAccountProp> = ({
scrollY.value = nativeEvent.contentOffset.y
}, [])
const ListHeaderComponent = useMemo(() => {
return (
<View style={[styles.header, { borderBottomColor: theme.border }]}>
<AccountHeader account={data} />
<AccountInformation account={data} />
<AccountAttachments account={data} />
</View>
)
}, [data])
return (
<AccountContext.Provider value={{ accountState, accountDispatch }}>
<AccountNav scrollY={scrollY} account={data} />
@ -61,12 +82,7 @@ const ScreenSharedAccount: React.FC<SharedAccountProp> = ({
customProps={{
onScroll,
scrollEventThrottle: 16,
ListHeaderComponent: (
<>
<AccountHeader account={data} />
<AccountInformation account={data} />
</>
)
ListHeaderComponent
}}
/>
@ -86,4 +102,10 @@ const ScreenSharedAccount: React.FC<SharedAccountProp> = ({
)
}
const styles = StyleSheet.create({
header: {
borderBottomWidth: 1
}
})
export default ScreenSharedAccount

View File

@ -0,0 +1,140 @@
import GracefullyImage from '@components/GracefullyImage'
import Icon from '@components/Icon'
import { useNavigation } from '@react-navigation/native'
import { useTimelineQuery } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect } from 'react'
import {
Dimensions,
ListRenderItem,
Pressable,
StyleSheet,
View
} from 'react-native'
import { FlatList } from 'react-native-gesture-handler'
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
export interface Props {
account: Mastodon.Account | undefined
}
const AccountAttachments = React.memo(
({ account }: Props) => {
const navigation = useNavigation()
const { theme } = useTheme()
const width =
(Dimensions.get('screen').width -
StyleConstants.Spacing.Global.PagePadding * 2) /
4
const queryKeyParams = {
page: 'Account_Attachments' as 'Account_Attachments',
account: account?.id
}
const { data, refetch } = useTimelineQuery({
...queryKeyParams,
options: { enabled: false }
})
useEffect(() => {
if (account?.id) {
refetch()
}
}, [account])
const flattenData = (data?.pages
? data.pages.flatMap(d => [...d])
: []) as Mastodon.Status[]
useEffect(() => {
if (flattenData.length) {
layoutAnimation()
}
}, [flattenData.length])
const renderItem = useCallback<ListRenderItem<Mastodon.Status>>(
({ item, index }) => {
if (index === 3) {
return (
<Pressable
onPress={() =>
navigation.push('Screen-Shared-Attachments', { account })
}
children={
<View
style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: theme.backgroundOverlay,
width: width,
height: width,
justifyContent: 'center',
alignItems: 'center'
}}
children={
<Icon
name='MoreHorizontal'
color={theme.primaryOverlay}
size={StyleConstants.Font.Size.L * 1.5}
/>
}
/>
}
/>
)
} else {
return (
<GracefullyImage
uri={{
original: item.media_attachments[0].preview_url,
remote: item.media_attachments[0].remote_url
}}
blurhash={item.media_attachments[0].blurhash}
dimension={{ width: width, height: width }}
style={{ marginLeft: StyleConstants.Spacing.Global.PagePadding }}
onPress={() =>
navigation.push('Screen-Shared-Toot', { toot: item })
}
/>
)
}
},
[account]
)
const styleContainer = useAnimatedStyle(() => {
if (flattenData.length) {
return {
height: withTiming(
width + StyleConstants.Spacing.Global.PagePadding * 2
),
paddingVertical: StyleConstants.Spacing.Global.PagePadding,
borderTopWidth: 1,
borderTopColor: theme.border
}
} else {
return {}
}
}, [flattenData.length])
return (
<Animated.View style={[styles.base, styleContainer]}>
<FlatList
horizontal
data={flattenData.splice(0, 4)}
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
/>
</Animated.View>
)
},
(_, next) => next.account === undefined
)
const styles = StyleSheet.create({
base: {
flex: 1
}
})
export default AccountAttachments

View File

@ -6,6 +6,7 @@ import Animated, {
useSharedValue,
withTiming
} from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import AccountContext from './utils/createContext'
export interface Props {
@ -16,9 +17,10 @@ export interface Props {
const AccountHeader: React.FC<Props> = ({ account, limitHeight = false }) => {
const { accountState, accountDispatch } = useContext(AccountContext)
const { theme } = useTheme()
const topInset = useSafeAreaInsets().top
const height = useSharedValue(
Dimensions.get('screen').width * accountState.headerRatio
Dimensions.get('screen').width * accountState.headerRatio + topInset
)
const styleHeight = useAnimatedStyle(() => {
return {
@ -32,12 +34,12 @@ const AccountHeader: React.FC<Props> = ({ account, limitHeight = false }) => {
!account.header.includes('/headers/original/missing.png')
) {
Image.getSize(account.header, (width, height) => {
if (!limitHeight) {
accountDispatch({
type: 'headerRatio',
payload: height / width
})
}
// if (!limitHeight) {
// accountDispatch({
// type: 'headerRatio',
// payload: height / width
// })
// }
})
}
}, [account])

View File

@ -1,6 +1,5 @@
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { createRef, useCallback, useContext, useEffect } from 'react'
import React, { createRef, useEffect } from 'react'
import { Animated, StyleSheet, View } from 'react-native'
import AccountInformationAccount from './Information/Account'
import AccountInformationActions from './Information/Actions'
@ -11,7 +10,6 @@ import AccountInformationName from './Information/Name'
import AccountInformationNotes from './Information/Notes'
import AccountInformationStats from './Information/Stats'
import AccountInformationSwitch from './Information/Switch'
import AccountContext from './utils/createContext'
export interface Props {
account: Mastodon.Account | undefined
@ -22,10 +20,6 @@ const AccountInformation: React.FC<Props> = ({
account,
ownAccount = false
}) => {
const { theme } = useTheme()
const { accountDispatch } = useContext(AccountContext)
const shimmerAvatarRef = createRef<any>()
const shimmerNameRef = createRef<any>()
const shimmerAccountRef = createRef<any>()
const shimmerCreatedRef = createRef<any>()
@ -33,7 +27,6 @@ const AccountInformation: React.FC<Props> = ({
useEffect(() => {
const informationAnimated = Animated.stagger(400, [
Animated.parallel([
shimmerAvatarRef.current?.getAnimated(),
shimmerNameRef.current?.getAnimated(),
shimmerAccountRef.current?.getAnimated(),
shimmerCreatedRef.current?.getAnimated(),
@ -45,27 +38,11 @@ const AccountInformation: React.FC<Props> = ({
Animated.loop(informationAnimated).start()
}, [])
const onLayout = useCallback(
({ nativeEvent }) =>
accountDispatch &&
accountDispatch({
type: 'informationLayout',
payload: {
y: nativeEvent.layout.y,
height: nativeEvent.layout.height
}
}),
[]
)
return (
<View
style={[styles.base, { borderBottomColor: theme.border }]}
onLayout={onLayout}
>
<View style={styles.base}>
{/* <Text>Moved or not: {account.moved}</Text> */}
<View style={styles.avatarAndActions}>
<AccountInformationAvatar ref={shimmerAvatarRef} account={account} />
<AccountInformationAvatar account={account} />
<View style={styles.actions}>
{ownAccount ? (
<AccountInformationSwitch />
@ -105,8 +82,7 @@ const AccountInformation: React.FC<Props> = ({
const styles = StyleSheet.create({
base: {
marginTop: -StyleConstants.Spacing.Global.PagePadding * 3,
padding: StyleConstants.Spacing.Global.PagePadding,
borderBottomWidth: StyleSheet.hairlineWidth
padding: StyleConstants.Spacing.Global.PagePadding
},
avatarAndActions: {
flexDirection: 'row',

View File

@ -25,7 +25,11 @@ const AccountInformationAccount = forwardRef<ShimmerPlaceholder, Props>(
width={StyleConstants.Font.Size.M * 8}
height={StyleConstants.Font.LineHeight.M}
style={{ marginBottom: StyleConstants.Spacing.L }}
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
shimmerColors={[
theme.shimmerDefault,
theme.shimmerHighlight,
theme.shimmerDefault
]}
>
<View style={styles.account}>
<Text
@ -67,4 +71,7 @@ const styles = StyleSheet.create({
type: { marginLeft: StyleConstants.Spacing.S }
})
export default AccountInformationAccount
export default React.memo(
AccountInformationAccount,
(_, next) => next.account === undefined
)

View File

@ -1,47 +1,25 @@
import { StyleConstants } from '@root/utils/styles/constants'
import { useTheme } from '@root/utils/styles/ThemeManager'
import { LinearGradient } from 'expo-linear-gradient'
import React, { forwardRef, useState } from 'react'
import { Image, StyleSheet } from 'react-native'
import ShimmerPlaceholder, {
createShimmerPlaceholder
} from 'react-native-shimmer-placeholder'
import GracefullyImage from '@components/GracefullyImage'
import { StyleConstants } from '@utils/styles/constants'
import React from 'react'
export interface Props {
account: Mastodon.Account | undefined
}
const AccountInformationAvatar = forwardRef<ShimmerPlaceholder, Props>(
({ account }, ref) => {
const { theme } = useTheme()
const [avatarLoaded, setAvatarLoaded] = useState(false)
const ShimmerPlaceholder = createShimmerPlaceholder(LinearGradient)
const AccountInformationAvatar = React.memo(
({ account }: Props) => {
return (
<ShimmerPlaceholder
ref={ref}
visible={avatarLoaded}
width={StyleConstants.Avatar.L}
height={StyleConstants.Avatar.L}
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
>
<Image
source={{ uri: account?.avatar }}
style={styles.avatar}
onLoadEnd={() => setAvatarLoaded(true)}
/>
</ShimmerPlaceholder>
<GracefullyImage
uri={{ original: account?.avatar }}
dimension={{
width: StyleConstants.Avatar.L,
height: StyleConstants.Avatar.L
}}
style={{ borderRadius: 8, overflow: 'hidden' }}
/>
)
}
},
(_, next) => next.account === undefined
)
const styles = StyleSheet.create({
avatar: {
width: StyleConstants.Avatar.L,
height: StyleConstants.Avatar.L,
borderRadius: 8
}
})
export default AccountInformationAvatar

View File

@ -0,0 +1,13 @@
import Timeline from '@components/Timelines/Timeline'
import React from 'react'
import { SharedAttachmentsProp } from './sharedScreens'
const ScreenSharedAttachments: React.FC<SharedAttachmentsProp> = ({
route: {
params: { account }
}
}) => {
return <Timeline page='Account_Attachments' account={account.id} />
}
export default ScreenSharedAttachments

View File

@ -1,3 +1,4 @@
import ComponentSeparator from '@components/Separator'
import { useEmojisQuery } from '@utils/queryHooks/emojis'
import { useSearchQuery } from '@utils/queryHooks/search'
import { StyleConstants } from '@utils/styles/constants'
@ -67,8 +68,9 @@ const ComposeRoot: React.FC = () => {
}, [isFetching])
const listItem = useCallback(
({ item }) => (
({ item, index }) => (
<ComposeRootSuggestion
key={(item.id || item.name) + index}
item={item}
composeState={composeState}
composeDispatch={composeDispatch}
@ -85,9 +87,9 @@ const ComposeRoot: React.FC = () => {
keyboardShouldPersistTaps='handled'
ListHeaderComponent={ComposeRootHeader}
ListFooterComponent={ComposeRootFooter}
ItemSeparatorComponent={ComponentSeparator}
// @ts-ignore
data={data ? data[composeState.tag?.type] : undefined}
keyExtractor={({ item }) => item.acct || item.name}
/>
<ComposeActions />
<ComposePosting />

View File

@ -1,9 +1,7 @@
import ComponentAccount from '@components/Account'
import haptics from '@components/haptics'
import { ParseEmojis } from '@components/Parse'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { Dispatch, useCallback, useMemo } from 'react'
import { Image, Pressable, StyleSheet, Text, View } from 'react-native'
import ComponentHashtag from '@components/Timelines/Hashtag'
import React, { Dispatch, useCallback } from 'react'
import updateText from '../updateText'
import { ComposeAction, ComposeState } from '../utils/types'
@ -17,7 +15,6 @@ const ComposeRootSuggestion = React.memo(
composeState: ComposeState
composeDispatch: Dispatch<ComposeAction>
}) => {
const { theme } = useTheme()
const onPress = useCallback(() => {
const focusedInput = composeState.textInputFocus.current
updateText({
@ -37,91 +34,14 @@ const ComposeRootSuggestion = React.memo(
})
haptics('Light')
}, [])
const children = useMemo(
() =>
item.acct ? (
<View style={[styles.account, { borderBottomColor: theme.border }]}>
<Image source={{ uri: item.avatar }} style={styles.accountAvatar} />
<View>
<Text
style={[styles.accountName, { color: theme.primary }]}
numberOfLines={1}
>
<ParseEmojis
content={item.display_name || item.username}
emojis={item.emojis}
size='S'
/>
</Text>
<Text
style={[styles.accountAccount, { color: theme.primary }]}
numberOfLines={1}
>
@{item.acct}
</Text>
</View>
</View>
) : (
<View style={[styles.hashtag, { borderBottomColor: theme.border }]}>
<Text style={[styles.hashtagText, { color: theme.primary }]}>
#{item.name}
</Text>
</View>
),
[]
)
return (
<Pressable
onPress={onPress}
style={styles.suggestion}
children={children}
/>
return item.acct ? (
<ComponentAccount account={item} onPress={onPress} />
) : (
<ComponentHashtag tag={item} onPress={onPress} />
)
},
() => true
)
const styles = StyleSheet.create({
suggestion: {
flex: 1
},
account: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
paddingTop: StyleConstants.Spacing.S,
paddingBottom: StyleConstants.Spacing.S,
marginLeft: StyleConstants.Spacing.Global.PagePadding,
marginRight: StyleConstants.Spacing.Global.PagePadding,
borderBottomWidth: StyleSheet.hairlineWidth
},
accountAvatar: {
width: StyleConstants.Font.LineHeight.M * 2,
height: StyleConstants.Font.LineHeight.M * 2,
marginRight: StyleConstants.Spacing.S,
borderRadius: StyleConstants.Avatar.M
},
accountName: {
...StyleConstants.FontStyle.S,
fontWeight: StyleConstants.Font.Weight.Bold,
marginBottom: StyleConstants.Spacing.XS
},
accountAccount: {
...StyleConstants.FontStyle.S
},
hashtag: {
flex: 1,
paddingTop: StyleConstants.Spacing.S,
paddingBottom: StyleConstants.Spacing.S,
marginLeft: StyleConstants.Spacing.Global.PagePadding,
marginRight: StyleConstants.Spacing.Global.PagePadding,
borderBottomWidth: StyleSheet.hairlineWidth
},
hashtagText: {
...StyleConstants.FontStyle.S,
fontWeight: StyleConstants.Font.Weight.Bold,
marginBottom: StyleConstants.Spacing.XS
}
})
export default ComposeRootSuggestion

View File

@ -43,7 +43,7 @@ const composePost = async (
return client<Mastodon.Status>({
method: 'post',
instance: 'local',
url: 'statusess',
url: 'statuses',
headers: {
'Idempotency-Key': await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,

View File

@ -57,7 +57,7 @@ const ScreenSharedRelationships: React.FC<SharedRelationshipsProp> = ({
renderTabBar={() => null}
onIndexChange={index => setSegment(index)}
navigationState={{ index: segment, routes }}
initialLayout={{ width: Dimensions.get('window').width }}
initialLayout={{ width: Dimensions.get('screen').width }}
/>
)
}

View File

@ -1,3 +1,4 @@
import ComponentHashtag from '@components/Timelines/Hashtag'
import { useNavigation } from '@react-navigation/native'
import ComponentAccount from '@root/components/Account'
import ComponentSeparator from '@root/components/Separator'
@ -9,7 +10,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
import {
KeyboardAvoidingView,
Platform,
Pressable,
SectionList,
StyleSheet,
Text,
@ -146,22 +146,25 @@ const ScreenSharedSearch: React.FC<Props> = ({ searchTerm }) => {
const listItem = useCallback(({ item, section, index }) => {
switch (section.title) {
case 'accounts':
return <ComponentAccount account={item} />
return (
<ComponentAccount
account={item}
onPress={() => {
navigation.push('Screen-Shared-Account', { item })
}}
/>
)
case 'hashtags':
return (
<Pressable
style={[styles.itemDefault, { borderBottomColor: theme.border }]}
<ComponentHashtag
tag={item}
onPress={() => {
navigation.goBack()
navigation.push('Screen-Shared-Hashtag', {
hashtag: item.name
})
}}
>
<Text style={[styles.itemHashtag, { color: theme.primary }]}>
#{item.name}
</Text>
</Pressable>
/>
)
case 'statuses':
return <TimelineDefault item={item} disableDetails />
@ -225,13 +228,6 @@ const styles = StyleSheet.create({
sectionFooterText: {
...StyleConstants.FontStyle.S,
textAlign: 'center'
},
itemDefault: {
padding: StyleConstants.Spacing.S * 1.5,
borderBottomWidth: StyleSheet.hairlineWidth
},
itemHashtag: {
...StyleConstants.FontStyle.M
}
})

View File

@ -1,4 +1,5 @@
import { HeaderLeft } from '@components/Header'
import { ParseEmojis } from '@components/Parse'
import { StackNavigationState, TypedNavigator } from '@react-navigation/native'
import { StackScreenProps } from '@react-navigation/stack'
import ScreenSharedAccount from '@screens/Shared/Account'
@ -21,6 +22,7 @@ import {
NativeStackNavigationEventMap,
NativeStackNavigatorProps
} from 'react-native-screens/lib/typescript/types'
import ScreenSharedAttachments from './Attachments'
export type BaseScreens =
| Nav.LocalStackParamList
@ -38,6 +40,11 @@ export type SharedAnnouncementsProp = StackScreenProps<
'Screen-Shared-Announcements'
>
export type SharedAttachmentsProp = StackScreenProps<
BaseScreens,
'Screen-Shared-Attachments'
>
export type SharedComposeProp = StackScreenProps<
BaseScreens,
'Screen-Shared-Compose'
@ -58,6 +65,11 @@ export type SharedRelationshipsProp = StackScreenProps<
'Screen-Shared-Relationships'
>
export type SharedSearchProp = StackScreenProps<
BaseScreens,
'Screen-Shared-Search'
>
export type SharedTootProp = StackScreenProps<BaseScreens, 'Screen-Shared-Toot'>
const sharedScreens = (
@ -111,6 +123,40 @@ const sharedScreens = (
headerShown: false
}}
/>,
<Stack.Screen
key='Screen-Shared-Attachments'
name='Screen-Shared-Attachments'
component={ScreenSharedAttachments}
options={({
route: {
params: { account }
},
navigation
}: SharedAttachmentsProp) => {
return {
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />,
headerCenter: () => (
<Text numberOfLines={1}>
<ParseEmojis
content={account.display_name || account.username}
emojis={account.emojis}
fontBold
/>
<Text
style={{
...StyleConstants.FontStyle.M,
color: theme.primary,
fontWeight: StyleConstants.Font.Weight.Bold
}}
>
{' '}
</Text>
</Text>
)
}
}}
/>,
<Stack.Screen
key='Screen-Shared-Compose'
name='Screen-Shared-Compose'
@ -124,7 +170,7 @@ const sharedScreens = (
key='Screen-Shared-Hashtag'
name='Screen-Shared-Hashtag'
component={ScreenSharedHashtag}
options={({ route, navigation }: any) => ({
options={({ route, navigation }: SharedHashtagProp) => ({
title: `#${decodeURIComponent(route.params.hashtag)}`,
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
})}
@ -143,15 +189,14 @@ const sharedScreens = (
key='Screen-Shared-Relationships'
name='Screen-Shared-Relationships'
component={ScreenSharedRelationships}
options={({ route, navigation }: any) => ({
title: route.params.account.display_name || route.params.account.name,
options={({ navigation }: SharedRelationshipsProp) => ({
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
})}
/>,
<Stack.Screen
key='Screen-Shared-Search'
name='Screen-Shared-Search'
options={({ navigation }: any) => ({
options={({ navigation }: SharedSearchProp) => ({
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />,
// https://github.com/react-navigation/react-navigation/issues/6746#issuecomment-583897436
headerCenter: () => (
@ -191,7 +236,7 @@ const sharedScreens = (
key='Screen-Shared-Toot'
name='Screen-Shared-Toot'
component={ScreenSharedToot}
options={({ navigation }: any) => ({
options={({ navigation }: SharedTootProp) => ({
title: t('sharedToot:heading'),
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
})}

View File

@ -6,12 +6,12 @@ const dev = () => {
if (__DEV__) {
Analytics.setDebugModeEnabled(true)
// log('log', 'devs', 'initializing wdyr')
// const whyDidYouRender = require('@welldone-software/why-did-you-render')
// whyDidYouRender(React, {
// trackHooks: true,
// hotReloadBufferMs: 1000
// })
log('log', 'devs', 'initializing wdyr')
const whyDidYouRender = require('@welldone-software/why-did-you-render')
whyDidYouRender(React, {
trackHooks: true,
hotReloadBufferMs: 1000
})
}
}

View File

@ -140,7 +140,7 @@ const queryFunction = ({
params
})
case 'Account_Media':
case 'Account_Attachments':
return client<Mastodon.Status[]>({
method: 'get',
instance: 'local',