Improve account page

This commit is contained in:
xmflsct 2023-02-26 21:51:31 +01:00
parent 71e0c88cc2
commit 120641b95e
10 changed files with 233 additions and 254 deletions

View File

@ -173,6 +173,10 @@ const ComponentInstance: React.FC<Props> = ({
lists: { shown: false },
announcements: { shown: false, unread: 0 }
},
page_account_timeline: {
excludeBoosts: true,
excludeReplies: true
},
drafts: [],
emojis_frequent: []
}

View File

@ -34,9 +34,8 @@ const AccountAttachments: React.FC<Props> = ({ remote_id, remote_domain }) => {
const { data } = useTimelineQuery({
page: 'Account',
type: 'attachments',
id: account?.id,
exclude_reblogs: false,
only_media: true,
...(remote_id && remote_domain && { remote_id, remote_domain }),
options: { enabled: !!account?.id || (!!remote_id && !!remote_domain) }
})
@ -53,6 +52,7 @@ const AccountAttachments: React.FC<Props> = ({ remote_id, remote_domain }) => {
flex: 1,
height: width + StyleConstants.Spacing.Global.PagePadding * 2,
paddingVertical: StyleConstants.Spacing.Global.PagePadding,
paddingRight: StyleConstants.Spacing.Global.PagePadding,
borderTopWidth: 1,
borderTopColor: colors.border
}}
@ -70,7 +70,7 @@ const AccountAttachments: React.FC<Props> = ({ remote_id, remote_domain }) => {
children={
<View
style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
marginLeft: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundOverlayInvert,
width: width,
height: width,
@ -110,7 +110,11 @@ const AccountAttachments: React.FC<Props> = ({ remote_id, remote_domain }) => {
blurhash: item.media_attachments[0]?.blurhash
}}
dimension={{ width, height: width }}
style={{ marginLeft: StyleConstants.Spacing.Global.PagePadding }}
style={{
marginLeft: StyleConstants.Spacing.Global.PagePadding,
borderRadius: StyleConstants.BorderRadius / 2,
overflow: 'hidden'
}}
onPress={() => navigation.push('Tab-Shared-Toot', { toot: item })}
dim
/>

View File

@ -9,11 +9,12 @@ const AccountHeader: React.FC = () => {
const { account } = useContext(AccountContext)
const topInset = useSafeAreaInsets().top
const height = Dimensions.get('window').width / 3 + topInset
return (
<GracefullyImage
sources={{ default: { uri: account?.header }, static: { uri: account?.header_static } }}
style={{ height: Dimensions.get('window').width / 3 + topInset }}
style={{ height }}
onPress={() => {
if (account) {
Image.getSize(account.header, (width, height) =>

View File

@ -19,7 +19,8 @@ const AccountInformationFields: React.FC = () => {
style={{
borderTopWidth: StyleSheet.hairlineWidth,
marginBottom: StyleConstants.Spacing.M,
borderTopColor: colors.border
borderTopColor: colors.border,
marginHorizontal: -StyleConstants.Spacing.Global.PagePadding
}}
>
{account.fields.map((field, index) => (

View File

@ -23,25 +23,6 @@ const AccountInformationStats: React.FC = () => {
return (
<View style={[styles.stats, { flexDirection: 'row' }]}>
{account ? (
<CustomText
style={[styles.stat, { color: colors.primaryDefault }]}
children={t('shared.account.summary.statuses_count', {
count: account.statuses_count || 0
})}
onPress={() => {
pageMe && account && navigation.push('Tab-Shared-Account', { account })
}}
/>
) : (
<PlaceholderLine
width={StyleConstants.Font.Size.S * 1.25}
height={StyleConstants.Font.LineHeight.S}
color={colors.shimmerDefault}
noMargin
style={{ borderRadius: 0 }}
/>
)}
{account ? (
<CustomText
style={[styles.stat, { color: colors.primaryDefault, textAlign: 'right' }]}
@ -95,13 +76,8 @@ const AccountInformationStats: React.FC = () => {
}
const styles = StyleSheet.create({
stats: {
flex: 1,
justifyContent: 'space-between'
},
stat: {
...StyleConstants.FontStyle.S
}
stats: { flex: 1, gap: StyleConstants.Spacing.L },
stat: { ...StyleConstants.FontStyle.S }
})
export default AccountInformationStats

View File

@ -1,9 +1,10 @@
import GracefullyImage from '@components/GracefullyImage'
import { ParseEmojis } from '@components/Parse'
import CustomText from '@components/Text'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react'
import { Dimensions, StyleSheet, View } from 'react-native'
import { StyleSheet, View } from 'react-native'
import Animated, { Extrapolate, interpolate, useAnimatedStyle } from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import AccountContext from './Context'
@ -18,23 +19,11 @@ const AccountNav: React.FC<Props> = ({ scrollY }) => {
const { colors } = useTheme()
const headerHeight = useSafeAreaInsets().top + 44
const nameY =
Dimensions.get('window').width / 3 +
StyleConstants.Avatar.L -
StyleConstants.Spacing.Global.PagePadding * 2 +
StyleConstants.Spacing.M -
headerHeight
const styleOpacity = useAnimatedStyle(() => {
return {
opacity: interpolate(scrollY.value, [0, 200], [0, 1], Extrapolate.CLAMP)
}
})
const styleMarginTop = useAnimatedStyle(() => {
return {
marginTop: interpolate(scrollY.value, [nameY, nameY + 20], [50, 0], Extrapolate.CLAMP)
}
})
return (
<Animated.View
@ -53,20 +42,32 @@ const AccountNav: React.FC<Props> = ({ scrollY }) => {
flex: 1,
alignItems: 'center',
overflow: 'hidden',
marginTop: useSafeAreaInsets().top + (44 - StyleConstants.Font.Size.L) / 2
marginTop: useSafeAreaInsets().top + StyleConstants.Font.Size.L / 2
}}
>
<Animated.View style={[{ flexDirection: 'row' }, styleMarginTop]}>
<View
style={{ flexDirection: 'row', alignItems: 'center', gap: StyleConstants.Spacing.XS }}
>
{account ? (
<CustomText numberOfLines={1}>
<ParseEmojis
content={account.display_name || account.username}
emojis={account.emojis}
fontBold
<>
<GracefullyImage
sources={{ default: { uri: account.avatar_static } }}
dimension={{
width: StyleConstants.Font.Size.L,
height: StyleConstants.Font.Size.L
}}
style={{ borderRadius: 99, overflow: 'hidden' }}
/>
</CustomText>
<CustomText numberOfLines={1}>
<ParseEmojis
content={account.display_name || account.username}
emojis={account.emojis}
fontBold
/>
</CustomText>
</>
) : null}
</Animated.View>
</View>
</View>
</Animated.View>
)

View File

@ -1,20 +1,20 @@
import menuAccount from '@components/contextMenu/account'
import menuShare from '@components/contextMenu/share'
import { HeaderLeft, 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 SegmentedControl from '@react-native-segmented-control/segmented-control'
import { useQueryClient } from '@tanstack/react-query'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { queryClient } from '@utils/queryHooks'
import { useAccountQuery } from '@utils/queryHooks/account'
import { useRelationshipQuery } from '@utils/queryHooks/relationship'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { useAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { Fragment, useEffect, useMemo, useState } from 'react'
import React, { Fragment, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform, Text, View } from 'react-native'
import { Platform, Pressable, Text, View } from 'react-native'
import { useSharedValue } from 'react-native-reanimated'
import * as DropdownMenu from 'zeego/dropdown-menu'
import AccountAttachments from './Attachments'
@ -29,7 +29,7 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
params: { account }
}
}) => {
const { t } = useTranslation('screenTabs')
const { t } = useTranslation(['common', 'screenTabs'])
const { colors, mode } = useTheme()
const { data, dataUpdatedAt, isFetched } = useAccountQuery({
@ -46,18 +46,15 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
id: data?.id,
options: { enabled: account._remote ? isFetched : true }
})
const queryClient = useQueryClient()
const [queryKey, setQueryKey] = useState<QueryKeyTimeline>([
const queryKeyDefault: QueryKeyTimeline = [
'Timeline',
{
page: 'Account',
type: 'default',
id: data?.id,
exclude_reblogs: true,
only_media: false,
...(account._remote && { remote_id: account.id, remote_domain: account._remote })
}
])
]
const mShare = menuShare({ type: 'account', url: data?.url })
const mAccount = menuAccount({ type: 'account', openChange: true, account: data })
@ -72,10 +69,10 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<HeaderRight
accessibilityLabel={t('shared.account.actions.accessibilityLabel', {
accessibilityLabel={t('screenTabs:shared.account.actions.accessibilityLabel', {
user: account.acct
})}
accessibilityHint={t('shared.account.actions.accessibilityHint')}
accessibilityHint={t('screenTabs:shared.account.actions.accessibilityHint')}
content='more-horizontal'
onPress={() => {}}
background
@ -150,15 +147,10 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
}
})
}, [mAccount])
useEffect(() => {
navigation.setParams({ queryKey })
}, [queryKey[1]])
const scrollY = useSharedValue(0)
const page = queryKey[1]
const [segment, setSegment] = useState<number>(0)
const [timelineSettings, setTimelineSettings] = useAccountStorage.object('page_account_timeline')
const ListHeaderComponent = useMemo(() => {
return (
<>
@ -170,45 +162,97 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
/>
</View>
{!data?.suspended ? (
// @ts-ignore
<SegmentedControl
appearance={mode}
values={[t('shared.account.toots.default'), t('shared.account.toots.all')]}
selectedIndex={segment}
onChange={({ nativeEvent }: any) => {
setSegment(nativeEvent.selectedSegmentIndex)
switch (nativeEvent.selectedSegmentIndex) {
case 0:
setQueryKey([
queryKey[0],
{
...page,
page: 'Account',
id: data?.id,
exclude_reblogs: true,
only_media: false
}
])
break
case 1:
setQueryKey([
queryKey[0],
{
...page,
page: 'Account',
id: data?.id,
exclude_reblogs: false,
only_media: false
}
])
break
}
}}
style={{
marginTop: StyleConstants.Spacing.M,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
}}
/>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Pressable
style={{
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: StyleConstants.Spacing.XS,
paddingVertical: StyleConstants.Spacing.S,
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
}}
>
<View style={{ flex: 1 }} />
<View
style={{ flex: 1 }}
children={
<CustomText
style={{ color: colors.secondary, alignSelf: 'center' }}
children={t('screenTabs:shared.account.summary.statuses_count', {
count: data?.statuses_count || 0
})}
/>
}
/>
<View
style={{
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-end'
}}
>
<Icon name='filter' color={colors.secondary} size={StyleConstants.Font.Size.M} />
</View>
</Pressable>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
<DropdownMenu.CheckboxItem
key='showBoosts'
value={
(
typeof timelineSettings?.excludeBoosts === 'boolean'
? timelineSettings.excludeBoosts
: true
)
? 'off'
: 'on'
}
onValueChange={() => {
setTimelineSettings({
...timelineSettings,
excludeBoosts: !timelineSettings?.excludeBoosts
})
queryClient.refetchQueries(queryKeyDefault)
}}
>
<DropdownMenu.ItemIndicator />
<DropdownMenu.ItemTitle
children={t('screenTabs:tabs.local.options.showBoosts')}
/>
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
key='showReplies'
value={
(
typeof timelineSettings?.excludeReplies === 'boolean'
? timelineSettings.excludeReplies
: true
)
? 'off'
: 'on'
}
onValueChange={() => {
setTimelineSettings({
...timelineSettings,
excludeReplies: !timelineSettings?.excludeReplies
})
queryClient.refetchQueries(queryKeyDefault)
}}
>
<DropdownMenu.ItemTitle
children={t('screenTabs:tabs.local.options.showReplies')}
/>
<DropdownMenu.ItemIndicator />
</DropdownMenu.CheckboxItem>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
) : null}
{data?.suspended ? (
<View
@ -226,13 +270,13 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
textAlign: 'center'
}}
>
{t('shared.account.suspended')}
{t('screenTabs:shared.account.suspended')}
</Text>
</View>
) : null}
</>
)
}, [segment, dataUpdatedAt, mode])
}, [timelineSettings, dataUpdatedAt, mode])
const [domain] = useAccountStorage.string('auth.account.domain')
@ -252,16 +296,14 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
ListHeaderComponent
) : (
<Timeline
queryKey={queryKey}
queryKey={queryKeyDefault}
disableRefresh
customProps={{
keyboardShouldPersistTaps: 'always',
renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
onScroll: ({ nativeEvent }) => (scrollY.value = nativeEvent.contentOffset.y),
ListHeaderComponent,
maintainVisibleContentPosition: undefined,
onRefresh: () => queryClient.refetchQueries(queryKey),
refreshing: false
refreshing: false,
onRefresh: () => queryClient.refetchQueries(queryKeyDefault)
}}
/>
)}

View File

@ -46,7 +46,12 @@ const TabSharedAttachments: React.FC<TabSharedStackScreenProps<'Tab-Shared-Attac
const queryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'Account', id: account.id, exclude_reblogs: true, only_media: true }
{
page: 'Account',
type: 'attachments',
id: account.id,
...(account._remote && { remote_id: account.id, remote_domain: account._remote })
}
]
return <Timeline queryKey={queryKey} />

View File

@ -15,7 +15,6 @@ import { useNavState } from '@utils/navigation/navigators'
import { queryClient } from '@utils/queryHooks'
import { getAccountStorage, setAccountStorage } from '@utils/storage/actions'
import { AxiosError } from 'axios'
import { uniqBy } from 'lodash'
import { searchLocalStatus } from './search'
import deleteItem from './timeline/deleteItem'
import editItem from './timeline/editItem'
@ -43,10 +42,9 @@ export type QueryKeyTimeline = [
}
| {
page: 'Account'
type: 'default' | 'attachments'
id?: Mastodon.Account['id']
exclude_reblogs: boolean
only_media: boolean
// remote info
// remote
remote_id?: Mastodon.Account['id']
remote_domain?: string
}
@ -163,139 +161,82 @@ export const queryFunctionTimeline = async ({
})
case 'Account':
const reject = Promise.reject('Timeline query account id not provided')
if (!page.id) return Promise.reject('Timeline account missing id')
if (page.only_media) {
let res
if (page.remote_domain && page.remote_id) {
res = await apiGeneral<Mastodon.Status[]>({
method: 'get',
domain: page.remote_domain,
url: `api/v1/accounts/${page.remote_id}/statuses`,
params: {
only_media: true,
...params
}
})
.then(res => ({
...res,
body: res.body.map(status => appendRemote.status(status, page.remote_domain!))
}))
.catch(() => {})
}
if (!res && page.id) {
res = await apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${page.id}/statuses`,
params: {
only_media: true,
...params
}
})
}
return res || reject
} else if (page.exclude_reblogs) {
if (pageParam && pageParam.hasOwnProperty('max_id')) {
let res
if (page.remote_domain && page.remote_id) {
res = await apiGeneral<Mastodon.Status[]>({
method: 'get',
domain: page.remote_domain,
url: `api/v1/accounts/${page.remote_id}/statuses`,
params: {
exclude_replies: true,
...params
}
})
.then(res => ({
...res,
body: res.body.map(status => appendRemote.status(status, page.remote_domain!))
}))
.catch(() => {})
let typeParams
switch (page.type) {
case 'default':
const filters = getAccountStorage.object('page_account_timeline')
typeParams = {
exclude_reblogs:
typeof filters?.excludeBoosts === 'boolean' ? filters.excludeBoosts : true,
exclude_replies:
typeof filters?.excludeReplies === 'boolean' ? filters.excludeReplies : true
}
if (!res && page.id) {
res = await apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${page.id}/statuses`,
params: {
exclude_replies: true,
...params
}
})
}
return res || reject
} else {
let res
if (page.remote_domain && page.remote_id) {
res = await apiGeneral<Mastodon.Status[]>({
method: 'get',
domain: page.remote_domain,
url: `api/v1/accounts/${page.remote_id}/statuses`,
params: { exclude_replies: true }
})
.then(res => ({
...res,
body: res.body.map(status => appendRemote.status(status, page.remote_domain!))
}))
.catch(() => {})
}
if (!res && page.id) {
const resPinned = await apiInstance<(Mastodon.Status & { _pinned: boolean })[]>({
method: 'get',
url: `accounts/${page.id}/statuses`,
params: { pinned: true }
}).then(res => ({
...res,
body: res.body.map(status => {
status._pinned = true
return status
})
}))
const resDefault = await apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${page.id}/statuses`,
params: { exclude_replies: true }
})
return {
body: uniqBy([...resPinned.body, ...resDefault.body], 'id'),
links: resDefault.links
}
}
return res || reject
}
} else {
let res
if (page.remote_domain && page.remote_id) {
res = await apiGeneral<Mastodon.Status[]>({
method: 'get',
domain: page.remote_domain,
url: `api/v1/accounts/${page.remote_id}/statuses`,
params: {
...params,
exclude_replies: false,
only_media: false
}
})
.then(res => ({
...res,
body: res.body.map(status => appendRemote.status(status, page.remote_domain!))
}))
.catch(() => {})
}
if (!res && page.id) {
res = await apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${page.id}/statuses`,
params: {
...params,
exclude_replies: false,
only_media: false
}
})
}
return res || reject
break
case 'attachments':
typeParams = { only_media: true, exclude_reblogs: true }
break
}
let pinned
if (page.type === 'default' && !params.hasOwnProperty('max_id')) {
if (page.remote_domain && page.remote_id) {
pinned = await apiGeneral<Mastodon.Status[]>({
method: 'get',
domain: page.remote_domain,
url: `api/v1/accounts/${page.remote_id}/statuses`,
params: { pinned: true }
})
.then(res => ({
...res,
body: res.body.map(status => appendRemote.status(status, page.remote_domain!))
}))
.catch(() => {})
}
if (!pinned) {
pinned = await apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${page.id}/statuses`,
params: { pinned: true }
})
}
}
let res
if (page.remote_domain && page.remote_id) {
res = await apiGeneral<Mastodon.Status[]>({
method: 'get',
domain: page.remote_domain,
url: `api/v1/accounts/${page.remote_id}/statuses`,
params: {
...typeParams,
...params
}
})
.then(res => ({
...res,
body: res.body.map(status => appendRemote.status(status, page.remote_domain!))
}))
.catch(() => {})
}
if (!res) {
res = await apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${page.id}/statuses`,
params: {
...typeParams,
...params
}
})
}
return pinned
? {
body: [...pinned.body.map(status => ({ ...status, _pinned: true })), ...res.body],
links: res.links
}
: res
case 'Hashtag':
return apiInstance<Mastodon.Status[]>({
method: 'get',

View File

@ -53,6 +53,10 @@ export type AccountV0 = {
unread: number
}
}
page_account_timeline: {
excludeBoosts: boolean
excludeReplies: boolean
}
drafts: ComposeStateDraft[]
emojis_frequent: {
emoji: Pick<Mastodon.Emoji, 'url' | 'shortcode' | 'static_url'>