mirror of
https://github.com/tooot-app/app
synced 2025-04-13 09:51:58 +02:00
Fixed #476
This commit is contained in:
parent
6a9f951dba
commit
20a55efb9c
@ -168,6 +168,7 @@ export interface Props {
|
||||
highlighted?: boolean
|
||||
disableDetails?: boolean
|
||||
selectable?: boolean
|
||||
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
const ParseHTML = React.memo(
|
||||
@ -183,7 +184,8 @@ const ParseHTML = React.memo(
|
||||
expandHint,
|
||||
highlighted = false,
|
||||
disableDetails = false,
|
||||
selectable = false
|
||||
selectable = false,
|
||||
setSpoilerExpanded
|
||||
}: Props) => {
|
||||
const adaptiveFontsize = useSelector(getSettingsFontsize)
|
||||
const adaptedFontsize = adaptiveScale(
|
||||
@ -253,6 +255,9 @@ const ParseHTML = React.memo(
|
||||
onPress={() => {
|
||||
layoutAnimation()
|
||||
setExpanded(!expanded)
|
||||
if (setSpoilerExpanded) {
|
||||
setSpoilerExpanded(!expanded)
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
|
@ -4,90 +4,52 @@ import { useNavigation } from '@react-navigation/native'
|
||||
import { StackNavigationProp } from '@react-navigation/stack'
|
||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { isEqual } from 'lodash'
|
||||
import React, { useCallback } from 'react'
|
||||
import { Pressable, View } from 'react-native'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
import { useSelector } from 'react-redux'
|
||||
import TimelineActions from './Shared/Actions'
|
||||
import TimelineContent from './Shared/Content'
|
||||
import StatusContext from './Shared/Context'
|
||||
import TimelineHeaderConversation from './Shared/HeaderConversation'
|
||||
import TimelinePoll from './Shared/Poll'
|
||||
|
||||
const Avatars: React.FC<{ accounts: Mastodon.Account[] }> = ({ accounts }) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
marginRight: StyleConstants.Spacing.S,
|
||||
width: StyleConstants.Avatar.M,
|
||||
height: StyleConstants.Avatar.M,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap'
|
||||
}}
|
||||
>
|
||||
{accounts.slice(0, 4).map(account => (
|
||||
<GracefullyImage
|
||||
key={account.id}
|
||||
uri={{ original: account.avatar, static: account.avatar_static }}
|
||||
dimension={{
|
||||
width: StyleConstants.Avatar.M,
|
||||
height:
|
||||
accounts.length > 2
|
||||
? StyleConstants.Avatar.M / 2
|
||||
: StyleConstants.Avatar.M
|
||||
}}
|
||||
style={{ flex: 1, flexBasis: '50%' }}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
conversation: Mastodon.Conversation
|
||||
queryKey: QueryKeyTimeline
|
||||
highlighted?: boolean
|
||||
}
|
||||
|
||||
const TimelineConversation = React.memo(
|
||||
({ conversation, queryKey, highlighted = false }: Props) => {
|
||||
const instanceAccount = useSelector(
|
||||
getInstanceAccount,
|
||||
(prev, next) => prev?.id === next?.id
|
||||
)
|
||||
const { colors } = useTheme()
|
||||
const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlighted = false }) => {
|
||||
const { colors } = useTheme()
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const fireMutation = useCallback(() => {
|
||||
return apiInstance<Mastodon.Conversation>({
|
||||
method: 'post',
|
||||
url: `conversations/${conversation.id}/read`
|
||||
})
|
||||
}, [])
|
||||
const { mutate } = useMutation(fireMutation, {
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(queryKey)
|
||||
}
|
||||
const queryClient = useQueryClient()
|
||||
const fireMutation = useCallback(() => {
|
||||
return apiInstance<Mastodon.Conversation>({
|
||||
method: 'post',
|
||||
url: `conversations/${conversation.id}/read`
|
||||
})
|
||||
}, [])
|
||||
const { mutate } = useMutation(fireMutation, {
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(queryKey)
|
||||
}
|
||||
})
|
||||
|
||||
const navigation =
|
||||
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
const onPress = useCallback(() => {
|
||||
if (conversation.last_status) {
|
||||
conversation.unread && mutate()
|
||||
navigation.push('Tab-Shared-Toot', {
|
||||
toot: conversation.last_status,
|
||||
rootQueryKey: queryKey
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
const onPress = useCallback(() => {
|
||||
if (conversation.last_status) {
|
||||
conversation.unread && mutate()
|
||||
navigation.push('Tab-Shared-Toot', {
|
||||
toot: conversation.last_status,
|
||||
rootQueryKey: queryKey
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
return (
|
||||
<StatusContext.Provider value={{ queryKey, status: conversation.last_status }}>
|
||||
<Pressable
|
||||
style={[
|
||||
{
|
||||
@ -100,19 +62,39 @@ const TimelineConversation = React.memo(
|
||||
conversation.unread && {
|
||||
borderLeftWidth: StyleConstants.Spacing.XS,
|
||||
borderLeftColor: colors.blue,
|
||||
paddingLeft:
|
||||
StyleConstants.Spacing.Global.PagePadding -
|
||||
StyleConstants.Spacing.XS
|
||||
paddingLeft: StyleConstants.Spacing.Global.PagePadding - StyleConstants.Spacing.XS
|
||||
}
|
||||
]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
|
||||
<Avatars accounts={conversation.accounts} />
|
||||
<TimelineHeaderConversation
|
||||
queryKey={queryKey}
|
||||
conversation={conversation}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
marginRight: StyleConstants.Spacing.S,
|
||||
width: StyleConstants.Avatar.M,
|
||||
height: StyleConstants.Avatar.M,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap'
|
||||
}}
|
||||
>
|
||||
{conversation.accounts.slice(0, 4).map(account => (
|
||||
<GracefullyImage
|
||||
key={account.id}
|
||||
uri={{ original: account.avatar, static: account.avatar_static }}
|
||||
dimension={{
|
||||
width: StyleConstants.Avatar.M,
|
||||
height:
|
||||
conversation.accounts.length > 2
|
||||
? StyleConstants.Avatar.M / 2
|
||||
: StyleConstants.Avatar.M
|
||||
}}
|
||||
style={{ flex: 1, flexBasis: '50%' }}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
<TimelineHeaderConversation conversation={conversation} />
|
||||
</View>
|
||||
|
||||
{conversation.last_status ? (
|
||||
@ -120,40 +102,19 @@ const TimelineConversation = React.memo(
|
||||
<View
|
||||
style={{
|
||||
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
|
||||
paddingLeft: highlighted
|
||||
? 0
|
||||
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
<TimelineContent
|
||||
status={conversation.last_status}
|
||||
highlighted={highlighted}
|
||||
/>
|
||||
{conversation.last_status.poll ? (
|
||||
<TimelinePoll
|
||||
queryKey={queryKey}
|
||||
statusId={conversation.last_status.id}
|
||||
poll={conversation.last_status.poll}
|
||||
reblog={false}
|
||||
sameAccount={
|
||||
conversation.last_status.id === instanceAccount?.id
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<TimelineContent />
|
||||
<TimelinePoll />
|
||||
</View>
|
||||
<TimelineActions
|
||||
queryKey={queryKey}
|
||||
status={conversation.last_status}
|
||||
highlighted={highlighted}
|
||||
accts={conversation.accounts.map(account => account.acct)}
|
||||
reblog={false}
|
||||
/>
|
||||
|
||||
<TimelineActions />
|
||||
</>
|
||||
) : null}
|
||||
</Pressable>
|
||||
)
|
||||
},
|
||||
(prev, next) => isEqual(prev.conversation, next.conversation)
|
||||
)
|
||||
</StatusContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimelineConversation
|
||||
|
@ -16,11 +16,11 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { uniqBy } from 'lodash'
|
||||
import React, { useRef } from 'react'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { Pressable, StyleProp, View, ViewStyle } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
import * as ContextMenu from 'zeego/context-menu'
|
||||
import StatusContext from './Shared/Context'
|
||||
import TimelineFeedback from './Shared/Feedback'
|
||||
import TimelineFiltered, { shouldFilter } from './Shared/Filtered'
|
||||
import TimelineFullConversation from './Shared/FullConversation'
|
||||
@ -46,31 +46,28 @@ const TimelineDefault: React.FC<Props> = ({
|
||||
disableOnPress = false
|
||||
}) => {
|
||||
const { colors } = useTheme()
|
||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
|
||||
const actualStatus = item.reblog ? item.reblog : item
|
||||
|
||||
const ownAccount = actualStatus.account?.id === instanceAccount?.id
|
||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
||||
|
||||
const status = item.reblog ? item.reblog : item
|
||||
const ownAccount = status.account?.id === instanceAccount?.id
|
||||
const [spoilerExpanded, setSpoilerExpanded] = useState(
|
||||
instanceAccount.preferences['reading:expand:spoilers'] || false
|
||||
)
|
||||
const spoilerHidden = status.spoiler_text?.length
|
||||
? !instanceAccount.preferences['reading:expand:spoilers'] && !spoilerExpanded
|
||||
: false
|
||||
const copiableContent = useRef<{ content: string; complete: boolean }>({
|
||||
content: '',
|
||||
complete: false
|
||||
})
|
||||
|
||||
const filtered = queryKey && shouldFilter({ copiableContent, status: actualStatus, queryKey })
|
||||
const filtered = queryKey && shouldFilter({ copiableContent, status, queryKey })
|
||||
if (queryKey && filtered && !highlighted) {
|
||||
return <TimelineFiltered phrase={filtered} />
|
||||
}
|
||||
|
||||
const onPress = () => {
|
||||
if (highlighted) return
|
||||
navigation.push('Tab-Shared-Toot', {
|
||||
toot: actualStatus,
|
||||
rootQueryKey: queryKey
|
||||
})
|
||||
}
|
||||
|
||||
const mainStyle: StyleProp<ViewStyle> = {
|
||||
padding: StyleConstants.Spacing.Global.PagePadding,
|
||||
backgroundColor: colors.backgroundDefault,
|
||||
@ -79,24 +76,14 @@ const TimelineDefault: React.FC<Props> = ({
|
||||
const main = () => (
|
||||
<>
|
||||
{item.reblog ? (
|
||||
<TimelineActioned action='reblog' account={item.account} />
|
||||
<TimelineActioned action='reblog' />
|
||||
) : item._pinned ? (
|
||||
<TimelineActioned action='pinned' account={item.account} />
|
||||
<TimelineActioned action='pinned' />
|
||||
) : null}
|
||||
|
||||
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
|
||||
<TimelineAvatar
|
||||
queryKey={disableOnPress ? undefined : queryKey}
|
||||
account={actualStatus.account}
|
||||
highlighted={highlighted}
|
||||
/>
|
||||
<TimelineHeaderDefault
|
||||
queryKey={disableOnPress ? undefined : queryKey}
|
||||
rootQueryKey={rootQueryKey}
|
||||
status={actualStatus}
|
||||
highlighted={highlighted}
|
||||
copiableContent={copiableContent}
|
||||
/>
|
||||
<TimelineAvatar />
|
||||
<TimelineHeaderDefault />
|
||||
</View>
|
||||
|
||||
<View
|
||||
@ -105,120 +92,103 @@ const TimelineDefault: React.FC<Props> = ({
|
||||
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
{typeof actualStatus.content === 'string' && actualStatus.content.length > 0 ? (
|
||||
<TimelineContent
|
||||
status={actualStatus}
|
||||
highlighted={highlighted}
|
||||
disableDetails={disableDetails}
|
||||
/>
|
||||
) : null}
|
||||
{queryKey && actualStatus.poll ? (
|
||||
<TimelinePoll
|
||||
queryKey={queryKey}
|
||||
rootQueryKey={rootQueryKey}
|
||||
statusId={actualStatus.id}
|
||||
poll={actualStatus.poll}
|
||||
reblog={item.reblog ? true : false}
|
||||
sameAccount={ownAccount}
|
||||
/>
|
||||
) : null}
|
||||
{!disableDetails &&
|
||||
Array.isArray(actualStatus.media_attachments) &&
|
||||
actualStatus.media_attachments.length ? (
|
||||
<TimelineAttachment status={actualStatus} />
|
||||
) : null}
|
||||
{!disableDetails && actualStatus.card ? <TimelineCard card={actualStatus.card} /> : null}
|
||||
{!disableDetails ? (
|
||||
<TimelineFullConversation queryKey={queryKey} status={actualStatus} />
|
||||
) : null}
|
||||
<TimelineTranslate status={actualStatus} highlighted={highlighted} />
|
||||
<TimelineFeedback status={actualStatus} highlighted={highlighted} />
|
||||
<TimelineContent setSpoilerExpanded={setSpoilerExpanded} />
|
||||
<TimelinePoll />
|
||||
<TimelineAttachment />
|
||||
<TimelineCard />
|
||||
<TimelineFullConversation />
|
||||
<TimelineTranslate />
|
||||
<TimelineFeedback />
|
||||
</View>
|
||||
|
||||
{queryKey && !disableDetails ? (
|
||||
<TimelineActions
|
||||
queryKey={queryKey}
|
||||
rootQueryKey={rootQueryKey}
|
||||
highlighted={highlighted}
|
||||
status={actualStatus}
|
||||
ownAccount={ownAccount}
|
||||
accts={uniqBy(
|
||||
([actualStatus.account] as Mastodon.Account[] & Mastodon.Mention[])
|
||||
.concat(actualStatus.mentions)
|
||||
.filter(d => d?.id !== instanceAccount?.id),
|
||||
d => d?.id
|
||||
).map(d => d?.acct)}
|
||||
reblog={item.reblog ? true : false}
|
||||
/>
|
||||
) : null}
|
||||
<TimelineActions />
|
||||
</>
|
||||
)
|
||||
|
||||
const mShare = menuShare({
|
||||
visibility: actualStatus.visibility,
|
||||
visibility: status.visibility,
|
||||
type: 'status',
|
||||
url: actualStatus.url || actualStatus.uri,
|
||||
url: status.url || status.uri,
|
||||
copiableContent
|
||||
})
|
||||
const mStatus = menuStatus({ status: actualStatus, queryKey, rootQueryKey })
|
||||
const mInstance = menuInstance({ status: actualStatus, queryKey, rootQueryKey })
|
||||
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
|
||||
const mInstance = menuInstance({ status, queryKey, rootQueryKey })
|
||||
|
||||
return disableOnPress ? (
|
||||
<View style={mainStyle}>{main()}</View>
|
||||
) : (
|
||||
<>
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>
|
||||
<Pressable
|
||||
accessible={highlighted ? false : true}
|
||||
style={mainStyle}
|
||||
onPress={onPress}
|
||||
onLongPress={() => {}}
|
||||
children={main()}
|
||||
/>
|
||||
</ContextMenu.Trigger>
|
||||
return (
|
||||
<StatusContext.Provider
|
||||
value={{
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
status,
|
||||
isReblog: !!item.reblog,
|
||||
ownAccount,
|
||||
spoilerHidden,
|
||||
copiableContent,
|
||||
highlighted,
|
||||
disableDetails,
|
||||
disableOnPress
|
||||
}}
|
||||
>
|
||||
{disableOnPress ? (
|
||||
<View style={mainStyle}>{main()}</View>
|
||||
) : (
|
||||
<>
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>
|
||||
<Pressable
|
||||
accessible={highlighted ? false : true}
|
||||
style={mainStyle}
|
||||
disabled={highlighted}
|
||||
onPress={() =>
|
||||
navigation.push('Tab-Shared-Toot', {
|
||||
toot: status,
|
||||
rootQueryKey: queryKey
|
||||
})
|
||||
}
|
||||
onLongPress={() => {}}
|
||||
children={main()}
|
||||
/>
|
||||
</ContextMenu.Trigger>
|
||||
|
||||
<ContextMenu.Content>
|
||||
{mShare.map((mGroup, index) => (
|
||||
<ContextMenu.Group key={index}>
|
||||
{mGroup.map(menu => (
|
||||
<ContextMenu.Item key={menu.key} {...menu.item}>
|
||||
<ContextMenu.ItemTitle children={menu.title} />
|
||||
<ContextMenu.ItemIcon iosIconName={menu.icon} />
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Content>
|
||||
{mShare.map((mGroup, index) => (
|
||||
<ContextMenu.Group key={index}>
|
||||
{mGroup.map(menu => (
|
||||
<ContextMenu.Item key={menu.key} {...menu.item}>
|
||||
<ContextMenu.ItemTitle children={menu.title} />
|
||||
<ContextMenu.ItemIcon iosIconName={menu.icon} />
|
||||
</ContextMenu.Item>
|
||||
))}
|
||||
</ContextMenu.Group>
|
||||
))}
|
||||
</ContextMenu.Group>
|
||||
))}
|
||||
|
||||
{mStatus.map((mGroup, index) => (
|
||||
<ContextMenu.Group key={index}>
|
||||
{mGroup.map(menu => (
|
||||
<ContextMenu.Item key={menu.key} {...menu.item}>
|
||||
<ContextMenu.ItemTitle children={menu.title} />
|
||||
<ContextMenu.ItemIcon iosIconName={menu.icon} />
|
||||
</ContextMenu.Item>
|
||||
{mStatus.map((mGroup, index) => (
|
||||
<ContextMenu.Group key={index}>
|
||||
{mGroup.map(menu => (
|
||||
<ContextMenu.Item key={menu.key} {...menu.item}>
|
||||
<ContextMenu.ItemTitle children={menu.title} />
|
||||
<ContextMenu.ItemIcon iosIconName={menu.icon} />
|
||||
</ContextMenu.Item>
|
||||
))}
|
||||
</ContextMenu.Group>
|
||||
))}
|
||||
</ContextMenu.Group>
|
||||
))}
|
||||
|
||||
{mInstance.map((mGroup, index) => (
|
||||
<ContextMenu.Group key={index}>
|
||||
{mGroup.map(menu => (
|
||||
<ContextMenu.Item key={menu.key} {...menu.item}>
|
||||
<ContextMenu.ItemTitle children={menu.title} />
|
||||
<ContextMenu.ItemIcon iosIconName={menu.icon} />
|
||||
</ContextMenu.Item>
|
||||
{mInstance.map((mGroup, index) => (
|
||||
<ContextMenu.Group key={index}>
|
||||
{mGroup.map(menu => (
|
||||
<ContextMenu.Item key={menu.key} {...menu.item}>
|
||||
<ContextMenu.ItemTitle children={menu.title} />
|
||||
<ContextMenu.ItemIcon iosIconName={menu.icon} />
|
||||
</ContextMenu.Item>
|
||||
))}
|
||||
</ContextMenu.Group>
|
||||
))}
|
||||
</ContextMenu.Group>
|
||||
))}
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Root>
|
||||
<TimelineHeaderAndroid
|
||||
queryKey={disableOnPress ? undefined : queryKey}
|
||||
rootQueryKey={rootQueryKey}
|
||||
status={actualStatus}
|
||||
/>
|
||||
</>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Root>
|
||||
<TimelineHeaderAndroid />
|
||||
</>
|
||||
)}
|
||||
</StatusContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -16,11 +16,11 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { uniqBy } from 'lodash'
|
||||
import React, { useCallback, useRef } from 'react'
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import { Pressable, View } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
import * as ContextMenu from 'zeego/context-menu'
|
||||
import StatusContext from './Shared/Context'
|
||||
import TimelineFiltered, { shouldFilter } from './Shared/Filtered'
|
||||
import TimelineFullConversation from './Shared/FullConversation'
|
||||
import TimelineHeaderAndroid from './Shared/HeaderAndroid'
|
||||
@ -36,6 +36,17 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||
queryKey,
|
||||
highlighted = false
|
||||
}) => {
|
||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
||||
|
||||
const status = notification.status
|
||||
const account = notification.status ? notification.status.account : notification.account
|
||||
const ownAccount = notification.account?.id === instanceAccount?.id
|
||||
const [spoilerExpanded, setSpoilerExpanded] = useState(
|
||||
instanceAccount.preferences['reading:expand:spoilers'] || false
|
||||
)
|
||||
const spoilerHidden = notification.status?.spoiler_text?.length
|
||||
? !instanceAccount.preferences['reading:expand:spoilers'] && !spoilerExpanded
|
||||
: false
|
||||
const copiableContent = useRef<{ content: string; complete: boolean }>({
|
||||
content: '',
|
||||
complete: false
|
||||
@ -53,11 +64,8 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
const { colors } = useTheme()
|
||||
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev?.id === next?.id)
|
||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
|
||||
const actualAccount = notification.status ? notification.status.account : notification.account
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
notification.status &&
|
||||
navigation.push('Tab-Shared-Toot', {
|
||||
@ -70,11 +78,7 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||
return (
|
||||
<>
|
||||
{notification.type !== 'mention' ? (
|
||||
<TimelineActioned
|
||||
action={notification.type}
|
||||
account={notification.account}
|
||||
notification
|
||||
/>
|
||||
<TimelineActioned action={notification.type} isNotification account={account} />
|
||||
) : null}
|
||||
|
||||
<View
|
||||
@ -89,8 +93,8 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
|
||||
<TimelineAvatar queryKey={queryKey} account={actualAccount} highlighted={highlighted} />
|
||||
<TimelineHeaderNotification queryKey={queryKey} notification={notification} />
|
||||
<TimelineAvatar account={account} />
|
||||
<TimelineHeaderNotification notification={notification} />
|
||||
</View>
|
||||
|
||||
{notification.status ? (
|
||||
@ -100,41 +104,16 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
{notification.status.content.length > 0 ? (
|
||||
<TimelineContent status={notification.status} highlighted={highlighted} />
|
||||
) : null}
|
||||
{notification.status.poll ? (
|
||||
<TimelinePoll
|
||||
queryKey={queryKey}
|
||||
statusId={notification.status.id}
|
||||
poll={notification.status.poll}
|
||||
reblog={false}
|
||||
sameAccount={notification.account.id === instanceAccount?.id}
|
||||
/>
|
||||
) : null}
|
||||
{notification.status.media_attachments.length > 0 ? (
|
||||
<TimelineAttachment status={notification.status} />
|
||||
) : null}
|
||||
{notification.status.card ? <TimelineCard card={notification.status.card} /> : null}
|
||||
<TimelineFullConversation queryKey={queryKey} status={notification.status} />
|
||||
<TimelineContent setSpoilerExpanded={setSpoilerExpanded} />
|
||||
<TimelinePoll />
|
||||
<TimelineAttachment />
|
||||
<TimelineCard />
|
||||
<TimelineFullConversation />
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{notification.status ? (
|
||||
<TimelineActions
|
||||
queryKey={queryKey}
|
||||
status={notification.status}
|
||||
highlighted={highlighted}
|
||||
accts={uniqBy(
|
||||
([notification.status.account] as Mastodon.Account[] & Mastodon.Mention[])
|
||||
.concat(notification.status.mentions)
|
||||
.filter(d => d?.id !== instanceAccount?.id),
|
||||
d => d?.id
|
||||
).map(d => d?.acct)}
|
||||
reblog={false}
|
||||
/>
|
||||
) : null}
|
||||
<TimelineActions />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -149,7 +128,17 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||
const mInstance = menuInstance({ status: notification.status, queryKey })
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatusContext.Provider
|
||||
value={{
|
||||
queryKey,
|
||||
status,
|
||||
isReblog: !!status?.reblog,
|
||||
ownAccount,
|
||||
spoilerHidden,
|
||||
copiableContent,
|
||||
highlighted
|
||||
}}
|
||||
>
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>
|
||||
<Pressable
|
||||
@ -199,8 +188,8 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||
))}
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Root>
|
||||
<TimelineHeaderAndroid queryKey={queryKey} status={notification.status} />
|
||||
</>
|
||||
<TimelineHeaderAndroid />
|
||||
</StatusContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -5,163 +5,164 @@ import { StackNavigationProp } from '@react-navigation/stack'
|
||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import StatusContext from './Context'
|
||||
|
||||
export interface Props {
|
||||
account: Mastodon.Account
|
||||
action: Mastodon.Notification['type'] | ('reblog' | 'pinned')
|
||||
notification?: boolean
|
||||
action: Mastodon.Notification['type'] | 'reblog' | 'pinned'
|
||||
isNotification?: boolean
|
||||
account?: Mastodon.Account
|
||||
}
|
||||
|
||||
const TimelineActioned = React.memo(
|
||||
({ account, action, notification = false }: Props) => {
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { colors } = useTheme()
|
||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
const name = account?.display_name || account?.username
|
||||
const iconColor = colors.primaryDefault
|
||||
const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest }) => {
|
||||
const { status } = useContext(StatusContext)
|
||||
const account = isNotification ? rest.account : status?.account
|
||||
if (!status || !account) return null
|
||||
|
||||
const content = (content: string) => (
|
||||
<ParseEmojis content={content} emojis={account.emojis} size='S' />
|
||||
)
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { colors } = useTheme()
|
||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
const name = account?.display_name || account?.username
|
||||
const iconColor = colors.primaryDefault
|
||||
|
||||
const onPress = () => navigation.push('Tab-Shared-Account', { account })
|
||||
const content = (content: string) => (
|
||||
<ParseEmojis content={content} emojis={account.emojis} size='S' />
|
||||
)
|
||||
|
||||
const children = () => {
|
||||
switch (action) {
|
||||
case 'pinned':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Anchor'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
{content(t('shared.actioned.pinned'))}
|
||||
</>
|
||||
)
|
||||
case 'favourite':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Heart'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(t('shared.actioned.favourite', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
case 'follow':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='UserPlus'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(t('shared.actioned.follow', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
case 'follow_request':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='UserPlus'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(t('shared.actioned.follow_request', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
case 'poll':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='BarChart2'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
{content(t('shared.actioned.poll'))}
|
||||
</>
|
||||
)
|
||||
case 'reblog':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Repeat'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(
|
||||
notification
|
||||
? t('shared.actioned.reblog.notification', { name })
|
||||
: t('shared.actioned.reblog.default', { name })
|
||||
)}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
case 'status':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Activity'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(t('shared.actioned.status', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
case 'update':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='BarChart2'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
{content(t('shared.actioned.update'))}
|
||||
</>
|
||||
)
|
||||
default:
|
||||
return <></>
|
||||
}
|
||||
const onPress = () => navigation.push('Tab-Shared-Account', { account })
|
||||
|
||||
const children = () => {
|
||||
switch (action) {
|
||||
case 'pinned':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Anchor'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
{content(t('shared.actioned.pinned'))}
|
||||
</>
|
||||
)
|
||||
case 'favourite':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Heart'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(t('shared.actioned.favourite', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
case 'follow':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='UserPlus'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(t('shared.actioned.follow', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
case 'follow_request':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='UserPlus'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(t('shared.actioned.follow_request', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
case 'poll':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='BarChart2'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
{content(t('shared.actioned.poll'))}
|
||||
</>
|
||||
)
|
||||
case 'reblog':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Repeat'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(
|
||||
isNotification
|
||||
? t('shared.actioned.reblog.notification', { name })
|
||||
: t('shared.actioned.reblog.default', { name })
|
||||
)}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
case 'status':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Activity'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(t('shared.actioned.status', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
case 'update':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='BarChart2'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
{content(t('shared.actioned.update'))}
|
||||
</>
|
||||
)
|
||||
default:
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: StyleConstants.Spacing.S,
|
||||
paddingLeft: StyleConstants.Avatar.M - StyleConstants.Font.Size.S,
|
||||
paddingRight: StyleConstants.Spacing.Global.PagePadding
|
||||
}}
|
||||
>
|
||||
{children()}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: StyleConstants.Spacing.S,
|
||||
paddingLeft: StyleConstants.Avatar.M - StyleConstants.Font.Size.S,
|
||||
paddingRight: StyleConstants.Spacing.Global.PagePadding
|
||||
}}
|
||||
children={children()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
icon: {
|
||||
|
@ -10,32 +10,22 @@ import {
|
||||
QueryKeyTimeline,
|
||||
useTimelineMutation
|
||||
} from '@utils/queryHooks/timeline'
|
||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { uniqBy } from 'lodash'
|
||||
import React, { useCallback, useContext, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import { useSelector } from 'react-redux'
|
||||
import StatusContext from './Context'
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKeyTimeline
|
||||
rootQueryKey?: QueryKeyTimeline
|
||||
highlighted: boolean
|
||||
status: Mastodon.Status
|
||||
ownAccount?: boolean
|
||||
accts: Mastodon.Account['acct'][] // When replying to conversations
|
||||
reblog: boolean
|
||||
}
|
||||
const TimelineActions: React.FC = () => {
|
||||
const { queryKey, rootQueryKey, status, isReblog, ownAccount, highlighted, disableDetails } =
|
||||
useContext(StatusContext)
|
||||
if (!queryKey || !status || disableDetails) return null
|
||||
|
||||
const TimelineActions: React.FC<Props> = ({
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
highlighted,
|
||||
status,
|
||||
ownAccount = false,
|
||||
accts,
|
||||
reblog
|
||||
}) => {
|
||||
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { colors, theme } = useTheme()
|
||||
@ -83,16 +73,21 @@ const TimelineActions: React.FC<Props> = ({
|
||||
}
|
||||
})
|
||||
|
||||
const onPressReply = useCallback(
|
||||
() =>
|
||||
navigation.navigate('Screen-Compose', {
|
||||
type: 'reply',
|
||||
incomingStatus: status,
|
||||
accts,
|
||||
queryKey
|
||||
}),
|
||||
[status.replies_count]
|
||||
)
|
||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
||||
const onPressReply = useCallback(() => {
|
||||
const accts = uniqBy(
|
||||
([status.account] as Mastodon.Account[] & Mastodon.Mention[])
|
||||
.concat(status.mentions)
|
||||
.filter(d => d?.id !== instanceAccount?.id),
|
||||
d => d?.id
|
||||
).map(d => d?.acct)
|
||||
navigation.navigate('Screen-Compose', {
|
||||
type: 'reply',
|
||||
incomingStatus: status,
|
||||
accts,
|
||||
queryKey
|
||||
})
|
||||
}, [status.replies_count])
|
||||
const { showActionSheetWithOptions } = useActionSheet()
|
||||
const onPressReblog = useCallback(() => {
|
||||
if (!status.reblogged) {
|
||||
@ -114,7 +109,7 @@ const TimelineActions: React.FC<Props> = ({
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: status.id,
|
||||
reblog,
|
||||
isReblog,
|
||||
payload: {
|
||||
property: 'reblogged',
|
||||
currentValue: status.reblogged,
|
||||
@ -130,7 +125,7 @@ const TimelineActions: React.FC<Props> = ({
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: status.id,
|
||||
reblog,
|
||||
isReblog,
|
||||
payload: {
|
||||
property: 'reblogged',
|
||||
currentValue: status.reblogged,
|
||||
@ -149,7 +144,7 @@ const TimelineActions: React.FC<Props> = ({
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: status.id,
|
||||
reblog,
|
||||
isReblog,
|
||||
payload: {
|
||||
property: 'reblogged',
|
||||
currentValue: status.reblogged,
|
||||
@ -166,7 +161,7 @@ const TimelineActions: React.FC<Props> = ({
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: status.id,
|
||||
reblog,
|
||||
isReblog,
|
||||
payload: {
|
||||
property: 'favourited',
|
||||
currentValue: status.favourited,
|
||||
@ -181,7 +176,7 @@ const TimelineActions: React.FC<Props> = ({
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: status.id,
|
||||
reblog,
|
||||
isReblog,
|
||||
payload: {
|
||||
property: 'bookmarked',
|
||||
currentValue: status.bookmarked,
|
||||
|
@ -10,51 +10,140 @@ import { RootStackParamList } from '@utils/navigation/navigators'
|
||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable, View } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
import StatusContext from './Context'
|
||||
|
||||
export interface Props {
|
||||
status: Pick<Mastodon.Status, 'media_attachments' | 'sensitive'>
|
||||
}
|
||||
const TimelineAttachment = () => {
|
||||
const { status, disableDetails } = useContext(StatusContext)
|
||||
if (
|
||||
!status ||
|
||||
disableDetails ||
|
||||
!Array.isArray(status.media_attachments) ||
|
||||
!status.media_attachments.length
|
||||
)
|
||||
return null
|
||||
|
||||
const TimelineAttachment = React.memo(
|
||||
({ status }: Props) => {
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
|
||||
const account = useSelector(
|
||||
getInstanceAccount,
|
||||
(prev, next) =>
|
||||
prev.preferences['reading:expand:media'] === next.preferences['reading:expand:media']
|
||||
)
|
||||
const defaultSensitive = () => {
|
||||
switch (account.preferences['reading:expand:media']) {
|
||||
case 'show_all':
|
||||
return false
|
||||
case 'hide_all':
|
||||
return true
|
||||
default:
|
||||
return status.sensitive
|
||||
}
|
||||
const account = useSelector(
|
||||
getInstanceAccount,
|
||||
(prev, next) =>
|
||||
prev.preferences['reading:expand:media'] === next.preferences['reading:expand:media']
|
||||
)
|
||||
const defaultSensitive = () => {
|
||||
switch (account.preferences['reading:expand:media']) {
|
||||
case 'show_all':
|
||||
return false
|
||||
case 'hide_all':
|
||||
return true
|
||||
default:
|
||||
return status.sensitive
|
||||
}
|
||||
const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive())
|
||||
}
|
||||
const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive())
|
||||
|
||||
// @ts-ignore
|
||||
const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] =
|
||||
status.media_attachments
|
||||
.map(attachment => {
|
||||
// @ts-ignore
|
||||
const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] = status.media_attachments
|
||||
.map(attachment => {
|
||||
switch (attachment.type) {
|
||||
case 'image':
|
||||
return {
|
||||
id: attachment.id,
|
||||
preview_url: attachment.preview_url,
|
||||
url: attachment.url,
|
||||
remote_url: attachment.remote_url,
|
||||
blurhash: attachment.blurhash,
|
||||
width: attachment.meta?.original?.width,
|
||||
height: attachment.meta?.original?.height
|
||||
}
|
||||
default:
|
||||
if (
|
||||
attachment.preview_url?.endsWith('.jpg') ||
|
||||
attachment.preview_url?.endsWith('.jpeg') ||
|
||||
attachment.preview_url?.endsWith('.png') ||
|
||||
attachment.preview_url?.endsWith('.gif') ||
|
||||
attachment.remote_url?.endsWith('.jpg') ||
|
||||
attachment.remote_url?.endsWith('.jpeg') ||
|
||||
attachment.remote_url?.endsWith('.png') ||
|
||||
attachment.remote_url?.endsWith('.gif')
|
||||
) {
|
||||
return {
|
||||
id: attachment.id,
|
||||
preview_url: attachment.preview_url,
|
||||
url: attachment.url,
|
||||
remote_url: attachment.remote_url,
|
||||
blurhash: attachment.blurhash,
|
||||
width: attachment.meta?.original?.width,
|
||||
height: attachment.meta?.original?.height
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(i => i)
|
||||
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
|
||||
const navigateToImagesViewer = (id: string) => {
|
||||
navigation.navigate('Screen-ImagesViewer', { imageUrls, id })
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
marginTop: StyleConstants.Spacing.S,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
alignContent: 'stretch'
|
||||
}}
|
||||
>
|
||||
{status.media_attachments.map((attachment, index) => {
|
||||
switch (attachment.type) {
|
||||
case 'image':
|
||||
return {
|
||||
id: attachment.id,
|
||||
preview_url: attachment.preview_url,
|
||||
url: attachment.url,
|
||||
remote_url: attachment.remote_url,
|
||||
blurhash: attachment.blurhash,
|
||||
width: attachment.meta?.original?.width,
|
||||
height: attachment.meta?.original?.height
|
||||
}
|
||||
return (
|
||||
<AttachmentImage
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
image={attachment}
|
||||
navigateToImagesViewer={navigateToImagesViewer}
|
||||
/>
|
||||
)
|
||||
case 'video':
|
||||
return (
|
||||
<AttachmentVideo
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
video={attachment}
|
||||
/>
|
||||
)
|
||||
case 'gifv':
|
||||
return (
|
||||
<AttachmentVideo
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
video={attachment}
|
||||
gifv
|
||||
/>
|
||||
)
|
||||
case 'audio':
|
||||
return (
|
||||
<AttachmentAudio
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
audio={attachment}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
if (
|
||||
attachment.preview_url?.endsWith('.jpg') ||
|
||||
@ -66,176 +155,74 @@ const TimelineAttachment = React.memo(
|
||||
attachment.remote_url?.endsWith('.png') ||
|
||||
attachment.remote_url?.endsWith('.gif')
|
||||
) {
|
||||
return {
|
||||
id: attachment.id,
|
||||
preview_url: attachment.preview_url,
|
||||
url: attachment.url,
|
||||
remote_url: attachment.remote_url,
|
||||
blurhash: attachment.blurhash,
|
||||
width: attachment.meta?.original?.width,
|
||||
height: attachment.meta?.original?.height
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(i => i)
|
||||
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
|
||||
const navigateToImagesViewer = (id: string) => {
|
||||
navigation.navigate('Screen-ImagesViewer', { imageUrls, id })
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
marginTop: StyleConstants.Spacing.S,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
alignContent: 'stretch'
|
||||
}}
|
||||
>
|
||||
{status.media_attachments.map((attachment, index) => {
|
||||
switch (attachment.type) {
|
||||
case 'image':
|
||||
return (
|
||||
<AttachmentImage
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
// @ts-ignore
|
||||
image={attachment}
|
||||
navigateToImagesViewer={navigateToImagesViewer}
|
||||
/>
|
||||
)
|
||||
case 'video':
|
||||
} else {
|
||||
return (
|
||||
<AttachmentVideo
|
||||
<AttachmentUnsupported
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
video={attachment}
|
||||
attachment={attachment}
|
||||
/>
|
||||
)
|
||||
case 'gifv':
|
||||
return (
|
||||
<AttachmentVideo
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
video={attachment}
|
||||
gifv
|
||||
/>
|
||||
)
|
||||
case 'audio':
|
||||
return (
|
||||
<AttachmentAudio
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
audio={attachment}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
if (
|
||||
attachment.preview_url?.endsWith('.jpg') ||
|
||||
attachment.preview_url?.endsWith('.jpeg') ||
|
||||
attachment.preview_url?.endsWith('.png') ||
|
||||
attachment.preview_url?.endsWith('.gif') ||
|
||||
attachment.remote_url?.endsWith('.jpg') ||
|
||||
attachment.remote_url?.endsWith('.jpeg') ||
|
||||
attachment.remote_url?.endsWith('.png') ||
|
||||
attachment.remote_url?.endsWith('.gif')
|
||||
) {
|
||||
return (
|
||||
<AttachmentImage
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
// @ts-ignore
|
||||
image={attachment}
|
||||
navigateToImagesViewer={navigateToImagesViewer}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<AttachmentUnsupported
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
attachment={attachment}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
})}
|
||||
</View>
|
||||
}
|
||||
}
|
||||
})}
|
||||
</View>
|
||||
|
||||
{defaultSensitive() &&
|
||||
(sensitiveShown ? (
|
||||
<Pressable
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type='text'
|
||||
content={t('shared.attachment.sensitive.button')}
|
||||
overlay
|
||||
onPress={() => {
|
||||
layoutAnimation()
|
||||
setSensitiveShown(false)
|
||||
haptics('Light')
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
) : (
|
||||
{defaultSensitive() &&
|
||||
(sensitiveShown ? (
|
||||
<Pressable
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type='icon'
|
||||
content='EyeOff'
|
||||
round
|
||||
type='text'
|
||||
content={t('shared.attachment.sensitive.button')}
|
||||
overlay
|
||||
onPress={() => {
|
||||
setSensitiveShown(true)
|
||||
layoutAnimation()
|
||||
setSensitiveShown(false)
|
||||
haptics('Light')
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: StyleConstants.Spacing.S * 2,
|
||||
left: StyleConstants.Spacing.S
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
(prev, next) => {
|
||||
let isEqual = true
|
||||
|
||||
if (prev.status.media_attachments.length !== next.status.media_attachments.length) {
|
||||
isEqual = false
|
||||
return isEqual
|
||||
}
|
||||
|
||||
prev.status.media_attachments.forEach((attachment, index) => {
|
||||
if (attachment.preview_url !== next.status.media_attachments[index].preview_url) {
|
||||
isEqual = false
|
||||
}
|
||||
})
|
||||
|
||||
return isEqual
|
||||
}
|
||||
)
|
||||
</Pressable>
|
||||
) : (
|
||||
<Button
|
||||
type='icon'
|
||||
content='EyeOff'
|
||||
round
|
||||
overlay
|
||||
onPress={() => {
|
||||
setSensitiveShown(true)
|
||||
haptics('Light')
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: StyleConstants.Spacing.S * 2,
|
||||
left: StyleConstants.Spacing.S
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimelineAttachment
|
||||
|
@ -2,37 +2,37 @@ import GracefullyImage from '@components/GracefullyImage'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { StackNavigationProp } from '@react-navigation/stack'
|
||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import React, { useCallback } from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import StatusContext from './Context'
|
||||
|
||||
export interface Props {
|
||||
queryKey?: QueryKeyTimeline
|
||||
account: Mastodon.Account
|
||||
highlighted: boolean
|
||||
account?: Mastodon.Account
|
||||
}
|
||||
|
||||
const TimelineAvatar = React.memo(({ queryKey, account, highlighted }: Props) => {
|
||||
const TimelineAvatar: React.FC<Props> = ({ account }) => {
|
||||
const { status, highlighted, disableOnPress } = useContext(StatusContext)
|
||||
const actualAccount = account || status?.account
|
||||
if (!actualAccount) return null
|
||||
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
// Need to fix go back root
|
||||
const onPress = useCallback(() => {
|
||||
queryKey && navigation.push('Tab-Shared-Account', { account })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<GracefullyImage
|
||||
{...(highlighted && {
|
||||
accessibilityLabel: t('shared.avatar.accessibilityLabel', {
|
||||
name: account.display_name
|
||||
name: actualAccount.display_name
|
||||
}),
|
||||
accessibilityHint: t('shared.avatar.accessibilityHint', {
|
||||
name: account.display_name
|
||||
name: actualAccount.display_name
|
||||
})
|
||||
})}
|
||||
onPress={onPress}
|
||||
uri={{ original: account?.avatar, static: account?.avatar_static }}
|
||||
onPress={() =>
|
||||
!disableOnPress && navigation.push('Tab-Shared-Account', { account: actualAccount })
|
||||
}
|
||||
uri={{ original: actualAccount.avatar, static: actualAccount.avatar_static }}
|
||||
dimension={{
|
||||
width: StyleConstants.Avatar.M,
|
||||
height: StyleConstants.Avatar.M
|
||||
@ -44,6 +44,6 @@ const TimelineAvatar = React.memo(({ queryKey, account, highlighted }: Props) =>
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default TimelineAvatar
|
||||
|
@ -9,23 +9,23 @@ import { useSearchQuery } from '@utils/queryHooks/search'
|
||||
import { useStatusQuery } from '@utils/queryHooks/status'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { Circle } from 'react-native-animated-spinkit'
|
||||
import TimelineDefault from '../Default'
|
||||
import StatusContext from './Context'
|
||||
|
||||
export interface Props {
|
||||
card: Pick<Mastodon.Card, 'url' | 'image' | 'blurhash' | 'title' | 'description'>
|
||||
}
|
||||
const TimelineCard: React.FC = () => {
|
||||
const { status, spoilerHidden, disableDetails } = useContext(StatusContext)
|
||||
if (!status || !status.card) return null
|
||||
|
||||
const TimelineCard = React.memo(({ card }: Props) => {
|
||||
const { colors } = useTheme()
|
||||
const navigation = useNavigation()
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const isStatus = matchStatus(card.url)
|
||||
const isStatus = matchStatus(status.card.url)
|
||||
const [foundStatus, setFoundStatus] = useState<Mastodon.Status>()
|
||||
const isAccount = matchAccount(card.url)
|
||||
const isAccount = matchAccount(status.card.url)
|
||||
const [foundAccount, setFoundAccount] = useState<Mastodon.Account>()
|
||||
|
||||
const searchQuery = useSearchQuery({
|
||||
@ -38,7 +38,7 @@ const TimelineCard = React.memo(({ card }: Props) => {
|
||||
if (isStatus.sameInstance) {
|
||||
return
|
||||
} else {
|
||||
return card.url
|
||||
return status.card.url
|
||||
}
|
||||
}
|
||||
if (isAccount) {
|
||||
@ -49,7 +49,7 @@ const TimelineCard = React.memo(({ card }: Props) => {
|
||||
return isAccount.username
|
||||
}
|
||||
} else {
|
||||
return card.url
|
||||
return status.card.url
|
||||
}
|
||||
}
|
||||
})(),
|
||||
@ -136,10 +136,10 @@ const TimelineCard = React.memo(({ card }: Props) => {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{card.image ? (
|
||||
{status.card?.image ? (
|
||||
<GracefullyImage
|
||||
uri={{ original: card.image }}
|
||||
blurhash={card.blurhash}
|
||||
uri={{ original: status.card.image }}
|
||||
blurhash={status.card.blurhash}
|
||||
style={{ flexBasis: StyleConstants.Font.LineHeight.M * 5 }}
|
||||
imageStyle={{ borderTopLeftRadius: 6, borderBottomLeftRadius: 6 }}
|
||||
/>
|
||||
@ -155,9 +155,9 @@ const TimelineCard = React.memo(({ card }: Props) => {
|
||||
fontWeight='Bold'
|
||||
testID='title'
|
||||
>
|
||||
{card.title}
|
||||
{status.card?.title}
|
||||
</CustomText>
|
||||
{card.description ? (
|
||||
{status.card?.description ? (
|
||||
<CustomText
|
||||
fontStyle='S'
|
||||
numberOfLines={1}
|
||||
@ -167,17 +167,19 @@ const TimelineCard = React.memo(({ card }: Props) => {
|
||||
}}
|
||||
testID='description'
|
||||
>
|
||||
{card.description}
|
||||
{status.card.description}
|
||||
</CustomText>
|
||||
) : null}
|
||||
<CustomText fontStyle='S' numberOfLines={1} style={{ color: colors.secondary }}>
|
||||
{card.url}
|
||||
{status.card?.url}
|
||||
</CustomText>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (spoilerHidden || disableDetails) return null
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
accessible
|
||||
@ -192,10 +194,10 @@ const TimelineCard = React.memo(({ card }: Props) => {
|
||||
overflow: 'hidden',
|
||||
borderColor: colors.border
|
||||
}}
|
||||
onPress={async () => await openLink(card.url, navigation)}
|
||||
children={cardContent}
|
||||
onPress={async () => status.card && (await openLink(status.card.url, navigation))}
|
||||
children={cardContent()}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default TimelineCard
|
||||
|
@ -1,52 +1,36 @@
|
||||
import { ParseHTML } from '@components/Parse'
|
||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import StatusContext from './Context'
|
||||
|
||||
export interface Props {
|
||||
status: Pick<Mastodon.Status, 'content' | 'spoiler_text' | 'emojis'> & {
|
||||
mentions?: Mastodon.Status['mentions']
|
||||
tags?: Mastodon.Status['tags']
|
||||
}
|
||||
highlighted?: boolean
|
||||
disableDetails?: boolean
|
||||
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
const TimelineContent = React.memo(
|
||||
({ status, highlighted = false, disableDetails = false }: Props) => {
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
||||
const TimelineContent: React.FC<Props> = ({ setSpoilerExpanded }) => {
|
||||
const { status, highlighted, disableDetails } = useContext(StatusContext)
|
||||
if (!status || typeof status.content !== 'string' || !status.content.length) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{status.spoiler_text ? (
|
||||
<>
|
||||
<ParseHTML
|
||||
content={status.spoiler_text}
|
||||
size={highlighted ? 'L' : 'M'}
|
||||
adaptiveSize
|
||||
emojis={status.emojis}
|
||||
mentions={status.mentions}
|
||||
tags={status.tags}
|
||||
numberOfLines={999}
|
||||
highlighted={highlighted}
|
||||
disableDetails={disableDetails}
|
||||
/>
|
||||
<ParseHTML
|
||||
content={status.content}
|
||||
size={highlighted ? 'L' : 'M'}
|
||||
adaptiveSize
|
||||
emojis={status.emojis}
|
||||
mentions={status.mentions}
|
||||
tags={status.tags}
|
||||
numberOfLines={instanceAccount.preferences['reading:expand:spoilers'] ? 999 : 1}
|
||||
expandHint={t('shared.content.expandHint')}
|
||||
highlighted={highlighted}
|
||||
disableDetails={disableDetails}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
||||
|
||||
return (
|
||||
<>
|
||||
{status.spoiler_text?.length ? (
|
||||
<>
|
||||
<ParseHTML
|
||||
content={status.spoiler_text}
|
||||
size={highlighted ? 'L' : 'M'}
|
||||
adaptiveSize
|
||||
emojis={status.emojis}
|
||||
mentions={status.mentions}
|
||||
tags={status.tags}
|
||||
numberOfLines={999}
|
||||
highlighted={highlighted}
|
||||
disableDetails={disableDetails}
|
||||
/>
|
||||
<ParseHTML
|
||||
content={status.content}
|
||||
size={highlighted ? 'L' : 'M'}
|
||||
@ -54,16 +38,27 @@ const TimelineContent = React.memo(
|
||||
emojis={status.emojis}
|
||||
mentions={status.mentions}
|
||||
tags={status.tags}
|
||||
numberOfLines={highlighted ? 999 : undefined}
|
||||
numberOfLines={instanceAccount.preferences['reading:expand:spoilers'] ? 999 : 1}
|
||||
expandHint={t('shared.content.expandHint')}
|
||||
setSpoilerExpanded={setSpoilerExpanded}
|
||||
highlighted={highlighted}
|
||||
disableDetails={disableDetails}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
},
|
||||
(prev, next) =>
|
||||
prev.status.content === next.status.content &&
|
||||
prev.status.spoiler_text === next.status.spoiler_text
|
||||
)
|
||||
</>
|
||||
) : (
|
||||
<ParseHTML
|
||||
content={status.content}
|
||||
size={highlighted ? 'L' : 'M'}
|
||||
adaptiveSize
|
||||
emojis={status.emojis}
|
||||
mentions={status.mentions}
|
||||
tags={status.tags}
|
||||
numberOfLines={highlighted ? 999 : undefined}
|
||||
disableDetails={disableDetails}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimelineContent
|
||||
|
24
src/components/Timeline/Shared/Context.tsx
Normal file
24
src/components/Timeline/Shared/Context.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { createContext } from 'react'
|
||||
|
||||
type ContextType = {
|
||||
queryKey?: QueryKeyTimeline
|
||||
rootQueryKey?: QueryKeyTimeline
|
||||
|
||||
status?: Mastodon.Status
|
||||
|
||||
isReblog?: boolean
|
||||
ownAccount?: boolean
|
||||
spoilerHidden?: boolean
|
||||
copiableContent?: React.MutableRefObject<{
|
||||
content: string
|
||||
complete: boolean
|
||||
}>
|
||||
|
||||
highlighted?: boolean
|
||||
disableDetails?: boolean
|
||||
disableOnPress?: boolean
|
||||
}
|
||||
const StatusContext = createContext<ContextType>({} as ContextType)
|
||||
|
||||
export default StatusContext
|
@ -5,103 +5,92 @@ import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||
import { useStatusHistory } from '@utils/queryHooks/statusesHistory'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import StatusContext from './Context'
|
||||
|
||||
export interface Props {
|
||||
status: Pick<Mastodon.Status, 'id' | 'edited_at' | 'reblogs_count' | 'favourites_count'>
|
||||
highlighted: boolean
|
||||
}
|
||||
const TimelineFeedback = () => {
|
||||
const { status, highlighted } = useContext(StatusContext)
|
||||
if (!status || !highlighted) return null
|
||||
|
||||
const TimelineFeedback = React.memo(
|
||||
({ status, highlighted }: Props) => {
|
||||
if (!highlighted) {
|
||||
return null
|
||||
}
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { colors } = useTheme()
|
||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { colors } = useTheme()
|
||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
const { data } = useStatusHistory({
|
||||
id: status.id,
|
||||
options: { enabled: status.edited_at !== undefined }
|
||||
})
|
||||
|
||||
const { data } = useStatusHistory({
|
||||
id: status.id,
|
||||
options: { enabled: status.edited_at !== undefined }
|
||||
})
|
||||
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
{status.reblogs_count > 0 ? (
|
||||
<CustomText
|
||||
accessibilityLabel={t('shared.actionsUsers.reblogged_by.accessibilityLabel', {
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
{status.reblogs_count > 0 ? (
|
||||
<CustomText
|
||||
accessibilityLabel={t('shared.actionsUsers.reblogged_by.accessibilityLabel', {
|
||||
count: status.reblogs_count
|
||||
})}
|
||||
accessibilityHint={t('shared.actionsUsers.reblogged_by.accessibilityHint')}
|
||||
accessibilityRole='button'
|
||||
style={[styles.text, { color: colors.blue }]}
|
||||
onPress={() =>
|
||||
navigation.push('Tab-Shared-Users', {
|
||||
reference: 'statuses',
|
||||
id: status.id,
|
||||
type: 'reblogged_by',
|
||||
count: status.reblogs_count
|
||||
})}
|
||||
accessibilityHint={t('shared.actionsUsers.reblogged_by.accessibilityHint')}
|
||||
accessibilityRole='button'
|
||||
style={[styles.text, { color: colors.blue }]}
|
||||
onPress={() =>
|
||||
navigation.push('Tab-Shared-Users', {
|
||||
reference: 'statuses',
|
||||
id: status.id,
|
||||
type: 'reblogged_by',
|
||||
count: status.reblogs_count
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('shared.actionsUsers.reblogged_by.text', {
|
||||
count: status.reblogs_count
|
||||
})}
|
||||
</CustomText>
|
||||
) : null}
|
||||
{status.favourites_count > 0 ? (
|
||||
<CustomText
|
||||
accessibilityLabel={t('shared.actionsUsers.favourited_by.accessibilityLabel', {
|
||||
count: status.reblogs_count
|
||||
})}
|
||||
accessibilityHint={t('shared.actionsUsers.favourited_by.accessibilityHint')}
|
||||
accessibilityRole='button'
|
||||
style={[styles.text, { color: colors.blue }]}
|
||||
onPress={() =>
|
||||
navigation.push('Tab-Shared-Users', {
|
||||
reference: 'statuses',
|
||||
id: status.id,
|
||||
type: 'favourited_by',
|
||||
count: status.favourites_count
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('shared.actionsUsers.favourited_by.text', {
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('shared.actionsUsers.reblogged_by.text', {
|
||||
count: status.reblogs_count
|
||||
})}
|
||||
</CustomText>
|
||||
) : null}
|
||||
{status.favourites_count > 0 ? (
|
||||
<CustomText
|
||||
accessibilityLabel={t('shared.actionsUsers.favourited_by.accessibilityLabel', {
|
||||
count: status.reblogs_count
|
||||
})}
|
||||
accessibilityHint={t('shared.actionsUsers.favourited_by.accessibilityHint')}
|
||||
accessibilityRole='button'
|
||||
style={[styles.text, { color: colors.blue }]}
|
||||
onPress={() =>
|
||||
navigation.push('Tab-Shared-Users', {
|
||||
reference: 'statuses',
|
||||
id: status.id,
|
||||
type: 'favourited_by',
|
||||
count: status.favourites_count
|
||||
})}
|
||||
</CustomText>
|
||||
) : null}
|
||||
</View>
|
||||
<View>
|
||||
{data && data.length > 1 ? (
|
||||
<CustomText
|
||||
accessibilityLabel={t('shared.actionsUsers.history.accessibilityLabel', {
|
||||
count: data.length - 1
|
||||
})}
|
||||
accessibilityHint={t('shared.actionsUsers.history.accessibilityHint')}
|
||||
accessibilityRole='button'
|
||||
style={[styles.text, { marginRight: 0, color: colors.blue }]}
|
||||
onPress={() => navigation.push('Tab-Shared-History', { id: status.id })}
|
||||
>
|
||||
{t('shared.actionsUsers.history.text', {
|
||||
count: data.length - 1
|
||||
})}
|
||||
</CustomText>
|
||||
) : null}
|
||||
</View>
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('shared.actionsUsers.favourited_by.text', {
|
||||
count: status.favourites_count
|
||||
})}
|
||||
</CustomText>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
(prev, next) =>
|
||||
prev.status.edited_at === next.status.edited_at &&
|
||||
prev.status.reblogs_count === next.status.reblogs_count &&
|
||||
prev.status.favourites_count === next.status.favourites_count
|
||||
)
|
||||
<View>
|
||||
{data && data.length > 1 ? (
|
||||
<CustomText
|
||||
accessibilityLabel={t('shared.actionsUsers.history.accessibilityLabel', {
|
||||
count: data.length - 1
|
||||
})}
|
||||
accessibilityHint={t('shared.actionsUsers.history.accessibilityHint')}
|
||||
accessibilityRole='button'
|
||||
style={[styles.text, { marginRight: 0, color: colors.blue }]}
|
||||
onPress={() => navigation.push('Tab-Shared-History', { id: status.id })}
|
||||
>
|
||||
{t('shared.actionsUsers.history.text', {
|
||||
count: data.length - 1
|
||||
})}
|
||||
</CustomText>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
text: {
|
||||
|
@ -1,39 +1,32 @@
|
||||
import CustomText from '@components/Text'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import StatusContext from './Context'
|
||||
|
||||
export interface Props {
|
||||
queryKey?: QueryKeyTimeline
|
||||
status: Mastodon.Status
|
||||
const TimelineFullConversation = () => {
|
||||
const { queryKey, status, disableDetails } = useContext(StatusContext)
|
||||
if (!status || disableDetails) return null
|
||||
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { colors } = useTheme()
|
||||
|
||||
return queryKey &&
|
||||
queryKey[1].page !== 'Toot' &&
|
||||
status.in_reply_to_account_id &&
|
||||
(status.mentions.length === 0 ||
|
||||
status.mentions.filter(mention => mention.id !== status.in_reply_to_account_id).length) ? (
|
||||
<CustomText
|
||||
fontStyle='S'
|
||||
style={{
|
||||
color: colors.blue,
|
||||
marginTop: StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
{t('shared.fullConversation')}
|
||||
</CustomText>
|
||||
) : null
|
||||
}
|
||||
|
||||
const TimelineFullConversation = React.memo(
|
||||
({ queryKey, status }: Props) => {
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { colors } = useTheme()
|
||||
|
||||
return queryKey &&
|
||||
queryKey[1].page !== 'Toot' &&
|
||||
status.in_reply_to_account_id &&
|
||||
(status.mentions.length === 0 ||
|
||||
status.mentions.filter(
|
||||
mention => mention.id !== status.in_reply_to_account_id
|
||||
).length) ? (
|
||||
<CustomText
|
||||
fontStyle='S'
|
||||
style={{
|
||||
color: colors.blue,
|
||||
marginTop: StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
{t('shared.fullConversation')}
|
||||
</CustomText>
|
||||
) : null
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
export default TimelineFullConversation
|
||||
|
@ -3,21 +3,18 @@ import menuInstance from '@components/contextMenu/instance'
|
||||
import menuShare from '@components/contextMenu/share'
|
||||
import menuStatus from '@components/contextMenu/status'
|
||||
import Icon from '@components/Icon'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import { Platform, View } from 'react-native'
|
||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||
import StatusContext from './Context'
|
||||
|
||||
export interface Props {
|
||||
queryKey?: QueryKeyTimeline
|
||||
rootQueryKey?: QueryKeyTimeline
|
||||
status?: Mastodon.Status
|
||||
}
|
||||
const TimelineHeaderAndroid: React.FC = () => {
|
||||
const { queryKey, rootQueryKey, status, disableDetails, disableOnPress } =
|
||||
useContext(StatusContext)
|
||||
|
||||
const TimelineHeaderAndroid: React.FC<Props> = ({ queryKey, rootQueryKey, status }) => {
|
||||
if (Platform.OS !== 'android' || !status) return null
|
||||
if (Platform.OS !== 'android' || !status || disableDetails || disableOnPress) return null
|
||||
|
||||
const { colors } = useTheme()
|
||||
|
||||
|
@ -2,46 +2,25 @@ import Icon from '@components/Icon'
|
||||
import { displayMessage } from '@components/Message'
|
||||
import { ParseEmojis } from '@components/Parse'
|
||||
import CustomText from '@components/Text'
|
||||
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline'
|
||||
import { useTimelineMutation } from '@utils/queryHooks/timeline'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable, View } from 'react-native'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import StatusContext from './Context'
|
||||
import HeaderSharedCreated from './HeaderShared/Created'
|
||||
import HeaderSharedMuted from './HeaderShared/Muted'
|
||||
|
||||
const Names = ({ accounts }: { accounts: Mastodon.Account[] }) => {
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { colors } = useTheme()
|
||||
|
||||
return (
|
||||
<CustomText
|
||||
numberOfLines={1}
|
||||
style={{ ...StyleConstants.FontStyle.M, color: colors.secondary }}
|
||||
>
|
||||
<CustomText>{t('shared.header.conversation.withAccounts')}</CustomText>
|
||||
{accounts.map((account, index) => (
|
||||
<CustomText key={account.id} numberOfLines={1}>
|
||||
{index !== 0 ? t('common:separator') : undefined}
|
||||
<ParseEmojis
|
||||
content={account.display_name || account.username}
|
||||
emojis={account.emojis}
|
||||
fontBold
|
||||
/>
|
||||
</CustomText>
|
||||
))}
|
||||
</CustomText>
|
||||
)
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKeyTimeline
|
||||
conversation: Mastodon.Conversation
|
||||
}
|
||||
|
||||
const HeaderConversation = ({ queryKey, conversation }: Props) => {
|
||||
const HeaderConversation = ({ conversation }: Props) => {
|
||||
const { queryKey } = useContext(StatusContext)
|
||||
if (!queryKey) return null
|
||||
|
||||
const { colors, theme } = useTheme()
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
|
||||
@ -70,7 +49,22 @@ const HeaderConversation = ({ queryKey, conversation }: Props) => {
|
||||
return (
|
||||
<View style={{ flex: 1, flexDirection: 'row' }}>
|
||||
<View style={{ flex: 3 }}>
|
||||
<Names accounts={conversation.accounts} />
|
||||
<CustomText
|
||||
numberOfLines={1}
|
||||
style={{ ...StyleConstants.FontStyle.M, color: colors.secondary }}
|
||||
>
|
||||
<CustomText>{t('shared.header.conversation.withAccounts')}</CustomText>
|
||||
{conversation.accounts.map((account, index) => (
|
||||
<CustomText key={account.id} numberOfLines={1}>
|
||||
{index !== 0 ? t('common:separator') : undefined}
|
||||
<ParseEmojis
|
||||
content={account.display_name || account.username}
|
||||
emojis={account.emojis}
|
||||
fontBold
|
||||
/>
|
||||
</CustomText>
|
||||
))}
|
||||
</CustomText>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
|
@ -3,37 +3,24 @@ import menuInstance from '@components/contextMenu/instance'
|
||||
import menuShare from '@components/contextMenu/share'
|
||||
import menuStatus from '@components/contextMenu/status'
|
||||
import Icon from '@components/Icon'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Platform, Pressable, View } from 'react-native'
|
||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||
import StatusContext from './Context'
|
||||
import HeaderSharedAccount from './HeaderShared/Account'
|
||||
import HeaderSharedApplication from './HeaderShared/Application'
|
||||
import HeaderSharedCreated from './HeaderShared/Created'
|
||||
import HeaderSharedMuted from './HeaderShared/Muted'
|
||||
import HeaderSharedVisibility from './HeaderShared/Visibility'
|
||||
|
||||
export interface Props {
|
||||
queryKey?: QueryKeyTimeline
|
||||
rootQueryKey?: QueryKeyTimeline
|
||||
status: Mastodon.Status
|
||||
highlighted: boolean
|
||||
copiableContent: React.MutableRefObject<{
|
||||
content: string
|
||||
complete: boolean
|
||||
}>
|
||||
}
|
||||
const TimelineHeaderDefault: React.FC = () => {
|
||||
const { queryKey, rootQueryKey, status, copiableContent, highlighted, disableDetails } =
|
||||
useContext(StatusContext)
|
||||
if (!status) return null
|
||||
|
||||
const TimelineHeaderDefault: React.FC<Props> = ({
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
status,
|
||||
highlighted,
|
||||
copiableContent
|
||||
}) => {
|
||||
const { colors } = useTheme()
|
||||
const { t } = useTranslation('componentContextMenu')
|
||||
|
||||
@ -76,7 +63,7 @@ const TimelineHeaderDefault: React.FC<Props> = ({
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{Platform.OS !== 'android' && queryKey ? (
|
||||
{Platform.OS !== 'android' && !disableDetails ? (
|
||||
<Pressable
|
||||
accessibilityHint={t('accessibilityHint')}
|
||||
style={{ flex: 1, alignItems: 'center' }}
|
||||
|
@ -4,40 +4,41 @@ import menuShare from '@components/contextMenu/share'
|
||||
import menuStatus from '@components/contextMenu/status'
|
||||
import Icon from '@components/Icon'
|
||||
import { RelationshipIncoming, RelationshipOutgoing } from '@components/Relationship'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import { Platform, Pressable, View } from 'react-native'
|
||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||
import StatusContext from './Context'
|
||||
import HeaderSharedAccount from './HeaderShared/Account'
|
||||
import HeaderSharedApplication from './HeaderShared/Application'
|
||||
import HeaderSharedCreated from './HeaderShared/Created'
|
||||
import HeaderSharedMuted from './HeaderShared/Muted'
|
||||
import HeaderSharedVisibility from './HeaderShared/Visibility'
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKeyTimeline
|
||||
export type Props = {
|
||||
notification: Mastodon.Notification
|
||||
}
|
||||
|
||||
const TimelineHeaderNotification = ({ queryKey, notification }: Props) => {
|
||||
const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||
const { queryKey, status } = useContext(StatusContext)
|
||||
|
||||
const { colors } = useTheme()
|
||||
|
||||
const [openChange, setOpenChange] = useState(false)
|
||||
const mShare = menuShare({
|
||||
visibility: notification.status?.visibility,
|
||||
visibility: status?.visibility,
|
||||
type: 'status',
|
||||
url: notification.status?.url || notification.status?.uri
|
||||
url: status?.url || status?.uri
|
||||
})
|
||||
const mAccount = menuAccount({
|
||||
type: 'status',
|
||||
openChange,
|
||||
account: notification.status?.account,
|
||||
account: status?.account,
|
||||
queryKey
|
||||
})
|
||||
const mStatus = menuStatus({ status: notification.status, queryKey })
|
||||
const mInstance = menuInstance({ status: notification.status, queryKey })
|
||||
const mStatus = menuStatus({ status, queryKey })
|
||||
const mInstance = menuInstance({ status, queryKey })
|
||||
|
||||
const actions = () => {
|
||||
switch (notification.type) {
|
||||
@ -46,7 +47,7 @@ const TimelineHeaderNotification = ({ queryKey, notification }: Props) => {
|
||||
case 'follow_request':
|
||||
return <RelationshipIncoming id={notification.account.id} />
|
||||
default:
|
||||
if (notification.status) {
|
||||
if (status) {
|
||||
return (
|
||||
<Pressable
|
||||
style={{ flex: 1, alignItems: 'center' }}
|
||||
|
@ -7,39 +7,28 @@ import RelativeTime from '@components/RelativeTime'
|
||||
import CustomText from '@components/Text'
|
||||
import {
|
||||
MutationVarsTimelineUpdateStatusProperty,
|
||||
QueryKeyTimeline,
|
||||
useTimelineMutation
|
||||
} from '@utils/queryHooks/timeline'
|
||||
import updateStatusProperty from '@utils/queryHooks/timeline/updateStatusProperty'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { maxBy } from 'lodash'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useContext, useMemo, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Pressable, View } from 'react-native'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import StatusContext from './Context'
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKeyTimeline
|
||||
rootQueryKey?: QueryKeyTimeline
|
||||
statusId: Mastodon.Status['id']
|
||||
poll: NonNullable<Mastodon.Status['poll']>
|
||||
reblog: boolean
|
||||
sameAccount: boolean
|
||||
}
|
||||
const TimelinePoll: React.FC = () => {
|
||||
const { queryKey, rootQueryKey, status, isReblog, ownAccount, spoilerHidden, disableDetails } =
|
||||
useContext(StatusContext)
|
||||
if (!queryKey || !status || !status.poll) return null
|
||||
const poll = status.poll
|
||||
|
||||
const TimelinePoll: React.FC<Props> = ({
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
statusId,
|
||||
poll,
|
||||
reblog,
|
||||
sameAccount
|
||||
}) => {
|
||||
const { colors, theme } = useTheme()
|
||||
const { t, i18n } = useTranslation('componentTimeline')
|
||||
|
||||
const [allOptions, setAllOptions] = useState(new Array(poll.options.length).fill(false))
|
||||
const [allOptions, setAllOptions] = useState(new Array(status.poll.options.length).fill(false))
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const mutation = useTimelineMutation({
|
||||
@ -79,7 +68,7 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
|
||||
const pollButton = useMemo(() => {
|
||||
if (!poll.expired) {
|
||||
if (!sameAccount && !poll.voted) {
|
||||
if (!ownAccount && !poll.voted) {
|
||||
return (
|
||||
<View style={{ marginRight: StyleConstants.Spacing.S }}>
|
||||
<Button
|
||||
@ -88,8 +77,8 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
type: 'updateStatusProperty',
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: statusId,
|
||||
reblog,
|
||||
id: status.id,
|
||||
isReblog,
|
||||
payload: {
|
||||
property: 'poll',
|
||||
id: poll.id,
|
||||
@ -114,8 +103,8 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
type: 'updateStatusProperty',
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: statusId,
|
||||
reblog,
|
||||
id: status.id,
|
||||
isReblog,
|
||||
payload: {
|
||||
property: 'poll',
|
||||
id: poll.id,
|
||||
@ -258,6 +247,8 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (spoilerHidden || disableDetails) return null
|
||||
|
||||
return (
|
||||
<View style={{ marginTop: StyleConstants.Spacing.M }}>
|
||||
{poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow}
|
||||
|
@ -5,131 +5,119 @@ import { useTranslateQuery } from '@utils/queryHooks/translate'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import * as Localization from 'expo-localization'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable } from 'react-native'
|
||||
import { Circle } from 'react-native-animated-spinkit'
|
||||
import detectLanguage from 'react-native-language-detection'
|
||||
import StatusContext from './Context'
|
||||
|
||||
export interface Props {
|
||||
highlighted: boolean
|
||||
status: Pick<Mastodon.Status, 'language' | 'spoiler_text' | 'content' | 'emojis'>
|
||||
}
|
||||
const TimelineTranslate = () => {
|
||||
const { status, highlighted } = useContext(StatusContext)
|
||||
if (!status || !highlighted) return null
|
||||
|
||||
const TimelineTranslate = React.memo(
|
||||
({ highlighted, status }: Props) => {
|
||||
if (!highlighted) {
|
||||
return null
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { colors } = useTheme()
|
||||
|
||||
const text = status.spoiler_text ? [status.spoiler_text, status.content] : [status.content]
|
||||
|
||||
for (const i in text) {
|
||||
for (const emoji of status.emojis) {
|
||||
text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, ' ')
|
||||
}
|
||||
text[i] = text[i]
|
||||
.replace(/(<([^>]+)>)/gi, ' ')
|
||||
.replace(/@.*? /gi, ' ')
|
||||
.replace(/#.*? /gi, ' ')
|
||||
.replace(/http(s):\/\/.*? /gi, ' ')
|
||||
}
|
||||
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { colors } = useTheme()
|
||||
|
||||
const text = status.spoiler_text ? [status.spoiler_text, status.content] : [status.content]
|
||||
|
||||
for (const i in text) {
|
||||
for (const emoji of status.emojis) {
|
||||
text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, ' ')
|
||||
}
|
||||
text[i] = text[i]
|
||||
.replace(/(<([^>]+)>)/gi, ' ')
|
||||
.replace(/@.*? /gi, ' ')
|
||||
.replace(/#.*? /gi, ' ')
|
||||
.replace(/http(s):\/\/.*? /gi, ' ')
|
||||
const [detectedLanguage, setDetectedLanguage] = useState<string>('')
|
||||
useEffect(() => {
|
||||
const detect = async () => {
|
||||
const result = await detectLanguage(text.join(`\n\n`)).catch(() => {
|
||||
// No need to log language detection failure
|
||||
})
|
||||
result?.detected && setDetectedLanguage(result.detected.slice(0, 2))
|
||||
}
|
||||
detect()
|
||||
}, [])
|
||||
|
||||
const [detectedLanguage, setDetectedLanguage] = useState<string>('')
|
||||
useEffect(() => {
|
||||
const detect = async () => {
|
||||
const result = await detectLanguage(text.join(`\n\n`)).catch(() => {
|
||||
// No need to log language detection failure
|
||||
})
|
||||
result?.detected && setDetectedLanguage(result.detected.slice(0, 2))
|
||||
}
|
||||
detect()
|
||||
}, [])
|
||||
const settingsLanguage = getLanguage()
|
||||
const targetLanguage = settingsLanguage?.startsWith('en')
|
||||
? Localization.locale || settingsLanguage || 'en'
|
||||
: settingsLanguage || Localization.locale || 'en'
|
||||
|
||||
const settingsLanguage = getLanguage()
|
||||
const targetLanguage = settingsLanguage?.startsWith('en')
|
||||
? Localization.locale || settingsLanguage || 'en'
|
||||
: settingsLanguage || Localization.locale || 'en'
|
||||
const [enabled, setEnabled] = useState(false)
|
||||
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({
|
||||
source: detectedLanguage,
|
||||
target: targetLanguage,
|
||||
text,
|
||||
options: { enabled }
|
||||
})
|
||||
|
||||
const [enabled, setEnabled] = useState(false)
|
||||
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({
|
||||
source: detectedLanguage,
|
||||
target: targetLanguage,
|
||||
text,
|
||||
options: { enabled }
|
||||
})
|
||||
if (!detectedLanguage) {
|
||||
return null
|
||||
}
|
||||
if (Localization.locale.slice(0, 2).includes(detectedLanguage)) {
|
||||
return null
|
||||
}
|
||||
if (settingsLanguage?.slice(0, 2).includes(detectedLanguage)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!detectedLanguage) {
|
||||
return null
|
||||
}
|
||||
if (Localization.locale.slice(0, 2).includes(detectedLanguage)) {
|
||||
return null
|
||||
}
|
||||
if (settingsLanguage?.slice(0, 2).includes(detectedLanguage)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Pressable
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: StyleConstants.Spacing.S,
|
||||
paddingBottom: isSuccess ? 0 : undefined
|
||||
}}
|
||||
onPress={() => {
|
||||
if (enabled) {
|
||||
if (!isSuccess) {
|
||||
refetch()
|
||||
}
|
||||
} else {
|
||||
setEnabled(true)
|
||||
return (
|
||||
<>
|
||||
<Pressable
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: StyleConstants.Spacing.S,
|
||||
paddingBottom: isSuccess ? 0 : undefined
|
||||
}}
|
||||
onPress={() => {
|
||||
if (enabled) {
|
||||
if (!isSuccess) {
|
||||
refetch()
|
||||
}
|
||||
} else {
|
||||
setEnabled(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
style={{
|
||||
color: isLoading || isSuccess ? colors.secondary : isError ? colors.red : colors.blue
|
||||
}}
|
||||
>
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
style={{
|
||||
color: isLoading || isSuccess ? colors.secondary : isError ? colors.red : colors.blue
|
||||
}}
|
||||
>
|
||||
{isError
|
||||
? t('shared.translate.failed')
|
||||
: isSuccess
|
||||
? typeof data?.error === 'string'
|
||||
? t(`shared.translate.${data.error}`)
|
||||
: t('shared.translate.succeed', {
|
||||
provider: data?.provider,
|
||||
source: data?.sourceLanguage
|
||||
})
|
||||
: t('shared.translate.default')}
|
||||
</CustomText>
|
||||
<CustomText>
|
||||
{__DEV__ ? ` Source: ${detectedLanguage}; Target: ${targetLanguage}` : undefined}
|
||||
</CustomText>
|
||||
{isLoading ? (
|
||||
<Circle
|
||||
size={StyleConstants.Font.Size.M}
|
||||
color={colors.disabled}
|
||||
style={{ marginLeft: StyleConstants.Spacing.S }}
|
||||
/>
|
||||
) : null}
|
||||
</Pressable>
|
||||
{data && data.error === undefined
|
||||
? data.text.map((d, i) => (
|
||||
<ParseHTML key={i} content={d} size={'M'} numberOfLines={999} />
|
||||
))
|
||||
: null}
|
||||
</>
|
||||
)
|
||||
},
|
||||
(prev, next) =>
|
||||
prev.status.content === next.status.content &&
|
||||
prev.status.spoiler_text === next.status.spoiler_text
|
||||
)
|
||||
{isError
|
||||
? t('shared.translate.failed')
|
||||
: isSuccess
|
||||
? typeof data?.error === 'string'
|
||||
? t(`shared.translate.${data.error}`)
|
||||
: t('shared.translate.succeed', {
|
||||
provider: data?.provider,
|
||||
source: data?.sourceLanguage
|
||||
})
|
||||
: t('shared.translate.default')}
|
||||
</CustomText>
|
||||
<CustomText>
|
||||
{__DEV__ ? ` Source: ${detectedLanguage}; Target: ${targetLanguage}` : undefined}
|
||||
</CustomText>
|
||||
{isLoading ? (
|
||||
<Circle
|
||||
size={StyleConstants.Font.Size.M}
|
||||
color={colors.disabled}
|
||||
style={{ marginLeft: StyleConstants.Spacing.S }}
|
||||
/>
|
||||
) : null}
|
||||
</Pressable>
|
||||
{data && data.error === undefined
|
||||
? data.text.map((d, i) => <ParseHTML key={i} content={d} size={'M'} numberOfLines={999} />)
|
||||
: null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimelineTranslate
|
||||
|
@ -2,10 +2,7 @@ import apiInstance, { InstanceResponse } from '@api/instance'
|
||||
import haptics from '@components/haptics'
|
||||
import queryClient from '@helpers/queryClient'
|
||||
import { store } from '@root/store'
|
||||
import {
|
||||
checkInstanceFeature,
|
||||
getInstanceNotificationsFilter
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import { checkInstanceFeature, getInstanceNotificationsFilter } from '@utils/slices/instancesSlice'
|
||||
import { AxiosError } from 'axios'
|
||||
import { uniqBy } from 'lodash'
|
||||
import {
|
||||
@ -30,10 +27,7 @@ export type QueryKeyTimeline = [
|
||||
}
|
||||
]
|
||||
|
||||
const queryFunction = async ({
|
||||
queryKey,
|
||||
pageParam
|
||||
}: QueryFunctionContext<QueryKeyTimeline>) => {
|
||||
const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<QueryKeyTimeline>) => {
|
||||
const { page, account, hashtag, list, toot } = queryKey[1]
|
||||
let params: { [key: string]: string } = { ...pageParam }
|
||||
|
||||
@ -65,9 +59,9 @@ const queryFunction = async ({
|
||||
case 'Notifications':
|
||||
const rootStore = store.getState()
|
||||
const notificationsFilter = getInstanceNotificationsFilter(rootStore)
|
||||
const usePositiveFilter = checkInstanceFeature(
|
||||
'notification_types_positive_filter'
|
||||
)(rootStore)
|
||||
const usePositiveFilter = checkInstanceFeature('notification_types_positive_filter')(
|
||||
rootStore
|
||||
)
|
||||
return apiInstance<Mastodon.Notification[]>({
|
||||
method: 'get',
|
||||
url: 'notifications',
|
||||
@ -99,9 +93,7 @@ const queryFunction = async ({
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const res1 = await apiInstance<
|
||||
(Mastodon.Status & { _pinned: boolean })[]
|
||||
>({
|
||||
const res1 = await apiInstance<(Mastodon.Status & { _pinned: boolean })[]>({
|
||||
method: 'get',
|
||||
url: `accounts/${account}/statuses`,
|
||||
params: {
|
||||
@ -190,11 +182,7 @@ const queryFunction = async ({
|
||||
url: `statuses/${toot}/context`
|
||||
})
|
||||
return {
|
||||
body: [
|
||||
...res2_1.body.ancestors,
|
||||
res1_1.body,
|
||||
...res2_1.body.descendants
|
||||
]
|
||||
body: [...res2_1.body.ancestors, res1_1.body, ...res2_1.body.descendants]
|
||||
}
|
||||
default:
|
||||
return Promise.reject()
|
||||
@ -207,10 +195,7 @@ const useTimelineQuery = ({
|
||||
options,
|
||||
...queryKeyParams
|
||||
}: QueryKeyTimeline[1] & {
|
||||
options?: UseInfiniteQueryOptions<
|
||||
InstanceResponse<Mastodon.Status[]>,
|
||||
AxiosError
|
||||
>
|
||||
options?: UseInfiniteQueryOptions<InstanceResponse<Mastodon.Status[]>, AxiosError>
|
||||
}) => {
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { ...queryKeyParams }]
|
||||
return useInfiniteQuery(queryKey, queryFunction, {
|
||||
@ -284,7 +269,7 @@ export type MutationVarsTimelineUpdateStatusProperty = {
|
||||
queryKey: QueryKeyTimeline
|
||||
rootQueryKey?: QueryKeyTimeline
|
||||
id: Mastodon.Status['id'] | Mastodon.Poll['id']
|
||||
reblog?: boolean
|
||||
isReblog?: boolean
|
||||
payload:
|
||||
| {
|
||||
property: 'bookmarked' | 'muted' | 'pinned'
|
||||
@ -384,9 +369,9 @@ const mutationFunction = async (params: MutationVarsTimeline) => {
|
||||
}
|
||||
return apiInstance<Mastodon.Status>({
|
||||
method: 'post',
|
||||
url: `statuses/${params.id}/${
|
||||
params.payload.currentValue ? 'un' : ''
|
||||
}${MapPropertyToUrl[params.payload.property]}`,
|
||||
url: `statuses/${params.id}/${params.payload.currentValue ? 'un' : ''}${
|
||||
MapPropertyToUrl[params.payload.property]
|
||||
}`,
|
||||
...(params.payload.property === 'reblogged' && { body })
|
||||
})
|
||||
}
|
||||
@ -396,9 +381,9 @@ const mutationFunction = async (params: MutationVarsTimeline) => {
|
||||
case 'mute':
|
||||
return apiInstance<Mastodon.Account>({
|
||||
method: 'post',
|
||||
url: `accounts/${params.id}/${
|
||||
params.payload.currentValue ? 'un' : ''
|
||||
}${params.payload.property}`
|
||||
url: `accounts/${params.id}/${params.payload.currentValue ? 'un' : ''}${
|
||||
params.payload.property
|
||||
}`
|
||||
})
|
||||
case 'reports':
|
||||
return apiInstance<Mastodon.Account>({
|
||||
@ -455,8 +440,7 @@ const useTimelineMutation = ({
|
||||
...(onMutate && {
|
||||
onMutate: params => {
|
||||
queryClient.cancelQueries(params.queryKey)
|
||||
const oldData =
|
||||
params.queryKey && queryClient.getQueryData(params.queryKey)
|
||||
const oldData = params.queryKey && queryClient.getQueryData(params.queryKey)
|
||||
|
||||
haptics('Light')
|
||||
switch (params.type) {
|
||||
|
@ -2,32 +2,27 @@ import { MutationVarsTimelineUpdateStatusProperty } from '@utils/queryHooks/time
|
||||
|
||||
const updateStatus = ({
|
||||
item,
|
||||
reblog,
|
||||
isReblog,
|
||||
payload
|
||||
}: {
|
||||
item: Mastodon.Status
|
||||
reblog?: boolean
|
||||
isReblog?: boolean
|
||||
payload: MutationVarsTimelineUpdateStatusProperty['payload']
|
||||
}) => {
|
||||
switch (payload.property) {
|
||||
case 'poll':
|
||||
if (reblog) {
|
||||
if (isReblog) {
|
||||
item.reblog!.poll = payload.data
|
||||
} else {
|
||||
item.poll = payload.data
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (reblog) {
|
||||
if (isReblog) {
|
||||
item.reblog![payload.property] =
|
||||
typeof payload.currentValue === 'boolean'
|
||||
? !payload.currentValue
|
||||
: true
|
||||
typeof payload.currentValue === 'boolean' ? !payload.currentValue : true
|
||||
if (payload.propertyCount) {
|
||||
if (
|
||||
typeof payload.currentValue === 'boolean' &&
|
||||
payload.currentValue
|
||||
) {
|
||||
if (typeof payload.currentValue === 'boolean' && payload.currentValue) {
|
||||
item.reblog![payload.propertyCount] = payload.countValue - 1
|
||||
} else {
|
||||
item.reblog![payload.propertyCount] = payload.countValue + 1
|
||||
@ -35,14 +30,9 @@ const updateStatus = ({
|
||||
}
|
||||
} else {
|
||||
item[payload.property] =
|
||||
typeof payload.currentValue === 'boolean'
|
||||
? !payload.currentValue
|
||||
: true
|
||||
typeof payload.currentValue === 'boolean' ? !payload.currentValue : true
|
||||
if (payload.propertyCount) {
|
||||
if (
|
||||
typeof payload.currentValue === 'boolean' &&
|
||||
payload.currentValue
|
||||
) {
|
||||
if (typeof payload.currentValue === 'boolean' && payload.currentValue) {
|
||||
item[payload.propertyCount] = payload.countValue - 1
|
||||
} else {
|
||||
item[payload.propertyCount] = payload.countValue + 1
|
||||
|
@ -1,9 +1,6 @@
|
||||
import queryClient from '@helpers/queryClient'
|
||||
import { InfiniteData } from 'react-query'
|
||||
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'
|
||||
@ -12,12 +9,54 @@ const updateStatusProperty = ({
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id,
|
||||
reblog,
|
||||
isReblog,
|
||||
payload
|
||||
}: MutationVarsTimelineUpdateStatusProperty) => {
|
||||
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
|
||||
queryKey,
|
||||
old => {
|
||||
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(queryKey, old => {
|
||||
if (old) {
|
||||
let foundToot = false
|
||||
old.pages = old.pages.map(page => {
|
||||
// Skip rest of the pages if any toot is found
|
||||
if (foundToot) {
|
||||
return page
|
||||
} else {
|
||||
if (typeof (page.body as Mastodon.Conversation[])[0].unread === 'boolean') {
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return old
|
||||
})
|
||||
|
||||
rootQueryKey &&
|
||||
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(rootQueryKey, old => {
|
||||
if (old) {
|
||||
let foundToot = false
|
||||
old.pages = old.pages.map(page => {
|
||||
@ -25,39 +64,30 @@ const updateStatusProperty = ({
|
||||
if (foundToot) {
|
||||
return page
|
||||
} 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[]
|
||||
const tootIndex = items.findIndex(
|
||||
({ last_status }) => last_status?.id === id
|
||||
)
|
||||
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'
|
||||
) {
|
||||
} 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
|
||||
)
|
||||
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 = reblog
|
||||
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], reblog, payload })
|
||||
updateStatus({ item: items[tootIndex], isReblog, payload })
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,65 +97,7 @@ const updateStatusProperty = ({
|
||||
}
|
||||
|
||||
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) {
|
||||
return page
|
||||
} else {
|
||||
if (
|
||||
typeof (page.body as Mastodon.Conversation[])[0].unread ===
|
||||
'boolean'
|
||||
) {
|
||||
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 = reblog
|
||||
? 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], reblog, payload })
|
||||
}
|
||||
}
|
||||
|
||||
return page
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return old
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default updateStatusProperty
|
||||
|
Loading…
x
Reference in New Issue
Block a user