mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	| @@ -197,7 +197,7 @@ private_lane :build_android do | ||||
| end | ||||
|  | ||||
| lane :build do | ||||
|   releaseExists = get_github_release(url: GITHUB_REPO, version: GITHUB_RELEASE, api_token: ENV['GH_PAT_GET_RELEASE']) | ||||
|   releaseExists = get_github_release(url: GITHUB_REPO, version: "v#{VERSION}", api_token: ENV['GH_PAT_GET_RELEASE']) | ||||
|   if releaseExists | ||||
|     puts("Release #{GITHUB_RELEASE} exists. Continue with building React Native only.") | ||||
|   else | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
|     "native": "210511", | ||||
|     "major": 2, | ||||
|     "minor": 0, | ||||
|     "patch": 1, | ||||
|     "patch": 2, | ||||
|     "expo": "41.0.0" | ||||
|   }, | ||||
|   "description": "tooot app for Mastodon", | ||||
|   | ||||
							
								
								
									
										9
									
								
								src/@types/mastodon.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								src/@types/mastodon.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -261,6 +261,15 @@ declare namespace Mastodon { | ||||
|     verified_at: string | null | ||||
|   } | ||||
|  | ||||
|   type Filter = { | ||||
|     id: string | ||||
|     phrase: string | ||||
|     context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[] | ||||
|     expires_at?: string | ||||
|     irreversible: boolean | ||||
|     whole_word: boolean | ||||
|   } | ||||
|  | ||||
|   type List = { | ||||
|     id: string | ||||
|     title: string | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import pushUseReceive from '@utils/push/useReceive' | ||||
| import pushUseRespond from '@utils/push/useRespond' | ||||
| import { updatePreviousTab } from '@utils/slices/contextsSlice' | ||||
| import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences' | ||||
| import { updateFilters } from '@utils/slices/instances/updateFilters' | ||||
| import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { themes } from '@utils/styles/themes' | ||||
| @@ -106,6 +107,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => { | ||||
|   // Lazily update users's preferences, for e.g. composing default visibility | ||||
|   useEffect(() => { | ||||
|     if (instanceActive !== -1) { | ||||
|       dispatch(updateFilters()) | ||||
|       dispatch(updateAccountPreferences()) | ||||
|     } | ||||
|   }, [instanceActive]) | ||||
|   | ||||
| @@ -69,7 +69,10 @@ const apiGeneral = async <T = unknown>({ | ||||
|           error.response.status, | ||||
|           error.response.data.error | ||||
|         ) | ||||
|         return Promise.reject(error.response.data.error) | ||||
|         return Promise.reject({ | ||||
|           status: error.response.status, | ||||
|           message: error.response.data.error | ||||
|         }) | ||||
|       } else if (error.request) { | ||||
|         // The request was made but no response was received | ||||
|         // `error.request` is an instance of XMLHttpRequest in the browser and an instance of | ||||
|   | ||||
| @@ -98,7 +98,10 @@ const apiInstance = async <T = unknown>({ | ||||
|           error.response.status, | ||||
|           error.response.data.error | ||||
|         ) | ||||
|         return Promise.reject(error.response.data.error) | ||||
|         return Promise.reject({ | ||||
|           status: error.response.status, | ||||
|           message: error.response.data.error | ||||
|         }) | ||||
|       } else if (error.request) { | ||||
|         // The request was made but no response was received | ||||
|         // `error.request` is an instance of XMLHttpRequest in the browser and an instance of | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useMemo, useRef, useState } from 'react' | ||||
| import React, { useCallback, useMemo, useState } from 'react' | ||||
| import { | ||||
|   AccessibilityProps, | ||||
|   Image, | ||||
| @@ -52,32 +52,30 @@ const GracefullyImage = React.memo( | ||||
|     setImageDimensions | ||||
|   }: Props) => { | ||||
|     const { theme } = useTheme() | ||||
|     const originalFailed = useRef(false) | ||||
|     const [originalFailed, setOriginalFailed] = useState(false) | ||||
|     const [imageLoaded, setImageLoaded] = useState(false) | ||||
|  | ||||
|     const source = useMemo(() => { | ||||
|       if (originalFailed.current) { | ||||
|       if (originalFailed) { | ||||
|         return { uri: uri.remote || undefined } | ||||
|       } else { | ||||
|         return { uri: uri.original } | ||||
|       } | ||||
|     }, [originalFailed.current]) | ||||
|     const onLoad = useCallback( | ||||
|       ({ nativeEvent }) => { | ||||
|         setImageLoaded(true) | ||||
|         setImageDimensions && | ||||
|           setImageDimensions({ | ||||
|             width: nativeEvent.source.width, | ||||
|             height: nativeEvent.source.height | ||||
|           }) | ||||
|       }, | ||||
|       [source.uri] | ||||
|     ) | ||||
|     const onError = useCallback(() => { | ||||
|       if (!originalFailed.current) { | ||||
|         originalFailed.current = true | ||||
|     }, [originalFailed]) | ||||
|  | ||||
|     const onLoad = useCallback(() => { | ||||
|       setImageLoaded(true) | ||||
|       if (setImageDimensions && source.uri) { | ||||
|         Image.getSize(source.uri, (width, height) => | ||||
|           setImageDimensions({ width, height }) | ||||
|         ) | ||||
|       } | ||||
|     }, [originalFailed.current]) | ||||
|     }, [source.uri]) | ||||
|     const onError = useCallback(() => { | ||||
|       if (!originalFailed) { | ||||
|         setOriginalFailed(true) | ||||
|       } | ||||
|     }, [originalFailed]) | ||||
|  | ||||
|     const previewView = useMemo( | ||||
|       () => | ||||
|   | ||||
| @@ -76,11 +76,10 @@ const ParseEmojis = React.memo( | ||||
|                     : emojis[emojiIndex].url | ||||
|                   if (validUrl.isHttpsUri(uri)) { | ||||
|                     return ( | ||||
|                       <FastImage | ||||
|                         key={emojiShortcode + i} | ||||
|                         source={{ uri }} | ||||
|                         style={styles.image} | ||||
|                       /> | ||||
|                       <Text key={emojiShortcode + i}> | ||||
|                         {i === 0 ? ' ' : undefined} | ||||
|                         <FastImage source={{ uri }} style={styles.image} /> | ||||
|                       </Text> | ||||
|                     ) | ||||
|                   } else { | ||||
|                     return null | ||||
|   | ||||
| @@ -10,14 +10,16 @@ import TimelinePoll from '@components/Timeline/Shared/Poll' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { StackNavigationProp } from '@react-navigation/stack' | ||||
| import { QueryKeyTimeline } from '@utils/queryHooks/timeline' | ||||
| import { getInstanceAccount } from '@utils/slices/instancesSlice' | ||||
| import { getInstance, getInstanceAccount } from '@utils/slices/instancesSlice' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import htmlparser2 from 'htmlparser2-without-node-native' | ||||
| import { uniqBy } from 'lodash' | ||||
| import React, { useCallback } from 'react' | ||||
| import { Pressable, StyleSheet, View } from 'react-native' | ||||
| import { useSelector } from 'react-redux' | ||||
| import TimelineActionsUsers from './Shared/ActionsUsers' | ||||
| import TimelineFiltered, { shouldFilter } from './Shared/Filtered' | ||||
| import TimelineFullConversation from './Shared/FullConversation' | ||||
| import TimelineTranslate from './Shared/Translate' | ||||
|  | ||||
| @@ -49,6 +51,16 @@ const TimelineDefault: React.FC<Props> = ({ | ||||
|  | ||||
|   let actualStatus = item.reblog ? item.reblog : item | ||||
|  | ||||
|   const ownAccount = actualStatus.account.id === instanceAccount?.id | ||||
|  | ||||
|   if ( | ||||
|     !highlighted && | ||||
|     queryKey && | ||||
|     shouldFilter({ status: actualStatus, queryKey }) | ||||
|   ) { | ||||
|     return <TimelineFiltered /> | ||||
|   } | ||||
|  | ||||
|   const onPress = useCallback(() => { | ||||
|     analytics('timeline_default_press', { | ||||
|       page: queryKey ? queryKey[1].page : origin | ||||
| @@ -118,7 +130,7 @@ const TimelineDefault: React.FC<Props> = ({ | ||||
|             statusId={actualStatus.id} | ||||
|             poll={actualStatus.poll} | ||||
|             reblog={item.reblog ? true : false} | ||||
|             sameAccount={actualStatus.account.id === instanceAccount?.id} | ||||
|             sameAccount={ownAccount} | ||||
|           /> | ||||
|         ) : null} | ||||
|         {!disableDetails && | ||||
|   | ||||
| @@ -17,6 +17,7 @@ import { uniqBy } from 'lodash' | ||||
| import React, { useCallback } from 'react' | ||||
| import { Pressable, StyleSheet, View } from 'react-native' | ||||
| import { useSelector } from 'react-redux' | ||||
| import TimelineFiltered, { shouldFilter } from './Shared/Filtered' | ||||
| import TimelineFullConversation from './Shared/FullConversation' | ||||
|  | ||||
| export interface Props { | ||||
| @@ -30,6 +31,13 @@ const TimelineNotifications: React.FC<Props> = ({ | ||||
|   queryKey, | ||||
|   highlighted = false | ||||
| }) => { | ||||
|   if ( | ||||
|     notification.status && | ||||
|     shouldFilter({ status: notification.status, queryKey }) | ||||
|   ) { | ||||
|     return <TimelineFiltered /> | ||||
|   } | ||||
|  | ||||
|   const { theme } = useTheme() | ||||
|   const instanceAccount = useSelector( | ||||
|     getInstanceAccount, | ||||
| @@ -38,6 +46,7 @@ const TimelineNotifications: React.FC<Props> = ({ | ||||
|   const navigation = useNavigation< | ||||
|     StackNavigationProp<Nav.TabLocalStackParamList> | ||||
|   >() | ||||
|  | ||||
|   const actualAccount = notification.status | ||||
|     ? notification.status.account | ||||
|     : notification.account | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import AttachmentVideo from '@components/Timeline/Shared/Attachment/Video' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import layoutAnimation from '@utils/styles/layoutAnimation' | ||||
| import React, { useCallback, useMemo, useState } from 'react' | ||||
| import React, { useCallback, useMemo, useRef, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Pressable, StyleSheet, View } from 'react-native' | ||||
|  | ||||
| @@ -33,11 +33,13 @@ const TimelineAttachment = React.memo( | ||||
|       haptics('Light') | ||||
|     }, []) | ||||
|  | ||||
|     let imageUrls: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'] = [] | ||||
|     const imageUrls = useRef< | ||||
|       Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'] | ||||
|     >([]) | ||||
|     const navigation = useNavigation() | ||||
|     const navigateToImagesViewer = (id: string) => | ||||
|       navigation.navigate('Screen-ImagesViewer', { | ||||
|         imageUrls, | ||||
|         imageUrls: imageUrls.current, | ||||
|         id | ||||
|       }) | ||||
|     const attachments = useMemo( | ||||
| @@ -45,7 +47,7 @@ const TimelineAttachment = React.memo( | ||||
|         status.media_attachments.map((attachment, index) => { | ||||
|           switch (attachment.type) { | ||||
|             case 'image': | ||||
|               imageUrls.push({ | ||||
|               imageUrls.current.push({ | ||||
|                 id: attachment.id, | ||||
|                 preview_url: attachment.preview_url, | ||||
|                 url: attachment.url, | ||||
| @@ -106,7 +108,7 @@ const TimelineAttachment = React.memo( | ||||
|                 attachment.remote_url?.endsWith('.png') || | ||||
|                 attachment.remote_url?.endsWith('.gif') | ||||
|               ) { | ||||
|                 imageUrls.push({ | ||||
|                 imageUrls.current.push({ | ||||
|                   id: attachment.id, | ||||
|                   preview_url: attachment.preview_url, | ||||
|                   url: attachment.url, | ||||
|   | ||||
							
								
								
									
										105
									
								
								src/components/Timeline/Shared/Filtered.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/components/Timeline/Shared/Filtered.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| import { store } from '@root/store' | ||||
| import { QueryKeyTimeline } from '@utils/queryHooks/timeline' | ||||
| import { getInstance, getInstanceAccount } from '@utils/slices/instancesSlice' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import htmlparser2 from 'htmlparser2-without-node-native' | ||||
| import React from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Text, View } from 'react-native' | ||||
|  | ||||
| const TimelineFiltered = React.memo( | ||||
|   () => { | ||||
|     const { theme } = useTheme() | ||||
|     const { t } = useTranslation('componentTimeline') | ||||
|  | ||||
|     return ( | ||||
|       <View style={{ backgroundColor: theme.backgroundDefault }}> | ||||
|         <Text | ||||
|           style={{ | ||||
|             ...StyleConstants.FontStyle.S, | ||||
|             color: theme.secondary, | ||||
|             textAlign: 'center', | ||||
|             paddingVertical: StyleConstants.Spacing.S, | ||||
|             paddingLeft: StyleConstants.Avatar.M + StyleConstants.Spacing.S | ||||
|           }} | ||||
|         > | ||||
|           {t('shared.filtered')} | ||||
|         </Text> | ||||
|       </View> | ||||
|     ) | ||||
|   }, | ||||
|   () => true | ||||
| ) | ||||
|  | ||||
| export const shouldFilter = ({ | ||||
|   status, | ||||
|   queryKey | ||||
| }: { | ||||
|   status: Mastodon.Status | ||||
|   queryKey: QueryKeyTimeline | ||||
| }) => { | ||||
|   const instance = getInstance(store.getState()) | ||||
|   const ownAccount = | ||||
|     getInstanceAccount(store.getState())?.id === status.account.id | ||||
|  | ||||
|   let shouldFilter = false | ||||
|   if (queryKey && !ownAccount) { | ||||
|     const parser = new htmlparser2.Parser({ | ||||
|       ontext (text: string) { | ||||
|         const checkFilter = (filter: Mastodon.Filter) => { | ||||
|           switch (filter.whole_word) { | ||||
|             case true: | ||||
|               if (new RegExp('\\b' + filter.phrase + '\\b').test(text)) { | ||||
|                 shouldFilter = true | ||||
|               } | ||||
|               break | ||||
|             case false: | ||||
|               if (new RegExp(filter.phrase).test(text)) { | ||||
|                 shouldFilter = true | ||||
|               } | ||||
|               break | ||||
|           } | ||||
|         } | ||||
|         instance?.filters.forEach(filter => { | ||||
|           if (filter.expires_at) { | ||||
|             if (new Date().getTime() > new Date(filter.expires_at).getTime()) { | ||||
|               return | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           switch (queryKey[1].page) { | ||||
|             case 'Following': | ||||
|             case 'Local': | ||||
|             case 'List': | ||||
|             case 'Account_Default': | ||||
|               if (filter.context.includes('home')) { | ||||
|                 checkFilter(filter) | ||||
|               } | ||||
|               break | ||||
|             case 'Notifications': | ||||
|               if (filter.context.includes('notifications')) { | ||||
|                 checkFilter(filter) | ||||
|               } | ||||
|               break | ||||
|             case 'LocalPublic': | ||||
|               if (filter.context.includes('public')) { | ||||
|                 checkFilter(filter) | ||||
|               } | ||||
|               break | ||||
|             case 'Toot': | ||||
|               if (filter.context.includes('thread')) { | ||||
|                 checkFilter(filter) | ||||
|               } | ||||
|           } | ||||
|         }) | ||||
|       } | ||||
|     }) | ||||
|     parser.write(status.content) | ||||
|     parser.end() | ||||
|   } | ||||
|  | ||||
|   return shouldFilter | ||||
| } | ||||
|  | ||||
| export default TimelineFiltered | ||||
| @@ -31,7 +31,7 @@ const TimelineTranslate = React.memo( | ||||
|  | ||||
|     const settingsLanguage = useSelector(getSettingsLanguage) | ||||
|  | ||||
|     if (settingsLanguage.includes(tootLanguage)) { | ||||
|     if (settingsLanguage?.includes(tootLanguage)) { | ||||
|       return null | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -73,6 +73,7 @@ | ||||
|     "content": { | ||||
|       "expandHint": "hidden content" | ||||
|     }, | ||||
|     "filtered": "Filtered", | ||||
|     "fullConversation": "Read conversations", | ||||
|     "translate": { | ||||
|       "default": "Translate", | ||||
|   | ||||
| @@ -73,6 +73,7 @@ | ||||
|     "content": { | ||||
|       "expandHint": "隐藏内容" | ||||
|     }, | ||||
|     "filtered": "已过滤", | ||||
|     "fullConversation": "阅读全部对话", | ||||
|     "translate": { | ||||
|       "default": "翻译", | ||||
|   | ||||
| @@ -21,8 +21,6 @@ const ComposeTextInput: React.FC = () => { | ||||
|           borderBottomColor: theme.border | ||||
|         } | ||||
|       ]} | ||||
|       autoCapitalize='none' | ||||
|       autoCorrect={false} | ||||
|       autoFocus | ||||
|       enablesReturnKeyAutomatically | ||||
|       multiline | ||||
|   | ||||
| @@ -52,8 +52,8 @@ const ImageItem = ({ | ||||
|   const scrollViewRef = useRef<ScrollView>(null) | ||||
|   const [scaled, setScaled] = useState(false) | ||||
|   const [imageDimensions, setImageDimensions] = useState({ | ||||
|     width: imageSrc.width || 0, | ||||
|     height: imageSrc.height || 0 | ||||
|     width: imageSrc.width || 1, | ||||
|     height: imageSrc.height || 1 | ||||
|   }) | ||||
|   const handleDoubleTap = useDoubleTapToZoom(scrollViewRef, scaled, SCREEN) | ||||
|  | ||||
|   | ||||
| @@ -58,7 +58,7 @@ const saveIos = async ({ messageRef, mode, image }: CommonProps) => { | ||||
| } | ||||
|  | ||||
| const saveAndroid = async ({ messageRef, mode, image }: CommonProps) => { | ||||
|   const fileUri: string = `${FileSystem.documentDirectory}test.jpg` | ||||
|   const fileUri: string = `${FileSystem.documentDirectory}${image.id}.jpg` | ||||
|   const downloadedFile: FileSystem.FileSystemDownloadResult = await FileSystem.downloadAsync( | ||||
|     image.url, | ||||
|     fileUri | ||||
| @@ -80,7 +80,7 @@ const saveAndroid = async ({ messageRef, mode, image }: CommonProps) => { | ||||
|         ref: messageRef, | ||||
|         mode, | ||||
|         type: 'success', | ||||
|         message: 'test' | ||||
|         message: i18next.t('screenImageViewer:content.save.succeed') | ||||
|       }) | ||||
|     }) | ||||
|     .catch(() => { | ||||
|   | ||||
| @@ -43,11 +43,7 @@ const netInfo = async (): Promise<{ | ||||
|         }) | ||||
|         .catch(error => { | ||||
|           log('error', 'netInfo', 'local credential check failed') | ||||
|           if ( | ||||
|             error.status && | ||||
|             typeof error.status === 'number' && | ||||
|             error.status === 401 | ||||
|           ) { | ||||
|           if (error.status && error.status == 401) { | ||||
|             store.dispatch(removeInstance(instance)) | ||||
|           } | ||||
|           return Promise.resolve({ | ||||
|   | ||||
							
								
								
									
										12
									
								
								src/utils/slices/instances/updateFilters.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/utils/slices/instances/updateFilters.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import apiInstance from '@api/instance' | ||||
| import { createAsyncThunk } from '@reduxjs/toolkit' | ||||
|  | ||||
| export const updateFilters = createAsyncThunk( | ||||
|   'instances/updateFilters', | ||||
|   async (): Promise<Mastodon.Filter[]> => { | ||||
|     return apiInstance<Mastodon.Filter[]>({ | ||||
|       method: 'get', | ||||
|       url: `filters` | ||||
|     }).then(res => res.body) | ||||
|   } | ||||
| ) | ||||
| @@ -6,6 +6,7 @@ import { findIndex } from 'lodash' | ||||
| import addInstance from './instances/add' | ||||
| import removeInstance from './instances/remove' | ||||
| import { updateAccountPreferences } from './instances/updateAccountPreferences' | ||||
| import { updateFilters } from './instances/updateFilters' | ||||
| import { updateInstancePush } from './instances/updatePush' | ||||
| import { updateInstancePushAlert } from './instances/updatePushAlert' | ||||
| import { updateInstancePushDecode } from './instances/updatePushDecode' | ||||
| @@ -29,6 +30,7 @@ export type Instance = { | ||||
|     avatarStatic: Mastodon.Account['avatar_static'] | ||||
|     preferences: Mastodon.Preferences | ||||
|   } | ||||
|   filters: Mastodon.Filter[] | ||||
|   notifications_filter: { | ||||
|     follow: boolean | ||||
|     favourite: boolean | ||||
| @@ -236,6 +238,15 @@ const instancesSlice = createSlice({ | ||||
|         console.error(action.error) | ||||
|       }) | ||||
|  | ||||
|       // Update Instance Account Filters | ||||
|       .addCase(updateFilters.fulfilled, (state, action) => { | ||||
|         const activeIndex = findInstanceActive(state.instances) | ||||
|         state.instances[activeIndex].filters = action.payload | ||||
|       }) | ||||
|       .addCase(updateFilters.rejected, (_, action) => { | ||||
|         console.error(action.error) | ||||
|       }) | ||||
|  | ||||
|       // Update Instance Account Preferences | ||||
|       .addCase(updateAccountPreferences.fulfilled, (state, action) => { | ||||
|         const activeIndex = findInstanceActive(state.instances) | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import * as Analytics from 'expo-firebase-analytics' | ||||
| import * as Localization from 'expo-localization' | ||||
| import { pickBy } from 'lodash' | ||||
|  | ||||
| enum availableLanguages { | ||||
| enum AvailableLanguages { | ||||
|   'zh-Hans', | ||||
|   'en' | ||||
| } | ||||
| @@ -19,7 +19,7 @@ export const changeAnalytics = createAsyncThunk( | ||||
|  | ||||
| export type SettingsState = { | ||||
|   fontsize: -1 | 0 | 1 | 2 | 3 | ||||
|   language: keyof availableLanguages | ||||
|   language: string | ||||
|   theme: 'light' | 'dark' | 'auto' | ||||
|   browser: 'internal' | 'external' | ||||
|   analytics: boolean | ||||
| @@ -31,10 +31,10 @@ export const settingsInitialState = { | ||||
|     enabled: false | ||||
|   }, | ||||
|   language: Object.keys( | ||||
|     pickBy(availableLanguages, (_, key) => Localization.locale.includes(key)) | ||||
|     pickBy(AvailableLanguages, (_, key) => Localization.locale.includes(key)) | ||||
|   ) | ||||
|     ? Object.keys( | ||||
|         pickBy(availableLanguages, (_, key) => | ||||
|         pickBy(AvailableLanguages, (_, key) => | ||||
|           Localization.locale.includes(key) | ||||
|         ) | ||||
|       )[0] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user