diff --git a/src/components/Parse/HTML.tsx b/src/components/Parse/HTML.tsx
index b619b144..828ba608 100644
--- a/src/components/Parse/HTML.tsx
+++ b/src/components/Parse/HTML.tsx
@@ -168,6 +168,7 @@ export interface Props {
highlighted?: boolean
disableDetails?: boolean
selectable?: boolean
+ setSpoilerExpanded?: React.Dispatch>
}
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',
diff --git a/src/components/Timeline/Conversation.tsx b/src/components/Timeline/Conversation.tsx
index 5dbd3d2d..b3311a28 100644
--- a/src/components/Timeline/Conversation.tsx
+++ b/src/components/Timeline/Conversation.tsx
@@ -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 (
-
- {accounts.slice(0, 4).map(account => (
- 2
- ? StyleConstants.Avatar.M / 2
- : StyleConstants.Avatar.M
- }}
- style={{ flex: 1, flexBasis: '50%' }}
- />
- ))}
-
- )
-}
-
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 = ({ conversation, queryKey, highlighted = false }) => {
+ const { colors } = useTheme()
- const queryClient = useQueryClient()
- const fireMutation = useCallback(() => {
- return apiInstance({
- method: 'post',
- url: `conversations/${conversation.id}/read`
- })
- }, [])
- const { mutate } = useMutation(fireMutation, {
- onSettled: () => {
- queryClient.invalidateQueries(queryKey)
- }
+ const queryClient = useQueryClient()
+ const fireMutation = useCallback(() => {
+ return apiInstance({
+ method: 'post',
+ url: `conversations/${conversation.id}/read`
})
+ }, [])
+ const { mutate } = useMutation(fireMutation, {
+ onSettled: () => {
+ queryClient.invalidateQueries(queryKey)
+ }
+ })
- const navigation =
- useNavigation>()
- const onPress = useCallback(() => {
- if (conversation.last_status) {
- conversation.unread && mutate()
- navigation.push('Tab-Shared-Toot', {
- toot: conversation.last_status,
- rootQueryKey: queryKey
- })
- }
- }, [])
+ const navigation = useNavigation>()
+ const onPress = useCallback(() => {
+ if (conversation.last_status) {
+ conversation.unread && mutate()
+ navigation.push('Tab-Shared-Toot', {
+ toot: conversation.last_status,
+ rootQueryKey: queryKey
+ })
+ }
+ }, [])
- return (
+ return (
+
-
-
+
+ {conversation.accounts.slice(0, 4).map(account => (
+ 2
+ ? StyleConstants.Avatar.M / 2
+ : StyleConstants.Avatar.M
+ }}
+ style={{ flex: 1, flexBasis: '50%' }}
+ />
+ ))}
+
+
{conversation.last_status ? (
@@ -120,40 +102,19 @@ const TimelineConversation = React.memo(
-
- {conversation.last_status.poll ? (
-
- ) : null}
+
+
- account.acct)}
- reblog={false}
- />
+
+
>
) : null}
- )
- },
- (prev, next) => isEqual(prev.conversation, next.conversation)
-)
+
+ )
+}
export default TimelineConversation
diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx
index 7660f6a3..65b9d33d 100644
--- a/src/components/Timeline/Default.tsx
+++ b/src/components/Timeline/Default.tsx
@@ -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 = ({
disableOnPress = false
}) => {
const { colors } = useTheme()
- const instanceAccount = useSelector(getInstanceAccount, () => true)
const navigation = useNavigation>()
- 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
}
- const onPress = () => {
- if (highlighted) return
- navigation.push('Tab-Shared-Toot', {
- toot: actualStatus,
- rootQueryKey: queryKey
- })
- }
-
const mainStyle: StyleProp = {
padding: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault,
@@ -79,24 +76,14 @@ const TimelineDefault: React.FC = ({
const main = () => (
<>
{item.reblog ? (
-
+
) : item._pinned ? (
-
+
) : null}
-
-
+
+
= ({
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
- {typeof actualStatus.content === 'string' && actualStatus.content.length > 0 ? (
-
- ) : null}
- {queryKey && actualStatus.poll ? (
-
- ) : null}
- {!disableDetails &&
- Array.isArray(actualStatus.media_attachments) &&
- actualStatus.media_attachments.length ? (
-
- ) : null}
- {!disableDetails && actualStatus.card ? : null}
- {!disableDetails ? (
-
- ) : null}
-
-
+
+
+
+
+
+
+
- {queryKey && !disableDetails ? (
- d?.id !== instanceAccount?.id),
- d => d?.id
- ).map(d => d?.acct)}
- reblog={item.reblog ? true : false}
- />
- ) : null}
+
>
)
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 ? (
- {main()}
- ) : (
- <>
-
-
- {}}
- children={main()}
- />
-
+ return (
+
+ {disableOnPress ? (
+ {main()}
+ ) : (
+ <>
+
+
+
+ navigation.push('Tab-Shared-Toot', {
+ toot: status,
+ rootQueryKey: queryKey
+ })
+ }
+ onLongPress={() => {}}
+ children={main()}
+ />
+
-
- {mShare.map((mGroup, index) => (
-
- {mGroup.map(menu => (
-
-
-
-
+
+ {mShare.map((mGroup, index) => (
+
+ {mGroup.map(menu => (
+
+
+
+
+ ))}
+
))}
-
- ))}
- {mStatus.map((mGroup, index) => (
-
- {mGroup.map(menu => (
-
-
-
-
+ {mStatus.map((mGroup, index) => (
+
+ {mGroup.map(menu => (
+
+
+
+
+ ))}
+
))}
-
- ))}
- {mInstance.map((mGroup, index) => (
-
- {mGroup.map(menu => (
-
-
-
-
+ {mInstance.map((mGroup, index) => (
+
+ {mGroup.map(menu => (
+
+
+
+
+ ))}
+
))}
-
- ))}
-
-
-
- >
+
+
+
+ >
+ )}
+
)
}
diff --git a/src/components/Timeline/Notifications.tsx b/src/components/Timeline/Notifications.tsx
index 848896e2..802ae3d5 100644
--- a/src/components/Timeline/Notifications.tsx
+++ b/src/components/Timeline/Notifications.tsx
@@ -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 = ({
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 = ({
}
const { colors } = useTheme()
- const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev?.id === next?.id)
const navigation = useNavigation>()
- 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 = ({
return (
<>
{notification.type !== 'mention' ? (
-
+
) : null}
= ({
}}
>
-
-
+
+
{notification.status ? (
@@ -100,41 +104,16 @@ const TimelineNotifications: React.FC = ({
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
- {notification.status.content.length > 0 ? (
-
- ) : null}
- {notification.status.poll ? (
-
- ) : null}
- {notification.status.media_attachments.length > 0 ? (
-
- ) : null}
- {notification.status.card ? : null}
-
+
+
+
+
+
) : null}
- {notification.status ? (
- d?.id !== instanceAccount?.id),
- d => d?.id
- ).map(d => d?.acct)}
- reblog={false}
- />
- ) : null}
+
>
)
}
@@ -149,7 +128,17 @@ const TimelineNotifications: React.FC = ({
const mInstance = menuInstance({ status: notification.status, queryKey })
return (
- <>
+
= ({
))}
-
- >
+
+
)
}
diff --git a/src/components/Timeline/Shared/Actioned.tsx b/src/components/Timeline/Shared/Actioned.tsx
index 32390eae..2a5c8f86 100644
--- a/src/components/Timeline/Shared/Actioned.tsx
+++ b/src/components/Timeline/Shared/Actioned.tsx
@@ -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>()
- const name = account?.display_name || account?.username
- const iconColor = colors.primaryDefault
+const TimelineActioned: React.FC = ({ action, isNotification, ...rest }) => {
+ const { status } = useContext(StatusContext)
+ const account = isNotification ? rest.account : status?.account
+ if (!status || !account) return null
- const content = (content: string) => (
-
- )
+ const { t } = useTranslation('componentTimeline')
+ const { colors } = useTheme()
+ const navigation = useNavigation>()
+ const name = account?.display_name || account?.username
+ const iconColor = colors.primaryDefault
- const onPress = () => navigation.push('Tab-Shared-Account', { account })
+ const content = (content: string) => (
+
+ )
- const children = () => {
- switch (action) {
- case 'pinned':
- return (
- <>
-
- {content(t('shared.actioned.pinned'))}
- >
- )
- case 'favourite':
- return (
- <>
-
-
- {content(t('shared.actioned.favourite', { name }))}
-
- >
- )
- case 'follow':
- return (
- <>
-
-
- {content(t('shared.actioned.follow', { name }))}
-
- >
- )
- case 'follow_request':
- return (
- <>
-
-
- {content(t('shared.actioned.follow_request', { name }))}
-
- >
- )
- case 'poll':
- return (
- <>
-
- {content(t('shared.actioned.poll'))}
- >
- )
- case 'reblog':
- return (
- <>
-
-
- {content(
- notification
- ? t('shared.actioned.reblog.notification', { name })
- : t('shared.actioned.reblog.default', { name })
- )}
-
- >
- )
- case 'status':
- return (
- <>
-
-
- {content(t('shared.actioned.status', { name }))}
-
- >
- )
- case 'update':
- return (
- <>
-
- {content(t('shared.actioned.update'))}
- >
- )
- default:
- return <>>
- }
+ const onPress = () => navigation.push('Tab-Shared-Account', { account })
+
+ const children = () => {
+ switch (action) {
+ case 'pinned':
+ return (
+ <>
+
+ {content(t('shared.actioned.pinned'))}
+ >
+ )
+ case 'favourite':
+ return (
+ <>
+
+
+ {content(t('shared.actioned.favourite', { name }))}
+
+ >
+ )
+ case 'follow':
+ return (
+ <>
+
+
+ {content(t('shared.actioned.follow', { name }))}
+
+ >
+ )
+ case 'follow_request':
+ return (
+ <>
+
+
+ {content(t('shared.actioned.follow_request', { name }))}
+
+ >
+ )
+ case 'poll':
+ return (
+ <>
+
+ {content(t('shared.actioned.poll'))}
+ >
+ )
+ case 'reblog':
+ return (
+ <>
+
+
+ {content(
+ isNotification
+ ? t('shared.actioned.reblog.notification', { name })
+ : t('shared.actioned.reblog.default', { name })
+ )}
+
+ >
+ )
+ case 'status':
+ return (
+ <>
+
+
+ {content(t('shared.actioned.status', { name }))}
+
+ >
+ )
+ case 'update':
+ return (
+ <>
+
+ {content(t('shared.actioned.update'))}
+ >
+ )
+ default:
+ return <>>
}
+ }
- return (
-
- {children()}
-
- )
- },
- () => true
-)
+ return (
+
+ )
+}
const styles = StyleSheet.create({
icon: {
diff --git a/src/components/Timeline/Shared/Actions.tsx b/src/components/Timeline/Shared/Actions.tsx
index 7300935d..511cbaa9 100644
--- a/src/components/Timeline/Shared/Actions.tsx
+++ b/src/components/Timeline/Shared/Actions.tsx
@@ -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 = ({
- queryKey,
- rootQueryKey,
- highlighted,
- status,
- ownAccount = false,
- accts,
- reblog
-}) => {
const navigation = useNavigation>()
const { t } = useTranslation('componentTimeline')
const { colors, theme } = useTheme()
@@ -83,16 +73,21 @@ const TimelineActions: React.FC = ({
}
})
- 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 = ({
queryKey,
rootQueryKey,
id: status.id,
- reblog,
+ isReblog,
payload: {
property: 'reblogged',
currentValue: status.reblogged,
@@ -130,7 +125,7 @@ const TimelineActions: React.FC = ({
queryKey,
rootQueryKey,
id: status.id,
- reblog,
+ isReblog,
payload: {
property: 'reblogged',
currentValue: status.reblogged,
@@ -149,7 +144,7 @@ const TimelineActions: React.FC = ({
queryKey,
rootQueryKey,
id: status.id,
- reblog,
+ isReblog,
payload: {
property: 'reblogged',
currentValue: status.reblogged,
@@ -166,7 +161,7 @@ const TimelineActions: React.FC = ({
queryKey,
rootQueryKey,
id: status.id,
- reblog,
+ isReblog,
payload: {
property: 'favourited',
currentValue: status.favourited,
@@ -181,7 +176,7 @@ const TimelineActions: React.FC = ({
queryKey,
rootQueryKey,
id: status.id,
- reblog,
+ isReblog,
payload: {
property: 'bookmarked',
currentValue: status.bookmarked,
diff --git a/src/components/Timeline/Shared/Attachment.tsx b/src/components/Timeline/Shared/Attachment.tsx
index 60b98098..4ff5ab84 100644
--- a/src/components/Timeline/Shared/Attachment.tsx
+++ b/src/components/Timeline/Shared/Attachment.tsx
@@ -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
-}
+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>()
+ const navigateToImagesViewer = (id: string) => {
+ navigation.navigate('Screen-ImagesViewer', { imageUrls, id })
+ }
+
+ return (
+
+
+ {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 (
+
+ )
+ case 'video':
+ return (
+
+ )
+ case 'gifv':
+ return (
+
+ )
+ case 'audio':
+ return (
+
+ )
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>()
- const navigateToImagesViewer = (id: string) => {
- navigation.navigate('Screen-ImagesViewer', { imageUrls, id })
- }
-
- return (
-
-
- {status.media_attachments.map((attachment, index) => {
- switch (attachment.type) {
- case 'image':
return (
)
- case 'video':
+ } else {
return (
-
)
- case 'gifv':
- return (
-
- )
- case 'audio':
- return (
-
- )
- 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 (
-
- )
- } else {
- return (
-
- )
- }
- }
- })}
-
+ }
+ }
+ })}
+
- {defaultSensitive() &&
- (sensitiveShown ? (
-
-
- ) : (
+ {defaultSensitive() &&
+ (sensitiveShown ? (
+
- )
- },
- (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
- }
-)
+
+ ) : (
+
+ )
+}
export default TimelineAttachment
diff --git a/src/components/Timeline/Shared/Avatar.tsx b/src/components/Timeline/Shared/Avatar.tsx
index a41e8a81..e9d6a052 100644
--- a/src/components/Timeline/Shared/Avatar.tsx
+++ b/src/components/Timeline/Shared/Avatar.tsx
@@ -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 = ({ account }) => {
+ const { status, highlighted, disableOnPress } = useContext(StatusContext)
+ const actualAccount = account || status?.account
+ if (!actualAccount) return null
+
const { t } = useTranslation('componentTimeline')
const navigation = useNavigation>()
- // Need to fix go back root
- const onPress = useCallback(() => {
- queryKey && navigation.push('Tab-Shared-Account', { account })
- }, [])
return (
+ !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
diff --git a/src/components/Timeline/Shared/Card.tsx b/src/components/Timeline/Shared/Card.tsx
index d8f03112..31fcb1c9 100644
--- a/src/components/Timeline/Shared/Card.tsx
+++ b/src/components/Timeline/Shared/Card.tsx
@@ -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
-}
+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()
- const isAccount = matchAccount(card.url)
+ const isAccount = matchAccount(status.card.url)
const [foundAccount, setFoundAccount] = useState()
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 ? (
@@ -155,9 +155,9 @@ const TimelineCard = React.memo(({ card }: Props) => {
fontWeight='Bold'
testID='title'
>
- {card.title}
+ {status.card?.title}
- {card.description ? (
+ {status.card?.description ? (
{
}}
testID='description'
>
- {card.description}
+ {status.card.description}
) : null}
- {card.url}
+ {status.card?.url}
>
)
}
+ if (spoilerHidden || disableDetails) return null
+
return (
{
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
diff --git a/src/components/Timeline/Shared/Content.tsx b/src/components/Timeline/Shared/Content.tsx
index cfdbcbb7..5e2578a1 100644
--- a/src/components/Timeline/Shared/Content.tsx
+++ b/src/components/Timeline/Shared/Content.tsx
@@ -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 & {
- mentions?: Mastodon.Status['mentions']
- tags?: Mastodon.Status['tags']
- }
- highlighted?: boolean
- disableDetails?: boolean
+ setSpoilerExpanded?: React.Dispatch>
}
-const TimelineContent = React.memo(
- ({ status, highlighted = false, disableDetails = false }: Props) => {
- const { t } = useTranslation('componentTimeline')
- const instanceAccount = useSelector(getInstanceAccount, () => true)
+const TimelineContent: React.FC = ({ setSpoilerExpanded }) => {
+ const { status, highlighted, disableDetails } = useContext(StatusContext)
+ if (!status || typeof status.content !== 'string' || !status.content.length) return null
- return (
- <>
- {status.spoiler_text ? (
- <>
-
-
- >
- ) : (
+ const { t } = useTranslation('componentTimeline')
+ const instanceAccount = useSelector(getInstanceAccount, () => true)
+
+ return (
+ <>
+ {status.spoiler_text?.length ? (
+ <>
+
- )}
- >
- )
- },
- (prev, next) =>
- prev.status.content === next.status.content &&
- prev.status.spoiler_text === next.status.spoiler_text
-)
+ >
+ ) : (
+
+ )}
+ >
+ )
+}
export default TimelineContent
diff --git a/src/components/Timeline/Shared/Context.tsx b/src/components/Timeline/Shared/Context.tsx
new file mode 100644
index 00000000..5447643b
--- /dev/null
+++ b/src/components/Timeline/Shared/Context.tsx
@@ -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({} as ContextType)
+
+export default StatusContext
diff --git a/src/components/Timeline/Shared/Feedback.tsx b/src/components/Timeline/Shared/Feedback.tsx
index 0d2d11f4..8fa8f30a 100644
--- a/src/components/Timeline/Shared/Feedback.tsx
+++ b/src/components/Timeline/Shared/Feedback.tsx
@@ -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
- 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>()
- const { t } = useTranslation('componentTimeline')
- const { colors } = useTheme()
- const navigation = useNavigation>()
+ 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 (
-
-
- {status.reblogs_count > 0 ? (
-
+
+ {status.reblogs_count > 0 ? (
+
+ 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
- })}
-
- ) : null}
- {status.favourites_count > 0 ? (
-
- 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
+ })}
+
+ ) : null}
+ {status.favourites_count > 0 ? (
+
+ navigation.push('Tab-Shared-Users', {
+ reference: 'statuses',
+ id: status.id,
+ type: 'favourited_by',
count: status.favourites_count
- })}
-
- ) : null}
-
-
- {data && data.length > 1 ? (
- navigation.push('Tab-Shared-History', { id: status.id })}
- >
- {t('shared.actionsUsers.history.text', {
- count: data.length - 1
- })}
-
- ) : null}
-
+ })
+ }
+ >
+ {t('shared.actionsUsers.favourited_by.text', {
+ count: status.favourites_count
+ })}
+
+ ) : null}
- )
- },
- (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
-)
+
+ {data && data.length > 1 ? (
+ navigation.push('Tab-Shared-History', { id: status.id })}
+ >
+ {t('shared.actionsUsers.history.text', {
+ count: data.length - 1
+ })}
+
+ ) : null}
+
+
+ )
+}
const styles = StyleSheet.create({
text: {
diff --git a/src/components/Timeline/Shared/FullConversation.tsx b/src/components/Timeline/Shared/FullConversation.tsx
index 81fe3e06..8842ad77 100644
--- a/src/components/Timeline/Shared/FullConversation.tsx
+++ b/src/components/Timeline/Shared/FullConversation.tsx
@@ -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) ? (
+
+ {t('shared.fullConversation')}
+
+ ) : 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) ? (
-
- {t('shared.fullConversation')}
-
- ) : null
- },
- () => true
-)
-
export default TimelineFullConversation
diff --git a/src/components/Timeline/Shared/HeaderAndroid.tsx b/src/components/Timeline/Shared/HeaderAndroid.tsx
index e1034b10..204db6df 100644
--- a/src/components/Timeline/Shared/HeaderAndroid.tsx
+++ b/src/components/Timeline/Shared/HeaderAndroid.tsx
@@ -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 = ({ queryKey, rootQueryKey, status }) => {
- if (Platform.OS !== 'android' || !status) return null
+ if (Platform.OS !== 'android' || !status || disableDetails || disableOnPress) return null
const { colors } = useTheme()
diff --git a/src/components/Timeline/Shared/HeaderConversation.tsx b/src/components/Timeline/Shared/HeaderConversation.tsx
index 1d5f7543..baa255d7 100644
--- a/src/components/Timeline/Shared/HeaderConversation.tsx
+++ b/src/components/Timeline/Shared/HeaderConversation.tsx
@@ -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 (
-
- {t('shared.header.conversation.withAccounts')}
- {accounts.map((account, index) => (
-
- {index !== 0 ? t('common:separator') : undefined}
-
-
- ))}
-
- )
-}
-
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 (
-
+
+ {t('shared.header.conversation.withAccounts')}
+ {conversation.accounts.map((account, index) => (
+
+ {index !== 0 ? t('common:separator') : undefined}
+
+
+ ))}
+
-}
+const TimelineHeaderDefault: React.FC = () => {
+ const { queryKey, rootQueryKey, status, copiableContent, highlighted, disableDetails } =
+ useContext(StatusContext)
+ if (!status) return null
-const TimelineHeaderDefault: React.FC = ({
- queryKey,
- rootQueryKey,
- status,
- highlighted,
- copiableContent
-}) => {
const { colors } = useTheme()
const { t } = useTranslation('componentContextMenu')
@@ -76,7 +63,7 @@ const TimelineHeaderDefault: React.FC = ({
- {Platform.OS !== 'android' && queryKey ? (
+ {Platform.OS !== 'android' && !disableDetails ? (
{
+const TimelineHeaderNotification: React.FC = ({ 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
default:
- if (notification.status) {
+ if (status) {
return (
- 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 = ({
- 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 = ({
const pollButton = useMemo(() => {
if (!poll.expired) {
- if (!sameAccount && !poll.voted) {
+ if (!ownAccount && !poll.voted) {
return (