mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Merge branch 'main' into candidate
This commit is contained in:
2
src/@types/i18next.d.ts
vendored
2
src/@types/i18next.d.ts
vendored
@ -4,7 +4,6 @@ import common from '../i18n/en/common.json'
|
|||||||
import screens from '../i18n/en/screens.json'
|
import screens from '../i18n/en/screens.json'
|
||||||
|
|
||||||
import screenAccountSelection from '../i18n/en/screens/accountSelection.json'
|
import screenAccountSelection from '../i18n/en/screens/accountSelection.json'
|
||||||
import screenActions from '../i18n/en/screens/actions.json'
|
|
||||||
import screenAnnouncements from '../i18n/en/screens/announcements.json'
|
import screenAnnouncements from '../i18n/en/screens/announcements.json'
|
||||||
import screenCompose from '../i18n/en/screens/compose.json'
|
import screenCompose from '../i18n/en/screens/compose.json'
|
||||||
import screenImageViewer from '../i18n/en/screens/imageViewer.json'
|
import screenImageViewer from '../i18n/en/screens/imageViewer.json'
|
||||||
@ -26,7 +25,6 @@ declare module 'i18next' {
|
|||||||
screens: typeof screens
|
screens: typeof screens
|
||||||
|
|
||||||
screenAccountSelection: typeof screenAccountSelection
|
screenAccountSelection: typeof screenAccountSelection
|
||||||
screenActions: typeof screenActions
|
|
||||||
screenAnnouncements: typeof screenAnnouncements
|
screenAnnouncements: typeof screenAnnouncements
|
||||||
screenCompose: typeof screenCompose
|
screenCompose: typeof screenCompose
|
||||||
screenImageViewer: typeof screenImageViewer
|
screenImageViewer: typeof screenImageViewer
|
||||||
|
4
src/@types/mastodon.d.ts
vendored
4
src/@types/mastodon.d.ts
vendored
@ -552,6 +552,10 @@ declare namespace Mastodon {
|
|||||||
language?: string
|
language?: string
|
||||||
text?: string
|
text?: string
|
||||||
filtered?: FilterResult[]
|
filtered?: FilterResult[]
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
_level?: number
|
||||||
|
_remote?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatusHistory = {
|
type StatusHistory = {
|
||||||
|
@ -168,7 +168,10 @@ const ParseHTML: React.FC<Props> = ({
|
|||||||
matchedMention &&
|
matchedMention &&
|
||||||
!disableDetails &&
|
!disableDetails &&
|
||||||
!sameAccount &&
|
!sameAccount &&
|
||||||
navigation.push('Tab-Shared-Account', { account: matchedMention })
|
navigation.push('Tab-Shared-Account', {
|
||||||
|
account: matchedMention,
|
||||||
|
isRemote: status?._remote
|
||||||
|
})
|
||||||
}
|
}
|
||||||
children={node.children.map(unwrapNode).join('')}
|
children={node.children.map(unwrapNode).join('')}
|
||||||
/>
|
/>
|
||||||
|
@ -87,9 +87,9 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
const main = () => (
|
const main = () => (
|
||||||
<>
|
<>
|
||||||
{item.reblog ? (
|
{item.reblog ? (
|
||||||
<TimelineActioned action='reblog' />
|
<TimelineActioned action='reblog' rootStatus={item} />
|
||||||
) : item._pinned ? (
|
) : item._pinned ? (
|
||||||
<TimelineActioned action='pinned' />
|
<TimelineActioned action='pinned' rootStatus={item} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<View
|
<View
|
||||||
@ -167,7 +167,6 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
status,
|
status,
|
||||||
reblogStatus: item.reblog ? item : undefined,
|
|
||||||
ownAccount,
|
ownAccount,
|
||||||
spoilerHidden,
|
spoilerHidden,
|
||||||
rawContent,
|
rawContent,
|
||||||
|
@ -61,6 +61,7 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
|||||||
action={notification.type}
|
action={notification.type}
|
||||||
isNotification
|
isNotification
|
||||||
account={notification.account}
|
account={notification.account}
|
||||||
|
rootStatus={notification.status}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
@ -14,11 +14,12 @@ export interface Props {
|
|||||||
action: Mastodon.Notification['type'] | 'reblog' | 'pinned'
|
action: Mastodon.Notification['type'] | 'reblog' | 'pinned'
|
||||||
isNotification?: boolean
|
isNotification?: boolean
|
||||||
account?: Mastodon.Account // For notification
|
account?: Mastodon.Account // For notification
|
||||||
|
rootStatus?: Mastodon.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest }) => {
|
const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest }) => {
|
||||||
const { status, reblogStatus } = useContext(StatusContext)
|
const { status } = useContext(StatusContext)
|
||||||
const account = rest.account || (reblogStatus ? reblogStatus.account : status?.account)
|
const account = rest.account || (rest.rootStatus || status)?.account
|
||||||
if (!account) return null
|
if (!account) return null
|
||||||
|
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { t } = useTranslation('componentTimeline')
|
||||||
@ -36,7 +37,8 @@ const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest })
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPress = () => navigation.push('Tab-Shared-Account', { account })
|
const onPress = () =>
|
||||||
|
navigation.push('Tab-Shared-Account', { account, isRemote: status?._remote })
|
||||||
|
|
||||||
const children = () => {
|
const children = () => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
@ -22,7 +22,7 @@ import { Pressable, StyleSheet, View } from 'react-native'
|
|||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
|
|
||||||
const TimelineActions: React.FC = () => {
|
const TimelineActions: React.FC = () => {
|
||||||
const { queryKey, rootQueryKey, status, reblogStatus, ownAccount, highlighted, disableDetails } =
|
const { queryKey, rootQueryKey, status, ownAccount, highlighted, disableDetails } =
|
||||||
useContext(StatusContext)
|
useContext(StatusContext)
|
||||||
if (!queryKey || !status || disableDetails) return null
|
if (!queryKey || !status || disableDetails) return null
|
||||||
|
|
||||||
@ -38,16 +38,16 @@ const TimelineActions: React.FC = () => {
|
|||||||
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' && theParams.payload.property === 'bookmarked') ||
|
(queryKey[1].page === 'Bookmarks' && theParams.payload.type === 'bookmarked') ||
|
||||||
// Un-favourite from favourites page
|
// Un-favourite from favourites page
|
||||||
(queryKey[1].page === 'Favourites' && theParams.payload.property === 'favourited')
|
(queryKey[1].page === 'Favourites' && theParams.payload.type === 'favourited')
|
||||||
) {
|
) {
|
||||||
queryClient.invalidateQueries(queryKey)
|
queryClient.invalidateQueries(queryKey)
|
||||||
} else if (theParams.payload.property === 'favourited') {
|
} else if (theParams.payload.type === 'favourited') {
|
||||||
// When favourited, update favourited page
|
// When favourited, update favourited page
|
||||||
const tempQueryKey: QueryKeyTimeline = ['Timeline', { page: 'Favourites' }]
|
const tempQueryKey: QueryKeyTimeline = ['Timeline', { page: 'Favourites' }]
|
||||||
queryClient.invalidateQueries(tempQueryKey)
|
queryClient.invalidateQueries(tempQueryKey)
|
||||||
} else if (theParams.payload.property === 'bookmarked') {
|
} else if (theParams.payload.type === 'bookmarked') {
|
||||||
// When bookmarked, update bookmark page
|
// When bookmarked, update bookmark page
|
||||||
const tempQueryKey: QueryKeyTimeline = ['Timeline', { page: 'Bookmarks' }]
|
const tempQueryKey: QueryKeyTimeline = ['Timeline', { page: 'Bookmarks' }]
|
||||||
queryClient.invalidateQueries(tempQueryKey)
|
queryClient.invalidateQueries(tempQueryKey)
|
||||||
@ -60,7 +60,7 @@ const TimelineActions: React.FC = () => {
|
|||||||
type: 'error',
|
type: 'error',
|
||||||
message: t('common:message.error.message', {
|
message: t('common:message.error.message', {
|
||||||
function: t(
|
function: t(
|
||||||
`componentTimeline:shared.actions.${correctParam.payload.property}.function` as any
|
`componentTimeline:shared.actions.${correctParam.payload.type}.function` as any
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
...(err.status &&
|
...(err.status &&
|
||||||
@ -111,13 +111,9 @@ const TimelineActions: React.FC = () => {
|
|||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id: status.id,
|
status,
|
||||||
isReblog: !!reblogStatus,
|
|
||||||
payload: {
|
payload: {
|
||||||
property: 'reblogged',
|
type: 'reblogged',
|
||||||
currentValue: status.reblogged,
|
|
||||||
propertyCount: 'reblogs_count',
|
|
||||||
countValue: status.reblogs_count,
|
|
||||||
visibility: 'public'
|
visibility: 'public'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -127,13 +123,9 @@ const TimelineActions: React.FC = () => {
|
|||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id: status.id,
|
status,
|
||||||
isReblog: !!reblogStatus,
|
|
||||||
payload: {
|
payload: {
|
||||||
property: 'reblogged',
|
type: 'reblogged',
|
||||||
currentValue: status.reblogged,
|
|
||||||
propertyCount: 'reblogs_count',
|
|
||||||
countValue: status.reblogs_count,
|
|
||||||
visibility: 'unlisted'
|
visibility: 'unlisted'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -146,13 +138,9 @@ const TimelineActions: React.FC = () => {
|
|||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id: status.id,
|
status,
|
||||||
isReblog: !!reblogStatus,
|
|
||||||
payload: {
|
payload: {
|
||||||
property: 'reblogged',
|
type: 'reblogged',
|
||||||
currentValue: status.reblogged,
|
|
||||||
propertyCount: 'reblogs_count',
|
|
||||||
countValue: status.reblogs_count,
|
|
||||||
visibility: 'public'
|
visibility: 'public'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -163,13 +151,9 @@ const TimelineActions: React.FC = () => {
|
|||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id: status.id,
|
status,
|
||||||
isReblog: !!reblogStatus,
|
|
||||||
payload: {
|
payload: {
|
||||||
property: 'favourited',
|
type: 'favourited'
|
||||||
currentValue: status.favourited,
|
|
||||||
propertyCount: 'favourites_count',
|
|
||||||
countValue: status.favourites_count
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -178,13 +162,9 @@ const TimelineActions: React.FC = () => {
|
|||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id: status.id,
|
status,
|
||||||
isReblog: !!reblogStatus,
|
|
||||||
payload: {
|
payload: {
|
||||||
property: 'bookmarked',
|
type: 'bookmarked'
|
||||||
currentValue: status.bookmarked,
|
|
||||||
propertyCount: undefined,
|
|
||||||
countValue: undefined
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
|
||||||
import { RootStackParamList } from '@utils/navigation/navigators'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Alert } from 'react-native'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
sensitiveShown: boolean
|
sensitiveShown: boolean
|
||||||
@ -14,7 +13,7 @@ const AttachmentAltText: React.FC<Props> = ({ sensitiveShown, text }) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
|
const { t } = useTranslation('componentTimeline')
|
||||||
|
|
||||||
return !sensitiveShown ? (
|
return !sensitiveShown ? (
|
||||||
<Button
|
<Button
|
||||||
@ -28,7 +27,7 @@ const AttachmentAltText: React.FC<Props> = ({ sensitiveShown, text }) => {
|
|||||||
type='text'
|
type='text'
|
||||||
content='ALT'
|
content='ALT'
|
||||||
fontBold
|
fontBold
|
||||||
onPress={() => navigation.navigate('Screen-Actions', { type: 'alt_text', text })}
|
onPress={() => Alert.alert(t('shared.attachment.altText'), text)}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,8 @@ const TimelineAvatar: React.FC<Props> = ({ account }) => {
|
|||||||
})
|
})
|
||||||
})}
|
})}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
!disableOnPress && navigation.push('Tab-Shared-Account', { account: actualAccount })
|
!disableOnPress &&
|
||||||
|
navigation.push('Tab-Shared-Account', { account: actualAccount, isRemote: status?._remote })
|
||||||
}
|
}
|
||||||
uri={{ original: actualAccount.avatar, static: actualAccount.avatar_static }}
|
uri={{ original: actualAccount.avatar, static: actualAccount.avatar_static }}
|
||||||
dimension={
|
dimension={
|
||||||
|
@ -14,7 +14,7 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoilerExpanded }) => {
|
const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoilerExpanded }) => {
|
||||||
const { status, highlighted, inThread, disableDetails } = useContext(StatusContext)
|
const { status, highlighted, inThread } = useContext(StatusContext)
|
||||||
if (!status || typeof status.content !== 'string' || !status.content.length) return null
|
if (!status || typeof status.content !== 'string' || !status.content.length) return null
|
||||||
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
@ -9,7 +9,6 @@ type StatusContextType = {
|
|||||||
|
|
||||||
status?: Mastodon.Status
|
status?: Mastodon.Status
|
||||||
|
|
||||||
reblogStatus?: Mastodon.Status // When it is a reblog, pass the root status
|
|
||||||
ownAccount?: boolean
|
ownAccount?: boolean
|
||||||
spoilerHidden?: boolean
|
spoilerHidden?: boolean
|
||||||
rawContent?: React.MutableRefObject<string[]> // When highlighted, for translate, edit history
|
rawContent?: React.MutableRefObject<string[]> // When highlighted, for translate, edit history
|
||||||
|
@ -40,7 +40,12 @@ const HeaderSharedReplies: React.FC = () => {
|
|||||||
<CustomText
|
<CustomText
|
||||||
style={{ color: colors.blue, paddingLeft: StyleConstants.Spacing.S }}
|
style={{ color: colors.blue, paddingLeft: StyleConstants.Spacing.S }}
|
||||||
children={`@${mention.username}`}
|
children={`@${mention.username}`}
|
||||||
onPress={() => navigation.push('Tab-Shared-Account', { account: mention })}
|
onPress={() =>
|
||||||
|
navigation.push('Tab-Shared-Account', {
|
||||||
|
account: mention,
|
||||||
|
isRemote: status?._remote
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
|
@ -20,15 +20,8 @@ import { Pressable, View } from 'react-native'
|
|||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
|
|
||||||
const TimelinePoll: React.FC = () => {
|
const TimelinePoll: React.FC = () => {
|
||||||
const {
|
const { queryKey, rootQueryKey, status, ownAccount, spoilerHidden, disableDetails } =
|
||||||
queryKey,
|
useContext(StatusContext)
|
||||||
rootQueryKey,
|
|
||||||
status,
|
|
||||||
reblogStatus,
|
|
||||||
ownAccount,
|
|
||||||
spoilerHidden,
|
|
||||||
disableDetails
|
|
||||||
} = useContext(StatusContext)
|
|
||||||
if (!queryKey || !status || !status.poll) return null
|
if (!queryKey || !status || !status.poll) return null
|
||||||
const poll = status.poll
|
const poll = status.poll
|
||||||
|
|
||||||
@ -45,10 +38,9 @@ const TimelinePoll: React.FC = () => {
|
|||||||
rootQueryKey && queryClient.cancelQueries(rootQueryKey)
|
rootQueryKey && queryClient.cancelQueries(rootQueryKey)
|
||||||
|
|
||||||
haptics('Success')
|
haptics('Success')
|
||||||
switch (theParams.payload.property) {
|
switch (theParams.payload.type) {
|
||||||
case 'poll':
|
case 'poll':
|
||||||
theParams.payload.data = body as unknown as Mastodon.Poll
|
updateStatusProperty({ ...theParams, poll: body as unknown as Mastodon.Poll })
|
||||||
updateStatusProperty(theParams)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -84,12 +76,10 @@ const TimelinePoll: React.FC = () => {
|
|||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id: status.id,
|
status,
|
||||||
isReblog: !!reblogStatus,
|
|
||||||
payload: {
|
payload: {
|
||||||
property: 'poll',
|
type: 'poll',
|
||||||
id: poll.id,
|
action: 'vote',
|
||||||
type: 'vote',
|
|
||||||
options: allOptions
|
options: allOptions
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -110,12 +100,10 @@ const TimelinePoll: React.FC = () => {
|
|||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id: status.id,
|
status,
|
||||||
isReblog: !!reblogStatus,
|
|
||||||
payload: {
|
payload: {
|
||||||
property: 'poll',
|
type: 'poll',
|
||||||
id: poll.id,
|
action: 'refresh'
|
||||||
type: 'refresh'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -230,13 +218,9 @@ const TimelinePoll: React.FC = () => {
|
|||||||
|
|
||||||
const pollVoteCounts = () => {
|
const pollVoteCounts = () => {
|
||||||
if (poll.voters_count !== null) {
|
if (poll.voters_count !== null) {
|
||||||
return (
|
return t('componentTimeline:shared.poll.meta.count.voters', { count: poll.voters_count })
|
||||||
t('componentTimeline:shared.poll.meta.count.voters', { count: poll.voters_count }) + ' • '
|
|
||||||
)
|
|
||||||
} else if (poll.votes_count !== null) {
|
} else if (poll.votes_count !== null) {
|
||||||
return (
|
return t('componentTimeline:shared.poll.meta.count.votes', { count: poll.votes_count })
|
||||||
t('componentTimeline:shared.poll.meta.count.votes', { count: poll.votes_count }) + ' • '
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,11 +230,14 @@ const TimelinePoll: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
if (poll.expires_at) {
|
if (poll.expires_at) {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{' • '}
|
||||||
<Trans
|
<Trans
|
||||||
ns='componentTimeline'
|
ns='componentTimeline'
|
||||||
i18nKey='shared.poll.meta.expiration.until'
|
i18nKey='shared.poll.meta.expiration.until'
|
||||||
components={[<RelativeTime time={poll.expires_at} />]}
|
components={[<RelativeTime time={poll.expires_at} />]}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import { useNavigation } from '@react-navigation/native'
|
|||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { TabSharedStackParamList } from '@utils/navigation/navigators'
|
import { TabSharedStackParamList } from '@utils/navigation/navigators'
|
||||||
|
import { useAccountQuery } from '@utils/queryHooks/account'
|
||||||
import {
|
import {
|
||||||
QueryKeyRelationship,
|
QueryKeyRelationship,
|
||||||
useRelationshipMutation,
|
useRelationshipMutation,
|
||||||
@ -34,28 +35,34 @@ const menuAccount = ({
|
|||||||
queryKey?: QueryKeyTimeline
|
queryKey?: QueryKeyTimeline
|
||||||
rootQueryKey?: QueryKeyTimeline
|
rootQueryKey?: QueryKeyTimeline
|
||||||
}): ContextMenu[][] => {
|
}): ContextMenu[][] => {
|
||||||
if (!account) return []
|
|
||||||
|
|
||||||
const navigation =
|
const navigation =
|
||||||
useNavigation<NativeStackNavigationProp<TabSharedStackParamList, any, undefined>>()
|
useNavigation<NativeStackNavigationProp<TabSharedStackParamList, any, undefined>>()
|
||||||
const { t } = useTranslation(['common', 'componentContextMenu', 'componentRelationship'])
|
const { t } = useTranslation(['common', 'componentContextMenu', 'componentRelationship'])
|
||||||
|
|
||||||
const menus: ContextMenu[][] = [[]]
|
const menus: ContextMenu[][] = [[]]
|
||||||
|
|
||||||
const ownAccount = useAccountStorage.string('auth.account.id')['0'] === account.id
|
|
||||||
|
|
||||||
const [enabled, setEnabled] = useState(openChange)
|
const [enabled, setEnabled] = useState(openChange)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ownAccount && enabled === false && openChange === true) {
|
if (!ownAccount && enabled === false && openChange === true) {
|
||||||
setEnabled(true)
|
setEnabled(true)
|
||||||
}
|
}
|
||||||
}, [openChange, enabled])
|
}, [openChange, enabled])
|
||||||
const { data, isFetched } = useRelationshipQuery({ id: account.id, options: { enabled } })
|
const { data: fetchedAccount } = useAccountQuery({
|
||||||
|
remoteUrl: account?.url,
|
||||||
|
options: { enabled: !!status?._remote && enabled }
|
||||||
|
})
|
||||||
|
const actualAccount = status?._remote ? fetchedAccount : account
|
||||||
|
const { data, isFetched } = useRelationshipQuery({
|
||||||
|
id: actualAccount?.id,
|
||||||
|
options: { enabled: !!actualAccount?.id && enabled }
|
||||||
|
})
|
||||||
|
|
||||||
|
const ownAccount = useAccountStorage.string('auth.account.id')['0'] === actualAccount?.id
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const timelineMutation = useTimelineMutation({
|
const timelineMutation = useTimelineMutation({
|
||||||
onSuccess: (_, params) => {
|
onSuccess: (_, params) => {
|
||||||
queryClient.refetchQueries(['Relationship', { id: account.id }])
|
queryClient.refetchQueries(['Relationship', { id: actualAccount?.id }])
|
||||||
const theParams = params as MutationVarsTimelineUpdateAccountProperty
|
const theParams = params as MutationVarsTimelineUpdateAccountProperty
|
||||||
displayMessage({
|
displayMessage({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@ -101,7 +108,7 @@ const menuAccount = ({
|
|||||||
rootQueryKey && queryClient.invalidateQueries(rootQueryKey)
|
rootQueryKey && queryClient.invalidateQueries(rootQueryKey)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id: account.id }]
|
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id: actualAccount?.id }]
|
||||||
const relationshipMutation = useRelationshipMutation({
|
const relationshipMutation = useRelationshipMutation({
|
||||||
onSuccess: (res, { payload: { action } }) => {
|
onSuccess: (res, { payload: { action } }) => {
|
||||||
haptics('Success')
|
haptics('Success')
|
||||||
@ -128,14 +135,17 @@ const menuAccount = ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!account) return []
|
||||||
|
|
||||||
if (!ownAccount && Platform.OS !== 'android' && type !== 'account') {
|
if (!ownAccount && Platform.OS !== 'android' && type !== 'account') {
|
||||||
menus[0].push({
|
menus[0].push({
|
||||||
key: 'account-following',
|
key: 'account-following',
|
||||||
item: {
|
item: {
|
||||||
onSelect: () =>
|
onSelect: () =>
|
||||||
data &&
|
data &&
|
||||||
|
actualAccount &&
|
||||||
relationshipMutation.mutate({
|
relationshipMutation.mutate({
|
||||||
id: account.id,
|
id: actualAccount.id,
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
payload: { action: 'follow', state: !data?.requested ? data.following : true }
|
payload: { action: 'follow', state: !data?.requested ? data.following : true }
|
||||||
}),
|
}),
|
||||||
@ -173,8 +183,9 @@ const menuAccount = ({
|
|||||||
key: 'account-show-boosts',
|
key: 'account-show-boosts',
|
||||||
item: {
|
item: {
|
||||||
onSelect: () =>
|
onSelect: () =>
|
||||||
|
actualAccount &&
|
||||||
relationshipMutation.mutate({
|
relationshipMutation.mutate({
|
||||||
id: account.id,
|
id: actualAccount.id,
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
payload: { action: 'follow', state: false, reblogs: !data?.showing_reblogs }
|
payload: { action: 'follow', state: false, reblogs: !data?.showing_reblogs }
|
||||||
}),
|
}),
|
||||||
@ -192,10 +203,11 @@ const menuAccount = ({
|
|||||||
key: 'account-mute',
|
key: 'account-mute',
|
||||||
item: {
|
item: {
|
||||||
onSelect: () =>
|
onSelect: () =>
|
||||||
|
actualAccount &&
|
||||||
timelineMutation.mutate({
|
timelineMutation.mutate({
|
||||||
type: 'updateAccountProperty',
|
type: 'updateAccountProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
id: account.id,
|
id: actualAccount.id,
|
||||||
payload: { property: 'mute', currentValue: data?.muting }
|
payload: { property: 'mute', currentValue: data?.muting }
|
||||||
}),
|
}),
|
||||||
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
|
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
|
||||||
@ -215,17 +227,20 @@ const menuAccount = ({
|
|||||||
item: {
|
item: {
|
||||||
onSelect: () =>
|
onSelect: () =>
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
t('componentContextMenu:account.block.alert.title', { username: account.username }),
|
t('componentContextMenu:account.block.alert.title', {
|
||||||
|
username: actualAccount?.username
|
||||||
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: t('common:buttons.confirm'),
|
text: t('common:buttons.confirm'),
|
||||||
style: 'destructive',
|
style: 'destructive',
|
||||||
onPress: () =>
|
onPress: () =>
|
||||||
|
actualAccount &&
|
||||||
timelineMutation.mutate({
|
timelineMutation.mutate({
|
||||||
type: 'updateAccountProperty',
|
type: 'updateAccountProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
id: account.id,
|
id: actualAccount.id,
|
||||||
payload: { property: 'block', currentValue: data?.blocking }
|
payload: { property: 'block', currentValue: data?.blocking }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -247,8 +262,13 @@ const menuAccount = ({
|
|||||||
{
|
{
|
||||||
key: 'account-reports',
|
key: 'account-reports',
|
||||||
item: {
|
item: {
|
||||||
onSelect: () => navigation.navigate('Tab-Shared-Report', { account, status }),
|
onSelect: () =>
|
||||||
disabled: false,
|
actualAccount &&
|
||||||
|
navigation.navigate('Tab-Shared-Report', {
|
||||||
|
account: actualAccount,
|
||||||
|
status
|
||||||
|
}),
|
||||||
|
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
|
||||||
destructive: true,
|
destructive: true,
|
||||||
hidden: false
|
hidden: false
|
||||||
},
|
},
|
||||||
|
@ -35,7 +35,7 @@ const menuStatus = ({
|
|||||||
onMutate: true,
|
onMutate: true,
|
||||||
onError: (err: any, params, oldData) => {
|
onError: (err: any, params, oldData) => {
|
||||||
const theFunction = (params as MutationVarsTimelineUpdateStatusProperty).payload
|
const theFunction = (params as MutationVarsTimelineUpdateStatusProperty).payload
|
||||||
? (params as MutationVarsTimelineUpdateStatusProperty).payload.property
|
? (params as MutationVarsTimelineUpdateStatusProperty).payload.type
|
||||||
: 'delete'
|
: 'delete'
|
||||||
displayMessage({
|
displayMessage({
|
||||||
theme,
|
theme,
|
||||||
@ -195,15 +195,14 @@ const menuStatus = ({
|
|||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id: status.id,
|
status,
|
||||||
payload: {
|
payload: {
|
||||||
property: 'muted',
|
type: 'muted'
|
||||||
currentValue: status.muted
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
destructive: false,
|
destructive: false,
|
||||||
hidden: !ownAccount && !status.mentions?.filter(mention => mention.id === accountId).length
|
hidden: !ownAccount
|
||||||
},
|
},
|
||||||
title: t('componentContextMenu:status.mute.action', {
|
title: t('componentContextMenu:status.mute.action', {
|
||||||
defaultValue: 'false',
|
defaultValue: 'false',
|
||||||
@ -220,17 +219,14 @@ const menuStatus = ({
|
|||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id: status.id,
|
status,
|
||||||
payload: {
|
payload: {
|
||||||
property: 'pinned',
|
type: 'pinned'
|
||||||
currentValue: status.pinned,
|
|
||||||
propertyCount: undefined,
|
|
||||||
countValue: undefined
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
disabled: false,
|
disabled: status.visibility !== 'public' && status.visibility !== 'unlisted',
|
||||||
destructive: false,
|
destructive: false,
|
||||||
hidden: !ownAccount || (status.visibility !== 'public' && status.visibility !== 'unlisted')
|
hidden: !ownAccount
|
||||||
},
|
},
|
||||||
title: t('componentContextMenu:status.pin.action', {
|
title: t('componentContextMenu:status.pin.action', {
|
||||||
defaultValue: 'false',
|
defaultValue: 'false',
|
||||||
|
@ -2,7 +2,6 @@ export default {
|
|||||||
common: require('./common'),
|
common: require('./common'),
|
||||||
|
|
||||||
screens: require('./screens'),
|
screens: require('./screens'),
|
||||||
screenActions: require('./screens/actions'),
|
|
||||||
screenAnnouncements: require('./screens/announcements'),
|
screenAnnouncements: require('./screens/announcements'),
|
||||||
screenCompose: require('./screens/compose'),
|
screenCompose: require('./screens/compose'),
|
||||||
screenImageViewer: require('./screens/imageViewer'),
|
screenImageViewer: require('./screens/imageViewer'),
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": "Text alternatiu"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ export default {
|
|||||||
common: require('./common'),
|
common: require('./common'),
|
||||||
|
|
||||||
screens: require('./screens'),
|
screens: require('./screens'),
|
||||||
screenActions: require('./screens/actions'),
|
|
||||||
screenAnnouncements: require('./screens/announcements'),
|
screenAnnouncements: require('./screens/announcements'),
|
||||||
screenCompose: require('./screens/compose'),
|
screenCompose: require('./screens/compose'),
|
||||||
screenImageViewer: require('./screens/imageViewer'),
|
screenImageViewer: require('./screens/imageViewer'),
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": "Alternativtext"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -82,7 +82,8 @@
|
|||||||
"unsupported": {
|
"unsupported": {
|
||||||
"text": "Loading error",
|
"text": "Loading error",
|
||||||
"button": "Try remote link"
|
"button": "Try remote link"
|
||||||
}
|
},
|
||||||
|
"altText": "Alternative Text"
|
||||||
},
|
},
|
||||||
"avatar": {
|
"avatar": {
|
||||||
"accessibilityLabel": "Avatar of {{name}}",
|
"accessibilityLabel": "Avatar of {{name}}",
|
||||||
|
@ -3,7 +3,6 @@ export default {
|
|||||||
|
|
||||||
screens: require('./screens'),
|
screens: require('./screens'),
|
||||||
screenAccountSelection: require('./screens/accountSelection.json'),
|
screenAccountSelection: require('./screens/accountSelection.json'),
|
||||||
screenActions: require('./screens/actions'),
|
|
||||||
screenAnnouncements: require('./screens/announcements'),
|
screenAnnouncements: require('./screens/announcements'),
|
||||||
screenCompose: require('./screens/compose'),
|
screenCompose: require('./screens/compose'),
|
||||||
screenImageViewer: require('./screens/imageViewer'),
|
screenImageViewer: require('./screens/imageViewer'),
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": "Alternative Text"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -376,7 +376,11 @@
|
|||||||
"notFound": "Cannot find <bold>{{searchTerm}}</bold> related {{type}}"
|
"notFound": "Cannot find <bold>{{searchTerm}}</bold> related {{type}}"
|
||||||
},
|
},
|
||||||
"toot": {
|
"toot": {
|
||||||
"name": "Discussions"
|
"name": "Discussions",
|
||||||
|
"remoteFetch": {
|
||||||
|
"title": "Contains remote content",
|
||||||
|
"message": "Federated content are not always available on local instance. These content are fetched from remote instance and marked. You can interact with these content as usual."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"accounts": {
|
"accounts": {
|
||||||
|
@ -2,7 +2,6 @@ export default {
|
|||||||
common: require('./common'),
|
common: require('./common'),
|
||||||
|
|
||||||
screens: require('./screens'),
|
screens: require('./screens'),
|
||||||
screenActions: require('./screens/actions'),
|
|
||||||
screenAnnouncements: require('./screens/announcements'),
|
screenAnnouncements: require('./screens/announcements'),
|
||||||
screenCompose: require('./screens/compose'),
|
screenCompose: require('./screens/compose'),
|
||||||
screenImageViewer: require('./screens/imageViewer'),
|
screenImageViewer: require('./screens/imageViewer'),
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": "Texto alternativo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ export default {
|
|||||||
common: require('./common'),
|
common: require('./common'),
|
||||||
|
|
||||||
screens: require('./screens'),
|
screens: require('./screens'),
|
||||||
screenActions: require('./screens/actions'),
|
|
||||||
screenAnnouncements: require('./screens/announcements'),
|
screenAnnouncements: require('./screens/announcements'),
|
||||||
screenCompose: require('./screens/compose'),
|
screenCompose: require('./screens/compose'),
|
||||||
screenImageViewer: require('./screens/imageViewer'),
|
screenImageViewer: require('./screens/imageViewer'),
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": "Texte de remplacement"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ export default {
|
|||||||
common: require('./common'),
|
common: require('./common'),
|
||||||
|
|
||||||
screens: require('./screens'),
|
screens: require('./screens'),
|
||||||
screenActions: require('./screens/actions'),
|
|
||||||
screenAnnouncements: require('./screens/announcements'),
|
screenAnnouncements: require('./screens/announcements'),
|
||||||
screenCompose: require('./screens/compose'),
|
screenCompose: require('./screens/compose'),
|
||||||
screenImageViewer: require('./screens/imageViewer'),
|
screenImageViewer: require('./screens/imageViewer'),
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": "Testo descrittivo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ export default {
|
|||||||
common: require('./common'),
|
common: require('./common'),
|
||||||
|
|
||||||
screens: require('./screens'),
|
screens: require('./screens'),
|
||||||
screenActions: require('./screens/actions'),
|
|
||||||
screenAnnouncements: require('./screens/announcements'),
|
screenAnnouncements: require('./screens/announcements'),
|
||||||
screenCompose: require('./screens/compose'),
|
screenCompose: require('./screens/compose'),
|
||||||
screenImageViewer: require('./screens/imageViewer'),
|
screenImageViewer: require('./screens/imageViewer'),
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": "代替テキスト"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ export default {
|
|||||||
common: require('./common'),
|
common: require('./common'),
|
||||||
|
|
||||||
screens: require('./screens'),
|
screens: require('./screens'),
|
||||||
screenActions: require('./screens/actions'),
|
|
||||||
screenAnnouncements: require('./screens/announcements'),
|
screenAnnouncements: require('./screens/announcements'),
|
||||||
screenCompose: require('./screens/compose'),
|
screenCompose: require('./screens/compose'),
|
||||||
screenImageViewer: require('./screens/imageViewer'),
|
screenImageViewer: require('./screens/imageViewer'),
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": "대체 텍스트"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ export default {
|
|||||||
common: require('./common'),
|
common: require('./common'),
|
||||||
|
|
||||||
screens: require('./screens'),
|
screens: require('./screens'),
|
||||||
screenActions: require('./screens/actions'),
|
|
||||||
screenAnnouncements: require('./screens/announcements'),
|
screenAnnouncements: require('./screens/announcements'),
|
||||||
screenCompose: require('./screens/compose'),
|
screenCompose: require('./screens/compose'),
|
||||||
screenImageViewer: require('./screens/imageViewer'),
|
screenImageViewer: require('./screens/imageViewer'),
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": "Alternatieve tekst"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ export default {
|
|||||||
common: require('./common'),
|
common: require('./common'),
|
||||||
|
|
||||||
screens: require('./screens'),
|
screens: require('./screens'),
|
||||||
screenActions: require('./screens/actions'),
|
|
||||||
screenAnnouncements: require('./screens/announcements'),
|
screenAnnouncements: require('./screens/announcements'),
|
||||||
screenCompose: require('./screens/compose'),
|
screenCompose: require('./screens/compose'),
|
||||||
screenImageViewer: require('./screens/imageViewer'),
|
screenImageViewer: require('./screens/imageViewer'),
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": "Texto Alternativo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": "Альтернативный текст"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ export default {
|
|||||||
common: require('./common'),
|
common: require('./common'),
|
||||||
|
|
||||||
screens: require('./screens'),
|
screens: require('./screens'),
|
||||||
screenActions: require('./screens/actions'),
|
|
||||||
screenAnnouncements: require('./screens/announcements'),
|
screenAnnouncements: require('./screens/announcements'),
|
||||||
screenCompose: require('./screens/compose'),
|
screenCompose: require('./screens/compose'),
|
||||||
screenImageViewer: require('./screens/imageViewer'),
|
screenImageViewer: require('./screens/imageViewer'),
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": "Alternativtext"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ export default {
|
|||||||
common: require('./common'),
|
common: require('./common'),
|
||||||
|
|
||||||
screens: require('./screens'),
|
screens: require('./screens'),
|
||||||
screenActions: require('./screens/actions'),
|
|
||||||
screenAnnouncements: require('./screens/announcements'),
|
screenAnnouncements: require('./screens/announcements'),
|
||||||
screenCompose: require('./screens/compose'),
|
screenCompose: require('./screens/compose'),
|
||||||
screenImageViewer: require('./screens/imageViewer'),
|
screenImageViewer: require('./screens/imageViewer'),
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": "Альтернативний текст"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ export default {
|
|||||||
common: require('./common'),
|
common: require('./common'),
|
||||||
|
|
||||||
screens: require('./screens'),
|
screens: require('./screens'),
|
||||||
screenActions: require('./screens/actions'),
|
|
||||||
screenAnnouncements: require('./screens/announcements'),
|
screenAnnouncements: require('./screens/announcements'),
|
||||||
screenCompose: require('./screens/compose'),
|
screenCompose: require('./screens/compose'),
|
||||||
screenImageViewer: require('./screens/imageViewer'),
|
screenImageViewer: require('./screens/imageViewer'),
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": "Văn bản thay thế"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ export default {
|
|||||||
common: require('./common'),
|
common: require('./common'),
|
||||||
|
|
||||||
screens: require('./screens'),
|
screens: require('./screens'),
|
||||||
screenActions: require('./screens/actions'),
|
|
||||||
screenAnnouncements: require('./screens/announcements'),
|
screenAnnouncements: require('./screens/announcements'),
|
||||||
screenCompose: require('./screens/compose'),
|
screenCompose: require('./screens/compose'),
|
||||||
screenImageViewer: require('./screens/imageViewer'),
|
screenImageViewer: require('./screens/imageViewer'),
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": "替代文本"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ export default {
|
|||||||
common: require('./common'),
|
common: require('./common'),
|
||||||
|
|
||||||
screens: require('./screens'),
|
screens: require('./screens'),
|
||||||
screenActions: require('./screens/actions'),
|
|
||||||
screenAnnouncements: require('./screens/announcements'),
|
screenAnnouncements: require('./screens/announcements'),
|
||||||
screenCompose: require('./screens/compose'),
|
screenCompose: require('./screens/compose'),
|
||||||
screenImageViewer: require('./screens/imageViewer'),
|
screenImageViewer: require('./screens/imageViewer'),
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"content": {
|
|
||||||
"altText": {
|
|
||||||
"heading": "替代文字"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
|
||||||
import React, { useEffect } from 'react'
|
|
||||||
import { Dimensions, StyleSheet, View } from 'react-native'
|
|
||||||
import { PanGestureHandler, State, TapGestureHandler } from 'react-native-gesture-handler'
|
|
||||||
import Animated, {
|
|
||||||
Extrapolate,
|
|
||||||
interpolate,
|
|
||||||
runOnJS,
|
|
||||||
useAnimatedGestureHandler,
|
|
||||||
useAnimatedStyle,
|
|
||||||
useSharedValue,
|
|
||||||
withTiming
|
|
||||||
} from 'react-native-reanimated'
|
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
||||||
import ActionsAltText from './Actions/AltText'
|
|
||||||
|
|
||||||
const ScreenActions = ({
|
|
||||||
route: { params },
|
|
||||||
navigation
|
|
||||||
}: RootStackScreenProps<'Screen-Actions'>) => {
|
|
||||||
const { colors } = useTheme()
|
|
||||||
const insets = useSafeAreaInsets()
|
|
||||||
|
|
||||||
const DEFAULT_VALUE = 350
|
|
||||||
const screenHeight = Dimensions.get('window').height
|
|
||||||
const panY = useSharedValue(DEFAULT_VALUE)
|
|
||||||
useEffect(() => {
|
|
||||||
panY.value = withTiming(0)
|
|
||||||
}, [])
|
|
||||||
const styleTop = useAnimatedStyle(() => {
|
|
||||||
return {
|
|
||||||
bottom: interpolate(panY.value, [0, screenHeight], [0, -screenHeight], Extrapolate.CLAMP)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const dismiss = () => navigation.goBack()
|
|
||||||
|
|
||||||
const onGestureEvent = useAnimatedGestureHandler({
|
|
||||||
onActive: ({ translationY }) => {
|
|
||||||
panY.value = translationY
|
|
||||||
},
|
|
||||||
onEnd: ({ velocityY }) => {
|
|
||||||
if (velocityY > 500) {
|
|
||||||
runOnJS(dismiss)()
|
|
||||||
} else {
|
|
||||||
panY.value = withTiming(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const actions = () => {
|
|
||||||
switch (params.type) {
|
|
||||||
case 'alt_text':
|
|
||||||
return <ActionsAltText text={params.text} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Animated.View style={{ flex: 1 }}>
|
|
||||||
<TapGestureHandler
|
|
||||||
onHandlerStateChange={({ nativeEvent }) => {
|
|
||||||
if (nativeEvent.state === State.ACTIVE) {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Animated.View
|
|
||||||
style={[styles.overlay, { backgroundColor: colors.backgroundOverlayInvert }]}
|
|
||||||
>
|
|
||||||
<PanGestureHandler onGestureEvent={onGestureEvent}>
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
styles.container,
|
|
||||||
styleTop,
|
|
||||||
{
|
|
||||||
backgroundColor: colors.backgroundDefault,
|
|
||||||
paddingBottom: insets.bottom || StyleConstants.Spacing.L
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View style={[styles.handle, { backgroundColor: colors.primaryOverlay }]} />
|
|
||||||
{actions()}
|
|
||||||
</Animated.View>
|
|
||||||
</PanGestureHandler>
|
|
||||||
</Animated.View>
|
|
||||||
</TapGestureHandler>
|
|
||||||
</Animated.View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
overlay: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'flex-end'
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
paddingTop: StyleConstants.Spacing.M
|
|
||||||
},
|
|
||||||
handle: {
|
|
||||||
alignSelf: 'center',
|
|
||||||
width: StyleConstants.Spacing.S * 8,
|
|
||||||
height: StyleConstants.Spacing.S / 2,
|
|
||||||
borderRadius: 100,
|
|
||||||
top: -StyleConstants.Spacing.M * 2
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default ScreenActions
|
|
@ -1,42 +0,0 @@
|
|||||||
import Button from '@components/Button'
|
|
||||||
import MenuContainer from '@components/Menu/Container'
|
|
||||||
import MenuHeader from '@components/Menu/Header'
|
|
||||||
import CustomText from '@components/Text'
|
|
||||||
import { useNavigation } from '@react-navigation/native'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { Dimensions } from 'react-native'
|
|
||||||
import { ScrollView } from 'react-native-gesture-handler'
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
text: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const ActionsAltText: React.FC<Props> = ({ text }) => {
|
|
||||||
const navigation = useNavigation()
|
|
||||||
const { t } = useTranslation(['common', 'screenActions'])
|
|
||||||
const { colors } = useTheme()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<MenuContainer>
|
|
||||||
<MenuHeader heading={t(`screenActions:content.altText.heading`)} />
|
|
||||||
<ScrollView style={{ maxHeight: Dimensions.get('window').height / 2 }}>
|
|
||||||
<CustomText style={{ color: colors.primaryDefault }}>{text}</CustomText>
|
|
||||||
</ScrollView>
|
|
||||||
</MenuContainer>
|
|
||||||
<Button
|
|
||||||
type='text'
|
|
||||||
content={t('common:buttons.OK')}
|
|
||||||
onPress={() => navigation.goBack()}
|
|
||||||
style={{
|
|
||||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ActionsAltText
|
|
@ -141,6 +141,9 @@ const ComposeActions: React.FC = () => {
|
|||||||
<View
|
<View
|
||||||
accessibilityRole='toolbar'
|
accessibilityRole='toolbar'
|
||||||
style={{
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
width: '100%',
|
||||||
height: 45,
|
height: 45,
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
import ComponentAccount from '@components/Account'
|
|
||||||
import haptics from '@components/haptics'
|
|
||||||
import ComponentHashtag from '@components/Hashtag'
|
|
||||||
import React, { useContext, useEffect } from 'react'
|
|
||||||
import ComposeContext from '../utils/createContext'
|
|
||||||
import { formatText } from '../utils/processText'
|
|
||||||
|
|
||||||
type Props = { item: Mastodon.Account & Mastodon.Tag }
|
|
||||||
|
|
||||||
const ComposeRootSuggestion: React.FC<Props> = ({ item }) => {
|
|
||||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (composeState.text.raw.length === 0) {
|
|
||||||
composeDispatch({ type: 'tag', payload: undefined })
|
|
||||||
}
|
|
||||||
}, [composeState.text.raw.length])
|
|
||||||
|
|
||||||
const onPress = () => {
|
|
||||||
const focusedInput = composeState.textInputFocus.current
|
|
||||||
const updatedText = (): string => {
|
|
||||||
const main = item.acct ? `@${item.acct}` : `#${item.name}`
|
|
||||||
const textInput = composeState.textInputFocus.current
|
|
||||||
if (composeState.tag) {
|
|
||||||
const contentFront = composeState[textInput].raw.slice(0, composeState.tag.index)
|
|
||||||
const contentRear = composeState[textInput].raw.slice(composeState.tag.lastIndex)
|
|
||||||
|
|
||||||
const spaceFront =
|
|
||||||
composeState[textInput].raw.length === 0 || /\s/g.test(contentFront.slice(-1)) ? '' : ' '
|
|
||||||
const spaceRear = /\s/g.test(contentRear[0]) ? '' : ' '
|
|
||||||
|
|
||||||
return [contentFront, spaceFront, main, spaceRear, contentRear].join('')
|
|
||||||
} else {
|
|
||||||
return composeState[textInput].raw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
formatText({
|
|
||||||
textInput: focusedInput,
|
|
||||||
composeDispatch,
|
|
||||||
content: updatedText(),
|
|
||||||
disableDebounce: true
|
|
||||||
})
|
|
||||||
haptics('Light')
|
|
||||||
}
|
|
||||||
|
|
||||||
return item.acct ? (
|
|
||||||
<ComponentAccount account={item} props={{ onPress }} />
|
|
||||||
) : (
|
|
||||||
<ComponentHashtag hashtag={item} onPress={onPress} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ComposeRootSuggestion
|
|
135
src/screens/Compose/Root/Suggestions.tsx
Normal file
135
src/screens/Compose/Root/Suggestions.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import ComponentAccount from '@components/Account'
|
||||||
|
import haptics from '@components/haptics'
|
||||||
|
import ComponentHashtag from '@components/Hashtag'
|
||||||
|
import Icon from '@components/Icon'
|
||||||
|
import ComponentSeparator from '@components/Separator'
|
||||||
|
import { useSearchQuery } from '@utils/queryHooks/search'
|
||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
import React, { Fragment, useContext, useEffect } from 'react'
|
||||||
|
import { View } from 'react-native'
|
||||||
|
import { Circle } from 'react-native-animated-spinkit'
|
||||||
|
import ComposeContext from '../utils/createContext'
|
||||||
|
import { formatText } from '../utils/processText'
|
||||||
|
|
||||||
|
const ComposeRootSuggestions: React.FC = () => {
|
||||||
|
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||||
|
|
||||||
|
const { colors } = useTheme()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (composeState.text.raw.length === 0) {
|
||||||
|
composeDispatch({ type: 'tag', payload: undefined })
|
||||||
|
}
|
||||||
|
}, [composeState.text.raw.length])
|
||||||
|
|
||||||
|
const mapSchemaToType = () => {
|
||||||
|
if (composeState.tag) {
|
||||||
|
switch (composeState.tag?.schema) {
|
||||||
|
case '@':
|
||||||
|
return 'accounts'
|
||||||
|
case '#':
|
||||||
|
return 'hashtags'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { isFetching, data, refetch } = useSearchQuery({
|
||||||
|
type: mapSchemaToType(),
|
||||||
|
term: composeState.tag?.raw.substring(1),
|
||||||
|
limit: 10,
|
||||||
|
options: { enabled: false }
|
||||||
|
})
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
(composeState.tag?.schema === '@' || composeState.tag?.schema === '#') &&
|
||||||
|
composeState.tag?.raw
|
||||||
|
) {
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
}, [composeState.tag])
|
||||||
|
|
||||||
|
const onPress = (content: string) => {
|
||||||
|
const focusedInput = composeState.textInputFocus.current
|
||||||
|
const updatedText = (): string => {
|
||||||
|
const textInput = composeState.textInputFocus.current
|
||||||
|
if (composeState.tag) {
|
||||||
|
const contentFront = composeState[textInput].raw.slice(0, composeState.tag.index)
|
||||||
|
const contentRear = composeState[textInput].raw.slice(composeState.tag.lastIndex)
|
||||||
|
|
||||||
|
const spaceFront =
|
||||||
|
contentFront.length === 0 ||
|
||||||
|
composeState[textInput].raw.length === 0 ||
|
||||||
|
/\s/g.test(contentFront.slice(-1))
|
||||||
|
? ''
|
||||||
|
: ' '
|
||||||
|
const spaceRear = /\s/g.test(contentRear[0]) ? '' : ' '
|
||||||
|
|
||||||
|
return [contentFront, spaceFront, content, spaceRear, contentRear].join('')
|
||||||
|
} else {
|
||||||
|
return composeState[textInput].raw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formatText({
|
||||||
|
textInput: focusedInput,
|
||||||
|
composeDispatch,
|
||||||
|
content: updatedText(),
|
||||||
|
disableDebounce: true
|
||||||
|
})
|
||||||
|
haptics('Light')
|
||||||
|
}
|
||||||
|
|
||||||
|
const main = () => {
|
||||||
|
if (composeState.tag) {
|
||||||
|
switch (composeState.tag?.schema) {
|
||||||
|
case '@':
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{data?.accounts?.map(account => (
|
||||||
|
<Fragment key={account.id}>
|
||||||
|
<ComponentAccount
|
||||||
|
account={account}
|
||||||
|
props={{ onPress: () => onPress(`@${account.acct}`) }}
|
||||||
|
children={
|
||||||
|
<Icon
|
||||||
|
name='Plus'
|
||||||
|
size={StyleConstants.Font.Size.L}
|
||||||
|
color={colors.secondary}
|
||||||
|
style={{ marginLeft: 8 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ComponentSeparator />
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
case '#':
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{data?.hashtags?.map(hashtag => (
|
||||||
|
<Fragment key={hashtag.name}>
|
||||||
|
<ComponentHashtag hashtag={hashtag} onPress={() => onPress(`#${hashtag.name}`)} />
|
||||||
|
<ComponentSeparator />
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isFetching ? (
|
||||||
|
<View
|
||||||
|
key='listEmpty'
|
||||||
|
style={{ flex: 1, alignItems: 'center', marginVertical: StyleConstants.Spacing.M }}
|
||||||
|
>
|
||||||
|
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<>{main()}</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ComposeRootSuggestions
|
@ -1,21 +1,13 @@
|
|||||||
import ComponentSeparator from '@components/Separator'
|
import React, { useEffect, useRef } from 'react'
|
||||||
import { useSearchQuery } from '@utils/queryHooks/search'
|
import { AccessibilityInfo, findNodeHandle, ScrollView, View } from 'react-native'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
|
||||||
import React, { useContext, useEffect, useRef } from 'react'
|
|
||||||
import { AccessibilityInfo, findNodeHandle, FlatList, View } from 'react-native'
|
|
||||||
import { Circle } from 'react-native-animated-spinkit'
|
|
||||||
import ComposePosting from '../Posting'
|
import ComposePosting from '../Posting'
|
||||||
import ComposeContext from '../utils/createContext'
|
|
||||||
import ComposeActions from './Actions'
|
import ComposeActions from './Actions'
|
||||||
import ComposeDrafts from './Drafts'
|
import ComposeDrafts from './Drafts'
|
||||||
import ComposeRootFooter from './Footer'
|
import ComposeRootFooter from './Footer'
|
||||||
import ComposeRootHeader from './Header'
|
import ComposeRootHeader from './Header'
|
||||||
import ComposeRootSuggestion from './Suggestion'
|
import ComposeRootSuggestion from './Suggestions'
|
||||||
|
|
||||||
const ComposeRoot = () => {
|
const ComposeRoot = () => {
|
||||||
const { colors } = useTheme()
|
|
||||||
|
|
||||||
const accessibleRefDrafts = useRef(null)
|
const accessibleRefDrafts = useRef(null)
|
||||||
const accessibleRefAttachments = useRef(null)
|
const accessibleRefAttachments = useRef(null)
|
||||||
|
|
||||||
@ -24,60 +16,17 @@ const ComposeRoot = () => {
|
|||||||
tagDrafts && AccessibilityInfo.setAccessibilityFocus(tagDrafts)
|
tagDrafts && AccessibilityInfo.setAccessibilityFocus(tagDrafts)
|
||||||
}, [accessibleRefDrafts.current])
|
}, [accessibleRefDrafts.current])
|
||||||
|
|
||||||
const { composeState } = useContext(ComposeContext)
|
|
||||||
|
|
||||||
const mapSchemaToType = () => {
|
|
||||||
if (composeState.tag) {
|
|
||||||
switch (composeState.tag?.schema) {
|
|
||||||
case '@':
|
|
||||||
return 'accounts'
|
|
||||||
case '#':
|
|
||||||
return 'hashtags'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { isFetching, data, refetch } = useSearchQuery({
|
|
||||||
type: mapSchemaToType(),
|
|
||||||
term: composeState.tag?.raw.substring(1),
|
|
||||||
options: { enabled: false }
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
(composeState.tag?.schema === '@' || composeState.tag?.schema === '#') &&
|
|
||||||
composeState.tag?.raw
|
|
||||||
) {
|
|
||||||
refetch()
|
|
||||||
}
|
|
||||||
}, [composeState.tag])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<>
|
||||||
<FlatList
|
<ScrollView keyboardShouldPersistTaps='always' style={{ marginBottom: 50 }}>
|
||||||
renderItem={({ item }) => <ComposeRootSuggestion item={item} />}
|
<ComposeRootHeader />
|
||||||
ListEmptyComponent={
|
<ComposeRootSuggestion />
|
||||||
isFetching ? (
|
|
||||||
<View key='listEmpty' style={{ flex: 1, alignItems: 'center' }}>
|
|
||||||
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
|
||||||
</View>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
keyboardShouldPersistTaps='always'
|
|
||||||
ListHeaderComponent={ComposeRootHeader}
|
|
||||||
ListFooterComponent={
|
|
||||||
<ComposeRootFooter accessibleRefAttachments={accessibleRefAttachments} />
|
<ComposeRootFooter accessibleRefAttachments={accessibleRefAttachments} />
|
||||||
}
|
</ScrollView>
|
||||||
ItemSeparatorComponent={ComponentSeparator}
|
|
||||||
// @ts-ignore
|
|
||||||
data={data ? data[mapSchemaToType()] : undefined}
|
|
||||||
keyExtractor={() => Math.random().toString()}
|
|
||||||
/>
|
|
||||||
<ComposeActions />
|
|
||||||
<ComposeDrafts accessibleRefDrafts={accessibleRefDrafts} />
|
<ComposeDrafts accessibleRefDrafts={accessibleRefDrafts} />
|
||||||
<ComposePosting />
|
<ComposePosting />
|
||||||
</View>
|
<ComposeActions />
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import { handleError } from '@utils/api/helpers'
|
|||||||
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { useInstanceQuery } from '@utils/queryHooks/instance'
|
import { useInstanceQuery } from '@utils/queryHooks/instance'
|
||||||
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
|
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
|
||||||
|
import { searchFetchToot, SearchResult } from '@utils/queryHooks/search'
|
||||||
import { useTimelineMutation } from '@utils/queryHooks/timeline'
|
import { useTimelineMutation } from '@utils/queryHooks/timeline'
|
||||||
import {
|
import {
|
||||||
getAccountStorage,
|
getAccountStorage,
|
||||||
@ -155,6 +156,11 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
|||||||
content: params.accts.map(acct => `@${acct}`).join(' ') + ' ',
|
content: params.accts.map(acct => `@${acct}`).join(' ') + ' ',
|
||||||
disableDebounce: true
|
disableDebounce: true
|
||||||
})
|
})
|
||||||
|
searchFetchToot(params.incomingStatus.uri).then(status => {
|
||||||
|
if (status?.uri === params.incomingStatus.uri) {
|
||||||
|
composeDispatch({ type: 'updateReply', payload: status })
|
||||||
|
}
|
||||||
|
})
|
||||||
break
|
break
|
||||||
case 'conversation':
|
case 'conversation':
|
||||||
formatText({
|
formatText({
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import { ComposeAction, ComposeState } from './types'
|
import { ComposeAction, ComposeState } from './types'
|
||||||
|
|
||||||
const composeReducer = (
|
const composeReducer = (state: ComposeState, action: ComposeAction): ComposeState => {
|
||||||
state: ComposeState,
|
|
||||||
action: ComposeAction
|
|
||||||
): ComposeState => {
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'loadDraft':
|
case 'loadDraft':
|
||||||
const draft = action.payload
|
const draft = action.payload
|
||||||
@ -67,9 +64,7 @@ const composeReducer = (
|
|||||||
...state,
|
...state,
|
||||||
attachments: {
|
attachments: {
|
||||||
...state.attachments,
|
...state.attachments,
|
||||||
uploads: state.attachments.uploads.filter(
|
uploads: state.attachments.uploads.filter(upload => upload.local?.hash !== action.payload)
|
||||||
upload => upload.local?.hash !== action.payload
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'attachment/delete':
|
case 'attachment/delete':
|
||||||
@ -77,9 +72,7 @@ const composeReducer = (
|
|||||||
...state,
|
...state,
|
||||||
attachments: {
|
attachments: {
|
||||||
...state.attachments,
|
...state.attachments,
|
||||||
uploads: state.attachments.uploads.filter(
|
uploads: state.attachments.uploads.filter(upload => upload.remote?.id !== action.payload)
|
||||||
upload => upload.remote?.id !== action.payload
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'attachment/edit':
|
case 'attachment/edit':
|
||||||
@ -101,6 +94,8 @@ const composeReducer = (
|
|||||||
...state,
|
...state,
|
||||||
textInputFocus: { ...state.textInputFocus, ...action.payload }
|
textInputFocus: { ...state.textInputFocus, ...action.payload }
|
||||||
}
|
}
|
||||||
|
case 'updateReply':
|
||||||
|
return { ...state, replyToStatus: action.payload }
|
||||||
case 'removeReply':
|
case 'removeReply':
|
||||||
return { ...state, replyToStatus: undefined }
|
return { ...state, replyToStatus: undefined }
|
||||||
default:
|
default:
|
||||||
|
4
src/screens/Compose/utils/types.d.ts
vendored
4
src/screens/Compose/utils/types.d.ts
vendored
@ -126,6 +126,10 @@ export type ComposeAction =
|
|||||||
type: 'textInputFocus'
|
type: 'textInputFocus'
|
||||||
payload: Partial<ComposeState['textInputFocus']>
|
payload: Partial<ComposeState['textInputFocus']>
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'updateReply'
|
||||||
|
payload: Mastodon.Status
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'removeReply'
|
type: 'removeReply'
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,11 @@ import AccountHeader from '@screens/Tabs/Shared/Account/Header'
|
|||||||
import AccountInformation from '@screens/Tabs/Shared/Account/Information'
|
import AccountInformation from '@screens/Tabs/Shared/Account/Information'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
export interface Props {
|
const MyInfo: React.FC = () => {
|
||||||
account: Mastodon.Account | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const MyInfo: React.FC<Props> = ({ account }) => {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AccountHeader account={account} />
|
<AccountHeader />
|
||||||
<AccountInformation account={account} />
|
<AccountInformation />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,11 @@ import Logout from '@screens/Tabs/Me/Root/Logout'
|
|||||||
import MyInfo from '@screens/Tabs/Me/Root/MyInfo'
|
import MyInfo from '@screens/Tabs/Me/Root/MyInfo'
|
||||||
import Settings from '@screens/Tabs/Me/Root/Settings'
|
import Settings from '@screens/Tabs/Me/Root/Settings'
|
||||||
import AccountInformationSwitch from '@screens/Tabs/Me/Root/Switch'
|
import AccountInformationSwitch from '@screens/Tabs/Me/Root/Switch'
|
||||||
|
import AccountContext from '@screens/Tabs/Shared/Account/Context'
|
||||||
import AccountNav from '@screens/Tabs/Shared/Account/Nav'
|
import AccountNav from '@screens/Tabs/Shared/Account/Nav'
|
||||||
import AccountContext from '@screens/Tabs/Shared/Account/utils/createContext'
|
|
||||||
import accountInitialState from '@screens/Tabs/Shared/Account/utils/initialState'
|
|
||||||
import accountReducer from '@screens/Tabs/Shared/Account/utils/reducer'
|
|
||||||
import { useProfileQuery } from '@utils/queryHooks/profile'
|
import { useProfileQuery } from '@utils/queryHooks/profile'
|
||||||
import { useGlobalStorage } from '@utils/storage/actions'
|
import { useGlobalStorage } from '@utils/storage/actions'
|
||||||
import React, { useReducer, useRef } from 'react'
|
import React, { useRef } from 'react'
|
||||||
import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
|
import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
|
||||||
|
|
||||||
const TabMeRoot: React.FC = () => {
|
const TabMeRoot: React.FC = () => {
|
||||||
@ -24,23 +22,21 @@ const TabMeRoot: React.FC = () => {
|
|||||||
const scrollRef = useRef<Animated.ScrollView>(null)
|
const scrollRef = useRef<Animated.ScrollView>(null)
|
||||||
useScrollToTop(scrollRef)
|
useScrollToTop(scrollRef)
|
||||||
|
|
||||||
const [accountState, accountDispatch] = useReducer(accountReducer, accountInitialState)
|
|
||||||
|
|
||||||
const scrollY = useSharedValue(0)
|
const scrollY = useSharedValue(0)
|
||||||
const onScroll = useAnimatedScrollHandler(event => {
|
const onScroll = useAnimatedScrollHandler(event => {
|
||||||
scrollY.value = event.contentOffset.y
|
scrollY.value = event.contentOffset.y
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccountContext.Provider value={{ accountState, accountDispatch }}>
|
<AccountContext.Provider value={{ account: data, pageMe: true }}>
|
||||||
{accountActive && data ? <AccountNav scrollY={scrollY} account={data} /> : null}
|
{accountActive && data ? <AccountNav scrollY={scrollY} /> : null}
|
||||||
<Animated.ScrollView
|
<Animated.ScrollView
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
keyboardShouldPersistTaps='handled'
|
keyboardShouldPersistTaps='handled'
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
>
|
>
|
||||||
{accountActive ? <MyInfo account={data} /> : <ComponentInstance />}
|
{accountActive ? <MyInfo /> : <ComponentInstance />}
|
||||||
{accountActive ? <Collections /> : null}
|
{accountActive ? <Collections /> : null}
|
||||||
<Settings />
|
<Settings />
|
||||||
{accountActive ? <AccountInformationSwitch /> : null}
|
{accountActive ? <AccountInformationSwitch /> : null}
|
||||||
|
@ -7,17 +7,15 @@ import { useTimelineQuery } from '@utils/queryHooks/timeline'
|
|||||||
import { flattenPages } from '@utils/queryHooks/utils'
|
import { flattenPages } from '@utils/queryHooks/utils'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { Dimensions, Pressable, View } from 'react-native'
|
import { Dimensions, Pressable, View } from 'react-native'
|
||||||
import { FlatList } from 'react-native-gesture-handler'
|
import { FlatList } from 'react-native-gesture-handler'
|
||||||
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
|
import AccountContext from './Context'
|
||||||
|
|
||||||
export interface Props {
|
const AccountAttachments: React.FC = () => {
|
||||||
account: Mastodon.Account | undefined
|
const { account } = useContext(AccountContext)
|
||||||
}
|
|
||||||
|
|
||||||
const AccountAttachments: React.FC<Props> = ({ account }) => {
|
if (account?.suspended) return null
|
||||||
if (!account) return null
|
|
||||||
|
|
||||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
@ -28,30 +26,28 @@ const AccountAttachments: React.FC<Props> = ({ account }) => {
|
|||||||
|
|
||||||
const { data } = useTimelineQuery({
|
const { data } = useTimelineQuery({
|
||||||
page: 'Account',
|
page: 'Account',
|
||||||
account: account.id,
|
id: account?.id,
|
||||||
exclude_reblogs: false,
|
exclude_reblogs: false,
|
||||||
only_media: true
|
only_media: true,
|
||||||
|
options: { enabled: !!account?.id }
|
||||||
})
|
})
|
||||||
|
|
||||||
const flattenData = flattenPages(data)
|
const flattenData = flattenPages(data)
|
||||||
.filter(status => !(status as Mastodon.Status).sensitive)
|
.filter(status => !(status as Mastodon.Status).sensitive)
|
||||||
.splice(0, DISPLAY_AMOUNT)
|
.splice(0, DISPLAY_AMOUNT)
|
||||||
|
|
||||||
const styleContainer = useAnimatedStyle(() => {
|
if (!flattenData.length) return null
|
||||||
if (flattenData.length) {
|
|
||||||
return {
|
return (
|
||||||
height: withTiming(width + StyleConstants.Spacing.Global.PagePadding * 2),
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
height: width + StyleConstants.Spacing.Global.PagePadding * 2,
|
||||||
paddingVertical: StyleConstants.Spacing.Global.PagePadding,
|
paddingVertical: StyleConstants.Spacing.Global.PagePadding,
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: colors.border
|
borderTopColor: colors.border
|
||||||
}
|
}}
|
||||||
} else {
|
>
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}, [flattenData.length])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Animated.View style={[{ flex: 1 }, styleContainer]}>
|
|
||||||
<FlatList
|
<FlatList
|
||||||
horizontal
|
horizontal
|
||||||
data={flattenData}
|
data={flattenData}
|
||||||
@ -103,7 +99,7 @@ const AccountAttachments: React.FC<Props> = ({ account }) => {
|
|||||||
}}
|
}}
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
9
src/screens/Tabs/Shared/Account/Context.tsx
Normal file
9
src/screens/Tabs/Shared/Account/Context.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { createContext } from 'react'
|
||||||
|
|
||||||
|
type AccountContextType = {
|
||||||
|
account?: Mastodon.Account
|
||||||
|
pageMe?: boolean
|
||||||
|
}
|
||||||
|
const AccountContext = createContext<AccountContextType>({} as AccountContextType)
|
||||||
|
|
||||||
|
export default AccountContext
|
@ -1,15 +1,14 @@
|
|||||||
import GracefullyImage from '@components/GracefullyImage'
|
import GracefullyImage from '@components/GracefullyImage'
|
||||||
import navigationRef from '@utils/navigation/navigationRef'
|
import navigationRef from '@utils/navigation/navigationRef'
|
||||||
import { useGlobalStorage } from '@utils/storage/actions'
|
import { useGlobalStorage } from '@utils/storage/actions'
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { Dimensions, Image } from 'react-native'
|
import { Dimensions, Image } from 'react-native'
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
|
import AccountContext from './Context'
|
||||||
|
|
||||||
export interface Props {
|
const AccountHeader: React.FC = () => {
|
||||||
account?: Mastodon.Account
|
const { account } = useContext(AccountContext)
|
||||||
}
|
|
||||||
|
|
||||||
const AccountHeader: React.FC<Props> = ({ account }) => {
|
|
||||||
const topInset = useSafeAreaInsets().top
|
const topInset = useSafeAreaInsets().top
|
||||||
|
|
||||||
useGlobalStorage.string('account.active')
|
useGlobalStorage.string('account.active')
|
||||||
|
@ -13,11 +13,7 @@ import AccountInformationName from './Information/Name'
|
|||||||
import AccountInformationNote from './Information/Note'
|
import AccountInformationNote from './Information/Note'
|
||||||
import AccountInformationStats from './Information/Stats'
|
import AccountInformationStats from './Information/Stats'
|
||||||
|
|
||||||
export interface Props {
|
const AccountInformation: React.FC = () => {
|
||||||
account: Mastodon.Account | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const AccountInformation: React.FC<Props> = ({ account }) => {
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const { name } = useRoute()
|
const { name } = useRoute()
|
||||||
@ -31,21 +27,21 @@ const AccountInformation: React.FC<Props> = ({ account }) => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<View style={styles.avatarAndActions}>
|
<View style={styles.avatarAndActions}>
|
||||||
<AccountInformationAvatar account={account} myInfo={myInfo} />
|
<AccountInformationAvatar />
|
||||||
<AccountInformationActions account={account} myInfo={myInfo} />
|
<AccountInformationActions />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<AccountInformationName account={account} />
|
<AccountInformationName />
|
||||||
|
|
||||||
<AccountInformationAccount account={account} myInfo={myInfo} />
|
<AccountInformationAccount />
|
||||||
|
|
||||||
<AccountInformationFields account={account} myInfo={myInfo} />
|
<AccountInformationFields />
|
||||||
|
|
||||||
<AccountInformationNote account={account} myInfo={myInfo} />
|
<AccountInformationNote />
|
||||||
|
|
||||||
<AccountInformationCreated account={account} hidden={myInfo} />
|
<AccountInformationCreated />
|
||||||
|
|
||||||
<AccountInformationStats account={account} myInfo={myInfo} />
|
<AccountInformationStats />
|
||||||
</Placeholder>
|
</Placeholder>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
@ -4,31 +4,26 @@ import { useRelationshipQuery } from '@utils/queryHooks/relationship'
|
|||||||
import { getAccountStorage, useAccountStorage } from '@utils/storage/actions'
|
import { getAccountStorage, useAccountStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { PlaceholderLine } from 'rn-placeholder'
|
import { PlaceholderLine } from 'rn-placeholder'
|
||||||
|
import AccountContext from '../Context'
|
||||||
|
|
||||||
export interface Props {
|
const AccountInformationAccount: React.FC = () => {
|
||||||
account: Mastodon.Account | undefined
|
const { account, pageMe } = useContext(AccountContext)
|
||||||
myInfo?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
|
|
||||||
const { t } = useTranslation('screenTabs')
|
const { t } = useTranslation('screenTabs')
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const [acct] = useAccountStorage.string('auth.account.acct')
|
const [acct] = useAccountStorage.string('auth.account.acct')
|
||||||
const domain = getAccountStorage.string('auth.account.domain')
|
const domain = getAccountStorage.string('auth.account.domain')
|
||||||
|
|
||||||
const { data: relationship } = useRelationshipQuery({
|
const { data: relationship } = useRelationshipQuery({ id: account?.id })
|
||||||
id: account?.id || '',
|
|
||||||
options: { enabled: account !== undefined }
|
|
||||||
})
|
|
||||||
|
|
||||||
const localInstance = account?.acct.includes('@') ? account?.acct.includes(`@${domain}`) : true
|
const localInstance = account?.acct.includes('@') ? account?.acct.includes(`@${domain}`) : true
|
||||||
|
|
||||||
if (account || localInstance) {
|
if (account || pageMe) {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@ -51,7 +46,7 @@ const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
|
|||||||
}}
|
}}
|
||||||
selectable
|
selectable
|
||||||
>
|
>
|
||||||
@{myInfo ? acct : account?.acct}
|
@{pageMe ? acct : account?.acct}
|
||||||
{localInstance ? `@${domain}` : null}
|
{localInstance ? `@${domain}` : null}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
{relationship?.followed_by ? t('shared.account.followed_by') : null}
|
{relationship?.followed_by ? t('shared.account.followed_by') : null}
|
||||||
|
@ -5,17 +5,15 @@ import { useNavigation } from '@react-navigation/native'
|
|||||||
import { useRelationshipQuery } from '@utils/queryHooks/relationship'
|
import { useRelationshipQuery } from '@utils/queryHooks/relationship'
|
||||||
import { useAccountStorage } from '@utils/storage/actions'
|
import { useAccountStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { StyleSheet, View } from 'react-native'
|
||||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||||
|
import AccountContext from '../Context'
|
||||||
|
|
||||||
export interface Props {
|
const AccountInformationActions: React.FC = () => {
|
||||||
account: Mastodon.Account | undefined
|
const { account, pageMe } = useContext(AccountContext)
|
||||||
myInfo?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const AccountInformationActions: React.FC<Props> = ({ account, myInfo }) => {
|
|
||||||
if (!account || account.suspended) {
|
if (!account || account.suspended) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -36,7 +34,7 @@ const AccountInformationActions: React.FC<Props> = ({ account, myInfo }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (myInfo) {
|
if (pageMe) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.base}>
|
<View style={styles.base}>
|
||||||
<Button
|
<Button
|
||||||
@ -76,7 +74,7 @@ const AccountInformationActions: React.FC<Props> = ({ account, myInfo }) => {
|
|||||||
{mGroup.map(menu => (
|
{mGroup.map(menu => (
|
||||||
<DropdownMenu.Item key={menu.key} {...menu.item}>
|
<DropdownMenu.Item key={menu.key} {...menu.item}>
|
||||||
<DropdownMenu.ItemTitle children={menu.title} />
|
<DropdownMenu.ItemTitle children={menu.title} />
|
||||||
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
|
<DropdownMenu.ItemIcon ios={{ name: menu.icon }} />
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.Group>
|
</DropdownMenu.Group>
|
||||||
|
@ -5,14 +5,12 @@ import navigationRef from '@utils/navigation/navigationRef'
|
|||||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
import { useGlobalStorage } from '@utils/storage/actions'
|
import { useGlobalStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
|
import AccountContext from '../Context'
|
||||||
|
|
||||||
export interface Props {
|
const AccountInformationAvatar: React.FC = () => {
|
||||||
account: Mastodon.Account | undefined
|
const { account, pageMe } = useContext(AccountContext)
|
||||||
myInfo: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo }) => {
|
|
||||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
|
|
||||||
useGlobalStorage.string('account.active')
|
useGlobalStorage.string('account.active')
|
||||||
@ -29,7 +27,7 @@ const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo }) => {
|
|||||||
uri={{ original: account?.avatar, static: account?.avatar_static }}
|
uri={{ original: account?.avatar, static: account?.avatar_static }}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (account) {
|
if (account) {
|
||||||
if (myInfo) {
|
if (pageMe) {
|
||||||
navigation.push('Tab-Shared-Account', { account })
|
navigation.push('Tab-Shared-Account', { account })
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
|
@ -2,18 +2,16 @@ import Icon from '@components/Icon'
|
|||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { PlaceholderLine } from 'rn-placeholder'
|
import { PlaceholderLine } from 'rn-placeholder'
|
||||||
|
import AccountContext from '../Context'
|
||||||
|
|
||||||
export interface Props {
|
const AccountInformationCreated: React.FC = () => {
|
||||||
account: Mastodon.Account | undefined
|
const { account, pageMe } = useContext(AccountContext)
|
||||||
hidden?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const AccountInformationCreated: React.FC<Props> = ({ account, hidden = false }) => {
|
if (pageMe) {
|
||||||
if (hidden) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import { ParseHTML } from '@components/Parse'
|
import { ParseHTML } from '@components/Parse'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { StyleSheet, View } from 'react-native'
|
||||||
|
import AccountContext from '../Context'
|
||||||
|
|
||||||
export interface Props {
|
const AccountInformationFields: React.FC = () => {
|
||||||
account: Mastodon.Account | undefined
|
const { account, pageMe } = useContext(AccountContext)
|
||||||
myInfo?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const AccountInformationFields: React.FC<Props> = ({ account, myInfo }) => {
|
if (account?.suspended || pageMe || !account?.fields || account.fields.length === 0) {
|
||||||
if (account?.suspended || myInfo || !account?.fields || account.fields.length === 0) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,15 +2,14 @@ import { ParseEmojis } from '@components/Parse'
|
|||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { PlaceholderLine } from 'rn-placeholder'
|
import { PlaceholderLine } from 'rn-placeholder'
|
||||||
|
import AccountContext from '../Context'
|
||||||
|
|
||||||
export interface Props {
|
const AccountInformationName: React.FC = () => {
|
||||||
account: Mastodon.Account | undefined
|
const { account } = useContext(AccountContext)
|
||||||
}
|
|
||||||
|
|
||||||
const AccountInformationName: React.FC<Props> = ({ account }) => {
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
import { ParseHTML } from '@components/Parse'
|
import { ParseHTML } from '@components/Parse'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
|
import AccountContext from '../Context'
|
||||||
|
|
||||||
export interface Props {
|
const AccountInformationNote: React.FC = () => {
|
||||||
account: Mastodon.Account | undefined
|
const { account, pageMe } = useContext(AccountContext)
|
||||||
myInfo?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const AccountInformationNote: React.FC<Props> = ({ account, myInfo }) => {
|
|
||||||
if (
|
if (
|
||||||
account?.suspended ||
|
account?.suspended ||
|
||||||
myInfo ||
|
pageMe ||
|
||||||
!account?.note ||
|
!account?.note ||
|
||||||
account.note.length === 0 ||
|
account.note.length === 0 ||
|
||||||
account.note === '<p></p>'
|
account.note === '<p></p>'
|
||||||
|
@ -4,17 +4,15 @@ import { StackNavigationProp } from '@react-navigation/stack'
|
|||||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { StyleSheet, View } from 'react-native'
|
||||||
import { PlaceholderLine } from 'rn-placeholder'
|
import { PlaceholderLine } from 'rn-placeholder'
|
||||||
|
import AccountContext from '../Context'
|
||||||
|
|
||||||
export interface Props {
|
const AccountInformationStats: React.FC = () => {
|
||||||
account: Mastodon.Account | undefined
|
const { account, pageMe } = useContext(AccountContext)
|
||||||
myInfo: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
|
|
||||||
if (account?.suspended) {
|
if (account?.suspended) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -32,7 +30,7 @@ const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
|
|||||||
count: account.statuses_count || 0
|
count: account.statuses_count || 0
|
||||||
})}
|
})}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
myInfo && account && navigation.push('Tab-Shared-Account', { account })
|
pageMe && account && navigation.push('Tab-Shared-Account', { account })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -2,17 +2,19 @@ import { ParseEmojis } from '@components/Parse'
|
|||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { Dimensions, StyleSheet, View } from 'react-native'
|
import { Dimensions, StyleSheet, View } from 'react-native'
|
||||||
import Animated, { Extrapolate, interpolate, useAnimatedStyle } from 'react-native-reanimated'
|
import Animated, { Extrapolate, interpolate, useAnimatedStyle } from 'react-native-reanimated'
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
|
import AccountContext from './Context'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
scrollY: Animated.SharedValue<number>
|
scrollY: Animated.SharedValue<number>
|
||||||
account: Mastodon.Account | undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountNav: React.FC<Props> = ({ scrollY, account }) => {
|
const AccountNav: React.FC<Props> = ({ scrollY }) => {
|
||||||
|
const { account } = useContext(AccountContext)
|
||||||
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const headerHeight = useSafeAreaInsets().top + 44
|
const headerHeight = useSafeAreaInsets().top + 44
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import { Text, View } from 'react-native'
|
|||||||
import { useSharedValue } from 'react-native-reanimated'
|
import { useSharedValue } from 'react-native-reanimated'
|
||||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||||
import AccountAttachments from './Attachments'
|
import AccountAttachments from './Attachments'
|
||||||
|
import AccountContext from './Context'
|
||||||
import AccountHeader from './Header'
|
import AccountHeader from './Header'
|
||||||
import AccountInformation from './Information'
|
import AccountInformation from './Information'
|
||||||
import AccountNav from './Nav'
|
import AccountNav from './Nav'
|
||||||
@ -23,14 +24,19 @@ import AccountNav from './Nav'
|
|||||||
const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>> = ({
|
const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>> = ({
|
||||||
navigation,
|
navigation,
|
||||||
route: {
|
route: {
|
||||||
params: { account }
|
params: { account, isRemote }
|
||||||
}
|
}
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('screenTabs')
|
const { t } = useTranslation('screenTabs')
|
||||||
const { colors, mode } = useTheme()
|
const { colors, mode } = useTheme()
|
||||||
|
|
||||||
const mShare = menuShare({ type: 'account', url: account.url })
|
const { data, dataUpdatedAt } = useAccountQuery({
|
||||||
const mAccount = menuAccount({ type: 'account', openChange: true, account })
|
id: account.id,
|
||||||
|
...(isRemote && { remoteUrl: account.url })
|
||||||
|
})
|
||||||
|
|
||||||
|
const mShare = menuShare({ type: 'account', url: data?.url })
|
||||||
|
const mAccount = menuAccount({ type: 'account', openChange: true, account: data })
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerTransparent: true,
|
headerTransparent: true,
|
||||||
@ -76,14 +82,12 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
|
|||||||
})
|
})
|
||||||
}, [mAccount])
|
}, [mAccount])
|
||||||
|
|
||||||
const { data } = useAccountQuery({ id: account.id })
|
|
||||||
|
|
||||||
const scrollY = useSharedValue(0)
|
const scrollY = useSharedValue(0)
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [queryKey, setQueryKey] = useState<QueryKeyTimeline>([
|
const [queryKey, setQueryKey] = useState<QueryKeyTimeline>([
|
||||||
'Timeline',
|
'Timeline',
|
||||||
{ page: 'Account', account: account.id, exclude_reblogs: true, only_media: false }
|
{ page: 'Account', id: data?.id, exclude_reblogs: true, only_media: false }
|
||||||
])
|
])
|
||||||
const page = queryKey[1]
|
const page = queryKey[1]
|
||||||
|
|
||||||
@ -92,9 +96,9 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View style={{ borderBottomWidth: 1, borderBottomColor: colors.border }}>
|
<View style={{ borderBottomWidth: 1, borderBottomColor: colors.border }}>
|
||||||
<AccountHeader account={data} />
|
<AccountHeader />
|
||||||
<AccountInformation account={data} />
|
<AccountInformation />
|
||||||
{!data?.suspended ? <AccountAttachments account={data} /> : null}
|
<AccountAttachments />
|
||||||
</View>
|
</View>
|
||||||
{!data?.suspended ? (
|
{!data?.suspended ? (
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
@ -110,7 +114,7 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
|
|||||||
{
|
{
|
||||||
...page,
|
...page,
|
||||||
page: 'Account',
|
page: 'Account',
|
||||||
account: account.id,
|
id: data?.id,
|
||||||
exclude_reblogs: true,
|
exclude_reblogs: true,
|
||||||
only_media: false
|
only_media: false
|
||||||
}
|
}
|
||||||
@ -122,7 +126,7 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
|
|||||||
{
|
{
|
||||||
...page,
|
...page,
|
||||||
page: 'Account',
|
page: 'Account',
|
||||||
account: account.id,
|
id: data?.id,
|
||||||
exclude_reblogs: false,
|
exclude_reblogs: false,
|
||||||
only_media: false
|
only_media: false
|
||||||
}
|
}
|
||||||
@ -158,11 +162,11 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
|
|||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}, [segment, data, queryKey[1].page, mode])
|
}, [segment, dataUpdatedAt, mode])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<AccountContext.Provider value={{ account: data }}>
|
||||||
<AccountNav scrollY={scrollY} account={data} />
|
<AccountNav scrollY={scrollY} />
|
||||||
|
|
||||||
{data?.suspended ? (
|
{data?.suspended ? (
|
||||||
ListHeaderComponent
|
ListHeaderComponent
|
||||||
@ -170,6 +174,7 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
|
|||||||
<Timeline
|
<Timeline
|
||||||
queryKey={queryKey}
|
queryKey={queryKey}
|
||||||
disableRefresh
|
disableRefresh
|
||||||
|
queryOptions={{ enabled: isRemote ? !!data?.id : true }}
|
||||||
customProps={{
|
customProps={{
|
||||||
renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
||||||
onScroll: ({ nativeEvent }) => (scrollY.value = nativeEvent.contentOffset.y),
|
onScroll: ({ nativeEvent }) => (scrollY.value = nativeEvent.contentOffset.y),
|
||||||
@ -180,7 +185,7 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</AccountContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import { createContext, Dispatch } from 'react'
|
|
||||||
import { AccountAction, AccountState } from './types'
|
|
||||||
|
|
||||||
type ContextType = {
|
|
||||||
accountState: AccountState
|
|
||||||
accountDispatch: Dispatch<AccountAction>
|
|
||||||
}
|
|
||||||
const AccountContext = createContext<ContextType>({} as ContextType)
|
|
||||||
|
|
||||||
export default AccountContext
|
|
@ -1,9 +0,0 @@
|
|||||||
import { AccountState } from './types'
|
|
||||||
|
|
||||||
const accountInitialState: AccountState = {
|
|
||||||
headerRatio: 1 / 3,
|
|
||||||
informationLayout: { height: 0, y: 100 },
|
|
||||||
segmentedIndex: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
export default accountInitialState
|
|
@ -1,19 +0,0 @@
|
|||||||
import { AccountAction, AccountState } from "./types"
|
|
||||||
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default accountReducer
|
|
22
src/screens/Tabs/Shared/Account/utils/types.d.ts
vendored
22
src/screens/Tabs/Shared/Account/utils/types.d.ts
vendored
@ -1,22 +0,0 @@
|
|||||||
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']
|
|
||||||
}
|
|
@ -45,7 +45,7 @@ const TabSharedAttachments: React.FC<TabSharedStackScreenProps<'Tab-Shared-Attac
|
|||||||
|
|
||||||
const queryKey: QueryKeyTimeline = [
|
const queryKey: QueryKeyTimeline = [
|
||||||
'Timeline',
|
'Timeline',
|
||||||
{ page: 'Account', account: account.id, exclude_reblogs: true, only_media: true }
|
{ page: 'Account', id: account.id, exclude_reblogs: true, only_media: true }
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -5,6 +5,7 @@ import CustomText from '@components/Text'
|
|||||||
import apiInstance from '@utils/api/instance'
|
import apiInstance from '@utils/api/instance'
|
||||||
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { useRulesQuery } from '@utils/queryHooks/reports'
|
import { useRulesQuery } from '@utils/queryHooks/reports'
|
||||||
|
import { searchFetchToot } from '@utils/queryHooks/search'
|
||||||
import { getAccountStorage } from '@utils/storage/actions'
|
import { getAccountStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
@ -19,6 +20,7 @@ const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>>
|
|||||||
params: { account, status }
|
params: { account, status }
|
||||||
}
|
}
|
||||||
}) => {
|
}) => {
|
||||||
|
console.log('account', account.id)
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const { t } = useTranslation(['common', 'screenTabs'])
|
const { t } = useTranslation(['common', 'screenTabs'])
|
||||||
|
|
||||||
@ -49,10 +51,19 @@ const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>>
|
|||||||
type='text'
|
type='text'
|
||||||
content={t('screenTabs:shared.report.report')}
|
content={t('screenTabs:shared.report.report')}
|
||||||
destructive
|
destructive
|
||||||
onPress={() => {
|
onPress={async () => {
|
||||||
const body = new FormData()
|
const body = new FormData()
|
||||||
|
if (status) {
|
||||||
|
if (status._remote) {
|
||||||
|
const fetchedStatus = await searchFetchToot(status.uri)
|
||||||
|
if (fetchedStatus) {
|
||||||
|
body.append('status_ids[]', fetchedStatus.id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body.append('status_ids[]', status.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
body.append('account_id', account.id)
|
body.append('account_id', account.id)
|
||||||
status && body.append('status_ids[]', status.id)
|
|
||||||
comment.length && body.append('comment', comment)
|
comment.length && body.append('comment', comment)
|
||||||
body.append('forward', forward.toString())
|
body.append('forward', forward.toString())
|
||||||
body.append('category', categories.find(category => category.selected)?.type || 'other')
|
body.append('category', categories.find(category => category.selected)?.type || 'other')
|
||||||
@ -71,7 +82,7 @@ const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>>
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}, [isReporting, comment, forward, categories, rules])
|
}, [isReporting, comment, forward, categories, rules, account.id])
|
||||||
|
|
||||||
const localInstance = account?.acct.includes('@')
|
const localInstance = account?.acct.includes('@')
|
||||||
? account?.acct.includes(`@${getAccountStorage.string('auth.account.domain')}`)
|
? account?.acct.includes(`@${getAccountStorage.string('auth.account.domain')}`)
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import Button from '@components/Button'
|
|
||||||
import { HeaderLeft } from '@components/Header'
|
import { HeaderLeft } from '@components/Header'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import ComponentSeparator from '@components/Separator'
|
import ComponentSeparator from '@components/Separator'
|
||||||
@ -9,11 +8,12 @@ import apiGeneral from '@utils/api/general'
|
|||||||
import apiInstance from '@utils/api/instance'
|
import apiInstance from '@utils/api/instance'
|
||||||
import { getHost } from '@utils/helpers/urlMatcher'
|
import { getHost } from '@utils/helpers/urlMatcher'
|
||||||
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { FlatList, View } from 'react-native'
|
import { Alert, FlatList, Pressable, View } from 'react-native'
|
||||||
import { Circle } from 'react-native-animated-spinkit'
|
import { Circle } from 'react-native-animated-spinkit'
|
||||||
import { Path, Svg } from 'react-native-svg'
|
import { Path, Svg } from 'react-native-svg'
|
||||||
|
|
||||||
@ -31,7 +31,16 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerTitle: () => (
|
headerTitle: () => (
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
<Pressable
|
||||||
|
style={{ flexDirection: 'row', alignItems: 'center' }}
|
||||||
|
disabled={!hasRemoteContent}
|
||||||
|
onPress={() =>
|
||||||
|
Alert.alert(
|
||||||
|
t('screenTabs:shared.toot.remoteFetch.title'),
|
||||||
|
t('screenTabs:shared.toot.remoteFetch.message')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
{hasRemoteContent ? (
|
{hasRemoteContent ? (
|
||||||
<Icon
|
<Icon
|
||||||
name='Wifi'
|
name='Wifi'
|
||||||
@ -47,7 +56,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
|||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
children={t('screenTabs:shared.toot.name')}
|
children={t('screenTabs:shared.toot.name')}
|
||||||
/>
|
/>
|
||||||
</View>
|
</Pressable>
|
||||||
),
|
),
|
||||||
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
|
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
|
||||||
})
|
})
|
||||||
@ -56,12 +65,16 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
|||||||
const flRef = useRef<FlatList>(null)
|
const flRef = useRef<FlatList>(null)
|
||||||
const scrolled = useRef(false)
|
const scrolled = useRef(false)
|
||||||
|
|
||||||
const finalData = useRef<(Mastodon.Status & { _level?: number; _remote?: boolean })[]>([
|
const finalData = useRef<Mastodon.Status[]>([
|
||||||
{ ...toot, _level: 0, _remote: false }
|
{ ...toot, _level: 0, _remote: toot._remote }
|
||||||
])
|
])
|
||||||
const highlightIndex = useRef<number>(0)
|
const highlightIndex = useRef<number>(0)
|
||||||
|
const queryKey: { local: QueryKeyTimeline; remote: QueryKeyTimeline } = {
|
||||||
|
local: ['Timeline', { page: 'Toot', toot: toot.id, remote: false }],
|
||||||
|
remote: ['Timeline', { page: 'Toot', toot: toot.id, remote: true }]
|
||||||
|
}
|
||||||
const queryLocal = useQuery(
|
const queryLocal = useQuery(
|
||||||
['Timeline', { page: 'Toot', toot: toot.id, remote: false }],
|
queryKey.local,
|
||||||
async () => {
|
async () => {
|
||||||
const context = await apiInstance<{
|
const context = await apiInstance<{
|
||||||
ancestors: Mastodon.Status[]
|
ancestors: Mastodon.Status[]
|
||||||
@ -93,6 +106,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
|||||||
return { pages: [{ body: statuses }] }
|
return { pages: [{ body: statuses }] }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
enabled: !toot._remote,
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
refetchOnMount: true,
|
refetchOnMount: true,
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
@ -129,7 +143,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
useQuery(
|
useQuery(
|
||||||
['Timeline', { page: 'Toot', toot: toot.id, remote: true }],
|
queryKey.remote,
|
||||||
async () => {
|
async () => {
|
||||||
let context:
|
let context:
|
||||||
| {
|
| {
|
||||||
@ -186,17 +200,22 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
|||||||
{
|
{
|
||||||
enabled: toot.account.acct !== toot.account.username, // When on the same instance, these two values are the same
|
enabled: toot.account.acct !== toot.account.username, // When on the same instance, these two values are the same
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
refetchOnMount: false,
|
refetchOnMount: true,
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
if (finalData.current.length < 1 && data.pages[0].body.length < 1) {
|
if (finalData.current.length < 1 && data.pages[0].body.length < 1) {
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finalData.current?.length < data.pages[0].body.length) {
|
if (finalData.current.length < data.pages[0].body.length) {
|
||||||
finalData.current = data.pages[0].body.map(remote => {
|
finalData.current = data.pages[0].body.map(remote => {
|
||||||
const localMatch = finalData.current?.find(local => local.uri === remote.uri)
|
const localMatch = finalData.current.find(local => local.uri === remote.uri)
|
||||||
return localMatch ? { ...localMatch, _remote: false } : { ...remote, _remote: true }
|
if (localMatch) {
|
||||||
|
return localMatch
|
||||||
|
} else {
|
||||||
|
remote._remote = true
|
||||||
|
return remote
|
||||||
|
}
|
||||||
})
|
})
|
||||||
setHasRemoteContent(true)
|
setHasRemoteContent(true)
|
||||||
}
|
}
|
||||||
@ -222,52 +241,6 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const empty = () => {
|
|
||||||
switch (queryLocal.status) {
|
|
||||||
case 'error':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Icon name='Frown' size={StyleConstants.Font.Size.L} color={colors.primaryDefault} />
|
|
||||||
<CustomText
|
|
||||||
fontStyle='M'
|
|
||||||
style={{
|
|
||||||
marginTop: StyleConstants.Spacing.S,
|
|
||||||
marginBottom: StyleConstants.Spacing.L,
|
|
||||||
color: colors.primaryDefault
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('componentTimeline:empty.error.message')}
|
|
||||||
</CustomText>
|
|
||||||
<Button
|
|
||||||
type='text'
|
|
||||||
content={t('componentTimeline:empty.error.button')}
|
|
||||||
onPress={() => queryLocal.refetch()}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
case 'success':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Icon
|
|
||||||
name='Smartphone'
|
|
||||||
size={StyleConstants.Font.Size.L}
|
|
||||||
color={colors.primaryDefault}
|
|
||||||
/>
|
|
||||||
<CustomText
|
|
||||||
fontStyle='M'
|
|
||||||
style={{
|
|
||||||
marginTop: StyleConstants.Spacing.S,
|
|
||||||
marginBottom: StyleConstants.Spacing.L,
|
|
||||||
color: colors.secondary
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('componentTimeline:empty.success.message')}
|
|
||||||
</CustomText>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const heights = useRef<(number | undefined)[]>([])
|
const heights = useRef<(number | undefined)[]>([])
|
||||||
const MAX_LEVEL = 10
|
const MAX_LEVEL = 10
|
||||||
const ARC = StyleConstants.Avatar.XS / 4
|
const ARC = StyleConstants.Avatar.XS / 4
|
||||||
@ -279,9 +252,9 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
|||||||
windowSize={7}
|
windowSize={7}
|
||||||
data={finalData.current}
|
data={finalData.current}
|
||||||
renderItem={({ item, index }) => {
|
renderItem={({ item, index }) => {
|
||||||
const prev = finalData.current?.[index - 1]?._level || 0
|
const prev = finalData.current[index - 1]?._level || 0
|
||||||
const curr = item._level
|
const curr = item._level
|
||||||
const next = finalData.current?.[index + 1]?._level || 0
|
const next = finalData.current[index + 1]?._level || 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@ -299,7 +272,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
|||||||
>
|
>
|
||||||
<TimelineDefault
|
<TimelineDefault
|
||||||
item={item}
|
item={item}
|
||||||
queryKey={['Timeline', { page: 'Toot', toot: toot.id }]}
|
queryKey={item._remote ? queryKey.remote : queryKey.local}
|
||||||
rootQueryKey={rootQueryKey}
|
rootQueryKey={rootQueryKey}
|
||||||
highlighted={toot.id === item.id}
|
highlighted={toot.id === item.id}
|
||||||
isConversation={toot.id !== item.id}
|
isConversation={toot.id !== item.id}
|
||||||
@ -437,7 +410,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
|||||||
const offset = error.averageItemLength * error.index
|
const offset = error.averageItemLength * error.index
|
||||||
flRef.current?.scrollToOffset({ offset })
|
flRef.current?.scrollToOffset({ offset })
|
||||||
try {
|
try {
|
||||||
error.index < (finalData.current.length || 0) &&
|
error.index < finalData.current.length &&
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() =>
|
() =>
|
||||||
flRef.current?.scrollToIndex({
|
flRef.current?.scrollToIndex({
|
||||||
@ -448,19 +421,6 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
|||||||
)
|
)
|
||||||
} catch {}
|
} catch {}
|
||||||
}}
|
}}
|
||||||
ListEmptyComponent={
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
minHeight: '100%',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: colors.backgroundDefault
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{empty()}
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
|
@ -3,7 +3,6 @@ import { displayMessage, Message } from '@components/Message'
|
|||||||
import { NavigationContainer } from '@react-navigation/native'
|
import { NavigationContainer } from '@react-navigation/native'
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||||
import ScreenAccountSelection from '@screens/AccountSelection'
|
import ScreenAccountSelection from '@screens/AccountSelection'
|
||||||
import ScreenActions from '@screens/Actions'
|
|
||||||
import ScreenAnnouncements from '@screens/Announcements'
|
import ScreenAnnouncements from '@screens/Announcements'
|
||||||
import ScreenCompose from '@screens/Compose'
|
import ScreenCompose from '@screens/Compose'
|
||||||
import ScreenImagesViewer from '@screens/ImageViewer'
|
import ScreenImagesViewer from '@screens/ImageViewer'
|
||||||
@ -261,15 +260,6 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
options={{ headerShown: false }}
|
options={{ headerShown: false }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
|
||||||
name='Screen-Actions'
|
|
||||||
component={ScreenActions}
|
|
||||||
options={{
|
|
||||||
presentation: 'transparentModal',
|
|
||||||
animation: 'fade',
|
|
||||||
headerShown: false
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Screen-Announcements'
|
name='Screen-Announcements'
|
||||||
component={ScreenAnnouncements}
|
component={ScreenAnnouncements}
|
||||||
|
@ -7,14 +7,6 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
|||||||
|
|
||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
'Screen-Tabs': NavigatorScreenParams<ScreenTabsStackParamList>
|
'Screen-Tabs': NavigatorScreenParams<ScreenTabsStackParamList>
|
||||||
'Screen-Actions':
|
|
||||||
| {
|
|
||||||
type: 'notifications_filter'
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'alt_text'
|
|
||||||
text: string
|
|
||||||
}
|
|
||||||
'Screen-Announcements': { showAll: boolean }
|
'Screen-Announcements': { showAll: boolean }
|
||||||
'Screen-Compose':
|
'Screen-Compose':
|
||||||
| {
|
| {
|
||||||
@ -93,6 +85,7 @@ export type ScreenTabsScreenProps<T extends keyof ScreenTabsStackParamList> = Bo
|
|||||||
export type TabSharedStackParamList = {
|
export type TabSharedStackParamList = {
|
||||||
'Tab-Shared-Account': {
|
'Tab-Shared-Account': {
|
||||||
account: Partial<Mastodon.Account> & Pick<Mastodon.Account, 'id' | 'username' | 'acct'>
|
account: Partial<Mastodon.Account> & Pick<Mastodon.Account, 'id' | 'username' | 'acct'>
|
||||||
|
isRemote?: boolean
|
||||||
}
|
}
|
||||||
'Tab-Shared-Account-In-Lists': {
|
'Tab-Shared-Account-In-Lists': {
|
||||||
account: Pick<Mastodon.Account, 'id' | 'username'>
|
account: Pick<Mastodon.Account, 'id' | 'username'>
|
||||||
@ -107,7 +100,7 @@ export type TabSharedStackParamList = {
|
|||||||
}
|
}
|
||||||
'Tab-Shared-Report': {
|
'Tab-Shared-Report': {
|
||||||
account: Partial<Mastodon.Account> & Pick<Mastodon.Account, 'id' | 'acct' | 'username'>
|
account: Partial<Mastodon.Account> & Pick<Mastodon.Account, 'id' | 'acct' | 'username'>
|
||||||
status?: Pick<Mastodon.Status, 'id'>
|
status?: Pick<Mastodon.Status, 'id' | '_remote' | 'uri'>
|
||||||
}
|
}
|
||||||
'Tab-Shared-Search': undefined
|
'Tab-Shared-Search': undefined
|
||||||
'Tab-Shared-Toot': {
|
'Tab-Shared-Toot': {
|
||||||
|
@ -1,15 +1,42 @@
|
|||||||
import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
|
import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||||
import apiInstance from '@utils/api/instance'
|
import apiInstance from '@utils/api/instance'
|
||||||
import { AxiosError } from 'axios'
|
import { AxiosError } from 'axios'
|
||||||
|
import { SearchResult } from './search'
|
||||||
|
|
||||||
export type QueryKeyAccount = ['Account', { id: Mastodon.Account['id'] }]
|
export type QueryKeyAccount = ['Account', { id?: Mastodon.Account['id']; remoteUrl?: string }]
|
||||||
|
|
||||||
const accountQueryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyAccount>) => {
|
const accountQueryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyAccount>) => {
|
||||||
const { id } = queryKey[1]
|
const { id, remoteUrl } = queryKey[1]
|
||||||
|
if (!id && !remoteUrl) return Promise.reject()
|
||||||
|
|
||||||
|
let matchedId = id
|
||||||
|
|
||||||
|
if (remoteUrl) {
|
||||||
|
await apiInstance<SearchResult>({
|
||||||
|
version: 'v2',
|
||||||
|
method: 'get',
|
||||||
|
url: 'search',
|
||||||
|
params: {
|
||||||
|
q: remoteUrl,
|
||||||
|
type: 'accounts',
|
||||||
|
limit: 1,
|
||||||
|
resolve: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
const account = res.body.accounts[0]
|
||||||
|
if (account.url !== remoteUrl) {
|
||||||
|
return Promise.reject()
|
||||||
|
} else {
|
||||||
|
matchedId = account.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => Promise.reject())
|
||||||
|
}
|
||||||
|
|
||||||
const res = await apiInstance<Mastodon.Account>({
|
const res = await apiInstance<Mastodon.Account>({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: `accounts/${id}`
|
url: `accounts/${matchedId}`
|
||||||
})
|
})
|
||||||
return res.body
|
return res.body
|
||||||
}
|
}
|
||||||
@ -21,7 +48,7 @@ const useAccountQuery = ({
|
|||||||
options?: UseQueryOptions<Mastodon.Account, AxiosError>
|
options?: UseQueryOptions<Mastodon.Account, AxiosError>
|
||||||
}) => {
|
}) => {
|
||||||
const queryKey: QueryKeyAccount = ['Account', { ...queryKeyParams }]
|
const queryKey: QueryKeyAccount = ['Account', { ...queryKeyParams }]
|
||||||
return useQuery(queryKey, accountQueryFunction, options)
|
return useQuery(queryKey, accountQueryFunction, { ...options })
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----- */
|
/* ----- */
|
||||||
@ -43,7 +70,7 @@ const accountInListsQueryFunction = async ({
|
|||||||
const useAccountInListsQuery = ({
|
const useAccountInListsQuery = ({
|
||||||
options,
|
options,
|
||||||
...queryKeyParams
|
...queryKeyParams
|
||||||
}: QueryKeyAccount[1] & {
|
}: QueryKeyAccountInLists[1] & {
|
||||||
options?: UseQueryOptions<Mastodon.List[], AxiosError>
|
options?: UseQueryOptions<Mastodon.List[], AxiosError>
|
||||||
}) => {
|
}) => {
|
||||||
const queryKey: QueryKeyAccountInLists = ['AccountInLists', { ...queryKeyParams }]
|
const queryKey: QueryKeyAccountInLists = ['AccountInLists', { ...queryKeyParams }]
|
||||||
|
@ -8,10 +8,11 @@ import {
|
|||||||
import apiInstance from '@utils/api/instance'
|
import apiInstance from '@utils/api/instance'
|
||||||
import { AxiosError } from 'axios'
|
import { AxiosError } from 'axios'
|
||||||
|
|
||||||
export type QueryKeyRelationship = ['Relationship', { id: Mastodon.Account['id'] }]
|
export type QueryKeyRelationship = ['Relationship', { id?: Mastodon.Account['id'] }]
|
||||||
|
|
||||||
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyRelationship>) => {
|
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyRelationship>) => {
|
||||||
const { id } = queryKey[1]
|
const { id } = queryKey[1]
|
||||||
|
if (!id) return Promise.reject()
|
||||||
|
|
||||||
const res = await apiInstance<Mastodon.Relationship[]>({
|
const res = await apiInstance<Mastodon.Relationship[]>({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@ -32,6 +33,8 @@ const useRelationshipQuery = ({
|
|||||||
const queryKey: QueryKeyRelationship = ['Relationship', { ...queryKeyParams }]
|
const queryKey: QueryKeyRelationship = ['Relationship', { ...queryKeyParams }]
|
||||||
return useQuery(queryKey, queryFunction, {
|
return useQuery(queryKey, queryFunction, {
|
||||||
...options,
|
...options,
|
||||||
|
enabled:
|
||||||
|
(typeof options?.enabled === 'boolean' ? options.enabled : true) && !!queryKeyParams.id,
|
||||||
select: data => data[0]
|
select: data => data[0]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -46,4 +46,19 @@ const useSearchQuery = <T = SearchResult>({
|
|||||||
return useQuery(queryKey, queryFunction, options)
|
return useQuery(queryKey, queryFunction, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const searchFetchToot = (uri: Mastodon.Status['uri']): Promise<Mastodon.Status | void> =>
|
||||||
|
apiInstance<SearchResult>({
|
||||||
|
version: 'v2',
|
||||||
|
method: 'get',
|
||||||
|
url: 'search',
|
||||||
|
params: {
|
||||||
|
q: uri,
|
||||||
|
type: 'statuses',
|
||||||
|
limit: 1,
|
||||||
|
resolve: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(res => res.body.statuses[0])
|
||||||
|
.catch(() => {})
|
||||||
|
|
||||||
export { useSearchQuery }
|
export { useSearchQuery }
|
||||||
|
@ -4,9 +4,7 @@ import {
|
|||||||
QueryFunctionContext,
|
QueryFunctionContext,
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
UseInfiniteQueryOptions,
|
UseInfiniteQueryOptions,
|
||||||
useMutation,
|
useMutation
|
||||||
useQuery,
|
|
||||||
UseQueryOptions
|
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
import { PagedResponse } from '@utils/api/helpers'
|
import { PagedResponse } from '@utils/api/helpers'
|
||||||
import apiInstance from '@utils/api/instance'
|
import apiInstance from '@utils/api/instance'
|
||||||
@ -15,6 +13,7 @@ import queryClient from '@utils/queryHooks'
|
|||||||
import { getAccountStorage } from '@utils/storage/actions'
|
import { getAccountStorage } from '@utils/storage/actions'
|
||||||
import { AxiosError } from 'axios'
|
import { AxiosError } from 'axios'
|
||||||
import { uniqBy } from 'lodash'
|
import { uniqBy } from 'lodash'
|
||||||
|
import { searchFetchToot } from './search'
|
||||||
import deleteItem from './timeline/deleteItem'
|
import deleteItem from './timeline/deleteItem'
|
||||||
import editItem from './timeline/editItem'
|
import editItem from './timeline/editItem'
|
||||||
import updateStatusProperty from './timeline/updateStatusProperty'
|
import updateStatusProperty from './timeline/updateStatusProperty'
|
||||||
@ -40,13 +39,14 @@ export type QueryKeyTimeline = [
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
page: 'Account'
|
page: 'Account'
|
||||||
account: Mastodon.Account['id']
|
id?: Mastodon.Account['id']
|
||||||
exclude_reblogs: boolean
|
exclude_reblogs: boolean
|
||||||
only_media: boolean
|
only_media: boolean
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
page: 'Toot'
|
page: 'Toot'
|
||||||
toot: Mastodon.Status['id']
|
toot: Mastodon.Status['id']
|
||||||
|
remote: boolean
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@ -131,11 +131,13 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query
|
|||||||
})
|
})
|
||||||
|
|
||||||
case 'Account':
|
case 'Account':
|
||||||
|
if (!page.id) return Promise.reject()
|
||||||
|
|
||||||
if (page.exclude_reblogs) {
|
if (page.exclude_reblogs) {
|
||||||
if (pageParam && pageParam.hasOwnProperty('max_id')) {
|
if (pageParam && pageParam.hasOwnProperty('max_id')) {
|
||||||
return apiInstance<Mastodon.Status[]>({
|
return apiInstance<Mastodon.Status[]>({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: `accounts/${page.account}/statuses`,
|
url: `accounts/${page.id}/statuses`,
|
||||||
params: {
|
params: {
|
||||||
exclude_replies: 'true',
|
exclude_replies: 'true',
|
||||||
...params
|
...params
|
||||||
@ -144,7 +146,7 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query
|
|||||||
} else {
|
} else {
|
||||||
const res1 = await apiInstance<(Mastodon.Status & { _pinned: boolean })[]>({
|
const res1 = await apiInstance<(Mastodon.Status & { _pinned: boolean })[]>({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: `accounts/${page.account}/statuses`,
|
url: `accounts/${page.id}/statuses`,
|
||||||
params: {
|
params: {
|
||||||
pinned: 'true'
|
pinned: 'true'
|
||||||
}
|
}
|
||||||
@ -155,7 +157,7 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query
|
|||||||
})
|
})
|
||||||
const res2 = await apiInstance<Mastodon.Status[]>({
|
const res2 = await apiInstance<Mastodon.Status[]>({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: `accounts/${page.account}/statuses`,
|
url: `accounts/${page.id}/statuses`,
|
||||||
params: {
|
params: {
|
||||||
exclude_replies: 'true'
|
exclude_replies: 'true'
|
||||||
}
|
}
|
||||||
@ -168,7 +170,7 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query
|
|||||||
} else {
|
} else {
|
||||||
return apiInstance<Mastodon.Status[]>({
|
return apiInstance<Mastodon.Status[]>({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: `accounts/${page.account}/statuses`,
|
url: `accounts/${page.id}/statuses`,
|
||||||
params: {
|
params: {
|
||||||
...params,
|
...params,
|
||||||
exclude_replies: page.exclude_reblogs.toString(),
|
exclude_replies: page.exclude_reblogs.toString(),
|
||||||
@ -248,34 +250,23 @@ export type MutationVarsTimelineUpdateStatusProperty = {
|
|||||||
type: 'updateStatusProperty'
|
type: 'updateStatusProperty'
|
||||||
queryKey: QueryKeyTimeline
|
queryKey: QueryKeyTimeline
|
||||||
rootQueryKey?: QueryKeyTimeline
|
rootQueryKey?: QueryKeyTimeline
|
||||||
id: Mastodon.Status['id'] | Mastodon.Poll['id']
|
status: Mastodon.Status
|
||||||
isReblog?: boolean
|
|
||||||
payload:
|
payload:
|
||||||
| {
|
| {
|
||||||
property: 'bookmarked' | 'muted' | 'pinned'
|
type: 'bookmarked' | 'muted' | 'pinned' | 'favourited'
|
||||||
currentValue: boolean
|
|
||||||
propertyCount?: undefined
|
|
||||||
countValue?: undefined
|
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
property: 'favourited'
|
type: 'reblogged'
|
||||||
currentValue: boolean
|
|
||||||
propertyCount: 'favourites_count' | 'reblogs_count'
|
|
||||||
countValue: number
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
property: 'reblogged'
|
|
||||||
currentValue: boolean
|
|
||||||
propertyCount: 'favourites_count' | 'reblogs_count'
|
|
||||||
countValue: number
|
|
||||||
visibility: 'public' | 'unlisted'
|
visibility: 'public' | 'unlisted'
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
property: 'poll'
|
type: 'poll'
|
||||||
id: Mastodon.Poll['id']
|
action: 'vote'
|
||||||
type: 'vote' | 'refresh'
|
options: boolean[]
|
||||||
options?: boolean[]
|
}
|
||||||
data?: Mastodon.Poll
|
| {
|
||||||
|
type: 'poll'
|
||||||
|
action: 'refresh'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,10 +315,10 @@ export type MutationVarsTimeline =
|
|||||||
const mutationFunction = async (params: MutationVarsTimeline) => {
|
const mutationFunction = async (params: MutationVarsTimeline) => {
|
||||||
switch (params.type) {
|
switch (params.type) {
|
||||||
case 'updateStatusProperty':
|
case 'updateStatusProperty':
|
||||||
switch (params.payload.property) {
|
switch (params.payload.type) {
|
||||||
case 'poll':
|
case 'poll':
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
params.payload.type === 'vote' &&
|
params.payload.action === 'vote' &&
|
||||||
params.payload.options?.forEach((option, index) => {
|
params.payload.options?.forEach((option, index) => {
|
||||||
if (option) {
|
if (option) {
|
||||||
formData.append('choices[]', index.toString())
|
formData.append('choices[]', index.toString())
|
||||||
@ -335,24 +326,33 @@ const mutationFunction = async (params: MutationVarsTimeline) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return apiInstance<Mastodon.Poll>({
|
return apiInstance<Mastodon.Poll>({
|
||||||
method: params.payload.type === 'vote' ? 'post' : 'get',
|
method: params.payload.action === 'vote' ? 'post' : 'get',
|
||||||
url:
|
url:
|
||||||
params.payload.type === 'vote'
|
params.payload.action === 'vote'
|
||||||
? `polls/${params.payload.id}/votes`
|
? `polls/${params.status.poll?.id}/votes`
|
||||||
: `polls/${params.payload.id}`,
|
: `polls/${params.status.poll?.id}`,
|
||||||
...(params.payload.type === 'vote' && { body: formData })
|
...(params.payload.action === 'vote' && { body: formData })
|
||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
|
let tootId = params.status.id
|
||||||
|
if (params.status._remote) {
|
||||||
|
const fetched = await searchFetchToot(params.status.uri)
|
||||||
|
if (fetched) {
|
||||||
|
tootId = fetched.id
|
||||||
|
} else {
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
}
|
||||||
const body = new FormData()
|
const body = new FormData()
|
||||||
if (params.payload.property === 'reblogged') {
|
if (params.payload.type === 'reblogged') {
|
||||||
body.append('visibility', params.payload.visibility)
|
body.append('visibility', params.payload.visibility)
|
||||||
}
|
}
|
||||||
return apiInstance<Mastodon.Status>({
|
return apiInstance<Mastodon.Status>({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `statuses/${params.id}/${params.payload.currentValue ? 'un' : ''}${
|
url: `statuses/${tootId}/${params.status[params.payload.type] ? '' : 'un'}${
|
||||||
MapPropertyToUrl[params.payload.property]
|
MapPropertyToUrl[params.payload.type]
|
||||||
}`,
|
}`,
|
||||||
...(params.payload.property === 'reblogged' && { body })
|
...(params.payload.type === 'reblogged' && { body })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case 'updateAccountProperty':
|
case 'updateAccountProperty':
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
import { MutationVarsTimelineUpdateStatusProperty } from '@utils/queryHooks/timeline'
|
|
||||||
|
|
||||||
const updateConversation = ({
|
|
||||||
item,
|
|
||||||
payload
|
|
||||||
}: {
|
|
||||||
item: Mastodon.Conversation
|
|
||||||
payload: MutationVarsTimelineUpdateStatusProperty['payload']
|
|
||||||
}) => {
|
|
||||||
switch (payload.property) {
|
|
||||||
case 'poll':
|
|
||||||
if (item.last_status) {
|
|
||||||
item.last_status[payload.property] = payload.data
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
default:
|
|
||||||
if (item.last_status) {
|
|
||||||
item.last_status[payload.property] =
|
|
||||||
typeof payload.currentValue === 'boolean'
|
|
||||||
? !payload.currentValue
|
|
||||||
: true
|
|
||||||
if (payload.propertyCount) {
|
|
||||||
if (typeof payload.currentValue === 'boolean' && payload.currentValue) {
|
|
||||||
item.last_status[payload.propertyCount] = payload.countValue - 1
|
|
||||||
} else {
|
|
||||||
item.last_status[payload.propertyCount] = payload.countValue + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default updateConversation
|
|
@ -1,31 +0,0 @@
|
|||||||
import { MutationVarsTimelineUpdateStatusProperty } from '@utils/queryHooks/timeline'
|
|
||||||
|
|
||||||
const updateNotification = ({
|
|
||||||
item,
|
|
||||||
payload
|
|
||||||
}: {
|
|
||||||
item: Mastodon.Notification
|
|
||||||
payload: MutationVarsTimelineUpdateStatusProperty['payload']
|
|
||||||
}) => {
|
|
||||||
switch (payload.property) {
|
|
||||||
case 'poll':
|
|
||||||
return item
|
|
||||||
default:
|
|
||||||
if (item.status) {
|
|
||||||
item.status[payload.property] =
|
|
||||||
typeof payload.currentValue === 'boolean'
|
|
||||||
? !payload.currentValue
|
|
||||||
: true
|
|
||||||
if (payload.propertyCount) {
|
|
||||||
if (typeof payload.currentValue === 'boolean' && payload.currentValue) {
|
|
||||||
item.status[payload.propertyCount] = payload.countValue - 1
|
|
||||||
} else {
|
|
||||||
item.status[payload.propertyCount] = payload.countValue + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default updateNotification
|
|
@ -1,46 +0,0 @@
|
|||||||
import { MutationVarsTimelineUpdateStatusProperty } from '@utils/queryHooks/timeline'
|
|
||||||
|
|
||||||
const updateStatus = ({
|
|
||||||
item,
|
|
||||||
isReblog,
|
|
||||||
payload
|
|
||||||
}: {
|
|
||||||
item: Mastodon.Status
|
|
||||||
isReblog?: boolean
|
|
||||||
payload: MutationVarsTimelineUpdateStatusProperty['payload']
|
|
||||||
}) => {
|
|
||||||
switch (payload.property) {
|
|
||||||
case 'poll':
|
|
||||||
if (isReblog) {
|
|
||||||
item.reblog!.poll = payload.data
|
|
||||||
} else {
|
|
||||||
item.poll = payload.data
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
if (isReblog) {
|
|
||||||
item.reblog![payload.property] =
|
|
||||||
typeof payload.currentValue === 'boolean' ? !payload.currentValue : true
|
|
||||||
if (payload.propertyCount) {
|
|
||||||
if (typeof payload.currentValue === 'boolean' && payload.currentValue) {
|
|
||||||
item.reblog![payload.propertyCount] = payload.countValue - 1
|
|
||||||
} else {
|
|
||||||
item.reblog![payload.propertyCount] = payload.countValue + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
item[payload.property] =
|
|
||||||
typeof payload.currentValue === 'boolean' ? !payload.currentValue : true
|
|
||||||
if (payload.propertyCount) {
|
|
||||||
if (typeof payload.currentValue === 'boolean' && payload.currentValue) {
|
|
||||||
item[payload.propertyCount] = payload.countValue - 1
|
|
||||||
} else {
|
|
||||||
item[payload.propertyCount] = payload.countValue + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default updateStatus
|
|
@ -1,103 +1,76 @@
|
|||||||
import { InfiniteData } from '@tanstack/react-query'
|
import { InfiniteData } from '@tanstack/react-query'
|
||||||
import queryClient from '@utils/queryHooks'
|
import queryClient from '@utils/queryHooks'
|
||||||
import { MutationVarsTimelineUpdateStatusProperty, TimelineData } from '../timeline'
|
import { MutationVarsTimelineUpdateStatusProperty, TimelineData } from '../timeline'
|
||||||
import updateConversation from './update/conversation'
|
|
||||||
import updateNotification from './update/notification'
|
|
||||||
import updateStatus from './update/status'
|
|
||||||
|
|
||||||
const updateStatusProperty = ({
|
const updateStatusProperty = ({
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id,
|
status,
|
||||||
isReblog,
|
payload,
|
||||||
payload
|
poll
|
||||||
}: MutationVarsTimelineUpdateStatusProperty) => {
|
}: MutationVarsTimelineUpdateStatusProperty & { poll?: Mastodon.Poll }) => {
|
||||||
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(queryKey, old => {
|
for (const key of [queryKey, rootQueryKey]) {
|
||||||
|
if (!key) continue
|
||||||
|
|
||||||
|
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(key, old => {
|
||||||
if (old) {
|
if (old) {
|
||||||
let foundToot = false
|
let foundToot: Mastodon.Status | undefined = undefined
|
||||||
old.pages = old.pages.map(page => {
|
old.pages = old.pages.map(page => {
|
||||||
// Skip rest of the pages if any toot is found
|
|
||||||
if (foundToot) {
|
if (foundToot) {
|
||||||
return page
|
return page
|
||||||
} else {
|
} else {
|
||||||
if (typeof (page.body as Mastodon.Conversation[])[0].unread === 'boolean') {
|
if (typeof (page.body as Mastodon.Conversation[])[0].unread === 'boolean') {
|
||||||
const items = page.body as Mastodon.Conversation[]
|
foundToot = (page.body as Mastodon.Conversation[]).find(({ last_status }) =>
|
||||||
const tootIndex = items.findIndex(({ last_status }) => last_status?.id === id)
|
last_status?.reblog
|
||||||
if (tootIndex >= 0) {
|
? last_status.reblog.id === status.id
|
||||||
foundToot = true
|
: last_status?.id === status.id
|
||||||
updateConversation({ item: items[tootIndex], payload })
|
)?.last_status
|
||||||
}
|
|
||||||
return page
|
return page
|
||||||
} else if (typeof (page.body as Mastodon.Notification[])[0].type === 'string') {
|
} else if (typeof (page.body as Mastodon.Notification[])[0].type === 'string') {
|
||||||
const items = page.body as Mastodon.Notification[]
|
foundToot = (page.body as Mastodon.Notification[]).find(no =>
|
||||||
const tootIndex = items.findIndex(({ status }) => status?.id === id)
|
no.status?.reblog ? no.status.reblog.id === status.id : no.status?.id === status.id
|
||||||
if (tootIndex >= 0) {
|
)?.status
|
||||||
foundToot = true
|
|
||||||
updateNotification({ item: items[tootIndex], payload })
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const items = page.body as Mastodon.Status[]
|
foundToot = (page.body as Mastodon.Status[]).find(toot =>
|
||||||
const tootIndex = isReblog
|
toot.reblog ? toot.reblog.id === status.id : toot.id === status.id
|
||||||
? items.findIndex(({ reblog }) => reblog?.id === id)
|
)
|
||||||
: items.findIndex(toot => toot.id === id)
|
|
||||||
// if favourites page and notifications page, remove the item instead
|
|
||||||
if (tootIndex >= 0) {
|
|
||||||
foundToot = true
|
|
||||||
updateStatus({ item: items[tootIndex], isReblog, payload })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return page
|
return page
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
return old
|
|
||||||
})
|
|
||||||
|
|
||||||
rootQueryKey &&
|
|
||||||
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(rootQueryKey, old => {
|
|
||||||
if (old) {
|
|
||||||
let foundToot = false
|
|
||||||
old.pages = old.pages.map(page => {
|
|
||||||
// Skip rest of the pages if any toot is found
|
|
||||||
if (foundToot) {
|
if (foundToot) {
|
||||||
return page
|
enum MapPropertyToCount {
|
||||||
} else {
|
favourited = 'favourites_count',
|
||||||
if (typeof (page.body as Mastodon.Conversation[])[0].unread === 'boolean') {
|
reblogged = 'reblogs_count'
|
||||||
const items = page.body as Mastodon.Conversation[]
|
|
||||||
const tootIndex = items.findIndex(({ last_status }) => last_status?.id === id)
|
|
||||||
if (tootIndex >= 0) {
|
|
||||||
foundToot = true
|
|
||||||
updateConversation({ item: items[tootIndex], payload })
|
|
||||||
}
|
|
||||||
return page
|
|
||||||
} else if (typeof (page.body as Mastodon.Notification[])[0].type === 'string') {
|
|
||||||
const items = page.body as Mastodon.Notification[]
|
|
||||||
const tootIndex = items.findIndex(({ status }) => status?.id === id)
|
|
||||||
if (tootIndex >= 0) {
|
|
||||||
foundToot = true
|
|
||||||
updateNotification({ item: items[tootIndex], payload })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const items = page.body as Mastodon.Status[]
|
|
||||||
const tootIndex = isReblog
|
|
||||||
? items.findIndex(({ reblog }) => reblog?.id === id)
|
|
||||||
: items.findIndex(toot => toot.id === id)
|
|
||||||
// if favourites page and notifications page, remove the item instead
|
|
||||||
if (tootIndex >= 0) {
|
|
||||||
foundToot = true
|
|
||||||
updateStatus({ item: items[tootIndex], isReblog, payload })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return page
|
switch (payload.type) {
|
||||||
|
case 'poll':
|
||||||
|
status.poll = poll
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
status[payload.type] =
|
||||||
|
typeof status[payload.type] === 'boolean' ? !status[payload.type] : true
|
||||||
|
switch (payload.type) {
|
||||||
|
case 'favourited':
|
||||||
|
case 'reblogged':
|
||||||
|
if (typeof status[payload.type] === 'boolean' && status[payload.type]) {
|
||||||
|
status[MapPropertyToCount[payload.type]]--
|
||||||
|
} else {
|
||||||
|
status[MapPropertyToCount[payload.type]]++
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return old
|
return old
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default updateStatusProperty
|
export default updateStatusProperty
|
||||||
|
@ -19,7 +19,7 @@ const ManageThemeContext = createContext<ContextType>({
|
|||||||
|
|
||||||
export const useTheme = () => useContext(ManageThemeContext)
|
export const useTheme = () => useContext(ManageThemeContext)
|
||||||
|
|
||||||
const useColorSchemeDelay = (delay = 50) => {
|
const useColorSchemeDelay = (delay = 250) => {
|
||||||
const [colorScheme, setColorScheme] = React.useState(Appearance.getColorScheme())
|
const [colorScheme, setColorScheme] = React.useState(Appearance.getColorScheme())
|
||||||
const onColorSchemeChange = React.useCallback(
|
const onColorSchemeChange = React.useCallback(
|
||||||
throttle(
|
throttle(
|
||||||
|
Reference in New Issue
Block a user