This commit is contained in:
xmflsct 2022-12-14 23:37:41 +01:00
parent 7f8a8de898
commit 4a35e910c1
23 changed files with 425 additions and 302 deletions

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

@ -8,9 +8,7 @@ declare namespace App {
| 'Hashtag'
| 'List'
| 'Toot'
| 'Account_Default'
| 'Account_All'
| 'Account_Attachments'
| 'Account'
| 'Conversations'
| 'Bookmarks'
| 'Favourites'

View File

@ -1,6 +1,4 @@
import Icon from '@components/Icon'
import CustomText from '@components/Text'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { View } from 'react-native'
@ -9,16 +7,10 @@ export interface Props {
content?: string
inverted?: boolean
onPress?: () => void
dropdown?: boolean
}
// Used for Android mostly
const HeaderCenter: React.FC<Props> = ({
content,
inverted = false,
onPress,
dropdown = false
}) => {
const HeaderCenter: React.FC<Props> = ({ content, inverted = false, onPress }) => {
const { colors } = useTheme()
return (
@ -33,13 +25,6 @@ const HeaderCenter: React.FC<Props> = ({
children={content}
{...(onPress && { onPress })}
/>
<Icon
name='ChevronDown'
size={StyleConstants.Font.Size.M}
color={colors.primaryDefault}
style={{ marginLeft: StyleConstants.Spacing.XS, opacity: dropdown ? undefined : 0 }}
strokeWidth={3}
/>
</View>
)
}

View File

@ -11,6 +11,7 @@ export interface Props {
fill?: string
strokeWidth?: number
style?: StyleProp<ViewStyle>
crossOut?: boolean
}
const Icon: React.FC<Props> = ({
@ -20,7 +21,8 @@ const Icon: React.FC<Props> = ({
color,
fill,
strokeWidth = 2,
style
style,
crossOut = false
}) => {
return (
<View
@ -42,6 +44,17 @@ const Icon: React.FC<Props> = ({
fill,
strokeWidth
})}
{crossOut ? (
<View
style={{
position: 'absolute',
transform: [{ rotate: '45deg' }],
width: size * 1.35,
borderBottomColor: color,
borderBottomWidth: strokeWidth
}}
/>
) : null}
</View>
)
}

View File

@ -196,7 +196,7 @@ const ParseHTML = React.memo(
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const route = useRoute()
const { colors, theme } = useTheme()
const { t, i18n } = useTranslation('componentParse')
const { t } = useTranslation('componentParse')
if (!expandHint) {
expandHint = t('HTML.defaultHint')
}
@ -304,7 +304,7 @@ const ParseHTML = React.memo(
</View>
)
},
[theme, i18n.language]
[theme]
)
return (

View File

@ -30,8 +30,8 @@ const RelationshipOutgoing = React.memo(
haptics('Success')
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res])
if (action === 'block') {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Following' }]
queryClient.invalidateQueries(queryKey)
const queryKey = ['Timeline', { page: 'Following' }]
queryClient.invalidateQueries({ queryKey, exact: false })
}
},
onError: (err: any, { payload: { action } }) => {

View File

@ -45,6 +45,7 @@ export const shouldFilter = ({
status: Mastodon.Status
queryKey: QueryKeyTimeline
}): string | null => {
const page = queryKey[1]
const instance = getInstance(store.getState())
const ownAccount = getInstanceAccount(store.getState())?.id === status.account?.id
@ -93,11 +94,11 @@ export const shouldFilter = ({
}
}
switch (queryKey[1].page) {
switch (page.page) {
case 'Following':
case 'Local':
case 'List':
case 'Account_Default':
case 'Account':
if (filter.context.includes('home')) {
checkFilter(filter)
}

View File

@ -1,5 +1,4 @@
import menuAccount from '@components/contextMenu/account'
import menuInstance from '@components/contextMenu/instance'
import menuShare from '@components/contextMenu/share'
import menuStatus from '@components/contextMenu/status'
import Icon from '@components/Icon'
@ -31,7 +30,6 @@ const TimelineHeaderAndroid: React.FC = () => {
queryKey
})
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
const mInstance = menuInstance({ status, queryKey, rootQueryKey })
return (
<View style={{ position: 'absolute', top: 0, right: 0 }}>
@ -77,16 +75,6 @@ const TimelineHeaderAndroid: React.FC = () => {
))}
</DropdownMenu.Group>
))}
{mInstance.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
) : null}

View File

@ -1,5 +1,4 @@
import menuAccount from '@components/contextMenu/account'
import menuInstance from '@components/contextMenu/instance'
import menuShare from '@components/contextMenu/share'
import menuStatus from '@components/contextMenu/status'
import Icon from '@components/Icon'
@ -38,7 +37,6 @@ const TimelineHeaderDefault: React.FC = () => {
queryKey
})
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
const mInstance = menuInstance({ status, queryKey, rootQueryKey })
return (
<View style={{ flex: 1, flexDirection: 'row' }}>
@ -116,17 +114,6 @@ const TimelineHeaderDefault: React.FC = () => {
))}
</DropdownMenu.Group>
))}
{mInstance.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
</Pressable>

View File

@ -33,7 +33,7 @@ const TimelinePoll: React.FC = () => {
const poll = status.poll
const { colors, theme } = useTheme()
const { t, i18n } = useTranslation('componentTimeline')
const { t } = useTranslation('componentTimeline')
const [allOptions, setAllOptions] = useState(new Array(status.poll.options.length).fill(false))
@ -127,7 +127,7 @@ const TimelinePoll: React.FC = () => {
)
}
}
}, [theme, i18n.language, poll.expired, poll.voted, allOptions, mutation.isLoading])
}, [theme, poll.expired, poll.voted, allOptions, mutation.isLoading])
const isSelected = useCallback(
(index: number): string =>

View File

@ -50,7 +50,7 @@ const menuAccount = ({
setEnabled(true)
}
}, [openChange, enabled])
const { data, isFetching } = useRelationshipQuery({ id: account.id, options: { enabled } })
const { data, isFetched } = useRelationshipQuery({ id: account.id, options: { enabled } })
const queryClient = useQueryClient()
const timelineMutation = useTimelineMutation({
@ -99,8 +99,8 @@ const menuAccount = ({
haptics('Success')
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res])
if (action === 'block') {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Following' }]
queryClient.invalidateQueries(queryKey)
const queryKey = ['Timeline', { page: 'Following' }]
queryClient.invalidateQueries({ queryKey, exact: false })
}
},
onError: (err: any, { payload: { action } }) => {
@ -131,7 +131,7 @@ const menuAccount = ({
type: 'outgoing',
payload: { action: 'follow', state: !data?.requested ? data.following : true }
}),
disabled: !data || isFetching,
disabled: !data || !isFetched,
destructive: false,
hidden: false
},
@ -152,9 +152,9 @@ const menuAccount = ({
key: 'account-list',
item: {
onSelect: () => navigation.navigate('Tab-Shared-Account-In-Lists', { account }),
disabled: Platform.OS !== 'android' ? !data || isFetching : false,
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
destructive: false,
hidden: isFetching ? false : !data?.following
hidden: !isFetched || !data?.following
},
title: t('account.inLists'),
icon: 'checklist'
@ -169,7 +169,7 @@ const menuAccount = ({
id: account.id,
payload: { property: 'mute', currentValue: data?.muting }
}),
disabled: Platform.OS !== 'android' ? !data || isFetching : false,
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
destructive: false,
hidden: false
},
@ -192,7 +192,7 @@ const menuAccount = ({
id: account.id,
payload: { property: 'block', currentValue: data?.blocking }
}),
disabled: Platform.OS !== 'android' ? !data || isFetching : false,
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
destructive: !data?.blocking,
hidden: false
},

View File

@ -1,7 +1,11 @@
{
"tabs": {
"local": {
"name": "Following"
"name": "Following",
"options": {
"showBoosts": "Show boosts",
"showReplies": "Show replies"
}
},
"public": {
"segments": {

View File

@ -1,4 +1,6 @@
import { HeaderCenter, HeaderRight } from '@components/Header'
import { HeaderRight } from '@components/Header'
import Icon from '@components/Icon'
import CustomText from '@components/Text'
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import { NativeStackScreenProps } from '@react-navigation/native-stack'
@ -6,66 +8,160 @@ import { TabLocalStackParamList } from '@utils/navigation/navigators'
import usePopToTop from '@utils/navigation/usePopToTop'
import { useListsQuery } from '@utils/queryHooks/lists'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstanceFollowingPage, updateInstanceFollowingPage } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { View } from 'react-native'
import { useDispatch, useSelector } from 'react-redux'
import * as DropdownMenu from 'zeego/dropdown-menu'
const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-Root'>> = ({
navigation
}) => {
const { colors, mode } = useTheme()
const { t } = useTranslation('screenTabs')
const { data: lists } = useListsQuery()
const [queryKey, setQueryKey] = useState<QueryKeyTimeline>(['Timeline', { page: 'Following' }])
const dispatch = useDispatch()
const instanceFollowingPage = useSelector(getInstanceFollowingPage)
const [queryKey, setQueryKey] = useState<QueryKeyTimeline>([
'Timeline',
{ page: 'Following', ...instanceFollowingPage }
])
useEffect(() => {
const page = queryKey[1]
navigation.setOptions({
headerTitle: () => (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<HeaderCenter
dropdown={(lists?.length ?? 0) > 0}
content={
queryKey[1].page === 'List' && queryKey[1].list?.length
? lists?.find(list => list.id === queryKey[1].list)?.title
: t('tabs.local.name')
}
/>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{page.page === 'List' ? (
<Icon
name='List'
size={StyleConstants.Font.Size.M}
color={colors.primaryDefault}
style={{ marginRight: StyleConstants.Spacing.S }}
/>
) : null}
<CustomText
style={{ color: colors.primaryDefault }}
fontSize='L'
fontWeight='Bold'
numberOfLines={1}
children={
page.page === 'List' && page.list?.length
? lists?.find(list => list.id === page.list)?.title
: t('tabs.local.name')
}
/>
{page.page === 'Following' && !instanceFollowingPage.showBoosts ? (
<Icon
name='MessageCircle'
size={StyleConstants.Font.Size.M}
color={colors.red}
style={{ marginLeft: StyleConstants.Spacing.S }}
crossOut
/>
) : null}
{page.page === 'Following' && !instanceFollowingPage.showReplies ? (
<Icon
name='Repeat'
size={StyleConstants.Font.Size.M}
color={colors.red}
style={{ marginLeft: StyleConstants.Spacing.S }}
crossOut
/>
) : null}
<Icon
name='ChevronDown'
size={StyleConstants.Font.Size.M}
color={colors.primaryDefault}
style={{ marginLeft: StyleConstants.Spacing.S }}
/>
</View>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{lists?.length
? [
{
key: 'default',
item: {
onSelect: () => setQueryKey(['Timeline', { page: 'Following' }]),
disabled: queryKey[1].page === 'Following',
destructive: false,
hidden: false
},
title: t('tabs.local.name'),
icon: ''
},
...lists?.map(list => ({
key: list.id,
item: {
onSelect: () => setQueryKey(['Timeline', { page: 'List', list: list.id }]),
disabled: queryKey[1].page === 'List' && queryKey[1].list === list.id,
destructive: false,
hidden: false
},
title: list.title,
icon: ''
}))
].map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))
: undefined}
<DropdownMenu.Group>
<DropdownMenu.Item
key='default'
onSelect={() =>
setQueryKey(['Timeline', { page: 'Following', ...instanceFollowingPage }])
}
disabled={page.page === 'Following'}
>
<DropdownMenu.ItemTitle children={t('tabs.local.name')} />
<DropdownMenu.ItemIcon iosIconName='house' />
</DropdownMenu.Item>
<DropdownMenu.CheckboxItem
key='showBoosts'
value={instanceFollowingPage.showBoosts ? 'on' : 'mixed'}
onValueChange={() => {
setQueryKey([
'Timeline',
{
page: 'Following',
showBoosts: !instanceFollowingPage.showBoosts,
showReplies: instanceFollowingPage.showReplies
}
])
dispatch(
updateInstanceFollowingPage({ showBoosts: !instanceFollowingPage.showBoosts })
)
}}
>
<DropdownMenu.ItemIndicator />
<DropdownMenu.ItemTitle children={t('tabs.local.options.showBoosts')} />
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
key='showReplies'
value={instanceFollowingPage.showReplies ? 'on' : 'mixed'}
onValueChange={() => {
setQueryKey([
'Timeline',
{
page: 'Following',
showBoosts: instanceFollowingPage.showBoosts,
showReplies: !instanceFollowingPage.showReplies
}
])
dispatch(
updateInstanceFollowingPage({ showReplies: !instanceFollowingPage.showReplies })
)
}}
>
<DropdownMenu.ItemTitle children={t('tabs.local.options.showReplies')} />
<DropdownMenu.ItemIndicator />
</DropdownMenu.CheckboxItem>
</DropdownMenu.Group>
<DropdownMenu.Group>
{lists?.length
? [
...lists?.map(list => ({
key: list.id,
item: {
onSelect: () => setQueryKey(['Timeline', { page: 'List', list: list.id }]),
disabled: page.page === 'List' && page.list === list.id,
destructive: false,
hidden: false
},
title: list.title,
icon: 'list.bullet'
}))
].map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))
: undefined}
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
),
@ -78,7 +174,7 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
/>
)
})
}, [])
}, [mode, queryKey[1], instanceFollowingPage])
usePopToTop()

View File

@ -79,7 +79,7 @@ const TabMeProfileFields: React.FC<
navigation
}) => {
const { theme } = useTheme()
const { t, i18n } = useTranslation('screenTabs')
const { t } = useTranslation('screenTabs')
const { mutateAsync, status } = useProfileMutation()
const allProps: EmojisState['inputProps'] = []
@ -144,7 +144,7 @@ const TabMeProfileFields: React.FC<
/>
)
})
}, [theme, i18n.language, dirty, status, allProps.map(p => p.value)])
}, [theme, dirty, status, allProps.map(p => p.value)])
return (
<ComponentEmojis inputProps={allProps}>

View File

@ -23,7 +23,7 @@ const TabMeProfileName: React.FC<
navigation
}) => {
const { theme } = useTheme()
const { t, i18n } = useTranslation('screenTabs')
const { t } = useTranslation('screenTabs')
const { mutateAsync, status } = useProfileMutation()
const [value, setValue] = useState(display_name)
@ -90,7 +90,7 @@ const TabMeProfileName: React.FC<
/>
)
})
}, [theme, i18n.language, dirty, status, value])
}, [theme, dirty, status, value])
return (
<ComponentEmojis inputProps={[displayNameProps]}>

View File

@ -23,7 +23,7 @@ const TabMeProfileNote: React.FC<
navigation
}) => {
const { theme } = useTheme()
const { t, i18n } = useTranslation('screenTabs')
const { t } = useTranslation('screenTabs')
const { mutateAsync, status } = useProfileMutation()
const [notes, setNotes] = useState(note)
@ -90,7 +90,7 @@ const TabMeProfileNote: React.FC<
/>
)
})
}, [theme, i18n.language, dirty, status, notes])
}, [theme, dirty, status, notes])
return (
<ComponentEmojis inputProps={[notesProps]}>

View File

@ -26,7 +26,7 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
params: { account }
}
}) => {
const { t, i18n } = useTranslation('screenTabs')
const { t } = useTranslation('screenTabs')
const { colors, mode } = useTheme()
const mShare = menuShare({ type: 'account', url: account.url })
@ -89,8 +89,9 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
const [queryKey, setQueryKey] = useState<QueryKeyTimeline>([
'Timeline',
{ page: 'Account_Default', account: account.id }
{ page: 'Account', account: account.id, exclude_reblogs: true, only_media: false }
])
const page = queryKey[1]
const isFetchingTimeline = useIsFetching(queryKey)
const fetchedTimeline = useRef(false)
useEffect(() => {
@ -113,14 +114,32 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
<SegmentedControl
appearance={mode}
values={[t('shared.account.toots.default'), t('shared.account.toots.all')]}
selectedIndex={queryKey[1].page === 'Account_Default' ? 0 : 1}
selectedIndex={page.page === 'Account' ? 0 : 1}
onChange={({ nativeEvent }) => {
switch (nativeEvent.selectedSegmentIndex) {
case 0:
setQueryKey([queryKey[0], { ...queryKey[1], page: 'Account_Default' }])
setQueryKey([
queryKey[0],
{
...page,
page: 'Account',
account: account.id,
exclude_reblogs: true,
only_media: false
}
])
break
case 1:
setQueryKey([queryKey[0], { ...queryKey[1], page: 'Account_All' }])
setQueryKey([
queryKey[0],
{
...page,
page: 'Account',
account: account.id,
exclude_reblogs: false,
only_media: false
}
])
break
}
}}
@ -152,7 +171,7 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
) : null}
</>
)
}, [data, fetchedTimeline.current, queryKey[1].page, i18n.language, mode])
}, [data, fetchedTimeline.current, queryKey[1].page, mode])
return (
<>

View File

@ -3,11 +3,11 @@ import Icon from '@components/Icon'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { useTimelineQuery } from '@utils/queryHooks/timeline'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect } from 'react'
import { Dimensions, ListRenderItem, Pressable, StyleSheet, View } from 'react-native'
import { Dimensions, ListRenderItem, Pressable, View } from 'react-native'
import { FlatList } from 'react-native-gesture-handler'
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
@ -15,117 +15,111 @@ export interface Props {
account: Mastodon.Account | undefined
}
const AccountAttachments = React.memo(
({ account }: Props) => {
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const { colors } = useTheme()
const AccountAttachments: React.FC<Props> = ({ account }) => {
if (!account) return null
const DISPLAY_AMOUNT = 6
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const { colors } = useTheme()
const width =
(Dimensions.get('screen').width - StyleConstants.Spacing.Global.PagePadding * 2) / 4
const DISPLAY_AMOUNT = 6
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 width = (Dimensions.get('screen').width - StyleConstants.Spacing.Global.PagePadding * 2) / 4
const flattenData = data?.pages
? data.pages
.flatMap(d => [...d.body])
.filter(status => !(status as Mastodon.Status).sensitive)
.splice(0, DISPLAY_AMOUNT)
: []
const renderItem = useCallback<ListRenderItem<Mastodon.Status>>(
({ item, index }) => {
if (index === DISPLAY_AMOUNT - 1) {
return (
<Pressable
onPress={() => {
account && navigation.push('Tab-Shared-Attachments', { account })
}}
children={
<View
style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundOverlayInvert,
width: width,
height: width,
justifyContent: 'center',
alignItems: 'center'
}}
children={
<Icon
name='MoreHorizontal'
color={colors.primaryOverlay}
size={StyleConstants.Font.Size.L * 1.5}
/>
}
/>
}
/>
)
} else {
return (
<GracefullyImage
uri={{
original: item.media_attachments[0]?.preview_url || item.media_attachments[0]?.url,
remote: item.media_attachments[0]?.remote_url
}}
blurhash={
item.media_attachments[0] && (item.media_attachments[0].blurhash || undefined)
}
dimension={{ width: width, height: width }}
style={{ marginLeft: StyleConstants.Spacing.Global.PagePadding }}
onPress={() => navigation.push('Tab-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: colors.border
}
} else {
return {}
}
}, [flattenData.length])
return (
<Animated.View style={[styles.base, styleContainer]}>
<FlatList
horizontal
data={flattenData}
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
/>
</Animated.View>
)
},
(_, next) => next.account === undefined
)
const styles = StyleSheet.create({
base: {
flex: 1
const queryKeyParams: QueryKeyTimeline[1] = {
page: 'Account',
account: account.id,
exclude_reblogs: true,
only_media: true
}
})
const { data, refetch } = useTimelineQuery({
...queryKeyParams,
options: { enabled: false }
})
useEffect(() => {
if (account?.id) {
refetch()
}
}, [account])
const flattenData = data?.pages
? data.pages
.flatMap(d => [...d.body])
.filter(status => !(status as Mastodon.Status).sensitive)
.splice(0, DISPLAY_AMOUNT)
: []
const renderItem = useCallback<ListRenderItem<Mastodon.Status>>(
({ item, index }) => {
if (index === DISPLAY_AMOUNT - 1) {
return (
<Pressable
onPress={() => {
account && navigation.push('Tab-Shared-Attachments', { account })
}}
children={
<View
style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundOverlayInvert,
width: width,
height: width,
justifyContent: 'center',
alignItems: 'center'
}}
children={
<Icon
name='MoreHorizontal'
color={colors.primaryOverlay}
size={StyleConstants.Font.Size.L * 1.5}
/>
}
/>
}
/>
)
} else {
return (
<GracefullyImage
uri={{
original: item.media_attachments[0]?.preview_url || item.media_attachments[0]?.url,
remote: item.media_attachments[0]?.remote_url
}}
blurhash={
item.media_attachments[0] && (item.media_attachments[0].blurhash || undefined)
}
dimension={{ width: width, height: width }}
style={{ marginLeft: StyleConstants.Spacing.Global.PagePadding }}
onPress={() => navigation.push('Tab-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: colors.border
}
} else {
return {}
}
}, [flattenData.length])
return (
<Animated.View style={[{ flex: 1 }, styleContainer]}>
<FlatList
horizontal
data={flattenData}
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
/>
</Animated.View>
)
}
export default AccountAttachments

View File

@ -44,7 +44,7 @@ const TabSharedAttachments: React.FC<TabSharedStackScreenProps<'Tab-Shared-Attac
const queryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'Account_Attachments', account: account.id }
{ page: 'Account', account: account.id, exclude_reblogs: true, only_media: true }
]
return (

View File

@ -133,8 +133,11 @@ const instancesMigration = {
11: (state: { instances: InstanceV10[] }): { instances: InstanceV11[] } => {
return {
instances: state.instances.map(instance => {
delete instance.timelinesLookback
return {
...instance,
followingPage: { showBoosts: true, showReplies: true },
mePage: { ...instance.mePage, followedTags: { shown: false } },
notifications_filter: {
...instance.notifications_filter,

View File

@ -1,5 +1,4 @@
import { ComposeStateDraft } from '@screens/Compose/utils/types'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
export type InstanceV11 = {
active: boolean
@ -42,11 +41,9 @@ export type InstanceV11 = {
private?: string // legacy
}
}
timelinesLookback?: {
[key: string]: {
queryKey: QueryKeyTimeline
ids: Mastodon.Status['id'][]
}
followingPage: {
showBoosts: boolean
showReplies: boolean
}
mePage: {
followedTags: { shown: boolean }

View File

@ -18,25 +18,66 @@ import updateStatusProperty from './timeline/updateStatusProperty'
export type QueryKeyTimeline = [
'Timeline',
{
page: App.Pages
hashtag?: Mastodon.Tag['name']
list?: Mastodon.List['id']
toot?: Mastodon.Status['id']
account?: Mastodon.Account['id']
}
(
| {
page: Exclude<App.Pages, 'Following' | 'Hashtag' | 'List' | 'Toot' | 'Account'>
}
| {
page: 'Following'
showBoosts: boolean
showReplies: boolean
}
| {
page: 'Hashtag'
hashtag: Mastodon.Tag['name']
}
| {
page: 'List'
list: Mastodon.List['id']
}
| {
page: 'Toot'
toot: Mastodon.Status['id']
}
| {
page: 'Account'
account: Mastodon.Account['id']
exclude_reblogs: boolean
only_media: boolean
}
)
]
const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<QueryKeyTimeline>) => {
const { page, account, hashtag, list, toot } = queryKey[1]
let params: { [key: string]: string } = { ...pageParam }
const page = queryKey[1]
let params: { [key: string]: string } = { ...pageParam, limit: 40 }
switch (page) {
switch (page.page) {
case 'Following':
return apiInstance<Mastodon.Status[]>({
method: 'get',
url: 'timelines/home',
params
}).then(res => {
if (!page.showBoosts || !page.showReplies) {
return {
...res,
body: res.body
.filter(status => {
if (!page.showBoosts && status.reblog) {
return null
}
if (!page.showReplies && status.in_reply_to_id?.length) {
return null
}
return status
})
.filter(s => s)
}
} else {
return res
}
})
case 'Local':
@ -91,62 +132,57 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query
}
})
case 'Account_Default':
if (pageParam && pageParam.hasOwnProperty('max_id')) {
case 'Account':
if (page.exclude_reblogs) {
if (pageParam && pageParam.hasOwnProperty('max_id')) {
return apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${page.account}/statuses`,
params: {
exclude_replies: 'true',
...params
}
})
} else {
const res1 = await apiInstance<(Mastodon.Status & { _pinned: boolean })[]>({
method: 'get',
url: `accounts/${page.account}/statuses`,
params: {
pinned: 'true'
}
})
res1.body = res1.body.map(status => {
status._pinned = true
return status
})
const res2 = await apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${page.account}/statuses`,
params: {
exclude_replies: 'true'
}
})
return {
body: uniqBy([...res1.body, ...res2.body], 'id'),
...(res2.links.next && { links: { next: res2.links.next } })
}
}
} else {
return apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${account}/statuses`,
url: `accounts/${page.account}/statuses`,
params: {
exclude_replies: 'true',
...params
...params,
exclude_replies: page.exclude_reblogs.toString(),
only_media: page.only_media.toString()
}
})
} else {
const res1 = await apiInstance<(Mastodon.Status & { _pinned: boolean })[]>({
method: 'get',
url: `accounts/${account}/statuses`,
params: {
pinned: 'true'
}
})
res1.body = res1.body.map(status => {
status._pinned = true
return status
})
const res2 = await apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${account}/statuses`,
params: {
exclude_replies: 'true'
}
})
return {
body: uniqBy([...res1.body, ...res2.body], 'id'),
...(res2.links.next && { links: { next: res2.links.next } })
}
}
case 'Account_All':
return apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${account}/statuses`,
params
})
case 'Account_Attachments':
return apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${account}/statuses`,
params: {
only_media: 'true',
...params
}
})
case 'Hashtag':
return apiInstance<Mastodon.Status[]>({
method: 'get',
url: `timelines/tag/${hashtag}`,
url: `timelines/tag/${page.hashtag}`,
params
})
@ -174,21 +210,21 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query
case 'List':
return apiInstance<Mastodon.Status[]>({
method: 'get',
url: `timelines/list/${list}`,
url: `timelines/list/${page.list}`,
params
})
case 'Toot':
const res1_1 = await apiInstance<Mastodon.Status>({
method: 'get',
url: `statuses/${toot}`
url: `statuses/${page.toot}`
})
const res2_1 = await apiInstance<{
ancestors: Mastodon.Status[]
descendants: Mastodon.Status[]
}>({
method: 'get',
url: `statuses/${toot}/context`
url: `statuses/${page.toot}/context`
})
return {
body: [...res2_1.body.ancestors, res1_1.body, ...res2_1.body.descendants]

View File

@ -107,7 +107,10 @@ const addInstance = createAsyncThunk(
},
keys: { auth: undefined, public: undefined, private: undefined }
},
timelinesLookback: {},
followingPage: {
showBoosts: true,
showReplies: true
},
mePage: {
followedTags: { shown: false },
lists: { shown: false },

View File

@ -81,16 +81,15 @@ const instancesSlice = createSlice({
return newInstance
})
},
updateInstanceTimelineLookback: (
updateInstanceFollowingPage: (
{ instances },
action: PayloadAction<InstanceLatest['timelinesLookback']>
action: PayloadAction<Partial<InstanceLatest['followingPage']>>
) => {
const activeIndex = findInstanceActive(instances)
instances[activeIndex] &&
(instances[activeIndex].timelinesLookback = {
...instances[activeIndex].timelinesLookback,
...action.payload
})
instances[activeIndex].followingPage = {
...instances[activeIndex].followingPage,
...action.payload
}
},
updateInstanceMePage: (
{ instances },
@ -360,8 +359,8 @@ export const getInstanceNotificationsFilter = ({ instances: { instances } }: Roo
export const getInstancePush = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.push
export const getInstanceTimelinesLookback = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.timelinesLookback
export const getInstanceFollowingPage = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.followingPage
export const getInstanceMePage = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.mePage
@ -379,7 +378,7 @@ export const {
updateInstanceDraft,
removeInstanceDraft,
disableAllPushes,
updateInstanceTimelineLookback,
updateInstanceFollowingPage,
updateInstanceMePage,
countInstanceEmoji
} = instancesSlice.actions