diff --git a/src/components/Hr.tsx b/src/components/Hr.tsx new file mode 100644 index 00000000..1af52002 --- /dev/null +++ b/src/components/Hr.tsx @@ -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 ( + + ) +} + +export default Hr diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 02febb0c..736b2af6 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -9,7 +9,9 @@ import CustomText from './Text' export type Props = { title?: string multiline?: boolean -} & Pick, 'value' | 'selection' | 'isFocused'> & + invalid?: boolean +} & Pick, 'value'> & + Pick, '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 diff --git a/src/components/Menu/Row.tsx b/src/components/Menu/Row.tsx index d140aa37..ffad2741 100644 --- a/src/components/Menu/Row.tsx +++ b/src/components/Menu/Row.tsx @@ -44,7 +44,7 @@ const MenuRow: React.FC = ({ loading = false, onPress }) => { - const { colors, theme } = useTheme() + const { colors } = useTheme() const { screenReaderEnabled } = useAccessibility() return ( diff --git a/src/components/Selections.tsx b/src/components/Selections.tsx index 8e62594a..bbad0a2b 100644 --- a/src/components/Selections.tsx +++ b/src/components/Selections.tsx @@ -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> disabled?: boolean + invalid?: boolean } const Selections: React.FC = ({ + title, multiple = false, options, setOptions, - disabled = false + disabled = false, + invalid = false }) => { const { colors } = useTheme() @@ -32,52 +37,71 @@ const Selections: React.FC = ({ : 'circle' return ( - - {options.map((option, index) => ( - { - if (multiple) { - haptics('Light') - - setOptions(options.map((o, i) => (i === index ? { ...o, selected: !o.selected } : o))) - } else { - if (!option.selected) { + + {title ? ( + + ) : null} + + {options.map((option, index) => ( + { + 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 } + } + }) + ) + } } - } - }} - > - - - - + + - - - - ))} + + + + + + ))} + ) } diff --git a/src/components/Timeline/Shared/Filtered.tsx b/src/components/Timeline/Shared/Filtered.tsx index 9a8c11de..4a0c00da 100644 --- a/src/components/Timeline/Shared/Filtered.tsx +++ b/src/components/Timeline/Shared/Filtered.tsx @@ -99,7 +99,7 @@ export const shouldFilter = ({ break } } - const queryKeyFilters: QueryKeyFilters = ['Filters'] + const queryKeyFilters: QueryKeyFilters = ['Filters', { version: 'v1' }] queryClient.getQueryData[]>(queryKeyFilters)?.forEach(filter => { if (returnFilter) { return diff --git a/src/components/Timeline/Shared/Poll.tsx b/src/components/Timeline/Shared/Poll.tsx index 7cda64ec..47f006b8 100644 --- a/src/components/Timeline/Shared/Poll.tsx +++ b/src/components/Timeline/Shared/Poll.tsx @@ -133,7 +133,7 @@ const TimelinePoll: React.FC = () => { { ", + "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", diff --git a/src/screens/Tabs/Me/List/Edit.tsx b/src/screens/Tabs/Me/List/Edit.tsx index 77f011ac..2dc750b5 100644 --- a/src/screens/Tabs/Me/List/Edit.tsx +++ b/src/screens/Tabs/Me/List/Edit.tsx @@ -19,7 +19,7 @@ const TabMeListEdit: React.FC> = ({ 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> = ({ - - {t('screenTabs:me.listEdit.repliesPolicy.heading')} - - + diff --git a/src/screens/Tabs/Me/Preferences/Filter.tsx b/src/screens/Tabs/Me/Preferences/Filter.tsx new file mode 100644 index 00000000..7f3088ab --- /dev/null +++ b/src/screens/Tabs/Me/Preferences/Filter.tsx @@ -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 + } +> = ({ 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: () => ( + 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('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( + params.type === 'edit' ? params.filter.keywords.map(({ keyword }) => keyword) : [] + ) + + useEffect(() => { + let isLoading = false + navigation.setOptions({ + headerRight: () => ( + { + 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 ( + + + + + + 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]) + } + ) + } + /> +
+ + context.selected).length} + options={contexts} + setOptions={setContexts} + /> + +
+ + + + {[...Array(keywords.length)].map((_, i) => ( + setKeywords(keywords.map((curr, ii) => (i === ii ? k : curr))) + ]} + /> + ))} + + +