mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Fixed #476
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user