mirror of https://github.com/tooot-app/app
parent
a3335a1f88
commit
3427b613aa
|
@ -44,6 +44,7 @@
|
|||
"react-native-screens": "~2.15.0",
|
||||
"react-native-shimmer-placeholder": "^2.0.6",
|
||||
"react-native-svg": "12.1.0",
|
||||
"react-native-tab-view": "^2.15.2",
|
||||
"react-native-toast-message": "^1.3.4",
|
||||
"react-native-webview": "11.0.0",
|
||||
"react-navigation": "^4.4.3",
|
||||
|
@ -70,4 +71,4 @@
|
|||
"typescript": "~4.0.0"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useCallback, useState } from 'react'
|
||||
import { Pressable, Text } from 'react-native'
|
||||
import { Pressable, Text, View } from 'react-native'
|
||||
import HTMLView from 'react-native-htmlview'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
|
@ -98,6 +98,12 @@ const renderNode = ({
|
|||
</Text>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (node.name === 'p') {
|
||||
if (!node.children.length) {
|
||||
return <View key={index}></View> // bug when the tag is empty
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -134,26 +140,24 @@ const ParseContent: React.FC<Props> = ({
|
|||
}),
|
||||
[]
|
||||
)
|
||||
const textComponent = useCallback(
|
||||
({ children }) =>
|
||||
emojis && children ? (
|
||||
<Emojis
|
||||
content={children.toString()}
|
||||
emojis={emojis}
|
||||
size={StyleConstants.Font.Size[size]}
|
||||
/>
|
||||
) : (
|
||||
<Text>{children}</Text>
|
||||
),
|
||||
[]
|
||||
)
|
||||
const textComponent = useCallback(({ children }) => {
|
||||
return emojis && children ? (
|
||||
<Emojis
|
||||
content={children.toString()}
|
||||
emojis={emojis}
|
||||
size={StyleConstants.Font.Size[size]}
|
||||
/>
|
||||
) : (
|
||||
<Text>{children}</Text>
|
||||
)
|
||||
}, [])
|
||||
const rootComponent = useCallback(({ children }) => {
|
||||
const { theme } = useTheme()
|
||||
const [textLoaded, setTextLoaded] = useState(false)
|
||||
const [totalLines, setTotalLines] = useState<number | undefined>()
|
||||
const [lineHeight, setLineHeight] = useState<number | undefined>()
|
||||
const [shownLines, setShownLines] = useState(numberOfLines)
|
||||
|
||||
// console.log(children)
|
||||
return (
|
||||
<>
|
||||
<Text
|
||||
|
|
|
@ -39,7 +39,7 @@ const fireMutation = async ({
|
|||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
content: '隐藏域名失败,请重试',
|
||||
content: '投票失败,请重试',
|
||||
autoHide: false
|
||||
})
|
||||
return Promise.reject()
|
||||
|
@ -104,7 +104,7 @@ const TimelinePoll: React.FC<Props> = ({ queryKey, status: { poll } }) => {
|
|||
|
||||
const pollExpiration = useMemo(() => {
|
||||
// how many voted
|
||||
if (poll.expired) {
|
||||
if (poll!.expired) {
|
||||
return (
|
||||
<Text style={[styles.expiration, { color: theme.secondary }]}>
|
||||
投票已结束
|
||||
|
@ -113,20 +113,20 @@ const TimelinePoll: React.FC<Props> = ({ queryKey, status: { poll } }) => {
|
|||
} else {
|
||||
return (
|
||||
<Text style={[styles.expiration, { color: theme.secondary }]}>
|
||||
截止至{relativeTime(poll.expires_at)}
|
||||
{relativeTime(poll!.expires_at)}截止
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [singleOptions, setSingleOptions] = useState({
|
||||
...[false, false, false, false].slice(0, poll.options.length)
|
||||
...[false, false, false, false].slice(0, poll!.options.length)
|
||||
})
|
||||
const [multipleOptions, setMultipleOptions] = useState({
|
||||
...[false, false, false, false].slice(0, poll.options.length)
|
||||
...[false, false, false, false].slice(0, poll!.options.length)
|
||||
})
|
||||
const isSelected = (index: number) => {
|
||||
if (poll.multiple) {
|
||||
if (poll!.multiple) {
|
||||
return multipleOptions[index] ? 'check-square' : 'square'
|
||||
} else {
|
||||
return singleOptions[index] ? 'check-circle' : 'circle'
|
||||
|
@ -135,28 +135,28 @@ const TimelinePoll: React.FC<Props> = ({ queryKey, status: { poll } }) => {
|
|||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
{poll.options.map((option, index) =>
|
||||
poll.voted ? (
|
||||
{poll!.options.map((option, index) =>
|
||||
poll!.voted ? (
|
||||
<View key={index} style={styles.poll}>
|
||||
<View style={styles.optionSelected}>
|
||||
<View style={styles.contentSelected}>
|
||||
<Emojis
|
||||
content={option.title}
|
||||
emojis={poll.emojis}
|
||||
emojis={poll!.emojis}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
numberOfLines={1}
|
||||
/>
|
||||
{poll.own_votes!.includes(index) && (
|
||||
{poll!.own_votes!.includes(index) && (
|
||||
<Feather
|
||||
style={styles.voted}
|
||||
name={poll.multiple ? 'check-square' : 'check-circle'}
|
||||
name={poll!.multiple ? 'check-square' : 'check-circle'}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
color={theme.primary}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<Text style={[styles.percentage, { color: theme.primary }]}>
|
||||
{Math.round((option.votes_count / poll.votes_count) * 100)}%
|
||||
{Math.round((option.votes_count / poll!.votes_count) * 100)}%
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
@ -165,7 +165,7 @@ const TimelinePoll: React.FC<Props> = ({ queryKey, status: { poll } }) => {
|
|||
styles.background,
|
||||
{
|
||||
width: `${Math.round(
|
||||
(option.votes_count / poll.votes_count) * 100
|
||||
(option.votes_count / poll!.votes_count) * 100
|
||||
)}%`,
|
||||
backgroundColor: theme.border
|
||||
}
|
||||
|
@ -177,7 +177,7 @@ const TimelinePoll: React.FC<Props> = ({ queryKey, status: { poll } }) => {
|
|||
<Pressable
|
||||
style={[styles.optionUnselected]}
|
||||
onPress={() => {
|
||||
if (poll.multiple) {
|
||||
if (poll!.multiple) {
|
||||
setMultipleOptions({
|
||||
...multipleOptions,
|
||||
[index]: !multipleOptions[index]
|
||||
|
@ -189,7 +189,7 @@ const TimelinePoll: React.FC<Props> = ({ queryKey, status: { poll } }) => {
|
|||
index === 1,
|
||||
index === 2,
|
||||
index === 3
|
||||
].slice(0, poll.options.length)
|
||||
].slice(0, poll!.options.length)
|
||||
})
|
||||
}
|
||||
}}
|
||||
|
@ -203,7 +203,7 @@ const TimelinePoll: React.FC<Props> = ({ queryKey, status: { poll } }) => {
|
|||
<View style={styles.contentUnselected}>
|
||||
<Emojis
|
||||
content={option.title}
|
||||
emojis={poll.emojis}
|
||||
emojis={poll!.emojis}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
/>
|
||||
</View>
|
||||
|
@ -227,7 +227,7 @@ const TimelinePoll: React.FC<Props> = ({ queryKey, status: { poll } }) => {
|
|||
</View>
|
||||
)}
|
||||
<Text style={[styles.votes, { color: theme.secondary }]}>
|
||||
已投{poll.voters_count}人{' • '}
|
||||
已投{poll.voters_count || 0}人{' • '}
|
||||
</Text>
|
||||
{pollExpiration}
|
||||
</View>
|
||||
|
|
|
@ -21,9 +21,10 @@ import { useTranslation } from 'react-i18next'
|
|||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { ButtonRow } from '@components/Button'
|
||||
import ParseContent from '@root/components/ParseContent'
|
||||
import ShimmerPlaceholder from 'react-native-shimmer-placeholder'
|
||||
import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import { applicationFetch } from '@root/utils/fetches/applicationFetch'
|
||||
import { LinearGradient } from 'expo-linear-gradient'
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const { t } = useTranslation('meRoot')
|
||||
|
@ -36,6 +37,8 @@ const Login: React.FC = () => {
|
|||
clientSecret: string
|
||||
}>()
|
||||
|
||||
const ShimmerPlaceholder = createShimmerPlaceholder(LinearGradient)
|
||||
|
||||
const instanceQuery = useQuery(
|
||||
['Instance', { instanceDomain }],
|
||||
instanceFetch,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react'
|
||||
import { ScrollView } from 'react-native'
|
||||
import React, { createContext, Dispatch, useReducer, useRef } from 'react'
|
||||
import { Animated, ScrollView } from 'react-native'
|
||||
|
||||
// import * as relationshipsSlice from 'src/stacks/common/relationshipsSlice'
|
||||
|
||||
|
@ -8,6 +8,8 @@ import { accountFetch } from '@utils/fetches/accountFetch'
|
|||
import AccountToots from '@screens/Shared/Account/Toots'
|
||||
import AccountHeader from '@screens/Shared/Account/Header'
|
||||
import AccountInformation from '@screens/Shared/Account/Information'
|
||||
import AccountNav from './Account/Nav'
|
||||
import AccountSegmentedControl from './Account/SegmentedControl'
|
||||
|
||||
// Moved account example: https://m.cmx.im/web/accounts/27812
|
||||
|
||||
|
@ -19,6 +21,53 @@ export interface Props {
|
|||
}
|
||||
}
|
||||
|
||||
export type AccountState = {
|
||||
headerRatio: number
|
||||
informationLayout?: {
|
||||
y: number
|
||||
height: number
|
||||
}
|
||||
segmentedIndex: number
|
||||
}
|
||||
export type AccountAction =
|
||||
| {
|
||||
type: 'headerRatio'
|
||||
payload: AccountState['headerRatio']
|
||||
}
|
||||
| {
|
||||
type: 'informationLayout'
|
||||
payload: AccountState['informationLayout']
|
||||
}
|
||||
| {
|
||||
type: 'segmentedIndex'
|
||||
payload: AccountState['segmentedIndex']
|
||||
}
|
||||
const AccountInitialState: AccountState = {
|
||||
headerRatio: 0.4,
|
||||
informationLayout: { height: 0, y: 100 },
|
||||
segmentedIndex: 0
|
||||
}
|
||||
const accountReducer = (
|
||||
state: AccountState,
|
||||
action: AccountAction
|
||||
): AccountState => {
|
||||
switch (action.type) {
|
||||
case 'headerRatio':
|
||||
return { ...state, headerRatio: action.payload }
|
||||
case 'informationLayout':
|
||||
return { ...state, informationLayout: action.payload }
|
||||
case 'segmentedIndex':
|
||||
return { ...state, segmentedIndex: action.payload }
|
||||
default:
|
||||
throw new Error('Unexpected action')
|
||||
}
|
||||
}
|
||||
type ContextType = {
|
||||
accountState: AccountState
|
||||
accountDispatch: Dispatch<AccountAction>
|
||||
}
|
||||
export const AccountContext = createContext<ContextType>({} as ContextType)
|
||||
|
||||
const ScreenSharedAccount: React.FC<Props> = ({
|
||||
route: {
|
||||
params: { id }
|
||||
|
@ -27,13 +76,30 @@ const ScreenSharedAccount: React.FC<Props> = ({
|
|||
const { data } = useQuery(['Account', { id }], accountFetch)
|
||||
|
||||
// const stateRelationships = useSelector(relationshipsState)
|
||||
const scrollY = useRef(new Animated.Value(0)).current
|
||||
const [accountState, accountDispatch] = useReducer(
|
||||
accountReducer,
|
||||
AccountInitialState
|
||||
)
|
||||
|
||||
return (
|
||||
<ScrollView bounces={false}>
|
||||
<AccountHeader uri={data?.header} />
|
||||
<AccountInformation account={data} />
|
||||
<AccountToots id={id} />
|
||||
</ScrollView>
|
||||
<AccountContext.Provider value={{ accountState, accountDispatch }}>
|
||||
<AccountNav scrollY={scrollY} account={data} />
|
||||
<AccountSegmentedControl scrollY={scrollY} />
|
||||
<ScrollView
|
||||
contentContainerStyle={{ zIndex: 99 }}
|
||||
bounces={false}
|
||||
onScroll={Animated.event(
|
||||
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
|
||||
{ useNativeDriver: false }
|
||||
)}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
<AccountHeader uri={data?.header} />
|
||||
<AccountInformation account={data} />
|
||||
<AccountToots id={id} />
|
||||
</ScrollView>
|
||||
</AccountContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,37 +1,44 @@
|
|||
import React, { useEffect, useRef } from 'react'
|
||||
import React, { useContext, useEffect, useRef } from 'react'
|
||||
import { Animated, Dimensions, Image, StyleSheet } from 'react-native'
|
||||
import { AccountContext } from '../Account'
|
||||
|
||||
export interface Props {
|
||||
uri?: Mastodon.Account['header']
|
||||
limitHeight?: boolean
|
||||
}
|
||||
|
||||
const limitRatio = 0.4
|
||||
|
||||
const AccountHeader: React.FC<Props> = ({ uri, limitHeight = false }) => {
|
||||
const { accountState, accountDispatch } = useContext(AccountContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (uri) {
|
||||
if (uri.includes('/headers/original/missing.png')) {
|
||||
animateNewSize(limitRatio)
|
||||
animateNewSize(accountState.headerRatio)
|
||||
} else {
|
||||
Image.getSize(
|
||||
uri,
|
||||
(width, height) => {
|
||||
animateNewSize(limitHeight ? limitRatio : height / width)
|
||||
if (!limitHeight) {
|
||||
accountDispatch({ type: 'headerRatio', payload: height / width })
|
||||
}
|
||||
animateNewSize(
|
||||
limitHeight ? accountState.headerRatio : height / width
|
||||
)
|
||||
},
|
||||
() => {
|
||||
animateNewSize(limitRatio)
|
||||
animateNewSize(accountState.headerRatio)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
animateNewSize(limitRatio)
|
||||
animateNewSize(accountState.headerRatio)
|
||||
}
|
||||
}, [uri])
|
||||
|
||||
const windowWidth = Dimensions.get('window').width
|
||||
const imageHeight = useRef(new Animated.Value(windowWidth * limitRatio))
|
||||
.current
|
||||
const imageHeight = useRef(
|
||||
new Animated.Value(windowWidth * accountState.headerRatio)
|
||||
).current
|
||||
const animateNewSize = (ratio: number) => {
|
||||
Animated.timing(imageHeight, {
|
||||
toValue: windowWidth * ratio,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { createRef, useEffect, useState } from 'react'
|
||||
import React, { createRef, useContext, useEffect, useState } from 'react'
|
||||
import { Animated, Image, StyleSheet, Text, View } from 'react-native'
|
||||
import ShimmerPlaceholder from 'react-native-shimmer-placeholder'
|
||||
import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
|
||||
import ParseContent from '@components/ParseContent'
|
||||
|
@ -8,23 +8,27 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
|||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Emojis from '@components/Timelines/Timeline/Shared/Emojis'
|
||||
import { LinearGradient } from 'expo-linear-gradient'
|
||||
import { AccountContext } from '../Account'
|
||||
|
||||
export interface Props {
|
||||
account: Mastodon.Account | undefined
|
||||
}
|
||||
|
||||
const AccountInformation: React.FC<Props> = ({ account }) => {
|
||||
const { accountDispatch } = useContext(AccountContext)
|
||||
const { t } = useTranslation('sharedAccount')
|
||||
const { theme } = useTheme()
|
||||
const [avatarLoaded, setAvatarLoaded] = useState(false)
|
||||
|
||||
const shimmerAvatarRef = createRef<ShimmerPlaceholder>()
|
||||
const shimmerNameRef = createRef<ShimmerPlaceholder>()
|
||||
const shimmerAccountRef = createRef<ShimmerPlaceholder>()
|
||||
const shimmerCreatedRef = createRef<ShimmerPlaceholder>()
|
||||
const shimmerStatTootRef = createRef<ShimmerPlaceholder>()
|
||||
const shimmerStatFolloingRef = createRef<ShimmerPlaceholder>()
|
||||
const shimmerStatFollowerRef = createRef<ShimmerPlaceholder>()
|
||||
const ShimmerPlaceholder = createShimmerPlaceholder(LinearGradient)
|
||||
const shimmerAvatarRef = createRef<any>()
|
||||
const shimmerNameRef = createRef<any>()
|
||||
const shimmerAccountRef = createRef<any>()
|
||||
const shimmerCreatedRef = createRef<any>()
|
||||
const shimmerStatTootRef = createRef<any>()
|
||||
const shimmerStatFolloingRef = createRef<any>()
|
||||
const shimmerStatFollowerRef = createRef<any>()
|
||||
useEffect(() => {
|
||||
const informationAnimated = Animated.stagger(400, [
|
||||
Animated.parallel([
|
||||
|
@ -41,7 +45,18 @@ const AccountInformation: React.FC<Props> = ({ account }) => {
|
|||
}, [])
|
||||
|
||||
return (
|
||||
<View style={styles.information}>
|
||||
<View
|
||||
style={styles.information}
|
||||
onLayout={({ nativeEvent }) =>
|
||||
accountDispatch({
|
||||
type: 'informationLayout',
|
||||
payload: {
|
||||
y: nativeEvent.layout.y,
|
||||
height: nativeEvent.layout.height
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
{/* <Text>Moved or not: {account.moved}</Text> */}
|
||||
<ShimmerPlaceholder
|
||||
ref={shimmerAvatarRef}
|
||||
|
@ -246,7 +261,7 @@ const AccountInformation: React.FC<Props> = ({ account }) => {
|
|||
|
||||
const styles = StyleSheet.create({
|
||||
information: {
|
||||
marginTop: -30 - StyleConstants.Spacing.Global.PagePadding,
|
||||
marginTop: -StyleConstants.Spacing.Global.PagePadding * 3,
|
||||
padding: StyleConstants.Spacing.Global.PagePadding
|
||||
},
|
||||
avatar: {
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
import Emojis from '@root/components/Timelines/Timeline/Shared/Emojis'
|
||||
import { StyleConstants } from '@root/utils/styles/constants'
|
||||
import { useTheme } from '@root/utils/styles/ThemeManager'
|
||||
import React, { useContext } from 'react'
|
||||
import { Animated, Dimensions, StyleSheet, Text, View } from 'react-native'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { AccountContext } from '../Account'
|
||||
|
||||
export interface Props {
|
||||
scrollY: Animated.Value
|
||||
account: Mastodon.Account | undefined
|
||||
}
|
||||
|
||||
const AccountNav: React.FC<Props> = ({ scrollY, account }) => {
|
||||
const { accountState } = useContext(AccountContext)
|
||||
const { theme } = useTheme()
|
||||
const headerHeight = useSafeAreaInsets().top + 44
|
||||
|
||||
const nameY =
|
||||
Dimensions.get('screen').width * accountState.headerRatio +
|
||||
StyleConstants.Avatar.L -
|
||||
StyleConstants.Spacing.Global.PagePadding * 2 +
|
||||
StyleConstants.Spacing.M -
|
||||
headerHeight
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.base,
|
||||
{
|
||||
backgroundColor: theme.background,
|
||||
opacity: scrollY.interpolate({
|
||||
inputRange: [0, 200],
|
||||
outputRange: [0, 1],
|
||||
extrapolate: 'clamp'
|
||||
}),
|
||||
height: headerHeight
|
||||
}
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.content,
|
||||
{
|
||||
marginTop:
|
||||
useSafeAreaInsets().top + (44 - StyleConstants.Font.Size.L) / 2
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.display_name,
|
||||
{
|
||||
marginTop: scrollY.interpolate({
|
||||
inputRange: [nameY, nameY + 20],
|
||||
outputRange: [50, 0],
|
||||
extrapolate: 'clamp'
|
||||
})
|
||||
}
|
||||
]}
|
||||
>
|
||||
{account?.emojis ? (
|
||||
<Emojis
|
||||
content={account?.display_name || account?.username}
|
||||
emojis={account.emojis}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
fontBold={true}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={{
|
||||
color: theme.primary,
|
||||
fontSize: StyleConstants.Font.Size.L,
|
||||
fontWeight: StyleConstants.Font.Weight.Bold
|
||||
}}
|
||||
>
|
||||
{account?.display_name || account?.username}
|
||||
</Text>
|
||||
)}
|
||||
</Animated.View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
zIndex: 99
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
display_name: {
|
||||
flexDirection: 'row'
|
||||
}
|
||||
})
|
||||
|
||||
export default AccountNav
|
|
@ -0,0 +1,81 @@
|
|||
import SegmentedControl from '@react-native-community/segmented-control'
|
||||
import { StyleConstants } from '@root/utils/styles/constants'
|
||||
import { useTheme } from '@root/utils/styles/ThemeManager'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Animated, StyleSheet } from 'react-native'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { AccountContext } from '../Account'
|
||||
|
||||
export interface Props {
|
||||
scrollY: Animated.Value
|
||||
}
|
||||
|
||||
const AccountSegmentedControl: React.FC<Props> = ({ scrollY }) => {
|
||||
const { accountState, accountDispatch } = useContext(AccountContext)
|
||||
const { t } = useTranslation('sharedAccount')
|
||||
const { mode, theme } = useTheme()
|
||||
|
||||
const headerHeight = useSafeAreaInsets().top + 44
|
||||
const translateY = scrollY.interpolate({
|
||||
inputRange: [
|
||||
0,
|
||||
(accountState.informationLayout?.y || 0) +
|
||||
(accountState.informationLayout?.height || 0) -
|
||||
headerHeight
|
||||
],
|
||||
outputRange: [
|
||||
0,
|
||||
-(accountState.informationLayout?.y || 0) -
|
||||
(accountState.informationLayout?.height || 0) +
|
||||
headerHeight
|
||||
],
|
||||
extrapolate: 'clamp'
|
||||
})
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.base,
|
||||
{
|
||||
top:
|
||||
(accountState.informationLayout?.y || 0) +
|
||||
(accountState.informationLayout?.height || 0),
|
||||
transform: [{ translateY }],
|
||||
borderTopColor: theme.border,
|
||||
backgroundColor: theme.background
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SegmentedControl
|
||||
values={[
|
||||
t('content.segments.left'),
|
||||
t('content.segments.middle'),
|
||||
t('content.segments.right')
|
||||
]}
|
||||
selectedIndex={accountState.segmentedIndex}
|
||||
onChange={({ nativeEvent }) =>
|
||||
accountDispatch({
|
||||
type: 'segmentedIndex',
|
||||
payload: nativeEvent.selectedSegmentIndex
|
||||
})
|
||||
}
|
||||
appearance={mode}
|
||||
/>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 99,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
padding: StyleConstants.Spacing.Global.PagePadding
|
||||
}
|
||||
})
|
||||
|
||||
export default AccountSegmentedControl
|
|
@ -1,87 +1,60 @@
|
|||
import React, { useRef, useState } from 'react'
|
||||
import { Dimensions, FlatList, View } from 'react-native'
|
||||
import SegmentedControl from '@react-native-community/segmented-control'
|
||||
import React, { useCallback, useContext } from 'react'
|
||||
import { Dimensions, StyleSheet } from 'react-native'
|
||||
import { TabView, SceneMap } from 'react-native-tab-view'
|
||||
|
||||
import Timeline from '@components/Timelines/Timeline'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AccountContext } from '../Account'
|
||||
import { StyleConstants } from '@root/utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
id: Mastodon.Account['id']
|
||||
}
|
||||
|
||||
const AccountToots: React.FC<Props> = ({ id }) => {
|
||||
const { t } = useTranslation('sharedAccount')
|
||||
const [segment, setSegment] = useState(0)
|
||||
const [segmentManuallyTriggered, setSegmentManuallyTriggered] = useState(
|
||||
false
|
||||
)
|
||||
const horizontalPaging = useRef<any>()
|
||||
const { accountState, accountDispatch } = useContext(AccountContext)
|
||||
|
||||
const pages: ['Account_Default', 'Account_All', 'Account_Media'] = [
|
||||
'Account_Default',
|
||||
'Account_All',
|
||||
'Account_Media'
|
||||
]
|
||||
const [routes] = React.useState([
|
||||
{ key: 'Account_Default' },
|
||||
{ key: 'Account_All' },
|
||||
{ key: 'Account_Media' }
|
||||
])
|
||||
const singleScene = useCallback(
|
||||
({ route }) => (
|
||||
<Timeline
|
||||
page={route.key}
|
||||
account={id}
|
||||
disableRefresh
|
||||
scrollEnabled={false}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
const renderScene = SceneMap({
|
||||
Account_Default: singleScene,
|
||||
Account_All: singleScene,
|
||||
Account_Media: singleScene
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<SegmentedControl
|
||||
values={[
|
||||
t('content.segments.left'),
|
||||
t('content.segments.middle'),
|
||||
t('content.segments.right')
|
||||
]}
|
||||
selectedIndex={segment}
|
||||
onChange={({ nativeEvent }) => {
|
||||
setSegmentManuallyTriggered(true)
|
||||
setSegment(nativeEvent.selectedSegmentIndex)
|
||||
horizontalPaging.current.scrollToIndex({
|
||||
index: nativeEvent.selectedSegmentIndex
|
||||
})
|
||||
}}
|
||||
style={{ width: '100%', height: 30 }}
|
||||
/>
|
||||
<FlatList
|
||||
style={{ width: Dimensions.get('window').width, height: '100%' }}
|
||||
data={pages}
|
||||
keyExtractor={page => page}
|
||||
renderItem={({ item, index }) => {
|
||||
return (
|
||||
<View style={{ width: Dimensions.get('window').width }}>
|
||||
<Timeline
|
||||
key={index}
|
||||
page={item}
|
||||
account={id}
|
||||
disableRefresh
|
||||
scrollEnabled={false}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}}
|
||||
ref={horizontalPaging}
|
||||
bounces={false}
|
||||
getItemLayout={(data, index) => ({
|
||||
length: Dimensions.get('window').width,
|
||||
offset: Dimensions.get('window').width * index,
|
||||
index
|
||||
})}
|
||||
horizontal
|
||||
onMomentumScrollEnd={() => {
|
||||
setSegmentManuallyTriggered(false)
|
||||
}}
|
||||
onScroll={({ nativeEvent }) =>
|
||||
!segmentManuallyTriggered &&
|
||||
setSegment(
|
||||
nativeEvent.contentOffset.x <= Dimensions.get('window').width / 3
|
||||
? 0
|
||||
: 1
|
||||
)
|
||||
}
|
||||
pagingEnabled
|
||||
showsHorizontalScrollIndicator={false}
|
||||
/>
|
||||
</>
|
||||
<TabView
|
||||
style={styles.base}
|
||||
navigationState={{ index: accountState.segmentedIndex, routes }}
|
||||
renderScene={renderScene}
|
||||
renderTabBar={() => null}
|
||||
onIndexChange={index =>
|
||||
accountDispatch({ type: 'segmentedIndex', payload: index })
|
||||
}
|
||||
initialLayout={{ width: Dimensions.get('window').width }}
|
||||
lazy
|
||||
swipeEnabled
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
marginTop: StyleConstants.Spacing.Global.PagePadding + 33
|
||||
}
|
||||
})
|
||||
|
||||
export default AccountToots
|
||||
|
|
|
@ -90,7 +90,7 @@ export type ComposeState = {
|
|||
}
|
||||
}
|
||||
|
||||
export type PostAction =
|
||||
export type ComposeAction =
|
||||
| {
|
||||
type: 'spoiler'
|
||||
payload: Partial<ComposeState['spoiler']>
|
||||
|
@ -251,7 +251,10 @@ const composeExistingState = ({
|
|||
}
|
||||
}
|
||||
}
|
||||
const postReducer = (state: ComposeState, action: PostAction): ComposeState => {
|
||||
const composeReducer = (
|
||||
state: ComposeState,
|
||||
action: ComposeAction
|
||||
): ComposeState => {
|
||||
switch (action.type) {
|
||||
case 'spoiler':
|
||||
return { ...state, spoiler: { ...state.spoiler, ...action.payload } }
|
||||
|
@ -294,7 +297,7 @@ const postReducer = (state: ComposeState, action: PostAction): ComposeState => {
|
|||
|
||||
type ContextType = {
|
||||
composeState: ComposeState
|
||||
composeDispatch: Dispatch<PostAction>
|
||||
composeDispatch: Dispatch<ComposeAction>
|
||||
}
|
||||
export const ComposeContext = createContext<ContextType>({} as ContextType)
|
||||
|
||||
|
@ -332,7 +335,7 @@ const Compose: React.FC<Props> = ({ route: { params }, navigation }) => {
|
|||
}
|
||||
|
||||
const [composeState, composeDispatch] = useReducer(
|
||||
postReducer,
|
||||
composeReducer,
|
||||
params?.type && params?.incomingStatus
|
||||
? composeExistingState({
|
||||
type: params.type,
|
||||
|
|
|
@ -12,10 +12,11 @@ import { ComposeContext } from '@screens/Shared/Compose'
|
|||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import ShimmerPlaceholder from 'react-native-shimmer-placeholder'
|
||||
import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder'
|
||||
import { ButtonRound } from '@components/Button'
|
||||
import addAttachments from '@screens/Shared/Compose/addAttachments'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import { LinearGradient } from 'expo-linear-gradient'
|
||||
|
||||
const DEFAULT_HEIGHT = 200
|
||||
|
||||
|
@ -97,6 +98,8 @@ const ComposeAttachments: React.FC = () => {
|
|||
)
|
||||
|
||||
const listFooter = useCallback(() => {
|
||||
const ShimmerPlaceholder = createShimmerPlaceholder(LinearGradient)
|
||||
|
||||
return (
|
||||
<ShimmerPlaceholder
|
||||
style={styles.progressContainer}
|
||||
|
|
|
@ -18,14 +18,21 @@ const sharedScreens = (Stack: any) => {
|
|||
key='Screen-Shared-Account'
|
||||
name='Screen-Shared-Account'
|
||||
component={ScreenSharedAccount}
|
||||
options={({ navigation }: any) => ({
|
||||
headerTranslucent: true,
|
||||
headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' },
|
||||
headerCenter: () => null,
|
||||
headerLeft: () => (
|
||||
<HeaderLeft icon='chevron-left' onPress={() => navigation.goBack()} />
|
||||
)
|
||||
})}
|
||||
options={({ navigation }: any) => {
|
||||
return {
|
||||
headerTranslucent: true,
|
||||
headerStyle: {
|
||||
backgroundColor: `rgba(255, 255, 255, 0)`
|
||||
},
|
||||
headerCenter: () => null,
|
||||
headerLeft: () => (
|
||||
<HeaderLeft
|
||||
icon='chevron-left'
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
<Stack.Screen
|
||||
key='Screen-Shared-Hashtag'
|
||||
|
|
|
@ -91,26 +91,53 @@ export const timelineFetch = async (
|
|||
return Promise.resolve({ toots: res.body })
|
||||
|
||||
case 'Account_Default':
|
||||
res = await client({
|
||||
method: 'get',
|
||||
instance: 'local',
|
||||
url: `accounts/${account}/statuses`,
|
||||
params: {
|
||||
pinned: 'true'
|
||||
if (pagination && pagination.id) {
|
||||
if (pagination.direction === 'prev') {
|
||||
res = await client({
|
||||
method: 'get',
|
||||
instance: 'local',
|
||||
url: `accounts/${account}/statuses`,
|
||||
params: {
|
||||
pinned: 'true',
|
||||
...params
|
||||
}
|
||||
})
|
||||
return Promise.resolve({ toots: res.body })
|
||||
} else {
|
||||
res = await client({
|
||||
method: 'get',
|
||||
instance: 'local',
|
||||
url: `accounts/${account}/statuses`,
|
||||
params: {
|
||||
exclude_replies: 'true',
|
||||
...params
|
||||
}
|
||||
})
|
||||
return Promise.resolve({ toots: res.body })
|
||||
}
|
||||
})
|
||||
const pinnedLength = res.body.length
|
||||
let toots: Mastodon.Status[] = res.body
|
||||
res = await client({
|
||||
method: 'get',
|
||||
instance: 'local',
|
||||
url: `accounts/${account}/statuses`,
|
||||
params: {
|
||||
exclude_replies: 'true'
|
||||
}
|
||||
})
|
||||
toots = uniqBy([...toots, ...res.body], 'id')
|
||||
return Promise.resolve({ toots: toots, pinnedLength })
|
||||
} else {
|
||||
res = await client({
|
||||
method: 'get',
|
||||
instance: 'local',
|
||||
url: `accounts/${account}/statuses`,
|
||||
params: {
|
||||
pinned: 'true'
|
||||
}
|
||||
})
|
||||
const pinnedLength = res.body.length
|
||||
let toots: Mastodon.Status[] = res.body
|
||||
res = await client({
|
||||
method: 'get',
|
||||
instance: 'local',
|
||||
url: `accounts/${account}/statuses`,
|
||||
params: {
|
||||
exclude_replies: 'true'
|
||||
}
|
||||
})
|
||||
toots = uniqBy([...toots, ...res.body], 'id')
|
||||
return Promise.resolve({ toots: toots, pinnedLength })
|
||||
}
|
||||
break
|
||||
|
||||
case 'Account_All':
|
||||
res = await client({
|
||||
|
|
14
yarn.lock
14
yarn.lock
|
@ -2775,13 +2775,6 @@ expand-brackets@^2.1.4:
|
|||
snapdragon "^0.8.1"
|
||||
to-regex "^3.0.1"
|
||||
|
||||
expo-app-loading@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/expo-app-loading/-/expo-app-loading-1.0.1.tgz#a2ef73235c5dd0a99c0319ad9d3f4d278237564c"
|
||||
integrity sha512-qgsystchwMHep43YMAMHJAD5bRUikolQpb1d+ySWF4ZFTtNInmLAPqiBcVSey5taRuHD3cAvEijx3wc0W/HfVw==
|
||||
dependencies:
|
||||
expo-splash-screen "0.8.1"
|
||||
|
||||
expo-application@~2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/expo-application/-/expo-application-2.4.1.tgz#f8eb4a3a05a0b8a8f38f2e981f587a815945b685"
|
||||
|
@ -2915,7 +2908,7 @@ expo-secure-store@~9.3.0:
|
|||
resolved "https://registry.yarnpkg.com/expo-secure-store/-/expo-secure-store-9.3.0.tgz#b716d5d115cc50a34037d1afef84fe4b8ea0745c"
|
||||
integrity sha512-dNhKcoUUn+1kmEfFVxSU7h+YsqODqlExZQJcQgxgeiuCeeDvJWkE10t3jjrO6aNfrdM5i/X2l3oh401EDslWsQ==
|
||||
|
||||
expo-splash-screen@0.8.1, expo-splash-screen@~0.8.1:
|
||||
expo-splash-screen@~0.8.1:
|
||||
version "0.8.1"
|
||||
resolved "https://registry.yarnpkg.com/expo-splash-screen/-/expo-splash-screen-0.8.1.tgz#de4a018c82af879eeaa9b697013ef37d9567453a"
|
||||
integrity sha512-7NQo8OgkfQ4sv4mIHE58fkiQTGyl5dOP70uAUTb5YXC/QUsHIqEpKQ6ZXDpkLU1cbZ32c8Vtok4r2FRIUrzxcg==
|
||||
|
@ -5305,6 +5298,11 @@ react-native-svg@12.1.0:
|
|||
css-select "^2.1.0"
|
||||
css-tree "^1.0.0-alpha.39"
|
||||
|
||||
react-native-tab-view@^2.15.2:
|
||||
version "2.15.2"
|
||||
resolved "https://registry.yarnpkg.com/react-native-tab-view/-/react-native-tab-view-2.15.2.tgz#4bc7832d33a119306614efee667509672a7ee64e"
|
||||
integrity sha512-2hxLkBnZtEKFDyfvNO5EUywhy3f/EiLOBO8SWqKj4BMBTO0QwnybaPE5MVF00Fhz+VA4+h/iI40Dkrrtq70dGg==
|
||||
|
||||
react-native-toast-message@^1.3.4:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-toast-message/-/react-native-toast-message-1.4.1.tgz#8be7b91d8a5405e86179f83e52e56de58bbce25f"
|
||||
|
|
Loading…
Reference in New Issue