mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	First step of adding filter editing support
This commit is contained in:
		
							
								
								
									
										23
									
								
								src/components/Hr.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/components/Hr.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { View, ViewStyle } from 'react-native' | ||||
|  | ||||
| const Hr: React.FC<{ style?: ViewStyle }> = ({ style }) => { | ||||
|   const { colors } = useTheme() | ||||
|  | ||||
|   return ( | ||||
|     <View | ||||
|       style={[ | ||||
|         { | ||||
|           borderTopColor: colors.border, | ||||
|           borderTopWidth: 1, | ||||
|           height: 1, | ||||
|           marginVertical: StyleConstants.Spacing.S | ||||
|         }, | ||||
|         style | ||||
|       ]} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default Hr | ||||
| @@ -9,7 +9,9 @@ import CustomText from './Text' | ||||
| export type Props = { | ||||
|   title?: string | ||||
|   multiline?: boolean | ||||
| } & Pick<NonNullable<EmojisState['inputProps'][0]>, 'value' | 'selection' | 'isFocused'> & | ||||
|   invalid?: boolean | ||||
| } & Pick<NonNullable<EmojisState['inputProps'][0]>, 'value'> & | ||||
|   Pick<Partial<EmojisState['inputProps'][0]>, 'isFocused' | 'selection'> & | ||||
|   Omit< | ||||
|     TextInputProps, | ||||
|     | 'style' | ||||
| @@ -27,8 +29,9 @@ const ComponentInput = forwardRef( | ||||
|     { | ||||
|       title, | ||||
|       multiline = false, | ||||
|       invalid = false, | ||||
|       value: [value, setValue], | ||||
|       selection: [selection, setSelection], | ||||
|       selection, | ||||
|       isFocused, | ||||
|       ...props | ||||
|     }: Props, | ||||
| @@ -43,7 +46,7 @@ const ComponentInput = forwardRef( | ||||
|           paddingHorizontal: withTiming(StyleConstants.Spacing.XS), | ||||
|           left: withTiming(StyleConstants.Spacing.S), | ||||
|           top: withTiming(-(StyleConstants.Font.Size.S / 2) - 2), | ||||
|           backgroundColor: withTiming(colors.backgroundDefault) | ||||
|           backgroundColor: colors.backgroundDefault | ||||
|         } | ||||
|       } else { | ||||
|         return { | ||||
| @@ -62,7 +65,7 @@ const ComponentInput = forwardRef( | ||||
|           borderWidth: 1, | ||||
|           marginVertical: StyleConstants.Spacing.S, | ||||
|           padding: StyleConstants.Spacing.S, | ||||
|           borderColor: colors.border, | ||||
|           borderColor: invalid ? colors.red : colors.border, | ||||
|           flexDirection: multiline ? 'column' : 'row', | ||||
|           alignItems: 'stretch' | ||||
|         }} | ||||
| @@ -78,9 +81,13 @@ const ComponentInput = forwardRef( | ||||
|           }} | ||||
|           value={value} | ||||
|           onChangeText={setValue} | ||||
|           onFocus={() => (isFocused.current = true)} | ||||
|           onBlur={() => (isFocused.current = false)} | ||||
|           onSelectionChange={({ nativeEvent }) => setSelection(nativeEvent.selection)} | ||||
|           {...(isFocused !== undefined && { | ||||
|             onFocus: () => (isFocused.current = true), | ||||
|             onBlur: () => (isFocused.current = false) | ||||
|           })} | ||||
|           {...(selection !== undefined && { | ||||
|             onSelectionChange: ({ nativeEvent }) => selection[1](nativeEvent.selection) | ||||
|           })} | ||||
|           {...(multiline && { | ||||
|             multiline, | ||||
|             numberOfLines: Platform.OS === 'android' ? 5 : undefined | ||||
|   | ||||
| @@ -44,7 +44,7 @@ const MenuRow: React.FC<Props> = ({ | ||||
|   loading = false, | ||||
|   onPress | ||||
| }) => { | ||||
|   const { colors, theme } = useTheme() | ||||
|   const { colors } = useTheme() | ||||
|   const { screenReaderEnabled } = useAccessibility() | ||||
|  | ||||
|   return ( | ||||
|   | ||||
| @@ -8,17 +8,22 @@ import { ParseEmojis } from './Parse' | ||||
| import CustomText from './Text' | ||||
|  | ||||
| export interface Props { | ||||
|   title?: string | ||||
|  | ||||
|   multiple?: boolean | ||||
|   options: { selected: boolean; content: string }[] | ||||
|   setOptions: React.Dispatch<React.SetStateAction<{ selected: boolean; content: string }[]>> | ||||
|   disabled?: boolean | ||||
|   invalid?: boolean | ||||
| } | ||||
|  | ||||
| const Selections: React.FC<Props> = ({ | ||||
|   title, | ||||
|   multiple = false, | ||||
|   options, | ||||
|   setOptions, | ||||
|   disabled = false | ||||
|   disabled = false, | ||||
|   invalid = false | ||||
| }) => { | ||||
|   const { colors } = useTheme() | ||||
|  | ||||
| @@ -32,52 +37,71 @@ const Selections: React.FC<Props> = ({ | ||||
|       : 'circle' | ||||
|  | ||||
|   return ( | ||||
|     <View> | ||||
|       {options.map((option, index) => ( | ||||
|         <Pressable | ||||
|           key={index} | ||||
|           disabled={disabled} | ||||
|           style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }} | ||||
|           onPress={() => { | ||||
|             if (multiple) { | ||||
|               haptics('Light') | ||||
|  | ||||
|               setOptions(options.map((o, i) => (i === index ? { ...o, selected: !o.selected } : o))) | ||||
|             } else { | ||||
|               if (!option.selected) { | ||||
|     <View style={{ marginVertical: StyleConstants.Spacing.S }}> | ||||
|       {title ? ( | ||||
|         <CustomText | ||||
|           fontStyle='M' | ||||
|           children={title} | ||||
|           style={{ color: disabled ? colors.disabled : colors.primaryDefault }} | ||||
|         /> | ||||
|       ) : null} | ||||
|       <View | ||||
|         style={{ | ||||
|           paddingHorizontal: StyleConstants.Spacing.M, | ||||
|           paddingVertical: StyleConstants.Spacing.XS, | ||||
|           marginTop: StyleConstants.Spacing.S, | ||||
|           borderWidth: 1, | ||||
|           borderColor: disabled ? colors.disabled : invalid ? colors.red : colors.border | ||||
|         }} | ||||
|       > | ||||
|         {options.map((option, index) => ( | ||||
|           <Pressable | ||||
|             key={index} | ||||
|             disabled={disabled} | ||||
|             style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }} | ||||
|             onPress={() => { | ||||
|               if (multiple) { | ||||
|                 haptics('Light') | ||||
|  | ||||
|                 setOptions( | ||||
|                   options.map((o, i) => { | ||||
|                     if (i === index) { | ||||
|                       return { ...o, selected: true } | ||||
|                     } else { | ||||
|                       return { ...o, selected: false } | ||||
|                     } | ||||
|                   }) | ||||
|                   options.map((o, i) => (i === index ? { ...o, selected: !o.selected } : o)) | ||||
|                 ) | ||||
|               } else { | ||||
|                 if (!option.selected) { | ||||
|                   haptics('Light') | ||||
|                   setOptions( | ||||
|                     options.map((o, i) => { | ||||
|                       if (i === index) { | ||||
|                         return { ...o, selected: true } | ||||
|                       } else { | ||||
|                         return { ...o, selected: false } | ||||
|                       } | ||||
|                     }) | ||||
|                   ) | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           }} | ||||
|         > | ||||
|           <View style={{ flex: 1, flexDirection: 'row' }}> | ||||
|             <Icon | ||||
|               style={{ | ||||
|                 paddingTop: StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M, | ||||
|                 marginRight: StyleConstants.Spacing.S | ||||
|               }} | ||||
|               name={isSelected(index)} | ||||
|               size={StyleConstants.Font.Size.M} | ||||
|               color={disabled ? colors.disabled : colors.primaryDefault} | ||||
|             /> | ||||
|             <CustomText style={{ flex: 1 }}> | ||||
|               <ParseEmojis | ||||
|                 content={option.content} | ||||
|                 style={{ color: disabled ? colors.disabled : colors.primaryDefault }} | ||||
|             }} | ||||
|           > | ||||
|             <View style={{ flex: 1, flexDirection: 'row' }}> | ||||
|               <Icon | ||||
|                 style={{ | ||||
|                   marginTop: (StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M) / 2, | ||||
|                   marginRight: StyleConstants.Spacing.S | ||||
|                 }} | ||||
|                 name={isSelected(index)} | ||||
|                 size={StyleConstants.Font.Size.M} | ||||
|                 color={disabled ? colors.disabled : colors.primaryDefault} | ||||
|               /> | ||||
|             </CustomText> | ||||
|           </View> | ||||
|         </Pressable> | ||||
|       ))} | ||||
|               <CustomText fontStyle='S' style={{ flex: 1 }}> | ||||
|                 <ParseEmojis | ||||
|                   content={option.content} | ||||
|                   style={{ color: disabled ? colors.disabled : colors.primaryDefault }} | ||||
|                 /> | ||||
|               </CustomText> | ||||
|             </View> | ||||
|           </Pressable> | ||||
|         ))} | ||||
|       </View> | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -99,7 +99,7 @@ export const shouldFilter = ({ | ||||
|         break | ||||
|     } | ||||
|   } | ||||
|   const queryKeyFilters: QueryKeyFilters = ['Filters'] | ||||
|   const queryKeyFilters: QueryKeyFilters = ['Filters', { version: 'v1' }] | ||||
|   queryClient.getQueryData<Mastodon.Filter<'v1'>[]>(queryKeyFilters)?.forEach(filter => { | ||||
|     if (returnFilter) { | ||||
|       return | ||||
|   | ||||
| @@ -133,7 +133,7 @@ const TimelinePoll: React.FC = () => { | ||||
|         <View style={{ flex: 1, flexDirection: 'row' }}> | ||||
|           <Icon | ||||
|             style={{ | ||||
|               paddingTop: StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M, | ||||
|               marginTop: (StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M) / 2, | ||||
|               marginRight: StyleConstants.Spacing.S | ||||
|             }} | ||||
|             name={ | ||||
| @@ -205,7 +205,7 @@ const TimelinePoll: React.FC = () => { | ||||
|         <View style={{ flex: 1, flexDirection: 'row' }}> | ||||
|           <Icon | ||||
|             style={{ | ||||
|               paddingTop: StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M, | ||||
|               marginTop: (StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M) / 2, | ||||
|               marginRight: StyleConstants.Spacing.S | ||||
|             }} | ||||
|             name={isSelected(index)} | ||||
|   | ||||
| @@ -72,6 +72,15 @@ | ||||
|       "preferences": { | ||||
|         "name": "Preferences" | ||||
|       }, | ||||
|       "preferencesFilters": { | ||||
|         "name": "All content filters" | ||||
|       }, | ||||
|       "preferencesFilterAdd": { | ||||
|         "name": "Create a Filter" | ||||
|       }, | ||||
|       "preferencesFilterEdit": { | ||||
|         "name": "Edit Filter" | ||||
|       }, | ||||
|       "profile": { | ||||
|         "name": "Edit Profile" | ||||
|       }, | ||||
| @@ -127,7 +136,7 @@ | ||||
|     }, | ||||
|     "preferences": { | ||||
|       "visibility": { | ||||
|         "title": "Posting visibility", | ||||
|         "title": "Default posting visibility", | ||||
|         "options": { | ||||
|           "public": "Public", | ||||
|           "unlisted": "Unlisted", | ||||
| @@ -135,7 +144,7 @@ | ||||
|         } | ||||
|       }, | ||||
|       "sensitive": { | ||||
|         "title": "Posting media sensitive" | ||||
|         "title": "Default mark media as sensitive" | ||||
|       }, | ||||
|       "media": { | ||||
|         "title": "Media display", | ||||
| @@ -146,16 +155,63 @@ | ||||
|         } | ||||
|       }, | ||||
|       "spoilers": { | ||||
|         "title": "Auto expand toots with content warning", | ||||
|         "title": "Auto expand toots with content warning" | ||||
|       }, | ||||
|       "autoplay_gifs": { | ||||
|         "title": "Autoplay GIF in toots" | ||||
|       }, | ||||
|       "filters": { | ||||
|         "title": "Content filters", | ||||
|         "content": "{{count}} active" | ||||
|       }, | ||||
|       "web_only": { | ||||
|         "title": "Update settings", | ||||
|         "description": "Settings below can only be updated using the web UI" | ||||
|       } | ||||
|     }, | ||||
|     "preferencesFilters": { | ||||
|       "expired": "Expired", | ||||
|       "keywords_one": "{{count}} keyword", | ||||
|       "keywords_other": "{{count}} keywords", | ||||
|       "statuses_one": "{{count}} toot", | ||||
|       "statuses_other": "{{count}} toots", | ||||
|       "context": "Applies in <0 />", | ||||
|       "contexts": { | ||||
|         "home": "following and lists", | ||||
|         "notifications": "notification", | ||||
|         "public": "federated", | ||||
|         "thread": "conversation", | ||||
|         "account": "profile" | ||||
|       } | ||||
|     }, | ||||
|     "preferencesFilter": { | ||||
|       "name": "Name", | ||||
|       "expiration": "Expiration", | ||||
|       "expirationOptions": { | ||||
|         "0": "Never", | ||||
|         "1800": "After 30 minutes", | ||||
|         "3600": "After 1 hour", | ||||
|         "43200": "After 12 hours", | ||||
|         "86400": "After 1 day", | ||||
|         "604800": "After 1 week", | ||||
|         "18144000": "After 1 month" | ||||
|       }, | ||||
|       "context": "Applies in", | ||||
|       "contexts": { | ||||
|         "home": "Following and lists", | ||||
|         "notifications": "Notification", | ||||
|         "public": "Federated timeline", | ||||
|         "thread": "Conversation view", | ||||
|         "account": "Profile view" | ||||
|       }, | ||||
|       "action": "When matched", | ||||
|       "actions": { | ||||
|         "warn": "Collapsed but can be revealed", | ||||
|         "hide": "Hidden completely" | ||||
|       }, | ||||
|       "keywords": "Matches for these keywords", | ||||
|       "keyword": "Keyword" | ||||
|     }, | ||||
|     "profile": { | ||||
|       "feedback": { | ||||
|         "succeed": "{{type}} updated", | ||||
|   | ||||
| @@ -19,7 +19,7 @@ const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({ | ||||
|   navigation, | ||||
|   route: { params } | ||||
| }) => { | ||||
|   const { colors, theme } = useTheme() | ||||
|   const { colors } = useTheme() | ||||
|   const { t } = useTranslation(['common', 'screenTabs']) | ||||
|  | ||||
|   const messageRef = useRef(null) | ||||
| @@ -147,18 +147,11 @@ const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({ | ||||
|     <ScrollView style={{ paddingHorizontal: StyleConstants.Spacing.Global.PagePadding }}> | ||||
|       <ComponentInput {...inputProps} autoFocus title={t('screenTabs:me.listEdit.title')} /> | ||||
|  | ||||
|       <CustomText | ||||
|         fontStyle='M' | ||||
|         fontWeight='Bold' | ||||
|         style={{ | ||||
|           color: colors.primaryDefault, | ||||
|           marginBottom: StyleConstants.Spacing.XS, | ||||
|           marginTop: StyleConstants.Spacing.M | ||||
|         }} | ||||
|       > | ||||
|         {t('screenTabs:me.listEdit.repliesPolicy.heading')} | ||||
|       </CustomText> | ||||
|       <Selections options={options} setOptions={setOptions} /> | ||||
|       <Selections | ||||
|         title={t('screenTabs:me.listEdit.repliesPolicy.heading')} | ||||
|         options={options} | ||||
|         setOptions={setOptions} | ||||
|       /> | ||||
|  | ||||
|       <Message ref={messageRef} /> | ||||
|     </ScrollView> | ||||
|   | ||||
							
								
								
									
										265
									
								
								src/screens/Tabs/Me/Preferences/Filter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								src/screens/Tabs/Me/Preferences/Filter.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,265 @@ | ||||
| import Button from '@components/Button' | ||||
| import haptics from '@components/haptics' | ||||
| import { HeaderLeft, HeaderRight } from '@components/Header' | ||||
| import Hr from '@components/Hr' | ||||
| import ComponentInput from '@components/Input' | ||||
| import { MenuRow } from '@components/Menu' | ||||
| import Selections from '@components/Selections' | ||||
| import CustomText from '@components/Text' | ||||
| import { useActionSheet } from '@expo/react-native-action-sheet' | ||||
| import apiInstance from '@utils/api/instance' | ||||
| import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles' | ||||
| import { TabMePreferencesStackScreenProps } from '@utils/navigation/navigators' | ||||
| import { queryClient } from '@utils/queryHooks' | ||||
| import { QueryKeyFilters } from '@utils/queryHooks/filters' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { RefObject, useEffect, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { KeyboardAvoidingView, Platform, View } from 'react-native' | ||||
| import FlashMessage from 'react-native-flash-message' | ||||
| import { ScrollView } from 'react-native-gesture-handler' | ||||
| import { SafeAreaView } from 'react-native-safe-area-context' | ||||
|  | ||||
| const TabMePreferencesFilter: React.FC< | ||||
|   TabMePreferencesStackScreenProps<'Tab-Me-Preferences-Filter'> & { | ||||
|     messageRef: RefObject<FlashMessage> | ||||
|   } | ||||
| > = ({ navigation, route: { params } }) => { | ||||
|   const { colors } = useTheme() | ||||
|   const { t } = useTranslation(['common', 'screenTabs']) | ||||
|  | ||||
|   const { showActionSheetWithOptions } = useActionSheet() | ||||
|  | ||||
|   useEffect(() => { | ||||
|     navigation.setOptions({ | ||||
|       title: | ||||
|         params.type === 'add' | ||||
|           ? t('screenTabs:me.stacks.preferencesFilterAdd.name') | ||||
|           : t('screenTabs:me.stacks.preferencesFilterEdit.name'), | ||||
|       headerLeft: () => ( | ||||
|         <HeaderLeft | ||||
|           content='chevron-left' | ||||
|           onPress={() => navigation.navigate('Tab-Me-Preferences-Filters')} | ||||
|         /> | ||||
|       ) | ||||
|     }) | ||||
|   }, []) | ||||
|  | ||||
|   const titleState = useState(params.type === 'edit' ? params.filter.title : '') | ||||
|  | ||||
|   const expirations = ['0', '1800', '3600', '43200', '86400', '604800', '18144000'] as const | ||||
|   const [expiration, setExpiration] = useState<typeof expirations[number]>('0') | ||||
|  | ||||
|   const [contexts, setContexts] = useState< | ||||
|     { | ||||
|       selected: boolean | ||||
|       content: string | ||||
|       type: 'home' | 'notifications' | 'public' | 'thread' | 'account' | ||||
|     }[] | ||||
|   >([ | ||||
|     { | ||||
|       selected: params.type === 'edit' ? params.filter.context.includes('home') : true, | ||||
|       content: t('screenTabs:me.preferencesFilter.contexts.home'), | ||||
|       type: 'home' | ||||
|     }, | ||||
|     { | ||||
|       selected: params.type === 'edit' ? params.filter.context.includes('notifications') : false, | ||||
|       content: t('screenTabs:me.preferencesFilter.contexts.notifications'), | ||||
|       type: 'notifications' | ||||
|     }, | ||||
|     { | ||||
|       selected: params.type === 'edit' ? params.filter.context.includes('public') : false, | ||||
|       content: t('screenTabs:me.preferencesFilter.contexts.public'), | ||||
|       type: 'public' | ||||
|     }, | ||||
|     { | ||||
|       selected: params.type === 'edit' ? params.filter.context.includes('thread') : false, | ||||
|       content: t('screenTabs:me.preferencesFilter.contexts.thread'), | ||||
|       type: 'thread' | ||||
|     }, | ||||
|     { | ||||
|       selected: params.type === 'edit' ? params.filter.context.includes('account') : false, | ||||
|       content: t('screenTabs:me.preferencesFilter.contexts.account'), | ||||
|       type: 'account' | ||||
|     } | ||||
|   ]) | ||||
|  | ||||
|   const [actions, setActions] = useState< | ||||
|     { selected: boolean; content: string; type: 'warn' | 'hide' }[] | ||||
|   >([ | ||||
|     { | ||||
|       selected: params.type === 'edit' ? params.filter.filter_action === 'warn' : true, | ||||
|       content: t('screenTabs:me.preferencesFilter.actions.warn'), | ||||
|       type: 'warn' | ||||
|     }, | ||||
|     { | ||||
|       selected: params.type === 'edit' ? params.filter.filter_action === 'hide' : false, | ||||
|       content: t('screenTabs:me.preferencesFilter.actions.hide'), | ||||
|       type: 'hide' | ||||
|     } | ||||
|   ]) | ||||
|  | ||||
|   const [keywords, setKeywords] = useState<string[]>( | ||||
|     params.type === 'edit' ? params.filter.keywords.map(({ keyword }) => keyword) : [] | ||||
|   ) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     let isLoading = false | ||||
|     navigation.setOptions({ | ||||
|       headerRight: () => ( | ||||
|         <HeaderRight | ||||
|           content='save' | ||||
|           loading={isLoading} | ||||
|           onPress={async () => { | ||||
|             if (!titleState[0].length || !contexts.filter(context => context.selected).length) | ||||
|               return | ||||
|  | ||||
|             switch (params.type) { | ||||
|               case 'add': | ||||
|                 isLoading = true | ||||
|                 await apiInstance({ | ||||
|                   method: 'post', | ||||
|                   version: 'v2', | ||||
|                   url: 'filters', | ||||
|                   body: { | ||||
|                     title: titleState[0], | ||||
|                     context: contexts | ||||
|                       .filter(context => context.selected) | ||||
|                       .map(context => context.type), | ||||
|                     filter_action: actions.filter( | ||||
|                       action => action.type === 'hide' && action.selected | ||||
|                     ).length | ||||
|                       ? 'hide' | ||||
|                       : 'warn', | ||||
|                     ...(parseInt(expiration) && { expires_in: parseInt(expiration) }), | ||||
|                     ...(keywords.filter(keyword => keyword.length).length && { | ||||
|                       keywords_attributes: keywords | ||||
|                         .filter(keyword => keyword.length) | ||||
|                         .map(keyword => ({ keyword, whole_word: true })) | ||||
|                     }) | ||||
|                   } | ||||
|                 }) | ||||
|                   .then(() => { | ||||
|                     isLoading = false | ||||
|                     const queryKey: QueryKeyFilters = ['Filters', { version: 'v2' }] | ||||
|                     queryClient.refetchQueries(queryKey) | ||||
|                     navigation.navigate('Tab-Me-Preferences-Filters') | ||||
|                   }) | ||||
|                   .catch(() => { | ||||
|                     isLoading = false | ||||
|                     haptics('Error') | ||||
|                   }) | ||||
|                 break | ||||
|               case 'edit': | ||||
|                 break | ||||
|             } | ||||
|           }} | ||||
|         /> | ||||
|       ) | ||||
|     }) | ||||
|   }, [titleState[0], expiration, contexts, actions, keywords]) | ||||
|  | ||||
|   return ( | ||||
|     <KeyboardAvoidingView | ||||
|       style={{ flex: 1 }} | ||||
|       behavior={Platform.OS === 'ios' ? 'padding' : 'height'} | ||||
|     > | ||||
|       <SafeAreaView style={{ flex: 1 }} edges={['bottom']}> | ||||
|         <ScrollView style={{ padding: StyleConstants.Spacing.Global.PagePadding }}> | ||||
|           <ComponentInput title={t('screenTabs:me.preferencesFilter.name')} value={titleState} /> | ||||
|           <MenuRow | ||||
|             title={t('screenTabs:me.preferencesFilter.expiration')} | ||||
|             content={t(`screenTabs:me.preferencesFilter.expirationOptions.${expiration}`)} | ||||
|             iconBack='chevron-right' | ||||
|             onPress={() => | ||||
|               showActionSheetWithOptions( | ||||
|                 { | ||||
|                   title: t('screenTabs:me.preferencesFilter.expiration'), | ||||
|                   options: [ | ||||
|                     ...expirations.map(opt => | ||||
|                       t(`screenTabs:me.preferencesFilter.expirationOptions.${opt}`) | ||||
|                     ), | ||||
|                     t('common:buttons.cancel') | ||||
|                   ], | ||||
|                   cancelButtonIndex: expirations.length, | ||||
|                   ...androidActionSheetStyles(colors) | ||||
|                 }, | ||||
|                 (selectedIndex: number) => { | ||||
|                   selectedIndex < expirations.length && setExpiration(expirations[selectedIndex]) | ||||
|                 } | ||||
|               ) | ||||
|             } | ||||
|           /> | ||||
|           <Hr /> | ||||
|  | ||||
|           <Selections | ||||
|             title={t('screenTabs:me.preferencesFilter.context')} | ||||
|             multiple | ||||
|             invalid={!contexts.filter(context => context.selected).length} | ||||
|             options={contexts} | ||||
|             setOptions={setContexts} | ||||
|           /> | ||||
|           <Selections | ||||
|             title={t('screenTabs:me.preferencesFilter.action')} | ||||
|             options={actions} | ||||
|             setOptions={setActions} | ||||
|           /> | ||||
|           <Hr style={{ marginVertical: StyleConstants.Spacing.M }} /> | ||||
|  | ||||
|           <CustomText | ||||
|             fontStyle='M' | ||||
|             children={t('screenTabs:me.preferencesFilter.keywords')} | ||||
|             style={{ color: colors.primaryDefault }} | ||||
|           /> | ||||
|           <View | ||||
|             style={{ | ||||
|               marginTop: StyleConstants.Spacing.M, | ||||
|               marginBottom: StyleConstants.Spacing.S | ||||
|             }} | ||||
|           > | ||||
|             {[...Array(keywords.length)].map((_, i) => ( | ||||
|               <ComponentInput | ||||
|                 key={i} | ||||
|                 title={t('screenTabs:me.preferencesFilter.keyword')} | ||||
|                 value={[ | ||||
|                   keywords[i], | ||||
|                   k => setKeywords(keywords.map((curr, ii) => (i === ii ? k : curr))) | ||||
|                 ]} | ||||
|               /> | ||||
|             ))} | ||||
|           </View> | ||||
|           <View | ||||
|             style={{ | ||||
|               flex: 1, | ||||
|               flexDirection: 'row', | ||||
|               alignItems: 'center', | ||||
|               justifyContent: 'flex-end', | ||||
|               marginRight: StyleConstants.Spacing.M | ||||
|             }} | ||||
|           > | ||||
|             <Button | ||||
|               onPress={() => setKeywords(keywords.slice(0, keywords.length - 1))} | ||||
|               type='icon' | ||||
|               content='minus' | ||||
|               round | ||||
|               disabled={keywords.length < 1} | ||||
|             /> | ||||
|             <CustomText | ||||
|               style={{ marginHorizontal: StyleConstants.Spacing.M, color: colors.secondary }} | ||||
|               children={keywords.length} | ||||
|             /> | ||||
|             <Button | ||||
|               onPress={() => setKeywords([...keywords, ''])} | ||||
|               type='icon' | ||||
|               content='plus' | ||||
|               round | ||||
|             /> | ||||
|           </View> | ||||
|         </ScrollView> | ||||
|       </SafeAreaView> | ||||
|     </KeyboardAvoidingView> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default TabMePreferencesFilter | ||||
							
								
								
									
										166
									
								
								src/screens/Tabs/Me/Preferences/Filters.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								src/screens/Tabs/Me/Preferences/Filters.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | ||||
| import { HeaderLeft, HeaderRight } from '@components/Header' | ||||
| import Icon from '@components/Icon' | ||||
| import ComponentSeparator from '@components/Separator' | ||||
| import CustomText from '@components/Text' | ||||
| import apiInstance from '@utils/api/instance' | ||||
| import { TabMePreferencesStackScreenProps } from '@utils/navigation/navigators' | ||||
| import { useFiltersQuery } from '@utils/queryHooks/filters' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { Fragment, useEffect } from 'react' | ||||
| import { Trans, useTranslation } from 'react-i18next' | ||||
| import { Pressable, TouchableNativeFeedback, View } from 'react-native' | ||||
| import { SwipeListView } from 'react-native-swipe-list-view' | ||||
|  | ||||
| const TabMePreferencesFilters: React.FC< | ||||
|   TabMePreferencesStackScreenProps<'Tab-Me-Preferences-Filters'> | ||||
| > = ({ navigation }) => { | ||||
|   const { colors } = useTheme() | ||||
|   const { t } = useTranslation(['common', 'screenTabs']) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     navigation.setOptions({ | ||||
|       headerLeft: () => ( | ||||
|         <HeaderLeft | ||||
|           content='chevron-left' | ||||
|           onPress={() => navigation.navigate('Tab-Me-Preferences-Root')} | ||||
|         /> | ||||
|       ), | ||||
|       headerRight: () => ( | ||||
|         <HeaderRight | ||||
|           content='plus' | ||||
|           onPress={() => navigation.navigate('Tab-Me-Preferences-Filter', { type: 'add' })} | ||||
|         /> | ||||
|       ) | ||||
|     }) | ||||
|   }, []) | ||||
|  | ||||
|   const { data, refetch } = useFiltersQuery<'v2'>({ version: 'v2' }) | ||||
|  | ||||
|   return ( | ||||
|     <SwipeListView | ||||
|       renderHiddenItem={({ item }) => ( | ||||
|         <Pressable | ||||
|           style={{ | ||||
|             flex: 1, | ||||
|             justifyContent: 'center', | ||||
|             alignItems: 'flex-end', | ||||
|             backgroundColor: colors.red | ||||
|           }} | ||||
|           onPress={() => { | ||||
|             apiInstance({ method: 'delete', version: 'v2', url: `filters/${item.id}` }).then(() => | ||||
|               refetch() | ||||
|             ) | ||||
|           }} | ||||
|         > | ||||
|           <View style={{ paddingHorizontal: StyleConstants.Spacing.L }}> | ||||
|             <Icon name='trash' color='white' size={StyleConstants.Font.Size.L} /> | ||||
|           </View> | ||||
|         </Pressable> | ||||
|       )} | ||||
|       rightOpenValue={-(StyleConstants.Spacing.L * 2 + StyleConstants.Font.Size.L)} | ||||
|       disableRightSwipe | ||||
|       closeOnRowPress | ||||
|       data={data?.sort(filter => | ||||
|         filter.expires_at ? new Date().getTime() - new Date(filter.expires_at).getTime() : 1 | ||||
|       )} | ||||
|       renderItem={({ item: filter }) => ( | ||||
|         <TouchableNativeFeedback | ||||
|           onPress={() => navigation.navigate('Tab-Me-Preferences-Filter', { type: 'edit', filter })} | ||||
|         > | ||||
|           <View | ||||
|             style={{ | ||||
|               padding: StyleConstants.Spacing.Global.PagePadding, | ||||
|               flexDirection: 'row', | ||||
|               alignItems: 'center', | ||||
|               backgroundColor: colors.backgroundDefault | ||||
|             }} | ||||
|           > | ||||
|             <View style={{ flex: 1 }}> | ||||
|               <CustomText | ||||
|                 fontStyle='M' | ||||
|                 children={filter.title} | ||||
|                 style={{ color: colors.primaryDefault }} | ||||
|                 numberOfLines={1} | ||||
|               /> | ||||
|               <View | ||||
|                 style={{ | ||||
|                   flexDirection: 'row', | ||||
|                   alignItems: 'center', | ||||
|                   marginVertical: StyleConstants.Spacing.XS | ||||
|                 }} | ||||
|               > | ||||
|                 {filter.expires_at && new Date() > new Date(filter.expires_at) ? ( | ||||
|                   <CustomText | ||||
|                     fontStyle='S' | ||||
|                     fontWeight='Bold' | ||||
|                     children={t('screenTabs:me.preferencesFilters.expired')} | ||||
|                     style={{ color: colors.red, marginRight: StyleConstants.Spacing.M }} | ||||
|                   /> | ||||
|                 ) : null} | ||||
|                 {filter.keywords?.length ? ( | ||||
|                   <CustomText | ||||
|                     children={t('screenTabs:me.preferencesFilters.keywords', { | ||||
|                       count: filter.keywords.length | ||||
|                     })} | ||||
|                     style={{ color: colors.primaryDefault }} | ||||
|                   /> | ||||
|                 ) : null} | ||||
|                 {filter.keywords?.length && filter.statuses?.length ? ( | ||||
|                   <CustomText | ||||
|                     children={t('common:separator')} | ||||
|                     style={{ color: colors.primaryDefault }} | ||||
|                   /> | ||||
|                 ) : null} | ||||
|                 {filter.statuses?.length ? ( | ||||
|                   <CustomText | ||||
|                     children={t('screenTabs:me.preferencesFilters.statuses', { | ||||
|                       count: filter.statuses.length | ||||
|                     })} | ||||
|                     style={{ color: colors.primaryDefault }} | ||||
|                   /> | ||||
|                 ) : null} | ||||
|               </View> | ||||
|               <CustomText | ||||
|                 style={{ color: colors.secondary }} | ||||
|                 children={ | ||||
|                   <Trans | ||||
|                     ns='screenTabs' | ||||
|                     i18nKey='me.preferencesFilters.context' | ||||
|                     components={[ | ||||
|                       <> | ||||
|                         {filter.context.map((c, index) => ( | ||||
|                           <Fragment key={index}> | ||||
|                             <CustomText | ||||
|                               style={{ | ||||
|                                 color: colors.secondary, | ||||
|                                 textDecorationColor: colors.disabled, | ||||
|                                 textDecorationLine: 'underline', | ||||
|                                 textDecorationStyle: 'solid' | ||||
|                               }} | ||||
|                               children={t(`screenTabs:me.preferencesFilters.contexts.${c}`)} | ||||
|                             /> | ||||
|                             <CustomText children={t('common:separator')} /> | ||||
|                           </Fragment> | ||||
|                         ))} | ||||
|                       </> | ||||
|                     ]} | ||||
|                   /> | ||||
|                 } | ||||
|               /> | ||||
|             </View> | ||||
|             <Icon | ||||
|               name='chevron-right' | ||||
|               size={StyleConstants.Font.Size.L} | ||||
|               color={colors.primaryDefault} | ||||
|               style={{ marginLeft: 8 }} | ||||
|             /> | ||||
|           </View> | ||||
|         </TouchableNativeFeedback> | ||||
|       )} | ||||
|       ItemSeparatorComponent={ComponentSeparator} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default TabMePreferencesFilters | ||||
| @@ -1,21 +1,26 @@ | ||||
| import { MenuContainer, MenuRow } from '@components/Menu' | ||||
| import { Message } from '@components/Message' | ||||
| import { useActionSheet } from '@expo/react-native-action-sheet' | ||||
| import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles' | ||||
| import browserPackage from '@utils/helpers/browserPackage' | ||||
| import { TabMeProfileStackScreenProps } from '@utils/navigation/navigators' | ||||
| import { featureCheck } from '@utils/helpers/featureCheck' | ||||
| import { TabMePreferencesStackScreenProps } from '@utils/navigation/navigators' | ||||
| import { useFiltersQuery } from '@utils/queryHooks/filters' | ||||
| import { usePreferencesQuery } from '@utils/queryHooks/preferences' | ||||
| import { useProfileMutation } from '@utils/queryHooks/profile' | ||||
| import { getAccountStorage } from '@utils/storage/actions' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import * as WebBrowser from 'expo-web-browser' | ||||
| import React, { useRef } from 'react' | ||||
| import React, { RefObject } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import FlashMessage from 'react-native-flash-message' | ||||
| import { ScrollView } from 'react-native-gesture-handler' | ||||
| 
 | ||||
| const TabMePreferences: React.FC<TabMeProfileStackScreenProps<'Tab-Me-Profile-Root'>> = () => { | ||||
| const TabMePreferencesRoot: React.FC< | ||||
|   TabMePreferencesStackScreenProps<'Tab-Me-Preferences-Root'> & { | ||||
|     messageRef: RefObject<FlashMessage> | ||||
|   } | ||||
| > = ({ navigation, messageRef }) => { | ||||
|   const { colors } = useTheme() | ||||
|   const { t } = useTranslation(['common', 'screenTabs']) | ||||
| 
 | ||||
| @@ -25,7 +30,7 @@ const TabMePreferences: React.FC<TabMeProfileStackScreenProps<'Tab-Me-Profile-Ro | ||||
| 
 | ||||
|   const { data, isFetching, refetch } = usePreferencesQuery() | ||||
| 
 | ||||
|   const messageRef = useRef<FlashMessage>(null) | ||||
|   const { data: filters, isFetching: filtersIsFetching } = useFiltersQuery<'v2'>({ version: 'v2' }) | ||||
| 
 | ||||
|   return ( | ||||
|     <ScrollView> | ||||
| @@ -149,10 +154,23 @@ const TabMePreferences: React.FC<TabMeProfileStackScreenProps<'Tab-Me-Profile-Ro | ||||
|           /> | ||||
|         ) : null} | ||||
|       </MenuContainer> | ||||
| 
 | ||||
|       <Message ref={messageRef} /> | ||||
|       {featureCheck('filter_server_side') ? ( | ||||
|         <MenuContainer> | ||||
|           <MenuRow | ||||
|             title={t('screenTabs:me.preferences.filters.title')} | ||||
|             content={t('screenTabs:me.preferences.filters.content', { | ||||
|               count: filters?.filter(filter => | ||||
|                 filter.expires_at ? new Date(filter.expires_at) > new Date() : true | ||||
|               ).length | ||||
|             })} | ||||
|             loading={filtersIsFetching} | ||||
|             iconBack='chevron-right' | ||||
|             onPress={() => navigation.navigate('Tab-Me-Preferences-Filters')} | ||||
|           /> | ||||
|         </MenuContainer> | ||||
|       ) : null} | ||||
|     </ScrollView> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export default TabMePreferences | ||||
| export default TabMePreferencesRoot | ||||
							
								
								
									
										49
									
								
								src/screens/Tabs/Me/Preferences/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/screens/Tabs/Me/Preferences/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| import { HeaderLeft } from '@components/Header' | ||||
| import { Message } from '@components/Message' | ||||
| import { createNativeStackNavigator } from '@react-navigation/native-stack' | ||||
| import { TabMePreferencesStackParamList, TabMeStackScreenProps } from '@utils/navigation/navigators' | ||||
| import React, { useRef } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import FlashMessage from 'react-native-flash-message' | ||||
| import TabMePreferencesFilter from './Filter' | ||||
| import TabMePreferencesFilters from './Filters' | ||||
| import TabMePreferencesRoot from './Root' | ||||
|  | ||||
| const Stack = createNativeStackNavigator<TabMePreferencesStackParamList>() | ||||
|  | ||||
| const TabMePreferences: React.FC<TabMeStackScreenProps<'Tab-Me-Preferences'>> = ({ | ||||
|   navigation | ||||
| }) => { | ||||
|   const { t } = useTranslation('screenTabs') | ||||
|   const messageRef = useRef<FlashMessage>(null) | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Stack.Navigator screenOptions={{ headerShadowVisible: false }}> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-Preferences-Root' | ||||
|           options={{ | ||||
|             title: t('me.stacks.preferences.name'), | ||||
|             headerLeft: () => ( | ||||
|               <HeaderLeft content='chevron-down' onPress={() => navigation.goBack()} /> | ||||
|             ) | ||||
|           }} | ||||
|         > | ||||
|           {props => <TabMePreferencesRoot messageRef={messageRef} {...props} />} | ||||
|         </Stack.Screen> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-Preferences-Filters' | ||||
|           component={TabMePreferencesFilters} | ||||
|           options={{ title: t('me.stacks.preferencesFilters.name') }} | ||||
|         /> | ||||
|         <Stack.Screen name='Tab-Me-Preferences-Filter'> | ||||
|           {props => <TabMePreferencesFilter messageRef={messageRef} {...props} />} | ||||
|         </Stack.Screen> | ||||
|       </Stack.Navigator> | ||||
|  | ||||
|       <Message ref={messageRef} /> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default TabMePreferences | ||||
| @@ -90,7 +90,7 @@ const TabMeProfileFields: React.FC< | ||||
|     navigation.setOptions({ | ||||
|       headerLeft: () => ( | ||||
|         <HeaderLeft | ||||
|           content='x' | ||||
|           content='chevron-left' | ||||
|           onPress={() => { | ||||
|             if (dirty) { | ||||
|               Alert.alert(t('common:discard.title'), t('common:discard.message'), [ | ||||
|   | ||||
| @@ -44,7 +44,7 @@ const TabMeProfileName: React.FC< | ||||
|     navigation.setOptions({ | ||||
|       headerLeft: () => ( | ||||
|         <HeaderLeft | ||||
|           content='x' | ||||
|           content='chevron-left' | ||||
|           onPress={() => { | ||||
|             if (dirty) { | ||||
|               Alert.alert(t('common:discard.title'), t('common:discard.message'), [ | ||||
|   | ||||
| @@ -44,7 +44,7 @@ const TabMeProfileNote: React.FC< | ||||
|     navigation.setOptions({ | ||||
|       headerLeft: () => ( | ||||
|         <HeaderLeft | ||||
|           content='x' | ||||
|           content='chevron-left' | ||||
|           onPress={() => { | ||||
|             if (dirty) { | ||||
|               Alert.alert(t('common:discard.title'), t('common:discard.message'), [ | ||||
|   | ||||
| @@ -32,33 +32,25 @@ const TabMeProfile: React.FC<TabMeStackScreenProps<'Tab-Me-Switch'>> = ({ naviga | ||||
|             ) | ||||
|           }} | ||||
|         > | ||||
|           {({ route, navigation }) => ( | ||||
|             <TabMeProfileRoot messageRef={messageRef} route={route} navigation={navigation} /> | ||||
|           )} | ||||
|           {props => <TabMeProfileRoot messageRef={messageRef} {...props} />} | ||||
|         </Stack.Screen> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-Profile-Name' | ||||
|           options={{ title: t('me.stacks.profileName.name') }} | ||||
|         > | ||||
|           {({ route, navigation }) => ( | ||||
|             <TabMeProfileName messageRef={messageRef} route={route} navigation={navigation} /> | ||||
|           )} | ||||
|           {props => <TabMeProfileName messageRef={messageRef} {...props} />} | ||||
|         </Stack.Screen> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-Profile-Note' | ||||
|           options={{ title: t('me.stacks.profileNote.name') }} | ||||
|         > | ||||
|           {({ route, navigation }) => ( | ||||
|             <TabMeProfileNote messageRef={messageRef} route={route} navigation={navigation} /> | ||||
|           )} | ||||
|           {props => <TabMeProfileNote messageRef={messageRef} {...props} />} | ||||
|         </Stack.Screen> | ||||
|         <Stack.Screen | ||||
|           name='Tab-Me-Profile-Fields' | ||||
|           options={{ title: t('me.stacks.profileFields.name') }} | ||||
|         > | ||||
|           {({ route, navigation }) => ( | ||||
|             <TabMeProfileFields messageRef={messageRef} route={route} navigation={navigation} /> | ||||
|           )} | ||||
|           {props => <TabMeProfileFields messageRef={messageRef} {...props} />} | ||||
|         </Stack.Screen> | ||||
|       </Stack.Navigator> | ||||
|  | ||||
|   | ||||
| @@ -104,11 +104,7 @@ const TabMe: React.FC = () => { | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-Preferences' | ||||
|         component={TabMePreferences} | ||||
|         options={({ navigation }: any) => ({ | ||||
|           presentation: 'modal', | ||||
|           title: t('me.stacks.preferences.name'), | ||||
|           headerLeft: () => <HeaderLeft content='chevron-down' onPress={() => navigation.pop(1)} /> | ||||
|         })} | ||||
|         options={{ headerShown: false, presentation: 'modal' }} | ||||
|       /> | ||||
|       <Stack.Screen | ||||
|         name='Tab-Me-Profile' | ||||
| @@ -154,7 +150,9 @@ const TabMe: React.FC = () => { | ||||
|           presentation: 'modal', | ||||
|           headerShown: true, | ||||
|           title: t('me.stacks.switch.name'), | ||||
|           headerLeft: () => <HeaderLeft content='chevron-down' onPress={() => navigation.goBack()} /> | ||||
|           headerLeft: () => ( | ||||
|             <HeaderLeft content='chevron-down' onPress={() => navigation.goBack()} /> | ||||
|           ) | ||||
|         })} | ||||
|       /> | ||||
|  | ||||
|   | ||||
| @@ -48,7 +48,9 @@ const AccountInformationActions: React.FC = () => { | ||||
|           disabled={account === undefined} | ||||
|           content='sliders' | ||||
|           style={{ marginLeft: StyleConstants.Spacing.S }} | ||||
|           onPress={() => navigation.navigate('Tab-Me-Preferences')} | ||||
|           onPress={() => | ||||
|             navigation.navigate('Tab-Me-Preferences', { screen: 'Tab-Me-Preferences-Root' }) | ||||
|           } | ||||
|         /> | ||||
|       </View> | ||||
|     ) | ||||
|   | ||||
| @@ -125,7 +125,11 @@ const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>> | ||||
|           > | ||||
|             <CustomText | ||||
|               fontStyle='M' | ||||
|               style={{ color: colors.primaryDefault, paddingRight: StyleConstants.Spacing.M }} | ||||
|               style={{ | ||||
|                 flex: 1, | ||||
|                 color: colors.primaryDefault, | ||||
|                 paddingRight: StyleConstants.Spacing.M | ||||
|               }} | ||||
|               numberOfLines={2} | ||||
|             > | ||||
|               {t('screenTabs:shared.report.forward.heading', { | ||||
| @@ -140,15 +144,11 @@ const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>> | ||||
|           </View> | ||||
|         ) : null} | ||||
|  | ||||
|         <CustomText | ||||
|           fontStyle='M' | ||||
|           style={{ color: colors.primaryDefault, marginBottom: StyleConstants.Spacing.S }} | ||||
|         > | ||||
|           {t('screenTabs:shared.report.reasons.heading')} | ||||
|         </CustomText> | ||||
|         <View style={{ marginLeft: StyleConstants.Spacing.M }}> | ||||
|           <Selections options={categories} setOptions={setCategories} /> | ||||
|         </View> | ||||
|         <Selections | ||||
|           title={t('screenTabs:shared.report.reasons.heading')} | ||||
|           options={categories} | ||||
|           setOptions={setCategories} | ||||
|         /> | ||||
|  | ||||
|         {categories[1].selected || comment.length ? ( | ||||
|           <> | ||||
| @@ -200,26 +200,13 @@ const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>> | ||||
|         ) : null} | ||||
|  | ||||
|         {rules.length ? ( | ||||
|           <> | ||||
|             <CustomText | ||||
|               fontStyle='M' | ||||
|               style={{ | ||||
|                 color: categories[2].selected ? colors.primaryDefault : colors.disabled, | ||||
|                 marginTop: StyleConstants.Spacing.M, | ||||
|                 marginBottom: StyleConstants.Spacing.S | ||||
|               }} | ||||
|             > | ||||
|               {t('screenTabs:shared.report.violatedRules.heading')} | ||||
|             </CustomText> | ||||
|             <View style={{ marginLeft: StyleConstants.Spacing.M }}> | ||||
|               <Selections | ||||
|                 disabled={!categories[2].selected} | ||||
|                 multiple | ||||
|                 options={rules} | ||||
|                 setOptions={setRules} | ||||
|               /> | ||||
|             </View> | ||||
|           </> | ||||
|           <Selections | ||||
|             title={t('screenTabs:shared.report.violatedRules.heading')} | ||||
|             disabled={!categories[2].selected} | ||||
|             multiple | ||||
|             options={rules} | ||||
|             setOptions={setRules} | ||||
|           /> | ||||
|         ) : null} | ||||
|       </View> | ||||
|     </ScrollView> | ||||
|   | ||||
| @@ -186,3 +186,18 @@ export type TabMeProfileStackParamList = { | ||||
| } | ||||
| export type TabMeProfileStackScreenProps<T extends keyof TabMeProfileStackParamList> = | ||||
|   NativeStackScreenProps<TabMeProfileStackParamList, T> | ||||
|  | ||||
| export type TabMePreferencesStackParamList = { | ||||
|   'Tab-Me-Preferences-Root': undefined | ||||
|   'Tab-Me-Preferences-Filters': undefined | ||||
|   'Tab-Me-Preferences-Filter': | ||||
|     | { | ||||
|         type: 'add' | ||||
|       } | ||||
|     | { | ||||
|         type: 'edit' | ||||
|         filter: Mastodon.Filter<'v2'> | ||||
|       } | ||||
| } | ||||
| export type TabMePreferencesStackScreenProps<T extends keyof TabMePreferencesStackParamList> = | ||||
|   NativeStackScreenProps<TabMePreferencesStackParamList, T> | ||||
|   | ||||
| @@ -1,10 +1,4 @@ | ||||
| import { | ||||
|   QueryFunctionContext, | ||||
|   useMutation, | ||||
|   UseMutationOptions, | ||||
|   useQuery, | ||||
|   UseQueryOptions | ||||
| } from '@tanstack/react-query' | ||||
| import { useMutation, UseMutationOptions, useQuery, UseQueryOptions } from '@tanstack/react-query' | ||||
| import apiGeneral from '@utils/api/general' | ||||
| import apiInstance from '@utils/api/instance' | ||||
| import { AxiosError } from 'axios' | ||||
| @@ -12,7 +6,7 @@ import * as AuthSession from 'expo-auth-session' | ||||
|  | ||||
| export type QueryKeyApps = ['Apps'] | ||||
|  | ||||
| const queryFunctionApps = async ({ queryKey }: QueryFunctionContext<QueryKeyApps>) => { | ||||
| const queryFunctionApps = async () => { | ||||
|   const res = await apiInstance<Mastodon.Apps>({ | ||||
|     method: 'get', | ||||
|     url: 'apps/verify_credentials' | ||||
|   | ||||
| @@ -1,24 +1,57 @@ | ||||
| import { useQuery, UseQueryOptions } from '@tanstack/react-query' | ||||
| import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query' | ||||
| import apiInstance from '@utils/api/instance' | ||||
| import { AxiosError } from 'axios' | ||||
|  | ||||
| export type QueryKeyFilters = ['Filters'] | ||||
| export type QueryKeyFilter = ['Filter', { id: Mastodon.Filter<'v2'>['id'] }] | ||||
|  | ||||
| const queryFunction = () => | ||||
|   apiInstance<Mastodon.Filter<'v1'>[]>({ | ||||
| const filterQueryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyFilter>) => { | ||||
|   const res = await apiInstance<Mastodon.Filter<'v2'>>({ | ||||
|     method: 'get', | ||||
|     url: 'filters' | ||||
|   }).then(res => res.body) | ||||
|     version: 'v2', | ||||
|     url: `filters/${queryKey[1].id}` | ||||
|   }) | ||||
|   return res.body | ||||
| } | ||||
|  | ||||
| const useFiltersQuery = (params?: { | ||||
|   options: UseQueryOptions<Mastodon.Filter<'v1'>[], AxiosError> | ||||
| const useFilterQuery = ({ | ||||
|   filter, | ||||
|   options | ||||
| }: { | ||||
|   filter: Mastodon.Filter<'v2'> | ||||
|   options?: UseQueryOptions<Mastodon.Filter<'v2'>, AxiosError> | ||||
| }) => { | ||||
|   const queryKey: QueryKeyFilters = ['Filters'] | ||||
|   return useQuery(queryKey, queryFunction, { | ||||
|   const queryKey: QueryKeyFilter = ['Filter', { id: filter.id }] | ||||
|   return useQuery(queryKey, filterQueryFunction, { | ||||
|     ...options, | ||||
|     staleTime: Infinity, | ||||
|     cacheTime: Infinity | ||||
|   }) | ||||
| } | ||||
|  | ||||
| export type QueryKeyFilters = ['Filters', { version: 'v1' | 'v2' }] | ||||
|  | ||||
| const filtersQueryFunction = async <T extends 'v1' | 'v2' = 'v1'>({ | ||||
|   queryKey | ||||
| }: QueryFunctionContext<QueryKeyFilters>) => { | ||||
|   const version = queryKey[1].version | ||||
|   const res = await apiInstance<Mastodon.Filter<T>[]>({ | ||||
|     method: 'get', | ||||
|     version, | ||||
|     url: 'filters' | ||||
|   }) | ||||
|   return res.body | ||||
| } | ||||
|  | ||||
| const useFiltersQuery = <T extends 'v1' | 'v2' = 'v1'>(params?: { | ||||
|   version?: T | ||||
|   options?: UseQueryOptions<Mastodon.Filter<T>[], AxiosError> | ||||
| }) => { | ||||
|   const queryKey: QueryKeyFilters = ['Filters', { version: params?.version || 'v1' }] | ||||
|   return useQuery(queryKey, filtersQueryFunction, { | ||||
|     ...params?.options, | ||||
|     staleTime: Infinity, | ||||
|     cacheTime: Infinity | ||||
|   }) | ||||
| } | ||||
|  | ||||
| export { useFiltersQuery } | ||||
| export { useFilterQuery, useFiltersQuery } | ||||
|   | ||||
| @@ -42,9 +42,9 @@ const themeColors: { | ||||
|     dark_darker: 'rgb(130, 130, 130)' | ||||
|   }, | ||||
|   disabled: { | ||||
|     light: 'rgb(200, 200, 200)', | ||||
|     dark_lighter: 'rgb(120, 120, 120)', | ||||
|     dark_darker: 'rgb(66, 66, 66)' | ||||
|     light: 'rgb(220, 220, 220)', | ||||
|     dark_lighter: 'rgb(70, 70, 70)', | ||||
|     dark_darker: 'rgb(50, 50, 50)' | ||||
|   }, | ||||
|   blue: { | ||||
|     light: 'rgb(43, 144, 221)', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user