This commit is contained in:
Zhiyuan Zheng 2021-03-06 21:01:38 +01:00
parent 17f15a199c
commit 83048450e8
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
15 changed files with 476 additions and 520 deletions

View File

@ -9,9 +9,18 @@ import * as WebBrowser from 'expo-web-browser'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, Image, StyleSheet, Text, TextInput, View } from 'react-native' import {
Alert,
Image,
KeyboardAvoidingView,
Platform,
StyleSheet,
Text,
TextInput,
View
} from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { Placeholder, Fade } from 'rn-placeholder' import { Placeholder } from 'rn-placeholder'
import analytics from './analytics' import analytics from './analytics'
import InstanceAuth from './Instance/Auth' import InstanceAuth from './Instance/Auth'
import InstanceInfo from './Instance/Info' import InstanceInfo from './Instance/Info'
@ -115,7 +124,10 @@ const ComponentInstance: React.FC<Props> = ({
}, [domain, instanceQuery.data, appsQuery.data]) }, [domain, instanceQuery.data, appsQuery.data])
return ( return (
<> <KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
{!disableHeaderImage ? ( {!disableHeaderImage ? (
<View style={styles.imageContainer}> <View style={styles.imageContainer}>
<Image <Image
@ -157,18 +169,8 @@ const ComponentInstance: React.FC<Props> = ({
</View> </View>
<View> <View>
<Placeholder <Placeholder>
{...(instanceQuery.isFetching && {
Animation: props => (
<Fade
{...props}
style={{ backgroundColor: theme.shimmerHighlight }}
/>
)
})}
>
<InstanceInfo <InstanceInfo
visible={instanceQuery.data?.title !== undefined}
header={t('server.information.name')} header={t('server.information.name')}
content={instanceQuery.data?.title || undefined} content={instanceQuery.data?.title || undefined}
potentialWidth={2} potentialWidth={2}
@ -176,7 +178,6 @@ const ComponentInstance: React.FC<Props> = ({
<View style={styles.instanceStats}> <View style={styles.instanceStats}>
<InstanceInfo <InstanceInfo
style={styles.stat1} style={styles.stat1}
visible={instanceQuery.data?.stats?.user_count !== undefined}
header={t('server.information.accounts')} header={t('server.information.accounts')}
content={ content={
instanceQuery.data?.stats?.user_count?.toString() || undefined instanceQuery.data?.stats?.user_count?.toString() || undefined
@ -185,7 +186,6 @@ const ComponentInstance: React.FC<Props> = ({
/> />
<InstanceInfo <InstanceInfo
style={styles.stat2} style={styles.stat2}
visible={instanceQuery.data?.stats?.status_count !== undefined}
header={t('server.information.statuses')} header={t('server.information.statuses')}
content={ content={
instanceQuery.data?.stats?.status_count?.toString() || instanceQuery.data?.stats?.status_count?.toString() ||
@ -195,7 +195,6 @@ const ComponentInstance: React.FC<Props> = ({
/> />
<InstanceInfo <InstanceInfo
style={styles.stat3} style={styles.stat3}
visible={instanceQuery.data?.stats?.domain_count !== undefined}
header={t('server.information.domains')} header={t('server.information.domains')}
content={ content={
instanceQuery.data?.stats?.domain_count?.toString() || instanceQuery.data?.stats?.domain_count?.toString() ||
@ -231,7 +230,7 @@ const ComponentInstance: React.FC<Props> = ({
</View> </View>
{requestAuth} {requestAuth}
</> </KeyboardAvoidingView>
) )
} }

View File

@ -1,62 +0,0 @@
import analytics from '@components/analytics'
import Icon from '@components/Icon'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import * as WebBrowser from 'expo-web-browser'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, Text } from 'react-native'
export interface Props {
agreed: boolean
setAgreed: React.Dispatch<React.SetStateAction<boolean>>
}
const EULA = React.memo(
({ agreed, setAgreed }: Props) => {
const { t } = useTranslation('componentInstance')
const { theme } = useTheme()
return (
<Pressable style={styles.base} onPress={() => setAgreed(!agreed)}>
<Icon
style={styles.icon}
name={agreed ? 'CheckCircle' : 'Circle'}
size={StyleConstants.Font.Size.M}
color={theme.primary}
/>
<Text style={[styles.text, { color: theme.primary }]}>
{t('server.EULA.base')}
<Text
style={{ color: theme.blue }}
children={t('server.EULA.EULA')}
onPress={() => {
analytics('view_EULA')
WebBrowser.openBrowserAsync(
'https://tooot.app/end-user-license-agreement'
)
}}
/>
</Text>
</Pressable>
)
},
(prev, next) => prev.agreed === next.agreed
)
const styles = StyleSheet.create({
base: {
flexDirection: 'row',
marginTop: StyleConstants.Spacing.M,
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
alignItems: 'center'
},
icon: {
marginRight: StyleConstants.Spacing.XS
},
text: {
...StyleConstants.FontStyle.S
}
})
export default EULA

View File

@ -8,15 +8,13 @@ import { PlaceholderLine } from 'rn-placeholder'
export interface Props { export interface Props {
style?: ViewStyle style?: ViewStyle
visible: boolean
header: string header: string
content?: string content?: string
potentialWidth?: number potentialWidth?: number
potentialLines?: number
} }
const InstanceInfo = React.memo( const InstanceInfo = React.memo(
({ style, header, content, potentialWidth, potentialLines = 1 }: Props) => { ({ style, header, content, potentialWidth }: Props) => {
const { t } = useTranslation('componentInstance') const { t } = useTranslation('componentInstance')
const { theme } = useTheme() const { theme } = useTheme()
@ -31,24 +29,22 @@ const InstanceInfo = React.memo(
expandHint={t('server.information.description.expandHint')} expandHint={t('server.information.description.expandHint')}
/> />
) : ( ) : (
Array.from(Array(potentialLines)).map((_, i) => ( <PlaceholderLine
<PlaceholderLine width={
key={i} potentialWidth
width={ ? potentialWidth * StyleConstants.Font.Size.M
potentialWidth : undefined
? potentialWidth * StyleConstants.Font.Size.M }
: undefined height={StyleConstants.Font.LineHeight.M}
} color={theme.shimmerDefault}
height={StyleConstants.Font.LineHeight.M} noMargin
color={theme.shimmerDefault} style={{ borderRadius: 0 }}
noMargin />
style={{ borderRadius: 0 }}
/>
))
)} )}
</View> </View>
) )
} },
(prev, next) => prev.content === next.content
) )
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -41,7 +41,7 @@ const TimelineDefault: React.FC<Props> = ({
pinned pinned
}) => { }) => {
const { theme } = useTheme() const { theme } = useTheme()
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => true) const instanceAccount = useSelector(getInstanceAccount, () => true)
const navigation = useNavigation< const navigation = useNavigation<
StackNavigationProp<Nav.TabLocalStackParamList> StackNavigationProp<Nav.TabLocalStackParamList>
>() >()

View File

@ -103,7 +103,7 @@ const TimelineRefresh: React.FC<Props> = ({
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>( queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
queryKey, queryKey,
data => { data => {
if (data?.pages[0].body.length === 0) { if (data?.pages[0] && data.pages[0].body.length === 0) {
return { return {
pages: data.pages.slice(1), pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1) pageParams: data.pageParams.slice(1)

View File

@ -22,277 +22,274 @@ export interface Props {
reblog: boolean reblog: boolean
} }
const TimelineActions: React.FC<Props> = ({ const TimelineActions = React.memo(
queryKey, ({ queryKey, rootQueryKey, status, accts, reblog }: Props) => {
rootQueryKey, const navigation = useNavigation()
status, const { t } = useTranslation('componentTimeline')
accts, const { mode, theme } = useTheme()
reblog const iconColor = theme.secondary
}) => { const iconColorAction = (state: boolean) =>
const navigation = useNavigation() state ? theme.primary : theme.secondary
const { t } = useTranslation('componentTimeline')
const { mode, theme } = useTheme()
const iconColor = theme.secondary
const iconColorAction = (state: boolean) =>
state ? theme.primary : theme.secondary
const queryClient = useQueryClient() const queryClient = useQueryClient()
const mutation = useTimelineMutation({ const mutation = useTimelineMutation({
queryClient, queryClient,
onMutate: true, onMutate: true,
onSuccess: (_, params) => { onSuccess: (_, params) => {
const theParams = params as MutationVarsTimelineUpdateStatusProperty const theParams = params as MutationVarsTimelineUpdateStatusProperty
if ( if (
// Un-bookmark from bookmarks page // Un-bookmark from bookmarks page
(queryKey[1].page === 'Bookmarks' && (queryKey[1].page === 'Bookmarks' &&
theParams.payload.property === 'bookmarked') || theParams.payload.property === 'bookmarked') ||
// Un-favourite from favourites page // Un-favourite from favourites page
(queryKey[1].page === 'Favourites' && (queryKey[1].page === 'Favourites' &&
theParams.payload.property === 'favourited') || theParams.payload.property === 'favourited') ||
// Un-reblog from following page // Un-reblog from following page
(queryKey[1].page === 'Following' && (queryKey[1].page === 'Following' &&
theParams.payload.property === 'reblogged' &&
theParams.payload.currentValue === true)
) {
queryClient.invalidateQueries(queryKey)
} else if (
theParams.payload.property === 'reblogged' && theParams.payload.property === 'reblogged' &&
theParams.payload.currentValue === true) queryKey[1].page !== 'Following'
) { ) {
// When reblogged, update cache of following page
const tempQueryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'Following' }
]
queryClient.invalidateQueries(tempQueryKey)
} else if (theParams.payload.property === 'favourited') {
// When favourited, update favourited page
const tempQueryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'Favourites' }
]
queryClient.invalidateQueries(tempQueryKey)
} else if (theParams.payload.property === 'bookmarked') {
// When bookmarked, update bookmark page
const tempQueryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'Bookmarks' }
]
queryClient.invalidateQueries(tempQueryKey)
}
},
onError: (err: any, params, oldData) => {
const correctParam = params as MutationVarsTimelineUpdateStatusProperty
displayMessage({
mode,
type: 'error',
message: t('common:toastMessage.error.message', {
function: t(
`shared.actions.${correctParam.payload.property}.function`
)
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
queryClient.invalidateQueries(queryKey) queryClient.invalidateQueries(queryKey)
} else if (
theParams.payload.property === 'reblogged' &&
queryKey[1].page !== 'Following'
) {
// When reblogged, update cache of following page
const tempQueryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'Following' }
]
queryClient.invalidateQueries(tempQueryKey)
} else if (theParams.payload.property === 'favourited') {
// When favourited, update favourited page
const tempQueryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'Favourites' }
]
queryClient.invalidateQueries(tempQueryKey)
} else if (theParams.payload.property === 'bookmarked') {
// When bookmarked, update bookmark page
const tempQueryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'Bookmarks' }
]
queryClient.invalidateQueries(tempQueryKey)
} }
}, })
onError: (err: any, params, oldData) => {
const correctParam = params as MutationVarsTimelineUpdateStatusProperty const onPressReply = useCallback(() => {
displayMessage({ analytics('timeline_shared_actions_reply_press', {
mode, page: queryKey[1].page,
type: 'error', count: status.replies_count
message: t('common:toastMessage.error.message', {
function: t(
`shared.actions.${correctParam.payload.property}.function`
)
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
}) })
queryClient.invalidateQueries(queryKey) navigation.navigate('Screen-Compose', {
} type: 'reply',
}) incomingStatus: status,
accts,
queryKey
})
}, [status.replies_count])
const onPressReblog = useCallback(() => {
analytics('timeline_shared_actions_reblog_press', {
page: queryKey[1].page,
count: status.reblogs_count,
current: status.reblogged
})
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
reblog,
payload: {
property: 'reblogged',
currentValue: status.reblogged,
propertyCount: 'reblogs_count',
countValue: status.reblogs_count
}
})
}, [status.reblogged, status.reblogs_count])
const onPressFavourite = useCallback(() => {
analytics('timeline_shared_actions_favourite_press', {
page: queryKey[1].page,
count: status.favourites_count,
current: status.favourited
})
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
reblog,
payload: {
property: 'favourited',
currentValue: status.favourited,
propertyCount: 'favourites_count',
countValue: status.favourites_count
}
})
}, [status.favourited, status.favourites_count])
const onPressBookmark = useCallback(() => {
analytics('timeline_shared_actions_bookmark_press', {
page: queryKey[1].page,
current: status.bookmarked
})
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
reblog,
payload: {
property: 'bookmarked',
currentValue: status.bookmarked,
propertyCount: undefined,
countValue: undefined
}
})
}, [status.bookmarked])
const onPressReply = useCallback(() => { const childrenReply = useMemo(
analytics('timeline_shared_actions_reply_press', { () => (
page: queryKey[1].page, <>
count: status.replies_count <Icon
}) name='MessageCircle'
navigation.navigate('Screen-Compose', { color={iconColor}
type: 'reply', size={StyleConstants.Font.Size.L}
incomingStatus: status, />
accts, {status.replies_count > 0 && (
queryKey <Text
}) style={{
}, [status.replies_count]) color: theme.secondary,
const onPressReblog = useCallback(() => { fontSize: StyleConstants.Font.Size.M,
analytics('timeline_shared_actions_reblog_press', { marginLeft: StyleConstants.Spacing.XS
page: queryKey[1].page, }}
count: status.reblogs_count, >
current: status.reblogged {status.replies_count}
}) </Text>
mutation.mutate({ )}
type: 'updateStatusProperty', </>
queryKey, ),
rootQueryKey, [status.replies_count]
id: status.id, )
reblog, const childrenReblog = useMemo(
payload: { () => (
property: 'reblogged', <>
currentValue: status.reblogged, <Icon
propertyCount: 'reblogs_count', name='Repeat'
countValue: status.reblogs_count color={
} status.visibility === 'private' || status.visibility === 'direct'
}) ? theme.disabled
}, [status.reblogged, status.reblogs_count]) : iconColorAction(status.reblogged)
const onPressFavourite = useCallback(() => { }
analytics('timeline_shared_actions_favourite_press', { size={StyleConstants.Font.Size.L}
page: queryKey[1].page, />
count: status.favourites_count, {status.reblogs_count > 0 && (
current: status.favourited <Text
}) style={{
mutation.mutate({ color: iconColorAction(status.reblogged),
type: 'updateStatusProperty', fontSize: StyleConstants.Font.Size.M,
queryKey, marginLeft: StyleConstants.Spacing.XS
rootQueryKey, }}
id: status.id, >
reblog, {status.reblogs_count}
payload: { </Text>
property: 'favourited', )}
currentValue: status.favourited, </>
propertyCount: 'favourites_count', ),
countValue: status.favourites_count [status.reblogged, status.reblogs_count]
} )
}) const childrenFavourite = useMemo(
}, [status.favourited, status.favourites_count]) () => (
const onPressBookmark = useCallback(() => { <>
analytics('timeline_shared_actions_bookmark_press', { <Icon
page: queryKey[1].page, name='Heart'
current: status.bookmarked color={iconColorAction(status.favourited)}
}) size={StyleConstants.Font.Size.L}
mutation.mutate({ />
type: 'updateStatusProperty', {status.favourites_count > 0 && (
queryKey, <Text
rootQueryKey, style={{
id: status.id, color: iconColorAction(status.favourited),
reblog, fontSize: StyleConstants.Font.Size.M,
payload: { marginLeft: StyleConstants.Spacing.XS,
property: 'bookmarked', marginTop: 0
currentValue: status.bookmarked, }}
propertyCount: undefined, >
countValue: undefined {status.favourites_count}
} </Text>
}) )}
}, [status.bookmarked]) </>
),
const childrenReply = useMemo( [status.favourited, status.favourites_count]
() => ( )
<> const childrenBookmark = useMemo(
() => (
<Icon <Icon
name='MessageCircle' name='Bookmark'
color={iconColor} color={iconColorAction(status.bookmarked)}
size={StyleConstants.Font.Size.L} size={StyleConstants.Font.Size.L}
/> />
{status.replies_count > 0 && ( ),
<Text [status.bookmarked]
style={{ )
color: theme.secondary,
fontSize: StyleConstants.Font.Size.M, return (
marginLeft: StyleConstants.Spacing.XS
}}
>
{status.replies_count}
</Text>
)}
</>
),
[status.replies_count]
)
const childrenReblog = useMemo(
() => (
<> <>
<Icon <View style={styles.actions}>
name='Repeat' <Pressable
color={ style={styles.action}
status.visibility === 'private' || status.visibility === 'direct' onPress={onPressReply}
? theme.disabled children={childrenReply}
: iconColorAction(status.reblogged) />
}
size={StyleConstants.Font.Size.L} <Pressable
/> style={styles.action}
{status.reblogs_count > 0 && ( onPress={onPressReblog}
<Text children={childrenReblog}
style={{ disabled={
color: iconColorAction(status.reblogged), status.visibility === 'private' || status.visibility === 'direct'
fontSize: StyleConstants.Font.Size.M, }
marginLeft: StyleConstants.Spacing.XS />
}}
> <Pressable
{status.reblogs_count} style={styles.action}
</Text> onPress={onPressFavourite}
)} children={childrenFavourite}
/>
<Pressable
style={styles.action}
onPress={onPressBookmark}
children={childrenBookmark}
/>
</View>
</> </>
), )
[status.reblogged, status.reblogs_count] },
) () => true
const childrenFavourite = useMemo( )
() => (
<>
<Icon
name='Heart'
color={iconColorAction(status.favourited)}
size={StyleConstants.Font.Size.L}
/>
{status.favourites_count > 0 && (
<Text
style={{
color: iconColorAction(status.favourited),
fontSize: StyleConstants.Font.Size.M,
marginLeft: StyleConstants.Spacing.XS,
marginTop: 0
}}
>
{status.favourites_count}
</Text>
)}
</>
),
[status.favourited, status.favourites_count]
)
const childrenBookmark = useMemo(
() => (
<Icon
name='Bookmark'
color={iconColorAction(status.bookmarked)}
size={StyleConstants.Font.Size.L}
/>
),
[status.bookmarked]
)
return (
<>
<View style={styles.actions}>
<Pressable
style={styles.action}
onPress={onPressReply}
children={childrenReply}
/>
<Pressable
style={styles.action}
onPress={onPressReblog}
children={childrenReblog}
disabled={
status.visibility === 'private' || status.visibility === 'direct'
}
/>
<Pressable
style={styles.action}
onPress={onPressFavourite}
children={childrenFavourite}
/>
<Pressable
style={styles.action}
onPress={onPressBookmark}
children={childrenBookmark}
/>
</View>
</>
)
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
actions: { actions: {

View File

@ -15,28 +15,32 @@ import { useQueryClient } from 'react-query'
import HeaderSharedCreated from './HeaderShared/Created' import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted' import HeaderSharedMuted from './HeaderShared/Muted'
const Names: React.FC<{ accounts: Mastodon.Account[] }> = ({ accounts }) => { const Names = React.memo(
const { t } = useTranslation('componentTimeline') ({ accounts }: { accounts: Mastodon.Account[] }) => {
const { theme } = useTheme() const { t } = useTranslation('componentTimeline')
const { theme } = useTheme()
return ( return (
<Text numberOfLines={1}> <Text
<Text style={[styles.namesLeading, { color: theme.secondary }]}> numberOfLines={1}
{t('shared.header.conversation.withAccounts')} style={[styles.namesLeading, { color: theme.secondary }]}
>
<Text>{t('shared.header.conversation.withAccounts')}</Text>
{accounts.map((account, index) => (
<Text key={account.id} numberOfLines={1}>
{index !== 0 ? t('common:separator') : undefined}
<ParseEmojis
content={account.display_name || account.username}
emojis={account.emojis}
fontBold
/>
</Text>
))}
</Text> </Text>
{accounts.map((account, index) => ( )
<Text key={account.id} numberOfLines={1}> },
{index !== 0 ? t('common:separator') : undefined} () => true
<ParseEmojis )
content={account.display_name || account.username}
emojis={account.emojis}
fontBold
/>
</Text>
))}
</Text>
)
}
export interface Props { export interface Props {
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline

View File

@ -9,29 +9,32 @@ export interface Props {
withoutName?: boolean // For notification follow request etc. withoutName?: boolean // For notification follow request etc.
} }
const HeaderSharedAccount: React.FC<Props> = ({ const HeaderSharedAccount = React.memo(
account, ({ account, withoutName = false }: Props) => {
withoutName = false const { theme } = useTheme()
}) => {
const { theme } = useTheme()
return ( return (
<View style={styles.base}> <View style={styles.base}>
{withoutName ? null : ( {withoutName ? null : (
<Text style={styles.name} numberOfLines={1}> <Text style={styles.name} numberOfLines={1}>
<ParseEmojis <ParseEmojis
content={account.display_name || account.username} content={account.display_name || account.username}
emojis={account.emojis} emojis={account.emojis}
fontBold fontBold
/> />
</Text>
)}
<Text
style={[styles.acct, { color: theme.secondary }]}
numberOfLines={1}
>
@{account.acct}
</Text> </Text>
)} </View>
<Text style={[styles.acct, { color: theme.secondary }]} numberOfLines={1}> )
@{account.acct} },
</Text> () => true
</View> )
)
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {

View File

@ -10,24 +10,29 @@ export interface Props {
application?: Mastodon.Application application?: Mastodon.Application
} }
const HeaderSharedApplication: React.FC<Props> = ({ application }) => { const HeaderSharedApplication = React.memo(
const { theme } = useTheme() ({ application }: Props) => {
const { t } = useTranslation('componentTimeline') const { theme } = useTheme()
const { t } = useTranslation('componentTimeline')
return application && application.name !== 'Web' ? ( return application && application.name !== 'Web' ? (
<Text <Text
onPress={async () => { onPress={async () => {
analytics('timeline_shared_header_application_press', { analytics('timeline_shared_header_application_press', {
application application
}) })
application.website && (await openLink(application.website)) application.website && (await openLink(application.website))
}} }}
style={[styles.application, { color: theme.secondary }]} style={[styles.application, { color: theme.secondary }]}
> >
{t('shared.header.shared.application', { application: application.name })} {t('shared.header.shared.application', {
</Text> application: application.name
) : null })}
} </Text>
) : null
},
() => true
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
application: { application: {

View File

@ -8,18 +8,21 @@ export interface Props {
muted?: Mastodon.Status['muted'] muted?: Mastodon.Status['muted']
} }
const HeaderSharedMuted: React.FC<Props> = ({ muted }) => { const HeaderSharedMuted = React.memo(
const { theme } = useTheme() ({ muted }: Props) => {
const { theme } = useTheme()
return muted ? ( return muted ? (
<Icon <Icon
name='VolumeX' name='VolumeX'
size={StyleConstants.Font.Size.S} size={StyleConstants.Font.Size.S}
color={theme.secondary} color={theme.secondary}
style={styles.visibility} style={styles.visibility}
/> />
) : null ) : null
} },
() => true
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
visibility: { visibility: {

View File

@ -8,32 +8,35 @@ export interface Props {
visibility: Mastodon.Status['visibility'] visibility: Mastodon.Status['visibility']
} }
const HeaderSharedVisibility: React.FC<Props> = ({ visibility }) => { const HeaderSharedVisibility = React.memo(
const { theme } = useTheme() ({ visibility }: Props) => {
const { theme } = useTheme()
switch (visibility) { switch (visibility) {
case 'private': case 'private':
return ( return (
<Icon <Icon
name='Lock' name='Lock'
size={StyleConstants.Font.Size.S} size={StyleConstants.Font.Size.S}
color={theme.secondary} color={theme.secondary}
style={styles.visibility} style={styles.visibility}
/> />
) )
case 'direct': case 'direct':
return ( return (
<Icon <Icon
name='Mail' name='Mail'
size={StyleConstants.Font.Size.S} size={StyleConstants.Font.Size.S}
color={theme.secondary} color={theme.secondary}
style={styles.visibility} style={styles.visibility}
/> />
) )
default: default:
return null return null
} }
} },
() => true
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
visibility: { visibility: {

View File

@ -353,7 +353,7 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
<Stack.Screen <Stack.Screen
name='Screen-Compose-DraftsList' name='Screen-Compose-DraftsList'
component={ComposeDraftsList} component={ComposeDraftsList}
options={{ stackPresentation: 'modal' }} options={{ stackPresentation: 'modal', headerShown: false }}
/> />
<Stack.Screen <Stack.Screen
name='Screen-Compose-EditAttachment' name='Screen-Compose-EditAttachment'

View File

@ -1,7 +1,8 @@
import Timeline from '@components/Timeline' import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import { StackScreenProps } from '@react-navigation/stack' import { StackScreenProps } from '@react-navigation/stack'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React from 'react' import React, { useCallback } from 'react'
const ScreenMeListsList: React.FC<StackScreenProps< const ScreenMeListsList: React.FC<StackScreenProps<
Nav.TabMeStackParamList, Nav.TabMeStackParamList,
@ -12,8 +13,12 @@ const ScreenMeListsList: React.FC<StackScreenProps<
} }
}) => { }) => {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'List', list }] const queryKey: QueryKeyTimeline = ['Timeline', { page: 'List', list }]
const renderItem = useCallback(
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
[]
)
return <Timeline queryKey={queryKey} /> return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
} }
export default ScreenMeListsList export default ScreenMeListsList

View File

@ -2,7 +2,7 @@ import { HeaderCenter, HeaderLeft } from '@components/Header'
import { StackScreenProps } from '@react-navigation/stack' import { StackScreenProps } from '@react-navigation/stack'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native' import { KeyboardAvoidingView, Platform } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack' import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import ScreenMeSwitchRoot from './Switch/Root' import ScreenMeSwitchRoot from './Switch/Root'
@ -14,26 +14,33 @@ const ScreenMeSwitch: React.FC<StackScreenProps<
>> = ({ navigation }) => { >> = ({ navigation }) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Stack.Navigator <KeyboardAvoidingView
screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }} style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
> >
<Stack.Screen <Stack.Navigator
name='Screen-Me-Switch-Root' screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}
component={ScreenMeSwitchRoot} >
options={{ <Stack.Screen
headerTitle: t('meSwitch:heading'), name='Screen-Me-Switch-Root'
...(Platform.OS === 'android' && { component={ScreenMeSwitchRoot}
headerCenter: () => <HeaderCenter content={t('meSwitch:heading')} /> options={{
}), headerTitle: t('meSwitch:heading'),
headerLeft: () => ( ...(Platform.OS === 'android' && {
<HeaderLeft headerCenter: () => (
content='ChevronDown' <HeaderCenter content={t('meSwitch:heading')} />
onPress={() => navigation.goBack()} )
/> }),
) headerLeft: () => (
}} <HeaderLeft
/> content='ChevronDown'
</Stack.Navigator> onPress={() => navigation.goBack()}
/>
)
}}
/>
</Stack.Navigator>
</KeyboardAvoidingView>
) )
} }

View File

@ -60,56 +60,52 @@ const ScreenMeSwitchRoot: React.FC = () => {
const instanceActive = useSelector(getInstanceActive) const instanceActive = useSelector(getInstanceActive)
return ( return (
<KeyboardAvoidingView <ScrollView style={styles.base} keyboardShouldPersistTaps='handled'>
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} <View style={[styles.firstSection, { borderBottomColor: theme.border }]}>
style={{ flex: 1 }} <Text style={[styles.header, { color: theme.primary }]}>
> {t('content.existing')}
<ScrollView keyboardShouldPersistTaps='handled'> </Text>
<View <View style={styles.accountButtons}>
style={[styles.firstSection, { borderBottomColor: theme.border }]} {instances.length
> ? instances
<Text style={[styles.header, { color: theme.primary }]}> .slice()
{t('content.existing')} .sort((a, b) =>
</Text> `${a.uri}${a.account.acct}`.localeCompare(
<View style={styles.accountButtons}> `${b.uri}${b.account.acct}`
{instances.length
? instances
.slice()
.sort((a, b) =>
`${a.uri}${a.account.acct}`.localeCompare(
`${b.uri}${b.account.acct}`
)
) )
.map((instance, index) => { )
const localAccount = instances[instanceActive!] .map((instance, index) => {
return ( const localAccount = instances[instanceActive!]
<AccountButton return (
key={index} <AccountButton
instance={instance} key={index}
disabled={ instance={instance}
instance.url === localAccount.url && disabled={
instance.token === localAccount.token && instance.url === localAccount.url &&
instance.account.id === localAccount.account.id instance.token === localAccount.token &&
} instance.account.id === localAccount.account.id
/> }
) />
}) )
: null} })
</View> : null}
</View> </View>
</View>
<View style={styles.secondSection}> <View style={styles.secondSection}>
<Text style={[styles.header, { color: theme.primary }]}> <Text style={[styles.header, { color: theme.primary }]}>
{t('content.new')} {t('content.new')}
</Text> </Text>
<ComponentInstance disableHeaderImage goBack /> <ComponentInstance disableHeaderImage goBack />
</View> </View>
</ScrollView> </ScrollView>
</KeyboardAvoidingView>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: {
marginBottom: StyleConstants.Spacing.L
},
header: { header: {
...StyleConstants.FontStyle.M, ...StyleConstants.FontStyle.M,
textAlign: 'center', textAlign: 'center',