First step of adding filter editing support

This commit is contained in:
xmflsct 2023-01-26 00:57:48 +01:00
parent 2d91d1f7fb
commit d73857eef4
23 changed files with 773 additions and 151 deletions

23
src/components/Hr.tsx Normal file
View 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

View File

@ -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

View File

@ -44,7 +44,7 @@ const MenuRow: React.FC<Props> = ({
loading = false,
onPress
}) => {
const { colors, theme } = useTheme()
const { colors } = useTheme()
const { screenReaderEnabled } = useAccessibility()
return (

View File

@ -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>
)
}

View File

@ -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

View File

@ -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)}

View File

@ -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",

View File

@ -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>

View 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

View 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

View File

@ -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

View 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

View File

@ -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'), [

View File

@ -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'), [

View File

@ -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'), [

View File

@ -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>

View File

@ -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()} />
)
})}
/>

View File

@ -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>
)

View File

@ -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>

View File

@ -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>

View File

@ -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'

View File

@ -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 }

View File

@ -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)',