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 = { | export type Props = { | ||||||
|   title?: string |   title?: string | ||||||
|   multiline?: boolean |   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< |   Omit< | ||||||
|     TextInputProps, |     TextInputProps, | ||||||
|     | 'style' |     | 'style' | ||||||
| @@ -27,8 +29,9 @@ const ComponentInput = forwardRef( | |||||||
|     { |     { | ||||||
|       title, |       title, | ||||||
|       multiline = false, |       multiline = false, | ||||||
|  |       invalid = false, | ||||||
|       value: [value, setValue], |       value: [value, setValue], | ||||||
|       selection: [selection, setSelection], |       selection, | ||||||
|       isFocused, |       isFocused, | ||||||
|       ...props |       ...props | ||||||
|     }: Props, |     }: Props, | ||||||
| @@ -43,7 +46,7 @@ const ComponentInput = forwardRef( | |||||||
|           paddingHorizontal: withTiming(StyleConstants.Spacing.XS), |           paddingHorizontal: withTiming(StyleConstants.Spacing.XS), | ||||||
|           left: withTiming(StyleConstants.Spacing.S), |           left: withTiming(StyleConstants.Spacing.S), | ||||||
|           top: withTiming(-(StyleConstants.Font.Size.S / 2) - 2), |           top: withTiming(-(StyleConstants.Font.Size.S / 2) - 2), | ||||||
|           backgroundColor: withTiming(colors.backgroundDefault) |           backgroundColor: colors.backgroundDefault | ||||||
|         } |         } | ||||||
|       } else { |       } else { | ||||||
|         return { |         return { | ||||||
| @@ -62,7 +65,7 @@ const ComponentInput = forwardRef( | |||||||
|           borderWidth: 1, |           borderWidth: 1, | ||||||
|           marginVertical: StyleConstants.Spacing.S, |           marginVertical: StyleConstants.Spacing.S, | ||||||
|           padding: StyleConstants.Spacing.S, |           padding: StyleConstants.Spacing.S, | ||||||
|           borderColor: colors.border, |           borderColor: invalid ? colors.red : colors.border, | ||||||
|           flexDirection: multiline ? 'column' : 'row', |           flexDirection: multiline ? 'column' : 'row', | ||||||
|           alignItems: 'stretch' |           alignItems: 'stretch' | ||||||
|         }} |         }} | ||||||
| @@ -78,9 +81,13 @@ const ComponentInput = forwardRef( | |||||||
|           }} |           }} | ||||||
|           value={value} |           value={value} | ||||||
|           onChangeText={setValue} |           onChangeText={setValue} | ||||||
|           onFocus={() => (isFocused.current = true)} |           {...(isFocused !== undefined && { | ||||||
|           onBlur={() => (isFocused.current = false)} |             onFocus: () => (isFocused.current = true), | ||||||
|           onSelectionChange={({ nativeEvent }) => setSelection(nativeEvent.selection)} |             onBlur: () => (isFocused.current = false) | ||||||
|  |           })} | ||||||
|  |           {...(selection !== undefined && { | ||||||
|  |             onSelectionChange: ({ nativeEvent }) => selection[1](nativeEvent.selection) | ||||||
|  |           })} | ||||||
|           {...(multiline && { |           {...(multiline && { | ||||||
|             multiline, |             multiline, | ||||||
|             numberOfLines: Platform.OS === 'android' ? 5 : undefined |             numberOfLines: Platform.OS === 'android' ? 5 : undefined | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ const MenuRow: React.FC<Props> = ({ | |||||||
|   loading = false, |   loading = false, | ||||||
|   onPress |   onPress | ||||||
| }) => { | }) => { | ||||||
|   const { colors, theme } = useTheme() |   const { colors } = useTheme() | ||||||
|   const { screenReaderEnabled } = useAccessibility() |   const { screenReaderEnabled } = useAccessibility() | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|   | |||||||
| @@ -8,17 +8,22 @@ import { ParseEmojis } from './Parse' | |||||||
| import CustomText from './Text' | import CustomText from './Text' | ||||||
|  |  | ||||||
| export interface Props { | export interface Props { | ||||||
|  |   title?: string | ||||||
|  |  | ||||||
|   multiple?: boolean |   multiple?: boolean | ||||||
|   options: { selected: boolean; content: string }[] |   options: { selected: boolean; content: string }[] | ||||||
|   setOptions: React.Dispatch<React.SetStateAction<{ selected: boolean; content: string }[]>> |   setOptions: React.Dispatch<React.SetStateAction<{ selected: boolean; content: string }[]>> | ||||||
|   disabled?: boolean |   disabled?: boolean | ||||||
|  |   invalid?: boolean | ||||||
| } | } | ||||||
|  |  | ||||||
| const Selections: React.FC<Props> = ({ | const Selections: React.FC<Props> = ({ | ||||||
|  |   title, | ||||||
|   multiple = false, |   multiple = false, | ||||||
|   options, |   options, | ||||||
|   setOptions, |   setOptions, | ||||||
|   disabled = false |   disabled = false, | ||||||
|  |   invalid = false | ||||||
| }) => { | }) => { | ||||||
|   const { colors } = useTheme() |   const { colors } = useTheme() | ||||||
|  |  | ||||||
| @@ -32,52 +37,71 @@ const Selections: React.FC<Props> = ({ | |||||||
|       : 'circle' |       : 'circle' | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <View> |     <View style={{ marginVertical: StyleConstants.Spacing.S }}> | ||||||
|       {options.map((option, index) => ( |       {title ? ( | ||||||
|         <Pressable |         <CustomText | ||||||
|           key={index} |           fontStyle='M' | ||||||
|           disabled={disabled} |           children={title} | ||||||
|           style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }} |           style={{ color: disabled ? colors.disabled : colors.primaryDefault }} | ||||||
|           onPress={() => { |         /> | ||||||
|             if (multiple) { |       ) : null} | ||||||
|               haptics('Light') |       <View | ||||||
|  |         style={{ | ||||||
|               setOptions(options.map((o, i) => (i === index ? { ...o, selected: !o.selected } : o))) |           paddingHorizontal: StyleConstants.Spacing.M, | ||||||
|             } else { |           paddingVertical: StyleConstants.Spacing.XS, | ||||||
|               if (!option.selected) { |           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') |                 haptics('Light') | ||||||
|  |  | ||||||
|                 setOptions( |                 setOptions( | ||||||
|                   options.map((o, i) => { |                   options.map((o, i) => (i === index ? { ...o, selected: !o.selected } : o)) | ||||||
|                     if (i === index) { |  | ||||||
|                       return { ...o, selected: true } |  | ||||||
|                     } else { |  | ||||||
|                       return { ...o, selected: false } |  | ||||||
|                     } |  | ||||||
|                   }) |  | ||||||
|                 ) |                 ) | ||||||
|  |               } 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' }}> | ||||||
|           <View style={{ flex: 1, flexDirection: 'row' }}> |               <Icon | ||||||
|             <Icon |                 style={{ | ||||||
|               style={{ |                   marginTop: (StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M) / 2, | ||||||
|                 paddingTop: StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M, |                   marginRight: StyleConstants.Spacing.S | ||||||
|                 marginRight: StyleConstants.Spacing.S |                 }} | ||||||
|               }} |                 name={isSelected(index)} | ||||||
|               name={isSelected(index)} |                 size={StyleConstants.Font.Size.M} | ||||||
|               size={StyleConstants.Font.Size.M} |                 color={disabled ? colors.disabled : colors.primaryDefault} | ||||||
|               color={disabled ? colors.disabled : colors.primaryDefault} |  | ||||||
|             /> |  | ||||||
|             <CustomText style={{ flex: 1 }}> |  | ||||||
|               <ParseEmojis |  | ||||||
|                 content={option.content} |  | ||||||
|                 style={{ color: disabled ? colors.disabled : colors.primaryDefault }} |  | ||||||
|               /> |               /> | ||||||
|             </CustomText> |               <CustomText fontStyle='S' style={{ flex: 1 }}> | ||||||
|           </View> |                 <ParseEmojis | ||||||
|         </Pressable> |                   content={option.content} | ||||||
|       ))} |                   style={{ color: disabled ? colors.disabled : colors.primaryDefault }} | ||||||
|  |                 /> | ||||||
|  |               </CustomText> | ||||||
|  |             </View> | ||||||
|  |           </Pressable> | ||||||
|  |         ))} | ||||||
|  |       </View> | ||||||
|     </View> |     </View> | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -99,7 +99,7 @@ export const shouldFilter = ({ | |||||||
|         break |         break | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   const queryKeyFilters: QueryKeyFilters = ['Filters'] |   const queryKeyFilters: QueryKeyFilters = ['Filters', { version: 'v1' }] | ||||||
|   queryClient.getQueryData<Mastodon.Filter<'v1'>[]>(queryKeyFilters)?.forEach(filter => { |   queryClient.getQueryData<Mastodon.Filter<'v1'>[]>(queryKeyFilters)?.forEach(filter => { | ||||||
|     if (returnFilter) { |     if (returnFilter) { | ||||||
|       return |       return | ||||||
|   | |||||||
| @@ -133,7 +133,7 @@ const TimelinePoll: React.FC = () => { | |||||||
|         <View style={{ flex: 1, flexDirection: 'row' }}> |         <View style={{ flex: 1, flexDirection: 'row' }}> | ||||||
|           <Icon |           <Icon | ||||||
|             style={{ |             style={{ | ||||||
|               paddingTop: StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M, |               marginTop: (StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M) / 2, | ||||||
|               marginRight: StyleConstants.Spacing.S |               marginRight: StyleConstants.Spacing.S | ||||||
|             }} |             }} | ||||||
|             name={ |             name={ | ||||||
| @@ -205,7 +205,7 @@ const TimelinePoll: React.FC = () => { | |||||||
|         <View style={{ flex: 1, flexDirection: 'row' }}> |         <View style={{ flex: 1, flexDirection: 'row' }}> | ||||||
|           <Icon |           <Icon | ||||||
|             style={{ |             style={{ | ||||||
|               paddingTop: StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M, |               marginTop: (StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M) / 2, | ||||||
|               marginRight: StyleConstants.Spacing.S |               marginRight: StyleConstants.Spacing.S | ||||||
|             }} |             }} | ||||||
|             name={isSelected(index)} |             name={isSelected(index)} | ||||||
|   | |||||||
| @@ -72,6 +72,15 @@ | |||||||
|       "preferences": { |       "preferences": { | ||||||
|         "name": "Preferences" |         "name": "Preferences" | ||||||
|       }, |       }, | ||||||
|  |       "preferencesFilters": { | ||||||
|  |         "name": "All content filters" | ||||||
|  |       }, | ||||||
|  |       "preferencesFilterAdd": { | ||||||
|  |         "name": "Create a Filter" | ||||||
|  |       }, | ||||||
|  |       "preferencesFilterEdit": { | ||||||
|  |         "name": "Edit Filter" | ||||||
|  |       }, | ||||||
|       "profile": { |       "profile": { | ||||||
|         "name": "Edit Profile" |         "name": "Edit Profile" | ||||||
|       }, |       }, | ||||||
| @@ -127,7 +136,7 @@ | |||||||
|     }, |     }, | ||||||
|     "preferences": { |     "preferences": { | ||||||
|       "visibility": { |       "visibility": { | ||||||
|         "title": "Posting visibility", |         "title": "Default posting visibility", | ||||||
|         "options": { |         "options": { | ||||||
|           "public": "Public", |           "public": "Public", | ||||||
|           "unlisted": "Unlisted", |           "unlisted": "Unlisted", | ||||||
| @@ -135,7 +144,7 @@ | |||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "sensitive": { |       "sensitive": { | ||||||
|         "title": "Posting media sensitive" |         "title": "Default mark media as sensitive" | ||||||
|       }, |       }, | ||||||
|       "media": { |       "media": { | ||||||
|         "title": "Media display", |         "title": "Media display", | ||||||
| @@ -146,16 +155,63 @@ | |||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "spoilers": { |       "spoilers": { | ||||||
|         "title": "Auto expand toots with content warning", |         "title": "Auto expand toots with content warning" | ||||||
|       }, |       }, | ||||||
|       "autoplay_gifs": { |       "autoplay_gifs": { | ||||||
|         "title": "Autoplay GIF in toots" |         "title": "Autoplay GIF in toots" | ||||||
|       }, |       }, | ||||||
|  |       "filters": { | ||||||
|  |         "title": "Content filters", | ||||||
|  |         "content": "{{count}} active" | ||||||
|  |       }, | ||||||
|       "web_only": { |       "web_only": { | ||||||
|         "title": "Update settings", |         "title": "Update settings", | ||||||
|         "description": "Settings below can only be updated using the web UI" |         "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": { |     "profile": { | ||||||
|       "feedback": { |       "feedback": { | ||||||
|         "succeed": "{{type}} updated", |         "succeed": "{{type}} updated", | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({ | |||||||
|   navigation, |   navigation, | ||||||
|   route: { params } |   route: { params } | ||||||
| }) => { | }) => { | ||||||
|   const { colors, theme } = useTheme() |   const { colors } = useTheme() | ||||||
|   const { t } = useTranslation(['common', 'screenTabs']) |   const { t } = useTranslation(['common', 'screenTabs']) | ||||||
|  |  | ||||||
|   const messageRef = useRef(null) |   const messageRef = useRef(null) | ||||||
| @@ -147,18 +147,11 @@ const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({ | |||||||
|     <ScrollView style={{ paddingHorizontal: StyleConstants.Spacing.Global.PagePadding }}> |     <ScrollView style={{ paddingHorizontal: StyleConstants.Spacing.Global.PagePadding }}> | ||||||
|       <ComponentInput {...inputProps} autoFocus title={t('screenTabs:me.listEdit.title')} /> |       <ComponentInput {...inputProps} autoFocus title={t('screenTabs:me.listEdit.title')} /> | ||||||
|  |  | ||||||
|       <CustomText |       <Selections | ||||||
|         fontStyle='M' |         title={t('screenTabs:me.listEdit.repliesPolicy.heading')} | ||||||
|         fontWeight='Bold' |         options={options} | ||||||
|         style={{ |         setOptions={setOptions} | ||||||
|           color: colors.primaryDefault, |       /> | ||||||
|           marginBottom: StyleConstants.Spacing.XS, |  | ||||||
|           marginTop: StyleConstants.Spacing.M |  | ||||||
|         }} |  | ||||||
|       > |  | ||||||
|         {t('screenTabs:me.listEdit.repliesPolicy.heading')} |  | ||||||
|       </CustomText> |  | ||||||
|       <Selections options={options} setOptions={setOptions} /> |  | ||||||
|  |  | ||||||
|       <Message ref={messageRef} /> |       <Message ref={messageRef} /> | ||||||
|     </ScrollView> |     </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 { MenuContainer, MenuRow } from '@components/Menu' | ||||||
| import { Message } from '@components/Message' |  | ||||||
| import { useActionSheet } from '@expo/react-native-action-sheet' | import { useActionSheet } from '@expo/react-native-action-sheet' | ||||||
| import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles' | import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles' | ||||||
| import browserPackage from '@utils/helpers/browserPackage' | 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 { usePreferencesQuery } from '@utils/queryHooks/preferences' | ||||||
| import { useProfileMutation } from '@utils/queryHooks/profile' | import { useProfileMutation } from '@utils/queryHooks/profile' | ||||||
| import { getAccountStorage } from '@utils/storage/actions' | import { getAccountStorage } from '@utils/storage/actions' | ||||||
| import { StyleConstants } from '@utils/styles/constants' | import { StyleConstants } from '@utils/styles/constants' | ||||||
| import { useTheme } from '@utils/styles/ThemeManager' | import { useTheme } from '@utils/styles/ThemeManager' | ||||||
| import * as WebBrowser from 'expo-web-browser' | import * as WebBrowser from 'expo-web-browser' | ||||||
| import React, { useRef } from 'react' | import React, { RefObject } from 'react' | ||||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||||
| import FlashMessage from 'react-native-flash-message' | import FlashMessage from 'react-native-flash-message' | ||||||
| import { ScrollView } from 'react-native-gesture-handler' | 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 { colors } = useTheme() | ||||||
|   const { t } = useTranslation(['common', 'screenTabs']) |   const { t } = useTranslation(['common', 'screenTabs']) | ||||||
| 
 | 
 | ||||||
| @@ -25,7 +30,7 @@ const TabMePreferences: React.FC<TabMeProfileStackScreenProps<'Tab-Me-Profile-Ro | |||||||
| 
 | 
 | ||||||
|   const { data, isFetching, refetch } = usePreferencesQuery() |   const { data, isFetching, refetch } = usePreferencesQuery() | ||||||
| 
 | 
 | ||||||
|   const messageRef = useRef<FlashMessage>(null) |   const { data: filters, isFetching: filtersIsFetching } = useFiltersQuery<'v2'>({ version: 'v2' }) | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <ScrollView> |     <ScrollView> | ||||||
| @@ -149,10 +154,23 @@ const TabMePreferences: React.FC<TabMeProfileStackScreenProps<'Tab-Me-Profile-Ro | |||||||
|           /> |           /> | ||||||
|         ) : null} |         ) : null} | ||||||
|       </MenuContainer> |       </MenuContainer> | ||||||
| 
 |       {featureCheck('filter_server_side') ? ( | ||||||
|       <Message ref={messageRef} /> |         <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> |     </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({ |     navigation.setOptions({ | ||||||
|       headerLeft: () => ( |       headerLeft: () => ( | ||||||
|         <HeaderLeft |         <HeaderLeft | ||||||
|           content='x' |           content='chevron-left' | ||||||
|           onPress={() => { |           onPress={() => { | ||||||
|             if (dirty) { |             if (dirty) { | ||||||
|               Alert.alert(t('common:discard.title'), t('common:discard.message'), [ |               Alert.alert(t('common:discard.title'), t('common:discard.message'), [ | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ const TabMeProfileName: React.FC< | |||||||
|     navigation.setOptions({ |     navigation.setOptions({ | ||||||
|       headerLeft: () => ( |       headerLeft: () => ( | ||||||
|         <HeaderLeft |         <HeaderLeft | ||||||
|           content='x' |           content='chevron-left' | ||||||
|           onPress={() => { |           onPress={() => { | ||||||
|             if (dirty) { |             if (dirty) { | ||||||
|               Alert.alert(t('common:discard.title'), t('common:discard.message'), [ |               Alert.alert(t('common:discard.title'), t('common:discard.message'), [ | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ const TabMeProfileNote: React.FC< | |||||||
|     navigation.setOptions({ |     navigation.setOptions({ | ||||||
|       headerLeft: () => ( |       headerLeft: () => ( | ||||||
|         <HeaderLeft |         <HeaderLeft | ||||||
|           content='x' |           content='chevron-left' | ||||||
|           onPress={() => { |           onPress={() => { | ||||||
|             if (dirty) { |             if (dirty) { | ||||||
|               Alert.alert(t('common:discard.title'), t('common:discard.message'), [ |               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 }) => ( |           {props => <TabMeProfileRoot messageRef={messageRef} {...props} />} | ||||||
|             <TabMeProfileRoot messageRef={messageRef} route={route} navigation={navigation} /> |  | ||||||
|           )} |  | ||||||
|         </Stack.Screen> |         </Stack.Screen> | ||||||
|         <Stack.Screen |         <Stack.Screen | ||||||
|           name='Tab-Me-Profile-Name' |           name='Tab-Me-Profile-Name' | ||||||
|           options={{ title: t('me.stacks.profileName.name') }} |           options={{ title: t('me.stacks.profileName.name') }} | ||||||
|         > |         > | ||||||
|           {({ route, navigation }) => ( |           {props => <TabMeProfileName messageRef={messageRef} {...props} />} | ||||||
|             <TabMeProfileName messageRef={messageRef} route={route} navigation={navigation} /> |  | ||||||
|           )} |  | ||||||
|         </Stack.Screen> |         </Stack.Screen> | ||||||
|         <Stack.Screen |         <Stack.Screen | ||||||
|           name='Tab-Me-Profile-Note' |           name='Tab-Me-Profile-Note' | ||||||
|           options={{ title: t('me.stacks.profileNote.name') }} |           options={{ title: t('me.stacks.profileNote.name') }} | ||||||
|         > |         > | ||||||
|           {({ route, navigation }) => ( |           {props => <TabMeProfileNote messageRef={messageRef} {...props} />} | ||||||
|             <TabMeProfileNote messageRef={messageRef} route={route} navigation={navigation} /> |  | ||||||
|           )} |  | ||||||
|         </Stack.Screen> |         </Stack.Screen> | ||||||
|         <Stack.Screen |         <Stack.Screen | ||||||
|           name='Tab-Me-Profile-Fields' |           name='Tab-Me-Profile-Fields' | ||||||
|           options={{ title: t('me.stacks.profileFields.name') }} |           options={{ title: t('me.stacks.profileFields.name') }} | ||||||
|         > |         > | ||||||
|           {({ route, navigation }) => ( |           {props => <TabMeProfileFields messageRef={messageRef} {...props} />} | ||||||
|             <TabMeProfileFields messageRef={messageRef} route={route} navigation={navigation} /> |  | ||||||
|           )} |  | ||||||
|         </Stack.Screen> |         </Stack.Screen> | ||||||
|       </Stack.Navigator> |       </Stack.Navigator> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -104,11 +104,7 @@ const TabMe: React.FC = () => { | |||||||
|       <Stack.Screen |       <Stack.Screen | ||||||
|         name='Tab-Me-Preferences' |         name='Tab-Me-Preferences' | ||||||
|         component={TabMePreferences} |         component={TabMePreferences} | ||||||
|         options={({ navigation }: any) => ({ |         options={{ headerShown: false, presentation: 'modal' }} | ||||||
|           presentation: 'modal', |  | ||||||
|           title: t('me.stacks.preferences.name'), |  | ||||||
|           headerLeft: () => <HeaderLeft content='chevron-down' onPress={() => navigation.pop(1)} /> |  | ||||||
|         })} |  | ||||||
|       /> |       /> | ||||||
|       <Stack.Screen |       <Stack.Screen | ||||||
|         name='Tab-Me-Profile' |         name='Tab-Me-Profile' | ||||||
| @@ -154,7 +150,9 @@ const TabMe: React.FC = () => { | |||||||
|           presentation: 'modal', |           presentation: 'modal', | ||||||
|           headerShown: true, |           headerShown: true, | ||||||
|           title: t('me.stacks.switch.name'), |           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} |           disabled={account === undefined} | ||||||
|           content='sliders' |           content='sliders' | ||||||
|           style={{ marginLeft: StyleConstants.Spacing.S }} |           style={{ marginLeft: StyleConstants.Spacing.S }} | ||||||
|           onPress={() => navigation.navigate('Tab-Me-Preferences')} |           onPress={() => | ||||||
|  |             navigation.navigate('Tab-Me-Preferences', { screen: 'Tab-Me-Preferences-Root' }) | ||||||
|  |           } | ||||||
|         /> |         /> | ||||||
|       </View> |       </View> | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -125,7 +125,11 @@ const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>> | |||||||
|           > |           > | ||||||
|             <CustomText |             <CustomText | ||||||
|               fontStyle='M' |               fontStyle='M' | ||||||
|               style={{ color: colors.primaryDefault, paddingRight: StyleConstants.Spacing.M }} |               style={{ | ||||||
|  |                 flex: 1, | ||||||
|  |                 color: colors.primaryDefault, | ||||||
|  |                 paddingRight: StyleConstants.Spacing.M | ||||||
|  |               }} | ||||||
|               numberOfLines={2} |               numberOfLines={2} | ||||||
|             > |             > | ||||||
|               {t('screenTabs:shared.report.forward.heading', { |               {t('screenTabs:shared.report.forward.heading', { | ||||||
| @@ -140,15 +144,11 @@ const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>> | |||||||
|           </View> |           </View> | ||||||
|         ) : null} |         ) : null} | ||||||
|  |  | ||||||
|         <CustomText |         <Selections | ||||||
|           fontStyle='M' |           title={t('screenTabs:shared.report.reasons.heading')} | ||||||
|           style={{ color: colors.primaryDefault, marginBottom: StyleConstants.Spacing.S }} |           options={categories} | ||||||
|         > |           setOptions={setCategories} | ||||||
|           {t('screenTabs:shared.report.reasons.heading')} |         /> | ||||||
|         </CustomText> |  | ||||||
|         <View style={{ marginLeft: StyleConstants.Spacing.M }}> |  | ||||||
|           <Selections options={categories} setOptions={setCategories} /> |  | ||||||
|         </View> |  | ||||||
|  |  | ||||||
|         {categories[1].selected || comment.length ? ( |         {categories[1].selected || comment.length ? ( | ||||||
|           <> |           <> | ||||||
| @@ -200,26 +200,13 @@ const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>> | |||||||
|         ) : null} |         ) : null} | ||||||
|  |  | ||||||
|         {rules.length ? ( |         {rules.length ? ( | ||||||
|           <> |           <Selections | ||||||
|             <CustomText |             title={t('screenTabs:shared.report.violatedRules.heading')} | ||||||
|               fontStyle='M' |             disabled={!categories[2].selected} | ||||||
|               style={{ |             multiple | ||||||
|                 color: categories[2].selected ? colors.primaryDefault : colors.disabled, |             options={rules} | ||||||
|                 marginTop: StyleConstants.Spacing.M, |             setOptions={setRules} | ||||||
|                 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> |  | ||||||
|           </> |  | ||||||
|         ) : null} |         ) : null} | ||||||
|       </View> |       </View> | ||||||
|     </ScrollView> |     </ScrollView> | ||||||
|   | |||||||
| @@ -186,3 +186,18 @@ export type TabMeProfileStackParamList = { | |||||||
| } | } | ||||||
| export type TabMeProfileStackScreenProps<T extends keyof TabMeProfileStackParamList> = | export type TabMeProfileStackScreenProps<T extends keyof TabMeProfileStackParamList> = | ||||||
|   NativeStackScreenProps<TabMeProfileStackParamList, T> |   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 { | import { useMutation, UseMutationOptions, useQuery, UseQueryOptions } from '@tanstack/react-query' | ||||||
|   QueryFunctionContext, |  | ||||||
|   useMutation, |  | ||||||
|   UseMutationOptions, |  | ||||||
|   useQuery, |  | ||||||
|   UseQueryOptions |  | ||||||
| } from '@tanstack/react-query' |  | ||||||
| import apiGeneral from '@utils/api/general' | import apiGeneral from '@utils/api/general' | ||||||
| import apiInstance from '@utils/api/instance' | import apiInstance from '@utils/api/instance' | ||||||
| import { AxiosError } from 'axios' | import { AxiosError } from 'axios' | ||||||
| @@ -12,7 +6,7 @@ import * as AuthSession from 'expo-auth-session' | |||||||
|  |  | ||||||
| export type QueryKeyApps = ['Apps'] | export type QueryKeyApps = ['Apps'] | ||||||
|  |  | ||||||
| const queryFunctionApps = async ({ queryKey }: QueryFunctionContext<QueryKeyApps>) => { | const queryFunctionApps = async () => { | ||||||
|   const res = await apiInstance<Mastodon.Apps>({ |   const res = await apiInstance<Mastodon.Apps>({ | ||||||
|     method: 'get', |     method: 'get', | ||||||
|     url: 'apps/verify_credentials' |     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 apiInstance from '@utils/api/instance' | ||||||
| import { AxiosError } from 'axios' | import { AxiosError } from 'axios' | ||||||
|  |  | ||||||
| export type QueryKeyFilters = ['Filters'] | export type QueryKeyFilter = ['Filter', { id: Mastodon.Filter<'v2'>['id'] }] | ||||||
|  |  | ||||||
| const queryFunction = () => | const filterQueryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyFilter>) => { | ||||||
|   apiInstance<Mastodon.Filter<'v1'>[]>({ |   const res = await apiInstance<Mastodon.Filter<'v2'>>({ | ||||||
|     method: 'get', |     method: 'get', | ||||||
|     url: 'filters' |     version: 'v2', | ||||||
|   }).then(res => res.body) |     url: `filters/${queryKey[1].id}` | ||||||
|  |   }) | ||||||
|  |   return res.body | ||||||
|  | } | ||||||
|  |  | ||||||
| const useFiltersQuery = (params?: { | const useFilterQuery = ({ | ||||||
|   options: UseQueryOptions<Mastodon.Filter<'v1'>[], AxiosError> |   filter, | ||||||
|  |   options | ||||||
|  | }: { | ||||||
|  |   filter: Mastodon.Filter<'v2'> | ||||||
|  |   options?: UseQueryOptions<Mastodon.Filter<'v2'>, AxiosError> | ||||||
| }) => { | }) => { | ||||||
|   const queryKey: QueryKeyFilters = ['Filters'] |   const queryKey: QueryKeyFilter = ['Filter', { id: filter.id }] | ||||||
|   return useQuery(queryKey, queryFunction, { |   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, |     ...params?.options, | ||||||
|     staleTime: Infinity, |     staleTime: Infinity, | ||||||
|     cacheTime: Infinity |     cacheTime: Infinity | ||||||
|   }) |   }) | ||||||
| } | } | ||||||
|  |  | ||||||
| export { useFiltersQuery } | export { useFilterQuery, useFiltersQuery } | ||||||
|   | |||||||
| @@ -42,9 +42,9 @@ const themeColors: { | |||||||
|     dark_darker: 'rgb(130, 130, 130)' |     dark_darker: 'rgb(130, 130, 130)' | ||||||
|   }, |   }, | ||||||
|   disabled: { |   disabled: { | ||||||
|     light: 'rgb(200, 200, 200)', |     light: 'rgb(220, 220, 220)', | ||||||
|     dark_lighter: 'rgb(120, 120, 120)', |     dark_lighter: 'rgb(70, 70, 70)', | ||||||
|     dark_darker: 'rgb(66, 66, 66)' |     dark_darker: 'rgb(50, 50, 50)' | ||||||
|   }, |   }, | ||||||
|   blue: { |   blue: { | ||||||
|     light: 'rgb(43, 144, 221)', |     light: 'rgb(43, 144, 221)', | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user