mirror of
https://github.com/tooot-app/app
synced 2025-03-27 17:00:12 +01:00
Updates
This commit is contained in:
parent
9f4a4e908c
commit
5ec9118fb2
2
src/@types/app.d.ts
vendored
2
src/@types/app.d.ts
vendored
@ -10,7 +10,7 @@ declare namespace App {
|
||||
| 'Toot'
|
||||
| 'Account_Default'
|
||||
| 'Account_All'
|
||||
| 'Account_Media'
|
||||
| 'Account_Attachments'
|
||||
| 'Conversations'
|
||||
| 'Bookmarks'
|
||||
| 'Favourites'
|
||||
|
1
src/@types/react-navigation.d.ts
vendored
1
src/@types/react-navigation.d.ts
vendored
@ -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'
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
171
src/components/GracefullyImage.tsx
Normal file
171
src/components/GracefullyImage.tsx
Normal 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
|
@ -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(
|
||||
() => (
|
||||
|
@ -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>
|
||||
|
36
src/components/Timelines/Hashtag.tsx
Normal file
36
src/components/Timelines/Hashtag.tsx
Normal 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
|
@ -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}
|
||||
/>
|
||||
)
|
||||
|
@ -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%',
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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 ? (
|
||||
|
@ -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)
|
||||
|
@ -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%',
|
||||
|
@ -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年',
|
||||
|
@ -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
|
||||
|
140
src/screens/Shared/Account/Attachments.tsx
Normal file
140
src/screens/Shared/Account/Attachments.tsx
Normal 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
|
@ -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])
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
|
13
src/screens/Shared/Attachments.tsx
Normal file
13
src/screens/Shared/Attachments.tsx
Normal 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
|
@ -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 />
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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()} />
|
||||
})}
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,7 +140,7 @@ const queryFunction = ({
|
||||
params
|
||||
})
|
||||
|
||||
case 'Account_Media':
|
||||
case 'Account_Attachments':
|
||||
return client<Mastodon.Status[]>({
|
||||
method: 'get',
|
||||
instance: 'local',
|
||||
|
Loading…
x
Reference in New Issue
Block a user