mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	| @@ -1,6 +1,3 @@ | ||||
| name({ | ||||
|   'default' => "tooot" | ||||
| }) | ||||
| keywords({ | ||||
|   'default' => "Mastodon,tooot,social,decentralized,长毛象,社交,去中心" | ||||
| }) | ||||
|   | ||||
							
								
								
									
										1
									
								
								fastlane/metadata/android/en-US/title.txt
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								fastlane/metadata/android/en-US/title.txt
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| ../../en-US/name.txt | ||||
| @@ -1 +0,0 @@ | ||||
| tooot | ||||
							
								
								
									
										1
									
								
								fastlane/metadata/android/zh-CN/title.txt
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								fastlane/metadata/android/zh-CN/title.txt
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| ../../zh-Hans/name.txt | ||||
							
								
								
									
										1
									
								
								fastlane/metadata/default/name.txt
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								fastlane/metadata/default/name.txt
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| ../en-US/name.txt | ||||
							
								
								
									
										1
									
								
								fastlane/metadata/en-US/name.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								fastlane/metadata/en-US/name.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| tooot - multilingual Mastodon app | ||||
| @@ -1 +1 @@ | ||||
| Open source Mastodon client | ||||
| Simple, just works | ||||
							
								
								
									
										1
									
								
								fastlane/metadata/zh-Hans/name.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								fastlane/metadata/zh-Hans/name.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| tooot - 探索联邦宇宙 | ||||
| @@ -1 +1 @@ | ||||
| 开源毛象客户端 | ||||
| 简约,想你所想 | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "tooot", | ||||
|   "version": "4.8.5", | ||||
|   "version": "4.8.6", | ||||
|   "description": "tooot for Mastodon", | ||||
|   "author": "xmflsct <me@xmflsct.com>", | ||||
|   "license": "GPL-3.0-or-later", | ||||
|   | ||||
							
								
								
									
										18
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								src/App.tsx
									
									
									
									
									
								
							| @@ -2,6 +2,7 @@ import { ActionSheetProvider } from '@expo/react-native-action-sheet' | ||||
| import * as Sentry from '@sentry/react-native' | ||||
| import { QueryClientProvider } from '@tanstack/react-query' | ||||
| import AccessibilityManager from '@utils/accessibility/AccessibilityManager' | ||||
| import { connectVerify } from '@utils/api/helpers/connect' | ||||
| import getLanguage from '@utils/helpers/getLanguage' | ||||
| import { queryClient } from '@utils/queryHooks' | ||||
| import audio from '@utils/startup/audio' | ||||
| @@ -23,6 +24,10 @@ import { enableFreeze } from 'react-native-screens' | ||||
| import i18n from './i18n' | ||||
| import Screens from './screens' | ||||
|  | ||||
| export const GLOBAL: { connect?: boolean } = { | ||||
|   connect: undefined | ||||
| } | ||||
|  | ||||
| Platform.select({ | ||||
|   android: LogBox.ignoreLogs(['Setting a timer for a long period of time']) | ||||
| }) | ||||
| @@ -50,7 +55,17 @@ const App: React.FC = () => { | ||||
|           await migrateFromAsyncStorage() | ||||
|           setHasMigrated(true) | ||||
|         } catch {} | ||||
|       } else { | ||||
|       } | ||||
|  | ||||
|       const useConnect = getGlobalStorage.boolean('app.connect') | ||||
|       GLOBAL.connect = useConnect | ||||
|       log('log', 'App', `connect: ${useConnect}`) | ||||
|       if (useConnect) { | ||||
|         await connectVerify() | ||||
|           .then(() => log('log', 'App', 'connected')) | ||||
|           .catch(() => log('warn', 'App', 'connect verify failed')) | ||||
|       } | ||||
|  | ||||
|       log('log', 'App', 'loading from MMKV') | ||||
|       const account = getGlobalStorage.string('account.active') | ||||
|       if (account) { | ||||
| @@ -65,7 +80,6 @@ const App: React.FC = () => { | ||||
|           setGlobalStorage('account.active', undefined) | ||||
|         } | ||||
|       } | ||||
|       } | ||||
|  | ||||
|       log('log', 'App', `locale: ${Localization.locale}`) | ||||
|       const language = getLanguage() | ||||
|   | ||||
| @@ -45,6 +45,7 @@ const ComponentAccount: React.FC<PropsWithChildren & Props> = ({ account, props, | ||||
|                 borderRadius: 8, | ||||
|                 marginRight: StyleConstants.Spacing.S | ||||
|               }} | ||||
|               dim | ||||
|             /> | ||||
|             <View style={{ flex: 1 }}> | ||||
|               <CustomText numberOfLines={1}> | ||||
|   | ||||
| @@ -1,9 +1,13 @@ | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { ReadableAccountType, setAccount } from '@utils/storage/actions' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React from 'react' | ||||
| import Button from './Button' | ||||
| import { Pressable } from 'react-native' | ||||
| import GracefullyImage from './GracefullyImage' | ||||
| import haptics from './haptics' | ||||
| import Icon from './Icon' | ||||
| import CustomText from './Text' | ||||
|  | ||||
| interface Props { | ||||
|   account: ReadableAccountType | ||||
| @@ -11,26 +15,56 @@ interface Props { | ||||
| } | ||||
|  | ||||
| const AccountButton: React.FC<Props> = ({ account, additionalActions }) => { | ||||
|   const { colors } = useTheme() | ||||
|   const navigation = useNavigation() | ||||
|  | ||||
|   return ( | ||||
|     <Button | ||||
|       type='text' | ||||
|       selected={account.active} | ||||
|     <Pressable | ||||
|       style={{ | ||||
|         marginBottom: StyleConstants.Spacing.M, | ||||
|         marginRight: StyleConstants.Spacing.M | ||||
|         flexDirection: 'row', | ||||
|         alignItems: 'center', | ||||
|         paddingVertical: StyleConstants.Spacing.S, | ||||
|         paddingHorizontal: StyleConstants.Spacing.S * 1.5, | ||||
|         borderColor: account.active ? colors.blue : colors.border, | ||||
|         borderWidth: 1, | ||||
|         borderRadius: 99, | ||||
|         marginBottom: StyleConstants.Spacing.S | ||||
|       }} | ||||
|       content={account.acct} | ||||
|       onPress={() => { | ||||
|       onPress={async () => { | ||||
|         await setAccount(account.key) | ||||
|         haptics('Light') | ||||
|         setAccount(account.key) | ||||
|         navigation.goBack() | ||||
|         if (additionalActions) { | ||||
|           additionalActions() | ||||
|         } | ||||
|       }} | ||||
|     > | ||||
|       <GracefullyImage | ||||
|         uri={{ original: account.avatar_static }} | ||||
|         dimension={{ | ||||
|           width: StyleConstants.Font.Size.L, | ||||
|           height: StyleConstants.Font.Size.L | ||||
|         }} | ||||
|         style={{ borderRadius: StyleConstants.Font.Size.L / 2, overflow: 'hidden' }} | ||||
|       /> | ||||
|       <CustomText | ||||
|         fontStyle='M' | ||||
|         fontWeight={account.active ? 'Bold' : 'Normal'} | ||||
|         style={{ | ||||
|           color: account.active ? colors.blue : colors.primaryDefault, | ||||
|           marginLeft: StyleConstants.Spacing.S | ||||
|         }} | ||||
|         children={account.acct} | ||||
|       /> | ||||
|       {account.active ? ( | ||||
|         <Icon | ||||
|           name='check' | ||||
|           size={StyleConstants.Font.Size.L} | ||||
|           color={colors.blue} | ||||
|           style={{ marginLeft: StyleConstants.Spacing.S }} | ||||
|         /> | ||||
|       ) : null} | ||||
|     </Pressable> | ||||
|   ) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { emojis } from '@components/Emojis' | ||||
| import Icon from '@components/Icon' | ||||
| import CustomText from '@components/Text' | ||||
| import { useAccessibility } from '@utils/accessibility/AccessibilityManager' | ||||
| import { connectImage } from '@utils/api/helpers/connect' | ||||
| import { StorageAccount } from '@utils/storage/account' | ||||
| import { getAccountStorage, setAccountStorage } from '@utils/storage/actions' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| @@ -133,7 +134,7 @@ const EmojisList = () => { | ||||
|                   emoji: emoji.shortcode | ||||
|                 })} | ||||
|                 accessibilityHint={t('screenCompose:content.root.footer.emojis.accessibilityHint')} | ||||
|                 source={{ uri }} | ||||
|                 source={connectImage({ uri })} | ||||
|                 style={{ width: 32, height: 32 }} | ||||
|               /> | ||||
|             </Pressable> | ||||
|   | ||||
| @@ -2,8 +2,7 @@ import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { Fragment } from 'react' | ||||
| import { Trans, useTranslation } from 'react-i18next' | ||||
| import { View, ViewStyle } from 'react-native' | ||||
| import { TouchableNativeFeedback } from 'react-native-gesture-handler' | ||||
| import { Pressable, View, ViewStyle } from 'react-native' | ||||
| import Icon from './Icon' | ||||
| import CustomText from './Text' | ||||
|  | ||||
| @@ -19,7 +18,7 @@ export const Filter: React.FC<Props> = ({ onPress, filter, button, style }) => { | ||||
|   const { colors } = useTheme() | ||||
|  | ||||
|   return ( | ||||
|     <TouchableNativeFeedback onPress={onPress}> | ||||
|     <Pressable onPress={onPress}> | ||||
|       <View | ||||
|         style={{ | ||||
|           paddingVertical: StyleConstants.Spacing.S, | ||||
| @@ -106,6 +105,6 @@ export const Filter: React.FC<Props> = ({ onPress, filter, button, style }) => { | ||||
|           /> | ||||
|         )} | ||||
|       </View> | ||||
|     </TouchableNativeFeedback> | ||||
|     </Pressable> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { useAccessibility } from '@utils/accessibility/AccessibilityManager' | ||||
| import { connectImage } from '@utils/api/helpers/connect' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useEffect, useState } from 'react' | ||||
| import { | ||||
| @@ -37,6 +38,7 @@ export interface Props { | ||||
|       height: number | ||||
|     }> | ||||
|   > | ||||
|   dim?: boolean | ||||
| } | ||||
|  | ||||
| const GracefullyImage = ({ | ||||
| @@ -49,14 +51,15 @@ const GracefullyImage = ({ | ||||
|   onPress, | ||||
|   style, | ||||
|   imageStyle, | ||||
|   setImageDimensions | ||||
|   setImageDimensions, | ||||
|   dim | ||||
| }: Props) => { | ||||
|   const { reduceMotionEnabled } = useAccessibility() | ||||
|   const { colors } = useTheme() | ||||
|   const { colors, theme } = useTheme() | ||||
|   const [imageLoaded, setImageLoaded] = useState(false) | ||||
|  | ||||
|   const [currentUri, setCurrentUri] = useState<string | undefined>(uri.original || uri.remote) | ||||
|   const source = { | ||||
|   const source: { uri?: string } = { | ||||
|     uri: reduceMotionEnabled && uri.static ? uri.static : currentUri | ||||
|   } | ||||
|   useEffect(() => { | ||||
| @@ -90,12 +93,12 @@ const GracefullyImage = ({ | ||||
|     > | ||||
|       {uri.preview && !imageLoaded ? ( | ||||
|         <FastImage | ||||
|           source={{ uri: uri.preview }} | ||||
|           source={connectImage({ uri: uri.preview })} | ||||
|           style={[styles.placeholder, { backgroundColor: colors.shimmerDefault }]} | ||||
|         /> | ||||
|       ) : null} | ||||
|       <FastImage | ||||
|         source={source} | ||||
|         source={connectImage(source)} | ||||
|         style={[{ flex: 1 }, imageStyle]} | ||||
|         onLoad={() => { | ||||
|           setImageLoaded(true) | ||||
| @@ -110,6 +113,14 @@ const GracefullyImage = ({ | ||||
|         }} | ||||
|       /> | ||||
|       {blurhashView()} | ||||
|       {dim && theme !== 'light' ? ( | ||||
|         <View | ||||
|           style={[ | ||||
|             styles.placeholder, | ||||
|             { backgroundColor: 'black', opacity: theme === 'dark_lighter' ? 0.18 : 0.36 } | ||||
|           ]} | ||||
|         /> | ||||
|       ) : null} | ||||
|     </Pressable> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,12 @@ | ||||
| import CustomText from '@components/Text' | ||||
| import { useAccessibility } from '@utils/accessibility/AccessibilityManager' | ||||
| import { connectImage } from '@utils/api/helpers/connect' | ||||
| import { useGlobalStorage } from '@utils/storage/actions' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { adaptiveScale } from '@utils/styles/scaling' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React from 'react' | ||||
| import { Platform, TextStyle } from 'react-native' | ||||
| import { ColorValue, Platform, TextStyle } from 'react-native' | ||||
| import FastImage from 'react-native-fast-image' | ||||
|  | ||||
| const regexEmoji = new RegExp(/(:[A-Za-z0-9_]+:)/) | ||||
| @@ -14,6 +15,7 @@ export interface Props { | ||||
|   content?: string | ||||
|   emojis?: Mastodon.Emoji[] | ||||
|   size?: 'S' | 'M' | 'L' | ||||
|   color?: ColorValue | ||||
|   adaptiveSize?: boolean | ||||
|   fontBold?: boolean | ||||
|   style?: TextStyle | ||||
| @@ -23,6 +25,7 @@ const ParseEmojis: React.FC<Props> = ({ | ||||
|   content, | ||||
|   emojis, | ||||
|   size = 'M', | ||||
|   color, | ||||
|   adaptiveSize = false, | ||||
|   fontBold = false, | ||||
|   style | ||||
| @@ -41,13 +44,13 @@ const ParseEmojis: React.FC<Props> = ({ | ||||
|     adaptiveSize ? adaptiveFontsize : 0 | ||||
|   ) | ||||
|  | ||||
|   const { colors, theme } = useTheme() | ||||
|   const { colors } = useTheme() | ||||
|  | ||||
|   return ( | ||||
|     <CustomText | ||||
|       style={[ | ||||
|         { | ||||
|           color: colors.primaryDefault, | ||||
|           color: color || colors.primaryDefault, | ||||
|           fontSize: adaptedFontsize, | ||||
|           lineHeight: adaptedLineheight | ||||
|         }, | ||||
| @@ -75,7 +78,7 @@ const ParseEmojis: React.FC<Props> = ({ | ||||
|                   <CustomText key={emojiShortcode + i}> | ||||
|                     {i === 0 ? ' ' : undefined} | ||||
|                     <FastImage | ||||
|                       source={{ uri: uri.trim() }} | ||||
|                       source={connectImage({ uri: uri.trim() })} | ||||
|                       style={{ | ||||
|                         width: adaptedFontsize, | ||||
|                         height: adaptedFontsize, | ||||
|   | ||||
| @@ -16,11 +16,12 @@ import { ElementType, parseDocument } from 'htmlparser2' | ||||
| import i18next from 'i18next' | ||||
| import React, { useContext, useRef, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Platform, Pressable, Text, View } from 'react-native' | ||||
| import { ColorValue, Platform, Pressable, Text, View } from 'react-native' | ||||
|  | ||||
| export interface Props { | ||||
|   content: string | ||||
|   size?: 'S' | 'M' | 'L' | ||||
|   color?: ColorValue | ||||
|   adaptiveSize?: boolean | ||||
|   showFullLink?: boolean | ||||
|   numberOfLines?: number | ||||
| @@ -34,6 +35,7 @@ export interface Props { | ||||
| const ParseHTML: React.FC<Props> = ({ | ||||
|   content, | ||||
|   size = 'M', | ||||
|   color, | ||||
|   adaptiveSize = false, | ||||
|   showFullLink = false, | ||||
|   numberOfLines = 10, | ||||
| @@ -58,6 +60,7 @@ const ParseHTML: React.FC<Props> = ({ | ||||
|   const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>() | ||||
|   const { params } = useRoute() | ||||
|   const { colors } = useTheme() | ||||
|   const colorPrimary = color || colors.primaryDefault | ||||
|   const { t } = useTranslation('componentParse') | ||||
|   if (!expandHint) { | ||||
|     expandHint = t('HTML.defaultHint') | ||||
| @@ -111,6 +114,7 @@ const ParseHTML: React.FC<Props> = ({ | ||||
|             content={content} | ||||
|             emojis={status?.emojis || emojis} | ||||
|             size={size} | ||||
|             color={colorPrimary} | ||||
|             adaptiveSize={adaptiveSize} | ||||
|           /> | ||||
|         ) | ||||
| @@ -181,7 +185,7 @@ const ParseHTML: React.FC<Props> = ({ | ||||
|                 return ( | ||||
|                   <Text | ||||
|                     key={index} | ||||
|                     style={{ color: matchedMention ? colors.blue : colors.primaryDefault }} | ||||
|                     style={{ color: matchedMention ? colors.blue : colorPrimary }} | ||||
|                     onPress={() => | ||||
|                       matchedMention && | ||||
|                       !disableDetails && | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { ColorValue, TouchableNativeFeedback, View } from 'react-native' | ||||
| import { ColorValue, Pressable, View } from 'react-native' | ||||
| import { SwipeListView } from 'react-native-swipe-list-view' | ||||
| import haptics from './haptics' | ||||
| import Icon, { IconName } from './Icon' | ||||
| @@ -25,7 +25,7 @@ export const SwipeToActions = <T extends unknown>({ | ||||
|       renderHiddenItem={({ item }) => ( | ||||
|         <View style={{ flex: 1, flexDirection: 'row', justifyContent: 'flex-end' }}> | ||||
|           {actions.map((action, index) => ( | ||||
|             <TouchableNativeFeedback | ||||
|             <Pressable | ||||
|               key={index} | ||||
|               onPress={() => { | ||||
|                 haptics(action.haptic || 'Light') | ||||
| @@ -43,7 +43,7 @@ export const SwipeToActions = <T extends unknown>({ | ||||
|               > | ||||
|                 <Icon name={action.icon} color='white' size={StyleConstants.Font.Size.L} /> | ||||
|               </View> | ||||
|             </TouchableNativeFeedback> | ||||
|             </Pressable> | ||||
|           ))} | ||||
|         </View> | ||||
|       )} | ||||
|   | ||||
| @@ -88,6 +88,7 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig | ||||
|                       : StyleConstants.Avatar.M | ||||
|                 }} | ||||
|                 style={{ flex: 1, flexBasis: '50%' }} | ||||
|                 dim | ||||
|               /> | ||||
|             ))} | ||||
|           </View> | ||||
|   | ||||
| @@ -33,6 +33,7 @@ export interface Props { | ||||
|   item: Mastodon.Status & { _pinned?: boolean } // For account page, internal property | ||||
|   queryKey?: QueryKeyTimeline | ||||
|   highlighted?: boolean | ||||
|   suppressSpoiler?: boolean // Same content as the main thread, can be dimmed | ||||
|   disableDetails?: boolean | ||||
|   disableOnPress?: boolean | ||||
|   isConversation?: boolean | ||||
| @@ -44,6 +45,7 @@ const TimelineDefault: React.FC<Props> = ({ | ||||
|   item, | ||||
|   queryKey, | ||||
|   highlighted = false, | ||||
|   suppressSpoiler = false, | ||||
|   disableDetails = false, | ||||
|   disableOnPress = false, | ||||
|   isConversation = false, | ||||
| @@ -170,6 +172,7 @@ const TimelineDefault: React.FC<Props> = ({ | ||||
|         detectedLanguage, | ||||
|         excludeMentions, | ||||
|         highlighted, | ||||
|         suppressSpoiler, | ||||
|         inThread: queryKey?.[1].page === 'Toot', | ||||
|         disableDetails, | ||||
|         disableOnPress, | ||||
|   | ||||
| @@ -17,9 +17,9 @@ import Animated, { | ||||
|   Extrapolate, | ||||
|   interpolate, | ||||
|   runOnJS, | ||||
|   SharedValue, | ||||
|   useAnimatedReaction, | ||||
|   useAnimatedStyle, | ||||
|   useDerivedValue, | ||||
|   useSharedValue, | ||||
|   withTiming | ||||
| } from 'react-native-reanimated' | ||||
| @@ -27,9 +27,10 @@ import Animated, { | ||||
| export interface Props { | ||||
|   flRef: RefObject<FlatList<any>> | ||||
|   queryKey: QueryKeyTimeline | ||||
|   fetchingActive: React.MutableRefObject<boolean> | ||||
|   scrollY: Animated.SharedValue<number> | ||||
|   fetchingType: Animated.SharedValue<0 | 1 | 2> | ||||
|   isFetchingPrev: SharedValue<boolean> | ||||
|   setFetchedCount: React.Dispatch<React.SetStateAction<number | null>> | ||||
|   scrollY: SharedValue<number> | ||||
|   fetchingType: SharedValue<0 | 1 | 2> | ||||
|   disableRefresh?: boolean | ||||
|   readMarker?: 'read_marker_following' | ||||
| } | ||||
| @@ -41,7 +42,8 @@ export const SEPARATION_Y_2 = -(CONTAINER_HEIGHT * 1.5 + StyleConstants.Font.Siz | ||||
| const TimelineRefresh: React.FC<Props> = ({ | ||||
|   flRef, | ||||
|   queryKey, | ||||
|   fetchingActive, | ||||
|   isFetchingPrev, | ||||
|   setFetchedCount, | ||||
|   scrollY, | ||||
|   fetchingType, | ||||
|   disableRefresh = false, | ||||
| @@ -55,20 +57,11 @@ const TimelineRefresh: React.FC<Props> = ({ | ||||
|   } | ||||
|  | ||||
|   const PREV_PER_BATCH = 1 | ||||
|   const prevActive = useRef<boolean>(false) | ||||
|   const prevCache = useRef<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>() | ||||
|   const prevStatusId = useRef<Mastodon.Status['id']>() | ||||
|  | ||||
|   const queryClient = useQueryClient() | ||||
|   const { refetch, isRefetching } = useTimelineQuery({ ...queryKey[1] }) | ||||
|  | ||||
|   useDerivedValue(() => { | ||||
|     if (prevActive.current || isRefetching) { | ||||
|       fetchingActive.current = true | ||||
|     } else { | ||||
|       fetchingActive.current = false | ||||
|     } | ||||
|   }, [prevActive.current, isRefetching]) | ||||
|   const { refetch } = useTimelineQuery({ ...queryKey[1] }) | ||||
|  | ||||
|   const { t } = useTranslation('componentTimeline') | ||||
|   const { colors } = useTheme() | ||||
| @@ -96,7 +89,7 @@ const TimelineRefresh: React.FC<Props> = ({ | ||||
|   const arrowStage = useSharedValue(0) | ||||
|   useAnimatedReaction( | ||||
|     () => { | ||||
|       if (fetchingActive.current) { | ||||
|       if (isFetchingPrev.value) { | ||||
|         return false | ||||
|       } | ||||
|       switch (arrowStage.value) { | ||||
| @@ -128,13 +121,12 @@ const TimelineRefresh: React.FC<Props> = ({ | ||||
|       if (data) { | ||||
|         runOnJS(haptics)('Light') | ||||
|       } | ||||
|     }, | ||||
|     [fetchingActive.current] | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   const fetchAndScrolled = useSharedValue(false) | ||||
|   const runFetchPrevious = async () => { | ||||
|     if (prevActive.current) return | ||||
|     if (isFetchingPrev.value) return | ||||
|  | ||||
|     const firstPage = | ||||
|       queryClient.getQueryData< | ||||
| @@ -143,7 +135,7 @@ const TimelineRefresh: React.FC<Props> = ({ | ||||
|         > | ||||
|       >(queryKey)?.pages[0] | ||||
|  | ||||
|     prevActive.current = true | ||||
|     isFetchingPrev.value = true | ||||
|     prevStatusId.current = firstPage?.body[0]?.id | ||||
|  | ||||
|     await queryFunctionTimeline({ | ||||
| @@ -151,7 +143,9 @@ const TimelineRefresh: React.FC<Props> = ({ | ||||
|       pageParam: firstPage?.links?.prev, | ||||
|       meta: {} | ||||
|     }) | ||||
|       .then(res => { | ||||
|       .then(async res => { | ||||
|         setFetchedCount(res.body.length) | ||||
|  | ||||
|         if (!res.body.length) return | ||||
|  | ||||
|         queryClient.setQueryData< | ||||
| @@ -172,7 +166,7 @@ const TimelineRefresh: React.FC<Props> = ({ | ||||
|       }) | ||||
|       .then(async nextLength => { | ||||
|         if (!nextLength) { | ||||
|           prevActive.current = false | ||||
|           isFetchingPrev.value = false | ||||
|           return | ||||
|         } | ||||
|  | ||||
| @@ -209,7 +203,7 @@ const TimelineRefresh: React.FC<Props> = ({ | ||||
|             } | ||||
|           }) | ||||
|         } | ||||
|         prevActive.current = false | ||||
|         isFetchingPrev.value = false | ||||
|       }) | ||||
|   } | ||||
|  | ||||
| @@ -218,6 +212,18 @@ const TimelineRefresh: React.FC<Props> = ({ | ||||
|     if (readMarker) { | ||||
|       setAccountStorage([{ key: readMarker, value: undefined }]) | ||||
|     } | ||||
|     queryClient.setQueryData< | ||||
|       InfiniteData< | ||||
|         PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]> | ||||
|       > | ||||
|     >(queryKey, old => { | ||||
|       if (!old) return old | ||||
|  | ||||
|       return { | ||||
|         pages: [old.pages[0]], | ||||
|         pageParams: [old.pageParams[0]] | ||||
|       } | ||||
|     }) | ||||
|     await refetch() | ||||
|     setTimeout(() => flRef.current?.scrollToOffset({ offset: 0 }), 50) | ||||
|   } | ||||
|   | ||||
| @@ -83,11 +83,9 @@ const AttachmentAudio: React.FC<Props> = ({ total, index, sensitiveShown, audio | ||||
|           <> | ||||
|             {audio.preview_url ? ( | ||||
|               <GracefullyImage | ||||
|                 uri={{ | ||||
|                   original: audio.preview_url, | ||||
|                   remote: audio.preview_remote_url | ||||
|                 }} | ||||
|                 uri={{ original: audio.preview_url, remote: audio.preview_remote_url }} | ||||
|                 style={styles.background} | ||||
|                 dim | ||||
|               /> | ||||
|             ) : null} | ||||
|             <Button | ||||
|   | ||||
| @@ -39,6 +39,7 @@ const AttachmentImage = ({ | ||||
|           blurhash={image.blurhash} | ||||
|           onPress={() => navigateToImagesViewer(image.id)} | ||||
|           style={{ aspectRatio: aspectRatio({ total, index, ...image.meta?.original }) }} | ||||
|           dim | ||||
|         /> | ||||
|       </View> | ||||
|       <AttachmentAltText sensitiveShown={sensitiveShown} text={image.description} /> | ||||
|   | ||||
| @@ -31,8 +31,7 @@ const TimelineAvatar: React.FC<Props> = ({ account }) => { | ||||
|         }) | ||||
|       })} | ||||
|       onPress={() => | ||||
|         !disableOnPress && | ||||
|         navigation.push('Tab-Shared-Account', { account: actualAccount }) | ||||
|         !disableOnPress && navigation.push('Tab-Shared-Account', { account: actualAccount }) | ||||
|       } | ||||
|       uri={{ original: actualAccount.avatar, static: actualAccount.avatar_static }} | ||||
|       dimension={ | ||||
| @@ -51,6 +50,7 @@ const TimelineAvatar: React.FC<Props> = ({ account }) => { | ||||
|         overflow: 'hidden', | ||||
|         marginRight: StyleConstants.Spacing.S | ||||
|       }} | ||||
|       dim | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -86,6 +86,7 @@ const TimelineCard: React.FC = () => { | ||||
|             blurhash={status.card.blurhash} | ||||
|             style={{ flexBasis: StyleConstants.Font.LineHeight.M * 5 }} | ||||
|             imageStyle={{ borderTopLeftRadius: 6, borderBottomLeftRadius: 6 }} | ||||
|             dim | ||||
|           /> | ||||
|         ) : null} | ||||
|         <View style={{ flex: 1, padding: StyleConstants.Spacing.S }}> | ||||
|   | ||||
| @@ -14,7 +14,7 @@ export interface Props { | ||||
| } | ||||
|  | ||||
| const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoilerExpanded }) => { | ||||
|   const { status, highlighted, inThread } = useContext(StatusContext) | ||||
|   const { status, highlighted, suppressSpoiler, inThread } = useContext(StatusContext) | ||||
|   if (!status || typeof status.content !== 'string' || !status.content.length) return null | ||||
|  | ||||
|   const { colors } = useTheme() | ||||
| @@ -35,6 +35,7 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi | ||||
|             size={highlighted ? 'L' : 'M'} | ||||
|             adaptiveSize | ||||
|             numberOfLines={999} | ||||
|             color={suppressSpoiler ? colors.disabled : undefined} | ||||
|           /> | ||||
|           {inThread ? ( | ||||
|             <CustomText | ||||
|   | ||||
| @@ -15,6 +15,7 @@ type StatusContextType = { | ||||
|   excludeMentions?: React.MutableRefObject<Mastodon.Mention[]> | ||||
|  | ||||
|   highlighted?: boolean | ||||
|   suppressSpoiler?: boolean | ||||
|   inThread?: boolean | ||||
|   disableDetails?: boolean | ||||
|   disableOnPress?: boolean | ||||
|   | ||||
| @@ -15,8 +15,8 @@ const TimelineFullConversation = () => { | ||||
|   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) ? ( | ||||
|     (status.mentions?.length === 0 || | ||||
|       status.mentions?.filter(mention => mention.id !== status.in_reply_to_account_id).length) ? ( | ||||
|     <CustomText | ||||
|       fontStyle='S' | ||||
|       style={{ | ||||
|   | ||||
| @@ -22,7 +22,7 @@ const HeaderSharedReplies: React.FC = () => { | ||||
|   excludeMentions && | ||||
|     (excludeMentions.current = | ||||
|       mentionsBeginning?.length && status?.mentions | ||||
|         ? status.mentions.filter(mention => mentionsBeginning.includes(`@${mention.username}`)) | ||||
|         ? status.mentions?.filter(mention => mentionsBeginning.includes(`@${mention.username}`)) | ||||
|         : []) | ||||
|  | ||||
|   return excludeMentions?.current.length ? ( | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import ComponentSeparator from '@components/Separator' | ||||
| import CustomText from '@components/Text' | ||||
| import TimelineDefault from '@components/Timeline/Default' | ||||
| import { useScrollToTop } from '@react-navigation/native' | ||||
| import { UseInfiniteQueryOptions } from '@tanstack/react-query' | ||||
| @@ -11,9 +12,21 @@ import { | ||||
| } from '@utils/storage/actions' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { RefObject, useRef } from 'react' | ||||
| import React, { RefObject, useRef, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { FlatList, FlatListProps, Platform, RefreshControl } from 'react-native' | ||||
| import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated' | ||||
| import Animated, { | ||||
|   Easing, | ||||
|   runOnJS, | ||||
|   useAnimatedReaction, | ||||
|   useAnimatedScrollHandler, | ||||
|   useAnimatedStyle, | ||||
|   useDerivedValue, | ||||
|   useSharedValue, | ||||
|   withDelay, | ||||
|   withSequence, | ||||
|   withTiming | ||||
| } from 'react-native-reanimated' | ||||
| import TimelineEmpty from './Empty' | ||||
| import TimelineFooter from './Footer' | ||||
| import TimelineRefresh, { SEPARATION_Y_1, SEPARATION_Y_2 } from './Refresh' | ||||
| @@ -42,9 +55,10 @@ const Timeline: React.FC<Props> = ({ | ||||
|   readMarker = undefined, | ||||
|   customProps | ||||
| }) => { | ||||
|   const { colors } = useTheme() | ||||
|   const { colors, theme } = useTheme() | ||||
|   const { t } = useTranslation('componentTimeline') | ||||
|  | ||||
|   const { data, refetch, isFetching, isLoading, fetchNextPage, isFetchingNextPage } = | ||||
|   const { data, refetch, isFetching, isLoading, isRefetching, fetchNextPage, isFetchingNextPage } = | ||||
|     useTimelineQuery({ | ||||
|       ...queryKey[1], | ||||
|       options: { | ||||
| @@ -57,7 +71,47 @@ const Timeline: React.FC<Props> = ({ | ||||
|     }) | ||||
|  | ||||
|   const flRef = useRef<FlatList>(null) | ||||
|   const fetchingActive = useRef<boolean>(false) | ||||
|   const isFetchingPrev = useSharedValue<boolean>(false) | ||||
|   const [fetchedCount, setFetchedCount] = useState<number | null>(null) | ||||
|   const fetchedNoticeHeight = useSharedValue<number>(100) | ||||
|   const notifiedFetchedNotice = useSharedValue<boolean>(false) | ||||
|   useAnimatedReaction( | ||||
|     () => isFetchingPrev.value, | ||||
|     (curr, prev) => { | ||||
|       if (curr === true && prev === false) { | ||||
|         notifiedFetchedNotice.value = true | ||||
|       } | ||||
|     } | ||||
|   ) | ||||
|   useAnimatedReaction( | ||||
|     () => fetchedCount, | ||||
|     (curr, prev) => { | ||||
|       if (curr !== null && prev === null) { | ||||
|         notifiedFetchedNotice.value = false | ||||
|       } | ||||
|     }, | ||||
|     [fetchedCount] | ||||
|   ) | ||||
|   const fetchedNoticeTop = useDerivedValue(() => { | ||||
|     if (notifiedFetchedNotice.value || fetchedCount !== null) { | ||||
|       return withSequence( | ||||
|         withTiming(fetchedNoticeHeight.value + 16 + 4), | ||||
|         withDelay( | ||||
|           2000, | ||||
|           withTiming( | ||||
|             0, | ||||
|             { easing: Easing.out(Easing.ease) }, | ||||
|             finished => finished && runOnJS(setFetchedCount)(null) | ||||
|           ) | ||||
|         ) | ||||
|       ) | ||||
|     } else { | ||||
|       return 0 | ||||
|     } | ||||
|   }, [fetchedCount]) | ||||
|   const fetchedNoticeAnimate = useAnimatedStyle(() => ({ | ||||
|     transform: [{ translateY: fetchedNoticeTop.value }] | ||||
|   })) | ||||
|  | ||||
|   const scrollY = useSharedValue(0) | ||||
|   const fetchingType = useSharedValue<0 | 1 | 2>(0) | ||||
| @@ -95,7 +149,12 @@ const Timeline: React.FC<Props> = ({ | ||||
|               const marker = readMarker ? getAccountStorage.string(readMarker) : undefined | ||||
|  | ||||
|               const firstItemId = viewableItems.filter(item => item.isViewable)[0]?.item.id | ||||
|               if (!fetchingActive.current && firstItemId && firstItemId > (marker || '0')) { | ||||
|               if ( | ||||
|                 !isFetchingPrev.value && | ||||
|                 !isRefetching && | ||||
|                 firstItemId && | ||||
|                 firstItemId > (marker || '0') | ||||
|               ) { | ||||
|                 setAccountStorage([{ key: readMarker, value: firstItemId }]) | ||||
|               } else { | ||||
|                 // setAccountStorage([{ key: readMarker, value: '109519141378761752' }]) | ||||
| @@ -135,7 +194,8 @@ const Timeline: React.FC<Props> = ({ | ||||
|       <TimelineRefresh | ||||
|         flRef={flRef} | ||||
|         queryKey={queryKey} | ||||
|         fetchingActive={fetchingActive} | ||||
|         isFetchingPrev={isFetchingPrev} | ||||
|         setFetchedCount={setFetchedCount} | ||||
|         scrollY={scrollY} | ||||
|         fetchingType={fetchingType} | ||||
|         disableRefresh={disableRefresh} | ||||
| @@ -176,6 +236,44 @@ const Timeline: React.FC<Props> = ({ | ||||
|         {...androidRefreshControl} | ||||
|         {...customProps} | ||||
|       /> | ||||
|       {!disableRefresh ? ( | ||||
|         <Animated.View | ||||
|           style={[ | ||||
|             { | ||||
|               position: 'absolute', | ||||
|               alignSelf: 'center', | ||||
|               top: -fetchedNoticeHeight.value - 16, | ||||
|               paddingVertical: StyleConstants.Spacing.S, | ||||
|               paddingHorizontal: StyleConstants.Spacing.M, | ||||
|               backgroundColor: colors.backgroundDefault, | ||||
|               shadowColor: colors.primaryDefault, | ||||
|               shadowOffset: { width: 0, height: 0 }, | ||||
|               shadowOpacity: theme === 'light' ? 0.16 : 0.24, | ||||
|               borderRadius: 99, | ||||
|               justifyContent: 'center', | ||||
|               alignItems: 'center' | ||||
|             }, | ||||
|             fetchedNoticeAnimate | ||||
|           ]} | ||||
|           onLayout={({ | ||||
|             nativeEvent: { | ||||
|               layout: { height } | ||||
|             } | ||||
|           }) => (fetchedNoticeHeight.value = height)} | ||||
|         > | ||||
|           <CustomText | ||||
|             fontStyle='S' | ||||
|             style={{ color: colors.primaryDefault }} | ||||
|             children={ | ||||
|               fetchedCount !== null | ||||
|                 ? fetchedCount > 0 | ||||
|                   ? t('refresh.fetched.found', { count: fetchedCount }) | ||||
|                   : t('refresh.fetched.none') | ||||
|                 : t('refresh.fetching') | ||||
|             } | ||||
|           /> | ||||
|         </Animated.View> | ||||
|       ) : null} | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -197,7 +197,7 @@ const menuStatus = ({ | ||||
|         hidden: | ||||
|           !ownAccount && | ||||
|           queryKey[1].page !== 'Notifications' && | ||||
|           !status.mentions.find( | ||||
|           !status.mentions?.find( | ||||
|             mention => mention.acct === accountAcct && mention.username === accountAcct | ||||
|           ) && | ||||
|           !status.muted | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|       "action_true": "Deixa de silenciar l'usuari" | ||||
|     }, | ||||
|     "followAs": { | ||||
|       "trigger": "", | ||||
|       "trigger": "Segueix com...", | ||||
|       "succeed_default": "Seguint a @{{target}} com @{{source}}", | ||||
|       "succeed_locked": "Enviada la sol·licitud de seguiment a @{{target}} com {{source}}, pendent d'aprovar-la", | ||||
|       "failed": "Segueix com" | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "Més recent des d'aquí", | ||||
|     "refetch": "A l'últim" | ||||
|     "refetch": "A l'últim", | ||||
|     "fetching": "Obtenint publicacions...", | ||||
|     "fetched": { | ||||
|       "none": "No n'hi ha de noves", | ||||
|       "found": "S'ha obtingut {{count}}" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -46,7 +46,7 @@ | ||||
|         "name": "Etiquetes seguides" | ||||
|       }, | ||||
|       "fontSize": { | ||||
|         "name": "Mida de la font de la publicació" | ||||
|         "name": "Mida de la font" | ||||
|       }, | ||||
|       "language": { | ||||
|         "name": "Idioma" | ||||
| @@ -136,7 +136,7 @@ | ||||
|     }, | ||||
|     "preferences": { | ||||
|       "visibility": { | ||||
|         "title": "Visibilitat de la publicació per defecte", | ||||
|         "title": "Visibilitat per defecte", | ||||
|         "options": { | ||||
|           "public": "Públic", | ||||
|           "unlisted": "Sense llistar", | ||||
| @@ -147,11 +147,11 @@ | ||||
|         "title": "Marca el contingut com a sensible" | ||||
|       }, | ||||
|       "media": { | ||||
|         "title": "Mostra el contingut multimèdia", | ||||
|         "title": "Multimèdia", | ||||
|         "options": { | ||||
|           "default": "Amaga el contingut multimèdia com a sensible", | ||||
|           "show_all": "Mostra sempre el contingut multimèdia", | ||||
|           "hide_all": "Amaga sempre el contingut multimèdia" | ||||
|           "default": "Amaga els sensibles", | ||||
|           "show_all": "Mostra'ls sempre", | ||||
|           "hide_all": "Amaga'ls sempre" | ||||
|         } | ||||
|       }, | ||||
|       "spoilers": { | ||||
| @@ -178,7 +178,7 @@ | ||||
|       "context": "Aplica a <0 />", | ||||
|       "contexts": { | ||||
|         "home": "Seguits i llistes", | ||||
|         "notifications": "Notificació", | ||||
|         "notifications": "Notificacions", | ||||
|         "public": "federat", | ||||
|         "thread": "Conversa", | ||||
|         "account": "Perfil" | ||||
| @@ -199,17 +199,17 @@ | ||||
|       "context": "Aplica a", | ||||
|       "contexts": { | ||||
|         "home": "Seguits i llistes", | ||||
|         "notifications": "Notificació", | ||||
|         "notifications": "Notificacions", | ||||
|         "public": "Línia de temps federada", | ||||
|         "thread": "Vista de conversa", | ||||
|         "account": "Vista del perfil" | ||||
|       }, | ||||
|       "action": "Quan coincideix", | ||||
|       "actions": { | ||||
|         "warn": "", | ||||
|         "warn": "Contret però pot ser revelat", | ||||
|         "hide": "Amagat completament" | ||||
|       }, | ||||
|       "keywords": "Coincidències per aquestes paraules claus", | ||||
|       "keywords": "Coincidències amb", | ||||
|       "keyword": "Paraula clau", | ||||
|       "statuses": "Coincideixen aquestes publicacions" | ||||
|     }, | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "", | ||||
|     "refetch": "" | ||||
|     "refetch": "", | ||||
|     "fetching": "", | ||||
|     "fetched": { | ||||
|       "none": "", | ||||
|       "found": "" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "Neuere Einträge", | ||||
|     "refetch": "Zum letzten" | ||||
|     "refetch": "Zum letzten", | ||||
|     "fetching": "", | ||||
|     "fetched": { | ||||
|       "none": "", | ||||
|       "found": "" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "Νεότερες από εδώ", | ||||
|     "refetch": "Μέχρι την τελευταία" | ||||
|     "refetch": "Μέχρι την τελευταία", | ||||
|     "fetching": "", | ||||
|     "fetched": { | ||||
|       "none": "", | ||||
|       "found": "" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "Newer from here", | ||||
|     "refetch": "To latest" | ||||
|     "refetch": "To latest", | ||||
|     "fetching": "Fetching newer toots ...", | ||||
|     "fetched": { | ||||
|       "none": "No newer toot", | ||||
|       "found": "Fetched {{count}} toots" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|       "action_true": "Dejar de silenciar al usuario" | ||||
|     }, | ||||
|     "followAs": { | ||||
|       "trigger": "", | ||||
|       "trigger": "Seguir como...", | ||||
|       "succeed_default": "Siguiendo @{{target}} como @{{source}}", | ||||
|       "succeed_locked": "Enviado la solicitud de seguimiento a @{{target}} como {{source}}, pendiente de aprobación", | ||||
|       "failed": "Seguir como" | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "Más reciente desde aquí", | ||||
|     "refetch": "Al último" | ||||
|     "refetch": "Al último", | ||||
|     "fetching": "Obteniendo publicaciones...", | ||||
|     "fetched": { | ||||
|       "none": "No hay nuevas", | ||||
|       "found": "Se ha obtenido {{count}}" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -136,7 +136,7 @@ | ||||
|     }, | ||||
|     "preferences": { | ||||
|       "visibility": { | ||||
|         "title": "Visibilidad de publicación predeterminada", | ||||
|         "title": "Visibilidad por defecto", | ||||
|         "options": { | ||||
|           "public": "Público", | ||||
|           "unlisted": "No listado", | ||||
| @@ -147,11 +147,11 @@ | ||||
|         "title": "Marcar el contenido multimedia como sensibles por defecto" | ||||
|       }, | ||||
|       "media": { | ||||
|         "title": "Mostrar el contenido multimedia", | ||||
|         "title": "Multimedia", | ||||
|         "options": { | ||||
|           "default": "Ocultar los contenidos multimedia marcados como sensibles", | ||||
|           "show_all": "Mostrar siempre el contenido multimedia", | ||||
|           "hide_all": "Siempre ocultar el contenido multimedia" | ||||
|           "default": "Ocultar los sensibles", | ||||
|           "show_all": "Mostrar siempre", | ||||
|           "hide_all": "Ocultar siempre" | ||||
|         } | ||||
|       }, | ||||
|       "spoilers": { | ||||
| @@ -178,7 +178,7 @@ | ||||
|       "context": "Se aplica en <0 />", | ||||
|       "contexts": { | ||||
|         "home": "Seguidos y listas", | ||||
|         "notifications": "Notificación", | ||||
|         "notifications": "Notificaciones", | ||||
|         "public": "Federado", | ||||
|         "thread": "Conversación", | ||||
|         "account": "Perfil" | ||||
| @@ -199,17 +199,17 @@ | ||||
|       "context": "Se aplica en", | ||||
|       "contexts": { | ||||
|         "home": "Seguidos y listas", | ||||
|         "notifications": "Notificación", | ||||
|         "notifications": "Notificaciones", | ||||
|         "public": "Cronología federada", | ||||
|         "thread": "Vista de conversación", | ||||
|         "account": "Vista de perfil" | ||||
|       }, | ||||
|       "action": "Al coincidir", | ||||
|       "actions": { | ||||
|         "warn": "", | ||||
|         "warn": "Contraído pero puede ser revelado", | ||||
|         "hide": "Oculto completamente" | ||||
|       }, | ||||
|       "keywords": "Coincide con estas palabras clave", | ||||
|       "keywords": "Coincide con", | ||||
|       "keyword": "Palabra clave", | ||||
|       "statuses": "Coincide con estas publicaciones" | ||||
|     }, | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "Berrienak hemendik hasita", | ||||
|     "refetch": "Azkenekora arte" | ||||
|     "refetch": "Azkenekora arte", | ||||
|     "fetching": "", | ||||
|     "fetched": { | ||||
|       "none": "", | ||||
|       "found": "" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "Plus récent à partir d'ici", | ||||
|     "refetch": "À la dernière" | ||||
|     "refetch": "À la dernière", | ||||
|     "fetching": "", | ||||
|     "fetched": { | ||||
|       "none": "", | ||||
|       "found": "" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "Più recenti da qui", | ||||
|     "refetch": "Al più recente" | ||||
|     "refetch": "Al più recente", | ||||
|     "fetching": "", | ||||
|     "fetched": { | ||||
|       "none": "", | ||||
|       "found": "" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "前のページを取得", | ||||
|     "refetch": "更新" | ||||
|     "refetch": "更新", | ||||
|     "fetching": "", | ||||
|     "fetched": { | ||||
|       "none": "", | ||||
|       "found": "" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "이 시점에 이어서 불러오기", | ||||
|     "refetch": "최신 내용 불러오기" | ||||
|     "refetch": "최신 내용 불러오기", | ||||
|     "fetching": "", | ||||
|     "fetched": { | ||||
|       "none": "", | ||||
|       "found": "" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "Nieuwere vanaf hier", | ||||
|     "refetch": "Naar nieuwste" | ||||
|     "refetch": "Naar nieuwste", | ||||
|     "fetching": "Nieuwere toots ophalen...", | ||||
|     "fetched": { | ||||
|       "none": "Geen nieuwere toot", | ||||
|       "found": "{{count}} toots opgehaald" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "Nowsze", | ||||
|     "refetch": "Do najnowszych" | ||||
|     "refetch": "Do najnowszych", | ||||
|     "fetching": "", | ||||
|     "fetched": { | ||||
|       "none": "", | ||||
|       "found": "" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|     "cancel": "Cancelar", | ||||
|     "discard": "Descartar", | ||||
|     "continue": "Continuar", | ||||
|     "create": "", | ||||
|     "create": "Criar", | ||||
|     "delete": "Excluir", | ||||
|     "done": "Concluído", | ||||
|     "confirm": "Confirmar" | ||||
|   | ||||
| @@ -6,9 +6,9 @@ | ||||
|       "action_false": "Seguir usuário", | ||||
|       "action_true": "Deixar de seguir usuário" | ||||
|     }, | ||||
|     "inLists": "", | ||||
|     "inLists": "Listas contendo usuário ...", | ||||
|     "showBoosts": { | ||||
|       "action_false": "", | ||||
|       "action_false": "Mostrar boosts do usuário", | ||||
|       "action_true": "" | ||||
|     }, | ||||
|     "mute": { | ||||
| @@ -21,7 +21,7 @@ | ||||
|       "succeed_locked": "", | ||||
|       "failed": "" | ||||
|     }, | ||||
|     "blockReport": "", | ||||
|     "blockReport": "Bloquear e denunciar", | ||||
|     "block": { | ||||
|       "action_false": "Bloquear usuário", | ||||
|       "action_true": "Desbloquear usuário", | ||||
| @@ -56,11 +56,11 @@ | ||||
|   }, | ||||
|   "hashtag": { | ||||
|     "follow": { | ||||
|       "action_false": "", | ||||
|       "action_true": "" | ||||
|       "action_false": "Seguir", | ||||
|       "action_true": "Deixar de seguir" | ||||
|     }, | ||||
|     "filter": { | ||||
|       "action": "" | ||||
|       "action": "Filtrar hashtag ..." | ||||
|     } | ||||
|   }, | ||||
|   "share": { | ||||
| @@ -99,8 +99,8 @@ | ||||
|       "action_true": "Desafixar toot" | ||||
|     }, | ||||
|     "filter": { | ||||
|       "action_false": "", | ||||
|       "action_true": "" | ||||
|       "action_false": "Filtrar toot ...", | ||||
|       "action_true": "Gerenciar filtros ..." | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "Mais novo aqui", | ||||
|     "refetch": "Mais recente" | ||||
|     "refetch": "Mais recente", | ||||
|     "fetching": "Buscando toots mais recentes...", | ||||
|     "fetched": { | ||||
|       "none": "Nenhum novo toot", | ||||
|       "found": "" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
| @@ -116,7 +121,7 @@ | ||||
|             "accessibilityHint": "Conta do usuário" | ||||
|           } | ||||
|         }, | ||||
|         "application": "", | ||||
|         "application": "via {{application}}", | ||||
|         "edited": { | ||||
|           "accessibilityLabel": "Toot editado" | ||||
|         }, | ||||
|   | ||||
| @@ -61,7 +61,7 @@ | ||||
|         "name": "Criar uma lista" | ||||
|       }, | ||||
|       "listEdit": { | ||||
|         "name": "" | ||||
|         "name": "Editar Detalhes da Lista" | ||||
|       }, | ||||
|       "lists": { | ||||
|         "name": "Listas" | ||||
| @@ -116,7 +116,7 @@ | ||||
|       "empty": "Nenhum usuário adicionado a esta lista" | ||||
|     }, | ||||
|     "listEdit": { | ||||
|       "heading": "", | ||||
|       "heading": "Editar detalhes da lista", | ||||
|       "title": "Título", | ||||
|       "repliesPolicy": { | ||||
|         "heading": "Mostrar respostas para:", | ||||
| @@ -144,7 +144,7 @@ | ||||
|         } | ||||
|       }, | ||||
|       "sensitive": { | ||||
|         "title": "" | ||||
|         "title": "Marcar mídia como sensível por padrão" | ||||
|       }, | ||||
|       "media": { | ||||
|         "title": "", | ||||
| @@ -166,22 +166,22 @@ | ||||
|       }, | ||||
|       "web_only": { | ||||
|         "title": "", | ||||
|         "description": "" | ||||
|         "description": "As configurações abaixo só podem ser atualizadas usando a interface web" | ||||
|       } | ||||
|     }, | ||||
|     "preferencesFilters": { | ||||
|       "expired": "", | ||||
|       "keywords_one": "", | ||||
|       "keywords_other": "", | ||||
|       "statuses_one": "", | ||||
|       "statuses_other": "", | ||||
|       "context": "", | ||||
|       "keywords_one": "{{count}} palavra-chave", | ||||
|       "keywords_other": "{{count}} palavras-chave", | ||||
|       "statuses_one": "{{count}} toot", | ||||
|       "statuses_other": "{{count}} toots", | ||||
|       "context": "Aplica-se em <0 />", | ||||
|       "contexts": { | ||||
|         "home": "", | ||||
|         "notifications": "", | ||||
|         "public": "", | ||||
|         "home": "seguindo e listas", | ||||
|         "notifications": "notificação", | ||||
|         "public": "global", | ||||
|         "thread": "", | ||||
|         "account": "" | ||||
|         "account": "perfil" | ||||
|       } | ||||
|     }, | ||||
|     "preferencesFilter": { | ||||
| @@ -196,22 +196,22 @@ | ||||
|         "604800": "Após 1 semana", | ||||
|         "18144000": "Após 1 mês" | ||||
|       }, | ||||
|       "context": "", | ||||
|       "context": "Aplica-se em", | ||||
|       "contexts": { | ||||
|         "home": "", | ||||
|         "notifications": "", | ||||
|         "public": "", | ||||
|         "home": "Seguindo e listas", | ||||
|         "notifications": "Notificação", | ||||
|         "public": "Linha do tempo global", | ||||
|         "thread": "", | ||||
|         "account": "" | ||||
|       }, | ||||
|       "action": "", | ||||
|       "actions": { | ||||
|         "warn": "", | ||||
|         "hide": "" | ||||
|         "warn": "Recolhido, mas pode ser revelado", | ||||
|         "hide": "Esconder completamente" | ||||
|       }, | ||||
|       "keywords": "", | ||||
|       "keyword": "", | ||||
|       "statuses": "" | ||||
|       "keyword": "Palavra-chave", | ||||
|       "statuses": "Corresponde a estes toots" | ||||
|     }, | ||||
|     "profile": { | ||||
|       "feedback": { | ||||
| @@ -252,7 +252,7 @@ | ||||
|         "label": "Rótulo", | ||||
|         "content": "Conteúdo" | ||||
|       }, | ||||
|       "mediaSelectionFailed": "" | ||||
|       "mediaSelectionFailed": "Falha no processamento da imagem. Por favor, tente novamente." | ||||
|     }, | ||||
|     "push": { | ||||
|       "notAvailable": "Seu telefone não suporta notificação de envio de tooot", | ||||
| @@ -343,7 +343,7 @@ | ||||
|         "heading": "Tema escuro", | ||||
|         "options": { | ||||
|           "lighter": "Padrão", | ||||
|           "darker": "" | ||||
|           "darker": "Preto verdadeiro" | ||||
|         } | ||||
|       }, | ||||
|       "browser": { | ||||
| @@ -354,7 +354,7 @@ | ||||
|         } | ||||
|       }, | ||||
|       "autoplayGifv": { | ||||
|         "heading": "" | ||||
|         "heading": "Reproduzir GIFs automaticamente na linha do tempo" | ||||
|       }, | ||||
|       "feedback": { | ||||
|         "heading": "Pedidos de Funcionalidades" | ||||
| @@ -392,37 +392,37 @@ | ||||
|       "suspended": "Conta suspensa pelos moderadores do seu servidor" | ||||
|     }, | ||||
|     "accountInLists": { | ||||
|       "name": "", | ||||
|       "name": "Listas de @{{username}}", | ||||
|       "inLists": "", | ||||
|       "notInLists": "" | ||||
|       "notInLists": "Outras listas" | ||||
|     }, | ||||
|     "attachments": { | ||||
|       "name": "<0 /><1>\"s mídia</1>" | ||||
|     }, | ||||
|     "filter": { | ||||
|       "name": "", | ||||
|       "existed": "" | ||||
|       "name": "Adicionar ao filtro", | ||||
|       "existed": "Existe nestes filtros" | ||||
|     }, | ||||
|     "history": { | ||||
|       "name": "Histórico de Edição" | ||||
|     }, | ||||
|     "report": { | ||||
|       "name": "", | ||||
|       "report": "", | ||||
|       "name": "Denuncia {{acct}}", | ||||
|       "report": "Denunciar", | ||||
|       "forward": { | ||||
|         "heading": "" | ||||
|         "heading": "Encaminhar anonimamente para o servidor remoto {{instance}}" | ||||
|       }, | ||||
|       "reasons": { | ||||
|         "heading": "", | ||||
|         "spam": "", | ||||
|         "other": "", | ||||
|         "violation": "" | ||||
|         "heading": "O que há de errado com essa conta?", | ||||
|         "spam": "É spam", | ||||
|         "other": "É outra coisa", | ||||
|         "violation": "Viola as regras do servidor" | ||||
|       }, | ||||
|       "comment": { | ||||
|         "heading": "" | ||||
|         "heading": "Deseja nos dizer mais alguma coisa?" | ||||
|       }, | ||||
|       "violatedRules": { | ||||
|         "heading": "" | ||||
|         "heading": "Regras violadas do servidor" | ||||
|       } | ||||
|     }, | ||||
|     "search": { | ||||
| @@ -442,7 +442,7 @@ | ||||
|           } | ||||
|         }, | ||||
|         "trending": { | ||||
|           "tags": "" | ||||
|           "tags": "Hashtags em alta" | ||||
|         } | ||||
|       }, | ||||
|       "sections": { | ||||
| @@ -451,13 +451,13 @@ | ||||
|         "statuses": "Toot" | ||||
|       }, | ||||
|       "notFound": "Não foi possível encontrar <bold>{{searchTerm}}</bold> {{type}} relacionado", | ||||
|       "noResult": "" | ||||
|       "noResult": "Não foi possível encontrar nada, tente um termo diferente" | ||||
|     }, | ||||
|     "toot": { | ||||
|       "name": "Discussões", | ||||
|       "remoteFetch": { | ||||
|         "title": "", | ||||
|         "message": "" | ||||
|         "message": "O conteúdo global nem sempre está disponível na instância local. Estes conteúdos são obtidos de instâncias remotas e marcados. Você pode interagir com esses conteúdos normalmente." | ||||
|       } | ||||
|     }, | ||||
|     "users": { | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "", | ||||
|     "refetch": "" | ||||
|     "refetch": "", | ||||
|     "fetching": "", | ||||
|     "fetched": { | ||||
|       "none": "", | ||||
|       "found": "" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "Nyare härifrån", | ||||
|     "refetch": "Till senaste" | ||||
|     "refetch": "Till senaste", | ||||
|     "fetching": "", | ||||
|     "fetched": { | ||||
|       "none": "", | ||||
|       "found": "" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "З цього моменту", | ||||
|     "refetch": "До кінця" | ||||
|     "refetch": "До кінця", | ||||
|     "fetching": "Отримання нових дмухів ...", | ||||
|     "fetched": { | ||||
|       "none": "Немає нових дмухів", | ||||
|       "found": "Отримано {{count}} дмухів" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "Trước đó", | ||||
|     "refetch": "Trang cuối" | ||||
|     "refetch": "Trang cuối", | ||||
|     "fetching": "", | ||||
|     "fetched": { | ||||
|       "none": "", | ||||
|       "found": "" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "较新于此的嘟嘟", | ||||
|     "refetch": "最新的嘟嘟" | ||||
|     "refetch": "最新的嘟嘟", | ||||
|     "fetching": "获取较新的嘟文…", | ||||
|     "fetched": { | ||||
|       "none": "没有更新的嘟文", | ||||
|       "found": "已获取 {{count}} 条嘟文" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -16,7 +16,12 @@ | ||||
|   }, | ||||
|   "refresh": { | ||||
|     "fetchPreviousPage": "較新的嘟文", | ||||
|     "refetch": "到最新的位置" | ||||
|     "refetch": "到最新的位置", | ||||
|     "fetching": "取得較新的嘟文 …", | ||||
|     "fetched": { | ||||
|       "none": "沒有更新的嘟文", | ||||
|       "found": "已取得 {{count}} 條嘟文" | ||||
|     } | ||||
|   }, | ||||
|   "shared": { | ||||
|     "actioned": { | ||||
|   | ||||
| @@ -92,7 +92,7 @@ const ScreenAccountSelection = ({ | ||||
|   const { colors } = useTheme() | ||||
|   const { t } = useTranslation('screenAccountSelection') | ||||
|  | ||||
|   const accounts = getReadableAccounts(true) | ||||
|   const accounts = getReadableAccounts() | ||||
|  | ||||
|   return ( | ||||
|     <ScrollView | ||||
| @@ -129,7 +129,7 @@ const ScreenAccountSelection = ({ | ||||
|             return ( | ||||
|               <AccountButton | ||||
|                 key={index} | ||||
|                 account={account} | ||||
|                 account={{ ...account, active: false }} | ||||
|                 additionalActions={() => | ||||
|                   navigationRef.navigate('Screen-Compose', { | ||||
|                     type: 'share', | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import RelativeTime from '@components/RelativeTime' | ||||
| import CustomText from '@components/Text' | ||||
| import { BlurView } from '@react-native-community/blur' | ||||
| import { useAccessibility } from '@utils/accessibility/AccessibilityManager' | ||||
| import { connectImage } from '@utils/api/helpers/connect' | ||||
| import { RootStackScreenProps } from '@utils/navigation/navigators' | ||||
| import { useAnnouncementMutation, useAnnouncementQuery } from '@utils/queryHooks/announcement' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| @@ -139,9 +140,9 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'> | ||||
|               > | ||||
|                 {reaction.url ? ( | ||||
|                   <FastImage | ||||
|                     source={{ | ||||
|                     source={connectImage({ | ||||
|                       uri: reduceMotionEnabled ? reaction.static_url : reaction.url | ||||
|                     }} | ||||
|                     })} | ||||
|                     style={{ | ||||
|                       width: StyleConstants.Font.LineHeight.M + 3, | ||||
|                       height: StyleConstants.Font.LineHeight.M | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import Icon from '@components/Icon' | ||||
| import { SwipeToActions } from '@components/SwipeToActions' | ||||
| import CustomText from '@components/Text' | ||||
| import HeaderSharedCreated from '@components/Timeline/Shared/HeaderShared/Created' | ||||
| import { connectImage } from '@utils/api/helpers/connect' | ||||
| import apiInstance from '@utils/api/instance' | ||||
| import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators' | ||||
| import { getAccountStorage, setAccountStorage, useAccountStorage } from '@utils/storage/actions' | ||||
| @@ -154,9 +155,11 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose- | ||||
|                             4, | ||||
|                           marginLeft: index !== 0 ? StyleConstants.Spacing.S : 0 | ||||
|                         }} | ||||
|                         source={{ | ||||
|                           uri: attachment.local?.thumbnail || attachment.remote?.preview_url | ||||
|                         }} | ||||
|                         source={ | ||||
|                           attachment.local?.thumbnail | ||||
|                             ? { uri: attachment.local?.thumbnail } | ||||
|                             : connectImage({ uri: attachment.remote?.preview_url }) | ||||
|                         } | ||||
|                       /> | ||||
|                     ))} | ||||
|                   </View> | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import { MAX_MEDIA_ATTACHMENTS } from '@components/mediaSelector' | ||||
| import CustomText from '@components/Text' | ||||
| import { useActionSheet } from '@expo/react-native-action-sheet' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { connectImage } from '@utils/api/helpers/connect' | ||||
| import { featureCheck } from '@utils/helpers/featureCheck' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import layoutAnimation from '@utils/styles/layoutAnimation' | ||||
| @@ -105,7 +106,11 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => { | ||||
|       > | ||||
|         <FastImage | ||||
|           style={{ width: '100%', height: '100%' }} | ||||
|           source={{ uri: item.local?.thumbnail || item.remote?.preview_url }} | ||||
|           source={ | ||||
|             item.local?.thumbnail | ||||
|               ? { uri: item.local?.thumbnail } | ||||
|               : connectImage({ uri: item.remote?.preview_url }) | ||||
|           } | ||||
|         /> | ||||
|         {item.remote?.meta?.original?.duration ? ( | ||||
|           <CustomText | ||||
| @@ -164,7 +169,8 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => { | ||||
|                 haptics('Success') | ||||
|               }} | ||||
|             /> | ||||
|             {composeState.type === 'edit' && featureCheck('edit_media_details') ? ( | ||||
|             {composeState.type !== 'edit' || | ||||
|             (composeState.type === 'edit' && featureCheck('edit_media_details')) ? ( | ||||
|               <Button | ||||
|                 accessibilityLabel={t('content.root.footer.attachments.edit.accessibilityLabel', { | ||||
|                   attachment: index + 1 | ||||
|   | ||||
| @@ -96,10 +96,10 @@ const Collections: React.FC = () => { | ||||
|         iconBack='chevron-right' | ||||
|         title={t('screenTabs:me.stacks.push.name')} | ||||
|         content={ | ||||
|           typeof instancePush.global === 'boolean' | ||||
|           typeof instancePush?.global === 'boolean' | ||||
|             ? t('screenTabs:me.root.push.content', { | ||||
|                 defaultValue: 'false', | ||||
|                 context: instancePush.global.toString() | ||||
|                 context: instancePush?.global.toString() | ||||
|               }) | ||||
|             : undefined | ||||
|         } | ||||
|   | ||||
| @@ -1,16 +1,18 @@ | ||||
| import haptics from '@components/haptics' | ||||
| import { MenuContainer, MenuRow } from '@components/Menu' | ||||
| import { useActionSheet } from '@expo/react-native-action-sheet' | ||||
| import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles' | ||||
| import { LOCALES } from '@i18n/locales' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { connectVerify } from '@utils/api/helpers/connect' | ||||
| import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles' | ||||
| import { useGlobalStorage } from '@utils/storage/actions' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import * as Localization from 'expo-localization' | ||||
| import React from 'react' | ||||
| import React, { useEffect, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Linking, Platform } from 'react-native' | ||||
| import { GLOBAL } from '../../../../App' | ||||
| import { mapFontsizeToName } from '../SettingsFontsize' | ||||
| import { LOCALES } from '@i18n/locales' | ||||
|  | ||||
| const SettingsApp: React.FC = () => { | ||||
|   const navigation = useNavigation<any>() | ||||
| @@ -24,6 +26,23 @@ const SettingsApp: React.FC = () => { | ||||
|   const [browser, setBrowser] = useGlobalStorage.string('app.browser') | ||||
|   const [autoplayGifv, setAutoplayGifv] = useGlobalStorage.boolean('app.auto_play_gifv') | ||||
|  | ||||
|   const [connect, setConnect] = useGlobalStorage.boolean('app.connect') | ||||
|   const [showConnect, setShowConnect] = useState(connect) | ||||
|   useEffect(() => { | ||||
|     connectVerify() | ||||
|       .then(() => { | ||||
|         setShowConnect(true) | ||||
|       }) | ||||
|       .catch(() => { | ||||
|         if (connect) { | ||||
|           GLOBAL.connect = false | ||||
|           setConnect(false) | ||||
|         } else { | ||||
|           setShowConnect(false) | ||||
|         } | ||||
|       }) | ||||
|   }, []) | ||||
|  | ||||
|   return ( | ||||
|     <MenuContainer> | ||||
|       <MenuRow | ||||
| @@ -152,6 +171,16 @@ const SettingsApp: React.FC = () => { | ||||
|         switchValue={autoplayGifv} | ||||
|         switchOnValueChange={() => setAutoplayGifv(!autoplayGifv)} | ||||
|       /> | ||||
|       {showConnect ? ( | ||||
|         <MenuRow | ||||
|           title='使用代理' | ||||
|           switchValue={connect || false} | ||||
|           switchOnValueChange={() => { | ||||
|             GLOBAL.connect = !connect | ||||
|             setConnect(!connect) | ||||
|           }} | ||||
|         /> | ||||
|       ) : null} | ||||
|     </MenuContainer> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -93,6 +93,7 @@ const AccountAttachments: React.FC = () => { | ||||
|                 dimension={{ width: width, height: width }} | ||||
|                 style={{ marginLeft: StyleConstants.Spacing.Global.PagePadding }} | ||||
|                 onPress={() => navigation.push('Tab-Shared-Toot', { toot: item })} | ||||
|                 dim | ||||
|               /> | ||||
|             ) | ||||
|           } | ||||
|   | ||||
| @@ -28,6 +28,7 @@ const AccountHeader: React.FC = () => { | ||||
|           ) | ||||
|         } | ||||
|       }} | ||||
|       dim | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -42,6 +42,7 @@ const AccountInformationAvatar: React.FC = () => { | ||||
|           } | ||||
|         } | ||||
|       }} | ||||
|       dim | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -35,6 +35,9 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'> | ||||
|     account, | ||||
|     _local: true, | ||||
|     options: { | ||||
|       placeholderData: (account._remote | ||||
|         ? { ...account, id: undefined } | ||||
|         : account) as Mastodon.Account, | ||||
|       onSuccess: a => { | ||||
|         if (account._remote) { | ||||
|           setQueryKey([ | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import TimelineDefault from '@components/Timeline/Default' | ||||
| import { useQuery } from '@tanstack/react-query' | ||||
| import apiGeneral from '@utils/api/general' | ||||
| import apiInstance from '@utils/api/instance' | ||||
| import { appendRemote } from '@utils/helpers/appendRemote' | ||||
| import { urlMatcher } from '@utils/helpers/urlMatcher' | ||||
| import { TabSharedStackScreenProps } from '@utils/navigation/navigators' | ||||
| import { queryClient } from '@utils/queryHooks' | ||||
| @@ -206,26 +207,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({ | ||||
|           if (localMatch) { | ||||
|             return localMatch | ||||
|           } else { | ||||
|             return { | ||||
|               ...ancestor, | ||||
|               _remote: true, | ||||
|               account: { ...ancestor.account, _remote: true }, | ||||
|               mentions: ancestor.mentions.map(mention => ({ | ||||
|                 ...mention, | ||||
|                 _remote: true | ||||
|               })), | ||||
|               ...(ancestor.reblog && { | ||||
|                 reblog: { | ||||
|                   ...ancestor.reblog, | ||||
|                   _remote: true, | ||||
|                   account: { ...ancestor.reblog.account, _remote: true }, | ||||
|                   mentions: ancestor.reblog.mentions.map(mention => ({ | ||||
|                     ...mention, | ||||
|                     _remote: true | ||||
|                   })) | ||||
|                 } | ||||
|               }) | ||||
|             } | ||||
|             return appendRemote.status(ancestor) | ||||
|           } | ||||
|         }) | ||||
|       } | ||||
| @@ -268,23 +250,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({ | ||||
|                       if (localMatch) { | ||||
|                         return { ...localMatch, _level: remote._level } | ||||
|                       } else { | ||||
|                         return { | ||||
|                           ...remote, | ||||
|                           _remote: true, | ||||
|                           account: { ...remote.account, _remote: true }, | ||||
|                           mentions: remote.mentions.map(mention => ({ ...mention, _remote: true })), | ||||
|                           ...(remote.reblog && { | ||||
|                             reblog: { | ||||
|                               ...remote.reblog, | ||||
|                               _remote: true, | ||||
|                               account: { ...remote.reblog.account, _remote: true }, | ||||
|                               mentions: remote.reblog.mentions.map(mention => ({ | ||||
|                                 ...mention, | ||||
|                                 _remote: true | ||||
|                               })) | ||||
|                             } | ||||
|                           }) | ||||
|                         } | ||||
|                         return appendRemote.status(remote) | ||||
|                       } | ||||
|                     }) | ||||
|                   } | ||||
| @@ -380,8 +346,13 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({ | ||||
|             <TimelineDefault | ||||
|               item={item} | ||||
|               queryKey={item._remote ? queryKey.remote : queryKey.local} | ||||
|               highlighted={toot.id === item.id || item.id === 'cached'} | ||||
|               isConversation={toot.id !== item.id && item.id !== 'cached'} | ||||
|               highlighted={toot.id === item.id} | ||||
|               suppressSpoiler={ | ||||
|                 toot.id !== item.id && | ||||
|                 !!toot.spoiler_text?.length && | ||||
|                 toot.spoiler_text === item.spoiler_text | ||||
|               } | ||||
|               isConversation={toot.id !== item.id} | ||||
|               noBackground | ||||
|             /> | ||||
|             {/* <CustomText | ||||
|   | ||||
| @@ -4,14 +4,12 @@ import Icon from '@components/Icon' | ||||
| import { Loading } from '@components/Loading' | ||||
| import ComponentSeparator from '@components/Separator' | ||||
| import CustomText from '@components/Text' | ||||
| import apiInstance from '@utils/api/instance' | ||||
| import { TabSharedStackScreenProps } from '@utils/navigation/navigators' | ||||
| import { SearchResult } from '@utils/queryHooks/search' | ||||
| import { QueryKeyUsers, useUsersQuery } from '@utils/queryHooks/users' | ||||
| import { flattenPages } from '@utils/queryHooks/utils' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useEffect, useState } from 'react' | ||||
| import React, { useEffect } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { View } from 'react-native' | ||||
| import { FlatList } from 'react-native-gesture-handler' | ||||
| @@ -36,8 +34,6 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> = | ||||
|     ...queryKey[1] | ||||
|   }) | ||||
|  | ||||
|   const [isSearching, setIsSearching] = useState<number | null>(null) | ||||
|  | ||||
|   return ( | ||||
|     <FlatList | ||||
|       windowSize={7} | ||||
| @@ -46,38 +42,10 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> = | ||||
|         minHeight: '100%', | ||||
|         paddingVertical: StyleConstants.Spacing.Global.PagePadding | ||||
|       }} | ||||
|       renderItem={({ item, index }) => ( | ||||
|       renderItem={({ item }) => ( | ||||
|         <ComponentAccount | ||||
|           account={item} | ||||
|           props={{ | ||||
|             disabled: isSearching === index, | ||||
|             onPress: () => { | ||||
|               if (data?.pages[0]?.remoteData) { | ||||
|                 setIsSearching(index) | ||||
|                 apiInstance<SearchResult>({ | ||||
|                   version: 'v2', | ||||
|                   method: 'get', | ||||
|                   url: 'search', | ||||
|                   params: { | ||||
|                     q: `@${item.acct}`, | ||||
|                     type: 'accounts', | ||||
|                     limit: 1, | ||||
|                     resolve: true | ||||
|                   } | ||||
|                 }) | ||||
|                   .then(res => { | ||||
|                     setIsSearching(null) | ||||
|                     if (res.body.accounts[0]) { | ||||
|                       navigation.push('Tab-Shared-Account', { account: res.body.accounts[0] }) | ||||
|                     } | ||||
|                   }) | ||||
|                   .catch(() => setIsSearching(null)) | ||||
|               } else { | ||||
|                 navigation.push('Tab-Shared-Account', { account: item }) | ||||
|               } | ||||
|             } | ||||
|           }} | ||||
|           children={<Loading />} | ||||
|           props={{ onPress: () => navigation.push('Tab-Shared-Account', { account: item }) }} | ||||
|         /> | ||||
|       )} | ||||
|       onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()} | ||||
|   | ||||
| @@ -2,11 +2,11 @@ import GracefullyImage from '@components/GracefullyImage' | ||||
| import haptics from '@components/haptics' | ||||
| import Icon from '@components/Icon' | ||||
| import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' | ||||
| import { RootStackScreenProps, ScreenTabsStackParamList } from '@utils/navigation/navigators' | ||||
| import { ScreenTabsStackParamList } from '@utils/navigation/navigators' | ||||
| import { getGlobalStorage, useAccountStorage, useGlobalStorage } from '@utils/storage/actions' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React from 'react' | ||||
| import { Platform } from 'react-native' | ||||
| import { Platform, View } from 'react-native' | ||||
| import TabLocal from './Local' | ||||
| import TabMe from './Me' | ||||
| import TabNotifications from './Notifications' | ||||
| @@ -14,7 +14,7 @@ import TabPublic from './Public' | ||||
|  | ||||
| const Tab = createBottomTabNavigator<ScreenTabsStackParamList>() | ||||
|  | ||||
| const ScreenTabs = ({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => { | ||||
| const ScreenTabs = () => { | ||||
|   const { colors } = useTheme() | ||||
|  | ||||
|   const [accountActive] = useGlobalStorage.string('account.active') | ||||
| @@ -50,19 +50,19 @@ const ScreenTabs = ({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => { | ||||
|               return <Icon name='bell' size={size} color={color} /> | ||||
|             case 'Tab-Me': | ||||
|               return ( | ||||
|                 <View style={{ flexDirection: 'row', alignItems: 'center' }}> | ||||
|                   <GracefullyImage | ||||
|                     uri={{ original: avatarStatic }} | ||||
|                   dimension={{ | ||||
|                     width: size, | ||||
|                     height: size | ||||
|                   }} | ||||
|                     dimension={{ width: size, height: size }} | ||||
|                     style={{ | ||||
|                       borderRadius: size, | ||||
|                       overflow: 'hidden', | ||||
|                       borderWidth: focused ? 2 : 0, | ||||
|                     borderColor: focused ? colors.secondary : color | ||||
|                       borderColor: focused ? colors.primaryDefault : color | ||||
|                     }} | ||||
|                   /> | ||||
|                   <Icon name='more-vertical' size={size / 1.5} color={colors.secondary} /> | ||||
|                 </View> | ||||
|               ) | ||||
|             default: | ||||
|               return <Icon name='alert-octagon' size={size} color={color} /> | ||||
| @@ -74,13 +74,13 @@ const ScreenTabs = ({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => { | ||||
|       <Tab.Screen name='Tab-Public' component={TabPublic} /> | ||||
|       <Tab.Screen | ||||
|         name='Tab-Compose' | ||||
|         listeners={{ | ||||
|         listeners={({ navigation }) => ({ | ||||
|           tabPress: e => { | ||||
|             e.preventDefault() | ||||
|             haptics('Light') | ||||
|             navigation.navigate('Screen-Compose') | ||||
|           } | ||||
|         }} | ||||
|         })} | ||||
|       > | ||||
|         {() => null} | ||||
|       </Tab.Screen> | ||||
| @@ -88,15 +88,13 @@ const ScreenTabs = ({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => { | ||||
|       <Tab.Screen | ||||
|         name='Tab-Me' | ||||
|         component={TabMe} | ||||
|         listeners={{ | ||||
|         listeners={({ navigation }) => ({ | ||||
|           tabLongPress: () => { | ||||
|             haptics('Light') | ||||
|             //@ts-ignore | ||||
|             navigation.navigate('Tab-Me', { screen: 'Tab-Me-Root' }) | ||||
|             //@ts-ignore | ||||
|             navigation.navigate('Tab-Me', { screen: 'Tab-Me-Switch' }) | ||||
|           } | ||||
|         }} | ||||
|         })} | ||||
|       /> | ||||
|     </Tab.Navigator> | ||||
|   ) | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| import axios from 'axios' | ||||
| import { GLOBAL } from '../../App' | ||||
| import { ctx, handleError, PagedResponse, parseHeaderLinks, userAgent } from './helpers' | ||||
| import { CONNECT_DOMAIN } from './helpers/connect' | ||||
|  | ||||
| export type Params = { | ||||
|   method: 'get' | 'post' | 'put' | 'delete' | ||||
| @@ -35,14 +37,15 @@ const apiGeneral = async <T = unknown>({ | ||||
|   return axios({ | ||||
|     timeout: method === 'post' ? 1000 * 60 : 1000 * 15, | ||||
|     method, | ||||
|     baseURL: `https://${domain}/`, | ||||
|     baseURL: `https://${GLOBAL.connect ? CONNECT_DOMAIN() : domain}`, | ||||
|     url, | ||||
|     params, | ||||
|     headers: { | ||||
|       Accept: 'application/json', | ||||
|       ...userAgent, | ||||
|       ...headers, | ||||
|       ...(body && body instanceof FormData && { 'Content-Type': 'multipart/form-data' }) | ||||
|       ...(body && body instanceof FormData && { 'Content-Type': 'multipart/form-data' }), | ||||
|       ...(GLOBAL.connect && { 'x-tooot-domain': domain }) | ||||
|     }, | ||||
|     data: body | ||||
|   }) | ||||
|   | ||||
							
								
								
									
										145
									
								
								src/utils/api/helpers/connect.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/utils/api/helpers/connect.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,145 @@ | ||||
| import { mapEnvironment } from '@utils/helpers/checkEnvironment' | ||||
| import { setGlobalStorage } from '@utils/storage/actions' | ||||
| import axios from 'axios' | ||||
| import parse from 'url-parse' | ||||
| import { userAgent } from '.' | ||||
| import { GLOBAL } from '../../../App' | ||||
|  | ||||
| const list = [ | ||||
|   'n61owz4leck', | ||||
|   'z9skyp2f0m', | ||||
|   'nc2dqtyxevj', | ||||
|   'tgl97fgudrf', | ||||
|   'eo2sj0ut2s', | ||||
|   'a75auwihvyi', | ||||
|   'vzkpud5y5b', | ||||
|   '3uivf7yyex', | ||||
|   'pxfoa1wbor', | ||||
|   '3cor5jempc', | ||||
|   '9o32znuepr', | ||||
|   '9ayt1l2dzpi', | ||||
|   '60iu4rz8js', | ||||
|   'dzoa1lbxbv', | ||||
|   '82rpiiqw21', | ||||
|   'fblij1c9gyl', | ||||
|   'wk2x048g8gl', | ||||
|   '9x91yrbtmn', | ||||
|   'dgu5p7eif6', | ||||
|   'uftwyhrkgrh', | ||||
|   'vv5hay15vjk', | ||||
|   'ooj9ihtyur', | ||||
|   'o8r7phzd58', | ||||
|   'pujwyg269s', | ||||
|   'l6yq5nr8lv', | ||||
|   'ocyrlfmdnl', | ||||
|   'rdtpeip5e2', | ||||
|   'ykzb5784js', | ||||
|   'm34z7j5us1i', | ||||
|   'tqsfr0orqa', | ||||
|   '8ncrt0mifa', | ||||
|   'ygce2fdmsm', | ||||
|   '22vk7csljz', | ||||
|   '7mmb6hrih1', | ||||
|   'grla5cpgau', | ||||
|   '0vygyvs4k7', | ||||
|   '1texbe32sf', | ||||
|   'ckwvauiiol', | ||||
|   'qkxryrbpxx', | ||||
|   'ptb19c0ks9g', | ||||
|   '3bpe76o6stg', | ||||
|   'd507ejce9g', | ||||
|   'jpul5v2mqej', | ||||
|   '6m5uxemc79', | ||||
|   'wxbtoo9t3p', | ||||
|   '8qco3d0idh', | ||||
|   'u00c2xiabvf', | ||||
|   'hutkqwrcy8', | ||||
|   't6vrkzhpzo', | ||||
|   'wy6e529mnb', | ||||
|   'kzzrlfa59pg', | ||||
|   'mmo4sv4a7s', | ||||
|   'u0dishl20k', | ||||
|   '8qyx25bq3u', | ||||
|   'd3mucdzlu1', | ||||
|   'y123m81vsjl', | ||||
|   '51opvzdo6k', | ||||
|   'r4z333th9u', | ||||
|   'q77hl0ggfr', | ||||
|   'bsk1f2wi52g', | ||||
|   'eubnxpv0pz', | ||||
|   'h11pk7qm8i', | ||||
|   'brhxw45vd5', | ||||
|   'vtnvlsrn1z', | ||||
|   '0q5w0hhzb5', | ||||
|   'vq2rz02ayf', | ||||
|   'hml3igfwkq', | ||||
|   '39qs7vhenl', | ||||
|   '5vcv775rug', | ||||
|   'kjom5gr7i3', | ||||
|   't2kmaoeb5x', | ||||
|   'ni6ow1z11b', | ||||
|   'yvgtoc3d88', | ||||
|   'iax04eatnz', | ||||
|   'esxyu9zujg', | ||||
|   '73xa28n278', | ||||
|   '5x63a8l24k', | ||||
|   'dy1trb0b3sj', | ||||
|   'd4c31j23m8', | ||||
|   'ho76046l0j', | ||||
|   'sw8lj5u2ef', | ||||
|   'z5cn21mew5', | ||||
|   'wxj73nmqwa', | ||||
|   'gdj00dlx98', | ||||
|   '0v76xag64i', | ||||
|   'j35104qduhj', | ||||
|   'l63r7h0ss6', | ||||
|   'e5xdv7t1q0h', | ||||
|   '4icoh8t4c8', | ||||
|   'nbk36jt4sq', | ||||
|   'zi0n0cv4tk', | ||||
|   'o7qkfp3rxu', | ||||
|   'xd2wefzd27', | ||||
|   'rg7e6tsacx', | ||||
|   '9lrq3s4vfm', | ||||
|   'srs9p21lxoh', | ||||
|   'n8xymau42t', | ||||
|   'q5cik283fg', | ||||
|   '68ye9feqs5', | ||||
|   'xjc5anubnv' | ||||
| ] | ||||
|  | ||||
| export const CONNECT_DOMAIN = () => | ||||
|   mapEnvironment({ | ||||
|     release: `${list[Math.floor(Math.random() * (100 - 0) + 0)]}.tooot.app`, | ||||
|     candidate: 'connect-candidate.tooot.app', | ||||
|     development: 'connect-development.tooot.app' | ||||
|   }) | ||||
|  | ||||
| export const connectImage = ({ | ||||
|   uri | ||||
| }: { | ||||
|   uri?: string | ||||
| }): { uri?: string; headers?: { 'x-tooot-domain': string } } => { | ||||
|   if (GLOBAL.connect) { | ||||
|     if (uri) { | ||||
|       const host = parse(uri).host | ||||
|       return { uri: uri.replace(host, CONNECT_DOMAIN()), headers: { 'x-tooot-domain': host } } | ||||
|     } else { | ||||
|       return { uri } | ||||
|     } | ||||
|   } else { | ||||
|     return { uri } | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const connectVerify = () => | ||||
|   axios({ | ||||
|     method: 'get', | ||||
|     baseURL: `https://${CONNECT_DOMAIN()}`, | ||||
|     url: 'verify', | ||||
|     headers: { ...userAgent } | ||||
|   }).catch(err => { | ||||
|     GLOBAL.connect = false | ||||
|     setGlobalStorage('app.connect', false) | ||||
|     return Promise.reject(err) | ||||
|   }) | ||||
| @@ -1,8 +1,10 @@ | ||||
| import * as Sentry from '@sentry/react-native' | ||||
| import { setGlobalStorage } from '@utils/storage/actions' | ||||
| import chalk from 'chalk' | ||||
| import Constants from 'expo-constants' | ||||
| import { Platform } from 'react-native' | ||||
| import parse from 'url-parse' | ||||
| import { GLOBAL } from '../../../App' | ||||
|  | ||||
| const userAgent = { | ||||
|   'User-Agent': `tooot/${Constants.expoConfig?.version} ${Platform.OS}/${Platform.Version}` | ||||
| @@ -18,6 +20,12 @@ const handleError = | ||||
|     } | void | ||||
|   ) => | ||||
|   (error: any) => { | ||||
|     if (GLOBAL.connect) { | ||||
|       if (error?.response?.status == 403 && error?.response?.data == 'connect_blocked') { | ||||
|         GLOBAL.connect = false | ||||
|         setGlobalStorage('app.connect', false) | ||||
|       } | ||||
|     } | ||||
|     const shouldReportToSentry = config && (config.captureRequest || config.captureResponse) | ||||
|     shouldReportToSentry && Sentry.setContext('Error object', error) | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| import { getAccountDetails } from '@utils/storage/actions' | ||||
| import { StorageGlobal } from '@utils/storage/global' | ||||
| import axios, { AxiosRequestConfig } from 'axios' | ||||
| import { GLOBAL } from '../../App' | ||||
| import { ctx, handleError, PagedResponse, parseHeaderLinks, userAgent } from './helpers' | ||||
| import { CONNECT_DOMAIN } from './helpers/connect' | ||||
|  | ||||
| export type Params = { | ||||
|   account?: StorageGlobal['account.active'] | ||||
| @@ -43,11 +45,13 @@ const apiInstance = async <T = unknown>({ | ||||
|     method + ctx.blue(' -> ') + `/${url}` + (params ? ctx.blue(' -> ') : ''), | ||||
|     params ? params : '' | ||||
|   ) | ||||
|   console.log('body', body) | ||||
|  | ||||
|   return axios({ | ||||
|     timeout: method === 'post' ? 1000 * 60 : 1000 * 15, | ||||
|     method, | ||||
|     baseURL: `https://${accountDetails['auth.domain']}/api/${version}/`, | ||||
|     baseURL: `https://${ | ||||
|       GLOBAL.connect ? CONNECT_DOMAIN() : accountDetails['auth.domain'] | ||||
|     }/api/${version}`, | ||||
|     url, | ||||
|     params, | ||||
|     headers: { | ||||
| @@ -55,7 +59,8 @@ const apiInstance = async <T = unknown>({ | ||||
|       ...userAgent, | ||||
|       ...headers, | ||||
|       Authorization: `Bearer ${accountDetails['auth.token']}`, | ||||
|       ...(body && body instanceof FormData && { 'Content-Type': 'multipart/form-data' }) | ||||
|       ...(body && body instanceof FormData && { 'Content-Type': 'multipart/form-data' }), | ||||
|       ...(GLOBAL.connect && { 'x-tooot-domain': accountDetails['auth.domain'] }) | ||||
|     }, | ||||
|     data: body, | ||||
|     ...extras | ||||
|   | ||||
							
								
								
									
										23
									
								
								src/utils/helpers/appendRemote.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/utils/helpers/appendRemote.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| // Central place appending _remote internal prop | ||||
|  | ||||
| export const appendRemote = { | ||||
|   status: (status: Mastodon.Status) => ({ | ||||
|     ...status, | ||||
|     ...(status.reblog && { | ||||
|       reblog: { | ||||
|         ...status.reblog, | ||||
|         account: appendRemote.account(status.reblog.account), | ||||
|         mentions: appendRemote.mentions(status.reblog.mentions) | ||||
|       } | ||||
|     }), | ||||
|     account: appendRemote.account(status.account), | ||||
|     mentions: appendRemote.mentions(status.mentions), | ||||
|     _remote: true | ||||
|   }), | ||||
|   account: (account: Mastodon.Account) => ({ | ||||
|     ...account, | ||||
|     _remote: true | ||||
|   }), | ||||
|   mentions: (mentions: Mastodon.Mention[]) => | ||||
|     mentions?.map(mention => ({ ...mention, _remote: true })) | ||||
| } | ||||
| @@ -1,6 +1,9 @@ | ||||
| import { getAccountStorage } from '@utils/storage/actions' | ||||
| import parse from 'url-parse' | ||||
|  | ||||
| // Would mess with the /@username format | ||||
| const BLACK_LIST = ['matters.news', 'medium.com'] | ||||
|  | ||||
| export const urlMatcher = ( | ||||
|   url: string | ||||
| ): | ||||
| @@ -14,6 +17,10 @@ export const urlMatcher = ( | ||||
|   if (!parsed.hostname.length || !parsed.pathname.length) return undefined | ||||
|  | ||||
|   const domain = parsed.hostname | ||||
|   if (BLACK_LIST.includes(domain)) { | ||||
|     return | ||||
|   } | ||||
|  | ||||
|   const _remote = parsed.hostname !== getAccountStorage.string('auth.domain') | ||||
|  | ||||
|   let statusId: string | undefined | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query' | ||||
| import apiGeneral from '@utils/api/general' | ||||
| import apiInstance from '@utils/api/instance' | ||||
| import { appendRemote } from '@utils/helpers/appendRemote' | ||||
| import { urlMatcher } from '@utils/helpers/urlMatcher' | ||||
| import { AxiosError } from 'axios' | ||||
| import { searchLocalAccount } from './search' | ||||
| @@ -34,14 +35,14 @@ const accountQueryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyA | ||||
|             method: 'get', | ||||
|             domain: domain, | ||||
|             url: `api/v1/accounts/${id}` | ||||
|           }).then(res => ({ ...res.body, _remote: true })) | ||||
|           }).then(res => appendRemote.account(res.body)) | ||||
|         } else if (acct) { | ||||
|           matchedAccount = await apiGeneral<Mastodon.Account>({ | ||||
|             method: 'get', | ||||
|             domain: domain, | ||||
|             url: 'api/v1/accounts/lookup', | ||||
|             params: { acct } | ||||
|           }).then(res => ({ ...res.body, _remote: true })) | ||||
|           }).then(res => appendRemote.account(res.body)) | ||||
|         } | ||||
|       } catch {} | ||||
|     } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query' | ||||
| import apiGeneral from '@utils/api/general' | ||||
| import apiInstance from '@utils/api/instance' | ||||
| import { appendRemote } from '@utils/helpers/appendRemote' | ||||
| import { urlMatcher } from '@utils/helpers/urlMatcher' | ||||
| import { AxiosError } from 'axios' | ||||
| import { searchLocalStatus } from './search' | ||||
| @@ -26,7 +27,7 @@ const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyStatus>) | ||||
|         method: 'get', | ||||
|         domain, | ||||
|         url: `api/v1/statuses/${id}` | ||||
|       }).then(res => ({ ...res.body, _remote: true })) | ||||
|       }).then(res => appendRemote.status(res.body)) | ||||
|     } catch {} | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import { | ||||
| import apiGeneral from '@utils/api/general' | ||||
| import { PagedResponse } from '@utils/api/helpers' | ||||
| import apiInstance from '@utils/api/instance' | ||||
| import { appendRemote } from '@utils/helpers/appendRemote' | ||||
| import { urlMatcher } from '@utils/helpers/urlMatcher' | ||||
| import { TabSharedStackParamList } from '@utils/navigation/navigators' | ||||
| import { AxiosError } from 'axios' | ||||
| @@ -54,7 +55,11 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query | ||||
|               url: `api/v1/accounts/${resLookup.body.id}/${page.type}`, | ||||
|               params | ||||
|             }) | ||||
|             return { ...res, remoteData: true } | ||||
|             return { | ||||
|               ...res, | ||||
|               body: res.body.map(account => appendRemote.account(account)), | ||||
|               remoteData: true | ||||
|             } | ||||
|           } else { | ||||
|             throw new Error() | ||||
|           } | ||||
|   | ||||
| @@ -323,27 +323,30 @@ export const removeAccount = async (account: string, warning: boolean = true) => | ||||
| } | ||||
|  | ||||
| export type ReadableAccountType = { | ||||
|   avatar_static: string | ||||
|   acct: string | ||||
|   key: string | ||||
|   active: boolean | ||||
| } | ||||
| export const getReadableAccounts = (withoutActive: boolean = false): ReadableAccountType[] => { | ||||
|   const accountActive = !withoutActive && getGlobalStorage.string('account.active') | ||||
|   const accounts = getGlobalStorage.object('accounts')?.sort((a, b) => a.localeCompare(b)) | ||||
|   !withoutActive && | ||||
|     accounts?.splice( | ||||
|       accounts.findIndex(a => a === accountActive), | ||||
|       1 | ||||
|     ) | ||||
|   !withoutActive && accounts?.unshift(accountActive || '') | ||||
| export const getReadableAccounts = (): ReadableAccountType[] => { | ||||
|   const accountActive = getGlobalStorage.string('account.active') | ||||
|   const accounts = getGlobalStorage.object('accounts') | ||||
|  | ||||
|   return ( | ||||
|     accounts?.map(account => { | ||||
|       const details = getAccountDetails( | ||||
|         ['auth.account.acct', 'auth.account.domain', 'auth.domain', 'auth.account.id'], | ||||
|         [ | ||||
|           'auth.account.avatar_static', | ||||
|           'auth.account.acct', | ||||
|           'auth.account.domain', | ||||
|           'auth.domain', | ||||
|           'auth.account.id' | ||||
|         ], | ||||
|         account | ||||
|       ) | ||||
|       if (details) { | ||||
|         return { | ||||
|           avatar_static: details['auth.account.avatar_static'], | ||||
|           acct: `@${details['auth.account.acct']}@${details['auth.account.domain']}`, | ||||
|           key: generateAccountKey({ | ||||
|             domain: details['auth.domain'], | ||||
| @@ -352,7 +355,7 @@ export const getReadableAccounts = (withoutActive: boolean = false): ReadableAcc | ||||
|           active: account === accountActive | ||||
|         } | ||||
|       } else { | ||||
|         return { acct: '', key: '', active: false } | ||||
|         return { avatar_static: '', acct: '', key: '', active: false } | ||||
|       } | ||||
|     }) || [] | ||||
|   ).filter(a => a.acct.length) | ||||
|   | ||||
| @@ -17,6 +17,7 @@ export type GlobalV0 = { | ||||
|   'version.account': number | ||||
|   // boolean | ||||
|   'app.auto_play_gifv'?: boolean | ||||
|   'app.connect'?: boolean | ||||
|  | ||||
|   //// account | ||||
|   // string | ||||
|   | ||||
		Reference in New Issue
	
	Block a user